@mandujs/core 0.18.20 → 0.18.22
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 +3 -1
- package/src/brain/architecture/analyzer.ts +3 -5
- package/src/brain/architecture/types.ts +4 -4
- package/src/brain/doctor/analyzer.ts +1 -0
- package/src/brain/doctor/index.ts +1 -1
- package/src/brain/doctor/patcher.ts +10 -6
- package/src/brain/doctor/reporter.ts +4 -4
- package/src/brain/types.ts +14 -10
- package/src/bundler/build.ts +17 -17
- package/src/bundler/css.ts +3 -2
- package/src/bundler/dev.ts +1 -1
- package/src/client/island.ts +10 -9
- package/src/client/router.ts +1 -1
- package/src/config/mcp-ref.ts +6 -6
- package/src/config/metadata.test.ts +1 -1
- package/src/config/metadata.ts +36 -16
- package/src/config/symbols.ts +1 -1
- package/src/config/validate.ts +17 -1
- package/src/content/content.test.ts +3 -3
- package/src/content/loaders/file.ts +3 -0
- package/src/content/loaders/glob.ts +1 -0
- package/src/contract/client-safe.test.ts +1 -1
- package/src/contract/client.test.ts +2 -1
- package/src/contract/client.ts +18 -18
- package/src/contract/define.ts +32 -17
- package/src/contract/handler.ts +11 -11
- package/src/contract/index.ts +2 -5
- package/src/contract/infer.test.ts +2 -1
- package/src/contract/normalize.test.ts +1 -1
- package/src/contract/normalize.ts +17 -11
- package/src/contract/registry.test.ts +1 -1
- package/src/contract/zod-utils.ts +155 -0
- package/src/devtools/client/catchers/error-catcher.ts +3 -3
- package/src/devtools/client/catchers/network-proxy.ts +5 -1
- 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/init.ts +2 -2
- package/src/devtools/protocol.ts +4 -4
- package/src/devtools/server/source-context.ts +9 -3
- package/src/devtools/types.ts +5 -5
- package/src/devtools/worker/redaction-worker.ts +12 -5
- package/src/error/index.ts +1 -1
- package/src/error/result.ts +14 -0
- package/src/filling/deps.ts +5 -2
- package/src/filling/filling.ts +1 -1
- package/src/generator/templates.ts +2 -2
- package/src/guard/contract-guard.test.ts +1 -0
- package/src/guard/file-type.test.ts +1 -1
- package/src/guard/index.ts +1 -1
- package/src/guard/negotiation.ts +29 -1
- package/src/guard/presets/index.ts +3 -0
- package/src/guard/semantic-slots.ts +4 -4
- package/src/index.ts +10 -1
- package/src/intent/index.ts +28 -17
- package/src/island/index.ts +8 -8
- package/src/openapi/generator.ts +49 -31
- package/src/plugins/index.ts +1 -1
- package/src/plugins/registry.ts +28 -18
- package/src/plugins/types.ts +2 -2
- package/src/resource/__tests__/backward-compat.test.ts +2 -2
- package/src/resource/__tests__/edge-cases.test.ts +14 -13
- package/src/resource/__tests__/fixtures.ts +2 -2
- package/src/resource/__tests__/generator.test.ts +1 -1
- package/src/resource/__tests__/performance.test.ts +8 -6
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-routes.ts +34 -40
- 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/logger.test.ts +3 -3
- package/src/runtime/logger.ts +1 -1
- package/src/runtime/server.ts +18 -16
- package/src/runtime/ssr.ts +1 -1
- package/src/runtime/stable-selector.ts +1 -2
- package/src/runtime/streaming-ssr.ts +15 -6
- package/src/seo/index.ts +5 -0
- package/src/seo/integration/ssr.ts +4 -4
- 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/seo/resolve/url.ts +7 -0
- package/src/seo/types.ts +13 -0
- package/src/spec/schema.ts +89 -61
- package/src/types/branded.ts +56 -0
- package/src/types/index.ts +1 -0
- package/src/utils/hasher.test.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/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/init.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface KitchenInstance {
|
|
|
30
30
|
/** DevTools 언마운트 및 정리 */
|
|
31
31
|
destroy: () => void;
|
|
32
32
|
/** 상태 관리자 접근 */
|
|
33
|
-
getState: () =>
|
|
33
|
+
getState: () => Record<string, unknown>;
|
|
34
34
|
/** 에러 리포트 */
|
|
35
35
|
reportError: (error: Error | string) => void;
|
|
36
36
|
/** DevTools 열기 */
|
|
@@ -213,7 +213,7 @@ function createInstance(): KitchenInstance {
|
|
|
213
213
|
function createNoopInstance(): KitchenInstance {
|
|
214
214
|
return {
|
|
215
215
|
destroy: () => {},
|
|
216
|
-
getState: () => ({}
|
|
216
|
+
getState: () => ({}),
|
|
217
217
|
reportError: () => {},
|
|
218
218
|
open: () => {},
|
|
219
219
|
close: () => {},
|
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',
|
|
@@ -235,8 +235,14 @@ export interface SourcemapParseResult {
|
|
|
235
235
|
/**
|
|
236
236
|
* 간단한 Sourcemap 파서 (Base64 VLQ 디코딩)
|
|
237
237
|
*/
|
|
238
|
+
interface RawSourcemap {
|
|
239
|
+
mappings: string;
|
|
240
|
+
sources: string[];
|
|
241
|
+
names?: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
238
244
|
export class SourcemapParser {
|
|
239
|
-
private sourcemap:
|
|
245
|
+
private sourcemap: RawSourcemap;
|
|
240
246
|
|
|
241
247
|
constructor(sourcemapContent: string) {
|
|
242
248
|
try {
|
|
@@ -405,7 +411,7 @@ export function createViteMiddleware(projectRoot: string) {
|
|
|
405
411
|
const provider = new SourceContextProvider({ projectRoot });
|
|
406
412
|
const handler = provider.createHandler();
|
|
407
413
|
|
|
408
|
-
return async (req:
|
|
414
|
+
return async (req: { url?: string }, res: { setHeader: (key: string, value: string) => void; statusCode: number; end: (data: string) => void }, next: () => void) => {
|
|
409
415
|
// __mandu_source__ 엔드포인트만 처리
|
|
410
416
|
if (!req.url?.startsWith('/api/__mandu_source__')) {
|
|
411
417
|
return next();
|
|
@@ -435,7 +441,7 @@ export function manduSourceContextPlugin(options?: Partial<SourceContextProvider
|
|
|
435
441
|
return {
|
|
436
442
|
name: 'mandu-source-context',
|
|
437
443
|
|
|
438
|
-
configureServer(server:
|
|
444
|
+
configureServer(server: { config: { root?: string }; middlewares: { use: (middleware: ReturnType<typeof createViteMiddleware>) => void } }) {
|
|
439
445
|
const projectRoot = options?.projectRoot ?? server.config.root ?? process.cwd();
|
|
440
446
|
|
|
441
447
|
server.middlewares.use(createViteMiddleware(projectRoot));
|
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;
|
|
@@ -185,12 +185,14 @@ function handleMessage(request: WorkerRequest): WorkerResponse {
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
default:
|
|
188
|
+
default: {
|
|
189
|
+
const unknownType: string = request.type;
|
|
189
190
|
return {
|
|
190
191
|
id: request.id,
|
|
191
192
|
success: false,
|
|
192
|
-
error: `Unknown request type: ${
|
|
193
|
+
error: `Unknown request type: ${unknownType}`,
|
|
193
194
|
};
|
|
195
|
+
}
|
|
194
196
|
}
|
|
195
197
|
} catch (error) {
|
|
196
198
|
return {
|
|
@@ -202,10 +204,15 @@ function handleMessage(request: WorkerRequest): WorkerResponse {
|
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
// Worker 컨텍스트에서만 실행
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
interface WorkerSelf {
|
|
208
|
+
postMessage: (message: WorkerResponse) => void;
|
|
209
|
+
onmessage: ((event: MessageEvent<WorkerRequest>) => void) | null;
|
|
210
|
+
}
|
|
211
|
+
const workerSelf = typeof self !== 'undefined' ? (self as unknown as WorkerSelf) : undefined;
|
|
212
|
+
if (workerSelf && typeof workerSelf.postMessage === 'function') {
|
|
213
|
+
workerSelf.onmessage = (event: MessageEvent<WorkerRequest>) => {
|
|
207
214
|
const response = handleMessage(event.data);
|
|
208
|
-
|
|
215
|
+
workerSelf.postMessage(response);
|
|
209
216
|
};
|
|
210
217
|
}
|
|
211
218
|
|
package/src/error/index.ts
CHANGED
package/src/error/result.ts
CHANGED
|
@@ -12,6 +12,20 @@ export type Result<T> =
|
|
|
12
12
|
export const ok = <T>(value: T): Result<T> => ({ ok: true, value });
|
|
13
13
|
export const err = (error: ManduError): Result<never> => ({ ok: false, error });
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Result가 성공인지 검사하는 type guard
|
|
17
|
+
*/
|
|
18
|
+
export function isOk<T>(result: Result<T>): result is { ok: true; value: T } {
|
|
19
|
+
return result.ok === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Result가 실패인지 검사하는 type guard
|
|
24
|
+
*/
|
|
25
|
+
export function isErr<T>(result: Result<T>): result is { ok: false; error: ManduError } {
|
|
26
|
+
return result.ok === false;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
/**
|
|
16
30
|
* ManduError -> HTTP status 매핑
|
|
17
31
|
*/
|
package/src/filling/deps.ts
CHANGED
|
@@ -163,7 +163,7 @@ export function createMockDeps(overrides: Partial<FillingDeps> = {}): FillingDep
|
|
|
163
163
|
|
|
164
164
|
return {
|
|
165
165
|
db: {
|
|
166
|
-
query: async () => [] as
|
|
166
|
+
query: async () => [] as never,
|
|
167
167
|
transaction: async (fn) => fn(),
|
|
168
168
|
},
|
|
169
169
|
cache: {
|
|
@@ -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" });
|
|
@@ -304,7 +304,7 @@ interface Props {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
|
|
307
|
-
const serverData = (loaderData || {}) as
|
|
307
|
+
const serverData = (loaderData || {}) as Record<string, unknown>;
|
|
308
308
|
const setupResult = islandModule.definition.setup(serverData);
|
|
309
309
|
return islandModule.definition.render(setupResult) as React.ReactElement;
|
|
310
310
|
}
|
|
@@ -333,7 +333,7 @@ interface Props {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
export default function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
|
|
336
|
-
const serverData = (loaderData || {}) as
|
|
336
|
+
const serverData = (loaderData || {}) as Record<string, unknown>;
|
|
337
337
|
const setupResult = islandModule.definition.setup(serverData);
|
|
338
338
|
return islandModule.definition.render(setupResult) as React.ReactElement;
|
|
339
339
|
}
|
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
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/guard/negotiation.ts
CHANGED
|
@@ -988,16 +988,44 @@ export interface ${toPascalCase(name)}Dto {
|
|
|
988
988
|
export interface ${toPascalCase(name)}ResponseDto {
|
|
989
989
|
// TODO: Define response DTO fields
|
|
990
990
|
}
|
|
991
|
+
`;
|
|
992
|
+
|
|
993
|
+
case "controller":
|
|
994
|
+
return `/**
|
|
995
|
+
* ${purpose}
|
|
996
|
+
*
|
|
997
|
+
* Controller - 요청/응답 처리
|
|
998
|
+
*/
|
|
999
|
+
|
|
1000
|
+
export class ${toPascalCase(name)}Controller {
|
|
1001
|
+
// TODO: Implement controller methods
|
|
1002
|
+
}
|
|
1003
|
+
`;
|
|
1004
|
+
|
|
1005
|
+
case "hook":
|
|
1006
|
+
return `/**
|
|
1007
|
+
* ${purpose}
|
|
1008
|
+
*
|
|
1009
|
+
* Custom Hook
|
|
1010
|
+
*/
|
|
1011
|
+
|
|
1012
|
+
export function use${toPascalCase(name)}() {
|
|
1013
|
+
// TODO: Implement hook logic
|
|
1014
|
+
}
|
|
991
1015
|
`;
|
|
992
1016
|
|
|
993
1017
|
case "util":
|
|
994
|
-
default:
|
|
995
1018
|
return `/**
|
|
996
1019
|
* ${purpose}
|
|
997
1020
|
*/
|
|
998
1021
|
|
|
999
1022
|
// TODO: Implement utility functions
|
|
1000
1023
|
`;
|
|
1024
|
+
|
|
1025
|
+
default: {
|
|
1026
|
+
const _exhaustive: never = template;
|
|
1027
|
+
throw new Error(`Unhandled file template: ${_exhaustive}`);
|
|
1028
|
+
}
|
|
1001
1029
|
}
|
|
1002
1030
|
}
|
|
1003
1031
|
|
|
@@ -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
|
@@ -23,12 +23,21 @@ export * from "./intent";
|
|
|
23
23
|
export * from "./devtools";
|
|
24
24
|
export * from "./paths";
|
|
25
25
|
export * from "./resource";
|
|
26
|
+
export * from "./types";
|
|
27
|
+
|
|
28
|
+
// ── Resolve export * ambiguities (TS2308) ──
|
|
29
|
+
// When the same name is exported from multiple submodules via `export *`,
|
|
30
|
+
// TypeScript considers them ambiguous. Explicit re-exports resolve this.
|
|
31
|
+
export { formatViolation } from "./guard";
|
|
32
|
+
export { type HttpMethod } from "./filling";
|
|
33
|
+
export { type GuardViolation } from "./guard";
|
|
34
|
+
export { type Severity } from "./guard";
|
|
26
35
|
|
|
27
36
|
// Consolidated Mandu namespace
|
|
28
37
|
import { ManduFilling, ManduContext, ManduFillingFactory, createSSEConnection } from "./filling";
|
|
29
38
|
import { createContract, defineHandler, defineRoute, createClient, contractFetch, createClientContract, querySchema, bodySchema, apiError } from "./contract";
|
|
30
39
|
import { defineContract, generateAllFromContract, generateOpenAPISpec } from "./contract/define";
|
|
31
|
-
import { island, isIsland, type IslandComponent, type
|
|
40
|
+
import { island, isIsland, type IslandComponent, type IslandHydrationStrategy } from "./island";
|
|
32
41
|
import { intent, isIntent, getIntentDocs, generateOpenAPIFromIntent } from "./intent";
|
|
33
42
|
import { initializeHook, reportError, ManduDevTools, getStateManager } from "./devtools";
|
|
34
43
|
import type { ContractDefinition, ContractInstance, ContractSchema } from "./contract";
|
package/src/intent/index.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
import { z, type ZodType } from 'zod';
|
|
26
26
|
import { ManduFillingFactory, type ManduFilling } from '../filling/filling';
|
|
27
27
|
import type { ManduContext } from '../filling/context';
|
|
28
|
+
import { getZodTypeName, getZodObjectShape, getZodArrayElementType } from '../contract/zod-utils';
|
|
29
|
+
import type { ZodTypeAny } from 'zod';
|
|
28
30
|
|
|
29
31
|
// ============================================================================
|
|
30
32
|
// Types
|
|
@@ -51,7 +53,7 @@ export interface IntentDefinition<TInput = unknown, TOutput = unknown> {
|
|
|
51
53
|
guard?: (ctx: ManduContext) => Response | void | Promise<Response | void>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
export type IntentMap = Record<string, IntentDefinition<
|
|
56
|
+
export type IntentMap = Record<string, IntentDefinition<unknown, unknown>>;
|
|
55
57
|
|
|
56
58
|
export interface IntentMeta {
|
|
57
59
|
__intent: true;
|
|
@@ -86,7 +88,7 @@ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
|
|
|
86
88
|
const docs: IntentDocumentation[] = [];
|
|
87
89
|
|
|
88
90
|
// 메서드별로 핸들러 그룹화
|
|
89
|
-
const methodHandlers: Record<HttpMethod, IntentDefinition<
|
|
91
|
+
const methodHandlers: Record<HttpMethod, IntentDefinition<unknown, unknown>[]> = {
|
|
90
92
|
GET: [],
|
|
91
93
|
POST: [],
|
|
92
94
|
PUT: [],
|
|
@@ -115,12 +117,13 @@ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
// 각 메서드에 대해 핸들러 등록
|
|
118
|
-
const registerMethod = (method: HttpMethod, handlers: IntentDefinition<
|
|
120
|
+
const registerMethod = (method: HttpMethod, handlers: IntentDefinition<unknown, unknown>[]) => {
|
|
119
121
|
if (handlers.length === 0) return;
|
|
120
122
|
|
|
121
123
|
const methodLower = method.toLowerCase() as Lowercase<HttpMethod>;
|
|
124
|
+
const fillingWithMethods = filling as ManduFilling & Record<Lowercase<HttpMethod>, (handler: (ctx: ManduContext) => Response | Promise<Response>) => ManduFilling>;
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
fillingWithMethods[methodLower](async (ctx: ManduContext) => {
|
|
124
127
|
// 경로 매칭 (path가 있는 경우)
|
|
125
128
|
for (const def of handlers) {
|
|
126
129
|
if (def.path && !matchPath(ctx.url, def.path)) {
|
|
@@ -135,11 +138,15 @@ export function intent(intents: IntentMap): ManduFilling & IntentMeta {
|
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
// Input 검증
|
|
141
|
+
// Input 검증 (ctx.body() throws ValidationError on schema mismatch)
|
|
139
142
|
if (def.input && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
try {
|
|
144
|
+
await ctx.body(def.input);
|
|
145
|
+
} catch (validationError) {
|
|
146
|
+
return ctx.error(
|
|
147
|
+
'Validation failed',
|
|
148
|
+
validationError instanceof Error ? validationError : new Error(String(validationError))
|
|
149
|
+
);
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
152
|
|
|
@@ -247,26 +254,30 @@ export function generateOpenAPIFromIntent(
|
|
|
247
254
|
*/
|
|
248
255
|
function zodToJsonSchema(schema: ZodType<unknown>): Record<string, unknown> {
|
|
249
256
|
// 실제 구현은 zod-to-json-schema 라이브러리 사용 권장
|
|
250
|
-
const
|
|
257
|
+
const typeName = getZodTypeName(schema as ZodTypeAny);
|
|
251
258
|
|
|
252
|
-
if (
|
|
259
|
+
if (typeName === 'ZodString') {
|
|
253
260
|
return { type: 'string' };
|
|
254
261
|
}
|
|
255
|
-
if (
|
|
262
|
+
if (typeName === 'ZodNumber') {
|
|
256
263
|
return { type: 'number' };
|
|
257
264
|
}
|
|
258
|
-
if (
|
|
265
|
+
if (typeName === 'ZodBoolean') {
|
|
259
266
|
return { type: 'boolean' };
|
|
260
267
|
}
|
|
261
|
-
if (
|
|
268
|
+
if (typeName === 'ZodObject') {
|
|
262
269
|
const properties: Record<string, unknown> = {};
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
const shape = getZodObjectShape(schema as ZodTypeAny);
|
|
271
|
+
if (shape) {
|
|
272
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
273
|
+
properties[key] = zodToJsonSchema(value as ZodType<unknown>);
|
|
274
|
+
}
|
|
265
275
|
}
|
|
266
276
|
return { type: 'object', properties };
|
|
267
277
|
}
|
|
268
|
-
if (
|
|
269
|
-
|
|
278
|
+
if (typeName === 'ZodArray') {
|
|
279
|
+
const elementType = getZodArrayElementType(schema as ZodTypeAny);
|
|
280
|
+
return { type: 'array', items: elementType ? zodToJsonSchema(elementType as ZodType<unknown>) : {} };
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
return { type: 'unknown' };
|
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'
|
|
@@ -133,10 +133,10 @@ export function island<P extends Record<string, unknown>>(
|
|
|
133
133
|
// isIsland() - Island 컴포넌트 체크
|
|
134
134
|
// ============================================================================
|
|
135
135
|
|
|
136
|
-
export function isIsland(component: unknown): component is IslandComponent<
|
|
136
|
+
export function isIsland(component: unknown): component is IslandComponent<any> {
|
|
137
137
|
return (
|
|
138
138
|
typeof component === 'function' &&
|
|
139
|
-
(component as IslandComponent<
|
|
139
|
+
(component as IslandComponent<any>).__island === true
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -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
|
}
|