export type Framework = "preact" | "react"; export type CollectionDef = { /** Identifier used at the call site (e.g. `"blog"`). */ name: string; /** Directory (relative to the project root) holding the entries. */ path: string; /** Optional schema. Enforced by `zfb check`. */ schema?: Record; /** * Optional include globs (Astro-style, evaluated relative to `path`). * When set and non-empty, an entry is kept only if at least one * pattern matches its relative path. When omitted or empty, no * include-filtering happens. Patterns use the `globset` dialect * (Unix-style: `*`, `**`, `?`, `[…]`). */ include?: string[]; /** * Optional exclude globs. When set, an entry is dropped if any * pattern matches its relative path. Evaluated AFTER `include`. * Together they mirror Astro's `['**\/*.mdx', '!**\/*.en.mdx']` * convention (zfb splits the negative side into its own field). */ exclude?: string[]; /** * Optional suffix to strip from each kept entry's slug + module * specifier. Use with multi-locale layouts where one source * directory holds both `foo.mdx` (default locale) and `foo.en.mdx` * (locale override) — set `idStripSuffix: ".en"` so the EN * collection's slugs round-trip as `foo` instead of `foo.en`. */ idStripSuffix?: string; /** * Opt-in to a `path` that escapes the project root via `..` (e.g. a * monorepo-shared content dir living outside this package). Default * `false` keeps the standard project-root guard. Absolute paths and * Windows drive-relative/prefix forms are rejected regardless of * this flag — only `..`-relative escapes are relaxed. * * Security note: if this collection comes from a preset, the preset * author — not the consuming project — controls `path`. Setting * `allowOutsideRoot: true` on a preset-provided collection widens * the project's read surface to wherever that preset points, so * treat it the same as any other preset-granted filesystem access. */ allowOutsideRoot?: boolean; }; export type TailwindConfig = { /** Whether Tailwind is enabled. Default: `true`. */ enabled?: boolean; }; /** * Prefetch options. Mirrors `PrefetchConfig` in `crates/zfb/src/config.rs`. */ export type PrefetchConfig = { /** * Disable prefetch entirely. * * When `true`, the bundler emits `globalThis.__zfb.prefetchDisabled = true` * in `entry.mjs`, and `` renders * `` in ``. * The sibling prefetch-core module reads that meta tag at `init()` time * and short-circuits — no prefetch wiring runs. * * The flag is site-wide and static — set once at bundle-emit time, * never recomputed per-page. Default: `false`. */ disabled?: boolean; }; /** * Bundler options. Mirrors `BundleConfig` in `crates/zfb/src/config.rs`. */ export type BundleConfig = { /** * Project-relative glob patterns (gitignore-style) for source files * the bundler must NOT pull into the esbuild graph. * * Why this exists: an eager `import.meta.glob('components/**\/*.stories.tsx', * { eager: true })` expands to a static import of every matched file. If a * matched file imports a CJS-only package whose `package.json` resolves only * via `main`/`module` or a `require`-only `exports` condition (e.g. * `msw` → `path-to-regexp@6`), esbuild — invoked with `--platform=neutral` * for the worker bundle — rejects it with "Could not resolve … Main fields * must be configured explicitly when using the neutral platform." Listing the * offending file here keeps the migration build green. * * Each pattern is matched against the file's path RELATIVE TO THE PROJECT * ROOT, in POSIX form (e.g. `components/Foo.stories.tsx` or * `components/**\/*.stories.tsx`). A matched file is: * * - never copied/symlinked into the bundler's shadow tree, and * - dropped from any eager `import.meta.glob(...)` expansion that would * otherwise statically import it. * * Unset / empty → behaviour is byte-identical to a build without this knob: * no files are skipped. * * Mirrors `Config::bundle` in crates/zfb/src/config.rs. */ exclude?: string[]; /** * Explicit esbuild `main-fields` list for the `--platform=neutral` page/SSR * pass. Under `neutral` esbuild's main-fields list is EMPTY by default, so a * dep resolved purely via `package.json` `main`/`module` (no `exports` map) * is rejected ("The "main" field here was ignored. Main fields must be * configured explicitly when using the neutral platform."). Set e.g. * `["main", "module"]` to let such CJS-main-only deps resolve (#676 — * `msw` → `path-to-regexp@6`). Applies to every framework; unset/empty → * byte-identical to a build without the knob (the React-only `main,module` * shim still applies). * * Mirrors `BundleConfig::main_fields` in `crates/zfb/src/config.rs`. */ mainFields?: string[]; /** * Bare specifiers to mark external in the `--platform=neutral` page/SSR * pass, so esbuild leaves them unbundled instead of resolving them (the * other #676 escape hatch — externalize a CJS-only dep rather than * resolving it). Appended to the framework-provided externals. Unset/empty * → no extra externals. * * Mirrors `BundleConfig::external` in `crates/zfb/src/config.rs`. */ external?: string[]; /** * Additional esbuild loaders keyed by file extension (for example * `{ ".txt": "text" }`). Only inline loaders are supported: `file` and * `copy` are intentionally excluded because they emit sibling assets the * client bundlers do not publish. `.css`, `.module.css`, `.mdx`, and `.md` * are reserved by zfb and rejected during config validation. */ loaders?: Record; /** * Operator-authored esbuild define substitutions. Values are raw esbuild * expressions; string values must be pre-quoted JSON (for example * `{ __APP_NAME__: '"my-app"' }`). The mode-owned keys * `import.meta.env.PROD`, `import.meta.env.DEV`, and * `process.env.NODE_ENV` are reserved and rejected at config-load time. */ define?: Record; }; /** * One plugin entry in `zfb.config.ts`. * * `name` MUST be a module reference that Node's resolver can locate from * the project root. The zfb config loader * (`crates/zfb-config-loader/js/config-loader.mjs`) resolves it to an * absolute module specifier and the build / dev plugin host loads it via * dynamic `import()`: * * - `"./plugins/my-plugin.mjs"` / `"../shared/plugin.mjs"` — * path-relative to the project root (the dir containing `zfb.config.ts`). * - `"/abs/path/to/plugin.mjs"` — absolute filesystem path. * - `"@takazudo/zfb-plugin-search"` / `"my-plugin"` — npm bare specifier * resolved against the project's `node_modules`. * * Inline-function hooks are NOT supported; the plugin module's default * export must be a [`ZfbPlugin`] (see `@takazudo/zfb/plugins`). * * `options` is passed verbatim to the plugin's hook contexts; treat * the schema as plugin-specific. */ export type PluginConfig = { name: string; options?: Record; }; export type ZfbConfig = { /** Output directory for built assets. Default: `dist`. */ outDir?: string; /** Public/static directory copied verbatim. Default: `public`. */ publicDir?: string; /** Optional dev/preview server bind host. */ host?: string; /** Optional dev/preview server port. */ port?: number; /** * Host header values the dev/preview server accepts when bound to a * non-localhost interface (`--host 0.0.0.0`, the bare `--host` LAN * shortcut, or `host` above) — the DNS-rebinding guard, mirroring * Vite's `server.allowedHosts`. * * Defaults: only consulted for non-loopback binds — the default * `localhost` bind skips validation entirely. `localhost`, the * explicitly bound host, and any IP-literal Host — `127.0.0.1`, * `[::1]`, the LAN URLs the startup banner prints — are always * allowed (DNS rebinding needs a DNS name, so raw IPs are safe; * Vite parity); requests with any other Host get a 403. * * Matching rules (the request Host's port is stripped first and * comparison is case-insensitive): * * - `"example.com"` — matches exactly that host. * - `".example.com"` (leading dot) — matches `example.com` and every * subdomain (`api.example.com`). * - IPv6 entries may be written with or without brackets * (`"[::1]"` / `"::1"`). * * Mirrors `Config::allowed_hosts` in `crates/zfb/src/config.rs`. */ allowedHosts?: string[]; /** JSX framework runtime. Default: `preact`. */ framework?: Framework; /** Content collections. Mirrors the JSON form one-for-one. */ collections?: CollectionDef[]; /** Tailwind options; absent = defaults. */ tailwind?: TailwindConfig; /** * Prefetch options. When `disabled: true`, the build emits a meta tag * that the runtime's prefetch-core module reads at init time to skip * all prefetch wiring. Mirrors `Config::prefetch` in * `crates/zfb/src/config.rs`. */ prefetch?: PrefetchConfig; /** * Minify production HTML output from `zfb build`. Default: `false`. * * The implementation is Rust-only and does not spawn a Node.js minifier * subprocess. The first version is intentionally conservative: rendered * `.html` pages are candidates, source `.html` passthrough pages remain * verbatim, and non-HTML outputs are skipped. * * Mirrors `Config::minify_html` in `crates/zfb/src/config.rs`. */ minifyHtml?: boolean; /** * Raise broken-link diagnostics to errors during `zfb build`, failing the * build (exit non-zero) instead of merely warning. Default: `false`. * * This is the effective boolean the CLI's `--strict-broken` / * `--no-strict-broken` tri-state resolves against. Precedence: explicit * CLI flag > this config field > default `false`. * * Force-enable semantics: if `markdown.features.linkValidation` is absent * entirely, enabling this force-enables link validation with its * defaults — a strict flag that silently did nothing on a bare project * would be a footgun. * * Scope: the `linkValidation` mechanism only. The separate * `resolveMarkdownLinks.onBrokenLinks` mechanism keeps its own knob and is * not affected by this field. * * Build-only: it does not affect `zfb dev`. * * Mirrors `Config::strict_broken_links` in `crates/zfb/src/config.rs`. */ strictBrokenLinks?: boolean; /** * Fail `zfb build` (exit non-zero) when a content-collection `.md`/`.mdx` * entry falls back to `
` because its
     * compiled JSX does not parse. Default: `false`.
     *
     * This is the effective boolean the CLI's `--strict-content-bridge` /
     * `--no-strict-content-bridge` tri-state resolves against. Precedence:
     * explicit CLI flag > this config field > default `false`.
     *
     * Unlike `strictBrokenLinks`, there is no adjacent feature to
     * force-enable: the content-bridge gate always runs for every compiled
     * collection entry.
     *
     * Build-only: it does not affect `zfb dev` — dev keeps warning and
     * serving the fallback shape.
     *
     * Mirrors `Config::strict_content_bridge` in `crates/zfb/src/config.rs`.
     */
    strictContentBridge?: boolean;
    /**
     * Whether `zfb build` writes a JSON render artifact for every
     * markdown/MDX-backed HTML route whose rendered page contains exactly
     * one top-level content region — the content-region HTML as shipped,
     * compiler-allocated headings with slugs, a contract version, and a
     * raw-source digest (Render Artifact Export epic #2421). The extraction
     * pass and artifact writer are Rust-side
     * (`crate::commands::render_artifact::export_render_artifacts`),
     * running between the link-base rewrite and HTML minification. See the
     * [Render Artifacts docs](https://github.com/Takazudo/zudo-front-builder/blob/main/docs/src/content/docs/concepts/render-artifacts.mdx)
     * for the full JSON contract.
     *
     * This is the effective boolean the CLI's `--emit-render-artifacts` /
     * `--no-emit-render-artifacts` tri-state resolves against. Precedence:
     * explicit CLI flag > this config field > default `false`.
     *
     * Default: `false` (explicit opt-in), unlike `emitRoutesManifest`'s
     * default-on posture — the writer instruments every rendered region
     * with sentinel markers before stripping them back out, and the epic
     * keeps that opt-in until the confirm sub-issue proves flag-off output
     * stays byte-identical.
     *
     * Build-only: it does not affect `zfb dev`.
     *
     * Mirrors `Config::emit_render_artifacts` in `crates/zfb/src/config.rs`.
     */
    emitRenderArtifacts?: boolean;
    /**
     * Bundler options. `bundle.exclude` lists project-relative globs of
     * source files to keep out of the esbuild graph (e.g.
     * `["components/*.stories.tsx"]`) — see {@link BundleConfig.exclude} for
     * why this is needed. Unset → byte-identical to a build without the knob.
     * Mirrors `Config::bundle` in `crates/zfb/src/config.rs`.
     */
    bundle?: BundleConfig;
    /** User-supplied plugins. */
    plugins?: PluginConfig[];
    /**
     * Deploy-target adapter package name. Omit (or `"none"`) for a pure
     * static build — any route exporting `prerender = false` is then a
     * hard build error. A package name like
     * `"@takazudo/zfb-adapter-cloudflare"` selects the matching adapter,
     * and `zfb build` invokes that package's bin to wrap the SSR bundle
     * into a deploy-ready entry (e.g. `dist/_worker.js` for Cloudflare
     * Workers Static Assets, Pages-compatible).
     *
     * Mirrors `Config::adapter` in crates/zfb/src/config.rs.
     */
    adapter?: string;
    /**
     * Strip `.md` / `.mdx` from internal `` paths during MDX
     * compilation, and append a trailing `/` so the resulting URL shape
     * converges with the rest of the site (mirrors the JS engine's
     * `rehypeStripMdExtension`). Default: `false`.
     *
     * Enable this when content authors hand-write `[label](other.md)`
     * style references that should resolve to the rendered route URL
     * (e.g. `other/`) instead of a literal file path. Built dist and
     * `pnpm dev` honour the same flag, so previews match shipped output.
     *
     * Mirrors `Config::strip_md_ext` in crates/zfb/src/config.rs.
     */
    stripMdExt?: boolean;
    /**
     * Public URL prefix mounted in front of every absolute HTML asset
     * URL the build emits — ``, `