fusee-framework 1.8.0 → 1.9.1

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 (109) hide show
  1. package/README.md +64 -3
  2. package/app/benchmark/vite.config.js +23 -0
  3. package/auto-imports.d.ts +4 -0
  4. package/bin/create-fusee-linux +0 -0
  5. package/bin/create-fusee-mac-arm64 +0 -0
  6. package/bin/create-fusee-mac-intel +0 -0
  7. package/bin/create-fusee-win.exe +0 -0
  8. package/bin/create-fusee.exe +0 -0
  9. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +15 -9
  10. package/create-fusee-go/internal/assets/embed/framework/core/async-context.js +27 -0
  11. package/create-fusee-go/internal/assets/embed/framework/core/compiler/ast.js +167 -0
  12. package/create-fusee-go/internal/assets/embed/framework/core/compiler/errors-list.js +47 -0
  13. package/create-fusee-go/internal/assets/embed/framework/core/compiler/errors.js +107 -0
  14. package/create-fusee-go/internal/assets/embed/framework/core/compiler/generator.js +363 -0
  15. package/create-fusee-go/internal/assets/embed/framework/core/compiler/lexer.js +347 -0
  16. package/create-fusee-go/internal/assets/embed/framework/core/compiler/main-compiler.js +115 -0
  17. package/create-fusee-go/internal/assets/embed/framework/core/compiler/parser.js +414 -0
  18. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/actions-plugin.js +263 -0
  19. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  20. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  21. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  22. package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/router-plugin.js +196 -0
  23. package/create-fusee-go/internal/assets/embed/framework/core/compiler/transformer.js +398 -0
  24. package/create-fusee-go/internal/assets/embed/framework/core/component.js +61 -23
  25. package/create-fusee-go/internal/assets/embed/framework/core/di.js +187 -0
  26. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +40 -96
  27. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +1 -1
  28. package/create-fusee-go/internal/assets/embed/framework/core/h.js +430 -0
  29. package/create-fusee-go/internal/assets/embed/framework/core/hydration.js +83 -0
  30. package/create-fusee-go/internal/assets/embed/framework/core/reconcile.js +151 -0
  31. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +269 -0
  32. package/create-fusee-go/internal/assets/embed/framework/core/store.js +322 -81
  33. package/create-fusee-go/internal/assets/embed/framework/index.js +5 -3
  34. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +1 -1
  35. package/create-fusee-go/internal/assets/embed/framework/router/router.js +113 -21
  36. package/create-fusee-go/internal/assets/embed/framework/server/actions.js +173 -0
  37. package/create-fusee-go/internal/assets/embed/framework/server/actions.server.js +88 -0
  38. package/create-fusee-go/internal/assets/embed/framework/server/index.js +18 -0
  39. package/create-fusee-go/internal/assets/embed/framework/types/actions.d.ts +50 -0
  40. package/create-fusee-go/internal/assets/embed/framework/types/actions.server.d.ts +36 -0
  41. package/create-fusee-go/internal/assets/embed/framework/types/compiler/ast.d.ts +166 -0
  42. package/create-fusee-go/internal/assets/embed/framework/types/compiler/errors-codes.d.ts +23 -0
  43. package/create-fusee-go/internal/assets/embed/framework/types/compiler/errors.d.ts +43 -0
  44. package/create-fusee-go/internal/assets/embed/framework/types/compiler/generator.d.ts +8 -0
  45. package/create-fusee-go/internal/assets/embed/framework/types/compiler/h.d.ts +46 -0
  46. package/create-fusee-go/internal/assets/embed/framework/types/compiler/lexer.d.ts +27 -0
  47. package/create-fusee-go/internal/assets/embed/framework/types/compiler/main-compiler.d.ts +60 -0
  48. package/create-fusee-go/internal/assets/embed/framework/types/compiler/parser.d.ts +4 -0
  49. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/actions.d.ts +30 -0
  50. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  51. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  52. package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  53. package/create-fusee-go/internal/assets/embed/framework/types/compiler/transformer.d.ts +28 -0
  54. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +89 -88
  55. package/create-fusee-go/internal/assets/embed/framework/types/di.d.ts +43 -0
  56. package/create-fusee-go/internal/assets/embed/framework/types/hydration.d.ts +30 -0
  57. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +38 -22
  58. package/create-fusee-go/internal/assets/embed/framework/types/reconcile.d.ts +7 -0
  59. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +11 -1
  60. package/create-fusee-go/internal/assets/embed/framework/types/server.d.ts +11 -0
  61. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +142 -79
  62. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +66 -8
  63. package/framework/auto-import-preset.js +3 -1
  64. package/framework/core/async-context.js +27 -0
  65. package/framework/core/compiler/ast.js +167 -0
  66. package/framework/core/compiler/errors-list.js +47 -0
  67. package/framework/core/compiler/errors.js +107 -0
  68. package/framework/core/compiler/generator.js +363 -0
  69. package/framework/core/compiler/lexer.js +347 -0
  70. package/framework/core/compiler/main-compiler.js +115 -0
  71. package/framework/core/compiler/parser.js +414 -0
  72. package/framework/core/compiler/plugins/actions-plugin.js +263 -0
  73. package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  74. package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  75. package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  76. package/framework/core/compiler/plugins/router-plugin.js +196 -0
  77. package/framework/core/compiler/transformer.js +398 -0
  78. package/framework/core/component.js +34 -10
  79. package/framework/core/evaluator.js +1 -1
  80. package/framework/core/h.js +430 -0
  81. package/framework/core/hydration.js +83 -0
  82. package/framework/core/signal.js +17 -15
  83. package/framework/server/actions.js +173 -0
  84. package/framework/server/actions.server.js +88 -0
  85. package/framework/server/index.js +18 -0
  86. package/framework/types/actions.d.ts +50 -0
  87. package/framework/types/actions.server.d.ts +36 -0
  88. package/framework/types/compiler/ast.d.ts +166 -0
  89. package/framework/types/compiler/errors-codes.d.ts +23 -0
  90. package/framework/types/compiler/errors.d.ts +43 -0
  91. package/framework/types/compiler/generator.d.ts +8 -0
  92. package/framework/types/compiler/h.d.ts +46 -0
  93. package/framework/types/compiler/lexer.d.ts +27 -0
  94. package/framework/types/compiler/main-compiler.d.ts +60 -0
  95. package/framework/types/compiler/parser.d.ts +4 -0
  96. package/framework/types/compiler/plugins/actions.d.ts +30 -0
  97. package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  98. package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  99. package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  100. package/framework/types/compiler/transformer.d.ts +28 -0
  101. package/framework/types/hydration.d.ts +30 -0
  102. package/framework/types/index.d.ts +6 -0
  103. package/framework/types/server.d.ts +11 -0
  104. package/framework/types/signal.d.ts +2 -7
  105. package/ghid_go_fusee_integrat_go1_26.txt +276 -0
  106. package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
  107. package/package.json +44 -36
  108. package/vite.config.js +65 -53
  109. package/create-fusee-go/internal/assets/embed/framework/bin/create-fusee.js +0 -199
