@mandujs/core 0.18.19 → 0.18.21

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/brain/architecture/analyzer.ts +3 -5
  3. package/src/brain/architecture/types.ts +4 -4
  4. package/src/brain/doctor/index.ts +1 -1
  5. package/src/brain/doctor/reporter.ts +2 -2
  6. package/src/bundler/css.ts +3 -2
  7. package/src/bundler/dev.ts +29 -1
  8. package/src/config/validate.ts +1 -1
  9. package/src/contract/client.test.ts +2 -1
  10. package/src/contract/define.ts +3 -3
  11. package/src/contract/index.ts +2 -5
  12. package/src/contract/infer.test.ts +2 -1
  13. package/src/contract/normalize.ts +1 -1
  14. package/src/devtools/client/catchers/error-catcher.ts +3 -3
  15. package/src/devtools/client/components/kitchen-root.tsx +2 -2
  16. package/src/devtools/client/components/panel/guard-panel.tsx +3 -3
  17. package/src/devtools/client/state-manager.ts +9 -9
  18. package/src/devtools/index.ts +8 -8
  19. package/src/devtools/protocol.ts +4 -4
  20. package/src/devtools/types.ts +5 -5
  21. package/src/filling/deps.ts +4 -1
  22. package/src/filling/filling.ts +1 -1
  23. package/src/guard/index.ts +1 -1
  24. package/src/guard/presets/index.ts +3 -0
  25. package/src/guard/semantic-slots.ts +4 -4
  26. package/src/index.ts +9 -1
  27. package/src/island/index.ts +6 -6
  28. package/src/plugins/index.ts +1 -1
  29. package/src/plugins/types.ts +2 -2
  30. package/src/router/fs-routes.ts +5 -5
  31. package/src/router/fs-types.ts +2 -2
  32. package/src/router/index.ts +1 -1
  33. package/src/runtime/boundary.tsx +4 -4
  34. package/src/runtime/server.ts +15 -13
  35. package/src/runtime/ssr.ts +1 -1
  36. package/src/runtime/streaming-ssr.ts +4 -4
  37. package/src/seo/integration/ssr.ts +2 -2
  38. package/src/seo/render/basic.ts +12 -4
  39. package/src/seo/render/opengraph.ts +12 -6
  40. package/src/seo/render/twitter.ts +3 -2
  41. package/src/spec/schema.ts +6 -6
  42. package/src/utils/hasher.ts +2 -2
  43. package/src/utils/index.ts +1 -1
  44. package/src/watcher/watcher.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.18.19",
