@takazudo/zfb 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +6 -4
- package/dist/config.d.ts +8 -5
- package/dist/config.js.map +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Behavior changes
|
|
8
|
+
|
|
9
|
+
**Transcluded files now honour `markdown.gfm`** (#2390):
|
|
10
|
+
|
|
11
|
+
Files pulled in by `:::include{file="./snippet.md"}` were parsed with **every GFM construct off**, no matter what `markdown.gfm` said — no tables, strikethrough, task lists, footnotes, or autolink literals.
|
|
12
|
+
|
|
13
|
+
The symptom was that identical markdown rendered differently depending on where it was written: a pipe table in the page became a table, the byte-identical table in an included file stayed literal text. Since 2.5.0 turned `autolinkLiteral` on by default, bare URLs in included files also silently stopped autolinking.
|
|
14
|
+
|
|
15
|
+
Included files now inherit the project's resolved `markdown.gfm` configuration and render the same as the equivalent content written inline.
|
|
16
|
+
|
|
17
|
+
**This changes rendered output for existing sites.** If a project transcludes GFM-flavoured content and was (knowingly or not) relying on it staying literal, that content now renders as GFM. Set the relevant `markdown.gfm.*` flags to `false` to keep the old output. A project that never enabled GFM is unaffected.
|
|
18
|
+
|
|
19
|
+
The same hardcoded construct set was fixed at a second parse site — the body of a directive written **without** blank lines, and the page prose sitting between two such runs (`DirectiveRegistry::reparse_block`). In practice that one is **not** expected to change rendered output: the re-parse is only reached for content the main parse left as a single plain text run, and the main parse now shares the same constructs. It is fixed to keep the two parse sites in lockstep rather than to change behaviour.
|
|
20
|
+
|
|
21
|
+
The divergence #2390 left open — math constructs staying off at both secondary parse sites — is closed by #2397 below.
|
|
22
|
+
|
|
23
|
+
**Math in transcluded files now matches the surrounding page** (#2397):
|
|
24
|
+
|
|
25
|
+
This finishes what #2390 started: content reached through a secondary parse site now renders the same as the equivalent content written inline, on **each** path separately.
|
|
26
|
+
|
|
27
|
+
zfb parses markdown with a different construct set per path. The HTML serializer keeps math off, so `$$…$$` is literal text; the MDX/JSX path turns it on, because the emitter has dedicated arms for math nodes. Both secondary parse sites — transclusion, and the re-parse of a directive body written without blank lines — hardcoded the HTML set, so they diverged from the top level whenever a page was compiled to JSX.
|
|
28
|
+
|
|
29
|
+
For MDX/JSX pages that was not a rendering nuance. A single `$$…$$` in an included file leaked LaTeX as bare `{…}` expression containers, esbuild rejected the module, and the bundler's defensive skip degraded the **entire page** to `<pre data-zfb-content-fallback>`. Math in included files is now safe.
|
|
30
|
+
|
|
31
|
+
**HTML-path output is unchanged.** Math stays off there, exactly as before — turning it on would change rendered output for every existing project and would pull in markdown-rs's single-dollar behaviour, where a literal `$` in prose becomes math. The asymmetry being removed is between transcluded and inline content, not between the two paths.
|
|
32
|
+
|
|
33
|
+
The directive re-parse site was brought into lockstep the same way. As with #2390's GFM change, it is **not** expected to alter rendered output: that re-parse is only reached for content the main parse left as a single plain text run, and math rich enough to render differently is tokenised by the main parse first.
|
|
34
|
+
|
|
35
|
+
**CJK-friendly emphasis and hard breaks now apply inside transcluded files and directive bodies** (#2398):
|
|
36
|
+
|
|
37
|
+
`CjkFriendlyPlugin` and `HardBreaksPlugin` are visitors in the pipeline's own mdast chain, so they never saw a subtree parsed later by `TranscludePlugin` or `DirectiveRegistry::reparse_block` — the same class of gap #2390 and #2397 fixed for GFM and math constructs. On a CJK site with `markdown.cjkFriendly` on (the default), CJK emphasis flanking was corrected in the page body but not inside `:::include`d snippets or collapsed directive bodies; with `markdown.hardBreaks` on, soft line breaks became `<br>` in the page but not in that same content.
|
|
38
|
+
|
|
39
|
+
Both passes now run at both secondary parse sites, gated on the project's own `cjkFriendly` / `hardBreaks` settings rather than on any GFM construct. **This changes rendered output for existing sites** that transclude CJK-flanked emphasis markup, or newline-sensitive prose, and were relying on the secondary-parse gap to leave it untouched — set `cjkFriendly: false` / `hardBreaks: false` to keep the old output; a project that never enabled either is unaffected.
|
|
40
|
+
|
|
41
|
+
**One deliberate asymmetry:** `HardBreaksPlugin` is gated by where the re-parsed content *lands*, not by which site produced it. Content that ends up among the children of an MDX JSX element — a collapsed directive body, prose between nested directive runs, or an `:::include` written inside a literal `<Note>…</Note>` — gets hard breaks on the JSX-emit path only. On the HTML path such an element renders through a lossy catch-all that stringifies `Break` nodes to an **empty string**, so applying the plugin there would silently delete the author's newlines instead of turning them into `<br>` — a regression, not parity. HTML-path output for that content is unchanged. Top-level transcluded content is unaffected by the gate and gets `<br>` on both paths. `CjkFriendlyPlugin` has no such asymmetry and applies on both paths at both sites.
|
|
42
|
+
|
|
7
43
|
### Build compatibility
|
|
8
44
|
|
|
9
45
|
**Explicit workspace-root alias claims** (#1883):
|
package/README.md
CHANGED
|
@@ -219,7 +219,7 @@ constructs the MDX parser recognises. The field accepts three shapes:
|
|
|
219
219
|
|
|
220
220
|
2. **Partial object** — toggle individual constructs. Fields you omit
|
|
221
221
|
fall back to the conservative default (`strikethrough: true`,
|
|
222
|
-
`table: true`, everything else off).
|
|
222
|
+
`table: true`, `autolinkLiteral: true`, everything else off).
|
|
223
223
|
|
|
224
224
|
```ts
|
|
225
225
|
// zfb.config.ts
|
|
@@ -239,12 +239,14 @@ constructs the MDX parser recognises. The field accepts three shapes:
|
|
|
239
239
|
```
|
|
240
240
|
|
|
241
241
|
3. **Omitted entirely** — the parser uses the conservative default.
|
|
242
|
-
`~~text~~` parses as `<del>text</del
|
|
243
|
-
`<table
|
|
242
|
+
`~~text~~` parses as `<del>text</del>`, pipe tables render as
|
|
243
|
+
`<table>`, and bare URLs like `https://example.com` become links;
|
|
244
|
+
task lists and footnote definitions stay off.
|
|
244
245
|
|
|
245
246
|
```ts
|
|
246
247
|
export default defineConfig({
|
|
247
|
-
// no `markdown` field — strikethrough + table
|
|
248
|
+
// no `markdown` field — strikethrough + table + autolink literals on,
|
|
249
|
+
// task lists + footnotes off
|
|
248
250
|
});
|
|
249
251
|
```
|
|
250
252
|
|
package/dist/config.d.ts
CHANGED
|
@@ -783,10 +783,12 @@ export type MarkdownConfig = {
|
|
|
783
783
|
* below.
|
|
784
784
|
*
|
|
785
785
|
* When `markdown` itself is omitted entirely, the conservative
|
|
786
|
-
* default applies: `strikethrough: true`, `table: true`,
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
786
|
+
* default applies: `strikethrough: true`, `table: true`,
|
|
787
|
+
* `autolinkLiteral: true`, task lists and footnotes off. Those three
|
|
788
|
+
* are the constructs GFM-accustomed authors expect without config;
|
|
789
|
+
* task lists and footnotes change document structure, so they stay
|
|
790
|
+
* opt-in. Projects that want the full GFM surface should opt in with
|
|
791
|
+
* `gfm: true`.
|
|
790
792
|
*/
|
|
791
793
|
gfm?: GfmFlag;
|
|
792
794
|
/**
|
|
@@ -1158,7 +1160,8 @@ export type GfmFlag = boolean | GfmConstructs;
|
|
|
1158
1160
|
/**
|
|
1159
1161
|
* Per-construct opt-in / opt-out for GFM. Every field is optional;
|
|
1160
1162
|
* omitted fields fall back to the conservative default
|
|
1161
|
-
* (`strikethrough: true`, `table: true`,
|
|
1163
|
+
* (`strikethrough: true`, `table: true`, `autolinkLiteral: true`,
|
|
1164
|
+
* others `false`).
|
|
1162
1165
|
*
|
|
1163
1166
|
* Mirrors `GfmConstructs` in crates/zfb/src/config.rs.
|
|
1164
1167
|
*/
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,kEAAkE;AAClE,qEAAqE;AACrE,uEAAuE;AACvE,oEAAoE;AACpE,+BAA+B;AAC/B,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AAgyCvE;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAC1B,aAAqB,EACrB,MAA0B;IAE1B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO;QACL,GAAG,MAAM;QACT,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACrC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5E,wEAAwE;gBACxE,mEAAmE;gBACnE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CAAC;YACtD,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["// `zfb/config` — TypeScript helper for the `zfb.config.ts` form.\n//\n// The zfb config loader (`crates/zfb/src/config.rs`) accepts both\n// `zfb.config.ts` and `zfb.config.json`; TS wins when both files are\n// present. JSON remains accepted for projects predating the TS loader,\n// while new projects should prefer the TS form for editor types and\n// `defineConfig` autocomplete.\n//\n// At parse time, zfb bundles the user's `zfb.config.ts` with esbuild and\n// aliases this `zfb/config` import to an internal stub that re-exports\n// `defineConfig` as the identity function — so a user project does not\n// need the `zfb` npm package installed locally just to be parsed.\n//\n// The shape mirrors the Rust `Config` struct one-for-one. Keep them in\n// sync; the `defineConfig` identity helper is the single anchor point.\n\nexport type Framework = \"preact\" | \"react\";\n\nexport type CollectionDef = {\n /** Identifier used at the call site (e.g. `\"blog\"`). */\n name: string;\n /** Directory (relative to the project root) holding the entries. */\n path: string;\n /** Optional schema. Enforced by `zfb check`. */\n schema?: Record<string, unknown>;\n /**\n * Optional include globs (Astro-style, evaluated relative to `path`).\n * When set and non-empty, an entry is kept only if at least one\n * pattern matches its relative path. When omitted or empty, no\n * include-filtering happens. Patterns use the `globset` dialect\n * (Unix-style: `*`, `**`, `?`, `[…]`).\n */\n include?: string[];\n /**\n * Optional exclude globs. When set, an entry is dropped if any\n * pattern matches its relative path. Evaluated AFTER `include`.\n * Together they mirror Astro's `['**\\/*.mdx', '!**\\/*.en.mdx']`\n * convention (zfb splits the negative side into its own field).\n */\n exclude?: string[];\n /**\n * Optional suffix to strip from each kept entry's slug + module\n * specifier. Use with multi-locale layouts where one source\n * directory holds both `foo.mdx` (default locale) and `foo.en.mdx`\n * (locale override) — set `idStripSuffix: \".en\"` so the EN\n * collection's slugs round-trip as `foo` instead of `foo.en`.\n */\n idStripSuffix?: string;\n /**\n * Opt-in to a `path` that escapes the project root via `..` (e.g. a\n * monorepo-shared content dir living outside this package). Default\n * `false` keeps the standard project-root guard. Absolute paths and\n * Windows drive-relative/prefix forms are rejected regardless of\n * this flag — only `..`-relative escapes are relaxed.\n *\n * Security note: if this collection comes from a preset, the preset\n * author — not the consuming project — controls `path`. Setting\n * `allowOutsideRoot: true` on a preset-provided collection widens\n * the project's read surface to wherever that preset points, so\n * treat it the same as any other preset-granted filesystem access.\n */\n allowOutsideRoot?: boolean;\n};\n\nexport type TailwindConfig = {\n /** Whether Tailwind is enabled. Default: `true`. */\n enabled?: boolean;\n};\n\n/**\n * Prefetch options. Mirrors `PrefetchConfig` in `crates/zfb/src/config.rs`.\n */\nexport type PrefetchConfig = {\n /**\n * Disable prefetch entirely.\n *\n * When `true`, the bundler emits `globalThis.__zfb.prefetchDisabled = true`\n * in `entry.mjs`, and `<ClientRouter />` renders\n * `<meta name=\"zfb-prefetch-disabled\" content=\"true\">` in `<head>`.\n * The sibling prefetch-core module reads that meta tag at `init()` time\n * and short-circuits — no prefetch wiring runs.\n *\n * The flag is site-wide and static — set once at bundle-emit time,\n * never recomputed per-page. Default: `false`.\n */\n disabled?: boolean;\n};\n\n/**\n * Bundler options. Mirrors `BundleConfig` in `crates/zfb/src/config.rs`.\n */\nexport type BundleConfig = {\n /**\n * Project-relative glob patterns (gitignore-style) for source files\n * the bundler must NOT pull into the esbuild graph.\n *\n * Why this exists: an eager `import.meta.glob('components/**\\/*.stories.tsx',\n * { eager: true })` expands to a static import of every matched file. If a\n * matched file imports a CJS-only package whose `package.json` resolves only\n * via `main`/`module` or a `require`-only `exports` condition (e.g.\n * `msw` → `path-to-regexp@6`), esbuild — invoked with `--platform=neutral`\n * for the worker bundle — rejects it with \"Could not resolve … Main fields\n * must be configured explicitly when using the neutral platform.\" Listing the\n * offending file here keeps the migration build green.\n *\n * Each pattern is matched against the file's path RELATIVE TO THE PROJECT\n * ROOT, in POSIX form (e.g. `components/Foo.stories.tsx` or\n * `components/**\\/*.stories.tsx`). A matched file is:\n *\n * - never copied/symlinked into the bundler's shadow tree, and\n * - dropped from any eager `import.meta.glob(...)` expansion that would\n * otherwise statically import it.\n *\n * Unset / empty → behaviour is byte-identical to a build without this knob:\n * no files are skipped.\n *\n * Mirrors `Config::bundle` in crates/zfb/src/config.rs.\n */\n exclude?: string[];\n\n /**\n * Explicit esbuild `main-fields` list for the `--platform=neutral` page/SSR\n * pass. Under `neutral` esbuild's main-fields list is EMPTY by default, so a\n * dep resolved purely via `package.json` `main`/`module` (no `exports` map)\n * is rejected (\"The \"main\" field here was ignored. Main fields must be\n * configured explicitly when using the neutral platform.\"). Set e.g.\n * `[\"main\", \"module\"]` to let such CJS-main-only deps resolve (#676 —\n * `msw` → `path-to-regexp@6`). Applies to every framework; unset/empty →\n * byte-identical to a build without the knob (the React-only `main,module`\n * shim still applies).\n *\n * Mirrors `BundleConfig::main_fields` in `crates/zfb/src/config.rs`.\n */\n mainFields?: string[];\n\n /**\n * Bare specifiers to mark external in the `--platform=neutral` page/SSR\n * pass, so esbuild leaves them unbundled instead of resolving them (the\n * other #676 escape hatch — externalize a CJS-only dep rather than\n * resolving it). Appended to the framework-provided externals. Unset/empty\n * → no extra externals.\n *\n * Mirrors `BundleConfig::external` in `crates/zfb/src/config.rs`.\n */\n external?: string[];\n\n /**\n * Additional esbuild loaders keyed by file extension (for example\n * `{ \".txt\": \"text\" }`). Only inline loaders are supported: `file` and\n * `copy` are intentionally excluded because they emit sibling assets the\n * client bundlers do not publish. `.css`, `.module.css`, `.mdx`, and `.md`\n * are reserved by zfb and rejected during config validation.\n */\n loaders?: Record<string, \"text\" | \"json\" | \"base64\" | \"dataurl\" | \"binary\" | \"empty\">;\n\n /**\n * Operator-authored esbuild define substitutions. Values are raw esbuild\n * expressions; string values must be pre-quoted JSON (for example\n * `{ __APP_NAME__: '\"my-app\"' }`). The mode-owned keys\n * `import.meta.env.PROD`, `import.meta.env.DEV`, and\n * `process.env.NODE_ENV` are reserved and rejected at config-load time.\n */\n define?: Record<string, string>;\n};\n\n/**\n * One plugin entry in `zfb.config.ts`.\n *\n * `name` MUST be a module reference that Node's resolver can locate from\n * the project root. The zfb config loader\n * (`crates/zfb-config-loader/js/config-loader.mjs`) resolves it to an\n * absolute module specifier and the build / dev plugin host loads it via\n * dynamic `import()`:\n *\n * - `\"./plugins/my-plugin.mjs\"` / `\"../shared/plugin.mjs\"` —\n * path-relative to the project root (the dir containing `zfb.config.ts`).\n * - `\"/abs/path/to/plugin.mjs\"` — absolute filesystem path.\n * - `\"@takazudo/zfb-plugin-search\"` / `\"my-plugin\"` — npm bare specifier\n * resolved against the project's `node_modules`.\n *\n * Inline-function hooks are NOT supported; the plugin module's default\n * export must be a [`ZfbPlugin`] (see `@takazudo/zfb/plugins`).\n *\n * `options` is passed verbatim to the plugin's hook contexts; treat\n * the schema as plugin-specific.\n */\nexport type PluginConfig = {\n name: string;\n options?: Record<string, unknown>;\n};\n\nexport type ZfbConfig = {\n /** Output directory for built assets. Default: `dist`. */\n outDir?: string;\n /** Public/static directory copied verbatim. Default: `public`. */\n publicDir?: string;\n /** Optional dev/preview server bind host. */\n host?: string;\n /** Optional dev/preview server port. */\n port?: number;\n /**\n * Host header values the dev/preview server accepts when bound to a\n * non-localhost interface (`--host 0.0.0.0`, the bare `--host` LAN\n * shortcut, or `host` above) — the DNS-rebinding guard, mirroring\n * Vite's `server.allowedHosts`.\n *\n * Defaults: only consulted for non-loopback binds — the default\n * `localhost` bind skips validation entirely. `localhost`, the\n * explicitly bound host, and any IP-literal Host — `127.0.0.1`,\n * `[::1]`, the LAN URLs the startup banner prints — are always\n * allowed (DNS rebinding needs a DNS name, so raw IPs are safe;\n * Vite parity); requests with any other Host get a 403.\n *\n * Matching rules (the request Host's port is stripped first and\n * comparison is case-insensitive):\n *\n * - `\"example.com\"` — matches exactly that host.\n * - `\".example.com\"` (leading dot) — matches `example.com` and every\n * subdomain (`api.example.com`).\n * - IPv6 entries may be written with or without brackets\n * (`\"[::1]\"` / `\"::1\"`).\n *\n * Mirrors `Config::allowed_hosts` in `crates/zfb/src/config.rs`.\n */\n allowedHosts?: string[];\n /** JSX framework runtime. Default: `preact`. */\n framework?: Framework;\n /** Content collections. Mirrors the JSON form one-for-one. */\n collections?: CollectionDef[];\n /** Tailwind options; absent = defaults. */\n tailwind?: TailwindConfig;\n /**\n * Prefetch options. When `disabled: true`, the build emits a meta tag\n * that the runtime's prefetch-core module reads at init time to skip\n * all prefetch wiring. Mirrors `Config::prefetch` in\n * `crates/zfb/src/config.rs`.\n */\n prefetch?: PrefetchConfig;\n /**\n * Minify production HTML output from `zfb build`. Default: `false`.\n *\n * The implementation is Rust-only and does not spawn a Node.js minifier\n * subprocess. The first version is intentionally conservative: rendered\n * `.html` pages are candidates, source `.html` passthrough pages remain\n * verbatim, and non-HTML outputs are skipped.\n *\n * Mirrors `Config::minify_html` in `crates/zfb/src/config.rs`.\n */\n minifyHtml?: boolean;\n /**\n * Raise broken-link diagnostics to errors during `zfb build`, failing the\n * build (exit non-zero) instead of merely warning. Default: `false`.\n *\n * This is the effective boolean the CLI's `--strict-broken` /\n * `--no-strict-broken` tri-state resolves against. Precedence: explicit\n * CLI flag > this config field > default `false`.\n *\n * Force-enable semantics: if `markdown.features.linkValidation` is absent\n * entirely, enabling this force-enables link validation with its\n * defaults — a strict flag that silently did nothing on a bare project\n * would be a footgun.\n *\n * Scope: the `linkValidation` mechanism only. The separate\n * `resolveMarkdownLinks.onBrokenLinks` mechanism keeps its own knob and is\n * not affected by this field.\n *\n * Build-only: it does not affect `zfb dev`.\n *\n * Mirrors `Config::strict_broken_links` in `crates/zfb/src/config.rs`.\n */\n strictBrokenLinks?: boolean;\n /**\n * Fail `zfb build` (exit non-zero) when a content-collection `.md`/`.mdx`\n * entry falls back to `<pre data-zfb-content-fallback>` because its\n * compiled JSX does not parse. Default: `false`.\n *\n * This is the effective boolean the CLI's `--strict-content-bridge` /\n * `--no-strict-content-bridge` tri-state resolves against. Precedence:\n * explicit CLI flag > this config field > default `false`.\n *\n * Unlike `strictBrokenLinks`, there is no adjacent feature to\n * force-enable: the content-bridge gate always runs for every compiled\n * collection entry.\n *\n * Build-only: it does not affect `zfb dev` — dev keeps warning and\n * serving the fallback shape.\n *\n * Mirrors `Config::strict_content_bridge` in `crates/zfb/src/config.rs`.\n */\n strictContentBridge?: boolean;\n /**\n * Bundler options. `bundle.exclude` lists project-relative globs of\n * source files to keep out of the esbuild graph (e.g.\n * `[\"components/*.stories.tsx\"]`) — see {@link BundleConfig.exclude} for\n * why this is needed. Unset → byte-identical to a build without the knob.\n * Mirrors `Config::bundle` in `crates/zfb/src/config.rs`.\n */\n bundle?: BundleConfig;\n /** User-supplied plugins. */\n plugins?: PluginConfig[];\n /**\n * Deploy-target adapter package name. Omit (or `\"none\"`) for a pure\n * static build — any route exporting `prerender = false` is then a\n * hard build error. A package name like\n * `\"@takazudo/zfb-adapter-cloudflare\"` selects the matching adapter,\n * and `zfb build` invokes that package's bin to wrap the SSR bundle\n * into a deploy-ready entry (e.g. `dist/_worker.js` for Cloudflare\n * Workers Static Assets, Pages-compatible).\n *\n * Mirrors `Config::adapter` in crates/zfb/src/config.rs.\n */\n adapter?: string;\n /**\n * Strip `.md` / `.mdx` from internal `<a href>` paths during MDX\n * compilation, and append a trailing `/` so the resulting URL shape\n * converges with the rest of the site (mirrors the JS engine's\n * `rehypeStripMdExtension`). Default: `false`.\n *\n * Enable this when content authors hand-write `[label](other.md)`\n * style references that should resolve to the rendered route URL\n * (e.g. `other/`) instead of a literal file path. Built dist and\n * `pnpm dev` honour the same flag, so previews match shipped output.\n *\n * Mirrors `Config::strip_md_ext` in crates/zfb/src/config.rs.\n */\n stripMdExt?: boolean;\n\n /**\n * Public URL prefix mounted in front of every absolute HTML asset\n * URL the build emits — `<link rel=\"stylesheet\">`, `<script type=\"module\">`,\n * and any other `/assets/...`-prefixed reference rewritten by the\n * production asset pipeline.\n *\n * Use this when the site is deployed under a sub-path (e.g.\n * `https://example.com/pj/zudo-doc/`) instead of the domain root.\n * With `base: \"/pj/zudo-doc/\"` the dist HTML emits\n * `<link rel=\"stylesheet\" href=\"/pj/zudo-doc/assets/styles-<hash>.css\">`\n * instead of the unprefixed `/assets/styles-<hash>.css`.\n *\n * Accepted shapes (all normalised to a single canonical form\n * internally):\n *\n * - omitted / `undefined` / `\"\"` / `\"/\"` — no prefix; behaviour is\n * byte-identical to the pre-`base` build (root-mounted site).\n * - leading-and-trailing-slash path like `\"/pj/zudo-doc/\"` — prefix\n * that path onto every asset URL.\n * - absolute URL like `\"https://cdn.example.com/\"` — emit absolute\n * URLs (CDN-hosted assets).\n *\n * Inputs missing a leading or trailing `/` are normalised at config-\n * load time (paths) or asset-emit time (URL prefixes); callers do\n * not have to pre-trim.\n *\n * Mirrors `Config::base` in crates/zfb/src/config.rs.\n */\n base?: string;\n\n /**\n * Canonical origin URL for the site (e.g. `\"https://example.com\"`).\n *\n * When set, the bundler emits `globalThis.__zfb.site = <value>` in\n * `entry.mjs` so layouts can build canonical `<link>` tags,\n * OpenGraph `og:url` meta, sitemap absolute hrefs, and hreflang\n * `<link rel=\"alternate\">` from a single config-level source of truth.\n *\n * **Distinct from `base`**: `base` is a sub-path mount prefix used\n * for asset URLs (e.g. `\"/pj/my-site/\"`). `site` is the full\n * canonical origin (scheme + host, no path) used to construct\n * absolute page URLs for SEO/social metadata. Both may be set\n * simultaneously.\n *\n * Accepted shape: an absolute HTTP or HTTPS URL. Relative URLs,\n * non-HTTP(S) schemes, and empty strings are rejected at config-load\n * time. Trailing slash normalisation is the consumer's responsibility.\n *\n * When absent, `globalThis.__zfb.site` is not emitted — the build\n * output is byte-for-byte identical to builds without this field.\n *\n * Mirrors `Config::site` in crates/zfb/src/config.rs.\n */\n site?: string;\n\n /**\n * Markdown link resolver (port of `remarkResolveMarkdownLinks`).\n *\n * When `enabled: true`, the build appends `ResolveLinksPlugin` to the\n * mdast pipeline so author-written `[label](./other.mdx)` links are\n * rewritten to the corresponding rendered route URL — bypassing the\n * file→directory transformation that breaks relative paths in dist\n * HTML when `foo.mdx` becomes `foo/index.html`. Extensionless\n * (`./other`) and directory-style (`other/`) targets resolve too,\n * probing `{name}.mdx`, `{name}.md`, `{name}/index.mdx`,\n * `{name}/index.md` in that order. Relative targets resolve from the\n * source file's directory; for a directory-style link written from a\n * non-index page against its rendered URL — which sits one directory\n * deeper, e.g. `../sibling/` from `section/article.mdx` — a URL-space\n * fallback retries the probe from the page's route directory when\n * every file-space candidate misses.\n *\n * Two ways to specify the source dirs:\n *\n * - **Single dir (legacy):** set `docsDir` and the build assumes the\n * `/docs/` route prefix. Convenient for single-locale projects.\n * - **Multi dir (`dirs` non-empty):** explicit `{ dir, routePrefix }`\n * entries — required for any project with locale mirrors (e.g.\n * `docs/` AND `docs-ja/`) so each dir maps to its own route prefix\n * (`/docs/` vs `/ja/docs/`). When `dirs` is non-empty, `docsDir`\n * is ignored.\n *\n * Mirrors `Config::resolve_markdown_links` in crates/zfb/src/config.rs.\n */\n resolveMarkdownLinks?: ResolveMarkdownLinksConfig;\n\n /**\n * Whether the basePath rewriter should append a trailing `/` to\n * extensionless absolute hrefs (`<a href=\"/docs/foo\">` becomes\n * `<a href=\"/pj/zudo-doc/docs/foo/\">` when `base = \"/pj/zudo-doc/\"`\n * and this is `true`).\n *\n * Off by default — preserves byte-for-byte parity with the\n * pre-`trailingSlash` build for projects that haven't opted in.\n * Enable when the deploy target serves canonical URLs with trailing\n * slashes (Cloudflare Pages with `trailingSlash: always`, Netlify\n * pretty URLs, etc.) so the dist HTML doesn't ship non-canonical\n * hrefs that 301-redirect on every click.\n *\n * Only the trailing slash for extensionless hrefs is affected.\n * Hrefs that already end in `/`, that have a file extension\n * (`.png`, `.pdf`, …), or that opt out via `data-no-base` pass\n * through unchanged.\n *\n * Mirrors `Config::trailing_slash` in crates/zfb/src/config.rs.\n */\n trailingSlash?: boolean;\n\n /**\n * Markdown / MDX parsing options. Currently the only knob exposed is\n * [`gfm`](MarkdownConfig.gfm), which toggles GFM constructs\n * (strikethrough, table, autolink-literal, task-list-item,\n * footnote-definition) on or off.\n *\n * Mirrors `Config::markdown` in crates/zfb/src/config.rs.\n */\n markdown?: MarkdownConfig;\n\n /**\n * Extra absolute filesystem paths watched by the dev server in\n * addition to the project-root tree.\n *\n * Use this when project content reads from outside the project root\n * (a sibling knowledge-base repo, a shared filesystem directory, a\n * `file:` dep that ships content alongside code, etc.) and you want\n * `zfb dev` to live-reload when those external files change.\n *\n * Semantics:\n *\n * - Each entry MUST be an absolute path. Relative paths are\n * rejected at config-load time with a clear error message.\n * - Paths are canonicalised when the watcher boots; events match\n * the canonical form.\n * - A path that does NOT exist at boot is skipped with a warning;\n * the watcher does NOT re-watch the path if it appears later.\n * Restart `zfb dev` after creating the path.\n * - Each entry is watched recursively.\n * - Events from outside the project root bypass fine-grained graph\n * classification and may trigger a broader rebuild than equivalent\n * in-tree edits.\n *\n * **Security note:** opt-in only — do NOT point this at unbounded\n * directories like `$HOME` or `/`. On Linux the recursive watcher\n * registers every subdirectory and can hit the inotify\n * `max_user_watches` ceiling on large trees.\n *\n * Mirrors `Config::extra_watch_paths` in crates/zfb/src/config.rs.\n */\n extraWatchPaths?: string[];\n\n /**\n * Whether `zfb build` writes the post-build route manifest to disk\n * at `<outDir>/__zfb/routes.json` (#347).\n *\n * The on-disk file mirrors the in-memory `ctx.routes` shape that the\n * plugin API hands to `postBuild` hooks — same fields, same\n * url-sorted order — so any consumer script wired into `pnpm build`\n * can read the manifest without writing a zfb plugin. The plugin\n * `ctx.routes` and the on-disk `routes.json` are two access shapes\n * over the same data, not two contracts.\n *\n * Default: emit (`undefined` is treated as `true`). Set `false` to\n * skip the write — useful for projects that strip everything but\n * shipped assets out of `dist/` before deploy.\n *\n * Mirrors `Config::emit_routes_manifest` in crates/zfb/src/config.rs.\n */\n emitRoutesManifest?: boolean;\n\n /**\n * Syntect code-highlight options; absent = default theme\n * (`base16-ocean.dark`) and inline color mode. See\n * {@link CodeHighlightConfig} for accepted theme names, custom-theme\n * loading, and the class-emission mode (Highlight Tokens epic).\n *\n * Mirrors `Config::code_highlight` in crates/zfb/src/config.rs.\n */\n codeHighlight?: CodeHighlightConfig;\n\n /**\n * Maximum seconds a single plugin lifecycle hook (preBuild, postBuild,\n * setup, etc.) may run before the build fails with a diagnostic error\n * and the plugin host is force-killed.\n *\n * Absent falls through to the `ZFB_PLUGIN_HOOK_TIMEOUT` env var, then\n * the 120s built-in default. Set this when your plugins do long but\n * bounded work (e.g. large sitemap generation) and you want a tighter\n * or more explicit budget.\n *\n * Mirrors `Config::plugin_hook_timeout_secs` in crates/zfb/src/config.rs.\n */\n pluginHookTimeoutSecs?: number;\n\n /**\n * Whether `copy_public_dir` copies `public/` under the `base`\n * sub-path segment (`true`, default) or flat to the `dist/` root\n * (`false`).\n *\n * - **`true` (default):** files land at\n * `<outDir>/<base-segment>/<rel>`, matching the base-prefixed URLs\n * that `withBase()` emits in the rendered HTML. Use this for\n * projects served directly at their configured sub-path.\n * - **`false`:** files land flat at `<outDir>/<rel>` regardless of\n * `base`. Use this when the deploy pipeline relocates the entire\n * `dist/` tree into the base segment itself (e.g.\n * `cp -a dist/. deploy-root/pj/site/`), so putting the files under\n * `<outDir>/<base>/...` would result in a double-nested path.\n *\n * **Note on `zfb preview`:** with `false`, base-prefixed public-asset\n * URLs 404 under `zfb preview` because the flat copy lives at the\n * dist root and `zfb preview` does not simulate deploy-side\n * relocation. This is a known trade-off of the flat-copy deploy\n * scheme.\n *\n * Mirrors `Config::copy_public_with_base` in crates/zfb/src/config.rs.\n */\n copyPublicWithBase?: boolean;\n\n /**\n * Opt into `notify`'s poll-based watch backend for the dev server's\n * watchers instead of the OS-native backend (FSEvents on macOS,\n * inotify on Linux, ...).\n *\n * Use this as a fallback when the native backend is unavailable or\n * unreliable on the host (network-mounted project directories, some\n * CI/sandboxed containers) — the poll backend re-scans the watched\n * roots on an interval instead of relying on OS filesystem-change\n * notifications.\n *\n * Default: `false` (native backend). See\n * {@link watchPollIntervalMs} for the re-scan cadence.\n *\n * Mirrors `Config::watch_poll_fallback` in crates/zfb/src/config.rs.\n */\n watchPollFallback?: boolean;\n\n /**\n * Re-scan interval, in milliseconds, for the poll watch backend. Only\n * takes effect when {@link watchPollFallback} is `true`.\n *\n * Validated at config-load time: must be between `50` and `10000`\n * (inclusive) — values outside that range are rejected (too low\n * busy-loops the poll thread; too high makes hot-reload feel broken).\n * A value below `100` is accepted but logs a warning (elevated\n * re-scan CPU cost on large trees). Setting this WITHOUT\n * `watchPollFallback: true` is accepted and dormant, with a logged\n * warning rather than an error — a preset may pre-stage the interval\n * ahead of a project enabling the fallback itself.\n *\n * Absent falls through to the built-in 500ms default, applied by the\n * consuming command.\n *\n * Mirrors `Config::watch_poll_interval_ms` in crates/zfb/src/config.rs.\n */\n watchPollIntervalMs?: number;\n\n /**\n * Project output mode. Drives the V8-mode decision the build engine\n * makes right after the no-SSR-without-adapter precondition check\n * (sub-task 4.1b / issue #373):\n *\n * - `\"static\"` — declare a pure-static (SSG-only) project. Errors at\n * build start if any route exports `prerender = false`, pointing\n * at the offending route. Use this on projects that must never\n * accidentally pick up an SSR route as a result of a copy-paste.\n * - `\"hybrid\"` — declare a project that may host SSR routes. V8-on\n * regardless of detection, even when no `prerender = false` route\n * currently exists. Useful for projects that will add SSR routes\n * later and want a stable build topology in the meantime.\n * - `\"auto\"` (default) — detection-driven. Non-empty `prerender =\n * false` route set => V8-on; empty => V8-off.\n *\n * Today's load-bearing role is the `\"static\"` precondition check.\n * The V8-off branch does NOT skip V8 host startup on the shipping\n * `zfb` binary — SSG still needs V8 to render pages. The flag exists\n * as infrastructure for the future shipping path (Tauri sidecar /\n * standalone SSR server). See the\n * [Build engine docs](https://github.com/Takazudo/zudo-front-builder/blob/main/docs/src/content/docs/architecture/build-engine.mdx)\n * for the gate decision table.\n *\n * Mirrors `Config::output` in crates/zfb/src/config.rs.\n */\n output?: OutputMode;\n\n /**\n * Config presets to merge before validation (#1196).\n *\n * Each preset is a partial `ZfbConfig`-shaped object. The merge pass runs\n * BEFORE field validation and folds preset contributions using additive\n * semantics:\n *\n * - **Array fields** (`plugins`, `collections`, `extraWatchPaths`,\n * `allowedHosts`): preset values are prepended so the main config's\n * entries retain their relative position after the preset's.\n * - **Scalar / optional fields**: a preset value fills in only when the\n * main config leaves the field at its default — the main config is\n * authoritative; presets act as defaults.\n *\n * Nested `presets` inside a preset are NOT recursively expanded.\n *\n * Mirrors `Config::presets` in crates/zfb/src/config.rs.\n */\n presets?: Partial<ZfbConfig>[];\n};\n\n/**\n * Project output mode.\n *\n * - `\"static\"` — pure-static (SSG-only); errors on detected SSR routes.\n * - `\"hybrid\"` — may host SSR routes; V8-on regardless of detection.\n * - `\"auto\"` — detection-driven; the default.\n *\n * Mirrors `OutputMode` in crates/zfb/src/config.rs.\n */\nexport type OutputMode = \"static\" | \"hybrid\" | \"auto\";\n\n/**\n * Syntect code-highlight options.\n *\n * Unknown theme names are rejected at build start with a clear error\n * rather than silently falling back.\n *\n * **Single-theme mode** (the default): set `theme` to a syntect theme name,\n * or omit it to use the default (`\"base16-ocean.dark\"`). Tokens are colored\n * with inline `color:`.\n *\n * **Dual-theme mode**: set both `themeLight` and `themeDark`. Tokens are\n * colored with CSS custom properties (`--shiki-light` / `--shiki-dark`),\n * and the consumer applies a `light-dark()` rule to pick the active color.\n * The `<pre>` element carries `class=\"syntect-dual\"` and\n * `--shiki-light-bg` / `--shiki-dark-bg` in its `style` attribute.\n *\n * `theme` and the dual pair are mutually exclusive. Setting only one of\n * `themeLight` / `themeDark` is an error.\n *\n * All theme names are **SYNTECT** built-in or user-loaded names (e.g.\n * `\"base16-ocean.light\"`, `\"base16-ocean.dark\"`, `\"InspiredGitHub\"`,\n * `\"Solarized (dark)\"`), NOT Shiki names like `\"dracula\"`.\n *\n * **Class mode** (Highlight Tokens epic, zfb#1528): set `mode: \"class\"`.\n * Each token gets a semantic role class instead of an inline color, so\n * highlight colors become re-themeable CSS design tokens. Mutually\n * exclusive with `theme` / `themeLight` / `themeDark` / `themesDir` —\n * themes don't affect class emission, so setting both is a build error.\n *\n * Mirrors `CodeHighlightConfig` in crates/zfb/src/config.rs.\n */\nexport type CodeHighlightConfig = {\n /**\n * Syntect built-in or user-loaded theme name. When absent the\n * pipeline defaults to `\"base16-ocean.dark\"`.\n *\n * Mutually exclusive with {@link themeLight} / {@link themeDark}.\n * Must be a SYNTECT theme name (e.g. `\"InspiredGitHub\"`), NOT a Shiki name.\n */\n theme?: string;\n /**\n * Path to a directory of `.tmTheme` files, relative to the project\n * root. Every `.tmTheme` file in the directory is loaded and becomes\n * available by its declared `name` via {@link theme}, {@link themeLight},\n * or {@link themeDark}. When absent only syntect's bundled themes are\n * available.\n *\n * The path must be relative and must not escape the project root via\n * `..`. A missing directory is reported as an error at build start.\n *\n * Applies to both single-theme and dual-theme mode.\n */\n themesDir?: string;\n /**\n * Light-mode syntect theme name for dual-theme highlighting.\n *\n * Must be set together with {@link themeDark} — setting only one of\n * the two is a build error. When both are set, tokens are colored with\n * CSS custom properties (`--shiki-light` / `--shiki-dark`) instead of\n * inline `color:`. Mutually exclusive with {@link theme}.\n *\n * Must be a SYNTECT theme name (e.g. `\"base16-ocean.light\"`),\n * NOT a Shiki name like `\"dracula\"`.\n */\n themeLight?: string;\n /**\n * Dark-mode syntect theme name for dual-theme highlighting.\n *\n * Must be set together with {@link themeLight} — setting only one of\n * the two is a build error. Mutually exclusive with {@link theme}.\n *\n * Must be a SYNTECT theme name (e.g. `\"base16-ocean.dark\"`),\n * NOT a Shiki name like `\"dracula\"`.\n */\n themeDark?: string;\n /**\n * Output mode for fenced-code highlighting (Highlight Tokens epic,\n * zfb#1528). `\"inline\"` (default) bakes per-token colors into\n * `style=\"color:#rrggbb\"` (or the dual `--shiki-*` custom properties).\n * `\"class\"` emits a semantic role class per token instead, so colors\n * become re-themeable CSS design tokens rather than baked-in HTML.\n *\n * Mutually exclusive with {@link theme} / {@link themeLight} /\n * {@link themeDark} / {@link themesDir} — themes don't affect class\n * emission, so setting both is rejected rather than silently ignoring\n * the theme.\n */\n mode?: CodeHighlightMode;\n /**\n * Class-name prefix for class-mode role classes (e.g. the default\n * `\"hi-\"` yields `hi-kw`, `hi-str`, ...). Must match\n * `/^[A-Za-z][A-Za-z0-9_-]*$/`. Only meaningful when {@link mode} is\n * `\"class\"`. Default: `\"hi-\"`.\n */\n classPrefix?: string;\n /**\n * Per-role class overrides for class mode, e.g.\n * `{ keyword: \"text-violet-600 dark:text-violet-400\" }` to map a role\n * onto Tailwind utilities instead of the default `{classPrefix}{role}`\n * class. Keys must be one of the 18 fixed role names (see\n * {@link CodeHighlightRole}); a value may hold multiple\n * space-separated classes and must not contain the bare token `\"line\"`\n * (collides with the code-enrichment line wrapper class). Absent uses\n * `{classPrefix}{role}` for every role.\n *\n * Setting this while `tailwind.enabled` is `false` (the authored-CSS\n * path) is allowed but emits a build warning — no Tailwind safelist can\n * be generated on that path, so the mapped utilities must already exist\n * in your own CSS.\n */\n roleClasses?: Partial<Record<CodeHighlightRole, string>>;\n /**\n * Whether to inject the built-in `--zfb-hi-*` token stylesheet\n * (`zfb-hi.css`) into the combined `styles.css` output. Only meaningful\n * in class mode. Default: `true`.\n */\n defaultStylesheet?: boolean;\n};\n\n/**\n * `codeHighlight.mode` — see {@link CodeHighlightConfig.mode}.\n *\n * Mirrors `CodeHighlightMode` in crates/zfb/src/config.rs.\n */\nexport type CodeHighlightMode = \"inline\" | \"class\";\n\n/**\n * The fixed 18-role semantic taxonomy for class-mode syntax highlighting\n * (Highlight Tokens epic, zfb#1528) — valid {@link CodeHighlightConfig.roleClasses}\n * keys.\n *\n * Mirrors `CODE_HIGHLIGHT_ROLES` in crates/zfb/src/config.rs.\n */\nexport type CodeHighlightRole =\n | \"escape\"\n | \"operator\"\n | \"comment\"\n | \"string\"\n | \"number\"\n | \"constant\"\n | \"keyword\"\n | \"function\"\n | \"type\"\n | \"namespace\"\n | \"property\"\n | \"variable\"\n | \"tag\"\n | \"attribute\"\n | \"punctuation\"\n | \"inserted\"\n | \"deleted\"\n | \"heading\";\n\n/**\n * Table-of-contents options. Wire via `markdown.toc` in `zfb.config.ts`.\n *\n * When present, a TOC `<ul>/<li>` list is inserted as the next sibling\n * of the first heading whose text matches `heading` (case-insensitive).\n * Each `<a href=\"#id\">` links to the deduplicated `id` that\n * `HeadingLinksPlugin` placed on the corresponding heading.\n *\n * Mirrors `TocConfig` in `crates/zfb-content/src/plugins/toc.rs`.\n */\nexport type TocConfig = {\n /**\n * Heading text that triggers TOC insertion. Matched\n * case-insensitively after whitespace trimming. Default: `\"TOC\"`.\n */\n heading?: string;\n\n /**\n * Number of heading levels to include starting from `<h2>`.\n *\n * - `1` — h2 only\n * - `2` (default) — h2 + h3\n * - `3` — h2, h3, h4\n * - …up to `5` (h2 through h6)\n */\n maxDepth?: number;\n};\n\n/**\n * Markdown / MDX parsing options.\n *\n * See [`ZfbConfig.markdown`] for the embed point. Fields: [`gfm`],\n * [`toc`], [`externalLinks`], [`cjkFriendly`], and [`features`].\n * Future markdown knobs would also live here.\n *\n * See the \"Markdown Features\" docs category for the per-feature option\n * reference once individual features are ported.\n *\n * Mirrors `MarkdownConfig` in crates/zfb/src/config.rs.\n */\nexport type MarkdownConfig = {\n /**\n * Enable GFM constructs.\n *\n * Accepts three shapes:\n *\n * - `true` — turn every GFM construct ON (strikethrough, table,\n * autolink-literal, task-list-item, footnote-definition).\n * - `false` — turn every GFM construct OFF.\n * - partial object — set individual fields explicitly; fields you\n * omit fall back to the conservative-default values described\n * below.\n *\n * When `markdown` itself is omitted entirely, the conservative\n * default applies: `strikethrough: true`, `table: true`, every other\n * GFM construct off. This is the smallest behavioural delta from\n * zfb's historical effective state (table-only). Projects that want\n * the full GFM surface should opt in with `gfm: true`.\n */\n gfm?: GfmFlag;\n\n /**\n * Table-of-contents options. When present, a `<ul>/<li>` list is\n * inserted after the first heading whose text matches `heading`\n * (default `\"TOC\"`, case-insensitive). Each link points to the\n * deduplicated `id` that `HeadingLinksPlugin` placed on the heading.\n *\n * Omitting this field entirely leaves the build byte-for-byte identical\n * to the pre-TOC build. See [`TocConfig`] for the available options.\n *\n * Mirrors `MarkdownConfig::toc` in crates/zfb/src/config.rs.\n */\n toc?: TocConfig;\n /**\n * External-link rewriter. When set, every `<a>` whose href is\n * classified as external receives the configured `target` and `rel`\n * attributes.\n *\n * An href is external when it is an absolute HTTP/HTTPS URL AND its\n * origin differs from the top-level `site` URL (if `site` is\n * configured). When `site` is absent, any absolute HTTP/HTTPS URL is\n * treated as external.\n *\n * `mailto:`, `tel:`, and other non-HTTP(S) schemes are always left\n * unchanged. Relative URLs (`/internal/`, `./file.mdx`, `#anchor`) are\n * always internal.\n *\n * Omitting this field keeps the output byte-for-byte identical to the\n * pre-feature behaviour.\n *\n * Mirrors `ExternalLinksConfig` in crates/zfb/src/config.rs.\n */\n externalLinks?: ExternalLinksConfig;\n\n /**\n * Enable CJK-friendly markdown handling.\n *\n * Governs two post-parse fixups that adapt CommonMark/GFM rules to CJK\n * text:\n *\n * 1. **Emphasis/strong flanking** (`CjkFriendlyPlugin`). CommonMark's\n * left-/right-flanking delimiter-run rules treat CJK characters as\n * non-whitespace non-punctuation, which causes `**foo**` adjacent to\n * CJK text (e.g. `**テスト。**テスト`) to render as literal stars\n * instead of `<strong>`.\n * 2. **Bare-URL autolink boundary** (`CjkAutolinkBoundaryPlugin`,\n * zfb#1105). The GFM autolink-literal path grammar terminates only on\n * ASCII whitespace, so a bare URL flush against CJK text\n * (`詳細はhttps://example.com参照`) swallows the trailing CJK run into\n * the `href`. This fixup terminates the link at the first CJK\n * character. Only active when `gfm.autolinkLiteral` is also on.\n *\n * - **absent / `true` (default):** CJK-friendly handling is on.\n * Preserves today's behaviour — existing CJK-content sites are\n * unaffected.\n * - **`false`:** opt-out. Neither plugin is added to the pipeline;\n * emphasis markers and bare-URL autolinks adjacent to CJK characters\n * follow base CommonMark/GFM rules. Rarely the right choice; provided\n * as an escape hatch for projects that need strict CommonMark/GFM\n * output.\n *\n * **GFM strikethrough** (`~~foo~~`) at CJK boundaries is unaffected\n * by this toggle — it is handled by markdown-rs's GFM tokeniser, not\n * by these plugins, and works correctly in both modes.\n *\n * Mirrors `MarkdownConfig::cjk_friendly` in crates/zfb/src/config.rs.\n */\n cjkFriendly?: boolean;\n\n /**\n * Convert every soft line break (a single `\\n` inside a paragraph) into\n * `<br>` (remark-breaks parity).\n *\n * - **absent / `false` (default):** soft line breaks follow standard\n * CommonMark behaviour — collapsed into a single space.\n * - **`true`:** every `\\n` inside a paragraph becomes `<br>`. Use this\n * when your content relies on newline→`<br>` fidelity (e.g. product\n * descriptions, lyrics, or other newline-sensitive prose).\n *\n * Mirrors `MarkdownConfig::hard_breaks` in crates/zfb/src/config.rs.\n */\n hardBreaks?: boolean;\n\n /**\n * Per-feature markdown pipeline toggles.\n *\n * Each field is a [`FeatureToggle`] (`true` / `false` / options object)\n * or a feature-specific config type (for features that require extra\n * parameters). Absent / `undefined` means all features are disabled,\n * preserving the behaviour of the pre-features build byte-for-byte.\n *\n * Unknown keys are rejected at deserialization time by the Rust loader\n * so a typo in `zfb.config.ts` surfaces as a clear error.\n *\n * Mirrors `MarkdownFeaturesConfig` in crates/zfb/src/config.rs.\n */\n features?: MarkdownFeaturesConfig;\n};\n\n/**\n * Per-feature toggle: `boolean` shorthand or an options object.\n *\n * `true` enables the feature with defaults; `false` (or absent) disables it.\n * The object form carries per-feature options (fields vary by feature and\n * are filled in by each feature's port sub-issue — stubs today).\n *\n * Mirrors `FeatureToggle` in crates/zfb/src/config.rs.\n */\nexport type FeatureToggle = boolean | FeatureOptions;\n\n/**\n * Empty options object for features that accept `{ ... }` but have no\n * user-facing knobs yet. Fields are filled in by each feature's port\n * sub-issue; this stub satisfies the schema shape requirement.\n *\n * Mirrors `FeatureOptions` in crates/zfb/src/config.rs.\n */\nexport type FeatureOptions = Record<string, never>;\n\n/**\n * Options for the `codeEnrichment` feature.\n *\n * All flags default to `true` when the feature is enabled with\n * `codeEnrichment: {}` or when a field is absent.\n *\n * Mirrors `CodeEnrichmentConfig` in `crates/zfb-md-ast/src/features_config.rs`.\n */\nexport type CodeEnrichmentConfig = {\n /**\n * Enable diff-marker processing for markers such as `// [!code ++]`\n * and `// [!code --]`. Default: `true`.\n */\n diffMarkers?: boolean;\n /**\n * Enable line-highlight processing for fence ranges such as `{1,3-5}`.\n * Default: `true`.\n */\n lineHighlight?: boolean;\n /**\n * Enable visible-text word emphasis for slash-delimited fence metadata\n * such as `/answer/`. Default: `true`.\n */\n wordHighlight?: boolean;\n};\n\n/**\n * Options for the `tocExport` feature.\n *\n * Controls which headings are included in the exported `toc` JSON.\n * `maxDepth` is the **absolute** heading depth (2–6):\n * - `2` → h2 only\n * - `3` (default) → h2 + h3\n *\n * This differs from `headingMarkerToc.maxDepth`, which counts levels\n * starting from h2. The two features are independent.\n *\n * Mirrors `TocExportConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type TocExportConfig = {\n /** Maximum heading depth to include (absolute, 2–6). Default: 3. */\n maxDepth?: number;\n};\n\n/**\n * Options for the `imageDimensions` feature.\n *\n * Auto-detects and injects `width`/`height` on local `<img>` elements. Raster\n * formats are probed header-only; SVGs are read from their markup\n * (`width`/`height`/`viewBox`).\n *\n * Mirrors `ImageDimensionsConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type ImageDimensionsConfig = {\n /**\n * When `true` (the default), `http://` and `https://` image sources are\n * silently skipped and not probed for dimensions. Set to `false` only for\n * testing or unusual setups — remote images require network access at build\n * time and slow the pipeline.\n */\n skipRemote?: boolean;\n};\n\n/**\n * Options for the `linkValidation` feature.\n *\n * Validates internal `[text](file.md#anchor)` and `[text](#anchor)` links at\n * build time. External URLs (`http://`, `https://`, `mailto:`) are always\n * skipped — network validation is out of scope.\n *\n * Mirrors `LinkValidationConfig` in `crates/zfb-md-ast/src/features_config.rs`.\n */\nexport type LinkValidationConfig = {\n /**\n * When `true`, broken links are reported as errors (build can fail).\n * Default: `false` (warn-only).\n */\n failOnBroken?: boolean;\n};\n\n/**\n * Options for the `transclude` feature.\n *\n * Enables `:::include{file=\"./path.md\"}` directives that inline another\n * file's parsed mdast at the include site.\n *\n * Mirrors `TranscludeConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type TranscludeConfig = {\n /**\n * Maximum transclusion depth (chain length A→B→C→…).\n *\n * A depth of `1` allows only direct includes (the included file itself\n * cannot include further files). Default: `5`. A cycle (A→B→A) is\n * always detected regardless of `maxDepth` and treated as an error.\n */\n maxDepth?: number;\n};\n\n/**\n * Options for the `readingTime` feature.\n *\n * Mirrors `ReadingTimeOptions` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type ReadingTimeConfig = {\n /** Words-per-minute rate for the reading-time estimate. Default: 200. */\n wpm?: number;\n};\n\n/**\n * `readingTime` feature value: either a `boolean` shorthand or a\n * {@link ReadingTimeConfig} options object.\n *\n * Mirrors `ReadingTimeFeature` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type ReadingTimeFeature = boolean | ReadingTimeConfig;\n\n/**\n * Per-feature markdown pipeline configuration.\n *\n * All fields are optional; absent = feature disabled, behaviour unchanged\n * from the pre-features build. Unknown keys are rejected at deserialization\n * time by the Rust loader so a typo surfaces as a clear error.\n *\n * Mirrors `MarkdownFeaturesConfig` in crates/zfb/src/config.rs.\n */\nexport type MarkdownFeaturesConfig = {\n /** GitHub-style alert blocks (`> [!NOTE]`, `> [!WARNING]`, etc.). */\n githubAlerts?: FeatureToggle;\n\n /**\n * Reading-time estimate injected into the document frontmatter.\n * Accepts `true` / `false` shorthand or `{ wpm: N }` for a custom rate.\n */\n readingTime?: ReadingTimeFeature;\n\n /** Code-block enrichment (copy button, language label, etc.). */\n codeEnrichment?: CodeEnrichmentConfig;\n\n /** Grouped code blocks rendered as tabs. */\n codeTabs?: FeatureToggle;\n\n /** Ruby annotation support (`{base}^{ruby}` syntax). */\n ruby?: FeatureToggle;\n\n /** Export the page TOC as structured data (e.g. for sidebar rendering). */\n tocExport?: TocExportConfig;\n\n /** Auto-detect and inject `width`/`height` on `<img>` elements. */\n imageDimensions?: ImageDimensionsConfig;\n\n /**\n * Validate internal links (file-relative paths and anchor fragments) at\n * build time. External URLs are always skipped — network validation is\n * out of scope.\n */\n linkValidation?: LinkValidationConfig;\n\n /**\n * Transclusion of other markdown/MDX files via\n * `:::include{file=\"./path.md\"}` — NOT the Obsidian `[[path]]` wikilink\n * syntax.\n */\n transclude?: TranscludeConfig;\n\n /**\n * Generic `:::name` → component map. You supply the components; no defaults\n * are registered. Keys are directive names (e.g. `\"foo\"`), values are\n * {@link DirectiveSpec} (bare component name string or options object).\n *\n * Mirrors `directives` in `MarkdownFeaturesConfig` in crates/zfb/src/config.rs.\n */\n directives?: Record<string, DirectiveSpec>;\n\n /** Mermaid diagram rendering. */\n mermaid?: FeatureToggle;\n\n /**\n * Inline heading-marker TOC. Accepts either a `boolean` shorthand\n * (`true` = enable with defaults, `false` = disable) or a full\n * {@link TocConfig} options object — same union shape as the Rust\n * `HeadingMarkerTocFeature` enum.\n */\n headingMarkerToc?: HeadingMarkerTocFeature;\n\n /**\n * Heading-ID strategy for the always-on `HeadingLinks` plugin.\n * Absent → `\"flat\"` (the long-standing github-slugger scheme).\n * `{ strategy: \"hierarchical\" }` opts into ancestor-prefixed anchor\n * IDs (`## Foo` / `### Moo` / `#### Mew` → `foo`, `foo-moo`,\n * `foo-moo-mew`) — see {@link HeadingIdsConfig}.\n */\n headingIds?: HeadingIdsConfig;\n};\n\n/**\n * Options for the `headingIds` entry in `markdown.features`.\n *\n * Configures the always-on `HeadingLinks` plugin rather than toggling an\n * opt-in feature. Note: switching to `\"hierarchical\"` is anchor-breaking\n * for existing deep links to nested headings.\n *\n * Mirrors `HeadingIdsConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type HeadingIdsConfig = {\n /**\n * `\"flat\"` (default): github-slugger slugs with a per-document dedup\n * counter shared across h2–h6 (`overview`, `overview-1`, …).\n * `\"hierarchical\"`: each heading's slug is prefixed with its ancestor\n * chain and deduped on the full path — anchors become reconstructible\n * from the heading outline.\n */\n strategy?: \"flat\" | \"hierarchical\";\n};\n\n/**\n * `headingMarkerToc` feature value: either a `boolean` shorthand or a\n * full {@link TocConfig} options object.\n *\n * Mirrors `HeadingMarkerTocFeature` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type HeadingMarkerTocFeature = boolean | TocConfig;\n\n/**\n * Spec for one user-defined directive: either a bare component name string\n * or a full {@link DirectiveFullSpec} options object.\n *\n * Mirrors `DirectiveSpec` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type DirectiveSpec = string | DirectiveFullSpec;\n\n/**\n * Full options object for one user-defined directive.\n *\n * Mirrors `DirectiveFullSpec` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type DirectiveFullSpec = {\n /** JSX component identifier (e.g. `\"Spoiler\"`, `\"Kbd\"`). */\n component: string;\n /** Container/leaf/text shape. Defaults to `\"container\"` when absent. */\n kind?: \"container\" | \"leaf\" | \"text\";\n /** Whether the bracketed `[label]` becomes a `title` attribute. Defaults to `true`. */\n titleFromLabel?: boolean;\n};\n\n/**\n * Options for the external-link rewriter (port of `rehype-external-links`).\n *\n * All fields are optional; omitting a field applies the documented default.\n *\n * Mirrors `ExternalLinksConfig` in crates/zfb/src/config.rs.\n */\nexport type ExternalLinksConfig = {\n /**\n * `rel` tokens applied to external links.\n *\n * Default: `[\"noopener\", \"noreferrer\"]`.\n *\n * Tokens are deduplicated (case-insensitive) and merged with any\n * existing `rel` attribute on the `<a>` element — existing tokens\n * appear first.\n */\n rel?: string[];\n /**\n * `target` value for external links.\n *\n * Default: `\"_blank\"`.\n */\n target?: string;\n};\n\n/**\n * Either the shorthand boolean form (`true` = all GFM constructs on,\n * `false` = all off) or a partial object that toggles individual\n * constructs.\n *\n * Mirrors `GfmFlag` in crates/zfb/src/config.rs.\n */\nexport type GfmFlag = boolean | GfmConstructs;\n\n/**\n * Per-construct opt-in / opt-out for GFM. Every field is optional;\n * omitted fields fall back to the conservative default\n * (`strikethrough: true`, `table: true`, others `false`).\n *\n * Mirrors `GfmConstructs` in crates/zfb/src/config.rs.\n */\nexport type GfmConstructs = {\n /** GFM strikethrough (`~~text~~` → `<del>text</del>`). */\n strikethrough?: boolean;\n /** GFM pipe-style tables. */\n table?: boolean;\n /**\n * GFM autolink literal — bare URLs like `https://example.com` become\n * clickable links without `<…>` brackets.\n */\n autolinkLiteral?: boolean;\n /** GFM task list items (`- [x]` / `- [ ]`). */\n taskListItem?: boolean;\n /** GFM footnote definitions (`[^ref]: …`). */\n footnoteDefinition?: boolean;\n};\n\n/**\n * What to do when a `.md`/`.mdx` link cannot be resolved.\n *\n * Mirrors `OnBrokenLinks` in crates/zfb/src/config.rs.\n */\nexport type OnBrokenLinks = \"warn\" | \"error\" | \"ignore\";\n\n/**\n * Config for the markdown link resolver. See\n * [`ZfbConfig.resolveMarkdownLinks`] for the design rationale.\n */\nexport type ResolveMarkdownLinksConfig = {\n /** Whether to enable link resolution. Default: `false`. */\n enabled?: boolean;\n\n /**\n * Legacy single-dir field. Used only when [`dirs`] is empty. When\n * non-empty, scanned against the hard-coded `/docs/` route prefix.\n */\n docsDir?: string;\n\n /**\n * Explicit per-dir source map. Each entry is one collection (e.g.\n * EN docs at `src/content/docs/` → `/docs/`, JA docs at\n * `src/content/docs-ja/` → `/ja/docs/`). Takes precedence over\n * [`docsDir`] when non-empty.\n */\n dirs?: ResolveMarkdownLinksDir[];\n\n /** What to do with unresolved `.md`/`.mdx` links. Default: `\"warn\"`. */\n onBrokenLinks?: OnBrokenLinks;\n};\n\n/** One source-dir entry for [`ResolveMarkdownLinksConfig.dirs`]. */\nexport type ResolveMarkdownLinksDir = {\n /**\n * Directory (relative to project root) whose `.md`/`.mdx` files are\n * scanned. Must be relative and must not escape the root via `..`.\n */\n dir: string;\n\n /**\n * Route prefix prepended to each file's slug. Include leading and\n * trailing slashes (e.g. `\"/docs/\"` or `\"/ja/docs/\"`).\n */\n routePrefix: string;\n};\n\n/**\n * Identity helper: returns the supplied config as-is, but typed against\n * [`ZfbConfig`]. Use as the default export of `zfb.config.ts` so editors\n * surface field-level types and typos surface at compile time.\n */\nexport function defineConfig(config: ZfbConfig): ZfbConfig {\n return config;\n}\n\n/**\n * Preset authoring helper: stamps each object entry in `config.plugins`\n * with `source_package: sourcePackage` so the Rust loader can attribute\n * plugin contributions back to the preset package that provided them.\n *\n * - Only plain-object plugin entries are stamped; non-object entries pass\n * through unchanged (defensive — the current schema requires objects,\n * but this guard keeps the helper safe if the schema is ever relaxed).\n * - An entry that ALREADY carries a `source_package` is left untouched, so a\n * preset composing another `definePreset`-returned preset (by spreading its\n * `plugins`) keeps the inner preset's provenance instead of clobbering it\n * with the outer package name (the spread below lets the existing marker win).\n * - When `config.plugins` is absent, the config is returned as-is.\n * - All other fields of `config` pass through unchanged.\n *\n * The key `source_package` (snake_case) mirrors the Rust `PluginConfig`\n * serde field added in T4. `PluginConfig` has no `#[serde(rename_all)]`\n * so the serde key is the field name verbatim — do NOT use camelCase.\n *\n * SYNC REQUIREMENT: keep this implementation behaviourally identical to\n * the stub in crates/zfb-config-loader/js/zfb-config-stub.mjs, which is\n * injected at config-eval time when the user's project does not have the\n * zfb npm package installed locally.\n */\nexport function definePreset(\n sourcePackage: string,\n config: Partial<ZfbConfig>,\n): Partial<ZfbConfig> {\n if (!config.plugins) {\n return config;\n }\n return {\n ...config,\n plugins: config.plugins.map((plugin) => {\n if (plugin !== null && typeof plugin === \"object\" && !Array.isArray(plugin)) {\n // Default first, then spread the plugin so an existing `source_package`\n // (from a composed inner preset) wins over the outer package name.\n return { source_package: sourcePackage, ...plugin };\n }\n return plugin;\n }),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,kEAAkE;AAClE,qEAAqE;AACrE,uEAAuE;AACvE,oEAAoE;AACpE,+BAA+B;AAC/B,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AAmyCvE;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAC1B,aAAqB,EACrB,MAA0B;IAE1B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO;QACL,GAAG,MAAM;QACT,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACrC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5E,wEAAwE;gBACxE,mEAAmE;gBACnE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CAAC;YACtD,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["// `zfb/config` — TypeScript helper for the `zfb.config.ts` form.\n//\n// The zfb config loader (`crates/zfb/src/config.rs`) accepts both\n// `zfb.config.ts` and `zfb.config.json`; TS wins when both files are\n// present. JSON remains accepted for projects predating the TS loader,\n// while new projects should prefer the TS form for editor types and\n// `defineConfig` autocomplete.\n//\n// At parse time, zfb bundles the user's `zfb.config.ts` with esbuild and\n// aliases this `zfb/config` import to an internal stub that re-exports\n// `defineConfig` as the identity function — so a user project does not\n// need the `zfb` npm package installed locally just to be parsed.\n//\n// The shape mirrors the Rust `Config` struct one-for-one. Keep them in\n// sync; the `defineConfig` identity helper is the single anchor point.\n\nexport type Framework = \"preact\" | \"react\";\n\nexport type CollectionDef = {\n /** Identifier used at the call site (e.g. `\"blog\"`). */\n name: string;\n /** Directory (relative to the project root) holding the entries. */\n path: string;\n /** Optional schema. Enforced by `zfb check`. */\n schema?: Record<string, unknown>;\n /**\n * Optional include globs (Astro-style, evaluated relative to `path`).\n * When set and non-empty, an entry is kept only if at least one\n * pattern matches its relative path. When omitted or empty, no\n * include-filtering happens. Patterns use the `globset` dialect\n * (Unix-style: `*`, `**`, `?`, `[…]`).\n */\n include?: string[];\n /**\n * Optional exclude globs. When set, an entry is dropped if any\n * pattern matches its relative path. Evaluated AFTER `include`.\n * Together they mirror Astro's `['**\\/*.mdx', '!**\\/*.en.mdx']`\n * convention (zfb splits the negative side into its own field).\n */\n exclude?: string[];\n /**\n * Optional suffix to strip from each kept entry's slug + module\n * specifier. Use with multi-locale layouts where one source\n * directory holds both `foo.mdx` (default locale) and `foo.en.mdx`\n * (locale override) — set `idStripSuffix: \".en\"` so the EN\n * collection's slugs round-trip as `foo` instead of `foo.en`.\n */\n idStripSuffix?: string;\n /**\n * Opt-in to a `path` that escapes the project root via `..` (e.g. a\n * monorepo-shared content dir living outside this package). Default\n * `false` keeps the standard project-root guard. Absolute paths and\n * Windows drive-relative/prefix forms are rejected regardless of\n * this flag — only `..`-relative escapes are relaxed.\n *\n * Security note: if this collection comes from a preset, the preset\n * author — not the consuming project — controls `path`. Setting\n * `allowOutsideRoot: true` on a preset-provided collection widens\n * the project's read surface to wherever that preset points, so\n * treat it the same as any other preset-granted filesystem access.\n */\n allowOutsideRoot?: boolean;\n};\n\nexport type TailwindConfig = {\n /** Whether Tailwind is enabled. Default: `true`. */\n enabled?: boolean;\n};\n\n/**\n * Prefetch options. Mirrors `PrefetchConfig` in `crates/zfb/src/config.rs`.\n */\nexport type PrefetchConfig = {\n /**\n * Disable prefetch entirely.\n *\n * When `true`, the bundler emits `globalThis.__zfb.prefetchDisabled = true`\n * in `entry.mjs`, and `<ClientRouter />` renders\n * `<meta name=\"zfb-prefetch-disabled\" content=\"true\">` in `<head>`.\n * The sibling prefetch-core module reads that meta tag at `init()` time\n * and short-circuits — no prefetch wiring runs.\n *\n * The flag is site-wide and static — set once at bundle-emit time,\n * never recomputed per-page. Default: `false`.\n */\n disabled?: boolean;\n};\n\n/**\n * Bundler options. Mirrors `BundleConfig` in `crates/zfb/src/config.rs`.\n */\nexport type BundleConfig = {\n /**\n * Project-relative glob patterns (gitignore-style) for source files\n * the bundler must NOT pull into the esbuild graph.\n *\n * Why this exists: an eager `import.meta.glob('components/**\\/*.stories.tsx',\n * { eager: true })` expands to a static import of every matched file. If a\n * matched file imports a CJS-only package whose `package.json` resolves only\n * via `main`/`module` or a `require`-only `exports` condition (e.g.\n * `msw` → `path-to-regexp@6`), esbuild — invoked with `--platform=neutral`\n * for the worker bundle — rejects it with \"Could not resolve … Main fields\n * must be configured explicitly when using the neutral platform.\" Listing the\n * offending file here keeps the migration build green.\n *\n * Each pattern is matched against the file's path RELATIVE TO THE PROJECT\n * ROOT, in POSIX form (e.g. `components/Foo.stories.tsx` or\n * `components/**\\/*.stories.tsx`). A matched file is:\n *\n * - never copied/symlinked into the bundler's shadow tree, and\n * - dropped from any eager `import.meta.glob(...)` expansion that would\n * otherwise statically import it.\n *\n * Unset / empty → behaviour is byte-identical to a build without this knob:\n * no files are skipped.\n *\n * Mirrors `Config::bundle` in crates/zfb/src/config.rs.\n */\n exclude?: string[];\n\n /**\n * Explicit esbuild `main-fields` list for the `--platform=neutral` page/SSR\n * pass. Under `neutral` esbuild's main-fields list is EMPTY by default, so a\n * dep resolved purely via `package.json` `main`/`module` (no `exports` map)\n * is rejected (\"The \"main\" field here was ignored. Main fields must be\n * configured explicitly when using the neutral platform.\"). Set e.g.\n * `[\"main\", \"module\"]` to let such CJS-main-only deps resolve (#676 —\n * `msw` → `path-to-regexp@6`). Applies to every framework; unset/empty →\n * byte-identical to a build without the knob (the React-only `main,module`\n * shim still applies).\n *\n * Mirrors `BundleConfig::main_fields` in `crates/zfb/src/config.rs`.\n */\n mainFields?: string[];\n\n /**\n * Bare specifiers to mark external in the `--platform=neutral` page/SSR\n * pass, so esbuild leaves them unbundled instead of resolving them (the\n * other #676 escape hatch — externalize a CJS-only dep rather than\n * resolving it). Appended to the framework-provided externals. Unset/empty\n * → no extra externals.\n *\n * Mirrors `BundleConfig::external` in `crates/zfb/src/config.rs`.\n */\n external?: string[];\n\n /**\n * Additional esbuild loaders keyed by file extension (for example\n * `{ \".txt\": \"text\" }`). Only inline loaders are supported: `file` and\n * `copy` are intentionally excluded because they emit sibling assets the\n * client bundlers do not publish. `.css`, `.module.css`, `.mdx`, and `.md`\n * are reserved by zfb and rejected during config validation.\n */\n loaders?: Record<string, \"text\" | \"json\" | \"base64\" | \"dataurl\" | \"binary\" | \"empty\">;\n\n /**\n * Operator-authored esbuild define substitutions. Values are raw esbuild\n * expressions; string values must be pre-quoted JSON (for example\n * `{ __APP_NAME__: '\"my-app\"' }`). The mode-owned keys\n * `import.meta.env.PROD`, `import.meta.env.DEV`, and\n * `process.env.NODE_ENV` are reserved and rejected at config-load time.\n */\n define?: Record<string, string>;\n};\n\n/**\n * One plugin entry in `zfb.config.ts`.\n *\n * `name` MUST be a module reference that Node's resolver can locate from\n * the project root. The zfb config loader\n * (`crates/zfb-config-loader/js/config-loader.mjs`) resolves it to an\n * absolute module specifier and the build / dev plugin host loads it via\n * dynamic `import()`:\n *\n * - `\"./plugins/my-plugin.mjs\"` / `\"../shared/plugin.mjs\"` —\n * path-relative to the project root (the dir containing `zfb.config.ts`).\n * - `\"/abs/path/to/plugin.mjs\"` — absolute filesystem path.\n * - `\"@takazudo/zfb-plugin-search\"` / `\"my-plugin\"` — npm bare specifier\n * resolved against the project's `node_modules`.\n *\n * Inline-function hooks are NOT supported; the plugin module's default\n * export must be a [`ZfbPlugin`] (see `@takazudo/zfb/plugins`).\n *\n * `options` is passed verbatim to the plugin's hook contexts; treat\n * the schema as plugin-specific.\n */\nexport type PluginConfig = {\n name: string;\n options?: Record<string, unknown>;\n};\n\nexport type ZfbConfig = {\n /** Output directory for built assets. Default: `dist`. */\n outDir?: string;\n /** Public/static directory copied verbatim. Default: `public`. */\n publicDir?: string;\n /** Optional dev/preview server bind host. */\n host?: string;\n /** Optional dev/preview server port. */\n port?: number;\n /**\n * Host header values the dev/preview server accepts when bound to a\n * non-localhost interface (`--host 0.0.0.0`, the bare `--host` LAN\n * shortcut, or `host` above) — the DNS-rebinding guard, mirroring\n * Vite's `server.allowedHosts`.\n *\n * Defaults: only consulted for non-loopback binds — the default\n * `localhost` bind skips validation entirely. `localhost`, the\n * explicitly bound host, and any IP-literal Host — `127.0.0.1`,\n * `[::1]`, the LAN URLs the startup banner prints — are always\n * allowed (DNS rebinding needs a DNS name, so raw IPs are safe;\n * Vite parity); requests with any other Host get a 403.\n *\n * Matching rules (the request Host's port is stripped first and\n * comparison is case-insensitive):\n *\n * - `\"example.com\"` — matches exactly that host.\n * - `\".example.com\"` (leading dot) — matches `example.com` and every\n * subdomain (`api.example.com`).\n * - IPv6 entries may be written with or without brackets\n * (`\"[::1]\"` / `\"::1\"`).\n *\n * Mirrors `Config::allowed_hosts` in `crates/zfb/src/config.rs`.\n */\n allowedHosts?: string[];\n /** JSX framework runtime. Default: `preact`. */\n framework?: Framework;\n /** Content collections. Mirrors the JSON form one-for-one. */\n collections?: CollectionDef[];\n /** Tailwind options; absent = defaults. */\n tailwind?: TailwindConfig;\n /**\n * Prefetch options. When `disabled: true`, the build emits a meta tag\n * that the runtime's prefetch-core module reads at init time to skip\n * all prefetch wiring. Mirrors `Config::prefetch` in\n * `crates/zfb/src/config.rs`.\n */\n prefetch?: PrefetchConfig;\n /**\n * Minify production HTML output from `zfb build`. Default: `false`.\n *\n * The implementation is Rust-only and does not spawn a Node.js minifier\n * subprocess. The first version is intentionally conservative: rendered\n * `.html` pages are candidates, source `.html` passthrough pages remain\n * verbatim, and non-HTML outputs are skipped.\n *\n * Mirrors `Config::minify_html` in `crates/zfb/src/config.rs`.\n */\n minifyHtml?: boolean;\n /**\n * Raise broken-link diagnostics to errors during `zfb build`, failing the\n * build (exit non-zero) instead of merely warning. Default: `false`.\n *\n * This is the effective boolean the CLI's `--strict-broken` /\n * `--no-strict-broken` tri-state resolves against. Precedence: explicit\n * CLI flag > this config field > default `false`.\n *\n * Force-enable semantics: if `markdown.features.linkValidation` is absent\n * entirely, enabling this force-enables link validation with its\n * defaults — a strict flag that silently did nothing on a bare project\n * would be a footgun.\n *\n * Scope: the `linkValidation` mechanism only. The separate\n * `resolveMarkdownLinks.onBrokenLinks` mechanism keeps its own knob and is\n * not affected by this field.\n *\n * Build-only: it does not affect `zfb dev`.\n *\n * Mirrors `Config::strict_broken_links` in `crates/zfb/src/config.rs`.\n */\n strictBrokenLinks?: boolean;\n /**\n * Fail `zfb build` (exit non-zero) when a content-collection `.md`/`.mdx`\n * entry falls back to `<pre data-zfb-content-fallback>` because its\n * compiled JSX does not parse. Default: `false`.\n *\n * This is the effective boolean the CLI's `--strict-content-bridge` /\n * `--no-strict-content-bridge` tri-state resolves against. Precedence:\n * explicit CLI flag > this config field > default `false`.\n *\n * Unlike `strictBrokenLinks`, there is no adjacent feature to\n * force-enable: the content-bridge gate always runs for every compiled\n * collection entry.\n *\n * Build-only: it does not affect `zfb dev` — dev keeps warning and\n * serving the fallback shape.\n *\n * Mirrors `Config::strict_content_bridge` in `crates/zfb/src/config.rs`.\n */\n strictContentBridge?: boolean;\n /**\n * Bundler options. `bundle.exclude` lists project-relative globs of\n * source files to keep out of the esbuild graph (e.g.\n * `[\"components/*.stories.tsx\"]`) — see {@link BundleConfig.exclude} for\n * why this is needed. Unset → byte-identical to a build without the knob.\n * Mirrors `Config::bundle` in `crates/zfb/src/config.rs`.\n */\n bundle?: BundleConfig;\n /** User-supplied plugins. */\n plugins?: PluginConfig[];\n /**\n * Deploy-target adapter package name. Omit (or `\"none\"`) for a pure\n * static build — any route exporting `prerender = false` is then a\n * hard build error. A package name like\n * `\"@takazudo/zfb-adapter-cloudflare\"` selects the matching adapter,\n * and `zfb build` invokes that package's bin to wrap the SSR bundle\n * into a deploy-ready entry (e.g. `dist/_worker.js` for Cloudflare\n * Workers Static Assets, Pages-compatible).\n *\n * Mirrors `Config::adapter` in crates/zfb/src/config.rs.\n */\n adapter?: string;\n /**\n * Strip `.md` / `.mdx` from internal `<a href>` paths during MDX\n * compilation, and append a trailing `/` so the resulting URL shape\n * converges with the rest of the site (mirrors the JS engine's\n * `rehypeStripMdExtension`). Default: `false`.\n *\n * Enable this when content authors hand-write `[label](other.md)`\n * style references that should resolve to the rendered route URL\n * (e.g. `other/`) instead of a literal file path. Built dist and\n * `pnpm dev` honour the same flag, so previews match shipped output.\n *\n * Mirrors `Config::strip_md_ext` in crates/zfb/src/config.rs.\n */\n stripMdExt?: boolean;\n\n /**\n * Public URL prefix mounted in front of every absolute HTML asset\n * URL the build emits — `<link rel=\"stylesheet\">`, `<script type=\"module\">`,\n * and any other `/assets/...`-prefixed reference rewritten by the\n * production asset pipeline.\n *\n * Use this when the site is deployed under a sub-path (e.g.\n * `https://example.com/pj/zudo-doc/`) instead of the domain root.\n * With `base: \"/pj/zudo-doc/\"` the dist HTML emits\n * `<link rel=\"stylesheet\" href=\"/pj/zudo-doc/assets/styles-<hash>.css\">`\n * instead of the unprefixed `/assets/styles-<hash>.css`.\n *\n * Accepted shapes (all normalised to a single canonical form\n * internally):\n *\n * - omitted / `undefined` / `\"\"` / `\"/\"` — no prefix; behaviour is\n * byte-identical to the pre-`base` build (root-mounted site).\n * - leading-and-trailing-slash path like `\"/pj/zudo-doc/\"` — prefix\n * that path onto every asset URL.\n * - absolute URL like `\"https://cdn.example.com/\"` — emit absolute\n * URLs (CDN-hosted assets).\n *\n * Inputs missing a leading or trailing `/` are normalised at config-\n * load time (paths) or asset-emit time (URL prefixes); callers do\n * not have to pre-trim.\n *\n * Mirrors `Config::base` in crates/zfb/src/config.rs.\n */\n base?: string;\n\n /**\n * Canonical origin URL for the site (e.g. `\"https://example.com\"`).\n *\n * When set, the bundler emits `globalThis.__zfb.site = <value>` in\n * `entry.mjs` so layouts can build canonical `<link>` tags,\n * OpenGraph `og:url` meta, sitemap absolute hrefs, and hreflang\n * `<link rel=\"alternate\">` from a single config-level source of truth.\n *\n * **Distinct from `base`**: `base` is a sub-path mount prefix used\n * for asset URLs (e.g. `\"/pj/my-site/\"`). `site` is the full\n * canonical origin (scheme + host, no path) used to construct\n * absolute page URLs for SEO/social metadata. Both may be set\n * simultaneously.\n *\n * Accepted shape: an absolute HTTP or HTTPS URL. Relative URLs,\n * non-HTTP(S) schemes, and empty strings are rejected at config-load\n * time. Trailing slash normalisation is the consumer's responsibility.\n *\n * When absent, `globalThis.__zfb.site` is not emitted — the build\n * output is byte-for-byte identical to builds without this field.\n *\n * Mirrors `Config::site` in crates/zfb/src/config.rs.\n */\n site?: string;\n\n /**\n * Markdown link resolver (port of `remarkResolveMarkdownLinks`).\n *\n * When `enabled: true`, the build appends `ResolveLinksPlugin` to the\n * mdast pipeline so author-written `[label](./other.mdx)` links are\n * rewritten to the corresponding rendered route URL — bypassing the\n * file→directory transformation that breaks relative paths in dist\n * HTML when `foo.mdx` becomes `foo/index.html`. Extensionless\n * (`./other`) and directory-style (`other/`) targets resolve too,\n * probing `{name}.mdx`, `{name}.md`, `{name}/index.mdx`,\n * `{name}/index.md` in that order. Relative targets resolve from the\n * source file's directory; for a directory-style link written from a\n * non-index page against its rendered URL — which sits one directory\n * deeper, e.g. `../sibling/` from `section/article.mdx` — a URL-space\n * fallback retries the probe from the page's route directory when\n * every file-space candidate misses.\n *\n * Two ways to specify the source dirs:\n *\n * - **Single dir (legacy):** set `docsDir` and the build assumes the\n * `/docs/` route prefix. Convenient for single-locale projects.\n * - **Multi dir (`dirs` non-empty):** explicit `{ dir, routePrefix }`\n * entries — required for any project with locale mirrors (e.g.\n * `docs/` AND `docs-ja/`) so each dir maps to its own route prefix\n * (`/docs/` vs `/ja/docs/`). When `dirs` is non-empty, `docsDir`\n * is ignored.\n *\n * Mirrors `Config::resolve_markdown_links` in crates/zfb/src/config.rs.\n */\n resolveMarkdownLinks?: ResolveMarkdownLinksConfig;\n\n /**\n * Whether the basePath rewriter should append a trailing `/` to\n * extensionless absolute hrefs (`<a href=\"/docs/foo\">` becomes\n * `<a href=\"/pj/zudo-doc/docs/foo/\">` when `base = \"/pj/zudo-doc/\"`\n * and this is `true`).\n *\n * Off by default — preserves byte-for-byte parity with the\n * pre-`trailingSlash` build for projects that haven't opted in.\n * Enable when the deploy target serves canonical URLs with trailing\n * slashes (Cloudflare Pages with `trailingSlash: always`, Netlify\n * pretty URLs, etc.) so the dist HTML doesn't ship non-canonical\n * hrefs that 301-redirect on every click.\n *\n * Only the trailing slash for extensionless hrefs is affected.\n * Hrefs that already end in `/`, that have a file extension\n * (`.png`, `.pdf`, …), or that opt out via `data-no-base` pass\n * through unchanged.\n *\n * Mirrors `Config::trailing_slash` in crates/zfb/src/config.rs.\n */\n trailingSlash?: boolean;\n\n /**\n * Markdown / MDX parsing options. Currently the only knob exposed is\n * [`gfm`](MarkdownConfig.gfm), which toggles GFM constructs\n * (strikethrough, table, autolink-literal, task-list-item,\n * footnote-definition) on or off.\n *\n * Mirrors `Config::markdown` in crates/zfb/src/config.rs.\n */\n markdown?: MarkdownConfig;\n\n /**\n * Extra absolute filesystem paths watched by the dev server in\n * addition to the project-root tree.\n *\n * Use this when project content reads from outside the project root\n * (a sibling knowledge-base repo, a shared filesystem directory, a\n * `file:` dep that ships content alongside code, etc.) and you want\n * `zfb dev` to live-reload when those external files change.\n *\n * Semantics:\n *\n * - Each entry MUST be an absolute path. Relative paths are\n * rejected at config-load time with a clear error message.\n * - Paths are canonicalised when the watcher boots; events match\n * the canonical form.\n * - A path that does NOT exist at boot is skipped with a warning;\n * the watcher does NOT re-watch the path if it appears later.\n * Restart `zfb dev` after creating the path.\n * - Each entry is watched recursively.\n * - Events from outside the project root bypass fine-grained graph\n * classification and may trigger a broader rebuild than equivalent\n * in-tree edits.\n *\n * **Security note:** opt-in only — do NOT point this at unbounded\n * directories like `$HOME` or `/`. On Linux the recursive watcher\n * registers every subdirectory and can hit the inotify\n * `max_user_watches` ceiling on large trees.\n *\n * Mirrors `Config::extra_watch_paths` in crates/zfb/src/config.rs.\n */\n extraWatchPaths?: string[];\n\n /**\n * Whether `zfb build` writes the post-build route manifest to disk\n * at `<outDir>/__zfb/routes.json` (#347).\n *\n * The on-disk file mirrors the in-memory `ctx.routes` shape that the\n * plugin API hands to `postBuild` hooks — same fields, same\n * url-sorted order — so any consumer script wired into `pnpm build`\n * can read the manifest without writing a zfb plugin. The plugin\n * `ctx.routes` and the on-disk `routes.json` are two access shapes\n * over the same data, not two contracts.\n *\n * Default: emit (`undefined` is treated as `true`). Set `false` to\n * skip the write — useful for projects that strip everything but\n * shipped assets out of `dist/` before deploy.\n *\n * Mirrors `Config::emit_routes_manifest` in crates/zfb/src/config.rs.\n */\n emitRoutesManifest?: boolean;\n\n /**\n * Syntect code-highlight options; absent = default theme\n * (`base16-ocean.dark`) and inline color mode. See\n * {@link CodeHighlightConfig} for accepted theme names, custom-theme\n * loading, and the class-emission mode (Highlight Tokens epic).\n *\n * Mirrors `Config::code_highlight` in crates/zfb/src/config.rs.\n */\n codeHighlight?: CodeHighlightConfig;\n\n /**\n * Maximum seconds a single plugin lifecycle hook (preBuild, postBuild,\n * setup, etc.) may run before the build fails with a diagnostic error\n * and the plugin host is force-killed.\n *\n * Absent falls through to the `ZFB_PLUGIN_HOOK_TIMEOUT` env var, then\n * the 120s built-in default. Set this when your plugins do long but\n * bounded work (e.g. large sitemap generation) and you want a tighter\n * or more explicit budget.\n *\n * Mirrors `Config::plugin_hook_timeout_secs` in crates/zfb/src/config.rs.\n */\n pluginHookTimeoutSecs?: number;\n\n /**\n * Whether `copy_public_dir` copies `public/` under the `base`\n * sub-path segment (`true`, default) or flat to the `dist/` root\n * (`false`).\n *\n * - **`true` (default):** files land at\n * `<outDir>/<base-segment>/<rel>`, matching the base-prefixed URLs\n * that `withBase()` emits in the rendered HTML. Use this for\n * projects served directly at their configured sub-path.\n * - **`false`:** files land flat at `<outDir>/<rel>` regardless of\n * `base`. Use this when the deploy pipeline relocates the entire\n * `dist/` tree into the base segment itself (e.g.\n * `cp -a dist/. deploy-root/pj/site/`), so putting the files under\n * `<outDir>/<base>/...` would result in a double-nested path.\n *\n * **Note on `zfb preview`:** with `false`, base-prefixed public-asset\n * URLs 404 under `zfb preview` because the flat copy lives at the\n * dist root and `zfb preview` does not simulate deploy-side\n * relocation. This is a known trade-off of the flat-copy deploy\n * scheme.\n *\n * Mirrors `Config::copy_public_with_base` in crates/zfb/src/config.rs.\n */\n copyPublicWithBase?: boolean;\n\n /**\n * Opt into `notify`'s poll-based watch backend for the dev server's\n * watchers instead of the OS-native backend (FSEvents on macOS,\n * inotify on Linux, ...).\n *\n * Use this as a fallback when the native backend is unavailable or\n * unreliable on the host (network-mounted project directories, some\n * CI/sandboxed containers) — the poll backend re-scans the watched\n * roots on an interval instead of relying on OS filesystem-change\n * notifications.\n *\n * Default: `false` (native backend). See\n * {@link watchPollIntervalMs} for the re-scan cadence.\n *\n * Mirrors `Config::watch_poll_fallback` in crates/zfb/src/config.rs.\n */\n watchPollFallback?: boolean;\n\n /**\n * Re-scan interval, in milliseconds, for the poll watch backend. Only\n * takes effect when {@link watchPollFallback} is `true`.\n *\n * Validated at config-load time: must be between `50` and `10000`\n * (inclusive) — values outside that range are rejected (too low\n * busy-loops the poll thread; too high makes hot-reload feel broken).\n * A value below `100` is accepted but logs a warning (elevated\n * re-scan CPU cost on large trees). Setting this WITHOUT\n * `watchPollFallback: true` is accepted and dormant, with a logged\n * warning rather than an error — a preset may pre-stage the interval\n * ahead of a project enabling the fallback itself.\n *\n * Absent falls through to the built-in 500ms default, applied by the\n * consuming command.\n *\n * Mirrors `Config::watch_poll_interval_ms` in crates/zfb/src/config.rs.\n */\n watchPollIntervalMs?: number;\n\n /**\n * Project output mode. Drives the V8-mode decision the build engine\n * makes right after the no-SSR-without-adapter precondition check\n * (sub-task 4.1b / issue #373):\n *\n * - `\"static\"` — declare a pure-static (SSG-only) project. Errors at\n * build start if any route exports `prerender = false`, pointing\n * at the offending route. Use this on projects that must never\n * accidentally pick up an SSR route as a result of a copy-paste.\n * - `\"hybrid\"` — declare a project that may host SSR routes. V8-on\n * regardless of detection, even when no `prerender = false` route\n * currently exists. Useful for projects that will add SSR routes\n * later and want a stable build topology in the meantime.\n * - `\"auto\"` (default) — detection-driven. Non-empty `prerender =\n * false` route set => V8-on; empty => V8-off.\n *\n * Today's load-bearing role is the `\"static\"` precondition check.\n * The V8-off branch does NOT skip V8 host startup on the shipping\n * `zfb` binary — SSG still needs V8 to render pages. The flag exists\n * as infrastructure for the future shipping path (Tauri sidecar /\n * standalone SSR server). See the\n * [Build engine docs](https://github.com/Takazudo/zudo-front-builder/blob/main/docs/src/content/docs/architecture/build-engine.mdx)\n * for the gate decision table.\n *\n * Mirrors `Config::output` in crates/zfb/src/config.rs.\n */\n output?: OutputMode;\n\n /**\n * Config presets to merge before validation (#1196).\n *\n * Each preset is a partial `ZfbConfig`-shaped object. The merge pass runs\n * BEFORE field validation and folds preset contributions using additive\n * semantics:\n *\n * - **Array fields** (`plugins`, `collections`, `extraWatchPaths`,\n * `allowedHosts`): preset values are prepended so the main config's\n * entries retain their relative position after the preset's.\n * - **Scalar / optional fields**: a preset value fills in only when the\n * main config leaves the field at its default — the main config is\n * authoritative; presets act as defaults.\n *\n * Nested `presets` inside a preset are NOT recursively expanded.\n *\n * Mirrors `Config::presets` in crates/zfb/src/config.rs.\n */\n presets?: Partial<ZfbConfig>[];\n};\n\n/**\n * Project output mode.\n *\n * - `\"static\"` — pure-static (SSG-only); errors on detected SSR routes.\n * - `\"hybrid\"` — may host SSR routes; V8-on regardless of detection.\n * - `\"auto\"` — detection-driven; the default.\n *\n * Mirrors `OutputMode` in crates/zfb/src/config.rs.\n */\nexport type OutputMode = \"static\" | \"hybrid\" | \"auto\";\n\n/**\n * Syntect code-highlight options.\n *\n * Unknown theme names are rejected at build start with a clear error\n * rather than silently falling back.\n *\n * **Single-theme mode** (the default): set `theme` to a syntect theme name,\n * or omit it to use the default (`\"base16-ocean.dark\"`). Tokens are colored\n * with inline `color:`.\n *\n * **Dual-theme mode**: set both `themeLight` and `themeDark`. Tokens are\n * colored with CSS custom properties (`--shiki-light` / `--shiki-dark`),\n * and the consumer applies a `light-dark()` rule to pick the active color.\n * The `<pre>` element carries `class=\"syntect-dual\"` and\n * `--shiki-light-bg` / `--shiki-dark-bg` in its `style` attribute.\n *\n * `theme` and the dual pair are mutually exclusive. Setting only one of\n * `themeLight` / `themeDark` is an error.\n *\n * All theme names are **SYNTECT** built-in or user-loaded names (e.g.\n * `\"base16-ocean.light\"`, `\"base16-ocean.dark\"`, `\"InspiredGitHub\"`,\n * `\"Solarized (dark)\"`), NOT Shiki names like `\"dracula\"`.\n *\n * **Class mode** (Highlight Tokens epic, zfb#1528): set `mode: \"class\"`.\n * Each token gets a semantic role class instead of an inline color, so\n * highlight colors become re-themeable CSS design tokens. Mutually\n * exclusive with `theme` / `themeLight` / `themeDark` / `themesDir` —\n * themes don't affect class emission, so setting both is a build error.\n *\n * Mirrors `CodeHighlightConfig` in crates/zfb/src/config.rs.\n */\nexport type CodeHighlightConfig = {\n /**\n * Syntect built-in or user-loaded theme name. When absent the\n * pipeline defaults to `\"base16-ocean.dark\"`.\n *\n * Mutually exclusive with {@link themeLight} / {@link themeDark}.\n * Must be a SYNTECT theme name (e.g. `\"InspiredGitHub\"`), NOT a Shiki name.\n */\n theme?: string;\n /**\n * Path to a directory of `.tmTheme` files, relative to the project\n * root. Every `.tmTheme` file in the directory is loaded and becomes\n * available by its declared `name` via {@link theme}, {@link themeLight},\n * or {@link themeDark}. When absent only syntect's bundled themes are\n * available.\n *\n * The path must be relative and must not escape the project root via\n * `..`. A missing directory is reported as an error at build start.\n *\n * Applies to both single-theme and dual-theme mode.\n */\n themesDir?: string;\n /**\n * Light-mode syntect theme name for dual-theme highlighting.\n *\n * Must be set together with {@link themeDark} — setting only one of\n * the two is a build error. When both are set, tokens are colored with\n * CSS custom properties (`--shiki-light` / `--shiki-dark`) instead of\n * inline `color:`. Mutually exclusive with {@link theme}.\n *\n * Must be a SYNTECT theme name (e.g. `\"base16-ocean.light\"`),\n * NOT a Shiki name like `\"dracula\"`.\n */\n themeLight?: string;\n /**\n * Dark-mode syntect theme name for dual-theme highlighting.\n *\n * Must be set together with {@link themeLight} — setting only one of\n * the two is a build error. Mutually exclusive with {@link theme}.\n *\n * Must be a SYNTECT theme name (e.g. `\"base16-ocean.dark\"`),\n * NOT a Shiki name like `\"dracula\"`.\n */\n themeDark?: string;\n /**\n * Output mode for fenced-code highlighting (Highlight Tokens epic,\n * zfb#1528). `\"inline\"` (default) bakes per-token colors into\n * `style=\"color:#rrggbb\"` (or the dual `--shiki-*` custom properties).\n * `\"class\"` emits a semantic role class per token instead, so colors\n * become re-themeable CSS design tokens rather than baked-in HTML.\n *\n * Mutually exclusive with {@link theme} / {@link themeLight} /\n * {@link themeDark} / {@link themesDir} — themes don't affect class\n * emission, so setting both is rejected rather than silently ignoring\n * the theme.\n */\n mode?: CodeHighlightMode;\n /**\n * Class-name prefix for class-mode role classes (e.g. the default\n * `\"hi-\"` yields `hi-kw`, `hi-str`, ...). Must match\n * `/^[A-Za-z][A-Za-z0-9_-]*$/`. Only meaningful when {@link mode} is\n * `\"class\"`. Default: `\"hi-\"`.\n */\n classPrefix?: string;\n /**\n * Per-role class overrides for class mode, e.g.\n * `{ keyword: \"text-violet-600 dark:text-violet-400\" }` to map a role\n * onto Tailwind utilities instead of the default `{classPrefix}{role}`\n * class. Keys must be one of the 18 fixed role names (see\n * {@link CodeHighlightRole}); a value may hold multiple\n * space-separated classes and must not contain the bare token `\"line\"`\n * (collides with the code-enrichment line wrapper class). Absent uses\n * `{classPrefix}{role}` for every role.\n *\n * Setting this while `tailwind.enabled` is `false` (the authored-CSS\n * path) is allowed but emits a build warning — no Tailwind safelist can\n * be generated on that path, so the mapped utilities must already exist\n * in your own CSS.\n */\n roleClasses?: Partial<Record<CodeHighlightRole, string>>;\n /**\n * Whether to inject the built-in `--zfb-hi-*` token stylesheet\n * (`zfb-hi.css`) into the combined `styles.css` output. Only meaningful\n * in class mode. Default: `true`.\n */\n defaultStylesheet?: boolean;\n};\n\n/**\n * `codeHighlight.mode` — see {@link CodeHighlightConfig.mode}.\n *\n * Mirrors `CodeHighlightMode` in crates/zfb/src/config.rs.\n */\nexport type CodeHighlightMode = \"inline\" | \"class\";\n\n/**\n * The fixed 18-role semantic taxonomy for class-mode syntax highlighting\n * (Highlight Tokens epic, zfb#1528) — valid {@link CodeHighlightConfig.roleClasses}\n * keys.\n *\n * Mirrors `CODE_HIGHLIGHT_ROLES` in crates/zfb/src/config.rs.\n */\nexport type CodeHighlightRole =\n | \"escape\"\n | \"operator\"\n | \"comment\"\n | \"string\"\n | \"number\"\n | \"constant\"\n | \"keyword\"\n | \"function\"\n | \"type\"\n | \"namespace\"\n | \"property\"\n | \"variable\"\n | \"tag\"\n | \"attribute\"\n | \"punctuation\"\n | \"inserted\"\n | \"deleted\"\n | \"heading\";\n\n/**\n * Table-of-contents options. Wire via `markdown.toc` in `zfb.config.ts`.\n *\n * When present, a TOC `<ul>/<li>` list is inserted as the next sibling\n * of the first heading whose text matches `heading` (case-insensitive).\n * Each `<a href=\"#id\">` links to the deduplicated `id` that\n * `HeadingLinksPlugin` placed on the corresponding heading.\n *\n * Mirrors `TocConfig` in `crates/zfb-content/src/plugins/toc.rs`.\n */\nexport type TocConfig = {\n /**\n * Heading text that triggers TOC insertion. Matched\n * case-insensitively after whitespace trimming. Default: `\"TOC\"`.\n */\n heading?: string;\n\n /**\n * Number of heading levels to include starting from `<h2>`.\n *\n * - `1` — h2 only\n * - `2` (default) — h2 + h3\n * - `3` — h2, h3, h4\n * - …up to `5` (h2 through h6)\n */\n maxDepth?: number;\n};\n\n/**\n * Markdown / MDX parsing options.\n *\n * See [`ZfbConfig.markdown`] for the embed point. Fields: [`gfm`],\n * [`toc`], [`externalLinks`], [`cjkFriendly`], and [`features`].\n * Future markdown knobs would also live here.\n *\n * See the \"Markdown Features\" docs category for the per-feature option\n * reference once individual features are ported.\n *\n * Mirrors `MarkdownConfig` in crates/zfb/src/config.rs.\n */\nexport type MarkdownConfig = {\n /**\n * Enable GFM constructs.\n *\n * Accepts three shapes:\n *\n * - `true` — turn every GFM construct ON (strikethrough, table,\n * autolink-literal, task-list-item, footnote-definition).\n * - `false` — turn every GFM construct OFF.\n * - partial object — set individual fields explicitly; fields you\n * omit fall back to the conservative-default values described\n * below.\n *\n * When `markdown` itself is omitted entirely, the conservative\n * default applies: `strikethrough: true`, `table: true`,\n * `autolinkLiteral: true`, task lists and footnotes off. Those three\n * are the constructs GFM-accustomed authors expect without config;\n * task lists and footnotes change document structure, so they stay\n * opt-in. Projects that want the full GFM surface should opt in with\n * `gfm: true`.\n */\n gfm?: GfmFlag;\n\n /**\n * Table-of-contents options. When present, a `<ul>/<li>` list is\n * inserted after the first heading whose text matches `heading`\n * (default `\"TOC\"`, case-insensitive). Each link points to the\n * deduplicated `id` that `HeadingLinksPlugin` placed on the heading.\n *\n * Omitting this field entirely leaves the build byte-for-byte identical\n * to the pre-TOC build. See [`TocConfig`] for the available options.\n *\n * Mirrors `MarkdownConfig::toc` in crates/zfb/src/config.rs.\n */\n toc?: TocConfig;\n /**\n * External-link rewriter. When set, every `<a>` whose href is\n * classified as external receives the configured `target` and `rel`\n * attributes.\n *\n * An href is external when it is an absolute HTTP/HTTPS URL AND its\n * origin differs from the top-level `site` URL (if `site` is\n * configured). When `site` is absent, any absolute HTTP/HTTPS URL is\n * treated as external.\n *\n * `mailto:`, `tel:`, and other non-HTTP(S) schemes are always left\n * unchanged. Relative URLs (`/internal/`, `./file.mdx`, `#anchor`) are\n * always internal.\n *\n * Omitting this field keeps the output byte-for-byte identical to the\n * pre-feature behaviour.\n *\n * Mirrors `ExternalLinksConfig` in crates/zfb/src/config.rs.\n */\n externalLinks?: ExternalLinksConfig;\n\n /**\n * Enable CJK-friendly markdown handling.\n *\n * Governs two post-parse fixups that adapt CommonMark/GFM rules to CJK\n * text:\n *\n * 1. **Emphasis/strong flanking** (`CjkFriendlyPlugin`). CommonMark's\n * left-/right-flanking delimiter-run rules treat CJK characters as\n * non-whitespace non-punctuation, which causes `**foo**` adjacent to\n * CJK text (e.g. `**テスト。**テスト`) to render as literal stars\n * instead of `<strong>`.\n * 2. **Bare-URL autolink boundary** (`CjkAutolinkBoundaryPlugin`,\n * zfb#1105). The GFM autolink-literal path grammar terminates only on\n * ASCII whitespace, so a bare URL flush against CJK text\n * (`詳細はhttps://example.com参照`) swallows the trailing CJK run into\n * the `href`. This fixup terminates the link at the first CJK\n * character. Only active when `gfm.autolinkLiteral` is also on.\n *\n * - **absent / `true` (default):** CJK-friendly handling is on.\n * Preserves today's behaviour — existing CJK-content sites are\n * unaffected.\n * - **`false`:** opt-out. Neither plugin is added to the pipeline;\n * emphasis markers and bare-URL autolinks adjacent to CJK characters\n * follow base CommonMark/GFM rules. Rarely the right choice; provided\n * as an escape hatch for projects that need strict CommonMark/GFM\n * output.\n *\n * **GFM strikethrough** (`~~foo~~`) at CJK boundaries is unaffected\n * by this toggle — it is handled by markdown-rs's GFM tokeniser, not\n * by these plugins, and works correctly in both modes.\n *\n * Mirrors `MarkdownConfig::cjk_friendly` in crates/zfb/src/config.rs.\n */\n cjkFriendly?: boolean;\n\n /**\n * Convert every soft line break (a single `\\n` inside a paragraph) into\n * `<br>` (remark-breaks parity).\n *\n * - **absent / `false` (default):** soft line breaks follow standard\n * CommonMark behaviour — collapsed into a single space.\n * - **`true`:** every `\\n` inside a paragraph becomes `<br>`. Use this\n * when your content relies on newline→`<br>` fidelity (e.g. product\n * descriptions, lyrics, or other newline-sensitive prose).\n *\n * Mirrors `MarkdownConfig::hard_breaks` in crates/zfb/src/config.rs.\n */\n hardBreaks?: boolean;\n\n /**\n * Per-feature markdown pipeline toggles.\n *\n * Each field is a [`FeatureToggle`] (`true` / `false` / options object)\n * or a feature-specific config type (for features that require extra\n * parameters). Absent / `undefined` means all features are disabled,\n * preserving the behaviour of the pre-features build byte-for-byte.\n *\n * Unknown keys are rejected at deserialization time by the Rust loader\n * so a typo in `zfb.config.ts` surfaces as a clear error.\n *\n * Mirrors `MarkdownFeaturesConfig` in crates/zfb/src/config.rs.\n */\n features?: MarkdownFeaturesConfig;\n};\n\n/**\n * Per-feature toggle: `boolean` shorthand or an options object.\n *\n * `true` enables the feature with defaults; `false` (or absent) disables it.\n * The object form carries per-feature options (fields vary by feature and\n * are filled in by each feature's port sub-issue — stubs today).\n *\n * Mirrors `FeatureToggle` in crates/zfb/src/config.rs.\n */\nexport type FeatureToggle = boolean | FeatureOptions;\n\n/**\n * Empty options object for features that accept `{ ... }` but have no\n * user-facing knobs yet. Fields are filled in by each feature's port\n * sub-issue; this stub satisfies the schema shape requirement.\n *\n * Mirrors `FeatureOptions` in crates/zfb/src/config.rs.\n */\nexport type FeatureOptions = Record<string, never>;\n\n/**\n * Options for the `codeEnrichment` feature.\n *\n * All flags default to `true` when the feature is enabled with\n * `codeEnrichment: {}` or when a field is absent.\n *\n * Mirrors `CodeEnrichmentConfig` in `crates/zfb-md-ast/src/features_config.rs`.\n */\nexport type CodeEnrichmentConfig = {\n /**\n * Enable diff-marker processing for markers such as `// [!code ++]`\n * and `// [!code --]`. Default: `true`.\n */\n diffMarkers?: boolean;\n /**\n * Enable line-highlight processing for fence ranges such as `{1,3-5}`.\n * Default: `true`.\n */\n lineHighlight?: boolean;\n /**\n * Enable visible-text word emphasis for slash-delimited fence metadata\n * such as `/answer/`. Default: `true`.\n */\n wordHighlight?: boolean;\n};\n\n/**\n * Options for the `tocExport` feature.\n *\n * Controls which headings are included in the exported `toc` JSON.\n * `maxDepth` is the **absolute** heading depth (2–6):\n * - `2` → h2 only\n * - `3` (default) → h2 + h3\n *\n * This differs from `headingMarkerToc.maxDepth`, which counts levels\n * starting from h2. The two features are independent.\n *\n * Mirrors `TocExportConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type TocExportConfig = {\n /** Maximum heading depth to include (absolute, 2–6). Default: 3. */\n maxDepth?: number;\n};\n\n/**\n * Options for the `imageDimensions` feature.\n *\n * Auto-detects and injects `width`/`height` on local `<img>` elements. Raster\n * formats are probed header-only; SVGs are read from their markup\n * (`width`/`height`/`viewBox`).\n *\n * Mirrors `ImageDimensionsConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type ImageDimensionsConfig = {\n /**\n * When `true` (the default), `http://` and `https://` image sources are\n * silently skipped and not probed for dimensions. Set to `false` only for\n * testing or unusual setups — remote images require network access at build\n * time and slow the pipeline.\n */\n skipRemote?: boolean;\n};\n\n/**\n * Options for the `linkValidation` feature.\n *\n * Validates internal `[text](file.md#anchor)` and `[text](#anchor)` links at\n * build time. External URLs (`http://`, `https://`, `mailto:`) are always\n * skipped — network validation is out of scope.\n *\n * Mirrors `LinkValidationConfig` in `crates/zfb-md-ast/src/features_config.rs`.\n */\nexport type LinkValidationConfig = {\n /**\n * When `true`, broken links are reported as errors (build can fail).\n * Default: `false` (warn-only).\n */\n failOnBroken?: boolean;\n};\n\n/**\n * Options for the `transclude` feature.\n *\n * Enables `:::include{file=\"./path.md\"}` directives that inline another\n * file's parsed mdast at the include site.\n *\n * Mirrors `TranscludeConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type TranscludeConfig = {\n /**\n * Maximum transclusion depth (chain length A→B→C→…).\n *\n * A depth of `1` allows only direct includes (the included file itself\n * cannot include further files). Default: `5`. A cycle (A→B→A) is\n * always detected regardless of `maxDepth` and treated as an error.\n */\n maxDepth?: number;\n};\n\n/**\n * Options for the `readingTime` feature.\n *\n * Mirrors `ReadingTimeOptions` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type ReadingTimeConfig = {\n /** Words-per-minute rate for the reading-time estimate. Default: 200. */\n wpm?: number;\n};\n\n/**\n * `readingTime` feature value: either a `boolean` shorthand or a\n * {@link ReadingTimeConfig} options object.\n *\n * Mirrors `ReadingTimeFeature` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type ReadingTimeFeature = boolean | ReadingTimeConfig;\n\n/**\n * Per-feature markdown pipeline configuration.\n *\n * All fields are optional; absent = feature disabled, behaviour unchanged\n * from the pre-features build. Unknown keys are rejected at deserialization\n * time by the Rust loader so a typo surfaces as a clear error.\n *\n * Mirrors `MarkdownFeaturesConfig` in crates/zfb/src/config.rs.\n */\nexport type MarkdownFeaturesConfig = {\n /** GitHub-style alert blocks (`> [!NOTE]`, `> [!WARNING]`, etc.). */\n githubAlerts?: FeatureToggle;\n\n /**\n * Reading-time estimate injected into the document frontmatter.\n * Accepts `true` / `false` shorthand or `{ wpm: N }` for a custom rate.\n */\n readingTime?: ReadingTimeFeature;\n\n /** Code-block enrichment (copy button, language label, etc.). */\n codeEnrichment?: CodeEnrichmentConfig;\n\n /** Grouped code blocks rendered as tabs. */\n codeTabs?: FeatureToggle;\n\n /** Ruby annotation support (`{base}^{ruby}` syntax). */\n ruby?: FeatureToggle;\n\n /** Export the page TOC as structured data (e.g. for sidebar rendering). */\n tocExport?: TocExportConfig;\n\n /** Auto-detect and inject `width`/`height` on `<img>` elements. */\n imageDimensions?: ImageDimensionsConfig;\n\n /**\n * Validate internal links (file-relative paths and anchor fragments) at\n * build time. External URLs are always skipped — network validation is\n * out of scope.\n */\n linkValidation?: LinkValidationConfig;\n\n /**\n * Transclusion of other markdown/MDX files via\n * `:::include{file=\"./path.md\"}` — NOT the Obsidian `[[path]]` wikilink\n * syntax.\n */\n transclude?: TranscludeConfig;\n\n /**\n * Generic `:::name` → component map. You supply the components; no defaults\n * are registered. Keys are directive names (e.g. `\"foo\"`), values are\n * {@link DirectiveSpec} (bare component name string or options object).\n *\n * Mirrors `directives` in `MarkdownFeaturesConfig` in crates/zfb/src/config.rs.\n */\n directives?: Record<string, DirectiveSpec>;\n\n /** Mermaid diagram rendering. */\n mermaid?: FeatureToggle;\n\n /**\n * Inline heading-marker TOC. Accepts either a `boolean` shorthand\n * (`true` = enable with defaults, `false` = disable) or a full\n * {@link TocConfig} options object — same union shape as the Rust\n * `HeadingMarkerTocFeature` enum.\n */\n headingMarkerToc?: HeadingMarkerTocFeature;\n\n /**\n * Heading-ID strategy for the always-on `HeadingLinks` plugin.\n * Absent → `\"flat\"` (the long-standing github-slugger scheme).\n * `{ strategy: \"hierarchical\" }` opts into ancestor-prefixed anchor\n * IDs (`## Foo` / `### Moo` / `#### Mew` → `foo`, `foo-moo`,\n * `foo-moo-mew`) — see {@link HeadingIdsConfig}.\n */\n headingIds?: HeadingIdsConfig;\n};\n\n/**\n * Options for the `headingIds` entry in `markdown.features`.\n *\n * Configures the always-on `HeadingLinks` plugin rather than toggling an\n * opt-in feature. Note: switching to `\"hierarchical\"` is anchor-breaking\n * for existing deep links to nested headings.\n *\n * Mirrors `HeadingIdsConfig` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type HeadingIdsConfig = {\n /**\n * `\"flat\"` (default): github-slugger slugs with a per-document dedup\n * counter shared across h2–h6 (`overview`, `overview-1`, …).\n * `\"hierarchical\"`: each heading's slug is prefixed with its ancestor\n * chain and deduped on the full path — anchors become reconstructible\n * from the heading outline.\n */\n strategy?: \"flat\" | \"hierarchical\";\n};\n\n/**\n * `headingMarkerToc` feature value: either a `boolean` shorthand or a\n * full {@link TocConfig} options object.\n *\n * Mirrors `HeadingMarkerTocFeature` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type HeadingMarkerTocFeature = boolean | TocConfig;\n\n/**\n * Spec for one user-defined directive: either a bare component name string\n * or a full {@link DirectiveFullSpec} options object.\n *\n * Mirrors `DirectiveSpec` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type DirectiveSpec = string | DirectiveFullSpec;\n\n/**\n * Full options object for one user-defined directive.\n *\n * Mirrors `DirectiveFullSpec` in crates/zfb-md-ast/src/features_config.rs.\n */\nexport type DirectiveFullSpec = {\n /** JSX component identifier (e.g. `\"Spoiler\"`, `\"Kbd\"`). */\n component: string;\n /** Container/leaf/text shape. Defaults to `\"container\"` when absent. */\n kind?: \"container\" | \"leaf\" | \"text\";\n /** Whether the bracketed `[label]` becomes a `title` attribute. Defaults to `true`. */\n titleFromLabel?: boolean;\n};\n\n/**\n * Options for the external-link rewriter (port of `rehype-external-links`).\n *\n * All fields are optional; omitting a field applies the documented default.\n *\n * Mirrors `ExternalLinksConfig` in crates/zfb/src/config.rs.\n */\nexport type ExternalLinksConfig = {\n /**\n * `rel` tokens applied to external links.\n *\n * Default: `[\"noopener\", \"noreferrer\"]`.\n *\n * Tokens are deduplicated (case-insensitive) and merged with any\n * existing `rel` attribute on the `<a>` element — existing tokens\n * appear first.\n */\n rel?: string[];\n /**\n * `target` value for external links.\n *\n * Default: `\"_blank\"`.\n */\n target?: string;\n};\n\n/**\n * Either the shorthand boolean form (`true` = all GFM constructs on,\n * `false` = all off) or a partial object that toggles individual\n * constructs.\n *\n * Mirrors `GfmFlag` in crates/zfb/src/config.rs.\n */\nexport type GfmFlag = boolean | GfmConstructs;\n\n/**\n * Per-construct opt-in / opt-out for GFM. Every field is optional;\n * omitted fields fall back to the conservative default\n * (`strikethrough: true`, `table: true`, `autolinkLiteral: true`,\n * others `false`).\n *\n * Mirrors `GfmConstructs` in crates/zfb/src/config.rs.\n */\nexport type GfmConstructs = {\n /** GFM strikethrough (`~~text~~` → `<del>text</del>`). */\n strikethrough?: boolean;\n /** GFM pipe-style tables. */\n table?: boolean;\n /**\n * GFM autolink literal — bare URLs like `https://example.com` become\n * clickable links without `<…>` brackets.\n */\n autolinkLiteral?: boolean;\n /** GFM task list items (`- [x]` / `- [ ]`). */\n taskListItem?: boolean;\n /** GFM footnote definitions (`[^ref]: …`). */\n footnoteDefinition?: boolean;\n};\n\n/**\n * What to do when a `.md`/`.mdx` link cannot be resolved.\n *\n * Mirrors `OnBrokenLinks` in crates/zfb/src/config.rs.\n */\nexport type OnBrokenLinks = \"warn\" | \"error\" | \"ignore\";\n\n/**\n * Config for the markdown link resolver. See\n * [`ZfbConfig.resolveMarkdownLinks`] for the design rationale.\n */\nexport type ResolveMarkdownLinksConfig = {\n /** Whether to enable link resolution. Default: `false`. */\n enabled?: boolean;\n\n /**\n * Legacy single-dir field. Used only when [`dirs`] is empty. When\n * non-empty, scanned against the hard-coded `/docs/` route prefix.\n */\n docsDir?: string;\n\n /**\n * Explicit per-dir source map. Each entry is one collection (e.g.\n * EN docs at `src/content/docs/` → `/docs/`, JA docs at\n * `src/content/docs-ja/` → `/ja/docs/`). Takes precedence over\n * [`docsDir`] when non-empty.\n */\n dirs?: ResolveMarkdownLinksDir[];\n\n /** What to do with unresolved `.md`/`.mdx` links. Default: `\"warn\"`. */\n onBrokenLinks?: OnBrokenLinks;\n};\n\n/** One source-dir entry for [`ResolveMarkdownLinksConfig.dirs`]. */\nexport type ResolveMarkdownLinksDir = {\n /**\n * Directory (relative to project root) whose `.md`/`.mdx` files are\n * scanned. Must be relative and must not escape the root via `..`.\n */\n dir: string;\n\n /**\n * Route prefix prepended to each file's slug. Include leading and\n * trailing slashes (e.g. `\"/docs/\"` or `\"/ja/docs/\"`).\n */\n routePrefix: string;\n};\n\n/**\n * Identity helper: returns the supplied config as-is, but typed against\n * [`ZfbConfig`]. Use as the default export of `zfb.config.ts` so editors\n * surface field-level types and typos surface at compile time.\n */\nexport function defineConfig(config: ZfbConfig): ZfbConfig {\n return config;\n}\n\n/**\n * Preset authoring helper: stamps each object entry in `config.plugins`\n * with `source_package: sourcePackage` so the Rust loader can attribute\n * plugin contributions back to the preset package that provided them.\n *\n * - Only plain-object plugin entries are stamped; non-object entries pass\n * through unchanged (defensive — the current schema requires objects,\n * but this guard keeps the helper safe if the schema is ever relaxed).\n * - An entry that ALREADY carries a `source_package` is left untouched, so a\n * preset composing another `definePreset`-returned preset (by spreading its\n * `plugins`) keeps the inner preset's provenance instead of clobbering it\n * with the outer package name (the spread below lets the existing marker win).\n * - When `config.plugins` is absent, the config is returned as-is.\n * - All other fields of `config` pass through unchanged.\n *\n * The key `source_package` (snake_case) mirrors the Rust `PluginConfig`\n * serde field added in T4. `PluginConfig` has no `#[serde(rename_all)]`\n * so the serde key is the field name verbatim — do NOT use camelCase.\n *\n * SYNC REQUIREMENT: keep this implementation behaviourally identical to\n * the stub in crates/zfb-config-loader/js/zfb-config-stub.mjs, which is\n * injected at config-eval time when the user's project does not have the\n * zfb npm package installed locally.\n */\nexport function definePreset(\n sourcePackage: string,\n config: Partial<ZfbConfig>,\n): Partial<ZfbConfig> {\n if (!config.plugins) {\n return config;\n }\n return {\n ...config,\n plugins: config.plugins.map((plugin) => {\n if (plugin !== null && typeof plugin === \"object\" && !Array.isArray(plugin)) {\n // Default first, then spread the plugin so an existing `source_package`\n // (from a composed inner preset) wins over the outer package name.\n return { source_package: sourcePackage, ...plugin };\n }\n return plugin;\n }),\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zfb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Rust-built static-site engine for Astro and Next.js users — millisecond rebuilds, single binary. SDK with islands, content collections, pagination, and config helpers.",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"LICENSE"
|
|
75
75
|
],
|
|
76
76
|
"optionalDependencies": {
|
|
77
|
-
"@takazudo/zfb-darwin-arm64": "2.
|
|
78
|
-
"@takazudo/zfb-darwin-x64": "2.
|
|
79
|
-
"@takazudo/zfb-linux-arm64-gnu": "2.
|
|
80
|
-
"@takazudo/zfb-linux-x64-gnu": "2.
|
|
81
|
-
"@takazudo/zfb-win32-x64-msvc": "2.
|
|
77
|
+
"@takazudo/zfb-darwin-arm64": "2.5.1",
|
|
78
|
+
"@takazudo/zfb-darwin-x64": "2.5.1",
|
|
79
|
+
"@takazudo/zfb-linux-arm64-gnu": "2.5.1",
|
|
80
|
+
"@takazudo/zfb-linux-x64-gnu": "2.5.1",
|
|
81
|
+
"@takazudo/zfb-win32-x64-msvc": "2.5.1"
|
|
82
82
|
},
|
|
83
83
|
"publishConfig": {
|
|
84
84
|
"access": "public"
|