@mandujs/core 0.53.0 → 0.53.2

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.
Files changed (112) hide show
  1. package/package.json +38 -2
  2. package/src/a11y/run-audit.ts +6 -6
  3. package/src/brain/adapters/oauth-flow.ts +1 -1
  4. package/src/brain/architecture/analyzer.ts +0 -2
  5. package/src/brain/credentials.ts +2 -2
  6. package/src/bundler/__tests__/build-runner.ts +13 -1
  7. package/src/bundler/__tests__/extended-watch.test.ts +3 -2
  8. package/src/bundler/__tests__/fast-refresh.test.ts +10 -12
  9. package/src/bundler/build.test.ts +30 -33
  10. package/src/bundler/build.ts +56 -27
  11. package/src/bundler/dev.ts +44 -39
  12. package/src/bundler/prerender.ts +63 -0
  13. package/src/bundler/safe-build.test.ts +2 -1
  14. package/src/bundler/types.ts +13 -6
  15. package/src/change/history.ts +1 -1
  16. package/src/change/snapshot.ts +18 -9
  17. package/src/client/Link.tsx +9 -9
  18. package/src/client/globals.ts +3 -2
  19. package/src/client/hooks.ts +0 -4
  20. package/src/client/hydrate.ts +1 -1
  21. package/src/client/router.ts +12 -12
  22. package/src/client/rpc.ts +1 -1
  23. package/src/client/use-fetch.ts +6 -6
  24. package/src/client/use-sse.ts +5 -5
  25. package/src/config/mcp-status.ts +0 -1
  26. package/src/config/validate.ts +4 -4
  27. package/src/config/watcher.ts +1 -1
  28. package/src/content/collection.ts +8 -12
  29. package/src/content/content-layer.ts +5 -5
  30. package/src/content/data-store.ts +2 -2
  31. package/src/content/loader-context.ts +0 -1
  32. package/src/content/loaders/file.ts +1 -1
  33. package/src/content/loaders/glob.ts +4 -4
  34. package/src/content/loaders/types.ts +1 -1
  35. package/src/content/meta-store.ts +1 -1
  36. package/src/content/types.ts +1 -1
  37. package/src/contract/define.ts +11 -12
  38. package/src/contract/index.ts +3 -3
  39. package/src/contract/protection.ts +2 -2
  40. package/src/contract/types.ts +1 -1
  41. package/src/contract/validator.ts +0 -1
  42. package/src/db/__tests__/db.test.ts +4 -7
  43. package/src/db/index.ts +138 -51
  44. package/src/db/migrations/index.ts +3 -0
  45. package/src/db/migrations/lock.ts +78 -23
  46. package/src/db/migrations/runner.ts +118 -101
  47. package/src/deploy/cache.ts +1 -0
  48. package/src/design/extract.ts +2 -1
  49. package/src/design/patch.ts +0 -1
  50. package/src/devtools/ai/mcp-connector.ts +24 -24
  51. package/src/devtools/client/components/kitchen-root.tsx +1 -1
  52. package/src/devtools/client/components/panel/errors-panel.tsx +1 -1
  53. package/src/devtools/client/state-manager.ts +1 -1
  54. package/src/devtools/init.ts +2 -3
  55. package/src/devtools/server/source-context.ts +1 -1
  56. package/src/devtools/worker/redaction-worker.ts +8 -4
  57. package/src/devtools/worker/worker-manager.ts +4 -3
  58. package/src/error/classifier.ts +1 -1
  59. package/src/error/domains.ts +1 -1
  60. package/src/filling/sse.ts +5 -5
  61. package/src/generator/generate.ts +2 -2
  62. package/src/guard/analyzer.ts +1 -1
  63. package/src/guard/ast-analyzer.ts +7 -7
  64. package/src/guard/auto-correct.ts +2 -2
  65. package/src/guard/config-guard.ts +0 -2
  66. package/src/guard/contract-guard.ts +1 -1
  67. package/src/guard/decision-memory.ts +2 -2
  68. package/src/guard/design-inline-class.ts +2 -1
  69. package/src/guard/graph.ts +1 -1
  70. package/src/guard/healing.ts +36 -41
  71. package/src/guard/negotiation.ts +3 -3
  72. package/src/guard/reporter.ts +1 -4
  73. package/src/guard/semantic-slots.ts +2 -2
  74. package/src/guard/suggestions.ts +2 -2
  75. package/src/guard/watcher.ts +5 -4
  76. package/src/index.ts +5 -5
  77. package/src/intent/index.ts +1 -1
  78. package/src/kitchen/api/agent-devtools-api.ts +544 -0
  79. package/src/kitchen/api/contract-api.ts +1 -1
  80. package/src/kitchen/api/file-api.ts +3 -3
  81. package/src/kitchen/kitchen-handler.ts +63 -38
  82. package/src/kitchen/kitchen-ui.ts +346 -62
  83. package/src/lockfile/validate.ts +0 -1
  84. package/src/openapi/generator.ts +2 -1
  85. package/src/openapi/openapi.test.ts +43 -14
  86. package/src/perf/user-marks.ts +1 -1
  87. package/src/plugins/registry.ts +1 -2
  88. package/src/plugins/types.ts +1 -1
  89. package/src/resource/__tests__/generator.test.ts +32 -15
  90. package/src/resource/ddl/__tests__/emit.test.ts +24 -0
  91. package/src/resource/ddl/diff.ts +0 -1
  92. package/src/resource/ddl/emit.ts +12 -1
  93. package/src/resource/generator-repo.ts +40 -20
  94. package/src/resource/generator.ts +1 -1
  95. package/src/resource/generators/contract.ts +1 -1
  96. package/src/resource/schema.ts +2 -1
  97. package/src/router/fs-routes.ts +6 -6
  98. package/src/router/fs-scanner.ts +1 -1
  99. package/src/runtime/env.ts +1 -1
  100. package/src/runtime/image-handler.ts +2 -1
  101. package/src/runtime/middleware.ts +2 -1
  102. package/src/runtime/server.ts +67 -60
  103. package/src/runtime/ssr.ts +3 -3
  104. package/src/runtime/streaming-ssr.ts +25 -27
  105. package/src/scheduler/validate.ts +1 -1
  106. package/src/seo/integration/ssr.ts +2 -3
  107. package/src/seo/render/opengraph.ts +1 -1
  108. package/src/seo/resolve/index.ts +0 -4
  109. package/src/slot/corrector.ts +1 -1
  110. package/src/slot/validator.ts +4 -4
  111. package/src/watcher/reporter.ts +1 -1
  112. package/src/watcher/watcher.ts +19 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.53.0",
