@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.
@@ -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"]}
@@ -1,72 +1 @@
1
- export interface ClientRouterProps {
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";
@@ -1,153 +1,26 @@
1
- // `@takazudo/zfb-runtime` — `<ClientRouter />` component.
1
+ // `@takazudo/zfb-runtime` — `<ClientRouter />` activation shim.
2
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.
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
- // 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()`. 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
- import { init as prefetchInit } from "./client-router/prefetch.js";
40
- // Side-effect: wire click + submit intercepts on first import of this component.
41
- // Guarded by the idempotent `initialized` flag in router.ts — safe for multiple
42
- // <ClientRouter /> mounts and HMR re-runs. (W3C3 init idempotency.)
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
- export { ClientRouter } from "./client-router.js";
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,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"]}
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"]}
@@ -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.
@@ -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"]}