@takazudo/zfb 3.1.0 → 4.0.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.
Files changed (61) hide show
  1. package/dist/config.d.ts +57 -3
  2. package/dist/config.js +23 -12
  3. package/dist/config.js.map +1 -1
  4. package/dist/content.js.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/island-boundary.d.ts +2 -2
  8. package/dist/island-boundary.js +4 -2
  9. package/dist/island-boundary.js.map +1 -1
  10. package/dist/island.d.ts +8 -4
  11. package/dist/island.js +1 -1
  12. package/dist/island.js.map +1 -1
  13. package/dist/jsx-types.d.ts +1 -1
  14. package/dist/jsx-types.js.map +1 -1
  15. package/dist/plugins.d.ts +18 -4
  16. package/dist/plugins.js.map +1 -1
  17. package/dist/runtime.js.map +1 -1
  18. package/dist/slugify.js.map +1 -1
  19. package/dist/zudo-react/description.d.ts +18 -0
  20. package/dist/zudo-react/description.js +41 -0
  21. package/dist/zudo-react/description.js.map +1 -1
  22. package/dist/zudo-react/dom-bindings.d.ts +2 -1
  23. package/dist/zudo-react/dom-bindings.js +16 -22
  24. package/dist/zudo-react/dom-bindings.js.map +1 -1
  25. package/dist/zudo-react/forms.js.map +1 -1
  26. package/dist/zudo-react/hydrate.js +52 -31
  27. package/dist/zudo-react/hydrate.js.map +1 -1
  28. package/dist/zudo-react/index.d.ts +2 -1
  29. package/dist/zudo-react/index.js.map +1 -1
  30. package/dist/zudo-react/jsx-dev-runtime.d.ts +2 -6
  31. package/dist/zudo-react/jsx-dev-runtime.js +3 -3
  32. package/dist/zudo-react/jsx-dev-runtime.js.map +1 -1
  33. package/dist/zudo-react/jsx-runtime.js +2 -2
  34. package/dist/zudo-react/jsx-runtime.js.map +1 -1
  35. package/dist/zudo-react/jsx-types.d.ts +73 -26
  36. package/dist/zudo-react/jsx-types.js.map +1 -1
  37. package/dist/zudo-react/props-transport.d.ts +2 -0
  38. package/dist/zudo-react/props-transport.js +42 -14
  39. package/dist/zudo-react/props-transport.js.map +1 -1
  40. package/dist/zudo-react/raw-html.d.ts +1 -0
  41. package/dist/zudo-react/raw-html.js +260 -0
  42. package/dist/zudo-react/raw-html.js.map +1 -0
  43. package/dist/zudo-react/reactive.js.map +1 -1
  44. package/dist/zudo-react/render-html.d.ts +11 -1
  45. package/dist/zudo-react/render-html.js +83 -56
  46. package/dist/zudo-react/render-html.js.map +1 -1
  47. package/dist/zudo-react/root.d.ts +9 -1
  48. package/dist/zudo-react/root.js +45 -3
  49. package/dist/zudo-react/root.js.map +1 -1
  50. package/dist/zudo-react/server.d.ts +2 -0
  51. package/dist/zudo-react/server.js.map +1 -1
  52. package/dist/zudo-react/style.d.ts +10 -0
  53. package/dist/zudo-react/style.js +525 -0
  54. package/dist/zudo-react/style.js.map +1 -0
  55. package/dist/zudo-react/testing.d.ts +27 -0
  56. package/dist/zudo-react/testing.js +117 -0
  57. package/dist/zudo-react/testing.js.map +1 -0
  58. package/dist/zudo-react/vocabulary.d.ts +7 -0
  59. package/dist/zudo-react/vocabulary.js +64 -5
  60. package/dist/zudo-react/vocabulary.js.map +1 -1
  61. package/package.json +16 -10
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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","sourcesContent":["// `zfb/content` — minimal v0 content collection loader.\n//\n// Reads `*.md` files from a content collection directory, parses YAML\n// frontmatter, and returns typed entries. This is a deliberately small\n// stub so the bundled basic-blog template can call `getCollection(\"blog\")` today;\n// the production path lives in `crates/zfb-content` and will replace this\n// once the JS-runtime decision (ADR-001) lands and the renderer wires the\n// Rust pipeline back through to user code.\n//\n// Scope (v0):\n// - YAML-ish frontmatter only: `key: value`, plus `key:\\n - item` arrays.\n// Quoted strings are unwrapped. ISO dates stay as strings.\n// - Body is the post content **after** the closing `---`, returned as raw\n// text. This is intentionally NOT pre-rendered HTML: the markdown\n// pipeline lives in the Rust crate and the JS stub does not duplicate\n// it.\n// - Collection root is resolved from\n// `process.env.ZFB_CONTENT_ROOT` (set by the dev/build pipeline), or\n// `<cwd>/content` as a fallback for unit tests and direct invocation.\n//\n// TODO(zfb-content): swap this stub for the runtime-provided implementation\n// once the content engine ships end-to-end.\n\n// `node:fs` and `node:path` are intentionally NOT imported statically at\n// the top level.\n//\n// Why: this module is reachable via the package root (`@takazudo/zfb`) — the\n// barrel re-exports `defaultComponents` / `ContentH2` / etc. from\n// `./content.js`. The islands per-island bundler (`crates/zfb-islands`) walks\n// `import * as Mod from \"@takazudo/zfb\"` and esbuild's static tree-shaker\n// cannot prune a module behind a wildcard barrel access, so the WHOLE\n// content.ts module ends up in the browser-side island bundle. Top-level\n// `node:fs` / `node:path` imports would then fail the bundle with\n// `Could not resolve \"node:fs\"`. Loading them indirectly through a\n// runtime-constructed `createRequire` keeps the Node-runtime fs path\n// working while letting the islands bundler emit browser-safe output.\n// (Discovered while investigating zudolab/zudo-doc#1355 Wave 3 — see also\n// upstream PR #134 / #130 Gap A.) Defense-in-depth: the islands esbuild\n// invocation also passes `--platform=browser --external:node:*` so any\n// stray `node:*` import that does end up in a browser bundle is\n// externalized rather than failing the build.\n//\n// `getCollection` is synchronous per ADR-004, so the node modules are\n// loaded synchronously on first fs-path use. Type-only imports below stay\n// at the top because TypeScript erases them at compile time — they leave\n// no runtime traces for esbuild to chase.\nimport { Fragment, jsx, jsxs } from \"./zudo-react/jsx-runtime.js\";\n\nimport type * as NodeFs from \"node:fs\";\nimport type * as NodePath from \"node:path\";\n\nimport { parseFrontmatter } from \"./frontmatter.js\";\nimport type { ParsedFrontmatter } from \"./frontmatter.js\";\nimport type { VNode } from \"./jsx-types.js\";\nimport type { Description } from \"./zudo-react/description.js\";\n\n// Re-export the parser surface so existing `zfb/content` consumers that\n// import `parseFrontmatter` / `ParsedFrontmatter` from the content\n// subpath keep working. The implementation now lives in `./frontmatter.ts`\n// (BCI-3 fs-free subpath) — this re-export is the bridge for callers\n// that have not migrated yet.\nexport { parseFrontmatter };\nexport type { ParsedFrontmatter };\n\n// ---------------------------------------------------------------------------\n// In-memory ContentSnapshot bridge (consumed by `@takazudo/zfb-runtime`).\n//\n// At build time, the Rust pipeline produces a `ContentSnapshot` (see\n// `crates/zfb-content/src/content_bridge.rs`) and embeds it into the\n// Worker bundle. On Worker boot, `createPageRouter` calls\n// `setContentSnapshot(snapshot)` (below) before serving the first\n// request. From that point on, `getCollection(name)` resolves from the\n// embedded snapshot rather than the Node `fs` API — required because the\n// workerd / Cloudflare Workers runtime has no filesystem.\n//\n// The fs path remains the source of truth in two contexts:\n// 1. unit tests for this module (no snapshot installed → fs path),\n// 2. dev-preview / direct-Node invocations of `getCollection` outside\n// the Worker bundle (kept as v0 fallback so older callers still work).\n//\n// Keep [`SnapshotEntry`] / [`Snapshot`] aligned with the Rust struct\n// (`EntrySnapshot` / `ContentSnapshot`) and the runtime-package mirror\n// (`@takazudo/zfb-runtime/snapshot`). Field names are snake_case to\n// match the JSON serialization (`module_specifier`, `rel_path`).\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in an embedded content snapshot. Mirrors\n * `crates/zfb-content/src/content_bridge.rs::EntrySnapshot`. Re-exported\n * by `@takazudo/zfb-runtime/snapshot` for the runtime-side bundle. See\n * that module for field-by-field documentation.\n */\nexport interface SnapshotEntry {\n readonly slug: string;\n readonly frontmatter: unknown;\n readonly body: string;\n readonly module_specifier: string;\n readonly rel_path: string;\n /**\n * Render-artifact metadata, present only when the build ran with\n * `emitRenderArtifacts` on and only for markdown entries. Mirrors\n * `crates/zfb-content/src/render_metadata.rs::RenderRegionMetadata`.\n */\n readonly render_metadata?: SnapshotRenderMetadata;\n}\n\n/**\n * `{ headings, source_digest }` for one content region. `source_digest`\n * is `\"sha256:\" + 64 hex` over the entry's RAW on-disk source bytes\n * (frontmatter included, no BOM strip, no CRLF normalization) — it\n * identifies the source, not the rendered output. See\n * `@takazudo/zfb-runtime/snapshot` for the full field documentation.\n */\nexport interface SnapshotRenderMetadata {\n readonly headings: readonly {\n readonly depth: number;\n readonly text: string;\n readonly slug: string;\n }[];\n readonly source_digest: string;\n}\n\n/**\n * Point-in-time snapshot of every configured collection. Mirrors\n * `crates/zfb-content/src/content_bridge.rs::ContentSnapshot`.\n */\nexport interface Snapshot {\n readonly collections: Readonly<Record<string, readonly SnapshotEntry[]>>;\n}\n\n/**\n * Where the installed [`Snapshot`] lives.\n *\n * The state hangs off `globalThis.__zfb.contentSnapshot`, NOT a\n * module-level `let`. This matters because under the production worker\n * bundle the consumer's pnpm-strict `node_modules` layout exposes\n * two physical paths to `@takazudo/zfb`:\n *\n * - top-level `node_modules/@takazudo/zfb` (imported by user pages), AND\n * - nested `node_modules/.pnpm/@takazudo+zfb-runtime@.../node_modules/\n * @takazudo/zfb` (imported by `@takazudo/zfb-runtime` itself).\n *\n * The bundler passes `esbuild --preserve-symlinks` whenever a custom\n * `node_modules_dir` is configured (see `crates/zfb-build/src/bundler.rs`\n * around `--external:node:*`), so esbuild treats those two symlink\n * targets as distinct sources and inlines `content.js` TWICE — yielding\n * two module instances of `zfb/content` in the final worker bundle.\n *\n * If `installedSnapshot` were a per-module `let`, `createPageRouter`\n * would install the snapshot on the runtime's copy and `getCollection`\n * (called from a user `paths()` export) would read from the user\n * page's copy — see `undefined`, and fall through to the `node:fs`\n * branch, which then throws because `node:*` is externalized in the\n * worker bundle. This is the regression #442 / #449 surfaced.\n *\n * Routing the slot through `globalThis` makes the snapshot bridge\n * symmetric with the existing `globalThis.__zfb.content` MDX-component\n * bridge (set by the build pipeline at `crates/zfb-build/src/bundler.rs`,\n * read by `Content` below): both pieces of cross-module state share\n * one well-known global, so any number of `zfb/content` module\n * instances in the same JS realm see the same value.\n *\n * Tracked under #449 (production fix for #442); the test-fixture\n * counterpart was #413.\n */\ntype SnapshotBridgeNamespace = {\n contentSnapshot?: Snapshot | undefined;\n};\n\ntype SnapshotBridgeGlobal = typeof globalThis & {\n __zfb?: SnapshotBridgeNamespace;\n};\n\n/**\n * Register a [`Snapshot`] so [`getCollection`] resolves from memory.\n *\n * Pass `undefined` to clear (used by tests that need to restore the v0\n * filesystem path between runs). Idempotent: the latest call wins.\n *\n * Stored on `globalThis.__zfb.contentSnapshot` rather than a\n * module-level `let` so a worker bundle that ends up with two\n * `zfb/content` module instances still sees a single shared snapshot —\n * see the [`SnapshotBridgeNamespace`] doc above for the full\n * pnpm-symlink rationale.\n */\nexport function setContentSnapshot(snapshot: Snapshot | undefined): void {\n const g = globalThis as SnapshotBridgeGlobal;\n const ns = (g.__zfb ?? {}) as SnapshotBridgeNamespace;\n ns.contentSnapshot = snapshot;\n g.__zfb = ns;\n}\n\n/**\n * Read the currently-installed [`Snapshot`], or `undefined` if none is\n * registered. Exposed mostly for tests; production callers should not\n * need to introspect the bridge state.\n *\n * Reads from `globalThis.__zfb.contentSnapshot`; see\n * [`setContentSnapshot`] for why the slot lives on `globalThis`.\n */\nexport function getContentSnapshot(): Snapshot | undefined {\n return (globalThis as SnapshotBridgeGlobal).__zfb?.contentSnapshot;\n}\n\n/**\n * Flat map of element-name → override component, used by both\n * [`ContentProps.components`] and the global slot\n * (`globalThis.__zfb?.mdxComponents`). Keys are lowercase HTML tag names\n * (`h2`, `p`, `a`, …) or PascalCase custom-component names.\n */\nexport type MdxComponents = Record<string, unknown>;\n\n/**\n * Props accepted by an entry's [`CollectionEntry.Content`] component.\n *\n * `components` mirrors Astro's `<Content components={...}>` contract:\n * a flat record of element-name → override component (e.g. `{ h1: MyH1 }`).\n * The default-components convention ships from `zfb`'s root export\n * (`defaultComponents`, lands in Sub 6) and users compose with their own\n * via `{ ...defaultComponents, ...mine }`.\n */\nexport interface ContentProps {\n /** Element-name → override component map. Optional. */\n components?: MdxComponents;\n}\n\n/**\n * Public JSX-element shape returned by [`CollectionEntry.Content`].\n *\n * A description-shaped value produced by the owned JSX runtime. Consumers\n * should treat this as opaque and renderable.\n */\nexport type ContentElement = Description;\n\n/**\n * Bridge contract published by the Rust-side `zfb-render` `Renderer` before\n * evaluating each page module. Cross-referenced from the Rust side in\n * `crates/zfb-render/src/loader.rs` so the two halves stay in sync — see\n * `packages/zfb/CONTRIBUTING.md` for the full contract narrative.\n *\n * The renderer installs `globalThis.__zfb.content.get(specifier)` keyed on\n * the entry's `module_specifier` (Sub 4 convention: `mdx://<collection>/<slug>#<hash>`,\n * collapsed to `mdx://<collection>/<slug>` from the JS stub side which has\n * no hash to compute). When `get` returns `undefined` (or the bridge as a\n * whole is absent — typical of unit tests, dev sandboxes, and any\n * non-renderer evaluation context), `Content` renders a clearly-marked\n * `<pre data-zfb-content-fallback>` fallback so the visual distinction is\n * obvious even in unstyled environments.\n */\ntype ContentBridge = {\n get(specifier: string): ((props: ContentProps) => unknown) | undefined;\n};\n\ntype ZfbBridgeNamespace = {\n content?: ContentBridge;\n /**\n * Global component-override slot. Populated by sub-task A2 (bridge\n * installer); A1 only reads it. Absent ⇒ no-op in the merge.\n */\n mdxComponents?: MdxComponents;\n /**\n * Render-region marker switch (epic #2421). When `true`, every\n * bridge-resolved `Content` render is wrapped in the inert\n * `<template data-zfb-render-region>` sentinel pair that the build's\n * extraction pass slices on — see [`wrapInRenderRegion`].\n *\n * **Build-only by construction.** The bundler emits the setter into\n * the synthetic `entry.mjs` only when the run is `zfb build`\n * (`BundleMode::Production`) AND `emitRenderArtifacts` resolved on;\n * `zfb dev` passes `BundleMode::Development`, so the setter's bytes\n * are never written into a dev bundle. It is never emitted into a\n * client/island bundle either — `entry.mjs` is the SSR entry.\n */\n renderArtifacts?: boolean;\n};\n\ntype BridgeGlobal = typeof globalThis & {\n __zfb?: ZfbBridgeNamespace;\n};\n\n/**\n * Generic shape returned for one entry in a content collection. The `data`\n * field carries parsed frontmatter, typed by the caller via the generic\n * parameter.\n */\nexport type CollectionEntry<T = Record<string, unknown>> = {\n /** Filename without `.md` extension. Stable across runs. */\n slug: string;\n /** Parsed frontmatter. */\n data: T;\n /** Raw markdown body (frontmatter stripped). */\n body: string;\n /**\n * Stable module specifier used as the bridge lookup key. Format:\n * `mdx://<collection>/<slug>` (no hash component — the JS stub does\n * not compile MDX, so it has no body hash to attach; the production\n * Rust-side `zfb-content::collection::Entry::module_specifier` adds a\n * `#<hash>` suffix and the bridge is responsible for matching either\n * form against its registered components).\n *\n * This field is part of the v0+ JS surface so the bridge has something\n * deterministic to key on without consulting per-call state.\n */\n module_specifier: string;\n /**\n * Renderable component for this entry.\n *\n * **Bridge contract.** At call time, `Content` consults\n * `globalThis.__zfb?.content?.get(entry.module_specifier)`. If the\n * bridge is present and returns a function, that function is invoked\n * with `props` and its result returned verbatim.\n *\n * **Fallback.** Outside the renderer (unit tests, dev sandboxes, or any\n * environment where `globalThis.__zfb.content.get` is absent or returns\n * `undefined`), `Content` returns a JSX-shaped element rendering the\n * raw markdown body inside a `<pre data-zfb-content-fallback>` block,\n * with a leading `[zfb fallback render]` marker line so the visual\n * distinction survives unstyled environments. The marker is also a\n * grep target for \"did the production renderer not run?\" diagnostics.\n *\n * **Typed signature.** Returns `ContentElement` (a structural alias for\n * `JSX.Element`) so consumers can drop `<entry.Content components={...} />`\n * into owned JSX without additional type setup.\n *\n * @example\n * const post = (await getCollection(\"blog\"))[0];\n * return <post.Content components={{ ...defaultComponents, h1: MyH1 }} />;\n */\n Content: (props: ContentProps) => ContentElement;\n};\n\n// Cached node:fs / node:path module references. Populated lazily on first\n// fs-path use (see [`loadNodeModules`]); reused on subsequent calls.\nlet cachedNodeFs: typeof NodeFs | undefined;\nlet cachedNodePath: typeof NodePath | undefined;\n\n/**\n * Synchronously load `node:fs` and `node:path`, caching the results.\n *\n * The node specifiers are concatenated at runtime (`\"node:\" + \"fs\"`) so\n * esbuild's static analyzer cannot follow them — that's the load-bearing\n * detail here, because this module is reachable from browser-bundled\n * island chains via the `@takazudo/zfb` root barrel (see top-of-file note).\n *\n * Uses CommonJS `require` via [`createRequire`] (stable, sync) rather than\n * `await import()` (async, would force `getCollection` async and violate\n * ADR-004). `createRequire` itself is fetched from `node:module` through\n * the same runtime-built specifier pattern.\n *\n * If `createRequire` cannot be obtained at all (i.e. truly running in a\n * browser-shaped runtime — which would mean a misconfigured island\n * bundle), throws so the failure is loud rather than silent.\n */\nfunction loadNodeModules(): { fs: typeof NodeFs; path: typeof NodePath } {\n if (cachedNodeFs !== undefined && cachedNodePath !== undefined) {\n return { fs: cachedNodeFs, path: cachedNodePath };\n }\n // Runtime-built specifiers: opaque to esbuild's static analyzer.\n const moduleSpecifier = \"node:\" + \"module\";\n const fsSpecifier = \"node:\" + \"fs\";\n const pathSpecifier = \"node:\" + \"path\";\n // Strategy A: prefer the host `require` from a CommonJS context. We\n // probe via `globalThis` and `Function`-built lookup so neither esbuild\n // nor stricter ESM tooling errors out at the lookup site.\n const dynamicGlobal = globalThis as unknown as { require?: NodeJS.Require };\n let nodeRequire: NodeJS.Require | undefined = dynamicGlobal.require;\n // Strategy B: ESM context — synthesize a require via `node:module`'s\n // `createRequire`. Loading `node:module` itself through the same\n // dynamic specifier shields it from esbuild's static walker.\n if (typeof nodeRequire !== \"function\") {\n // `Function(\"return require\")()` returns the enclosing `require` when\n // the bundler/loader injects one (Node CJS, esbuild default). Falls\n // through if undefined — caught below.\n try {\n nodeRequire = new Function(\"return typeof require === 'function' ? require : undefined\")() as\n | NodeJS.Require\n | undefined;\n } catch {\n nodeRequire = undefined;\n }\n }\n if (typeof nodeRequire !== \"function\") {\n // Last resort: synthesize via createRequire. Reaches `node:module`\n // through a dynamic require we have to bootstrap somehow — the only\n // way without a static `import` is `process.getBuiltinModule` (Node\n // 22+) which exposes built-ins synchronously without a require.\n const proc = (\n globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => unknown } }\n ).process;\n const getBuiltin = proc?.getBuiltinModule;\n if (typeof getBuiltin === \"function\") {\n const mod = getBuiltin(moduleSpecifier) as typeof import(\"node:module\");\n nodeRequire = mod.createRequire(import.meta.url);\n }\n }\n if (typeof nodeRequire !== \"function\") {\n throw new Error(\n \"zfb/content: cannot load node:fs / node:path — no Node-style require available. \" +\n \"This module's filesystem path requires a Node runtime; if you see this in a browser \" +\n \"bundle, the bundler should externalize node:* imports (the islands bundler does so).\",\n );\n }\n cachedNodeFs = nodeRequire(fsSpecifier) as typeof NodeFs;\n cachedNodePath = nodeRequire(pathSpecifier) as typeof NodePath;\n return { fs: cachedNodeFs, path: cachedNodePath };\n}\n\n/**\n * Resolve the directory that holds a named content collection. Override\n * via `ZFB_CONTENT_ROOT` so tests / fixtures can point at an arbitrary\n * directory.\n */\nfunction resolveCollectionDir(name: string): string {\n const { path } = loadNodeModules();\n const envRoot = process.env[\"ZFB_CONTENT_ROOT\"];\n const root = envRoot ? path.resolve(envRoot) : path.resolve(process.cwd(), \"content\");\n return path.join(root, name);\n}\n\n/**\n * Build the v0 stub's bridge specifier for an entry. Mirrors the Rust-side\n * convention (`mdx://<collection>/<slug>`) minus the body hash — the JS\n * stub does not compile MDX, so it has no hash to attach. The bridge\n * resolver on the renderer side is responsible for matching either form.\n */\nfunction buildModuleSpecifier(collection: string, slug: string): string {\n return `mdx://${collection}/${slug}`;\n}\n\n/**\n * Build the `Content` component for an entry. Captures `module_specifier`\n * + `body` in the closure so the returned function takes only `props`.\n *\n * The bridge lookup is done lazily on every call (not at entry-construction\n * time) so the renderer can install / swap `globalThis.__zfb.content` at\n * any point before the first render without ordering hazards.\n */\nfunction buildContentComponent(\n module_specifier: string,\n body: string,\n): (props: ContentProps) => ContentElement {\n return function Content(props: ContentProps): ContentElement {\n const zfb = (globalThis as BridgeGlobal).__zfb;\n const bridge = zfb?.content;\n const renderer = bridge?.get(module_specifier);\n if (typeof renderer === \"function\") {\n // Merge components in documented precedence order before delegating:\n // defaultComponents → globalThis.__zfb.mdxComponents → props.components\n // This is output-neutral because defaultComponents entries are pure\n // passthroughs; the seam is established here for A2 to populate.\n const mergedProps: ContentProps = {\n ...props,\n components: mergeMdxComponents(zfb?.mdxComponents, props.components),\n };\n // Trust the bridge to return a JSX-element-shaped value — we don't\n // try to validate; the compiled renderer is the source of truth for its returned value.\n const rendered = renderer(mergedProps) as ContentElement;\n // Render-artifact instrumentation (epic #2421). Off by default and\n // never set outside `zfb build`, so the common path returns the\n // bridge's value verbatim and the emitted HTML is byte-identical\n // to a build without the feature.\n return zfb?.renderArtifacts === true\n ? wrapInRenderRegion(module_specifier, rendered)\n : rendered;\n }\n return renderFallback(body);\n };\n}\n\n/**\n * Attribute names of the render-region sentinel pair. The\n * `data-zfb-render-region` / `data-zfb-region-id` namespace is reserved\n * by the render-artifact contract (epic #2421): the build's extraction\n * pass is an exact-byte state machine over these two attributes, so\n * nothing else may emit them.\n */\nconst RENDER_REGION_ATTR = \"data-zfb-render-region\";\nconst REGION_ID_ATTR = \"data-zfb-region-id\";\n\n/**\n * Wrap a bridge-rendered content region in its sentinel pair:\n *\n * ```html\n * <template data-zfb-render-region=\"start\" data-zfb-region-id=\"<id>\"></template>\n * …region…\n * <template data-zfb-render-region=\"end\" data-zfb-region-id=\"<id>\"></template>\n * ```\n *\n * `<template>` is inert in every HTML context (its contents are not\n * rendered and it carries no layout), and the pair is emitted as three\n * Fragment children with **no text nodes between them** — the extraction\n * pass slices on exact bytes, so an introduced space or newline would\n * land inside the captured fragment.\n *\n * `id` is the entry's `module_specifier`, the region id the artifact\n * writer joins its `{ headings, sourceDigest }` metadata on. Repeated\n * `Content` calls therefore emit sibling pairs sharing one id, and a\n * `Content` rendered inside another emits properly nested pairs; the\n * extraction state machine matches identical-id pairs by nesting order.\n *\n * Fragment and `jsxs` are imported directly from the owned JSX runtime.\n */\nfunction wrapInRenderRegion(regionId: string, rendered: ContentElement): ContentElement {\n return jsxs(Fragment, {\n children: [\n renderRegionMarker(\"start\", regionId),\n rendered,\n renderRegionMarker(\"end\", regionId),\n ],\n });\n}\n\n/** One `<template>` sentinel. See [`wrapInRenderRegion`]. */\nfunction renderRegionMarker(edge: \"start\" | \"end\", regionId: string): ContentElement {\n return mintElement(\"template\", {\n [RENDER_REGION_ATTR]: edge,\n [REGION_ID_ATTR]: regionId,\n });\n}\n\n/**\n * Mint a content element through the owned JSX runtime. `children` is\n * passed inside `props` so a single child or an array both pass through\n * verbatim. Same migration as `Island` in this package. Kept private so\n * callers keep treating `ContentElement` / `ContentComponentElement` as\n * opaque. (Empty-MDX-body history: zudo-doc#505.)\n */\nfunction mintElement(type: string, props: Record<string, unknown>): ContentElement {\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 owned factory's first-param type.\n return jsx(type as Parameters<typeof jsx>[0], props);\n}\n\n/**\n * Build the structural JSX element returned when the bridge is absent.\n *\n * Shape: `<pre data-zfb-content-fallback>{marker}\\n{body}</pre>` — the\n * leading `[zfb fallback render]` marker line is part of the public\n * fallback contract (it's both a visual signal and a grep target). Tests\n * pin both the attribute and the marker line.\n */\nfunction renderFallback(body: string): ContentElement {\n return mintElement(\"pre\", {\n \"data-zfb-content-fallback\": \"\",\n children: `${FALLBACK_MARKER}\\n${body}`,\n });\n}\n\n/** Leading marker line emitted by [`renderFallback`]. Public contract. */\nconst FALLBACK_MARKER = \"[zfb fallback render]\";\n\n/**\n * Load every `*.md` file in the named collection. Files starting with `.`\n * or that lack a `.md` extension are ignored.\n *\n * **ADR-004 contract: this function is synchronous.** TSX page modules\n * call it from anywhere — top-level, inside a render body, inside a\n * `useMemo` — and SSR completes in a single pass without yielding. The\n * snapshot path returns from memory; the filesystem fallback uses sync\n * `node:fs` APIs so the surface stays unified. (The legacy async\n * implementation was an oversight — the ADR predates it; SSG paths\n * always saw a Promise where ADR-004 says they should see an array,\n * which is why migrations from Astro tripped on `getCollection().filter\n * is not a function`.)\n *\n * @example\n * const posts = getCollection<{ title: string; date: string }>(\"blog\");\n */\nexport function getCollection<T = Record<string, unknown>>(name: string): CollectionEntry<T>[] {\n // Snapshot path: installed by `@takazudo/zfb-runtime`'s\n // `createPageRouter` at Worker boot. Worker runtimes have no `fs`, so\n // this branch is the production path under the embedded V8 host.\n //\n // The snapshot lookup reads `globalThis.__zfb.contentSnapshot`\n // (see `setContentSnapshot` above) rather than a per-module slot so\n // the cross-`zfb/content`-instance case under `--preserve-symlinks`\n // resolves through the same shared state — see #449.\n const installedSnapshot = (globalThis as SnapshotBridgeGlobal).__zfb?.contentSnapshot;\n if (installedSnapshot !== undefined) {\n const list = installedSnapshot.collections[name] ?? [];\n return list.map((entry) => entryFromSnapshot<T>(entry));\n }\n // Filesystem fallback (v0 path). Used by unit tests and direct Node\n // invocations outside the Worker bundle.\n //\n // BCI-6: traversal is now recursive — subdirectories are walked so a\n // collection rooted at `content/blog/` can contain nested `*.md` files\n // (e.g. `content/blog/2024/hello.md`). Slugs are derived from the\n // relative path so callers get stable, unique identifiers across nesting\n // levels.\n const dir = resolveCollectionDir(name);\n let mdPaths: string[];\n try {\n mdPaths = collectMdFilesSync(dir);\n } catch (err) {\n // Guard the `code` access at runtime — a thrown non-`Error` value\n // (rare, but possible) would otherwise crash here. We only swallow\n // a true ENOENT; anything else propagates.\n if (\n err !== null &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err as { code: unknown }).code === \"ENOENT\"\n ) {\n return [];\n }\n throw err;\n }\n const { fs, path } = loadNodeModules();\n return mdPaths.map((fullPath) => {\n const raw = fs.readFileSync(fullPath, \"utf8\");\n const { data, body } = parseFrontmatter(raw);\n // Derive a stable slug from the relative path (relative to collection\n // root), stripping the `.md` extension. For top-level files this\n // produces the same value as before; for nested files it produces a\n // path-based slug (e.g. `2024/hello`).\n const rel = path.relative(dir, fullPath);\n const slug = _relPathToSlug(rel);\n const module_specifier = buildModuleSpecifier(name, slug);\n return {\n slug,\n data: data as T,\n body,\n module_specifier,\n Content: buildContentComponent(module_specifier, body),\n };\n });\n}\n\n/**\n * Look up a single entry in a content collection by slug.\n *\n * Thin wrapper over [`getCollection`]: inherits both resolution paths\n * (snapshot via `globalThis.__zfb.contentSnapshot` and the `node:fs`\n * fallback) for free. Returns `undefined` when either the collection does\n * not exist or no entry matches `slug`.\n *\n * **Runtime vs. generated types divergence.** The generated `types.d.ts`\n * emits a keyed overload (`K extends keyof ZfbCollections`) that ties the\n * return type to the collection's declared schema. That schema is enforced\n * by `zfb check`; this runtime form is intentionally structural — it does\n * not reference `ZfbCollections` and does not attempt to reconcile with the\n * keyed shape. (#857)\n *\n * @example\n * const post = getEntry<{ title: string }>(\"blog\", \"hello-zfb\");\n * if (!post) return null;\n * return <post.Content />;\n */\nexport function getEntry<T = Record<string, unknown>>(\n name: string,\n slug: string,\n): CollectionEntry<T> | undefined {\n return getCollection<T>(name).find((e) => e.slug === slug);\n}\n\n/**\n * Construct a [`CollectionEntry`] from a [`SnapshotEntry`]. The snapshot\n * carries `frontmatter` as a possibly-`null` JSON value (matches the\n * Rust contract for entries with no frontmatter); we normalise `null` /\n * `undefined` to an empty object so consumers' `.data.title` reads\n * never have to deal with `null`.\n *\n * **Type-safety note:** `T` is the caller-supplied frontmatter shape\n * but we do **not** validate it at runtime — if the page declares a\n * shape that the actual frontmatter doesn't match, the cast below\n * lies. Callers are expected to keep their `getCollection<MySchema>()`\n * generic in sync with the actual frontmatter; we acknowledge the\n * unsafety with the explicit `unknown` indirection rather than a\n * direct (and silently lossy) cast.\n */\nfunction entryFromSnapshot<T>(entry: SnapshotEntry): CollectionEntry<T> {\n const data =\n entry.frontmatter === null || entry.frontmatter === undefined\n ? ({} as T)\n : (entry.frontmatter as unknown as T);\n return {\n slug: entry.slug,\n data,\n body: entry.body,\n module_specifier: entry.module_specifier,\n Content: buildContentComponent(entry.module_specifier, entry.body),\n };\n}\n\n/**\n * Recursively collect every `*.md` file under `dir` (synchronous).\n *\n * BCI-6: replaces the old flat `readdir(dir).filter(n => n.endsWith(\".md\"))`\n * approach. Hidden files (names starting with `.`) and hidden directories\n * are skipped at every nesting level, matching the top-level behaviour of\n * the previous implementation.\n *\n * Returns absolute paths sorted lexicographically so the result order is\n * deterministic across platforms and Node versions.\n *\n * Synchronous to honour ADR-004 — see [`getCollection`].\n */\nfunction collectMdFilesSync(dir: string): string[] {\n const result: string[] = [];\n const { fs, path } = loadNodeModules();\n walkDirSync(fs, path, dir, result);\n result.sort();\n return result;\n}\n\nfunction walkDirSync(\n fs: typeof NodeFs,\n path: typeof NodePath,\n current: string,\n out: string[],\n): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) continue;\n const fullPath = path.join(current, entry.name);\n // Skip symlinks to avoid infinite loops caused by cycles (e.g. a symlink\n // pointing at a parent directory). Content files are expected to be plain\n // regular files; following symlinks provides no value here.\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n walkDirSync(fs, path, fullPath, out);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n out.push(fullPath);\n }\n }\n}\n\n/**\n * @internal\n *\n * Convert a `path.relative()` result into a forward-slash-separated\n * slug with the trailing `.md` extension stripped.\n *\n * Slugs are URL-flavored identifiers, not filesystem paths — they\n * MUST use `/` regardless of the host OS so a nested entry like\n * `2024/hello.md` produces the slug `2024/hello` on both POSIX and\n * Windows. Without this normalisation, Windows callers would see\n * `2024\\hello`, which then leaks through to `module_specifier` and\n * any URL the consumer derives from the slug.\n *\n * Exported solely so the unit test suite can pin the Windows\n * behaviour without needing an actual Windows host. Do not depend on\n * this from application code — name and signature may change.\n */\nexport function _relPathToSlug(relPath: string): string {\n const { path } = loadNodeModules();\n const posix = path.sep === \"/\" ? relPath : relPath.split(path.sep).join(\"/\");\n // Some Node versions normalise `\\` even when sep is `/`, so be\n // defensive: collapse any straggling backslashes too.\n const normalised = posix.includes(\"\\\\\") ? posix.split(\"\\\\\").join(\"/\") : posix;\n return normalised.endsWith(\".md\") ? normalised.slice(0, -\".md\".length) : normalised;\n}\n\n// ---------------------------------------------------------------------------\n// `defaultComponents` — htmlOverrides convention\n//\n// Ported from zudo-doc's `src/components/content/component-map.ts`. Users opt\n// in by spreading the map into their own `components` prop:\n//\n// import { defaultComponents } from \"zfb\";\n// <entry.Content components={{ ...defaultComponents, h2: MyH2 }} />\n//\n// Each component is a thin passthrough mirroring its zudo-doc counterpart\n// (e.g. `ContentParagraph` → `<p {...rest}>{children}</p>`). v0 ships the\n// passthroughs unstyled; layering smart-break / heading-anchor / link-icon\n// behaviour on top is independent follow-up — keeping the v0 deliverable\n// focused on infrastructure (issue #33).\n//\n// **`h1` is deliberately not in the map** — page titles render `<h1>` from\n// frontmatter, per the zudo-doc convention. Adding `h1` here would silently\n// double-render the page title.\n//\n// **Each override is exported as a named const AND included in\n// `defaultComponents`** so consumers can tree-shake-import a single component\n// (`import { ContentLink } from \"zfb\"`) without dragging in the whole map.\n//\n// Overrides return descriptions minted by the owned JSX runtime.\n// ---------------------------------------------------------------------------\n\n/**\n * Public JSX-element shape returned by every override in [`defaultComponents`].\n *\n * Mirrors [`ContentElement`] and [`IslandElement`]: a structural alias for\n * `JSX.Element` for owned JSX consumers.\n */\nexport type ContentComponentElement = Description;\n\n/**\n * Props accepted by every default override. `children` and any extra\n * attributes (`class`, `id`, `href`, …) are passed through verbatim\n * to the underlying HTML element.\n */\nexport interface ContentComponentProps {\n children?: VNode;\n [key: string]: unknown;\n}\n\n/** Internal helper: build a structural JSX element of the given tag. */\nfunction buildOverrideElement(tag: string, props: ContentComponentProps): ContentComponentElement {\n const { children, ...rest } = props;\n // Minted through the owned JSX runtime.\n return mintElement(tag, { ...rest, children });\n}\n\n/**\n * `<h2>` passthrough override. Ported from zudo-doc's `HeadingH2`, stripped\n * of styling — v0 ships pass-through behaviour; visual treatment is layered\n * on by the consumer (or by a follow-up enhancement pass).\n */\nexport function ContentH2(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"h2\", props);\n}\n\n/** `<h3>` passthrough override. See [`ContentH2`] for the contract. */\nexport function ContentH3(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"h3\", props);\n}\n\n/** `<h4>` passthrough override. See [`ContentH2`] for the contract. */\nexport function ContentH4(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"h4\", props);\n}\n\n/** `<p>` passthrough override. Mirrors zudo-doc's `ContentParagraph`. */\nexport function ContentParagraph(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"p\", props);\n}\n\n/** `<a>` passthrough override. Mirrors zudo-doc's `ContentLink`. */\nexport function ContentLink(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"a\", props);\n}\n\n/** `<strong>` passthrough override. Mirrors zudo-doc's `ContentStrong`. */\nexport function ContentStrong(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"strong\", props);\n}\n\n/** `<blockquote>` passthrough override. Mirrors zudo-doc's `ContentBlockquote`. */\nexport function ContentBlockquote(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"blockquote\", props);\n}\n\n/** `<ul>` passthrough override. Mirrors zudo-doc's `ContentUl`. */\nexport function ContentUl(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"ul\", props);\n}\n\n/** `<ol>` passthrough override. Mirrors zudo-doc's `ContentOl`. */\nexport function ContentOl(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"ol\", props);\n}\n\n/** `<table>` passthrough override. Mirrors zudo-doc's `ContentTable`. */\nexport function ContentTable(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"table\", props);\n}\n\n/** `<code>` passthrough override. Mirrors zudo-doc's `ContentCode`. */\nexport function ContentCode(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"code\", props);\n}\n\n/**\n * Default per-element override map — eleven entries covering the markdown\n * tags the zudo-doc convention overrides (`h2`, `h3`, `h4`, `p`, `a`,\n * `strong`, `blockquote`, `ul`, `ol`, `table`, `code`).\n *\n * `h1` is intentionally absent: page titles render from frontmatter, per\n * the zudo-doc convention.\n *\n * Spread into a `components` prop to compose with custom overrides:\n *\n * ```tsx\n * import { defaultComponents } from \"zfb\";\n *\n * <entry.Content components={{ ...defaultComponents, h2: MyFancyH2 }} />\n * ```\n */\nexport const defaultComponents = {\n h2: ContentH2,\n h3: ContentH3,\n h4: ContentH4,\n p: ContentParagraph,\n a: ContentLink,\n strong: ContentStrong,\n blockquote: ContentBlockquote,\n ul: ContentUl,\n ol: ContentOl,\n table: ContentTable,\n code: ContentCode,\n} as const;\n\n/**\n * Merge component maps with the documented precedence order:\n * built-in `defaultComponents` → global slot (`globalThis.__zfb?.mdxComponents`)\n * → per-call `props.components`.\n *\n * Spread in stable key order so the resulting map is deterministic; later\n * entries in the spread win on collision (lowest → highest priority). Absent\n * layers (`undefined`) are no-ops via spread-of-undefined.\n *\n * **Output-neutral by design:** `defaultComponents` entries are pure\n * passthroughs (e.g. `ContentH2` → `<h2>{...props}</h2>`), so introducing\n * this merge into `buildContentComponent` does not change the rendered output.\n */\nexport function mergeMdxComponents(\n globalSlot: MdxComponents | undefined,\n perCall: MdxComponents | undefined,\n): MdxComponents {\n return { ...defaultComponents, ...globalSlot, ...perCall };\n}\n"]}
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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","sourcesContent":["// `zfb/content` — minimal v0 content collection loader.\n//\n// Reads `*.md` files from a content collection directory, parses YAML\n// frontmatter, and returns typed entries. This is a deliberately small\n// stub so the bundled basic-blog template can call `getCollection(\"blog\")` today;\n// the production path lives in `crates/zfb-content` and will replace this\n// once the JS-runtime decision (ADR-001) lands and the renderer wires the\n// Rust pipeline back through to user code.\n//\n// Scope (v0):\n// - YAML-ish frontmatter only: `key: value`, plus `key:\\n - item` arrays.\n// Quoted strings are unwrapped. ISO dates stay as strings.\n// - Body is the post content **after** the closing `---`, returned as raw\n// text. This is intentionally NOT pre-rendered HTML: the markdown\n// pipeline lives in the Rust crate and the JS stub does not duplicate\n// it.\n// - Collection root is resolved from\n// `process.env.ZFB_CONTENT_ROOT` (set by the dev/build pipeline), or\n// `<cwd>/content` as a fallback for unit tests and direct invocation.\n//\n// TODO(zfb-content): swap this stub for the runtime-provided implementation\n// once the content engine ships end-to-end.\n\n// `node:fs` and `node:path` are intentionally NOT imported statically at\n// the top level.\n//\n// Why: this module is reachable via the package root (`@takazudo/zfb`) — the\n// barrel re-exports `defaultComponents` / `ContentH2` / etc. from\n// `./content.js`. The islands per-island bundler (`crates/zfb-islands`) walks\n// `import * as Mod from \"@takazudo/zfb\"` and esbuild's static tree-shaker\n// cannot prune a module behind a wildcard barrel access, so the WHOLE\n// content.ts module ends up in the browser-side island bundle. Top-level\n// `node:fs` / `node:path` imports would then fail the bundle with\n// `Could not resolve \"node:fs\"`. Loading them indirectly through a\n// runtime-constructed `createRequire` keeps the Node-runtime fs path\n// working while letting the islands bundler emit browser-safe output.\n// (Discovered while investigating zudolab/zudo-doc#1355 Wave 3 — see also\n// upstream PR #134 / #130 Gap A.) Defense-in-depth: the islands esbuild\n// invocation also passes `--platform=browser --external:node:*` so any\n// stray `node:*` import that does end up in a browser bundle is\n// externalized rather than failing the build.\n//\n// `getCollection` is synchronous per ADR-004, so the node modules are\n// loaded synchronously on first fs-path use. Type-only imports below stay\n// at the top because TypeScript erases them at compile time — they leave\n// no runtime traces for esbuild to chase.\nimport { Fragment, jsx, jsxs } from \"./zudo-react/jsx-runtime.js\";\n\nimport type * as NodeFs from \"node:fs\";\nimport type * as NodePath from \"node:path\";\n\nimport { parseFrontmatter } from \"./frontmatter.js\";\nimport type { ParsedFrontmatter } from \"./frontmatter.js\";\nimport type { VNode } from \"./jsx-types.js\";\nimport type { Description } from \"./zudo-react/description.js\";\n\n// Re-export the parser surface so existing `zfb/content` consumers that\n// import `parseFrontmatter` / `ParsedFrontmatter` from the content\n// subpath keep working. The implementation now lives in `./frontmatter.ts`\n// (BCI-3 fs-free subpath) — this re-export is the bridge for callers\n// that have not migrated yet.\nexport { parseFrontmatter };\nexport type { ParsedFrontmatter };\n\n// ---------------------------------------------------------------------------\n// In-memory ContentSnapshot bridge (consumed by `@takazudo/zfb-runtime`).\n//\n// At build time, the Rust pipeline produces a `ContentSnapshot` (see\n// `crates/zfb-content/src/content_bridge.rs`) and embeds it into the\n// Worker bundle. On Worker boot, `createPageRouter` calls\n// `setContentSnapshot(snapshot)` (below) before serving the first\n// request. From that point on, `getCollection(name)` resolves from the\n// embedded snapshot rather than the Node `fs` API — required because the\n// workerd / Cloudflare Workers runtime has no filesystem.\n//\n// The fs path remains the source of truth in two contexts:\n// 1. unit tests for this module (no snapshot installed → fs path),\n// 2. dev-preview / direct-Node invocations of `getCollection` outside\n// the Worker bundle (kept as v0 fallback so older callers still work).\n//\n// Keep [`SnapshotEntry`] / [`Snapshot`] aligned with the Rust struct\n// (`EntrySnapshot` / `ContentSnapshot`) and the runtime-package mirror\n// (`@takazudo/zfb-runtime/snapshot`). Field names are snake_case to\n// match the JSON serialization (`module_specifier`, `rel_path`).\n// ---------------------------------------------------------------------------\n\n/**\n * One entry in an embedded content snapshot. Mirrors\n * `crates/zfb-content/src/content_bridge.rs::EntrySnapshot`. Re-exported\n * by `@takazudo/zfb-runtime/snapshot` for the runtime-side bundle. See\n * that module for field-by-field documentation.\n */\nexport interface SnapshotEntry {\n readonly slug: string;\n readonly frontmatter: unknown;\n readonly body: string;\n readonly module_specifier: string;\n readonly rel_path: string;\n /**\n * Render-artifact metadata, present only when the build ran with\n * `emitRenderArtifacts` on and only for markdown entries. Mirrors\n * `crates/zfb-content/src/render_metadata.rs::RenderRegionMetadata`.\n */\n readonly render_metadata?: SnapshotRenderMetadata;\n}\n\n/**\n * `{ headings, source_digest }` for one content region. `source_digest`\n * is `\"sha256:\" + 64 hex` over the entry's RAW on-disk source bytes\n * (frontmatter included, no BOM strip, no CRLF normalization) — it\n * identifies the source, not the rendered output. See\n * `@takazudo/zfb-runtime/snapshot` for the full field documentation.\n */\nexport interface SnapshotRenderMetadata {\n readonly headings: readonly {\n readonly depth: number;\n readonly text: string;\n readonly slug: string;\n }[];\n readonly source_digest: string;\n}\n\n/**\n * Point-in-time snapshot of every configured collection. Mirrors\n * `crates/zfb-content/src/content_bridge.rs::ContentSnapshot`.\n */\nexport interface Snapshot {\n readonly collections: Readonly<Record<string, readonly SnapshotEntry[]>>;\n}\n\n/**\n * Where the installed [`Snapshot`] lives.\n *\n * The state hangs off `globalThis.__zfb.contentSnapshot`, NOT a\n * module-level `let`. This matters because under the production worker\n * bundle the consumer's pnpm-strict `node_modules` layout exposes\n * two physical paths to `@takazudo/zfb`:\n *\n * - top-level `node_modules/@takazudo/zfb` (imported by user pages), AND\n * - nested `node_modules/.pnpm/@takazudo+zfb-runtime@.../node_modules/\n * @takazudo/zfb` (imported by `@takazudo/zfb-runtime` itself).\n *\n * The bundler passes `esbuild --preserve-symlinks` whenever a custom\n * `node_modules_dir` is configured (see `crates/zfb-build/src/bundler.rs`\n * around `--external:node:*`), so esbuild treats those two symlink\n * targets as distinct sources and inlines `content.js` TWICE — yielding\n * two module instances of `zfb/content` in the final worker bundle.\n *\n * If `installedSnapshot` were a per-module `let`, `createPageRouter`\n * would install the snapshot on the runtime's copy and `getCollection`\n * (called from a user `paths()` export) would read from the user\n * page's copy — see `undefined`, and fall through to the `node:fs`\n * branch, which then throws because `node:*` is externalized in the\n * worker bundle. This is the regression #442 / #449 surfaced.\n *\n * Routing the slot through `globalThis` makes the snapshot bridge\n * symmetric with the existing `globalThis.__zfb.content` MDX-component\n * bridge (set by the build pipeline at `crates/zfb-build/src/bundler.rs`,\n * read by `Content` below): both pieces of cross-module state share\n * one well-known global, so any number of `zfb/content` module\n * instances in the same JS realm see the same value.\n *\n * Tracked under #449 (production fix for #442); the test-fixture\n * counterpart was #413.\n */\ntype SnapshotBridgeNamespace = {\n contentSnapshot?: Snapshot | undefined;\n};\n\ntype SnapshotBridgeGlobal = typeof globalThis & {\n __zfb?: SnapshotBridgeNamespace;\n};\n\n/**\n * Register a [`Snapshot`] so [`getCollection`] resolves from memory.\n *\n * Pass `undefined` to clear (used by tests that need to restore the v0\n * filesystem path between runs). Idempotent: the latest call wins.\n *\n * Stored on `globalThis.__zfb.contentSnapshot` rather than a\n * module-level `let` so a worker bundle that ends up with two\n * `zfb/content` module instances still sees a single shared snapshot —\n * see the [`SnapshotBridgeNamespace`] doc above for the full\n * pnpm-symlink rationale.\n */\nexport function setContentSnapshot(snapshot: Snapshot | undefined): void {\n const g = globalThis as SnapshotBridgeGlobal;\n const ns = (g.__zfb ?? {}) as SnapshotBridgeNamespace;\n ns.contentSnapshot = snapshot;\n g.__zfb = ns;\n}\n\n/**\n * Read the currently-installed [`Snapshot`], or `undefined` if none is\n * registered. Exposed mostly for tests; production callers should not\n * need to introspect the bridge state.\n *\n * Reads from `globalThis.__zfb.contentSnapshot`; see\n * [`setContentSnapshot`] for why the slot lives on `globalThis`.\n */\nexport function getContentSnapshot(): Snapshot | undefined {\n return (globalThis as SnapshotBridgeGlobal).__zfb?.contentSnapshot;\n}\n\n/**\n * Flat map of element-name → override component, used by both\n * [`ContentProps.components`] and the global slot\n * (`globalThis.__zfb?.mdxComponents`). Keys are lowercase HTML tag names\n * (`h2`, `p`, `a`, …) or PascalCase custom-component names.\n */\nexport type MdxComponents = Record<string, unknown>;\n\n/**\n * Props accepted by an entry's [`CollectionEntry.Content`] component.\n *\n * `components` mirrors Astro's `<Content components={...}>` contract:\n * a flat record of element-name → override component (e.g. `{ h1: MyH1 }`).\n * The default-components convention ships from `zfb`'s root export\n * (`defaultComponents`, lands in Sub 6) and users compose with their own\n * via `{ ...defaultComponents, ...mine }`.\n */\nexport interface ContentProps {\n /** Element-name → override component map. Optional. */\n components?: MdxComponents;\n}\n\n/**\n * Public JSX-element shape returned by [`CollectionEntry.Content`].\n *\n * A description-shaped value produced by the owned JSX runtime. Consumers\n * should treat this as opaque and renderable.\n */\nexport type ContentElement = Description;\n\n/**\n * Bridge contract published by the Rust-side `zfb-render` `Renderer` before\n * evaluating each page module. Cross-referenced from the Rust side in\n * `crates/zfb-render/src/loader.rs` so the two halves stay in sync — see\n * `packages/zfb/CONTRIBUTING.md` for the full contract narrative.\n *\n * The renderer installs `globalThis.__zfb.content.get(specifier)` keyed on\n * the entry's `module_specifier` (Sub 4 convention: `mdx://<collection>/<slug>#<hash>`,\n * collapsed to `mdx://<collection>/<slug>` from the JS stub side which has\n * no hash to compute). When `get` returns `undefined` (or the bridge as a\n * whole is absent — typical of unit tests, dev sandboxes, and any\n * non-renderer evaluation context), `Content` renders a clearly-marked\n * `<pre data-zfb-content-fallback>` fallback so the visual distinction is\n * obvious even in unstyled environments.\n */\ntype ContentBridge = {\n get(specifier: string): ((props: ContentProps) => unknown) | undefined;\n};\n\ntype ZfbBridgeNamespace = {\n content?: ContentBridge;\n /**\n * Global component-override slot. Populated by sub-task A2 (bridge\n * installer); A1 only reads it. Absent ⇒ no-op in the merge.\n */\n mdxComponents?: MdxComponents;\n /**\n * Render-region marker switch (epic #2421). When `true`, every\n * bridge-resolved `Content` render is wrapped in the inert\n * `<template data-zfb-render-region>` sentinel pair that the build's\n * extraction pass slices on — see [`wrapInRenderRegion`].\n *\n * **Build-only by construction.** The bundler emits the setter into\n * the synthetic `entry.mjs` only when the run is `zfb build`\n * (`BundleMode::Production`) AND `emitRenderArtifacts` resolved on;\n * `zfb dev` passes `BundleMode::Development`, so the setter's bytes\n * are never written into a dev bundle. It is never emitted into a\n * client/island bundle either — `entry.mjs` is the SSR entry.\n */\n renderArtifacts?: boolean;\n};\n\ntype BridgeGlobal = typeof globalThis & {\n __zfb?: ZfbBridgeNamespace;\n};\n\n/**\n * Generic shape returned for one entry in a content collection. The `data`\n * field carries parsed frontmatter, typed by the caller via the generic\n * parameter.\n */\nexport type CollectionEntry<T = Record<string, unknown>> = {\n /** Filename without `.md` extension. Stable across runs. */\n slug: string;\n /** Parsed frontmatter. */\n data: T;\n /** Raw markdown body (frontmatter stripped). */\n body: string;\n /**\n * Stable module specifier used as the bridge lookup key. Format:\n * `mdx://<collection>/<slug>` (no hash component — the JS stub does\n * not compile MDX, so it has no body hash to attach; the production\n * Rust-side `zfb-content::collection::Entry::module_specifier` adds a\n * `#<hash>` suffix and the bridge is responsible for matching either\n * form against its registered components).\n *\n * This field is part of the v0+ JS surface so the bridge has something\n * deterministic to key on without consulting per-call state.\n */\n module_specifier: string;\n /**\n * Renderable component for this entry.\n *\n * **Bridge contract.** At call time, `Content` consults\n * `globalThis.__zfb?.content?.get(entry.module_specifier)`. If the\n * bridge is present and returns a function, that function is invoked\n * with `props` and its result returned verbatim.\n *\n * **Fallback.** Outside the renderer (unit tests, dev sandboxes, or any\n * environment where `globalThis.__zfb.content.get` is absent or returns\n * `undefined`), `Content` returns a JSX-shaped element rendering the\n * raw markdown body inside a `<pre data-zfb-content-fallback>` block,\n * with a leading `[zfb fallback render]` marker line so the visual\n * distinction survives unstyled environments. The marker is also a\n * grep target for \"did the production renderer not run?\" diagnostics.\n *\n * **Typed signature.** Returns `ContentElement` (a structural alias for\n * `JSX.Element`) so consumers can drop `<entry.Content components={...} />`\n * into owned JSX without additional type setup.\n *\n * @example\n * const post = (await getCollection(\"blog\"))[0];\n * return <post.Content components={{ ...defaultComponents, h1: MyH1 }} />;\n */\n Content: (props: ContentProps) => ContentElement;\n};\n\n// Cached node:fs / node:path module references. Populated lazily on first\n// fs-path use (see [`loadNodeModules`]); reused on subsequent calls.\nlet cachedNodeFs: typeof NodeFs | undefined;\nlet cachedNodePath: typeof NodePath | undefined;\n\n/**\n * Synchronously load `node:fs` and `node:path`, caching the results.\n *\n * The node specifiers are concatenated at runtime (`\"node:\" + \"fs\"`) so\n * esbuild's static analyzer cannot follow them — that's the load-bearing\n * detail here, because this module is reachable from browser-bundled\n * island chains via the `@takazudo/zfb` root barrel (see top-of-file note).\n *\n * Uses CommonJS `require` via [`createRequire`] (stable, sync) rather than\n * `await import()` (async, would force `getCollection` async and violate\n * ADR-004). `createRequire` itself is fetched from `node:module` through\n * the same runtime-built specifier pattern.\n *\n * If `createRequire` cannot be obtained at all (i.e. truly running in a\n * browser-shaped runtime — which would mean a misconfigured island\n * bundle), throws so the failure is loud rather than silent.\n */\nfunction loadNodeModules(): { fs: typeof NodeFs; path: typeof NodePath } {\n if (cachedNodeFs !== undefined && cachedNodePath !== undefined) {\n return { fs: cachedNodeFs, path: cachedNodePath };\n }\n // Runtime-built specifiers: opaque to esbuild's static analyzer.\n const moduleSpecifier = \"node:\" + \"module\";\n const fsSpecifier = \"node:\" + \"fs\";\n const pathSpecifier = \"node:\" + \"path\";\n // Strategy A: prefer the host `require` from a CommonJS context. We\n // probe via `globalThis` and `Function`-built lookup so neither esbuild\n // nor stricter ESM tooling errors out at the lookup site.\n const dynamicGlobal = globalThis as unknown as { require?: NodeJS.Require };\n let nodeRequire: NodeJS.Require | undefined = dynamicGlobal.require;\n // Strategy B: ESM context — synthesize a require via `node:module`'s\n // `createRequire`. Loading `node:module` itself through the same\n // dynamic specifier shields it from esbuild's static walker.\n if (typeof nodeRequire !== \"function\") {\n // `Function(\"return require\")()` returns the enclosing `require` when\n // the bundler/loader injects one (Node CJS, esbuild default). Falls\n // through if undefined — caught below.\n try {\n nodeRequire = new Function(\"return typeof require === 'function' ? require : undefined\")() as\n | NodeJS.Require\n | undefined;\n } catch {\n nodeRequire = undefined;\n }\n }\n if (typeof nodeRequire !== \"function\") {\n // Last resort: synthesize via createRequire. Reaches `node:module`\n // through a dynamic require we have to bootstrap somehow — the only\n // way without a static `import` is `process.getBuiltinModule` (Node\n // 22+) which exposes built-ins synchronously without a require.\n const proc = (\n globalThis as unknown as { process?: { getBuiltinModule?: (id: string) => unknown } }\n ).process;\n const getBuiltin = proc?.getBuiltinModule;\n if (typeof getBuiltin === \"function\") {\n const mod = getBuiltin(moduleSpecifier) as typeof import(\"node:module\");\n nodeRequire = mod.createRequire(import.meta.url);\n }\n }\n if (typeof nodeRequire !== \"function\") {\n throw new Error(\n \"zfb/content: cannot load node:fs / node:path — no Node-style require available. \" +\n \"This module's filesystem path requires a Node runtime; if you see this in a browser \" +\n \"bundle, the bundler should externalize node:* imports (the islands bundler does so).\",\n );\n }\n cachedNodeFs = nodeRequire(fsSpecifier) as typeof NodeFs;\n cachedNodePath = nodeRequire(pathSpecifier) as typeof NodePath;\n return { fs: cachedNodeFs, path: cachedNodePath };\n}\n\n/**\n * Resolve the directory that holds a named content collection. Override\n * via `ZFB_CONTENT_ROOT` so tests / fixtures can point at an arbitrary\n * directory.\n */\nfunction resolveCollectionDir(name: string): string {\n const { path } = loadNodeModules();\n const envRoot = process.env[\"ZFB_CONTENT_ROOT\"];\n const root = envRoot ? path.resolve(envRoot) : path.resolve(process.cwd(), \"content\");\n return path.join(root, name);\n}\n\n/**\n * Build the v0 stub's bridge specifier for an entry. Mirrors the Rust-side\n * convention (`mdx://<collection>/<slug>`) minus the body hash — the JS\n * stub does not compile MDX, so it has no hash to attach. The bridge\n * resolver on the renderer side is responsible for matching either form.\n */\nfunction buildModuleSpecifier(collection: string, slug: string): string {\n return `mdx://${collection}/${slug}`;\n}\n\n/**\n * Build the `Content` component for an entry. Captures `module_specifier`\n * + `body` in the closure so the returned function takes only `props`.\n *\n * The bridge lookup is done lazily on every call (not at entry-construction\n * time) so the renderer can install / swap `globalThis.__zfb.content` at\n * any point before the first render without ordering hazards.\n */\nfunction buildContentComponent(\n module_specifier: string,\n body: string,\n): (props: ContentProps) => ContentElement {\n return function Content(props: ContentProps): ContentElement {\n const zfb = (globalThis as BridgeGlobal).__zfb;\n const bridge = zfb?.content;\n const renderer = bridge?.get(module_specifier);\n if (typeof renderer === \"function\") {\n // Merge components in documented precedence order before delegating:\n // defaultComponents → globalThis.__zfb.mdxComponents → props.components\n // This is output-neutral because defaultComponents entries are pure\n // passthroughs; the seam is established here for A2 to populate.\n const mergedProps: ContentProps = {\n ...props,\n components: mergeMdxComponents(zfb?.mdxComponents, props.components),\n };\n // Trust the bridge to return a JSX-element-shaped value — we don't\n // try to validate; the compiled renderer is the source of truth for its returned value.\n const rendered = renderer(mergedProps) as ContentElement;\n // Render-artifact instrumentation (epic #2421). Off by default and\n // never set outside `zfb build`, so the common path returns the\n // bridge's value verbatim and the emitted HTML is byte-identical\n // to a build without the feature.\n return zfb?.renderArtifacts === true\n ? wrapInRenderRegion(module_specifier, rendered)\n : rendered;\n }\n return renderFallback(body);\n };\n}\n\n/**\n * Attribute names of the render-region sentinel pair. The\n * `data-zfb-render-region` / `data-zfb-region-id` namespace is reserved\n * by the render-artifact contract (epic #2421): the build's extraction\n * pass is an exact-byte state machine over these two attributes, so\n * nothing else may emit them.\n */\nconst RENDER_REGION_ATTR = \"data-zfb-render-region\";\nconst REGION_ID_ATTR = \"data-zfb-region-id\";\n\n/**\n * Wrap a bridge-rendered content region in its sentinel pair:\n *\n * ```html\n * <template data-zfb-render-region=\"start\" data-zfb-region-id=\"<id>\"></template>\n * …region…\n * <template data-zfb-render-region=\"end\" data-zfb-region-id=\"<id>\"></template>\n * ```\n *\n * `<template>` is inert in every HTML context (its contents are not\n * rendered and it carries no layout), and the pair is emitted as three\n * Fragment children with **no text nodes between them** — the extraction\n * pass slices on exact bytes, so an introduced space or newline would\n * land inside the captured fragment.\n *\n * `id` is the entry's `module_specifier`, the region id the artifact\n * writer joins its `{ headings, sourceDigest }` metadata on. Repeated\n * `Content` calls therefore emit sibling pairs sharing one id, and a\n * `Content` rendered inside another emits properly nested pairs; the\n * extraction state machine matches identical-id pairs by nesting order.\n *\n * Fragment and `jsxs` are imported directly from the owned JSX runtime.\n */\nfunction wrapInRenderRegion(regionId: string, rendered: ContentElement): ContentElement {\n return jsxs(Fragment, {\n children: [\n renderRegionMarker(\"start\", regionId),\n rendered,\n renderRegionMarker(\"end\", regionId),\n ],\n });\n}\n\n/** One `<template>` sentinel. See [`wrapInRenderRegion`]. */\nfunction renderRegionMarker(edge: \"start\" | \"end\", regionId: string): ContentElement {\n return mintElement(\"template\", {\n [RENDER_REGION_ATTR]: edge,\n [REGION_ID_ATTR]: regionId,\n });\n}\n\n/**\n * Mint a content element through the owned JSX runtime. `children` is\n * passed inside `props` so a single child or an array both pass through\n * verbatim. Same migration as `Island` in this package. Kept private so\n * callers keep treating `ContentElement` / `ContentComponentElement` as\n * opaque. (Empty-MDX-body history: zudo-doc#505.)\n */\nfunction mintElement(type: string, props: Record<string, unknown>): ContentElement {\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 owned factory's first-param type.\n return jsx(type as Parameters<typeof jsx>[0], props);\n}\n\n/**\n * Build the structural JSX element returned when the bridge is absent.\n *\n * Shape: `<pre data-zfb-content-fallback>{marker}\\n{body}</pre>` — the\n * leading `[zfb fallback render]` marker line is part of the public\n * fallback contract (it's both a visual signal and a grep target). Tests\n * pin both the attribute and the marker line.\n */\nfunction renderFallback(body: string): ContentElement {\n return mintElement(\"pre\", {\n \"data-zfb-content-fallback\": \"\",\n children: `${FALLBACK_MARKER}\\n${body}`,\n });\n}\n\n/** Leading marker line emitted by [`renderFallback`]. Public contract. */\nconst FALLBACK_MARKER = \"[zfb fallback render]\";\n\n/**\n * Load every `*.md` file in the named collection. Files starting with `.`\n * or that lack a `.md` extension are ignored.\n *\n * **ADR-004 contract: this function is synchronous.** TSX page modules\n * call it from anywhere — top-level, inside a render body, inside a\n * `useMemo` — and SSR completes in a single pass without yielding. The\n * snapshot path returns from memory; the filesystem fallback uses sync\n * `node:fs` APIs so the surface stays unified. (The legacy async\n * implementation was an oversight — the ADR predates it; SSG paths\n * always saw a Promise where ADR-004 says they should see an array,\n * which is why migrations from Astro tripped on `getCollection().filter\n * is not a function`.)\n *\n * @example\n * const posts = getCollection<{ title: string; date: string }>(\"blog\");\n */\nexport function getCollection<T = Record<string, unknown>>(name: string): CollectionEntry<T>[] {\n // Snapshot path: installed by `@takazudo/zfb-runtime`'s\n // `createPageRouter` at Worker boot. Worker runtimes have no `fs`, so\n // this branch is the production path under the embedded V8 host.\n //\n // The snapshot lookup reads `globalThis.__zfb.contentSnapshot`\n // (see `setContentSnapshot` above) rather than a per-module slot so\n // the cross-`zfb/content`-instance case under `--preserve-symlinks`\n // resolves through the same shared state — see #449.\n const installedSnapshot = (globalThis as SnapshotBridgeGlobal).__zfb?.contentSnapshot;\n if (installedSnapshot !== undefined) {\n const list = installedSnapshot.collections[name] ?? [];\n return list.map((entry) => entryFromSnapshot<T>(entry));\n }\n // Filesystem fallback (v0 path). Used by unit tests and direct Node\n // invocations outside the Worker bundle.\n //\n // BCI-6: traversal is now recursive — subdirectories are walked so a\n // collection rooted at `content/blog/` can contain nested `*.md` files\n // (e.g. `content/blog/2024/hello.md`). Slugs are derived from the\n // relative path so callers get stable, unique identifiers across nesting\n // levels.\n const dir = resolveCollectionDir(name);\n let mdPaths: string[];\n try {\n mdPaths = collectMdFilesSync(dir);\n } catch (err) {\n // Guard the `code` access at runtime — a thrown non-`Error` value\n // (rare, but possible) would otherwise crash here. We only swallow\n // a true ENOENT; anything else propagates.\n if (\n err !== null &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err as { code: unknown }).code === \"ENOENT\"\n ) {\n return [];\n }\n throw err;\n }\n const { fs, path } = loadNodeModules();\n return mdPaths.map((fullPath) => {\n const raw = fs.readFileSync(fullPath, \"utf8\");\n const { data, body } = parseFrontmatter(raw);\n // Derive a stable slug from the relative path (relative to collection\n // root), stripping the `.md` extension. For top-level files this\n // produces the same value as before; for nested files it produces a\n // path-based slug (e.g. `2024/hello`).\n const rel = path.relative(dir, fullPath);\n const slug = _relPathToSlug(rel);\n const module_specifier = buildModuleSpecifier(name, slug);\n return {\n slug,\n data: data as T,\n body,\n module_specifier,\n Content: buildContentComponent(module_specifier, body),\n };\n });\n}\n\n/**\n * Look up a single entry in a content collection by slug.\n *\n * Thin wrapper over [`getCollection`]: inherits both resolution paths\n * (snapshot via `globalThis.__zfb.contentSnapshot` and the `node:fs`\n * fallback) for free. Returns `undefined` when either the collection does\n * not exist or no entry matches `slug`.\n *\n * **Runtime vs. generated types divergence.** The generated `types.d.ts`\n * emits a keyed overload (`K extends keyof ZfbCollections`) that ties the\n * return type to the collection's declared schema. That schema is enforced\n * by `zfb check`; this runtime form is intentionally structural — it does\n * not reference `ZfbCollections` and does not attempt to reconcile with the\n * keyed shape. (#857)\n *\n * @example\n * const post = getEntry<{ title: string }>(\"blog\", \"hello-zfb\");\n * if (!post) return null;\n * return <post.Content />;\n */\nexport function getEntry<T = Record<string, unknown>>(\n name: string,\n slug: string,\n): CollectionEntry<T> | undefined {\n return getCollection<T>(name).find((e) => e.slug === slug);\n}\n\n/**\n * Construct a [`CollectionEntry`] from a [`SnapshotEntry`]. The snapshot\n * carries `frontmatter` as a possibly-`null` JSON value (matches the\n * Rust contract for entries with no frontmatter); we normalise `null` /\n * `undefined` to an empty object so consumers' `.data.title` reads\n * never have to deal with `null`.\n *\n * **Type-safety note:** `T` is the caller-supplied frontmatter shape\n * but we do **not** validate it at runtime — if the page declares a\n * shape that the actual frontmatter doesn't match, the cast below\n * lies. Callers are expected to keep their `getCollection<MySchema>()`\n * generic in sync with the actual frontmatter; we acknowledge the\n * unsafety with the explicit `unknown` indirection rather than a\n * direct (and silently lossy) cast.\n */\nfunction entryFromSnapshot<T>(entry: SnapshotEntry): CollectionEntry<T> {\n const data =\n entry.frontmatter === null || entry.frontmatter === undefined\n ? ({} as T)\n : (entry.frontmatter as unknown as T);\n return {\n slug: entry.slug,\n data,\n body: entry.body,\n module_specifier: entry.module_specifier,\n Content: buildContentComponent(entry.module_specifier, entry.body),\n };\n}\n\n/**\n * Recursively collect every `*.md` file under `dir` (synchronous).\n *\n * BCI-6: replaces the old flat `readdir(dir).filter(n => n.endsWith(\".md\"))`\n * approach. Hidden files (names starting with `.`) and hidden directories\n * are skipped at every nesting level, matching the top-level behaviour of\n * the previous implementation.\n *\n * Returns absolute paths sorted lexicographically so the result order is\n * deterministic across platforms and Node versions.\n *\n * Synchronous to honour ADR-004 — see [`getCollection`].\n */\nfunction collectMdFilesSync(dir: string): string[] {\n const result: string[] = [];\n const { fs, path } = loadNodeModules();\n walkDirSync(fs, path, dir, result);\n result.sort();\n return result;\n}\n\nfunction walkDirSync(\n fs: typeof NodeFs,\n path: typeof NodePath,\n current: string,\n out: string[],\n): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) continue;\n const fullPath = path.join(current, entry.name);\n // Skip symlinks to avoid infinite loops caused by cycles (e.g. a symlink\n // pointing at a parent directory). Content files are expected to be plain\n // regular files; following symlinks provides no value here.\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n walkDirSync(fs, path, fullPath, out);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n out.push(fullPath);\n }\n }\n}\n\n/**\n * @internal\n *\n * Convert a `path.relative()` result into a forward-slash-separated\n * slug with the trailing `.md` extension stripped.\n *\n * Slugs are URL-flavored identifiers, not filesystem paths — they\n * MUST use `/` regardless of the host OS so a nested entry like\n * `2024/hello.md` produces the slug `2024/hello` on both POSIX and\n * Windows. Without this normalisation, Windows callers would see\n * `2024\\hello`, which then leaks through to `module_specifier` and\n * any URL the consumer derives from the slug.\n *\n * Exported solely so the unit test suite can pin the Windows\n * behaviour without needing an actual Windows host. Do not depend on\n * this from application code — name and signature may change.\n */\nexport function _relPathToSlug(relPath: string): string {\n const { path } = loadNodeModules();\n const posix = path.sep === \"/\" ? relPath : relPath.split(path.sep).join(\"/\");\n // Some Node versions normalise `\\` even when sep is `/`, so be\n // defensive: collapse any straggling backslashes too.\n const normalised = posix.includes(\"\\\\\") ? posix.split(\"\\\\\").join(\"/\") : posix;\n return normalised.endsWith(\".md\") ? normalised.slice(0, -\".md\".length) : normalised;\n}\n\n// ---------------------------------------------------------------------------\n// `defaultComponents` — htmlOverrides convention\n//\n// Ported from zudo-doc's `src/components/content/component-map.ts`. Users opt\n// in by spreading the map into their own `components` prop:\n//\n// import { defaultComponents } from \"zfb\";\n// <entry.Content components={{ ...defaultComponents, h2: MyH2 }} />\n//\n// Each component is a thin passthrough mirroring its zudo-doc counterpart\n// (e.g. `ContentParagraph` → `<p {...rest}>{children}</p>`). v0 ships the\n// passthroughs unstyled; layering smart-break / heading-anchor / link-icon\n// behaviour on top is independent follow-up — keeping the v0 deliverable\n// focused on infrastructure (issue #33).\n//\n// **`h1` is deliberately not in the map** — page titles render `<h1>` from\n// frontmatter, per the zudo-doc convention. Adding `h1` here would silently\n// double-render the page title.\n//\n// **Each override is exported as a named const AND included in\n// `defaultComponents`** so consumers can tree-shake-import a single component\n// (`import { ContentLink } from \"zfb\"`) without dragging in the whole map.\n//\n// Overrides return descriptions minted by the owned JSX runtime.\n// ---------------------------------------------------------------------------\n\n/**\n * Public JSX-element shape returned by every override in [`defaultComponents`].\n *\n * Mirrors [`ContentElement`] and [`IslandElement`]: a structural alias for\n * `JSX.Element` for owned JSX consumers.\n */\nexport type ContentComponentElement = Description;\n\n/**\n * Props accepted by every default override. `children` and any extra\n * attributes (`class`, `id`, `href`, …) are passed through verbatim\n * to the underlying HTML element.\n */\nexport interface ContentComponentProps {\n children?: VNode;\n [key: string]: unknown;\n}\n\n/** Internal helper: build a structural JSX element of the given tag. */\nfunction buildOverrideElement(tag: string, props: ContentComponentProps): ContentComponentElement {\n const { children, ...rest } = props;\n // Minted through the owned JSX runtime.\n return mintElement(tag, { ...rest, children });\n}\n\n/**\n * `<h2>` passthrough override. Ported from zudo-doc's `HeadingH2`, stripped\n * of styling — v0 ships pass-through behaviour; visual treatment is layered\n * on by the consumer (or by a follow-up enhancement pass).\n */\nexport function ContentH2(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"h2\", props);\n}\n\n/** `<h3>` passthrough override. See [`ContentH2`] for the contract. */\nexport function ContentH3(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"h3\", props);\n}\n\n/** `<h4>` passthrough override. See [`ContentH2`] for the contract. */\nexport function ContentH4(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"h4\", props);\n}\n\n/** `<p>` passthrough override. Mirrors zudo-doc's `ContentParagraph`. */\nexport function ContentParagraph(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"p\", props);\n}\n\n/** `<a>` passthrough override. Mirrors zudo-doc's `ContentLink`. */\nexport function ContentLink(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"a\", props);\n}\n\n/** `<strong>` passthrough override. Mirrors zudo-doc's `ContentStrong`. */\nexport function ContentStrong(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"strong\", props);\n}\n\n/** `<blockquote>` passthrough override. Mirrors zudo-doc's `ContentBlockquote`. */\nexport function ContentBlockquote(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"blockquote\", props);\n}\n\n/** `<ul>` passthrough override. Mirrors zudo-doc's `ContentUl`. */\nexport function ContentUl(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"ul\", props);\n}\n\n/** `<ol>` passthrough override. Mirrors zudo-doc's `ContentOl`. */\nexport function ContentOl(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"ol\", props);\n}\n\n/** `<table>` passthrough override. Mirrors zudo-doc's `ContentTable`. */\nexport function ContentTable(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"table\", props);\n}\n\n/** `<code>` passthrough override. Mirrors zudo-doc's `ContentCode`. */\nexport function ContentCode(props: ContentComponentProps): ContentComponentElement {\n return buildOverrideElement(\"code\", props);\n}\n\n/**\n * Default per-element override map — eleven entries covering the markdown\n * tags the zudo-doc convention overrides (`h2`, `h3`, `h4`, `p`, `a`,\n * `strong`, `blockquote`, `ul`, `ol`, `table`, `code`).\n *\n * `h1` is intentionally absent: page titles render from frontmatter, per\n * the zudo-doc convention.\n *\n * Spread into a `components` prop to compose with custom overrides:\n *\n * ```tsx\n * import { defaultComponents } from \"zfb\";\n *\n * <entry.Content components={{ ...defaultComponents, h2: MyFancyH2 }} />\n * ```\n */\nexport const defaultComponents = {\n h2: ContentH2,\n h3: ContentH3,\n h4: ContentH4,\n p: ContentParagraph,\n a: ContentLink,\n strong: ContentStrong,\n blockquote: ContentBlockquote,\n ul: ContentUl,\n ol: ContentOl,\n table: ContentTable,\n code: ContentCode,\n} as const;\n\n/**\n * Merge component maps with the documented precedence order:\n * built-in `defaultComponents` → global slot (`globalThis.__zfb?.mdxComponents`)\n * → per-call `props.components`.\n *\n * Spread in stable key order so the resulting map is deterministic; later\n * entries in the spread win on collision (lowest → highest priority). Absent\n * layers (`undefined`) are no-ops via spread-of-undefined.\n *\n * **Output-neutral by design:** `defaultComponents` entries are pure\n * passthroughs (e.g. `ContentH2` → `<h2>{...props}</h2>`), so introducing\n * this merge into `buildContentComponent` does not change the rendered output.\n */\nexport function mergeMdxComponents(\n globalSlot: MdxComponents | undefined,\n perCall: MdxComponents | undefined,\n): MdxComponents {\n return { ...defaultComponents, ...globalSlot, ...perCall };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -3,8 +3,9 @@ export { HYDRATE_MARKER_ATTR, Island, SKIP_SSR_MARKER_ATTR, resolveWhen, type Is
3
3
  export { scheduleHydrate, mountIslands, mountNewIslands, cancelPendingIslands, unmountIslands, ISLAND_MOUNTED_ATTR, } from "./runtime.js";