3
+ "version": "0.53.2",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -24,19 +24,36 @@
24
24
  "./content/slug": "./src/content/slug.ts",
25
25
  "./content/llms-txt": "./src/content/llms-txt.ts",
26
26
  "./content/schema": "./src/content/schema.ts",
27
+ "./config": "./src/config/index.ts",
28
+ "./config/mandu": "./src/config/mandu.ts",
29
+ "./config/validate": "./src/config/validate.ts",
27
30
  "./db": "./src/db/index.ts",
31
+ "./db/migrations": "./src/db/migrations/index.ts",
32
+ "./db/migrations/history-table": "./src/db/migrations/history-table.ts",
33
+ "./db/migrations/runner": "./src/db/migrations/runner.ts",
28
34
  "./deploy": "./src/deploy/index.ts",
29
35
  "./design": "./src/design/index.ts",
30
36
  "./desktop": "./src/desktop/index.ts",
31
37
  "./desktop/worker": "./src/desktop/worker.ts",
32
38
  "./email": "./src/email/index.ts",
39
+ "./filling": "./src/filling/index.ts",
40
+ "./filling/cookie-codec": "./src/filling/cookie-codec.ts",
33
41
  "./filling/session-sqlite": "./src/filling/session-sqlite.ts",
34
42
  "./kitchen": "./src/kitchen/index.ts",
43
+ "./logging": "./src/logging/index.ts",
35
44
  "./middleware": "./src/middleware/index.ts",
