@mandujs/core 0.54.12 → 0.54.14

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 (80) hide show
  1. package/package.json +9 -1
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +49 -1
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +8 -0
  11. package/src/agent/verify.ts +100 -2
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +36 -16
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/__tests__/fast-refresh.test.ts +24 -17
  17. package/src/bundler/analyzer.ts +15 -15
  18. package/src/bundler/build.test.ts +136 -48
  19. package/src/bundler/build.ts +193 -122
  20. package/src/bundler/css.ts +42 -42
  21. package/src/bundler/manifest-schema.ts +21 -21
  22. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  23. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  24. package/src/bundler/types.ts +31 -31
  25. package/src/client/island.ts +79 -79
  26. package/src/config/validate.ts +1 -1
  27. package/src/contract/schema.ts +7 -0
  28. package/src/deploy/inference/context.ts +82 -82
  29. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  30. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  31. package/src/error/formatter.ts +10 -1
  32. package/src/experimental/index.ts +10 -0
  33. package/src/filling/context.ts +17 -17
  34. package/src/filling/filling.ts +22 -1
  35. package/src/filling/index.ts +15 -1
  36. package/src/generator/generate.ts +30 -30
  37. package/src/generator/index.ts +3 -3
  38. package/src/generator/templates.ts +210 -210
  39. package/src/guard/check.ts +9 -9
  40. package/src/guard/config-guard.ts +13 -13
  41. package/src/guard/fs-routes-policy.ts +51 -51
  42. package/src/guard/index.ts +11 -11
  43. package/src/index.ts +0 -10
  44. package/src/internal/index.ts +25 -0
  45. package/src/kitchen/api/file-api.ts +11 -11
  46. package/src/report/index.ts +1 -1
  47. package/src/resource/__tests__/generator.test.ts +6 -6
  48. package/src/resource/__tests__/schema.test.ts +14 -14
  49. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  50. package/src/resource/ddl/emit.ts +146 -146
  51. package/src/resource/generator-schema.ts +11 -11
  52. package/src/resource/generators/slot.ts +72 -72
  53. package/src/resource/schema.ts +21 -21
  54. package/src/router/client-entry.test.ts +84 -41
  55. package/src/router/client-entry.ts +156 -89
  56. package/src/router/fs-routes.test.ts +90 -0
  57. package/src/router/fs-routes.ts +37 -29
  58. package/src/router/fs-scanner.ts +81 -33
  59. package/src/router/fs-types.ts +8 -5
  60. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  61. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  62. package/src/runtime/__tests__/page-render-response.test.ts +164 -103
  63. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  64. package/src/runtime/devtools-adapter.ts +68 -68
  65. package/src/runtime/escape.ts +34 -34
  66. package/src/runtime/image-feature.ts +15 -0
  67. package/src/runtime/observability-lifecycle.ts +290 -290
  68. package/src/runtime/page-render-response.ts +208 -106
  69. package/src/runtime/rate-limit.ts +231 -0
  70. package/src/runtime/request-middleware.ts +31 -31
  71. package/src/runtime/router.test.ts +4 -4
  72. package/src/runtime/router.ts +10 -12
  73. package/src/runtime/scheduler-lifecycle.ts +64 -0
  74. package/src/runtime/server.ts +148 -353
  75. package/src/runtime/ssr.ts +59 -59
  76. package/src/runtime/static-files.ts +289 -289
  77. package/src/runtime/streaming-ssr.ts +22 -22
  78. package/src/spec/schema.ts +4 -3
  79. package/src/watcher/__tests__/watcher.test.ts +59 -59
  80. package/src/watcher/watcher.ts +61 -61
@@ -55,28 +55,28 @@ export interface BundleManifest {
55
55
  priority: "immediate" | "visible" | "idle" | "interaction";
56
56
  }
57
57
  >;