4
4
  export type { IslandManifest, IslandManifestValue } from "./runtime.js";
5
5
  export type { VNode, VNodeArray, VNodeObject } from "./jsx-types.js";
6
+ export type { Child, Description } from "./zudo-react/description.js";
6
7
  export { DEFAULT_WHEN, isWhen, WHEN_VALUES, type When } from "./types.js";
7
- export { definePlugin, type ZfbBuildHookContext, type ZfbDevMiddlewareContext, type ZfbDevMiddlewareHandler, type ZfbDevMiddlewareRequest, type ZfbDevMiddlewareResponse, type ZfbPlugin, type ZfbPluginLogger, type ZfbPreviewMiddlewareContext, type ZfbPreviewMiddlewareHandler, } from "./plugins.js";
8
+ export { definePlugin, type ZfbBuildHookContext, type ZfbDevMiddlewareContext, type ZfbDevMiddlewareHandler, type ZfbDevMiddlewareRequest, type ZfbDevMiddlewareResponse, type ZfbPlugin, type ZfbPluginLogger, type ZfbPluginDiagnosticMetadata, type ZfbPreviewMiddlewareContext, type ZfbPreviewMiddlewareHandler, } from "./plugins.js";
8
9
  export { slugify, SlugAllocator } from "./slugify.js";
