@buzola/vite-plugin 0.0.11 → 0.0.13

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.
Files changed (76) hide show
  1. package/dist/__tests__/plugin.test.d.ts +2 -0
  2. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  3. package/dist/__tests__/plugin.test.js +110 -0
  4. package/dist/__tests__/plugin.test.js.map +1 -0
  5. package/dist/index.d.ts +0 -14
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +0 -7
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugin.d.ts +2 -20
  10. package/dist/plugin.d.ts.map +1 -1
  11. package/dist/plugin.js +12 -31
  12. package/dist/plugin.js.map +1 -1
  13. package/package.json +3 -5
  14. package/src/__tests__/plugin.test.ts +129 -0
  15. package/src/index.ts +0 -21
  16. package/src/plugin.ts +22 -56
  17. package/dist/__tests__/codegen.test.d.ts +0 -2
  18. package/dist/__tests__/codegen.test.d.ts.map +0 -1
  19. package/dist/__tests__/codegen.test.js +0 -208
  20. package/dist/__tests__/codegen.test.js.map +0 -1
  21. package/dist/__tests__/conventions.test.d.ts +0 -2
  22. package/dist/__tests__/conventions.test.d.ts.map +0 -1
  23. package/dist/__tests__/conventions.test.js +0 -80
  24. package/dist/__tests__/conventions.test.js.map +0 -1
  25. package/dist/__tests__/page-extractor.test.d.ts +0 -2
  26. package/dist/__tests__/page-extractor.test.d.ts.map +0 -1
  27. package/dist/__tests__/page-extractor.test.js +0 -82
  28. package/dist/__tests__/page-extractor.test.js.map +0 -1
  29. package/dist/__tests__/tree-builder.test.d.ts +0 -2
  30. package/dist/__tests__/tree-builder.test.d.ts.map +0 -1
  31. package/dist/__tests__/tree-builder.test.js +0 -474
  32. package/dist/__tests__/tree-builder.test.js.map +0 -1
  33. package/dist/cli.d.ts +0 -3
  34. package/dist/cli.d.ts.map +0 -1
  35. package/dist/cli.js +0 -39
  36. package/dist/cli.js.map +0 -1
  37. package/dist/config.d.ts +0 -20
  38. package/dist/config.d.ts.map +0 -1
  39. package/dist/config.js +0 -20
  40. package/dist/config.js.map +0 -1
  41. package/dist/conventions.d.ts +0 -53
  42. package/dist/conventions.d.ts.map +0 -1
  43. package/dist/conventions.js +0 -82
  44. package/dist/conventions.js.map +0 -1
  45. package/dist/generate.d.ts +0 -26
  46. package/dist/generate.d.ts.map +0 -1
  47. package/dist/generate.js +0 -44
  48. package/dist/generate.js.map +0 -1
  49. package/dist/generator/codegen.d.ts +0 -19
  50. package/dist/generator/codegen.d.ts.map +0 -1
  51. package/dist/generator/codegen.js +0 -170
  52. package/dist/generator/codegen.js.map +0 -1
  53. package/dist/generator/page-extractor.d.ts +0 -21
  54. package/dist/generator/page-extractor.d.ts.map +0 -1
  55. package/dist/generator/page-extractor.js +0 -23
  56. package/dist/generator/page-extractor.js.map +0 -1
  57. package/dist/generator/scanner.d.ts +0 -11
  58. package/dist/generator/scanner.d.ts.map +0 -1
  59. package/dist/generator/scanner.js +0 -34
  60. package/dist/generator/scanner.js.map +0 -1
  61. package/dist/generator/tree-builder.d.ts +0 -54
  62. package/dist/generator/tree-builder.d.ts.map +0 -1
  63. package/dist/generator/tree-builder.js +0 -364
  64. package/dist/generator/tree-builder.js.map +0 -1
  65. package/src/__tests__/codegen.test.ts +0 -229
  66. package/src/__tests__/conventions.test.ts +0 -93
  67. package/src/__tests__/page-extractor.test.ts +0 -95
  68. package/src/__tests__/tree-builder.test.ts +0 -554
  69. package/src/cli.ts +0 -43
  70. package/src/config.ts +0 -35
  71. package/src/conventions.ts +0 -116
  72. package/src/generate.ts +0 -64
  73. package/src/generator/codegen.ts +0 -221
  74. package/src/generator/page-extractor.ts +0 -48
  75. package/src/generator/scanner.ts +0 -45
  76. package/src/generator/tree-builder.ts +0 -454
