kiru 0.49.2 → 0.50.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/errorBoundary.d.ts +7 -0
- package/dist/components/errorBoundary.d.ts.map +1 -0
- package/dist/components/errorBoundary.js +6 -0
- package/dist/components/errorBoundary.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/lazy.d.ts.map +1 -1
- package/dist/components/lazy.js +11 -136
- package/dist/components/lazy.js.map +1 -1
- package/dist/components/suspense.d.ts +34 -0
- package/dist/components/suspense.d.ts.map +1 -0
- package/dist/components/suspense.js +110 -0
- package/dist/components/suspense.js.map +1 -0
- package/dist/constants.d.ts +4 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -2
- package/dist/constants.js.map +1 -1
- package/dist/hmr.d.ts +1 -0
- package/dist/hmr.d.ts.map +1 -1
- package/dist/hmr.js +7 -0
- package/dist/hmr.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/recursiveRender.d.ts +6 -0
- package/dist/recursiveRender.d.ts.map +1 -0
- package/dist/recursiveRender.js +109 -0
- package/dist/recursiveRender.js.map +1 -0
- package/dist/renderToString.d.ts.map +1 -1
- package/dist/renderToString.js +10 -59
- package/dist/renderToString.js.map +1 -1
- package/dist/router/config.d.ts +3 -0
- package/dist/router/config.d.ts.map +1 -0
- package/dist/router/config.js +13 -0
- package/dist/router/config.js.map +1 -0
- package/dist/router/context.d.ts +15 -0
- package/dist/router/context.d.ts.map +1 -0
- package/dist/router/context.js +11 -0
- package/dist/router/context.js.map +1 -0
- package/dist/router/errors.d.ts +4 -0
- package/dist/router/errors.d.ts.map +1 -0
- package/dist/router/errors.js +7 -0
- package/dist/router/errors.js.map +1 -0
- package/dist/router/fileRouter.d.ts +48 -0
- package/dist/router/fileRouter.d.ts.map +1 -0
- package/dist/router/fileRouter.js +311 -0
- package/dist/router/fileRouter.js.map +1 -0
- package/dist/router/globals.d.ts +5 -0
- package/dist/router/globals.d.ts.map +1 -0
- package/dist/router/globals.js +4 -0
- package/dist/router/globals.js.map +1 -0
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +7 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/link.d.ts +8 -0
- package/dist/router/link.d.ts.map +1 -0
- package/dist/router/link.js +15 -0
- package/dist/router/link.js.map +1 -0
- package/dist/router/types.d.ts +63 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.internal.d.ts +12 -0
- package/dist/router/types.internal.d.ts.map +1 -0
- package/dist/router/types.internal.js +2 -0
- package/dist/router/types.internal.js.map +1 -0
- package/dist/router/types.js +2 -0
- package/dist/router/types.js.map +1 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +35 -10
- package/dist/scheduler.js.map +1 -1
- package/dist/ssr/server.d.ts +4 -1
- package/dist/ssr/server.d.ts.map +1 -1
- package/dist/ssr/server.js +50 -82
- package/dist/ssr/server.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.utils.d.ts +6 -7
- package/dist/types.utils.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js.map +1 -1
- package/dist/utils/vdom.d.ts +3 -1
- package/dist/utils/vdom.d.ts.map +1 -1
- package/dist/utils/vdom.js +6 -5
- package/dist/utils/vdom.js.map +1 -1
- package/package.json +4 -8
- package/src/components/errorBoundary.ts +16 -0
- package/src/components/index.ts +2 -0
- package/src/components/lazy.ts +12 -169
- package/src/components/suspense.ts +191 -0
- package/src/constants.ts +6 -2
- package/src/hmr.ts +8 -0
- package/src/index.ts +1 -0
- package/src/recursiveRender.ts +127 -0
- package/src/renderToString.ts +10 -73
- package/src/router/config.ts +15 -0
- package/src/router/context.ts +23 -0
- package/src/router/errors.ts +6 -0
- package/src/router/fileRouter.ts +475 -0
- package/src/router/globals.ts +5 -0
- package/src/router/index.ts +6 -0
- package/src/router/link.ts +32 -0
- package/src/router/types.internal.ts +14 -0
- package/src/router/types.ts +81 -0
- package/src/scheduler.ts +45 -12
- package/src/ssr/server.ts +58 -95
- package/src/types.ts +11 -2
- package/src/types.utils.ts +6 -11
- package/src/utils/runtime.ts +1 -1
- package/src/utils/vdom.ts +11 -4
- package/dist/components/router/index.d.ts +0 -3
- package/dist/components/router/index.d.ts.map +0 -1
- package/dist/components/router/index.js +0 -3
- package/dist/components/router/index.js.map +0 -1
- package/dist/components/router/route.d.ts +0 -46
- package/dist/components/router/route.d.ts.map +0 -1
- package/dist/components/router/route.js +0 -8
- package/dist/components/router/route.js.map +0 -1
- package/dist/components/router/router.d.ts +0 -62
- package/dist/components/router/router.d.ts.map +0 -1
- package/dist/components/router/router.js +0 -181
- package/dist/components/router/router.js.map +0 -1
- package/dist/components/router/routerUtils.d.ts +0 -5
- package/dist/components/router/routerUtils.d.ts.map +0 -1
- package/dist/components/router/routerUtils.js +0 -39
- package/dist/components/router/routerUtils.js.map +0 -1
- package/dist/ssr/hydrationBoundary.d.ts +0 -27
- package/dist/ssr/hydrationBoundary.d.ts.map +0 -1
- package/dist/ssr/hydrationBoundary.js +0 -30
- package/dist/ssr/hydrationBoundary.js.map +0 -1
- package/dist/ssr/index.d.ts +0 -2
- package/dist/ssr/index.d.ts.map +0 -1
- package/dist/ssr/index.js +0 -2
- package/dist/ssr/index.js.map +0 -1
- package/src/components/router/index.ts +0 -2
- package/src/components/router/route.ts +0 -51
- package/src/components/router/router.ts +0 -280
- package/src/components/router/routerUtils.ts +0 -49
- package/src/ssr/hydrationBoundary.ts +0 -63
- package/src/ssr/index.ts +0 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { Signal, computed, flushSync } from "../index.js"
|
|
2
|
+
import { __DEV__ } from "../env.js"
|
|
3
|
+
import { createElement } from "../element.js"
|
|
4
|
+
import { useState, useEffect } from "../hooks/index.js"
|
|
5
|
+
import { RouterContext, type FileRouterContextType } from "./context.js"
|
|
6
|
+
import { FileRouterDataLoadError } from "./errors.js"
|
|
7
|
+
import { fileRouterInstance } from "./globals.js"
|
|
8
|
+
import type {
|
|
9
|
+
ErrorPageProps,
|
|
10
|
+
FileRouterConfig,
|
|
11
|
+
PageConfig,
|
|
12
|
+
PageProps,
|
|
13
|
+
RouteQuery,
|
|
14
|
+
RouterState,
|
|
15
|
+
} from "./types.js"
|
|
16
|
+
import type {
|
|
17
|
+
DefaultComponentModule,
|
|
18
|
+
PageModule,
|
|
19
|
+
ViteImportMap,
|
|
20
|
+
} from "./types.internal.js"
|
|
21
|
+
|
|
22
|
+
export class FileRouterController {
|
|
23
|
+
private enableTransitions: boolean
|
|
24
|
+
private pages: ViteImportMap
|
|
25
|
+
private layouts: ViteImportMap
|
|
26
|
+
private abortController: AbortController
|
|
27
|
+
private currentPage: Signal<{
|
|
28
|
+
component: Kiru.FC<any>
|
|
29
|
+
config?: PageConfig
|
|
30
|
+
route: string
|
|
31
|
+
} | null>
|
|
32
|
+
private currentPageProps: Signal<PageProps<PageConfig>>
|
|
33
|
+
private currentLayouts: Signal<Kiru.FC[]>
|
|
34
|
+
private loading: Signal<boolean>
|
|
35
|
+
private state: Signal<RouterState>
|
|
36
|
+
private contextValue: Signal<FileRouterContextType>
|
|
37
|
+
private cleanups: (() => void)[] = []
|
|
38
|
+
private filePathToPageRoute: Map<
|
|
39
|
+
string,
|
|
40
|
+
{
|
|
41
|
+
route: string
|
|
42
|
+
config: PageConfig
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
private pageRouteToConfig: Map<string, PageConfig>
|
|
46
|
+
private currentRoute: string | null
|
|
47
|
+
|
|
48
|
+
constructor(props: FileRouterProps) {
|
|
49
|
+
fileRouterInstance.current = this
|
|
50
|
+
this.pages = {}
|
|
51
|
+
this.layouts = {}
|
|
52
|
+
this.abortController = new AbortController()
|
|
53
|
+
this.currentPage = new Signal(null)
|
|
54
|
+
this.currentPageProps = new Signal({})
|
|
55
|
+
this.currentLayouts = new Signal([])
|
|
56
|
+
this.loading = new Signal(true)
|
|
57
|
+
this.state = new Signal<RouterState>({
|
|
58
|
+
path: window.location.pathname,
|
|
59
|
+
params: {},
|
|
60
|
+
query: {},
|
|
61
|
+
signal: this.abortController.signal,
|
|
62
|
+
})
|
|
63
|
+
this.contextValue = computed<FileRouterContextType>(() => ({
|
|
64
|
+
state: this.state.value,
|
|
65
|
+
navigate: this.navigate.bind(this),
|
|
66
|
+
setQuery: this.setQuery.bind(this),
|
|
67
|
+
reload: (options?: { transition?: boolean }) =>
|
|
68
|
+
this.loadRoute(void 0, void 0, options?.transition),
|
|
69
|
+
}))
|
|
70
|
+
this.filePathToPageRoute = new Map()
|
|
71
|
+
this.pageRouteToConfig = new Map()
|
|
72
|
+
this.currentRoute = null
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
pages,
|
|
76
|
+
layouts,
|
|
77
|
+
dir = "/pages",
|
|
78
|
+
baseUrl = "/",
|
|
79
|
+
transition,
|
|
80
|
+
} = props.config
|
|
81
|
+
this.enableTransitions = !!transition
|
|
82
|
+
const [normalizedDir, normalizedBaseUrl] = [
|
|
83
|
+
normalizePrefixPath(dir),
|
|
84
|
+
normalizePrefixPath(baseUrl),
|
|
85
|
+
]
|
|
86
|
+
debugger
|
|
87
|
+
this.pages = formatViteImportMap(
|
|
88
|
+
pages as ViteImportMap,
|
|
89
|
+
normalizedDir,
|
|
90
|
+
normalizedBaseUrl
|
|
91
|
+
)
|
|
92
|
+
this.layouts = formatViteImportMap(
|
|
93
|
+
layouts as ViteImportMap,
|
|
94
|
+
normalizedDir,
|
|
95
|
+
normalizedBaseUrl
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
this.loadRoute()
|
|
99
|
+
|
|
100
|
+
const handlePopState = () => this.loadRoute()
|
|
101
|
+
window.addEventListener("popstate", handlePopState)
|
|
102
|
+
this.cleanups.push(() =>
|
|
103
|
+
window.removeEventListener("popstate", handlePopState)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public onPageConfigDefined<T extends PageConfig>(fp: string, config: T) {
|
|
108
|
+
const existing = this.filePathToPageRoute.get(fp)
|
|
109
|
+
if (existing === undefined) {
|
|
110
|
+
const route = this.currentRoute
|
|
111
|
+
if (!route) return
|
|
112
|
+
this.filePathToPageRoute.set(fp, { route, config })
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const curPage = this.currentPage.value
|
|
116
|
+
if (curPage?.route === existing.route && config.loader) {
|
|
117
|
+
const p = this.currentPageProps.value
|
|
118
|
+
let transition = this.enableTransitions
|
|
119
|
+
if (config.loader.transition !== undefined) {
|
|
120
|
+
transition = config.loader.transition
|
|
121
|
+
}
|
|
122
|
+
const props = {
|
|
123
|
+
...p,
|
|
124
|
+
loading: true,
|
|
125
|
+
data: null,
|
|
126
|
+
error: null,
|
|
127
|
+
}
|
|
128
|
+
handleStateTransition(this.state.value.signal, transition, () => {
|
|
129
|
+
this.currentPageProps.value = props
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
this.loadRouteData(config.loader, props, this.state.value, transition)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.pageRouteToConfig.set(existing.route, config)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public getContextValue() {
|
|
139
|
+
return this.contextValue.value
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public getChildren() {
|
|
143
|
+
const page = this.currentPage.value,
|
|
144
|
+
props = this.currentPageProps.value,
|
|
145
|
+
layouts = this.currentLayouts.value
|
|
146
|
+
|
|
147
|
+
if (page) {
|
|
148
|
+
// Wrap component with layouts (outermost first)
|
|
149
|
+
return layouts.reduceRight(
|
|
150
|
+
(children, Layout) => createElement(Layout, { children }),
|
|
151
|
+
createElement(page.component, props)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public dispose() {
|
|
159
|
+
this.cleanups.forEach((cleanup) => cleanup())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private matchRoute(pathSegments: string[]) {
|
|
163
|
+
outer: for (const [route, pageModuleLoader] of Object.entries(this.pages)) {
|
|
164
|
+
const routeSegments = route.split("/").filter(Boolean)
|
|
165
|
+
|
|
166
|
+
const pathMatchingSegments = routeSegments.filter(
|
|
167
|
+
(seg) => !seg.startsWith("(") && !seg.endsWith(")")
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (pathMatchingSegments.length !== pathSegments.length) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
const params: Record<string, string> = {}
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < pathMatchingSegments.length; i++) {
|
|
176
|
+
const routeSeg = pathMatchingSegments[i]
|
|
177
|
+
if (routeSeg.startsWith(":")) {
|
|
178
|
+
const key = routeSeg.slice(1)
|
|
179
|
+
params[key] = pathSegments[i]
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
if (routeSeg !== pathSegments[i]) {
|
|
183
|
+
continue outer
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { route, pageModuleLoader, params, routeSegments }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async loadRoute(
|
|
194
|
+
path: string = window.location.pathname,
|
|
195
|
+
props: PageProps<PageConfig> = {},
|
|
196
|
+
enableTransition = this.enableTransitions
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
this.loading.value = true
|
|
199
|
+
this.abortController?.abort()
|
|
200
|
+
|
|
201
|
+
const query = parseQuery(window.location.search)
|
|
202
|
+
const controller = (this.abortController = new AbortController())
|
|
203
|
+
const signal = controller.signal
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const pathSegments = path.split("/").filter(Boolean)
|
|
207
|
+
const routeMatch = this.matchRoute(pathSegments)
|
|
208
|
+
|
|
209
|
+
if (!routeMatch) {
|
|
210
|
+
const _404 = this.matchRoute(["404"])
|
|
211
|
+
if (!_404) {
|
|
212
|
+
if (__DEV__) {
|
|
213
|
+
console.error(
|
|
214
|
+
`No 404 route defined (path: ${path}). See https://kirujs.dev/404 for more information.`
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
const errorProps = {
|
|
220
|
+
source: { path },
|
|
221
|
+
} satisfies ErrorPageProps
|
|
222
|
+
|
|
223
|
+
return this.navigate("/404", { replace: true, props: errorProps })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { route, pageModuleLoader, params, routeSegments } = routeMatch
|
|
227
|
+
|
|
228
|
+
this.currentRoute = route
|
|
229
|
+
const pagePromise = pageModuleLoader()
|
|
230
|
+
|
|
231
|
+
const layoutPromises = ["/", ...routeSegments].reduce((acc, _, i) => {
|
|
232
|
+
const layoutPath = "/" + routeSegments.slice(0, i).join("/")
|
|
233
|
+
const layoutLoad = this.layouts[layoutPath]
|
|
234
|
+
|
|
235
|
+
if (!layoutLoad) {
|
|
236
|
+
return acc
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return [...acc, layoutLoad()]
|
|
240
|
+
}, [] as Promise<DefaultComponentModule>[])
|
|
241
|
+
|
|
242
|
+
const [page, ...layouts] = await Promise.all([
|
|
243
|
+
pagePromise,
|
|
244
|
+
...layoutPromises,
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
this.currentRoute = null
|
|
248
|
+
if (controller.signal.aborted) return
|
|
249
|
+
|
|
250
|
+
if (typeof page.default !== "function") {
|
|
251
|
+
throw new Error("Route component must be a default exported function")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const routerState: RouterState = {
|
|
255
|
+
path,
|
|
256
|
+
params,
|
|
257
|
+
query,
|
|
258
|
+
signal,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let config = (page as unknown as PageModule).config
|
|
262
|
+
if (this.pageRouteToConfig.has(route)) {
|
|
263
|
+
config = this.pageRouteToConfig.get(route)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (config?.loader) {
|
|
267
|
+
props = { ...props, loading: true, data: null, error: null }
|
|
268
|
+
this.loadRouteData(config.loader, props, routerState, enableTransition)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
handleStateTransition(signal, enableTransition, () => {
|
|
272
|
+
this.currentPage.value = {
|
|
273
|
+
component: page.default,
|
|
274
|
+
config,
|
|
275
|
+
route: "/" + routeSegments.join("/"),
|
|
276
|
+
}
|
|
277
|
+
this.state.value = routerState
|
|
278
|
+
this.currentPageProps.value = props
|
|
279
|
+
this.currentLayouts.value = layouts
|
|
280
|
+
.filter((m) => typeof m.default === "function")
|
|
281
|
+
.map((m) => m.default)
|
|
282
|
+
})
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("Failed to load route component:", error)
|
|
285
|
+
this.currentPage.value = null
|
|
286
|
+
} finally {
|
|
287
|
+
this.loading.value = false
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async loadRouteData(
|
|
292
|
+
loader: NonNullable<PageConfig["loader"]>,
|
|
293
|
+
props: PageProps<PageConfig>,
|
|
294
|
+
routerState: RouterState,
|
|
295
|
+
enableTransition = this.enableTransitions
|
|
296
|
+
) {
|
|
297
|
+
loader
|
|
298
|
+
.load(routerState)
|
|
299
|
+
.then(
|
|
300
|
+
(data) => ({ data, error: null }),
|
|
301
|
+
(error) => ({
|
|
302
|
+
data: null,
|
|
303
|
+
error: new FileRouterDataLoadError(error),
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
.then(({ data, error }) => {
|
|
307
|
+
if (routerState.signal.aborted) return
|
|
308
|
+
|
|
309
|
+
let transition = enableTransition
|
|
310
|
+
if (loader.transition !== undefined) {
|
|
311
|
+
transition = loader.transition
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
handleStateTransition(routerState.signal, transition, () => {
|
|
315
|
+
this.currentPageProps.value = {
|
|
316
|
+
...props,
|
|
317
|
+
loading: false,
|
|
318
|
+
data,
|
|
319
|
+
error,
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async navigate(
|
|
326
|
+
path: string,
|
|
327
|
+
options?: {
|
|
328
|
+
replace?: boolean
|
|
329
|
+
transition?: boolean
|
|
330
|
+
props?: Record<string, unknown>
|
|
331
|
+
}
|
|
332
|
+
) {
|
|
333
|
+
const f = options?.replace ? "replaceState" : "pushState"
|
|
334
|
+
window.history[f]({}, "", path)
|
|
335
|
+
window.dispatchEvent(new PopStateEvent("popstate", { state: {} }))
|
|
336
|
+
return this.loadRoute(path, options?.props, options?.transition)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private setQuery(query: RouteQuery) {
|
|
340
|
+
const queryString = buildQueryString(query)
|
|
341
|
+
const newUrl = `${this.state.value.path}${
|
|
342
|
+
queryString ? `?${queryString}` : ""
|
|
343
|
+
}`
|
|
344
|
+
window.history.pushState(null, "", newUrl)
|
|
345
|
+
this.state.value = { ...this.state.value, query }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface FileRouterProps {
|
|
350
|
+
/**
|
|
351
|
+
* The router configuration
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
*<FileRouter
|
|
355
|
+
config={{
|
|
356
|
+
dir: "/fbr-app", // optional, defaults to "/pages"
|
|
357
|
+
baseUrl: "/app", // optional, defaults to "/"
|
|
358
|
+
pages: import.meta.glob("/∗∗/index.tsx"),
|
|
359
|
+
layouts: import.meta.glob("/∗∗/layout.tsx"),
|
|
360
|
+
transition: true
|
|
361
|
+
}}
|
|
362
|
+
/>
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
config: FileRouterConfig
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function FileRouter(props: FileRouterProps): JSX.Element {
|
|
369
|
+
const [controller] = useState(() => new FileRouterController(props))
|
|
370
|
+
useEffect(() => () => controller.dispose(), [controller])
|
|
371
|
+
|
|
372
|
+
return createElement(
|
|
373
|
+
RouterContext.Provider,
|
|
374
|
+
{ value: controller.getContextValue() },
|
|
375
|
+
controller.getChildren()
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Utility functions
|
|
380
|
+
|
|
381
|
+
function parseQuery(
|
|
382
|
+
search: string
|
|
383
|
+
): Record<string, string | string[] | undefined> {
|
|
384
|
+
const params = new URLSearchParams(search)
|
|
385
|
+
const query: Record<string, string | string[] | undefined> = {}
|
|
386
|
+
|
|
387
|
+
for (const [key, value] of params.entries()) {
|
|
388
|
+
if (query[key]) {
|
|
389
|
+
// Convert to array if multiple values
|
|
390
|
+
if (Array.isArray(query[key])) {
|
|
391
|
+
;(query[key] as string[]).push(value)
|
|
392
|
+
} else {
|
|
393
|
+
query[key] = [query[key] as string, value]
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
query[key] = value
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return query
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildQueryString(
|
|
404
|
+
query: Record<string, string | string[] | undefined>
|
|
405
|
+
): string {
|
|
406
|
+
const params = new URLSearchParams()
|
|
407
|
+
|
|
408
|
+
for (const [key, value] of Object.entries(query)) {
|
|
409
|
+
if (value !== undefined) {
|
|
410
|
+
if (Array.isArray(value)) {
|
|
411
|
+
value.forEach((v) => params.append(key, v))
|
|
412
|
+
} else {
|
|
413
|
+
params.set(key, value)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return params.toString()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function formatViteImportMap(
|
|
422
|
+
map: ViteImportMap,
|
|
423
|
+
dir: string,
|
|
424
|
+
baseUrl: string
|
|
425
|
+
): ViteImportMap {
|
|
426
|
+
return Object.keys(map).reduce((acc, key) => {
|
|
427
|
+
let k = key
|
|
428
|
+
const dirIndex = k.indexOf(dir)
|
|
429
|
+
if (dirIndex === -1) {
|
|
430
|
+
return acc
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
k = k.slice(dirIndex + dir.length)
|
|
434
|
+
while (k.startsWith("/")) {
|
|
435
|
+
k = k.slice(1)
|
|
436
|
+
}
|
|
437
|
+
k = k.split("/").slice(0, -1).join("/") // remove filename
|
|
438
|
+
k = k.replace(/\[([^\]]+)\]/g, ":$1") // replace [param] with :param
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
...acc,
|
|
442
|
+
[baseUrl + k]: map[key],
|
|
443
|
+
}
|
|
444
|
+
}, {})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function normalizePrefixPath(path: string) {
|
|
448
|
+
while (path.startsWith(".")) {
|
|
449
|
+
path = path.slice(1)
|
|
450
|
+
}
|
|
451
|
+
path = `/${path}/`
|
|
452
|
+
while (path.startsWith("//")) {
|
|
453
|
+
path = path.slice(1)
|
|
454
|
+
}
|
|
455
|
+
while (path.endsWith("//")) {
|
|
456
|
+
path = path.slice(0, -1)
|
|
457
|
+
}
|
|
458
|
+
return path
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function handleStateTransition(
|
|
462
|
+
signal: AbortSignal,
|
|
463
|
+
enableTransition: boolean,
|
|
464
|
+
callback: () => void
|
|
465
|
+
) {
|
|
466
|
+
if (!enableTransition || typeof document.startViewTransition !== "function") {
|
|
467
|
+
return callback()
|
|
468
|
+
}
|
|
469
|
+
const vt = document.startViewTransition(() => {
|
|
470
|
+
callback()
|
|
471
|
+
flushSync()
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
signal.addEventListener("abort", () => vt.skipTransition())
|
|
475
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ElementProps } from "../types"
|
|
2
|
+
import { createElement } from "../element.js"
|
|
3
|
+
import { useCallback } from "../hooks/index.js"
|
|
4
|
+
import { useFileRouter } from "./context.js"
|
|
5
|
+
|
|
6
|
+
export interface LinkProps extends ElementProps<"a"> {
|
|
7
|
+
to: string
|
|
8
|
+
replace?: boolean
|
|
9
|
+
transition?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Link: Kiru.FC<LinkProps> = ({
|
|
13
|
+
to,
|
|
14
|
+
onclick,
|
|
15
|
+
replace,
|
|
16
|
+
transition,
|
|
17
|
+
...props
|
|
18
|
+
}) => {
|
|
19
|
+
const { navigate } = useFileRouter()
|
|
20
|
+
|
|
21
|
+
const handleClick = useCallback(
|
|
22
|
+
(e: Kiru.MouseEvent<HTMLAnchorElement>) => {
|
|
23
|
+
onclick?.(e)
|
|
24
|
+
if (e.defaultPrevented) return
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
navigate(to, { replace, transition })
|
|
27
|
+
},
|
|
28
|
+
[to, navigate, onclick, replace]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return createElement("a", { href: to, onclick: handleClick, ...props })
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PageConfig } from "./types"
|
|
2
|
+
|
|
3
|
+
export interface DefaultComponentModule {
|
|
4
|
+
default: Kiru.FC
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PageModule {
|
|
8
|
+
default: DefaultComponentModule
|
|
9
|
+
config?: PageConfig
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ViteImportMap {
|
|
13
|
+
[fp: string]: () => Promise<DefaultComponentModule>
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AsyncTaskState } from "../types.utils"
|
|
2
|
+
import { FileRouterDataLoadError } from "./errors"
|
|
3
|
+
|
|
4
|
+
export interface FileRouterConfig {
|
|
5
|
+
/**
|
|
6
|
+
* The directory to load routes from
|
|
7
|
+
* @default "/pages"
|
|
8
|
+
*/
|
|
9
|
+
dir?: string
|
|
10
|
+
/**
|
|
11
|
+
* The import map to use for loading pages
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <FileRouter config={{ pages: import.meta.glob("/∗∗/index.tsx"), ... }} />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
pages: Record<string, unknown>
|
|
18
|
+
/**
|
|
19
|
+
* The import map to use for loading layouts
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <FileRouter config={{ pages: import.meta.glob("/∗∗/layout.tsx"), ... }} />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
layouts: Record<string, unknown>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The base url to use as a prefix for route matching
|
|
29
|
+
* @default "/"
|
|
30
|
+
*/
|
|
31
|
+
baseUrl?: string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enable transitions for all routes and loading states
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
transition?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RouteParams {
|
|
41
|
+
[key: string]: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RouteQuery {
|
|
45
|
+
[key: string]: string | string[] | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RouterState {
|
|
49
|
+
path: string
|
|
50
|
+
params: RouteParams
|
|
51
|
+
query: RouteQuery
|
|
52
|
+
signal: AbortSignal
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type PageDataLoaderContext = RouterState & {}
|
|
56
|
+
|
|
57
|
+
export interface PageDataLoaderConfig<T = unknown> {
|
|
58
|
+
load: (context: PageDataLoaderContext) => Promise<T>
|
|
59
|
+
transition?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PageConfig {
|
|
63
|
+
loader?: PageDataLoaderConfig
|
|
64
|
+
// title?: string
|
|
65
|
+
// description?: string
|
|
66
|
+
// meta?: Record<string, string>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type PageProps<T extends PageConfig> =
|
|
70
|
+
T["loader"] extends PageDataLoaderConfig
|
|
71
|
+
? AsyncTaskState<
|
|
72
|
+
Awaited<ReturnType<T["loader"]["load"]>>,
|
|
73
|
+
FileRouterDataLoadError
|
|
74
|
+
>
|
|
75
|
+
: {}
|
|
76
|
+
|
|
77
|
+
export interface ErrorPageProps {
|
|
78
|
+
source?: {
|
|
79
|
+
path: string
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/scheduler.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ContextProviderNode,
|
|
3
3
|
DomVNode,
|
|
4
|
+
ErrorBoundaryNode,
|
|
4
5
|
FunctionVNode,
|
|
5
6
|
} from "./types.utils"
|
|
6
7
|
import {
|
|
7
8
|
$CONTEXT_PROVIDER,
|
|
9
|
+
$ERROR_BOUNDARY,
|
|
8
10
|
CONSECUTIVE_DIRTY_LIMIT,
|
|
9
11
|
FLAG_DELETION,
|
|
10
12
|
FLAG_DIRTY,
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
traverseApply,
|
|
31
33
|
isExoticType,
|
|
32
34
|
getVNodeAppContext,
|
|
35
|
+
findParentErrorBoundary,
|
|
33
36
|
} from "./utils/index.js"
|
|
34
37
|
import type { AppContext } from "./appContext"
|
|
35
38
|
|
|
@@ -202,21 +205,10 @@ function doWork(): void {
|
|
|
202
205
|
function performUnitOfWork(vNode: VNode): VNode | void {
|
|
203
206
|
let renderChild = true
|
|
204
207
|
try {
|
|
205
|
-
const { props } = vNode
|
|
206
208
|
if (typeof vNode.type === "string") {
|
|
207
209
|
updateHostComponent(vNode as DomVNode)
|
|
208
210
|
} else if (isExoticType(vNode.type)) {
|
|
209
|
-
|
|
210
|
-
const {
|
|
211
|
-
props: { dependents, value },
|
|
212
|
-
prev,
|
|
213
|
-
} = vNode as ContextProviderNode<unknown>
|
|
214
|
-
|
|
215
|
-
if (dependents.size && prev && prev.props.value !== value) {
|
|
216
|
-
dependents.forEach(queueUpdate)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
vNode.child = reconcileChildren(vNode, props.children)
|
|
211
|
+
updateExoticComponent(vNode)
|
|
220
212
|
} else {
|
|
221
213
|
renderChild = updateFunctionComponent(vNode as FunctionVNode)
|
|
222
214
|
}
|
|
@@ -229,6 +221,18 @@ function performUnitOfWork(vNode: VNode): VNode | void {
|
|
|
229
221
|
)
|
|
230
222
|
}
|
|
231
223
|
|
|
224
|
+
const handler = findParentErrorBoundary(vNode)
|
|
225
|
+
if (handler) {
|
|
226
|
+
const e = (handler.error =
|
|
227
|
+
error instanceof Error ? error : new Error(String(error)))
|
|
228
|
+
|
|
229
|
+
handler.props.onError?.(e)
|
|
230
|
+
if (handler.depth < currentWorkRoot!.depth) {
|
|
231
|
+
currentWorkRoot = handler
|
|
232
|
+
}
|
|
233
|
+
return handler
|
|
234
|
+
}
|
|
235
|
+
|
|
232
236
|
if (KiruError.isKiruError(error)) {
|
|
233
237
|
if (error.customNodeStack) {
|
|
234
238
|
setTimeout(() => {
|
|
@@ -279,6 +283,35 @@ function performUnitOfWork(vNode: VNode): VNode | void {
|
|
|
279
283
|
}
|
|
280
284
|
}
|
|
281
285
|
|
|
286
|
+
function updateExoticComponent(vNode: VNode) {
|
|
287
|
+
const { props, type } = vNode
|
|
288
|
+
let children = props.children
|
|
289
|
+
|
|
290
|
+
if (type === $CONTEXT_PROVIDER) {
|
|
291
|
+
const {
|
|
292
|
+
props: { dependents, value },
|
|
293
|
+
prev,
|
|
294
|
+
} = vNode as ContextProviderNode<unknown>
|
|
295
|
+
|
|
296
|
+
if (dependents.size && prev && prev.props.value !== value) {
|
|
297
|
+
dependents.forEach(queueUpdate)
|
|
298
|
+
}
|
|
299
|
+
} else if (type === $ERROR_BOUNDARY) {
|
|
300
|
+
const n = vNode as ErrorBoundaryNode
|
|
301
|
+
const { error } = n
|
|
302
|
+
if (error) {
|
|
303
|
+
children =
|
|
304
|
+
typeof props.fallback === "function"
|
|
305
|
+
? props.fallback(error)
|
|
306
|
+
: props.fallback
|
|
307
|
+
|
|
308
|
+
delete n.error
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
vNode.child = reconcileChildren(vNode, children)
|
|
313
|
+
}
|
|
314
|
+
|
|
282
315
|
function updateFunctionComponent(vNode: FunctionVNode) {
|
|
283
316
|
const { type, props, subs, prev, flags } = vNode
|
|
284
317
|
if (flags & FLAG_MEMO) {
|