9
10
  export { clientScript } from "./client-script.js";
10
11
  export { ContentBlockquote, ContentCode, ContentH2, ContentH3, ContentH4, ContentLink, ContentOl, ContentParagraph, ContentStrong, ContentTable, ContentUl, defaultComponents, mergeMdxComponents, type ContentComponentElement, type ContentComponentProps, type MdxComponents, } from "./content.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,iFAAiF;AACjF,uDAAuD;AACvD,gFAAgF;AAChF,uCAAuC;AAEvC,yEAAyE;AACzE,2EAA2E;AAC3E,OAAO,WAAW,CAAC;AAEnB,OAAO,EACL,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,WAAW,GAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAE1E,yEAAyE;AACzE,iFAAiF;AACjF,oEAAoE;AACpE,+FAA+F;AAC/F,qCAAqC;AACrC,kEAAkE;AAClE,sEAAsE;AACtE,wEAAwE;AACxE,YAAY;AACZ,OAAO,EACL,YAAY,GAUb,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,kBAAkB,GAInB,MAAM,cAAc,CAAC","sourcesContent":["// Public entry point for the \"@takazudo/zfb\" package.\n//\n// User TSX pages reach this module via `import { Island } from \"@takazudo/zfb\"`.\n// The hydration runtime (Sub 3) reaches the helper via\n// `import { scheduleHydrate } from \"@takazudo/zfb/runtime\"` (or by inlining the\n// same logic; coordinated separately).\n\n// Keep the global Wasm declaration reachable from this package's emitted\n// root declaration. The compiled module is intentionally empty at runtime.\nimport \"./wasm.js\";\n\nexport {\n HYDRATE_MARKER_ATTR,\n Island,\n SKIP_SSR_MARKER_ATTR,\n resolveWhen,\n type IslandElement,\n type IslandProps,\n} from \"./island.js\";\nexport {\n scheduleHydrate,\n mountIslands,\n mountNewIslands,\n cancelPendingIslands,\n unmountIslands,\n ISLAND_MOUNTED_ATTR,\n} from \"./runtime.js\";\nexport type { IslandManifest, IslandManifestValue } from \"./runtime.js\";\nexport type { VNode, VNodeArray, VNodeObject } from \"./jsx-types.js\";\nexport { DEFAULT_WHEN, isWhen, WHEN_VALUES, type When } from \"./types.js\";\n\n// `defaultComponents` (htmlOverrides convention) is re-exported from the\n// root entry point so `import { defaultComponents } from \"@takazudo/zfb\"` is the\n// canonical access path. Each named override is also re-exported so\n// consumers can tree-shake-import a single one (`import { ContentLink } from \"@takazudo/zfb\"`)\n// without dragging in the whole map.\n// Plugin lifecycle types + `definePlugin` identity helper. Plugin\n// authors typically import these from \"@takazudo/zfb/plugins\" but the\n// root entry re-exports them so simple plugins can pull everything from\n// one path.\nexport {\n definePlugin,\n type ZfbBuildHookContext,\n type ZfbDevMiddlewareContext,\n type ZfbDevMiddlewareHandler,\n type ZfbDevMiddlewareRequest,\n type ZfbDevMiddlewareResponse,\n type ZfbPlugin,\n type ZfbPluginLogger,\n type ZfbPreviewMiddlewareContext,\n type ZfbPreviewMiddlewareHandler,\n} from \"./plugins.js\";\n\nexport { slugify, SlugAllocator } from \"./slugify.js\";\n\nexport { clientScript } from \"./client-script.js\";\n\nexport {\n ContentBlockquote,\n ContentCode,\n ContentH2,\n ContentH3,\n ContentH4,\n ContentLink,\n ContentOl,\n ContentParagraph,\n ContentStrong,\n ContentTable,\n ContentUl,\n defaultComponents,\n mergeMdxComponents,\n type ContentComponentElement,\n type ContentComponentProps,\n type MdxComponents,\n} from \"./content.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,iFAAiF;AACjF,uDAAuD;AACvD,gFAAgF;AAChF,uCAAuC;AAEvC,yEAAyE;AACzE,2EAA2E;AAC3E,OAAO,WAAW,CAAC;AAEnB,OAAO,EACL,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,WAAW,GAGZ,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAItB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAE1E,yEAAyE;AACzE,iFAAiF;AACjF,oEAAoE;AACpE,+FAA+F;AAC/F,qCAAqC;AACrC,kEAAkE;AAClE,sEAAsE;AACtE,wEAAwE;AACxE,YAAY;AACZ,OAAO,EACL,YAAY,GAWb,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,kBAAkB,GAInB,MAAM,cAAc,CAAC","sourcesContent":["// Public entry point for the \"@takazudo/zfb\" package.\n//\n// User TSX pages reach this module via `import { Island } from \"@takazudo/zfb\"`.\n// The hydration runtime (Sub 3) reaches the helper via\n// `import { scheduleHydrate } from \"@takazudo/zfb/runtime\"` (or by inlining the\n// same logic; coordinated separately).\n\n// Keep the global Wasm declaration reachable from this package's emitted\n// root declaration. The compiled module is intentionally empty at runtime.\nimport \"./wasm.js\";\n\nexport {\n HYDRATE_MARKER_ATTR,\n Island,\n SKIP_SSR_MARKER_ATTR,\n resolveWhen,\n type IslandElement,\n type IslandProps,\n} from \"./island.js\";\nexport {\n scheduleHydrate,\n mountIslands,\n mountNewIslands,\n cancelPendingIslands,\n unmountIslands,\n ISLAND_MOUNTED_ATTR,\n} from \"./runtime.js\";\nexport type { IslandManifest, IslandManifestValue } from \"./runtime.js\";\nexport type { VNode, VNodeArray, VNodeObject } from \"./jsx-types.js\";\nexport type { Child, Description } from \"./zudo-react/description.js\";\nexport { DEFAULT_WHEN, isWhen, WHEN_VALUES, type When } from \"./types.js\";\n\n// `defaultComponents` (htmlOverrides convention) is re-exported from the\n// root entry point so `import { defaultComponents } from \"@takazudo/zfb\"` is the\n// canonical access path. Each named override is also re-exported so\n// consumers can tree-shake-import a single one (`import { ContentLink } from \"@takazudo/zfb\"`)\n// without dragging in the whole map.\n// Plugin lifecycle types + `definePlugin` identity helper. Plugin\n// authors typically import these from \"@takazudo/zfb/plugins\" but the\n// root entry re-exports them so simple plugins can pull everything from\n// one path.\nexport {\n definePlugin,\n type ZfbBuildHookContext,\n type ZfbDevMiddlewareContext,\n type ZfbDevMiddlewareHandler,\n type ZfbDevMiddlewareRequest,\n type ZfbDevMiddlewareResponse,\n type ZfbPlugin,\n type ZfbPluginLogger,\n type ZfbPluginDiagnosticMetadata,\n type ZfbPreviewMiddlewareContext,\n type ZfbPreviewMiddlewareHandler,\n} from \"./plugins.js\";\n\nexport { slugify, SlugAllocator } from \"./slugify.js\";\n\nexport { clientScript } from \"./client-script.js\";\n\nexport {\n ContentBlockquote,\n ContentCode,\n ContentH2,\n ContentH3,\n ContentH4,\n ContentLink,\n ContentOl,\n ContentParagraph,\n ContentStrong,\n ContentTable,\n ContentUl,\n defaultComponents,\n mergeMdxComponents,\n type ContentComponentElement,\n type ContentComponentProps,\n type MdxComponents,\n} from \"./content.js\";\n"]}
