@takazudo/zfb-runtime 2.5.2 → 2.7.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/README.md +47 -16
- package/dist/client-router/events.js +2 -1
- package/dist/client-router/events.js.map +1 -1
- package/dist/client-router/history-safe.d.ts +2 -0
- package/dist/client-router/history-safe.js +34 -0
- package/dist/client-router/history-safe.js.map +1 -0
- package/dist/client-router/index.js +5 -1
- package/dist/client-router/index.js.map +1 -1
- package/dist/client-router/router.d.ts +8 -2
- package/dist/client-router/router.js +187 -99
- package/dist/client-router/router.js.map +1 -1
- package/dist/client-router-component.d.ts +74 -0
- package/dist/client-router-component.js +157 -0
- package/dist/client-router-component.js.map +1 -0
- package/dist/client-router.d.ts +1 -72
- package/dist/client-router.js +19 -146
- package/dist/client-router.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/snapshot.d.ts +31 -0
- package/dist/snapshot.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// `@takazudo/zfb-runtime` — `<ClientRouter />` component (pure module).
|
|
2
|
+
//
|
|
3
|
+
// Ported from Astro's `ClientRouter.astro` (155 lines).
|
|
4
|
+
// Source: packages/astro/components/ClientRouter.astro
|
|
5
|
+
// Issue: zudolab/zudo-doc#1519 (W3D), parent epic zudolab/zudo-doc#1510.
|
|
6
|
+
//
|
|
7
|
+
// Named-cause deviation — inline script split (W1B §13.6):
|
|
8
|
+
// Astro's <script> block is processed by Vite at build time as a bundled
|
|
9
|
+
// module with virtual-module imports (`astro:transitions/client`). The zfb
|
|
10
|
+
// port emits meta tags + global styles from the JSX component, while the
|
|
11
|
+
// click/form intercepts live in `client-router/router.ts` and are registered
|
|
12
|
+
// via `init()`. No inline <script> dangerouslySetInnerHTML is emitted.
|
|
13
|
+
//
|
|
14
|
+
// Component/activation split (#2437): this module is the pure component —
|
|
15
|
+
// it imports ONLY `react/jsx-runtime` and `./client-router/prefetch.js`
|
|
16
|
+
// (`prefetch.ts` has zero module-scope side effects), never
|
|
17
|
+
// `./client-router/router.js`, and performs no top-level `document`/`window`
|
|
18
|
+
// access. Importing it — including via the root barrel `@takazudo/zfb-runtime`
|
|
19
|
+
// — registers no listeners and writes no history. `./client-router.ts` is the
|
|
20
|
+
// activation shim: it re-exports this module's surface and additionally runs
|
|
21
|
+
// `init()` as a side effect on import, which `import "@takazudo/zfb-runtime/client-router"`
|
|
22
|
+
// (auto-injected by the islands bundler) relies on for byte-compatible activation.
|
|
23
|
+
//
|
|
24
|
+
// Framework-agnostic element minting (no JSX syntax):
|
|
25
|
+
// `@takazudo/zfb-runtime` does not depend on a framework runtime. Head nodes
|
|
26
|
+
// are minted by calling `jsx` from `react/jsx-runtime` directly — NOT JSX
|
|
27
|
+
// syntax, so this stays a plain `.ts` file with no tsconfig JSX changes. The
|
|
28
|
+
// engine alias-rewrites `react/jsx-runtime` → `preact/jsx-runtime` in Preact
|
|
29
|
+
// mode (bundler.rs ~2886) and resolves it natively in React mode, so the same
|
|
30
|
+
// call mints a real element for whichever framework the project configured.
|
|
31
|
+
// The previous approach (a hand-rolled `{ type, props, key, constructor:
|
|
32
|
+
// undefined }` object literal — the Preact diff-path sentinel) only worked for
|
|
33
|
+
// Preact: React's renderer rejects such an object as a child with React error
|
|
34
|
+
// #31 ("Objects are not valid as a React child"), because a real React element
|
|
35
|
+
// carries `$$typeof: Symbol.for("react.element")` a literal cannot fake. Same
|
|
36
|
+
// migration as `Island` in @takazudo/zfb.
|
|
37
|
+
//
|
|
38
|
+
// The component renders three base sibling elements to <head> (plus optional
|
|
39
|
+
// metas emitted conditionally — see `prefetchDisabled` and `preserveHtmlAttrs`):
|
|
40
|
+
// 1. A <style> tag with the `.zfb-route-announcer` ARIA helper class.
|
|
41
|
+
// 2. <meta name="zfb-view-transitions-enabled" content="true" />
|
|
42
|
+
// 3. <meta name="zfb-view-transitions-fallback" content={fallback} />
|
|
43
|
+
//
|
|
44
|
+
// The route-announcer <div> is injected into <body> by `announce()` in
|
|
45
|
+
// `client-router/router.ts` on every navigation.
|
|
46
|
+
import { jsx } from "react/jsx-runtime";
|
|
47
|
+
import { init as prefetchInit } from "./client-router/prefetch.js";
|
|
48
|
+
// Module-level guard: prefetch bootstrap is triggered at most once per module
|
|
49
|
+
// lifetime even if <ClientRouter prefetchAll /> is mounted multiple times (#276).
|
|
50
|
+
let prefetchBootstrapped = false;
|
|
51
|
+
/**
|
|
52
|
+
* Mint a head element through the per-project JSX runtime.
|
|
53
|
+
*
|
|
54
|
+
* Calls `jsx` from `react/jsx-runtime` (alias-rewritten to
|
|
55
|
+
* `preact/jsx-runtime` in Preact mode by the engine, native in React mode)
|
|
56
|
+
* so the returned value is a real element for whichever framework the
|
|
57
|
+
* project configured — NOT a hand-rolled `{ type, props, key }` literal,
|
|
58
|
+
* which only Preact accepts and which makes React throw error #31. A stable
|
|
59
|
+
* `key` is passed because `ClientRouter()` returns these nodes in a plain
|
|
60
|
+
* array (React warns about keyless list children otherwise). Mirrors the
|
|
61
|
+
* `Island` migration in `@takazudo/zfb`.
|
|
62
|
+
*/
|
|
63
|
+
function makeVNode(type, props, key) {
|
|
64
|
+
// `jsx`'s `type` param is typed `ElementType` (string-literal intrinsic
|
|
65
|
+
// tags or component types), which rejects an arbitrary runtime `string`.
|
|
66
|
+
// The tag is dynamic here, so cast to the factory's own first-param type —
|
|
67
|
+
// robust whether the engine aliases `jsx` to react or preact at build time.
|
|
68
|
+
return jsx(type, props, key);
|
|
69
|
+
}
|
|
70
|
+
// CSS for the route-announcer element. Ported verbatim from Astro's
|
|
71
|
+
// `<style is:global>` block in ClientRouter.astro (lines 11–23), renaming
|
|
72
|
+
// `.astro-route-announcer` → `.zfb-route-announcer` per W1B §5.
|
|
73
|
+
// This is a global (non-scoped) <style> because the announcer <div> is
|
|
74
|
+
// appended to document.body at runtime, outside any Preact-controlled subtree.
|
|
75
|
+
const announcerCss = `
|
|
76
|
+
.zfb-route-announcer {
|
|
77
|
+
position: absolute;
|
|
78
|
+
left: 0;
|
|
79
|
+
top: 0;
|
|
80
|
+
clip: rect(0 0 0 0);
|
|
81
|
+
clip-path: inset(50%);
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
width: 1px;
|
|
85
|
+
height: 1px;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
/**
|
|
89
|
+
* `<ClientRouter />` — SPA soft-swap navigation with View Transition animations.
|
|
90
|
+
*
|
|
91
|
+
* Mount once in your page `<head>`. Emits the opt-in meta tags and the global
|
|
92
|
+
* `.zfb-route-announcer` stylesheet that the route-announcer ARIA div needs.
|
|
93
|
+
* Importing this module performs no side effects — click/form-submit
|
|
94
|
+
* intercepts are wired only when `./client-router.js` (the activation shim,
|
|
95
|
+
* imported directly or via the `@takazudo/zfb-runtime/client-router` subpath)
|
|
96
|
+
* is evaluated.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import { ClientRouter } from "@takazudo/zfb-runtime";
|
|
101
|
+
* // In your page <head>:
|
|
102
|
+
* <ClientRouter fallback="animate" />
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function ClientRouter({ fallback = "animate", prefetchAll: prefetchAllProp = false, preserveHtmlAttrs = [], traverseRefetch = false, } = {}) {
|
|
106
|
+
// Bootstrap prefetch exactly once on the client when prefetchAll is true.
|
|
107
|
+
// The initialized flag inside prefetchInit() provides a second safety layer
|
|
108
|
+
// in case of concurrent hydration or manual callers (#276).
|
|
109
|
+
if (typeof document !== "undefined" && prefetchAllProp && !prefetchBootstrapped) {
|
|
110
|
+
prefetchBootstrapped = true;
|
|
111
|
+
prefetchInit({ prefetchAll: true });
|
|
112
|
+
}
|
|
113
|
+
const nodes = [
|
|
114
|
+
// Global styles for the ARIA route-announcer div injected into <body>.
|
|
115
|
+
makeVNode("style", { dangerouslySetInnerHTML: { __html: announcerCss } }, "zfb-vt-style"),
|
|
116
|
+
// Opt-in meta tag: router checks for this to decide whether to intercept navigations.
|
|
117
|
+
makeVNode("meta", { name: "zfb-view-transitions-enabled", content: "true" }, "zfb-vt-enabled"),
|
|
118
|
+
// Fallback strategy meta tag: read by getFallback() in router.ts.
|
|
119
|
+
makeVNode("meta", { name: "zfb-view-transitions-fallback", content: fallback }, "zfb-vt-fallback"),
|
|
120
|
+
];
|
|
121
|
+
// Prefetch-disabled meta tag (#277): emitted when the bundler set
|
|
122
|
+
// `globalThis.__zfb.prefetchDisabled = true` (from `zfb.config.ts`
|
|
123
|
+
// `prefetch: { disabled: true }`). The sibling prefetch-core module reads
|
|
124
|
+
// `document.querySelector('meta[name="zfb-prefetch-disabled"][content="true"]')`
|
|
125
|
+
// at `init()` time and short-circuits if found.
|
|
126
|
+
//
|
|
127
|
+
// The flag is site-wide and static — set once at bundle-emit time, never
|
|
128
|
+
// per-page. This meta tag appears on every page that mounts `<ClientRouter />`
|
|
129
|
+
// or not at all.
|
|
130
|
+
//
|
|
131
|
+
// Pin the contract verbatim — the attribute names and content value are
|
|
132
|
+
// shared with the sibling prefetch-core sub-issue (#276).
|
|
133
|
+
if (globalThis.__zfb?.prefetchDisabled === true) {
|
|
134
|
+
nodes.push(makeVNode("meta", { name: "zfb-prefetch-disabled", content: "true" }, "zfb-prefetch-disabled"));
|
|
135
|
+
}
|
|
136
|
+
// Consumer-extensible <html> attribute preserve-list (#1103). swapRootAttributes
|
|
137
|
+
// reads this meta and re-applies the listed attributes' runtime values after each
|
|
138
|
+
// swap, so persisted-island state on <html> (data-theme, data-sidebar-hidden, …)
|
|
139
|
+
// survives navigation. Emitted only when non-empty, so non-opt-in output stays
|
|
140
|
+
// byte-identical. Same conditional-emission shape as the prefetch-disabled meta.
|
|
141
|
+
const preserveList = preserveHtmlAttrs.filter(Boolean);
|
|
142
|
+
if (preserveList.length > 0) {
|
|
143
|
+
nodes.push(makeVNode("meta", { name: "zfb-preserve-html-attrs", content: preserveList.join(" ") }, "zfb-preserve-html-attrs"));
|
|
144
|
+
}
|
|
145
|
+
// Traverse-refetch opt-out meta (#1376). A same-page Back/Forward traversal
|
|
146
|
+
// skips the re-fetch/re-swap by default (the router's traverse fast-path);
|
|
147
|
+
// this meta opts a per-request SSR page (`prerender = false`) back INTO the
|
|
148
|
+
// fetch, since skipping it would pin stale server-rendered content. Emitted
|
|
149
|
+
// only when opted in, so non-opt-in output stays byte-identical. Same
|
|
150
|
+
// conditional-emission shape as the prefetch-disabled / preserve-html-attrs
|
|
151
|
+
// metas; the router reads meta[name="zfb-traverse-refetch"][content="true"].
|
|
152
|
+
if (traverseRefetch) {
|
|
153
|
+
nodes.push(makeVNode("meta", { name: "zfb-traverse-refetch", content: "true" }, "zfb-traverse-refetch"));
|
|
154
|
+
}
|
|
155
|
+
return nodes;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=client-router-component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-router-component.js","sourceRoot":"","sources":["../src/client-router-component.ts"],"names":[],"mappings":"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","sourcesContent":["// `@takazudo/zfb-runtime` — `<ClientRouter />` component (pure module).\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()`. No inline <script> dangerouslySetInnerHTML is emitted.\n//\n// Component/activation split (#2437): this module is the pure component —\n// it imports ONLY `react/jsx-runtime` and `./client-router/prefetch.js`\n// (`prefetch.ts` has zero module-scope side effects), never\n// `./client-router/router.js`, and performs no top-level `document`/`window`\n// access. Importing it — including via the root barrel `@takazudo/zfb-runtime`\n// — registers no listeners and writes no history. `./client-router.ts` is the\n// activation shim: it re-exports this module's surface and additionally runs\n// `init()` as a side effect on import, which `import \"@takazudo/zfb-runtime/client-router\"`\n// (auto-injected by the islands bundler) relies on for byte-compatible activation.\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 as prefetchInit } from \"./client-router/prefetch.js\";\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 * Importing this module performs no side effects — click/form-submit\n * intercepts are wired only when `./client-router.js` (the activation shim,\n * imported directly or via the `@takazudo/zfb-runtime/client-router` subpath)\n * is evaluated.\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/client-router.d.ts
CHANGED
|
@@ -1,72 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
/** Fallback animation strategy when native View Transitions are not supported. */
|
|
3
|
-
fallback?: "none" | "animate" | "swap";
|
|
4
|
-
/**
|
|
5
|
-
* When true, opts every same-origin link into the default prefetch strategy
|
|
6
|
-
* (hover) by calling prefetchInit({ prefetchAll: true }) once on the client.
|
|
7
|
-
*/
|
|
8
|
-
prefetchAll?: boolean;
|
|
9
|
-
/**
|
|
10
|
-
* Extra `<html>` attribute names to preserve across SPA swaps. By default the
|
|
11
|
-
* client router copies the incoming server-rendered document's `<html>`
|
|
12
|
-
* attributes onto the live root, dropping any current attribute that isn't
|
|
13
|
-
* internal to the transition machinery — so a *runtime* attribute a consumer
|
|
14
|
-
* sets from a persisted island (e.g. `data-theme` or `data-sidebar-hidden`
|
|
15
|
-
* driven from `localStorage`) is lost on every navigation. List those names
|
|
16
|
-
* here and the router re-applies their current value after each swap. Emitted
|
|
17
|
-
* as a `<meta name="zfb-preserve-html-attrs">` tag that `swapRootAttributes`
|
|
18
|
-
* reads at swap time.
|
|
19
|
-
*
|
|
20
|
-
* Mount with the **same list on every page** that participates in SPA
|
|
21
|
-
* navigation: the preserve-list is read from the *current* (outgoing) page's
|
|
22
|
-
* meta at swap time, so a page that omits an entry drops that attribute when
|
|
23
|
-
* navigating away from it. Names are matched case-insensitively (DOM attribute
|
|
24
|
-
* names are lowercased). For dynamic/computed cases, mutate
|
|
25
|
-
* `event.newDocument.documentElement` in a `zfb:before-swap` listener instead.
|
|
26
|
-
* @see https://github.com/Takazudo/zudo-front-builder/issues/1103
|
|
27
|
-
*/
|
|
28
|
-
preserveHtmlAttrs?: string[];
|
|
29
|
-
/**
|
|
30
|
-
* Opt this page OUT of the same-page traverse fast-path (default `false` —
|
|
31
|
-
* fast-path ON). By default a Back/Forward traversal between two history
|
|
32
|
-
* entries sharing the same `pathname + search` is served instantly from the
|
|
33
|
-
* live DOM — no re-fetch, no re-swap, so island/client state is preserved.
|
|
34
|
-
*
|
|
35
|
-
* Set this on a **per-request SSR page** (`prerender = false`) whose
|
|
36
|
-
* server-rendered content can legitimately differ between two visits to the
|
|
37
|
-
* same URL: with the fast-path skipped such a traverse would pin the stale
|
|
38
|
-
* first-render content. Emitted as
|
|
39
|
-
* `<meta name="zfb-traverse-refetch" content="true">`, which the router reads
|
|
40
|
-
* on the current (target) page to force the fetch back on. Mount with the
|
|
41
|
-
* **same value on every page** that participates in SPA navigation, mirroring
|
|
42
|
-
* the `preserveHtmlAttrs` guidance.
|
|
43
|
-
* @see https://github.com/Takazudo/zudo-front-builder/issues/1376
|
|
44
|
-
*/
|
|
45
|
-
traverseRefetch?: boolean;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Public element shape for each node returned by `<ClientRouter />`.
|
|
49
|
-
* Structural type — intentionally matches the Preact/React VNode object shape
|
|
50
|
-
* so consumers do not type-infer through the internal representation.
|
|
51
|
-
*/
|
|
52
|
-
export type ClientRouterElement = {
|
|
53
|
-
readonly type: string;
|
|
54
|
-
readonly props: Readonly<Record<string, unknown>>;
|
|
55
|
-
readonly key: unknown;
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* `<ClientRouter />` — SPA soft-swap navigation with View Transition animations.
|
|
59
|
-
*
|
|
60
|
-
* Mount once in your page `<head>`. Emits the opt-in meta tags and the global
|
|
61
|
-
* `.zfb-route-announcer` stylesheet that the route-announcer ARIA div needs.
|
|
62
|
-
* Click and form-submit intercepts are registered as a side effect of importing
|
|
63
|
-
* this component (idempotent — safe to mount multiple times).
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```tsx
|
|
67
|
-
* import { ClientRouter } from "@takazudo/zfb-runtime";
|
|
68
|
-
* // In your page <head>:
|
|
69
|
-
* <ClientRouter fallback="animate" />
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export declare function ClientRouter({ fallback, prefetchAll: prefetchAllProp, preserveHtmlAttrs, traverseRefetch, }?: ClientRouterProps): readonly ClientRouterElement[];
|
|
1
|
+
export { ClientRouter, type ClientRouterProps, type ClientRouterElement, } from "./client-router-component.js";
|
package/dist/client-router.js
CHANGED
|
@@ -1,153 +1,26 @@
|
|
|
1
|
-
// `@takazudo/zfb-runtime` — `<ClientRouter />`
|
|
1
|
+
// `@takazudo/zfb-runtime` — `<ClientRouter />` activation shim.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Component/activation split (#2437): the pure `<ClientRouter />` component
|
|
4
|
+
// (JSX, props, VNode minting — no side effects) lives in
|
|
5
|
+
// `./client-router-component.ts`. This module re-exports that surface and
|
|
6
|
+
// additionally runs `init()` from `client-router/router.ts` as a side effect
|
|
7
|
+
// on import, wiring the click/form-submit intercepts. Keeping this exact file
|
|
8
|
+
// path (rather than folding it away) preserves the package.json `sideEffects`
|
|
9
|
+
// entry, deep-import back-compat, and the subpath activation chain:
|
|
10
|
+
// `import "@takazudo/zfb-runtime/client-router"` (auto-injected by the
|
|
11
|
+
// islands bundler, `crates/zfb-islands/src/esbuild.rs`) resolves through
|
|
12
|
+
// `client-router/index.ts` to here, and evaluating this module activates the
|
|
13
|
+
// router byte-compatibly with pre-split behavior.
|
|
6
14
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// port emits meta tags + global styles from the JSX component, while the
|
|
11
|
-
// click/form intercepts live in `client-router/router.ts` and are registered
|
|
12
|
-
// via `init()`. The component calls `init()` as a side effect on first import.
|
|
13
|
-
// No inline <script> dangerouslySetInnerHTML is emitted.
|
|
14
|
-
//
|
|
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.
|
|
28
|
-
//
|
|
29
|
-
// The component renders three base sibling elements to <head> (plus optional
|
|
30
|
-
// metas emitted conditionally — see `prefetchDisabled` and `preserveHtmlAttrs`):
|
|
31
|
-
// 1. A <style> tag with the `.zfb-route-announcer` ARIA helper class.
|
|
32
|
-
// 2. <meta name="zfb-view-transitions-enabled" content="true" />
|
|
33
|
-
// 3. <meta name="zfb-view-transitions-fallback" content={fallback} />
|
|
34
|
-
//
|
|
35
|
-
// The route-announcer <div> is injected into <body> by `announce()` in
|
|
36
|
-
// `client-router/router.ts` on every navigation.
|
|
37
|
-
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
// The root barrel (`src/index.ts`) imports `ClientRouter` from
|
|
16
|
+
// `./client-router-component.js` instead, so `import { ClientRouter } from
|
|
17
|
+
// "@takazudo/zfb-runtime"` alone performs zero side effects.
|
|
38
18
|
import { init } from "./client-router/router.js";
|
|
39
|
-
|
|
40
|
-
// Side-effect: wire click + submit intercepts on
|
|
41
|
-
// Guarded by the idempotent `initialized` flag in router.ts — safe for
|
|
42
|
-
//
|
|
19
|
+
export { ClientRouter, } from "./client-router-component.js";
|
|
20
|
+
// Side-effect: wire click + submit intercepts on import of this shim.
|
|
21
|
+
// Guarded by the idempotent `initialized` flag in router.ts — safe for
|
|
22
|
+
// multiple imports and HMR re-runs. (W3C3 init idempotency.)
|
|
43
23
|
if (typeof document !== "undefined") {
|
|
44
24
|
init();
|
|
45
25
|
}
|
|
46
|
-
// Module-level guard: prefetch bootstrap is triggered at most once per module
|
|
47
|
-
// lifetime even if <ClientRouter prefetchAll /> is mounted multiple times (#276).
|
|
48
|
-
let prefetchBootstrapped = false;
|
|
49
|
-
/**
|
|
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`.
|
|
60
|
-
*/
|
|
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);
|
|
67
|
-
}
|
|
68
|
-
// CSS for the route-announcer element. Ported verbatim from Astro's
|
|
69
|
-
// `<style is:global>` block in ClientRouter.astro (lines 11–23), renaming
|
|
70
|
-
// `.astro-route-announcer` → `.zfb-route-announcer` per W1B §5.
|
|
71
|
-
// This is a global (non-scoped) <style> because the announcer <div> is
|
|
72
|
-
// appended to document.body at runtime, outside any Preact-controlled subtree.
|
|
73
|
-
const announcerCss = `
|
|
74
|
-
.zfb-route-announcer {
|
|
75
|
-
position: absolute;
|
|
76
|
-
left: 0;
|
|
77
|
-
top: 0;
|
|
78
|
-
clip: rect(0 0 0 0);
|
|
79
|
-
clip-path: inset(50%);
|
|
80
|
-
overflow: hidden;
|
|
81
|
-
white-space: nowrap;
|
|
82
|
-
width: 1px;
|
|
83
|
-
height: 1px;
|
|
84
|
-
}
|
|
85
|
-
`;
|
|
86
|
-
/**
|
|
87
|
-
* `<ClientRouter />` — SPA soft-swap navigation with View Transition animations.
|
|
88
|
-
*
|
|
89
|
-
* Mount once in your page `<head>`. Emits the opt-in meta tags and the global
|
|
90
|
-
* `.zfb-route-announcer` stylesheet that the route-announcer ARIA div needs.
|
|
91
|
-
* Click and form-submit intercepts are registered as a side effect of importing
|
|
92
|
-
* this component (idempotent — safe to mount multiple times).
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```tsx
|
|
96
|
-
* import { ClientRouter } from "@takazudo/zfb-runtime";
|
|
97
|
-
* // In your page <head>:
|
|
98
|
-
* <ClientRouter fallback="animate" />
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
export function ClientRouter({ fallback = "animate", prefetchAll: prefetchAllProp = false, preserveHtmlAttrs = [], traverseRefetch = false, } = {}) {
|
|
102
|
-
// Bootstrap prefetch exactly once on the client when prefetchAll is true.
|
|
103
|
-
// The initialized flag inside prefetchInit() provides a second safety layer
|
|
104
|
-
// in case of concurrent hydration or manual callers (#276).
|
|
105
|
-
if (typeof document !== "undefined" && prefetchAllProp && !prefetchBootstrapped) {
|
|
106
|
-
prefetchBootstrapped = true;
|
|
107
|
-
prefetchInit({ prefetchAll: true });
|
|
108
|
-
}
|
|
109
|
-
const nodes = [
|
|
110
|
-
// Global styles for the ARIA route-announcer div injected into <body>.
|
|
111
|
-
makeVNode("style", { dangerouslySetInnerHTML: { __html: announcerCss } }, "zfb-vt-style"),
|
|
112
|
-
// Opt-in meta tag: router checks for this to decide whether to intercept navigations.
|
|
113
|
-
makeVNode("meta", { name: "zfb-view-transitions-enabled", content: "true" }, "zfb-vt-enabled"),
|
|
114
|
-
// Fallback strategy meta tag: read by getFallback() in router.ts.
|
|
115
|
-
makeVNode("meta", { name: "zfb-view-transitions-fallback", content: fallback }, "zfb-vt-fallback"),
|
|
116
|
-
];
|
|
117
|
-
// Prefetch-disabled meta tag (#277): emitted when the bundler set
|
|
118
|
-
// `globalThis.__zfb.prefetchDisabled = true` (from `zfb.config.ts`
|
|
119
|
-
// `prefetch: { disabled: true }`). The sibling prefetch-core module reads
|
|
120
|
-
// `document.querySelector('meta[name="zfb-prefetch-disabled"][content="true"]')`
|
|
121
|
-
// at `init()` time and short-circuits if found.
|
|
122
|
-
//
|
|
123
|
-
// The flag is site-wide and static — set once at bundle-emit time, never
|
|
124
|
-
// per-page. This meta tag appears on every page that mounts `<ClientRouter />`
|
|
125
|
-
// or not at all.
|
|
126
|
-
//
|
|
127
|
-
// Pin the contract verbatim — the attribute names and content value are
|
|
128
|
-
// shared with the sibling prefetch-core sub-issue (#276).
|
|
129
|
-
if (globalThis.__zfb?.prefetchDisabled === 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"));
|
|
150
|
-
}
|
|
151
|
-
return nodes;
|
|
152
|
-
}
|
|
153
26
|
//# sourceMappingURL=client-router.js.map
|
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"file":"client-router.js","sourceRoot":"","sources":["../src/client-router.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,yDAAyD;AACzD,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,oEAAoE;AACpE,uEAAuE;AACvE,yEAAyE;AACzE,6EAA6E;AAC7E,kDAAkD;AAClD,EAAE;AACF,+DAA+D;AAC/D,2EAA2E;AAC3E,6DAA6D;AAE7D,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AAEjD,OAAO,EACL,YAAY,GAGb,MAAM,8BAA8B,CAAC;AAEtC,sEAAsE;AACtE,uEAAuE;AACvE,6DAA6D;AAC7D,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;IACpC,IAAI,EAAE,CAAC;AACT,CAAC","sourcesContent":["// `@takazudo/zfb-runtime` — `<ClientRouter />` activation shim.\n//\n// Component/activation split (#2437): the pure `<ClientRouter />` component\n// (JSX, props, VNode minting — no side effects) lives in\n// `./client-router-component.ts`. This module re-exports that surface and\n// additionally runs `init()` from `client-router/router.ts` as a side effect\n// on import, wiring the click/form-submit intercepts. Keeping this exact file\n// path (rather than folding it away) preserves the package.json `sideEffects`\n// entry, deep-import back-compat, and the subpath activation chain:\n// `import \"@takazudo/zfb-runtime/client-router\"` (auto-injected by the\n// islands bundler, `crates/zfb-islands/src/esbuild.rs`) resolves through\n// `client-router/index.ts` to here, and evaluating this module activates the\n// router byte-compatibly with pre-split behavior.\n//\n// The root barrel (`src/index.ts`) imports `ClientRouter` from\n// `./client-router-component.js` instead, so `import { ClientRouter } from\n// \"@takazudo/zfb-runtime\"` alone performs zero side effects.\n\nimport { init } from \"./client-router/router.js\";\n\nexport {\n ClientRouter,\n type ClientRouterProps,\n type ClientRouterElement,\n} from \"./client-router-component.js\";\n\n// Side-effect: wire click + submit intercepts on import of this shim.\n// Guarded by the idempotent `initialized` flag in router.ts — safe for\n// multiple imports and HMR re-runs. (W3C3 init idempotency.)\nif (typeof document !== \"undefined\") {\n init();\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type { CreatePageRouterOptions, PageDefinition, PageHeading, PageModule,
|
|
|
2
2
|
export type { FrameworkAdapter } from "./framework.js";
|
|
3
3
|
export type { ContentSnapshot, EntrySnapshot } from "./snapshot.js";
|
|
4
4
|
export { ViewTransitions, type ViewTransitionsElement } from "./view-transitions.js";
|
|
5
|
-
export { ClientRouter, type ClientRouterProps } from "./client-router.js";
|
|
5
|
+
export { ClientRouter, type ClientRouterProps } from "./client-router-component.js";
|
|
6
6
|
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, syncHistoryEntry, } from "./client-router/router.js";
|
|
7
7
|
export { prefetch, init as prefetchInit } from "./client-router/prefetch.js";
|
|
8
8
|
export type { PrefetchStrategy, PrefetchInitOptions, PrefetchOptions, } from "./client-router/prefetch.js";
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,15 @@
|
|
|
27
27
|
export { ViewTransitions } from "./view-transitions.js";
|
|
28
28
|
// Client-router public surface (W3D — mirrors @takazudo/zfb-runtime/client-router barrel).
|
|
29
29
|
// See W1B §2 for the full public API spec.
|
|
30
|
-
|
|
30
|
+
//
|
|
31
|
+
// ClientRouter is imported from the pure component module, NOT the
|
|
32
|
+
// `./client-router.js` activation shim (#2437): the shim runs `init()` as a
|
|
33
|
+
// module-scope side effect, which would make `import { ClientRouter } from
|
|
34
|
+
// "@takazudo/zfb-runtime"` silently activate the router. The pure module
|
|
35
|
+
// performs zero side effects, so the root barrel stays side-effect-free.
|
|
36
|
+
// `import "@takazudo/zfb-runtime/client-router"` (the shim's subpath) remains
|
|
37
|
+
// the byte-compatible activation entry point — see client-router.ts.
|
|
38
|
+
export { ClientRouter } from "./client-router-component.js";
|
|
31
39
|
export { navigate, supportsViewTransitions, transitionEnabledOnThisPage, syncHistoryEntry, } from "./client-router/router.js";
|
|
32
40
|
// Prefetch public surface (#276).
|
|
33
41
|
// `init` from prefetch.ts is re-exported as `prefetchInit` to avoid collision
|
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,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,
|
|
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,EAAE;AACF,mEAAmE;AACnE,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AACzE,8EAA8E;AAC9E,qEAAqE;AACrE,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,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.\n//\n// ClientRouter is imported from the pure component module, NOT the\n// `./client-router.js` activation shim (#2437): the shim runs `init()` as a\n// module-scope side effect, which would make `import { ClientRouter } from\n// \"@takazudo/zfb-runtime\"` silently activate the router. The pure module\n// performs zero side effects, so the root barrel stays side-effect-free.\n// `import \"@takazudo/zfb-runtime/client-router\"` (the shim's subpath) remains\n// the byte-compatible activation entry point — see client-router.ts.\nexport { ClientRouter, type ClientRouterProps } from \"./client-router-component.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/snapshot.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One heading the MDX compiler allocated for an entry, in document order.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `crates/zfb-content/src/mdx_jsx_emit.rs::HeadingEntry`, and is
|
|
5
|
+
* the same record the compiled module's `export const headings` array
|
|
6
|
+
* carries — `slug` matches the rendered `<hN id="…">` because both come
|
|
7
|
+
* from one slug allocation.
|
|
8
|
+
*/
|
|
9
|
+
export interface RenderHeading {
|
|
10
|
+
readonly depth: number;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly slug: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render-artifact metadata for one content region. Mirrors
|
|
16
|
+
* `crates/zfb-content/src/render_metadata.rs::RenderRegionMetadata`.
|
|
17
|
+
*
|
|
18
|
+
* `source_digest` is `"sha256:" + 64 hex` over the entry's RAW on-disk
|
|
19
|
+
* source bytes — frontmatter included, no BOM strip, no CRLF
|
|
20
|
+
* normalization. It identifies the source, not the rendered output: a
|
|
21
|
+
* transcluded dependency can change what renders without changing this.
|
|
22
|
+
*/
|
|
23
|
+
export interface RenderRegionMetadata {
|
|
24
|
+
readonly headings: readonly RenderHeading[];
|
|
25
|
+
readonly source_digest: string;
|
|
26
|
+
}
|
|
1
27
|
/**
|
|
2
28
|
* One entry in a content collection, in the shape the JS bridge sees.
|
|
3
29
|
*
|
|
@@ -15,6 +41,10 @@
|
|
|
15
41
|
* no-hash form `mdx://collection/slug`.
|
|
16
42
|
* - `rel_path`: path relative to the collection root, normalized to
|
|
17
43
|
* forward slashes so JSON is platform-stable.
|
|
44
|
+
* - `render_metadata`: present only when the build ran with
|
|
45
|
+
* `emitRenderArtifacts` on, and only for markdown entries. The Rust
|
|
46
|
+
* side skips the field entirely when the feature is off, so an
|
|
47
|
+
* unflagged build's snapshot bytes are unchanged.
|
|
18
48
|
*/
|
|
19
49
|
export interface EntrySnapshot {
|
|
20
50
|
readonly slug: string;
|
|
@@ -22,6 +52,7 @@ export interface EntrySnapshot {
|
|
|
22
52
|
readonly body: string;
|
|
23
53
|
readonly module_specifier: string;
|
|
24
54
|
readonly rel_path: string;
|
|
55
|
+
readonly render_metadata?: RenderRegionMetadata;
|
|
25
56
|
}
|
|
26
57
|
/**
|
|
27
58
|
* Point-in-time snapshot of every configured collection.
|
package/dist/snapshot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,8BAA8B;AAC9B,EAAE;AACF,kFAAkF;AAClF,uEAAuE;AACvE,uEAAuE;AACvE,mFAAmF;AACnF,2EAA2E;AAC3E,6EAA6E;AAC7E,+BAA+B;AAC/B,mEAAmE;AACnE,cAAc;AACd,EAAE;AACF,wEAAwE;AACxE,iEAAiE","sourcesContent":["// `@takazudo/zfb-runtime/snapshot` — TypeScript mirror of the Rust\n// `ContentSnapshot` contract.\n//\n// The canonical shape lives in Rust at `crates/zfb-content/src/content_bridge.rs`\n// (see `ContentSnapshot` and `EntrySnapshot`). The build-time pipeline\n// constructs the snapshot, serializes it to JSON, and embeds it in the\n// Worker bundle that the embedded V8 host loads. At Worker boot the embedded value\n// is handed to [`createPageRouter`] (exported from the server-only subpath\n// `@takazudo/zfb-runtime/server`), which registers it with the `zfb/content`\n// module so user pages calling\n// `getCollection(\"blog\")` resolve from memory rather than from the\n// filesystem.\n//\n// Keep this in sync with the Rust struct. Field names are snake_case to\n// match the JSON serialization (`module_specifier`, `rel_path`).\n\n/**\n * One entry in a content collection, in the shape the JS bridge sees.\n *\n * Mirrors `crates/zfb-content/src/content_bridge.rs::EntrySnapshot`.\n *\n * - `slug`: filename stem (no extension).\n * - `frontmatter`: parsed frontmatter — `null` when the source had none.\n * Type-erased to `unknown` here; user pages narrow via the generic on\n * `getCollection<T>()`.\n * - `body`: markdown body for `.md` / `.mdx` entries; empty string for\n * `.tsx` entries (TSX has no separate markdown body).\n * - `module_specifier`: stable specifier addressing the compiled module\n * (`mdx://collection/slug#hash` / `tsx://collection/slug#hash`). The\n * bridge resolver matches either the full-with-hash form or the\n * no-hash form `mdx://collection/slug`.\n * - `rel_path`: path relative to the collection root, normalized to\n * forward slashes so JSON is platform-stable.\n */\nexport interface EntrySnapshot {\n readonly slug: string;\n readonly frontmatter: unknown;\n readonly body: string;\n readonly module_specifier: string;\n readonly rel_path: string;\n}\n\n/**\n * Point-in-time snapshot of every configured collection.\n *\n * Mirrors `crates/zfb-content/src/content_bridge.rs::ContentSnapshot`.\n *\n * Iteration order is documented as deterministic on the Rust side\n * (collections sorted by name, entries sorted by slug). The snapshot\n * delivered to JS preserves that order via stable JSON serialization.\n */\nexport interface ContentSnapshot {\n readonly collections: Readonly<Record<string, readonly EntrySnapshot[]>>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,8BAA8B;AAC9B,EAAE;AACF,kFAAkF;AAClF,uEAAuE;AACvE,uEAAuE;AACvE,mFAAmF;AACnF,2EAA2E;AAC3E,6EAA6E;AAC7E,+BAA+B;AAC/B,mEAAmE;AACnE,cAAc;AACd,EAAE;AACF,wEAAwE;AACxE,iEAAiE","sourcesContent":["// `@takazudo/zfb-runtime/snapshot` — TypeScript mirror of the Rust\n// `ContentSnapshot` contract.\n//\n// The canonical shape lives in Rust at `crates/zfb-content/src/content_bridge.rs`\n// (see `ContentSnapshot` and `EntrySnapshot`). The build-time pipeline\n// constructs the snapshot, serializes it to JSON, and embeds it in the\n// Worker bundle that the embedded V8 host loads. At Worker boot the embedded value\n// is handed to [`createPageRouter`] (exported from the server-only subpath\n// `@takazudo/zfb-runtime/server`), which registers it with the `zfb/content`\n// module so user pages calling\n// `getCollection(\"blog\")` resolve from memory rather than from the\n// filesystem.\n//\n// Keep this in sync with the Rust struct. Field names are snake_case to\n// match the JSON serialization (`module_specifier`, `rel_path`).\n\n/**\n * One heading the MDX compiler allocated for an entry, in document order.\n *\n * Mirrors `crates/zfb-content/src/mdx_jsx_emit.rs::HeadingEntry`, and is\n * the same record the compiled module's `export const headings` array\n * carries — `slug` matches the rendered `<hN id=\"…\">` because both come\n * from one slug allocation.\n */\nexport interface RenderHeading {\n readonly depth: number;\n readonly text: string;\n readonly slug: string;\n}\n\n/**\n * Render-artifact metadata for one content region. Mirrors\n * `crates/zfb-content/src/render_metadata.rs::RenderRegionMetadata`.\n *\n * `source_digest` is `\"sha256:\" + 64 hex` over the entry's RAW on-disk\n * source bytes — frontmatter included, no BOM strip, no CRLF\n * normalization. It identifies the source, not the rendered output: a\n * transcluded dependency can change what renders without changing this.\n */\nexport interface RenderRegionMetadata {\n readonly headings: readonly RenderHeading[];\n readonly source_digest: string;\n}\n\n/**\n * One entry in a content collection, in the shape the JS bridge sees.\n *\n * Mirrors `crates/zfb-content/src/content_bridge.rs::EntrySnapshot`.\n *\n * - `slug`: filename stem (no extension).\n * - `frontmatter`: parsed frontmatter — `null` when the source had none.\n * Type-erased to `unknown` here; user pages narrow via the generic on\n * `getCollection<T>()`.\n * - `body`: markdown body for `.md` / `.mdx` entries; empty string for\n * `.tsx` entries (TSX has no separate markdown body).\n * - `module_specifier`: stable specifier addressing the compiled module\n * (`mdx://collection/slug#hash` / `tsx://collection/slug#hash`). The\n * bridge resolver matches either the full-with-hash form or the\n * no-hash form `mdx://collection/slug`.\n * - `rel_path`: path relative to the collection root, normalized to\n * forward slashes so JSON is platform-stable.\n * - `render_metadata`: present only when the build ran with\n * `emitRenderArtifacts` on, and only for markdown entries. The Rust\n * side skips the field entirely when the feature is off, so an\n * unflagged build's snapshot bytes are unchanged.\n */\nexport interface EntrySnapshot {\n readonly slug: string;\n readonly frontmatter: unknown;\n readonly body: string;\n readonly module_specifier: string;\n readonly rel_path: string;\n readonly render_metadata?: RenderRegionMetadata;\n}\n\n/**\n * Point-in-time snapshot of every configured collection.\n *\n * Mirrors `crates/zfb-content/src/content_bridge.rs::ContentSnapshot`.\n *\n * Iteration order is documented as deterministic on the Rust side\n * (collections sorted by name, entries sorted by slug). The snapshot\n * delivered to JS preserves that order via stable JSON serialization.\n */\nexport interface ContentSnapshot {\n readonly collections: Readonly<Record<string, readonly EntrySnapshot[]>>;\n}\n"]}
|