@mandujs/core 0.42.0 โ†’ 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/metrics.ts +334 -334
  64. package/src/observability/tracing.ts +694 -694
  65. package/src/openapi/generator.ts +1 -1
  66. package/src/perf/user-marks.ts +553 -553
  67. package/src/plugins/registry.ts +387 -387
  68. package/src/resource/ddl/diff.ts +392 -392
  69. package/src/resource/ddl/snapshot.ts +448 -447
  70. package/src/resource/generator-schema.ts +477 -476
  71. package/src/resource/parser.ts +4 -2
  72. package/src/resource/schema.ts +1 -1
  73. package/src/router/fs-patterns.ts +422 -422
  74. package/src/runtime/fast-refresh-types.ts +126 -128
  75. package/src/runtime/handlers.ts +300 -0
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/index.ts +4 -0
  78. package/src/runtime/router.test.ts +476 -476
  79. package/src/runtime/security.ts +155 -155
  80. package/src/runtime/server.ts +36 -19
  81. package/src/runtime/session-key.ts +328 -328
  82. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  83. package/src/seo/resolve/index.ts +353 -353
  84. package/src/spec/load.ts +1 -1
  85. package/src/testing/reporter.ts +676 -676
  86. package/src/testing/server.ts +196 -196
  87. package/src/testing/snapshot.ts +444 -444
  88. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  89. package/src/utils/bun.ts +8 -8
