@mandujs/core 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,172 +1,280 @@
1
- import path from "path";
2
- import { readJsonFile } from "../utils/bun";
3
- import type { ManduAdapter } from "../runtime/adapter";
4
- import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
5
-
6
- export type GuardRuleSeverity = "error" | "warn" | "warning" | "off";
7
-
8
- /**
9
- * Test block configuration (Phase 12.1 — testing ecosystem).
10
- *
11
- * Shapes the CLI `mandu test` command's discovery, fixture, and reporter
12
- * behaviour. All fields are optional; omitting the block yields sensible
13
- * defaults that match Next.js / SvelteKit user expectations:
14
- *
15
- * - unit → `**\/*.test.ts` / `**\/*.test.tsx`, 30s timeout
16
- * - integration → `tests/integration/**\/*.test.ts`, in-memory fixtures
17
- * - e2e → reserved for Phase 12.2 (ATE integration)
18
- * - coverage → reserved for Phase 12.3 (bun + playwright merge)
19
- */
20
- export interface TestUnitConfig {
21
- /** Glob patterns for unit test files. Default: `["**\/*.test.ts", "**\/*.test.tsx"]`. */
22
- include?: string[];
23
- /** Glob patterns to exclude (applied after `include`). Default: `["node_modules/**", ".mandu/**", "dist/**"]`. */
24
- exclude?: string[];
25
- /** Per-test timeout in milliseconds. Default: `30_000` (30s). */
26
- timeout?: number;
27
- }
28
-
29
- export interface TestIntegrationConfig {
30
- /** Glob patterns for integration test files. Default: `["tests/integration/**\/*.test.ts"]`. */
31
- include?: string[];
32
- /** Glob patterns to exclude. Default: same as unit defaults. */
33
- exclude?: string[];
34
- /**
35
- * Database URL for fixtures. Default: `"sqlite::memory:"` (in-memory SQLite).
36
- * Accepts any Bun.sql-compatible URL — see `@mandujs/core/db` for the schema matrix.
37
- */
38
- dbUrl?: string;
39
- /**
40
- * Session storage strategy for `createTestSession`.
41
- * - `"memory"` (default): CookieSessionStorage with ephemeral secret
42
- * - `"sqlite"`: bun:sqlite-backed (Phase 2.5 storage, requires Phase 4)
43
- */
44
- sessionStore?: "memory" | "sqlite";
45
- /** Per-test timeout. Default: `60_000` (60s — integration work is slower). */
46
- timeout?: number;
47
- }
48
-
49
- export interface TestE2EConfig {
50
- /** Reserved for Phase 12.2. Currently a typed placeholder. */
51
- reserved?: true;
52
- }
53
-
54
- export interface TestCoverageConfig {
55
- /** Minimum line coverage percentage (0-100). Reserved for Phase 12.3. */
56
- lines?: number;
57
- /** Minimum branch coverage percentage (0-100). Reserved for Phase 12.3. */
58
- branches?: number;
59
- }
60
-
61
- export interface TestConfig {
62
- unit?: TestUnitConfig;
63
- integration?: TestIntegrationConfig;
64
- e2e?: TestE2EConfig;
65
- coverage?: TestCoverageConfig;
66
- }
67
-
68
- export interface ManduConfig {
69
- adapter?: ManduAdapter;
70
- server?: {
71
- port?: number;
72
- hostname?: string;
73
- cors?:
74
- | boolean
75
- | {
76
- origin?: string | string[];
77
- methods?: string[];
78
- credentials?: boolean;
79
- };
80
- streaming?: boolean;
81
- rateLimit?:
82
- | boolean
83
- | {
84
- windowMs?: number;
85
- max?: number;
86
- message?: string;
87
- statusCode?: number;
88
- headers?: boolean;
89
- };
90
- };
91
- guard?: {
92
- preset?: "mandu" | "fsd" | "clean" | "hexagonal" | "atomic" | "cqrs";
93
- srcDir?: string;
94
- exclude?: string[];
95
- realtime?: boolean;
96
- rules?: Record<string, GuardRuleSeverity>;
97
- contractRequired?: GuardRuleSeverity;
98
- };
99
- build?: {
100
- outDir?: string;
101
- minify?: boolean;
102
- sourcemap?: boolean;
103
- splitting?: boolean;
104
- };
105
- dev?: {
106
- hmr?: boolean;
107
- watchDirs?: string[];
108
- /** Observability SQLite 영구 저장 (기본: true) */
109
- observability?: boolean;
110
- };
111
- fsRoutes?: {
112
- routesDir?: string;
113
- extensions?: string[];
114
- exclude?: string[];
115
- islandSuffix?: string;
116
- };
117
- seo?: {
118
- enabled?: boolean;
119
- defaultTitle?: string;
120
- titleTemplate?: string;
121
- };
122
- /** Phase 12.1 — `mandu test` configuration block. */
123
- test?: TestConfig;
124
- plugins?: ManduPlugin[];
125
- hooks?: Partial<ManduHooks>;
126
- }
127
-
128
- export const CONFIG_FILES = [
129
- "mandu.config.ts",
130
- "mandu.config.js",
131
- "mandu.config.json",
132
- path.join(".mandu", "guard.json"),
133
- ];
134
-
135
- export function coerceConfig(raw: unknown, source: string): ManduConfig {
136
- if (!raw || typeof raw !== "object") return {};
137
-
138
- // .mandu/guard.json can be guard-only
139
- if (source.endsWith("guard.json") && !("guard" in (raw as Record<string, unknown>))) {
140
- return { guard: raw as ManduConfig["guard"] };
141
- }
142
-
143
- return raw as ManduConfig;
144
- }
145
-
146
- export async function loadManduConfig(rootDir: string): Promise<ManduConfig> {
147
- for (const fileName of CONFIG_FILES) {
148
- const filePath = path.join(rootDir, fileName);
149
- if (!(await Bun.file(filePath).exists())) {
150
- continue;
151
- }
152
-
153
- if (fileName.endsWith(".json")) {
154
- try {
155
- const parsed = await readJsonFile(filePath);
156
- return coerceConfig(parsed, fileName);
157
- } catch {
158
- return {};
159
- }
160
- }
161
-
162
- try {
163
- const module = await import(filePath);
164
- const raw = module?.default ?? module;
165
- return coerceConfig(raw, fileName);
166
- } catch {
167
- return {};
168
- }
169
- }
170
-
171
- return {};
172
- }
1
+ import path from "path";
2
+ import { readJsonFile } from "../utils/bun";
3
+ import type { ManduAdapter } from "../runtime/adapter";
4
+ import type { ManduPlugin, ManduHooks } from "../plugins/hooks";
5
+
6
+ export type GuardRuleSeverity = "error" | "warn" | "warning" | "off";
7
+
8
+ /**
9
+ * Test block configuration (Phase 12.1 — testing ecosystem).
10
+ *
11
+ * Shapes the CLI `mandu test` command's discovery, fixture, and reporter
12
+ * behaviour. All fields are optional; omitting the block yields sensible
13
+ * defaults that match Next.js / SvelteKit user expectations:
14
+ *
15
+ * - unit → `**\/*.test.ts` / `**\/*.test.tsx`, 30s timeout
16
+ * - integration → `tests/integration/**\/*.test.ts`, in-memory fixtures
17
+ * - e2e → reserved for Phase 12.2 (ATE integration)
18
+ * - coverage → reserved for Phase 12.3 (bun + playwright merge)
19
+ */
20
+ export interface TestUnitConfig {
21
+ /** Glob patterns for unit test files. Default: `["**\/*.test.ts", "**\/*.test.tsx"]`. */
22
+ include?: string[];
23
+ /** Glob patterns to exclude (applied after `include`). Default: `["node_modules/**", ".mandu/**", "dist/**"]`. */
24
+ exclude?: string[];
25
+ /** Per-test timeout in milliseconds. Default: `30_000` (30s). */
26
+ timeout?: number;
27
+ }
28
+
29
+ export interface TestIntegrationConfig {
30
+ /** Glob patterns for integration test files. Default: `["tests/integration/**\/*.test.ts"]`. */
31
+ include?: string[];
32
+ /** Glob patterns to exclude. Default: same as unit defaults. */
33
+ exclude?: string[];
34
+ /**
35
+ * Database URL for fixtures. Default: `"sqlite::memory:"` (in-memory SQLite).
36
+ * Accepts any Bun.sql-compatible URL — see `@mandujs/core/db` for the schema matrix.
37
+ */
38
+ dbUrl?: string;
39
+ /**
40
+ * Session storage strategy for `createTestSession`.
41
+ * - `"memory"` (default): CookieSessionStorage with ephemeral secret
42
+ * - `"sqlite"`: bun:sqlite-backed (Phase 2.5 storage, requires Phase 4)
43
+ */
44
+ sessionStore?: "memory" | "sqlite";
45
+ /** Per-test timeout. Default: `60_000` (60s — integration work is slower). */
46
+ timeout?: number;
47
+ }
48
+
49
+ export interface TestE2EConfig {
50
+ /** Reserved for Phase 12.2. Currently a typed placeholder. */
51
+ reserved?: true;
52
+ }
53
+
54
+ export interface TestCoverageConfig {
55
+ /** Minimum line coverage percentage (0-100). Reserved for Phase 12.3. */
56
+ lines?: number;
57
+ /** Minimum branch coverage percentage (0-100). Reserved for Phase 12.3. */
58
+ branches?: number;
59
+ }
60
+
61
+ export interface TestConfig {
62
+ unit?: TestUnitConfig;
63
+ integration?: TestIntegrationConfig;
64
+ e2e?: TestE2EConfig;
65
+ coverage?: TestCoverageConfig;
66
+ }
67
+
68
+ export interface ManduConfig {
69
+ adapter?: ManduAdapter;
70
+ /**
71
+ * Issue #192 — Enable CSS View Transitions for cross-document
72
+ * navigations. When `true` (default) Mandu injects
73
+ * `<style>@view-transition { navigation: auto; }</style>` into the SSR
74
+ * `<head>`, which lets supporting browsers (Chrome/Edge ≥ 111) play a
75
+ * crossfade between the outgoing and incoming pages. Non-supporting
76
+ * browsers ignore the at-rule and fall back to the classic
77
+ * full-reload — zero regression.
78
+ *
79
+ * Set to `false` to opt out entirely (e.g. if your app ships a
80
+ * hand-rolled navigation animation or a conflicting CSS rule).
81
+ *
82
+ * Default: `true`.
83
+ */
84
+ transitions?: boolean;
85
+ /**
86
+ * Issue #192 — Enable hover-based link prefetch. When `true` (default)
87
+ * Mandu injects a ~500-byte inline script that listens for `mouseover`
88
+ * events on internal links (`<a href="/...">`) and issues a
89
+ * `<link rel="prefetch">` for each unique target. The browser's HTTP
90
+ * cache services the subsequent navigation, removing most of the TTFB
91
+ * for above-the-fold links.
92
+ *
93
+ * Per-link opt-out: add `data-no-prefetch` to an `<a>` tag to skip it.
94
+ * Global opt-out: set this field to `false`.
95
+ *
96
+ * Default: `true`.
97
+ */
98
+ prefetch?: boolean;
99
+ /**
100
+ * Issue #193 — Enable opt-out SPA navigation. When `true` (default)
101
+ * Mandu intercepts every internal same-origin `<a href="/...">` click
102
+ * and routes it through the client-side router, using the View
103
+ * Transitions API where available for a zero-flash experience. Plain
104
+ * `<a href="/about">` tags "just work" without a component wrapper.
105
+ *
106
+ * Escape hatches (the anchor always falls through to the browser):
107
+ * - Per-link opt-out: `data-no-spa` on the `<a>` tag.
108
+ * - External / cross-origin `href`.
109
+ * - `mailto:` / `tel:` / `javascript:` / etc. (non-http schemes).
110
+ * - `target="_blank"` (any `target` other than `_self`).
111
+ * - `download` attribute.
112
+ * - Modifier keys (Ctrl / Cmd / Shift / Alt) or middle/right-click.
113
+ *
114
+ * Global opt-out: set this field to `false` to revert to the legacy
115
+ * opt-in behavior, where only `<a data-mandu-link href="/...">` is
116
+ * intercepted. This is a breaking-change escape hatch for projects
117
+ * that relied on the pre-v0.22 default.
118
+ *
119
+ * Default: `true`.
120
+ */
121
+ spa?: boolean;
122
+ server?: {
123
+ port?: number;
124
+ hostname?: string;
125
+ cors?:
126
+ | boolean
127
+ | {
128
+ origin?: string | string[];
129
+ methods?: string[];
130
+ credentials?: boolean;
131
+ };
132
+ streaming?: boolean;
133
+ rateLimit?:
134
+ | boolean
135
+ | {
136
+ windowMs?: number;
137
+ max?: number;
138
+ message?: string;
139
+ statusCode?: number;
140
+ headers?: boolean;
141
+ };
142
+ };
143
+ guard?: {
144
+ preset?: "mandu" | "fsd" | "clean" | "hexagonal" | "atomic" | "cqrs";
145
+ srcDir?: string;
146
+ exclude?: string[];
147
+ realtime?: boolean;
148
+ rules?: Record<string, GuardRuleSeverity>;
149
+ contractRequired?: GuardRuleSeverity;
150
+ };
151
+ build?: {
152
+ outDir?: string;
153
+ minify?: boolean;
154
+ sourcemap?: boolean;
155
+ splitting?: boolean;
156
+ };
157
+ dev?: {
158
+ hmr?: boolean;
159
+ watchDirs?: string[];
160
+ /** Observability SQLite 영구 저장 (기본: true) */
161
+ observability?: boolean;
162
+ /**
163
+ * Issue #191 Dev-only `_devtools.js` (~1.15 MB React dev runtime +
164
+ * Mandu Kitchen panel) injection override.
165
+ *
166
+ * - `true` → force inject on every page (SSR-only projects that
167
+ * still want the Kitchen panel in dev).
168
+ * - `false` → force skip on every page (Kitchen-off dev loop).
169
+ * - `undefined` → default. Inject iff the page's bundle manifest
170
+ * has at least one island. Pure-SSR pages download
171
+ * zero devtools bytes.
172
+ *
173
+ * Production builds never emit `_devtools.js`, so this flag is
174
+ * a no-op in prod regardless of value.
175
+ */
176
+ devtools?: boolean;
177
+ /**
178
+ * Issue #196 — Auto-run `scripts/prebuild-*.ts` before `mandu dev`
179
+ * boots, and re-run them when files under `contentDir` change in
180
+ * watch mode.
181
+ *
182
+ * - `true` → always run discovered prebuild scripts, regardless
183
+ * of whether `content/` exists (useful for projects
184
+ * that ship generators that write outside `content/`).
185
+ * - `false` → never auto-run. User stays responsible for the
186
+ * chain (`bun scripts/prebuild-*.ts && mandu dev`).
187
+ * - `undefined` → default. Auto-enabled iff the project has a
188
+ * `content/` directory OR at least one
189
+ * `scripts/prebuild-*.ts`. Silent no-op otherwise.
190
+ *
191
+ * See `@mandujs/core/content/prebuild` for the discovery + execution
192
+ * contract.
193
+ */
194
+ autoPrebuild?: boolean;
195
+ /**
196
+ * Issue #196 — Directory whose changes trigger a watch-mode
197
+ * prebuild re-run. Defaults to `"content"`. Ignored when
198
+ * `autoPrebuild === false`. Relative to project root.
199
+ */
200
+ contentDir?: string;
201
+ };
202
+ fsRoutes?: {
203
+ routesDir?: string;
204
+ extensions?: string[];
205
+ exclude?: string[];
206
+ islandSuffix?: string;
207
+ };
208
+ seo?: {
209
+ enabled?: boolean;
210
+ defaultTitle?: string;
211
+ titleTemplate?: string;
212
+ };
213
+ /** Phase 12.1 — `mandu test` configuration block. */
214
+ test?: TestConfig;
215
+ /**
216
+ * Phase 17 — observability endpoint toggles.
217
+ *
218
+ * Both fields default to `undefined`, which means "use mode default":
219
+ * - dev mode → endpoint exposed
220
+ * - prod mode → endpoint hidden unless `MANDU_DEBUG_HEAP=1`
221
+ *
222
+ * Explicit `true` / `false` overrides the mode default, so operators
223
+ * can opt-in to exposing metrics in prod (for a trusted internal
224
+ * network) or opt-out in dev (for a clean test harness).
225
+ */
226
+ observability?: {
227
+ /** `/_mandu/heap` JSON exposure toggle. */
228
+ heapEndpoint?: boolean;
229
+ /** `/_mandu/metrics` Prometheus text exposure toggle. */
230
+ metricsEndpoint?: boolean;
231
+ };
232
+ plugins?: ManduPlugin[];
233
+ hooks?: Partial<ManduHooks>;
234
+ }
235
+
236
+ export const CONFIG_FILES = [
237
+ "mandu.config.ts",
238
+ "mandu.config.js",
239
+ "mandu.config.json",
240
+ path.join(".mandu", "guard.json"),
241
+ ];
242
+
243
+ export function coerceConfig(raw: unknown, source: string): ManduConfig {
244
+ if (!raw || typeof raw !== "object") return {};
245
+
246
+ // .mandu/guard.json can be guard-only
247
+ if (source.endsWith("guard.json") && !("guard" in (raw as Record<string, unknown>))) {
248
+ return { guard: raw as ManduConfig["guard"] };
249
+ }
250
+
251
+ return raw as ManduConfig;
252
+ }
253
+
254
+ export async function loadManduConfig(rootDir: string): Promise<ManduConfig> {
255
+ for (const fileName of CONFIG_FILES) {
256
+ const filePath = path.join(rootDir, fileName);
257
+ if (!(await Bun.file(filePath).exists())) {
258
+ continue;
259
+ }
260
+
261
+ if (fileName.endsWith(".json")) {
262
+ try {
263
+ const parsed = await readJsonFile(filePath);
264
+ return coerceConfig(parsed, fileName);
265
+ } catch {
266
+ return {};
267
+ }
268
+ }
269
+
270
+ try {
271
+ const module = await import(filePath);
272
+ const raw = module?.default ?? module;
273
+ return coerceConfig(raw, fileName);
274
+ } catch {
275
+ return {};
276
+ }
277
+ }
278
+
279
+ return {};
280
+ }