45
+ "./middleware/csrf": "./src/middleware/csrf.ts",
36
46
  "./middleware/oauth": "./src/middleware/oauth/index.ts",
37
47
  "./middleware/secure": "./src/middleware/secure/index.ts",
38
48
  "./middleware/rate-limit": "./src/middleware/rate-limit/index.ts",
49
+ "./middleware/session": "./src/middleware/session.ts",
39
50
  "./testing": "./src/testing/index.ts",
51
+ "./testing/db": "./src/testing/db.ts",
52
+ "./testing/mocks": "./src/testing/mocks.ts",
53
+ "./testing/reporter": "./src/testing/reporter.ts",
54
+ "./testing/server": "./src/testing/server.ts",
55
+ "./testing/session": "./src/testing/session.ts",
56
+ "./testing/snapshot": "./src/testing/snapshot.ts",
40
57
  "./plugins": "./src/plugins/index.ts",
41
58
  "./plugins/define": "./src/plugins/define.ts",
42
59
  "./plugins/runner": "./src/plugins/runner.ts",
@@ -44,17 +61,36 @@
44
61
  "./i18n": "./src/i18n/index.ts",
45
62
  "./id": "./src/id/index.ts",
46
63
  "./observability": "./src/observability/index.ts",
64
+ "./openapi/generator": "./src/openapi/generator.ts",
47
65
  "./perf": "./src/perf/index.ts",
48
66
  "./perf/hmr-markers": "./src/perf/hmr-markers.ts",
49
67
  "./perf/user-marks": "./src/perf/user-marks.ts",
68
+ "./resource": "./src/resource/index.ts",
69
+ "./resource/ddl/diff": "./src/resource/ddl/diff.ts",
70
+ "./resource/ddl/emit": "./src/resource/ddl/emit.ts",
71
+ "./resource/ddl/snapshot": "./src/resource/ddl/snapshot.ts",
72
+ "./resource/ddl/type-map": "./src/resource/ddl/type-map.ts",
73
+ "./resource/ddl/types": "./src/resource/ddl/types.ts",
74
+ "./resource/generator-repo": "./src/resource/generator-repo.ts",
75
+ "./resource/parser": "./src/resource/parser.ts",
76
+ "./resource/schema": "./src/resource/schema.ts",
77
+ "./router": "./src/router/index.ts",
50
78
  "./routes": "./src/routes/index.ts",
79
+ "./runtime": "./src/runtime/index.ts",
80
+ "./runtime/cache": "./src/runtime/cache.ts",
81
+ "./runtime/router": "./src/runtime/router.ts",
82
+ "./runtime/server": "./src/runtime/server.ts",
51
83
  "./scheduler": "./src/scheduler/index.ts",
52
84
  "./storage/s3": "./src/storage/s3/index.ts",
85
+ "./guard": "./src/guard/index.ts",
53
86
  "./guard/define-rule": "./src/guard/define-rule.ts",
87
+ "./guard/design-inline-class": "./src/guard/design-inline-class.ts",
54
88
  "./guard/rule-presets": "./src/guard/rule-presets.ts",
55
89
  "./guard/tsgolint-bridge": "./src/guard/tsgolint-bridge.ts",
90
+ "./bundler": "./src/bundler/index.ts",
56
91
  "./bundler/prerender": "./src/bundler/prerender.ts",
57
92
  "./bundler/safe-build": "./src/bundler/safe-build.ts",
93
+ "./bundler/types": "./src/bundler/types.ts",
58
94
  "./bundler/hmr-types": "./src/bundler/hmr-types.ts",
59
95
  "./bundler/scenario-matrix": "./src/bundler/scenario-matrix.ts",
60
96
  "./bundler/vendor-cache-types": "./src/bundler/vendor-cache-types.ts",
@@ -71,7 +107,7 @@
71
107
  "./middleware/bridge": "./src/middleware/bridge.ts",
72
108
  "./client/hydrate": "./src/client/hydrate.ts",
73
109
  "./runtime/fast-refresh-types": "./src/runtime/fast-refresh-types.ts",
