fusee-framework 1.6.0 → 1.8.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 (129) hide show
  1. package/README.md +145 -123
  2. package/app/benchmark/benchmark.js +633 -0
  3. package/app/benchmark/index.html +102 -0
  4. package/app/benchmark/styles.css +249 -0
  5. package/app/components/Counter.js +17 -7
  6. package/app/components/CounterSection.js +3 -4
  7. package/app/main.js +50 -62
  8. package/app/main.ts +78 -63
  9. package/app/pages/404.js +16 -0
  10. package/app/pages/[...all].js +35 -0
  11. package/app/pages/[...all].ts +35 -0
  12. package/app/pages/_layout.js +53 -0
  13. package/app/pages/_layout.ts +26 -0
  14. package/app/pages/about.js +23 -0
  15. package/app/pages/about.ts +29 -0
  16. package/app/pages/async-work.js +65 -0
  17. package/app/pages/composables.js +158 -0
  18. package/app/pages/composables.ts +158 -0
  19. package/app/pages/dynamic.js +40 -0
  20. package/app/pages/dynamic.ts +40 -0
  21. package/app/pages/index.js +137 -0
  22. package/app/pages/index.ts +87 -0
  23. package/app/pages/login.ts +21 -0
  24. package/app/pages/objects/[id].js +56 -0
  25. package/app/pages/objects/[id].ts +56 -0
  26. package/app/pages/posts/[category]/[slug].js +56 -0
  27. package/app/pages/posts/[category]/[slug].ts +56 -0
  28. package/app/pages/router-example.js +79 -0
  29. package/app/pages/router-example.ts +56 -0
  30. package/app/pages/scroll-demo.ts +102 -0
  31. package/app/pages/test-for.js +65 -0
  32. package/app/pages/test-for.ts +80 -0
  33. package/app/pages/users/[id].js +24 -0
  34. package/app/pages/users/[id].ts +46 -0
  35. package/app/pages/users/index.js +22 -0
  36. package/app/pages/users/index.ts +29 -0
  37. package/app/pages/users.js +43 -0
  38. package/app/pages/users.ts +78 -0
  39. package/app/providers/ChildComponent.js +8 -2
  40. package/app/providers/ChildComponent.ts +47 -47
  41. package/app/providers/GrandchildComponent.js +19 -0
  42. package/app/providers/ParentComponent.js +5 -1
  43. package/app/routes/About.js +1 -1
  44. package/app/routes/About.ts +1 -1
  45. package/app/routes/AuthRoutes.ts +41 -0
  46. package/app/routes/Home.js +9 -0
  47. package/app/routes/NotFound.js +16 -0
  48. package/app/routes/NotFound.ts +16 -0
  49. package/app/routes/RouterExample.js +78 -0
  50. package/app/routes/RouterExample.ts +56 -0
  51. package/app/routes/TestResource.js +47 -0
  52. package/app/routes/WildcardDemo.js +35 -0
  53. package/app/routes/WildcardDemo.ts +35 -0
  54. package/app/services.js +15 -0
  55. package/auto-imports.d.ts +9 -0
  56. package/bin/create-fusee-linux +0 -0
  57. package/bin/create-fusee-mac-arm64 +0 -0
  58. package/bin/create-fusee-mac-intel +0 -0
  59. package/bin/create-fusee-win.exe +0 -0
  60. package/bin/create-fusee.exe +0 -0
  61. package/bin/index.js +38 -0
  62. package/create-fusee-go/cmd/generate.go +72 -0
  63. package/create-fusee-go/cmd/init.go +38 -0
  64. package/create-fusee-go/cmd/root.go +121 -0
  65. package/create-fusee-go/go.mod +10 -0
  66. package/create-fusee-go/go.sum +10 -0
  67. package/create-fusee-go/internal/assets/assets.go +78 -0
  68. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  69. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  70. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  71. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  72. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  73. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  74. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  75. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  76. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  77. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  78. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  79. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  80. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  83. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  84. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  85. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  86. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  87. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  88. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  89. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  90. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  91. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  92. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  93. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  94. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  95. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  96. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  97. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  98. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  99. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  100. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  101. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  102. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  103. package/create-fusee-go/main.go +7 -0
  104. package/framework/auto-import-preset.js +13 -9
  105. package/framework/core/compiler.js +252 -249
  106. package/framework/core/component.js +279 -260
  107. package/framework/core/di.js +187 -0
  108. package/framework/core/directives.js +819 -786
  109. package/framework/core/evaluator.js +153 -70
  110. package/framework/core/event-delegation.js +33 -14
  111. package/framework/core/reconcile.js +151 -0
  112. package/framework/core/signal.js +679 -403
  113. package/framework/core/store.js +322 -81
  114. package/framework/index.js +12 -11
  115. package/framework/router/file-router.js +177 -0
  116. package/framework/router/router.js +554 -80
  117. package/framework/types/component.d.ts +89 -88
  118. package/framework/types/di.d.ts +43 -0
  119. package/framework/types/event-delegation.d.ts +0 -58
  120. package/framework/types/index.d.ts +32 -22
  121. package/framework/types/reconcile.d.ts +7 -0
  122. package/framework/types/router.d.ts +42 -15
  123. package/framework/types/signal.d.ts +147 -78
  124. package/framework/types/store.d.ts +66 -8
  125. package/index.html +638 -626
  126. package/package.json +5 -3
  127. package/tsconfig.json +1 -0
  128. package/vite.config.js +7 -1
  129. /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
