kiru 0.51.0-preview.1 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/router/cache.d.ts +71 -0
- package/dist/router/cache.d.ts.map +1 -0
- package/dist/router/cache.js +325 -0
- package/dist/router/cache.js.map +1 -0
- package/dist/router/client/index.d.ts.map +1 -1
- package/dist/router/client/index.js +62 -1
- package/dist/router/client/index.js.map +1 -1
- package/dist/router/context.d.ts +8 -0
- package/dist/router/context.d.ts.map +1 -1
- package/dist/router/context.js.map +1 -1
- package/dist/router/fileRouterController.d.ts +1 -0
- package/dist/router/fileRouterController.d.ts.map +1 -1
- package/dist/router/fileRouterController.js +124 -36
- package/dist/router/fileRouterController.js.map +1 -1
- package/dist/router/globals.d.ts +4 -0
- package/dist/router/globals.d.ts.map +1 -1
- package/dist/router/globals.js +3 -0
- package/dist/router/globals.js.map +1 -1
- package/dist/router/head.d.ts +4 -36
- package/dist/router/head.d.ts.map +1 -1
- package/dist/router/head.js +33 -53
- package/dist/router/head.js.map +1 -1
- package/dist/router/index.d.ts +57 -3
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +67 -3
- package/dist/router/index.js.map +1 -1
- package/dist/router/server/index.d.ts +1 -3
- package/dist/router/server/index.d.ts.map +1 -1
- package/dist/router/server/index.js +23 -20
- package/dist/router/server/index.js.map +1 -1
- package/dist/router/types.d.ts +31 -3
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/types.internal.d.ts +9 -8
- package/dist/router/types.internal.d.ts.map +1 -1
- package/dist/router/utils/index.d.ts +1 -6
- package/dist/router/utils/index.d.ts.map +1 -1
- package/dist/router/utils/index.js +1 -1
- package/dist/router/utils/index.js.map +1 -1
- package/dist/types.dom.d.ts +2 -1
- package/dist/types.dom.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +1 -0
- package/src/router/cache.ts +385 -0
- package/src/router/client/index.ts +81 -1
- package/src/router/context.ts +8 -0
- package/src/router/fileRouterController.ts +140 -47
- package/src/router/globals.ts +5 -0
- package/src/router/head.ts +44 -54
- package/src/router/index.ts +70 -3
- package/src/router/server/index.ts +33 -24
- package/src/router/types.internal.ts +10 -8
- package/src/router/types.ts +43 -13
- package/src/router/utils/index.ts +1 -1
- package/src/types.dom.ts +5 -1
- package/dist/router/dev/index.d.ts +0 -2
- package/dist/router/dev/index.d.ts.map +0 -1
- package/dist/router/dev/index.js +0 -46
- package/dist/router/dev/index.js.map +0 -1
- package/src/router/dev/index.ts +0 -63
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Signal } from "../signals/base.js"
|
|
2
|
-
import { flushSync } from "../scheduler.js"
|
|
2
|
+
import { flushSync, nextIdle } from "../scheduler.js"
|
|
3
3
|
import { __DEV__ } from "../env.js"
|
|
4
4
|
import { type FileRouterContextType } from "./context.js"
|
|
5
5
|
import { FileRouterDataLoadError } from "./errors.js"
|
|
6
|
-
import { fileRouterInstance, fileRouterRoute } from "./globals.js"
|
|
6
|
+
import { fileRouterInstance, fileRouterRoute, routerCache } from "./globals.js"
|
|
7
7
|
import type {
|
|
8
8
|
FileRouterConfig,
|
|
9
9
|
PageConfig,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
parseQuery,
|
|
27
27
|
wrapWithLayouts,
|
|
28
28
|
} from "./utils/index.js"
|
|
29
|
+
import { RouterCache, type CacheKey } from "./cache.js"
|
|
29
30
|
|
|
30
31
|
interface PageConfigWithLoader<T = unknown> extends PageConfig {
|
|
31
32
|
loader: PageDataLoaderConfig<T>
|
|
@@ -34,7 +35,7 @@ interface PageConfigWithLoader<T = unknown> extends PageConfig {
|
|
|
34
35
|
export class FileRouterController {
|
|
35
36
|
public contextValue: FileRouterContextType
|
|
36
37
|
private enableTransitions: boolean
|
|
37
|
-
private pages: FormattedViteImportMap
|
|
38
|
+
private pages: FormattedViteImportMap<PageModule>
|
|
38
39
|
private layouts: FormattedViteImportMap
|
|
39
40
|
private abortController: AbortController
|
|
40
41
|
private currentPage: Signal<{
|
|
@@ -53,6 +54,7 @@ export class FileRouterController {
|
|
|
53
54
|
private pageRouteToConfig?: Map<string, PageConfig>
|
|
54
55
|
|
|
55
56
|
constructor() {
|
|
57
|
+
routerCache.current ??= new RouterCache()
|
|
56
58
|
this.enableTransitions = false
|
|
57
59
|
this.pages = {}
|
|
58
60
|
this.layouts = {}
|
|
@@ -68,6 +70,7 @@ export class FileRouterController {
|
|
|
68
70
|
}
|
|
69
71
|
const __this = this
|
|
70
72
|
this.contextValue = {
|
|
73
|
+
invalidate: this.invalidate.bind(this),
|
|
71
74
|
get state() {
|
|
72
75
|
return __this.state
|
|
73
76
|
},
|
|
@@ -114,11 +117,12 @@ export class FileRouterController {
|
|
|
114
117
|
route,
|
|
115
118
|
params,
|
|
116
119
|
query,
|
|
120
|
+
cacheData,
|
|
117
121
|
} = preloaded
|
|
118
122
|
this.state = {
|
|
119
123
|
params,
|
|
120
124
|
query,
|
|
121
|
-
path:
|
|
125
|
+
path: window.location.pathname,
|
|
122
126
|
signal: this.abortController.signal,
|
|
123
127
|
}
|
|
124
128
|
this.currentPage.value = {
|
|
@@ -139,20 +143,30 @@ export class FileRouterController {
|
|
|
139
143
|
validateRoutes(this.pages)
|
|
140
144
|
}
|
|
141
145
|
const loader = page.config?.loader
|
|
142
|
-
if (
|
|
143
|
-
if (
|
|
146
|
+
if (loader && loader.mode !== "static" && pageProps.loading === true) {
|
|
147
|
+
if (cacheData === null) {
|
|
144
148
|
this.loadRouteData(
|
|
145
149
|
page.config as PageConfigWithLoader,
|
|
146
150
|
pageProps,
|
|
147
151
|
this.state
|
|
148
152
|
)
|
|
153
|
+
} else {
|
|
154
|
+
nextIdle(() => {
|
|
155
|
+
const props = {
|
|
156
|
+
...pageProps,
|
|
157
|
+
data: cacheData.value,
|
|
158
|
+
error: null,
|
|
159
|
+
loading: false,
|
|
160
|
+
}
|
|
161
|
+
let transition = this.enableTransitions
|
|
162
|
+
if (loader.transition !== undefined) {
|
|
163
|
+
transition = loader.transition
|
|
164
|
+
}
|
|
165
|
+
handleStateTransition(this.state.signal, transition, () => {
|
|
166
|
+
this.currentPageProps.value = props
|
|
167
|
+
})
|
|
168
|
+
})
|
|
149
169
|
}
|
|
150
|
-
} else if (loader && loader.mode !== "static") {
|
|
151
|
-
this.loadRouteData(
|
|
152
|
-
page.config as PageConfigWithLoader,
|
|
153
|
-
pageProps,
|
|
154
|
-
this.state
|
|
155
|
-
)
|
|
156
170
|
}
|
|
157
171
|
} else {
|
|
158
172
|
this.pages = formatViteImportMap(
|
|
@@ -182,28 +196,55 @@ export class FileRouterController {
|
|
|
182
196
|
return
|
|
183
197
|
}
|
|
184
198
|
const curPage = this.currentPage.value
|
|
185
|
-
|
|
199
|
+
const loader = config.loader
|
|
200
|
+
if (curPage?.route === existing.route && loader) {
|
|
186
201
|
const p = this.currentPageProps.value
|
|
187
202
|
let transition = this.enableTransitions
|
|
188
|
-
if (
|
|
189
|
-
transition =
|
|
203
|
+
if (loader.mode !== "static" && loader.transition !== undefined) {
|
|
204
|
+
transition = loader.transition
|
|
190
205
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
206
|
+
|
|
207
|
+
// Check cache first if caching is enabled
|
|
208
|
+
let cachedData = null
|
|
209
|
+
if (loader.mode !== "static" && loader.cache) {
|
|
210
|
+
const cacheKey: CacheKey = {
|
|
211
|
+
path: this.state.path,
|
|
212
|
+
params: this.state.params,
|
|
213
|
+
query: this.state.query,
|
|
214
|
+
}
|
|
215
|
+
cachedData = routerCache.current!.get(cacheKey, loader.cache)
|
|
196
216
|
}
|
|
197
|
-
handleStateTransition(this.state.signal, transition, () => {
|
|
198
|
-
this.currentPageProps.value = props
|
|
199
|
-
})
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
props
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
if (cachedData !== null) {
|
|
219
|
+
// Use cached data immediately - no loading state needed
|
|
220
|
+
const props = {
|
|
221
|
+
...p,
|
|
222
|
+
data: cachedData.value,
|
|
223
|
+
error: null,
|
|
224
|
+
loading: false,
|
|
225
|
+
}
|
|
226
|
+
handleStateTransition(this.state.signal, transition, () => {
|
|
227
|
+
this.currentPageProps.value = props
|
|
228
|
+
})
|
|
229
|
+
} else {
|
|
230
|
+
// No cached data - show loading state and load data
|
|
231
|
+
const props = {
|
|
232
|
+
...p,
|
|
233
|
+
loading: true,
|
|
234
|
+
data: null,
|
|
235
|
+
error: null,
|
|
236
|
+
}
|
|
237
|
+
handleStateTransition(this.state.signal, transition, () => {
|
|
238
|
+
this.currentPageProps.value = props
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
this.loadRouteData(
|
|
242
|
+
config as PageConfigWithLoader,
|
|
243
|
+
props,
|
|
244
|
+
this.state,
|
|
245
|
+
transition
|
|
246
|
+
)
|
|
247
|
+
}
|
|
207
248
|
}
|
|
208
249
|
|
|
209
250
|
this.pageRouteToConfig?.set(existing.route, config)
|
|
@@ -269,7 +310,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
269
310
|
)
|
|
270
311
|
|
|
271
312
|
const [page, ...layouts] = await Promise.all([
|
|
272
|
-
pagePromise
|
|
313
|
+
pagePromise,
|
|
273
314
|
...layoutPromises,
|
|
274
315
|
])
|
|
275
316
|
|
|
@@ -301,19 +342,41 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
301
342
|
|
|
302
343
|
if (loader) {
|
|
303
344
|
if (loader.mode !== "static" || __DEV__) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
345
|
+
// Check cache first if caching is enabled
|
|
346
|
+
let cachedData = null
|
|
347
|
+
if (loader.mode !== "static" && loader.cache) {
|
|
348
|
+
const cacheKey: CacheKey = {
|
|
349
|
+
path: routerState.path,
|
|
350
|
+
params: routerState.params,
|
|
351
|
+
query: routerState.query,
|
|
352
|
+
}
|
|
353
|
+
cachedData = routerCache.current!.get(cacheKey, loader.cache)
|
|
354
|
+
}
|
|
310
355
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
props
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
356
|
+
if (cachedData !== null) {
|
|
357
|
+
// Use cached data immediately - no loading state needed
|
|
358
|
+
props = {
|
|
359
|
+
...props,
|
|
360
|
+
data: cachedData.value,
|
|
361
|
+
error: null,
|
|
362
|
+
loading: false,
|
|
363
|
+
} satisfies PageProps<PageConfig<unknown>>
|
|
364
|
+
} else {
|
|
365
|
+
// No cached data - show loading state and load data
|
|
366
|
+
props = {
|
|
367
|
+
...props,
|
|
368
|
+
loading: true,
|
|
369
|
+
data: null,
|
|
370
|
+
error: null,
|
|
371
|
+
} satisfies PageProps<PageConfig<unknown>>
|
|
372
|
+
|
|
373
|
+
this.loadRouteData(
|
|
374
|
+
config as PageConfigWithLoader,
|
|
375
|
+
props,
|
|
376
|
+
routerState,
|
|
377
|
+
enableTransition
|
|
378
|
+
)
|
|
379
|
+
}
|
|
317
380
|
} else {
|
|
318
381
|
const staticProps = page.__KIRU_STATIC_PROPS__?.[path]
|
|
319
382
|
if (!staticProps) {
|
|
@@ -355,15 +418,28 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
355
418
|
enableTransition = this.enableTransitions
|
|
356
419
|
) {
|
|
357
420
|
const { loader } = config
|
|
421
|
+
|
|
422
|
+
// Load data from loader (cache check is now done earlier in loadRoute)
|
|
358
423
|
loader
|
|
359
424
|
.load(routerState)
|
|
360
425
|
.then(
|
|
361
|
-
(data) =>
|
|
362
|
-
|
|
426
|
+
(data) => {
|
|
427
|
+
// Cache the data if caching is enabled
|
|
428
|
+
if (loader.mode !== "static" && loader.cache) {
|
|
429
|
+
const cacheKey: CacheKey = {
|
|
430
|
+
path: routerState.path,
|
|
431
|
+
params: routerState.params,
|
|
432
|
+
query: routerState.query,
|
|
433
|
+
}
|
|
434
|
+
routerCache.current!.set(cacheKey, data, loader.cache)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
363
438
|
data,
|
|
364
439
|
error: null,
|
|
365
440
|
loading: false,
|
|
366
|
-
} satisfies PageProps<PageConfig<unknown>>
|
|
441
|
+
} satisfies PageProps<PageConfig<unknown>>
|
|
442
|
+
},
|
|
367
443
|
(error) =>
|
|
368
444
|
({
|
|
369
445
|
data: null,
|
|
@@ -375,7 +451,7 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
375
451
|
if (routerState.signal.aborted) return
|
|
376
452
|
|
|
377
453
|
let transition = enableTransition
|
|
378
|
-
if (loader.transition !== undefined) {
|
|
454
|
+
if (loader.mode !== "static" && loader.transition !== undefined) {
|
|
379
455
|
transition = loader.transition
|
|
380
456
|
}
|
|
381
457
|
|
|
@@ -388,6 +464,23 @@ See https://kirujs.dev/docs/api/file-router#404 for more information.`
|
|
|
388
464
|
})
|
|
389
465
|
}
|
|
390
466
|
|
|
467
|
+
private invalidate(...paths: string[]) {
|
|
468
|
+
// Invalidate cache entries
|
|
469
|
+
routerCache.current!.invalidate(...paths)
|
|
470
|
+
|
|
471
|
+
// Check if current page matches any invalidated paths
|
|
472
|
+
const currentPath = this.state.path
|
|
473
|
+
const shouldRefresh = routerCache.current!.pathMatchesPattern(
|
|
474
|
+
currentPath,
|
|
475
|
+
paths
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if (shouldRefresh) {
|
|
479
|
+
// Refresh the current page to get fresh data
|
|
480
|
+
this.loadRoute(currentPath, {}, this.enableTransitions)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
391
484
|
private async navigate(
|
|
392
485
|
path: string,
|
|
393
486
|
options?: {
|
|
@@ -482,7 +575,7 @@ function validateRoutes(pageMap: FormattedViteImportMap) {
|
|
|
482
575
|
let warning = "[kiru/router]: Route conflicts detected:\n"
|
|
483
576
|
warning += routeConflicts
|
|
484
577
|
.map(([route1, route2]) => {
|
|
485
|
-
return ` - "${route1.
|
|
578
|
+
return ` - "${route1.absolutePath}" conflicts with "${route2.absolutePath}"\n`
|
|
486
579
|
})
|
|
487
580
|
.join("")
|
|
488
581
|
warning += "Routes are ordered by specificity (higher specificity wins)"
|
package/src/router/globals.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RouterCache } from "./cache"
|
|
1
2
|
import type { FileRouterController } from "./fileRouterController"
|
|
2
3
|
|
|
3
4
|
export const fileRouterInstance = {
|
|
@@ -7,3 +8,7 @@ export const fileRouterInstance = {
|
|
|
7
8
|
export const fileRouterRoute = {
|
|
8
9
|
current: null as string | null,
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
export const routerCache = {
|
|
13
|
+
current: null as RouterCache | null,
|
|
14
|
+
}
|
package/src/router/head.ts
CHANGED
|
@@ -1,66 +1,56 @@
|
|
|
1
|
-
import { createElement } from "../index.js"
|
|
2
1
|
import { Signal } from "../signals/base.js"
|
|
3
2
|
import { isValidTextChild, isVNode } from "../utils/index.js"
|
|
3
|
+
import { createElement } from "../element.js"
|
|
4
|
+
import { __DEV__ } from "../env.js"
|
|
5
|
+
import { KiruError } from "../error.js"
|
|
6
|
+
import { node } from "../globals.js"
|
|
4
7
|
|
|
5
|
-
export {
|
|
8
|
+
export { HeadContent, HeadOutlet }
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.filter(isVNode)
|
|
26
|
-
.forEach(({ type, props }) => {
|
|
27
|
-
switch (type) {
|
|
28
|
-
case "title":
|
|
29
|
-
const title = (
|
|
30
|
-
Array.isArray(props.children) ? props.children : [props.children]
|
|
31
|
-
)
|
|
32
|
-
.map((c) => (Signal.isSignal(c) ? c.value : c))
|
|
33
|
-
.filter(isValidTextChild)
|
|
34
|
-
.join("")
|
|
35
|
-
return (document.title = title)
|
|
36
|
-
case "meta":
|
|
37
|
-
return document
|
|
38
|
-
.querySelector(`meta[name="${props.name}"]`)
|
|
39
|
-
?.setAttribute("content", String(props.content))
|
|
40
|
-
}
|
|
10
|
+
const validHeadChildren = ["title", "base", "link", "meta", "style", "script"]
|
|
11
|
+
|
|
12
|
+
function HeadContent({ children }: { children: JSX.Children }): JSX.Element {
|
|
13
|
+
if (__DEV__) {
|
|
14
|
+
const n = node.current!
|
|
15
|
+
const asArray = Array.isArray(children) ? children : [children]
|
|
16
|
+
const invalidNodes = asArray.filter(
|
|
17
|
+
(c) =>
|
|
18
|
+
!isVNode(c) ||
|
|
19
|
+
typeof c.type !== "string" ||
|
|
20
|
+
!validHeadChildren.includes(c.type)
|
|
21
|
+
)
|
|
22
|
+
if (invalidNodes.length) {
|
|
23
|
+
throw new KiruError({
|
|
24
|
+
message: `[kiru/router]: <Head.Content> only accepts title, base, link, meta, style and script elements as children. Received: ${invalidNodes.map(
|
|
25
|
+
(n) => (isVNode(n) ? `<${n.type.toString()}>` : `"${n}"`)
|
|
26
|
+
)}`,
|
|
27
|
+
vNode: n,
|
|
41
28
|
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if ("window" in globalThis) {
|
|
32
|
+
const asArray = Array.isArray(children) ? children : [children]
|
|
33
|
+
const titleNode = asArray.find(
|
|
34
|
+
(c) => isVNode(c) && c.type === "title"
|
|
35
|
+
) as Kiru.VNode
|
|
36
|
+
|
|
37
|
+
if (titleNode) {
|
|
38
|
+
const props = titleNode.props
|
|
39
|
+
const titleChildren = Array.isArray(props.children)
|
|
40
|
+
? props.children
|
|
41
|
+
: [props.children]
|
|
42
|
+
|
|
43
|
+
document.title = titleChildren
|
|
44
|
+
.map((c) => (Signal.isSignal(c) ? c.value : c))
|
|
45
|
+
.filter(isValidTextChild)
|
|
46
|
+
.join("")
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
return null
|
|
43
50
|
}
|
|
44
51
|
return createElement("kiru-head-content", { children })
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
* Used with SSG. Renders content to the document head from a `<Head>` component in the currently rendered page.
|
|
49
|
-
* @example
|
|
50
|
-
* // document.tsx
|
|
51
|
-
* export default function Document() {
|
|
52
|
-
* return (
|
|
53
|
-
* <html lang="en">
|
|
54
|
-
* <head>
|
|
55
|
-
* <meta charset="utf-8" />
|
|
56
|
-
* <meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
57
|
-
* <Head.Outlet />
|
|
58
|
-
* </head>
|
|
59
|
-
* <body>{children}</body>
|
|
60
|
-
* </html>
|
|
61
|
-
* )
|
|
62
|
-
}
|
|
63
|
-
*/
|
|
64
|
-
function Outlet() {
|
|
54
|
+
function HeadOutlet(): JSX.Element {
|
|
65
55
|
return createElement("kiru-head-outlet")
|
|
66
56
|
}
|
package/src/router/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createElement } from "../element.js"
|
|
2
|
+
import { __DEV__ } from "../env.js"
|
|
3
|
+
|
|
1
4
|
export { useFileRouter, type FileRouterContextType } from "./context.js"
|
|
2
5
|
export * from "./errors.js"
|
|
3
6
|
export { FileRouter, type FileRouterProps } from "./fileRouter.js"
|
|
@@ -5,9 +8,73 @@ export * from "./link.js"
|
|
|
5
8
|
export * from "./pageConfig.js"
|
|
6
9
|
export type * from "./types.js"
|
|
7
10
|
|
|
8
|
-
import {
|
|
11
|
+
import { HeadContent, HeadOutlet } from "./head.js"
|
|
9
12
|
|
|
10
13
|
export const Head = {
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* - During SSG, renders content to the document head via a corresponding `<Head.Outlet>` component placed in your `document.tsx`.
|
|
16
|
+
* - During CSR, updates document title.
|
|
17
|
+
* @example
|
|
18
|
+
* // src/pages/index.tsx
|
|
19
|
+
* export default function Index() {
|
|
20
|
+
* return (
|
|
21
|
+
* <div>
|
|
22
|
+
* <Head.Content>
|
|
23
|
+
* <title>My App - Home</title>
|
|
24
|
+
* </Head.Content>
|
|
25
|
+
* <h1>Home</h1>
|
|
26
|
+
* </div>
|
|
27
|
+
* )
|
|
28
|
+
}
|
|
29
|
+
*/
|
|
30
|
+
Content: HeadContent,
|
|
31
|
+
/**
|
|
32
|
+
* Used with SSG. Renders content to the document head from a `<Head>` component in the currently rendered page.
|
|
33
|
+
* @example
|
|
34
|
+
* // src/pages/document.tsx
|
|
35
|
+
* export default function Document() {
|
|
36
|
+
* return (
|
|
37
|
+
* <html lang="en">
|
|
38
|
+
* <head>
|
|
39
|
+
* <meta charset="utf-8" />
|
|
40
|
+
* <meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
41
|
+
* <Head.Outlet />
|
|
42
|
+
* </head>
|
|
43
|
+
* <Body.Outlet />
|
|
44
|
+
* </html>
|
|
45
|
+
* )
|
|
46
|
+
}
|
|
47
|
+
*/
|
|
48
|
+
Outlet: HeadOutlet,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Body = {
|
|
52
|
+
/**
|
|
53
|
+
* Used with SSG 'document' files. Renders content to the document body via a corresponding `<Body.Outlet>` component placed in your `document.tsx`.
|
|
54
|
+
* @example
|
|
55
|
+
* // src/pages/document.tsx
|
|
56
|
+
* export default function Document() {
|
|
57
|
+
* return (
|
|
58
|
+
* <html lang="en">
|
|
59
|
+
* <head>
|
|
60
|
+
* <meta charset="utf-8" />
|
|
61
|
+
* <meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
62
|
+
* <Head.Outlet />
|
|
63
|
+
* </head>
|
|
64
|
+
* <Body.Outlet />
|
|
65
|
+
* </html>
|
|
66
|
+
* )
|
|
67
|
+
}
|
|
68
|
+
*/
|
|
69
|
+
Outlet: BodyOutlet,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function BodyOutlet() {
|
|
73
|
+
return createElement("kiru-body-outlet")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (__DEV__) {
|
|
77
|
+
;(Head.Content as Kiru.FC).displayName = "Kiru.Head.Content"
|
|
78
|
+
;(Head.Outlet as Kiru.FC).displayName = "Kiru.Head.Outlet"
|
|
79
|
+
;(Body.Outlet as Kiru.FC).displayName = "Kiru.Body.Outlet"
|
|
13
80
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createElement } from "../../element.js"
|
|
2
|
-
|
|
1
|
+
import { createElement, Fragment } from "../../element.js"
|
|
2
|
+
|
|
3
3
|
import {
|
|
4
4
|
matchLayouts,
|
|
5
5
|
matchRoute,
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
} from "../utils/index.js"
|
|
10
10
|
import { RouterContext } from "../context.js"
|
|
11
11
|
import type { PageConfig, PageProps, RouterState } from "../types.js"
|
|
12
|
-
import type { Readable } from "node:stream"
|
|
13
12
|
import { FormattedViteImportMap, PageModule } from "../types.internal.js"
|
|
14
13
|
import { __DEV__ } from "../../env.js"
|
|
15
14
|
import { FileRouterDataLoadError } from "../errors.js"
|
|
15
|
+
import { renderToString } from "../../renderToString.js"
|
|
16
16
|
|
|
17
17
|
export interface RenderContext {
|
|
18
18
|
pages: FormattedViteImportMap
|
|
@@ -24,8 +24,7 @@ export interface RenderContext {
|
|
|
24
24
|
|
|
25
25
|
export interface RenderResult {
|
|
26
26
|
status: number
|
|
27
|
-
|
|
28
|
-
stream: Readable | null
|
|
27
|
+
body: string
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export async function render(
|
|
@@ -52,15 +51,12 @@ export async function render(
|
|
|
52
51
|
}
|
|
53
52
|
return {
|
|
54
53
|
status: 404,
|
|
55
|
-
|
|
56
|
-
"<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
|
|
57
|
-
stream: null,
|
|
54
|
+
body: "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
57
|
return render("/404", ctx, {
|
|
61
58
|
...(result ?? {}),
|
|
62
|
-
|
|
63
|
-
stream: null,
|
|
59
|
+
body: "",
|
|
64
60
|
status: 404,
|
|
65
61
|
})
|
|
66
62
|
}
|
|
@@ -72,7 +68,7 @@ export async function render(
|
|
|
72
68
|
|
|
73
69
|
if (__DEV__) {
|
|
74
70
|
;[pageEntry, ...layoutEntries].forEach((e) => {
|
|
75
|
-
ctx.registerModule(e.
|
|
71
|
+
ctx.registerModule(e.absolutePath!)
|
|
76
72
|
})
|
|
77
73
|
}
|
|
78
74
|
|
|
@@ -131,8 +127,19 @@ export async function render(
|
|
|
131
127
|
props
|
|
132
128
|
)
|
|
133
129
|
|
|
130
|
+
let documentShell = renderToString(createElement(ctx.Document))
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
documentShell.includes("</body>") ||
|
|
134
|
+
!documentShell.includes("<kiru-body-outlet>")
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#ssg"
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
const app = createElement(RouterContext.Provider, {
|
|
135
|
-
children:
|
|
142
|
+
children: Fragment({ children }),
|
|
136
143
|
value: {
|
|
137
144
|
state: {
|
|
138
145
|
params,
|
|
@@ -143,36 +150,38 @@ export async function render(
|
|
|
143
150
|
},
|
|
144
151
|
})
|
|
145
152
|
|
|
146
|
-
let
|
|
147
|
-
const
|
|
148
|
-
const
|
|
153
|
+
let pageOutletContent = renderToString(app)
|
|
154
|
+
const hasHeadContent = pageOutletContent.includes("<kiru-head-content>")
|
|
155
|
+
const hasHeadOutlet = documentShell.includes("<kiru-head-outlet>")
|
|
149
156
|
|
|
150
157
|
if (hasHeadOutlet && hasHeadContent) {
|
|
151
158
|
let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
|
|
152
|
-
|
|
159
|
+
pageOutletContent.split(/<kiru-head-content>|<\/kiru-head-content>/)
|
|
153
160
|
|
|
154
|
-
|
|
161
|
+
documentShell = documentShell.replace(
|
|
155
162
|
"<kiru-head-outlet>",
|
|
156
163
|
headContentInner
|
|
157
164
|
)
|
|
158
|
-
|
|
165
|
+
pageOutletContent = `${preHeadContent}${postHeadContent}`
|
|
159
166
|
} else if (hasHeadContent) {
|
|
160
167
|
// remove head content element and everything within it
|
|
161
|
-
|
|
168
|
+
pageOutletContent = pageOutletContent.replace(
|
|
162
169
|
/<kiru-head-content>(.*?)<\/kiru-head-content>/,
|
|
163
170
|
""
|
|
164
171
|
)
|
|
165
172
|
} else if (hasHeadOutlet) {
|
|
166
173
|
// remove head outlet element and everything within it
|
|
167
|
-
|
|
174
|
+
documentShell = documentShell.replaceAll("<kiru-head-outlet>", "")
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
const [prePageOutlet, postPageOutlet] =
|
|
178
|
+
documentShell.split("<kiru-body-outlet>")
|
|
179
|
+
|
|
170
180
|
// console.log("immediate", immediate)
|
|
171
181
|
|
|
172
182
|
return {
|
|
173
183
|
status: is404Route ? 404 : result?.status ?? 200,
|
|
174
|
-
|
|
175
|
-
stream,
|
|
184
|
+
body: `<!doctype html>${prePageOutlet}<body>${pageOutletContent}</body>${postPageOutlet}`,
|
|
176
185
|
}
|
|
177
186
|
}
|
|
178
187
|
|
|
@@ -190,7 +199,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
|
|
|
190
199
|
|
|
191
200
|
const hasDynamic = urlSegments.some((s) => s.startsWith(":"))
|
|
192
201
|
if (!hasDynamic) {
|
|
193
|
-
results[basePath === "" ? "/" : basePath] = entry.
|
|
202
|
+
results[basePath === "" ? "/" : basePath] = entry.absolutePath
|
|
194
203
|
continue
|
|
195
204
|
}
|
|
196
205
|
try {
|
|
@@ -206,7 +215,7 @@ export async function generateStaticPaths(pages: FormattedViteImportMap) {
|
|
|
206
215
|
const value = params[key]
|
|
207
216
|
p = p.replace(`:${key}*`, value).replace(`:${key}`, value)
|
|
208
217
|
}
|
|
209
|
-
results[p] = entry.
|
|
218
|
+
results[p] = entry.absolutePath
|
|
210
219
|
}
|
|
211
220
|
} catch {}
|
|
212
221
|
}
|
|
@@ -17,18 +17,20 @@ export interface ViteImportMap {
|
|
|
17
17
|
[fp: string]: () => Promise<DefaultComponentModule>
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
export interface FormattedViteImportMapEntry<T = DefaultComponentModule> {
|
|
21
|
+
load: () => Promise<T>
|
|
22
|
+
specificity: number
|
|
23
|
+
segments: string[]
|
|
24
|
+
absolutePath: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FormattedViteImportMap<T = DefaultComponentModule> {
|
|
28
|
+
[key: string]: FormattedViteImportMapEntry<T>
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface RouteMatch {
|
|
30
32
|
route: string
|
|
31
|
-
pageEntry:
|
|
33
|
+
pageEntry: FormattedViteImportMapEntry<PageModule>
|
|
32
34
|
params: Record<string, string>
|
|
33
35
|
routeSegments: string[]
|
|
34
36
|
}
|