@@ -1,4 +1,4 @@
1
1
  import { type Description } from "./zudo-react/index.js";
2
- import type { VNode } from "./jsx-types.js";
2
+ import type { Child } from "./zudo-react/description.js";
3
3
  import type { When } from "./types.js";
4
- export declare function ownedIslandBoundary(child: VNode, fallback: VNode | undefined, when: When, media: string | undefined): Description;
4
+ export declare function ownedIslandBoundary(child: Description | undefined, fallback: Child | undefined, when: When, media: string | undefined, persist?: string, persistProps?: boolean): Description;
@@ -13,7 +13,7 @@ function singleChild(value) {
13
13
  throw new TypeError("ZR_ISLAND_CHILD: exactly one function component child required");
14
14
  return value;
15
15
  }
16
- export function ownedIslandBoundary(child, fallback, when, media) {
16
+ export function ownedIslandBoundary(child, fallback, when, media, persist, persistProps) {
17
17
  const description = singleChild(child);
18
18
  const functionName = description.type.name;
19
19
  const displayName = description.type
@@ -35,8 +35,10 @@ export function ownedIslandBoundary(child, fallback, when, media) {
35
35
  identity: { component, build },
36
36
  when,
37
37
  media,
38
+ persist,
39
+ persistProps,
38
40
  skipSsr: fallback !== undefined,
39
- fallback: fallback,
41
+ fallback,
40
42
  });
41
43
  }