@@ -0,0 +1,263 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const ACTION_BASE_URL = '/__fusee/actions'
5
+ const VIRTUAL_CLIENT_ID = 'virtual:fusee-actions'
6
+ const VIRTUAL_SERVER_ID = 'virtual:fusee-action-routes'
7
+
8
+ export function actionsPlugin(options = {}) {
9
+ const {
10
+ serverActionsFile = 'src/actions.server.js',
11
+ clientActionsFile = 'src/actions.js',
12
+ baseUrl = ACTION_BASE_URL,
13
+ typesOutput = 'src/types/actions.d.ts',
14
+ } = options
15
+
16
+ let resolvedServerFile = null
17
+ let resolvedClientFile = null
18
+
19
+ return {
20
+ name: 'vite-plugin-fusee-actions',
21
+
22
+ configResolved(config) {
23
+ resolvedServerFile = path.resolve(config.root, serverActionsFile)
24
+ resolvedClientFile = path.resolve(config.root, clientActionsFile)
25
+ },
26
+
27
+ resolveId(id) {
28
+ if (id === VIRTUAL_CLIENT_ID)
29
+ return '\0' + VIRTUAL_CLIENT_ID
30
+ if (id === VIRTUAL_SERVER_ID)
31
+ return '\0' + VIRTUAL_SERVER_ID
32
+ },
33
+
34
+ async load(id) {
35
+ if (id === '\0' + VIRTUAL_CLIENT_ID) {
36
+ const serverActions = _extractServerActions(resolvedServerFile)
37
+ const clientActions = resolvedClientFile ? _extractClientActions(resolvedClientFile) : []
38
+
39
+ const { warnings, errors } = validateActions(serverActions, clientActions)
40
+ for (const w of warnings)
41
+ this.warn(w)
42
+ for (const e of errors)
43
+ this.error(e)
44
+
45
+ return generateClientStubs(serverActions, { baseUrl })
46
+ }
47
+
48
+ if (id === '\0' + VIRTUAL_SERVER_ID) {
49
+ const serverActions = _extractServerActions(resolvedServerFile)
50
+ return generateServerRoutes(serverActions, { serverActionsFile, baseUrl })
51
+ }
52
+ },
53
+
54
+ async buildEnd() {
55
+ if (typesOutput && resolvedServerFile) {
56
+ const serverActions = _extractServerActions(resolvedServerFile)
57
+ const types = generateActionTypes(serverActions)
58
+ const absOutput = path.resolve(process.cwd(), typesOutput)
59
+ fs.mkdirSync(path.dirname(absOutput), { recursive: true })
60
+ fs.writeFileSync(absOutput, types, 'utf8')
61
+ }
62
+ },
63
+
64
+ handleHotUpdate({ file, server }) {
65
+ if (file === resolvedServerFile || file === resolvedClientFile) {
66
+ const mod = server.moduleGraph.getModuleById('\0' + VIRTUAL_CLIENT_ID)
67
+ if (mod)
68
+ server.moduleGraph.invalidateModule(mod)
69
+ server.ws.send({ type: 'full-reload' })
70
+ }
71
+ },
72
+ }
73
+ }
74
+
75
+ export function _extractServerActions(filePath) {
76
+ if (!filePath || !fs.existsSync(filePath))
77
+ return []
78
+
79
+ const source = fs.readFileSync(filePath, 'utf8')
80
+ const actions = []
81
+
82
+ const RE_EXPORT = /export\s+(?:const|let|var)\s+(\w+)\s*=\s*defineAction\s*\(/g
83
+ const RE_NAMED = /defineAction\s*\(\s*(?:async\s+)?function\s+(\w+)/g
84
+ const RE_OPTS = /defineAction\s*\([^,)]+,\s*\{[^}]*name\s*:\s*['"](\w+)['"]/g
85
+
86
+ const seen = new Set()
87
+
88
+ let m
89
+ while ((m = RE_EXPORT.exec(source)) !== null) {
90
+ const name = m[1]
91
+ if (!seen.has(name)) { seen.add(name); actions.push({ name, exportName: name }) }
92
+ }
93
+
94
+ while ((m = RE_NAMED.exec(source)) !== null) {
95
+ const name = m[1]
96
+ if (!seen.has(name)) { seen.add(name); actions.push({ name, exportName: name }) }
97
+ }
98
+
99
+ while ((m = RE_OPTS.exec(source)) !== null) {
100
+ const name = m[1]
101
+ if (!seen.has(name)) { seen.add(name); actions.push({ name, exportName: name }) }
102
+ }
103
+
104
+ return actions
105
+ }
106
+
107
+ export function _extractClientActions(filePath) {
108
+ if (!filePath || !fs.existsSync(filePath))
109
+ return []
110
+
111
+ const source = fs.readFileSync(filePath, 'utf8')
112
+ const names = []
113
+ const RE = /defineAction\s*\(\s*['"](\w+)['"]/g
114
+ let m
115
+
116
+ while ((m = RE.exec(source)) !== null) {
117
+ names.push(m[1])
118
+ }
119
+
120
+ return names
121
+ }
122
+
123
+ export function validateActions(serverActions, clientActions) {
124
+ const warnings = []
125
+ const errors = []
126
+
127
+ const serverNames = new Set(serverActions.map(a => a.name))
128
+ const clientNames = new Set(clientActions)
129
+
130
+ for (const name of clientNames) {
131
+ if (!serverNames.has(name)) {
132
+ errors.push(`[fusée] Action "${name}" is defined on the client but has no corresponding defineAction() in actions.server.js`)
133
+ }
134
+ }
135
+
136
+ for (const name of serverNames) {
137
+ if (!clientNames.has(name) && clientActions.length > 0) {
138
+ warnings.push(`[fusée] Action "${name}" is defined on the server but is not used in actions.js — it will still be accessible via HTTP POST /${name}`
139
+ )
140
+ }
141
+ }
142
+
143
+ return { warnings, errors }
144
+ }
145
+
146
+ export function generateClientStubs(actions, options = {}) {
147
+ const { baseUrl = ACTION_BASE_URL } = options
148
+
149
+ const lines = [
150
+ '// [fusée] Auto-generated action stubs — do not edit',
151
+ '// Generated by compiler/plugins/actions-plugin.js',
152
+ '',
153
+ `import { createActionProxy, useAction } from '../core/actions.js'`,
154
+ '',
155
+ ]
156
+
157
+ for (const action of actions) {
158
+ lines.push(`export const ${action.exportName} = createActionProxy(` + `${JSON.stringify(action.name)}, { baseUrl: ${JSON.stringify(baseUrl)} })`)
159
+ }
160
+
161
+ lines.push('')
162
+ lines.push('export { useAction }')
163
+ lines.push('')
164
+
165
+ return lines.join('\n')
166
+ }
167
+
168
+ export function generateServerRoutes(actions, options = {}) {
169
+ const {
170
+ serverActionsFile = 'src/actions.server.js',
171
+ baseUrl = ACTION_BASE_URL,
172
+ } = options
173
+
174
+ const relPath = './' + serverActionsFile.replace(/^src\//, '')
175
+
176
+ const lines = [
177
+ '// [fusée] Auto-generated action routes — do not edit',
178
+ '// Generated by compiler/plugins/actions-plugin.js',
179
+ '',
180
+ `import { handleActionRequest, getRegisteredActions } from ${JSON.stringify(relPath)}`,
181
+ '',
182
+ '// Import side-effect: registers all actions in the registry',
183
+ `import ${JSON.stringify(relPath)}`,
184
+ '',
185
+ '/** Map of action name → request handler */',
186
+ 'export const actionHandlers = {',
187
+ ]
188
+
189
+ for (const action of actions) {
190
+ lines.push(` ${JSON.stringify(action.name)}: (req, res) => handleActionRequest(req, res),`)
191
+ }
192
+
193
+ lines.push('}')
194
+ lines.push('')
195
+ lines.push('/**')
196
+ lines.push(' * Mount all action routes on an Express app.')
197
+ lines.push(' * @param {import("express").Application} app')
198
+ lines.push(' */')
199
+ lines.push('export function mountActionRoutes(app) {')
200
+ lines.push(` const base = ${JSON.stringify(baseUrl)}`)
201
+ lines.push(' for (const [name, handler] of Object.entries(actionHandlers)) {')
202
+ lines.push(' app.post(`${base}/${name}`, handler)')
203
+ lines.push(' }')
204
+ lines.push('}')
205
+ lines.push('')
206
+ lines.push('/**')
207
+ lines.push(' * Fetch API compatible dispatcher (Bun / Deno / CF Workers).')
208
+ lines.push(' * @param {Request} request')
209
+ lines.push(' */')
210
+ lines.push('export async function dispatchAction(request) {')
211
+ lines.push(` const base = ${JSON.stringify(baseUrl)}`)
212
+ lines.push(' const url = new URL(request.url)')
213
+ lines.push(' const name = url.pathname.slice(base.length + 1)')
214
+ lines.push(' const handler = actionHandlers[name]')
215
+ lines.push(' if (!handler) return new Response(JSON.stringify({ error: `Action "${name}" not found` }), { status: 404 })')
216
+ lines.push(' return handler(request, null)')
217
+ lines.push('}')
218
+ lines.push('')
219
+
220
+ return lines.join('\n')
221
+ }
222
+
223
+ export function generateActionTypes(actions) {
224
+ const lines = [
225
+ '// [fusée] Auto-generated action types — do not edit',
226
+ '// Generated by compiler/plugins/actions-plugin.js',
227
+ '',
228
+ "import type { useAction } from '../core/actions.js'",
229
+ '',
230
+ '/** All registered server action names */',
231
+ `export type ActionName = ${actions.map(a => JSON.stringify(a.name)).join(' | ') || 'never'}`,
232
+ '',
233
+ '/** Client proxy function for a server action */',
234
+ 'export type ActionProxy<TArgs extends unknown[] = unknown[], TReturn = unknown> =',
235
+ ' ((...args: TArgs) => Promise<TReturn>) & {',
236
+ ' _isAction: true',
237
+ ' _actionName: string',
238
+ ' }',
239
+ '',
240
+ '/** useAction() result shape */',
241
+ 'export type UseActionResult<TReturn = unknown> = {',
242
+ ' execute: (...args: unknown[]) => Promise<TReturn | undefined>',
243
+ ' pending: () => boolean',
244
+ ' data: () => TReturn | undefined',
245
+ ' error: () => Error | undefined',
246
+ ' reset: () => void',
247
+ '}',
248
+ '',
249
+ '/** Auto-generated action stubs */',
250
+ 'export declare const actionStubs: Record<ActionName, ActionProxy>',
251
+ '',
252
+ ]
253
+
254
+ for (const action of actions) {
255
+ lines.push(`export declare const ${action.exportName}: ActionProxy`)
256
+ }
257
+
258
+ lines.push('')
259
+ lines.push("export { useAction } from '../core/actions.js'")
260
+ lines.push('')
261
+
262
+ return lines.join('\n')
263
+ }
@@ -0,0 +1,5 @@
1
+ export declare function fuseeCompilerPlugin(): {
2
+ name: 'vite-plugin-fusee-compiler'
3
+ enforce: 'pre'
4
+ transform(code: string, id: string): { code: string; map: null } | null
5
+ }
@@ -0,0 +1,45 @@
1
+ import { compile } from '../main-compiler.js';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const IMPORT_RE = /^import\s+(?:\{[^}]*\}|[\w$]+)\s+from\s+['"]([^'"]+\.template\.html)['"]\s*;?/gm;
6
+
7
+ export function fuseeCompilerPlugin() {
8
+ return {
9
+ name: 'vite-plugin-fusee-compiler',
10
+ enforce: 'pre',
11
+
12
+ transform(code, id) {
13
+ if (!id.endsWith('.js') && !id.endsWith('.ts'))
14
+ return null;
15
+ if (!IMPORT_RE.test(code))
16
+ return null;
17
+
18
+ IMPORT_RE.lastIndex = 0;
19
+
20
+ let result = code;
21
+ let match;
22
+
23
+ while ((match = IMPORT_RE.exec(code)) !== null) {
24
+ const [fullImport, templatePath] = match;
25
+ const absPath = path.resolve(path.dirname(id), templatePath);
26
+ const source = fs.readFileSync(absPath, 'utf-8');
27
+ const { code: compiledCode } = compile(source, {
28
+ runtimePath: 'fusee-framework/core/h.js'
29
+ });
30
+
31
+ const namedMatch = fullImport.match(/\{\s*render\s+as\s+([\w$]+)\s*\}/);
32
+ const defaultMatch = fullImport.match(/import\s+([\w$]+)\s+from/);
33
+ const localName = namedMatch ? namedMatch[1] : (defaultMatch ? defaultMatch[1] : 'render');
34
+ const inlined = compiledCode.replace(
35
+ 'export function render(',
36
+ `function ${localName}(`
37
+ );
38
+
39
+ result = result.replace(fullImport, inlined);
40
+ }
41
+
42
+ return { code: result, map: null };
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,301 @@
1
+ import path from 'node:path'
2
+
3
+ export function fileRouterPlugin(options = {}) {
4
+ const {
5
+ pagesDir = 'src/pages',
6
+ loadingComponent = null,
7
+ } = options
8
+
9
+ return {
10
+ name: 'vite-plugin-fusee-file-router',
11
+ async buildStart() { },
12
+
13
+ resolveId(id) {
14
+ if (id === 'virtual:fusee-routes') return '\0virtual:fusee-routes'
15
+ },
16
+
17
+ async load(id) {
18
+ if (id !== '\0virtual:fusee-routes') return null
19
+
20
+ const { globSync } = await import('fast-glob')
21
+ const files = globSync(`${pagesDir}/**/*.{js,ts,jsx,tsx}`, {
22
+ cwd: process.cwd(),
23
+ absolute: false,
24
+ })
25
+
26
+ const routes = compileFileRoutes(files, { pagesDir, loadingComponent })
27
+ return generateRoutesModule(routes, pagesDir)
28
+ },
29
+ }
30
+ }
31
+
32
+ export function compileFileRoutes(files, options = {}) {
33
+ const { pagesDir = 'src/pages' } = options
34
+
35
+ const entries = _parseFileEntries(files, pagesDir)
36
+ const tree = _buildRouteTree(entries, false)
37
+ _sortRoutesRecursive(tree)
38
+
39
+ return tree
40
+ }
41
+
42
+ export function generateRoutesModule(routes, pagesDir = 'src/pages') {
43
+ const imports = []
44
+ const lines = []
45
+
46
+ _collectImports(routes, pagesDir, imports)
47
+
48
+ lines.push('// [fusée] Auto-generated at build-time — do not edit')
49
+ lines.push('// Generated by compiler/plugins/file-router-plugin.js')
50
+ lines.push('')
51
+
52
+ for (const { varName, filePath } of imports) {
53
+ lines.push(`const ${varName} = () => import(${JSON.stringify(filePath)})`)
54
+ }
55
+
56
+ lines.push('')
57
+ lines.push(`export const routes = ${_serializeRoutes(routes)}`)
58
+ lines.push('')
59
+ lines.push('export default routes')
60
+
61
+ return lines.join('\n')
62
+ }
63
+
64
+ export function validateRoutes(routes) {
65
+ const warnings = []
66
+ const errors = []
67
+
68
+ _validateLevel(routes, '', warnings, errors)
69
+
70
+ return { warnings, errors }
71
+ }
72
+
73
+ function _validateLevel(routes, prefix, warnings, errors) {
74
+ const seenPaths = new Map()
75
+ const paramNames = new Set()
76
+
77
+ for (const route of routes) {
78
+ const fullPath = prefix + route.path
79
+ if (seenPaths.has(route.path)) {
80
+ errors.push(`Duplicate route path "${fullPath}" — defined in multiple page files`)
81
+ } else {
82
+ seenPaths.set(route.path, route)
83
+ }
84
+ const paramMatch = route.path.match(/^:([^/]+)/)
85
+ if (paramMatch) {
86
+ const paramName = paramMatch[1]
87
+ if (paramNames.size > 0 && !paramNames.has(paramName)) {
88
+ warnings.push(`Conflicting dynamic segments at "${prefix}*": found ":${[...paramNames][0]}" and ":${paramName}" — only one dynamic segment per level is allowed`)
89
+ }
90
+ paramNames.add(paramName)
91
+ }
92
+ if (route.children !== undefined && route.children.length === 0) {
93
+ warnings.push(`Layout route "${fullPath}" has no children — it will never render any content`)
94
+ }
95
+
96
+ if (route.children) {
97
+ _validateLevel(route.children, fullPath + '/', warnings, errors)
98
+ }
99
+ }
100
+ }
101
+
102
+ export function generateRouteTypes(routes) {
103
+ const paths = []
104
+ const paramTypes = []
105
+
106
+ _collectRoutePaths(routes, '', paths, paramTypes)
107
+
108
+ const pathUnion = paths.map(p => JSON.stringify(p)).join('\n | ')
109
+ const paramEntries = paramTypes
110
+ .filter(e => Object.keys(e.params).length > 0)
111
+ .map(e => {
112
+ const paramFields = Object.entries(e.params)
113
+ .map(([k, v]) => `${k}: ${v}`)
114
+ .join('; ')
115
+ return ` ${JSON.stringify(e.path)}: { ${paramFields} }`
116
+ })
117
+ .join('\n')
118
+
119
+ return [
120
+ '// [fusée] Auto-generated route types — do not edit',
121
+ '// Generated by compiler/plugins/file-router-plugin.js',
122
+ '',
123
+ '/** All valid application route paths */',
124
+ `export type AppRoute =`,
125
+ ` | ${pathUnion}`,
126
+ '',
127
+ '/** Route params per path (only paths with dynamic segments) */',
128
+ 'export type RouteParams = {',
129
+ paramEntries,
130
+ '}',
131
+ '',
132
+ '/** Extract params for a specific route */',
133
+ 'export type ParamsFor<T extends AppRoute> =',
134
+ ' T extends keyof RouteParams ? RouteParams[T] : Record<string, never>',
135
+ '',
136
+ ].join('\n')
137
+ }
138
+
139
+ function _parseFileEntries(files, pagesDir) {
140
+ return files.map(filePath => {
141
+ let cleanPath = filePath
142
+ .replace(new RegExp(`^${pagesDir}/?`), '')
143
+ .replace(/\.[jt]sx?$/, '')
144
+
145
+ const parts = cleanPath.split('/')
146
+ const rawFileName = parts.pop()
147
+ const rawDirSegs = parts
148
+ const dirSegments = rawDirSegs.map(_transformSegment)
149
+ const fileName = _transformSegment(rawFileName)
150
+ const isIndex = rawFileName === 'index'
151
+ const isCatchAll = /^\.\.\./.test(rawFileName)
152
+
153
+ return { filePath, dirSegments, fileName, isIndex, isCatchAll }
154
+ })
155
+ }
156
+
157
+ function _transformSegment(seg) {
158
+ return seg
159
+ .replace(/\[\.\.\.([^\]]+)\]/g, '*')
160
+ .replace(/\[([^\]]+)\]/g, ':$1')
161
+ }
162
+
163
+ function _buildRouteTree(entries, isChild) {
164
+ const filesHere = new Map()
165
+ const grouped = new Map()
166
+
167
+ for (const entry of entries) {
168
+ if (entry.dirSegments.length === 0) {
169
+ filesHere.set(entry.fileName, entry)
170
+ } else {
171
+ const firstDir = entry.dirSegments[0]
172
+ if (!grouped.has(firstDir)) grouped.set(firstDir, [])
173
+ grouped.get(firstDir).push(entry)
174
+ }
175
+ }
176
+
177
+ let rootLayoutEntry = null
178
+ if (filesHere.has('_layout')) {
179
+ rootLayoutEntry = filesHere.get('_layout')
180
+ filesHere.delete('_layout')
181
+ }
182
+
183
+ const routes = []
184
+ const usedAsLayout = new Set()
185
+
186
+ for (const [dirName, dirEntries] of grouped) {
187
+ const layoutEntry = filesHere.get(dirName)
188
+
189
+ if (layoutEntry) {
190
+ usedAsLayout.add(dirName)
191
+ const childEntries = dirEntries.map(e => ({
192
+ ...e,
193
+ dirSegments: e.dirSegments.slice(1)
194
+ }))
195
+ const children = _buildRouteTree(childEntries, true)
196
+ routes.push({
197
+ path: isChild ? dirName : '/' + dirName,
198
+ filePath: layoutEntry.filePath,
199
+ children,
200
+ })
201
+ } else {
202
+ for (const entry of dirEntries) {
203
+ const segments = [...entry.dirSegments]
204
+ if (!entry.isIndex) segments.push(entry.fileName)
205
+ routes.push({
206
+ path: isChild ? segments.join('/') : '/' + segments.join('/'),
207
+ filePath: entry.filePath,
208
+ })
209
+ }
210
+ }
211
+ }
212
+
213
+ for (const [name, entry] of filesHere) {
214
+ if (usedAsLayout.has(name)) continue
215
+ if (entry.isIndex) {
216
+ routes.push({ path: isChild ? '' : '/', filePath: entry.filePath })
217
+ } else if (entry.isCatchAll || name === '*') {
218
+ routes.push({ path: '*', filePath: entry.filePath })
219
+ } else {
220
+ routes.push({ path: isChild ? name : '/' + name, filePath: entry.filePath })
221
+ }
222
+ }
223
+
224
+ if (rootLayoutEntry) {
225
+ return [{ path: '', filePath: rootLayoutEntry.filePath, children: routes }]
226
+ }
227
+
228
+ return routes
229
+ }
230
+
231
+ function _sortRoutesRecursive(routes) {
232
+ routes.sort(_compareRoutes)
233
+ for (const route of routes) {
234
+ if (route.children) _sortRoutesRecursive(route.children)
235
+ }
236
+ }
237
+
238
+ function _compareRoutes(a, b) {
239
+ if (a.path === '*' && b.path !== '*') return 1
240
+ if (a.path !== '*' && b.path === '*') return -1
241
+ const aWild = a.path.endsWith('/*')
242
+ const bWild = b.path.endsWith('/*')
243
+ if (aWild && !bWild) return 1
244
+ if (!aWild && bWild) return -1
245
+ const aDyn = a.path.includes(':')
246
+ const bDyn = b.path.includes(':')
247
+ if (aDyn && !bDyn) return 1
248
+ if (!aDyn && bDyn) return -1
249
+ if (a.children && !b.children) return -1
250
+ if (!a.children && b.children) return 1
251
+ return a.path.localeCompare(b.path)
252
+ }
253
+
254
+ let _importCounter = 0
255
+
256
+ function _collectImports(routes, pagesDir, imports) {
257
+ for (const route of routes) {
258
+ if (route.filePath) {
259
+ const varName = `_page${_importCounter++}`
260
+ route._varName = varName
261
+ imports.push({ varName, filePath: './' + route.filePath })
262
+ }
263
+ if (route.children)
264
+ _collectImports(route.children, pagesDir, imports)
265
+ }
266
+ }
267
+
268
+ function _serializeRoutes(routes) {
269
+ const items = routes.map(route => {
270
+ const parts = [`path: ${JSON.stringify(route.path)}`]
271
+ if (route._varName) {
272
+ parts.push(`component: ${route._varName}`)
273
+ }
274
+ if (route.children) {
275
+ parts.push(`children: ${_serializeRoutes(route.children)}`)
276
+ }
277
+ return `{ ${parts.join(', ')} }`
278
+ })
279
+ return `[\n ${items.join(',\n ')}\n]`
280
+ }
281
+
282
+ function _collectRoutePaths(routes, prefix, paths, paramTypes) {
283
+ for (const route of routes) {
284
+ const fullPath = prefix + route.path
285
+ if (fullPath) {
286
+ paths.push(fullPath)
287
+
288
+ const params = {}
289
+ const paramRe = /:([^/]+)/g
290
+ let m
291
+ while ((m = paramRe.exec(fullPath)) !== null) {
292
+ params[m[1]] = 'string'
293
+ }
294
+ paramTypes.push({ path: fullPath, params })
295
+ }
296
+
297
+ if (route.children) {
298
+ _collectRoutePaths(route.children, fullPath + '/', paths, paramTypes)
299
+ }
300
+ }
301
+ }