@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
@@ -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";
@@ -408,11 +409,12 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
408
409
  } = options;
409
410
 
410
411
  // 초기 빌드
411
- console.log("🔨 Initial client bundle build...");
412
- const initialBuild = await buildClientBundles(manifest, rootDir, {
413
- minify: false,
414
- sourcemap: true,
415
- reactCompiler,
412
+ console.log("🔨 Initial client bundle build...");
413
+ const initialBuild = await buildClientBundles(manifest, rootDir, {
414
+ mode: "development",
415
+ minify: false,
416
+ sourcemap: true,
417
+ reactCompiler,
416
418
  });
417
419
 
418
420
  if (initialBuild.success) {
@@ -1086,7 +1088,7 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1086
1088
  const handleContentChange = async (files: readonly string[]): Promise<void> => {
1087
1089
  if (files.length === 0) return;
1088
1090
  try {
1089
- const mod: typeof import("../content/collection") = await import("../content/collection");
1091
+ const mod: typeof __ManduContentCollectionTypes0 = await import("../content/collection");
1090
1092
  if (typeof mod.invalidateAllCollections === "function") {
1091
1093
  mod.invalidateAllCollections();
1092
1094
  }
@@ -1320,10 +1322,11 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1320
1322
  const targetIds = Array.from(new Set(islandRoots.map((r) => r.routeId)));
1321
1323
  const startTime = performance.now();
1322
1324
  try {
1323
- const result = await buildClientBundles(manifest, rootDir, {
1324
- minify: false,
1325
- sourcemap: true,
1326
- targetRouteIds: targetIds,
1325
+ const result = await buildClientBundles(manifest, rootDir, {
1326
+ mode: "development",
1327
+ minify: false,
1328
+ sourcemap: true,
1329
+ targetRouteIds: targetIds,
1327
1330
  reactCompiler,
1328
1331
  });
1329
1332
  const buildTime = performance.now() - startTime;
@@ -1378,10 +1381,11 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1378
1381
 
1379
1382
  try {
1380
1383
  // #185: framework 번들 (runtime/router/vendor/devtools) 스킵 — 사용자 코드 변경 시 불필요
1381
- const result = await buildClientBundles(manifest, rootDir, {
1382
- minify: false,
1383
- sourcemap: true,
1384
- skipFrameworkBundles: true,
1384
+ const result = await buildClientBundles(manifest, rootDir, {
1385
+ mode: "development",
1386
+ minify: false,
1387
+ sourcemap: true,
1388
+ skipFrameworkBundles: true,
1385
1389
  reactCompiler,
1386
1390
  });
1387
1391
 
@@ -1447,19 +1451,19 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1447
1451
  // SSR 모듈 변경 감지 (page.tsx, layout.tsx) — #151
1448
1452
  if (onSSRChange && serverModuleSet.has(normalizedPath)) {
1449
1453
  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
- }
1454
+ // Refresh the changed file's imports so the reverse graph
1455
+ // tracks refactors (a new import added to an SSR module).
1456
+ await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1457
+ await Promise.resolve(onSSRChange(normalizedPath));
1458
+ return;
1459
+ }
1456
1460
  // API 모듈 변경 감지 (route.ts)
1457
1461
  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
- }
1462
+ console.log(`\n🔄 API route changed: ${path.basename(changedFile)}`);
1463
+ await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1464
+ await Promise.resolve(onAPIChange(normalizedPath));
1465
+ return;
1466
+ }
1463
1467
  // Phase 7.0 R2 Agent D — route middleware change.
1464
1468
  // `app/**/middleware.ts` isn't in apiModuleSet (not registered as a
1465
1469
  // route handler) but reuses the API reload path: the underlying
@@ -1467,11 +1471,11 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1467
1471
  // import chain. Falls through to `onAPIChange` so the CLI can
1468
1472
  // reuse its existing `handleAPIChange` plumbing.
1469
1473
  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
- }
1474
+ console.log(`\n🔄 Middleware changed: ${path.basename(changedFile)}`);
1475
+ await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
1476
+ await Promise.resolve(onAPIChange(normalizedPath));
1477
+ return;
1478
+ }
1475
1479
  // #189 — reverse import-graph fallback.
1476
1480
  //
1477
1481
  // The changed file matched none of our direct dispatch sets.
@@ -1503,10 +1507,11 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1503
1507
 
1504
1508
  try {
1505
1509
  // 단일 island만 재빌드 (Runtime/Router/Vendor 스킵, #122)
1506
- const result = await buildClientBundles(manifest, rootDir, {
1507
- minify: false,
1508
- sourcemap: true,
1509
- targetRouteIds: [routeId],
1510
+ const result = await buildClientBundles(manifest, rootDir, {
1511
+ mode: "development",
1512
+ minify: false,
1513
+ sourcemap: true,
1514
+ targetRouteIds: [routeId],
1510
1515
  reactCompiler,
1511
1516
  });
1512
1517
 
@@ -2188,10 +2193,10 @@ export function createHMRServer(
2188
2193
  } catch {
2189
2194
  // 무시
2190
2195
  }
2191
- }
2192
- clients.clear();
2193
- server.stop();
2194
- },
2196
+ }
2197
+ clients.clear();
2198
+ void server.stop();
2199
+ },
2195
2200
  setRestartHandler: (handler: () => Promise<void>) => {
2196
2201
  restartHandler = handler;
2197
2202
  },
@@ -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
  */
@@ -121,9 +123,14 @@ export interface IslandFileEntry {
121
123
  /**
122
124
  * 번들러 옵션
123
125
  */
124
- export interface BundlerOptions {
125
- /** 코드 압축 여부 (기본: production에서 true) */
126
- minify?: boolean;
126
+ export interface BundlerOptions {
127
+ /**
128
+ * Build mode. This controls development-only transforms such as React
129
+ * Fast Refresh independently from minification.
130
+ */
131
+ mode?: "development" | "production";
132
+ /** 코드 압축 여부 (기본: production에서 true) */
133
+ minify?: boolean;
127
134
  /** 소스맵 생성 여부 */
128
135
  sourcemap?: boolean;
129
136
  /** 파일 감시 모드 */
@@ -187,13 +194,13 @@ export interface BundlerOptions {
187
194
  * via `runDefineBundlerPlugin()` and fed into every `safeBuild(...)`
188
195
  * call-site; library users may populate it directly.
189
196
  */
190
- pluginBundlerPlugins?: readonly import("bun").BunPlugin[];
197
+ pluginBundlerPlugins?: readonly __ManduBunTypes0.BunPlugin[];
191
198
 
192
199
  /**
193
200
  * Phase 18.τ — fire per-build `onBundleComplete(stats)` hook after
194
201
  * `buildClientBundles()` finishes. Paired with `configHooks` for
195
202
  * config-level hooks. Zero-overhead when both are omitted.
196
203
  */
197
- plugins?: readonly import("../plugins/hooks").ManduPlugin[];
198
- configHooks?: Partial<import("../plugins/hooks").ManduHooks>;
204
+ plugins?: readonly __ManduPluginsHooksTypes1.ManduPlugin[];
205
+ configHooks?: Partial<__ManduPluginsHooksTypes1.ManduHooks>;
199
206
  }
@@ -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
+
@@ -7,10 +7,6 @@ import { useState, useEffect, useCallback, useMemo, useSyncExternalStore } from
7
7
  import {
8
8
  subscribe,
9
9
  getRouterState,
10
- getCurrentRoute,
11
- getLoaderData,
12
- getActionData,
13
- getNavigationState,
14
10
  navigate,
15
11
  submitAction,
16
12
  type RouteInfo,
@@ -110,7 +110,7 @@ function noopDispose(): Disposer {
110
110
  */
111
111
  function scheduleLoad(hydrate: () => void): Disposer {
112
112
  let cancelled = false;
113
- Promise.resolve().then(() => {
113
+ void Promise.resolve().then(() => {
114
114
  if (!cancelled) hydrate();
115
115
  });
116
116
  return () => {
@@ -3,7 +3,7 @@
3
3
  * SPA 스타일 네비게이션을 위한 클라이언트 라우터
4
4
  */
5
5
 
6
- import type { ReactNode } from "react";
6
+
7
7
  import { startTransition } from "react";
8
8
  import {
9
9
  getManduData,
@@ -404,12 +404,12 @@ export async function navigate(
404
404
  function handlePopState(event: PopStateEvent): void {
405
405
  const state = event.state;
406
406
 
407
- if (state?.routeId) {
408
- // Mandu로 방문한 페이지 - 데이터 다시 fetch
409
- navigate(window.location.pathname + window.location.search, {
410
- replace: true,
411
- scroll: false,
412
- });
407
+ if (state?.routeId) {
408
+ // Mandu로 방문한 페이지 - 데이터 다시 fetch
409
+ void navigate(window.location.pathname + window.location.search, {
410
+ replace: true,
411
+ scroll: false,
412
+ });
413
413
  } else {
414
414
  // SPA 네비게이션 이력이 아닌 페이지 — 현재 라우터 상태 유지
415
415
  // (getManduRoute()는 SSR 초기값이라 SPA 네비게이션 후에는 stale)
@@ -576,10 +576,10 @@ function handleLinkClick(event: MouseEvent): void {
576
576
  // All clear — prevent the default full-page navigation and hand off
577
577
  // to the client-side router. `href` preserves the user's original
578
578
  // string (relative paths, fragments, query strings) so the router
579
- // can normalize as needed.
580
- event.preventDefault();
581
- navigate(href);
582
- }
579
+ // can normalize as needed.
580
+ event.preventDefault();
581
+ void navigate(href);
582
+ }
583
583
 
584
584
  // ========== Prefetch ==========
585
585
 
@@ -702,7 +702,7 @@ export async function submitAction(
702
702
  actionData: nextActionData,
703
703
  loaderData: result._revalidated ? result.loaderData as unknown : undefined,
704
704
  };
705
- } catch (error) {
705
+ } catch {
706
706
  if (controller.signal.aborted) return { ok: false };
707
707
 
708
708
  setRouterStateInternal({
package/src/client/rpc.ts CHANGED
@@ -50,7 +50,7 @@ export interface RpcClientOptions {
50
50
  * const { id } = await api.post({ body: { title: "New" } });
51
51
  * ```
52
52
  */
53
- export function createClient<TContract = unknown>(
53
+ export function createClient<_TContract = unknown>(
54
54
  path: string,
55
55
  options?: RpcClientOptions
56
56
  ): RpcMethods {
@@ -228,12 +228,12 @@ export function useFetch<T = unknown>(
228
228
  }
229
229
  }, [url, method, stableQuery, stableHeaders, stableBody, cacheTime, cacheKey, transform]);
230
230
 
231
- useEffect(() => {
232
- if (!immediate) return;
233
- if (data && dedupe) return;
234
- fetchData();
235
- return () => { abortRef.current?.abort(); };
236
- }, [fetchData, immediate, dedupe]);
231
+ useEffect(() => {
232
+ if (!immediate) return;
233
+ if (data && dedupe) return;
234
+ void fetchData();
235
+ return () => { abortRef.current?.abort(); };
236
+ }, [fetchData, immediate, dedupe]);
237
237
 
238
238
  const mutate = useCallback((updater: T | ((prev: T | null) => T)) => {
239
239
  setData(prev => typeof updater === "function" ? (updater as (prev: T | null) => T)(prev) : updater);
@@ -210,7 +210,7 @@ export function useSSE(options: UseSSEOptions = {}): UseSSEReturn {
210
210
  }
211
211
 
212
212
  // Start reading in a properly-yielding async loop
213
- (async () => {
213
+ void (async () => {
214
214
  const reader = body.getReader();
215
215
  const decoder = new TextDecoder();
216
216
 
@@ -218,7 +218,7 @@ export function useSSE(options: UseSSEOptions = {}): UseSSEReturn {
218
218
  while (true) {
219
219
  // Check for abort
220
220
  if (controller.signal.aborted) {
221
- reader.cancel();
221
+ await reader.cancel();
222
222
  break;
223
223
  }
224
224
 
@@ -227,7 +227,7 @@ export function useSSE(options: UseSSEOptions = {}): UseSSEReturn {
227
227
 
228
228
  // Check for abort again (may have been aborted during read)
229
229
  if (controller.signal.aborted) {
230
- reader.cancel();
230
+ await reader.cancel();
231
231
  break;
232
232
  }
233
233
 
@@ -342,7 +342,7 @@ export async function readStreamWithYield(
342
342
  try {
343
343
  while (true) {
344
344
  if (signal?.aborted) {
345
- reader.cancel();
345
+ await reader.cancel();
346
346
  break;
347
347
  }
348
348
 
@@ -350,7 +350,7 @@ export async function readStreamWithYield(
350
350
  if (done) break;
351
351
 
352
352
  if (signal?.aborted) {
353
- reader.cancel();
353
+ await reader.cancel();
354
354
  break;
355
355
  }
356
356
 
@@ -7,7 +7,6 @@
7
7
  */
8
8
 
9
9
  import {
10
- MCP_SERVER_STATUS,
11
10
  type McpServerStatusMetadata,
12
11
  } from "./symbols.js";
13
12
 
@@ -15,7 +15,7 @@ import { isGuardRuleLike, type GuardRule as CustomGuardRule } from "../guard/def
15
15
  * Creates a schema that warns about unknown keys instead of failing
16
16
  * This provides the benefits of .strict() while maintaining compatibility
17
17
  */
18
- function strictWithWarnings<T extends z.ZodRawShape>(
18
+ function _strictWithWarnings<T extends z.ZodRawShape>(
19
19
  schema: z.ZodObject<T>,
20
20
  schemaName: string
21
21
  ): z.ZodEffects<z.ZodObject<T>> {
@@ -237,9 +237,9 @@ const BuildBudgetConfigSchema = z
237
237
  /**
238
238
  * Build 설정 스키마 (strict)
239
239
  */
240
- const BuildConfigSchema = z
241
- .object({
242
- outDir: z.string().default(".mandu"),
240
+ const BuildConfigSchema = z
241
+ .object({
242
+ outDir: z.string().default(".mandu/client"),
243
243
  minify: z.boolean().default(true),
244
244
  sourcemap: z.boolean().default(false),
245
245
  splitting: z.boolean().default(false),
@@ -150,7 +150,7 @@ export async function watchConfig(
150
150
 
151
151
  debounceTimer = setTimeout(() => {
152
152
  debounceTimer = null;
153
- doReload(changedPath);
153
+ void doReload(changedPath);
154
154
  }, debounceMs);
155
155
  };
156
156
 
@@ -607,9 +607,8 @@ interface RenderedMarkdown {
607
607
  * remark/rehype plugin chain; when any piece is missing, falls back
608
608
  * to returning a `<pre>` shell so the caller gets a stable API.
609
609
  *
610
- * We go through `Function("return import(...)")` instead of a direct
611
- * dynamic `import()` so TS doesn't resolve the optional modules
612
- * during typecheck — they are NOT in `@mandujs/core` deps by design.
610
+ * We use a variable dynamic import so TS doesn't resolve the optional
611
+ * modules during typecheck they are NOT in `@mandujs/core` deps by design.
613
612
  *
614
613
  * The `options.remarkPlugins` / `options.rehypePlugins` arrays let
615
614
  * docs sites add `rehype-slug`, `rehype-autolink-headings`, `shiki`,
@@ -621,15 +620,12 @@ async function renderMarkdownSafe(
621
620
  ): Promise<RenderedMarkdown> {
622
621
  const { remarkPlugins = [], rehypePlugins = [], silent = false } = options;
623
622
 
624
- // Passing the module specifier through a Function-wrapped dynamic
625
- // import keeps TypeScript from erroring on optional peer deps; if
626
- // any module is missing we fall through to the raw-markdown path.
627
- const tryImport = async (id: string): Promise<unknown> => {
628
- try {
629
- return await (Function("x", "return import(x)") as (x: string) => Promise<unknown>)(id);
630
- } catch {
631
- return null;
632
- }
623
+ const tryImport = async (id: string): Promise<unknown> => {
624
+ try {
625
+ return await import(id);
626
+ } catch {
627
+ return null;
628
+ }
633
629
  };
634
630
 
635
631
  const unified = (await tryImport("unified")) as
@@ -253,11 +253,11 @@ export class ContentLayer {
253
253
  /**
254
254
  * 리소스 정리
255
255
  */
256
- dispose(): void {
257
- this.dataStore.dispose();
258
- this.watcher?.close();
259
- }
260
- }
256
+ dispose(): void {
257
+ this.dataStore.dispose();
258
+ void this.watcher?.close();
259
+ }
260
+ }
261
261
 
262
262
  /**
263
263
  * ContentLayer 팩토리
@@ -131,7 +131,7 @@ export class ContentDataStore {
131
131
  }
132
132
 
133
133
  this.saveTimer = setTimeout(() => {
134
- this.save();
134
+ void this.save();
135
135
  }, this.options.saveDebounce);
136
136
  }
137
137
  }
@@ -217,7 +217,7 @@ export class ContentDataStore {
217
217
 
218
218
  // 남은 변경사항 저장
219
219
  if (this.dirty && this.options.filePath) {
220
- this.save();
220
+ void this.save();
221
221
  }
222
222
  }
223
223
 
@@ -13,7 +13,6 @@ import type {
13
13
  ContentWatcher,
14
14
  ParseDataOptions,
15
15
  RenderedContent,
16
- ValidationError as ContentValidationError,
17
16
  } from "./types";
18
17
  import { ValidationError } from "./types";
19
18
  import { generateDigest } from "./digest";
@@ -98,7 +98,7 @@ export function file(options: FileLoaderOptions): Loader {
98
98
  name: "file",
99
99
 
100
100
  async load(context: LoaderContext): Promise<void> {
101
- const { store, config, logger, parseData, generateDigest } = context;
101
+ const { store, config, logger, parseData, generateDigest: _generateDigest } = context;
102
102
 
103
103
  // 절대 경로 계산
104
104
  const absolutePath = path.isAbsolute(filePath)