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.
Files changed (77) hide show
  1. package/README.md +206 -139
  2. package/app/benchmark/benchmark.js +2 -2
  3. package/app/benchmark/vite.config.js +23 -0
  4. package/app/main.js +24 -3
  5. package/app/main.ts +7 -1
  6. package/app/pages/_layout.js +27 -0
  7. package/app/pages/async-work.js +65 -0
  8. package/app/pages/index.js +25 -5
  9. package/app/pages/index.ts +9 -0
  10. package/app/pages/router-example.js +24 -1
  11. package/app/pages/scroll-demo.ts +102 -0
  12. package/app/providers/ChildComponent.js +8 -2
  13. package/app/providers/ChildComponent.ts +47 -47
  14. package/app/providers/GrandchildComponent.js +19 -0
  15. package/app/providers/ParentComponent.js +5 -1
  16. package/app/routes/RouterExample.js +23 -1
  17. package/app/routes/TestResource.js +47 -0
  18. package/app/services.js +15 -0
  19. package/auto-imports.d.ts +12 -0
  20. package/framework/auto-import-preset.js +15 -9
  21. package/framework/core/async-context.js +27 -0
  22. package/framework/core/compiler/ast.js +167 -0
  23. package/framework/core/compiler/errors-list.js +47 -0
  24. package/framework/core/compiler/errors.js +107 -0
  25. package/framework/core/compiler/generator.js +363 -0
  26. package/framework/core/compiler/lexer.js +347 -0
  27. package/framework/core/compiler/main-compiler.js +115 -0
  28. package/framework/core/compiler/parser.js +414 -0
  29. package/framework/core/compiler/plugins/actions-plugin.js +263 -0
  30. package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  31. package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  32. package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  33. package/framework/core/compiler/plugins/router-plugin.js +196 -0
  34. package/framework/core/compiler/transformer.js +398 -0
  35. package/framework/core/component.js +61 -23
  36. package/framework/core/di.js +187 -0
  37. package/framework/core/directives.js +40 -96
  38. package/framework/core/evaluator.js +1 -1
  39. package/framework/core/h.js +430 -0
  40. package/framework/core/hydration.js +83 -0
  41. package/framework/core/reconcile.js +151 -0
  42. package/framework/core/signal.js +269 -0
  43. package/framework/core/store.js +322 -81
  44. package/framework/index.js +5 -3
  45. package/framework/router/file-router.js +1 -1
  46. package/framework/router/router.js +113 -21
  47. package/framework/server/actions.js +173 -0
  48. package/framework/server/actions.server.js +88 -0
  49. package/framework/server/index.js +18 -0
  50. package/framework/types/actions.d.ts +50 -0
  51. package/framework/types/actions.server.d.ts +36 -0
  52. package/framework/types/compiler/ast.d.ts +166 -0
  53. package/framework/types/compiler/errors-codes.d.ts +23 -0
  54. package/framework/types/compiler/errors.d.ts +43 -0
  55. package/framework/types/compiler/generator.d.ts +8 -0
  56. package/framework/types/compiler/h.d.ts +46 -0
  57. package/framework/types/compiler/lexer.d.ts +27 -0
  58. package/framework/types/compiler/main-compiler.d.ts +60 -0
  59. package/framework/types/compiler/parser.d.ts +4 -0
  60. package/framework/types/compiler/plugins/actions.d.ts +30 -0
  61. package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  62. package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  63. package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  64. package/framework/types/compiler/transformer.d.ts +28 -0
  65. package/framework/types/component.d.ts +89 -88
  66. package/framework/types/di.d.ts +43 -0
  67. package/framework/types/hydration.d.ts +30 -0
  68. package/framework/types/index.d.ts +38 -22
  69. package/framework/types/reconcile.d.ts +7 -0
  70. package/framework/types/router.d.ts +11 -1
  71. package/framework/types/server.d.ts +11 -0
  72. package/framework/types/signal.d.ts +142 -79
  73. package/framework/types/store.d.ts +66 -8
  74. package/ghid_go_fusee_integrat_go1_26.txt +276 -0
  75. package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
  76. package/package.json +44 -36
  77. 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
+ }