import path from "path"; import { readJsonFile } from "../utils/bun"; import type { ManduAdapter } from "../runtime/adapter"; import type { ManduPlugin, ManduHooks } from "../plugins/hooks"; import type { Middleware } from "../middleware/define"; import type { RpcDefinition, RpcProcedureRecord } from "../contract/rpc"; import type { CronDef } from "../scheduler"; import type { GuardRule as CustomGuardRule } from "../guard/define-rule"; import type { I18nStrategy, LocaleCode } from "../i18n/types"; export type GuardRuleSeverity = "error" | "warn" | "warning" | "off"; /** * Test block configuration (Phase 12.1 — testing ecosystem). * * Shapes the CLI `mandu test` command's discovery, fixture, and reporter * behaviour. All fields are optional; omitting the block yields sensible * defaults that match Next.js / SvelteKit user expectations: * * - unit → `**\/*.test.ts` / `**\/*.test.tsx`, 30s timeout * - integration → `tests/integration/**\/*.test.ts`, in-memory fixtures * - e2e → reserved for Phase 12.2 (ATE integration) * - coverage → reserved for Phase 12.3 (bun + playwright merge) */ export interface TestUnitConfig { /** Glob patterns for unit test files. Default: `["**\/*.test.ts", "**\/*.test.tsx"]`. */ include?: string[]; /** Glob patterns to exclude (applied after `include`). Default: `["node_modules/**", ".mandu/**", "dist/**"]`. */ exclude?: string[]; /** Per-test timeout in milliseconds. Default: `30_000` (30s). */ timeout?: number; } export interface TestIntegrationConfig { /** Glob patterns for integration test files. Default: `["tests/integration/**\/*.test.ts"]`. */ include?: string[]; /** Glob patterns to exclude. Default: same as unit defaults. */ exclude?: string[]; /** * Database URL for fixtures. Default: `"sqlite::memory:"` (in-memory SQLite). * Accepts any Bun.sql-compatible URL — see `@mandujs/core/db` for the schema matrix. */ dbUrl?: string; /** * Session storage strategy for `createTestSession`. * - `"memory"` (default): CookieSessionStorage with ephemeral secret * - `"sqlite"`: bun:sqlite-backed (Phase 2.5 storage, requires Phase 4) */ sessionStore?: "memory" | "sqlite"; /** Per-test timeout. Default: `60_000` (60s — integration work is slower). */ timeout?: number; } export interface TestE2EConfig { /** Reserved for Phase 12.2. Currently a typed placeholder. */ reserved?: true; } export interface TestCoverageConfig { /** Minimum line coverage percentage (0-100). Reserved for Phase 12.3. */ lines?: number; /** Minimum branch coverage percentage (0-100). Reserved for Phase 12.3. */ branches?: number; } export interface TestConfig { unit?: TestUnitConfig; integration?: TestIntegrationConfig; e2e?: TestE2EConfig; coverage?: TestCoverageConfig; } export interface ManduConfig { adapter?: ManduAdapter; /** * Issue #192 — Enable CSS View Transitions for cross-document * navigations. When `true` (default) Mandu injects * `` into the SSR * ``, which lets supporting browsers (Chrome/Edge ≥ 111) play a * crossfade between the outgoing and incoming pages. Non-supporting * browsers ignore the at-rule and fall back to the classic * full-reload — zero regression. * * Set to `false` to opt out entirely (e.g. if your app ships a * hand-rolled navigation animation or a conflicting CSS rule). * * Default: `true`. */ transitions?: boolean; /** * Issue #192 — Enable hover-based link prefetch. When `true` (default) * Mandu injects a ~500-byte inline script that listens for `mouseover` * events on internal links (``) and issues a * `` for each unique target. The browser's HTTP * cache services the subsequent navigation, removing most of the TTFB * for above-the-fold links. * * Per-link opt-out: add `data-no-prefetch` to an `` tag to skip it. * Global opt-out: set this field to `false`. * * Default: `true`. */ prefetch?: boolean; /** * Issue #193 — Enable opt-out SPA navigation. When `true` (default) * Mandu intercepts every internal same-origin `` click * and routes it through the client-side router, using the View * Transitions API where available for a zero-flash experience. Plain * `` tags "just work" without a component wrapper. * * Escape hatches (the anchor always falls through to the browser): * - Per-link opt-out: `data-no-spa` on the `` tag. * - External / cross-origin `href`. * - `mailto:` / `tel:` / `javascript:` / etc. (non-http schemes). * - `target="_blank"` (any `target` other than `_self`). * - `download` attribute. * - Modifier keys (Ctrl / Cmd / Shift / Alt) or middle/right-click. * * Global opt-out: set this field to `false` to revert to the legacy * opt-in behavior, where only `` is * intercepted. This is a breaking-change escape hatch for projects * that relied on the pre-v0.22 default. * * Default: `true`. */ spa?: boolean; server?: { port?: number; /** * Bind hostname for the HTTP server. * * Default: `"::"` (IPv6 wildcard, dual-stack). Bun leaves * `IPV6_V6ONLY` off, so this one socket accepts both IPv6 clients * (e.g. Node 17+ `fetch("localhost:PORT")` on Windows resolves to * `::1`) and IPv4 clients (as IPv4-mapped IPv6) — you effectively * get `0.0.0.0` + `::` for free. * * Set `"0.0.0.0"` to bind IPv4 only (container/firewall setups that * need it). Note: on Windows, an IPv4-only bind makes Node's * `fetch("localhost:PORT")` fail with `ECONNREFUSED ::1:PORT` * because Node prefers the IPv6 address for `localhost`. `curl` * and browsers silently fall back to IPv4, hiding the bug — Mandu * emits a one-line warning on Windows when you pick this value * explicitly so the trap is discoverable. * * Set `"127.0.0.1"` or `"::1"` to bind loopback-only (no LAN * visibility). Set any other value (e.g. `"10.0.0.2"`, * `"myhost.example.com"`) to bind that specific interface. * * @see issues #190 #223 #225 */ hostname?: string; cors?: | boolean | { origin?: string | string[]; methods?: string[]; credentials?: boolean; }; streaming?: boolean; rateLimit?: | boolean | { windowMs?: number; max?: number; message?: string; statusCode?: number; headers?: boolean; }; }; guard?: { preset?: "mandu" | "fsd" | "clean" | "hexagonal" | "atomic" | "cqrs"; srcDir?: string; exclude?: string[]; realtime?: boolean; /** * Built-in rule severity overrides (map) OR consumer-defined * custom rules (array). The Guard runner dispatches on shape: * * - `Record` → override severity of * Mandu's built-in rules by id. * - `GuardRule[]` (Phase 18.ν) → register consumer-defined rules * alongside the built-in presets. See `@mandujs/core/guard/define-rule`. * * Passing both at once is not supported; pick one shape per config. * Mixed input falls back to "custom rules only" and the built-in * rule severity overrides become unreachable. */ rules?: Record | CustomGuardRule[]; contractRequired?: GuardRuleSeverity; /** * Issue #207 — hard-fail on direct `__generated__/` imports at the * bundler level. When `true` (default), every `mandu dev` / * `mandu build` pass installs the * `mandu:block-generated-imports` Bun plugin, which throws * `ForbiddenGeneratedImportError` the moment any source file * resolves an import whose specifier contains `__generated__`. * * Set to `false` only when a migration path literally requires * the legacy barrel re-export pattern. User code should always * prefer `getGenerated()` from `@mandujs/core/runtime`; see * https://mandujs.com/docs/architect/generated-access. * * Default: `true`. */ blockGeneratedImport?: boolean; /** * Follow-up E — opt-in type-aware lint layer backed by * `oxlint --type-aware` (tsgolint). Declaring this block makes * `mandu guard` invoke oxlint after the architecture check and * merge the results into the final report; MCP's * `mandu_guard_check` surface gains a `typeAware` field. * * @see `@mandujs/core/guard/tsgolint-bridge` */ typeAware?: { rules?: string[]; severity?: "off" | "warn" | "error"; configPath?: string; }; }; build?: { outDir?: string; minify?: boolean; sourcemap?: boolean; splitting?: boolean; /** * Phase 18.η — emit `.mandu/analyze/report.html` + `report.json` after * a successful build. Equivalent to `mandu build --analyze`. Default: * `false`. Report artefacts are self-contained (no CDN, no external * JS) and safe to commit to a private dashboard or inspect locally. * * The CLI `--analyze` flag wins over this field; `--analyze=json` * writes JSON only (useful for CI, skips the HTML render cost). */ analyze?: boolean; /** * Issue #213 — tune the prerender link-crawler. * * The crawler (enabled by `mandu build` when `build.prerender !== false`) * scans rendered HTML for `` and enqueues those paths * for prerendering. Doc sites that ship code examples (`

     * <Link href="/path" />
`) previously leaked those * illustrative URLs into the render queue, producing spurious * `.mandu/static/path/index.html` files. * * The engine strips `
`, ``, fenced markdown, and inline
     * code spans before scanning. It also applies a small default
     * denylist of placeholder paths (`/path`, `/example`, `/your-*`,
     * etc.). Use this block to extend or replace the denylist for your
     * project.
     */
    /**
     * Phase 18.φ — bundle-size budget. Declaring any field turns on
     * framework-level size-ceiling enforcement during `mandu build`.
     *
     *   - `maxRawBytes`      : per-island raw-byte cap.
     *   - `maxGzBytes`       : per-island gzip-byte cap.
     *   - `maxTotalRawBytes` : project-wide raw cap (islands + shared).
     *   - `maxTotalGzBytes`  : project-wide gzip cap.
     *   - `perIsland`        : per-island overrides, additive per axis.
     *                          `{ home: { gz: 50_000 } }` tightens only
     *                          `home`'s gzip and leaves its raw cap at
     *                          the global `maxRawBytes`.
     *   - `mode`             : `'warning'` (default) prints a table and
     *                          continues; `'error'` exits non-zero.
     *
     * Declaring the empty block `budget: {}` is interpreted as "I know
     * about budgets" and auto-applies a 250 KB gzip per-island ceiling
     * (matches Next.js `largePageDataBytes` + Astro rules of thumb).
     * Omitting the block entirely is the zero-overhead opt-out.
     *
     * CLI override: `mandu build --no-budget` skips enforcement for a
     * single run regardless of config.
     *
     * @see `docs/architect/bundle-budget.md`
     * @see `@mandujs/core/bundler/budget`
     */
    budget?: {
      maxRawBytes?: number;
      maxGzBytes?: number;
      maxTotalRawBytes?: number;
      maxTotalGzBytes?: number;
      perIsland?: Record;
      mode?: "error" | "warning";
    };
    crawl?: {
      /**
       * Extra pathnames or simple globs (`*`) to exclude when crawling.
       * Merged with the built-in default denylist unless
       * {@link replaceDefaultExclude} is `true`.
       */
      exclude?: string[];
      /**
       * When `true`, `exclude` replaces the built-in denylist entirely.
       * Default `false`.
       */
      replaceDefaultExclude?: boolean;
      /**
       * Issue #219 — file extensions treated as non-HTML assets. When
       * a discovered href's pathname ends with one of these, the
       * crawler skips it instead of enqueuing it for prerender.
       *
       * Example: `` used to make the engine render
       * the asset URL as HTML and overwrite the real `.webp` on disk.
       * The default set covers common image / font / document /
       * media / text-asset extensions.
       *
       * Entries may be written with or without a leading dot
       * (`"webp"` and `".webp"` are equivalent). Matching is
       * case-insensitive; query strings and hash fragments are
       * stripped before comparison.
       *
       * Merged with the built-in default set unless
       * {@link replaceDefaultAssetExtensions} is `true`.
       */
      assetExtensions?: string[];
      /**
       * When `true`, `assetExtensions` replaces the built-in set
       * entirely. Default `false`.
       */
      replaceDefaultAssetExtensions?: boolean;
    };
  };
  dev?: {
    hmr?: boolean;
    watchDirs?: string[];
    /** Observability SQLite 영구 저장 (기본: true) */
    observability?: boolean;
    /**
     * Issue #191 — Dev-only `_devtools.js` (~1.15 MB React dev runtime +
     * Mandu Kitchen panel) injection override.
     *
     *   - `true`      → force inject on every page (SSR-only projects that
     *                   still want the Kitchen panel in dev).
     *   - `false`     → force skip on every page (Kitchen-off dev loop).
     *   - `undefined` → default. Inject iff the page's bundle manifest
     *                   has at least one island. Pure-SSR pages download
     *                   zero devtools bytes.
     *
     * Production builds never emit `_devtools.js`, so this flag is
     * a no-op in prod regardless of value.
     */
    devtools?: boolean;
    /**
     * Issue #196 — Auto-run `scripts/prebuild-*.ts` before `mandu dev`
     * boots, and re-run them when files under `contentDir` change in
     * watch mode.
     *
     *   - `true`      → always run discovered prebuild scripts, regardless
     *                   of whether `content/` exists (useful for projects
     *                   that ship generators that write outside `content/`).
     *   - `false`     → never auto-run. User stays responsible for the
     *                   chain (`bun scripts/prebuild-*.ts && mandu dev`).
     *   - `undefined` → default. Auto-enabled iff the project has a
     *                   `content/` directory OR at least one
     *                   `scripts/prebuild-*.ts`. Silent no-op otherwise.
     *
     * See `@mandujs/core/content/prebuild` for the discovery + execution
     * contract.
     */
    autoPrebuild?: boolean;
    /**
     * Issue #196 — Directory whose changes trigger a watch-mode
     * prebuild re-run. Defaults to `"content"`. Ignored when
     * `autoPrebuild === false`. Relative to project root.
     */
    contentDir?: string;
    /**
     * Issue #203 — Per-script wall-clock timeout for prebuild scripts
     * (milliseconds). Default: `120_000` (2 minutes), matching the MCP
     * `runCommand()` convention (#136). Override for projects that ship
     * slow seed generators (e.g. large docs indexers, image pipelines).
     *
     * Precedence at runtime, highest first:
     *   1. `MANDU_PREBUILD_TIMEOUT_MS` env var — useful for one-off CI
     *      overrides without committing to the config.
     *   2. This field (`dev.prebuildTimeoutMs`).
     *   3. Default 120_000 ms.
     *
     * When the timeout fires, `runPrebuildScripts` throws a
     * `PrebuildTimeoutError` whose message names the failing script path,
     * the limit, AND the two override paths — so the user does not need
     * to re-read this comment to recover.
     */
    prebuildTimeoutMs?: number;
    /**
     * Phase 18.α — Dev-only full-screen error overlay (Next.js / Astro
     * style). When `true` (default) Mandu injects a ~10 KB inline
     * `