@mandujs/core 0.54.9 → 0.54.11
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 +1 -1
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +81 -62
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +120 -88
- package/src/bundler/build.ts +511 -511
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/filling/context.ts +17 -17
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +66 -65
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +3 -3
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +192 -89
- package/src/router/client-entry.ts +516 -400
- package/src/router/fs-routes.ts +16 -16
- package/src/router/fs-scanner.ts +16 -27
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +103 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/server.ts +243 -243
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
package/src/runtime/server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Server, ServerWebSocket } from "bun";
|
|
1
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
2
2
|
import type { RoutesManifest, RouteSpec, HydrationConfig, StaticParamSetSchema } from "../spec/schema";
|
|
3
3
|
import type { BundleManifest } from "../bundler/types";
|
|
4
|
-
import type { ManduFilling, RenderMode } from "../filling/filling";
|
|
5
|
-
import { ManduContext, CookieManager } from "../filling/context";
|
|
6
|
-
import { Router } from "./router";
|
|
7
|
-
import { renderSSR, resolveAsyncElement } from "./ssr";
|
|
4
|
+
import type { ManduFilling, RenderMode } from "../filling/filling";
|
|
5
|
+
import { ManduContext, CookieManager } from "../filling/context";
|
|
6
|
+
import { Router } from "./router";
|
|
7
|
+
import { renderSSR, resolveAsyncElement } from "./ssr";
|
|
8
8
|
import {
|
|
9
9
|
resolveMetadata,
|
|
10
10
|
renderMetadata,
|
|
@@ -50,34 +50,34 @@ import {
|
|
|
50
50
|
isCorsRequest,
|
|
51
51
|
} from "./cors";
|
|
52
52
|
import { validateImportPath } from "./security";
|
|
53
|
-
import {
|
|
54
|
-
createRuntimeDevtoolsAdapter,
|
|
55
|
-
recordRuntimeRequest,
|
|
56
|
-
shouldRecordRuntimeRequest,
|
|
57
|
-
type RuntimeDevtoolsAdapter,
|
|
58
|
-
type RuntimeKitchenHandler,
|
|
59
|
-
} from "./devtools-adapter";
|
|
60
|
-
import {
|
|
61
|
-
createRuntimeObservabilityLifecycle,
|
|
62
|
-
type RuntimeObservabilityLifecycle,
|
|
63
|
-
} from "./observability-lifecycle";
|
|
64
|
-
import {
|
|
65
|
-
handleOpenAPIRequest,
|
|
66
|
-
isOpenAPIEndpointEnabled,
|
|
67
|
-
resolveOpenAPIEndpointSettings,
|
|
68
|
-
type OpenAPIEndpointSettings,
|
|
69
|
-
} from "./openapi-endpoint";
|
|
70
|
-
import {
|
|
71
|
-
type MiddlewareFn,
|
|
72
|
-
type MiddlewareConfig,
|
|
73
|
-
loadMiddlewareSync,
|
|
74
|
-
} from "./middleware";
|
|
75
|
-
import {
|
|
76
|
-
buildRequestMiddlewareChain,
|
|
77
|
-
runRequestMiddleware,
|
|
78
|
-
type ComposedHandler,
|
|
79
|
-
} from "./request-middleware";
|
|
80
|
-
import type { Middleware } from "../middleware/define";
|
|
53
|
+
import {
|
|
54
|
+
createRuntimeDevtoolsAdapter,
|
|
55
|
+
recordRuntimeRequest,
|
|
56
|
+
shouldRecordRuntimeRequest,
|
|
57
|
+
type RuntimeDevtoolsAdapter,
|
|
58
|
+
type RuntimeKitchenHandler,
|
|
59
|
+
} from "./devtools-adapter";
|
|
60
|
+
import {
|
|
61
|
+
createRuntimeObservabilityLifecycle,
|
|
62
|
+
type RuntimeObservabilityLifecycle,
|
|
63
|
+
} from "./observability-lifecycle";
|
|
64
|
+
import {
|
|
65
|
+
handleOpenAPIRequest,
|
|
66
|
+
isOpenAPIEndpointEnabled,
|
|
67
|
+
resolveOpenAPIEndpointSettings,
|
|
68
|
+
type OpenAPIEndpointSettings,
|
|
69
|
+
} from "./openapi-endpoint";
|
|
70
|
+
import {
|
|
71
|
+
type MiddlewareFn,
|
|
72
|
+
type MiddlewareConfig,
|
|
73
|
+
loadMiddlewareSync,
|
|
74
|
+
} from "./middleware";
|
|
75
|
+
import {
|
|
76
|
+
buildRequestMiddlewareChain,
|
|
77
|
+
runRequestMiddleware,
|
|
78
|
+
type ComposedHandler,
|
|
79
|
+
} from "./request-middleware";
|
|
80
|
+
import type { Middleware } from "../middleware/define";
|
|
81
81
|
// Phase 18.λ — scheduler wiring (statically imported so `startServer` stays
|
|
82
82
|
// synchronous; the cost of unused code is trivial — `defineCron` is a thin
|
|
83
83
|
// wrapper around `Bun.cron`).
|
|
@@ -85,16 +85,16 @@ import { defineCron as schedulerDefineCron, type CronDef, type CronRegistration
|
|
|
85
85
|
import { setActiveSchedulerRegistration } from "../middleware/scheduler-cron";
|
|
86
86
|
import { createFetchHandler } from "./handler";
|
|
87
87
|
import { wrapBunWebSocket, type WSHandlers, type WSUpgradeData } from "../filling/ws";
|
|
88
|
-
import { handleImageRequest } from "./image-handler";
|
|
89
|
-
import {
|
|
90
|
-
serveStaticFile,
|
|
91
|
-
computeStrongEtag,
|
|
92
|
-
computeStaticCacheControl,
|
|
93
|
-
matchesEtag,
|
|
94
|
-
} from "./static-files";
|
|
95
|
-
export { __clearStaticEtagCacheForTests } from "./static-files";
|
|
96
|
-
import { extractShellHtml, createPPRResponse } from "./ppr";
|
|
97
|
-
import { renderPageResponse } from "./page-render-response";
|
|
88
|
+
import { handleImageRequest } from "./image-handler";
|
|
89
|
+
import {
|
|
90
|
+
serveStaticFile,
|
|
91
|
+
computeStrongEtag,
|
|
92
|
+
computeStaticCacheControl,
|
|
93
|
+
matchesEtag,
|
|
94
|
+
} from "./static-files";
|
|
95
|
+
export { __clearStaticEtagCacheForTests } from "./static-files";
|
|
96
|
+
import { extractShellHtml, createPPRResponse } from "./ppr";
|
|
97
|
+
import { renderPageResponse } from "./page-render-response";
|
|
98
98
|
import { isRedirectResponse } from "./redirect";
|
|
99
99
|
import { isNotFoundResponse } from "./not-found";
|
|
100
100
|
import { newId } from "../id";
|
|
@@ -315,8 +315,8 @@ function createRateLimitResponse(decision: RateLimitDecision, options: Normalize
|
|
|
315
315
|
return appendRateLimitHeaders(response, decision, options);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
// ========== Server Options ==========
|
|
319
|
-
export interface ServerOptions {
|
|
318
|
+
// ========== Server Options ==========
|
|
319
|
+
export interface ServerOptions {
|
|
320
320
|
port?: number;
|
|
321
321
|
hostname?: string;
|
|
322
322
|
/** 프로젝트 루트 디렉토리 */
|
|
@@ -599,15 +599,15 @@ export interface ServerOptions {
|
|
|
599
599
|
silent?: boolean;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
export interface ManduServer {
|
|
603
|
-
server: Server<undefined>;
|
|
604
|
-
router: Router;
|
|
605
|
-
/** 이 서버 인스턴스의 레지스트리 */
|
|
606
|
-
registry: ServerRegistry;
|
|
607
|
-
/** Replace the live dispatch table after FS routes change in dev mode. */
|
|
608
|
-
updateManifest: (nextManifest: RoutesManifest) => void;
|
|
609
|
-
stop: () => void;
|
|
610
|
-
}
|
|
602
|
+
export interface ManduServer {
|
|
603
|
+
server: Server<undefined>;
|
|
604
|
+
router: Router;
|
|
605
|
+
/** 이 서버 인스턴스의 레지스트리 */
|
|
606
|
+
registry: ServerRegistry;
|
|
607
|
+
/** Replace the live dispatch table after FS routes change in dev mode. */
|
|
608
|
+
updateManifest: (nextManifest: RoutesManifest) => void;
|
|
609
|
+
stop: () => void;
|
|
610
|
+
}
|
|
611
611
|
|
|
612
612
|
export type ApiHandler = (req: Request, params: Record<string, string>) => Response | Promise<Response>;
|
|
613
613
|
export type PageLoader = () => Promise<{ default: React.ComponentType<{ params: Record<string, string> }> }>;
|
|
@@ -746,8 +746,8 @@ export interface ServerRegistrySettings {
|
|
|
746
746
|
* in-head `<script>` and the 500-response HTML overlay. No-op in prod.
|
|
747
747
|
*/
|
|
748
748
|
errorOverlay?: boolean;
|
|
749
|
-
/** Runtime observability lifecycle: endpoints, tracing, perf, counters. */
|
|
750
|
-
observability?: RuntimeObservabilityLifecycle;
|
|
749
|
+
/** Runtime observability lifecycle: endpoints, tracing, perf, counters. */
|
|
750
|
+
observability?: RuntimeObservabilityLifecycle;
|
|
751
751
|
/**
|
|
752
752
|
* Production OpenAPI endpoint — resolved from `ServerOptions.openapi`.
|
|
753
753
|
* `undefined` means the endpoint is disabled (hot path branch-free).
|
|
@@ -832,10 +832,10 @@ export class ServerRegistry {
|
|
|
832
832
|
* to the framework's built-in 404 JSON error.
|
|
833
833
|
*/
|
|
834
834
|
notFoundHandler: PageHandler | null = null;
|
|
835
|
-
/** Runtime devtools adapter (dev mode only) */
|
|
836
|
-
devtoolsAdapter: RuntimeDevtoolsAdapter | null = null;
|
|
837
|
-
/** Kitchen dev dashboard handler (dev mode only, kept for compatibility) */
|
|
838
|
-
kitchen: RuntimeKitchenHandler | null = null;
|
|
835
|
+
/** Runtime devtools adapter (dev mode only) */
|
|
836
|
+
devtoolsAdapter: RuntimeDevtoolsAdapter | null = null;
|
|
837
|
+
/** Kitchen dev dashboard handler (dev mode only, kept for compatibility) */
|
|
838
|
+
kitchen: RuntimeKitchenHandler | null = null;
|
|
839
839
|
/** 라우트별 캐시 옵션 (filling.loader()의 cacheOptions에서 등록) */
|
|
840
840
|
readonly cacheOptions: Map<string, { revalidate?: number; staleWhileRevalidate?: number; tags?: string[] }> = new Map();
|
|
841
841
|
/** 라우트별 렌더 모드 */
|
|
@@ -1272,9 +1272,9 @@ function createDefaultAppFactory(registry: ServerRegistry) {
|
|
|
1272
1272
|
};
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
|
-
const INTERNAL_CACHE_ENDPOINT = "/_mandu/cache";
|
|
1276
|
-
|
|
1277
|
-
// ========== Request Handler ==========
|
|
1275
|
+
const INTERNAL_CACHE_ENDPOINT = "/_mandu/cache";
|
|
1276
|
+
|
|
1277
|
+
// ========== Request Handler ==========
|
|
1278
1278
|
|
|
1279
1279
|
function unauthorizedControlResponse(): Response {
|
|
1280
1280
|
return Response.json({ error: "Unauthorized runtime control request" }, { status: 401 });
|
|
@@ -1362,29 +1362,29 @@ async function handleInternalCacheControlRequest(
|
|
|
1362
1362
|
return new Response(JSON.stringify({ error: "Method not allowed", allowed: ["GET", "POST", "DELETE"], hint: `Received '${req.method}'. This endpoint accepts GET (read stats), POST (clear by path/tag), and DELETE (clear all).` }), { status: 405, headers: { "Content-Type": "application/json", "Allow": "GET, POST, DELETE" } });
|
|
1363
1363
|
}
|
|
1364
1364
|
|
|
1365
|
-
async function handleRequest(req: Request, router: Router, registry: ServerRegistry): Promise<Response> {
|
|
1366
|
-
const requestStart = Date.now();
|
|
1367
|
-
// Phase 1-4: Correlation ID — 한 요청에서 발생하는 모든 이벤트를 추적
|
|
1368
|
-
const correlationId = req.headers.get("x-mandu-request-id") ?? newId();
|
|
1369
|
-
|
|
1370
|
-
const observability = registry.settings.observability;
|
|
1371
|
-
if (observability) {
|
|
1372
|
-
return await observability.runRequest(
|
|
1373
|
-
req,
|
|
1374
|
-
requestStart,
|
|
1375
|
-
correlationId,
|
|
1376
|
-
() => handleRequestObserved(req, router, registry, requestStart, correlationId)
|
|
1377
|
-
);
|
|
1378
|
-
}
|
|
1379
|
-
return await handleRequestObserved(req, router, registry, requestStart, correlationId);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
/**
|
|
1383
|
-
* Inner request handler. Observability lifecycle wrapping happens at
|
|
1384
|
-
* {@link handleRequest}; this function keeps response post-processing in one
|
|
1385
|
-
* place without owning tracing or metrics wiring directly.
|
|
1386
|
-
*/
|
|
1387
|
-
async function handleRequestObserved(
|
|
1365
|
+
async function handleRequest(req: Request, router: Router, registry: ServerRegistry): Promise<Response> {
|
|
1366
|
+
const requestStart = Date.now();
|
|
1367
|
+
// Phase 1-4: Correlation ID — 한 요청에서 발생하는 모든 이벤트를 추적
|
|
1368
|
+
const correlationId = req.headers.get("x-mandu-request-id") ?? newId();
|
|
1369
|
+
|
|
1370
|
+
const observability = registry.settings.observability;
|
|
1371
|
+
if (observability) {
|
|
1372
|
+
return await observability.runRequest(
|
|
1373
|
+
req,
|
|
1374
|
+
requestStart,
|
|
1375
|
+
correlationId,
|
|
1376
|
+
() => handleRequestObserved(req, router, registry, requestStart, correlationId)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
return await handleRequestObserved(req, router, registry, requestStart, correlationId);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Inner request handler. Observability lifecycle wrapping happens at
|
|
1384
|
+
* {@link handleRequest}; this function keeps response post-processing in one
|
|
1385
|
+
* place without owning tracing or metrics wiring directly.
|
|
1386
|
+
*/
|
|
1387
|
+
async function handleRequestObserved(
|
|
1388
1388
|
req: Request,
|
|
1389
1389
|
router: Router,
|
|
1390
1390
|
registry: ServerRegistry,
|
|
@@ -1396,24 +1396,24 @@ async function handleRequestObserved(
|
|
|
1396
1396
|
if (!result.ok) {
|
|
1397
1397
|
const errorResponse = errorToResponse(result.error, registry.settings.isDev);
|
|
1398
1398
|
if (registry.settings.isDev) {
|
|
1399
|
-
// #177: dev 모드 에러 응답도 캐시 방지
|
|
1400
|
-
if (!errorResponse.headers.has("Cache-Control")) {
|
|
1401
|
-
errorResponse.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
1402
|
-
}
|
|
1403
|
-
const url = new URL(req.url);
|
|
1404
|
-
const p = url.pathname;
|
|
1405
|
-
if (shouldRecordRuntimeRequest(p)) {
|
|
1406
|
-
const elapsed = Date.now() - requestStart;
|
|
1407
|
-
registry.settings.observability?.recordDevRequest({
|
|
1408
|
-
req,
|
|
1409
|
-
path: p,
|
|
1410
|
-
status: errorResponse.status,
|
|
1411
|
-
duration: elapsed,
|
|
1412
|
-
correlationId,
|
|
1413
|
-
error: true,
|
|
1414
|
-
recordRequest: recordRuntimeRequest,
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1399
|
+
// #177: dev 모드 에러 응답도 캐시 방지
|
|
1400
|
+
if (!errorResponse.headers.has("Cache-Control")) {
|
|
1401
|
+
errorResponse.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
1402
|
+
}
|
|
1403
|
+
const url = new URL(req.url);
|
|
1404
|
+
const p = url.pathname;
|
|
1405
|
+
if (shouldRecordRuntimeRequest(p)) {
|
|
1406
|
+
const elapsed = Date.now() - requestStart;
|
|
1407
|
+
registry.settings.observability?.recordDevRequest({
|
|
1408
|
+
req,
|
|
1409
|
+
path: p,
|
|
1410
|
+
status: errorResponse.status,
|
|
1411
|
+
duration: elapsed,
|
|
1412
|
+
correlationId,
|
|
1413
|
+
error: true,
|
|
1414
|
+
recordRequest: recordRuntimeRequest,
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
1417
|
}
|
|
1418
1418
|
// Phase 18.μ — stamp locale hint on error responses too.
|
|
1419
1419
|
const errI18nState = i18nRequestState.get(req);
|
|
@@ -1437,21 +1437,21 @@ async function handleRequestObserved(
|
|
|
1437
1437
|
result.value.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
1438
1438
|
}
|
|
1439
1439
|
|
|
1440
|
-
if (shouldRecordRuntimeRequest(p)) {
|
|
1441
|
-
const elapsed = Date.now() - requestStart;
|
|
1442
|
-
const status = result.value.status;
|
|
1443
|
-
const cacheHdr = result.value.headers.get("X-Mandu-Cache") ?? "";
|
|
1444
|
-
registry.settings.observability?.recordDevRequest({
|
|
1445
|
-
req,
|
|
1446
|
-
path: p,
|
|
1447
|
-
status,
|
|
1448
|
-
duration: elapsed,
|
|
1449
|
-
correlationId,
|
|
1450
|
-
cacheStatus: cacheHdr || undefined,
|
|
1451
|
-
recordRequest: recordRuntimeRequest,
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1440
|
+
if (shouldRecordRuntimeRequest(p)) {
|
|
1441
|
+
const elapsed = Date.now() - requestStart;
|
|
1442
|
+
const status = result.value.status;
|
|
1443
|
+
const cacheHdr = result.value.headers.get("X-Mandu-Cache") ?? "";
|
|
1444
|
+
registry.settings.observability?.recordDevRequest({
|
|
1445
|
+
req,
|
|
1446
|
+
path: p,
|
|
1447
|
+
status,
|
|
1448
|
+
duration: elapsed,
|
|
1449
|
+
correlationId,
|
|
1450
|
+
cacheStatus: cacheHdr || undefined,
|
|
1451
|
+
recordRequest: recordRuntimeRequest,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
1455
|
|
|
1456
1456
|
// Phase 18.μ — stamp locale-sensitive caching hints onto the final
|
|
1457
1457
|
// response. `Vary: Accept-Language, Cookie` ensures upstream caches
|
|
@@ -2232,26 +2232,26 @@ async function renderPageSSR(
|
|
|
2232
2232
|
|
|
2233
2233
|
// Island 래핑: 레이아웃 적용 전에 페이지 콘텐츠만 island div로 감쌈
|
|
2234
2234
|
// 이렇게 하면 레이아웃은 island 바깥에 위치하여 하이드레이션 시 레이아웃이 유지됨
|
|
2235
|
-
const needsIslandHydration = !!(
|
|
2236
|
-
route.hydration &&
|
|
2237
|
-
route.hydration.strategy !== "none" &&
|
|
2238
|
-
settings.bundleManifest
|
|
2239
|
-
);
|
|
2240
|
-
const routeBundle = settings.bundleManifest?.bundles[route.id];
|
|
2241
|
-
const bundleSrc = routeBundle?.js ? `${routeBundle.js}?t=${Date.now()}` : "";
|
|
2242
|
-
const needsIslandWrap = needsIslandHydration && bundleSrc.length > 0;
|
|
2243
|
-
|
|
2244
|
-
if (needsIslandHydration && !needsIslandWrap && settings.isDev) {
|
|
2245
|
-
console.warn(
|
|
2246
|
-
`[Mandu] Hydration requested for route "${route.id}" but no client bundle was found. ` +
|
|
2247
|
-
`Run mandu build/generate and ensure the route has a clientModule.`,
|
|
2248
|
-
);
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
if (needsIslandWrap) {
|
|
2252
|
-
const priority = route.hydration!.priority || "visible";
|
|
2253
|
-
app = React.createElement("div", {
|
|
2254
|
-
"data-mandu-island": route.id,
|
|
2235
|
+
const needsIslandHydration = !!(
|
|
2236
|
+
route.hydration &&
|
|
2237
|
+
route.hydration.strategy !== "none" &&
|
|
2238
|
+
settings.bundleManifest
|
|
2239
|
+
);
|
|
2240
|
+
const routeBundle = settings.bundleManifest?.bundles[route.id];
|
|
2241
|
+
const bundleSrc = routeBundle?.js ? `${routeBundle.js}?t=${Date.now()}` : "";
|
|
2242
|
+
const needsIslandWrap = needsIslandHydration && bundleSrc.length > 0;
|
|
2243
|
+
|
|
2244
|
+
if (needsIslandHydration && !needsIslandWrap && settings.isDev) {
|
|
2245
|
+
console.warn(
|
|
2246
|
+
`[Mandu] Hydration requested for route "${route.id}" but no client bundle was found. ` +
|
|
2247
|
+
`Run mandu build/generate and ensure the route has a clientModule.`,
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
if (needsIslandWrap) {
|
|
2252
|
+
const priority = route.hydration!.priority || "visible";
|
|
2253
|
+
app = React.createElement("div", {
|
|
2254
|
+
"data-mandu-island": route.id,
|
|
2255
2255
|
"data-mandu-src": bundleSrc,
|
|
2256
2256
|
"data-mandu-priority": priority,
|
|
2257
2257
|
style: { display: "contents" },
|
|
@@ -2263,37 +2263,37 @@ async function renderPageSSR(
|
|
|
2263
2263
|
app = await wrapWithLayouts(app, route.layoutChain, registry, params, layoutData);
|
|
2264
2264
|
}
|
|
2265
2265
|
|
|
2266
|
-
// #186: layout chain + page metadata 병합
|
|
2267
|
-
const builtMeta = await buildSSRMetadata(route, params, url, registry);
|
|
2268
|
-
|
|
2266
|
+
// #186: layout chain + page metadata 병합
|
|
2267
|
+
const builtMeta = await buildSSRMetadata(route, params, url, registry);
|
|
2268
|
+
|
|
2269
2269
|
// Streaming SSR 모드 결정
|
|
2270
2270
|
const useStreaming = route.streaming !== undefined
|
|
2271
2271
|
? route.streaming
|
|
2272
2272
|
: settings.streaming;
|
|
2273
2273
|
|
|
2274
|
-
const pageResponse = await renderPageResponse({
|
|
2275
|
-
app,
|
|
2276
|
-
useStreaming,
|
|
2277
|
-
title: builtMeta.title,
|
|
2278
|
-
headTags: builtMeta.headTags,
|
|
2279
|
-
isDev: settings.isDev,
|
|
2280
|
-
hmrPort: settings.hmrPort,
|
|
2281
|
-
routeId: route.id,
|
|
2282
|
-
routePattern: route.pattern,
|
|
2283
|
-
layoutChain: route.layoutChain,
|
|
2284
|
-
hydration: route.hydration,
|
|
2285
|
-
bundleManifest: settings.bundleManifest,
|
|
2286
|
-
loaderData,
|
|
2287
|
-
cssPath: settings.cssPath,
|
|
2288
|
-
islandPreWrapped: needsIslandWrap,
|
|
2289
|
-
transitions: settings.transitions,
|
|
2290
|
-
prefetch: settings.prefetch,
|
|
2291
|
-
spa: settings.spa,
|
|
2292
|
-
devtools: settings.devtools,
|
|
2293
|
-
cookies,
|
|
2294
|
-
});
|
|
2295
|
-
return ok(pageResponse);
|
|
2296
|
-
} catch (error) {
|
|
2274
|
+
const pageResponse = await renderPageResponse({
|
|
2275
|
+
app,
|
|
2276
|
+
useStreaming,
|
|
2277
|
+
title: builtMeta.title,
|
|
2278
|
+
headTags: builtMeta.headTags,
|
|
2279
|
+
isDev: settings.isDev,
|
|
2280
|
+
hmrPort: settings.hmrPort,
|
|
2281
|
+
routeId: route.id,
|
|
2282
|
+
routePattern: route.pattern,
|
|
2283
|
+
layoutChain: route.layoutChain,
|
|
2284
|
+
hydration: route.hydration,
|
|
2285
|
+
bundleManifest: settings.bundleManifest,
|
|
2286
|
+
loaderData,
|
|
2287
|
+
cssPath: settings.cssPath,
|
|
2288
|
+
islandPreWrapped: needsIslandWrap,
|
|
2289
|
+
transitions: settings.transitions,
|
|
2290
|
+
prefetch: settings.prefetch,
|
|
2291
|
+
spa: settings.spa,
|
|
2292
|
+
devtools: settings.devtools,
|
|
2293
|
+
cookies,
|
|
2294
|
+
});
|
|
2295
|
+
return ok(pageResponse);
|
|
2296
|
+
} catch (error) {
|
|
2297
2297
|
const renderError = error instanceof Error ? error : new Error(String(error));
|
|
2298
2298
|
|
|
2299
2299
|
// Route-level ErrorBoundary: errorModule이 있으면 해당 컴포넌트로 에러 렌더링
|
|
@@ -3377,14 +3377,14 @@ async function handleRequestInternal(
|
|
|
3377
3377
|
return ok(await handleInternalCacheControlRequest(req, settings));
|
|
3378
3378
|
}
|
|
3379
3379
|
|
|
3380
|
-
// 1.7. Internal observability endpoints: EventBus stream/recent, heap,
|
|
3381
|
-
// Prometheus metrics, and user perf snapshot composition.
|
|
3382
|
-
const observabilityResponse = settings.observability?.handleEndpoint(req, pathname);
|
|
3383
|
-
if (observabilityResponse) {
|
|
3384
|
-
return ok(observabilityResponse);
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
// Production OpenAPI endpoint — `/__mandu/openapi.json` + `.yaml`.
|
|
3380
|
+
// 1.7. Internal observability endpoints: EventBus stream/recent, heap,
|
|
3381
|
+
// Prometheus metrics, and user perf snapshot composition.
|
|
3382
|
+
const observabilityResponse = settings.observability?.handleEndpoint(req, pathname);
|
|
3383
|
+
if (observabilityResponse) {
|
|
3384
|
+
return ok(observabilityResponse);
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// Production OpenAPI endpoint — `/__mandu/openapi.json` + `.yaml`.
|
|
3388
3388
|
// Gated behind `ManduConfig.openapi.enabled` (or MANDU_OPENAPI_ENABLED=1).
|
|
3389
3389
|
// Every response carries an ETag so CDNs / browsers revalidate cheaply
|
|
3390
3390
|
// after a deploy. Falls through to route dispatch if disabled OR the
|
|
@@ -3404,35 +3404,35 @@ async function handleRequestInternal(
|
|
|
3404
3404
|
if (openapiResponse) return ok(openapiResponse);
|
|
3405
3405
|
}
|
|
3406
3406
|
|
|
3407
|
-
// 2. Runtime devtools / Kitchen dashboard (dev mode only)
|
|
3408
|
-
if (registry.devtoolsAdapter) {
|
|
3409
|
-
const devtoolsResponse = await registry.devtoolsAdapter.handleRequest(req, pathname);
|
|
3410
|
-
if (devtoolsResponse) return ok(devtoolsResponse);
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
// ─── Phase 18.ε — canonical request-level middleware chain ───────────────
|
|
3414
|
-
// Runs BEFORE route dispatch, AFTER infrastructure fast-paths (static
|
|
3415
|
-
// files, CORS preflight, internal endpoints, γ's prerendered pass-through,
|
|
3416
|
-
// Kitchen). Each composed layer can short-circuit with its own Response
|
|
3417
|
-
// or wrap the downstream Response after `next()` returns. Zero overhead
|
|
3418
|
-
// when no middleware is configured (settings.middlewareChain === undefined).
|
|
3419
|
-
const middlewareResponse = await runRequestMiddleware({
|
|
3420
|
-
req,
|
|
3421
|
-
middlewareChain: settings.middlewareChain,
|
|
3422
|
-
skipMiddleware,
|
|
3423
|
-
finalHandler: async (finalReq) => {
|
|
3424
|
-
const result = await handleRequestInternal(finalReq, router, registry, true);
|
|
3425
|
-
if (result.ok) return result.value;
|
|
3426
|
-
// Surface error-path responses to the chain so logging / metrics
|
|
3427
|
-
// layers see the final status. The outer `handleRequest` still owns
|
|
3428
|
-
// dev-mode Cache-Control stamping + observability lifecycle emission.
|
|
3429
|
-
return errorToResponse(result.error, settings.isDev);
|
|
3430
|
-
},
|
|
3431
|
-
});
|
|
3432
|
-
if (middlewareResponse) {
|
|
3433
|
-
return ok(middlewareResponse);
|
|
3434
|
-
}
|
|
3435
|
-
// ─── End Phase 18.ε ──────────────────────────────────────────────────────
|
|
3407
|
+
// 2. Runtime devtools / Kitchen dashboard (dev mode only)
|
|
3408
|
+
if (registry.devtoolsAdapter) {
|
|
3409
|
+
const devtoolsResponse = await registry.devtoolsAdapter.handleRequest(req, pathname);
|
|
3410
|
+
if (devtoolsResponse) return ok(devtoolsResponse);
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// ─── Phase 18.ε — canonical request-level middleware chain ───────────────
|
|
3414
|
+
// Runs BEFORE route dispatch, AFTER infrastructure fast-paths (static
|
|
3415
|
+
// files, CORS preflight, internal endpoints, γ's prerendered pass-through,
|
|
3416
|
+
// Kitchen). Each composed layer can short-circuit with its own Response
|
|
3417
|
+
// or wrap the downstream Response after `next()` returns. Zero overhead
|
|
3418
|
+
// when no middleware is configured (settings.middlewareChain === undefined).
|
|
3419
|
+
const middlewareResponse = await runRequestMiddleware({
|
|
3420
|
+
req,
|
|
3421
|
+
middlewareChain: settings.middlewareChain,
|
|
3422
|
+
skipMiddleware,
|
|
3423
|
+
finalHandler: async (finalReq) => {
|
|
3424
|
+
const result = await handleRequestInternal(finalReq, router, registry, true);
|
|
3425
|
+
if (result.ok) return result.value;
|
|
3426
|
+
// Surface error-path responses to the chain so logging / metrics
|
|
3427
|
+
// layers see the final status. The outer `handleRequest` still owns
|
|
3428
|
+
// dev-mode Cache-Control stamping + observability lifecycle emission.
|
|
3429
|
+
return errorToResponse(result.error, settings.isDev);
|
|
3430
|
+
},
|
|
3431
|
+
});
|
|
3432
|
+
if (middlewareResponse) {
|
|
3433
|
+
return ok(middlewareResponse);
|
|
3434
|
+
}
|
|
3435
|
+
// ─── End Phase 18.ε ──────────────────────────────────────────────────────
|
|
3436
3436
|
|
|
3437
3437
|
// ─── Phase 18.κ — typed RPC dispatch ──────────────────────────────────────
|
|
3438
3438
|
// Runs AFTER γ's prerendered pass-through (handled earlier at step 0.5),
|
|
@@ -3754,9 +3754,9 @@ export function formatServerAddresses(
|
|
|
3754
3754
|
return { primary: `http://${hosts[0]}:${port}`, additional: [] };
|
|
3755
3755
|
}
|
|
3756
3756
|
|
|
3757
|
-
export function startServer(manifest: RoutesManifest, options: ServerOptions = {}): ManduServer {
|
|
3758
|
-
const {
|
|
3759
|
-
port = 3333,
|
|
3757
|
+
export function startServer(manifest: RoutesManifest, options: ServerOptions = {}): ManduServer {
|
|
3758
|
+
const {
|
|
3759
|
+
port = 3333,
|
|
3760
3760
|
// Default to `"::"` (IPv6 wildcard, dual-stack). Bun leaves IPV6_V6ONLY
|
|
3761
3761
|
// off, so this single socket accepts both IPv4 (as IPv4-mapped IPv6)
|
|
3762
3762
|
// and IPv6 clients — covering `127.0.0.1`, `[::1]`, and LAN addresses
|
|
@@ -3838,8 +3838,8 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
3838
3838
|
// are NOT consulted for the middleware chain itself.
|
|
3839
3839
|
void pluginsOption;
|
|
3840
3840
|
void configHooksOption;
|
|
3841
|
-
const middlewareChain: ComposedHandler | undefined =
|
|
3842
|
-
buildRequestMiddlewareChain(middlewareOption);
|
|
3841
|
+
const middlewareChain: ComposedHandler | undefined =
|
|
3842
|
+
buildRequestMiddlewareChain(middlewareOption);
|
|
3843
3843
|
|
|
3844
3844
|
// Phase 18 — normalize prerender pass-through settings. `undefined`
|
|
3845
3845
|
// defaults to enabled (Next.js parity); explicit `false` opts out.
|
|
@@ -3880,12 +3880,12 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
3880
3880
|
console.warn(" cors: { origin: ['https://yourdomain.com'] }");
|
|
3881
3881
|
}
|
|
3882
3882
|
|
|
3883
|
-
const observabilityLifecycle = createRuntimeObservabilityLifecycle({
|
|
3884
|
-
isDev,
|
|
3885
|
-
heapEndpoint: observabilityOption?.heapEndpoint,
|
|
3886
|
-
metricsEndpoint: observabilityOption?.metricsEndpoint,
|
|
3887
|
-
tracing: observabilityOption?.tracing,
|
|
3888
|
-
});
|
|
3883
|
+
const observabilityLifecycle = createRuntimeObservabilityLifecycle({
|
|
3884
|
+
isDev,
|
|
3885
|
+
heapEndpoint: observabilityOption?.heapEndpoint,
|
|
3886
|
+
metricsEndpoint: observabilityOption?.metricsEndpoint,
|
|
3887
|
+
tracing: observabilityOption?.tracing,
|
|
3888
|
+
});
|
|
3889
3889
|
|
|
3890
3890
|
// Registry settings 저장 (초기값)
|
|
3891
3891
|
registry.settings = {
|
|
@@ -3903,7 +3903,7 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
3903
3903
|
prefetch,
|
|
3904
3904
|
spa,
|
|
3905
3905
|
devtools,
|
|
3906
|
-
observability: observabilityLifecycle,
|
|
3906
|
+
observability: observabilityLifecycle,
|
|
3907
3907
|
// Production OpenAPI endpoint — default OFF so an internet-facing
|
|
3908
3908
|
// deployment does not leak its API surface without explicit opt-in.
|
|
3909
3909
|
// `MANDU_OPENAPI_ENABLED=1` in the environment forces-on without a
|
|
@@ -3975,16 +3975,16 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
3975
3975
|
}
|
|
3976
3976
|
// ─── End Phase 18.ζ ────────────────────────────────────────────────────
|
|
3977
3977
|
|
|
3978
|
-
// Runtime devtools / Kitchen dashboard (dev mode only)
|
|
3979
|
-
const devtoolsAdapter = createRuntimeDevtoolsAdapter({
|
|
3980
|
-
isDev,
|
|
3981
|
-
rootDir,
|
|
3982
|
-
manifest,
|
|
3983
|
-
guardConfig,
|
|
3984
|
-
});
|
|
3985
|
-
devtoolsAdapter.start();
|
|
3986
|
-
registry.devtoolsAdapter = devtoolsAdapter;
|
|
3987
|
-
registry.kitchen = devtoolsAdapter.kitchen;
|
|
3978
|
+
// Runtime devtools / Kitchen dashboard (dev mode only)
|
|
3979
|
+
const devtoolsAdapter = createRuntimeDevtoolsAdapter({
|
|
3980
|
+
isDev,
|
|
3981
|
+
rootDir,
|
|
3982
|
+
manifest,
|
|
3983
|
+
guardConfig,
|
|
3984
|
+
});
|
|
3985
|
+
devtoolsAdapter.start();
|
|
3986
|
+
registry.devtoolsAdapter = devtoolsAdapter;
|
|
3987
|
+
registry.kitchen = devtoolsAdapter.kitchen;
|
|
3988
3988
|
|
|
3989
3989
|
const router = new Router(manifest.routes);
|
|
3990
3990
|
|
|
@@ -4033,9 +4033,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
4033
4033
|
},
|
|
4034
4034
|
} : undefined;
|
|
4035
4035
|
|
|
4036
|
-
const bumpCounter = (req: Request, res: Response | undefined): void => {
|
|
4037
|
-
registry.settings.observability?.recordHttpResponse(req, res);
|
|
4038
|
-
};
|
|
4036
|
+
const bumpCounter = (req: Request, res: Response | undefined): void => {
|
|
4037
|
+
registry.settings.observability?.recordHttpResponse(req, res);
|
|
4038
|
+
};
|
|
4039
4039
|
|
|
4040
4040
|
// fetch handler: WS upgrade 감지 추가
|
|
4041
4041
|
//
|
|
@@ -4135,9 +4135,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
4135
4135
|
if (streaming) {
|
|
4136
4136
|
console.log(`🌊 Streaming SSR enabled`);
|
|
4137
4137
|
}
|
|
4138
|
-
if (registry.devtoolsAdapter?.dashboardPath) {
|
|
4139
|
-
console.log(`🍳 Kitchen dashboard at ${addresses.primary}${registry.devtoolsAdapter.dashboardPath}`);
|
|
4140
|
-
}
|
|
4138
|
+
if (registry.devtoolsAdapter?.dashboardPath) {
|
|
4139
|
+
console.log(`🍳 Kitchen dashboard at ${addresses.primary}${registry.devtoolsAdapter.dashboardPath}`);
|
|
4140
|
+
}
|
|
4141
4141
|
} else {
|
|
4142
4142
|
console.log(`🥟 Mandu server listening at ${addresses.primary}`);
|
|
4143
4143
|
if (addresses.additional.length > 0) {
|
|
@@ -4187,19 +4187,19 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
4187
4187
|
}
|
|
4188
4188
|
}
|
|
4189
4189
|
|
|
4190
|
-
return {
|
|
4191
|
-
server,
|
|
4192
|
-
router,
|
|
4193
|
-
registry,
|
|
4194
|
-
updateManifest: (nextManifest: RoutesManifest) => {
|
|
4195
|
-
router.setRoutes(nextManifest.routes);
|
|
4196
|
-
registry.devtoolsAdapter?.updateManifest(nextManifest);
|
|
4197
|
-
},
|
|
4198
|
-
stop: () => {
|
|
4199
|
-
registry.devtoolsAdapter?.stop();
|
|
4200
|
-
registry.devtoolsAdapter = null;
|
|
4201
|
-
registry.kitchen = null;
|
|
4202
|
-
// Fire-and-forget the async scheduler drain so `stop()` stays
|
|
4190
|
+
return {
|
|
4191
|
+
server,
|
|
4192
|
+
router,
|
|
4193
|
+
registry,
|
|
4194
|
+
updateManifest: (nextManifest: RoutesManifest) => {
|
|
4195
|
+
router.setRoutes(nextManifest.routes);
|
|
4196
|
+
registry.devtoolsAdapter?.updateManifest(nextManifest);
|
|
4197
|
+
},
|
|
4198
|
+
stop: () => {
|
|
4199
|
+
registry.devtoolsAdapter?.stop();
|
|
4200
|
+
registry.devtoolsAdapter = null;
|
|
4201
|
+
registry.kitchen = null;
|
|
4202
|
+
// Fire-and-forget the async scheduler drain so `stop()` stays
|
|
4203
4203
|
// synchronous for backwards compatibility with existing consumers.
|
|
4204
4204
|
// Tests that need to await drain can reach for `registration.stop()`
|
|
4205
4205
|
// directly; `server.stop()` triggers shutdown but doesn't block on
|