kiru 0.50.0 → 0.50.2
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/dist/appContext.d.ts.map +1 -1
- package/dist/appContext.js +16 -6
- package/dist/appContext.js.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/dom.js +4 -4
- package/dist/dom.js.map +1 -1
- package/dist/globalContext.d.ts +3 -3
- package/dist/globalContext.d.ts.map +1 -1
- package/dist/globalContext.js +3 -15
- package/dist/globalContext.js.map +1 -1
- package/dist/hmr.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +1 -1
- package/dist/reconciler.js +2 -2
- package/dist/reconciler.js.map +1 -1
- package/dist/router/fileRouter.d.ts +1 -28
- package/dist/router/fileRouter.d.ts.map +1 -1
- package/dist/router/fileRouter.js +2 -302
- package/dist/router/fileRouter.js.map +1 -1
- package/dist/router/fileRouterController.d.ts +28 -0
- package/dist/router/fileRouterController.d.ts.map +1 -0
- package/dist/router/fileRouterController.js +418 -0
- package/dist/router/fileRouterController.js.map +1 -0
- package/dist/router/globals.d.ts +1 -1
- package/dist/router/globals.d.ts.map +1 -1
- package/dist/router/index.d.ts +3 -3
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -3
- package/dist/router/index.js.map +1 -1
- package/dist/router/{config.d.ts → pageConfig.d.ts} +1 -1
- package/dist/router/pageConfig.d.ts.map +1 -0
- package/dist/router/{config.js → pageConfig.js} +1 -1
- package/dist/router/pageConfig.js.map +1 -0
- package/dist/scheduler.js +7 -7
- package/dist/scheduler.js.map +1 -1
- package/dist/signals/watch.d.ts.map +1 -1
- package/dist/signals/watch.js +1 -2
- package/dist/signals/watch.js.map +1 -1
- package/dist/swr.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/appContext.ts +18 -7
- package/src/context.ts +1 -1
- package/src/dom.ts +4 -4
- package/src/globalContext.ts +7 -20
- package/src/hmr.ts +2 -2
- package/src/index.ts +0 -9
- package/src/reconciler.ts +2 -2
- package/src/router/fileRouter.ts +4 -442
- package/src/router/fileRouterController.ts +591 -0
- package/src/router/globals.ts +1 -1
- package/src/router/index.ts +3 -3
- package/src/scheduler.ts +8 -8
- package/src/signals/watch.ts +2 -5
- package/src/swr.ts +1 -1
- package/src/types.ts +1 -1
- package/dist/router/config.d.ts.map +0 -1
- package/dist/router/config.js.map +0 -1
- /package/src/router/{config.ts → pageConfig.ts} +0 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { Signal, computed, flushSync } from "../index.js"
|
|
2
|
+
import { __DEV__ } from "../env.js"
|
|
3
|
+
import { createElement } from "../element.js"
|
|
4
|
+
import { type FileRouterContextType } from "./context.js"
|
|
5
|
+
import { FileRouterDataLoadError } from "./errors.js"
|
|
6
|
+
import { fileRouterInstance } from "./globals.js"
|
|
7
|
+
import type {
|
|
8
|
+
ErrorPageProps,
|
|
9
|
+
FileRouterConfig,
|
|
10
|
+
PageConfig,
|
|
11
|
+
PageProps,
|
|
12
|
+
RouteQuery,
|
|
13
|
+
RouterState,
|
|
14
|
+
} from "./types.js"
|
|
15
|
+
import type {
|
|
16
|
+
DefaultComponentModule,
|
|
17
|
+
PageModule,
|
|
18
|
+
ViteImportMap,
|
|
19
|
+
} from "./types.internal.js"
|
|
20
|
+
|
|
21
|
+
interface FormattedViteImportMap {
|
|
22
|
+
[key: string]: {
|
|
23
|
+
load: () => Promise<DefaultComponentModule>
|
|
24
|
+
specificity: number
|
|
25
|
+
segments: string[]
|
|
26
|
+
filePath?: string
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class FileRouterController {
|
|
31
|
+
private enableTransitions: boolean
|
|
32
|
+
private pages: FormattedViteImportMap
|
|
33
|
+
private layouts: FormattedViteImportMap
|
|
34
|
+
private abortController: AbortController
|
|
35
|
+
private currentPage: Signal<{
|
|
36
|
+
component: Kiru.FC<any>
|
|
37
|
+
config?: PageConfig
|
|
38
|
+
route: string
|
|
39
|
+
} | null>
|
|
40
|
+
private currentPageProps: Signal<PageProps<PageConfig>>
|
|
41
|
+
private currentLayouts: Signal<Kiru.FC[]>
|
|
42
|
+
private state: Signal<RouterState>
|
|
43
|
+
private contextValue: Signal<FileRouterContextType>
|
|
44
|
+
private cleanups: (() => void)[] = []
|
|
45
|
+
private filePathToPageRoute?: Map<
|
|
46
|
+
string,
|
|
47
|
+
{ route: string; config: PageConfig }
|
|
48
|
+
>
|
|
49
|
+
private pageRouteToConfig?: Map<string, PageConfig>
|
|
50
|
+
private currentRoute: string | null
|
|
51
|
+
|
|
52
|
+
constructor(config: FileRouterConfig) {
|
|
53
|
+
fileRouterInstance.current = this
|
|
54
|
+
this.pages = {}
|
|
55
|
+
this.layouts = {}
|
|
56
|
+
this.abortController = new AbortController()
|
|
57
|
+
this.currentPage = new Signal(null)
|
|
58
|
+
this.currentPageProps = new Signal({})
|
|
59
|
+
this.currentLayouts = new Signal([])
|
|
60
|
+
this.state = new Signal<RouterState>({
|
|
61
|
+
path: window.location.pathname,
|
|
62
|
+
params: {},
|
|
63
|
+
query: {},
|
|
64
|
+
signal: this.abortController.signal,
|
|
65
|
+
})
|
|
66
|
+
this.contextValue = computed<FileRouterContextType>(() => ({
|
|
67
|
+
state: this.state.value,
|
|
68
|
+
navigate: this.navigate.bind(this),
|
|
69
|
+
setQuery: this.setQuery.bind(this),
|
|
70
|
+
reload: (options?: { transition?: boolean }) =>
|
|
71
|
+
this.loadRoute(void 0, void 0, options?.transition),
|
|
72
|
+
}))
|
|
73
|
+
if (__DEV__) {
|
|
74
|
+
this.filePathToPageRoute = new Map()
|
|
75
|
+
this.pageRouteToConfig = new Map()
|
|
76
|
+
}
|
|
77
|
+
this.currentRoute = null
|
|
78
|
+
|
|
79
|
+
const { pages, layouts, dir = "/pages", baseUrl = "/", transition } = config
|
|
80
|
+
this.enableTransitions = !!transition
|
|
81
|
+
const [normalizedDir, normalizedBaseUrl] = [
|
|
82
|
+
normalizePrefixPath(dir),
|
|
83
|
+
normalizePrefixPath(baseUrl),
|
|
84
|
+
]
|
|
85
|
+
this.pages = formatViteImportMap(
|
|
86
|
+
pages as ViteImportMap,
|
|
87
|
+
normalizedDir,
|
|
88
|
+
normalizedBaseUrl
|
|
89
|
+
)
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
validateRoutes(this.pages)
|
|
92
|
+
}
|
|
93
|
+
this.layouts = formatViteImportMap(
|
|
94
|
+
layouts as ViteImportMap,
|
|
95
|
+
normalizedDir,
|
|
96
|
+
normalizedBaseUrl
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
this.loadRoute()
|
|
100
|
+
|
|
101
|
+
const handlePopState = () => this.loadRoute()
|
|
102
|
+
window.addEventListener("popstate", handlePopState)
|
|
103
|
+
this.cleanups.push(() =>
|
|
104
|
+
window.removeEventListener("popstate", handlePopState)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public onPageConfigDefined<T extends PageConfig>(fp: string, config: T) {
|
|
109
|
+
const existing = this.filePathToPageRoute?.get(fp)
|
|
110
|
+
if (existing === undefined) {
|
|
111
|
+
const route = this.currentRoute
|
|
112
|
+
if (!route) return
|
|
113
|
+
this.filePathToPageRoute?.set(fp, { route, config })
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
const curPage = this.currentPage.value
|
|
117
|
+
if (curPage?.route === existing.route && config.loader) {
|
|
118
|
+
const p = this.currentPageProps.value
|
|
119
|
+
let transition = this.enableTransitions
|
|
120
|
+
if (config.loader.transition !== undefined) {
|
|
121
|
+
transition = config.loader.transition
|
|
122
|
+
}
|
|
123
|
+
const props = {
|
|
124
|
+
...p,
|
|
125
|
+
loading: true,
|
|
126
|
+
data: null,
|
|
127
|
+
error: null,
|
|
128
|
+
}
|
|
129
|
+
handleStateTransition(this.state.value.signal, transition, () => {
|
|
130
|
+
this.currentPageProps.value = props
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
this.loadRouteData(config.loader, props, this.state.value, transition)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.pageRouteToConfig?.set(existing.route, config)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public getContextValue() {
|
|
140
|
+
return this.contextValue.value
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public getChildren() {
|
|
144
|
+
const page = this.currentPage.value,
|
|
145
|
+
props = this.currentPageProps.value,
|
|
146
|
+
layouts = this.currentLayouts.value
|
|
147
|
+
|
|
148
|
+
if (page) {
|
|
149
|
+
// Wrap component with layouts (outermost first)
|
|
150
|
+
return layouts.reduceRight(
|
|
151
|
+
(children, Layout) => createElement(Layout, { children }),
|
|
152
|
+
createElement(page.component, props)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public dispose() {
|
|
160
|
+
this.cleanups.forEach((cleanup) => cleanup())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private matchRoute(pathSegments: string[]) {
|
|
164
|
+
const matches: Array<{
|
|
165
|
+
route: string
|
|
166
|
+
pageEntry: FormattedViteImportMap[string]
|
|
167
|
+
params: Record<string, string>
|
|
168
|
+
routeSegments: string[]
|
|
169
|
+
}> = []
|
|
170
|
+
|
|
171
|
+
// Find all matching routes
|
|
172
|
+
outer: for (const [route, pageEntry] of Object.entries(this.pages)) {
|
|
173
|
+
const routeSegments = pageEntry.segments
|
|
174
|
+
const pathMatchingSegments = routeSegments.filter(
|
|
175
|
+
(seg) => !seg.startsWith("(") && !seg.endsWith(")")
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
const params: Record<string, string> = {}
|
|
179
|
+
let hasCatchall = false
|
|
180
|
+
|
|
181
|
+
// Check if route matches
|
|
182
|
+
for (
|
|
183
|
+
let i = 0;
|
|
184
|
+
i < pathMatchingSegments.length && i < pathSegments.length;
|
|
185
|
+
i++
|
|
186
|
+
) {
|
|
187
|
+
const routeSeg = pathMatchingSegments[i]
|
|
188
|
+
|
|
189
|
+
if (routeSeg.startsWith(":")) {
|
|
190
|
+
const key = routeSeg.slice(1)
|
|
191
|
+
|
|
192
|
+
if (routeSeg.endsWith("*")) {
|
|
193
|
+
// Catchall route - matches remaining segments
|
|
194
|
+
hasCatchall = true
|
|
195
|
+
const catchallKey = key.slice(0, -1) // Remove the *
|
|
196
|
+
params[catchallKey] = pathSegments.slice(i).join("/")
|
|
197
|
+
break
|
|
198
|
+
} else {
|
|
199
|
+
// Regular dynamic segment
|
|
200
|
+
if (i >= pathSegments.length) {
|
|
201
|
+
continue outer
|
|
202
|
+
}
|
|
203
|
+
params[key] = pathSegments[i]
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
// Static segment
|
|
207
|
+
if (routeSeg !== pathSegments[i]) {
|
|
208
|
+
continue outer
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// For non-catchall routes, ensure exact length match
|
|
214
|
+
if (!hasCatchall && pathMatchingSegments.length !== pathSegments.length) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
matches.push({
|
|
219
|
+
route,
|
|
220
|
+
pageEntry,
|
|
221
|
+
params,
|
|
222
|
+
routeSegments,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Sort by specificity (highest first) and return the best match
|
|
227
|
+
if (matches.length === 0) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
matches.sort((a, b) => b.pageEntry.specificity - a.pageEntry.specificity)
|
|
232
|
+
const bestMatch = matches[0]
|
|
233
|
+
|
|
234
|
+
return bestMatch
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async loadRoute(
|
|
238
|
+
path: string = window.location.pathname,
|
|
239
|
+
props: PageProps<PageConfig> = {},
|
|
240
|
+
enableTransition = this.enableTransitions
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
this.abortController?.abort()
|
|
243
|
+
const signal = (this.abortController = new AbortController()).signal
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const pathSegments = path.split("/").filter(Boolean)
|
|
247
|
+
const routeMatch = this.matchRoute(pathSegments)
|
|
248
|
+
|
|
249
|
+
if (!routeMatch) {
|
|
250
|
+
const _404 = this.matchRoute(["404"])
|
|
251
|
+
if (!_404) {
|
|
252
|
+
if (__DEV__) {
|
|
253
|
+
console.error(
|
|
254
|
+
`[kiru/router]: No 404 route defined (path: ${path}).
|
|
255
|
+
See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
const errorProps = {
|
|
261
|
+
source: { path },
|
|
262
|
+
} satisfies ErrorPageProps
|
|
263
|
+
|
|
264
|
+
return this.navigate("/404", { replace: true, props: errorProps })
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { route, pageEntry, params, routeSegments } = routeMatch
|
|
268
|
+
|
|
269
|
+
this.currentRoute = route
|
|
270
|
+
const pagePromise = pageEntry.load()
|
|
271
|
+
|
|
272
|
+
const layoutPromises = ["/", ...routeSegments].reduce((acc, _, i) => {
|
|
273
|
+
const layoutPath = "/" + routeSegments.slice(0, i).join("/")
|
|
274
|
+
const layout = this.layouts[layoutPath]
|
|
275
|
+
|
|
276
|
+
if (!layout) {
|
|
277
|
+
return acc
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [...acc, layout.load()]
|
|
281
|
+
}, [] as Promise<DefaultComponentModule>[])
|
|
282
|
+
|
|
283
|
+
const query = parseQuery(window.location.search)
|
|
284
|
+
const [page, ...layouts] = await Promise.all([
|
|
285
|
+
pagePromise,
|
|
286
|
+
...layoutPromises,
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
this.currentRoute = null
|
|
290
|
+
if (signal.aborted) return
|
|
291
|
+
|
|
292
|
+
if (typeof page.default !== "function") {
|
|
293
|
+
throw new Error(
|
|
294
|
+
"[kiru/router]: Route component must be a default exported function"
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const routerState: RouterState = {
|
|
299
|
+
path,
|
|
300
|
+
params,
|
|
301
|
+
query,
|
|
302
|
+
signal,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let config = (page as unknown as PageModule).config
|
|
306
|
+
if (__DEV__) {
|
|
307
|
+
if (this.pageRouteToConfig?.has(route)) {
|
|
308
|
+
config = this.pageRouteToConfig.get(route)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (config?.loader) {
|
|
313
|
+
props = { ...props, loading: true, data: null, error: null }
|
|
314
|
+
this.loadRouteData(config.loader, props, routerState, enableTransition)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.state.value = routerState
|
|
318
|
+
handleStateTransition(signal, enableTransition, () => {
|
|
319
|
+
this.currentPage.value = {
|
|
320
|
+
component: page.default,
|
|
321
|
+
config,
|
|
322
|
+
route: "/" + routeSegments.join("/"),
|
|
323
|
+
}
|
|
324
|
+
this.currentPageProps.value = props
|
|
325
|
+
this.currentLayouts.value = layouts
|
|
326
|
+
.filter((m) => typeof m.default === "function")
|
|
327
|
+
.map((m) => m.default)
|
|
328
|
+
})
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error("[kiru/router]: Failed to load route component:", error)
|
|
331
|
+
this.currentPage.value = null
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async loadRouteData(
|
|
336
|
+
loader: NonNullable<PageConfig["loader"]>,
|
|
337
|
+
props: PageProps<PageConfig>,
|
|
338
|
+
routerState: RouterState,
|
|
339
|
+
enableTransition = this.enableTransitions
|
|
340
|
+
) {
|
|
341
|
+
loader
|
|
342
|
+
.load(routerState)
|
|
343
|
+
.then(
|
|
344
|
+
(data) => ({ data, error: null }),
|
|
345
|
+
(error) => ({
|
|
346
|
+
data: null,
|
|
347
|
+
error: new FileRouterDataLoadError(error),
|
|
348
|
+
})
|
|
349
|
+
)
|
|
350
|
+
.then(({ data, error }) => {
|
|
351
|
+
if (routerState.signal.aborted) return
|
|
352
|
+
|
|
353
|
+
let transition = enableTransition
|
|
354
|
+
if (loader.transition !== undefined) {
|
|
355
|
+
transition = loader.transition
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
handleStateTransition(routerState.signal, transition, () => {
|
|
359
|
+
this.currentPageProps.value = {
|
|
360
|
+
...props,
|
|
361
|
+
loading: false,
|
|
362
|
+
data,
|
|
363
|
+
error,
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async navigate(
|
|
370
|
+
path: string,
|
|
371
|
+
options?: {
|
|
372
|
+
replace?: boolean
|
|
373
|
+
transition?: boolean
|
|
374
|
+
props?: Record<string, unknown>
|
|
375
|
+
}
|
|
376
|
+
) {
|
|
377
|
+
const f = options?.replace ? "replaceState" : "pushState"
|
|
378
|
+
window.history[f]({}, "", path)
|
|
379
|
+
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
|
|
380
|
+
return this.loadRoute(path, options?.props, options?.transition)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private setQuery(query: RouteQuery) {
|
|
384
|
+
const queryString = buildQueryString(query)
|
|
385
|
+
const newUrl = `${this.state.value.path}${
|
|
386
|
+
queryString ? `?${queryString}` : ""
|
|
387
|
+
}`
|
|
388
|
+
window.history.pushState(null, "", newUrl)
|
|
389
|
+
this.state.value = { ...this.state.value, query }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function parseQuery(
|
|
394
|
+
search: string
|
|
395
|
+
): Record<string, string | string[] | undefined> {
|
|
396
|
+
const params = new URLSearchParams(search)
|
|
397
|
+
const query: Record<string, string | string[] | undefined> = {}
|
|
398
|
+
|
|
399
|
+
for (const [key, value] of params.entries()) {
|
|
400
|
+
if (query[key]) {
|
|
401
|
+
// Convert to array if multiple values
|
|
402
|
+
if (Array.isArray(query[key])) {
|
|
403
|
+
;(query[key] as string[]).push(value)
|
|
404
|
+
} else {
|
|
405
|
+
query[key] = [query[key] as string, value]
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
query[key] = value
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return query
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildQueryString(
|
|
416
|
+
query: Record<string, string | string[] | undefined>
|
|
417
|
+
): string {
|
|
418
|
+
const params = new URLSearchParams()
|
|
419
|
+
|
|
420
|
+
for (const [key, value] of Object.entries(query)) {
|
|
421
|
+
if (value !== undefined) {
|
|
422
|
+
if (Array.isArray(value)) {
|
|
423
|
+
value.forEach((v) => params.append(key, v))
|
|
424
|
+
} else {
|
|
425
|
+
params.set(key, value)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return params.toString()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatViteImportMap(
|
|
434
|
+
map: ViteImportMap,
|
|
435
|
+
dir: string,
|
|
436
|
+
baseUrl: string
|
|
437
|
+
): FormattedViteImportMap {
|
|
438
|
+
return Object.keys(map).reduce<FormattedViteImportMap>((acc, key) => {
|
|
439
|
+
const dirIndex = key.indexOf(dir)
|
|
440
|
+
if (dirIndex === -1) {
|
|
441
|
+
return acc
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let specificity = 0
|
|
445
|
+
let k = key.slice(dirIndex + dir.length)
|
|
446
|
+
while (k.startsWith("/")) {
|
|
447
|
+
k = k.slice(1)
|
|
448
|
+
}
|
|
449
|
+
const segments: string[] = []
|
|
450
|
+
const parts = k.split("/").slice(0, -1)
|
|
451
|
+
|
|
452
|
+
for (let i = 0; i < parts.length; i++) {
|
|
453
|
+
const part = parts[i]
|
|
454
|
+
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
455
|
+
if (i !== parts.length - 1) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`[kiru/router]: Catchall must be the folder name. Got "${key}"`
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
segments.push(`:${part.slice(4, -1)}*`)
|
|
461
|
+
specificity += 1
|
|
462
|
+
break
|
|
463
|
+
}
|
|
464
|
+
if (part.startsWith("[") && part.endsWith("]")) {
|
|
465
|
+
segments.push(`:${part.slice(1, -1)}`)
|
|
466
|
+
specificity += 10
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
specificity += 100
|
|
470
|
+
segments.push(part)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const value: FormattedViteImportMap[string] = {
|
|
474
|
+
load: map[key],
|
|
475
|
+
specificity,
|
|
476
|
+
segments,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (__DEV__) {
|
|
480
|
+
value.filePath = key
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
...acc,
|
|
485
|
+
[baseUrl + segments.join("/")]: value,
|
|
486
|
+
}
|
|
487
|
+
}, {})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function normalizePrefixPath(path: string) {
|
|
491
|
+
while (path.startsWith(".")) {
|
|
492
|
+
path = path.slice(1)
|
|
493
|
+
}
|
|
494
|
+
path = `/${path}/`
|
|
495
|
+
while (path.startsWith("//")) {
|
|
496
|
+
path = path.slice(1)
|
|
497
|
+
}
|
|
498
|
+
while (path.endsWith("//")) {
|
|
499
|
+
path = path.slice(0, -1)
|
|
500
|
+
}
|
|
501
|
+
return path
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function handleStateTransition(
|
|
505
|
+
signal: AbortSignal,
|
|
506
|
+
enableTransition: boolean,
|
|
507
|
+
callback: () => void
|
|
508
|
+
) {
|
|
509
|
+
if (!enableTransition || typeof document.startViewTransition !== "function") {
|
|
510
|
+
return callback()
|
|
511
|
+
}
|
|
512
|
+
const vt = document.startViewTransition(() => {
|
|
513
|
+
callback()
|
|
514
|
+
flushSync()
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
signal.addEventListener("abort", () => vt.skipTransition())
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function validateRoutes(pageMap: FormattedViteImportMap) {
|
|
521
|
+
type Entry = FormattedViteImportMap[string]
|
|
522
|
+
const routeConflicts: [Entry, Entry][] = []
|
|
523
|
+
const routes = Object.keys(pageMap)
|
|
524
|
+
for (let i = 0; i < routes.length; i++) {
|
|
525
|
+
for (let j = i + 1; j < routes.length; j++) {
|
|
526
|
+
const route1 = routes[i]
|
|
527
|
+
const route2 = routes[j]
|
|
528
|
+
|
|
529
|
+
if (routesConflict(route1, route2)) {
|
|
530
|
+
routeConflicts.push([pageMap[route1], pageMap[route2]])
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (routeConflicts.length > 0) {
|
|
536
|
+
let warning = "[kiru/router]: Route conflicts detected:\n"
|
|
537
|
+
warning += routeConflicts.map(([route1, route2]) => {
|
|
538
|
+
return ` - "${route1.filePath}" conflicts with "${route2.filePath}"\n`
|
|
539
|
+
})
|
|
540
|
+
warning += "Routes are ordered by specificity (higher specificity wins)"
|
|
541
|
+
console.warn(warning)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function routesConflict(route1: string, route2: string): boolean {
|
|
546
|
+
const segments1 = route1.split("/").filter(Boolean)
|
|
547
|
+
const segments2 = route2.split("/").filter(Boolean)
|
|
548
|
+
|
|
549
|
+
// Filter out route groups for comparison
|
|
550
|
+
const pathSegments1 = segments1.filter(
|
|
551
|
+
(seg) => !seg.startsWith("(") && !seg.endsWith(")")
|
|
552
|
+
)
|
|
553
|
+
const pathSegments2 = segments2.filter(
|
|
554
|
+
(seg) => !seg.startsWith("(") && !seg.endsWith(")")
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
// Routes conflict if they have the same path structure
|
|
558
|
+
if (pathSegments1.length !== pathSegments2.length) {
|
|
559
|
+
return false
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (let i = 0; i < pathSegments1.length; i++) {
|
|
563
|
+
const seg1 = pathSegments1[i]
|
|
564
|
+
const seg2 = pathSegments2[i]
|
|
565
|
+
|
|
566
|
+
// If both are static segments, they must match exactly
|
|
567
|
+
if (!seg1.startsWith(":") && !seg2.startsWith(":")) {
|
|
568
|
+
if (seg1 !== seg2) {
|
|
569
|
+
return false
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// If one is static and one is dynamic, they conflict
|
|
573
|
+
else if (
|
|
574
|
+
(seg1.startsWith(":") && !seg2.startsWith(":")) ||
|
|
575
|
+
(!seg1.startsWith(":") && seg2.startsWith(":"))
|
|
576
|
+
) {
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
// If both are dynamic, they conflict
|
|
580
|
+
else if (seg1.startsWith(":") && seg2.startsWith(":")) {
|
|
581
|
+
// Both are dynamic, check if they're the same type
|
|
582
|
+
const isCatchall1 = seg1.endsWith("*")
|
|
583
|
+
const isCatchall2 = seg2.endsWith("*")
|
|
584
|
+
if (isCatchall1 !== isCatchall2) {
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return true
|
|
591
|
+
}
|
package/src/router/globals.ts
CHANGED
package/src/router/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { useFileRouter, type FileRouterContextType } from "./context.js"
|
|
2
2
|
export * from "./errors.js"
|
|
3
3
|
export { FileRouter, type FileRouterProps } from "./fileRouter.js"
|
|
4
|
-
export { useFileRouter } from "./context.js"
|
|
5
4
|
export * from "./link.js"
|
|
6
|
-
export * from "./
|
|
5
|
+
export * from "./pageConfig.js"
|
|
6
|
+
export type * from "./types.js"
|
package/src/scheduler.ts
CHANGED
|
@@ -112,7 +112,7 @@ function queueUpdate(vNode: VNode) {
|
|
|
112
112
|
// If this node is currently being rendered, just mark it dirty
|
|
113
113
|
if (node.current === vNode) {
|
|
114
114
|
if (__DEV__) {
|
|
115
|
-
window.__kiru
|
|
115
|
+
window.__kiru.profilingContext?.emit("updateDirtied", appCtx!)
|
|
116
116
|
}
|
|
117
117
|
isRenderDirtied = true
|
|
118
118
|
return
|
|
@@ -143,7 +143,7 @@ function doWork(): void {
|
|
|
143
143
|
const n = deletions[0] ?? treesInProgress[0]
|
|
144
144
|
if (n) {
|
|
145
145
|
appCtx = getVNodeAppContext(n)!
|
|
146
|
-
window.__kiru
|
|
146
|
+
window.__kiru.profilingContext?.beginTick(appCtx)
|
|
147
147
|
} else {
|
|
148
148
|
appCtx = null
|
|
149
149
|
}
|
|
@@ -186,8 +186,8 @@ function doWork(): void {
|
|
|
186
186
|
immediateEffectDirtiedRender = false
|
|
187
187
|
consecutiveDirtyCount++
|
|
188
188
|
if (__DEV__) {
|
|
189
|
-
window.__kiru
|
|
190
|
-
window.__kiru
|
|
189
|
+
window.__kiru.profilingContext?.endTick(appCtx!)
|
|
190
|
+
window.__kiru.profilingContext?.emit("updateDirtied", appCtx!)
|
|
191
191
|
}
|
|
192
192
|
return flushSync()
|
|
193
193
|
}
|
|
@@ -196,9 +196,9 @@ function doWork(): void {
|
|
|
196
196
|
onWorkFinished()
|
|
197
197
|
flushEffects(postEffects)
|
|
198
198
|
if (__DEV__) {
|
|
199
|
-
window.__kiru
|
|
200
|
-
window.__kiru
|
|
201
|
-
window.__kiru
|
|
199
|
+
window.__kiru.emit("update", appCtx!)
|
|
200
|
+
window.__kiru.profilingContext?.emit("update", appCtx!)
|
|
201
|
+
window.__kiru.profilingContext?.endTick(appCtx!)
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -214,7 +214,7 @@ function performUnitOfWork(vNode: VNode): VNode | void {
|
|
|
214
214
|
}
|
|
215
215
|
} catch (error) {
|
|
216
216
|
if (__DEV__) {
|
|
217
|
-
window.__kiru
|
|
217
|
+
window.__kiru.emit(
|
|
218
218
|
"error",
|
|
219
219
|
appCtx!,
|
|
220
220
|
error instanceof Error ? error : new Error(String(error))
|
package/src/signals/watch.ts
CHANGED
|
@@ -41,7 +41,7 @@ export class WatchEffect<const Deps extends readonly Signal<unknown>[] = []> {
|
|
|
41
41
|
},
|
|
42
42
|
}
|
|
43
43
|
if ("window" in globalThis) {
|
|
44
|
-
const signals = window.__kiru
|
|
44
|
+
const signals = window.__kiru.HMRContext!.signals
|
|
45
45
|
if (signals.isWaitingForNextWatchCall()) {
|
|
46
46
|
signals.pushWatch(this as WatchEffect)
|
|
47
47
|
}
|
|
@@ -59,10 +59,7 @@ export class WatchEffect<const Deps extends readonly Signal<unknown>[] = []> {
|
|
|
59
59
|
|
|
60
60
|
if (__DEV__) {
|
|
61
61
|
// postpone execution during HMR
|
|
62
|
-
if (
|
|
63
|
-
"window" in globalThis &&
|
|
64
|
-
window.__kiru?.HMRContext?.isReplacement()
|
|
65
|
-
) {
|
|
62
|
+
if ("window" in globalThis && window.__kiru.HMRContext?.isReplacement()) {
|
|
66
63
|
return queueMicrotask(() => {
|
|
67
64
|
if (this.isRunning) {
|
|
68
65
|
WatchEffect.run(this as WatchEffect)
|
package/src/swr.ts
CHANGED
|
@@ -95,7 +95,7 @@ const SWRGlobalState = {
|
|
|
95
95
|
|
|
96
96
|
if ("window" in globalThis) {
|
|
97
97
|
if (__DEV__) {
|
|
98
|
-
SWRGlobalState.cache = window.__kiru
|
|
98
|
+
SWRGlobalState.cache = window.__kiru.SWRGlobalCache ??= new Map()
|
|
99
99
|
} else {
|
|
100
100
|
SWRGlobalState.cache = new Map()
|
|
101
101
|
}
|
package/src/types.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/router/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAUnE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/router/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAGjD,MAAM,UAAU,gBAAgB,CAAuB,MAAS;IAC9D,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAA;QAChE,MAAM,UAAU,GAAG,kBAAkB,CAAC,OAAO,CAAA;QAC7C,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;YAC3B,UAAU,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QAClD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
File without changes
|