@mandujs/core 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +0 -14
- package/package.json +4 -1
- package/src/brain/architecture/analyzer.ts +4 -4
- package/src/brain/doctor/analyzer.ts +18 -14
- package/src/bundler/build.test.ts +127 -0
- package/src/bundler/build.ts +291 -113
- package/src/bundler/css.ts +20 -5
- package/src/bundler/dev.ts +55 -2
- package/src/bundler/prerender.ts +195 -0
- package/src/change/snapshot.ts +4 -23
- package/src/change/types.ts +2 -3
- package/src/client/Form.tsx +105 -0
- package/src/client/__tests__/use-sse.test.ts +153 -0
- package/src/client/hooks.ts +105 -6
- package/src/client/index.ts +35 -6
- package/src/client/router.ts +670 -433
- package/src/client/rpc.ts +140 -0
- package/src/client/runtime.ts +24 -21
- package/src/client/use-fetch.ts +239 -0
- package/src/client/use-head.ts +197 -0
- package/src/client/use-sse.ts +378 -0
- package/src/components/Image.tsx +162 -0
- package/src/config/mandu.ts +5 -0
- package/src/config/validate.ts +34 -0
- package/src/content/index.ts +5 -1
- package/src/devtools/client/catchers/error-catcher.ts +17 -0
- package/src/devtools/client/catchers/network-proxy.ts +390 -367
- package/src/devtools/client/components/kitchen-root.tsx +479 -467
- package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
- package/src/devtools/client/components/panel/index.ts +45 -32
- package/src/devtools/client/components/panel/panel-container.tsx +332 -312
- package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
- package/src/devtools/client/state-manager.ts +535 -478
- package/src/devtools/design-tokens.ts +265 -264
- package/src/devtools/types.ts +345 -319
- package/src/filling/filling.ts +336 -14
- package/src/filling/index.ts +5 -1
- package/src/filling/session.ts +216 -0
- package/src/filling/ws.ts +78 -0
- package/src/generator/generate.ts +2 -2
- package/src/guard/auto-correct.ts +0 -29
- package/src/guard/check.ts +14 -31
- package/src/guard/presets/index.ts +296 -294
- package/src/guard/rules.ts +15 -19
- package/src/guard/validator.ts +834 -834
- package/src/index.ts +5 -1
- package/src/island/index.ts +373 -304
- package/src/kitchen/api/contract-api.ts +225 -0
- package/src/kitchen/api/diff-parser.ts +108 -0
- package/src/kitchen/api/file-api.ts +273 -0
- package/src/kitchen/api/guard-api.ts +83 -0
- package/src/kitchen/api/guard-decisions.ts +100 -0
- package/src/kitchen/api/routes-api.ts +50 -0
- package/src/kitchen/index.ts +21 -0
- package/src/kitchen/kitchen-handler.ts +256 -0
- package/src/kitchen/kitchen-ui.ts +1732 -0
- package/src/kitchen/stream/activity-sse.ts +145 -0
- package/src/kitchen/stream/file-tailer.ts +99 -0
- package/src/middleware/compress.ts +62 -0
- package/src/middleware/cors.ts +47 -0
- package/src/middleware/index.ts +10 -0
- package/src/middleware/jwt.ts +134 -0
- package/src/middleware/logger.ts +58 -0
- package/src/middleware/timeout.ts +55 -0
- package/src/paths.ts +0 -4
- package/src/plugins/hooks.ts +64 -0
- package/src/plugins/index.ts +3 -0
- package/src/plugins/types.ts +5 -0
- package/src/report/build.ts +0 -6
- package/src/resource/__tests__/backward-compat.test.ts +0 -1
- package/src/router/fs-patterns.ts +11 -1
- package/src/router/fs-routes.ts +78 -14
- package/src/router/fs-scanner.ts +2 -2
- package/src/router/fs-types.ts +2 -1
- package/src/runtime/adapter-bun.ts +62 -0
- package/src/runtime/adapter.ts +47 -0
- package/src/runtime/cache.ts +310 -0
- package/src/runtime/handler.ts +65 -0
- package/src/runtime/image-handler.ts +195 -0
- package/src/runtime/index.ts +12 -0
- package/src/runtime/middleware.ts +263 -0
- package/src/runtime/server.ts +662 -83
- package/src/runtime/ssr.ts +55 -29
- package/src/runtime/streaming-ssr.ts +106 -82
- package/src/spec/index.ts +0 -1
- package/src/spec/schema.ts +1 -0
- package/src/testing/index.ts +144 -0
- package/src/watcher/watcher.ts +27 -1
- package/src/spec/lock.ts +0 -56
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Lifecycle Hook System
|
|
3
|
+
*
|
|
4
|
+
* Lightweight hook runner for build/dev/start lifecycle events.
|
|
5
|
+
* Hooks defined in mandu.config.ts run first, then plugin hooks
|
|
6
|
+
* in registration order. Each hook is isolated -- one failure
|
|
7
|
+
* does not block subsequent hooks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ManduHooks {
|
|
11
|
+
onBeforeBuild?: () => void | Promise<void>;
|
|
12
|
+
onAfterBuild?: (result: { success: boolean; duration: number }) => void | Promise<void>;
|
|
13
|
+
onDevStart?: (info: { port: number; hostname: string }) => void | Promise<void>;
|
|
14
|
+
onDevStop?: () => void | Promise<void>;
|
|
15
|
+
onRouteChange?: (info: { routeId: string; pattern: string; kind: string }) => void | Promise<void>;
|
|
16
|
+
onBeforeStart?: () => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ManduPlugin {
|
|
20
|
+
name: string;
|
|
21
|
+
hooks?: Partial<ManduHooks>;
|
|
22
|
+
setup?: (config: Record<string, unknown>) => void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run a named lifecycle hook across config-level hooks and plugins.
|
|
27
|
+
*
|
|
28
|
+
* Execution order:
|
|
29
|
+
* 1. Config hook (from mandu.config.ts `hooks` field)
|
|
30
|
+
* 2. Plugin hooks (from `plugins[].hooks`, in array order)
|
|
31
|
+
*
|
|
32
|
+
* Each invocation is wrapped in try/catch so a single failing hook
|
|
33
|
+
* does not prevent the remaining hooks from executing.
|
|
34
|
+
*/
|
|
35
|
+
export async function runHook<K extends keyof ManduHooks>(
|
|
36
|
+
hookName: K,
|
|
37
|
+
plugins: ManduPlugin[],
|
|
38
|
+
configHooks: Partial<ManduHooks> | undefined,
|
|
39
|
+
...args: Parameters<NonNullable<ManduHooks[K]>>
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const invoke = async (
|
|
42
|
+
label: string,
|
|
43
|
+
fn: ((...a: unknown[]) => void | Promise<void>) | undefined,
|
|
44
|
+
) => {
|
|
45
|
+
if (!fn) return;
|
|
46
|
+
try {
|
|
47
|
+
await fn(...args);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
50
|
+
console.error(`[plugin] ${hookName} failed in ${label}: ${msg}`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Config-level hook runs first
|
|
55
|
+
await invoke("config", configHooks?.[hookName] as ((...a: unknown[]) => void | Promise<void>) | undefined);
|
|
56
|
+
|
|
57
|
+
// Plugin hooks run in registration order
|
|
58
|
+
for (const plugin of plugins) {
|
|
59
|
+
await invoke(
|
|
60
|
+
plugin.name,
|
|
61
|
+
plugin.hooks?.[hookName] as ((...a: unknown[]) => void | Promise<void>) | undefined,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/plugins/index.ts
CHANGED
package/src/plugins/types.ts
CHANGED
package/src/report/build.ts
CHANGED
|
@@ -18,9 +18,6 @@ export function buildGuardReport(checkResult: GuardCheckResult): ManuduReport {
|
|
|
18
18
|
const nextActions: string[] = [];
|
|
19
19
|
|
|
20
20
|
if (!checkResult.passed) {
|
|
21
|
-
const hasHashMismatch = checkResult.violations.some(
|
|
22
|
-
(v) => v.ruleId === "SPEC_HASH_MISMATCH"
|
|
23
|
-
);
|
|
24
21
|
const hasManualEdit = checkResult.violations.some(
|
|
25
22
|
(v) => v.ruleId === "GENERATED_MANUAL_EDIT"
|
|
26
23
|
);
|
|
@@ -31,9 +28,6 @@ export function buildGuardReport(checkResult: GuardCheckResult): ManuduReport {
|
|
|
31
28
|
(v) => v.ruleId === "FORBIDDEN_IMPORT_IN_GENERATED"
|
|
32
29
|
);
|
|
33
30
|
|
|
34
|
-
if (hasHashMismatch) {
|
|
35
|
-
nextActions.push("bunx mandu routes generate");
|
|
36
|
-
}
|
|
37
31
|
if (hasManualEdit) {
|
|
38
32
|
nextActions.push("bunx mandu generate");
|
|
39
33
|
}
|
|
@@ -43,7 +43,6 @@ describe("Backward Compatibility - Path Structure", () => {
|
|
|
43
43
|
expect(paths.typesDir).toBeDefined();
|
|
44
44
|
expect(paths.mapDir).toBeDefined();
|
|
45
45
|
expect(paths.manifestPath).toBeDefined();
|
|
46
|
-
expect(paths.lockPath).toBeDefined();
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
test("should not conflict with existing generated directories", async () => {
|
|
@@ -62,6 +62,15 @@ export function parseSegment(segment: string): RouteSegment {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Slot / Parallel route: @name
|
|
66
|
+
if (segment.startsWith("@")) {
|
|
67
|
+
return {
|
|
68
|
+
raw: segment,
|
|
69
|
+
type: "slot",
|
|
70
|
+
paramName: segment.slice(1), // @modal → "modal"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
// Static segment
|
|
66
75
|
return {
|
|
67
76
|
raw: segment,
|
|
@@ -123,7 +132,7 @@ export function segmentsToPattern(segments: RouteSegment[]): string {
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
const parts = segments
|
|
126
|
-
.filter((seg) => seg.type !== "group") //
|
|
135
|
+
.filter((seg) => seg.type !== "group" && seg.type !== "slot") // 그룹과 slot(@name)은 URL에 포함 안 됨
|
|
127
136
|
.map((seg) => segmentToPatternPart(seg));
|
|
128
137
|
|
|
129
138
|
return "/" + parts.join("/");
|
|
@@ -272,6 +281,7 @@ export function generateRouteId(relativePath: string): string {
|
|
|
272
281
|
const SEGMENT_PRIORITY: Record<SegmentType, number> = {
|
|
273
282
|
static: 0,
|
|
274
283
|
group: 1, // 그룹은 URL에 영향 없으므로 static과 동일
|
|
284
|
+
slot: 1, // slot(@name)도 URL에 영향 없음 — layout named prop으로 전달
|
|
275
285
|
dynamic: 2,
|
|
276
286
|
catchAll: 3,
|
|
277
287
|
optionalCatchAll: 4,
|
package/src/router/fs-routes.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module router/fs-routes
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
9
|
+
import { writeFile, readFile, mkdir } from "fs/promises";
|
|
10
10
|
import { join, dirname } from "path";
|
|
11
11
|
import type { RoutesManifest, RouteSpec } from "../spec/schema";
|
|
12
12
|
import type { FSRouteConfig, FSScannerConfig, ScanResult } from "./fs-types";
|
|
@@ -50,33 +50,38 @@ export interface GenerateOptions {
|
|
|
50
50
|
/**
|
|
51
51
|
* FSRouteConfig를 RouteSpec으로 변환
|
|
52
52
|
*/
|
|
53
|
+
/** Normalize path separators to forward slashes for cross-platform consistency */
|
|
54
|
+
function normalizePath(p: string): string {
|
|
55
|
+
return p.replace(/\\/g, "/");
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
export function fsRouteToRouteSpec(fsRoute: FSRouteConfig): RouteSpec {
|
|
54
59
|
const base = {
|
|
55
60
|
id: fsRoute.id,
|
|
56
61
|
pattern: fsRoute.pattern,
|
|
57
|
-
module: fsRoute.module,
|
|
62
|
+
module: normalizePath(fsRoute.module),
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
if (fsRoute.kind === "page") {
|
|
61
66
|
const pageRoute: RouteSpec = {
|
|
62
67
|
...base,
|
|
63
68
|
kind: "page" as const,
|
|
64
|
-
componentModule: fsRoute.componentModule ?? "",
|
|
69
|
+
componentModule: normalizePath(fsRoute.componentModule ?? ""),
|
|
65
70
|
...(fsRoute.clientModule
|
|
66
71
|
? {
|
|
67
|
-
clientModule: fsRoute.clientModule,
|
|
72
|
+
clientModule: normalizePath(fsRoute.clientModule),
|
|
68
73
|
hydration: fsRoute.hydration ?? {
|
|
69
74
|
strategy: "island" as const,
|
|
70
|
-
priority: "
|
|
75
|
+
priority: "immediate" as const,
|
|
71
76
|
preload: false,
|
|
72
77
|
},
|
|
73
78
|
}
|
|
74
79
|
: {}),
|
|
75
80
|
...(fsRoute.layoutChain && fsRoute.layoutChain.length > 0
|
|
76
|
-
? { layoutChain: fsRoute.layoutChain }
|
|
81
|
+
? { layoutChain: fsRoute.layoutChain.map(normalizePath) }
|
|
77
82
|
: {}),
|
|
78
|
-
...(fsRoute.loadingModule ? { loadingModule: fsRoute.loadingModule } : {}),
|
|
79
|
-
...(fsRoute.errorModule ? { errorModule: fsRoute.errorModule } : {}),
|
|
83
|
+
...(fsRoute.loadingModule ? { loadingModule: normalizePath(fsRoute.loadingModule) } : {}),
|
|
84
|
+
...(fsRoute.errorModule ? { errorModule: normalizePath(fsRoute.errorModule) } : {}),
|
|
80
85
|
};
|
|
81
86
|
return pageRoute;
|
|
82
87
|
}
|
|
@@ -115,19 +120,54 @@ export async function resolveAutoLinks(
|
|
|
115
120
|
manifest: RoutesManifest,
|
|
116
121
|
rootDir: string
|
|
117
122
|
): Promise<void> {
|
|
123
|
+
// Slot = server-side data loader (.slot.ts/.tsx)
|
|
124
|
+
// Client = client-side island module (.client.ts/.tsx) — sets clientModule, NOT slotModule
|
|
125
|
+
const slotExtensions = [".slot.ts", ".slot.tsx"];
|
|
126
|
+
const clientExtensions = [".client.ts", ".client.tsx"];
|
|
127
|
+
|
|
118
128
|
await Promise.all(
|
|
119
129
|
manifest.routes.map(async (route) => {
|
|
120
|
-
const slotPath = join(rootDir, "spec", "slots", `${route.id}.slot.ts`);
|
|
121
130
|
const contractPath = join(rootDir, "spec", "contracts", `${route.id}.contract.ts`);
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
// Check all extensions in parallel
|
|
133
|
+
const slotChecks = slotExtensions.map(async (ext) => {
|
|
134
|
+
const path = join(rootDir, "spec", "slots", `${route.id}${ext}`);
|
|
135
|
+
return (await Bun.file(path).exists()) ? ext : null;
|
|
136
|
+
});
|
|
137
|
+
const clientChecks = clientExtensions.map(async (ext) => {
|
|
138
|
+
const path = join(rootDir, "spec", "slots", `${route.id}${ext}`);
|
|
139
|
+
return (await Bun.file(path).exists()) ? ext : null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const [contractExists, ...allResults] = await Promise.all([
|
|
125
143
|
Bun.file(contractPath).exists(),
|
|
144
|
+
...slotChecks,
|
|
145
|
+
...clientChecks,
|
|
126
146
|
]);
|
|
127
147
|
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
const slotResults = allResults.slice(0, slotExtensions.length);
|
|
149
|
+
const clientResults = allResults.slice(slotExtensions.length);
|
|
150
|
+
|
|
151
|
+
// Set slotModule for .slot.ts(x) files (server data loaders)
|
|
152
|
+
const matchedSlotExt = slotResults.find((ext) => ext !== null);
|
|
153
|
+
if (matchedSlotExt) {
|
|
154
|
+
route.slotModule = `spec/slots/${route.id}${matchedSlotExt}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Set clientModule for .client.ts(x) files (island hydration) — only if not already set
|
|
158
|
+
const matchedClientExt = clientResults.find((ext) => ext !== null);
|
|
159
|
+
if (matchedClientExt && !route.clientModule) {
|
|
160
|
+
route.clientModule = `spec/slots/${route.id}${matchedClientExt}`;
|
|
161
|
+
// Also set default hydration config if not present
|
|
162
|
+
if (!route.hydration) {
|
|
163
|
+
route.hydration = {
|
|
164
|
+
strategy: "island" as const,
|
|
165
|
+
priority: "immediate" as const,
|
|
166
|
+
preload: false,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
130
169
|
}
|
|
170
|
+
|
|
131
171
|
if (contractExists) {
|
|
132
172
|
route.contractModule = `spec/contracts/${route.id}.contract.ts`;
|
|
133
173
|
}
|
|
@@ -189,10 +229,34 @@ export async function generateManifest(
|
|
|
189
229
|
// Auto-linking: spec/slots/, spec/contracts/ 자동 연결
|
|
190
230
|
await resolveAutoLinks(manifest, rootDir);
|
|
191
231
|
|
|
192
|
-
//
|
|
232
|
+
// 기존 매니페스트에서 사용자 설정 필드 보존 (clientModule, hydration 등)
|
|
193
233
|
const outputPath = options.outputPath ?? ".mandu/routes.manifest.json";
|
|
194
234
|
const outputFullPath = join(rootDir, outputPath);
|
|
195
235
|
await mkdir(dirname(outputFullPath), { recursive: true });
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const existingRaw = await readFile(outputFullPath, "utf-8");
|
|
239
|
+
const existingManifest = JSON.parse(existingRaw) as RoutesManifest;
|
|
240
|
+
if (existingManifest.routes) {
|
|
241
|
+
const existingMap = new Map(
|
|
242
|
+
existingManifest.routes.map((r) => [r.id, r])
|
|
243
|
+
);
|
|
244
|
+
for (const route of manifest.routes) {
|
|
245
|
+
const prev = existingMap.get(route.id);
|
|
246
|
+
if (!prev) continue;
|
|
247
|
+
// 사용자가 설정한 clientModule/hydration 보존
|
|
248
|
+
if (prev.clientModule && !route.clientModule) {
|
|
249
|
+
route.clientModule = prev.clientModule;
|
|
250
|
+
}
|
|
251
|
+
if (prev.hydration && !route.hydration) {
|
|
252
|
+
route.hydration = prev.hydration;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// 기존 매니페스트가 없으면 무시
|
|
258
|
+
}
|
|
259
|
+
|
|
196
260
|
await writeFile(outputFullPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
197
261
|
|
|
198
262
|
return {
|
package/src/router/fs-scanner.ts
CHANGED
|
@@ -241,7 +241,7 @@ export class FSScanner {
|
|
|
241
241
|
const pattern = segmentsToPattern(file.segments);
|
|
242
242
|
const patternShape = getPatternShape(pattern);
|
|
243
243
|
const routeId = generateRouteId(file.relativePath);
|
|
244
|
-
const modulePath = join(this.config.routesDir, file.relativePath);
|
|
244
|
+
const modulePath = join(this.config.routesDir, file.relativePath).replace(/\\/g, "/");
|
|
245
245
|
|
|
246
246
|
// 중복 패턴 체크
|
|
247
247
|
const existingRoute = patternMap.get(pattern);
|
|
@@ -288,7 +288,7 @@ export class FSScanner {
|
|
|
288
288
|
|
|
289
289
|
if (islands?.[0]) {
|
|
290
290
|
// 우선순위: 명시적 island 파일
|
|
291
|
-
clientModule = join(this.config.routesDir, islands[0].relativePath);
|
|
291
|
+
clientModule = join(this.config.routesDir, islands[0].relativePath).replace(/\\/g, "/");
|
|
292
292
|
|
|
293
293
|
// SSR shell + island placeholder 패턴은 hydration mismatch 위험이 매우 높으므로 에러로 처리
|
|
294
294
|
if (pageFileContent && this.hasHydrationShellMismatchRisk(pageFileContent, islands[0].relativePath)) {
|
package/src/router/fs-types.ts
CHANGED
|
@@ -20,7 +20,8 @@ export type SegmentType =
|
|
|
20
20
|
| "dynamic" // 동적 세그먼트 (예: "[slug]")
|
|
21
21
|
| "catchAll" // Catch-all 세그먼트 (예: "[...path]")
|
|
22
22
|
| "optionalCatchAll" // Optional catch-all (예: "[[...path]]")
|
|
23
|
-
| "group"
|
|
23
|
+
| "group" // 라우트 그룹 (예: "(marketing)")
|
|
24
|
+
| "slot"; // Named slot / Parallel route (예: "@modal")
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* 파싱된 라우트 세그먼트
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Bun Adapter (기본 어댑터)
|
|
3
|
+
* Bun.serve() 기반 서버 생성
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManduAdapter, AdapterOptions, AdapterServer } from "./adapter";
|
|
7
|
+
import { startServer, type ManduServer } from "./server";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bun 어댑터 (기본)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // mandu.config.ts
|
|
15
|
+
* import { adapterBun } from "@mandujs/core";
|
|
16
|
+
*
|
|
17
|
+
* export default {
|
|
18
|
+
* adapter: adapterBun(),
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function adapterBun(): ManduAdapter {
|
|
23
|
+
return {
|
|
24
|
+
name: "adapter-bun",
|
|
25
|
+
|
|
26
|
+
createServer(options: AdapterOptions): AdapterServer {
|
|
27
|
+
let manduServer: ManduServer | null = null;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
async fetch(req: Request): Promise<Response> {
|
|
31
|
+
if (!manduServer) {
|
|
32
|
+
return new Response("Server not started", { status: 503 });
|
|
33
|
+
}
|
|
34
|
+
// 내부 서버로 프록시
|
|
35
|
+
const url = new URL(req.url);
|
|
36
|
+
const targetUrl = `http://localhost:${manduServer.server.port}${url.pathname}${url.search}`;
|
|
37
|
+
return globalThis.fetch(new Request(targetUrl, req));
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async listen(port: number, hostname?: string) {
|
|
41
|
+
manduServer = startServer(options.manifest, {
|
|
42
|
+
...options.serverOptions,
|
|
43
|
+
port,
|
|
44
|
+
hostname,
|
|
45
|
+
rootDir: options.rootDir,
|
|
46
|
+
bundleManifest: options.bundleManifest,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
port: manduServer.server.port ?? port,
|
|
51
|
+
hostname: hostname ?? "localhost",
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async close() {
|
|
56
|
+
manduServer?.stop();
|
|
57
|
+
manduServer = null;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Adapter Interface
|
|
3
|
+
* 런타임 중립적 서버 어댑터 추상화
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
7
|
+
import type { BundleManifest } from "../bundler/types";
|
|
8
|
+
import type { ServerOptions } from "./server";
|
|
9
|
+
|
|
10
|
+
// ========== Types ==========
|
|
11
|
+
|
|
12
|
+
export interface AdapterOptions {
|
|
13
|
+
manifest: RoutesManifest;
|
|
14
|
+
bundleManifest?: BundleManifest;
|
|
15
|
+
rootDir: string;
|
|
16
|
+
serverOptions: ServerOptions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AdapterServer {
|
|
20
|
+
/** fetch handler (Web Fetch API — 런타임 중립) */
|
|
21
|
+
fetch: (request: Request) => Promise<Response>;
|
|
22
|
+
/** 서버 시작 */
|
|
23
|
+
listen(port: number, hostname?: string): Promise<{ port: number; hostname: string }>;
|
|
24
|
+
/** 서버 중지 */
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mandu Adapter 인터페이스
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // mandu.config.ts
|
|
34
|
+
* import adapterBun from "@mandujs/adapter-bun";
|
|
35
|
+
*
|
|
36
|
+
* export default {
|
|
37
|
+
* adapter: adapterBun(),
|
|
38
|
+
* };
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface ManduAdapter {
|
|
42
|
+
name: string;
|
|
43
|
+
/** 빌드 타임: 배포 산출물 생성 (SSG, 서버리스 번들 등) */
|
|
44
|
+
build?(options: AdapterOptions): Promise<void>;
|
|
45
|
+
/** 런타임: 서버 인스턴스 생성 */
|
|
46
|
+
createServer(options: AdapterOptions): AdapterServer;
|
|
47
|
+
}
|