3
+ "version": "0.18.21",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -510,15 +510,13 @@ ${JSON.stringify(this.config.folders, null, 2)}
510
510
  짧고 명확하게 답변하세요 (3줄 이내).`;
511
511
 
512
512
  try {
513
- const result = await brain.complete([
514
- { role: "user", content: prompt },
515
- ]);
513
+ const result = await brain.generate(prompt);
516
514
 
517
515
  // 응답에서 경로 추출 시도
518
- const pathMatch = result.content.match(/(?:spec\/|\.mandu\/|app\/|src\/|packages\/)[^\s,)]+/);
516
+ const pathMatch = result.match(/(?:spec\/|\.mandu\/|app\/|src\/|packages\/)[^\s,)]+/);
519
517
 
520
518
  return {
521
- suggestion: result.content,
519
+ suggestion: result,
522
520
  recommendedPath: pathMatch?.[0],
523
521
  };
524
522
  } catch {
@@ -39,7 +39,7 @@ export interface ImportRule {
39
39
  /**
40
40
  * 레이어 의존성 규칙
41
41
  */
42
- export interface LayerRule {
42
+ export interface ArchLayerRule {
43
43
  /** 레이어 이름 */
44
44
  name: string;
45
45
  /** 레이어에 속하는 폴더 패턴 */
@@ -73,17 +73,17 @@ export interface ArchitectureConfig {
73
73
  /** Import 규칙 */
74
74
  imports?: ImportRule[];
75
75
  /** 레이어 규칙 */
76
- layers?: LayerRule[];
76
+ layers?: ArchLayerRule[];
77
77
  /** 네이밍 규칙 */
78
78
  naming?: NamingRule[];
79
79
  /** 커스텀 규칙 */
80
- custom?: CustomRule[];
80
+ custom?: ArchCustomRule[];
81
81
  }
82
82
 
83
83
  /**
84
84
  * 커스텀 규칙
85
85
  */
86
- export interface CustomRule {
86
+ export interface ArchCustomRule {
87
87
  /** 규칙 ID */
88
88
  id: string;
89
89
  /** 규칙 설명 */
@@ -30,7 +30,7 @@ export {
30
30
  } from "./patcher";
31
31
 
32
32
  export {
33
- formatViolation,
33
+ formatDoctorViolation,
34
34
  formatPatch,
35
35
  printDoctorReport,
36
36
  generateJsonReport,
@@ -50,7 +50,7 @@ function color(text: string, colorCode: string): string {
50
50
  /**
51
51
  * Format a violation for terminal output
52
52
  */
53
- export function formatViolation(violation: GuardViolation): string {
53
+ export function formatDoctorViolation(violation: GuardViolation): string {
54
54
  const lines: string[] = [];
55
55
 
56
56
  const severity = violation.severity || "error";
@@ -137,7 +137,7 @@ export function printDoctorReport(analysis: DoctorAnalysis): void {
137
137
  console.log();
138
138
 
139
139
  for (const violation of violations) {
140
- console.log(formatViolation(violation));
140
+ console.log(formatDoctorViolation(violation));
141
141
  console.log();
142
142
  }
143
143
  }
@@ -11,6 +11,7 @@
11
11
  import { spawn, type Subprocess } from "bun";
12
12
  import path from "path";
13
13
  import fs from "fs/promises";
14
+ import { watch as fsWatch, type FSWatcher } from "fs";
14
15
 
15
16
  // ========== Types ==========
16
17
 
@@ -223,12 +224,12 @@ export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatche
223
224
 
224
225
  // 출력 파일 워처로 빌드 완료 감지 (stdout 패턴보다 신뢰성 높음, #111)
225
226
  // Tailwind CLI stdout 출력 형식은 버전마다 달라질 수 있으므로 파일 변경으로 감지
226
- let fsWatcher: ReturnType<typeof fs.watch> | null = null;
227
+ let fsWatcher: FSWatcher | null = null;
227
228
  let lastMtime = 0;
228
229
 
229
230
  const startFileWatcher = () => {
230
231
  try {
231
- fsWatcher = fs.watch(outputPath, () => {
232
+ fsWatcher = fsWatch(outputPath, () => {
232
233
  // 연속 이벤트 중복 방지 (50ms 이내 재발생 무시)
233
234
  const now = Date.now();
234
235
  if (now - lastMtime < 50) return;
@@ -19,6 +19,11 @@ export interface DevBundlerOptions {
19
19
  onRebuild?: (result: RebuildResult) => void;
20
20
  /** 에러 콜백 */
21
21
  onError?: (error: Error, routeId?: string) => void;
22
+ /**
23
+ * SSR 파일 변경 콜백 (page.tsx, layout.tsx 등)
24
+ * 클라이언트 번들 리빌드 없이 서버 핸들러 재등록이 필요한 경우 호출
25
+ */
26
+ onSSRChange?: (filePath: string) => void;
22
27
  /**
23
28
  * 추가 watch 디렉토리 (공통 컴포넌트 등)
24
29
  * 상대 경로 또는 절대 경로 모두 지원
@@ -75,6 +80,7 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
75
80
  manifest,
76
81
  onRebuild,
77
82
  onError,
83
+ onSSRChange,
78
84
  watchDirs: customWatchDirs = [],
79
85
  disableDefaultWatchDirs = false,
80
86
  } = options;
@@ -94,6 +100,7 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
94
100
 
95
101
  // clientModule 경로에서 routeId 매핑 생성
96
102
  const clientModuleToRoute = new Map<string, string>();
103
+ const serverModuleSet = new Set<string>(); // SSR 모듈 (page.tsx, layout.tsx)
97
104
  const watchDirs = new Set<string>();
98
105
  const commonWatchDirs = new Set<string>(); // 공통 디렉토리 (전체 재빌드 트리거)
99
106
 
@@ -117,6 +124,20 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
117
124
  // 감시할 디렉토리 추가
118
125
  watchDirs.add(dir);
119
126
  }
127
+
128
+ // SSR 모듈 등록 (page.tsx, layout.tsx) — #151
129
+ if (route.componentModule) {
130
+ const absPath = path.resolve(rootDir, route.componentModule).replace(/\\/g, "/");
131
+ serverModuleSet.add(absPath);
132
+ watchDirs.add(path.dirname(path.resolve(rootDir, route.componentModule)));
133
+ }
134
+ if (route.layoutChain) {
135
+ for (const layoutPath of route.layoutChain) {
136
+ const absPath = path.resolve(rootDir, layoutPath).replace(/\\/g, "/");
137
+ serverModuleSet.add(absPath);
138
+ watchDirs.add(path.dirname(path.resolve(rootDir, layoutPath)));
139
+ }
140
+ }
120
141
  }
121
142
 
122
143
  // spec/slots 디렉토리도 추가
@@ -248,7 +269,14 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
248
269
  }
249
270
  }
250
271
 
251
- if (!routeId) return;
272
+ if (!routeId) {
273
+ // SSR 모듈 변경 감지 (page.tsx, layout.tsx) — #151
274
+ if (onSSRChange && serverModuleSet.has(normalizedPath)) {
275
+ console.log(`\n🔄 SSR file changed: ${path.basename(changedFile)}`);
276
+ onSSRChange(normalizedPath);
277
+ }
278
+ return;
279
+ }
252
280
 
253
281
  const route = manifest.routes.find((r) => r.id === routeId);
254
282
  if (!route || !route.clientModule) return;
@@ -13,7 +13,7 @@ import { readJsonFile } from "../utils/bun";
13
13
  function strictWithWarnings<T extends z.ZodRawShape>(
14
14
  schema: z.ZodObject<T>,
15
15
  schemaName: string
16
- ): z.ZodObject<T> {
16
+ ): z.ZodEffects<z.ZodObject<T>> {
17
17
  return schema.superRefine((data, ctx) => {
18
18
  if (typeof data !== "object" || data === null) return;
19
19
 
@@ -6,7 +6,8 @@
6
6
 
7
7
  import { describe, it, expect, mock } from "bun:test";
8
8
  import { z } from "zod";
9
- import { Mandu, createClient, contractFetch } from "./index";
9
+ import { Mandu } from "../index";
10
+ import { createClient, contractFetch } from "./index";
10
11
 
11
12
  // === Test Contract ===
12
13
  const testContract = Mandu.contract({
@@ -126,19 +126,19 @@ export function isContract<T extends ContractDefinition>(
126
126
 
127
127
  /** Contract에서 Input 타입 추출 */
128
128
  export type ContractInput<
129
- C extends Contract<any>,
129
+ C extends ContractMeta<ContractDefinition>,
130
130
  K extends keyof C['__endpoints']
131
131
  > = C['__endpoints'][K]['input'] extends ZodType<infer T> ? T : never;
132
132
 
133
133
  /** Contract에서 Output 타입 추출 */
134
134
  export type ContractOutput<
135
- C extends Contract<any>,
135
+ C extends ContractMeta<ContractDefinition>,
136
136
  K extends keyof C['__endpoints']
137
137
  > = C['__endpoints'][K]['output'] extends ZodType<infer T> ? T : never;
138
138
 
139
139
  /** Contract에서 Params 타입 추출 */
140
140
  export type ContractParams<
141
- C extends Contract<any>,
141
+ C extends ContractMeta<ContractDefinition>,
142
142
  K extends keyof C['__endpoints']
143
143
  > = C['__endpoints'][K]['params'] extends ZodType<infer T> ? T : never;
144
144
 
@@ -204,8 +204,5 @@ export const ManduContract = {
204
204
  fetch: contractFetch,
205
205
  } as const;
206
206
 
207
- /**
208
- * Alias for backward compatibility within contract module
209
- * 외부에서는 메인 index.ts의 Mandu를 사용하세요
210
- */
211
- export const Mandu = ManduContract;
207
+ // Note: The unified `Mandu` namespace is defined in the main index.ts.
208
+ // ManduContract is available for direct import from this module.
@@ -9,7 +9,8 @@
9
9
 
10
10
  import { describe, it, expect } from "bun:test";
11
11
  import { z } from "zod";
12
- import { Mandu, type InferContract, type InferQuery, type InferBody, type InferResponse } from "./index";
12
+ import { Mandu } from "../index";
13
+ import type { InferContract, InferQuery, InferBody, InferResponse } from "./index";
13
14
 
14
15
  // === Test Schemas ===
15
16
  const UserSchema = z.object({
@@ -146,7 +146,7 @@ export function normalizeSchema<T extends ZodTypeAny>(
146
146
  return schema;
147
147
  }
148
148
 
149
- return applyNormalizeMode(schema, opts.mode) as T;
149
+ return applyNormalizeMode(schema, opts.mode) as unknown as T;
150
150
  }
151
151
 
152
152
  /**
@@ -5,7 +5,7 @@
5
5
  * 전역 에러를 캐치하여 DevTools로 전달
6
6
  */
7
7
 
8
- import type { NormalizedError, ErrorType, Severity } from '../../types';
8
+ import type { NormalizedError, ErrorType, DevToolsSeverity } from '../../types';
9
9
  import { getOrCreateHook } from '../../hook';
10
10
  import { createErrorEvent } from '../../protocol';
11
11
 
@@ -69,7 +69,7 @@ function normalizeError(
69
69
  return {
70
70
  id: generateErrorId(),
71
71
  type,
72
- severity: determineSeverity(type, error),
72
+ severity: determineDevToolsSeverity(type, error),
73
73
  message: isError ? error.message : String(error),
74
74
  stack: isError ? error.stack : undefined,
75
75
  timestamp: Date.now(),
@@ -78,7 +78,7 @@ function normalizeError(
78
78
  };
79
79
  }
80
80
 
81
- function determineSeverity(type: ErrorType, _error: unknown): Severity {
81
+ function determineDevToolsSeverity(type: ErrorType, _error: unknown): DevToolsSeverity {
82
82
  switch (type) {
83
83
  case 'runtime':
84
84
  case 'unhandled':
@@ -7,7 +7,7 @@
7
7
 
8
8
  import React, { useEffect, useState, useCallback, useMemo } from 'react';
9
9
  import { createRoot, type Root } from 'react-dom/client';
10
- import type { NormalizedError, DevToolsConfig, IslandSnapshot, NetworkRequest, GuardViolation } from '../../types';
10
+ import type { NormalizedError, DevToolsConfig, IslandSnapshot, NetworkRequest, DevToolsGuardViolation } from '../../types';
11
11
  import { generateCSSVariables, testIds, zIndex } from '../../design-tokens';
12
12
  import { getStateManager, type KitchenState } from '../state-manager';
13
13
  import { getOrCreateHook } from '../../hook';
@@ -241,7 +241,7 @@ function KitchenApp({ config }: KitchenAppProps): React.ReactElement | null {
241
241
  }, []);
242
242
 
243
243
  const handleClearGuard = useCallback(() => {
244
- getStateManager().clearGuardViolations();
244
+ getStateManager().clearDevToolsGuardViolations();
245
245
  }, []);
246
246
 
247
247
  const handleRestart = useCallback(async () => {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import React, { useMemo } from 'react';
7
- import type { GuardViolation } from '../../../types';
7
+ import type { DevToolsGuardViolation } from '../../../types';
8
8
  import { colors, typography, spacing, borderRadius, animation } from '../../../design-tokens';
9
9
 
10
10
  // ============================================================================
@@ -130,7 +130,7 @@ const severityStyles: Record<string, { border: string; bg: string; color: string
130
130
  // ============================================================================
131
131
 
132
132
  export interface GuardPanelProps {
133
- violations: GuardViolation[];
133
+ violations: DevToolsGuardViolation[];
134
134
  onClear: () => void;
135
135
  }
136
136
 
@@ -141,7 +141,7 @@ export interface GuardPanelProps {
141
141
  export function GuardPanel({ violations, onClear }: GuardPanelProps): React.ReactElement {
142
142
  // Group by rule
143
143
  const groupedByRule = useMemo(() => {
144
- const groups = new Map<string, GuardViolation[]>();
144
+ const groups = new Map<string, DevToolsGuardViolation[]>();
145
145
  for (const v of violations) {
146
146
  const existing = groups.get(v.ruleId) ?? [];
147
147
  groups.set(v.ruleId, [...existing, v]);
@@ -10,7 +10,7 @@ import type {
10
10
  NormalizedError,
11
11
  IslandSnapshot,
12
12
  NetworkRequest,
13
- GuardViolation,
13
+ DevToolsGuardViolation,
14
14
  DevToolsConfig,
15
15
  ManduState,
16
16
  } from '../types';
@@ -30,7 +30,7 @@ export interface KitchenState {
30
30
  errors: NormalizedError[];
31
31
  islands: Map<string, IslandSnapshot>;
32
32
  networkRequests: Map<string, NetworkRequest>;
33
- guardViolations: GuardViolation[];
33
+ guardViolations: DevToolsGuardViolation[];
34
34
 
35
35
  // Mandu Character State
36
36
  manduState: ManduState;
@@ -78,7 +78,7 @@ export class KitchenStateManager {
78
78
  private listeners: Set<StateListener> = new Set();
79
79
  private maxErrors = 100;
80
80
  private maxNetworkRequests = 200;
81
- private maxGuardViolations = 50;
81
+ private maxDevToolsGuardViolations = 50;
82
82
 
83
83
  constructor(config?: Partial<DevToolsConfig>) {
84
84
  this.state = createInitialState(config);
@@ -104,7 +104,7 @@ export class KitchenStateManager {
104
104
  return Array.from(this.state.networkRequests.values());
105
105
  }
106
106
 
107
- getGuardViolations(): GuardViolation[] {
107
+ getDevToolsGuardViolations(): DevToolsGuardViolation[] {
108
108
  return [...this.state.guardViolations];
109
109
  }
110
110
 
@@ -304,11 +304,11 @@ export class KitchenStateManager {
304
304
  // Guard Actions
305
305
  // --------------------------------------------------------------------------
306
306
 
307
- addGuardViolation(violation: GuardViolation): void {
307
+ addDevToolsGuardViolation(violation: DevToolsGuardViolation): void {
308
308
  const guardViolations = [violation, ...this.state.guardViolations];
309
309
 
310
310
  // 최대 개수 제한
311
- if (guardViolations.length > this.maxGuardViolations) {
311
+ if (guardViolations.length > this.maxDevToolsGuardViolations) {
312
312
  guardViolations.pop();
313
313
  }
314
314
 
@@ -321,7 +321,7 @@ export class KitchenStateManager {
321
321
  this.setState({ guardViolations, manduState });
322
322
  }
323
323
 
324
- clearGuardViolations(ruleId?: string): void {
324
+ clearDevToolsGuardViolations(ruleId?: string): void {
325
325
  if (ruleId) {
326
326
  const guardViolations = this.state.guardViolations.filter(
327
327
  (v) => v.ruleId !== ruleId
@@ -395,11 +395,11 @@ export class KitchenStateManager {
395
395
  break;
396
396
 
397
397
  case 'guard:violation':
398
- this.addGuardViolation(event.data as GuardViolation);
398
+ this.addDevToolsGuardViolation(event.data as DevToolsGuardViolation);
399
399
  break;
400
400
 
401
401
  case 'guard:clear':
402
- this.clearGuardViolations((event.data as { ruleId?: string }).ruleId);
402
+ this.clearDevToolsGuardViolations((event.data as { ruleId?: string }).ruleId);
403
403
  break;
404
404
 
405
405
  case 'hmr:update':
@@ -16,11 +16,11 @@ export type {
16
16
 
17
17
  // Error
18
18
  ErrorType,
19
- Severity,
19
+ DevToolsSeverity,
20
20
  NormalizedError,
21
21
 
22
22
  // Island
23
- HydrationStrategy,
23
+ DevToolsHydrationStrategy,
24
24
  IslandStatus,
25
25
  IslandSnapshot,
26
26
 
@@ -29,7 +29,7 @@ export type {
29
29
  NetworkBodyPolicy,
30
30
 
31
31
  // Guard
32
- GuardViolation,
32
+ DevToolsGuardViolation,
33
33
 
34
34
  // AI Context
35
35
  CodeContextInfo,
@@ -170,7 +170,7 @@ export {
170
170
  createIslandHydrateEndEvent,
171
171
  createNetworkRequestEvent,
172
172
  createNetworkResponseEvent,
173
- createGuardViolationEvent,
173
+ createDevToolsGuardViolationEvent,
174
174
  createHmrUpdateEvent,
175
175
  createHmrErrorEvent,
176
176
 
@@ -199,12 +199,12 @@ export {
199
199
  // ============================================================================
200
200
 
201
201
  import { getOrCreateHook } from './hook';
202
- import type { NormalizedError, GuardViolation } from './types';
202
+ import type { NormalizedError, DevToolsGuardViolation } from './types';
203
203
  import {
204
204
  createErrorEvent,
205
205
  createHmrUpdateEvent,
206
206
  createHmrErrorEvent,
207
- createGuardViolationEvent,
207
+ createDevToolsGuardViolationEvent,
208
208
  } from './protocol';
209
209
 
210
210
  /**
@@ -261,10 +261,10 @@ export function notifyHmrError(message: string, stack?: string): void {
261
261
  * Guard 위반 리포트 (간편 API)
262
262
  */
263
263
  export function reportGuardViolation(
264
- violation: Omit<GuardViolation, 'id' | 'timestamp'>
264
+ violation: Omit<DevToolsGuardViolation, 'id' | 'timestamp'>
265
265
  ): void {
266
266
  const hook = getOrCreateHook();
267
- hook.emit(createGuardViolationEvent(violation));
267
+ hook.emit(createDevToolsGuardViolationEvent(violation));
268
268
  }
269
269
 
270
270
  // ============================================================================
@@ -8,7 +8,7 @@ import type {
8
8
  NormalizedError,
9
9
  IslandSnapshot,
10
10
  NetworkRequest,
11
- GuardViolation,
11
+ DevToolsGuardViolation,
12
12
  } from './types';
13
13
 
14
14
  // ============================================================================
@@ -33,7 +33,7 @@ export type KitchenEvents =
33
33
  | KitchenEvent<'network:error', { id: string; error: string }>
34
34
 
35
35
  // Guard events
36
- | KitchenEvent<'guard:violation', GuardViolation>
36
+ | KitchenEvent<'guard:violation', DevToolsGuardViolation>
37
37
  | KitchenEvent<'guard:clear', { ruleId?: string }>
38
38
 
39
39
  // HMR events
@@ -144,8 +144,8 @@ export function createNetworkResponseEvent(
144
144
  };
145
145
  }
146
146
 
147
- export function createGuardViolationEvent(
148
- violation: Omit<GuardViolation, 'id' | 'timestamp'>
147
+ export function createDevToolsGuardViolationEvent(
148
+ violation: Omit<DevToolsGuardViolation, 'id' | 'timestamp'>
149
149
  ): KitchenEvents {
150
150
  return {
151
151
  type: 'guard:violation',
@@ -18,12 +18,12 @@ export interface KitchenEvent<T extends string = string, D = unknown> {
18
18
  // ============================================================================
19
19
 
20
20
  export type ErrorType = 'runtime' | 'unhandled' | 'react' | 'network' | 'hmr' | 'guard';
21
- export type Severity = 'critical' | 'error' | 'warning' | 'info';
21
+ export type DevToolsSeverity = 'critical' | 'error' | 'warning' | 'info';
22
22
 
23
23
  export interface NormalizedError {
24
24
  id: string;
25
25
  type: ErrorType;
26
- severity: Severity;
26
+ severity: DevToolsSeverity;
27
27
  message: string;
28
28
  stack?: string;
29
29
  source?: string;
@@ -39,13 +39,13 @@ export interface NormalizedError {
39
39
  // Island Types
40
40
  // ============================================================================
41
41
 
42
- export type HydrationStrategy = 'load' | 'idle' | 'visible' | 'media' | 'never';
42
+ export type DevToolsHydrationStrategy = 'load' | 'idle' | 'visible' | 'media' | 'never';
43
43
  export type IslandStatus = 'ssr' | 'pending' | 'hydrating' | 'hydrated' | 'error';
44
44
 
45
45
  export interface IslandSnapshot {
46
46
  id: string;
47
47
  name: string;
48
- strategy: HydrationStrategy;
48
+ strategy: DevToolsHydrationStrategy;
49
49
  status: IslandStatus;
50
50
  ssrRenderTime?: number;
51
51
  hydrateStartTime?: number;
@@ -89,7 +89,7 @@ export interface NetworkBodyPolicy {
89
89
  // Guard Types
90
90
  // ============================================================================
91
91
 
92
- export interface GuardViolation {
92
+ export interface DevToolsGuardViolation {
93
93
  id: string;
94
94
  ruleId: string;
95
95
  ruleName: string;
@@ -171,7 +171,10 @@ export function createMockDeps(overrides: Partial<FillingDeps> = {}): FillingDep
171
171
  set: asyncNoop,
172
172
  delete: asyncNoop,
173
173
  },
174
- fetch: async () => new Response(),
174
+ fetch: Object.assign(
175
+ async (): Promise<Response> => new Response(),
176
+ { preconnect: (_url: string) => {} }
177
+ ) as typeof fetch,
175
178
  logger: {
176
179
  debug: noop,
177
180
  info: noop,
@@ -361,7 +361,7 @@ export class ManduFilling<TLoaderData = unknown> {
361
361
  if (error instanceof ValidationError) {
362
362
  return ctx.json({ errorType: "LOGIC_ERROR", code: ErrorCode.SLOT_VALIDATION_ERROR, message: "Validation failed", summary: "입력 검증 실패 - 요청 데이터 확인 필요", fix: { file: routeContext ? `spec/slots/${routeContext.routeId}.slot.ts` : "spec/slots/", suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요" }, route: routeContext, errors: error.errors, timestamp: new Date().toISOString() }, 400);
363
363
  }
364
- const classifier = new ErrorClassifier(null, routeContext);
364
+ const classifier = new ErrorClassifier(null, routeContext ? { id: routeContext.routeId, pattern: routeContext.pattern } : undefined);
365
365
  const manduError = classifier.classify(error);
366
366
  console.error(`[Mandu] ${manduError.errorType}:`, manduError.message);
367
367
  const response = formatErrorResponse(manduError, { isDev: process.env.NODE_ENV !== "production" });
@@ -272,7 +272,7 @@ export {
272
272
  type SlotMetadata,
273
273
  type CustomRule,
274
274
  type ConstraintViolation,
275
- type SlotValidationResult,
275
+ type SemanticSlotValidationResult,
276
276
  } from "./semantic-slots";
277
277
 
278
278
  // ═══════════════════════════════════════════════════════════════════════════
@@ -11,6 +11,9 @@ import { hexagonalPreset, HEXAGONAL_HIERARCHY } from "./hexagonal";
11
11
  import { atomicPreset, ATOMIC_HIERARCHY } from "./atomic";
12
12
  import { cqrsPreset, CQRS_HIERARCHY } from "./cqrs";
13
13
 
14
+ // Re-export types
15
+ export type { GuardPreset, PresetDefinition } from "../types";
16
+
14
17
  // Re-export
15
18
  export { fsdPreset, FSD_HIERARCHY } from "./fsd";
16
19
  export { cleanPreset, CLEAN_HIERARCHY } from "./clean";
@@ -144,7 +144,7 @@ export interface ConstraintViolation {
144
144
  /**
145
145
  * 슬롯 검증 결과
146
146
  */
147
- export interface SlotValidationResult {
147
+ export interface SemanticSlotValidationResult {
148
148
  /** 유효 여부 */
149
149
  valid: boolean;
150
150
 
@@ -461,7 +461,7 @@ export async function validateSlotConstraints(
461
461
  filePath: string,
462
462
  constraints: SlotConstraints,
463
463
  rootDir?: string
464
- ): Promise<SlotValidationResult> {
464
+ ): Promise<SemanticSlotValidationResult> {
465
465
  const violations: ConstraintViolation[] = [];
466
466
  const suggestions: string[] = [];
467
467
 
@@ -727,9 +727,9 @@ export async function validateSlots(
727
727
  totalSlots: number;
728
728
  validSlots: number;
729
729
  invalidSlots: number;
730
- results: SlotValidationResult[];
730
+ results: SemanticSlotValidationResult[];
731
731
  }> {
732
- const results: SlotValidationResult[] = [];
732
+ const results: SemanticSlotValidationResult[] = [];
733
733
 
734
734
  for (const filePath of slotFiles) {
735
735
  // 파일에서 메타데이터 추출 시도
package/src/index.ts CHANGED
@@ -24,11 +24,19 @@ export * from "./devtools";
24
24
  export * from "./paths";
25
25
  export * from "./resource";
26
26
 
27
+ // ── Resolve export * ambiguities (TS2308) ──
28
+ // When the same name is exported from multiple submodules via `export *`,
29
+ // TypeScript considers them ambiguous. Explicit re-exports resolve this.
30
+ export { formatViolation } from "./guard";
31
+ export { type HttpMethod } from "./filling";
32
+ export { type GuardViolation } from "./guard";
33
+ export { type Severity } from "./guard";
34
+
27
35
  // Consolidated Mandu namespace
28
36
  import { ManduFilling, ManduContext, ManduFillingFactory, createSSEConnection } from "./filling";
29
37
  import { createContract, defineHandler, defineRoute, createClient, contractFetch, createClientContract, querySchema, bodySchema, apiError } from "./contract";
30
38
  import { defineContract, generateAllFromContract, generateOpenAPISpec } from "./contract/define";
31
- import { island, isIsland, type IslandComponent, type HydrationStrategy } from "./island";
39
+ import { island, isIsland, type IslandComponent, type IslandHydrationStrategy } from "./island";
32
40
  import { intent, isIntent, getIntentDocs, generateOpenAPIFromIntent } from "./intent";
33
41
  import { initializeHook, reportError, ManduDevTools, getStateManager } from "./devtools";
34
42
  import type { ContractDefinition, ContractInstance, ContractSchema } from "./contract";
@@ -20,7 +20,7 @@ import { z } from 'zod';
20
20
  // ============================================================================
21
21
 
22
22
  /** 하이드레이션 타이밍 */
23
- export type HydrationStrategy =
23
+ export type IslandHydrationStrategy =
24
24
  | 'load' // 페이지 로드 즉시
25
25
  | 'idle' // requestIdleCallback
26
26
  | 'visible' // IntersectionObserver
@@ -30,7 +30,7 @@ export type HydrationStrategy =
30
30
  /** Island 옵션 */
31
31
  export interface IslandOptions<P = unknown> {
32
32
  /** 하이드레이션 전략 */
33
- hydrate: HydrationStrategy;
33
+ hydrate: IslandHydrationStrategy;
34
34
  /** 미디어 쿼리 (hydrate: 'media' 일 때) */
35
35
  media?: string;
36
36
  /** SSR 폴백 컴포넌트 */
@@ -44,7 +44,7 @@ export interface IslandOptions<P = unknown> {
44
44
  /** Island 컴포넌트 메타데이터 */
45
45
  export interface IslandMeta {
46
46
  __island: true;
47
- __hydrate: HydrationStrategy;
47
+ __hydrate: IslandHydrationStrategy;
48
48
  __media?: string;
49
49
  __fallback?: ReactNode;
50
50
  __name: string;
@@ -95,7 +95,7 @@ export function getAllIslands(): Map<string, IslandComponent<any>> {
95
95
  * });
96
96
  */
97
97
  export function island<P extends Record<string, unknown>>(
98
- strategy: HydrationStrategy,
98
+ strategy: IslandHydrationStrategy,
99
99
  Component: ComponentType<P>
100
100
  ): IslandComponent<P>;
101
101
 
@@ -105,7 +105,7 @@ export function island<P extends Record<string, unknown>>(
105
105
  ): IslandComponent<P>;
106
106
 
107
107
  export function island<P extends Record<string, unknown>>(
108
- strategyOrOptions: HydrationStrategy | IslandOptions<P>,
108
+ strategyOrOptions: IslandHydrationStrategy | IslandOptions<P>,
109
109
  Component: ComponentType<P>
110
110
  ): IslandComponent<P> {
111
111
  const options: IslandOptions<P> = typeof strategyOrOptions === 'string'
@@ -194,7 +194,7 @@ export function deserializeIslandProps(json: string): Record<string, unknown> {
194
194
  export interface IslandPlaceholderProps {
195
195
  name: string;
196
196
  props: Record<string, unknown>;
197
- hydrate: HydrationStrategy;
197
+ hydrate: IslandHydrationStrategy;
198
198
  media?: string;
199
199
  fallback?: ReactNode;
200
200
  }
@@ -24,7 +24,7 @@ export type {
24
24
  GuardPresetPlugin,
25
25
  GuardRule,
26
26
  GuardRuleContext,
27
- GuardViolation,
27
+ PluginGuardViolation,
28
28
  LayerDefinition,
29
29
  ImportInfo,
30
30
  ExportInfo,
@@ -174,7 +174,7 @@ export interface GuardRule {
174
174
  name: string;
175
175
  description?: string;
176
176
  severity: "error" | "warn" | "off";
177
- check: (context: GuardRuleContext) => GuardViolation[];
177
+ check: (context: GuardRuleContext) => PluginGuardViolation[];
178
178
  }
179
179
 
180
180
  /**
@@ -211,7 +211,7 @@ export interface ExportInfo {
211
211
  /**
212
212
  * Guard 위반
213
213
  */
214
- export interface GuardViolation {
214
+ export interface PluginGuardViolation {
215
215
  ruleId: string;
216
216
  message: string;
217
217
  severity: "error" | "warn";
@@ -21,7 +21,7 @@ import { loadManduConfig } from "../config";
21
21
  /**
22
22
  * 매니페스트 생성 결과
23
23
  */
24
- export interface GenerateResult {
24
+ export interface FSGenerateResult {
25
25
  /** 생성된 매니페스트 */
26
26
  manifest: RoutesManifest;
27
27
 
@@ -176,7 +176,7 @@ async function resolveScannerConfig(
176
176
  export async function generateManifest(
177
177
  rootDir: string,
178
178
  options: GenerateOptions = {}
179
- ): Promise<GenerateResult> {
179
+ ): Promise<FSGenerateResult> {
180
180
  const scannerConfig = await resolveScannerConfig(rootDir, options.scanner);
181
181
 
182
182
  // FS Routes 스캔
@@ -215,7 +215,7 @@ export async function generateManifest(
215
215
  /**
216
216
  * 라우트 변경 콜백
217
217
  */
218
- export type RouteChangeCallback = (result: GenerateResult) => void | Promise<void>;
218
+ export type RouteChangeCallback = (result: FSGenerateResult) => void | Promise<void>;
219
219
 
220
220
  /**
221
221
  * FS Routes 감시자 인터페이스
@@ -225,7 +225,7 @@ export interface FSRoutesWatcher {
225
225
  close(): void;
226
226
 
227
227
  /** 수동 재스캔 */
228
- rescan(): Promise<GenerateResult>;
228
+ rescan(): Promise<FSGenerateResult>;
229
229
  }
230
230
 
231
231
  /**
@@ -281,7 +281,7 @@ export async function watchFSRoutes(
281
281
 
282
282
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
283
283
 
284
- const triggerRescan = async (): Promise<GenerateResult> => {
284
+ const triggerRescan = async (): Promise<FSGenerateResult> => {
285
285
  const result = await generateManifest(rootDir, generateOptions);
286
286
  if (onChange) {
287
287
  await onChange(result);
@@ -6,7 +6,7 @@
6
6
  * @module router/fs-types
7
7
  */
8
8
 
9
- import type { RouteKind, HydrationConfig, HttpMethod } from "../spec/schema";
9
+ import type { RouteKind, HydrationConfig, SpecHttpMethod } from "../spec/schema";
10
10
 
11
11
  // ═══════════════════════════════════════════════════════════════════════════
12
12
  // Segment Types
@@ -93,7 +93,7 @@ export interface FSRouteConfig {
93
93
  kind: RouteKind;
94
94
 
95
95
  /** HTTP 메서드 (API 라우트용) */
96
- methods?: HttpMethod[];
96
+ methods?: SpecHttpMethod[];
97
97
 
98
98
  /** 페이지 컴포넌트 모듈 경로 */
99
99
  componentModule?: string;
@@ -69,7 +69,7 @@ export {
69
69
  export { FSScanner, createFSScanner, scanRoutes } from "./fs-scanner";
70
70
 
71
71
  // Generator
72
- export type { GenerateResult, GenerateOptions, RouteChangeCallback, FSRoutesWatcher } from "./fs-routes";
72
+ export type { FSGenerateResult, GenerateOptions, RouteChangeCallback, FSRoutesWatcher } from "./fs-routes";
73
73
 
74
74
  export {
75
75
  fsRouteToRouteSpec,
@@ -57,7 +57,7 @@ interface ErrorBoundaryState {
57
57
  * </LoadingBoundary>
58
58
  * ```
