@buzola/vite-plugin 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/codegen.test.js +49 -1
- package/dist/__tests__/codegen.test.js.map +1 -1
- package/dist/__tests__/conventions.test.js +1 -1
- package/dist/__tests__/conventions.test.js.map +1 -1
- package/dist/__tests__/page-extractor.test.js +1 -1
- package/dist/__tests__/page-extractor.test.js.map +1 -1
- package/dist/__tests__/tree-builder.test.js +217 -78
- package/dist/__tests__/tree-builder.test.js.map +1 -1
- package/dist/generator/codegen.d.ts +1 -5
- package/dist/generator/codegen.d.ts.map +1 -1
- package/dist/generator/codegen.js +7 -9
- package/dist/generator/codegen.js.map +1 -1
- package/dist/generator/scanner.js +1 -1
- package/dist/generator/scanner.js.map +1 -1
- package/dist/generator/tree-builder.d.ts +7 -3
- package/dist/generator/tree-builder.d.ts.map +1 -1
- package/dist/generator/tree-builder.js +275 -188
- package/dist/generator/tree-builder.js.map +1 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +12 -11
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/codegen.test.ts +53 -1
- package/src/__tests__/conventions.test.ts +1 -1
- package/src/__tests__/page-extractor.test.ts +1 -1
- package/src/__tests__/tree-builder.test.ts +245 -81
- package/src/generator/codegen.ts +9 -11
- package/src/generator/scanner.ts +1 -1
- package/src/generator/tree-builder.ts +318 -224
- package/src/index.ts +12 -12
- package/src/plugin.ts +23 -12
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAMjD,MAAM,WAAW,mBAAmB;IACnC,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAMjD,MAAM,WAAW,mBAAmB;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CA4FtE"}
|
package/dist/plugin.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { generateRouteModule } from './generator/codegen';
|
|
4
|
-
import { scanRouteFiles } from './generator/scanner';
|
|
5
|
-
import { buildFileRouteTree } from './generator/tree-builder';
|
|
6
|
-
const VIRTUAL_MODULE_ID = 'virtual:buzola/routes';
|
|
7
|
-
const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
3
|
+
import { generateRouteModule } from './generator/codegen.js';
|
|
4
|
+
import { scanRouteFiles } from './generator/scanner.js';
|
|
5
|
+
import { buildFileRouteTree } from './generator/tree-builder.js';
|
|
8
6
|
/**
|
|
9
7
|
* Vite plugin for Buzola page-centric routing.
|
|
10
8
|
*
|
|
@@ -17,7 +15,10 @@ const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
|
17
15
|
* Provides `virtual:buzola/routes` virtual module.
|
|
18
16
|
*/
|
|
19
17
|
export function buzolaPlugin(options = {}) {
|
|
20
|
-
const { routesDir: routesDirOption = 'src/routes', output: outputOption = 'src/buzola.gen.ts', persistentParams: persistentParamsOption, } = options;
|
|
18
|
+
const { name: nameOption, routesDir: routesDirOption = 'src/routes', output: outputOption = 'src/buzola.gen.ts', persistentParams: persistentParamsOption, } = options;
|
|
19
|
+
const pluginName = nameOption ? `buzola:${nameOption}` : 'buzola';
|
|
20
|
+
const virtualModuleId = nameOption ? `virtual:buzola/${nameOption}/routes` : 'virtual:buzola/routes';
|
|
21
|
+
const resolvedVirtualId = '\0' + virtualModuleId;
|
|
21
22
|
let root;
|
|
22
23
|
let routesDir;
|
|
23
24
|
let outputPath;
|
|
@@ -34,7 +35,7 @@ export function buzolaPlugin(options = {}) {
|
|
|
34
35
|
return writeIfChanged(outputPath, code);
|
|
35
36
|
}
|
|
36
37
|
return {
|
|
37
|
-
name:
|
|
38
|
+
name: pluginName,
|
|
38
39
|
enforce: 'pre',
|
|
39
40
|
configResolved(config) {
|
|
40
41
|
root = config.root;
|
|
@@ -68,12 +69,12 @@ export function buzolaPlugin(options = {}) {
|
|
|
68
69
|
}
|
|
69
70
|
},
|
|
70
71
|
resolveId(id) {
|
|
71
|
-
if (id ===
|
|
72
|
-
return
|
|
72
|
+
if (id === virtualModuleId) {
|
|
73
|
+
return resolvedVirtualId;
|
|
73
74
|
}
|
|
74
75
|
},
|
|
75
76
|
load(id) {
|
|
76
|
-
if (id ===
|
|
77
|
+
if (id === resolvedVirtualId) {
|
|
77
78
|
return `export { routes, pageRegistry } from '${outputPath.replace(/\.ts$/, '')}';\n`;
|
|
78
79
|
}
|
|
79
80
|
},
|
|
@@ -82,7 +83,7 @@ export function buzolaPlugin(options = {}) {
|
|
|
82
83
|
return;
|
|
83
84
|
const changed = await generate((p) => server.ssrLoadModule(p));
|
|
84
85
|
if (changed) {
|
|
85
|
-
const mod = server.moduleGraph.getModuleById(
|
|
86
|
+
const mod = server.moduleGraph.getModuleById(resolvedVirtualId);
|
|
86
87
|
if (mod) {
|
|
87
88
|
server.moduleGraph.invalidateModule(mod);
|
|
88
89
|
}
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buzola/vite-plugin",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"directory": "packages/vite-plugin"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@buzola/router": "^0.0.
|
|
24
|
+
"@buzola/router": "^0.0.5"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^25.3.3",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { generateRouteModule } from '../generator/codegen'
|
|
2
|
+
import { generateRouteModule } from '../generator/codegen.js'
|
|
3
3
|
|
|
4
4
|
describe('generateRouteModule', () => {
|
|
5
5
|
it('emits BuzolaPersistentParams when persistentParams is provided', () => {
|
|
@@ -95,6 +95,58 @@ describe('generateRouteModule', () => {
|
|
|
95
95
|
expect(output).toContain("'users/detail': '/users/detail'")
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
+
it('generates matchPath when routePattern differs from file position', () => {
|
|
99
|
+
const output = generateRouteModule({
|
|
100
|
+
tree: [{
|
|
101
|
+
segment: 'login',
|
|
102
|
+
fullPath: '/login',
|
|
103
|
+
filePath: '/tmp/routes/login/_layout.tsx',
|
|
104
|
+
isLayout: true,
|
|
105
|
+
isIndex: false,
|
|
106
|
+
isNotFound: false,
|
|
107
|
+
isPathlessGroup: false,
|
|
108
|
+
children: [{
|
|
109
|
+
segment: '',
|
|
110
|
+
fullPath: '/login',
|
|
111
|
+
filePath: '/tmp/routes/login/index.tsx',
|
|
112
|
+
exportName: 'default',
|
|
113
|
+
isLayout: false,
|
|
114
|
+
isIndex: true,
|
|
115
|
+
isNotFound: false,
|
|
116
|
+
isPathlessGroup: false,
|
|
117
|
+
pageExports: [{ pageId: 'login', exportName: 'default', routePattern: '/', params: [] }],
|
|
118
|
+
children: [],
|
|
119
|
+
}],
|
|
120
|
+
}],
|
|
121
|
+
routesDir: '/tmp/routes',
|
|
122
|
+
outputPath: '/tmp/buzola.gen.ts',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(output).toContain("matchPath: '/'")
|
|
126
|
+
expect(output).toContain('isIndex: true')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('does not generate matchPath when routePattern matches file position', () => {
|
|
130
|
+
const output = generateRouteModule({
|
|
131
|
+
tree: [{
|
|
132
|
+
segment: 'users',
|
|
133
|
+
fullPath: '/users',
|
|
134
|
+
filePath: '/tmp/routes/users.tsx',
|
|
135
|
+
exportName: 'default',
|
|
136
|
+
isLayout: false,
|
|
137
|
+
isIndex: false,
|
|
138
|
+
isNotFound: false,
|
|
139
|
+
isPathlessGroup: false,
|
|
140
|
+
pageExports: [{ pageId: 'users', exportName: 'default', routePattern: '/users', params: [] }],
|
|
141
|
+
children: [],
|
|
142
|
+
}],
|
|
143
|
+
routesDir: '/tmp/routes',
|
|
144
|
+
outputPath: '/tmp/buzola.gen.ts',
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(output).not.toContain('matchPath')
|
|
148
|
+
})
|
|
149
|
+
|
|
98
150
|
it('generates separate imports for default and named exports from same file', () => {
|
|
99
151
|
const output = generateRouteModule({
|
|
100
152
|
tree: [{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { isRouteFile, parseDirName, parseFileName, parseRouteFile } from '../conventions'
|
|
2
|
+
import { isRouteFile, parseDirName, parseFileName, parseRouteFile } from '../conventions.js'
|
|
3
3
|
|
|
4
4
|
describe('isRouteFile', () => {
|
|
5
5
|
it('accepts .tsx files', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import type { ModuleLoader } from '../generator/page-extractor'
|
|
3
|
-
import type { ScannedFile } from '../generator/scanner'
|
|
4
|
-
import { buildFileRouteTree } from '../generator/tree-builder'
|
|
2
|
+
import type { ModuleLoader } from '../generator/page-extractor.js'
|
|
3
|
+
import type { ScannedFile } from '../generator/scanner.js'
|
|
4
|
+
import { buildFileRouteTree } from '../generator/tree-builder.js'
|
|
5
5
|
|
|
6
6
|
/** Module loader that returns empty modules (no page exports). */
|
|
7
7
|
const emptyLoader: ModuleLoader = async () => ({})
|
|
@@ -59,9 +59,27 @@ describe('buildFileRouteTree', () => {
|
|
|
59
59
|
{ absolutePath: '/app/src/routes/(auth)/login.tsx', relativePath: '(auth)/login.tsx' },
|
|
60
60
|
]
|
|
61
61
|
|
|
62
|
+
const tree = await buildFileRouteTree(files, emptyLoader)
|
|
63
|
+
// Without a pathless group layout, the file goes under root directly
|
|
64
|
+
const login = tree.find(n => n.segment === 'login')
|
|
65
|
+
expect(login).toBeDefined()
|
|
66
|
+
expect(login!.fullPath).toBe('/login')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('handles pathless group with layout', async () => {
|
|
70
|
+
const files: ScannedFile[] = [
|
|
71
|
+
{ absolutePath: '/app/src/routes/(auth)/_layout.tsx', relativePath: '(auth)/_layout.tsx' },
|
|
72
|
+
{ absolutePath: '/app/src/routes/(auth)/login.tsx', relativePath: '(auth)/login.tsx' },
|
|
73
|
+
]
|
|
74
|
+
|
|
62
75
|
const tree = await buildFileRouteTree(files, emptyLoader)
|
|
63
76
|
const authGroup = tree.find(n => n.isPathlessGroup)
|
|
64
77
|
expect(authGroup).toBeDefined()
|
|
78
|
+
expect(authGroup!.fullPath).toBe('/')
|
|
79
|
+
|
|
80
|
+
const login = authGroup!.children.find(n => n.segment === 'login')
|
|
81
|
+
expect(login).toBeDefined()
|
|
82
|
+
expect(login!.fullPath).toBe('/login')
|
|
65
83
|
})
|
|
66
84
|
|
|
67
85
|
it('uses plain file names as segments', async () => {
|
|
@@ -70,11 +88,10 @@ describe('buildFileRouteTree', () => {
|
|
|
70
88
|
]
|
|
71
89
|
|
|
72
90
|
const tree = await buildFileRouteTree(files, emptyLoader)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
77
|
-
expect(users!.children[0].fullPath).toBe('/users/detail')
|
|
91
|
+
// Without a users layout, the page goes under root with full segment
|
|
92
|
+
const detail = tree.find(n => n.fullPath === '/users/detail')
|
|
93
|
+
expect(detail).toBeDefined()
|
|
94
|
+
expect(detail!.segment).toBe('users/detail')
|
|
78
95
|
})
|
|
79
96
|
|
|
80
97
|
it('derives segment from page .route() via module loader', async () => {
|
|
@@ -97,16 +114,45 @@ describe('buildFileRouteTree', () => {
|
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
const tree = await buildFileRouteTree(files, loader)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(
|
|
103
|
-
expect(
|
|
104
|
-
expect(
|
|
105
|
-
expect(users!.children[0].pageExports).toEqual([
|
|
117
|
+
// Tree position is file-based (/users/detail), route pattern is in pageExports
|
|
118
|
+
const page = tree.find(n => n.fullPath === '/users/detail')
|
|
119
|
+
expect(page).toBeDefined()
|
|
120
|
+
expect(page!.segment).toBe('users/detail')
|
|
121
|
+
expect(page!.pageExports).toEqual([
|
|
106
122
|
{ pageId: 'users/detail', exportName: 'default', routePattern: '/users/:userId', params: [{ name: 'userId', optional: false, array: false }] },
|
|
107
123
|
])
|
|
108
124
|
})
|
|
109
125
|
|
|
126
|
+
it('derives segment from page .route() when under a layout', async () => {
|
|
127
|
+
const files: ScannedFile[] = [
|
|
128
|
+
{ absolutePath: '/app/src/routes/users/_layout.tsx', relativePath: 'users/_layout.tsx' },
|
|
129
|
+
{ absolutePath: '/app/src/routes/users/detail.tsx', relativePath: 'users/detail.tsx' },
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
const loader: ModuleLoader = async (p) => {
|
|
133
|
+
if (p === '/app/src/routes/users/detail.tsx') {
|
|
134
|
+
return {
|
|
135
|
+
default: {
|
|
136
|
+
__buzolaPage: true,
|
|
137
|
+
component: () => null,
|
|
138
|
+
route: '/users/:userId',
|
|
139
|
+
paramsMeta: [{ name: 'userId', optional: false, array: false }],
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tree = await buildFileRouteTree(files, loader)
|
|
147
|
+
const usersLayout = tree.find(n => n.isLayout && n.segment === 'users')
|
|
148
|
+
expect(usersLayout).toBeDefined()
|
|
149
|
+
// Tree position is file-based (detail), route pattern only affects URL matching
|
|
150
|
+
const page = usersLayout!.children.find(n => n.fullPath === '/users/detail')
|
|
151
|
+
expect(page).toBeDefined()
|
|
152
|
+
expect(page!.segment).toBe('detail')
|
|
153
|
+
expect(page!.pageExports![0].routePattern).toBe('/users/:userId')
|
|
154
|
+
})
|
|
155
|
+
|
|
110
156
|
it('derives route from file path when no .route()', async () => {
|
|
111
157
|
const files: ScannedFile[] = [
|
|
112
158
|
{ absolutePath: '/app/src/routes/users/detail.tsx', relativePath: 'users/detail.tsx' },
|
|
@@ -122,11 +168,9 @@ describe('buildFileRouteTree', () => {
|
|
|
122
168
|
})
|
|
123
169
|
|
|
124
170
|
const tree = await buildFileRouteTree(files, loader)
|
|
125
|
-
const
|
|
126
|
-
expect(
|
|
127
|
-
expect(
|
|
128
|
-
expect(users!.children[0].fullPath).toBe('/users/detail')
|
|
129
|
-
expect(users!.children[0].pageExports).toEqual([
|
|
171
|
+
const page = tree.find(n => n.fullPath === '/users/detail')
|
|
172
|
+
expect(page).toBeDefined()
|
|
173
|
+
expect(page!.pageExports).toEqual([
|
|
130
174
|
{ pageId: 'users/detail', exportName: 'default', routePattern: '/users/detail', params: [{ name: 'userId', optional: false, array: false }] },
|
|
131
175
|
])
|
|
132
176
|
})
|
|
@@ -145,14 +189,143 @@ describe('buildFileRouteTree', () => {
|
|
|
145
189
|
})
|
|
146
190
|
|
|
147
191
|
const tree = await buildFileRouteTree(files, loader)
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
expect(
|
|
151
|
-
expect(index!.pageExports).toEqual([
|
|
192
|
+
const page = tree.find(n => n.fullPath === '/users')
|
|
193
|
+
expect(page).toBeDefined()
|
|
194
|
+
expect(page!.pageExports).toEqual([
|
|
152
195
|
{ pageId: 'users', exportName: 'default', routePattern: '/users', params: [] },
|
|
153
196
|
])
|
|
154
197
|
})
|
|
155
198
|
|
|
199
|
+
// ─── Route escaping via .route() ─────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
it('index file with .route() stays under its file-system layout', async () => {
|
|
202
|
+
const files: ScannedFile[] = [
|
|
203
|
+
{ absolutePath: '/app/src/routes/login/_layout.tsx', relativePath: 'login/_layout.tsx' },
|
|
204
|
+
{ absolutePath: '/app/src/routes/login/index.tsx', relativePath: 'login/index.tsx' },
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
const loader: ModuleLoader = async (p) => {
|
|
208
|
+
if (p === '/app/src/routes/login/index.tsx') {
|
|
209
|
+
return {
|
|
210
|
+
default: {
|
|
211
|
+
__buzolaPage: true,
|
|
212
|
+
component: () => null,
|
|
213
|
+
route: '/login/testtest',
|
|
214
|
+
paramsMeta: [],
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const tree = await buildFileRouteTree(files, loader)
|
|
222
|
+
const loginLayout = tree.find(n => n.isLayout && n.segment === 'login')
|
|
223
|
+
expect(loginLayout).toBeDefined()
|
|
224
|
+
|
|
225
|
+
// Page stays under login layout as index (file-hierarchy), with custom routePattern
|
|
226
|
+
const page = loginLayout!.children.find(n => n.isIndex)
|
|
227
|
+
expect(page).toBeDefined()
|
|
228
|
+
expect(page!.fullPath).toBe('/login')
|
|
229
|
+
expect(page!.pageExports![0].routePattern).toBe('/login/testtest')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('index file with .route("/xx") stays under its file-system layout', async () => {
|
|
233
|
+
const files: ScannedFile[] = [
|
|
234
|
+
{ absolutePath: '/app/src/routes/login/_layout.tsx', relativePath: 'login/_layout.tsx' },
|
|
235
|
+
{ absolutePath: '/app/src/routes/login/index.tsx', relativePath: 'login/index.tsx' },
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
const loader: ModuleLoader = async (p) => {
|
|
239
|
+
if (p === '/app/src/routes/login/index.tsx') {
|
|
240
|
+
return {
|
|
241
|
+
default: {
|
|
242
|
+
__buzolaPage: true,
|
|
243
|
+
component: () => null,
|
|
244
|
+
route: '/xx',
|
|
245
|
+
paramsMeta: [],
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const tree = await buildFileRouteTree(files, loader)
|
|
253
|
+
const loginLayout = tree.find(n => n.isLayout && n.segment === 'login')
|
|
254
|
+
expect(loginLayout).toBeDefined()
|
|
255
|
+
|
|
256
|
+
// Page stays under login layout as index (file-hierarchy), URL is /xx
|
|
257
|
+
const page = loginLayout!.children.find(n => n.isIndex)
|
|
258
|
+
expect(page).toBeDefined()
|
|
259
|
+
expect(page!.fullPath).toBe('/login')
|
|
260
|
+
expect(page!.pageExports![0].routePattern).toBe('/xx')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('index file with .route("/") stays under its file-system layout', async () => {
|
|
264
|
+
const files: ScannedFile[] = [
|
|
265
|
+
{ absolutePath: '/app/src/routes/login/_layout.tsx', relativePath: 'login/_layout.tsx' },
|
|
266
|
+
{ absolutePath: '/app/src/routes/login/index.tsx', relativePath: 'login/index.tsx' },
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
const loader: ModuleLoader = async (p) => {
|
|
270
|
+
if (p === '/app/src/routes/login/index.tsx') {
|
|
271
|
+
return {
|
|
272
|
+
default: {
|
|
273
|
+
__buzolaPage: true,
|
|
274
|
+
component: () => null,
|
|
275
|
+
route: '/',
|
|
276
|
+
paramsMeta: [],
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const tree = await buildFileRouteTree(files, loader)
|
|
284
|
+
const loginLayout = tree.find(n => n.isLayout && n.segment === 'login')
|
|
285
|
+
expect(loginLayout).toBeDefined()
|
|
286
|
+
|
|
287
|
+
// Page stays under login layout as index, URL is /
|
|
288
|
+
const page = loginLayout!.children.find(n => n.isIndex)
|
|
289
|
+
expect(page).toBeDefined()
|
|
290
|
+
expect(page!.fullPath).toBe('/login')
|
|
291
|
+
expect(page!.pageExports![0].routePattern).toBe('/')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('index file with .route("/") under root layout keeps file-hierarchy position', async () => {
|
|
295
|
+
const files: ScannedFile[] = [
|
|
296
|
+
{ absolutePath: '/app/src/routes/_layout.tsx', relativePath: '_layout.tsx' },
|
|
297
|
+
{ absolutePath: '/app/src/routes/login/index.tsx', relativePath: 'login/index.tsx' },
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
const loader: ModuleLoader = async (p) => {
|
|
301
|
+
if (p === '/app/src/routes/login/index.tsx') {
|
|
302
|
+
return {
|
|
303
|
+
default: {
|
|
304
|
+
__buzolaPage: true,
|
|
305
|
+
component: () => null,
|
|
306
|
+
route: '/',
|
|
307
|
+
paramsMeta: [],
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return {}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const tree = await buildFileRouteTree(files, loader)
|
|
315
|
+
expect(tree).toHaveLength(1)
|
|
316
|
+
const rootLayout = tree[0]
|
|
317
|
+
expect(rootLayout.isLayout).toBe(true)
|
|
318
|
+
|
|
319
|
+
// No login layout → page is a child of root layout at file position /login
|
|
320
|
+
const page = rootLayout.children.find(n => n.fullPath === '/login')
|
|
321
|
+
expect(page).toBeDefined()
|
|
322
|
+
expect(page!.segment).toBe('login')
|
|
323
|
+
expect(page!.isIndex).toBe(false)
|
|
324
|
+
expect(page!.pageExports![0].routePattern).toBe('/')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// ─── Sorting ─────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
156
329
|
it('sorts not-found routes last', async () => {
|
|
157
330
|
const files: ScannedFile[] = [
|
|
158
331
|
{ absolutePath: '/app/src/routes/about.tsx', relativePath: 'about.tsx' },
|
|
@@ -196,7 +369,7 @@ describe('buildFileRouteTree', () => {
|
|
|
196
369
|
})
|
|
197
370
|
|
|
198
371
|
const tree = await buildFileRouteTree(files, loader)
|
|
199
|
-
const users = tree.find(n => n.segment === 'users')
|
|
372
|
+
const users = tree.find(n => n.segment === 'users' && n.filePath)
|
|
200
373
|
expect(users).toBeDefined()
|
|
201
374
|
expect(users!.filePath).toBe('/app/src/routes/users.tsx')
|
|
202
375
|
expect(users!.exportName).toBe('default')
|
|
@@ -204,20 +377,15 @@ describe('buildFileRouteTree', () => {
|
|
|
204
377
|
{ pageId: 'users', exportName: 'default', routePattern: '/users', params: [] },
|
|
205
378
|
])
|
|
206
379
|
|
|
207
|
-
// Named exports
|
|
208
|
-
|
|
209
|
-
const detail =
|
|
380
|
+
// Named exports: /users/detail and /users/settings are also under root
|
|
381
|
+
// (since there's no /users layout, they're siblings)
|
|
382
|
+
const detail = tree.find(n => n.fullPath === '/users/detail')
|
|
210
383
|
expect(detail).toBeDefined()
|
|
211
384
|
expect(detail!.filePath).toBe('/app/src/routes/users.tsx')
|
|
212
385
|
expect(detail!.exportName).toBe('detail')
|
|
213
|
-
expect(detail!.fullPath).toBe('/users/detail')
|
|
214
|
-
expect(detail!.pageExports).toEqual([
|
|
215
|
-
{ pageId: 'users/detail', exportName: 'detail', routePattern: '/users/detail', params: [{ name: 'id', optional: false, array: false }] },
|
|
216
|
-
])
|
|
217
386
|
|
|
218
|
-
const settings =
|
|
387
|
+
const settings = tree.find(n => n.fullPath === '/users/settings')
|
|
219
388
|
expect(settings).toBeDefined()
|
|
220
|
-
expect(settings!.fullPath).toBe('/users/settings')
|
|
221
389
|
})
|
|
222
390
|
|
|
223
391
|
it('creates sibling nodes for named exports from index files', async () => {
|
|
@@ -239,20 +407,17 @@ describe('buildFileRouteTree', () => {
|
|
|
239
407
|
})
|
|
240
408
|
|
|
241
409
|
const tree = await buildFileRouteTree(files, loader)
|
|
242
|
-
const users = tree.find(n => n.segment === 'users')
|
|
243
|
-
expect(users).toBeDefined()
|
|
244
410
|
|
|
245
|
-
// Default export → index
|
|
246
|
-
const
|
|
247
|
-
expect(
|
|
248
|
-
expect(
|
|
411
|
+
// Default export → /users (index-like, placed at root without layout)
|
|
412
|
+
const usersPage = tree.find(n => n.fullPath === '/users' && n.filePath)
|
|
413
|
+
expect(usersPage).toBeDefined()
|
|
414
|
+
expect(usersPage!.filePath).toBe('/app/src/routes/users/index.tsx')
|
|
249
415
|
|
|
250
|
-
// Named export →
|
|
251
|
-
const detail =
|
|
416
|
+
// Named export → /users/detail
|
|
417
|
+
const detail = tree.find(n => n.fullPath === '/users/detail')
|
|
252
418
|
expect(detail).toBeDefined()
|
|
253
419
|
expect(detail!.filePath).toBe('/app/src/routes/users/index.tsx')
|
|
254
420
|
expect(detail!.exportName).toBe('detail')
|
|
255
|
-
expect(detail!.fullPath).toBe('/users/detail')
|
|
256
421
|
expect(detail!.pageExports).toEqual([
|
|
257
422
|
{ pageId: 'users/detail', exportName: 'detail', routePattern: '/users/detail', params: [{ name: 'id', optional: false, array: false }] },
|
|
258
423
|
])
|
|
@@ -277,17 +442,13 @@ describe('buildFileRouteTree', () => {
|
|
|
277
442
|
})
|
|
278
443
|
|
|
279
444
|
const tree = await buildFileRouteTree(files, loader)
|
|
280
|
-
const
|
|
281
|
-
expect(
|
|
282
|
-
|
|
283
|
-
expect(
|
|
284
|
-
|
|
285
|
-
expect(users!.children).toHaveLength(2)
|
|
286
|
-
expect(users!.children.find(n => n.segment === 'detail')).toBeDefined()
|
|
287
|
-
expect(users!.children.find(n => n.segment === 'list')).toBeDefined()
|
|
445
|
+
const detail = tree.find(n => n.fullPath === '/users/detail')
|
|
446
|
+
expect(detail).toBeDefined()
|
|
447
|
+
const list = tree.find(n => n.fullPath === '/users/list')
|
|
448
|
+
expect(list).toBeDefined()
|
|
288
449
|
})
|
|
289
450
|
|
|
290
|
-
it('named export with .route()
|
|
451
|
+
it('named export with .route() keeps file-hierarchy position with custom routePattern', async () => {
|
|
291
452
|
const files: ScannedFile[] = [
|
|
292
453
|
{ absolutePath: '/app/src/routes/users.tsx', relativePath: 'users.tsx' },
|
|
293
454
|
]
|
|
@@ -302,13 +463,13 @@ describe('buildFileRouteTree', () => {
|
|
|
302
463
|
})
|
|
303
464
|
|
|
304
465
|
const tree = await buildFileRouteTree(files, loader)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
expect(
|
|
308
|
-
expect(
|
|
466
|
+
// Tree position is file-based (/users/detail), route pattern in pageExports
|
|
467
|
+
const page = tree.find(n => n.fullPath === '/users/detail')
|
|
468
|
+
expect(page).toBeDefined()
|
|
469
|
+
expect(page!.pageExports![0].routePattern).toBe('/users/:userId')
|
|
309
470
|
})
|
|
310
471
|
|
|
311
|
-
it('throws on collision:
|
|
472
|
+
it('throws on collision: two pages at same path', async () => {
|
|
312
473
|
const files: ScannedFile[] = [
|
|
313
474
|
{ absolutePath: '/app/src/routes/users.tsx', relativePath: 'users.tsx' },
|
|
314
475
|
{ absolutePath: '/app/src/routes/users/detail.tsx', relativePath: 'users/detail.tsx' },
|
|
@@ -336,45 +497,48 @@ describe('buildFileRouteTree', () => {
|
|
|
336
497
|
expect(buildFileRouteTree(files, loader)).rejects.toThrow(/Route collision/)
|
|
337
498
|
})
|
|
338
499
|
|
|
339
|
-
it('
|
|
500
|
+
it('named exports from root index.tsx have correct paths (no double slashes)', async () => {
|
|
340
501
|
const files: ScannedFile[] = [
|
|
341
|
-
{ absolutePath: '/app/src/routes/
|
|
342
|
-
{ absolutePath: '/app/src/routes/users/index.tsx', relativePath: 'users/index.tsx' },
|
|
502
|
+
{ absolutePath: '/app/src/routes/index.tsx', relativePath: 'index.tsx' },
|
|
343
503
|
]
|
|
344
504
|
|
|
345
|
-
const loader: ModuleLoader = async (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
__buzolaPage: true,
|
|
358
|
-
component: () => null,
|
|
359
|
-
paramsMeta: [],
|
|
360
|
-
},
|
|
361
|
-
}
|
|
362
|
-
}
|
|
505
|
+
const loader: ModuleLoader = async () => ({
|
|
506
|
+
default: {
|
|
507
|
+
__buzolaPage: true,
|
|
508
|
+
component: () => null,
|
|
509
|
+
paramsMeta: [],
|
|
510
|
+
},
|
|
511
|
+
dashboard: {
|
|
512
|
+
__buzolaPage: true,
|
|
513
|
+
component: () => null,
|
|
514
|
+
paramsMeta: [],
|
|
515
|
+
},
|
|
516
|
+
})
|
|
363
517
|
|
|
364
|
-
|
|
518
|
+
const tree = await buildFileRouteTree(files, loader)
|
|
519
|
+
|
|
520
|
+
const index = tree.find(n => n.isIndex)
|
|
521
|
+
expect(index).toBeDefined()
|
|
522
|
+
expect(index!.fullPath).toBe('/')
|
|
523
|
+
|
|
524
|
+
const dashboard = tree.find(n => n.segment === 'dashboard')
|
|
525
|
+
expect(dashboard).toBeDefined()
|
|
526
|
+
expect(dashboard!.fullPath).toBe('/dashboard')
|
|
527
|
+
expect(dashboard!.pageExports![0].routePattern).toBe('/dashboard')
|
|
365
528
|
})
|
|
366
529
|
|
|
367
530
|
it('handles nested _404 within a directory', async () => {
|
|
368
531
|
const files: ScannedFile[] = [
|
|
532
|
+
{ absolutePath: '/app/src/routes/users/_layout.tsx', relativePath: 'users/_layout.tsx' },
|
|
369
533
|
{ absolutePath: '/app/src/routes/users/index.tsx', relativePath: 'users/index.tsx' },
|
|
370
534
|
{ absolutePath: '/app/src/routes/users/_404.tsx', relativePath: 'users/_404.tsx' },
|
|
371
535
|
]
|
|
372
536
|
|
|
373
537
|
const tree = await buildFileRouteTree(files, emptyLoader)
|
|
374
|
-
const
|
|
375
|
-
expect(
|
|
538
|
+
const usersLayout = tree.find(n => n.isLayout && n.segment === 'users')
|
|
539
|
+
expect(usersLayout).toBeDefined()
|
|
376
540
|
|
|
377
|
-
const notFound =
|
|
541
|
+
const notFound = usersLayout!.children.find(n => n.isNotFound)
|
|
378
542
|
expect(notFound).toBeDefined()
|
|
379
543
|
expect(notFound!.segment).toBe(':__notFound+')
|
|
380
544
|
expect(notFound!.fullPath).toBe('/users/:__notFound+')
|