@mandujs/core 0.54.10 → 0.54.12

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 (67) hide show
  1. package/package.json +1 -1
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +17 -65
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +10 -18
  11. package/src/agent/verify.ts +17 -115
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +81 -62
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/analyzer.ts +15 -15
  17. package/src/bundler/build.test.ts +130 -83
  18. package/src/bundler/build.ts +570 -524
  19. package/src/bundler/css.ts +42 -42
  20. package/src/bundler/manifest-schema.ts +21 -21
  21. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  22. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  23. package/src/bundler/types.ts +31 -31
  24. package/src/client/island.ts +79 -79
  25. package/src/config/validate.ts +1 -1
  26. package/src/deploy/inference/context.ts +82 -82
  27. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  28. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  29. package/src/error/formatter.ts +22 -31
  30. package/src/filling/context.ts +17 -17
  31. package/src/generator/generate.ts +30 -30
  32. package/src/generator/index.ts +3 -3
  33. package/src/generator/templates.test.ts +66 -65
  34. package/src/generator/templates.ts +210 -210
  35. package/src/guard/check.ts +9 -9
  36. package/src/guard/config-guard.ts +13 -13
  37. package/src/guard/fs-routes-policy.ts +51 -51
  38. package/src/guard/index.ts +11 -11
  39. package/src/index.ts +3 -3
  40. package/src/kitchen/api/file-api.ts +11 -11
  41. package/src/report/index.ts +1 -1
  42. package/src/resource/__tests__/generator.test.ts +6 -6
  43. package/src/resource/__tests__/schema.test.ts +14 -14
  44. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  45. package/src/resource/ddl/emit.ts +146 -146
  46. package/src/resource/generator-schema.ts +11 -11
  47. package/src/resource/generators/slot.ts +72 -72
  48. package/src/resource/schema.ts +21 -21
  49. package/src/router/client-entry.test.ts +192 -140
  50. package/src/router/client-entry.ts +516 -486
  51. package/src/router/fs-routes.ts +16 -16
  52. package/src/router/fs-scanner.ts +16 -28
  53. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  54. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  55. package/src/runtime/__tests__/page-render-response.test.ts +103 -103
  56. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  57. package/src/runtime/devtools-adapter.ts +68 -68
  58. package/src/runtime/escape.ts +34 -34
  59. package/src/runtime/observability-lifecycle.ts +290 -290
  60. package/src/runtime/page-render-response.ts +106 -106
  61. package/src/runtime/request-middleware.ts +31 -31
  62. package/src/runtime/server.ts +243 -243
  63. package/src/runtime/ssr.ts +59 -59
  64. package/src/runtime/static-files.ts +289 -289
  65. package/src/runtime/streaming-ssr.ts +22 -22
  66. package/src/watcher/__tests__/watcher.test.ts +59 -59
  67. package/src/watcher/watcher.ts +61 -61
@@ -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