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
* <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
* `