@mandujs/core 0.13.0 → 0.13.2
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 +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- 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 +9 -0
- package/src/config/validate.ts +12 -0
- 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/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- 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 +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- 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/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 +6 -1
- 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/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- 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 +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- 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/config/watcher.ts
CHANGED
|
@@ -1,311 +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
|
-
}
|
|
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
|
+
}
|