@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.
- package/package.json +38 -2
- package/src/a11y/run-audit.ts +6 -6
- package/src/brain/adapters/oauth-flow.ts +1 -1
- package/src/brain/architecture/analyzer.ts +0 -2
- package/src/brain/credentials.ts +2 -2
- package/src/bundler/__tests__/build-runner.ts +13 -1
- package/src/bundler/__tests__/extended-watch.test.ts +3 -2
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -12
- package/src/bundler/build.test.ts +30 -33
- package/src/bundler/build.ts +56 -27
- package/src/bundler/dev.ts +44 -39
- package/src/bundler/prerender.ts +63 -0
- package/src/bundler/safe-build.test.ts +2 -1
- package/src/bundler/types.ts +13 -6
- package/src/change/history.ts +1 -1
- package/src/change/snapshot.ts +18 -9
- package/src/client/Link.tsx +9 -9
- package/src/client/globals.ts +3 -2
- package/src/client/hooks.ts +0 -4
- package/src/client/hydrate.ts +1 -1
- package/src/client/router.ts +12 -12
- package/src/client/rpc.ts +1 -1
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-sse.ts +5 -5
- package/src/config/mcp-status.ts +0 -1
- package/src/config/validate.ts +4 -4
- package/src/config/watcher.ts +1 -1
- package/src/content/collection.ts +8 -12
- package/src/content/content-layer.ts +5 -5
- package/src/content/data-store.ts +2 -2
- package/src/content/loader-context.ts +0 -1
- package/src/content/loaders/file.ts +1 -1
- package/src/content/loaders/glob.ts +4 -4
- package/src/content/loaders/types.ts +1 -1
- package/src/content/meta-store.ts +1 -1
- package/src/content/types.ts +1 -1
- package/src/contract/define.ts +11 -12
- package/src/contract/index.ts +3 -3
- package/src/contract/protection.ts +2 -2
- package/src/contract/types.ts +1 -1
- package/src/contract/validator.ts +0 -1
- package/src/db/__tests__/db.test.ts +4 -7
- package/src/db/index.ts +138 -51
- package/src/db/migrations/index.ts +3 -0
- package/src/db/migrations/lock.ts +78 -23
- package/src/db/migrations/runner.ts +118 -101
- package/src/deploy/cache.ts +1 -0
- package/src/design/extract.ts +2 -1
- package/src/design/patch.ts +0 -1
- package/src/devtools/ai/mcp-connector.ts +24 -24
- package/src/devtools/client/components/kitchen-root.tsx +1 -1
- package/src/devtools/client/components/panel/errors-panel.tsx +1 -1
- package/src/devtools/client/state-manager.ts +1 -1
- package/src/devtools/init.ts +2 -3
- package/src/devtools/server/source-context.ts +1 -1
- package/src/devtools/worker/redaction-worker.ts +8 -4
- package/src/devtools/worker/worker-manager.ts +4 -3
- package/src/error/classifier.ts +1 -1
- package/src/error/domains.ts +1 -1
- package/src/filling/sse.ts +5 -5
- package/src/generator/generate.ts +2 -2
- package/src/guard/analyzer.ts +1 -1
- package/src/guard/ast-analyzer.ts +7 -7
- package/src/guard/auto-correct.ts +2 -2
- package/src/guard/config-guard.ts +0 -2
- package/src/guard/contract-guard.ts +1 -1
- package/src/guard/decision-memory.ts +2 -2
- package/src/guard/design-inline-class.ts +2 -1
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -41
- package/src/guard/negotiation.ts +3 -3
- package/src/guard/reporter.ts +1 -4
- package/src/guard/semantic-slots.ts +2 -2
- package/src/guard/suggestions.ts +2 -2
- package/src/guard/watcher.ts +5 -4
- package/src/index.ts +5 -5
- package/src/intent/index.ts +1 -1
- package/src/kitchen/api/agent-devtools-api.ts +544 -0
- package/src/kitchen/api/contract-api.ts +1 -1
- package/src/kitchen/api/file-api.ts +3 -3
- package/src/kitchen/kitchen-handler.ts +63 -38
- package/src/kitchen/kitchen-ui.ts +346 -62
- package/src/lockfile/validate.ts +0 -1
- package/src/openapi/generator.ts +2 -1
- package/src/openapi/openapi.test.ts +43 -14
- package/src/perf/user-marks.ts +1 -1
- package/src/plugins/registry.ts +1 -2
- package/src/plugins/types.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -15
- package/src/resource/ddl/__tests__/emit.test.ts +24 -0
- package/src/resource/ddl/diff.ts +0 -1
- package/src/resource/ddl/emit.ts +12 -1
- package/src/resource/generator-repo.ts +40 -20
- package/src/resource/generator.ts +1 -1
- package/src/resource/generators/contract.ts +1 -1
- package/src/resource/schema.ts +2 -1
- package/src/router/fs-routes.ts +6 -6
- package/src/router/fs-scanner.ts +1 -1
- package/src/runtime/env.ts +1 -1
- package/src/runtime/image-handler.ts +2 -1
- package/src/runtime/middleware.ts +2 -1
- package/src/runtime/server.ts +67 -60
- package/src/runtime/ssr.ts +3 -3
- package/src/runtime/streaming-ssr.ts +25 -27
- package/src/scheduler/validate.ts +1 -1
- package/src/seo/integration/ssr.ts +2 -3
- package/src/seo/render/opengraph.ts +1 -1
- package/src/seo/resolve/index.ts +0 -4
- package/src/slot/corrector.ts +1 -1
- package/src/slot/validator.ts +4 -4
- package/src/watcher/reporter.ts +1 -1
- package/src/watcher/watcher.ts +19 -19
package/src/bundler/dev.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
},
|
package/src/bundler/prerender.ts
CHANGED
|
@@ -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
|
-
|
|
166
|
+
for (;;) {
|
|
167
|
+
if (stop) break;
|
|
167
168
|
const { active } = _getConcurrencyState();
|
|
168
169
|
if (active > peak) peak = active;
|
|
169
170
|
samples++;
|
package/src/bundler/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
126
|
-
|
|
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
|
|
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
|
|
198
|
-
configHooks?: Partial<
|
|
204
|
+
plugins?: readonly __ManduPluginsHooksTypes1.ManduPlugin[];
|
|
205
|
+
configHooks?: Partial<__ManduPluginsHooksTypes1.ManduHooks>;
|
|
199
206
|
}
|
package/src/change/history.ts
CHANGED
|
@@ -114,7 +114,7 @@ export async function pruneHistory(
|
|
|
114
114
|
/**
|
|
115
115
|
* History 설정 로드 (향후 mandu.config.json에서 로드)
|
|
116
116
|
*/
|
|
117
|
-
export async function loadHistoryConfig(
|
|
117
|
+
export async function loadHistoryConfig(_rootDir: string): Promise<HistoryConfig> {
|
|
118
118
|
// 향후 mandu.config.json에서 로드하도록 확장
|
|
119
119
|
// 현재는 기본값 반환
|
|
120
120
|
return DEFAULT_HISTORY_CONFIG;
|
package/src/change/snapshot.ts
CHANGED
|
@@ -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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
return reverseCopy(
|
|
262
|
+
entries
|
|
263
|
+
.map((entry) => entry.replace(".snapshot.json", ""))
|
|
264
|
+
.sort()
|
|
265
|
+
); // 최신 순
|
|
257
266
|
} catch {
|
|
258
267
|
return [];
|
|
259
268
|
}
|
package/src/client/Link.tsx
CHANGED
|
@@ -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
|
}
|
package/src/client/globals.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
62
|
+
|
package/src/client/hooks.ts
CHANGED
package/src/client/hydrate.ts
CHANGED
|
@@ -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 () => {
|
package/src/client/router.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SPA 스타일 네비게이션을 위한 클라이언트 라우터
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
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<
|
|
53
|
+
export function createClient<_TContract = unknown>(
|
|
54
54
|
path: string,
|
|
55
55
|
options?: RpcClientOptions
|
|
56
56
|
): RpcMethods {
|
package/src/client/use-fetch.ts
CHANGED
|
@@ -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);
|
package/src/client/use-sse.ts
CHANGED
|
@@ -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
|
|
package/src/config/mcp-status.ts
CHANGED
package/src/config/validate.ts
CHANGED
|
@@ -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
|
|
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),
|
package/src/config/watcher.ts
CHANGED
|
@@ -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
|
|
611
|
-
*
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
|
@@ -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)
|