58
- /** Per-island bundles (code splitting: each island file gets its own JS bundle) */
59
- islands?: Record<
60
- string,
61
- {
62
- /** JavaScript bundle path */
58
+ /** Per-island bundles (code splitting: each island file gets its own JS bundle) */
59
+ islands?: Record<
60
+ string,
61
+ {
62
+ /** JavaScript bundle path */
63
63
  js: string;
64
64
  /** Route that owns this island */
65
65
  route: string;
66
66
  /** Hydration priority */
67
- priority: "immediate" | "visible" | "idle" | "interaction";
68
- }
69
- >;
70
- /** Inline partial bundles (from *.partial.tsx / *.partial.ts files) */
71
- partials?: Record<
72
- string,
73
- {
74
- /** JavaScript bundle path */
75
- js: string;
76
- /** Hydration priority */
77
- priority: "immediate" | "visible" | "idle" | "interaction";
78
- }
79
- >;
67
+ priority: "immediate" | "visible" | "idle" | "interaction";
68
+ }
69
+ >;
70
+ /** Inline partial bundles (from *.partial.tsx / *.partial.ts files) */
71
+ partials?: Record<
72
+ string,
73
+ {
74
+ /** JavaScript bundle path */
75
+ js: string;
76
+ /** Hydration priority */
77
+ priority: "immediate" | "visible" | "idle" | "interaction";
78
+ }
79
+ >;
80
80
  /** 공유 청크 */