59
59
  */
60
- export function LoadingBoundary({ fallback, children }: LoadingBoundaryProps): JSX.Element {
60
+ export function LoadingBoundary({ fallback, children }: LoadingBoundaryProps): React.ReactElement {
61
61
  return <Suspense fallback={fallback}>{children}</Suspense>;
62
62
  }
63
63
 
@@ -127,7 +127,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
127
127
  /**
128
128
  * 기본 로딩 컴포넌트
129
129
  */
130
- export function DefaultLoading(): JSX.Element {
130
+ export function DefaultLoading(): React.ReactElement {
131
131
  return (
132
132
  <div
133
133
  style={{
@@ -146,7 +146,7 @@ export function DefaultLoading(): JSX.Element {
146
146
  /**
147
147
  * 기본 에러 컴포넌트
148
148
  */
149
- export function DefaultError({ error, resetError }: ErrorFallbackProps): JSX.Element {
149
+ export function DefaultError({ error, resetError }: ErrorFallbackProps): React.ReactElement {
150
150
  return (
151
151
  <div
152
152
  style={{
@@ -220,7 +220,7 @@ export function PageBoundary({
220
220
  errorComponent,
221
221
  children,
222
222
  onError,
223
- }: PageBoundaryProps): JSX.Element {
223
+ }: PageBoundaryProps): React.ReactElement {
224
224
  const LoadingFallback = loadingComponent ?? <DefaultLoading />;
225
225
  const ErrorFallback = errorComponent ?? DefaultError;
226
226
 
@@ -1,5 +1,5 @@
1
1
  import type { Server } from "bun";
2
- import type { RoutesManifest } from "../spec/schema";
2
+ import type { RoutesManifest, HydrationConfig } from "../spec/schema";
3
3
  import type { BundleManifest } from "../bundler/types";
4
4
  import type { ManduFilling } from "../filling/filling";
5
5
  import { ManduContext } from "../filling/context";
@@ -304,7 +304,7 @@ export interface ServerOptions {
304
304
  }
305
305
 
306
306
  export interface ManduServer {
307
- server: Server;
307
+ server: Server<undefined>;
308
308
  router: Router;
309
309
  /** 이 서버 인스턴스의 레지스트리 */
310
310
  registry: ServerRegistry;
@@ -659,7 +659,7 @@ async function wrapWithLayouts(
659
659
  for (let i = layouts.length - 1; i >= 0; i--) {
660
660
  const Layout = layouts[i];
661
661
  if (Layout) {
662
- wrapped = React.createElement(Layout, { params }, wrapped);
662
+ wrapped = React.createElement(Layout, { params, children: wrapped });
663
663
  }
664
664
  }
665
665
 
@@ -916,14 +916,14 @@ async function loadPageData(
916
916
  if (loader) {
917
917
  try {
918
918
  const module = await loader();
919
- const exported = module.default;
919
+ const exported: unknown = module.default;
920
920
  const component = typeof exported === "function"
921
- ? exported
922
- : exported?.component ?? exported;
923
- registry.registerRouteComponent(route.id, component);
921
+ ? (exported as RouteComponent)
922
+ : (exported as any)?.component ?? exported;
923
+ registry.registerRouteComponent(route.id, component as RouteComponent);
924
924
 
925
925
  // filling이 있으면 loader 실행
926
- const filling = typeof exported === "object" ? exported?.filling : null;
926
+ const filling = typeof exported === "object" && exported !== null ? (exported as any)?.filling : null;
927
927
  if (filling?.hasLoader?.()) {
928
928
  const ctx = new ManduContext(req, params);
929
929
  loaderData = await filling.executeLoader(ctx);
@@ -948,7 +948,7 @@ async function loadPageData(
948
948
  * SSR 렌더링 (Streaming/Non-streaming)
949
949
  */
950
950
  async function renderPageSSR(
951
- route: { id: string; pattern: string; layoutChain?: string[]; streaming?: boolean; hydration?: unknown },
951
+ route: { id: string; pattern: string; layoutChain?: string[]; streaming?: boolean; hydration?: HydrationConfig },
952
952
  params: Record<string, string>,
953
953
  loaderData: unknown,
954
954
  url: string,
@@ -1041,7 +1041,7 @@ async function renderPageSSR(
1041
1041
  async function handlePageRoute(
1042
1042
  req: Request,
1043
1043
  url: URL,
1044
- route: { id: string; pattern: string; layoutChain?: string[]; streaming?: boolean; hydration?: unknown },
1044
+ route: { id: string; pattern: string; layoutChain?: string[]; streaming?: boolean; hydration?: HydrationConfig },
1045
1045
  params: Record<string, string>,
1046
1046
  registry: ServerRegistry
1047
1047
  ): Promise<Result<Response>> {
@@ -1084,7 +1084,7 @@ async function handleRequestInternal(
1084
1084
 
1085
1085
  // 0. CORS Preflight 요청 처리
1086
1086
  if (settings.cors && isPreflightRequest(req)) {
1087
- const corsOptions = settings.cors === true ? {} : settings.cors;
1087
+ const corsOptions: CorsOptions = typeof settings.cors === 'object' ? settings.cors : {};
1088
1088
  return ok(handlePreflightRequest(req, corsOptions));
1089
1089
  }
1090
1090
 
@@ -1092,7 +1092,7 @@ async function handleRequestInternal(
1092
1092
  const staticResponse = await serveStaticFile(pathname, settings);
1093
1093
  if (staticResponse) {
1094
1094
  if (settings.cors && isCorsRequest(req)) {
1095
- const corsOptions = settings.cors === true ? {} : settings.cors;
1095
+ const corsOptions: CorsOptions = typeof settings.cors === 'object' ? settings.cors : {};
1096
1096
  return ok(applyCorsToResponse(staticResponse, req, corsOptions));
1097
1097
  }
1098
1098
  return ok(staticResponse);
@@ -1158,7 +1158,7 @@ function startBunServerWithFallback(options: {
1158
1158
  port: number;
1159
1159
  hostname?: string;
1160
1160
  fetch: (req: Request) => Promise<Response>;
1161
- }): { server: Server; port: number; attempts: number } {
1161
+ }): { server: Server<undefined>; port: number; attempts: number } {
1162
1162
  const { port: startPort, hostname, fetch } = options;
1163
1163
  let lastError: unknown = null;
1164
1164
 
@@ -1168,6 +1168,7 @@ function startBunServerWithFallback(options: {
1168
1168
  port: 0,
1169
1169
  hostname,
1170
1170
  fetch,
1171
+ idleTimeout: 255,
1171
1172
  });
1172
1173
  return { server, port: server.port ?? 0, attempts: 0 };
1173
1174
  }
@@ -1182,6 +1183,7 @@ function startBunServerWithFallback(options: {
1182
1183
  port: candidate,
1183
1184
  hostname,
1184
1185
  fetch,
1186
+ idleTimeout: 255,
1185
1187
  });
1186
1188
  return { server, port: server.port ?? candidate, attempts: attempt };
1187
1189
  } catch (error) {
@@ -184,7 +184,7 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
184
184
  // CSS 링크 태그 생성
185
185
  // - cssPath가 string이면 해당 경로 사용
186
186
  // - cssPath가 false 또는 undefined이면 링크 미삽입 (404 방지)
187
- const cssLinkTag = cssPath && cssPath !== false
187
+ const cssLinkTag = cssPath
188
188
  ? `<link rel="stylesheet" href="${escapeHtmlAttr(`${cssPath}${isDev ? `?t=${Date.now()}` : ""}`)}">`
189
189
  : "";
190
190
 
@@ -378,7 +378,7 @@ function generateHTMLShell(options: StreamingSSROptions): string {
378
378
  // CSS 링크 태그 생성
379
379
  // - cssPath가 string이면 해당 경로 사용
380
380
  // - cssPath가 false 또는 undefined이면 링크 미삽입 (404 방지)
381
- const cssLinkTag = cssPath && cssPath !== false
381
+ const cssLinkTag = cssPath
382
382
  ? `<link rel="stylesheet" href="${escapeHtmlAttr(`${cssPath}${isDev ? `?t=${Date.now()}` : ""}`)}">`
383
383
  : "";
384
384
 
@@ -748,7 +748,7 @@ export async function renderToStream(
748
748
 
749
749
  async function readWithTimeout(): Promise<ReadableStreamReadResult<Uint8Array> | null> {
750
750
  if (!deadline) {
751
- return reader.read();
751
+ return reader.read() as Promise<ReadableStreamReadResult<Uint8Array>>;
752
752
  }
753
753
 
754
754
  const remaining = deadline - Date.now();
@@ -763,8 +763,8 @@ export async function renderToStream(
763
763
 
764
764
  const readPromise = reader
765
765
  .read()
766
- .then((result) => ({ kind: "read" as const, result }))
767
- .catch((error) => ({ kind: "error" as const, error }));
766
+ .then((result) => ({ kind: "read" as const, result: result as ReadableStreamReadResult<Uint8Array> }))
767
+ .catch((error: unknown) => ({ kind: "error" as const, error }));
768
768
 
769
769
  const result = await Promise.race([readPromise, timeoutPromise]);
770
770
 
@@ -119,12 +119,12 @@ export function resolveSEOSync(staticMetadata: Metadata): SEOResult {
119
119
  } else if ('absolute' in staticMetadata.title) {
120
120
  resolvedTitle = {
121
121
  absolute: staticMetadata.title.absolute,
122
- template: staticMetadata.title.template || null,
122
+ template: staticMetadata.title.template ?? null,
123
123
  }
124
124
  } else if ('default' in staticMetadata.title) {
125
125
  resolvedTitle = {
126
126
  absolute: staticMetadata.title.default,
127
- template: staticMetadata.title.template || null,
127
+ template: staticMetadata.title.template ?? null,
128
128
  }
129
129
  }
130
130
  }
@@ -212,7 +212,9 @@ export function renderIcons(metadata: ResolvedMetadata): string {
212
212
 
213
213
  // icon
214
214
  for (const icon of icons.icon) {
215
- const attrs = [`rel="icon"`, `href="${escapeHtml(urlToString(icon.url))}"`]
215
+ const iconUrl = urlToString(icon.url)
216
+ if (!iconUrl) continue
217
+ const attrs = [`rel="icon"`, `href="${escapeHtml(iconUrl)}"`]
216
218
  if (icon.type) attrs.push(`type="${escapeHtml(icon.type)}"`)
217
219
  if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
218
220
  tags.push(`<link ${attrs.join(' ')} />`)
@@ -220,19 +222,25 @@ export function renderIcons(metadata: ResolvedMetadata): string {
220
222
 
221
223
  // apple-touch-icon
222
224
  for (const icon of icons.apple) {
223
- const attrs = [`rel="apple-touch-icon"`, `href="${escapeHtml(urlToString(icon.url))}"`]
225
+ const iconUrl = urlToString(icon.url)
226
+ if (!iconUrl) continue
227
+ const attrs = [`rel="apple-touch-icon"`, `href="${escapeHtml(iconUrl)}"`]
224
228
  if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
225
229
  tags.push(`<link ${attrs.join(' ')} />`)
226
230
  }
227
231
 
228
232
  // shortcut icon
229
233
  for (const icon of icons.shortcut) {
230
- tags.push(`<link rel="shortcut icon" href="${escapeHtml(urlToString(icon.url))}" />`)
234
+ const iconUrl = urlToString(icon.url)
235
+ if (!iconUrl) continue
236
+ tags.push(`<link rel="shortcut icon" href="${escapeHtml(iconUrl)}" />`)
231
237
  }
232
238
 
233
239
  // other icons
234
240
  for (const icon of icons.other) {
235
- const attrs = [`href="${escapeHtml(urlToString(icon.url))}"`]
241
+ const iconUrl = urlToString(icon.url)
242
+ if (!iconUrl) continue
243
+ const attrs = [`href="${escapeHtml(iconUrl)}"`]
236
244
  if (icon.rel) attrs.push(`rel="${escapeHtml(icon.rel)}"`)
237
245
  if (icon.type) attrs.push(`type="${escapeHtml(icon.type)}"`)
238
246
  if (icon.sizes) attrs.push(`sizes="${escapeHtml(icon.sizes)}"`)
@@ -69,9 +69,11 @@ export function renderOpenGraph(metadata: ResolvedMetadata): string {
69
69
  // Images
70
70
  if (openGraph.images) {
71
71
  for (const image of openGraph.images) {
72
- tags.push(og('image', urlToString(image.url)))
72
+ const imageUrl = urlToString(image.url)
73
+ if (imageUrl) tags.push(og('image', imageUrl))
73
74
  if (image.secureUrl) {
74
- tags.push(og('image:secure_url', urlToString(image.secureUrl)))
75
+ const secureUrl = urlToString(image.secureUrl)
76
+ if (secureUrl) tags.push(og('image:secure_url', secureUrl))
75
77
  }
76
78
  if (image.type) {
77
79
  tags.push(og('image:type', image.type))
@@ -91,9 +93,11 @@ export function renderOpenGraph(metadata: ResolvedMetadata): string {
91
93
  // Videos
92
94
  if (openGraph.videos) {
93
95
  for (const video of openGraph.videos) {
94
- tags.push(og('video', urlToString(video.url)))
96
+ const videoUrl = urlToString(video.url)
97
+ if (videoUrl) tags.push(og('video', videoUrl))
95
98
  if (video.secureUrl) {
96
- tags.push(og('video:secure_url', urlToString(video.secureUrl)))
99
+ const secureUrl = urlToString(video.secureUrl)
100
+ if (secureUrl) tags.push(og('video:secure_url', secureUrl))
97
101
  }
98
102
  if (video.type) {
99
103
  tags.push(og('video:type', video.type))
@@ -110,9 +114,11 @@ export function renderOpenGraph(metadata: ResolvedMetadata): string {
110
114
  // Audio
111
115
  if (openGraph.audio) {
112
116
  for (const audio of openGraph.audio) {
113
- tags.push(og('audio', urlToString(audio.url)))
117
+ const audioUrl = urlToString(audio.url)
118
+ if (audioUrl) tags.push(og('audio', audioUrl))
114
119
  if (audio.secureUrl) {
115
- tags.push(og('audio:secure_url', urlToString(audio.secureUrl)))
120
+ const secureUrl = urlToString(audio.secureUrl)
121
+ if (secureUrl) tags.push(og('audio:secure_url', secureUrl))
116
122
  }
117
123
  if (audio.type) {
118
124
  tags.push(og('audio:type', audio.type))
@@ -68,14 +68,15 @@ export function renderTwitter(metadata: ResolvedMetadata): string {
68
68
  if (tw.images) {
69
69
  for (let i = 0; i < tw.images.length; i++) {
70
70
  const image = tw.images[i]
71
+ const imageUrl = urlToString(image.url)
71
72
  if (i === 0) {
72
- tags.push(twitter('image', urlToString(image.url)))
73
+ if (imageUrl) tags.push(twitter('image', imageUrl))
73
74
  if (image.alt) {
74
75
  tags.push(twitter('image:alt', image.alt))
75
76
  }
76
77
  } else {
77
78
  // Multiple images (for galleries)
78
- tags.push(twitter(`image${i}`, urlToString(image.url)))
79
+ if (imageUrl) tags.push(twitter(`image${i}`, imageUrl))
79
80
  if (image.alt) {
80
81
  tags.push(twitter(`image${i}:alt`, image.alt))
81
82
  }
@@ -2,8 +2,8 @@ import { z } from "zod";
2
2
 
3
3
  // ========== Hydration 설정 ==========
4
4
 
5
- export const HydrationStrategy = z.enum(["none", "island", "full", "progressive"]);
6
- export type HydrationStrategy = z.infer<typeof HydrationStrategy>;
5
+ export const SpecHydrationStrategy = z.enum(["none", "island", "full", "progressive"]);
6
+ export type SpecHydrationStrategy = z.infer<typeof SpecHydrationStrategy>;
7
7
 
8
8
  export const HydrationPriority = z.enum(["immediate", "visible", "idle", "interaction"]);
9
9
  export type HydrationPriority = z.infer<typeof HydrationPriority>;
@@ -16,7 +16,7 @@ export const HydrationConfig = z.object({
16
16
  * - full: 전체 페이지 hydrate
17
17
  * - progressive: 점진적 hydrate
18
18
  */
19
- strategy: HydrationStrategy,
19
+ strategy: SpecHydrationStrategy,
20
20
 
21
21
  /**
22
22
  * Hydration 우선순위
@@ -56,8 +56,8 @@ export type LoaderConfig = z.infer<typeof LoaderConfig>;
56
56
  export const RouteKind = z.enum(["page", "api"]);
57
57
  export type RouteKind = z.infer<typeof RouteKind>;
58
58
 
59
- export const HttpMethod = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
60
- export type HttpMethod = z.infer<typeof HttpMethod>;
59
+ export const SpecHttpMethod = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
60
+ export type SpecHttpMethod = z.infer<typeof SpecHttpMethod>;
61
61
 
62
62
  export const RouteSpec = z
63
63
  .object({
@@ -66,7 +66,7 @@ export const RouteSpec = z
66
66
  kind: RouteKind,
67
67
 
68
68
  // HTTP 메서드 (API용)
69
- methods: z.array(HttpMethod).optional(),
69
+ methods: z.array(SpecHttpMethod).optional(),
70
70
 
71
71
  // 서버 모듈 (generated route handler)
72
72
  module: z.string().min(1, "module 경로는 필수입니다"),
@@ -25,7 +25,7 @@ export interface HashOptions {
25
25
  exclude?: string[];
26
26
  }
27
27
 
28
- export interface NormalizeOptions {
28
+ export interface HashNormalizeOptions {
29
29
  /** 제외할 키 패턴 */
30
30
  exclude?: string[];
31
31
  /** Date를 ISO 문자열로 변환 (기본값: true) */
@@ -59,7 +59,7 @@ export interface NormalizeOptions {
59
59
  */
60
60
  export function normalizeForHash(
61
61
  value: unknown,
62
- options: NormalizeOptions = {},
62
+ options: HashNormalizeOptions = {},
63
63
  seen: WeakSet<object> = new WeakSet()
64
64
  ): unknown {
65
65
  const {
@@ -14,7 +14,7 @@ export {
14
14
  isHashable,
15
15
  getHashInfo,
16
16
  type HashOptions,
17
- type NormalizeOptions,
17
+ type HashNormalizeOptions,
18
18
  } from "./hasher.js";
19
19
 
20
20
  // Diff
@@ -157,8 +157,8 @@ export class FileWatcher {
157
157
  this.processFileEvent("delete", filePath);
158
158
  });
159
159
 
160
- this.chokidarWatcher.on("error", (error) => {
161
- console.error(`[Watch] Error:`, error.message);
160
+ this.chokidarWatcher.on("error", (error: unknown) => {
161
+ console.error(`[Watch] Error:`, error instanceof Error ? error.message : String(error));
162
162
  });
163
163
 
164
164
  this._active = true;