42
44
  //# sourceMappingURL=island-boundary.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"island-boundary.js","sourceRoot":"","sources":["../src/island-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAKpD,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,KAAK;aACnB,IAAI,CAAC,QAAQ,CAAC;aACd,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC;QAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,uDAAuD,CAAC,CAAC;QAC/E,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU;QAC3D,MAAM,IAAI,SAAS,CAAC,gEAAgE,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,KAAY,EACZ,QAA2B,EAC3B,IAAU,EACV,KAAyB;IAEzB,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,YAAY,GAAI,WAAW,CAAC,IAAiB,CAAC,IAAI,CAAC;IACzD,MAAM,WAAW,GAAI,WAAW,CAAC,IAA2D;SACzF,WAAW,CAAC;IACf,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,SAAS,CAAC,yCAAyC,CAAC,CAAC;IAClF,IAAI,WAAW,IAAI,WAAW,KAAK,YAAY;QAC7C,MAAM,IAAI,SAAS,CACjB,uBAAuB,YAAY,+BAA+B,WAAW,EAAE,CAChF,CAAC;IACJ,MAAM,SAAS,GAAG,WAAW,IAAI,YAAY,CAAC;IAC9C,MAAM,QAAQ,GACZ,UAGD,CAAC,KAAK,CAAC;IACR,MAAM,KAAK,GAAG,QAAQ,EAAE,cAAc,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,uBAAuB,SAAS,wBAAwB,CAAC,CAAC;IAC1F,IAAI,CAAC,QAAQ,EAAE,gBAAgB;QAC7B,MAAM,IAAI,SAAS,CAAC,uBAAuB,SAAS,mCAAmC,CAAC,CAAC;IAC3F,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC;QAChD,MAAM,IAAI,SAAS,CAAC,uBAAuB,SAAS,mCAAmC,CAAC,CAAC;IAC3F,OAAO,UAAU,CAAC,WAAW,EAAE;QAC7B,QAAQ,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;QAC9B,IAAI;QACJ,KAAK;QACL,OAAO,EAAE,QAAQ,KAAK,SAAS;QAC/B,QAAQ,EAAE,QAAiB;KAC5B,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { isDescription, type Description } from \"./zudo-react/index.js\";\nimport { islandRoot } from \"./zudo-react/server.js\";\nimport type { VNode } from \"./jsx-types.js\";\nimport type { Child } from \"./zudo-react/description.js\";\nimport type { When } from \"./types.js\";\n\nfunction singleChild(value: unknown): Description {\n if (Array.isArray(value)) {\n const children = value\n .flat(Infinity)\n .filter((child) => child != null && typeof child !== \"boolean\");\n if (children.length !== 1)\n throw new TypeError(\"ZR_ISLAND_CHILD: exactly one component child required\");\n return singleChild(children[0]);\n }\n if (!isDescription(value) || typeof value.type !== \"function\")\n throw new TypeError(\"ZR_ISLAND_CHILD: exactly one function component child required\");\n return value;\n}\n\nexport function ownedIslandBoundary(\n child: VNode,\n fallback: VNode | undefined,\n when: When,\n media: string | undefined,\n): Description {\n const description = singleChild(child);\n const functionName = (description.type as Function).name;\n const displayName = (description.type as typeof description.type & { displayName?: string })\n .displayName;\n if (!functionName) throw new TypeError(\"ZR_ISLAND_IDENTITY: anonymous component\");\n if (displayName && displayName !== functionName)\n throw new TypeError(\n `ZR_ISLAND_IDENTITY: ${functionName} conflicts with displayName ${displayName}`,\n );\n const component = displayName ?? functionName;\n const metadata = (\n globalThis as typeof globalThis & {\n __zfb?: { zudoReactBuild?: string; zudoReactIslands?: readonly string[] };\n }\n ).__zfb;\n const build = metadata?.zudoReactBuild;\n if (!build) throw new TypeError(`ZR_ISLAND_IDENTITY: ${component} has no build identity`);\n if (!metadata?.zudoReactIslands)\n throw new TypeError(`ZR_ISLAND_IDENTITY: ${component} has no scanner identity metadata`);\n if (!metadata.zudoReactIslands.includes(component))\n throw new TypeError(`ZR_ISLAND_IDENTITY: ${component} is not registered by the scanner`);\n return islandRoot(description, {\n identity: { component, build },\n when,\n media,\n skipSsr: fallback !== undefined,\n fallback: fallback as Child,\n });\n}\n"]}