81
81
  shared: {
82
82
  /** Hydration 런타임 */
@@ -123,19 +123,19 @@ export interface BundleStats {
123
123
  }
124
124
 
125
125
  /** Per-island code splitting entry (used by scanIslandFiles) */
126
- export interface IslandFileEntry {
127
- name: string;
128
- filePath: string;
129
- routeId: string;
130
- priority: "immediate" | "visible" | "idle" | "interaction";
131
- }
132
-
133
- /** Inline partial bundle entry (used by scanPartialFiles) */
134
- export interface PartialFileEntry {
135
- name: string;
136
- filePath: string;
137
- priority: "immediate" | "visible" | "idle" | "interaction";
138
- }
126
+ export interface IslandFileEntry {
127
+ name: string;
128
+ filePath: string;
129
+ routeId: string;
130
+ priority: "immediate" | "visible" | "idle" | "interaction";
131
+ }
132
+
133
+ /** Inline partial bundle entry (used by scanPartialFiles) */
134
+ export interface PartialFileEntry {
135
+ name: string;
136
+ filePath: string;
137
+ priority: "immediate" | "visible" | "idle" | "interaction";
138
+ }
139
139
 
140
140
  /**
141
141
  * 번들러 옵션
@@ -3,9 +3,9 @@
3
3
  * Hydration을 위한 클라이언트 사이드 컴포넌트 정의
4
4
  */
5
5
 
6
- import type { ReactNode } from "react";
7
- import { serializeProps } from "./serialize";
8
- import { getServerData as getGlobalServerData } from "./window-state";
6
+ import type { ReactNode } from "react";
7
+ import { serializeProps } from "./serialize";
8
+ import { getServerData as getGlobalServerData } from "./window-state";
9
9
 
10
10
  /**
11
11
  * Island 정의 타입
@@ -336,45 +336,45 @@ export interface PartialConfig {
336
336
  /**
337
337
  * Partial Island 정의 타입
338
338
  */
339
- export interface PartialDefinition<TProps> {
340
- /** Partial 고유 ID. 기본값은 component displayName/name 입니다. */
341
- id?: string;
342
- /** Partial 컴포넌트 */
343
- component: React.ComponentType<TProps>;
344
- /** 초기 props (SSR에서 전달) */
345
- initialProps?: TProps;
346
- /** 하이드레이션 우선순위 */
347
- priority?: "immediate" | "visible" | "idle" | "interaction";
348
- /** 명시적 번들 URL. 기본값은 `/.mandu/client/{id}.partial.js` 입니다. */
349
- src?: string;
350
- /** 에러 시 표시할 UI */
351
- errorBoundary?: (error: Error, reset: () => void) => ReactNode;
352
- /** 로딩 중 표시할 UI */
353
- loading?: () => ReactNode;
354
- }
339
+ export interface PartialDefinition<TProps> {
340
+ /** Partial 고유 ID. 기본값은 component displayName/name 입니다. */
341
+ id?: string;
342
+ /** Partial 컴포넌트 */
343
+ component: React.ComponentType<TProps>;
344
+ /** 초기 props (SSR에서 전달) */
345
+ initialProps?: TProps;
346
+ /** 하이드레이션 우선순위 */
347
+ priority?: "immediate" | "visible" | "idle" | "interaction";
348
+ /** 명시적 번들 URL. 기본값은 `/.mandu/client/{id}.partial.js` 입니다. */
349
+ src?: string;
350
+ /** 에러 시 표시할 UI */
351
+ errorBoundary?: (error: Error, reset: () => void) => ReactNode;
352
+ /** 로딩 중 표시할 UI */
353
+ loading?: () => ReactNode;
354
+ }
355
355
 
356
356
  /**
357
357
  * 컴파일된 Partial
358
358
  */
359
- export interface CompiledPartial<TProps> {
359
+ export interface CompiledPartial<TProps> {
360
360
  /** Partial 정의 */
361
361
  definition: PartialDefinition<TProps>;
362
362
  /** Mandu Partial 마커 */
363
363
  __mandu_partial: true;
364
364
  /** Partial ID */
365
- __mandu_partial_id?: string;
366
- }
367
-
368
- function normalizePartialId(id: string): string {
369
- return id
370
- .trim()
371
- .replace(/[^A-Za-z0-9_-]/g, "-")
372
- .replace(/^-+|-+$/g, "") || "partial";
373
- }
374
-
375
- function priorityToHydrate(priority: NonNullable<PartialDefinition<unknown>["priority"]>): string {
376
- return priority === "immediate" ? "load" : priority;
377
- }
365
+ __mandu_partial_id?: string;
366
+ }
367
+
368
+ function normalizePartialId(id: string): string {
369
+ return id
370
+ .trim()
371
+ .replace(/[^A-Za-z0-9_-]/g, "-")
372
+ .replace(/^-+|-+$/g, "") || "partial";
373
+ }
374
+
375
+ function priorityToHydrate(priority: NonNullable<PartialDefinition<unknown>["priority"]>): string {
376
+ return priority === "immediate" ? "load" : priority;
377
+ }
378
378
 
379
379
  /**
380
380
  * Partial Island 생성
@@ -399,56 +399,56 @@ function priorityToHydrate(priority: NonNullable<PartialDefinition<unknown>["pri
399
399
  * }
400
400
  * ```
401
401
  */
402
- export function partial<TProps extends Record<string, unknown>>(
403
- definition: PartialDefinition<TProps>
404
- ): CompiledPartial<TProps> & {
405
- Render: React.ComponentType<TProps>;
406
- } {
402
+ export function partial<TProps extends Record<string, unknown>>(
403
+ definition: PartialDefinition<TProps>
404
+ ): CompiledPartial<TProps> & {
405
+ Render: React.ComponentType<TProps>;
406
+ } {
407
407
  if (!definition.component) {
408
408
  throw new Error("[Mandu Partial] component is required");
409
409
  }
410
410
 
411
- const partialId = normalizePartialId(
412
- definition.id ||
413
- definition.component.displayName ||
414
- definition.component.name ||
415
- "partial",
416
- );
417
- const normalizedDefinition: PartialDefinition<TProps> = {
418
- ...definition,
419
- id: partialId,
420
- };
421
-
422
- const compiled: CompiledPartial<TProps> = {
423
- definition: normalizedDefinition,
424
- __mandu_partial: true,
425
- __mandu_partial_id: partialId,
426
- };
427
-
428
- // Render 컴포넌트 생성
429
- const React = require("react");
430
-
431
- const RenderComponent: React.FC<TProps> = (props) => {
432
- const renderProps = Object.keys(props).length > 0
433
- ? props
434
- : (normalizedDefinition.initialProps ?? props);
435
- const priority = normalizedDefinition.priority ?? "visible";
436
- const bundleSrc = normalizedDefinition.src ?? `/.mandu/client/${partialId}.partial.js`;
437
-
438
- return React.createElement(
439
- "div",
440
- {
441
- "data-mandu-island": partialId,
442
- "data-mandu-partial": partialId,
443
- "data-mandu-src": bundleSrc,
444
- "data-mandu-priority": priority,
445
- "data-hydrate": priorityToHydrate(priority),
446
- "data-props": serializeProps(renderProps),
447
- style: { display: "contents" },
448
- },
449
- React.createElement(normalizedDefinition.component, renderProps),
450
- );
451
- };
411
+ const partialId = normalizePartialId(
412
+ definition.id ||
413
+ definition.component.displayName ||
414
+ definition.component.name ||
415
+ "partial",
416
+ );
417
+ const normalizedDefinition: PartialDefinition<TProps> = {
418
+ ...definition,
419
+ id: partialId,
420
+ };
421
+
422
+ const compiled: CompiledPartial<TProps> = {
423
+ definition: normalizedDefinition,
424
+ __mandu_partial: true,
425
+ __mandu_partial_id: partialId,
426
+ };
427
+
428
+ // Render 컴포넌트 생성
429
+ const React = require("react");
430
+
431
+ const RenderComponent: React.FC<TProps> = (props) => {
432
+ const renderProps = Object.keys(props).length > 0
433
+ ? props
434
+ : (normalizedDefinition.initialProps ?? props);
435
+ const priority = normalizedDefinition.priority ?? "visible";
436
+ const bundleSrc = normalizedDefinition.src ?? `/.mandu/client/${partialId}.partial.js`;
437
+
438
+ return React.createElement(
439
+ "div",
440
+ {
441
+ "data-mandu-island": partialId,
442
+ "data-mandu-partial": partialId,
443
+ "data-mandu-src": bundleSrc,
444
+ "data-mandu-priority": priority,
445
+ "data-hydrate": priorityToHydrate(priority),
446
+ "data-props": serializeProps(renderProps),
447
+ style: { display: "contents" },
448
+ },
449
+ React.createElement(normalizedDefinition.component, renderProps),
450
+ );
451
+ };
452
452
 
453
453
  return Object.assign(compiled, { Render: RenderComponent });
454
454
  }
@@ -43,7 +43,7 @@ function _strictWithWarnings<T extends z.ZodRawShape>(
43
43
  */
44
44
  const ServerConfigSchema = z
45
45
  .object({
46
- port: z.number().min(1).max(65535).default(3333),
46
+ port: z.number().min(1).max(65535).default(3333),
47
47
  // Default `"::"` (IPv6 wildcard, dual-stack): accepts both IPv4 and
48
48
  // IPv6 clients on one socket. Fixes Windows Node 17+ fetch failing
49
49
  // with `ECONNREFUSED ::1:PORT` because `localhost` resolves to `::1`
@@ -152,6 +152,13 @@ export interface ContractInstance extends ContractSchema {
152
152
  _validated?: boolean;
153
153
  }
154
154
 
155
+ /**
156
+ * Public API schema boundary. Use this alias in docs and generated examples
157
+ * when distinguishing a request/response contract from an executable filling.
158
+ */
159
+ export type ApiContract<T extends ContractDefinition = ContractDefinition> =
160
+ T & ContractInstance;
161
+
155
162
  /** Validation error detail */
156
163
  export interface ContractValidationIssue {
157
164
  /** Field path (e.g., "body.email") */
@@ -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 },
@@ -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} (${error.route.pattern})`);
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";
@@ -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 {