@@ -1,128 +1,126 @@
1
- /**
2
- * Phase 7.1 โ€” Fast Refresh shared types
3
- *
4
- * Bun 1.3.12 ships a native `Bun.build({ reactFastRefresh: true })` flag
5
- * that performs the `$RefreshReg$` / `$RefreshSig$` source transform
6
- * (equivalent to `babel-plugin-react-refresh`). Agent B's implementation
7
- * wires this into Mandu's bundler + a browser-side runtime that glues
8
- * `react-refresh/runtime` to our existing `ManduHot` dispatcher from
9
- * Phase 7.0.C.
10
- *
11
- * This file is the CONTRACT. Types only โ€” the implementation lives in:
12
- * - packages/core/src/bundler/fast-refresh-plugin.ts (Agent B new)
13
- * - packages/core/src/runtime/fast-refresh-runtime.ts (Agent B new)
14
- * - packages/core/src/bundler/build.ts (Agent B extend โ€” vendor shim)
15
- * - packages/core/src/bundler/dev.ts (Agent B extend โ€” HTML preamble)
16
- *
17
- * References:
18
- * docs/bun/phase-7-1-diagnostics/fast-refresh-strategy.md
19
- * docs/bun/phase-7-1-team-plan.md ยง3.2
20
- * https://bun.com/reference/bun/NormalBuildConfig/reactFastRefresh
21
- */
22
-
23
- // ============================================
24
- // Browser-side `__MANDU_HMR__` global
25
- // ============================================
26
-
27
- /**
28
- * The single global object Mandu's bundler-emitted boundary code talks
29
- * to. Installed by the HTML preamble script before any island runs.
30
- *
31
- * Keeping all Fast Refresh wiring behind this single namespace makes
32
- * the attack surface (Phase 7.1.E security audit) a single module.
33
- */
34
- export interface ManduHMRGlobal {
35
- /**
36
- * Register a module URL as an HMR boundary. Emitted by the bundler
37
- * onLoad plugin (Agent B) for each `.client.tsx` / `.island.tsx`.
38
- * Idempotent โ€” registering the same URL twice is a no-op.
39
- */
40
- acceptFile(moduleUrl: string): void;
41
-
42
- /**
43
- * Invoke `react-refresh/runtime.performReactRefresh()` on the next
44
- * microtask. Coalesces multiple calls within the same tick into a
45
- * single refresh, so a batched rebuild that swaps 3 modules only
46
- * re-renders the React tree once.
47
- */
48
- performReactRefresh(): void;
49
-
50
- /**
51
- * Whether `moduleUrl` was ever registered via `acceptFile`. Used by
52
- * `dispatchReplacement` (Phase 7.0.C runtime/hmr-client.ts) to decide
53
- * between "call performReactRefresh" vs "full reload fallback".
54
- */
55
- isBoundary(moduleUrl: string): boolean;
56
-
57
- /**
58
- * Reset state โ€” test-only. Clears the boundary registry and React
59
- * refresh runtime scheduling state. Must NOT be exposed in production
60
- * bundles; tests import this via the `runtime/fast-refresh-runtime.ts`
61
- * module directly.
62
- */
63
- _testOnly_reset(): void;
64
- }
65
-
66
- /**
67
- * Metadata the bundler attaches to each emitted boundary. Not part of
68
- * the runtime API โ€” consumed only by dev-mode tooling (hmr-bench,
69
- * Kitchen DevTools) and by the security audit (Phase 7.1.E) to verify
70
- * that every injected boundary traces back to a real source file.
71
- */
72
- export interface RefreshBoundaryMetadata {
73
- /** URL the module is served under (e.g. `/.mandu/client/home.island.js`). */
74
- moduleUrl: string;
75
- /** Source files that were bundled into this module, from Bun.build's
76
- * inline sourcemap `sources[]` (same mechanism Phase 7.0.B uses). */
77
- sources: readonly string[];
78
- /** Unix ms when the boundary was registered โ€” for debug log ordering. */
79
- registeredAt: number;
80
- }
81
-
82
- // ============================================
83
- // Bundler side
84
- // ============================================
85
-
86
- /**
87
- * Options passed to the Fast Refresh Bun.build plugin (Agent B new file
88
- * `bundler/fast-refresh-plugin.ts`). All optional โ€” sensible defaults
89
- * match Vite's `@vitejs/plugin-react` behavior.
90
- */
91
- export interface FastRefreshPluginOptions {
92
- /** Only transform files matching this test. Default: `/\.(client|island)\.tsx?$/`. */
93
- include?: RegExp;
94
- /** Bypass the transform entirely. Used for prod builds. Default: `false`. */
95
- disabled?: boolean;
96
- /**
97
- * Runtime module specifier the plugin's injected import points at.
98
- * Default: `"@mandujs/core/runtime/fast-refresh-runtime"`. Tests can
99
- * override to a tmpdir-local stub.
100
- */
101
- runtimeImport?: string;
102
- }
103
-
104
- /**
105
- * Classification of why a module was (or wasn't) made a Fast Refresh
106
- * boundary. Used by the plugin's debug log + Agent D's E2E assertions.
107
- */
108
- export type BoundaryDecision =
109
- | { accepted: true; reason: "matched-include"; source: string }
110
- | { accepted: false; reason: "excluded-by-include" | "disabled" | "non-react" };
111
-
112
- // ============================================
113
- // Global augmentation โ€” Window type
114
- // ============================================
115
-
116
- /**
117
- * Expose `__MANDU_HMR__` on the browser Window without polluting Node
118
- * typings. Users of the plugin see `window.__MANDU_HMR__.acceptFile(...)`
119
- * as strongly typed in TSX files.
120
- */
121
- declare global {
122
- interface Window {
123
- /** Phase 7.1 Fast Refresh glue. Installed by HTML preamble. */
124
- __MANDU_HMR__?: ManduHMRGlobal;
125
- }
126
- }
127
-
128
- export {}; // ensure this file is a module for `declare global`
1
+ /**
2
+ * Phase 7.1 โ€” Fast Refresh shared types
3
+ *
4
+ * Bun 1.3.12 ships a native `Bun.build({ reactFastRefresh: true })` flag
5
+ * that performs the `$RefreshReg$` / `$RefreshSig$` source transform
6
+ * (equivalent to `babel-plugin-react-refresh`). Agent B's implementation
7
+ * wires this into Mandu's bundler + a browser-side runtime that glues
8
+ * `react-refresh/runtime` to our existing `ManduHot` dispatcher from
9
+ * Phase 7.0.C.
10
+ *
11
+ * This file is the CONTRACT. Types only โ€” the implementation lives in:
12
+ * - packages/core/src/bundler/fast-refresh-plugin.ts (Agent B new)
13
+ * - packages/core/src/runtime/fast-refresh-runtime.ts (Agent B new)
14
+ * - packages/core/src/bundler/build.ts (Agent B extend โ€” vendor shim)
15
+ * - packages/core/src/bundler/dev.ts (Agent B extend โ€” HTML preamble)
16
+ *
17
+ * References:
18
+ * docs/bun/phase-7-1-diagnostics/fast-refresh-strategy.md
19
+ * docs/bun/phase-7-1-team-plan.md ยง3.2
20
+ * https://bun.com/reference/bun/NormalBuildConfig/reactFastRefresh
21
+ */
22
+
23
+ // ============================================
24
+ // Browser-side `__MANDU_HMR__` global
25
+ // ============================================
26
+
27
+ /**
28
+ * The single global object Mandu's bundler-emitted boundary code talks
29
+ * to. Installed by the HTML preamble script before any island runs.
30
+ *
31
+ * Keeping all Fast Refresh wiring behind this single namespace makes
32
+ * the attack surface (Phase 7.1.E security audit) a single module.
33
+ */
34
+ export interface ManduHMRGlobal {
35
+ /**
36
+ * Register a module URL as an HMR boundary. Emitted by the bundler
37
+ * onLoad plugin (Agent B) for each `.client.tsx` / `.island.tsx`.
38
+ * Idempotent โ€” registering the same URL twice is a no-op.
39
+ */
40
+ acceptFile(moduleUrl: string): void;
41
+
42
+ /**
43
+ * Invoke `react-refresh/runtime.performReactRefresh()` on the next
44
+ * microtask. Coalesces multiple calls within the same tick into a
45
+ * single refresh, so a batched rebuild that swaps 3 modules only
46
+ * re-renders the React tree once.
47
+ */
48
+ performReactRefresh(): void;
49
+
50
+ /**
51
+ * Whether `moduleUrl` was ever registered via `acceptFile`. Used by
52
+ * `dispatchReplacement` (Phase 7.0.C runtime/hmr-client.ts) to decide
53
+ * between "call performReactRefresh" vs "full reload fallback".
54
+ */
55
+ isBoundary(moduleUrl: string): boolean;
56
+
57
+ /**
58
+ * Reset state โ€” test-only. Clears the boundary registry and React
59
+ * refresh runtime scheduling state. Must NOT be exposed in production
60
+ * bundles; tests import this via the `runtime/fast-refresh-runtime.ts`
61
+ * module directly.
62
+ */
63
+ _testOnly_reset(): void;
64
+ }
65
+
66
+ /**
67
+ * Metadata the bundler attaches to each emitted boundary. Not part of
68
+ * the runtime API โ€” consumed only by dev-mode tooling (hmr-bench,
69
+ * Kitchen DevTools) and by the security audit (Phase 7.1.E) to verify
70
+ * that every injected boundary traces back to a real source file.
71
+ */
72
+ export interface RefreshBoundaryMetadata {
73
+ /** URL the module is served under (e.g. `/.mandu/client/home.island.js`). */
74
+ moduleUrl: string;
75
+ /** Source files that were bundled into this module, from Bun.build's
76
+ * inline sourcemap `sources[]` (same mechanism Phase 7.0.B uses). */
77
+ sources: readonly string[];
78
+ /** Unix ms when the boundary was registered โ€” for debug log ordering. */
79
+ registeredAt: number;
80
+ }
81
+
82
+ // ============================================
83
+ // Bundler side
84
+ // ============================================
85
+
86
+ /**
87
+ * Options passed to the Fast Refresh Bun.build plugin (Agent B new file
88
+ * `bundler/fast-refresh-plugin.ts`). All optional โ€” sensible defaults
89
+ * match Vite's `@vitejs/plugin-react` behavior.
90
+ */
91
+ export interface FastRefreshPluginOptions {
92
+ /** Only transform files matching this test. Default: `/\.(client|island)\.tsx?$/`. */
93
+ include?: RegExp;
94
+ /** Bypass the transform entirely. Used for prod builds. Default: `false`. */
95
+ disabled?: boolean;
96
+ /**
97
+ * Runtime module specifier the plugin's injected import points at.
98
+ * Default: `"@mandujs/core/runtime/fast-refresh-runtime"`. Tests can
99
+ * override to a tmpdir-local stub.
100
+ */
101
+ runtimeImport?: string;
102
+ }
103
+
104
+ /**
105
+ * Classification of why a module was (or wasn't) made a Fast Refresh
106
+ * boundary. Used by the plugin's debug log + Agent D's E2E assertions.
107
+ */
108
+ export type BoundaryDecision =
109
+ | { accepted: true; reason: "matched-include"; source: string }
110
+ | { accepted: false; reason: "excluded-by-include" | "disabled" | "non-react" };
111
+
112
+ // ============================================
113
+ // Global augmentation โ€” Window type
114
+ // ============================================
115
+
116
+ /**
117
+ * Expose `__MANDU_HMR__` on the browser Window without polluting Node
118
+ * typings. Users of the plugin see `window.__MANDU_HMR__.acceptFile(...)`
119
+ * as strongly typed in TSX files.
120
+ */
121
+ declare global {
122
+ interface Window {
123
+ /** Phase 7.1 Fast Refresh glue. Installed by HTML preamble. */
124
+ __MANDU_HMR__?: ManduHMRGlobal;
125
+ }
126
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Manifest โ†’ runtime registry wiring.
3
+ *
4
+ * Lives in `@mandujs/core/runtime` (not in `@mandujs/cli`) because it
5
+ * runs at *server* boot, not at build time. Deploy adapters generate
6
+ * SSR entry files that need to call this โ€” having it here means the
7
+ * generated entry imports a single, public package (`@mandujs/core`)
8
+ * instead of reaching into a CLI subpath that has no `exports` map.
9
+ *
10
+ * @module core/runtime/handlers
11
+ */
12
+ import fs from "node:fs/promises";
13
+ import path from "node:path";
14
+
15
+ import {
16
+ registerApiHandler,
17
+ registerPageLoader,
18
+ registerPageHandler,
19
+ registerLayoutLoader,
20
+ registerNotFoundHandler,
21
+ registerMetadataHandler,
22
+ registerWSHandler,
23
+ type PageRegistration,
24
+ } from "./server";
25
+ import { registerManifest } from "./registry";
26
+ import { needsHydration, type RoutesManifest } from "../spec/schema";
27
+
28
+ type RouteModule = Record<string, unknown>;
29
+
30
+ /**
31
+ * Structural shape of a `ManduFilling` instance as seen by the registrar.
32
+ * Mirrors the subset of the public API we depend on (handle/hasWS/getWSHandlers)
33
+ * without importing the concrete class โ€” that would pull in runtime deps
34
+ * the CLI shouldn't need at import time.
35
+ */
36
+ interface FillingInstance {
37
+ handle: (req: Request, params?: Record<string, string>) => Response | Promise<Response>;
38
+ hasWS?: () => boolean;
39
+ getWSHandlers?: () => Parameters<typeof registerWSHandler>[1];
40
+ }
41
+
42
+ function isFillingInstance(value: unknown): value is FillingInstance {
43
+ return (
44
+ typeof value === "object" &&
45
+ value !== null &&
46
+ typeof (value as { handle?: unknown }).handle === "function"
47
+ );
48
+ }
49
+
50
+ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const;
51
+
52
+ type HttpMethod = (typeof HTTP_METHODS)[number];
53
+
54
+ function isHttpMethod(method: string): method is HttpMethod {
55
+ return (HTTP_METHODS as readonly string[]).includes(method);
56
+ }
57
+
58
+ function hasHttpMethodHandlers(module: RouteModule): boolean {
59
+ return HTTP_METHODS.some((method) => typeof module[method] === "function");
60
+ }
61
+
62
+ function createMethodDispatcher(module: RouteModule, routeId: string) {
63
+ return async (req: Request, params: Record<string, string> = {}) => {
64
+ const method = req.method.toUpperCase();
65
+ const handler = (isHttpMethod(method) ? module[method] : undefined) as
66
+ | ((request: Request, context?: { params: Record<string, string> }) => Response | Promise<Response>)
67
+ | undefined;
68
+
69
+ if (!handler) {
70
+ return Response.json(
71
+ {
72
+ error: `Method ${method} not allowed for route ${routeId}`,
73
+ },
74
+ {
75
+ status: 405,
76
+ headers: {
77
+ Allow: HTTP_METHODS.filter((m) => typeof module[m] === "function").join(", "),
78
+ },
79
+ }
80
+ );
81
+ }
82
+
83
+ return handler(req, { params });
84
+ };
85
+ }
86
+
87
+ export interface RegisterHandlersOptions {
88
+ /**
89
+ * Module import function (dev: importFresh, start: standard import).
90
+ * The optional `opts.changedFile` is forwarded into Phase 7.0 B5's
91
+ * incremental bundled-import: when the changed file is not in the
92
+ * module's import graph, `importFn` returns the cached bundle in ~0.1 ms
93
+ * instead of re-running Bun.build.
94
+ */
95
+ importFn: (modulePath: string, opts?: { changedFile?: string }) => Promise<unknown>;
96
+ /** Set for tracking already registered layout paths */
97
+ registeredLayouts: Set<string>;
98
+ /** Clear layout cache on reload */
99
+ isReload?: boolean;
100
+ /**
101
+ * Phase 7.0 B5 wire-up โ€” on a live SSR reload, the changed file that
102
+ * triggered the reload. Omit for cold boot / wildcard (full
103
+ * invalidation). Forwarded to `importFn` so the incremental
104
+ * `bundledImport` can skip rebuilds for modules the file isn't part of.
105
+ */
106
+ changedFile?: string;
107
+ }
108
+
109
+ /**
110
+ * Register manifest routes as server handlers.
111
+ * Shared between dev/build/start commands and deploy-target SSR entries.
112
+ */
113
+ export async function registerManifestHandlers(
114
+ manifest: RoutesManifest,
115
+ rootDir: string,
116
+ options: RegisterHandlersOptions
117
+ ): Promise<void> {
118
+ const { importFn, registeredLayouts, isReload = false, changedFile } = options;
119
+ const importOpts: { changedFile?: string } | undefined =
120
+ changedFile !== undefined ? { changedFile } : undefined;
121
+
122
+ if (isReload) {
123
+ registeredLayouts.clear();
124
+ }
125
+
126
+ // Expose the live manifest through the runtime registry so user code can
127
+ // read generated artifacts via `getGenerated("routes")` / `getManifest()`
128
+ // โ€” the only official path. See `packages/core/src/runtime/registry.ts`.
129
+ registerManifest("routes", manifest);
130
+
131
+ for (const route of manifest.routes) {
132
+ // Issue #206: metadata routes (sitemap/robots/llms.txt/manifest)
133
+ // register a thunk that lazily imports the user module. The
134
+ // runtime dispatcher invokes the default export on each request
135
+ // so HMR reloads pick up edits automatically (same pattern as
136
+ // API routes below).
137
+ if (route.kind === "metadata") {
138
+ const modulePath = path.resolve(rootDir, route.module);
139
+ registerMetadataHandler(route.id, async () => {
140
+ return importFn(modulePath, importOpts);
141
+ });
142
+ console.log(` ๐Ÿ—บ๏ธ Metadata: ${route.pattern} -> ${route.id}`);
143
+ continue;
144
+ }
145
+
146
+ if (route.kind === "api") {
147
+ const modulePath = path.resolve(rootDir, route.module);
148
+ try {
149
+ const module = (await importFn(modulePath, importOpts)) as RouteModule;
150
+ let handler: unknown = module.default ?? module.handler ?? module;
151
+
152
+ // 1) ManduFilling instance
153
+ if (isFillingInstance(handler)) {
154
+ console.log(` ๐Ÿ”„ ManduFilling wrapped: ${route.id}`);
155
+ const filling = handler;
156
+
157
+ // WebSocket ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก
158
+ if (typeof filling.hasWS === "function" && filling.hasWS() && filling.getWSHandlers) {
159
+ registerWSHandler(route.id, filling.getWSHandlers());
160
+ console.log(` ๐Ÿ”Œ WebSocket: ${route.pattern} -> ${route.id}`);
161
+ }
162
+
163
+ handler = async (req: Request, params?: Record<string, string>) => {
164
+ return filling.handle(req, params);
165
+ };
166
+ }
167
+ // 2) Route module with HTTP method exports (GET/POST/...)
168
+ else if (handler && typeof handler === "object" && hasHttpMethodHandlers(handler as RouteModule)) {
169
+ handler = createMethodDispatcher(handler as RouteModule, route.id);
170
+ }
171
+
172
+ if (typeof handler !== "function") {
173
+ console.warn(` โš ๏ธ API handler conversion failed: ${route.id} (type: ${typeof handler})`);
174
+ continue;
175
+ }
176
+
177
+ registerApiHandler(route.id, handler as (req: Request, params?: Record<string, string>) => Response | Promise<Response>);
178
+ console.log(` ๐Ÿ“ก API: ${route.pattern} -> ${route.id}`);
179
+ } catch (error) {
180
+ console.error(` โŒ Failed to load API handler: ${route.id}`, error);
181
+ }
182
+ } else if (route.kind === "page" && route.componentModule) {
183
+ const componentPath = path.resolve(rootDir, route.componentModule);
184
+ const isIsland = needsHydration(route);
185
+ const hasLayout = route.layoutChain && route.layoutChain.length > 0;
186
+
187
+ // Register layout loaders
188
+ if (route.layoutChain) {
189
+ for (const layoutPath of route.layoutChain) {
190
+ if (!registeredLayouts.has(layoutPath)) {
191
+ const absLayoutPath = path.resolve(rootDir, layoutPath);
192
+ registerLayoutLoader(layoutPath, (async () => {
193
+ // Layout modules must export a default component. Runtime
194
+ // validation in `renderToHTML` / page-loader asserts this โ€”
195
+ // so casting the unknown `importFn` result is safe here.
196
+ return importFn(absLayoutPath, importOpts);
197
+ }) as Parameters<typeof registerLayoutLoader>[1]);
198
+ registeredLayouts.add(layoutPath);
199
+ console.log(` ๐ŸŽจ Layout: ${layoutPath}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ // Use PageHandler if slotModule exists (filling.loader support)
205
+ if (route.slotModule) {
206
+ registerPageHandler(route.id, async () => {
207
+ const mod = (await importFn(componentPath, importOpts)) as Record<string, unknown>;
208
+ // Normalize the page module shape. Users write pages in two styles:
209
+ // (a) `export default function Page() {โ€ฆ}` + `export const filling = โ€ฆ`
210
+ // (b) `export default { component: โ€ฆ, filling: โ€ฆ }`
211
+ // Spreading a function default drops the component silently (you get
212
+ // the function's own props like `name`/`length`, not the function).
213
+ // Auto-promote form (a) to form (b) so both work without surprises.
214
+ const rawDefault = mod.default as unknown;
215
+
216
+ let registration: PageRegistration;
217
+ if (typeof rawDefault === "function") {
218
+ registration = {
219
+ component: rawDefault as PageRegistration["component"],
220
+ filling: mod.filling as PageRegistration["filling"],
221
+ };
222
+ } else if (typeof rawDefault === "object" && rawDefault !== null) {
223
+ registration = { ...(rawDefault as unknown as PageRegistration) };
224
+ } else {
225
+ throw new Error(
226
+ `[Mandu] Page module '${route.id}' has no default export. ` +
227
+ `Expected a React component or { component, filling } object.`,
228
+ );
229
+ }
230
+
231
+ // #186: page ๋ชจ๋“ˆ์˜ metadata / generateMetadata ๋ฅผ registration์— ์‹ค์–ด์„œ
232
+ // ensurePageRouteMetadata๊ฐ€ registry ์บ์‹œ์— ์ €์žฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ „๋‹ฌ.
233
+ if (mod.metadata && typeof mod.metadata === "object") {
234
+ registration.metadata = mod.metadata as PageRegistration["metadata"];
235
+ }
236
+ if (typeof mod.generateMetadata === "function") {
237
+ registration.generateMetadata = mod.generateMetadata as PageRegistration["generateMetadata"];
238
+ }
239
+ return registration;
240
+ });
241
+ console.log(
242
+ ` ๐Ÿ“„ Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " ๐Ÿ๏ธ" : ""}${hasLayout ? " ๐ŸŽจ" : ""}`
243
+ );
244
+ } else {
245
+ registerPageLoader(route.id, (() => importFn(componentPath, importOpts)) as Parameters<typeof registerPageLoader>[1]);
246
+ console.log(
247
+ ` ๐Ÿ“„ Page: ${route.pattern} -> ${route.id}${isIsland ? " ๐Ÿ๏ธ" : ""}${hasLayout ? " ๐ŸŽจ" : ""}`
248
+ );
249
+ }
250
+ }
251
+ }
252
+
253
+ // Phase 6.3: register `app/not-found.tsx` if it exists. Global, one per
254
+ // app โ€” the server falls through to the built-in 404 if unregistered.
255
+ await registerAppNotFound(rootDir, importFn, importOpts);
256
+ }
257
+
258
+ /**
259
+ * Phase 6.3: look for `app/not-found.tsx` (or its variants) at the
260
+ * project root and register it as the app-level 404 handler. Silent
261
+ * no-op if no file exists โ€” the server's built-in 404 covers that case.
262
+ */
263
+ async function registerAppNotFound(
264
+ rootDir: string,
265
+ importFn: (modulePath: string, opts?: { changedFile?: string }) => Promise<unknown>,
266
+ importOpts?: { changedFile?: string },
267
+ ): Promise<void> {
268
+ const candidates = [
269
+ "app/not-found.tsx",
270
+ "app/not-found.ts",
271
+ "app/not-found.jsx",
272
+ "app/not-found.js",
273
+ ];
274
+ for (const rel of candidates) {
275
+ const abs = path.resolve(rootDir, rel);
276
+ try {
277
+ await fs.access(abs);
278
+ } catch {
279
+ continue;
280
+ }
281
+ registerNotFoundHandler(async () => {
282
+ const module = (await importFn(abs, importOpts)) as Record<string, unknown>;
283
+ const rawDefault = module.default as unknown;
284
+ if (typeof rawDefault === "function") {
285
+ return {
286
+ component: rawDefault as PageRegistration["component"],
287
+ filling: module.filling as PageRegistration["filling"],
288
+ };
289
+ }
290
+ if (typeof rawDefault === "object" && rawDefault !== null) {
291
+ return { ...(rawDefault as PageRegistration) };
292
+ }
293
+ throw new Error(
294
+ `[Mandu] app/not-found.tsx has no valid default export (type: ${typeof rawDefault})`,
295
+ );
296
+ });
297
+ console.log(` ๐Ÿšซ Not-Found: ${rel}`);
298
+ return;
299
+ }
300
+ }