1
+ {"version":3,"file":"island-boundary.js","sourceRoot":"","sources":["../src/island-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAIpD,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,KAAK;aACnB,IAAI,CAAC,QAAQ,CAAC;aACd,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC;QAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,uDAAuD,CAAC,CAAC;QAC/E,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU;QAC3D,MAAM,IAAI,SAAS,CAAC,gEAAgE,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,KAA8B,EAC9B,QAA2B,EAC3B,IAAU,EACV,KAAyB,EACzB,OAAgB,EAChB,YAAsB;IAEtB,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,YAAY,GAAI,WAAW,CAAC,IAAiB,CAAC,IAAI,CAAC;IACzD,MAAM,WAAW,GAAI,WAAW,CAAC,IAA2D;SACzF,WAAW,CAAC;IACf,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,SAAS,CAAC,yCAAyC,CAAC,CAAC;IAClF,IAAI,WAAW,IAAI,WAAW,KAAK,YAAY;QAC7C,MAAM,IAAI,SAAS,CACjB,uBAAuB,YAAY,+BAA+B,WAAW,EAAE,CAChF,CAAC;IACJ,MAAM,SAAS,GAAG,WAAW,IAAI,YAAY,CAAC;IAC9C,MAAM,QAAQ,GACZ,UAGD,CAAC,KAAK,CAAC;IACR,MAAM,KAAK,GAAG,QAAQ,EAAE,cAAc,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,SAAS,CAAC,uBAAuB,SAAS,wBAAwB,CAAC,CAAC;IAC1F,IAAI,CAAC,QAAQ,EAAE,gBAAgB;QAC7B,MAAM,IAAI,SAAS,CAAC,uBAAuB,SAAS,mCAAmC,CAAC,CAAC;IAC3F,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC;QAChD,MAAM,IAAI,SAAS,CAAC,uBAAuB,SAAS,mCAAmC,CAAC,CAAC;IAC3F,OAAO,UAAU,CAAC,WAAW,EAAE;QAC7B,QAAQ,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;QAC9B,IAAI;QACJ,KAAK;QACL,OAAO;QACP,YAAY;QACZ,OAAO,EAAE,QAAQ,KAAK,SAAS;QAC/B,QAAQ;KACT,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { isDescription, type Description } from \"./zudo-react/index.js\";\nimport { islandRoot } from \"./zudo-react/server.js\";\nimport type { Child } from \"./zudo-react/description.js\";\nimport type { When } from \"./types.js\";\n\nfunction singleChild(value: unknown): Description {\n if (Array.isArray(value)) {\n const children = value\n .flat(Infinity)\n .filter((child) => child != null && typeof child !== \"boolean\");\n if (children.length !== 1)\n throw new TypeError(\"ZR_ISLAND_CHILD: exactly one component child required\");\n return singleChild(children[0]);\n }\n if (!isDescription(value) || typeof value.type !== \"function\")\n throw new TypeError(\"ZR_ISLAND_CHILD: exactly one function component child required\");\n return value;\n}\n\nexport function ownedIslandBoundary(\n child: Description | undefined,\n fallback: Child | undefined,\n when: When,\n media: string | undefined,\n persist?: string,\n persistProps?: boolean,\n): Description {\n const description = singleChild(child);\n const functionName = (description.type as Function).name;\n const displayName = (description.type as typeof description.type & { displayName?: string })\n .displayName;\n if (!functionName) throw new TypeError(\"ZR_ISLAND_IDENTITY: anonymous component\");\n if (displayName && displayName !== functionName)\n throw new TypeError(\n `ZR_ISLAND_IDENTITY: ${functionName} conflicts with displayName ${displayName}`,\n );\n const component = displayName ?? functionName;\n const metadata = (\n globalThis as typeof globalThis & {\n __zfb?: { zudoReactBuild?: string; zudoReactIslands?: readonly string[] };\n }\n ).__zfb;\n const build = metadata?.zudoReactBuild;\n if (!build) throw new TypeError(`ZR_ISLAND_IDENTITY: ${component} has no build identity`);\n if (!metadata?.zudoReactIslands)\n throw new TypeError(`ZR_ISLAND_IDENTITY: ${component} has no scanner identity metadata`);\n if (!metadata.zudoReactIslands.includes(component))\n throw new TypeError(`ZR_ISLAND_IDENTITY: ${component} is not registered by the scanner`);\n return islandRoot(description, {\n identity: { component, build },\n when,\n media,\n persist,\n persistProps,\n skipSsr: fallback !== undefined,\n fallback,\n });\n}\n"]}
package/dist/island.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { VNode } from "./jsx-types.js";
2
- import type { Description } from "./zudo-react/description.js";
1
+ import type { Child, Description } from "./zudo-react/description.js";
3
2
  import { type When } from "./types.js";
4
3
  export { resolveWhen } from "./types.js";
5
4
  export declare const HYDRATE_MARKER_ATTR = "data-zfb-island";
@@ -7,8 +6,13 @@ export declare const SKIP_SSR_MARKER_ATTR = "data-zfb-island-skip-ssr";
7
6
  export interface IslandProps {
8
7
  when?: When;
9
8
  media?: string;
10
- ssrFallback?: VNode;
11
- children?: VNode;
9
+ ssrFallback?: Child;
10
+ /** Stable key for preserving this island root across client-side navigation. */
11
+ persist?: string;
12
+ /** Keep the original props and state when the key matches (default: refresh changed props). */
13
+ persistProps?: boolean;
14
+ /** JSX erases the tag kind; the boundary checks for one function component at runtime. */
15
+ children?: Description;
12
16
  }
13
17
  export type IslandElement = Description;
14
18
  export declare function Island(props: IslandProps): IslandElement;
package/dist/island.js CHANGED
@@ -6,7 +6,7 @@ export const HYDRATE_MARKER_ATTR = "data-zfb-island";
6
6
  export const SKIP_SSR_MARKER_ATTR = "data-zfb-island-skip-ssr";
7
7
  export function Island(props) {
8
8
  const { when, media } = resolveMediaProps(props);
9
- return ownedIslandBoundary(props.children, props.ssrFallback, when, media);
9
+ return ownedIslandBoundary(props.children, props.ssrFallback, when, media, props.persist, props.persistProps);
10
10
  }
