@buzola/vite-plugin 0.0.10 → 0.0.12

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 -169
  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 -163
  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 -186
  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 -213
  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
package/src/config.ts DELETED
@@ -1,35 +0,0 @@
1
- import * as fs from 'node:fs'
2
- import * as path from 'node:path'
3
-
4
- export interface BuzolaConfig {
5
- /** Path to routes directory, relative to project root. Defaults to "src/routes". */
6
- routesDir?: string
7
- /** Path to output generated file, relative to project root. Defaults to "src/buzola.gen.ts". */
8
- output?: string
9
- /**
10
- * Parameter names that should be persistent across navigations.
11
- * When set, generates a BuzolaPersistentParams type augmentation
12
- * so these params become optional in Link and navigate() calls.
13
- */
14
- persistentParams?: string[]
15
- }
16
-
17
- const CONFIG_FILES = ['buzola.config.ts', 'buzola.config.js', 'buzola.config.mjs']
18
-
19
- /**
20
- * Load buzola config from the project root.
21
- * Looks for `buzola.config.ts`, `buzola.config.js`, or `buzola.config.mjs`.
22
- *
23
- * @returns The loaded config, or an empty object if no config file is found.
24
- */
25
- export async function loadConfig(root: string): Promise<BuzolaConfig> {
26
- for (const file of CONFIG_FILES) {
27
- const configPath = path.resolve(root, file)
28
- if (!fs.existsSync(configPath)) continue
29
-
30
- const mod = await import(configPath)
31
- return mod.default ?? mod
32
- }
33
-
34
- return {}
35
- }
@@ -1,116 +0,0 @@
1
- import * as path from 'node:path'
2
-
3
- /**
4
- * File naming conventions for page-centric routing.
5
- *
6
- * _layout.tsx → Layout wrapper (renders children via <Outlet />)
7
- * _404.tsx → Not-found catch-all (auto-assigned catch-all pattern within parent scope)
8
- * index.tsx → Index route (renders at parent's exact path)
9
- * about.tsx → Named page file (page ID derived from file name)
10
- * (group)/ → Pathless group (no URL segment)
11
- *
12
- * Files prefixed with `_` are special convention files, not regular pages.
13
- * Route patterns (URL paths) come exclusively from `.route()` on `createPage()`.
14
- * File names only determine page IDs and layout nesting.
15
- */
16
-
17
- export const ROUTE_FILE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']
18
-
19
- export interface ParsedRouteFile {
20
- /** The route segment this file contributes. */
21
- segment: string
22
- /** Whether this is a layout file. */
23
- isLayout: boolean
24
- /** Whether this is an index file. */
25
- isIndex: boolean
26
- /** Whether this is a not-found catch-all file. */
27
- isNotFound: boolean
28
- /** Whether this directory is a pathless group. */
29
- isPathlessGroup: boolean
30
- /** Original file path relative to routes dir. */
31
- relativePath: string
32
- }
33
-
34
- /**
35
- * Check if a file is a route file based on extension.
36
- */
37
- export function isRouteFile(filePath: string): boolean {
38
- return ROUTE_FILE_EXTENSIONS.some(ext => filePath.endsWith(ext))
39
- }
40
-
41
- /**
42
- * Parse a directory name for routing conventions.
43
- */
44
- export function parseDirName(name: string): { segment: string; isPathlessGroup: boolean } {
45
- // Pathless group: (auth)
46
- if (name.startsWith('(') && name.endsWith(')')) {
47
- return { segment: '', isPathlessGroup: true }
48
- }
49
-
50
- return { segment: name, isPathlessGroup: false }
51
- }
52
-
53
- /**
54
- * Parse a route file name (without extension) into routing info.
55
- */
56
- export function parseFileName(nameWithoutExt: string): {
57
- segment: string
58
- isLayout: boolean
59
- isIndex: boolean
60
- isNotFound: boolean
61
- } {
62
- if (nameWithoutExt === '_layout') {
63
- return { segment: '', isLayout: true, isIndex: false, isNotFound: false }
64
- }
65
-
66
- if (nameWithoutExt === '_404') {
67
- return { segment: '', isLayout: false, isIndex: false, isNotFound: true }
68
- }
69
-
70
- if (nameWithoutExt === 'index') {
71
- return { segment: '', isLayout: false, isIndex: true, isNotFound: false }
72
- }
73
-
74
- return { segment: nameWithoutExt, isLayout: false, isIndex: false, isNotFound: false }
75
- }
76
-
77
- /**
78
- * Parse a route file path relative to the routes directory.
79
- */
80
- export function parseRouteFile(relativePath: string): ParsedRouteFile {
81
- const ext = path.extname(relativePath)
82
- const withoutExt = relativePath.slice(0, -ext.length)
83
- const parts = withoutExt.split(path.sep)
84
- const fileName = parts.pop()!
85
- const dirs = parts
86
-
87
- const fileInfo = parseFileName(fileName)
88
-
89
- // Build segment from directory path + file
90
- const segments: string[] = []
91
- let isPathlessGroup = false
92
-
93
- for (const dir of dirs) {
94
- const dirInfo = parseDirName(dir)
95
- if (dirInfo.isPathlessGroup) {
96
- isPathlessGroup = true
97
- } else {
98
- segments.push(dirInfo.segment)
99
- }
100
- }
101
-
102
- if (fileInfo.segment) {
103
- segments.push(fileInfo.segment)
104
- }
105
-
106
- const segment = segments.length > 0 ? segments.join('/') : ''
107
-
108
- return {
109
- segment,
110
- isLayout: fileInfo.isLayout,
111
- isIndex: fileInfo.isIndex,
112
- isNotFound: fileInfo.isNotFound,
113
- isPathlessGroup,
114
- relativePath,
115
- }
116
- }
package/src/generate.ts DELETED
@@ -1,64 +0,0 @@
1
- import * as fs from 'node:fs'
2
- import { generateRouteModule } from './generator/codegen.js'
3
- import type { ModuleLoader } from './generator/page-extractor.js'
4
- import { scanRouteFiles } from './generator/scanner.js'
5
- import { buildFileRouteTree } from './generator/tree-builder.js'
6
-
7
- export interface GenerateOptions {
8
- /** Absolute path to the routes directory. */
9
- routesDir: string
10
- /** Absolute path to the output file. */
11
- outputPath: string
12
- /** Parameter names that should be persistent across navigations. */
13
- persistentParams?: string[]
14
- /** Custom module loader. Defaults to native `import()`. */
15
- moduleLoader?: ModuleLoader
16
- }
17
-
18
- /**
19
- * Generate the `buzola.gen.ts` route module from a routes directory.
20
- *
21
- * Works standalone (no Vite required) — uses native `import()` as the
22
- * default module loader. Pass a custom `moduleLoader` to integrate with
23
- * Vite's `ssrLoadModule` or other bundlers.
24
- *
25
- * Note: The default module loader uses native `import()`, which can load
26
- * `.ts`/`.tsx` files with Bun but not with Node.js. For Node.js, provide
27
- * a custom `moduleLoader` (e.g. via Vite's `ssrLoadModule`).
28
- *
29
- * @returns `true` if the output file was written (content changed), `false` otherwise.
30
- */
31
- export async function generate(options: GenerateOptions): Promise<boolean> {
32
- const { routesDir, outputPath, persistentParams, moduleLoader = defaultModuleLoader } = options
33
-
34
- if (!fs.existsSync(routesDir)) {
35
- throw new Error(`Routes directory does not exist: ${routesDir}`)
36
- }
37
-
38
- const files = scanRouteFiles(routesDir)
39
- const tree = await buildFileRouteTree(files, moduleLoader)
40
- const code = generateRouteModule({ tree, routesDir, outputPath, persistentParams })
41
-
42
- return writeIfChanged(outputPath, code)
43
- }
44
-
45
- async function defaultModuleLoader(absolutePath: string): Promise<Record<string, unknown>> {
46
- try {
47
- return await import(absolutePath)
48
- } catch (error) {
49
- throw new Error(
50
- `Failed to load module "${absolutePath}". The default module loader uses native import() which `
51
- + `supports .ts/.tsx files with Bun but not Node.js. For Node.js, provide a custom moduleLoader option.`,
52
- { cause: error },
53
- )
54
- }
55
- }
56
-
57
- function writeIfChanged(filePath: string, content: string): boolean {
58
- if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf-8') === content) {
59
- return false
60
- }
61
-
62
- fs.writeFileSync(filePath, content, 'utf-8')
63
- return true
64
- }
@@ -1,213 +0,0 @@
1
- import * as path from 'node:path'
2
- import type { FileRouteNode } from './tree-builder.js'
3
-
4
- export interface CodegenOptions {
5
- /** The route tree from file scanning. */
6
- tree: FileRouteNode[]
7
- /** Absolute path to the routes directory. */
8
- routesDir: string
9
- /** Absolute path to the output file. */
10
- outputPath: string
11
- /** Parameter names that should be persistent across navigations. */
12
- persistentParams?: string[]
13
- }
14
-
15
- /**
16
- * Generate the buzola.gen.ts file with:
17
- * 1. Module augmentation for BuzolaPageMap (type safety)
18
- * 2. Page registry (page ID → route pattern)
19
- * 3. Route tree construction code
20
- */
21
- export function generateRouteModule(options: CodegenOptions): string {
22
- const { tree, outputPath } = options
23
- const outputDir = path.dirname(outputPath)
24
- let importCounter = 0
25
-
26
- const lines: string[] = []
27
-
28
- lines.push('// This file is auto-generated by @buzola/vite-plugin. Do not edit.')
29
- lines.push('/* eslint-disable */')
30
- lines.push('')
31
-
32
- // Framework imports first — needed by lazy component declarations below
33
- lines.push(`import { lazy, type ComponentType } from 'react';`)
34
- lines.push(`import { buildRouteTree } from '@buzola/router';`)
35
- lines.push(`import type { RouteConfig } from '@buzola/router';`)
36
- lines.push('')
37
-
38
- // Collect route imports — importsByKey maps "filePath::exportName" → import info
39
- const imports: { varName: string; filePath: string; isLazy: boolean; exportName: string }[] = []
40
- const importsByKey = new Map<string, { varName: string; isLazy: boolean }>()
41
- collectImports(tree, outputDir, imports, importsByKey, () => importCounter++)
42
-
43
- // Generate route imports
44
- for (const imp of imports) {
45
- if (imp.isLazy) {
46
- lines.push(
47
- `const ${imp.varName} = () => import('${imp.filePath}').then(m => ({ default: m.${imp.exportName}.component })) as Promise<{ default: ComponentType }>;`,
48
- )
49
- } else {
50
- if (imp.exportName !== 'default') {
51
- lines.push(`import { ${imp.exportName} as ${imp.varName} } from '${imp.filePath}';`)
52
- } else {
53
- lines.push(`import ${imp.varName} from '${imp.filePath}';`)
54
- }
55
- }
56
- }
57
-
58
- lines.push('')
59
-
60
- // Collect page info for type augmentation and registry
61
- const pages = collectPages(tree)
62
-
63
- // Generate page map type augmentation
64
- lines.push('// Module augmentation for type-safe page-centric routing')
65
- lines.push(`declare module '@buzola/router' {`)
66
- lines.push(' interface BuzolaPageMap {')
67
- for (const page of pages) {
68
- if (page.params.length === 0) {
69
- lines.push(` '${page.pageId}': {};`)
70
- } else {
71
- const paramEntries = page.params.map(p => {
72
- const type = p.array ? 'string[]' : 'string'
73
- return p.optional ? `${p.name}?: ${type}` : `${p.name}: ${type}`
74
- }).join('; ')
75
- lines.push(` '${page.pageId}': { ${paramEntries} };`)
76
- }
77
- }
78
- lines.push(' }')
79
- if (options.persistentParams && options.persistentParams.length > 0) {
80
- lines.push(' interface BuzolaPersistentParams {')
81
- for (const param of options.persistentParams) {
82
- lines.push(` ${param}: true;`)
83
- }
84
- lines.push(' }')
85
- }
86
- lines.push('}')
87
- lines.push('')
88
-
89
- // Generate page registry
90
- lines.push('// Page registry — page ID → route pattern')
91
- lines.push('export const pageRegistry: Record<string, string> = {')
92
- for (const page of pages) {
93
- lines.push(` '${page.pageId}': '${page.routePattern}',`)
94
- }
95
- lines.push('};')
96
- lines.push('')
97
-
98
- // Generate route tree config
99
- lines.push('const routeConfigs: RouteConfig[] = [')
100
- generateTreeConfig(tree, lines, importsByKey, 1)
101
- lines.push('];')
102
- lines.push('')
103
- lines.push('export const routes = buildRouteTree(routeConfigs);')
104
- lines.push('')
105
-
106
- return lines.join('\n')
107
- }
108
-
109
- interface PageInfo {
110
- pageId: string
111
- routePattern: string
112
- params: { name: string; optional: boolean; array: boolean }[]
113
- }
114
-
115
- /**
116
- * Collect page info from the file route tree for codegen.
117
- */
118
- function collectPages(nodes: FileRouteNode[]): PageInfo[] {
119
- const result: PageInfo[] = []
120
-
121
- for (const node of nodes) {
122
- if (node.filePath && !node.isLayout && !node.isNotFound) {
123
- if (node.pageExports) {
124
- for (const exp of node.pageExports) {
125
- result.push({
126
- pageId: exp.pageId,
127
- routePattern: exp.routePattern,
128
- params: exp.params,
129
- })
130
- }
131
- }
132
- }
133
-
134
- if (node.children.length > 0) {
135
- result.push(...collectPages(node.children))
136
- }
137
- }
138
-
139
- return result
140
- }
141
-
142
- function importKey(filePath: string, exportName?: string): string {
143
- return `${filePath}::${exportName ?? 'default'}`
144
- }
145
-
146
- function collectImports(
147
- nodes: FileRouteNode[],
148
- outputDir: string,
149
- imports: { varName: string; filePath: string; isLazy: boolean; exportName: string }[],
150
- importsByKey: Map<string, { varName: string; isLazy: boolean }>,
151
- nextId: () => number,
152
- ): void {
153
- for (const node of nodes) {
154
- if (node.filePath) {
155
- const key = importKey(node.filePath, node.exportName)
156
- if (!importsByKey.has(key)) {
157
- const relativePath = path.relative(outputDir, node.filePath).replaceAll('\\', '/')
158
- const importPath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`
159
- // Strip extension for import
160
- const cleanPath = importPath.replace(/\.(tsx?|jsx?)$/, '')
161
- const varName = `Route${nextId()}`
162
- const isLazy = !node.isLayout // Layouts are eagerly loaded, pages are lazy
163
- const exportName = node.exportName ?? 'default'
164
- imports.push({ varName, filePath: cleanPath, isLazy, exportName })
165
- importsByKey.set(key, { varName, isLazy })
166
- }
167
- }
168
-
169
- collectImports(node.children, outputDir, imports, importsByKey, nextId)
170
- }
171
- }
172
-
173
- function generateTreeConfig(
174
- nodes: FileRouteNode[],
175
- lines: string[],
176
- importsByKey: Map<string, { varName: string; isLazy: boolean }>,
177
- indent: number,
178
- ): void {
179
- const pad = ' '.repeat(indent)
180
-
181
- for (const node of nodes) {
182
- const imp = node.filePath ? importsByKey.get(importKey(node.filePath, node.exportName)) : undefined
183
-
184
- lines.push(`${pad}{`)
185
- lines.push(`${pad} path: '${node.segment ? `/${node.segment}` : '/'}',`)
186
-
187
- // Emit matchPath when page has a custom route pattern that differs from file-hierarchy position
188
- if (node.pageExports && node.pageExports.length > 0 && node.pageExports[0].routePattern !== node.fullPath) {
189
- lines.push(`${pad} matchPath: '${node.pageExports[0].routePattern}',`)
190
- }
191
-
192
- if (imp) {
193
- if (imp.isLazy) {
194
- lines.push(`${pad} component: lazy(${imp.varName}),`)
195
- lines.push(`${pad} preload: ${imp.varName},`)
196
- } else {
197
- lines.push(`${pad} component: ${imp.varName},`)
198
- }
199
- }
200
-
201
- if (node.isLayout) lines.push(`${pad} isLayout: true,`)
202
- if (node.isIndex) lines.push(`${pad} isIndex: true,`)
203
- if (node.isPathlessGroup) lines.push(`${pad} isPathlessGroup: true,`)
204
-
205
- if (node.children.length > 0) {
206
- lines.push(`${pad} children: [`)
207
- generateTreeConfig(node.children, lines, importsByKey, indent + 2)
208
- lines.push(`${pad} ],`)
209
- }
210
-
211
- lines.push(`${pad}},`)
212
- }
213
- }
@@ -1,48 +0,0 @@
1
- export interface ExtractedPage {
2
- /** Export name (e.g., "detail", "list"). "default" for default exports. */
3
- exportName: string
4
- /** Explicit route pattern from .route('...'), if present. */
5
- route?: string
6
- /** Whether this is a default export. */
7
- isDefault: boolean
8
- /** Extracted param info from .params(z.object({...})). */
9
- params: ExtractedParam[]
10
- }
11
-
12
- export interface ExtractedParam {
13
- name: string
14
- optional: boolean
15
- array: boolean
16
- }
17
-
18
- export type ModuleLoader = (absolutePath: string) => Promise<Record<string, unknown>>
19
-
20
- function isPageDefinition(value: unknown): value is {
21
- __buzolaPage: true
22
- route?: string
23
- paramsMeta: { name: string; optional: boolean; array: boolean }[]
24
- } {
25
- return value != null && typeof value === 'object' && '__buzolaPage' in value && (value as any).__buzolaPage === true
26
- }
27
-
28
- /**
29
- * Extract createPage() metadata by loading the module and inspecting exports.
30
- */
31
- export async function extractPages(absolutePath: string, loadModule: ModuleLoader): Promise<ExtractedPage[]> {
32
- const mod = await loadModule(absolutePath)
33
- const pages: ExtractedPage[] = []
34
-
35
- for (const [key, value] of Object.entries(mod)) {
36
- if (!isPageDefinition(value)) continue
37
-
38
- const exportName = key === 'default' ? 'default' : key
39
- pages.push({
40
- exportName,
41
- isDefault: key === 'default',
42
- route: value.route,
43
- params: value.paramsMeta ?? [],
44
- })
45
- }
46
-
47
- return pages
48
- }
@@ -1,45 +0,0 @@
1
- import * as fs from 'node:fs'
2
- import * as path from 'node:path'
3
- import { isRouteFile } from '../conventions.js'
4
-
5
- export interface ScannedFile {
6
- /** Absolute path to the file. */
7
- absolutePath: string
8
- /** Path relative to the routes directory. */
9
- relativePath: string
10
- }
11
-
12
- /**
13
- * Scan a directory recursively for route files.
14
- */
15
- export function scanRouteFiles(routesDir: string): ScannedFile[] {
16
- const files: ScannedFile[] = []
17
-
18
- function scan(dir: string): void {
19
- if (!fs.existsSync(dir)) return
20
-
21
- const entries = fs.readdirSync(dir, { withFileTypes: true })
22
-
23
- for (const entry of entries) {
24
- const fullPath = path.join(dir, entry.name)
25
-
26
- if (entry.isDirectory()) {
27
- // Skip node_modules and hidden directories
28
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
29
- scan(fullPath)
30
- } else if (entry.isFile() && isRouteFile(entry.name)) {
31
- files.push({
32
- absolutePath: fullPath,
33
- relativePath: path.relative(routesDir, fullPath),
34
- })
35
- }
36
- }
37
- }
38
-
39
- scan(routesDir)
40
-
41
- // Sort for deterministic output
42
- files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
43
-
44
- return files
45
- }