@mandujs/core 0.18.20 → 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.
- package/package.json +1 -1
- package/src/brain/architecture/analyzer.ts +3 -5
- package/src/brain/architecture/types.ts +4 -4
- package/src/brain/doctor/index.ts +1 -1
- package/src/brain/doctor/reporter.ts +2 -2
- package/src/bundler/css.ts +3 -2
- package/src/config/validate.ts +1 -1
- package/src/contract/client.test.ts +2 -1
- package/src/contract/define.ts +3 -3
- package/src/contract/index.ts +2 -5
- package/src/contract/infer.test.ts +2 -1
- package/src/contract/normalize.ts +1 -1
- package/src/devtools/client/catchers/error-catcher.ts +3 -3
- package/src/devtools/client/components/kitchen-root.tsx +2 -2
- package/src/devtools/client/components/panel/guard-panel.tsx +3 -3
- package/src/devtools/client/state-manager.ts +9 -9
- package/src/devtools/index.ts +8 -8
- package/src/devtools/protocol.ts +4 -4
- package/src/devtools/types.ts +5 -5
- package/src/filling/deps.ts +4 -1
- package/src/filling/filling.ts +1 -1
- package/src/guard/index.ts +1 -1
- package/src/guard/presets/index.ts +3 -0
- package/src/guard/semantic-slots.ts +4 -4
- package/src/index.ts +9 -1
- package/src/island/index.ts +6 -6
- package/src/plugins/index.ts +1 -1
- package/src/plugins/types.ts +2 -2
- package/src/router/fs-routes.ts +5 -5
- package/src/router/fs-types.ts +2 -2
- package/src/router/index.ts +1 -1
- package/src/runtime/boundary.tsx +4 -4
- package/src/runtime/server.ts +13 -13
- package/src/runtime/ssr.ts +1 -1
- package/src/runtime/streaming-ssr.ts +4 -4
- package/src/seo/integration/ssr.ts +2 -2
- package/src/seo/render/basic.ts +12 -4
- package/src/seo/render/opengraph.ts +12 -6
- package/src/seo/render/twitter.ts +3 -2
- package/src/spec/schema.ts +6 -6
- package/src/utils/hasher.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/watcher/watcher.ts +2 -2
package/package.json
CHANGED
|
@@ -510,15 +510,13 @@ ${JSON.stringify(this.config.folders, null, 2)}
|
|
|
510
510
|
짧고 명확하게 답변하세요 (3줄 이내).`;
|
|
511
511
|
|
|
512
512
|
try {
|
|
513
|
-
const result = await brain.
|
|
514
|
-
{ role: "user", content: prompt },
|
|
515
|
-
]);
|
|
513
|
+
const result = await brain.generate(prompt);
|
|
516
514
|
|
|
517
515
|
// 응답에서 경로 추출 시도
|
|
518
|
-
const pathMatch = result.
|
|
516
|
+
const pathMatch = result.match(/(?:spec\/|\.mandu\/|app\/|src\/|packages\/)[^\s,)]+/);
|
|
519
517
|
|
|
520
518
|
return {
|
|
521
|
-
suggestion: result
|
|
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
|
|
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?:
|
|
76
|
+
layers?: ArchLayerRule[];
|
|
77
77
|
/** 네이밍 규칙 */
|
|
78
78
|
naming?: NamingRule[];
|
|
79
79
|
/** 커스텀 규칙 */
|
|
80
|
-
custom?:
|
|
80
|
+
custom?: ArchCustomRule[];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* 커스텀 규칙
|
|
85
85
|
*/
|
|
86
|
-
export interface
|
|
86
|
+
export interface ArchCustomRule {
|
|
87
87
|
/** 규칙 ID */
|
|
88
88
|
id: string;
|
|
89
89
|
/** 규칙 설명 */
|
|
@@ -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
|
|
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(
|
|
140
|
+
console.log(formatDoctorViolation(violation));
|
|
141
141
|
console.log();
|
|
142
142
|
}
|
|
143
143
|
}
|
package/src/bundler/css.ts
CHANGED
|
@@ -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:
|
|
227
|
+
let fsWatcher: FSWatcher | null = null;
|
|
227
228
|
let lastMtime = 0;
|
|
228
229
|
|
|
229
230
|
const startFileWatcher = () => {
|
|
230
231
|
try {
|
|
231
|
-
fsWatcher =
|
|
232
|
+
fsWatcher = fsWatch(outputPath, () => {
|
|
232
233
|
// 연속 이벤트 중복 방지 (50ms 이내 재발생 무시)
|
|
233
234
|
const now = Date.now();
|
|
234
235
|
if (now - lastMtime < 50) return;
|
package/src/config/validate.ts
CHANGED
|
@@ -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
|
|
9
|
+
import { Mandu } from "../index";
|
|
10
|
+
import { createClient, contractFetch } from "./index";
|
|
10
11
|
|
|
11
12
|
// === Test Contract ===
|
|
12
13
|
const testContract = Mandu.contract({
|
package/src/contract/define.ts
CHANGED
|
@@ -126,19 +126,19 @@ export function isContract<T extends ContractDefinition>(
|
|
|
126
126
|
|
|
127
127
|
/** Contract에서 Input 타입 추출 */
|
|
128
128
|
export type ContractInput<
|
|
129
|
-
C extends
|
|
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
|
|
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
|
|
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
|
|
package/src/contract/index.ts
CHANGED
|
@@ -204,8 +204,5 @@ export const ManduContract = {
|
|
|
204
204
|
fetch: contractFetch,
|
|
205
205
|
} as const;
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 전역 에러를 캐치하여 DevTools로 전달
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { NormalizedError, ErrorType,
|
|
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:
|
|
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
|
|
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,
|
|
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().
|
|
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 {
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
addDevToolsGuardViolation(violation: DevToolsGuardViolation): void {
|
|
308
308
|
const guardViolations = [violation, ...this.state.guardViolations];
|
|
309
309
|
|
|
310
310
|
// 최대 개수 제한
|
|
311
|
-
if (guardViolations.length > this.
|
|
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
|
-
|
|
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.
|
|
398
|
+
this.addDevToolsGuardViolation(event.data as DevToolsGuardViolation);
|
|
399
399
|
break;
|
|
400
400
|
|
|
401
401
|
case 'guard:clear':
|
|
402
|
-
this.
|
|
402
|
+
this.clearDevToolsGuardViolations((event.data as { ruleId?: string }).ruleId);
|
|
403
403
|
break;
|
|
404
404
|
|
|
405
405
|
case 'hmr:update':
|
package/src/devtools/index.ts
CHANGED
|
@@ -16,11 +16,11 @@ export type {
|
|
|
16
16
|
|
|
17
17
|
// Error
|
|
18
18
|
ErrorType,
|
|
19
|
-
|
|
19
|
+
DevToolsSeverity,
|
|
20
20
|
NormalizedError,
|
|
21
21
|
|
|
22
22
|
// Island
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
202
|
+
import type { NormalizedError, DevToolsGuardViolation } from './types';
|
|
203
203
|
import {
|
|
204
204
|
createErrorEvent,
|
|
205
205
|
createHmrUpdateEvent,
|
|
206
206
|
createHmrErrorEvent,
|
|
207
|
-
|
|
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<
|
|
264
|
+
violation: Omit<DevToolsGuardViolation, 'id' | 'timestamp'>
|
|
265
265
|
): void {
|
|
266
266
|
const hook = getOrCreateHook();
|
|
267
|
-
hook.emit(
|
|
267
|
+
hook.emit(createDevToolsGuardViolationEvent(violation));
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
// ============================================================================
|
package/src/devtools/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
NormalizedError,
|
|
9
9
|
IslandSnapshot,
|
|
10
10
|
NetworkRequest,
|
|
11
|
-
|
|
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',
|
|
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
|
|
148
|
-
violation: Omit<
|
|
147
|
+
export function createDevToolsGuardViolationEvent(
|
|
148
|
+
violation: Omit<DevToolsGuardViolation, 'id' | 'timestamp'>
|
|
149
149
|
): KitchenEvents {
|
|
150
150
|
return {
|
|
151
151
|
type: 'guard:violation',
|
package/src/devtools/types.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
92
|
+
export interface DevToolsGuardViolation {
|
|
93
93
|
id: string;
|
|
94
94
|
ruleId: string;
|
|
95
95
|
ruleName: string;
|
package/src/filling/deps.ts
CHANGED
|
@@ -171,7 +171,10 @@ export function createMockDeps(overrides: Partial<FillingDeps> = {}): FillingDep
|
|
|
171
171
|
set: asyncNoop,
|
|
172
172
|
delete: asyncNoop,
|
|
173
173
|
},
|
|
174
|
-
fetch:
|
|
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,
|
package/src/filling/filling.ts
CHANGED
|
@@ -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" });
|
package/src/guard/index.ts
CHANGED
|
@@ -272,7 +272,7 @@ export {
|
|
|
272
272
|
type SlotMetadata,
|
|
273
273
|
type CustomRule,
|
|
274
274
|
type ConstraintViolation,
|
|
275
|
-
type
|
|
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
|
|
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<
|
|
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:
|
|
730
|
+
results: SemanticSlotValidationResult[];
|
|
731
731
|
}> {
|
|
732
|
-
const results:
|
|
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
|
|
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";
|
package/src/island/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { z } from 'zod';
|
|
|
20
20
|
// ============================================================================
|
|
21
21
|
|
|
22
22
|
/** 하이드레이션 타이밍 */
|
|
23
|
-
export type
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
197
|
+
hydrate: IslandHydrationStrategy;
|
|
198
198
|
media?: string;
|
|
199
199
|
fallback?: ReactNode;
|
|
200
200
|
}
|
package/src/plugins/index.ts
CHANGED
package/src/plugins/types.ts
CHANGED
|
@@ -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) =>
|
|
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
|
|
214
|
+
export interface PluginGuardViolation {
|
|
215
215
|
ruleId: string;
|
|
216
216
|
message: string;
|
|
217
217
|
severity: "error" | "warn";
|
package/src/router/fs-routes.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { loadManduConfig } from "../config";
|
|
|
21
21
|
/**
|
|
22
22
|
* 매니페스트 생성 결과
|
|
23
23
|
*/
|
|
24
|
-
export interface
|
|
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<
|
|
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:
|
|
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<
|
|
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<
|
|
284
|
+
const triggerRescan = async (): Promise<FSGenerateResult> => {
|
|
285
285
|
const result = await generateManifest(rootDir, generateOptions);
|
|
286
286
|
if (onChange) {
|
|
287
287
|
await onChange(result);
|
package/src/router/fs-types.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module router/fs-types
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { RouteKind, HydrationConfig,
|
|
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?:
|
|
96
|
+
methods?: SpecHttpMethod[];
|
|
97
97
|
|
|
98
98
|
/** 페이지 컴포넌트 모듈 경로 */
|
|
99
99
|
componentModule?: string;
|
package/src/router/index.ts
CHANGED
|
@@ -69,7 +69,7 @@ export {
|
|
|
69
69
|
export { FSScanner, createFSScanner, scanRoutes } from "./fs-scanner";
|
|
70
70
|
|
|
71
71
|
// Generator
|
|
72
|
-
export type {
|
|
72
|
+
export type { FSGenerateResult, GenerateOptions, RouteChangeCallback, FSRoutesWatcher } from "./fs-routes";
|
|
73
73
|
|
|
74
74
|
export {
|
|
75
75
|
fsRouteToRouteSpec,
|
package/src/runtime/boundary.tsx
CHANGED
|
@@ -57,7 +57,7 @@ interface ErrorBoundaryState {
|
|
|
57
57
|
* </LoadingBoundary>
|
|
58
58
|
* ```
|
|
59
59
|
*/
|
|
60
|
-
export function LoadingBoundary({ fallback, children }: LoadingBoundaryProps):
|
|
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():
|
|
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):
|
|
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):
|
|
223
|
+
}: PageBoundaryProps): React.ReactElement {
|
|
224
224
|
const LoadingFallback = loadingComponent ?? <DefaultLoading />;
|
|
225
225
|
const ErrorFallback = errorComponent ?? DefaultError;
|
|
226
226
|
|
package/src/runtime/server.ts
CHANGED
|
@@ -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
|
|
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?:
|
|
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?:
|
|
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 ===
|
|
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 ===
|
|
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
|
|
1161
|
+
}): { server: Server<undefined>; port: number; attempts: number } {
|
|
1162
1162
|
const { port: startPort, hostname, fetch } = options;
|
|
1163
1163
|
let lastError: unknown = null;
|
|
1164
1164
|
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
127
|
+
template: staticMetadata.title.template ?? null,
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
}
|
package/src/seo/render/basic.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
72
|
+
const imageUrl = urlToString(image.url)
|
|
73
|
+
if (imageUrl) tags.push(og('image', imageUrl))
|
|
73
74
|
if (image.secureUrl) {
|
|
74
|
-
|
|
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
|
-
|
|
96
|
+
const videoUrl = urlToString(video.url)
|
|
97
|
+
if (videoUrl) tags.push(og('video', videoUrl))
|
|
95
98
|
if (video.secureUrl) {
|
|
96
|
-
|
|
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
|
-
|
|
117
|
+
const audioUrl = urlToString(audio.url)
|
|
118
|
+
if (audioUrl) tags.push(og('audio', audioUrl))
|
|
114
119
|
if (audio.secureUrl) {
|
|
115
|
-
|
|
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',
|
|
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}`,
|
|
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
|
}
|
package/src/spec/schema.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
// ========== Hydration 설정 ==========
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
export type
|
|
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:
|
|
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
|
|
60
|
-
export type
|
|
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(
|
|
69
|
+
methods: z.array(SpecHttpMethod).optional(),
|
|
70
70
|
|
|
71
71
|
// 서버 모듈 (generated route handler)
|
|
72
72
|
module: z.string().min(1, "module 경로는 필수입니다"),
|
package/src/utils/hasher.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface HashOptions {
|
|
|
25
25
|
exclude?: string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface
|
|
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:
|
|
62
|
+
options: HashNormalizeOptions = {},
|
|
63
63
|
seen: WeakSet<object> = new WeakSet()
|
|
64
64
|
): unknown {
|
|
65
65
|
const {
|
package/src/utils/index.ts
CHANGED
package/src/watcher/watcher.ts
CHANGED
|
@@ -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;
|