@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.
- package/package.json +21 -4
- package/src/auth/__tests__/login.test.ts +420 -419
- package/src/auth/__tests__/reset.test.ts +296 -296
- package/src/brain/adapters/anthropic-oauth.ts +421 -420
- package/src/brain/adapters/index.ts +2 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/adapters/openai-oauth.ts +534 -533
- package/src/brain/brain.ts +2 -1
- package/src/brain/redactor.ts +196 -196
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
- package/src/bundler/__tests__/cold-start.test.ts +504 -504
- package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
- package/src/bundler/__tests__/hdr.test.ts +1 -1
- package/src/bundler/analyzer.ts +958 -958
- package/src/bundler/build.ts +104 -14
- package/src/bundler/dev.ts +125 -0
- package/src/bundler/hmr-types.ts +1 -0
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
- package/src/bundler/plugins/index.ts +14 -0
- package/src/bundler/plugins/react-compiler-lint.ts +253 -0
- package/src/bundler/plugins/react-compiler.ts +162 -0
- package/src/bundler/types.ts +12 -0
- package/src/change/integrity.ts +2 -1
- package/src/client/index.ts +10 -0
- package/src/client/island.ts +38 -11
- package/src/client/router.ts +6 -1
- package/src/config/mandu.ts +57 -0
- package/src/config/validate.ts +42 -0
- package/src/content/collection.ts +844 -809
- package/src/content/content-layer.ts +316 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/digest.ts +133 -133
- package/src/content/generate-types.ts +168 -168
- package/src/content/index.ts +6 -1
- package/src/content/llms-txt.ts +277 -277
- package/src/contract/define.ts +474 -474
- package/src/contract/route-helpers.ts +2 -1
- package/src/contract/zod-utils.ts +158 -155
- package/src/db/index.ts +513 -513
- package/src/desktop/__tests__/smoke.test.ts +100 -100
- package/src/desktop/webview-fallback.ts +583 -583
- package/src/desktop/window.ts +3 -1
- package/src/dev-error-overlay/overlay-client.ts +300 -300
- package/src/devtools/ai/mcp-connector.ts +499 -498
- package/src/devtools/client/components/kitchen-root.tsx +7 -2
- package/src/email/resend.ts +163 -163
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/graph.ts +898 -898
- package/src/guard/index.ts +16 -0
- package/src/guard/statistics.ts +578 -578
- package/src/guard/tsgolint-bridge.ts +512 -0
- package/src/i18n/locale-resolver.ts +214 -214
- package/src/id/__tests__/id.test.ts +120 -120
- package/src/intent/index.ts +321 -321
- package/src/island/index.ts +39 -23
- package/src/kitchen/api/contract-api.ts +15 -8
- package/src/kitchen/kitchen-ui.ts +2137 -2137
- package/src/lockfile/index.ts +3 -2
- package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
- package/src/middleware/secure/index.ts +417 -417
- package/src/observability/metrics.ts +334 -334
- package/src/observability/tracing.ts +694 -694
- package/src/openapi/generator.ts +1 -1
- package/src/perf/user-marks.ts +553 -553
- package/src/plugins/registry.ts +387 -387
- package/src/resource/ddl/diff.ts +392 -392
- package/src/resource/ddl/snapshot.ts +448 -447
- package/src/resource/generator-schema.ts +477 -476
- package/src/resource/parser.ts +4 -2
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.ts +422 -422
- package/src/runtime/fast-refresh-types.ts +126 -128
- package/src/runtime/handlers.ts +300 -0
- package/src/runtime/image-handler.ts +206 -195
- package/src/runtime/index.ts +4 -0
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +36 -19
- package/src/runtime/session-key.ts +328 -328
- package/src/scheduler/__tests__/scheduler.test.ts +514 -514
- package/src/seo/resolve/index.ts +353 -353
- package/src/spec/load.ts +1 -1
- package/src/testing/reporter.ts +676 -676
- package/src/testing/server.ts +196 -196
- package/src/testing/snapshot.ts +444 -444
- package/src/utils/__tests__/lru-cache.test.ts +186 -186
- 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
|
+
}
|