11
11
  function resolveMediaProps(props) {
12
12
  const when = resolveWhen(props.when);
@@ -1 +1 @@
1
- {"version":3,"file":"island.js","sourceRoot":"","sources":["../src/island.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAElE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACrD,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAW/D,MAAM,UAAU,MAAM,CAAC,KAAkB;IACvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACjD,OAAO,mBAAmB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAkB;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAE1B,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,sFAAsF;gBACpF,oBAAoB,YAAY,IAAI,CACvC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAClD,CAAC;IAED,IAAI,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QAC9B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,+DAA+D;gBAC7D,iBAAiB,IAAI,gCAAgC,CACxD,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC","sourcesContent":["// Build-time Island wrapper for the owned zudo-react runtime.\nimport { ownedIslandBoundary } from \"./island-boundary.js\";\nimport type { VNode } from \"./jsx-types.js\";\nimport type { Description } from \"./zudo-react/description.js\";\nimport { DEFAULT_WHEN, resolveWhen, type When } from \"./types.js\";\n\nexport { resolveWhen } from \"./types.js\";\nexport const HYDRATE_MARKER_ATTR = \"data-zfb-island\";\nexport const SKIP_SSR_MARKER_ATTR = \"data-zfb-island-skip-ssr\";\n\nexport interface IslandProps {\n when?: When;\n media?: string;\n ssrFallback?: VNode;\n children?: VNode;\n}\n\nexport type IslandElement = Description;\n\nexport function Island(props: IslandProps): IslandElement {\n const { when, media } = resolveMediaProps(props);\n return ownedIslandBoundary(props.children, props.ssrFallback, when, media);\n}\n\nfunction resolveMediaProps(props: IslandProps): { when: When; media: string | undefined } {\n const when = resolveWhen(props.when);\n const media = props.media;\n\n if (when === \"media\" && !media) {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] <Island when=\"media\"> requires the \\`media\\` prop (a CSS media query string). ` +\n `Falling back to \"${DEFAULT_WHEN}\".`,\n );\n }\n return { when: DEFAULT_WHEN, media: undefined };\n }\n\n if (media && when !== \"media\") {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] The \\`media\\` prop is only used when \\`when=\"media\"\\`. ` +\n `Current when=\"${when}\" — \\`media\\` will be ignored.`,\n );\n }\n return { when, media: undefined };\n }\n\n return { when, media };\n}\n"]}
1
+ {"version":3,"file":"island.js","sourceRoot":"","sources":["../src/island.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAa,MAAM,YAAY,CAAC;AAElE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACrD,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAgB/D,MAAM,UAAU,MAAM,CAAC,KAAkB;IACvC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACjD,OAAO,mBAAmB,CACxB,KAAK,CAAC,QAAQ,EACd,KAAK,CAAC,WAAW,EACjB,IAAI,EACJ,KAAK,EACL,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,YAAY,CACnB,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAkB;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAE1B,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,sFAAsF;gBACpF,oBAAoB,YAAY,IAAI,CACvC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAClD,CAAC;IAED,IAAI,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QAC9B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;YAC9F,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,+DAA+D;gBAC7D,iBAAiB,IAAI,gCAAgC,CACxD,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC","sourcesContent":["// Build-time Island wrapper for the owned zudo-react runtime.\nimport { ownedIslandBoundary } from \"./island-boundary.js\";\nimport type { Child, Description } from \"./zudo-react/description.js\";\nimport { DEFAULT_WHEN, resolveWhen, type When } from \"./types.js\";\n\nexport { resolveWhen } from \"./types.js\";\nexport const HYDRATE_MARKER_ATTR = \"data-zfb-island\";\nexport const SKIP_SSR_MARKER_ATTR = \"data-zfb-island-skip-ssr\";\n\nexport interface IslandProps {\n when?: When;\n media?: string;\n ssrFallback?: Child;\n /** Stable key for preserving this island root across client-side navigation. */\n persist?: string;\n /** Keep the original props and state when the key matches (default: refresh changed props). */\n persistProps?: boolean;\n /** JSX erases the tag kind; the boundary checks for one function component at runtime. */\n children?: Description;\n}\n\nexport type IslandElement = Description;\n\nexport function Island(props: IslandProps): IslandElement {\n const { when, media } = resolveMediaProps(props);\n return ownedIslandBoundary(\n props.children,\n props.ssrFallback,\n when,\n media,\n props.persist,\n props.persistProps,\n );\n}\n\nfunction resolveMediaProps(props: IslandProps): { when: When; media: string | undefined } {\n const when = resolveWhen(props.when);\n const media = props.media;\n\n if (when === \"media\" && !media) {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] <Island when=\"media\"> requires the \\`media\\` prop (a CSS media query string). ` +\n `Falling back to \"${DEFAULT_WHEN}\".`,\n );\n }\n return { when: DEFAULT_WHEN, media: undefined };\n }\n\n if (media && when !== \"media\") {\n if (typeof process !== \"undefined\" && process.env && process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n `[zfb] The \\`media\\` prop is only used when \\`when=\"media\"\\`. ` +\n `Current when=\"${when}\" — \\`media\\` will be ignored.`,\n );\n }\n return { when, media: undefined };\n }\n\n return { when, media };\n}\n"]}
@@ -4,7 +4,7 @@ export type VNodeObject = {
4
4
  readonly props: Readonly<Record<string, unknown>>;
5
5
  readonly key: unknown;
6
6
  };
7
- /** Broad authored child input; the owned renderer validates actual children. */
7
+ /** @deprecated Broad legacy input. Use Child from @takazudo/zfb/zudo-react for owned JSX. */
8
8
  export type VNode = string | number | boolean | null | undefined | bigint | VNodeArray | VNodeObject | object;
9
9
  export interface VNodeArray extends ReadonlyArray<VNode> {
10
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"jsx-types.js","sourceRoot":"","sources":["../src/jsx-types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,0EAA0E;AAC1E,+BAA+B","sourcesContent":["// Structural input types used by SDK helpers that inspect authored children.\n// The owned Island boundary validates descriptions and component identity\n// at runtime before rendering.\n\n/** Opaque description-shaped object accepted at the SDK boundary. */\nexport type VNodeObject = {\n readonly type: string | ((...args: unknown[]) => unknown) | (new (...args: unknown[]) => unknown);\n readonly props: Readonly<Record<string, unknown>>;\n readonly key: unknown;\n};\n\n/** Broad authored child input; the owned renderer validates actual children. */\nexport type VNode =\n | string\n | number\n | boolean\n | null\n | undefined\n | bigint\n | VNodeArray\n | VNodeObject\n | object;\n\nexport interface VNodeArray extends ReadonlyArray<VNode> {}\n"]}
1
+ {"version":3,"file":"jsx-types.js","sourceRoot":"","sources":["../src/jsx-types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,0EAA0E;AAC1E,+BAA+B","sourcesContent":["// Structural input types used by SDK helpers that inspect authored children.\n// The owned Island boundary validates descriptions and component identity\n// at runtime before rendering.\n\n/** Opaque description-shaped object accepted at the SDK boundary. */\nexport type VNodeObject = {\n readonly type: string | ((...args: unknown[]) => unknown) | (new (...args: unknown[]) => unknown);\n readonly props: Readonly<Record<string, unknown>>;\n readonly key: unknown;\n};\n\n/** @deprecated Broad legacy input. Use Child from @takazudo/zfb/zudo-react for owned JSX. */\nexport type VNode =\n | string\n | number\n | boolean\n | null\n | undefined\n | bigint\n | VNodeArray\n | VNodeObject\n | object;\n\nexport interface VNodeArray extends ReadonlyArray<VNode> {}\n"]}
package/dist/plugins.d.ts CHANGED
@@ -1,16 +1,30 @@
1
1
  /**
2
2
  * Logger handed to every plugin hook. `info`/`warn`/`error` each render on
3
3
  * the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,
4
- * attributed to the plugin: `zfb <level>: [plugin:<name>] <message>`.
4
+ * attributed to the plugin: `zfb <level>: ZB010 plugin:<name>: <message>`
5
+ * (or the explicitly supplied diagnostic code and source).
5
6
  * `console.*` is redirected the same way, but note it maps onto only two
6
7
  * underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->
7
8
  * error) — `console.warn` therefore renders as `zfb error:`, not
8
9
  * `zfb warn:`. Prefer this logger over `console.*` when the level matters.
9
10
  */
10
11
  export type ZfbPluginLogger = {
11
- info(msg: string): void;
12
- warn(msg: string): void;
13
- error(msg: string): void;
12
+ info(msg: string, diagnostic?: ZfbPluginDiagnosticMetadata): void;
13
+ warn(msg: string, diagnostic?: ZfbPluginDiagnosticMetadata): void;
14
+ error(msg: string, diagnostic?: ZfbPluginDiagnosticMetadata): void;
15
+ };
16
+ /** Optional structured context; legacy one-argument logger calls remain valid. */
17
+ export type ZfbPluginDiagnosticMetadata = {
18
+ /** Stable plugin-owned code, preferably `<plugin-name>/<class>`; defaults to ZB010. */
19
+ code?: string;
20
+ /** Opaque source identity; defaults to `plugin:<name>`. */
21
+ sourceId?: string;
22
+ /** Authored source path, when known. */
23
+ file?: string;
24
+ /** One-based authored source line. */
25
+ line?: number;
26
+ /** One-based UTF-8 byte column. Do not pass a character or UTF-16 column. */
27
+ byteColumn?: number;
14
28
  };
15
29
  /**
16
30
  * One emitted route in the `postBuild` route manifest (#262).
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,sEAAsE;AACtE,6CAA6C;AAC7C,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AAqe/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// `zfb/plugins` — TypeScript helper for the zfb plugin lifecycle.\n//\n// A plugin is a JS module whose default export is a [`ZfbPlugin`] object.\n// `zfb.config.ts` references plugins by `name` (npm bare specifier or a\n// `./`-relative path); the zfb config loader resolves each `name` to an\n// absolute module specifier and the Rust-side plugin host loads the\n// module via dynamic `import()` and dispatches the lifecycle hooks.\n//\n// Sub 3 / issue #108 — initial drop. Three optional hooks: `preBuild`,\n// `postBuild`, `devMiddleware`. Astro-migration epic #253 / sub-issue\n// #255 adds a fourth: `setup`, which runs once before `preBuild` and\n// lets plugins register virtual modules, import aliases, and dev-only\n// injected routes. None of the hooks see real Node IPC objects across\n// the boundary; everything is JSON-friendly.\n//\n// ## Inline functions are NOT supported\n//\n// `PluginConfig` (in `./config.ts`) carries only data. A user cannot\n// inline a function in `zfb.config.ts` — the config goes through a\n// JSON round-trip and any function value would be silently dropped.\n// Plugins must live in their own module (npm package or local file)\n// and be referenced by `name`.\n\n/**\n * Logger handed to every plugin hook. `info`/`warn`/`error` each render on\n * the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,\n * attributed to the plugin: `zfb <level>: [plugin:<name>] <message>`.\n * `console.*` is redirected the same way, but note it maps onto only two\n * underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->\n * error) — `console.warn` therefore renders as `zfb error:`, not\n * `zfb warn:`. Prefer this logger over `console.*` when the level matters.\n */\nexport type ZfbPluginLogger = {\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n};\n\n/**\n * One emitted route in the `postBuild` route manifest (#262).\n * Present on `ctx.routes.routes` so a `postBuild` plugin can iterate\n * every URL the build produced (e.g. to write a `sitemap.xml`).\n */\nexport type ZfbRouteEntry = {\n /** Emitted URL path, e.g. `/`, `/blog/hello/`, `/sitemap.xml`. */\n url: string;\n /** Path under `outDir`, e.g. `index.html`, `blog/hello/index.html`, `sitemap.xml`. */\n output: string;\n /** File extension: `html`, `xml`, `rss`, `txt`, `json`, … */\n extension: string;\n /** Source page module relative to the project root, e.g. `pages/blog/[slug].tsx`. */\n source: string;\n /**\n * `true` when the page is prerendered to disk (default / SSG); `false`\n * when the page exports `prerender = false` and is served by the\n * runtime adapter (SSR — no on-disk artifact under `outDir`).\n *\n * Indexes that enumerate on-disk URLs (sitemap.xml, search-index.json,\n * etc.) should filter `r.prerender !== false` to avoid surfacing SSR\n * routes that have no static output.\n */\n prerender: boolean;\n /**\n * Bound route parameters. Absent for static routes.\n * Dynamic (`[slug]`) params are string scalars; catchall (`[...rest]`)\n * params are string arrays.\n */\n params?: Record<string, string | string[]>;\n};\n\n/**\n * The route manifest exposed on `ctx.routes` during a `postBuild` callback\n * (#262). Sorted by `url` for byte-stable output across runs.\n */\nexport type ZfbRouteManifest = {\n routes: ZfbRouteEntry[];\n};\n\n/**\n * Context passed to `preBuild` and `postBuild`. `outDir` is the\n * resolved absolute path of the configured `outDir` (default\n * `<projectRoot>/dist`). `projectRoot` is the directory containing\n * `zfb.config.ts`.\n *\n * `routes` is **only present on `postBuild`** calls; it is `undefined`\n * on `preBuild`. This is intentional: the route manifest is not\n * available until the build finishes writing `dist/` (#262).\n */\nexport type ZfbBuildHookContext = {\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n /** Resolved absolute path of the build output directory. */\n outDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from the matching `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n /**\n * All routes emitted by this build, sorted by URL (#262).\n * Present only on `postBuild` calls; `undefined` on `preBuild`.\n */\n routes?: ZfbRouteManifest;\n};\n\n/**\n * A request handed to a `devMiddleware` handler. Subset of the Node\n * `http.IncomingMessage` surface intentionally — the dev server is\n * Rust-side `axum`, not Node, so we expose only what survives a JSON\n * envelope hop.\n */\nexport type ZfbDevMiddlewareRequest = {\n method: string;\n url: string;\n /** Lower-cased header names → first value. */\n headers: Record<string, string>;\n /** Raw request body; absent for GET/HEAD. UTF-8 only — binary is out of scope for v1 dev plugins. */\n body?: string;\n};\n\n/**\n * Response returned by a `devMiddleware` handler. All fields optional\n * except `status`. `body` may be a string (UTF-8) or a base64-encoded\n * binary payload (set `bodyEncoding` to `\"base64\"` in that case).\n */\nexport type ZfbDevMiddlewareResponse = {\n status: number;\n headers?: Record<string, string>;\n body?: string;\n bodyEncoding?: \"utf8\" | \"base64\";\n};\n\n/**\n * Handler signature for a `devMiddleware` registration. The `next` callback\n * is reserved for future composition; v1 plugins should produce a response\n * directly. Returning `undefined` from the handler signals \"I did not handle\n * this request\" — the dev server then falls through to its built-in routes\n * (the page cache, /__zfb/livereload.js, etc.).\n */\nexport type ZfbDevMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `devMiddleware`. The `register` callback installs\n * one handler per URL path prefix. `path` is matched as an exact prefix\n * — a registration on `/doc-history` matches `/doc-history` and\n * `/doc-history/foo`, but NOT `/doc-historyx`.\n */\nexport type ZfbDevMiddlewareContext = {\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbDevMiddlewareHandler): void;\n};\n\n/**\n * Handler signature for a `previewMiddleware` registration (#1542).\n * Deliberately reuses [`ZfbDevMiddlewareRequest`] /\n * [`ZfbDevMiddlewareResponse`] verbatim — the wire shape crossing the\n * Rust↔JS boundary is genuinely the SAME for dev and preview (mirrors\n * the Rust side, which shares `DevRequest`/`DevResponse` between both\n * hooks too), so there is nothing preview-specific to say about the\n * request/response contract itself. `next` is likewise reserved for\n * future composition; returning `undefined` signals \"I did not handle\n * this request\" and the preview server falls through to its built-in\n * routes (static-file serving, or the wrangler-backed adapter in\n * adapter mode).\n */\nexport type ZfbPreviewMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `previewMiddleware` (#1542). Structurally identical\n * to [`ZfbDevMiddlewareContext`] today — one handler per URL path\n * prefix, matched the same way — but declared as its own named type\n * (unlike the request/response types above, which are reused verbatim)\n * because the *context* is where a hook-specific capability would land\n * first if one were ever added (e.g. something preview-only that\n * `devMiddleware` has no equivalent for). Keeping it a separate\n * declaration costs nothing today and avoids a breaking rename later.\n */\nexport type ZfbPreviewMiddlewareContext = {\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbPreviewMiddlewareHandler): void;\n};\n\n/**\n * Loader signature for a virtual-module registration. Must return the\n * **complete ESM module source text** as a string — the bundler /\n * embedded V8 host feeds the returned string in as the module's\n * source verbatim. The loader runs **eagerly**, not lazily on first\n * import: exactly once per `zfb build` run and once per `zfb dev`\n * host boot, during the setup phase right after every plugin's\n * `setup` hook has returned — even if the registered specifier is\n * never imported by any page or module. The resulting source is\n * memoised; every subsequent import of that specifier reuses it,\n * **unless a forced reload is requested** (#2167) — the plugin-host\n * protocol now supports bypassing the memo and re-invoking the loader,\n * intended for a loader whose registration also declares\n * [`watchFiles`](#watchFiles) and needs a fresh read after one of\n * those files changes on disk. `zfb dev` watches every declared\n * [`watchFiles`](#watchFiles) path and re-invokes the owning loader with\n * its memo bypassed when one of them changes (#2169, #2181); `zfb build`\n * invokes each loader exactly once and never re-invokes it. See the\n * Plugins concept page for the full refresh contract.\n * (Under `zfb preview`, `addVirtualModule` registrations are accepted\n * but inert — see [`ZfbSetupContext.command`](#command) — so the\n * loader never runs there.)\n *\n * Example:\n *\n * ```ts\n * addVirtualModule(\"virtual:my-data\", () =>\n * `export default ${JSON.stringify(myJson)}`,\n * );\n * ```\n */\nexport type ZfbVirtualModuleLoader = () => string | Promise<string>;\n\n/**\n * Optional third argument to `addVirtualModule` (#2167).\n */\nexport type ZfbVirtualModuleOptions = {\n /**\n * Extra absolute filesystem paths a `zfb dev` watcher should track on\n * this loader's behalf — useful when the loader's output depends on\n * files it reads directly (e.g. via `node:fs`) rather than static ESM\n * imports the dev bundler would otherwise notice on its own.\n *\n * Every entry **must be an absolute path**: this mirrors\n * `extraWatchPaths`'s absolute-only rule in `zfb.config.ts`, and for\n * the same reason — `watchFiles` entries are never resolved against\n * the project root, so a relative entry has no defined base directory\n * to resolve against. A relative (or otherwise malformed) entry throws\n * at `setup` time.\n */\n watchFiles?: string[];\n};\n\n/**\n * Context passed to the new `setup` hook (#255). Runs once per host\n * boot, in `Config.plugins` declaration order, **before** `preBuild`.\n *\n * `ctx.command` tells the plugin which lifecycle is active so it can\n * gate per-lifecycle registrations. A dev-only mock route stays gated\n * to `\"dev\"`; a package-owned page route is registered unconditionally\n * (it is prerendered during a build and dev-routed during dev):\n *\n * ```ts\n * setup({ command, injectRoute }) {\n * // package-owned page route (rendered in build and dev)\n * injectRoute(\"/preset-page\", \"./pages/preset-page.tsx\");\n * // dev-only mock endpoint\n * if (command === \"dev\") {\n * injectRoute(\"/api/dev/x\", \"./scripts/dev-x.ts\");\n * }\n * }\n * ```\n *\n * The hook's surface is intentionally **closed**: only `injectRoute`,\n * `addVirtualModule`, `addAlias`, and `addClientEntry`. There is no\n * `addRemarkPlugin` / `addRehypePlugin` / `addMarkdownVisitor` — by\n * design (see the concept doc for the rationale). `addVirtualModule`'s\n * optional `watchFiles` argument (#2167) is a registration OPTION on\n * that existing method, not a new closed-surface method — the closed\n * set of four stays exactly four.\n */\nexport type ZfbSetupContext = {\n /**\n * Active zfb command. `\"build\"` during `zfb build`; `\"dev\"` during\n * `zfb dev`; `\"preview\"` during `zfb preview` (#1542). It can guide\n * lifecycle-specific plugin behavior. `injectRoute` registrations are\n * accepted in both `\"dev\"` and `\"build\"`; user `pages/` routes retain\n * precedence over matching injected routes (see\n * [`injectRoute`](#injectRoute)).\n *\n * Under `\"preview\"`, `setup` still fires (Rust-side via the minimal\n * non-V8 `run_preview_setup` path) so plugin-side state\n * initialisation runs, but `zfb preview` serves an ALREADY-BUILT\n * `dist/` verbatim and never re-enters the scan → bundle → render\n * pipeline. Consequently `injectRoute` / `addVirtualModule` /\n * `addAlias` / `addClientEntry` calls made under `\"preview\"` are\n * accepted (for shape-consistency with `\"build\"`/`\"dev\"`) but are\n * **inert** — nothing downstream ever reads them. Only the hook's\n * side effects and a subsequent `previewMiddleware` registration do\n * anything meaningful under `\"preview\"`.\n */\n command: \"build\" | \"dev\" | \"preview\";\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n\n /**\n * Register an import alias. **Exact-match-only in v1**:\n * `addAlias(\"@/foo\", \"./src/foo.tsx\")` rewrites `import \"@/foo\"`\n * but does NOT match `import \"@/foo/bar\"`. Prefix-matching is\n * explicitly deferred to v2 — switch to one bare alias per file\n * until then.\n *\n * `to` is resolved relative to the project root. Two plugins\n * registering the same `from` with different `to` raises\n * `AliasConflict` and aborts the build.\n *\n * A `to` that resolves to a single existing file is also applied\n * to importers inside `node_modules`, via esbuild `--alias`. A\n * directory-shaped or missing `to` is applied through tsconfig\n * `paths` only, which esbuild does not honor for `node_modules`\n * importers — packages that need an alias to work from inside\n * `node_modules` should register one alias per file, or use\n * `addVirtualModule`. A `from` that is a slash-prefix of another\n * alias, of a virtual module specifier, or of a reserved name\n * such as `zfb` gets no `--alias` flag.\n */\n addAlias(from: string, to: string): void;\n\n /**\n * Register a virtual module. `specifier` is a bare import\n * specifier (recommended `virtual:` prefix, not enforced).\n * `loader` returns the complete ESM source text as a string and\n * runs **eagerly, once per build/dev-boot during setup** — not\n * lazily at first import (see [`ZfbVirtualModuleLoader`], including\n * its forced-reload amendment).\n *\n * The optional third argument's `watchFiles` (#2167) declares extra\n * absolute filesystem paths a `zfb dev` watcher should track on this\n * loader's behalf — see [`ZfbVirtualModuleOptions`]. Every entry must\n * be an absolute path; a relative entry throws.\n *\n * Two plugins registering the same `specifier` raises\n * `VirtualModuleConflict` and aborts the build.\n */\n addVirtualModule(\n specifier: string,\n loader: ZfbVirtualModuleLoader,\n options?: ZfbVirtualModuleOptions,\n ): void;\n\n /**\n * Register a synthetic / package-owned page route. `pattern` uses the\n * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,\n * `/docs/[...rest]`).\n * Patterns below `/__paths__/` are reserved for zfb's internal `paths()`\n * endpoint and raise `ReservedRoutePrefix` with this plugin's name.\n *\n * - In **build** (package-owned routes), the route is materialised\n * into a per-build overlay pages root and **prerendered** through\n * the normal scan → bundle → render pipeline, so a preset can own a\n * route without the project shipping a `pages/` stub file. A `\"/\"`\n * package route becomes the project's root page when no user\n * `pages/index` exists, enabling a truly empty/absent user `pages/`.\n * A package route whose URL shape collides with a user `pages/` route\n * is dropped (user `pages/` wins). This is the supported, complete path.\n * - In **dev**, both static and dynamic injected routes are rendered\n * by `zfb dev`. Static routes (where the URL equals the pattern,\n * e.g. `/preset-about`) are seeded into the dev route universe at\n * boot; dynamic routes (e.g. `/preset-docs/[slug]`) are rendered\n * on first request via a request-time synthetic entry — params are\n * extracted from the URL by the Hono router inside the live bundle.\n * User `pages/` files take precedence over any injected route of\n * the same shape, including `pages/index` over an injected `\"/\"`.\n * Without a user index, an injected root is staged, seeded, and served\n * like any other static injected route. **HMR:** content the\n * route reads from watched collections live-refreshes normally.\n * Editing the package's **compiled entrypoint under `node_modules`**\n * is NOT watched and requires a `zfb dev` restart (restart-only\n * contract — a published package is not project source). **Per-route\n * data:** an injected route loads per-route data via a **dynamic\n * route's `paths()` export** (which returns `{ params, props }`);\n * `getStaticProps` on a package page is not forwarded by the overlay\n * (only `default` + the `prerender` hint are forwarded — same as\n * `zfb build`). A route that needs per-route data should be a\n * dynamic route whose `paths()` reads the data.\n *\n * `opts.prerender` controls the route's prerender shape during a\n * build: omit it (or `true`) for the SSG default; `false` marks an\n * SSR-shaped route, which `output: 'static'` rejects. It is build-only\n * metadata and ignored in dev.\n *\n * Two plugins registering the same `pattern` (or one plugin\n * re-registering it with a different entrypoint) raises\n * `InjectRouteConflict`.\n */\n injectRoute(pattern: string, entrypoint: string, opts?: { prerender?: boolean }): void;\n\n /**\n * Register a package-owned client-side side-effect entry (#1196).\n *\n * `entrypoint` **must** point to a `*.client.{ts,tsx,js,jsx}` file —\n * this is enforced (#1191 review [9]): a path missing the `.client.`\n * infix, or a bare `.client.ts` with an empty stem, throws an error\n * (`addClientEntry` JS-host validation + Rust `InvalidClientEntry`)\n * rather than being silently accepted under an invented name. The entry\n * name is derived from the filename stem minus `.client`\n * (e.g. `my-lib.client.ts` → `my-lib`), via the same canonical helper\n * as user-authored `*.client.*` discovery.\n *\n * The entry is bundled and shipped as\n * `/assets/client/<name>.js` (stable URL) / `/assets/client/<name>-<hash>.js`\n * (production, hashed). User-authored files win on name collision —\n * the registered entry is silently dropped when a user-authored file of\n * the same name exists in the discovery roots.\n *\n * Two plugins registering the same entry name with different entrypoints\n * raises `ClientEntryConflict` and aborts the build.\n *\n * `entrypoint` is resolved relative to the project root if given as a\n * relative path (same rule as `injectRoute`).\n */\n addClientEntry(entrypoint: string): void;\n};\n\n/**\n * The plugin-module shape. `name` is informational (the resolved module\n * specifier wins for identification on the Rust side) and helps the\n * plugin self-identify in logs.\n *\n * Five optional hooks; declaration-order matters when multiple plugins\n * touch the same surface. Each hook is independent — a plugin may\n * declare any subset:\n *\n * - `setup` (#255) — register virtual modules, aliases, injected\n * routes. Runs once at host boot, before `preBuild`. Also runs under\n * `zfb preview` (#1542) via the minimal non-V8 `run_preview_setup`\n * path — see [`ZfbSetupContext.command`](#command) for what is and\n * isn't meaningful there.\n * - `preBuild` — file-generation work that downstream stages will\n * see. Runs once per `zfb build` and once per `zfb dev` boot. Does\n * **NOT** fire under `zfb preview` (#1542) — preview serves an\n * already-built `dist/` and never re-triggers file generation.\n * - `postBuild` — finalisation work that runs after `dist/` has been\n * written. Does not fire under `zfb preview` either, for the same\n * reason as `preBuild`.\n * - `devMiddleware` — register HTTP handlers for ad-hoc dev-only\n * URLs. Per-request dispatch, distinct from `injectRoute` (which\n * goes through the page renderer). Fires only during `zfb dev`.\n * - `previewMiddleware` (#1542) — register HTTP handlers for ad-hoc\n * preview-only URLs. Same register-context shape as `devMiddleware`,\n * fires only during `zfb preview`. A plugin wanting coverage in both\n * modes registers the same handler under both hooks — `zfb` does\n * NOT reuse a `devMiddleware` registration for preview automatically\n * (explicit per-mode opt-in, by design).\n */\nexport type ZfbPlugin = {\n /** Plugin display name; surfaces in error / log lines. */\n name: string;\n setup?(ctx: ZfbSetupContext): Promise<void> | void;\n preBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n postBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n devMiddleware?(ctx: ZfbDevMiddlewareContext): Promise<void> | void;\n previewMiddleware?(ctx: ZfbPreviewMiddlewareContext): Promise<void> | void;\n};\n\n/**\n * Identity helper that types the supplied object as a [`ZfbPlugin`].\n * Use as the default export of a plugin module so editors surface\n * field-level types and typos surface at compile time.\n *\n * ```ts\n * import { definePlugin } from \"@takazudo/zfb/plugins\";\n *\n * export default definePlugin({\n * name: \"my-plugin\",\n * async preBuild({ outDir, logger }) {\n * logger.info(`generating index into ${outDir}`);\n * },\n * });\n * ```\n */\nexport function definePlugin(plugin: ZfbPlugin): ZfbPlugin {\n return plugin;\n}\n"]}
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,0EAA0E;AAC1E,wEAAwE;AACxE,wEAAwE;AACxE,oEAAoE;AACpE,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,sEAAsE;AACtE,6CAA6C;AAC7C,EAAE;AACF,wCAAwC;AACxC,EAAE;AACF,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,+BAA+B;AAof/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// `zfb/plugins` — TypeScript helper for the zfb plugin lifecycle.\n//\n// A plugin is a JS module whose default export is a [`ZfbPlugin`] object.\n// `zfb.config.ts` references plugins by `name` (npm bare specifier or a\n// `./`-relative path); the zfb config loader resolves each `name` to an\n// absolute module specifier and the Rust-side plugin host loads the\n// module via dynamic `import()` and dispatches the lifecycle hooks.\n//\n// Sub 3 / issue #108 — initial drop. Three optional hooks: `preBuild`,\n// `postBuild`, `devMiddleware`. Astro-migration epic #253 / sub-issue\n// #255 adds a fourth: `setup`, which runs once before `preBuild` and\n// lets plugins register virtual modules, import aliases, and dev-only\n// injected routes. None of the hooks see real Node IPC objects across\n// the boundary; everything is JSON-friendly.\n//\n// ## Inline functions are NOT supported\n//\n// `PluginConfig` (in `./config.ts`) carries only data. A user cannot\n// inline a function in `zfb.config.ts` — the config goes through a\n// JSON round-trip and any function value would be silently dropped.\n// Plugins must live in their own module (npm package or local file)\n// and be referenced by `name`.\n\n/**\n * Logger handed to every plugin hook. `info`/`warn`/`error` each render on\n * the `zfb dev`/`zfb build`/`zfb preview` terminal at exactly that level,\n * attributed to the plugin: `zfb <level>: ZB010 plugin:<name>: <message>`\n * (or the explicitly supplied diagnostic code and source).\n * `console.*` is redirected the same way, but note it maps onto only two\n * underlying streams (stdout -> info, stderr -> warn/error/trace/assert ->\n * error) — `console.warn` therefore renders as `zfb error:`, not\n * `zfb warn:`. Prefer this logger over `console.*` when the level matters.\n */\nexport type ZfbPluginLogger = {\n info(msg: string, diagnostic?: ZfbPluginDiagnosticMetadata): void;\n warn(msg: string, diagnostic?: ZfbPluginDiagnosticMetadata): void;\n error(msg: string, diagnostic?: ZfbPluginDiagnosticMetadata): void;\n};\n\n/** Optional structured context; legacy one-argument logger calls remain valid. */\nexport type ZfbPluginDiagnosticMetadata = {\n /** Stable plugin-owned code, preferably `<plugin-name>/<class>`; defaults to ZB010. */\n code?: string;\n /** Opaque source identity; defaults to `plugin:<name>`. */\n sourceId?: string;\n /** Authored source path, when known. */\n file?: string;\n /** One-based authored source line. */\n line?: number;\n /** One-based UTF-8 byte column. Do not pass a character or UTF-16 column. */\n byteColumn?: number;\n};\n\n/**\n * One emitted route in the `postBuild` route manifest (#262).\n * Present on `ctx.routes.routes` so a `postBuild` plugin can iterate\n * every URL the build produced (e.g. to write a `sitemap.xml`).\n */\nexport type ZfbRouteEntry = {\n /** Emitted URL path, e.g. `/`, `/blog/hello/`, `/sitemap.xml`. */\n url: string;\n /** Path under `outDir`, e.g. `index.html`, `blog/hello/index.html`, `sitemap.xml`. */\n output: string;\n /** File extension: `html`, `xml`, `rss`, `txt`, `json`, … */\n extension: string;\n /** Source page module relative to the project root, e.g. `pages/blog/[slug].tsx`. */\n source: string;\n /**\n * `true` when the page is prerendered to disk (default / SSG); `false`\n * when the page exports `prerender = false` and is served by the\n * runtime adapter (SSR — no on-disk artifact under `outDir`).\n *\n * Indexes that enumerate on-disk URLs (sitemap.xml, search-index.json,\n * etc.) should filter `r.prerender !== false` to avoid surfacing SSR\n * routes that have no static output.\n */\n prerender: boolean;\n /**\n * Bound route parameters. Absent for static routes.\n * Dynamic (`[slug]`) params are string scalars; catchall (`[...rest]`)\n * params are string arrays.\n */\n params?: Record<string, string | string[]>;\n};\n\n/**\n * The route manifest exposed on `ctx.routes` during a `postBuild` callback\n * (#262). Sorted by `url` for byte-stable output across runs.\n */\nexport type ZfbRouteManifest = {\n routes: ZfbRouteEntry[];\n};\n\n/**\n * Context passed to `preBuild` and `postBuild`. `outDir` is the\n * resolved absolute path of the configured `outDir` (default\n * `<projectRoot>/dist`). `projectRoot` is the directory containing\n * `zfb.config.ts`.\n *\n * `routes` is **only present on `postBuild`** calls; it is `undefined`\n * on `preBuild`. This is intentional: the route manifest is not\n * available until the build finishes writing `dist/` (#262).\n */\nexport type ZfbBuildHookContext = {\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n /** Resolved absolute path of the build output directory. */\n outDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from the matching `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n /**\n * All routes emitted by this build, sorted by URL (#262).\n * Present only on `postBuild` calls; `undefined` on `preBuild`.\n */\n routes?: ZfbRouteManifest;\n};\n\n/**\n * A request handed to a `devMiddleware` handler. Subset of the Node\n * `http.IncomingMessage` surface intentionally — the dev server is\n * Rust-side `axum`, not Node, so we expose only what survives a JSON\n * envelope hop.\n */\nexport type ZfbDevMiddlewareRequest = {\n method: string;\n url: string;\n /** Lower-cased header names → first value. */\n headers: Record<string, string>;\n /** Raw request body; absent for GET/HEAD. UTF-8 only — binary is out of scope for v1 dev plugins. */\n body?: string;\n};\n\n/**\n * Response returned by a `devMiddleware` handler. All fields optional\n * except `status`. `body` may be a string (UTF-8) or a base64-encoded\n * binary payload (set `bodyEncoding` to `\"base64\"` in that case).\n */\nexport type ZfbDevMiddlewareResponse = {\n status: number;\n headers?: Record<string, string>;\n body?: string;\n bodyEncoding?: \"utf8\" | \"base64\";\n};\n\n/**\n * Handler signature for a `devMiddleware` registration. The `next` callback\n * is reserved for future composition; v1 plugins should produce a response\n * directly. Returning `undefined` from the handler signals \"I did not handle\n * this request\" — the dev server then falls through to its built-in routes\n * (the page cache, /__zfb/livereload.js, etc.).\n */\nexport type ZfbDevMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `devMiddleware`. The `register` callback installs\n * one handler per URL path prefix. `path` is matched as an exact prefix\n * — a registration on `/doc-history` matches `/doc-history` and\n * `/doc-history/foo`, but NOT `/doc-historyx`.\n */\nexport type ZfbDevMiddlewareContext = {\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbDevMiddlewareHandler): void;\n};\n\n/**\n * Handler signature for a `previewMiddleware` registration (#1542).\n * Deliberately reuses [`ZfbDevMiddlewareRequest`] /\n * [`ZfbDevMiddlewareResponse`] verbatim — the wire shape crossing the\n * Rust↔JS boundary is genuinely the SAME for dev and preview (mirrors\n * the Rust side, which shares `DevRequest`/`DevResponse` between both\n * hooks too), so there is nothing preview-specific to say about the\n * request/response contract itself. `next` is likewise reserved for\n * future composition; returning `undefined` signals \"I did not handle\n * this request\" and the preview server falls through to its built-in\n * routes (static-file serving, or the wrangler-backed adapter in\n * adapter mode).\n */\nexport type ZfbPreviewMiddlewareHandler = (\n req: ZfbDevMiddlewareRequest,\n) => Promise<ZfbDevMiddlewareResponse | undefined> | ZfbDevMiddlewareResponse | undefined;\n\n/**\n * Context passed to `previewMiddleware` (#1542). Structurally identical\n * to [`ZfbDevMiddlewareContext`] today — one handler per URL path\n * prefix, matched the same way — but declared as its own named type\n * (unlike the request/response types above, which are reused verbatim)\n * because the *context* is where a hook-specific capability would land\n * first if one were ever added (e.g. something preview-only that\n * `devMiddleware` has no equivalent for). Keeping it a separate\n * declaration costs nothing today and avoids a breaking rename later.\n */\nexport type ZfbPreviewMiddlewareContext = {\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n config: import(\"./config.js\").ZfbConfig;\n options: Record<string, unknown>;\n logger: ZfbPluginLogger;\n /** Register an HTTP handler at `path`. Calling twice on the same path overwrites. */\n register(path: string, handler: ZfbPreviewMiddlewareHandler): void;\n};\n\n/**\n * Loader signature for a virtual-module registration. Must return the\n * **complete ESM module source text** as a string — the bundler /\n * embedded V8 host feeds the returned string in as the module's\n * source verbatim. The loader runs **eagerly**, not lazily on first\n * import: exactly once per `zfb build` run and once per `zfb dev`\n * host boot, during the setup phase right after every plugin's\n * `setup` hook has returned — even if the registered specifier is\n * never imported by any page or module. The resulting source is\n * memoised; every subsequent import of that specifier reuses it,\n * **unless a forced reload is requested** (#2167) — the plugin-host\n * protocol now supports bypassing the memo and re-invoking the loader,\n * intended for a loader whose registration also declares\n * [`watchFiles`](#watchFiles) and needs a fresh read after one of\n * those files changes on disk. `zfb dev` watches every declared\n * [`watchFiles`](#watchFiles) path and re-invokes the owning loader with\n * its memo bypassed when one of them changes (#2169, #2181); `zfb build`\n * invokes each loader exactly once and never re-invokes it. See the\n * Plugins concept page for the full refresh contract.\n * (Under `zfb preview`, `addVirtualModule` registrations are accepted\n * but inert — see [`ZfbSetupContext.command`](#command) — so the\n * loader never runs there.)\n *\n * Example:\n *\n * ```ts\n * addVirtualModule(\"virtual:my-data\", () =>\n * `export default ${JSON.stringify(myJson)}`,\n * );\n * ```\n */\nexport type ZfbVirtualModuleLoader = () => string | Promise<string>;\n\n/**\n * Optional third argument to `addVirtualModule` (#2167).\n */\nexport type ZfbVirtualModuleOptions = {\n /**\n * Extra absolute filesystem paths a `zfb dev` watcher should track on\n * this loader's behalf — useful when the loader's output depends on\n * files it reads directly (e.g. via `node:fs`) rather than static ESM\n * imports the dev bundler would otherwise notice on its own.\n *\n * Every entry **must be an absolute path**: this mirrors\n * `extraWatchPaths`'s absolute-only rule in `zfb.config.ts`, and for\n * the same reason — `watchFiles` entries are never resolved against\n * the project root, so a relative entry has no defined base directory\n * to resolve against. A relative (or otherwise malformed) entry throws\n * at `setup` time.\n */\n watchFiles?: string[];\n};\n\n/**\n * Context passed to the new `setup` hook (#255). Runs once per host\n * boot, in `Config.plugins` declaration order, **before** `preBuild`.\n *\n * `ctx.command` tells the plugin which lifecycle is active so it can\n * gate per-lifecycle registrations. A dev-only mock route stays gated\n * to `\"dev\"`; a package-owned page route is registered unconditionally\n * (it is prerendered during a build and dev-routed during dev):\n *\n * ```ts\n * setup({ command, injectRoute }) {\n * // package-owned page route (rendered in build and dev)\n * injectRoute(\"/preset-page\", \"./pages/preset-page.tsx\");\n * // dev-only mock endpoint\n * if (command === \"dev\") {\n * injectRoute(\"/api/dev/x\", \"./scripts/dev-x.ts\");\n * }\n * }\n * ```\n *\n * The hook's surface is intentionally **closed**: only `injectRoute`,\n * `addVirtualModule`, `addAlias`, and `addClientEntry`. There is no\n * `addRemarkPlugin` / `addRehypePlugin` / `addMarkdownVisitor` — by\n * design (see the concept doc for the rationale). `addVirtualModule`'s\n * optional `watchFiles` argument (#2167) is a registration OPTION on\n * that existing method, not a new closed-surface method — the closed\n * set of four stays exactly four.\n */\nexport type ZfbSetupContext = {\n /**\n * Active zfb command. `\"build\"` during `zfb build`; `\"dev\"` during\n * `zfb dev`; `\"preview\"` during `zfb preview` (#1542). It can guide\n * lifecycle-specific plugin behavior. `injectRoute` registrations are\n * accepted in both `\"dev\"` and `\"build\"`; user `pages/` routes retain\n * precedence over matching injected routes (see\n * [`injectRoute`](#injectRoute)).\n *\n * Under `\"preview\"`, `setup` still fires (Rust-side via the minimal\n * non-V8 `run_preview_setup` path) so plugin-side state\n * initialisation runs, but `zfb preview` serves an ALREADY-BUILT\n * `dist/` verbatim and never re-enters the scan → bundle → render\n * pipeline. Consequently `injectRoute` / `addVirtualModule` /\n * `addAlias` / `addClientEntry` calls made under `\"preview\"` are\n * accepted (for shape-consistency with `\"build\"`/`\"dev\"`) but are\n * **inert** — nothing downstream ever reads them. Only the hook's\n * side effects and a subsequent `previewMiddleware` registration do\n * anything meaningful under `\"preview\"`.\n */\n command: \"build\" | \"dev\" | \"preview\";\n /** Project root — the directory containing `zfb.config.ts`. */\n projectRoot: string;\n /**\n * Absolute directory for the plugin's opaque intermediates\n * (`<scratch root>/plugins`; `<projectRoot>/.zfb-build/plugins` by\n * default). It is not an importable route location: generated route\n * modules placed here are not staged into the bundle. zfb does not\n * create it, so `mkdir(scratchDir, { recursive: true })` and use a\n * `<scratchDir>/<plugin-name>/` subdirectory. Under `--scratch-dir`,\n * plugin isolation only holds if the plugin adopts this directory.\n */\n scratchDir: string;\n /** The full loaded `ZfbConfig` (data-only view). */\n config: import(\"./config.js\").ZfbConfig;\n /** Plugin-specific options block, copied verbatim from `PluginConfig.options`. */\n options: Record<string, unknown>;\n /** Logger that wraps the Rust-side `tracing` subscriber. */\n logger: ZfbPluginLogger;\n\n /**\n * Register an import alias. **Exact-match-only in v1**:\n * `addAlias(\"@/foo\", \"./src/foo.tsx\")` rewrites `import \"@/foo\"`\n * but does NOT match `import \"@/foo/bar\"`. Prefix-matching is\n * explicitly deferred to v2 — switch to one bare alias per file\n * until then.\n *\n * `to` is resolved relative to the project root. Two plugins\n * registering the same `from` with different `to` raises\n * `AliasConflict` and aborts the build.\n *\n * A `to` that resolves to a single existing file is also applied\n * to importers inside `node_modules`, via esbuild `--alias`. A\n * directory-shaped or missing `to` is applied through tsconfig\n * `paths` only, which esbuild does not honor for `node_modules`\n * importers — packages that need an alias to work from inside\n * `node_modules` should register one alias per file, or use\n * `addVirtualModule`. A `from` that is a slash-prefix of another\n * alias, of a virtual module specifier, or of a reserved name\n * such as `zfb` gets no `--alias` flag.\n */\n addAlias(from: string, to: string): void;\n\n /**\n * Register a virtual module. `specifier` is a bare import\n * specifier (recommended `virtual:` prefix, not enforced).\n * `loader` returns the complete ESM source text as a string and\n * runs **eagerly, once per build/dev-boot during setup** — not\n * lazily at first import (see [`ZfbVirtualModuleLoader`], including\n * its forced-reload amendment).\n *\n * The optional third argument's `watchFiles` (#2167) declares extra\n * absolute filesystem paths a `zfb dev` watcher should track on this\n * loader's behalf — see [`ZfbVirtualModuleOptions`]. Every entry must\n * be an absolute path; a relative entry throws.\n *\n * Two plugins registering the same `specifier` raises\n * `VirtualModuleConflict` and aborts the build.\n */\n addVirtualModule(\n specifier: string,\n loader: ZfbVirtualModuleLoader,\n options?: ZfbVirtualModuleOptions,\n ): void;\n\n /**\n * Register a synthetic / package-owned page route. `pattern` uses the\n * same grammar as `pages/` filenames (`/blog/[slug]`, `/api/dev/x`,\n * `/docs/[...rest]`).\n * Patterns below `/__paths__/` are reserved for zfb's internal `paths()`\n * endpoint and raise `ReservedRoutePrefix` with this plugin's name.\n *\n * - In **build** (package-owned routes), the route is materialised\n * into a per-build overlay pages root and **prerendered** through\n * the normal scan → bundle → render pipeline, so a preset can own a\n * route without the project shipping a `pages/` stub file. A `\"/\"`\n * package route becomes the project's root page when no user\n * `pages/index` exists, enabling a truly empty/absent user `pages/`.\n * A package route whose URL shape collides with a user `pages/` route\n * is dropped (user `pages/` wins). This is the supported, complete path.\n * - In **dev**, both static and dynamic injected routes are rendered\n * by `zfb dev`. Static routes (where the URL equals the pattern,\n * e.g. `/preset-about`) are seeded into the dev route universe at\n * boot; dynamic routes (e.g. `/preset-docs/[slug]`) are rendered\n * on first request via a request-time synthetic entry — params are\n * extracted from the URL by the Hono router inside the live bundle.\n * User `pages/` files take precedence over any injected route of\n * the same shape, including `pages/index` over an injected `\"/\"`.\n * Without a user index, an injected root is staged, seeded, and served\n * like any other static injected route. **HMR:** content the\n * route reads from watched collections live-refreshes normally.\n * Editing the package's **compiled entrypoint under `node_modules`**\n * is NOT watched and requires a `zfb dev` restart (restart-only\n * contract — a published package is not project source). **Per-route\n * data:** an injected route loads per-route data via a **dynamic\n * route's `paths()` export** (which returns `{ params, props }`);\n * `getStaticProps` on a package page is not forwarded by the overlay\n * (only `default` + the `prerender` hint are forwarded — same as\n * `zfb build`). A route that needs per-route data should be a\n * dynamic route whose `paths()` reads the data.\n *\n * `opts.prerender` controls the route's prerender shape during a\n * build: omit it (or `true`) for the SSG default; `false` marks an\n * SSR-shaped route, which `output: 'static'` rejects. It is build-only\n * metadata and ignored in dev.\n *\n * Two plugins registering the same `pattern` (or one plugin\n * re-registering it with a different entrypoint) raises\n * `InjectRouteConflict`.\n */\n injectRoute(pattern: string, entrypoint: string, opts?: { prerender?: boolean }): void;\n\n /**\n * Register a package-owned client-side side-effect entry (#1196).\n *\n * `entrypoint` **must** point to a `*.client.{ts,tsx,js,jsx}` file —\n * this is enforced (#1191 review [9]): a path missing the `.client.`\n * infix, or a bare `.client.ts` with an empty stem, throws an error\n * (`addClientEntry` JS-host validation + Rust `InvalidClientEntry`)\n * rather than being silently accepted under an invented name. The entry\n * name is derived from the filename stem minus `.client`\n * (e.g. `my-lib.client.ts` → `my-lib`), via the same canonical helper\n * as user-authored `*.client.*` discovery.\n *\n * The entry is bundled and shipped as\n * `/assets/client/<name>.js` (stable URL) / `/assets/client/<name>-<hash>.js`\n * (production, hashed). User-authored files win on name collision —\n * the registered entry is silently dropped when a user-authored file of\n * the same name exists in the discovery roots.\n *\n * Two plugins registering the same entry name with different entrypoints\n * raises `ClientEntryConflict` and aborts the build.\n *\n * `entrypoint` is resolved relative to the project root if given as a\n * relative path (same rule as `injectRoute`).\n */\n addClientEntry(entrypoint: string): void;\n};\n\n/**\n * The plugin-module shape. `name` is informational (the resolved module\n * specifier wins for identification on the Rust side) and helps the\n * plugin self-identify in logs.\n *\n * Five optional hooks; declaration-order matters when multiple plugins\n * touch the same surface. Each hook is independent — a plugin may\n * declare any subset:\n *\n * - `setup` (#255) — register virtual modules, aliases, injected\n * routes. Runs once at host boot, before `preBuild`. Also runs under\n * `zfb preview` (#1542) via the minimal non-V8 `run_preview_setup`\n * path — see [`ZfbSetupContext.command`](#command) for what is and\n * isn't meaningful there.\n * - `preBuild` — file-generation work that downstream stages will\n * see. Runs once per `zfb build` and once per `zfb dev` boot. Does\n * **NOT** fire under `zfb preview` (#1542) — preview serves an\n * already-built `dist/` and never re-triggers file generation.\n * - `postBuild` — finalisation work that runs after `dist/` has been\n * written. Does not fire under `zfb preview` either, for the same\n * reason as `preBuild`.\n * - `devMiddleware` — register HTTP handlers for ad-hoc dev-only\n * URLs. Per-request dispatch, distinct from `injectRoute` (which\n * goes through the page renderer). Fires only during `zfb dev`.\n * - `previewMiddleware` (#1542) — register HTTP handlers for ad-hoc\n * preview-only URLs. Same register-context shape as `devMiddleware`,\n * fires only during `zfb preview`. A plugin wanting coverage in both\n * modes registers the same handler under both hooks — `zfb` does\n * NOT reuse a `devMiddleware` registration for preview automatically\n * (explicit per-mode opt-in, by design).\n */\nexport type ZfbPlugin = {\n /** Plugin display name; surfaces in error / log lines. */\n name: string;\n setup?(ctx: ZfbSetupContext): Promise<void> | void;\n preBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n postBuild?(ctx: ZfbBuildHookContext): Promise<void> | void;\n devMiddleware?(ctx: ZfbDevMiddlewareContext): Promise<void> | void;\n previewMiddleware?(ctx: ZfbPreviewMiddlewareContext): Promise<void> | void;\n};\n\n/**\n * Identity helper that types the supplied object as a [`ZfbPlugin`].\n * Use as the default export of a plugin module so editors surface\n * field-level types and typos surface at compile time.\n *\n * ```ts\n * import { definePlugin } from \"@takazudo/zfb/plugins\";\n *\n * export default definePlugin({\n * name: \"my-plugin\",\n * async preBuild({ outDir, logger }) {\n * logger.info(`generating index into ${outDir}`);\n * },\n * });\n * ```\n */\nexport function definePlugin(plugin: ZfbPlugin): ZfbPlugin {\n return plugin;\n}\n"]}