@mandujs/core 0.53.0 → 0.53.1

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 (104) 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 +2 -1
  11. package/src/bundler/dev.ts +23 -22
  12. package/src/bundler/prerender.ts +63 -0
  13. package/src/bundler/safe-build.test.ts +2 -1
  14. package/src/bundler/types.ts +5 -3
  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 +1 -1
  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/migrations/index.ts +3 -0
  44. package/src/db/migrations/lock.ts +11 -11
  45. package/src/deploy/cache.ts +1 -0
  46. package/src/design/extract.ts +2 -1
  47. package/src/design/patch.ts +0 -1
  48. package/src/devtools/ai/mcp-connector.ts +24 -24
  49. package/src/devtools/client/components/kitchen-root.tsx +1 -1
  50. package/src/devtools/client/components/panel/errors-panel.tsx +1 -1
  51. package/src/devtools/client/state-manager.ts +1 -1
  52. package/src/devtools/init.ts +2 -3
  53. package/src/devtools/server/source-context.ts +1 -1
  54. package/src/devtools/worker/redaction-worker.ts +8 -4
  55. package/src/devtools/worker/worker-manager.ts +4 -3
  56. package/src/error/classifier.ts +1 -1
  57. package/src/error/domains.ts +1 -1
  58. package/src/filling/sse.ts +5 -5
  59. package/src/generator/generate.ts +2 -2
  60. package/src/guard/analyzer.ts +1 -1
  61. package/src/guard/ast-analyzer.ts +7 -7
  62. package/src/guard/auto-correct.ts +2 -2
  63. package/src/guard/config-guard.ts +0 -2
  64. package/src/guard/contract-guard.ts +1 -1
  65. package/src/guard/decision-memory.ts +2 -2
  66. package/src/guard/design-inline-class.ts +2 -1
  67. package/src/guard/graph.ts +1 -1
  68. package/src/guard/healing.ts +36 -41
  69. package/src/guard/negotiation.ts +3 -3
  70. package/src/guard/reporter.ts +1 -4
  71. package/src/guard/semantic-slots.ts +2 -2
  72. package/src/guard/suggestions.ts +2 -2
  73. package/src/guard/watcher.ts +5 -4
  74. package/src/index.ts +5 -5
  75. package/src/intent/index.ts +1 -1
  76. package/src/kitchen/api/contract-api.ts +1 -1
  77. package/src/kitchen/api/file-api.ts +3 -3
  78. package/src/kitchen/kitchen-handler.ts +29 -21
  79. package/src/lockfile/validate.ts +0 -1
  80. package/src/openapi/generator.ts +2 -1
  81. package/src/openapi/openapi.test.ts +43 -14
  82. package/src/perf/user-marks.ts +1 -1
  83. package/src/plugins/registry.ts +1 -2
  84. package/src/plugins/types.ts +1 -1
  85. package/src/resource/ddl/diff.ts +0 -1
  86. package/src/resource/generator.ts +1 -1
  87. package/src/resource/generators/contract.ts +1 -1
  88. package/src/resource/schema.ts +2 -1
  89. package/src/router/fs-routes.ts +6 -6
  90. package/src/router/fs-scanner.ts +1 -1
  91. package/src/runtime/env.ts +1 -1
  92. package/src/runtime/image-handler.ts +2 -1
  93. package/src/runtime/middleware.ts +2 -1
  94. package/src/runtime/server.ts +67 -60
  95. package/src/runtime/ssr.ts +3 -3
  96. package/src/runtime/streaming-ssr.ts +25 -27
  97. package/src/scheduler/validate.ts +1 -1
  98. package/src/seo/integration/ssr.ts +2 -3
  99. package/src/seo/render/opengraph.ts +1 -1
  100. package/src/seo/resolve/index.ts +0 -4
  101. package/src/slot/corrector.ts +1 -1
  102. package/src/slot/validator.ts +4 -4
  103. package/src/watcher/reporter.ts +1 -1
  104. 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.1",
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 기반 클라이언트 번들 빌드
@@ -69,7 +70,7 @@ function manduClientPlugins(options: BundlerOptions): BunPlugin[] {
69
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({
@@ -1,9 +1,10 @@
1
+ import type * as __ManduContentCollectionTypes0 from "../content/collection";
1
2
  /**
2
3
  * Mandu Dev Bundler 🔥
3
4
  * 개발 모드 번들링 + HMR (Hot Module Replacement)
4
5
  */
5
6
 
6
- import type { RoutesManifest, RouteSpec } from "../spec/schema";
7
+ import type { RoutesManifest } from "../spec/schema";
7
8
  import { buildClientBundles } from "./build";
8
9
  import type { BundleResult } from "./types";
9
10
  import { PORTS, TIMEOUTS } from "../constants";
@@ -1086,7 +1087,7 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1086
1087
  const handleContentChange = async (files: readonly string[]): Promise<void> => {
1087
1088
  if (files.length === 0) return;
1088
1089
  try {
1089
- const mod: typeof import("../content/collection") = await import("../content/collection");
1090
+ const mod: typeof __ManduContentCollectionTypes0 = await import("../content/collection");
1090
1091
  if (typeof mod.invalidateAllCollections === "function") {
1091
1092
  mod.invalidateAllCollections();
1092
1093
  }
@@ -1447,19 +1448,19 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1447
1448
  // SSR 모듈 변경 감지 (page.tsx, layout.tsx) — #151
1448
1449
  if (onSSRChange && serverModuleSet.has(normalizedPath)) {
1449
1450
  console.log(`\n🔄 SSR file changed: ${path.basename(changedFile)}`);
1450
- // Refresh the changed file's imports so the reverse graph
1451
- // tracks refactors (a new import added to an SSR module).
1452
- await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1453
- onSSRChange(normalizedPath);
1454
- return;
1455
- }
1451
+ // Refresh the changed file's imports so the reverse graph
1452
+ // tracks refactors (a new import added to an SSR module).
1453
+ await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1454
+ await Promise.resolve(onSSRChange(normalizedPath));
1455
+ return;
1456
+ }
1456
1457
  // API 모듈 변경 감지 (route.ts)
1457
1458
  if (onAPIChange && apiModuleSet.has(normalizedPath)) {
1458
- console.log(`\n🔄 API route changed: ${path.basename(changedFile)}`);
1459
- await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1460
- onAPIChange(normalizedPath);
1461
- return;
1462
- }
1459
+ console.log(`\n🔄 API route changed: ${path.basename(changedFile)}`);
1460
+ await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1461
+ await Promise.resolve(onAPIChange(normalizedPath));
1462
+ return;
1463
+ }
1463
1464
  // Phase 7.0 R2 Agent D — route middleware change.
1464
1465
  // `app/**/middleware.ts` isn't in apiModuleSet (not registered as a
1465
1466
  // route handler) but reuses the API reload path: the underlying
@@ -1467,11 +1468,11 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1467
1468
  // import chain. Falls through to `onAPIChange` so the CLI can
1468
1469
  // reuse its existing `handleAPIChange` plumbing.
1469
1470
  if (onAPIChange && isRouteMiddlewareFile(normalizedPath)) {
1470
- console.log(`\n🔄 Middleware changed: ${path.basename(changedFile)}`);
1471
- await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1472
- onAPIChange(normalizedPath);
1473
- return;
1474
- }
1471
+ console.log(`\n🔄 Middleware changed: ${path.basename(changedFile)}`);
1472
+ await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1473
+ await Promise.resolve(onAPIChange(normalizedPath));
1474
+ return;
1475
+ }
1475
1476
  // #189 — reverse import-graph fallback.
1476
1477
  //
1477
1478
  // The changed file matched none of our direct dispatch sets.
@@ -2188,10 +2189,10 @@ export function createHMRServer(
2188
2189
  } catch {
2189
2190
  // 무시
2190
2191
  }
2191
- }
2192
- clients.clear();
2193
- server.stop();
2194
- },
2192
+ }
2193
+ clients.clear();
2194
+ void server.stop();
2195
+ },
2195
2196
  setRestartHandler: (handler: () => Promise<void>) => {
2196
2197
  restartHandler = handler;
2197
2198
  },
