Skip to content

Commit 486591d

Browse files
committed
fix(collection): handle root index collisions
1 parent 7d4ce16 commit 486591d

File tree

2 files changed

+94
-15
lines changed

2 files changed

+94
-15
lines changed

src/module/src/runtime/utils/collection.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ import { join, dirname, parse } from 'pathe'
55
import { withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
66
import { parseSourceBase } from './source'
77

8+
function joinSourcePath(root: string, path: string) {
9+
return withoutLeadingSlash(withoutTrailingSlash(!root || root === '/' ? path : join(root, path)))
10+
}
11+
12+
function getSourceRoot(source: ResolvedCollectionSource) {
13+
const { fixed } = parseSourceBase(source)
14+
const normalizedFixed = withoutLeadingSlash(withoutTrailingSlash(fixed))
15+
const normalizedCwd = withoutLeadingSlash(withoutTrailingSlash(source.cwd || ''))
16+
17+
let cwdRoot = ''
18+
19+
if (normalizedCwd === 'content' || normalizedCwd.endsWith('/content')) {
20+
cwdRoot = ''
21+
}
22+
else if (normalizedCwd.startsWith('content/')) {
23+
cwdRoot = normalizedCwd.slice('content/'.length)
24+
}
25+
else if (normalizedCwd.includes('/content/')) {
26+
cwdRoot = normalizedCwd.split('/content/').pop() || ''
27+
}
28+
else if (normalizedCwd) {
29+
cwdRoot = normalizedCwd
30+
}
31+
32+
return joinSourcePath(cwdRoot, normalizedFixed)
33+
}
34+
835
/**
936
* Generation methods
1037
*/
@@ -13,33 +40,29 @@ export function generateStemFromFsPath(path: string) {
1340
}
1441

1542
export function generateIdFromFsPath(path: string, collectionInfo: CollectionInfo) {
16-
const { fixed } = parseSourceBase(collectionInfo.source[0]!)
43+
const source = collectionInfo.source[0]!
44+
const sourceRoot = getSourceRoot(source)
45+
const normalizedPath = withoutLeadingSlash(path)
1746

18-
const pathWithoutFixed = path.substring(fixed.length)
47+
const pathWithoutFixed = sourceRoot && normalizedPath.startsWith(sourceRoot + '/')
48+
? normalizedPath.substring(sourceRoot.length + 1)
49+
: normalizedPath
1950

20-
return join(collectionInfo.name, collectionInfo.source[0]?.prefix || '', pathWithoutFixed)
51+
return join(collectionInfo.name, source.prefix || '', pathWithoutFixed)
2152
}
2253

2354
export function generateFsPathFromId(id: string, source: ResolvedCollectionSource) {
2455
const [_, ...rest] = id.split(/[/:]/)
2556
let path = rest.join('/')
26-
27-
const { fixed } = parseSourceBase(source)
28-
const normalizedFixed = withoutTrailingSlash(fixed)
57+
const sourceRoot = getSourceRoot(source)
2958

3059
// If source has a prefix and the path starts with the prefix, remove the prefix
3160
const prefix = withoutTrailingSlash(withoutLeadingSlash(source.prefix || ''))
3261
if (prefix && prefix !== '/' && path.startsWith(prefix + '/')) {
3362
path = path.substring(prefix.length + 1)
3463
}
3564

36-
// If path already starts with the fixed part, return as is
37-
if (normalizedFixed && path.startsWith(normalizedFixed)) {
38-
return path
39-
}
40-
41-
// Otherwise, join fixed part with path
42-
return join(fixed, path)
65+
return joinSourcePath(sourceRoot, path)
4366
}
4467

4568
/**
@@ -70,8 +93,9 @@ export function getCollectionByFilePath(path: string, collections: Record<string
7093
const paths = path === '/' ? ['index.yml', 'index.yaml', 'index.md', 'index.json'] : [path]
7194
return paths.some((p) => {
7295
matchedSource = collection.source.find((source) => {
73-
const include = minimatch(p, source.include, { dot: true })
74-
const exclude = source.exclude?.some(exclude => minimatch(p, exclude))
96+
const sourceRoot = getSourceRoot(source)
97+
const include = minimatch(p, joinSourcePath(sourceRoot, source.include), { dot: true })
98+
const exclude = source.exclude?.some(exclude => minimatch(p, joinSourcePath(sourceRoot, exclude)))
7599

76100
return include && !exclude
77101
})

src/module/test/utils/collection.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,61 @@ describe('getCollectionByFilePath', () => {
5353
const result = generateFsPathFromId(id, source)
5454
expect(result).toBe('fs-prefix/hello.md')
5555
})
56+
57+
it('should keep root and prefixed collection indexes distinct', () => {
58+
const marketing: CollectionInfo = {
59+
name: 'marketing',
60+
pascalName: 'Marketing',
61+
tableName: '_content_marketing',
62+
source: [
63+
{
64+
_resolved: true,
65+
include: '*.md',
66+
prefix: '/',
67+
cwd: 'content',
68+
},
69+
],
70+
type: 'page',
71+
fields: {},
72+
schema: {
73+
$schema: 'http://json-schema.org/draft-07/schema#',
74+
$ref: '#/definitions/marketing',
75+
definitions: {},
76+
},
77+
tableDefinition: '',
78+
}
79+
80+
const guides: CollectionInfo = {
81+
name: 'guides',
82+
pascalName: 'Guides',
83+
tableName: '_content_guides',
84+
source: [
85+
{
86+
_resolved: true,
87+
include: '**/*.md',
88+
prefix: '/guides',
89+
cwd: 'content/guides',
90+
},
91+
],
92+
type: 'page',
93+
fields: {},
94+
schema: {
95+
$schema: 'http://json-schema.org/draft-07/schema#',
96+
$ref: '#/definitions/guides',
97+
definitions: {},
98+
},
99+
tableDefinition: '',
100+
}
101+
102+
const collections = { marketing, guides }
103+
104+
expect(generateIdFromFsPath('index.md', marketing)).toBe('marketing/index.md')
105+
expect(generateIdFromFsPath('guides/index.md', guides)).toBe('guides/guides/index.md')
106+
expect(generateFsPathFromId('marketing/index.md', marketing.source[0]!)).toBe('index.md')
107+
expect(generateFsPathFromId('guides/guides/index.md', guides.source[0]!)).toBe('guides/index.md')
108+
expect(getCollectionByFilePath('index.md', collections)?.name).toBe('marketing')
109+
expect(getCollectionByFilePath('guides/index.md', collections)?.name).toBe('guides')
110+
})
56111
})
57112

58113
describe('generateFsPathFromId', () => {

0 commit comments

Comments
 (0)