@takazudo/zfb-runtime 0.1.0-next.9 → 0.1.0-next.90
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/CHANGELOG.md +6 -0
- package/README.md +220 -15
- package/dist/client-router/events.d.ts +0 -1
- package/dist/client-router/events.js +2 -2
- package/dist/client-router/events.js.map +1 -1
- package/dist/client-router/index.d.ts +2 -3
- package/dist/client-router/index.js +2 -2
- package/dist/client-router/index.js.map +1 -1
- package/dist/client-router/prefetch.d.ts +0 -1
- package/dist/client-router/prefetch.js +126 -53
- package/dist/client-router/prefetch.js.map +1 -1
- package/dist/client-router/router.d.ts +28 -2
- package/dist/client-router/router.js +364 -30
- package/dist/client-router/router.js.map +1 -1
- package/dist/client-router/swap-functions.d.ts +0 -1
- package/dist/client-router/swap-functions.js +71 -11
- package/dist/client-router/swap-functions.js.map +1 -1
- package/dist/client-router/types.d.ts +4 -1
- package/dist/client-router/types.js.map +1 -1
- package/dist/client-router.d.ts +38 -2
- package/dist/client-router.js +56 -20
- package/dist/client-router.js.map +1 -1
- package/dist/framework.d.ts +0 -1
- package/dist/framework.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +32 -2
- package/dist/router.js +163 -33
- package/dist/router.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +1 -0
- package/dist/snapshot.d.ts +0 -1
- package/dist/snapshot.js +3 -2
- package/dist/snapshot.js.map +1 -1
- package/dist/view-transitions.d.ts +0 -1
- package/dist/view-transitions.js.map +1 -1
- package/package.json +26 -4
- package/dist/client-router/cssesc.d.ts +0 -9
- package/dist/client-router/cssesc.d.ts.map +0 -1
- package/dist/client-router/cssesc.js +0 -95
- package/dist/client-router/cssesc.js.map +0 -1
- package/dist/client-router/events.d.ts.map +0 -1
- package/dist/client-router/index.d.ts.map +0 -1
- package/dist/client-router/prefetch.d.ts.map +0 -1
- package/dist/client-router/router.d.ts.map +0 -1
- package/dist/client-router/swap-functions.d.ts.map +0 -1
- package/dist/client-router/types.d.ts.map +0 -1
- package/dist/client-router.d.ts.map +0 -1
- package/dist/framework.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/snapshot.d.ts.map +0 -1
- package/dist/view-transitions.d.ts.map +0 -1
package/dist/client-router.js
CHANGED
|
@@ -12,21 +12,29 @@
|
|
|
12
12
|
// via `init()`. The component calls `init()` as a side effect on first import.
|
|
13
13
|
// No inline <script> dangerouslySetInnerHTML is emitted.
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
// `@takazudo/zfb-runtime` does not depend on
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// Framework-agnostic element minting (no JSX syntax):
|
|
16
|
+
// `@takazudo/zfb-runtime` does not depend on a framework runtime. Head nodes
|
|
17
|
+
// are minted by calling `jsx` from `react/jsx-runtime` directly — NOT JSX
|
|
18
|
+
// syntax, so this stays a plain `.ts` file with no tsconfig JSX changes. The
|
|
19
|
+
// engine alias-rewrites `react/jsx-runtime` → `preact/jsx-runtime` in Preact
|
|
20
|
+
// mode (bundler.rs ~2886) and resolves it natively in React mode, so the same
|
|
21
|
+
// call mints a real element for whichever framework the project configured.
|
|
22
|
+
// The previous approach (a hand-rolled `{ type, props, key, constructor:
|
|
23
|
+
// undefined }` object literal — the Preact diff-path sentinel) only worked for
|
|
24
|
+
// Preact: React's renderer rejects such an object as a child with React error
|
|
25
|
+
// #31 ("Objects are not valid as a React child"), because a real React element
|
|
26
|
+
// carries `$$typeof: Symbol.for("react.element")` a literal cannot fake. Same
|
|
27
|
+
// migration as `Island` in @takazudo/zfb.
|
|
22
28
|
//
|
|
23
|
-
// The component renders three sibling elements to <head
|
|
29
|
+
// The component renders three base sibling elements to <head> (plus optional
|
|
30
|
+
// metas emitted conditionally — see `prefetchDisabled` and `preserveHtmlAttrs`):
|
|
24
31
|
// 1. A <style> tag with the `.zfb-route-announcer` ARIA helper class.
|
|
25
32
|
// 2. <meta name="zfb-view-transitions-enabled" content="true" />
|
|
26
33
|
// 3. <meta name="zfb-view-transitions-fallback" content={fallback} />
|
|
27
34
|
//
|
|
28
35
|
// The route-announcer <div> is injected into <body> by `announce()` in
|
|
29
36
|
// `client-router/router.ts` on every navigation.
|
|
37
|
+
import { jsx } from "react/jsx-runtime";
|
|
30
38
|
import { init } from "./client-router/router.js";
|
|
31
39
|
import { init as prefetchInit } from "./client-router/prefetch.js";
|
|
32
40
|
// Side-effect: wire click + submit intercepts on first import of this component.
|
|
@@ -39,14 +47,23 @@ if (typeof document !== "undefined") {
|
|
|
39
47
|
// lifetime even if <ClientRouter prefetchAll /> is mounted multiple times (#276).
|
|
40
48
|
let prefetchBootstrapped = false;
|
|
41
49
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
50
|
+
* Mint a head element through the per-project JSX runtime.
|
|
51
|
+
*
|
|
52
|
+
* Calls `jsx` from `react/jsx-runtime` (alias-rewritten to
|
|
53
|
+
* `preact/jsx-runtime` in Preact mode by the engine, native in React mode)
|
|
54
|
+
* so the returned value is a real element for whichever framework the
|
|
55
|
+
* project configured — NOT a hand-rolled `{ type, props, key }` literal,
|
|
56
|
+
* which only Preact accepts and which makes React throw error #31. A stable
|
|
57
|
+
* `key` is passed because `ClientRouter()` returns these nodes in a plain
|
|
58
|
+
* array (React warns about keyless list children otherwise). Mirrors the
|
|
59
|
+
* `Island` migration in `@takazudo/zfb`.
|
|
45
60
|
*/
|
|
46
|
-
function makeVNode(type, props) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
function makeVNode(type, props, key) {
|
|
62
|
+
// `jsx`'s `type` param is typed `ElementType` (string-literal intrinsic
|
|
63
|
+
// tags or component types), which rejects an arbitrary runtime `string`.
|
|
64
|
+
// The tag is dynamic here, so cast to the factory's own first-param type —
|
|
65
|
+
// robust whether the engine aliases `jsx` to react or preact at build time.
|
|
66
|
+
return jsx(type, props, key);
|
|
50
67
|
}
|
|
51
68
|
// CSS for the route-announcer element. Ported verbatim from Astro's
|
|
52
69
|
// `<style is:global>` block in ClientRouter.astro (lines 11–23), renaming
|
|
@@ -81,7 +98,7 @@ const announcerCss = `
|
|
|
81
98
|
* <ClientRouter fallback="animate" />
|
|
82
99
|
* ```
|
|
83
100
|
*/
|
|
84
|
-
export function ClientRouter({ fallback = "animate", prefetchAll: prefetchAllProp = false, } = {}) {
|
|
101
|
+
export function ClientRouter({ fallback = "animate", prefetchAll: prefetchAllProp = false, preserveHtmlAttrs = [], traverseRefetch = false, } = {}) {
|
|
85
102
|
// Bootstrap prefetch exactly once on the client when prefetchAll is true.
|
|
86
103
|
// The initialized flag inside prefetchInit() provides a second safety layer
|
|
87
104
|
// in case of concurrent hydration or manual callers (#276).
|
|
@@ -91,11 +108,11 @@ export function ClientRouter({ fallback = "animate", prefetchAll: prefetchAllPro
|
|
|
91
108
|
}
|
|
92
109
|
const nodes = [
|
|
93
110
|
// Global styles for the ARIA route-announcer div injected into <body>.
|
|
94
|
-
makeVNode("style", { dangerouslySetInnerHTML: { __html: announcerCss } }),
|
|
111
|
+
makeVNode("style", { dangerouslySetInnerHTML: { __html: announcerCss } }, "zfb-vt-style"),
|
|
95
112
|
// Opt-in meta tag: router checks for this to decide whether to intercept navigations.
|
|
96
|
-
makeVNode("meta", { name: "zfb-view-transitions-enabled", content: "true" }),
|
|
113
|
+
makeVNode("meta", { name: "zfb-view-transitions-enabled", content: "true" }, "zfb-vt-enabled"),
|
|
97
114
|
// Fallback strategy meta tag: read by getFallback() in router.ts.
|
|
98
|
-
makeVNode("meta", { name: "zfb-view-transitions-fallback", content: fallback }),
|
|
115
|
+
makeVNode("meta", { name: "zfb-view-transitions-fallback", content: fallback }, "zfb-vt-fallback"),
|
|
99
116
|
];
|
|
100
117
|
// Prefetch-disabled meta tag (#277): emitted when the bundler set
|
|
101
118
|
// `globalThis.__zfb.prefetchDisabled = true` (from `zfb.config.ts`
|
|
@@ -110,7 +127,26 @@ export function ClientRouter({ fallback = "animate", prefetchAll: prefetchAllPro
|
|
|
110
127
|
// Pin the contract verbatim — the attribute names and content value are
|
|
111
128
|
// shared with the sibling prefetch-core sub-issue (#276).
|
|
112
129
|
if (globalThis.__zfb?.prefetchDisabled === true) {
|
|
113
|
-
nodes.push(makeVNode("meta", { name: "zfb-prefetch-disabled", content: "true" }));
|
|
130
|
+
nodes.push(makeVNode("meta", { name: "zfb-prefetch-disabled", content: "true" }, "zfb-prefetch-disabled"));
|
|
131
|
+
}
|
|
132
|
+
// Consumer-extensible <html> attribute preserve-list (#1103). swapRootAttributes
|
|
133
|
+
// reads this meta and re-applies the listed attributes' runtime values after each
|
|
134
|
+
// swap, so persisted-island state on <html> (data-theme, data-sidebar-hidden, …)
|
|
135
|
+
// survives navigation. Emitted only when non-empty, so non-opt-in output stays
|
|
136
|
+
// byte-identical. Same conditional-emission shape as the prefetch-disabled meta.
|
|
137
|
+
const preserveList = preserveHtmlAttrs.filter(Boolean);
|
|
138
|
+
if (preserveList.length > 0) {
|
|
139
|
+
nodes.push(makeVNode("meta", { name: "zfb-preserve-html-attrs", content: preserveList.join(" ") }, "zfb-preserve-html-attrs"));
|
|
140
|
+
}
|
|
141
|
+
// Traverse-refetch opt-out meta (#1376). A same-page Back/Forward traversal
|
|
142
|
+
// skips the re-fetch/re-swap by default (the router's traverse fast-path);
|
|
143
|
+
// this meta opts a per-request SSR page (`prerender = false`) back INTO the
|
|
144
|
+
// fetch, since skipping it would pin stale server-rendered content. Emitted
|
|
145
|
+
// only when opted in, so non-opt-in output stays byte-identical. Same
|
|
146
|
+
// conditional-emission shape as the prefetch-disabled / preserve-html-attrs
|
|
147
|
+
// metas; the router reads meta[name="zfb-traverse-refetch"][content="true"].
|
|
148
|
+
if (traverseRefetch) {
|
|
149
|
+
nodes.push(makeVNode("meta", { name: "zfb-traverse-refetch", content: "true" }, "zfb-traverse-refetch"));
|
|
114
150
|
}
|
|
115
151
|
return nodes;
|
|
116
152
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-router.js","sourceRoot":"","sources":["../src/client-router.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,EAAE;AACF,wDAAwD;AACxD,uDAAuD;AACvD,yEAAyE;AACzE,EAAE;AACF,2DAA2D;AAC3D,2EAA2E;AAC3E,6EAA6E;AAC7E,2EAA2E;AAC3E,+EAA+E;AAC/E,iFAAiF;AACjF,2DAA2D;AAC3D,EAAE;AACF,sFAAsF;AACtF,8EAA8E;AAC9E,gFAAgF;AAChF,6EAA6E;AAC7E,iFAAiF;AACjF,mFAAmF;AACnF,0EAA0E;AAC1E,EAAE;AACF,0DAA0D;AAC1D,wEAAwE;AACxE,mEAAmE;AACnE,wEAAwE;AACxE,EAAE;AACF,uEAAuE;AACvE,iDAAiD;AAEjD,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEnE,iFAAiF;AACjF,gFAAgF;AAChF,oEAAoE;AACpE,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;IACpC,IAAI,EAAE,CAAC;AACT,CAAC;AAED,8EAA8E;AAC9E,kFAAkF;AAClF,IAAI,oBAAoB,GAAG,KAAK,CAAC;AAuBjC;;;;GAIG;AACH,SAAS,SAAS,CAAC,IAAY,EAAE,KAA8B;IAC7D,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAKjC,CAAC;IACF,CAAC,CAAC,WAAW,GAAG,SAAS,CAAC;IAC1B,OAAO,CAAwB,CAAC;AAClC,CAAC;AAED,oEAAoE;AACpE,0EAA0E;AAC1E,gEAAgE;AAChE,uEAAuE;AACvE,+EAA+E;AAC/E,MAAM,YAAY,GAAG;;;;;;;;;;;;CAYpB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,QAAQ,GAAG,SAAS,EACpB,WAAW,EAAE,eAAe,GAAG,KAAK,MACf,EAAE;IACvB,0EAA0E;IAC1E,4EAA4E;IAC5E,4DAA4D;IAC5D,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,eAAe,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAChF,oBAAoB,GAAG,IAAI,CAAC;QAC5B,YAAY,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,KAAK,GAA0B;QACnC,uEAAuE;QACvE,SAAS,CAAC,OAAO,EAAE,EAAE,uBAAuB,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC;QACzE,sFAAsF;QACtF,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,8BAA8B,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAC5E,kEAAkE;QAClE,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,+BAA+B,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;KAChF,CAAC;IAEF,kEAAkE;IAClE,mEAAmE;IACnE,0EAA0E;IAC1E,iFAAiF;IACjF,gDAAgD;IAChD,EAAE;IACF,yEAAyE;IACzE,+EAA+E;IAC/E,iBAAiB;IACjB,EAAE;IACF,wEAAwE;IACxE,0DAA0D;IAC1D,IAAK,UAAyD,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,EAAE,CAAC;QAChG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"client-router.js","sourceRoot":"","sources":["../src/client-router.ts"],"names":[],"mappings":"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","sourcesContent":["// `@takazudo/zfb-runtime` — `<ClientRouter />` component.\n//\n// Ported from Astro's `ClientRouter.astro` (155 lines).\n// Source: packages/astro/components/ClientRouter.astro\n// Issue: zudolab/zudo-doc#1519 (W3D), parent epic zudolab/zudo-doc#1510.\n//\n// Named-cause deviation — inline script split (W1B §13.6):\n// Astro's <script> block is processed by Vite at build time as a bundled\n// module with virtual-module imports (`astro:transitions/client`). The zfb\n// port emits meta tags + global styles from the JSX component, while the\n// click/form intercepts live in `client-router/router.ts` and are registered\n// via `init()`. The component calls `init()` as a side effect on first import.\n// No inline <script> dangerouslySetInnerHTML is emitted.\n//\n// Framework-agnostic element minting (no JSX syntax):\n// `@takazudo/zfb-runtime` does not depend on a framework runtime. Head nodes\n// are minted by calling `jsx` from `react/jsx-runtime` directly — NOT JSX\n// syntax, so this stays a plain `.ts` file with no tsconfig JSX changes. The\n// engine alias-rewrites `react/jsx-runtime` → `preact/jsx-runtime` in Preact\n// mode (bundler.rs ~2886) and resolves it natively in React mode, so the same\n// call mints a real element for whichever framework the project configured.\n// The previous approach (a hand-rolled `{ type, props, key, constructor:\n// undefined }` object literal — the Preact diff-path sentinel) only worked for\n// Preact: React's renderer rejects such an object as a child with React error\n// #31 (\"Objects are not valid as a React child\"), because a real React element\n// carries `$$typeof: Symbol.for(\"react.element\")` a literal cannot fake. Same\n// migration as `Island` in @takazudo/zfb.\n//\n// The component renders three base sibling elements to <head> (plus optional\n// metas emitted conditionally — see `prefetchDisabled` and `preserveHtmlAttrs`):\n// 1. A <style> tag with the `.zfb-route-announcer` ARIA helper class.\n// 2. <meta name=\"zfb-view-transitions-enabled\" content=\"true\" />\n// 3. <meta name=\"zfb-view-transitions-fallback\" content={fallback} />\n//\n// The route-announcer <div> is injected into <body> by `announce()` in\n// `client-router/router.ts` on every navigation.\n\nimport { jsx } from \"react/jsx-runtime\";\n\nimport { init } from \"./client-router/router.js\";\nimport { init as prefetchInit } from \"./client-router/prefetch.js\";\n\n// Side-effect: wire click + submit intercepts on first import of this component.\n// Guarded by the idempotent `initialized` flag in router.ts — safe for multiple\n// <ClientRouter /> mounts and HMR re-runs. (W3C3 init idempotency.)\nif (typeof document !== \"undefined\") {\n init();\n}\n\n// Module-level guard: prefetch bootstrap is triggered at most once per module\n// lifetime even if <ClientRouter prefetchAll /> is mounted multiple times (#276).\nlet prefetchBootstrapped = false;\n\nexport interface ClientRouterProps {\n /** Fallback animation strategy when native View Transitions are not supported. */\n fallback?: \"none\" | \"animate\" | \"swap\";\n /**\n * When true, opts every same-origin link into the default prefetch strategy\n * (hover) by calling prefetchInit({ prefetchAll: true }) once on the client.\n */\n prefetchAll?: boolean;\n /**\n * Extra `<html>` attribute names to preserve across SPA swaps. By default the\n * client router copies the incoming server-rendered document's `<html>`\n * attributes onto the live root, dropping any current attribute that isn't\n * internal to the transition machinery — so a *runtime* attribute a consumer\n * sets from a persisted island (e.g. `data-theme` or `data-sidebar-hidden`\n * driven from `localStorage`) is lost on every navigation. List those names\n * here and the router re-applies their current value after each swap. Emitted\n * as a `<meta name=\"zfb-preserve-html-attrs\">` tag that `swapRootAttributes`\n * reads at swap time.\n *\n * Mount with the **same list on every page** that participates in SPA\n * navigation: the preserve-list is read from the *current* (outgoing) page's\n * meta at swap time, so a page that omits an entry drops that attribute when\n * navigating away from it. Names are matched case-insensitively (DOM attribute\n * names are lowercased). For dynamic/computed cases, mutate\n * `event.newDocument.documentElement` in a `zfb:before-swap` listener instead.\n * @see https://github.com/Takazudo/zudo-front-builder/issues/1103\n */\n preserveHtmlAttrs?: string[];\n /**\n * Opt this page OUT of the same-page traverse fast-path (default `false` —\n * fast-path ON). By default a Back/Forward traversal between two history\n * entries sharing the same `pathname + search` is served instantly from the\n * live DOM — no re-fetch, no re-swap, so island/client state is preserved.\n *\n * Set this on a **per-request SSR page** (`prerender = false`) whose\n * server-rendered content can legitimately differ between two visits to the\n * same URL: with the fast-path skipped such a traverse would pin the stale\n * first-render content. Emitted as\n * `<meta name=\"zfb-traverse-refetch\" content=\"true\">`, which the router reads\n * on the current (target) page to force the fetch back on. Mount with the\n * **same value on every page** that participates in SPA navigation, mirroring\n * the `preserveHtmlAttrs` guidance.\n * @see https://github.com/Takazudo/zudo-front-builder/issues/1376\n */\n traverseRefetch?: boolean;\n}\n\n/**\n * Public element shape for each node returned by `<ClientRouter />`.\n * Structural type — intentionally matches the Preact/React VNode object shape\n * so consumers do not type-infer through the internal representation.\n */\nexport type ClientRouterElement = {\n readonly type: string;\n readonly props: Readonly<Record<string, unknown>>;\n readonly key: unknown;\n};\n\n/**\n * Mint a head element through the per-project JSX runtime.\n *\n * Calls `jsx` from `react/jsx-runtime` (alias-rewritten to\n * `preact/jsx-runtime` in Preact mode by the engine, native in React mode)\n * so the returned value is a real element for whichever framework the\n * project configured — NOT a hand-rolled `{ type, props, key }` literal,\n * which only Preact accepts and which makes React throw error #31. A stable\n * `key` is passed because `ClientRouter()` returns these nodes in a plain\n * array (React warns about keyless list children otherwise). Mirrors the\n * `Island` migration in `@takazudo/zfb`.\n */\nfunction makeVNode(type: string, props: Record<string, unknown>, key: string): ClientRouterElement {\n // `jsx`'s `type` param is typed `ElementType` (string-literal intrinsic\n // tags or component types), which rejects an arbitrary runtime `string`.\n // The tag is dynamic here, so cast to the factory's own first-param type —\n // robust whether the engine aliases `jsx` to react or preact at build time.\n return jsx(type as Parameters<typeof jsx>[0], props, key) as unknown as ClientRouterElement;\n}\n\n// CSS for the route-announcer element. Ported verbatim from Astro's\n// `<style is:global>` block in ClientRouter.astro (lines 11–23), renaming\n// `.astro-route-announcer` → `.zfb-route-announcer` per W1B §5.\n// This is a global (non-scoped) <style> because the announcer <div> is\n// appended to document.body at runtime, outside any Preact-controlled subtree.\nconst announcerCss = `\n.zfb-route-announcer {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\tclip: rect(0 0 0 0);\n\tclip-path: inset(50%);\n\toverflow: hidden;\n\twhite-space: nowrap;\n\twidth: 1px;\n\theight: 1px;\n}\n`;\n\n/**\n * `<ClientRouter />` — SPA soft-swap navigation with View Transition animations.\n *\n * Mount once in your page `<head>`. Emits the opt-in meta tags and the global\n * `.zfb-route-announcer` stylesheet that the route-announcer ARIA div needs.\n * Click and form-submit intercepts are registered as a side effect of importing\n * this component (idempotent — safe to mount multiple times).\n *\n * @example\n * ```tsx\n * import { ClientRouter } from \"@takazudo/zfb-runtime\";\n * // In your page <head>:\n * <ClientRouter fallback=\"animate\" />\n * ```\n */\nexport function ClientRouter({\n fallback = \"animate\",\n prefetchAll: prefetchAllProp = false,\n preserveHtmlAttrs = [],\n traverseRefetch = false,\n}: ClientRouterProps = {}): readonly ClientRouterElement[] {\n // Bootstrap prefetch exactly once on the client when prefetchAll is true.\n // The initialized flag inside prefetchInit() provides a second safety layer\n // in case of concurrent hydration or manual callers (#276).\n if (typeof document !== \"undefined\" && prefetchAllProp && !prefetchBootstrapped) {\n prefetchBootstrapped = true;\n prefetchInit({ prefetchAll: true });\n }\n\n const nodes: ClientRouterElement[] = [\n // Global styles for the ARIA route-announcer div injected into <body>.\n makeVNode(\"style\", { dangerouslySetInnerHTML: { __html: announcerCss } }, \"zfb-vt-style\"),\n // Opt-in meta tag: router checks for this to decide whether to intercept navigations.\n makeVNode(\"meta\", { name: \"zfb-view-transitions-enabled\", content: \"true\" }, \"zfb-vt-enabled\"),\n // Fallback strategy meta tag: read by getFallback() in router.ts.\n makeVNode(\n \"meta\",\n { name: \"zfb-view-transitions-fallback\", content: fallback },\n \"zfb-vt-fallback\",\n ),\n ];\n\n // Prefetch-disabled meta tag (#277): emitted when the bundler set\n // `globalThis.__zfb.prefetchDisabled = true` (from `zfb.config.ts`\n // `prefetch: { disabled: true }`). The sibling prefetch-core module reads\n // `document.querySelector('meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]')`\n // at `init()` time and short-circuits if found.\n //\n // The flag is site-wide and static — set once at bundle-emit time, never\n // per-page. This meta tag appears on every page that mounts `<ClientRouter />`\n // or not at all.\n //\n // Pin the contract verbatim — the attribute names and content value are\n // shared with the sibling prefetch-core sub-issue (#276).\n if ((globalThis as { __zfb?: { prefetchDisabled?: boolean } }).__zfb?.prefetchDisabled === true) {\n nodes.push(\n makeVNode(\n \"meta\",\n { name: \"zfb-prefetch-disabled\", content: \"true\" },\n \"zfb-prefetch-disabled\",\n ),\n );\n }\n\n // Consumer-extensible <html> attribute preserve-list (#1103). swapRootAttributes\n // reads this meta and re-applies the listed attributes' runtime values after each\n // swap, so persisted-island state on <html> (data-theme, data-sidebar-hidden, …)\n // survives navigation. Emitted only when non-empty, so non-opt-in output stays\n // byte-identical. Same conditional-emission shape as the prefetch-disabled meta.\n const preserveList = preserveHtmlAttrs.filter(Boolean);\n if (preserveList.length > 0) {\n nodes.push(\n makeVNode(\n \"meta\",\n { name: \"zfb-preserve-html-attrs\", content: preserveList.join(\" \") },\n \"zfb-preserve-html-attrs\",\n ),\n );\n }\n\n // Traverse-refetch opt-out meta (#1376). A same-page Back/Forward traversal\n // skips the re-fetch/re-swap by default (the router's traverse fast-path);\n // this meta opts a per-request SSR page (`prerender = false`) back INTO the\n // fetch, since skipping it would pin stale server-rendered content. Emitted\n // only when opted in, so non-opt-in output stays byte-identical. Same\n // conditional-emission shape as the prefetch-disabled / preserve-html-attrs\n // metas; the router reads meta[name=\"zfb-traverse-refetch\"][content=\"true\"].\n if (traverseRefetch) {\n nodes.push(\n makeVNode(\"meta\", { name: \"zfb-traverse-refetch\", content: \"true\" }, \"zfb-traverse-refetch\"),\n );\n }\n\n return nodes;\n}\n"]}
|
package/dist/framework.d.ts
CHANGED
package/dist/framework.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework.js","sourceRoot":"","sources":["../src/framework.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,wEAAwE;AACxE,iEAAiE;AACjE,wEAAwE;AACxE,8DAA8D;AAC9D,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,2EAA2E;AAC3E,8EAA8E;AAC9E,EAAE;AACF,0EAA0E;AAC1E,kEAAkE;AAClE,4EAA4E"}
|
|
1
|
+
{"version":3,"file":"framework.js","sourceRoot":"","sources":["../src/framework.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,wEAAwE;AACxE,iEAAiE;AACjE,wEAAwE;AACxE,8DAA8D;AAC9D,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,2EAA2E;AAC3E,8EAA8E;AAC9E,EAAE;AACF,0EAA0E;AAC1E,kEAAkE;AAClE,4EAA4E","sourcesContent":["// `@takazudo/zfb-runtime` — framework adapter contract.\n//\n// The runtime is JSX-runtime-agnostic: it never imports preact or react\n// directly. Instead, the caller of [`createPageRouter`] passes a\n// [`FrameworkAdapter`] that pins how a JSX vnode (the value returned by\n// a page module's `default` export) becomes a string of HTML.\n//\n// This mirrors ADR-002's framework-adapter pattern on the Rust side — the\n// runtime owns the page-module evaluation lifecycle, the adapter owns the\n// JSX-runtime-specific render call. Wave 2 (T6) wires the embedded V8 host\n// with an adapter built on `preact-render-to-string` (or `react-dom/server`).\n//\n// BCI-5: `hydrate` reservation field removed — it was never called by the\n// runtime and no real consumer set it; adding it back when a real\n// implementation exists is a smaller change than carrying dead API surface.\n\n/**\n * Pluggable per-framework SSR contract.\n *\n * `renderToString` takes whatever shape the framework's JSX pragma\n * produces (Preact's VNode, React's element, etc.) and returns a string\n * of HTML. The runtime treats both the input and the return value as\n * opaque — it neither inspects the vnode nor escapes the output.\n */\nexport interface FrameworkAdapter {\n /**\n * Render a JSX vnode to an HTML string. Must be synchronous; SSR\n * happens inside a Hono request handler that resolves the response\n * body in one step.\n */\n renderToString: (vnode: unknown) => string;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
export { createPageRouter } from "./router.js";
|
|
2
1
|
export type { CreatePageRouterOptions, PageDefinition, PageHeading, PageModule, PageRouter, } from "./router.js";
|
|
3
2
|
export type { FrameworkAdapter } from "./framework.js";
|
|
4
3
|
export type { ContentSnapshot, EntrySnapshot } from "./snapshot.js";
|
|
5
4
|
export { ViewTransitions, type ViewTransitionsElement } from "./view-transitions.js";
|
|
6
5
|
export { ClientRouter, type ClientRouterProps } from "./client-router.js";
|
|
7
|
-
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, } from "./client-router/router.js";
|
|
6
|
+
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, syncHistoryEntry, } from "./client-router/router.js";
|
|
8
7
|
export { prefetch, init as prefetchInit } from "./client-router/prefetch.js";
|
|
9
8
|
export type { PrefetchStrategy, PrefetchInitOptions, PrefetchOptions, } from "./client-router/prefetch.js";
|
|
10
9
|
export { TRANSITION_BEFORE_PREPARATION, TRANSITION_AFTER_PREPARATION, TRANSITION_BEFORE_SWAP, TRANSITION_AFTER_SWAP, TRANSITION_PAGE_LOAD, TRANSITION_NAVIGATION_ABORTED, TransitionBeforePreparationEvent, TransitionBeforeSwapEvent, isTransitionBeforePreparationEvent, isTransitionBeforeSwapEvent, } from "./client-router/events.js";
|
|
11
10
|
export { swapFunctions, swap } from "./client-router/swap-functions.js";
|
|
12
|
-
export type { Direction, Fallback, NavigationTypeString, Options } from "./client-router/types.js";
|
|
11
|
+
export type { Direction, Fallback, NavigationTypeString, Options, SyncHistoryEntryOptions, } from "./client-router/types.js";
|
|
13
12
|
export type { ZfbPlugin, ZfbPluginLogger, ZfbBuildHookContext, ZfbDevMiddlewareContext, ZfbDevMiddlewareHandler, ZfbDevMiddlewareRequest, ZfbDevMiddlewareResponse, ZfbRouteEntry, ZfbRouteManifest, ZfbSetupContext, ZfbVirtualModuleLoader, } from "@takazudo/zfb/plugins";
|
|
14
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
// Public entry point for `@takazudo/zfb-runtime`.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// This is the **client-safe** barrel: everything re-exported here must be
|
|
4
|
+
// bundleable for `--platform=browser` without pulling a server-only
|
|
5
|
+
// dependency. In particular `createPageRouter` (and the Hono server router
|
|
6
|
+
// it builds) is intentionally NOT re-exported here — it lives at the
|
|
7
|
+
// server-only subpath `@takazudo/zfb-runtime/server`. Re-exporting it from
|
|
8
|
+
// this barrel makes any island that does `import ... from "@takazudo/zfb-runtime"`
|
|
9
|
+
// drag `hono` into the browser graph, which esbuild must then resolve
|
|
10
|
+
// (issue #1298). The page-router *types* below are `export type` only, so
|
|
11
|
+
// esbuild strips them and no runtime edge to `./router.js` survives.
|
|
12
|
+
//
|
|
13
|
+
// Worker bundles produced by T3 import the server subpath and invoke
|
|
4
14
|
// `createPageRouter` once, at module top, to obtain the fetch handler:
|
|
5
15
|
//
|
|
6
|
-
// import { createPageRouter } from "@takazudo/zfb-runtime";
|
|
16
|
+
// import { createPageRouter } from "@takazudo/zfb-runtime/server";
|
|
7
17
|
//
|
|
8
18
|
// const router = createPageRouter({
|
|
9
19
|
// pages: [...],
|
|
@@ -14,12 +24,11 @@
|
|
|
14
24
|
// export default { fetch: router };
|
|
15
25
|
//
|
|
16
26
|
// The README documents the bundle shape T6 (embedded V8 host) consumes.
|
|
17
|
-
export { createPageRouter } from "./router.js";
|
|
18
27
|
export { ViewTransitions } from "./view-transitions.js";
|
|
19
28
|
// Client-router public surface (W3D — mirrors @takazudo/zfb-runtime/client-router barrel).
|
|
20
29
|
// See W1B §2 for the full public API spec.
|
|
21
30
|
export { ClientRouter } from "./client-router.js";
|
|
22
|
-
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, } from "./client-router/router.js";
|
|
31
|
+
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, syncHistoryEntry, } from "./client-router/router.js";
|
|
23
32
|
// Prefetch public surface (#276).
|
|
24
33
|
// `init` from prefetch.ts is re-exported as `prefetchInit` to avoid collision
|
|
25
34
|
// with the router's `init`.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,EAAE;AACF,0EAA0E;AAC1E,oEAAoE;AACpE,2EAA2E;AAC3E,qEAAqE;AACrE,2EAA2E;AAC3E,mFAAmF;AACnF,sEAAsE;AACtE,0EAA0E;AAC1E,qEAAqE;AACrE,EAAE;AACF,qEAAqE;AACrE,uEAAuE;AACvE,EAAE;AACF,qEAAqE;AACrE,EAAE;AACF,sCAAsC;AACtC,oBAAoB;AACpB,4EAA4E;AAC5E,iFAAiF;AACjF,QAAQ;AACR,EAAE;AACF,sCAAsC;AACtC,EAAE;AACF,wEAAwE;AAWxE,OAAO,EAAE,eAAe,EAA+B,MAAM,uBAAuB,CAAC;AAErF,2FAA2F;AAC3F,2CAA2C;AAC3C,OAAO,EAAE,YAAY,EAA0B,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EACL,QAAQ,EACR,uBAAuB,EACvB,2BAA2B,EAC3B,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AAEnC,kCAAkC;AAClC,8EAA8E;AAC9E,4BAA4B;AAC5B,OAAO,EAAE,QAAQ,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAM7E,OAAO,EACL,6BAA6B,EAC7B,4BAA4B,EAC5B,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,6BAA6B,EAC7B,gCAAgC,EAChC,yBAAyB,EACzB,kCAAkC,EAClC,2BAA2B,GAC5B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,mCAAmC,CAAC","sourcesContent":["// Public entry point for `@takazudo/zfb-runtime`.\n//\n// This is the **client-safe** barrel: everything re-exported here must be\n// bundleable for `--platform=browser` without pulling a server-only\n// dependency. In particular `createPageRouter` (and the Hono server router\n// it builds) is intentionally NOT re-exported here — it lives at the\n// server-only subpath `@takazudo/zfb-runtime/server`. Re-exporting it from\n// this barrel makes any island that does `import ... from \"@takazudo/zfb-runtime\"`\n// drag `hono` into the browser graph, which esbuild must then resolve\n// (issue #1298). The page-router *types* below are `export type` only, so\n// esbuild strips them and no runtime edge to `./router.js` survives.\n//\n// Worker bundles produced by T3 import the server subpath and invoke\n// `createPageRouter` once, at module top, to obtain the fetch handler:\n//\n// import { createPageRouter } from \"@takazudo/zfb-runtime/server\";\n//\n// const router = createPageRouter({\n// pages: [...],\n// contentSnapshot: __ZFB_CONTENT_SNAPSHOT__, // embedded by the bundler\n// framework: { renderToString: render }, // preact-render-to-string etc.\n// });\n//\n// export default { fetch: router };\n//\n// The README documents the bundle shape T6 (embedded V8 host) consumes.\n\nexport type {\n CreatePageRouterOptions,\n PageDefinition,\n PageHeading,\n PageModule,\n PageRouter,\n} from \"./router.js\";\nexport type { FrameworkAdapter } from \"./framework.js\";\nexport type { ContentSnapshot, EntrySnapshot } from \"./snapshot.js\";\nexport { ViewTransitions, type ViewTransitionsElement } from \"./view-transitions.js\";\n\n// Client-router public surface (W3D — mirrors @takazudo/zfb-runtime/client-router barrel).\n// See W1B §2 for the full public API spec.\nexport { ClientRouter, type ClientRouterProps } from \"./client-router.js\";\nexport {\n navigate,\n supportsViewTransitions,\n transitionEnabledOnThisPage,\n syncHistoryEntry,\n} from \"./client-router/router.js\";\n\n// Prefetch public surface (#276).\n// `init` from prefetch.ts is re-exported as `prefetchInit` to avoid collision\n// with the router's `init`.\nexport { prefetch, init as prefetchInit } from \"./client-router/prefetch.js\";\nexport type {\n PrefetchStrategy,\n PrefetchInitOptions,\n PrefetchOptions,\n} from \"./client-router/prefetch.js\";\nexport {\n TRANSITION_BEFORE_PREPARATION,\n TRANSITION_AFTER_PREPARATION,\n TRANSITION_BEFORE_SWAP,\n TRANSITION_AFTER_SWAP,\n TRANSITION_PAGE_LOAD,\n TRANSITION_NAVIGATION_ABORTED,\n TransitionBeforePreparationEvent,\n TransitionBeforeSwapEvent,\n isTransitionBeforePreparationEvent,\n isTransitionBeforeSwapEvent,\n} from \"./client-router/events.js\";\nexport { swapFunctions, swap } from \"./client-router/swap-functions.js\";\nexport type {\n Direction,\n Fallback,\n NavigationTypeString,\n Options,\n SyncHistoryEntryOptions,\n} from \"./client-router/types.js\";\n\n// Plugin lifecycle types (#255). The runtime package re-exports the\n// `@takazudo/zfb/plugins` surface so consumers writing plugins from a\n// project that only depends on `@takazudo/zfb-runtime` can still\n// import the canonical types (e.g. `import type { ZfbPlugin } from\n// \"@takazudo/zfb-runtime\"`).\nexport type {\n ZfbPlugin,\n ZfbPluginLogger,\n ZfbBuildHookContext,\n ZfbDevMiddlewareContext,\n ZfbDevMiddlewareHandler,\n ZfbDevMiddlewareRequest,\n ZfbDevMiddlewareResponse,\n ZfbRouteEntry,\n ZfbRouteManifest,\n ZfbSetupContext,\n ZfbVirtualModuleLoader,\n} from \"@takazudo/zfb/plugins\";\n"]}
|
package/dist/router.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export interface PageHeading {
|
|
|
16
16
|
* - `default`: the JSX page component. Called with the props returned by
|
|
17
17
|
* `getStaticProps` (if exported) or the `props` from the matching
|
|
18
18
|
* `paths()` entry (for dynamic routes). The return value is fed straight
|
|
19
|
-
* to the framework adapter's `renderToString`.
|
|
19
|
+
* to the framework adapter's `renderToString`. A dynamic route with no
|
|
20
|
+
* `paths()` export is called with `{ params: urlParams }` instead of
|
|
21
|
+
* `{}` — see `createPageRouter`'s JSDoc for the full componentInput
|
|
22
|
+
* derivation table.
|
|
20
23
|
* - `prerender`: literal `false` opts a route OUT of build-time SSG (T5
|
|
21
24
|
* contract). The page router still serves it under the embedded V8 host
|
|
22
25
|
* so dev mode behaves identically; SSG callers filter the route list before
|
|
@@ -30,6 +33,12 @@ export interface PageHeading {
|
|
|
30
33
|
* for this route template. May be async. Returns an array of
|
|
31
34
|
* `{ params, props? }` objects identical in shape to the Astro/zfb
|
|
32
35
|
* `paths()` contract.
|
|
36
|
+
* **Evaluated once per router instance** — the result is memoised and
|
|
37
|
+
* shared across the `/__paths__` handler and all per-page render
|
|
38
|
+
* requests. This matches the build-time-enumerator contract: every
|
|
39
|
+
* entry rendered within a single build sees the same paths() snapshot.
|
|
40
|
+
* Dev mode is safe because each file-save triggers a fresh bundle,
|
|
41
|
+
* which creates a new router instance with a clean memo.
|
|
33
42
|
* - `getStaticProps`: optional async function for static routes that need
|
|
34
43
|
* to fetch data at build/render time. Called once per request (before
|
|
35
44
|
* `default`). Must return `{ props: Record<string, unknown> }`. The
|
|
@@ -69,6 +78,17 @@ export interface CreatePageRouterOptions {
|
|
|
69
78
|
readonly contentSnapshot: ContentSnapshot;
|
|
70
79
|
/** Framework adapter pinning the SSR call. */
|
|
71
80
|
readonly framework: FrameworkAdapter;
|
|
81
|
+
/**
|
|
82
|
+
* When `true`, the 500 body for render errors includes the full JS stack
|
|
83
|
+
* trace. When `false`, only the message + route are included. When
|
|
84
|
+
* omitted, the runtime checks `globalThis.__zfb.ssrDebug` at request time:
|
|
85
|
+
* that flag is set by the embedded V8 build/dev host (`globals_shim.js`)
|
|
86
|
+
* and is absent on the production Cloudflare Workers runtime.
|
|
87
|
+
*
|
|
88
|
+
* Default is effectively OFF for production (no flag ⇒ message + route only).
|
|
89
|
+
* Useful for explicit injection in unit tests without global mutation.
|
|
90
|
+
*/
|
|
91
|
+
readonly includeErrorStack?: boolean;
|
|
72
92
|
}
|
|
73
93
|
/**
|
|
74
94
|
* Fetch-handler shape returned by [`createPageRouter`]. Shaped as a plain
|
|
@@ -89,9 +109,19 @@ export type PageRouter = (request: Request) => Promise<Response>;
|
|
|
89
109
|
* `framework.renderToString(module.default({}))`, and returns the
|
|
90
110
|
* string in a `Response` with the appropriate `Content-Type`.
|
|
91
111
|
*
|
|
112
|
+
* Per-route `componentInput` (the object passed to `module.default(...)`) is
|
|
113
|
+
* derived from the route pattern and the page module's exports:
|
|
114
|
+
* - Dynamic route + `paths()` export: match the URL params against the
|
|
115
|
+
* `paths()` entries and pass `{ params, ...props }` (404 on no match).
|
|
116
|
+
* - Static route + `getStaticProps()` export: pass the returned `props`.
|
|
117
|
+
* - Dynamic route with NO `paths()` export (e.g. a per-request SSR page
|
|
118
|
+
* whose slugs can't be enumerated ahead of time): pass `{ params:
|
|
119
|
+
* urlParams }` so the component still knows which URL params it is
|
|
120
|
+
* serving, rather than being invoked with `{}`.
|
|
121
|
+
* - Anything else (static route, no `getStaticProps`): pass `{}`.
|
|
122
|
+
*
|
|
92
123
|
* The returned function is a plain `(request) => Promise<Response>` so a
|
|
93
124
|
* Worker entry point can `export default { fetch: createPageRouter(...) }`
|
|
94
125
|
* directly.
|
|
95
126
|
*/
|
|
96
127
|
export declare function createPageRouter(opts: CreatePageRouterOptions): PageRouter;
|
|
97
|
-
//# sourceMappingURL=router.d.ts.map
|
package/dist/router.js
CHANGED
|
@@ -77,6 +77,26 @@ function ensureHtml5Doctype(body, contentType) {
|
|
|
77
77
|
}
|
|
78
78
|
return body;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate `pathsFn` once per `routeKey`, sharing the result across all
|
|
82
|
+
* concurrent and subsequent callers via a Promise stored in `memo`.
|
|
83
|
+
*
|
|
84
|
+
* Rejections are evicted from the memo so that a transient paths() failure
|
|
85
|
+
* does not permanently poison the cache — the next request retries.
|
|
86
|
+
*/
|
|
87
|
+
async function getOrEvalPaths(memo, routeKey, pathsFn) {
|
|
88
|
+
const existing = memo.get(routeKey);
|
|
89
|
+
if (existing !== undefined) {
|
|
90
|
+
return existing;
|
|
91
|
+
}
|
|
92
|
+
const promise = Promise.resolve(pathsFn()).then((v) => v, (err) => {
|
|
93
|
+
// Evict on rejection so the next request retries.
|
|
94
|
+
memo.delete(routeKey);
|
|
95
|
+
return Promise.reject(err);
|
|
96
|
+
});
|
|
97
|
+
memo.set(routeKey, promise);
|
|
98
|
+
return promise;
|
|
99
|
+
}
|
|
80
100
|
/**
|
|
81
101
|
* Build a page router for the SSG-first architecture (ADR-005).
|
|
82
102
|
*
|
|
@@ -90,6 +110,17 @@ function ensureHtml5Doctype(body, contentType) {
|
|
|
90
110
|
* `framework.renderToString(module.default({}))`, and returns the
|
|
91
111
|
* string in a `Response` with the appropriate `Content-Type`.
|
|
92
112
|
*
|
|
113
|
+
* Per-route `componentInput` (the object passed to `module.default(...)`) is
|
|
114
|
+
* derived from the route pattern and the page module's exports:
|
|
115
|
+
* - Dynamic route + `paths()` export: match the URL params against the
|
|
116
|
+
* `paths()` entries and pass `{ params, ...props }` (404 on no match).
|
|
117
|
+
* - Static route + `getStaticProps()` export: pass the returned `props`.
|
|
118
|
+
* - Dynamic route with NO `paths()` export (e.g. a per-request SSR page
|
|
119
|
+
* whose slugs can't be enumerated ahead of time): pass `{ params:
|
|
120
|
+
* urlParams }` so the component still knows which URL params it is
|
|
121
|
+
* serving, rather than being invoked with `{}`.
|
|
122
|
+
* - Anything else (static route, no `getStaticProps`): pass `{}`.
|
|
123
|
+
*
|
|
93
124
|
* The returned function is a plain `(request) => Promise<Response>` so a
|
|
94
125
|
* Worker entry point can `export default { fetch: createPageRouter(...) }`
|
|
95
126
|
* directly.
|
|
@@ -104,6 +135,21 @@ export function createPageRouter(opts) {
|
|
|
104
135
|
for (const page of opts.pages) {
|
|
105
136
|
pagesByRoute.set(page.route, page);
|
|
106
137
|
}
|
|
138
|
+
// Per-router-instance memo for paths() results.
|
|
139
|
+
//
|
|
140
|
+
// Keyed on page.route (e.g. "/blog/:slug"). Stores the in-flight or
|
|
141
|
+
// settled Promise so that concurrent requests share one evaluation.
|
|
142
|
+
// Rejections are NOT cached: if paths() throws the Promise is removed
|
|
143
|
+
// from the map so the next request retries rather than re-propagating
|
|
144
|
+
// the same error indefinitely.
|
|
145
|
+
//
|
|
146
|
+
// Production SSR isolates evaluate paths() once per isolate, which
|
|
147
|
+
// matches the documented build-time-enumerator contract. Dev mode is
|
|
148
|
+
// safe because each re-bundle creates a fresh router instance
|
|
149
|
+
// (dev.rs reload_renderer), so stale paths() results never persist
|
|
150
|
+
// across a file-save cycle. Watch-time invalidation is explicitly
|
|
151
|
+
// out of scope per issue #507.
|
|
152
|
+
const pathsMemo = new Map();
|
|
107
153
|
// Sanity check: a user-authored route that happens to start with
|
|
108
154
|
// `/__paths__` (or a top-level catchall like `/:slug{.+}`) would
|
|
109
155
|
// shadow the synthetic endpoint registered below if Hono dispatched
|
|
@@ -153,7 +199,7 @@ export function createPageRouter(opts) {
|
|
|
153
199
|
}
|
|
154
200
|
let result;
|
|
155
201
|
try {
|
|
156
|
-
result = await mod.paths
|
|
202
|
+
result = await getOrEvalPaths(pathsMemo, routeKey, mod.paths);
|
|
157
203
|
}
|
|
158
204
|
catch (err) {
|
|
159
205
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -193,14 +239,28 @@ export function createPageRouter(opts) {
|
|
|
193
239
|
// an empty object — the component signature has no required props.
|
|
194
240
|
// For dynamic routes whose URL params do not match any paths()
|
|
195
241
|
// entry, we return a 404 rather than rendering with empty props.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
242
|
+
//
|
|
243
|
+
// Whether the route is dynamic is derived from the ROUTE PATTERN,
|
|
244
|
+
// not from the captured params: for an optional catchall
|
|
245
|
+
// (`/docs/:slug{.+}?`), Hono matches the bare `/docs` with NO
|
|
246
|
+
// params captured at all, so `Object.keys(c.req.param())` would
|
|
247
|
+
// be empty and the old gate skipped paths() entirely — rendering
|
|
248
|
+
// with `{}` instead of the entry whose `slug` is `[]`.
|
|
249
|
+
const declaredParams = routeParamSpecs(page.route);
|
|
250
|
+
const rawUrlParams = c.req.param() ?? {};
|
|
251
|
+
const urlParams = {};
|
|
252
|
+
for (const [k, v] of Object.entries(rawUrlParams)) {
|
|
253
|
+
// Unmatched optional params may surface as undefined — drop them
|
|
254
|
+
// so "param absent" is represented uniformly.
|
|
255
|
+
if (typeof v === "string")
|
|
256
|
+
urlParams[k] = v;
|
|
257
|
+
}
|
|
258
|
+
const isDynamicRoute = declaredParams.length > 0;
|
|
199
259
|
let componentInput = {};
|
|
200
|
-
if (
|
|
260
|
+
if (isDynamicRoute && typeof mod.paths === "function") {
|
|
201
261
|
let pathsResult;
|
|
202
262
|
try {
|
|
203
|
-
pathsResult = await mod.paths
|
|
263
|
+
pathsResult = await getOrEvalPaths(pathsMemo, page.route, mod.paths);
|
|
204
264
|
}
|
|
205
265
|
catch (err) {
|
|
206
266
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -226,15 +286,27 @@ export function createPageRouter(opts) {
|
|
|
226
286
|
// /docs/[...slug]), Hono returns a slash-joined string (e.g.
|
|
227
287
|
// "guides/install"), so we compare against the paths() entry's
|
|
228
288
|
// params.slug joined with "/".
|
|
289
|
+
//
|
|
290
|
+
// Matching iterates the params DECLARED by the route pattern so
|
|
291
|
+
// the zero-segment optional-catchall case is covered: when Hono
|
|
292
|
+
// matched `/docs` for `/docs/:slug{.+}?` no `slug` param exists
|
|
293
|
+
// in the URL, and the matching entry is the one whose param is
|
|
294
|
+
// the explicit empty array (`{ slug: [] }`).
|
|
229
295
|
const match = pathsResult.find((entry) => {
|
|
230
|
-
return
|
|
231
|
-
const
|
|
296
|
+
return declaredParams.every(({ name, optionalCatchall }) => {
|
|
297
|
+
const urlVal = urlParams[name];
|
|
298
|
+
const paramVal = entry.params[name];
|
|
299
|
+
if (urlVal === undefined) {
|
|
300
|
+
// Param absent from the URL: only valid for an optional
|
|
301
|
+
// catchall, and only against the explicit `[]` entry.
|
|
302
|
+
return optionalCatchall && Array.isArray(paramVal) && paramVal.length === 0;
|
|
303
|
+
}
|
|
232
304
|
if (Array.isArray(paramVal)) {
|
|
233
305
|
// catchall: paths() emits slug as string[] but Hono
|
|
234
306
|
// provides it as a "/"-joined string
|
|
235
|
-
return paramVal.join("/") ===
|
|
307
|
+
return paramVal.join("/") === urlVal;
|
|
236
308
|
}
|
|
237
|
-
return String(paramVal) ===
|
|
309
|
+
return String(paramVal) === urlVal;
|
|
238
310
|
});
|
|
239
311
|
});
|
|
240
312
|
if (!match) {
|
|
@@ -253,7 +325,17 @@ export function createPageRouter(opts) {
|
|
|
253
325
|
...(match.props ?? {}),
|
|
254
326
|
};
|
|
255
327
|
}
|
|
256
|
-
else if (
|
|
328
|
+
else if (isDynamicRoute) {
|
|
329
|
+
// Dynamic route with no `paths()` export: the natural shape for a
|
|
330
|
+
// per-request SSR page whose slugs can't be enumerated ahead of
|
|
331
|
+
// time. Neither branch above applies (no paths() to match against,
|
|
332
|
+
// and getStaticProps only fires for static routes), so without this
|
|
333
|
+
// the component would be invoked with `{}` — no params, no
|
|
334
|
+
// diagnostic, and no way to know which slug it is serving. Pass the
|
|
335
|
+
// URL params through directly instead.
|
|
336
|
+
componentInput = { params: urlParams };
|
|
337
|
+
}
|
|
338
|
+
else if (!isDynamicRoute && typeof mod.getStaticProps === "function") {
|
|
257
339
|
// Static route with `getStaticProps`: call it to fetch build-time
|
|
258
340
|
// data and pass the returned props to the default component. This
|
|
259
341
|
// supports the `export async function getStaticProps()` pattern
|
|
@@ -279,24 +361,44 @@ export function createPageRouter(opts) {
|
|
|
279
361
|
// `await` so async page modules (e.g. API routes typed as
|
|
280
362
|
// `(): Promise<Response>`) resolve before we inspect the value.
|
|
281
363
|
// For sync pages that return a VNode/string the await is a no-op.
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
364
|
+
//
|
|
365
|
+
// Wrapped in a try/catch so that any error thrown by the component
|
|
366
|
+
// or by renderToString surfaces as a descriptive 500 rather than
|
|
367
|
+
// escaping to Hono's generic error handler (which discards the real
|
|
368
|
+
// message). Mirrors the getStaticProps catch above.
|
|
369
|
+
try {
|
|
370
|
+
const result = await mod.default(componentInput);
|
|
371
|
+
// API route short-circuit: a page module that returns a Response
|
|
372
|
+
// directly (e.g. `pages/api/*.tsx` handlers that use Web Fetch
|
|
373
|
+
// primitives instead of returning JSX) is responsible for its own
|
|
374
|
+
// status, headers, and body — return it as-is rather than running
|
|
375
|
+
// it through the framework SSR path. A `return` from inside a
|
|
376
|
+
// `try` does not trigger the `catch`, so this passes through
|
|
377
|
+
// correctly without special-casing.
|
|
378
|
+
if (result instanceof Response) {
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
// Non-HTML routes (e.g. `sitemap.xml.tsx`, `feed.xml.tsx`) commonly
|
|
382
|
+
// return their body as a pre-serialised `string` instead of a
|
|
383
|
+
// VNode. Routing those through `framework.renderToString` would
|
|
384
|
+
// HTML-escape the angle brackets and ampersands, producing
|
|
385
|
+
// garbage XML. Pass strings through verbatim; only wrap actual
|
|
386
|
+
// VNodes.
|
|
387
|
+
const html = typeof result === "string" ? result : opts.framework.renderToString(result);
|
|
388
|
+
const contentType = mod.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
389
|
+
return c.body(ensureHtml5Doctype(html, contentType), 200, { "Content-Type": contentType });
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
393
|
+
// __zfb.ssrDebug is set only by the embedded V8 build/dev host
|
|
394
|
+
// (globals_shim.js), never by the production Workers runtime.
|
|
395
|
+
const includeStack = opts.includeErrorStack ??
|
|
396
|
+
globalThis.__zfb?.ssrDebug === true;
|
|
397
|
+
const stack = includeStack && err instanceof Error && err.stack ? `\n${err.stack}` : "";
|
|
398
|
+
return c.body(`[zfb-runtime] render threw for "${page.route}": ${msg}${stack}`, 500, {
|
|
399
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
400
|
+
});
|
|
290
401
|
}
|
|
291
|
-
// Non-HTML routes (e.g. `sitemap.xml.tsx`, `feed.xml.tsx`) commonly
|
|
292
|
-
// return their body as a pre-serialised `string` instead of a
|
|
293
|
-
// VNode. Routing those through `framework.renderToString` would
|
|
294
|
-
// HTML-escape the angle brackets and ampersands, producing
|
|
295
|
-
// garbage XML. Pass strings through verbatim; only wrap actual
|
|
296
|
-
// VNodes.
|
|
297
|
-
const html = typeof result === "string" ? result : opts.framework.renderToString(result);
|
|
298
|
-
const contentType = mod.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
299
|
-
return c.body(ensureHtml5Doctype(html, contentType), 200, { "Content-Type": contentType });
|
|
300
402
|
});
|
|
301
403
|
}
|
|
302
404
|
// Hono's `app.fetch` returns `Response | Promise<Response>`. The
|
|
@@ -317,14 +419,39 @@ function isPathsEntry(x) {
|
|
|
317
419
|
const params = x.params;
|
|
318
420
|
return typeof params === "object" && params !== null;
|
|
319
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Parse the params declared by a Hono route pattern (the `route` string
|
|
424
|
+
* the build pipeline registers, e.g. `/blog/:slug`, `/docs/:slug{.+}`,
|
|
425
|
+
* `/docs/:slug{.+}?`). Static routes return an empty list.
|
|
426
|
+
*
|
|
427
|
+
* Only the pattern shapes emitted by `bracket_to_hono` (zfb-build) are
|
|
428
|
+
* recognised — `:name`, `:name{.+}`, `:name{.+}?` — which is the entire
|
|
429
|
+
* grammar the file router produces.
|
|
430
|
+
*/
|
|
431
|
+
function routeParamSpecs(route) {
|
|
432
|
+
const specs = [];
|
|
433
|
+
for (const seg of route.split("/")) {
|
|
434
|
+
if (!seg.startsWith(":"))
|
|
435
|
+
continue;
|
|
436
|
+
const optionalCatchall = seg.endsWith("?");
|
|
437
|
+
const body = optionalCatchall ? seg.slice(1, -1) : seg.slice(1);
|
|
438
|
+
const braceIdx = body.indexOf("{");
|
|
439
|
+
const name = braceIdx === -1 ? body : body.slice(0, braceIdx);
|
|
440
|
+
if (name.length > 0) {
|
|
441
|
+
specs.push({ name, optionalCatchall });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return specs;
|
|
445
|
+
}
|
|
320
446
|
/**
|
|
321
447
|
* Heuristic check for user-authored routes that may shadow the
|
|
322
448
|
* synthetic `/__paths__/<encoded-route-key>` endpoint.
|
|
323
449
|
*
|
|
324
450
|
* Returns true when the route literal contains `/__paths__` (an
|
|
325
451
|
* obvious collision) or when its first segment is a Hono catchall
|
|
326
|
-
* (`:name{.+}
|
|
327
|
-
*
|
|
452
|
+
* (`:name{.+}`, optionally `:name{.+}?`) or a file-system catchall
|
|
453
|
+
* (`[...name]` / `[[...name]]`) at the root — those would match
|
|
454
|
+
* `/__paths__/...` once Hono dispatches to them.
|
|
328
455
|
*/
|
|
329
456
|
function routeShadowsPathsEndpoint(route) {
|
|
330
457
|
if (route.includes("/__paths__")) {
|
|
@@ -334,14 +461,17 @@ function routeShadowsPathsEndpoint(route) {
|
|
|
334
461
|
// anything deeper cannot match `/__paths__` because the literal
|
|
335
462
|
// first segment differs.
|
|
336
463
|
const firstSeg = route.replace(/^\/+/, "").split("/")[0] ?? "";
|
|
337
|
-
// Hono regex-quantifier catchall: `:name{.+}
|
|
338
|
-
if (/^:[A-Za-z_][\w]*\{\.[+*]\}
|
|
464
|
+
// Hono regex-quantifier catchall: `:name{.+}` / optional `:name{.+}?`.
|
|
465
|
+
if (/^:[A-Za-z_][\w]*\{\.[+*]\}\??$/.test(firstSeg)) {
|
|
339
466
|
return true;
|
|
340
467
|
}
|
|
341
|
-
// File-system catchall (pre-bracket-to-hono): `[...name]`.
|
|
468
|
+
// File-system catchall (pre-bracket-to-hono): `[...name]` / `[[...name]]`.
|
|
342
469
|
if (/^\[\.\.\.[A-Za-z_][\w]*\]$/.test(firstSeg)) {
|
|
343
470
|
return true;
|
|
344
471
|
}
|
|
472
|
+
if (/^\[\[\.\.\.[A-Za-z_][\w]*\]\]$/.test(firstSeg)) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
345
475
|
return false;
|
|
346
476
|
}
|
|
347
477
|
//# sourceMappingURL=router.js.map
|