@mandujs/core 0.12.2 → 0.13.0
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/README.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +94 -96
- package/src/config/validate.ts +213 -215
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +35 -14
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/rules.ts +5 -5
package/src/content/watcher.ts
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content Watcher - 개발 모드 파일 감시
|
|
3
|
-
*
|
|
4
|
-
* 콘텐츠 파일 변경 시 자동 리로드
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ContentWatcher } from "./types";
|
|
8
|
-
import chokidar, { type FSWatcher } from "chokidar";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* ContentWatcher 옵션
|
|
12
|
-
*/
|
|
13
|
-
export interface ContentWatcherOptions {
|
|
14
|
-
/** 루트 디렉토리 */
|
|
15
|
-
root: string;
|
|
16
|
-
/** 무시할 패턴 */
|
|
17
|
-
ignored?: string[];
|
|
18
|
-
/** 디바운스 (ms) */
|
|
19
|
-
debounce?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* ContentWatcher 구현
|
|
24
|
-
*/
|
|
25
|
-
class ContentWatcherImpl implements ContentWatcher {
|
|
26
|
-
private watcher: FSWatcher;
|
|
27
|
-
private handlers: Map<string, Set<(path: string) => void>> = new Map();
|
|
28
|
-
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
29
|
-
private debounceMs: number;
|
|
30
|
-
|
|
31
|
-
constructor(options: ContentWatcherOptions) {
|
|
32
|
-
const { root, ignored = ["**/node_modules/**", "**/.git/**"], debounce = 300 } = options;
|
|
33
|
-
|
|
34
|
-
this.debounceMs = debounce;
|
|
35
|
-
|
|
36
|
-
this.watcher = chokidar.watch([], {
|
|
37
|
-
cwd: root,
|
|
38
|
-
ignored,
|
|
39
|
-
ignoreInitial: true,
|
|
40
|
-
awaitWriteFinish: {
|
|
41
|
-
stabilityThreshold: debounce,
|
|
42
|
-
pollInterval: 100,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// 이벤트 핸들러 연결
|
|
47
|
-
this.watcher.on("change", (path) => this.emit("change", path));
|
|
48
|
-
this.watcher.on("add", (path) => this.emit("add", path));
|
|
49
|
-
this.watcher.on("unlink", (path) => this.emit("unlink", path));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 파일/패턴 감시 추가
|
|
54
|
-
*/
|
|
55
|
-
add(paths: string | string[]): void {
|
|
56
|
-
this.watcher.add(paths);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 파일/패턴 감시 제거
|
|
61
|
-
*/
|
|
62
|
-
remove(paths: string | string[]): void {
|
|
63
|
-
this.watcher.unwatch(paths);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 이벤트 핸들러 등록
|
|
68
|
-
*/
|
|
69
|
-
on(event: "change" | "add" | "unlink", handler: (path: string) => void): void {
|
|
70
|
-
if (!this.handlers.has(event)) {
|
|
71
|
-
this.handlers.set(event, new Set());
|
|
72
|
-
}
|
|
73
|
-
this.handlers.get(event)!.add(handler);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 이벤트 핸들러 제거
|
|
78
|
-
*/
|
|
79
|
-
off(event: "change" | "add" | "unlink", handler: (path: string) => void): void {
|
|
80
|
-
this.handlers.get(event)?.delete(handler);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 감시 종료
|
|
85
|
-
*/
|
|
86
|
-
async close(): Promise<void> {
|
|
87
|
-
// 모든 디바운스 타이머 정리
|
|
88
|
-
for (const timer of this.debounceTimers.values()) {
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
}
|
|
91
|
-
this.debounceTimers.clear();
|
|
92
|
-
|
|
93
|
-
await this.watcher.close();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 이벤트 발생 (디바운스 적용)
|
|
98
|
-
*/
|
|
99
|
-
private emit(event: "change" | "add" | "unlink", path: string): void {
|
|
100
|
-
const key = `${event}:${path}`;
|
|
101
|
-
|
|
102
|
-
// 기존 타이머 취소
|
|
103
|
-
if (this.debounceTimers.has(key)) {
|
|
104
|
-
clearTimeout(this.debounceTimers.get(key)!);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 디바운스 적용
|
|
108
|
-
const timer = setTimeout(() => {
|
|
109
|
-
this.debounceTimers.delete(key);
|
|
110
|
-
|
|
111
|
-
const handlers = this.handlers.get(event);
|
|
112
|
-
if (handlers) {
|
|
113
|
-
for (const handler of handlers) {
|
|
114
|
-
try {
|
|
115
|
-
handler(path);
|
|
116
|
-
} catch (error) {
|
|
117
|
-
console.error(
|
|
118
|
-
`[ContentWatcher] Handler error for ${event}:`,
|
|
119
|
-
error instanceof Error ? error.message : error
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}, this.debounceMs);
|
|
125
|
-
|
|
126
|
-
this.debounceTimers.set(key, timer);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* ContentWatcher 팩토리
|
|
132
|
-
*/
|
|
133
|
-
export function createContentWatcher(options: ContentWatcherOptions): ContentWatcher {
|
|
134
|
-
return new ContentWatcherImpl(options);
|
|
135
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Content Watcher - 개발 모드 파일 감시
|
|
3
|
+
*
|
|
4
|
+
* 콘텐츠 파일 변경 시 자동 리로드
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ContentWatcher } from "./types";
|
|
8
|
+
import chokidar, { type FSWatcher } from "chokidar";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ContentWatcher 옵션
|
|
12
|
+
*/
|
|
13
|
+
export interface ContentWatcherOptions {
|
|
14
|
+
/** 루트 디렉토리 */
|
|
15
|
+
root: string;
|
|
16
|
+
/** 무시할 패턴 */
|
|
17
|
+
ignored?: string[];
|
|
18
|
+
/** 디바운스 (ms) */
|
|
19
|
+
debounce?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ContentWatcher 구현
|
|
24
|
+
*/
|
|
25
|
+
class ContentWatcherImpl implements ContentWatcher {
|
|
26
|
+
private watcher: FSWatcher;
|
|
27
|
+
private handlers: Map<string, Set<(path: string) => void>> = new Map();
|
|
28
|
+
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
29
|
+
private debounceMs: number;
|
|
30
|
+
|
|
31
|
+
constructor(options: ContentWatcherOptions) {
|
|
32
|
+
const { root, ignored = ["**/node_modules/**", "**/.git/**"], debounce = 300 } = options;
|
|
33
|
+
|
|
34
|
+
this.debounceMs = debounce;
|
|
35
|
+
|
|
36
|
+
this.watcher = chokidar.watch([], {
|
|
37
|
+
cwd: root,
|
|
38
|
+
ignored,
|
|
39
|
+
ignoreInitial: true,
|
|
40
|
+
awaitWriteFinish: {
|
|
41
|
+
stabilityThreshold: debounce,
|
|
42
|
+
pollInterval: 100,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 이벤트 핸들러 연결
|
|
47
|
+
this.watcher.on("change", (path) => this.emit("change", path));
|
|
48
|
+
this.watcher.on("add", (path) => this.emit("add", path));
|
|
49
|
+
this.watcher.on("unlink", (path) => this.emit("unlink", path));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 파일/패턴 감시 추가
|
|
54
|
+
*/
|
|
55
|
+
add(paths: string | string[]): void {
|
|
56
|
+
this.watcher.add(paths);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 파일/패턴 감시 제거
|
|
61
|
+
*/
|
|
62
|
+
remove(paths: string | string[]): void {
|
|
63
|
+
this.watcher.unwatch(paths);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 이벤트 핸들러 등록
|
|
68
|
+
*/
|
|
69
|
+
on(event: "change" | "add" | "unlink", handler: (path: string) => void): void {
|
|
70
|
+
if (!this.handlers.has(event)) {
|
|
71
|
+
this.handlers.set(event, new Set());
|
|
72
|
+
}
|
|
73
|
+
this.handlers.get(event)!.add(handler);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 이벤트 핸들러 제거
|
|
78
|
+
*/
|
|
79
|
+
off(event: "change" | "add" | "unlink", handler: (path: string) => void): void {
|
|
80
|
+
this.handlers.get(event)?.delete(handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 감시 종료
|
|
85
|
+
*/
|
|
86
|
+
async close(): Promise<void> {
|
|
87
|
+
// 모든 디바운스 타이머 정리
|
|
88
|
+
for (const timer of this.debounceTimers.values()) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
}
|
|
91
|
+
this.debounceTimers.clear();
|
|
92
|
+
|
|
93
|
+
await this.watcher.close();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 이벤트 발생 (디바운스 적용)
|
|
98
|
+
*/
|
|
99
|
+
private emit(event: "change" | "add" | "unlink", path: string): void {
|
|
100
|
+
const key = `${event}:${path}`;
|
|
101
|
+
|
|
102
|
+
// 기존 타이머 취소
|
|
103
|
+
if (this.debounceTimers.has(key)) {
|
|
104
|
+
clearTimeout(this.debounceTimers.get(key)!);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 디바운스 적용
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
this.debounceTimers.delete(key);
|
|
110
|
+
|
|
111
|
+
const handlers = this.handlers.get(event);
|
|
112
|
+
if (handlers) {
|
|
113
|
+
for (const handler of handlers) {
|
|
114
|
+
try {
|
|
115
|
+
handler(path);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(
|
|
118
|
+
`[ContentWatcher] Handler error for ${event}:`,
|
|
119
|
+
error instanceof Error ? error.message : error
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, this.debounceMs);
|
|
125
|
+
|
|
126
|
+
this.debounceTimers.set(key, timer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* ContentWatcher 팩토리
|
|
132
|
+
*/
|
|
133
|
+
export function createContentWatcher(options: ContentWatcherOptions): ContentWatcher {
|
|
134
|
+
return new ContentWatcherImpl(options);
|
|
135
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { Mandu, createClientContract } from "../index";
|
|
4
|
-
|
|
5
|
-
describe("Client-safe contract", () => {
|
|
6
|
-
const contract = Mandu.contract({
|
|
7
|
-
request: {
|
|
8
|
-
GET: {
|
|
9
|
-
query: z.object({ id: z.string() }),
|
|
10
|
-
},
|
|
11
|
-
POST: {
|
|
12
|
-
body: z.object({ name: z.string() }),
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
response: {
|
|
16
|
-
200: z.object({ ok: z.boolean() }),
|
|
17
|
-
201: z.object({ id: z.string() }),
|
|
18
|
-
400: z.object({ error: z.string() }),
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("should pick only selected schemas", () => {
|
|
23
|
-
const clientContract = createClientContract(contract, {
|
|
24
|
-
request: {
|
|
25
|
-
POST: { body: true },
|
|
26
|
-
},
|
|
27
|
-
response: [201],
|
|
28
|
-
includeErrors: true,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(clientContract.request.GET).toBeUndefined();
|
|
32
|
-
expect(clientContract.request.POST?.body).toBeDefined();
|
|
33
|
-
expect(clientContract.response[200]).toBeUndefined();
|
|
34
|
-
expect(clientContract.response[201]).toBeDefined();
|
|
35
|
-
expect(clientContract.response[400]).toBeDefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("should return original contract when no options are provided", () => {
|
|
39
|
-
const clientContract = createClientContract(contract);
|
|
40
|
-
expect(clientContract).toBe(contract);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Mandu, createClientContract } from "../index";
|
|
4
|
+
|
|
5
|
+
describe("Client-safe contract", () => {
|
|
6
|
+
const contract = Mandu.contract({
|
|
7
|
+
request: {
|
|
8
|
+
GET: {
|
|
9
|
+
query: z.object({ id: z.string() }),
|
|
10
|
+
},
|
|
11
|
+
POST: {
|
|
12
|
+
body: z.object({ name: z.string() }),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
response: {
|
|
16
|
+
200: z.object({ ok: z.boolean() }),
|
|
17
|
+
201: z.object({ id: z.string() }),
|
|
18
|
+
400: z.object({ error: z.string() }),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should pick only selected schemas", () => {
|
|
23
|
+
const clientContract = createClientContract(contract, {
|
|
24
|
+
request: {
|
|
25
|
+
POST: { body: true },
|
|
26
|
+
},
|
|
27
|
+
response: [201],
|
|
28
|
+
includeErrors: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(clientContract.request.GET).toBeUndefined();
|
|
32
|
+
expect(clientContract.request.POST?.body).toBeDefined();
|
|
33
|
+
expect(clientContract.response[200]).toBeUndefined();
|
|
34
|
+
expect(clientContract.response[201]).toBeDefined();
|
|
35
|
+
expect(clientContract.response[400]).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return original contract when no options are provided", () => {
|
|
39
|
+
const clientContract = createClientContract(contract);
|
|
40
|
+
expect(clientContract).toBe(contract);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Client-safe Contract Utilities
|
|
3
|
-
* Reduce contract exposure for client usage (forms, UI validation)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ContractSchema,
|
|
8
|
-
ContractMethod,
|
|
9
|
-
MethodRequestSchema,
|
|
10
|
-
ClientSafeOptions,
|
|
11
|
-
ContractRequestSchema,
|
|
12
|
-
ContractResponseSchema,
|
|
13
|
-
} from "./schema";
|
|
14
|
-
|
|
15
|
-
const ERROR_STATUS_CODES = [400, 401, 403, 404, 500] as const;
|
|
16
|
-
|
|
17
|
-
function normalizeResponseSelection(
|
|
18
|
-
selection: ClientSafeOptions["response"]
|
|
19
|
-
): number[] {
|
|
20
|
-
if (!selection) return [];
|
|
21
|
-
if (Array.isArray(selection)) return selection;
|
|
22
|
-
|
|
23
|
-
const result: number[] = [];
|
|
24
|
-
for (const [code, enabled] of Object.entries(selection)) {
|
|
25
|
-
if (enabled) {
|
|
26
|
-
const num = Number(code);
|
|
27
|
-
if (!Number.isNaN(num)) {
|
|
28
|
-
result.push(num);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function pickRequestSchema(
|
|
36
|
-
methodSchema: MethodRequestSchema,
|
|
37
|
-
selection: NonNullable<ClientSafeOptions["request"]>[ContractMethod]
|
|
38
|
-
): MethodRequestSchema | undefined {
|
|
39
|
-
if (!selection) return undefined;
|
|
40
|
-
|
|
41
|
-
const picked: MethodRequestSchema = {};
|
|
42
|
-
|
|
43
|
-
if (selection.query && methodSchema.query) {
|
|
44
|
-
picked.query = methodSchema.query;
|
|
45
|
-
}
|
|
46
|
-
if (selection.body && methodSchema.body) {
|
|
47
|
-
picked.body = methodSchema.body;
|
|
48
|
-
}
|
|
49
|
-
if (selection.params && methodSchema.params) {
|
|
50
|
-
picked.params = methodSchema.params;
|
|
51
|
-
}
|
|
52
|
-
if (selection.headers && methodSchema.headers) {
|
|
53
|
-
picked.headers = methodSchema.headers;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Create a client-safe contract by selecting exposed schemas.
|
|
61
|
-
* If options are omitted and contract.clientSafe is not defined,
|
|
62
|
-
* the original contract is returned (with a warning).
|
|
63
|
-
*/
|
|
64
|
-
export function createClientContract<T extends ContractSchema>(
|
|
65
|
-
contract: T,
|
|
66
|
-
options?: ClientSafeOptions
|
|
67
|
-
): ContractSchema {
|
|
68
|
-
const resolved = options ?? contract.clientSafe;
|
|
69
|
-
|
|
70
|
-
if (!resolved) {
|
|
71
|
-
console.warn(
|
|
72
|
-
"[Mandu] clientContract: no clientSafe options provided. Returning original contract."
|
|
73
|
-
);
|
|
74
|
-
return contract;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const requestSelection = resolved.request ?? {};
|
|
78
|
-
const responseSelection = normalizeResponseSelection(resolved.response);
|
|
79
|
-
const safeRequest: ContractRequestSchema = {};
|
|
80
|
-
const safeResponse: ContractResponseSchema = {};
|
|
81
|
-
|
|
82
|
-
for (const method of Object.keys(requestSelection) as ContractMethod[]) {
|
|
83
|
-
const methodSchema = contract.request[method] as MethodRequestSchema | undefined;
|
|
84
|
-
if (!methodSchema) continue;
|
|
85
|
-
|
|
86
|
-
const picked = pickRequestSchema(methodSchema, requestSelection[method]);
|
|
87
|
-
if (picked) {
|
|
88
|
-
safeRequest[method] = picked;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const allowedResponses = new Set(responseSelection);
|
|
93
|
-
|
|
94
|
-
if (resolved.includeErrors) {
|
|
95
|
-
for (const code of ERROR_STATUS_CODES) {
|
|
96
|
-
if (contract.response[code]) {
|
|
97
|
-
allowedResponses.add(code);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const code of allowedResponses) {
|
|
103
|
-
const schema = contract.response[code];
|
|
104
|
-
if (schema) {
|
|
105
|
-
safeResponse[code] = schema;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
...contract,
|
|
111
|
-
request: safeRequest,
|
|
112
|
-
response: safeResponse,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Client-safe Contract Utilities
|
|
3
|
+
* Reduce contract exposure for client usage (forms, UI validation)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ContractSchema,
|
|
8
|
+
ContractMethod,
|
|
9
|
+
MethodRequestSchema,
|
|
10
|
+
ClientSafeOptions,
|
|
11
|
+
ContractRequestSchema,
|
|
12
|
+
ContractResponseSchema,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
|
|
15
|
+
const ERROR_STATUS_CODES = [400, 401, 403, 404, 500] as const;
|
|
16
|
+
|
|
17
|
+
function normalizeResponseSelection(
|
|
18
|
+
selection: ClientSafeOptions["response"]
|
|
19
|
+
): number[] {
|
|
20
|
+
if (!selection) return [];
|
|
21
|
+
if (Array.isArray(selection)) return selection;
|
|
22
|
+
|
|
23
|
+
const result: number[] = [];
|
|
24
|
+
for (const [code, enabled] of Object.entries(selection)) {
|
|
25
|
+
if (enabled) {
|
|
26
|
+
const num = Number(code);
|
|
27
|
+
if (!Number.isNaN(num)) {
|
|
28
|
+
result.push(num);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pickRequestSchema(
|
|
36
|
+
methodSchema: MethodRequestSchema,
|
|
37
|
+
selection: NonNullable<ClientSafeOptions["request"]>[ContractMethod]
|
|
38
|
+
): MethodRequestSchema | undefined {
|
|
39
|
+
if (!selection) return undefined;
|
|
40
|
+
|
|
41
|
+
const picked: MethodRequestSchema = {};
|
|
42
|
+
|
|
43
|
+
if (selection.query && methodSchema.query) {
|
|
44
|
+
picked.query = methodSchema.query;
|
|
45
|
+
}
|
|
46
|
+
if (selection.body && methodSchema.body) {
|
|
47
|
+
picked.body = methodSchema.body;
|
|
48
|
+
}
|
|
49
|
+
if (selection.params && methodSchema.params) {
|
|
50
|
+
picked.params = methodSchema.params;
|
|
51
|
+
}
|
|
52
|
+
if (selection.headers && methodSchema.headers) {
|
|
53
|
+
picked.headers = methodSchema.headers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a client-safe contract by selecting exposed schemas.
|
|
61
|
+
* If options are omitted and contract.clientSafe is not defined,
|
|
62
|
+
* the original contract is returned (with a warning).
|
|
63
|
+
*/
|
|
64
|
+
export function createClientContract<T extends ContractSchema>(
|
|
65
|
+
contract: T,
|
|
66
|
+
options?: ClientSafeOptions
|
|
67
|
+
): ContractSchema {
|
|
68
|
+
const resolved = options ?? contract.clientSafe;
|
|
69
|
+
|
|
70
|
+
if (!resolved) {
|
|
71
|
+
console.warn(
|
|
72
|
+
"[Mandu] clientContract: no clientSafe options provided. Returning original contract."
|
|
73
|
+
);
|
|
74
|
+
return contract;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const requestSelection = resolved.request ?? {};
|
|
78
|
+
const responseSelection = normalizeResponseSelection(resolved.response);
|
|
79
|
+
const safeRequest: ContractRequestSchema = {};
|
|
80
|
+
const safeResponse: ContractResponseSchema = {};
|
|
81
|
+
|
|
82
|
+
for (const method of Object.keys(requestSelection) as ContractMethod[]) {
|
|
83
|
+
const methodSchema = contract.request[method] as MethodRequestSchema | undefined;
|
|
84
|
+
if (!methodSchema) continue;
|
|
85
|
+
|
|
86
|
+
const picked = pickRequestSchema(methodSchema, requestSelection[method]);
|
|
87
|
+
if (picked) {
|
|
88
|
+
safeRequest[method] = picked;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const allowedResponses = new Set(responseSelection);
|
|
93
|
+
|
|
94
|
+
if (resolved.includeErrors) {
|
|
95
|
+
for (const code of ERROR_STATUS_CODES) {
|
|
96
|
+
if (contract.response[code]) {
|
|
97
|
+
allowedResponses.add(code);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const code of allowedResponses) {
|
|
103
|
+
const schema = contract.response[code];
|
|
104
|
+
if (schema) {
|
|
105
|
+
safeResponse[code] = schema;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...contract,
|
|
111
|
+
request: safeRequest,
|
|
112
|
+
response: safeResponse,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/contract/client.ts
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* - 타입 안전 fetch 호출
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { z } from "zod";
|
|
11
|
-
import type {
|
|
12
|
-
ContractSchema,
|
|
13
|
-
ContractMethod,
|
|
14
|
-
MethodRequestSchema,
|
|
15
|
-
} from "./schema";
|
|
16
|
-
import type { InferResponseSchema } from "./types";
|
|
17
|
-
import { TIMEOUTS } from "../constants";
|
|
10
|
+
import type { z } from "zod";
|
|
11
|
+
import type {
|
|
12
|
+
ContractSchema,
|
|
13
|
+
ContractMethod,
|
|
14
|
+
MethodRequestSchema,
|
|
15
|
+
} from "./schema";
|
|
16
|
+
import type { InferResponseSchema } from "./types";
|
|
17
|
+
import { TIMEOUTS } from "../constants";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Client options for making requests
|
|
@@ -74,12 +74,12 @@ type InferRequestOptions<T extends MethodRequestSchema | undefined> =
|
|
|
74
74
|
/**
|
|
75
75
|
* Infer success response from contract
|
|
76
76
|
*/
|
|
77
|
-
type InferSuccessResponse<TResponse extends ContractSchema["response"]> =
|
|
78
|
-
InferResponseSchema<TResponse[200]> extends never
|
|
79
|
-
? InferResponseSchema<TResponse[201]> extends never
|
|
80
|
-
? unknown
|
|
81
|
-
: InferResponseSchema<TResponse[201]>
|
|
82
|
-
: InferResponseSchema<TResponse[200]>;
|
|
77
|
+
type InferSuccessResponse<TResponse extends ContractSchema["response"]> =
|
|
78
|
+
InferResponseSchema<TResponse[200]> extends never
|
|
79
|
+
? InferResponseSchema<TResponse[201]> extends never
|
|
80
|
+
? unknown
|
|
81
|
+
: InferResponseSchema<TResponse[201]>
|
|
82
|
+
: InferResponseSchema<TResponse[200]>;
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Contract client method
|
|
@@ -177,7 +177,7 @@ export function createClient<T extends ContractSchema>(
|
|
|
177
177
|
baseUrl,
|
|
178
178
|
headers: defaultHeaders = {},
|
|
179
179
|
fetch: customFetch = fetch,
|
|
180
|
-
timeout = TIMEOUTS.CLIENT_DEFAULT,
|
|
180
|
+
timeout = TIMEOUTS.CLIENT_DEFAULT,
|
|
181
181
|
} = options;
|
|
182
182
|
|
|
183
183
|
const methods: ContractMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
@@ -287,7 +287,7 @@ export async function contractFetch<
|
|
|
287
287
|
const {
|
|
288
288
|
headers: defaultHeaders = {},
|
|
289
289
|
fetch: customFetch = fetch,
|
|
290
|
-
timeout = TIMEOUTS.CLIENT_DEFAULT,
|
|
290
|
+
timeout = TIMEOUTS.CLIENT_DEFAULT,
|
|
291
291
|
} = clientOptions;
|
|
292
292
|
|
|
293
293
|
// Build URL
|