@mandujs/core 0.9.46 → 0.11.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.md +79 -10
- package/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +9 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/config/validate.ts +122 -65
- package/src/config/watcher.ts +311 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/errors/extractor.ts +409 -0
- package/src/errors/index.ts +19 -0
- package/src/filling/context.ts +29 -1
- package/src/filling/deps.ts +238 -0
- package/src/filling/filling.ts +94 -8
- package/src/filling/index.ts +18 -0
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +4 -1
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/transports.ts +365 -0
- package/src/plugins/index.ts +38 -0
- package/src/plugins/registry.ts +377 -0
- package/src/plugins/types.ts +363 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/runtime/session-key.ts +328 -0
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
- package/src/utils/string-safe.ts +298 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNA-006: Config Hot Reload
|
|
3
|
+
*
|
|
4
|
+
* 설정 파일 변경 감시 및 핫 리로드
|
|
5
|
+
* - 디바운스로 연속 변경 병합
|
|
6
|
+
* - 에러 발생 시 기존 설정 유지
|
|
7
|
+
* - 클린업 함수 반환
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { watch, type FSWatcher } from "fs";
|
|
11
|
+
import { loadManduConfig, type ManduConfig } from "./mandu.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 설정 변경 이벤트 타입
|
|
15
|
+
*/
|
|
16
|
+
export type ConfigChangeEvent = {
|
|
17
|
+
/** 이전 설정 */
|
|
18
|
+
previous: ManduConfig;
|
|
19
|
+
/** 새 설정 */
|
|
20
|
+
current: ManduConfig;
|
|
21
|
+
/** 변경된 파일 경로 */
|
|
22
|
+
path: string;
|
|
23
|
+
/** 변경 시간 */
|
|
24
|
+
timestamp: Date;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 설정 감시 옵션
|
|
29
|
+
*/
|
|
30
|
+
export interface WatchConfigOptions {
|
|
31
|
+
/** 디바운스 딜레이 (ms, 기본: 100) */
|
|
32
|
+
debounceMs?: number;
|
|
33
|
+
/** 초기 로드 시에도 콜백 호출 (기본: false) */
|
|
34
|
+
immediate?: boolean;
|
|
35
|
+
/** 에러 핸들러 */
|
|
36
|
+
onError?: (error: Error) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 설정 변경 콜백
|
|
41
|
+
*/
|
|
42
|
+
export type ConfigChangeCallback = (
|
|
43
|
+
newConfig: ManduConfig,
|
|
44
|
+
event: ConfigChangeEvent
|
|
45
|
+
) => void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 설정 감시 결과
|
|
49
|
+
*/
|
|
50
|
+
export interface ConfigWatcher {
|
|
51
|
+
/** 감시 중지 */
|
|
52
|
+
stop: () => void;
|
|
53
|
+
/** 현재 설정 */
|
|
54
|
+
getConfig: () => ManduConfig;
|
|
55
|
+
/** 수동 리로드 */
|
|
56
|
+
reload: () => Promise<ManduConfig>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 설정 파일 감시 및 핫 리로드
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const watcher = await watchConfig(
|
|
65
|
+
* "/path/to/project",
|
|
66
|
+
* (newConfig, event) => {
|
|
67
|
+
* console.log("Config changed:", event.path);
|
|
68
|
+
* applyConfig(newConfig);
|
|
69
|
+
* },
|
|
70
|
+
* { debounceMs: 200 }
|
|
71
|
+
* );
|
|
72
|
+
*
|
|
73
|
+
* // 나중에 정리
|
|
74
|
+
* watcher.stop();
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export async function watchConfig(
|
|
78
|
+
rootDir: string,
|
|
79
|
+
onReload: ConfigChangeCallback,
|
|
80
|
+
options: WatchConfigOptions = {}
|
|
81
|
+
): Promise<ConfigWatcher> {
|
|
82
|
+
const { debounceMs = 100, immediate = false, onError } = options;
|
|
83
|
+
|
|
84
|
+
// 현재 설정 로드
|
|
85
|
+
let currentConfig = await loadManduConfig(rootDir);
|
|
86
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
87
|
+
let watchers: FSWatcher[] = [];
|
|
88
|
+
let isWatching = true;
|
|
89
|
+
|
|
90
|
+
// 감시 대상 파일들
|
|
91
|
+
const configFiles = [
|
|
92
|
+
"mandu.config.ts",
|
|
93
|
+
"mandu.config.js",
|
|
94
|
+
"mandu.config.json",
|
|
95
|
+
".mandu/guard.json",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 설정 리로드 수행
|
|
100
|
+
*/
|
|
101
|
+
const doReload = async (changedPath: string): Promise<void> => {
|
|
102
|
+
if (!isWatching) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const previous = currentConfig;
|
|
106
|
+
const newConfig = await loadManduConfig(rootDir);
|
|
107
|
+
|
|
108
|
+
// 설정이 동일하면 무시
|
|
109
|
+
if (JSON.stringify(previous) === JSON.stringify(newConfig)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 이전에 설정이 있었는데 새 설정이 비어있으면 (파싱 에러 등)
|
|
114
|
+
// 기존 설정 유지하고 에러 핸들러 호출
|
|
115
|
+
const previousHasContent = Object.keys(previous).length > 0;
|
|
116
|
+
const newIsEmpty = Object.keys(newConfig).length === 0;
|
|
117
|
+
|
|
118
|
+
if (previousHasContent && newIsEmpty) {
|
|
119
|
+
if (onError) {
|
|
120
|
+
onError(new Error(`Failed to reload config from ${changedPath}`));
|
|
121
|
+
}
|
|
122
|
+
return; // 기존 설정 유지
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
currentConfig = newConfig;
|
|
126
|
+
|
|
127
|
+
const event: ConfigChangeEvent = {
|
|
128
|
+
previous,
|
|
129
|
+
current: newConfig,
|
|
130
|
+
path: changedPath,
|
|
131
|
+
timestamp: new Date(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
onReload(newConfig, event);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (onError && error instanceof Error) {
|
|
137
|
+
onError(error);
|
|
138
|
+
}
|
|
139
|
+
// 에러 시 기존 설정 유지
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 디바운스된 리로드
|
|
145
|
+
*/
|
|
146
|
+
const scheduleReload = (changedPath: string): void => {
|
|
147
|
+
if (debounceTimer) {
|
|
148
|
+
clearTimeout(debounceTimer);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
debounceTimer = setTimeout(() => {
|
|
152
|
+
debounceTimer = null;
|
|
153
|
+
doReload(changedPath);
|
|
154
|
+
}, debounceMs);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// 각 설정 파일 감시 시작
|
|
158
|
+
for (const fileName of configFiles) {
|
|
159
|
+
const filePath = `${rootDir}/${fileName}`;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const watcher = watch(filePath, (eventType) => {
|
|
163
|
+
if (eventType === "change" && isWatching) {
|
|
164
|
+
scheduleReload(filePath);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
watcher.on("error", () => {
|
|
169
|
+
// 파일이 없거나 접근 불가 - 무시
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
watchers.push(watcher);
|
|
173
|
+
} catch {
|
|
174
|
+
// 파일이 없으면 감시 생략
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 초기 콜백 호출
|
|
179
|
+
if (immediate) {
|
|
180
|
+
const event: ConfigChangeEvent = {
|
|
181
|
+
previous: {},
|
|
182
|
+
current: currentConfig,
|
|
183
|
+
path: rootDir,
|
|
184
|
+
timestamp: new Date(),
|
|
185
|
+
};
|
|
186
|
+
onReload(currentConfig, event);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
stop: () => {
|
|
191
|
+
isWatching = false;
|
|
192
|
+
if (debounceTimer) {
|
|
193
|
+
clearTimeout(debounceTimer);
|
|
194
|
+
debounceTimer = null;
|
|
195
|
+
}
|
|
196
|
+
for (const watcher of watchers) {
|
|
197
|
+
watcher.close();
|
|
198
|
+
}
|
|
199
|
+
watchers = [];
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
getConfig: () => currentConfig,
|
|
203
|
+
|
|
204
|
+
reload: async () => {
|
|
205
|
+
const previous = currentConfig;
|
|
206
|
+
currentConfig = await loadManduConfig(rootDir);
|
|
207
|
+
|
|
208
|
+
if (JSON.stringify(previous) !== JSON.stringify(currentConfig)) {
|
|
209
|
+
const event: ConfigChangeEvent = {
|
|
210
|
+
previous,
|
|
211
|
+
current: currentConfig,
|
|
212
|
+
path: rootDir,
|
|
213
|
+
timestamp: new Date(),
|
|
214
|
+
};
|
|
215
|
+
onReload(currentConfig, event);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return currentConfig;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 간단한 단일 파일 감시
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* const stop = watchConfigFile(
|
|
229
|
+
* "/path/to/mandu.config.ts",
|
|
230
|
+
* async (path) => {
|
|
231
|
+
* const config = await loadManduConfig(dirname(path));
|
|
232
|
+
* applyConfig(config);
|
|
233
|
+
* }
|
|
234
|
+
* );
|
|
235
|
+
*
|
|
236
|
+
* // 정리
|
|
237
|
+
* stop();
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export function watchConfigFile(
|
|
241
|
+
filePath: string,
|
|
242
|
+
onChange: (path: string) => void,
|
|
243
|
+
debounceMs = 100
|
|
244
|
+
): () => void {
|
|
245
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
246
|
+
let watcher: FSWatcher | null = null;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
watcher = watch(filePath, (eventType) => {
|
|
250
|
+
if (eventType === "change") {
|
|
251
|
+
if (timer) clearTimeout(timer);
|
|
252
|
+
timer = setTimeout(() => {
|
|
253
|
+
timer = null;
|
|
254
|
+
onChange(filePath);
|
|
255
|
+
}, debounceMs);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} catch {
|
|
259
|
+
// 파일 없음 - 빈 함수 반환
|
|
260
|
+
return () => {};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return () => {
|
|
264
|
+
if (timer) {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
timer = null;
|
|
267
|
+
}
|
|
268
|
+
if (watcher) {
|
|
269
|
+
watcher.close();
|
|
270
|
+
watcher = null;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 설정 변경 감지 헬퍼
|
|
277
|
+
*
|
|
278
|
+
* 특정 설정 섹션의 변경 여부 확인
|
|
279
|
+
*/
|
|
280
|
+
export function hasConfigChanged(
|
|
281
|
+
previous: ManduConfig,
|
|
282
|
+
current: ManduConfig,
|
|
283
|
+
section?: keyof ManduConfig
|
|
284
|
+
): boolean {
|
|
285
|
+
if (section) {
|
|
286
|
+
return JSON.stringify(previous[section]) !== JSON.stringify(current[section]);
|
|
287
|
+
}
|
|
288
|
+
return JSON.stringify(previous) !== JSON.stringify(current);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 변경된 설정 섹션 목록
|
|
293
|
+
*/
|
|
294
|
+
export function getChangedSections(
|
|
295
|
+
previous: ManduConfig,
|
|
296
|
+
current: ManduConfig
|
|
297
|
+
): (keyof ManduConfig)[] {
|
|
298
|
+
const sections: (keyof ManduConfig)[] = [
|
|
299
|
+
"server",
|
|
300
|
+
"guard",
|
|
301
|
+
"build",
|
|
302
|
+
"dev",
|
|
303
|
+
"fsRoutes",
|
|
304
|
+
"seo",
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
return sections.filter(
|
|
308
|
+
(section) =>
|
|
309
|
+
JSON.stringify(previous[section]) !== JSON.stringify(current[section])
|
|
310
|
+
);
|
|
311
|
+
}
|
package/src/contract/index.ts
CHANGED
|
@@ -10,18 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
export * from "./schema";
|
|
12
12
|
export * from "./types";
|
|
13
|
-
export * from "./validator";
|
|
14
|
-
export * from "./handler";
|
|
15
|
-
export * from "./client";
|
|
16
|
-
export * from "./normalize";
|
|
17
|
-
export * from "./registry";
|
|
18
|
-
export * from "./client-safe";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import type {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
13
|
+
export * from "./validator";
|
|
14
|
+
export * from "./handler";
|
|
15
|
+
export * from "./client";
|
|
16
|
+
export * from "./normalize";
|
|
17
|
+
export * from "./registry";
|
|
18
|
+
export * from "./client-safe";
|
|
19
|
+
export * from "./protection";
|
|
20
|
+
|
|
21
|
+
import type { ContractDefinition, ContractInstance, ContractSchema } from "./schema";
|
|
22
|
+
import type { ContractHandlers, RouteDefinition } from "./handler";
|
|
23
|
+
import { defineHandler, defineRoute } from "./handler";
|
|
24
|
+
import { createClient, contractFetch, type ClientOptions } from "./client";
|
|
25
|
+
import { createClientContract } from "./client-safe";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Create a Mandu API Contract
|
|
@@ -123,7 +124,7 @@ export function createContract<T extends ContractDefinition>(definition: T): T &
|
|
|
123
124
|
* Contract-specific Mandu functions
|
|
124
125
|
* Note: Use `ManduContract` to avoid conflict with other Mandu exports
|
|
125
126
|
*/
|
|
126
|
-
export const ManduContract = {
|
|
127
|
+
export const ManduContract = {
|
|
127
128
|
/**
|
|
128
129
|
* Create a typed Contract
|
|
129
130
|
* Contract 스키마 정의 및 타입 추론
|
|
@@ -165,9 +166,9 @@ export const ManduContract = {
|
|
|
165
166
|
*/
|
|
166
167
|
route: defineRoute,
|
|
167
168
|
|
|
168
|
-
/**
|
|
169
|
-
* Create a type-safe API client from contract
|
|
170
|
-
* Contract 기반 타입 안전 클라이언트 생성
|
|
169
|
+
/**
|
|
170
|
+
* Create a type-safe API client from contract
|
|
171
|
+
* Contract 기반 타입 안전 클라이언트 생성
|
|
171
172
|
*
|
|
172
173
|
* @example
|
|
173
174
|
* ```typescript
|
|
@@ -180,13 +181,13 @@ export const ManduContract = {
|
|
|
180
181
|
* const newUser = await client.POST({ body: { name: "Alice" } });
|
|
181
182
|
* ```
|
|
182
183
|
*/
|
|
183
|
-
client: createClient,
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Create a client-safe contract
|
|
187
|
-
* Client에서 노출할 스키마만 선택
|
|
188
|
-
*/
|
|
189
|
-
clientContract: createClientContract,
|
|
184
|
+
client: createClient,
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a client-safe contract
|
|
188
|
+
* Client에서 노출할 스키마만 선택
|
|
189
|
+
*/
|
|
190
|
+
clientContract: createClientContract,
|
|
190
191
|
|
|
191
192
|
/**
|
|
192
193
|
* Single type-safe fetch call
|
|
@@ -199,8 +200,8 @@ export const ManduContract = {
|
|
|
199
200
|
* });
|
|
200
201
|
* ```
|
|
201
202
|
*/
|
|
202
|
-
fetch: contractFetch,
|
|
203
|
-
} as const;
|
|
203
|
+
fetch: contractFetch,
|
|
204
|
+
} as const;
|
|
204
205
|
|
|
205
206
|
/**
|
|
206
207
|
* Alias for backward compatibility within contract module
|