74
- "./*": "./src/*"
110
+ "./components/Image": "./src/components/Image.tsx"
75
111
  },
76
112
  "files": [
77
113
  "src/**/*"
@@ -66,7 +66,7 @@ interface AxeRunResult {
66
66
  /** Minimal structural typing for the DOM provider handle. */
67
67
  interface DomProvider {
68
68
  kind: "jsdom" | "happy-dom";
69
- fromHtml(html: string, url: string): Promise<{ window: unknown; dispose: () => void }>;
69
+ fromHtml(html: string, url: string): Promise<{ window: unknown; dispose: () => Promise<void> }>;
70
70
  }
71
71
 
72
72
  const DEFAULT_MAX_FILES = 500;
@@ -139,7 +139,7 @@ async function resolveDomProvider(options: RunAuditOptions): Promise<DomProvider
139
139
  const window = (instance as { window: unknown }).window;
140
140
  return {
141
141
  window,
142
- dispose() {
142
+ async dispose() {
143
143
  const w = window as { close?: () => void };
144
144
  if (typeof w.close === "function") {
145
145
  try { w.close(); } catch { /* no-op */ }
@@ -170,9 +170,9 @@ async function resolveDomProvider(options: RunAuditOptions): Promise<DomProvider
170
170
  window.document.close();
171
171
  return {
172
172
  window,
173
- dispose() {
173
+ async dispose() {
174
174
  if (typeof window.close === "function") {
175
- try { window.close(); } catch { /* no-op */ }
175
+ try { await window.close(); } catch { /* no-op */ }
176
176
  }
177
177
  },
178
178
  };
@@ -235,7 +235,7 @@ async function auditFile(
235
235
  }
236
236
 
237
237
  const pageUrl = "file://" + absFile.replace(/\\/g, "/");
238
- let handle: { window: unknown; dispose: () => void };
238
+ let handle: { window: unknown; dispose: () => void | Promise<void> };
239
239
  try {
240
240
  handle = await dom.fromHtml(html, pageUrl);
241
241
  } catch {
@@ -265,7 +265,7 @@ async function auditFile(
265
265
  } catch {
266
266
  return null;
267
267
  } finally {
268
- handle.dispose();
268
+ await handle.dispose();
269
269
  }
270
270
  }
271
271
 
@@ -236,7 +236,7 @@ export function prepareLoopbackFlow(
236
236
  stopped = true;
237
237
  clearTimeout(timer);
238
238
  try {
239
- server.stop(true);
239
+ void server.stop(true);
240
240
  } catch {
241
241
  /* best-effort */
242
242
  }
@@ -13,8 +13,6 @@ import type {
13
13
  CheckImportResult,
14
14
  ProjectStructure,
15
15
  FolderInfo,
16
- FolderRule,
17
- ImportRule,
18
16
  } from "./types";
19
17
  import { getBrain } from "../brain";
20
18
  import path from "path";
@@ -28,7 +28,7 @@
28
28
  * expires_at) — `CredentialStore` serializes it as a JSON string.
29
29
  */
30
30
 
31
- import { promises as fs, constants as fsConstants } from "node:fs";
31
+ import { promises as fs } from "node:fs";
32
32
  import os from "node:os";
33
33
  import path from "node:path";
34
34
 
@@ -157,7 +157,7 @@ async function runWithStdin(
157
157
  stdout: "pipe",
158
158
  stderr: "pipe",
159
159
  });
160
- proc.stdin.write(stdin);
160
+ await proc.stdin.write(stdin);
161
161
  await proc.stdin.end();
162
162
  const [stdout, exitCode] = await Promise.all([
163
163
  new Response(proc.stdout).text(),
@@ -30,7 +30,15 @@
30
30
  * - stdout: JSON blob terminated by `\n`:
31
31
  * { "success": boolean,
32
32
  * "errors": string[],
33
- * "manifest": { shared: { fastRefresh?: { runtime: string; glue: string } } }
33
+ * "manifest": {
34
+ * shared: {
35
+ * runtime?: string;
36
+ * vendor?: string;
37
+ * router?: string;
38
+ * fastRefresh?: { runtime: string; glue: string };
39
+ * };
40
+ * bundles: Record<string, unknown>;
41
+ * }
34
42
  * }
35
43
  * Only the fields tests consume are serialized; the in-memory manifest
36
44
  * is also persisted at `<rootDir>/.mandu/manifest.json` by the build.
@@ -83,8 +91,12 @@ try {
83
91
  errors: result.errors,
84
92
  manifest: {
85
93
  shared: {
94
+ runtime: result.manifest.shared?.runtime ?? null,
95
+ vendor: result.manifest.shared?.vendor ?? null,
96
+ router: result.manifest.shared?.router ?? null,
86
97
  fastRefresh: result.manifest.shared?.fastRefresh ?? null,
87
98
  },
99
+ bundles: result.manifest.bundles ?? {},
88
100
  },
89
101
  }) + "\n",
90
102
  );
@@ -1,3 +1,4 @@
1
+ import type * as __ManduHmrTypesTypes0 from "../hmr-types";
1
2
  /**
2
3
  * Phase 7.0 R2 Agent D — Extended file watch tests.
3
4
  *
@@ -702,8 +703,8 @@ describe("Phase 7.0 R2 Agent D — CoalescedChange kind surface", () => {
702
703
 
703
704
  // Literal assignments — if the union shrinks, TS errors in this
704
705
  // file before the suite even runs.
705
- const a: import("../hmr-types").CoalescedChange["kind"] = "config-reload";
706
- const b: import("../hmr-types").CoalescedChange["kind"] = "resource-regen";
706
+ const a: __ManduHmrTypesTypes0.CoalescedChange["kind"] = "config-reload";
707
+ const b: __ManduHmrTypesTypes0.CoalescedChange["kind"] = "resource-regen";
707
708
  expect(a).toBe("config-reload");
708
709
  expect(b).toBe("resource-regen");
709
710
  });
@@ -541,18 +541,16 @@ describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
541
541
  // 7-parallel shim fan-out fail with `AggregateError: Bundle failed`
542
542
  // ~100 % of the time for the affected shim(s). A subprocess has a
543
543
  // clean module graph.
544
- const { spawn } = await import("node:child_process");
545
- const runner = path.join(import.meta.dir, "build-runner.ts");
546
- const cwd = path.resolve(import.meta.dir, "..", "..", "..");
547
- const out = await new Promise<string>((resolve) => {
548
- const proc = spawn(process.execPath, ["run", runner, rootDir], {
549
- cwd,
550
- stdio: ["ignore", "pipe", "inherit"],
551
- });
552
- let buf = "";
553
- proc.stdout.on("data", (d: Buffer) => (buf += d.toString("utf-8")));
554
- proc.on("close", () => resolve(buf));
555
- });
544
+ const runner = path.join(import.meta.dir, "build-runner.ts");
545
+ const cwd = path.resolve(import.meta.dir, "..", "..", "..");
546
+ const proc = Bun.spawn([process.execPath, "run", runner, rootDir], {
547
+ cwd,
548
+ stdin: "ignore",
549
+ stdout: "pipe",
550
+ stderr: "inherit",
551
+ });
552
+ const out = await new Response(proc.stdout).text();
553
+ await proc.exited;
556
554
  const jsonLine =
557
555
  out
558
556
  .split(/\r?\n/)
@@ -2,7 +2,6 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
3
3
  import path from "path";
4
4
  import { pathToFileURL } from "url";
5
- import { spawn } from "node:child_process";
6
5
 
7
6
  // 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
8
7
  let rootDir: string;
@@ -34,40 +33,38 @@ async function runBuildInSubprocess(root: string): Promise<{
34
33
  "__tests__",
35
34
  "build-runner.ts",
36
35
  );
37
- return new Promise((resolve) => {
38
- const proc = spawn(process.execPath, ["run", runner, root], {
36
+ try {
37
+ const proc = Bun.spawn([process.execPath, "run", runner, root], {
39
38
  cwd: path.resolve(import.meta.dir, "..", ".."),
40
- stdio: ["ignore", "pipe", "inherit"],
39
+ stdin: "ignore",
40
+ stdout: "pipe",
41
+ stderr: "inherit",
41
42
  });
42
- let out = "";
43
- proc.stdout.on("data", (chunk: Buffer) => {
44
- out += chunk.toString("utf-8");
45
- });
46
- proc.on("close", () => {
47
- // Find the final JSON line the runner may log Mandu dev banners
48
- // (e.g. "[Mandu] DevTools …") before emitting the payload. The
49
- // contract is: last non-empty line is the JSON blob.
50
- const lines = out.split(/\r?\n/).filter((l) => l.trim().length > 0);
51
- const last = lines[lines.length - 1] ?? "";
52
- try {
53
- const parsed = JSON.parse(last);
54
- resolve({
55
- success: parsed.success === true,
56
- errors: Array.isArray(parsed.errors) ? parsed.errors : [],
57
- });
58
- } catch (e) {
59
- resolve({
60
- success: false,
61
- errors: [
62
- `build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
63
- ],
64
- });
65
- }
66
- });
67
- proc.on("error", (err) => {
68
- resolve({ success: false, errors: [`spawn failed: ${String(err)}`] });
69
- });
70
- });
43
+ const out = await new Response(proc.stdout).text();
44
+ await proc.exited;
45
+
46
+ // Find the final JSON line — the runner may log Mandu dev banners
47
+ // (e.g. "[Mandu] DevTools …") before emitting the payload. The
48
+ // contract is: last non-empty line is the JSON blob.
49
+ const lines = out.split(/\r?\n/).filter((l) => l.trim().length > 0);
50
+ const last = lines[lines.length - 1] ?? "";
51
+ try {
52
+ const parsed = JSON.parse(last);
53
+ return {
54
+ success: parsed.success === true,
55
+ errors: Array.isArray(parsed.errors) ? parsed.errors : [],
56
+ };
57
+ } catch (e) {
58
+ return {
59
+ success: false,
60
+ errors: [
61
+ `build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
62
+ ],
63
+ };
64
+ }
65
+ } catch (err) {
66
+ return { success: false, errors: [`spawn failed: ${String(err)}`] };
67
+ }
71
68
  }
72
69
 
73
70
  beforeAll(async () => {
@@ -1,3 +1,4 @@
1
+ import type * as __ManduPluginsReactCompilerTypes0 from "./plugins/react-compiler";
1
2
  /**
2
3
  * Mandu Client Bundler 📦
3
4
  * Bun.build 기반 클라이언트 번들 빌드
@@ -64,19 +65,51 @@ function manduDefaultPlugins(options: BundlerOptions): BunPlugin[] {
64
65
  * this; SSR builds use `manduDefaultPlugins()` directly because React
65
66
  * Compiler offers zero benefit for one-shot HTML rendering.
66
67
  */
67
- function manduClientPlugins(options: BundlerOptions): BunPlugin[] {
68
- const base = manduDefaultPlugins(options);
69
- if (options.reactCompiler?.enabled !== true) return base;
68
+ function manduClientPlugins(options: BundlerOptions): BunPlugin[] {
69
+ const base = manduDefaultPlugins(options);
70
+ if (options.reactCompiler?.enabled !== true) return base;
70
71
  // Lazy import to avoid pulling the react-compiler module into every
71
72
  // build graph — SSR / non-client paths never touch this branch.
72
- const { reactCompiler } = require("./plugins/react-compiler") as typeof import("./plugins/react-compiler");
73
+ const { reactCompiler } = require("./plugins/react-compiler") as typeof __ManduPluginsReactCompilerTypes0;
73
74
  return [
74
75
  ...base,
75
76
  reactCompiler({
76
77
  reactCompilerConfig: options.reactCompiler.compilerConfig,
77
- }),
78
- ];
79
- }
78
+ }),
79
+ ];
80
+ }
81
+
82
+ function resolveBundlerMode(options: BundlerOptions): "development" | "production" {
83
+ return options.mode ?? (process.env.NODE_ENV === "production" ? "production" : "development");
84
+ }
85
+
86
+ function isDevelopmentBuild(options: BundlerOptions): boolean {
87
+ return resolveBundlerMode(options) === "development";
88
+ }
89
+
90
+ function shouldMinify(options: BundlerOptions): boolean {
91
+ return options.minify ?? (resolveBundlerMode(options) === "production");
92
+ }
93
+
94
+ function shouldSplitChunks(options: BundlerOptions): boolean {
95
+ return options.splitting ?? (resolveBundlerMode(options) === "production");
96
+ }
97
+
98
+ function nodeEnvDefine(options: BundlerOptions): string {
99
+ return JSON.stringify(resolveBundlerMode(options));
100
+ }
101
+
102
+ function resolveClientOutDir(rootDir: string, outDir?: string): string {
103
+ const defaultOutDir = path.join(rootDir, ".mandu/client");
104
+ if (!outDir) return defaultOutDir;
105
+
106
+ const resolvedOutDir = path.isAbsolute(outDir) ? outDir : path.resolve(rootDir, outDir);
107
+ if (path.normalize(resolvedOutDir) === path.normalize(path.join(rootDir, ".mandu"))) {
108
+ return defaultOutDir;
109
+ }
110
+
111
+ return resolvedOutDir;
112
+ }
80
113
 
81
114
  /**
82
115
  * Scan for *.island.tsx / *.island.ts files across hydrated route directories.
@@ -229,20 +262,20 @@ async function buildPerIslandBundle(
229
262
  // Phase 7.1 B-1/B-4: wire Bun's native React Fast Refresh transform +
230
263
  // Mandu's boundary injection plugin — but only in dev. Production
231
264
  // bundles stay clean of `$RefreshReg$` / `$RefreshSig$` stubs.
232
- const isDev = (options.minify ?? process.env.NODE_ENV === "production") === false;
265
+ const isDev = isDevelopmentBuild(options);
233
266
  try {
234
267
  await Bun.write(entryPath, generateIslandEntry(entry.name, entry.filePath));
235
268
  const result = await safeBuild({
236
269
  entrypoints: [entryPath],
237
270
  outdir: outDir,
238
271
  naming: outputName,
239
- minify: options.minify ?? process.env.NODE_ENV === "production",
272
+ minify: shouldMinify(options),
240
273
  sourcemap: options.sourcemap ? "external" : "none",
241
274
  target: "browser",
242
275
  ...(isDev ? { reactFastRefresh: true } : {}),
243
276
  plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
244
277
  external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
245
- define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), ...options.define },
278
+ define: { "process.env.NODE_ENV": nodeEnvDefine(options), ...options.define },
246
279
  });
247
280
  await fs.unlink(entryPath).catch(() => {});
248
281
  if (!result.success) {
@@ -1234,12 +1267,12 @@ async function buildRouterRuntime(
1234
1267
  entrypoints: [routerPath],
1235
1268
  outdir: outDir,
1236
1269
  naming: outputName,
1237
- minify: options.minify ?? process.env.NODE_ENV === "production",
1270
+ minify: shouldMinify(options),
1238
1271
  sourcemap: options.sourcemap ? "external" : "none",
1239
1272
  target: "browser",
1240
1273
  plugins: manduDefaultPlugins(options),
1241
1274
  define: {
1242
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
1275
+ "process.env.NODE_ENV": nodeEnvDefine(options),
1243
1276
  ...options.define,
1244
1277
  },
1245
1278
  });
@@ -1310,13 +1343,13 @@ async function buildRuntime(
1310
1343
  entrypoints: [runtimePath],
1311
1344
  outdir: outDir,
1312
1345
  naming: outputName,
1313
- minify: options.minify ?? process.env.NODE_ENV === "production",
1346
+ minify: shouldMinify(options),
1314
1347
  sourcemap: options.sourcemap ? "external" : "none",
1315
1348
  target: "browser",
1316
1349
  external: ["react", "react-dom", "react-dom/client"],
1317
1350
  plugins: manduDefaultPlugins(options),
1318
1351
  define: {
1319
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
1352
+ "process.env.NODE_ENV": nodeEnvDefine(options),
1320
1353
  ...options.define,
1321
1354
  },
1322
1355
  });
@@ -1471,8 +1504,7 @@ async function buildVendorShims(
1471
1504
  // Phase 7.1 B-2: dev-only Fast Refresh shims. In production we skip
1472
1505
  // them entirely so `react-refresh/runtime` is never bundled and the
1473
1506
  // attack surface / bundle size regressions stay zero for deploys.
1474
- const isDev =
1475
- (options.minify ?? process.env.NODE_ENV === "production") === false;
1507
+ const isDev = isDevelopmentBuild(options);
1476
1508
 
1477
1509
  const shims: Array<{ name: string; source: string; key: VendorShimKey; cacheId: string }> = [
1478
1510
  { name: "_react", source: generateReactShimSource(), key: "react", cacheId: "react" },
@@ -1600,13 +1632,13 @@ async function buildVendorShims(
1600
1632
  entrypoints: [srcPath],
1601
1633
  outdir: outDir,
1602
1634
  naming: outputName,
1603
- minify: options.minify ?? process.env.NODE_ENV === "production",
1635
+ minify: shouldMinify(options),
1604
1636
  sourcemap: options.sourcemap ? "external" : "none",
1605
1637
  target: "browser",
1606
1638
  external: shimExternal,
1607
1639
  plugins: manduDefaultPlugins(options),
1608
1640
  define: {
1609
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
1641
+ "process.env.NODE_ENV": nodeEnvDefine(options),
1610
1642
  ...options.define,
1611
1643
  },
1612
1644
  });
@@ -1696,8 +1728,7 @@ async function buildIsland(
1696
1728
 
1697
1729
  // Phase 7.1 B-1/B-4: wire native Fast Refresh transform + Mandu's
1698
1730
  // boundary injection plugin. Dev-only; prod bundles remain clean.
1699
- const isDev =
1700
- (options.minify ?? process.env.NODE_ENV === "production") === false;
1731
+ const isDev = isDevelopmentBuild(options);
1701
1732
  try {
1702
1733
  // 엔트리 래퍼 생성
1703
1734
  await Bun.write(entryPath, generateIslandEntry(route.id, clientModulePath));
@@ -1708,15 +1739,15 @@ async function buildIsland(
1708
1739
  entrypoints: [entryPath],
1709
1740
  outdir: outDir,
1710
1741
  naming: options.splitting ? "[name]-[hash].js" : outputName,
1711
- minify: options.minify ?? process.env.NODE_ENV === "production",
1742
+ minify: shouldMinify(options),
1712
1743
  sourcemap: options.sourcemap ? "external" : "none",
1713
1744
  target: "browser",
1714
- splitting: options.splitting ?? (process.env.NODE_ENV === "production"),
1745
+ splitting: shouldSplitChunks(options),
1715
1746
  ...(isDev ? { reactFastRefresh: true } : {}),
1716
1747
  plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
1717
1748
  external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
1718
1749
  define: {
1719
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
1750
+ "process.env.NODE_ENV": nodeEnvDefine(options),
1720
1751
  ...options.define,
1721
1752
  },
1722
1753
  });
@@ -1910,15 +1941,13 @@ export async function buildClientBundles(
1910
1941
  errors.push(`onBundleComplete[${e.source}]: ${e.error.message}`);
1911
1942
  }
1912
1943
  };
1913
- const env = (process.env.NODE_ENV === "production" ? "production" : "development") as
1914
- | "development"
1915
- | "production";
1944
+ const env = resolveBundlerMode(options);
1916
1945
 
1917
1946
  // 1. Hydration이 필요한 라우트 필터링
1918
1947
  const hydratedRoutes = getHydratedRoutes(manifest);
1919
1948
 
1920
1949
  // 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
1921
- const outDir = options.outDir || path.join(rootDir, ".mandu/client");
1950
+ const outDir = resolveClientOutDir(rootDir, options.outDir);
1922
1951
  await fs.mkdir(outDir, { recursive: true });
1923
1952
 
1924
1953
  // Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함