@@ -1,81 +1,322 @@
1
- import { getCurrentInstance, setCurrentInstance } from './component.js'
2
-
3
- const storesRegistry = new Map()
4
- const storePlugins = []
5
-
6
- export function registerStorePlugin(plugin) {
7
- if (typeof plugin === 'function') {
8
- storePlugins.push(plugin)
9
- }
10
- }
11
-
12
- const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
13
- if (isDev && typeof window !== 'undefined') {
14
- window.__FRAMEWORK_STORES__ = storesRegistry
15
- }
16
-
17
- export function defineStore(id, setup) {
18
- if (typeof id !== 'string' || !id.trim()) {
19
- console.error('[framework] defineStore requires a valid string ID.')
20
- }
21
-
22
- function useStore() {
23
- let store = storesRegistry.get(id)
24
-
25
- if (!store) {
26
- const prevInstance = getCurrentInstance()
27
-
28
- setCurrentInstance(null)
29
-
30
- try {
31
- store = setup()
32
-
33
- if (store instanceof Promise) {
34
- console.error(`[framework] Store "${id}" cannot be async.`)
35
- throw new Error(`Async store "${id}" is not supported.`)
36
- }
37
-
38
- } catch (err) {
39
- console.error(`[framework] Error initializing store "${id}":`, err)
40
- throw err
41
- } finally {
42
- setCurrentInstance(prevInstance)
43
- }
44
-
45
- Object.defineProperties(store, {
46
- $id: {
47
- value: id,
48
- enumerable: false
49
- },
50
- $type: {
51
- value: 'store',
52
- enumerable: false
53
- }
54
- })
55
-
56
- for (const plugin of storePlugins) {
57
- try {
58
- plugin(store, id)
59
- } catch (e) {
60
- console.warn(`[framework] Store plugin error for "${id}":`, e)
61
- }
62
- }
63
-
64
- storesRegistry.set(id, store)
65
- }
66
-
67
- return store
68
- }
69
-
70
- useStore.$id = id
71
-
72
- return useStore
73
- }
74
-
75
- export function resetStore(id) {
76
- storesRegistry.delete(id)
77
- }
78
-
79
- export function clearStores() {
80
- storesRegistry.clear()
81
- }
1
+ import { getCurrentInstance, setCurrentInstance } from './component.js'
2
+ import { batch, effect } from './signal.js'
3
+
4
+
5
+ const storesRegistry = new Map()
6
+ const storePlugins = []
7
+ const initializationStack = new Set()
8
+
9
+ export const MutationType = {
10
+ DIRECT: 'direct',
11
+ PATCH_OBJECT: 'patch object',
12
+ PATCH_FUNCTION: 'patch function'
13
+ }
14
+
15
+ export function registerStorePlugin(plugin) {
16
+ if (typeof plugin === 'function') {
17
+ storePlugins.push(plugin)
18
+ }
19
+ }
20
+
21
+ export function useNestedStore(useStoreFn) {
22
+ if (typeof useStoreFn !== 'function' || !useStoreFn.$id) {
23
+ console.error('[framework] useNestedStore requires a valid store hook (defineStore result)')
24
+ throw new Error('Invalid store hook provided to useNestedStore')
25
+ }
26
+
27
+ const targetId = useStoreFn.$id
28
+
29
+ if (initializationStack.has(targetId)) {
30
+ const chain = Array.from(initializationStack).concat(targetId).join(' -> ')
31
+ console.error(`[framework] Circular dependency detected: ${chain}`)
32
+ throw new Error(`Circular store dependency detected: ${chain}`)
33
+ }
34
+
35
+ return useStoreFn()
36
+ }
37
+
38
+ const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
39
+ if (isDev && typeof window !== 'undefined') {
40
+ window.__FRAMEWORK_STORES__ = storesRegistry
41
+ }
42
+
43
+ export function defineStore(id, setup) {
44
+ if (typeof id !== 'string' || !id.trim()) {
45
+ console.error('[framework] defineStore requires a valid string ID.')
46
+ }
47
+
48
+ function useStore() {
49
+ let store = storesRegistry.get(id)
50
+
51
+ if (!store) {
52
+ const prevInstance = getCurrentInstance()
53
+
54
+ setCurrentInstance(null)
55
+ initializationStack.add(id)
56
+
57
+ try {
58
+ store = setup()
59
+
60
+ if (store instanceof Promise) {
61
+ console.error(`[framework] Store "${id}" cannot be async.`)
62
+ throw new Error(`Async store "${id}" is not supported.`)
63
+ }
64
+
65
+ } catch (err) {
66
+ console.error(`[framework] Error initializing store "${id}":`, err)
67
+ throw err
68
+ } finally {
69
+ initializationStack.delete(id)
70
+ setCurrentInstance(prevInstance)
71
+ }
72
+
73
+ const initialState = {}
74
+ const signalKeys = []
75
+ const getterKeys = []
76
+ for (const key in store) {
77
+ if (typeof store[key] === 'function' && store[key].isSignal) {
78
+ if (store[key].readonly) {
79
+ getterKeys.push(key)
80
+ } else {
81
+ initialState[key] = store[key]()
82
+ signalKeys.push(key)
83
+ }
84
+ }
85
+ }
86
+
87
+ // Subscription management
88
+ const subscriptions = new Map()
89
+ let subscriptionId = 0
90
+ let currentMutation = null
91
+
92
+ function notifySubscribers() {
93
+ if (!currentMutation) return
94
+ const mutation = currentMutation
95
+ currentMutation = null
96
+
97
+ const state = {}
98
+ for (const key of signalKeys) {
99
+ state[key] = store[key]()
100
+ }
101
+
102
+ for (const [subId, sub] of subscriptions) {
103
+ if (sub.flush === 'sync') {
104
+ sub.callback(mutation, state)
105
+ } else {
106
+ // Default: post (after all batched updates)
107
+ queueMicrotask(() => {
108
+ if (subscriptions.has(subId)) {
109
+ sub.callback(mutation, state)
110
+ }
111
+ })
112
+ }
113
+ }
114
+ }
115
+
116
+ function wrapSignal(key, originalSignal) {
117
+ const wrapped = function(newValue) {
118
+ if (arguments.length === 0) {
119
+ return originalSignal()
120
+ } else {
121
+ const result = originalSignal(newValue)
122
+ if (!currentMutation) {
123
+ currentMutation = {
124
+ type: MutationType.DIRECT,
125
+ storeId: id,
126
+ payload: { [key]: newValue }
127
+ }
128
+ notifySubscribers()
129
+ }
130
+ return result
131
+ }
132
+ }
133
+ wrapped.isSignal = true
134
+ for (const method of ['map', 'filter', 'slice', 'concat', 'flat', 'flatMap',
135
+ 'find', 'findLast', 'findIndex', 'findLastIndex',
136
+ 'indexOf', 'lastIndexOf', 'includes', 'every', 'some',
137
+ 'reduce', 'at', 'join', 'push', 'pop', 'shift', 'unshift',
138
+ 'splice', 'remove', 'clear', 'sort', 'reverse']) {
139
+ if (originalSignal[method]) {
140
+ wrapped[method] = originalSignal[method]
141
+ }
142
+ }
143
+ return wrapped
144
+ }
145
+
146
+ function wrapGetter(key, originalGetter) {
147
+ const wrapped = function(newValue) {
148
+ if (arguments.length === 0) {
149
+ return originalGetter()
150
+ } else {
151
+ console.warn(`[framework] Getter "${key}" is read-only`)
152
+ return originalGetter()
153
+ }
154
+ }
155
+ wrapped.isSignal = true
156
+ wrapped.readonly = true
157
+ for (const method of ['map', 'filter', 'slice', 'concat', 'flat', 'flatMap',
158
+ 'find', 'findLast', 'findIndex', 'findLastIndex',
159
+ 'indexOf', 'lastIndexOf', 'includes', 'every', 'some',
160
+ 'reduce', 'at', 'join']) {
161
+ if (originalGetter[method]) {
162
+ wrapped[method] = originalGetter[method]
163
+ }
164
+ }
165
+ return wrapped
166
+ }
167
+
168
+ const wrappedSignals = {}
169
+ for (const key of signalKeys) {
170
+ wrappedSignals[key] = wrapSignal(key, store[key])
171
+ store[key] = wrappedSignals[key]
172
+ }
173
+
174
+ for (const key of getterKeys) {
175
+ store[key] = wrapGetter(key, store[key])
176
+ }
177
+
178
+ Object.defineProperties(store, {
179
+ id: {
180
+ value: id,
181
+ enumerable: false
182
+ },
183
+ type: {
184
+ value: 'store',
185
+ enumerable: false
186
+ },
187
+ patch: {
188
+ value: (arg) => {
189
+ const isObjectPatch = arg && typeof arg === 'object'
190
+ currentMutation = {
191
+ type: isObjectPatch ? MutationType.PATCH_OBJECT : MutationType.PATCH_FUNCTION,
192
+ storeId: id,
193
+ payload: isObjectPatch ? { ...arg } : undefined
194
+ }
195
+
196
+ batch(() => {
197
+ if (typeof arg === 'function') {
198
+ arg(store)
199
+ } else if (isObjectPatch) {
200
+ for (const key in arg) {
201
+ if (typeof store[key] === 'function' && store[key].isSignal && !store[key].readonly) {
202
+ const originalSignal = wrappedSignals[key]
203
+ originalSignal(arg[key])
204
+ }
205
+ }
206
+ }
207
+ })
208
+
209
+ notifySubscribers()
210
+ },
211
+ enumerable: false
212
+ },
213
+ reset: {
214
+ value: () => {
215
+ currentMutation = {
216
+ type: MutationType.PATCH_FUNCTION,
217
+ storeId: id,
218
+ payload: undefined
219
+ }
220
+
221
+ batch(() => {
222
+ for (const key in initialState) {
223
+ const originalSignal = wrappedSignals[key]
224
+ originalSignal(initialState[key])
225
+ }
226
+ })
227
+
228
+ notifySubscribers()
229
+ },
230
+ enumerable: false
231
+ },
232
+ subscribe: {
233
+ value: (callback, options = {}) => {
234
+ if (typeof callback !== 'function') {
235
+ console.warn('[framework] subscribe callback must be a function')
236
+ return () => {}
237
+ }
238
+
239
+ const subId = ++subscriptionId
240
+ const sub = {
241
+ callback,
242
+ flush: options.flush || 'post',
243
+ detached: options.detached || false
244
+ }
245
+ subscriptions.set(subId, sub)
246
+
247
+ if (!sub.detached) {
248
+ const instance = getCurrentInstance()
249
+ if (instance && instance._unmountHooks) {
250
+ instance._unmountHooks.push(() => {
251
+ subscriptions.delete(subId)
252
+ })
253
+ }
254
+ }
255
+
256
+ return () => {
257
+ subscriptions.delete(subId)
258
+ }
259
+ },
260
+ enumerable: false
261
+ }
262
+ })
263
+
264
+ for (const plugin of storePlugins) {
265
+ try {
266
+ plugin(store, id)
267
+ } catch (e) {
268
+ console.warn(`[framework] Store plugin error for "${id}":`, e)
269
+ }
270
+ }
271
+
272
+ storesRegistry.set(id, store)
273
+ }
274
+
275
+ return store
276
+ }
277
+
278
+ useStore.$id = id
279
+
280
+ return useStore
281
+ }
282
+
283
+ export function resetStore(id) {
284
+ storesRegistry.delete(id)
285
+ }
286
+
287
+ export function clearStores() {
288
+ storesRegistry.clear()
289
+ }
290
+
291
+ export function storeToRefs(store) {
292
+ const refs = {}
293
+ for (const key in store) {
294
+ const value = store[key]
295
+ if (typeof value === 'function' && value.isSignal) {
296
+ refs[key] = value
297
+ }
298
+ }
299
+ return refs
300
+ }
301
+
302
+ export function storeToState(store) {
303
+ const state = {}
304
+ for (const key in store) {
305
+ const value = store[key]
306
+ if (typeof value === 'function' && value.isSignal && !value.readonly) {
307
+ state[key] = value
308
+ }
309
+ }
310
+ return state
311
+ }
312
+
313
+ export function storeToGetters(store) {
314
+ const getters = {}
315
+ for (const key in store) {
316
+ const value = store[key]
317
+ if (typeof value === 'function' && value.isSignal && value.readonly) {
318
+ getters[key] = value
319
+ }
320
+ }
321
+ return getters
322
+ }
@@ -1,11 +1,12 @@
1
- // ─── Framework Public API ─────────────────────────────────────────────────────
2
- export { signal, computed, effect, batch, untrack, inspect, watch } from './core/signal.js'
3
- export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject, getCurrentInstance } from './core/component.js'
4
- export { createRouter, navigate, mountOutlet } from './router/router.js'
5
- export { mountTemplate } from './core/compiler.js'
6
-
7
- export { defineComposable, assertSetupContext } from './core/composable.js'
8
- export { defineStore, resetStore, clearStores, registerStorePlugin } from './core/store.js'
9
- export { directive } from './core/directives.js'
10
-
11
- export const emit = () => console.warn('[framework] emit() can only be used inside component setup()')
1
+ // ─── Framework Public API ─────────────────────────────────────────────────────
2
+ export { signal, computed, effect, batch, untrack, inspect, watch, onCleanup, resource, createSuspense, scheduleAsyncJob } from './core/signal.js'
3
+ export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject, getCurrentInstance } from './core/component.js'
4
+ export { InjectionToken, provideGlobal } from './core/di.js'
5
+ export { createRouter, navigate, mountOutlet, currentRoute, routeParams, routeQuery, matchedRoutes } from './router/router.js'
6
+ export { generateRoutes } from './router/file-router.js'
7
+ export { mountTemplate } from './core/compiler.js'
8
+ export { defineComposable, assertSetupContext } from './core/composable.js'
9
+ export { defineStore, resetStore, clearStores, registerStorePlugin, storeToRefs, storeToState, storeToGetters, useNestedStore, MutationType } from './core/store.js'
10
+ export { directive } from './core/directives.js'
11
+ export { reconcile } from './core/reconcile.js'
12
+ export const emit = () => console.warn('[framework] emit() can only be used inside component setup()')
@@ -0,0 +1,177 @@
1
+ import { defineAsyncComponent } from '../core/component.js';
2
+
3
+ export function generateRoutes(globResults, options = {}) {
4
+ const entries = _parseGlobEntries(globResults, options);
5
+ const tree = _buildRouteTree(entries, false);
6
+ _sortRoutesRecursive(tree);
7
+ return tree;
8
+ }
9
+
10
+ function _parseGlobEntries(globResults, options) {
11
+ const entries = [];
12
+
13
+ for (const filePath of Object.keys(globResults)) {
14
+ const loader = globResults[filePath];
15
+
16
+ let cleanPath = filePath
17
+ .replace(/^\.?\/?app\/pages\//, '')
18
+ .replace(/^\.?\/?pages\//, '')
19
+ .replace(/\.[jt]sx?$/, '');
20
+
21
+ const parts = cleanPath.split('/');
22
+ const rawFileName = parts.pop();
23
+ const rawDirSegments = parts;
24
+
25
+ const dirSegments = rawDirSegments.map(_transformSegment);
26
+ const fileName = _transformSegment(rawFileName);
27
+
28
+ const isIndex = rawFileName === 'index';
29
+ const isCatchAll = /^\[\.\.\./.test(rawFileName);
30
+
31
+ entries.push({
32
+ filePath,
33
+ loader,
34
+ dirSegments,
35
+ fileName,
36
+ isIndex,
37
+ isCatchAll,
38
+ component: defineAsyncComponent({
39
+ loader,
40
+ loadingComponent: options.loadingComponent
41
+ })
42
+ });
43
+ }
44
+
45
+ return entries;
46
+ }
47
+
48
+ function _transformSegment(seg) {
49
+ return seg
50
+ .replace(/\[\.\.\.([^\]]+)\]/g, '*')
51
+ .replace(/\[([^\]]+)\]/g, ':$1');
52
+ }
53
+
54
+ function _buildRouteTree(entries, isChild) {
55
+ const filesHere = new Map();
56
+ const grouped = new Map();
57
+
58
+ for (const entry of entries) {
59
+ if (entry.dirSegments.length === 0) {
60
+ filesHere.set(entry.fileName, entry);
61
+ } else {
62
+ const firstDir = entry.dirSegments[0];
63
+ if (!grouped.has(firstDir)) grouped.set(firstDir, []);
64
+ grouped.get(firstDir).push(entry);
65
+ }
66
+ }
67
+
68
+ let rootLayoutEntry = null;
69
+ if (filesHere.has('_layout')) {
70
+ rootLayoutEntry = filesHere.get('_layout');
71
+ filesHere.delete('_layout');
72
+ }
73
+
74
+ const routes = [];
75
+ const usedAsLayout = new Set();
76
+
77
+ for (const [dirName, dirEntries] of grouped) {
78
+ const layoutEntry = filesHere.get(dirName);
79
+
80
+ if (layoutEntry) {
81
+ usedAsLayout.add(dirName);
82
+
83
+ const childEntries = dirEntries.map(e => ({
84
+ ...e,
85
+ dirSegments: e.dirSegments.slice(1)
86
+ }));
87
+
88
+ const children = _buildRouteTree(childEntries, true);
89
+
90
+ routes.push({
91
+ path: isChild ? dirName : '/' + dirName,
92
+ component: layoutEntry.component,
93
+ children
94
+ });
95
+ } else {
96
+ for (const entry of dirEntries) {
97
+ const segments = [...entry.dirSegments];
98
+ if (!entry.isIndex) segments.push(entry.fileName);
99
+
100
+ const routePath = isChild
101
+ ? segments.join('/')
102
+ : '/' + segments.join('/');
103
+
104
+ routes.push({
105
+ path: routePath,
106
+ component: entry.component
107
+ });
108
+ }
109
+ }
110
+ }
111
+
112
+ for (const [name, entry] of filesHere) {
113
+ if (usedAsLayout.has(name)) continue;
114
+
115
+ if (entry.isIndex) {
116
+ routes.push({ path: isChild ? '' : '/', component: entry.component });
117
+ } else if (entry.isCatchAll || name === '*') {
118
+ routes.push({ path: '*', component: entry.component });
119
+ } else {
120
+ routes.push({
121
+ path: isChild ? name : '/' + name,
122
+ component: entry.component
123
+ });
124
+ }
125
+ }
126
+
127
+ if (rootLayoutEntry) {
128
+ return [{
129
+ path: '',
130
+ component: rootLayoutEntry.component,
131
+ children: routes
132
+ }];
133
+ }
134
+
135
+ return routes;
136
+ }
137
+
138
+ function _sortRoutesRecursive(routes) {
139
+ routes.sort(_compareRoutes);
140
+ for (const route of routes) {
141
+ if (route.children) _sortRoutesRecursive(route.children);
142
+ }
143
+ }
144
+
145
+ function _getPathArray(path) {
146
+ return Array.isArray(path) ? path : [path];
147
+ }
148
+
149
+ function _compareRoutes(a, b) {
150
+ const aPaths = _getPathArray(a.path);
151
+ const bPaths = _getPathArray(b.path);
152
+
153
+ const aPath = aPaths[0];
154
+ const bPath = bPaths[0];
155
+
156
+ if (aPath === '*' && bPath !== '*') return 1;
157
+ if (aPath !== '*' && bPath === '*') return -1;
158
+
159
+ const aWild = aPath.endsWith('/*');
160
+ const bWild = bPath.endsWith('/*');
161
+ if (aWild && !bWild) return 1;
162
+ if (!aWild && bWild) return -1;
163
+
164
+ const aDynamic = aPath.includes(':');
165
+ const bDynamic = bPath.includes(':');
166
+ if (aDynamic && !bDynamic) return 1;
167
+ if (!aDynamic && bDynamic) return -1;
168
+
169
+ if (a.children && !b.children) return -1;
170
+ if (!a.children && b.children) return 1;
171
+
172
+ const aSegs = aPath.split('/').filter(Boolean).length;
173
+ const bSegs = bPath.split('/').filter(Boolean).length;
174
+ if (aSegs !== bSegs) return bSegs - aSegs;
175
+
176
+ return aPath.localeCompare(bPath);
177
+ }