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.
- package/README.md +64 -3
- package/app/benchmark/vite.config.js +23 -0
- package/auto-imports.d.ts +4 -0
- package/bin/create-fusee-linux +0 -0
- package/bin/create-fusee-mac-arm64 +0 -0
- package/bin/create-fusee-mac-intel +0 -0
- package/bin/create-fusee-win.exe +0 -0
- package/bin/create-fusee.exe +0 -0
- package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +15 -9
- package/create-fusee-go/internal/assets/embed/framework/core/async-context.js +27 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/ast.js +167 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/errors-list.js +47 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/errors.js +107 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/generator.js +363 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/lexer.js +347 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/main-compiler.js +115 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/parser.js +414 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/actions-plugin.js +263 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.js +45 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/file-router-plugin.js +301 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/router-plugin.js +196 -0
- package/create-fusee-go/internal/assets/embed/framework/core/compiler/transformer.js +398 -0
- package/create-fusee-go/internal/assets/embed/framework/core/component.js +61 -23
- package/create-fusee-go/internal/assets/embed/framework/core/di.js +187 -0
- package/create-fusee-go/internal/assets/embed/framework/core/directives.js +40 -96
- package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +1 -1
- package/create-fusee-go/internal/assets/embed/framework/core/h.js +430 -0
- package/create-fusee-go/internal/assets/embed/framework/core/hydration.js +83 -0
- package/create-fusee-go/internal/assets/embed/framework/core/reconcile.js +151 -0
- package/create-fusee-go/internal/assets/embed/framework/core/signal.js +269 -0
- package/create-fusee-go/internal/assets/embed/framework/core/store.js +322 -81
- package/create-fusee-go/internal/assets/embed/framework/index.js +5 -3
- package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +1 -1
- package/create-fusee-go/internal/assets/embed/framework/router/router.js +113 -21
- package/create-fusee-go/internal/assets/embed/framework/server/actions.js +173 -0
- package/create-fusee-go/internal/assets/embed/framework/server/actions.server.js +88 -0
- package/create-fusee-go/internal/assets/embed/framework/server/index.js +18 -0
- package/create-fusee-go/internal/assets/embed/framework/types/actions.d.ts +50 -0
- package/create-fusee-go/internal/assets/embed/framework/types/actions.server.d.ts +36 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/ast.d.ts +166 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/errors-codes.d.ts +23 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/errors.d.ts +43 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/generator.d.ts +8 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/h.d.ts +46 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/lexer.d.ts +27 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/main-compiler.d.ts +60 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/parser.d.ts +4 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/actions.d.ts +30 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
- package/create-fusee-go/internal/assets/embed/framework/types/compiler/transformer.d.ts +28 -0
- package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +89 -88
- package/create-fusee-go/internal/assets/embed/framework/types/di.d.ts +43 -0
- package/create-fusee-go/internal/assets/embed/framework/types/hydration.d.ts +30 -0
- package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +38 -22
- package/create-fusee-go/internal/assets/embed/framework/types/reconcile.d.ts +7 -0
- package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +11 -1
- package/create-fusee-go/internal/assets/embed/framework/types/server.d.ts +11 -0
- package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +142 -79
- package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +66 -8
- package/framework/auto-import-preset.js +3 -1
- package/framework/core/async-context.js +27 -0
- package/framework/core/compiler/ast.js +167 -0
- package/framework/core/compiler/errors-list.js +47 -0
- package/framework/core/compiler/errors.js +107 -0
- package/framework/core/compiler/generator.js +363 -0
- package/framework/core/compiler/lexer.js +347 -0
- package/framework/core/compiler/main-compiler.js +115 -0
- package/framework/core/compiler/parser.js +414 -0
- package/framework/core/compiler/plugins/actions-plugin.js +263 -0
- package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
- package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
- package/framework/core/compiler/plugins/router-plugin.js +196 -0
- package/framework/core/compiler/transformer.js +398 -0
- package/framework/core/component.js +34 -10
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/signal.js +17 -15
- package/framework/server/actions.js +173 -0
- package/framework/server/actions.server.js +88 -0
- package/framework/server/index.js +18 -0
- package/framework/types/actions.d.ts +50 -0
- package/framework/types/actions.server.d.ts +36 -0
- package/framework/types/compiler/ast.d.ts +166 -0
- package/framework/types/compiler/errors-codes.d.ts +23 -0
- package/framework/types/compiler/errors.d.ts +43 -0
- package/framework/types/compiler/generator.d.ts +8 -0
- package/framework/types/compiler/h.d.ts +46 -0
- package/framework/types/compiler/lexer.d.ts +27 -0
- package/framework/types/compiler/main-compiler.d.ts +60 -0
- package/framework/types/compiler/parser.d.ts +4 -0
- package/framework/types/compiler/plugins/actions.d.ts +30 -0
- package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
- package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
- package/framework/types/compiler/transformer.d.ts +28 -0
- package/framework/types/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +6 -0
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +2 -7
- package/ghid_go_fusee_integrat_go1_26.txt +276 -0
- package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
- package/package.json +44 -36
- package/vite.config.js +65 -53
- package/create-fusee-go/internal/assets/embed/framework/bin/create-fusee.js +0 -199
package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/actions-plugin.js
ADDED
|
@@ -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
|
+
}
|
package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/compiler-plugin.js
ADDED
|
@@ -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
|
+
}
|
package/create-fusee-go/internal/assets/embed/framework/core/compiler/plugins/file-router-plugin.js
ADDED
|
@@ -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
|
+
}
|