@@ -555,6 +555,46 @@ export async function prerenderRoutes(
555
555
  }
556
556
  }
557
557
 
558
+ // 5.5. Issue #254 — emit `404.html` at the dist root when the project
559
+ // declares an `app/not-found.tsx`. Static hosts (Vercel, Netlify,
560
+ // Cloudflare Pages, Firebase Hosting) auto-serve `dist/404.html` for
561
+ // unmatched URLs; without this, visitors hit the platform's plain-
562
+ // text default.
563
+ //
564
+ // The probe path is multi-segment + sentinel-prefixed so it doesn't
565
+ // collide with single-segment dynamic routes (e.g. `[lang]`). Catch-
566
+ // all routes at the root (`[...slug]`) WILL still catch this — those
567
+ // projects own their own 404 surface anyway, so the probe falls
568
+ // through and we surface a clear warning instead of writing a wrong
569
+ // 404.html.
570
+ if (await hasAppNotFound(rootDir)) {
571
+ const probePath =
572
+ "/__mandu_not_found_probe_38f17a2c__/__inner_38f17a2c__";
573
+ try {
574
+ const request = new Request(`http://localhost${probePath}`);
575
+ const response = await fetchHandler(request);
576
+ if (response.status === 404) {
577
+ const html = await response.text();
578
+ const filePath = path.join(outputDir, "404.html");
579
+ await fs.writeFile(filePath, html, "utf-8");
580
+ pages.push({ path: "/404", size: html.length, duration: 0 });
581
+ // Not added to pageIndex — `/404` is a host-level fallback,
582
+ // not a routable page. The runtime keeps using its registered
583
+ // handler for live SSR.
584
+ } else {
585
+ // A catch-all route matched the probe. Skip the 404 emit and
586
+ // tell the user how to handle it themselves — emitting the
587
+ // catch-all body as 404.html would be wrong.
588
+ errors.push(
589
+ `[404.html] not emitted: probe path matched a catch-all route (HTTP ${response.status}). ` +
590
+ `Add an explicit 404 page or a sentinel route, or write \`dist/404.html\` from a postbuild script.`,
591
+ );
592
+ }
593
+ } catch (err) {
594
+ errors.push(`[404.html] ${describeError(err)}`);
595
+ }
596
+ }
597
+
558
598
  // 6. Emit runtime index.
