@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.22",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
10
|
"./client": "./src/client/index.ts",
|
|
11
|
+
"./plugins": "./src/plugins/index.ts",
|
|
12
|
+
"./error": "./src/error/index.ts",
|
|
11
13
|
"./*": "./src/*"
|
|
12
14
|
},
|
|
13
15
|
"files": [
|
|
@@ -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
|
/** 규칙 설명 */
|
|
@@ -66,7 +66,7 @@ export function deduplicatePatches(patches: PatchSuggestion[]): PatchSuggestion[
|
|
|
66
66
|
const result: PatchSuggestion[] = [];
|
|
67
67
|
|
|
68
68
|
for (const patch of patches) {
|
|
69
|
-
const key = `${patch.file}:${patch.type}:${patch.command
|
|
69
|
+
const key = `${patch.file}:${patch.type}:${patch.type === "command" ? patch.command : ""}`;
|
|
70
70
|
if (!seen.has(key)) {
|
|
71
71
|
seen.add(key);
|
|
72
72
|
result.push(patch);
|
|
@@ -103,8 +103,10 @@ export function generatePatchDescription(patch: PatchSuggestion): string {
|
|
|
103
103
|
case "delete":
|
|
104
104
|
return `[파일 삭제] ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`;
|
|
105
105
|
|
|
106
|
-
default:
|
|
107
|
-
|
|
106
|
+
default: {
|
|
107
|
+
const _exhaustive: never = patch;
|
|
108
|
+
return `[unknown] ${(_exhaustive as PatchSuggestion).file}\n 설명: ${(_exhaustive as PatchSuggestion).description}`;
|
|
109
|
+
}
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -229,12 +231,14 @@ export async function applyPatch(
|
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
default:
|
|
234
|
+
default: {
|
|
235
|
+
const _exhaustive: never = patch;
|
|
233
236
|
return {
|
|
234
237
|
applied: false,
|
|
235
|
-
patch,
|
|
236
|
-
error: `Unknown patch type
|
|
238
|
+
patch: _exhaustive as PatchSuggestion,
|
|
239
|
+
error: `Unknown patch type`,
|
|
237
240
|
};
|
|
241
|
+
}
|
|
238
242
|
}
|
|
239
243
|
} catch (error) {
|
|
240
244
|
return {
|
|
@@ -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
|
}
|
|
@@ -210,7 +210,7 @@ export function generateJsonReport(analysis: DoctorAnalysis): string {
|
|
|
210
210
|
file: p.file,
|
|
211
211
|
type: p.type,
|
|
212
212
|
description: p.description,
|
|
213
|
-
command: p.command,
|
|
213
|
+
...(p.type === "command" ? { command: p.command } : {}),
|
|
214
214
|
confidence: p.confidence,
|
|
215
215
|
})),
|
|
216
216
|
nextCommand: analysis.nextCommand,
|
|
@@ -281,7 +281,7 @@ export function generateDoctorMarkdownReport(analysis: DoctorAnalysis): string {
|
|
|
281
281
|
lines.push(`- **대상**: \`${p.file}\``);
|
|
282
282
|
lines.push(`- **신뢰도**: ${confidenceLabel}`);
|
|
283
283
|
|
|
284
|
-
if (p.command) {
|
|
284
|
+
if (p.type === "command") {
|
|
285
285
|
lines.push("");
|
|
286
286
|
lines.push("```bash");
|
|
287
287
|
lines.push(p.command);
|
package/src/brain/types.ts
CHANGED
|
@@ -73,25 +73,29 @@ export interface AdapterConfig {
|
|
|
73
73
|
// ========== Doctor Types ==========
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Patch suggestion from Doctor
|
|
76
|
+
* Patch suggestion from Doctor (discriminated union by type)
|
|
77
|
+
*
|
|
78
|
+
* type별로 필요한 필드가 타입 레벨에서 강제됨:
|
|
79
|
+
* - add: content 필수
|
|
80
|
+
* - modify: content 필수, line optional
|
|
81
|
+
* - delete: file + confidence만 필요
|
|
82
|
+
* - command: command 필수
|
|
77
83
|
*/
|
|
78
|
-
|
|
84
|
+
interface PatchSuggestionBase {
|
|
79
85
|
/** Target file path */
|
|
80
86
|
file: string;
|
|
81
87
|
/** Description of the change */
|
|
82
88
|
description: string;
|
|
83
|
-
/** Type of patch */
|
|
84
|
-
type: "add" | "modify" | "delete" | "command";
|
|
85
|
-
/** Content to add/modify (for add/modify types) */
|
|
86
|
-
content?: string;
|
|
87
|
-
/** Line number to modify (for modify type) */
|
|
88
|
-
line?: number;
|
|
89
|
-
/** Command to run (for command type) */
|
|
90
|
-
command?: string;
|
|
91
89
|
/** Confidence level (0-1) */
|
|
92
90
|
confidence: number;
|
|
93
91
|
}
|
|
94
92
|
|
|
93
|
+
export type PatchSuggestion =
|
|
94
|
+
| (PatchSuggestionBase & { type: "add"; content: string })
|
|
95
|
+
| (PatchSuggestionBase & { type: "modify"; content: string; line?: number })
|
|
96
|
+
| (PatchSuggestionBase & { type: "delete" })
|
|
97
|
+
| (PatchSuggestionBase & { type: "command"; command: string });
|
|
98
|
+
|
|
95
99
|
/**
|
|
96
100
|
* Doctor analysis result
|
|
97
101
|
*/
|
package/src/bundler/build.ts
CHANGED
|
@@ -929,14 +929,15 @@ async function buildRuntime(
|
|
|
929
929
|
outputPath: `/.mandu/client/${outputName}`,
|
|
930
930
|
errors: [],
|
|
931
931
|
};
|
|
932
|
-
} catch (error:
|
|
932
|
+
} catch (error: unknown) {
|
|
933
933
|
// 예외 발생 시에도 디버깅을 위해 소스 파일을 남겨둠
|
|
934
934
|
const extra: string[] = [];
|
|
935
|
-
|
|
936
|
-
|
|
935
|
+
const errObj = error as Record<string, unknown> | null;
|
|
936
|
+
if (errObj && Array.isArray(errObj.errors)) {
|
|
937
|
+
extra.push(...errObj.errors.map((e: unknown) => String((e as Record<string, unknown>)?.message || e)));
|
|
937
938
|
}
|
|
938
|
-
if (
|
|
939
|
-
extra.push(...
|
|
939
|
+
if (errObj && Array.isArray(errObj.logs)) {
|
|
940
|
+
extra.push(...errObj.logs.map((l: unknown) => String((l as Record<string, unknown>)?.message || l)));
|
|
940
941
|
}
|
|
941
942
|
|
|
942
943
|
return {
|
|
@@ -1328,19 +1329,18 @@ export async function buildClientBundles(
|
|
|
1328
1329
|
}
|
|
1329
1330
|
|
|
1330
1331
|
// 3-4. Runtime, Router, Vendor, DevTools 번들 병렬 빌드 (서로 독립적)
|
|
1331
|
-
const buildPromises: Promise<any>[] = [
|
|
1332
|
-
buildRuntime(outDir, options),
|
|
1333
|
-
buildRouterRuntime(outDir, options),
|
|
1334
|
-
buildVendorShims(outDir, options),
|
|
1335
|
-
];
|
|
1336
|
-
|
|
1337
|
-
// DevTools 번들은 dev 모드에서만 빌드
|
|
1338
1332
|
const isDev = env === "development";
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1333
|
+
const runtimePromise = buildRuntime(outDir, options);
|
|
1334
|
+
const routerPromise = buildRouterRuntime(outDir, options);
|
|
1335
|
+
const vendorPromise = buildVendorShims(outDir, options);
|
|
1336
|
+
const devtoolsPromise = isDev ? buildDevtoolsBundle(outDir, options) : null;
|
|
1337
|
+
|
|
1338
|
+
const [runtimeResult, routerResult, vendorResult, devtoolsResult] = await Promise.all([
|
|
1339
|
+
runtimePromise,
|
|
1340
|
+
routerPromise,
|
|
1341
|
+
vendorPromise,
|
|
1342
|
+
devtoolsPromise,
|
|
1343
|
+
]);
|
|
1344
1344
|
|
|
1345
1345
|
if (!runtimeResult.success) {
|
|
1346
1346
|
errors.push(...runtimeResult.errors.map((e: string) => `[Runtime] ${e}`));
|
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/bundler/dev.ts
CHANGED
|
@@ -396,7 +396,7 @@ export interface HMRMessage {
|
|
|
396
396
|
* HMR WebSocket 서버 생성
|
|
397
397
|
*/
|
|
398
398
|
export function createHMRServer(port: number): HMRServer {
|
|
399
|
-
const clients = new Set<
|
|
399
|
+
const clients = new Set<{ send: (data: string) => void; close: () => void }>();
|
|
400
400
|
const hmrPort = port + PORTS.HMR_OFFSET;
|
|
401
401
|
let restartHandler: (() => Promise<void>) | null = null;
|
|
402
402
|
|
package/src/client/island.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Hydration을 위한 클라이언트 사이드 컴포넌트 정의
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ReactNode } from "react";
|
|
7
|
-
import { getServerData as getGlobalServerData } from "./window-state";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { getServerData as getGlobalServerData } from "./window-state";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Island 정의 타입
|
|
@@ -118,12 +118,12 @@ export function island<TServerData, TSetupResult = TServerData>(
|
|
|
118
118
|
* SSR 데이터에 안전하게 접근하는 훅
|
|
119
119
|
* 서버 데이터가 없는 경우 fallback 반환
|
|
120
120
|
*/
|
|
121
|
-
export function useServerData<T>(key: string, fallback: T): T {
|
|
122
|
-
if (typeof window === "undefined") return fallback;
|
|
123
|
-
|
|
124
|
-
const data = getGlobalServerData<T>(key);
|
|
125
|
-
return data === undefined ? fallback : data;
|
|
126
|
-
}
|
|
121
|
+
export function useServerData<T>(key: string, fallback: T): T {
|
|
122
|
+
if (typeof window === "undefined") return fallback;
|
|
123
|
+
|
|
124
|
+
const data = getGlobalServerData<T>(key);
|
|
125
|
+
return data === undefined ? fallback : data;
|
|
126
|
+
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* Hydration 상태를 추적하는 훅
|
|
@@ -171,7 +171,7 @@ export function useIslandEvent<T = unknown>(
|
|
|
171
171
|
handler: (data: T) => void
|
|
172
172
|
): IslandEventHandle<T>["emit"] & IslandEventHandle<T> {
|
|
173
173
|
if (typeof window === "undefined") {
|
|
174
|
-
const noop = (() => {}) as IslandEventHandle<T>["emit"] & IslandEventHandle<T>;
|
|
174
|
+
const noop = (() => {}) as unknown as IslandEventHandle<T>["emit"] & IslandEventHandle<T>;
|
|
175
175
|
noop.emit = noop;
|
|
176
176
|
noop.cleanup = () => {};
|
|
177
177
|
return noop;
|
|
@@ -473,6 +473,7 @@ export interface PartialGroup {
|
|
|
473
473
|
* ```
|
|
474
474
|
*/
|
|
475
475
|
export function createPartialGroup(): PartialGroup {
|
|
476
|
+
// React ComponentType variance requires `any` for heterogeneous component storage
|
|
476
477
|
const partials = new Map<string, CompiledPartial<any>>();
|
|
477
478
|
|
|
478
479
|
return {
|
package/src/client/router.ts
CHANGED
|
@@ -421,7 +421,7 @@ export function cleanupRouter(): void {
|
|
|
421
421
|
|
|
422
422
|
window.removeEventListener("popstate", handlePopState);
|
|
423
423
|
document.removeEventListener("click", handleLinkClick);
|
|
424
|
-
listeners.clear();
|
|
424
|
+
listeners.current.clear();
|
|
425
425
|
initialized = false;
|
|
426
426
|
}
|
|
427
427
|
|
package/src/config/mcp-ref.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { z } from "zod";
|
|
20
|
-
import { withMetadata, withMetadataMultiple } from "./metadata.js";
|
|
20
|
+
import { withMetadata, withMetadataMultiple, getMetadata, hasMetadata } from "./metadata.js";
|
|
21
21
|
import {
|
|
22
22
|
SCHEMA_REFERENCE,
|
|
23
23
|
SENSITIVE_FIELD,
|
|
@@ -313,7 +313,7 @@ export function createMcpServerSchemaWithSecrets() {
|
|
|
313
313
|
* 스키마가 MCP 서버 참조인지 확인
|
|
314
314
|
*/
|
|
315
315
|
export function isMcpServerRef(schema: z.ZodType): boolean {
|
|
316
|
-
const ref = (schema
|
|
316
|
+
const ref = getMetadata(schema, SCHEMA_REFERENCE);
|
|
317
317
|
return ref?.type === "mcpServer";
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -321,7 +321,7 @@ export function isMcpServerRef(schema: z.ZodType): boolean {
|
|
|
321
321
|
* 스키마에서 MCP 서버 이름 추출
|
|
322
322
|
*/
|
|
323
323
|
export function getMcpServerName(schema: z.ZodType): string | undefined {
|
|
324
|
-
const ref = (schema
|
|
324
|
+
const ref = getMetadata(schema, SCHEMA_REFERENCE);
|
|
325
325
|
return ref?.type === "mcpServer" ? ref.name : undefined;
|
|
326
326
|
}
|
|
327
327
|
|
|
@@ -329,20 +329,20 @@ export function getMcpServerName(schema: z.ZodType): string | undefined {
|
|
|
329
329
|
* 스키마가 민감 필드인지 확인
|
|
330
330
|
*/
|
|
331
331
|
export function isSensitiveField(schema: z.ZodType): boolean {
|
|
332
|
-
return
|
|
332
|
+
return hasMetadata(schema, SENSITIVE_FIELD);
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
/**
|
|
336
336
|
* 스키마가 보호된 필드인지 확인
|
|
337
337
|
*/
|
|
338
338
|
export function isProtectedField(schema: z.ZodType): boolean {
|
|
339
|
-
return
|
|
339
|
+
return hasMetadata(schema, PROTECTED_FIELD);
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
/**
|
|
343
343
|
* 스키마가 환경 변수 기반인지 확인
|
|
344
344
|
*/
|
|
345
345
|
export function isEnvBasedField(schema: z.ZodType): boolean {
|
|
346
|
-
const source = (schema
|
|
346
|
+
const source = getMetadata(schema, FIELD_SOURCE);
|
|
347
347
|
return source?.source === "env";
|
|
348
348
|
}
|
|
@@ -228,7 +228,7 @@ describe("runtimeInjected", () => {
|
|
|
228
228
|
const schema = runtimeInjected(z.string());
|
|
229
229
|
const { RUNTIME_INJECTED } = require("./symbols.js");
|
|
230
230
|
|
|
231
|
-
expect((schema as
|
|
231
|
+
expect((schema as unknown as Record<symbol, unknown>)[RUNTIME_INJECTED]).toBe(true); // accessing runtime-injected symbol
|
|
232
232
|
});
|
|
233
233
|
});
|
|
234
234
|
|
package/src/config/metadata.ts
CHANGED
|
@@ -31,6 +31,20 @@ import {
|
|
|
31
31
|
VALIDATION_CONTEXT,
|
|
32
32
|
} from "./symbols.js";
|
|
33
33
|
|
|
34
|
+
// ============================================
|
|
35
|
+
// 타입 안전한 Symbol 프로퍼티 접근 유틸리티
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SchemaRecord: Zod 스키마를 symbol 키 접근 가능한 레코드로 변환.
|
|
40
|
+
* `as any` 캐스팅을 이 한 곳에서만 수행하여 나머지 코드의 타입 안전성을 보장.
|
|
41
|
+
*/
|
|
42
|
+
type SchemaRecord = Record<symbol, unknown>;
|
|
43
|
+
|
|
44
|
+
function asRecord(schema: z.ZodType): SchemaRecord {
|
|
45
|
+
return schema as unknown as SchemaRecord;
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
// ============================================
|
|
35
49
|
// 메타데이터 부착
|
|
36
50
|
// ============================================
|
|
@@ -60,7 +74,7 @@ export function withMetadata<
|
|
|
60
74
|
key: K,
|
|
61
75
|
value: SymbolMetadataMap[K]
|
|
62
76
|
): T {
|
|
63
|
-
(schema
|
|
77
|
+
asRecord(schema)[key] = value;
|
|
64
78
|
return schema;
|
|
65
79
|
}
|
|
66
80
|
|
|
@@ -79,8 +93,9 @@ export function withMetadataMultiple<T extends z.ZodType>(
|
|
|
79
93
|
schema: T,
|
|
80
94
|
entries: Array<[symbol, unknown]>
|
|
81
95
|
): T {
|
|
96
|
+
const record = asRecord(schema);
|
|
82
97
|
for (const [key, value] of entries) {
|
|
83
|
-
|
|
98
|
+
record[key] = value;
|
|
84
99
|
}
|
|
85
100
|
return schema;
|
|
86
101
|
}
|
|
@@ -100,14 +115,14 @@ export function getMetadata<K extends keyof SymbolMetadataMap>(
|
|
|
100
115
|
schema: z.ZodType,
|
|
101
116
|
key: K
|
|
102
117
|
): SymbolMetadataMap[K] | undefined {
|
|
103
|
-
return (schema as
|
|
118
|
+
return asRecord(schema)[key] as SymbolMetadataMap[K] | undefined;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|
|
107
122
|
* 스키마에 특정 메타데이터가 있는지 확인
|
|
108
123
|
*/
|
|
109
124
|
export function hasMetadata(schema: z.ZodType, key: symbol): boolean {
|
|
110
|
-
return key in (schema
|
|
125
|
+
return key in asRecord(schema);
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
/**
|
|
@@ -116,15 +131,16 @@ export function hasMetadata(schema: z.ZodType, key: symbol): boolean {
|
|
|
116
131
|
export function getAllMetadata(
|
|
117
132
|
schema: z.ZodType
|
|
118
133
|
): Partial<SymbolMetadataMap> {
|
|
119
|
-
const result
|
|
134
|
+
const result = {} as Record<symbol, unknown>;
|
|
135
|
+
const record = asRecord(schema);
|
|
120
136
|
|
|
121
137
|
for (const sym of ALL_METADATA_SYMBOLS) {
|
|
122
|
-
if (sym in
|
|
123
|
-
|
|
138
|
+
if (sym in record) {
|
|
139
|
+
result[sym] = record[sym];
|
|
124
140
|
}
|
|
125
141
|
}
|
|
126
142
|
|
|
127
|
-
return result
|
|
143
|
+
return result as Partial<SymbolMetadataMap>;
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
// ============================================
|
|
@@ -138,7 +154,7 @@ export function removeMetadata<T extends z.ZodType>(
|
|
|
138
154
|
schema: T,
|
|
139
155
|
key: symbol
|
|
140
156
|
): T {
|
|
141
|
-
delete (schema
|
|
157
|
+
delete asRecord(schema)[key];
|
|
142
158
|
return schema;
|
|
143
159
|
}
|
|
144
160
|
|
|
@@ -146,9 +162,10 @@ export function removeMetadata<T extends z.ZodType>(
|
|
|
146
162
|
* 스키마에서 모든 mandu 메타데이터 제거
|
|
147
163
|
*/
|
|
148
164
|
export function clearAllMetadata<T extends z.ZodType>(schema: T): T {
|
|
165
|
+
const record = asRecord(schema);
|
|
149
166
|
for (const sym of ALL_METADATA_SYMBOLS) {
|
|
150
|
-
if (sym in
|
|
151
|
-
delete
|
|
167
|
+
if (sym in record) {
|
|
168
|
+
delete record[sym];
|
|
152
169
|
}
|
|
153
170
|
}
|
|
154
171
|
return schema;
|
|
@@ -165,9 +182,11 @@ export function copyMetadata<T extends z.ZodType>(
|
|
|
165
182
|
from: z.ZodType,
|
|
166
183
|
to: T
|
|
167
184
|
): T {
|
|
185
|
+
const fromRecord = asRecord(from);
|
|
186
|
+
const toRecord = asRecord(to);
|
|
168
187
|
for (const sym of ALL_METADATA_SYMBOLS) {
|
|
169
|
-
if (sym in
|
|
170
|
-
|
|
188
|
+
if (sym in fromRecord) {
|
|
189
|
+
toRecord[sym] = fromRecord[sym];
|
|
171
190
|
}
|
|
172
191
|
}
|
|
173
192
|
return to;
|
|
@@ -273,7 +292,7 @@ export function getManduSymbolKeys(obj: object): symbol[] {
|
|
|
273
292
|
* 스키마에 메타데이터가 있는지 확인
|
|
274
293
|
*/
|
|
275
294
|
export function hasAnyMetadata(schema: z.ZodType): boolean {
|
|
276
|
-
return getManduSymbolKeys(schema as
|
|
295
|
+
return getManduSymbolKeys(schema as object).length > 0;
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
/**
|
|
@@ -283,10 +302,11 @@ export function serializeMetadata(
|
|
|
283
302
|
schema: z.ZodType
|
|
284
303
|
): Record<string, unknown> {
|
|
285
304
|
const result: Record<string, unknown> = {};
|
|
305
|
+
const record = asRecord(schema);
|
|
286
306
|
|
|
287
|
-
for (const sym of getManduSymbolKeys(schema as
|
|
307
|
+
for (const sym of getManduSymbolKeys(schema as object)) {
|
|
288
308
|
const name = sym.description ?? sym.toString();
|
|
289
|
-
result[name] =
|
|
309
|
+
result[name] = record[sym];
|
|
290
310
|
}
|
|
291
311
|
|
|
292
312
|
return result;
|
package/src/config/symbols.ts
CHANGED
|
@@ -133,7 +133,7 @@ export const ALL_METADATA_SYMBOLS = [
|
|
|
133
133
|
* 심볼이 mandu 메타데이터 심볼인지 확인
|
|
134
134
|
*/
|
|
135
135
|
export function isManduMetadataSymbol(sym: symbol): boolean {
|
|
136
|
-
return ALL_METADATA_SYMBOLS.includes(sym
|
|
136
|
+
return (ALL_METADATA_SYMBOLS as readonly symbol[]).includes(sym);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
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
|
|
|
@@ -156,6 +156,22 @@ export interface ValidationResult {
|
|
|
156
156
|
source?: string;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Assertion function: narrows unknown config to ValidatedManduConfig or throws.
|
|
161
|
+
*
|
|
162
|
+
* Useful in code paths that receive untrusted config objects and need
|
|
163
|
+
* to guarantee the type after the call without a separate null-check.
|
|
164
|
+
*/
|
|
165
|
+
export function assertValidConfig(cfg: unknown): asserts cfg is ValidatedManduConfig {
|
|
166
|
+
const result = ManduConfigSchema.safeParse(cfg);
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
const messages = result.error.errors.map(
|
|
169
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
170
|
+
);
|
|
171
|
+
throw new Error(`Invalid ManduConfig:\n ${messages.join("\n ")}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
/**
|
|
160
176
|
* 설정 파일 검증
|
|
161
177
|
*/
|
|
@@ -345,8 +345,8 @@ describe("LoaderContext", () => {
|
|
|
345
345
|
data: { title: "Hello", count: 5 },
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
expect(valid.title).toBe("Hello");
|
|
349
|
-
expect(valid.count).toBe(5);
|
|
348
|
+
expect((valid as Record<string, unknown>).title).toBe("Hello");
|
|
349
|
+
expect((valid as Record<string, unknown>).count).toBe(5);
|
|
350
350
|
|
|
351
351
|
// Invalid data should throw
|
|
352
352
|
await expect(
|
|
@@ -370,7 +370,7 @@ describe("LoaderContext", () => {
|
|
|
370
370
|
data: { anything: "goes" },
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
-
expect(data.anything).toBe("goes");
|
|
373
|
+
expect((data as Record<string, unknown>).anything).toBe("goes");
|
|
374
374
|
});
|
|
375
375
|
|
|
376
376
|
test("generateDigest creates hash", () => {
|
|
@@ -39,6 +39,7 @@ function parseJson(content: string, filePath: string): unknown {
|
|
|
39
39
|
async function parseYaml(content: string, filePath: string): Promise<unknown> {
|
|
40
40
|
try {
|
|
41
41
|
// yaml 패키지 동적 로드
|
|
42
|
+
// @ts-ignore dynamic optional import
|
|
42
43
|
const yaml = await import("yaml").catch(() => null);
|
|
43
44
|
|
|
44
45
|
if (!yaml) {
|
|
@@ -64,7 +65,9 @@ async function parseYaml(content: string, filePath: string): Promise<unknown> {
|
|
|
64
65
|
async function parseToml(content: string, filePath: string): Promise<unknown> {
|
|
65
66
|
try {
|
|
66
67
|
// @iarna/toml 또는 toml 패키지 동적 로드
|
|
68
|
+
// @ts-ignore dynamic optional import
|
|
67
69
|
const toml = await import("@iarna/toml").catch(() =>
|
|
70
|
+
// @ts-ignore dynamic optional import
|
|
68
71
|
import("toml").catch(() => null)
|
|
69
72
|
);
|
|
70
73
|
|
|
@@ -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({
|