kiru 0.53.0 → 0.54.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/derive.d.ts +1 -1
- package/dist/components/derive.d.ts.map +1 -1
- package/dist/components/derive.js +3 -2
- package/dist/components/derive.js.map +1 -1
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +6 -2
- package/dist/dom.js.map +1 -1
- package/dist/globals.d.ts +1 -1
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js.map +1 -1
- package/dist/hooks/usePromise.d.ts +2 -1
- package/dist/hooks/usePromise.d.ts.map +1 -1
- package/dist/hooks/usePromise.js +31 -62
- package/dist/hooks/usePromise.js.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/router/client/index.d.ts +4 -2
- package/dist/router/client/index.d.ts.map +1 -1
- package/dist/router/client/index.js +59 -13
- package/dist/router/client/index.js.map +1 -1
- package/dist/router/constants.d.ts +2 -0
- package/dist/router/constants.d.ts.map +1 -0
- package/dist/router/constants.js +2 -0
- package/dist/router/constants.js.map +1 -0
- package/dist/router/context.d.ts +2 -0
- package/dist/router/context.d.ts.map +1 -1
- package/dist/router/context.js +5 -1
- package/dist/router/context.js.map +1 -1
- package/dist/router/fileRouterController.d.ts +2 -0
- package/dist/router/fileRouterController.d.ts.map +1 -1
- package/dist/router/fileRouterController.js +195 -107
- package/dist/router/fileRouterController.js.map +1 -1
- package/dist/router/globals.d.ts +3 -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/guard.d.ts +17 -0
- package/dist/router/guard.d.ts.map +1 -0
- package/dist/router/guard.js +45 -0
- package/dist/router/guard.js.map +1 -0
- package/dist/router/head.d.ts.map +1 -1
- package/dist/router/head.js +5 -7
- package/dist/router/head.js.map +1 -1
- package/dist/router/index.d.ts +2 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/{server → ssg}/index.d.ts +4 -3
- package/dist/router/ssg/index.d.ts.map +1 -0
- package/dist/router/{server → ssg}/index.js +8 -5
- package/dist/router/ssg/index.js.map +1 -0
- package/dist/router/ssr/index.d.ts +20 -0
- package/dist/router/ssr/index.d.ts.map +1 -0
- package/dist/router/ssr/index.js +163 -0
- package/dist/router/ssr/index.js.map +1 -0
- package/dist/router/types.d.ts +42 -16
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/types.internal.d.ts +4 -0
- package/dist/router/types.internal.d.ts.map +1 -1
- package/dist/router/utils/index.d.ts +8 -3
- package/dist/router/utils/index.d.ts.map +1 -1
- package/dist/router/utils/index.js +38 -6
- package/dist/router/utils/index.js.map +1 -1
- package/dist/scheduler.d.ts +14 -3
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +3 -4
- package/dist/scheduler.js.map +1 -1
- package/dist/ssr/client.d.ts +1 -1
- package/dist/ssr/client.d.ts.map +1 -1
- package/dist/ssr/server.d.ts +9 -3
- package/dist/ssr/server.d.ts.map +1 -1
- package/dist/ssr/server.js +37 -30
- package/dist/ssr/server.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/format.d.ts +2 -1
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +4 -1
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/promise.d.ts +2 -0
- package/dist/utils/promise.d.ts.map +1 -1
- package/dist/utils/promise.js +45 -1
- package/dist/utils/promise.js.map +1 -1
- package/dist/utils/runtime.d.ts +1 -1
- package/dist/utils/runtime.js +1 -1
- package/package.json +8 -4
- package/src/components/derive.ts +5 -3
- package/src/dom.ts +5 -1
- package/src/globals.ts +1 -1
- package/src/hooks/usePromise.ts +57 -77
- package/src/index.ts +1 -1
- package/src/router/client/index.ts +114 -16
- package/src/router/constants.ts +1 -0
- package/src/router/context.ts +7 -1
- package/src/router/fileRouterController.ts +304 -132
- package/src/router/globals.ts +4 -0
- package/src/router/guard.ts +72 -0
- package/src/router/head.ts +5 -7
- package/src/router/index.ts +12 -1
- package/src/router/{server → ssg}/index.ts +17 -10
- package/src/router/ssr/index.ts +252 -0
- package/src/router/types.internal.ts +5 -0
- package/src/router/types.ts +53 -16
- package/src/router/utils/index.ts +74 -8
- package/src/scheduler.ts +20 -3
- package/src/ssr/client.ts +1 -1
- package/src/ssr/server.ts +58 -34
- package/src/types.ts +3 -0
- package/src/utils/format.ts +5 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/promise.ts +70 -1
- package/src/utils/runtime.ts +1 -1
- package/dist/router/server/index.d.ts.map +0 -1
- package/dist/router/server/index.js.map +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { NavigationHook } from "./types"
|
|
2
|
+
import type { GuardModule } from "./types.internal"
|
|
3
|
+
|
|
4
|
+
export type GuardBeforeEach = NavigationHook<
|
|
5
|
+
void | string | Promise<void | string>
|
|
6
|
+
>
|
|
7
|
+
export type GuardAfterEach = NavigationHook<void | Promise<void>>
|
|
8
|
+
|
|
9
|
+
export interface NavGuard {
|
|
10
|
+
beforeEach: GuardBeforeEach
|
|
11
|
+
afterEach: GuardAfterEach
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const $NAVGUARD_INTERNAL = Symbol.for("kiru:navguard")
|
|
15
|
+
|
|
16
|
+
export function resolveNavguard(module: GuardModule): NavGuard | null {
|
|
17
|
+
if (
|
|
18
|
+
"guard" in module &&
|
|
19
|
+
$NAVGUARD_INTERNAL in ((module.guard ?? {}) as NavGuardBuilder)
|
|
20
|
+
) {
|
|
21
|
+
return (module.guard as NavGuardBuilder)[$NAVGUARD_INTERNAL]
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NavGuardBuilder {
|
|
27
|
+
beforeEach(...fns: GuardBeforeEach[]): NavGuardBuilder
|
|
28
|
+
afterEach(...fns: GuardAfterEach[]): NavGuardBuilder
|
|
29
|
+
get [$NAVGUARD_INTERNAL](): NavGuard
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createNavGuard_impl(
|
|
33
|
+
beforeEach: GuardBeforeEach[],
|
|
34
|
+
afterEach: GuardAfterEach[]
|
|
35
|
+
): NavGuard {
|
|
36
|
+
return {
|
|
37
|
+
beforeEach: async (ctx, to, from) => {
|
|
38
|
+
for (const fn of beforeEach) {
|
|
39
|
+
const res = await fn(ctx, to, from)
|
|
40
|
+
if (typeof res === "string") {
|
|
41
|
+
return res
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return
|
|
45
|
+
},
|
|
46
|
+
afterEach: async (ctx, to, from) => {
|
|
47
|
+
for (const fn of afterEach) {
|
|
48
|
+
await fn(ctx, to, from)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createNavGuard(): NavGuardBuilder {
|
|
55
|
+
const beforeEach: GuardBeforeEach[] = []
|
|
56
|
+
const afterEach: GuardAfterEach[] = []
|
|
57
|
+
const guard = createNavGuard_impl(beforeEach, afterEach)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
beforeEach(...fns: GuardBeforeEach[]) {
|
|
61
|
+
beforeEach.push(...fns)
|
|
62
|
+
return this
|
|
63
|
+
},
|
|
64
|
+
afterEach(...fns: GuardAfterEach[]) {
|
|
65
|
+
afterEach.push(...fns)
|
|
66
|
+
return this
|
|
67
|
+
},
|
|
68
|
+
get [$NAVGUARD_INTERNAL]() {
|
|
69
|
+
return guard
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/router/head.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Signal } from "../signals/base.js"
|
|
2
|
-
import { isValidTextChild, isVNode } from "../utils/index.js"
|
|
2
|
+
import { isValidTextChild, isVNode, toArray } from "../utils/index.js"
|
|
3
3
|
import { createElement } from "../element.js"
|
|
4
4
|
import { __DEV__ } from "../env.js"
|
|
5
5
|
import { KiruError } from "../error.js"
|
|
@@ -12,7 +12,7 @@ const validHeadChildren = ["title", "base", "link", "meta", "style", "script"]
|
|
|
12
12
|
function HeadContent({ children }: { children: JSX.Children }): JSX.Element {
|
|
13
13
|
if (__DEV__) {
|
|
14
14
|
const n = node.current!
|
|
15
|
-
const asArray =
|
|
15
|
+
const asArray = toArray(children)
|
|
16
16
|
const invalidNodes = asArray.filter(
|
|
17
17
|
(c) =>
|
|
18
18
|
!isVNode(c) ||
|
|
@@ -29,16 +29,14 @@ function HeadContent({ children }: { children: JSX.Children }): JSX.Element {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
if ("window" in globalThis) {
|
|
32
|
-
const
|
|
33
|
-
const titleNode =
|
|
32
|
+
const c = toArray(children)
|
|
33
|
+
const titleNode = c.find(
|
|
34
34
|
(c) => isVNode(c) && c.type === "title"
|
|
35
35
|
) as Kiru.VNode
|
|
36
36
|
|
|
37
37
|
if (titleNode) {
|
|
38
38
|
const props = titleNode.props
|
|
39
|
-
const titleChildren =
|
|
40
|
-
? props.children
|
|
41
|
-
: [props.children]
|
|
39
|
+
const titleChildren = toArray(props.children)
|
|
42
40
|
|
|
43
41
|
document.title = titleChildren
|
|
44
42
|
.map((c) => (Signal.isSignal(c) ? c.value : c))
|
package/src/router/index.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { createElement } from "../element.js"
|
|
2
2
|
import { __DEV__ } from "../env.js"
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
useRequestContext,
|
|
6
|
+
useFileRouter,
|
|
7
|
+
type FileRouterContextType,
|
|
8
|
+
} from "./context.js"
|
|
5
9
|
export * from "./errors.js"
|
|
6
10
|
export { FileRouter, type FileRouterProps } from "./fileRouter.js"
|
|
11
|
+
export {
|
|
12
|
+
createNavGuard,
|
|
13
|
+
type GuardBeforeEach,
|
|
14
|
+
type GuardAfterEach,
|
|
15
|
+
type NavGuard,
|
|
16
|
+
type NavGuardBuilder,
|
|
17
|
+
} from "./guard.js"
|
|
7
18
|
export * from "./link.js"
|
|
8
19
|
export * from "./pageConfig.js"
|
|
9
20
|
export type * from "./types.js"
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { createElement, Fragment } from "../../element.js"
|
|
2
|
-
|
|
3
2
|
import {
|
|
4
|
-
|
|
3
|
+
matchModules,
|
|
5
4
|
matchRoute,
|
|
6
5
|
match404Route,
|
|
7
6
|
parseQuery,
|
|
8
7
|
wrapWithLayouts,
|
|
9
8
|
} from "../utils/index.js"
|
|
10
9
|
import { RouterContext } from "../context.js"
|
|
11
|
-
import type { PageConfig, PageProps, RouterState } from "../types.js"
|
|
12
|
-
import { FormattedViteImportMap, PageModule } from "../types.internal.js"
|
|
13
10
|
import { __DEV__ } from "../../env.js"
|
|
14
11
|
import { FileRouterDataLoadError } from "../errors.js"
|
|
15
12
|
import { renderToString } from "../../renderToString.js"
|
|
13
|
+
import type { PageConfig, PageProps, RouterState } from "../types.js"
|
|
14
|
+
import type {
|
|
15
|
+
FormattedViteImportMap,
|
|
16
|
+
GuardModule,
|
|
17
|
+
PageModule,
|
|
18
|
+
} from "../types.internal.js"
|
|
16
19
|
|
|
17
20
|
export interface RenderContext {
|
|
18
|
-
pages: FormattedViteImportMap
|
|
21
|
+
pages: FormattedViteImportMap<PageModule>
|
|
19
22
|
layouts: FormattedViteImportMap
|
|
23
|
+
guards: FormattedViteImportMap<GuardModule>
|
|
20
24
|
Document: Kiru.FC
|
|
21
25
|
registerModule: (moduleId: string) => void
|
|
22
|
-
|
|
26
|
+
registerStaticProps: (props: Record<string, unknown>) => void
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export interface RenderResult {
|
|
@@ -64,7 +68,7 @@ export async function render(
|
|
|
64
68
|
|
|
65
69
|
const { pageEntry, routeSegments, params } = routeMatch
|
|
66
70
|
const is404Route = routeMatch.routeSegments.includes("404")
|
|
67
|
-
const layoutEntries =
|
|
71
|
+
const layoutEntries = matchModules(ctx.layouts, routeSegments)
|
|
68
72
|
|
|
69
73
|
;[pageEntry, ...layoutEntries].forEach((e) => {
|
|
70
74
|
ctx.registerModule(e.filePath)
|
|
@@ -82,7 +86,7 @@ export async function render(
|
|
|
82
86
|
const abortController = new AbortController()
|
|
83
87
|
|
|
84
88
|
if (config.loader) {
|
|
85
|
-
if (config.loader.
|
|
89
|
+
if (!config.loader.static || __DEV__) {
|
|
86
90
|
props = { loading: true, data: null, error: null }
|
|
87
91
|
} else {
|
|
88
92
|
const routerState: RouterState = {
|
|
@@ -99,7 +103,10 @@ export async function render(
|
|
|
99
103
|
}, 10000)
|
|
100
104
|
|
|
101
105
|
try {
|
|
102
|
-
const data = await config.loader.load(
|
|
106
|
+
const data = await config.loader.load({
|
|
107
|
+
...routerState,
|
|
108
|
+
context: {},
|
|
109
|
+
})
|
|
103
110
|
props = {
|
|
104
111
|
data,
|
|
105
112
|
error: null,
|
|
@@ -113,7 +120,7 @@ export async function render(
|
|
|
113
120
|
}
|
|
114
121
|
} finally {
|
|
115
122
|
clearTimeout(timeout)
|
|
116
|
-
ctx.
|
|
123
|
+
ctx.registerStaticProps({ data: props.data, error: props.error })
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { createElement, Fragment } from "../../element.js"
|
|
3
|
+
import { __DEV__ } from "../../env.js"
|
|
4
|
+
import { renderToString } from "../../renderToString.js"
|
|
5
|
+
import { renderToReadableStream } from "../../ssr/server.js"
|
|
6
|
+
import { toArray } from "../../utils/format.js"
|
|
7
|
+
import {
|
|
8
|
+
matchModules,
|
|
9
|
+
matchRoute,
|
|
10
|
+
match404Route,
|
|
11
|
+
parseQuery,
|
|
12
|
+
wrapWithLayouts,
|
|
13
|
+
runBeforeEachGuards,
|
|
14
|
+
runAfterEachGuards,
|
|
15
|
+
runBeforeEnterHooks,
|
|
16
|
+
} from "../utils/index.js"
|
|
17
|
+
import { RouterContext, RequestContext } from "../context.js"
|
|
18
|
+
import type { PageConfig, PageProps, RouterState } from "../types.js"
|
|
19
|
+
import type {
|
|
20
|
+
FormattedViteImportMap,
|
|
21
|
+
GuardModule,
|
|
22
|
+
PageModule,
|
|
23
|
+
} from "../types.internal.js"
|
|
24
|
+
import { createStatefulPromise } from "../../utils/promise.js"
|
|
25
|
+
import { PAGE_DATA_PROMISE_ID } from "../constants.js"
|
|
26
|
+
|
|
27
|
+
export interface SSRRenderContext {
|
|
28
|
+
pages: FormattedViteImportMap<PageModule>
|
|
29
|
+
layouts: FormattedViteImportMap
|
|
30
|
+
guards: FormattedViteImportMap<GuardModule>
|
|
31
|
+
Document: Kiru.FC
|
|
32
|
+
userContext: Kiru.RequestContext
|
|
33
|
+
registerModule: (moduleId: string) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SSRHttpResponse {
|
|
37
|
+
html: string
|
|
38
|
+
statusCode: number
|
|
39
|
+
headers: Array<[string, string]>
|
|
40
|
+
stream: ReadableStream | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SSRRenderResult {
|
|
44
|
+
httpResponse: SSRHttpResponse | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function render(
|
|
48
|
+
url: string,
|
|
49
|
+
ctx: SSRRenderContext
|
|
50
|
+
): Promise<SSRRenderResult> {
|
|
51
|
+
const extName = path.extname(url)
|
|
52
|
+
if (extName && extName.length > 0) {
|
|
53
|
+
return { httpResponse: null }
|
|
54
|
+
} else if (url.startsWith("/@")) {
|
|
55
|
+
return { httpResponse: null }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const u = new URL(url, "http://localhost")
|
|
59
|
+
const pathSegments = u.pathname.split("/").filter(Boolean)
|
|
60
|
+
let routeMatch = matchRoute(ctx.pages, pathSegments)
|
|
61
|
+
|
|
62
|
+
if (!routeMatch) {
|
|
63
|
+
// Try to find a 404 page in parent directories
|
|
64
|
+
const fourOhFourMatch = match404Route(ctx.pages, pathSegments)
|
|
65
|
+
if (fourOhFourMatch) {
|
|
66
|
+
routeMatch = fourOhFourMatch
|
|
67
|
+
} else {
|
|
68
|
+
// Fallback to root 404 or default fallback
|
|
69
|
+
if (url === "/404") {
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.warn(
|
|
72
|
+
"[kiru/router]: No 404 route defined. Using fallback 404 page."
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
httpResponse: {
|
|
77
|
+
statusCode: 404,
|
|
78
|
+
headers: [["Content-Type", "text/html"]],
|
|
79
|
+
html: "<!doctype html><html><head><title>Not Found</title></head><body><h1>404</h1></body></html>",
|
|
80
|
+
stream: null,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Recursively render the 404 page
|
|
85
|
+
const notFoundResponse = await render("/404", ctx)
|
|
86
|
+
return {
|
|
87
|
+
httpResponse: {
|
|
88
|
+
html: notFoundResponse.httpResponse?.html ?? "",
|
|
89
|
+
headers: notFoundResponse.httpResponse?.headers ?? [
|
|
90
|
+
["Content-Type", "text/html"],
|
|
91
|
+
],
|
|
92
|
+
...notFoundResponse,
|
|
93
|
+
statusCode: 404,
|
|
94
|
+
stream: null,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { pageEntry, routeSegments, params } = routeMatch
|
|
100
|
+
const is404Route = routeMatch.routeSegments.includes("404")
|
|
101
|
+
|
|
102
|
+
const guardEntries = matchModules(ctx.guards, routeSegments)
|
|
103
|
+
const guardModules = await Promise.all(
|
|
104
|
+
guardEntries.map((entry) => entry.load() as unknown as Promise<GuardModule>)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const redirectPath = await runBeforeEachGuards(
|
|
108
|
+
guardModules,
|
|
109
|
+
{ ...ctx.userContext },
|
|
110
|
+
u.pathname
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if (redirectPath !== null) {
|
|
114
|
+
return createRedirectResult(redirectPath)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const layoutEntries = matchModules(ctx.layouts, routeSegments)
|
|
118
|
+
|
|
119
|
+
// Register all modules for CSS collection
|
|
120
|
+
;[pageEntry, ...layoutEntries].forEach((e) => {
|
|
121
|
+
ctx.registerModule(e.filePath)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const [page, ...layouts] = await Promise.all([
|
|
125
|
+
pageEntry.load(),
|
|
126
|
+
...layoutEntries.map((layoutEntry) => layoutEntry.load()),
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
const onBeforeEnter = page.config?.hooks?.onBeforeEnter
|
|
130
|
+
if (onBeforeEnter) {
|
|
131
|
+
const redirectPath = await runBeforeEnterHooks(
|
|
132
|
+
toArray(onBeforeEnter),
|
|
133
|
+
{ ...ctx.userContext },
|
|
134
|
+
u.pathname
|
|
135
|
+
)
|
|
136
|
+
if (redirectPath !== null) {
|
|
137
|
+
return createRedirectResult(redirectPath)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let documentShell = renderToString(createElement(ctx.Document))
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
documentShell.includes("</body>") ||
|
|
145
|
+
!documentShell.includes("<kiru-body-outlet>")
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"[kiru/router]: Document is expected to contain a <Body.Outlet> element. See https://kirujs.dev/docs/api/file-router#general-usage"
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const query = parseQuery(u.search)
|
|
153
|
+
|
|
154
|
+
let props = {} as PageProps<PageConfig>
|
|
155
|
+
const config = page.config ?? {}
|
|
156
|
+
const abortSignal = new AbortController().signal
|
|
157
|
+
|
|
158
|
+
const routerState: RouterState = {
|
|
159
|
+
params,
|
|
160
|
+
query,
|
|
161
|
+
pathname: u.pathname,
|
|
162
|
+
hash: "",
|
|
163
|
+
signal: abortSignal,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let pageDataPromise: Kiru.StatefulPromise<unknown> | null = null
|
|
167
|
+
if (config.loader && !config.loader.static) {
|
|
168
|
+
props = {
|
|
169
|
+
data: null,
|
|
170
|
+
error: null,
|
|
171
|
+
loading: true,
|
|
172
|
+
}
|
|
173
|
+
const promise = config.loader.load({
|
|
174
|
+
...routerState,
|
|
175
|
+
context: { ...ctx.userContext },
|
|
176
|
+
})
|
|
177
|
+
pageDataPromise = createStatefulPromise(PAGE_DATA_PROMISE_ID, promise)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const children = wrapWithLayouts(
|
|
181
|
+
layouts
|
|
182
|
+
.map((layout) => layout.default)
|
|
183
|
+
.filter((l) => typeof l === "function"),
|
|
184
|
+
page.default,
|
|
185
|
+
props
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const app = createElement(RouterContext.Provider, {
|
|
189
|
+
children: createElement(RequestContext.Provider, {
|
|
190
|
+
children: Fragment({ children }),
|
|
191
|
+
value: ctx.userContext,
|
|
192
|
+
}),
|
|
193
|
+
value: { state: routerState },
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
let { immediate: pageOutletContent, stream } = renderToReadableStream(app, {
|
|
197
|
+
data: pageDataPromise ? [pageDataPromise] : [],
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const hasHeadContent = pageOutletContent.includes("<kiru-head-content>")
|
|
201
|
+
const hasHeadOutlet = documentShell.includes("<kiru-head-outlet>")
|
|
202
|
+
|
|
203
|
+
if (hasHeadOutlet && hasHeadContent) {
|
|
204
|
+
let [preHeadContent = "", headContentInner = "", postHeadContent = ""] =
|
|
205
|
+
pageOutletContent.split(/<kiru-head-content>|<\/kiru-head-content>/)
|
|
206
|
+
|
|
207
|
+
documentShell = documentShell.replace(
|
|
208
|
+
"<kiru-head-outlet>",
|
|
209
|
+
headContentInner
|
|
210
|
+
)
|
|
211
|
+
pageOutletContent = `${preHeadContent}${postHeadContent}`
|
|
212
|
+
} else if (hasHeadContent) {
|
|
213
|
+
// remove head content element and everything within it
|
|
214
|
+
pageOutletContent = pageOutletContent.replace(
|
|
215
|
+
/<kiru-head-content>(.*?)<\/kiru-head-content>/,
|
|
216
|
+
""
|
|
217
|
+
)
|
|
218
|
+
} else if (hasHeadOutlet) {
|
|
219
|
+
// remove head outlet element and everything within it
|
|
220
|
+
documentShell = documentShell.replaceAll("<kiru-head-outlet>", "")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const [prePageOutlet, postPageOutlet] =
|
|
224
|
+
documentShell.split("<kiru-body-outlet>")
|
|
225
|
+
|
|
226
|
+
const html = `<!DOCTYPE html>${prePageOutlet}<body>${pageOutletContent}</body>${postPageOutlet}`
|
|
227
|
+
const statusCode = is404Route ? 404 : 200
|
|
228
|
+
|
|
229
|
+
queueMicrotask(() => {
|
|
230
|
+
runAfterEachGuards(guardModules, { ...ctx.userContext }, u.pathname)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
httpResponse: {
|
|
235
|
+
html,
|
|
236
|
+
statusCode,
|
|
237
|
+
headers: [["Content-Type", "text/html;charset=utf-8"]],
|
|
238
|
+
stream,
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createRedirectResult(to: string): SSRRenderResult {
|
|
244
|
+
return {
|
|
245
|
+
httpResponse: {
|
|
246
|
+
statusCode: 302,
|
|
247
|
+
headers: [["Location", to]],
|
|
248
|
+
html: "",
|
|
249
|
+
stream: null,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FileRouterContextType } from "./context"
|
|
2
2
|
import type { PageConfig } from "./types"
|
|
3
|
+
import type { NavGuardBuilder } from "./guard"
|
|
3
4
|
|
|
4
5
|
export interface CurrentPage {
|
|
5
6
|
component: Kiru.FC<any>
|
|
@@ -20,6 +21,10 @@ export interface PageModule {
|
|
|
20
21
|
>
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
export interface GuardModule {
|
|
25
|
+
guard: NavGuardBuilder
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export interface ViteImportMap {
|
|
24
29
|
[fp: string]: () => Promise<DefaultComponentModule>
|
|
25
30
|
}
|
package/src/router/types.ts
CHANGED
|
@@ -3,14 +3,17 @@ import type { FileRouterDataLoadError } from "./errors"
|
|
|
3
3
|
import type {
|
|
4
4
|
DefaultComponentModule,
|
|
5
5
|
FormattedViteImportMap,
|
|
6
|
+
GuardModule,
|
|
6
7
|
PageModule,
|
|
7
8
|
} from "./types.internal"
|
|
8
9
|
|
|
9
10
|
export interface FileRouterPreloadConfig {
|
|
10
|
-
pages: FormattedViteImportMap
|
|
11
|
+
pages: FormattedViteImportMap<PageModule>
|
|
11
12
|
layouts: FormattedViteImportMap
|
|
13
|
+
guards?: FormattedViteImportMap<GuardModule>
|
|
12
14
|
page: PageModule
|
|
13
15
|
pageProps: Record<string, unknown>
|
|
16
|
+
pagePropsPromise?: Promise<AsyncTaskState<unknown, FileRouterDataLoadError>>
|
|
14
17
|
pageLayouts: DefaultComponentModule[]
|
|
15
18
|
params: RouteParams
|
|
16
19
|
query: RouteQuery
|
|
@@ -40,6 +43,14 @@ export interface FileRouterConfig {
|
|
|
40
43
|
* ```
|
|
41
44
|
*/
|
|
42
45
|
layouts: Record<string, unknown>
|
|
46
|
+
/**
|
|
47
|
+
* The import map to use for loading nav guards
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <FileRouter config={{ guards: import.meta.glob("/∗∗/guard.{ts,js}"), ... }} />
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
guards?: Record<string, unknown>
|
|
43
54
|
|
|
44
55
|
/**
|
|
45
56
|
* The base url to use as a prefix for route matching
|
|
@@ -98,28 +109,40 @@ export interface RouterState {
|
|
|
98
109
|
signal: AbortSignal
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
type PageDataLoaderContext = RouterState & {}
|
|
102
|
-
|
|
103
112
|
export interface PageDataLoaderCacheConfig {
|
|
104
113
|
type: "memory" | "localStorage" | "sessionStorage"
|
|
105
114
|
ttl: number
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
interface LoaderContext extends RouterState {
|
|
118
|
+
/**
|
|
119
|
+
* The request context - in SSR, this is the data from the server
|
|
120
|
+
* that's passed to the `renderPage` function
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* // server.ts
|
|
124
|
+
* renderPage({ url, context: { test: 123 } })
|
|
125
|
+
*
|
|
126
|
+
* // page.tsx
|
|
127
|
+
* loader: {
|
|
128
|
+
* load: ({ context }) => context.test
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
context: Kiru.RequestContext
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
export type PageDataLoaderConfig<T = unknown> = {
|
|
109
136
|
/**
|
|
110
137
|
* The function to load the page data
|
|
111
138
|
*/
|
|
112
|
-
load: (context:
|
|
139
|
+
load: (context: LoaderContext) => Promise<T>
|
|
113
140
|
} & (
|
|
114
141
|
| {
|
|
115
142
|
/**
|
|
116
|
-
*
|
|
117
|
-
* @default "client"
|
|
118
|
-
* @description
|
|
119
|
-
* - **static**: The page data is loaded at build time and never updated
|
|
120
|
-
* - **client**: The page data is loaded upon navigation and updated on subsequent navigations
|
|
143
|
+
* Indicates that the page data should only be loaded at build time
|
|
121
144
|
*/
|
|
122
|
-
|
|
145
|
+
static?: false
|
|
123
146
|
/**
|
|
124
147
|
* Enable transitions when swapping between "load", "error" and "data" states
|
|
125
148
|
*/
|
|
@@ -139,16 +162,28 @@ export type PageDataLoaderConfig<T = unknown> = {
|
|
|
139
162
|
}
|
|
140
163
|
| {
|
|
141
164
|
/**
|
|
142
|
-
*
|
|
143
|
-
* @default "client"
|
|
144
|
-
* @description
|
|
145
|
-
* - **static**: The page data is loaded at build time and never updated
|
|
146
|
-
* - **client**: The page data is loaded upon navigation and updated on subsequent navigations
|
|
165
|
+
* Indicates that the page data should only be loaded at build time
|
|
147
166
|
*/
|
|
148
|
-
|
|
167
|
+
static: true
|
|
149
168
|
}
|
|
150
169
|
)
|
|
151
170
|
|
|
171
|
+
export type NavigationHook<T> = (
|
|
172
|
+
context: Kiru.RequestContext,
|
|
173
|
+
to: string,
|
|
174
|
+
from: string
|
|
175
|
+
) => T
|
|
176
|
+
|
|
177
|
+
export type OnBeforeEnterHook = NavigationHook<
|
|
178
|
+
string | void | Promise<string | void>
|
|
179
|
+
>
|
|
180
|
+
export type OnBeforeLeaveHook = NavigationHook<false | void>
|
|
181
|
+
|
|
182
|
+
interface PageContextHooks {
|
|
183
|
+
onBeforeEnter?: OnBeforeEnterHook | OnBeforeEnterHook[]
|
|
184
|
+
onBeforeLeave?: OnBeforeLeaveHook | OnBeforeLeaveHook[]
|
|
185
|
+
}
|
|
186
|
+
|
|
152
187
|
export interface PageConfig<T = unknown> {
|
|
153
188
|
/**
|
|
154
189
|
* The loader configuration for this page
|
|
@@ -159,6 +194,8 @@ export interface PageConfig<T = unknown> {
|
|
|
159
194
|
* returned, a page will be generated
|
|
160
195
|
*/
|
|
161
196
|
generateStaticParams?: () => RouteParams[] | Promise<RouteParams[]>
|
|
197
|
+
|
|
198
|
+
hooks?: PageContextHooks
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
export type PageProps<T extends PageConfig<any>> = T extends PageConfig<infer U>
|