@mandujs/core 0.54.12 → 0.54.13
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 +9 -1
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +49 -1
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/agent/types.ts +8 -0
- package/src/agent/verify.ts +100 -2
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +5 -4
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +66 -15
- package/src/bundler/build.ts +165 -103
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/contract/schema.ts +7 -0
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/error/formatter.ts +10 -1
- package/src/experimental/index.ts +10 -0
- package/src/filling/context.ts +17 -17
- package/src/filling/filling.ts +22 -1
- package/src/filling/index.ts +15 -1
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +0 -10
- package/src/internal/index.ts +25 -0
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +69 -33
- package/src/router/client-entry.ts +134 -74
- package/src/router/fs-routes.ts +24 -22
- package/src/router/fs-scanner.ts +21 -17
- package/src/router/fs-types.ts +8 -5
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +103 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/image-feature.ts +15 -0
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/rate-limit.ts +231 -0
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/scheduler-lifecycle.ts +64 -0
- package/src/runtime/server.ts +27 -295
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/spec/schema.ts +4 -3
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
|
@@ -78,8 +78,8 @@ export async function buildDeployInferenceContext(
|
|
|
78
78
|
// still gets the manifest metadata and falls back to defaults.
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const imports = extractImports(source);
|
|
82
|
-
const dependencyClasses = classifySourceDependencies(source, imports);
|
|
81
|
+
const imports = extractImports(source);
|
|
82
|
+
const dependencyClasses = classifySourceDependencies(source, imports);
|
|
83
83
|
// Mandu's manifest patterns use `:param` / `*` (path-pattern style),
|
|
84
84
|
// not bracket-form. Detect both shapes so the heuristic doesn't
|
|
85
85
|
// misclassify dynamic routes as prerenderable. Examples:
|
|
@@ -139,82 +139,82 @@ export function extractImports(source: string): string[] {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/** Map import specifiers to coarse dependency classes. */
|
|
142
|
-
export function classifyImports(imports: string[]): ReadonlySet<DependencyClass> {
|
|
143
|
-
const classes = new Set<DependencyClass>();
|
|
144
|
-
for (const spec of imports) {
|
|
142
|
+
export function classifyImports(imports: string[]): ReadonlySet<DependencyClass> {
|
|
143
|
+
const classes = new Set<DependencyClass>();
|
|
144
|
+
for (const spec of imports) {
|
|
145
145
|
const cls = classifyOne(spec);
|
|
146
146
|
if (cls) classes.add(cls);
|
|
147
147
|
}
|
|
148
148
|
if (classes.size === 0) classes.add("fetch-only");
|
|
149
|
-
return classes;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Classify dependency signals that do not always appear as bare imports.
|
|
154
|
-
*
|
|
155
|
-
* Dogfooding surfaced Mandu API routes importing project-local
|
|
156
|
-
* `src/server/infra/db` helpers and using `db` tagged templates. Those
|
|
157
|
-
* are server-only even when the bare imports look edge-safe.
|
|
158
|
-
*/
|
|
159
|
-
export function classifySourceDependencies(
|
|
160
|
-
source: string,
|
|
161
|
-
imports: string[] = extractImports(source),
|
|
162
|
-
): ReadonlySet<DependencyClass> {
|
|
163
|
-
const classes = new Set<DependencyClass>(classifyImports(imports));
|
|
164
|
-
if (classes.size === 1 && classes.has("fetch-only")) {
|
|
165
|
-
classes.delete("fetch-only");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const allImports = extractAllImportSpecifiers(source);
|
|
169
|
-
if (allImports.some(isServerInfraImport)) {
|
|
170
|
-
classes.add("db");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (/\bBun\.SQL\b|\bBun\.sqlite\b/i.test(source)) {
|
|
174
|
-
classes.add("bun-native");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
/(?:^|[^\w$])db\s*`/.test(source) ||
|
|
179
|
-
/\bctx\.deps\.db\b/.test(source) ||
|
|
180
|
-
/\bdb\.(?:query|execute|select|insert|update|delete)\b/.test(source)
|
|
181
|
-
) {
|
|
182
|
-
classes.add("db");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (classes.size === 0) {
|
|
186
|
-
classes.add("fetch-only");
|
|
187
|
-
}
|
|
188
|
-
return classes;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function extractAllImportSpecifiers(source: string): string[] {
|
|
192
|
-
const out = new Set<string>();
|
|
193
|
-
const staticImport = /^\s*import\b[^"']*?["']([^"']+)["']/gm;
|
|
194
|
-
const dynamicImport = /\bimport\(\s*["']([^"']+)["']\s*\)/g;
|
|
195
|
-
for (const re of [staticImport, dynamicImport]) {
|
|
196
|
-
let m: RegExpExecArray | null;
|
|
197
|
-
while ((m = re.exec(source)) !== null) {
|
|
198
|
-
out.add(m[1]!);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return [...out].sort();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function isServerInfraImport(specifier: string): boolean {
|
|
205
|
-
const s = specifier.replace(/\\/g, "/").toLowerCase();
|
|
206
|
-
return (
|
|
207
|
-
s === "@/server/infra" ||
|
|
208
|
-
s.startsWith("@/server/infra/") ||
|
|
209
|
-
s === "src/server/infra" ||
|
|
210
|
-
s.startsWith("src/server/infra/") ||
|
|
211
|
-
s.endsWith("/server/infra") ||
|
|
212
|
-
s.includes("/server/infra/")
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function classifyOne(spec: string): DependencyClass | null {
|
|
217
|
-
const s = spec.toLowerCase();
|
|
149
|
+
return classes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Classify dependency signals that do not always appear as bare imports.
|
|
154
|
+
*
|
|
155
|
+
* Dogfooding surfaced Mandu API routes importing project-local
|
|
156
|
+
* `src/server/infra/db` helpers and using `db` tagged templates. Those
|
|
157
|
+
* are server-only even when the bare imports look edge-safe.
|
|
158
|
+
*/
|
|
159
|
+
export function classifySourceDependencies(
|
|
160
|
+
source: string,
|
|
161
|
+
imports: string[] = extractImports(source),
|
|
162
|
+
): ReadonlySet<DependencyClass> {
|
|
163
|
+
const classes = new Set<DependencyClass>(classifyImports(imports));
|
|
164
|
+
if (classes.size === 1 && classes.has("fetch-only")) {
|
|
165
|
+
classes.delete("fetch-only");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const allImports = extractAllImportSpecifiers(source);
|
|
169
|
+
if (allImports.some(isServerInfraImport)) {
|
|
170
|
+
classes.add("db");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (/\bBun\.SQL\b|\bBun\.sqlite\b/i.test(source)) {
|
|
174
|
+
classes.add("bun-native");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
/(?:^|[^\w$])db\s*`/.test(source) ||
|
|
179
|
+
/\bctx\.deps\.db\b/.test(source) ||
|
|
180
|
+
/\bdb\.(?:query|execute|select|insert|update|delete)\b/.test(source)
|
|
181
|
+
) {
|
|
182
|
+
classes.add("db");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (classes.size === 0) {
|
|
186
|
+
classes.add("fetch-only");
|
|
187
|
+
}
|
|
188
|
+
return classes;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractAllImportSpecifiers(source: string): string[] {
|
|
192
|
+
const out = new Set<string>();
|
|
193
|
+
const staticImport = /^\s*import\b[^"']*?["']([^"']+)["']/gm;
|
|
194
|
+
const dynamicImport = /\bimport\(\s*["']([^"']+)["']\s*\)/g;
|
|
195
|
+
for (const re of [staticImport, dynamicImport]) {
|
|
196
|
+
let m: RegExpExecArray | null;
|
|
197
|
+
while ((m = re.exec(source)) !== null) {
|
|
198
|
+
out.add(m[1]!);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...out].sort();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isServerInfraImport(specifier: string): boolean {
|
|
205
|
+
const s = specifier.replace(/\\/g, "/").toLowerCase();
|
|
206
|
+
return (
|
|
207
|
+
s === "@/server/infra" ||
|
|
208
|
+
s.startsWith("@/server/infra/") ||
|
|
209
|
+
s === "src/server/infra" ||
|
|
210
|
+
s.startsWith("src/server/infra/") ||
|
|
211
|
+
s.endsWith("/server/infra") ||
|
|
212
|
+
s.includes("/server/infra/")
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function classifyOne(spec: string): DependencyClass | null {
|
|
217
|
+
const s = spec.toLowerCase();
|
|
218
218
|
if (
|
|
219
219
|
s === "bun:sqlite" ||
|
|
220
220
|
s === "bun:ffi" ||
|
|
@@ -231,14 +231,14 @@ function classifyOne(spec: string): DependencyClass | null {
|
|
|
231
231
|
if (s === "node:child_process" || s === "child_process" || s === "node:worker_threads" || s === "worker_threads") {
|
|
232
232
|
return "node-child";
|
|
233
233
|
}
|
|
234
|
-
if (
|
|
235
|
-
/^(postgres|pg|mysql2?|drizzle-orm(\/.*)?|@prisma\/client|prisma|mongodb|mongoose|@neondatabase\/.+|kysely|sqlite3|better-sqlite3|@planetscale\/.+)$/.test(s)
|
|
236
|
-
) {
|
|
237
|
-
return "db";
|
|
238
|
-
}
|
|
239
|
-
if (s === "@mandujs/core/db" || s.startsWith("@mandujs/core/db/")) {
|
|
240
|
-
return "db";
|
|
241
|
-
}
|
|
234
|
+
if (
|
|
235
|
+
/^(postgres|pg|mysql2?|drizzle-orm(\/.*)?|@prisma\/client|prisma|mongodb|mongoose|@neondatabase\/.+|kysely|sqlite3|better-sqlite3|@planetscale\/.+)$/.test(s)
|
|
236
|
+
) {
|
|
237
|
+
return "db";
|
|
238
|
+
}
|
|
239
|
+
if (s === "@mandujs/core/db" || s.startsWith("@mandujs/core/db/")) {
|
|
240
|
+
return "db";
|
|
241
|
+
}
|
|
242
242
|
if (/^(@anthropic-ai\/sdk|openai|ai|@ai-sdk\/.+|@google\/generative-ai|cohere-ai)$/.test(s)) {
|
|
243
243
|
return "ai-sdk";
|
|
244
244
|
}
|
|
@@ -183,28 +183,28 @@ export function IslandsPanel({ islands }: IslandsPanelProps): React.ReactElement
|
|
|
183
183
|
);
|
|
184
184
|
}, [islands]);
|
|
185
185
|
|
|
186
|
-
if (islands.length === 0) {
|
|
187
|
-
return (
|
|
188
|
-
<div style={styles.container}>
|
|
189
|
-
<div style={styles.emptyState}>
|
|
190
|
-
<p>
|
|
191
|
-
아직 hydration 이벤트가 없습니다.<br />
|
|
192
|
-
정적 상태는 mandu.runtime.status에서 확인하세요.
|
|
193
|
-
</p>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
186
|
+
if (islands.length === 0) {
|
|
187
|
+
return (
|
|
188
|
+
<div style={styles.container}>
|
|
189
|
+
<div style={styles.emptyState}>
|
|
190
|
+
<p>
|
|
191
|
+
아직 hydration 이벤트가 없습니다.<br />
|
|
192
|
+
정적 상태는 mandu.runtime.status에서 확인하세요.
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
198
|
|
|
199
199
|
return (
|
|
200
200
|
<div style={styles.container}>
|
|
201
201
|
{/* Header Stats */}
|
|
202
202
|
<div style={styles.header}>
|
|
203
203
|
<div style={styles.stats}>
|
|
204
|
-
<div style={styles.stat}>
|
|
205
|
-
<span>Mounts:</span>
|
|
206
|
-
<span style={styles.statValue}>{stats.total}</span>
|
|
207
|
-
</div>
|
|
204
|
+
<div style={styles.stat}>
|
|
205
|
+
<span>Mounts:</span>
|
|
206
|
+
<span style={styles.statValue}>{stats.total}</span>
|
|
207
|
+
</div>
|
|
208
208
|
<div style={styles.stat}>
|
|
209
209
|
<span>Ready</span>
|
|
210
210
|
<span style={styles.statValue}>{stats.hydrated}</span>
|
|
@@ -28,7 +28,7 @@ export interface PanelContainerProps {
|
|
|
28
28
|
|
|
29
29
|
export const TABS: TabDefinition[] = [
|
|
30
30
|
{ id: 'errors', label: 'Issues', icon: 'ERR', testId: testIds.tabErrors },
|
|
31
|
-
{ id: 'islands', label: 'Client Mounts', icon: 'HYD', testId: testIds.tabIslands },
|
|
31
|
+
{ id: 'islands', label: 'Client Mounts', icon: 'HYD', testId: testIds.tabIslands },
|
|
32
32
|
{ id: 'network', label: 'Network', icon: 'NET', testId: testIds.tabNetwork },
|
|
33
33
|
{ id: 'guard', label: 'Guard', icon: 'GRD', testId: testIds.tabGuard },
|
|
34
34
|
{ id: 'preview', label: 'Changes', icon: 'CHG', testId: testIds.tabPreview },
|
package/src/error/formatter.ts
CHANGED
|
@@ -24,11 +24,16 @@ export function formatErrorResponse(error: ManduError, options: FormatOptions =
|
|
|
24
24
|
code: error.code,
|
|
25
25
|
message: error.message,
|
|
26
26
|
summary: error.summary,
|
|
27
|
+
cause: error.summary,
|
|
27
28
|
fix: error.fix,
|
|
29
|
+
filePath: error.fix.file,
|
|
30
|
+
solution: error.fix.suggestion,
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
if (error.route) {
|
|
31
34
|
response.route = error.route;
|
|
35
|
+
response.routeId = error.route.id;
|
|
36
|
+
response.routePattern = error.route.pattern;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// 개발 모드에서만 디버그 정보 포함
|
|
@@ -65,6 +70,7 @@ export function formatErrorForConsole(error: ManduError, options: FormatOptions
|
|
|
65
70
|
} else {
|
|
66
71
|
lines.push(` → ${error.summary}`);
|
|
67
72
|
}
|
|
73
|
+
lines.push(` Cause: ${error.summary}`);
|
|
68
74
|
|
|
69
75
|
// 수정 안내
|
|
70
76
|
lines.push("");
|
|
@@ -75,11 +81,14 @@ export function formatErrorForConsole(error: ManduError, options: FormatOptions
|
|
|
75
81
|
lines.push(` Fix: ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
|
|
76
82
|
lines.push(` ${error.fix.suggestion}`);
|
|
77
83
|
}
|
|
84
|
+
lines.push(` File: ${error.fix.file}${error.fix.line ? `:${error.fix.line}` : ""}`);
|
|
85
|
+
lines.push(` Solution: ${error.fix.suggestion}`);
|
|
78
86
|
|
|
79
87
|
// 라우트 컨텍스트
|
|
80
88
|
if (error.route) {
|
|
81
89
|
lines.push("");
|
|
82
|
-
lines.push(` Route: ${error.route.id}
|
|
90
|
+
lines.push(` Route ID: ${error.route.id}`);
|
|
91
|
+
lines.push(` Route: ${error.route.pattern}`);
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
// 디버그 정보 (개발 모드)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * as a11y from "../a11y";
|
|
2
|
+
export * as agent from "../agent";
|
|
3
|
+
export * as brain from "../brain";
|
|
4
|
+
export * as deploy from "../deploy";
|
|
5
|
+
export * as design from "../design";
|
|
6
|
+
export * as desktop from "../desktop";
|
|
7
|
+
export * as devtools from "../devtools";
|
|
8
|
+
export * as diagnose from "../diagnose";
|
|
9
|
+
export * as kitchen from "../kitchen";
|
|
10
|
+
export * as scheduler from "../scheduler";
|
package/src/filling/context.ts
CHANGED
|
@@ -628,23 +628,23 @@ export class ManduContext {
|
|
|
628
628
|
return this.withCookies(new Response(null, { status: 204 }));
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
/** 400 Bad Request, or custom 4xx/5xx error with ctx.error(status, message). */
|
|
632
|
-
error(message: string, details?: unknown): Response;
|
|
633
|
-
error(status: number, message: string, details?: unknown): Response;
|
|
634
|
-
error(
|
|
635
|
-
statusOrMessage: number | string,
|
|
636
|
-
messageOrDetails?: string | unknown,
|
|
637
|
-
maybeDetails?: unknown
|
|
638
|
-
): Response {
|
|
639
|
-
if (typeof statusOrMessage === "number") {
|
|
640
|
-
const status = Number.isInteger(statusOrMessage) && statusOrMessage >= 400 && statusOrMessage <= 599
|
|
641
|
-
? statusOrMessage
|
|
642
|
-
: 400;
|
|
643
|
-
const message = typeof messageOrDetails === "string" ? messageOrDetails : "Error";
|
|
644
|
-
return this.json({ status: "error", message, details: maybeDetails }, status);
|
|
645
|
-
}
|
|
646
|
-
return this.json({ status: "error", message: statusOrMessage, details: messageOrDetails }, 400);
|
|
647
|
-
}
|
|
631
|
+
/** 400 Bad Request, or custom 4xx/5xx error with ctx.error(status, message). */
|
|
632
|
+
error(message: string, details?: unknown): Response;
|
|
633
|
+
error(status: number, message: string, details?: unknown): Response;
|
|
634
|
+
error(
|
|
635
|
+
statusOrMessage: number | string,
|
|
636
|
+
messageOrDetails?: string | unknown,
|
|
637
|
+
maybeDetails?: unknown
|
|
638
|
+
): Response {
|
|
639
|
+
if (typeof statusOrMessage === "number") {
|
|
640
|
+
const status = Number.isInteger(statusOrMessage) && statusOrMessage >= 400 && statusOrMessage <= 599
|
|
641
|
+
? statusOrMessage
|
|
642
|
+
: 400;
|
|
643
|
+
const message = typeof messageOrDetails === "string" ? messageOrDetails : "Error";
|
|
644
|
+
return this.json({ status: "error", message, details: maybeDetails }, status);
|
|
645
|
+
}
|
|
646
|
+
return this.json({ status: "error", message: statusOrMessage, details: messageOrDetails }, 400);
|
|
647
|
+
}
|
|
648
648
|
|
|
649
649
|
/** 401 Unauthorized */
|
|
650
650
|
unauthorized(message: string = "Unauthorized"): Response {
|
package/src/filling/filling.ts
CHANGED
|
@@ -62,9 +62,17 @@ export interface MiddlewarePlugin {
|
|
|
62
62
|
* `redirect(url)` helper for the common case; throwing a `Response` is
|
|
63
63
|
* also accepted (Remix idiom).
|
|
64
64
|
*/
|
|
65
|
+
export type LoaderResult<T = unknown> = T | Response;
|
|
66
|
+
|
|
65
67
|
export type Loader<T = unknown> = (
|
|
66
68
|
ctx: ManduContext
|
|
67
|
-
) => T |
|
|
69
|
+
) => LoaderResult<T> | Promise<LoaderResult<T>>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Page-level SSR data reader. Prefer this name in public docs when the
|
|
73
|
+
* distinction from mutation actions or schema contracts matters.
|
|
74
|
+
*/
|
|
75
|
+
export type RouteDataLoader<T = unknown> = Loader<T>;
|
|
68
76
|
|
|
69
77
|
/** Loader 실행 옵션 */
|
|
70
78
|
export interface LoaderOptions<T = unknown> {
|
|
@@ -105,6 +113,19 @@ export class LoaderTimeoutError extends Error {
|
|
|
105
113
|
/** Action handler type — named mutation handler */
|
|
106
114
|
export type ActionHandler = (ctx: ManduContext) => Response | Promise<Response>;
|
|
107
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Named mutation/interaction handler. Alias of `ActionHandler`, exported so
|
|
118
|
+
* docs and generated examples can name the action responsibility directly.
|
|
119
|
+
*/
|
|
120
|
+
export type MutationAction = ActionHandler;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Executable route pipeline: handlers, loader, actions, middleware, cache,
|
|
124
|
+
* render mode, and deploy intent. This is intentionally separate from the
|
|
125
|
+
* API schema contract.
|
|
126
|
+
*/
|
|
127
|
+
export type RouteFilling<TLoaderData = unknown> = ManduFilling<TLoaderData>;
|
|
128
|
+
|
|
108
129
|
interface FillingConfig<TLoaderData = unknown> {
|
|
109
130
|
handlers: Map<HttpMethod, Handler>;
|
|
110
131
|
actions: Map<string, ActionHandler>;
|
package/src/filling/index.ts
CHANGED
|
@@ -7,7 +7,21 @@
|
|
|
7
7
|
export { ManduContext, ValidationError, CookieManager } from "./context";
|
|
8
8
|
export type { CookieOptions } from "./context";
|
|
9
9
|
export { ManduFilling, ManduFillingFactory, LoaderTimeoutError } from "./filling";
|
|
10
|
-
export type {
|
|
10
|
+
export type {
|
|
11
|
+
Handler,
|
|
12
|
+
Guard,
|
|
13
|
+
ActionHandler,
|
|
14
|
+
MutationAction,
|
|
15
|
+
HttpMethod,
|
|
16
|
+
Loader,
|
|
17
|
+
RouteDataLoader,
|
|
18
|
+
LoaderResult,
|
|
19
|
+
LoaderOptions,
|
|
20
|
+
LoaderCacheOptions,
|
|
21
|
+
RenderMode,
|
|
22
|
+
MiddlewarePlugin,
|
|
23
|
+
RouteFilling,
|
|
24
|
+
} from "./filling";
|
|
11
25
|
export { createCookieSessionStorage, Session } from "./session";
|
|
12
26
|
export type { SessionStorage, SessionData, CookieSessionOptions } from "./session";
|
|
13
27
|
export { wrapBunWebSocket } from "./ws";
|
|
@@ -99,25 +99,25 @@ export interface GeneratedMap {
|
|
|
99
99
|
frameworkPaths: string[];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
async function ensureDir(dirPath: string): Promise<void> {
|
|
103
|
-
try {
|
|
104
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
105
|
-
} catch {
|
|
106
|
-
// ignore if exists
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function touchGenerateStamp(rootDir: string): void {
|
|
111
|
-
const stampDir = path.join(rootDir, ".mandu");
|
|
112
|
-
if (!fsSync.existsSync(stampDir)) {
|
|
113
|
-
fsSync.mkdirSync(stampDir, { recursive: true });
|
|
114
|
-
}
|
|
115
|
-
fsSync.writeFileSync(path.join(stampDir, "generate.stamp"), Date.now().toString());
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function getExistingFiles(dir: string): Promise<string[]> {
|
|
119
|
-
try {
|
|
120
|
-
const files = await fs.readdir(dir);
|
|
102
|
+
async function ensureDir(dirPath: string): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
105
|
+
} catch {
|
|
106
|
+
// ignore if exists
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function touchGenerateStamp(rootDir: string): void {
|
|
111
|
+
const stampDir = path.join(rootDir, ".mandu");
|
|
112
|
+
if (!fsSync.existsSync(stampDir)) {
|
|
113
|
+
fsSync.mkdirSync(stampDir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
fsSync.writeFileSync(path.join(stampDir, "generate.stamp"), Date.now().toString());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function getExistingFiles(dir: string): Promise<string[]> {
|
|
119
|
+
try {
|
|
120
|
+
const files = await fs.readdir(dir);
|
|
121
121
|
return files.filter((f) => f.endsWith(".route.ts") || f.endsWith(".route.tsx"));
|
|
122
122
|
} catch {
|
|
123
123
|
return [];
|
|
@@ -146,10 +146,10 @@ export async function generateRoutes(
|
|
|
146
146
|
warnings: [],
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
-
// Suppress watcher during generation to avoid false positives
|
|
150
|
-
const watcher = getWatcher();
|
|
151
|
-
watcher?.suppress();
|
|
152
|
-
touchGenerateStamp(rootDir);
|
|
149
|
+
// Suppress watcher during generation to avoid false positives
|
|
150
|
+
const watcher = getWatcher();
|
|
151
|
+
watcher?.suppress();
|
|
152
|
+
touchGenerateStamp(rootDir);
|
|
153
153
|
|
|
154
154
|
const generatedPaths = resolveGeneratedPaths(rootDir);
|
|
155
155
|
const serverRoutesDir = generatedPaths.serverRoutesDir;
|
|
@@ -363,10 +363,10 @@ export async function generateRoutes(
|
|
|
363
363
|
const mapPath = path.join(mapDir, "generated.map.json");
|
|
364
364
|
await Bun.write(mapPath, JSON.stringify(generatedMap, null, 2));
|
|
365
365
|
|
|
366
|
-
// Cross-process timestamp: watcher skips warnings if generate finished recently
|
|
367
|
-
touchGenerateStamp(rootDir);
|
|
368
|
-
// Resume watcher after the stamp is visible.
|
|
369
|
-
watcher?.resume();
|
|
370
|
-
|
|
371
|
-
return result;
|
|
372
|
-
}
|
|
366
|
+
// Cross-process timestamp: watcher skips warnings if generate finished recently
|
|
367
|
+
touchGenerateStamp(rootDir);
|
|
368
|
+
// Resume watcher after the stamp is visible.
|
|
369
|
+
watcher?.resume();
|
|
370
|
+
|
|
371
|
+
return result;
|
|
372
|
+
}
|
package/src/generator/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./generate";
|
|
2
|
-
export * from "./templates";
|
|
3
|
-
export * from "./contract-glue";
|
|
1
|
+
export * from "./generate";
|
|
2
|
+
export * from "./templates";
|
|
3
|
+
export * from "./contract-glue";
|