fusee-framework 1.7.0 → 1.9.0
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 +206 -139
- package/app/benchmark/benchmark.js +2 -2
- package/app/benchmark/vite.config.js +23 -0
- package/app/main.js +24 -3
- package/app/main.ts +7 -1
- package/app/pages/_layout.js +27 -0
- package/app/pages/async-work.js +65 -0
- package/app/pages/index.js +25 -5
- package/app/pages/index.ts +9 -0
- package/app/pages/router-example.js +24 -1
- package/app/pages/scroll-demo.ts +102 -0
- package/app/providers/ChildComponent.js +8 -2
- package/app/providers/ChildComponent.ts +47 -47
- package/app/providers/GrandchildComponent.js +19 -0
- package/app/providers/ParentComponent.js +5 -1
- package/app/routes/RouterExample.js +23 -1
- package/app/routes/TestResource.js +47 -0
- package/app/services.js +15 -0
- package/auto-imports.d.ts +12 -0
- package/framework/auto-import-preset.js +15 -9
- 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 +61 -23
- package/framework/core/di.js +187 -0
- package/framework/core/directives.js +40 -96
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/reconcile.js +151 -0
- package/framework/core/signal.js +269 -0
- package/framework/core/store.js +322 -81
- package/framework/index.js +5 -3
- package/framework/router/file-router.js +1 -1
- package/framework/router/router.js +113 -21
- 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/component.d.ts +89 -88
- package/framework/types/di.d.ts +43 -0
- package/framework/types/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +38 -22
- package/framework/types/reconcile.d.ts +7 -0
- package/framework/types/router.d.ts +11 -1
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +142 -79
- package/framework/types/store.d.ts +66 -8
- 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 -47
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function routerPlugin(options = {}) {
|
|
5
|
+
const {
|
|
6
|
+
typesOutput = 'src/types/router.d.ts',
|
|
7
|
+
pagesDir = 'src/pages',
|
|
8
|
+
strict = false,
|
|
9
|
+
} = options
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: 'vite-plugin-fusee-router',
|
|
13
|
+
|
|
14
|
+
async generateBundle() {
|
|
15
|
+
let routes = []
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { fileRouterPlugin, compileFileRoutes } = await import('./file-router-plugin.js')
|
|
19
|
+
const { globSync } = await import('fast-glob')
|
|
20
|
+
const files = globSync(`${pagesDir}/**/*.{js,ts,jsx,tsx}`, {
|
|
21
|
+
cwd: process.cwd()
|
|
22
|
+
})
|
|
23
|
+
routes = compileFileRoutes(files, { pagesDir })
|
|
24
|
+
} catch {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { warnings, errors } = validateRoutes(routes)
|
|
29
|
+
for (const w of warnings) this.warn(`[fusee router] ${w}`)
|
|
30
|
+
for (const e of errors) {
|
|
31
|
+
if (strict) this.error(`[fusee router] ${e}`)
|
|
32
|
+
else this.warn(`[fusee router] ${e}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typesOutput) {
|
|
36
|
+
const types = generateRouterTypes(routes)
|
|
37
|
+
const absPath = path.resolve(process.cwd(), typesOutput)
|
|
38
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true })
|
|
39
|
+
fs.writeFileSync(absPath, types, 'utf8')
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateRoutes(routes) {
|
|
46
|
+
const warnings = []
|
|
47
|
+
const errors = []
|
|
48
|
+
_validateLevel(routes, '', warnings, errors)
|
|
49
|
+
return { warnings, errors }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _validateLevel(routes, prefix, warnings, errors) {
|
|
53
|
+
const seenStatic = new Map()
|
|
54
|
+
let dynamicSeg = null
|
|
55
|
+
let hasWildcard = false
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < routes.length; i++) {
|
|
58
|
+
const route = routes[i]
|
|
59
|
+
const fullPath = (prefix + '/' + route.path).replace(/\/+/g, '/') || '/'
|
|
60
|
+
|
|
61
|
+
if (hasWildcard && route.path !== '*') {
|
|
62
|
+
errors.push(`Route "${fullPath}" is defined after a wildcard "*" at the same level — it will never be reached`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (route.path === '*') {
|
|
66
|
+
if (hasWildcard) {
|
|
67
|
+
errors.push(`Duplicate wildcard "*" route at level "${prefix || '/'}"`)
|
|
68
|
+
}
|
|
69
|
+
hasWildcard = true
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const segment = route.path.split('/').filter(Boolean)[0] ?? ''
|
|
74
|
+
|
|
75
|
+
if (!segment.startsWith(':') && !segment.startsWith('*')) {
|
|
76
|
+
if (seenStatic.has(route.path)) {
|
|
77
|
+
errors.push(`Duplicate route path "${fullPath}" — defined more than once at the same level`)
|
|
78
|
+
} else {
|
|
79
|
+
seenStatic.set(route.path, route)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (segment.startsWith(':')) {
|
|
84
|
+
if (dynamicSeg && dynamicSeg !== segment) {
|
|
85
|
+
warnings.push(`Conflicting dynamic segments at level "${prefix || '/'}": ` + `"${dynamicSeg}" and "${segment}" — only one dynamic segment per level is predictable. ` + `Consider renaming them consistently.`)
|
|
86
|
+
}
|
|
87
|
+
dynamicSeg = segment
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (route.children !== undefined && route.children.length === 0) {
|
|
91
|
+
warnings.push(`Layout route "${fullPath}" has no child routes — ` + `<router-view> inside it will never render`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (route.path === '' || route.path === '/') {
|
|
95
|
+
const conflict = routes.find(
|
|
96
|
+
r => r !== route && (r.path === '' || r.path === '/')
|
|
97
|
+
)
|
|
98
|
+
if (conflict) {
|
|
99
|
+
errors.push(`Duplicate index route at level "${prefix || '/'}" — ` + `both "" and "/" resolve to the same path`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (route.children?.length) {
|
|
104
|
+
_validateLevel(route.children, fullPath, warnings, errors)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function generateRouterTypes(routes) {
|
|
110
|
+
const allPaths = []
|
|
111
|
+
const paramMap = {}
|
|
112
|
+
|
|
113
|
+
_collectPaths(routes, '', allPaths, paramMap)
|
|
114
|
+
|
|
115
|
+
const staticPaths = allPaths.filter(p => !p.includes(':') && p !== '*')
|
|
116
|
+
const dynamicPaths = allPaths.filter(p => p.includes(':'))
|
|
117
|
+
|
|
118
|
+
const pathUnion = [...staticPaths, ...dynamicPaths]
|
|
119
|
+
.map(p => JSON.stringify(p))
|
|
120
|
+
.join('\n | ')
|
|
121
|
+
|
|
122
|
+
const paramEntries = Object.entries(paramMap)
|
|
123
|
+
.filter(([, params]) => Object.keys(params).length > 0)
|
|
124
|
+
.map(([p, params]) => {
|
|
125
|
+
const fields = Object.entries(params)
|
|
126
|
+
.map(([k]) => ` ${k}: string`)
|
|
127
|
+
.join('\n')
|
|
128
|
+
return ` ${JSON.stringify(p)}: {\n${fields}\n }`
|
|
129
|
+
})
|
|
130
|
+
.join('\n')
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
'// [fusée] Auto-generated router types — do not edit',
|
|
134
|
+
'// Generated by compiler/plugins/router-plugin.js',
|
|
135
|
+
'',
|
|
136
|
+
'/**',
|
|
137
|
+
' * All valid application route paths.',
|
|
138
|
+
' * Auto-completed in navigate(), <a href>, and useRoute().',
|
|
139
|
+
' */',
|
|
140
|
+
'export type AppRoute =',
|
|
141
|
+
` | ${pathUnion || "'/'"}`,
|
|
142
|
+
'',
|
|
143
|
+
'/**',
|
|
144
|
+
' * Dynamic route params per path.',
|
|
145
|
+
' * Only paths with `:param` segments appear here.',
|
|
146
|
+
' */',
|
|
147
|
+
'export type RouteParams = {',
|
|
148
|
+
paramEntries,
|
|
149
|
+
'}',
|
|
150
|
+
'',
|
|
151
|
+
'/**',
|
|
152
|
+
' * Extract params type for a specific route.',
|
|
153
|
+
' * @example',
|
|
154
|
+
' * type Params = ParamsFor<\'/users/:id\'> // { id: string }',
|
|
155
|
+
' */',
|
|
156
|
+
'export type ParamsFor<T extends AppRoute> =',
|
|
157
|
+
' T extends keyof RouteParams ? RouteParams[T] : Record<string, never>',
|
|
158
|
+
'',
|
|
159
|
+
'/**',
|
|
160
|
+
' * Type-safe navigate() — accepts only valid AppRoute paths.',
|
|
161
|
+
' * Import from fusee/router to get autocomplete.',
|
|
162
|
+
' */',
|
|
163
|
+
'export declare function navigate(path: AppRoute): void',
|
|
164
|
+
'',
|
|
165
|
+
'/**',
|
|
166
|
+
' * Current route params, typed as a record.',
|
|
167
|
+
' * Use ParamsFor<T> for specific route param shapes.',
|
|
168
|
+
' */',
|
|
169
|
+
'export declare function useRouteParams<T extends AppRoute = AppRoute>(): ParamsFor<T>',
|
|
170
|
+
'',
|
|
171
|
+
].join('\n')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _collectPaths(routes, prefix, allPaths, paramMap) {
|
|
175
|
+
for (const route of routes) {
|
|
176
|
+
const seg = route.path
|
|
177
|
+
const fullPath = seg === '' ? (prefix || '/') : prefix + (prefix.endsWith('/') ? '' : '/') + seg
|
|
178
|
+
const normalized = fullPath.replace(/\/+/g, '/') || '/'
|
|
179
|
+
|
|
180
|
+
if (normalized && !allPaths.includes(normalized)) {
|
|
181
|
+
allPaths.push(normalized)
|
|
182
|
+
|
|
183
|
+
const params = {}
|
|
184
|
+
const re = /:([^/]+)/g
|
|
185
|
+
let m
|
|
186
|
+
while ((m = re.exec(normalized)) !== null) {
|
|
187
|
+
params[m[1]] = 'string'
|
|
188
|
+
}
|
|
189
|
+
paramMap[normalized] = params
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (route.children?.length) {
|
|
193
|
+
_collectPaths(route.children, normalized === '/' ? '' : normalized, allPaths, paramMap)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|