559
599
  if (writeIndex) {
560
600
  const indexContents: PrerenderIndex = {
@@ -874,3 +914,26 @@ function describeError(error: unknown): string {
874
914
  if (error instanceof Error) return error.message;
875
915
  return String(error);
876
916
  }
917
+
918
+ /**
919
+ * Issue #254 — true when the project ships an `app/not-found.tsx`
920
+ * (or one of its non-TSX variants). Used by the prerender step to
921
+ * decide whether to emit a `404.html` fallback at the dist root.
922
+ */
923
+ async function hasAppNotFound(rootDir: string): Promise<boolean> {
924
+ const candidates = [
925
+ "app/not-found.tsx",
926
+ "app/not-found.ts",
927
+ "app/not-found.jsx",
928
+ "app/not-found.js",
929
+ ];
930
+ for (const rel of candidates) {
931
+ try {
932
+ await fs.access(path.join(rootDir, rel));
933
+ return true;
934
+ } catch {
935
+ // try next
936
+ }
937
+ }
938
+ return false;
939
+ }
@@ -163,7 +163,8 @@ describe("safeBuild", () => {
163
163
  // of landing inside it). The strict assertion is still `peak <= max`.
164
164
  let stop = false;
165
165
  const sample = async () => {
166
- while (!stop) {
166
+ for (;;) {
167
+ if (stop) break;
167
168
  const { active } = _getConcurrencyState();
168
169
  if (active > peak) peak = active;
169
170
  samples++;
@@ -1,3 +1,5 @@
1
+ import type * as __ManduBunTypes0 from "bun";
2
+ import type * as __ManduPluginsHooksTypes1 from "../plugins/hooks";
1
3
  /**
2
4
  * Mandu Bundler Types
3
5
  */
@@ -187,13 +189,13 @@ export interface BundlerOptions {
187
189
  * via `runDefineBundlerPlugin()` and fed into every `safeBuild(...)`
188
190
  * call-site; library users may populate it directly.
189
191
  */
190
- pluginBundlerPlugins?: readonly import("bun").BunPlugin[];
192
+ pluginBundlerPlugins?: readonly __ManduBunTypes0.BunPlugin[];
191
193
 
192
194
  /**
193
195
  * Phase 18.τ — fire per-build `onBundleComplete(stats)` hook after
194
196
  * `buildClientBundles()` finishes. Paired with `configHooks` for
195
197
  * config-level hooks. Zero-overhead when both are omitted.
196
198
  */
197
- plugins?: readonly import("../plugins/hooks").ManduPlugin[];
198
- configHooks?: Partial<import("../plugins/hooks").ManduHooks>;
199
+ plugins?: readonly __ManduPluginsHooksTypes1.ManduPlugin[];
200
+ configHooks?: Partial<__ManduPluginsHooksTypes1.ManduHooks>;
199
201
  }
@@ -114,7 +114,7 @@ export async function pruneHistory(
114
114
  /**
115
115
  * History 설정 로드 (향후 mandu.config.json에서 로드)
116
116
  */
117
- export async function loadHistoryConfig(rootDir: string): Promise<HistoryConfig> {
117
+ export async function loadHistoryConfig(_rootDir: string): Promise<HistoryConfig> {
118
118
  // 향후 mandu.config.json에서 로드하도록 확장
119
119
  // 현재는 기본값 반환
120
120
  return DEFAULT_HISTORY_CONFIG;
@@ -13,11 +13,19 @@ import { validateAndReport } from "../config";
13
13
  const MANDU_DIR = ".mandu";
14
14
  const SPEC_DIR = "spec";
15
15
  const MANIFEST_FILE = "routes.manifest.json";
16
- const SLOTS_DIR = "slots";
17
- const HISTORY_DIR = "history";
18
- const SNAPSHOTS_DIR = "snapshots";
19
-
20
- /**
16
+ const SLOTS_DIR = "slots";
17
+ const HISTORY_DIR = "history";
18
+ const SNAPSHOTS_DIR = "snapshots";
19
+
20
+ function reverseCopy<T>(items: readonly T[]): T[] {
21
+ const reversed: T[] = [];
22
+ for (let index = items.length - 1; index >= 0; index -= 1) {
23
+ reversed.push(items[index] as T);
24
+ }
25
+ return reversed;
26
+ }
27
+
28
+ /**
21
29
  * 스냅샷 ID 생성 (YYYYMMDD-HHmmss-xxx)
22
30
  */
23
31
  function generateSnapshotId(): string {
@@ -250,10 +258,11 @@ export async function listSnapshotIds(rootDir: string): Promise<string[]> {
250
258
  })
251
259
  );
252
260
 
253
- return entries
254
- .map((entry) => entry.replace(".snapshot.json", ""))
255
- .sort()
256
- .reverse(); // 최신 순
261
+ return reverseCopy(
262
+ entries
263
+ .map((entry) => entry.replace(".snapshot.json", ""))
264
+ .sort()
265
+ ); // 최신 순
257
266
  } catch {
258
267
  return [];
259
268
  }
@@ -88,10 +88,10 @@ export function Link({
88
88
  } catch {
89
89
  return;
90
90
  }
91
-
92
- // Client-side 네비게이션
93
- event.preventDefault();
94
- navigate(href, { replace, scroll });
91
+
92
+ // Client-side 네비게이션
93
+ event.preventDefault();
94
+ void navigate(href, { replace, scroll });
95
95
  },
96
96
  [href, replace, scroll, onClick]
97
97
  );
@@ -101,11 +101,11 @@ export function Link({
101
101
  if (!shouldPrefetch || prefetchedRef.current) return;
102
102
 
103
103
  try {
104
- const url = new URL(href, window.location.origin);
105
- if (url.origin === window.location.origin) {
106
- prefetch(href);
107
- prefetchedRef.current = true;
108
- }
104
+ const url = new URL(href, window.location.origin);
105
+ if (url.origin === window.location.origin) {
106
+ void prefetch(href);
107
+ prefetchedRef.current = true;
108
+ }
109
109
  } catch {
110
110
  // 무시
111
111
  }
@@ -1,3 +1,4 @@
1
+ import type * as __ManduReactTypes0 from "react";
1
2
  /**
2
3
  * Mandu 전역 타입 선언
3
4
  * 클라이언트 측 전역 상태의 타입 정의
@@ -37,7 +38,7 @@ declare global {
37
38
  __MANDU_ROOTS__?: Map<string, Root>;
38
39
 
39
40
  /** React 인스턴스 공유 */
40
- __MANDU_REACT__?: typeof import("react");
41
+ __MANDU_REACT__?: typeof __ManduReactTypes0;
41
42
 
42
43
  /**
43
44
  * Issue #193 — global SPA navigation toggle.
@@ -58,4 +59,4 @@ declare global {
58
59
  }
59
60
  }
60
61
 
61
- export {};
62
+