@@ -1,454 +0,0 @@
1
- import * as path from 'node:path'
2
- import { parseDirName, parseFileName } from '../conventions.js'
3
- import type { ExtractedPage, ModuleLoader } from './page-extractor.js'
4
- import { extractPages } from './page-extractor.js'
5
- import type { ScannedFile } from './scanner.js'
6
-
7
- /**
8
- * Page export info attached to a file route node.
9
- */
10
- export interface PageExportInfo {
11
- /** Page ID (e.g., "project/detail"). */
12
- pageId: string
13
- /** Export name (e.g., "default", "detail"). */
14
- exportName: string
15
- /** Route pattern (e.g., "/project/:id"). */
16
- routePattern: string
17
- /** Param info extracted from the page schema. */
18
- params: { name: string; optional: boolean; array: boolean }[]
19
- }
20
-
21
- /**
22
- * A node in the route tree (before compilation into RouteNode).
23
- */
24
- export interface FileRouteNode {
25
- /** Route path segment (from file name, or derived from .route() pattern). */
26
- segment: string
27
- /** Full path from root. */
28
- fullPath: string
29
- /** Absolute file path to the component. */
30
- filePath?: string
31
- /** Which export to use from the file ("default" or a named export). */
32
- exportName?: string
33
- /** Whether this is a layout. */
34
- isLayout: boolean
35
- /** Whether this is an index route. */
36
- isIndex: boolean
37
- /** Whether this is a not-found catch-all. */
38
- isNotFound: boolean
39
- /** Whether this is a pathless group. */
40
- isPathlessGroup: boolean
41
- /** Page exports discovered in this file. */
42
- pageExports?: PageExportInfo[]
43
- /** Children nodes. */
44
- children: FileRouteNode[]
45
- }
46
-
47
- // ─── Flat route entry (intermediate representation) ─────────────────────────
48
-
49
- interface RouteEntry {
50
- routePattern: string
51
- /** File-hierarchy-based position for tree building (may differ from routePattern when .route() is used). */
52
- treePosition: string
53
- filePath: string
54
- exportName: string
55
- kind: 'page' | 'layout' | 'notFound'
56
- pageId?: string
57
- params: { name: string; optional: boolean; array: boolean }[]
58
- /** File-system-derived URL path (ignoring pathless groups). */
59
- fileScope: string
60
- /** Chain of pathless group directory paths this file is nested under. */
61
- pathlessGroups: string[]
62
- /** For pathless group layouts: the directory path of the group. */
63
- pathlessGroupId?: string
64
- }
65
-
66
- // ─── Public API ─────────────────────────────────────────────────────────────
67
-
68
- /**
69
- * Build a route tree from scanned files.
70
- * Loads each module via moduleLoader to extract createPage() metadata.
71
- *
72
- * Two-phase approach:
73
- * 1. Extract flat route entries from all files.
74
- * 2. Build the tree using route-pattern prefix matching (not file hierarchy).
75
- */
76
- export async function buildFileRouteTree(files: ScannedFile[], moduleLoader: ModuleLoader): Promise<FileRouteNode[]> {
77
- const entries = await extractEntries(files, moduleLoader)
78
- return buildTreeFromEntries(entries)
79
- }
80
-
81
- // ─── Phase 1: Extract flat route entries ────────────────────────────────────
82
-
83
- async function extractEntries(files: ScannedFile[], moduleLoader: ModuleLoader): Promise<RouteEntry[]> {
84
- const entries: RouteEntry[] = []
85
-
86
- for (const file of files) {
87
- const ext = path.extname(file.relativePath)
88
- const withoutExt = file.relativePath.slice(0, -ext.length)
89
- const parts = withoutExt.split(path.sep)
90
- const fileName = parts.pop()!
91
- const dirs = parts
92
- const fileInfo = parseFileName(fileName)
93
-
94
- // Compute fileScope (URL path from directory structure, skipping pathless groups)
95
- // and pathlessGroups chain
96
- let fileScope = ''
97
- const pathlessGroups: string[] = []
98
- let groupPath = ''
99
-
100
- for (const dir of dirs) {
101
- groupPath = groupPath ? `${groupPath}/${dir}` : dir
102
- const dirInfo = parseDirName(dir)
103
- if (dirInfo.isPathlessGroup) {
104
- pathlessGroups.push(groupPath)
105
- } else {
106
- fileScope = joinPath(fileScope, dirInfo.segment)
107
- }
108
- }
109
- if (!fileScope) fileScope = '/'
110
-
111
- if (fileInfo.isLayout) {
112
- // Detect pathless group layout: immediate parent dir is a pathless group
113
- const lastDir = dirs.length > 0 ? dirs[dirs.length - 1] : null
114
- const isPathlessGroupLayout = lastDir != null && parseDirName(lastDir).isPathlessGroup
115
-
116
- entries.push({
117
- routePattern: fileScope,
118
- treePosition: fileScope,
119
- filePath: file.absolutePath,
120
- exportName: 'default',
121
- kind: 'layout',
122
- params: [],
123
- fileScope,
124
- pathlessGroups,
125
- pathlessGroupId: isPathlessGroupLayout ? groupPath : undefined,
126
- })
127
- } else if (fileInfo.isNotFound) {
128
- const catchAllPattern = fileScope === '/' ? '/:__notFound+' : `${fileScope}/:__notFound+`
129
- const pageId = dirs.length === 0 ? '404' : `${dirs.join('/')}/404`
130
- entries.push({
131
- routePattern: catchAllPattern,
132
- treePosition: catchAllPattern,
133
- filePath: file.absolutePath,
134
- exportName: 'default',
135
- kind: 'notFound',
136
- pageId,
137
- params: [{ name: '__notFound', optional: false, array: false }],
138
- fileScope,
139
- pathlessGroups,
140
- })
141
- } else {
142
- const isIndex = fileInfo.isIndex
143
- const basePath = isIndex ? fileScope : joinPath(fileScope, fileName)
144
-
145
- const pages = await safeExtractPages(file, moduleLoader)
146
-
147
- if (pages.length === 0) {
148
- // No createPage() — plain component
149
- entries.push({
150
- routePattern: basePath,
151
- treePosition: basePath,
152
- filePath: file.absolutePath,
153
- exportName: 'default',
154
- kind: 'page',
155
- params: [],
156
- fileScope,
157
- pathlessGroups,
158
- })
159
- } else {
160
- for (const page of pages) {
161
- const pageId = derivePageId(dirs, fileName, isIndex, page.exportName)
162
- const defaultPattern = page.isDefault ? basePath : joinPath(basePath, page.exportName)
163
- const routePattern = page.route ?? defaultPattern
164
- entries.push({
165
- routePattern,
166
- treePosition: defaultPattern,
167
- filePath: file.absolutePath,
168
- exportName: page.exportName,
169
- kind: 'page',
170
- pageId,
171
- params: page.params,
172
- fileScope,
173
- pathlessGroups,
174
- })
175
- }
176
- }
177
- }
178
- }
179
-
180
- return entries
181
- }
182
-
183
- // ─── Phase 2: Build tree from flat entries ──────────────────────────────────
184
-
185
- interface LayoutRef {
186
- node: FileRouteNode
187
- pathlessGroupId?: string
188
- }
189
-
190
- function buildTreeFromEntries(entries: RouteEntry[]): FileRouteNode[] {
191
- const root: FileRouteNode = {
192
- segment: '',
193
- fullPath: '/',
194
- isLayout: false,
195
- isIndex: false,
196
- isNotFound: false,
197
- isPathlessGroup: false,
198
- children: [],
199
- }
200
-
201
- const layouts = entries.filter(e => e.kind === 'layout')
202
- const pages = entries.filter(e => e.kind !== 'layout')
203
-
204
- // Sort layouts: shorter routePattern first, non-pathless before pathless
205
- layouts.sort((a, b) => {
206
- if (a.routePattern.length !== b.routePattern.length) return a.routePattern.length - b.routePattern.length
207
- if (a.pathlessGroupId && !b.pathlessGroupId) return 1
208
- if (!a.pathlessGroupId && b.pathlessGroupId) return -1
209
- return 0
210
- })
211
-
212
- // Insert layouts into tree
213
- // Track all layout nodes for page assignment
214
- const layoutRefs: LayoutRef[] = [{ node: root }]
215
-
216
- for (const layout of layouts) {
217
- if (layout.pathlessGroupId) {
218
- // Pathless group layout — find parent by walking layoutRefs, respecting pathless group chain
219
- const parent = findParentForPathlessGroupLayout(layoutRefs, layout)
220
- const node: FileRouteNode = {
221
- segment: '',
222
- fullPath: layout.treePosition,
223
- filePath: layout.filePath,
224
- isLayout: true,
225
- isIndex: false,
226
- isNotFound: false,
227
- isPathlessGroup: true,
228
- children: [],
229
- }
230
- parent.node.children.push(node)
231
- layoutRefs.push({ node, pathlessGroupId: layout.pathlessGroupId })
232
- } else if (layout.treePosition === '/' && !root.filePath) {
233
- // Root layout
234
- root.isLayout = true
235
- root.filePath = layout.filePath
236
- } else if (layout.treePosition === '/') {
237
- // Additional layout at root (shouldn't happen normally, but handle)
238
- root.isLayout = true
239
- root.filePath = layout.filePath
240
- } else {
241
- // Regular layout — find parent by prefix matching
242
- const parent = findDeepestLayoutParent(layoutRefs, layout.treePosition, layout.pathlessGroups)
243
- const segment = computeSegmentFromParent(parent.node.fullPath, layout.treePosition)
244
- const node: FileRouteNode = {
245
- segment,
246
- fullPath: layout.treePosition,
247
- filePath: layout.filePath,
248
- isLayout: true,
249
- isIndex: false,
250
- isNotFound: false,
251
- isPathlessGroup: false,
252
- children: [],
253
- }
254
- parent.node.children.push(node)
255
- layoutRefs.push({ node })
256
- }
257
- }
258
-
259
- // Insert pages into tree (using file-hierarchy position, not route pattern)
260
- for (const entry of pages) {
261
- const parent = findDeepestLayoutParent(layoutRefs, entry.treePosition, entry.pathlessGroups)
262
- const isIndex = entry.treePosition === parent.node.fullPath
263
- const segment = isIndex ? '' : computeSegmentFromParent(parent.node.fullPath, entry.treePosition)
264
-
265
- const pageExports: PageExportInfo[] | undefined = entry.pageId
266
- ? [{
267
- pageId: entry.pageId,
268
- exportName: entry.exportName,
269
- routePattern: entry.routePattern,
270
- params: entry.params,
271
- }]
272
- : undefined
273
-
274
- // For named exports from the same file as an existing node, check collision
275
- if (!isIndex) {
276
- checkCollision(parent.node.children, segment, entry.filePath, entry.exportName)
277
- }
278
-
279
- // Try to reuse an existing container node (e.g., from a directory that has no _layout.tsx)
280
- const existing = !isIndex
281
- ? parent.node.children.find(c => c.segment === segment && !c.filePath && !c.isLayout && !c.isPathlessGroup)
282
- : undefined
283
-
284
- if (existing) {
285
- existing.filePath = entry.filePath
286
- existing.exportName = entry.exportName
287
- existing.pageExports = pageExports
288
- } else {
289
- parent.node.children.push({
290
- segment,
291
- fullPath: entry.treePosition,
292
- filePath: entry.filePath,
293
- exportName: entry.exportName,
294
- isLayout: false,
295
- isIndex,
296
- isNotFound: entry.kind === 'notFound',
297
- isPathlessGroup: false,
298
- pageExports,
299
- children: [],
300
- })
301
- }
302
- }
303
-
304
- // Sort children recursively
305
- sortChildren(root)
306
-
307
- if (root.isLayout && root.filePath) {
308
- return [root]
309
- }
310
-
311
- return root.children
312
- }
313
-
314
- /**
315
- * Find the parent layout for a pathless group layout.
316
- * The parent is the deepest layout at the same routePattern level,
317
- * considering the pathless group nesting chain.
318
- */
319
- function findParentForPathlessGroupLayout(layoutRefs: LayoutRef[], entry: RouteEntry): LayoutRef {
320
- // The parent is the layout whose pathlessGroupId is the previous entry in the chain,
321
- // or the deepest non-pathless layout at the same routePattern if this is the first group.
322
- const groupChain = entry.pathlessGroups
323
- const thisGroupIndex = groupChain.indexOf(entry.pathlessGroupId!)
324
-
325
- if (thisGroupIndex > 0) {
326
- // Has a parent pathless group — find it
327
- const parentGroupId = groupChain[thisGroupIndex - 1]
328
- const parentRef = layoutRefs.find(r => r.pathlessGroupId === parentGroupId)
329
- if (parentRef) return parentRef
330
- }
331
-
332
- // First pathless group in chain (or parent not found) — find the deepest regular layout at prefix
333
- let best = layoutRefs[0] // root
334
- for (const ref of layoutRefs) {
335
- if (ref.pathlessGroupId) continue // skip other pathless groups
336
- if (isPathPrefixOrEqual(ref.node.fullPath, entry.routePattern) && ref.node.fullPath.length >= best.node.fullPath.length) {
337
- best = ref
338
- }
339
- }
340
- return best
341
- }
342
-
343
- /**
344
- * Find the deepest layout whose routePattern is a prefix of the given pattern.
345
- * Respects pathless group membership.
346
- */
347
- function findDeepestLayoutParent(layoutRefs: LayoutRef[], routePattern: string, pathlessGroups: string[]): LayoutRef {
348
- let best = layoutRefs[0] // root
349
-
350
- for (const ref of layoutRefs) {
351
- if (!isPathPrefixOrEqual(ref.node.fullPath, routePattern)) continue
352
-
353
- // For pathless groups, only match if the entry belongs to this group
354
- if (ref.pathlessGroupId && !pathlessGroups.includes(ref.pathlessGroupId)) continue
355
-
356
- // Prefer deeper matches. For ties (pathless groups at same depth), prefer pathless group
357
- // that the entry belongs to over non-pathless.
358
- if (ref.node.fullPath.length > best.node.fullPath.length) {
359
- best = ref
360
- } else if (ref.node.fullPath.length === best.node.fullPath.length && ref.pathlessGroupId && pathlessGroups.includes(ref.pathlessGroupId)) {
361
- best = ref
362
- }
363
- }
364
-
365
- return best
366
- }
367
-
368
- function isPathPrefixOrEqual(prefix: string, path: string): boolean {
369
- if (prefix === '/') return true
370
- return path === prefix || path.startsWith(prefix + '/')
371
- }
372
-
373
- function computeSegmentFromParent(parentFullPath: string, childFullPath: string): string {
374
- if (parentFullPath === '/') return childFullPath.slice(1)
375
- return childFullPath.slice(parentFullPath.length + 1)
376
- }
377
-
378
- // ─── Helpers ────────────────────────────────────────────────────────────────
379
-
380
- /**
381
- * Derive page ID from file path and export name.
382
- */
383
- function derivePageId(dirs: string[], fileName: string, isIndex: boolean, exportName: string): string {
384
- const segments = [...dirs]
385
-
386
- if (isIndex) {
387
- if (segments.length === 0) {
388
- return exportName === 'default' ? 'index' : `index/${exportName}`
389
- }
390
- const base = segments.join('/')
391
- return exportName === 'default' ? base : `${base}/${exportName}`
392
- }
393
-
394
- segments.push(fileName)
395
- const base = segments.join('/')
396
-
397
- return exportName === 'default' ? base : `${base}/${exportName}`
398
- }
399
-
400
- async function safeExtractPages(file: ScannedFile, moduleLoader: ModuleLoader): Promise<ExtractedPage[]> {
401
- try {
402
- return await extractPages(file.absolutePath, moduleLoader)
403
- } catch (error) {
404
- const message = error instanceof Error ? error.message : String(error)
405
- const stack = error instanceof Error && error.stack ? `\n${error.stack}` : ''
406
- console.warn(
407
- `[buzola] Failed to extract page metadata from "${file.relativePath}". `
408
- + `The file will be treated as a plain component (no typed params, no loader, no custom route). `
409
- + `Cause: ${message}${stack}`,
410
- )
411
- return []
412
- }
413
- }
414
-
415
- function checkCollision(siblings: FileRouteNode[], segment: string, filePath: string, exportName: string): void {
416
- const existing = siblings.find(c => c.segment === segment && !c.isLayout && !c.isPathlessGroup)
417
- if (!existing || !existing.filePath) return
418
-
419
- const existingDesc = existing.exportName && existing.exportName !== 'default'
420
- ? `named export '${existing.exportName}' in '${existing.filePath}'`
421
- : `'${existing.filePath}'`
422
- const newDesc = exportName !== 'default'
423
- ? `named export '${exportName}' in '${filePath}'`
424
- : `'${filePath}'`
425
-
426
- throw new Error(`Route collision: ${newDesc} conflicts with ${existingDesc} — both resolve to segment '${segment}'`)
427
- }
428
-
429
- function sortChildren(node: FileRouteNode): void {
430
- for (const child of node.children) {
431
- sortChildren(child)
432
- }
433
- node.children.sort((a, b) => sortWeight(a) - sortWeight(b))
434
- }
435
-
436
- const SORT_INDEX = 0
437
- const SORT_STATIC = 1
438
- const SORT_DYNAMIC = 2
439
- const SORT_CATCHALL = 3
440
- const SORT_NOT_FOUND = 4
441
-
442
- function sortWeight(node: FileRouteNode): number {
443
- if (node.isIndex) return SORT_INDEX
444
- if (node.isNotFound) return SORT_NOT_FOUND
445
- if (node.segment.includes(':') && node.segment.includes('+')) return SORT_CATCHALL
446
- if (node.segment.includes(':')) return SORT_DYNAMIC
447
- return SORT_STATIC
448
- }
449
-
450
- function joinPath(parent: string, child: string): string {
451
- if (!child) return parent || '/'
452
- if (!parent || parent === '/') return `/${child}`
453
- return `${parent}/${child}`
454
- }