@mandujs/cli 0.9.24 → 0.9.43
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 +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/commands/build.ts +50 -22
- package/src/commands/check.ts +16 -18
- package/src/commands/contract.ts +50 -42
- package/src/commands/dev.ts +294 -212
- package/src/commands/doctor.ts +27 -25
- package/src/commands/guard-arch.ts +25 -10
- package/src/commands/init.ts +8 -7
- package/src/commands/monitor.ts +2 -3
- package/src/commands/openapi.ts +107 -48
- package/src/commands/routes.ts +11 -1
- package/src/errors/codes.ts +35 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/messages.ts +143 -0
- package/src/main.ts +103 -157
- package/src/util/bun.ts +6 -0
- package/src/util/manifest.ts +52 -0
- package/src/util/port.ts +71 -0
- package/templates/default/AGENTS.md +96 -0
- package/templates/default/app/globals.css +45 -33
- package/templates/default/package.json +15 -12
- package/templates/default/postcss.config.js +0 -6
- package/templates/default/tailwind.config.ts +0 -64
package/src/commands/dev.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
startServer,
|
|
3
|
-
registerApiHandler,
|
|
4
|
-
registerPageLoader,
|
|
5
|
-
registerPageHandler,
|
|
6
|
-
registerLayoutLoader,
|
|
1
|
+
import {
|
|
2
|
+
startServer,
|
|
3
|
+
registerApiHandler,
|
|
4
|
+
registerPageLoader,
|
|
5
|
+
registerPageHandler,
|
|
6
|
+
registerLayoutLoader,
|
|
7
7
|
startDevBundler,
|
|
8
8
|
createHMRServer,
|
|
9
9
|
needsHydration,
|
|
10
10
|
loadEnv,
|
|
11
|
-
generateManifest,
|
|
12
11
|
watchFSRoutes,
|
|
13
12
|
clearDefaultRegistry,
|
|
14
13
|
createGuardWatcher,
|
|
@@ -17,77 +16,89 @@ import {
|
|
|
17
16
|
formatReportForAgent,
|
|
18
17
|
formatReportAsAgentJSON,
|
|
19
18
|
getPreset,
|
|
19
|
+
validateAndReport,
|
|
20
|
+
isTailwindProject,
|
|
21
|
+
startCSSWatch,
|
|
20
22
|
type RoutesManifest,
|
|
21
23
|
type GuardConfig,
|
|
22
|
-
type GuardPreset,
|
|
23
24
|
type Violation,
|
|
25
|
+
type CSSWatcher,
|
|
24
26
|
} from "@mandujs/core";
|
|
25
|
-
import {
|
|
26
|
-
import { resolveOutputFormat
|
|
27
|
-
import
|
|
28
|
-
|
|
27
|
+
import { resolveFromCwd } from "../util/fs";
|
|
28
|
+
import { resolveOutputFormat } from "../util/output";
|
|
29
|
+
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
30
|
+
import { importFresh } from "../util/bun";
|
|
31
|
+
import { resolveManifest } from "../util/manifest";
|
|
32
|
+
import { resolveAvailablePort } from "../util/port";
|
|
33
|
+
import path from "path";
|
|
34
|
+
|
|
29
35
|
export interface DevOptions {
|
|
30
36
|
port?: number;
|
|
31
|
-
/** HMR 비활성화 */
|
|
32
|
-
noHmr?: boolean;
|
|
33
|
-
/** FS Routes 비활성화 (레거시 모드) */
|
|
34
|
-
legacy?: boolean;
|
|
35
|
-
/** Architecture Guard 활성화 */
|
|
36
|
-
guard?: boolean;
|
|
37
|
-
/** Guard 프리셋 */
|
|
38
|
-
guardPreset?: GuardPreset;
|
|
39
|
-
/** Guard 출력 형식 */
|
|
40
|
-
guardFormat?: OutputFormat;
|
|
41
37
|
}
|
|
42
|
-
|
|
38
|
+
|
|
43
39
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
44
40
|
const rootDir = resolveFromCwd(".");
|
|
41
|
+
const config = await validateAndReport(rootDir);
|
|
42
|
+
|
|
43
|
+
if (!config) {
|
|
44
|
+
printCLIError(CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const serverConfig = config.server ?? {};
|
|
49
|
+
const devConfig = config.dev ?? {};
|
|
50
|
+
const guardConfigFromFile = config.guard ?? {};
|
|
51
|
+
const HMR_OFFSET = 1;
|
|
52
|
+
|
|
53
|
+
console.log(`🥟 Mandu Dev Server`);
|
|
54
|
+
|
|
55
|
+
// .env 파일 로드
|
|
56
|
+
const envResult = await loadEnv({
|
|
57
|
+
rootDir,
|
|
58
|
+
env: "development",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (envResult.loaded.length > 0) {
|
|
62
|
+
console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
|
|
63
|
+
}
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
console.log(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let manifest = result.manifest;
|
|
79
|
-
console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
|
|
80
|
-
|
|
81
|
-
const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
|
|
82
|
-
const guardPreset = options.guardPreset || "mandu";
|
|
83
|
-
const guardFormat = resolveOutputFormat(options.guardFormat);
|
|
65
|
+
// 라우트 스캔 (FS Routes 우선, 없으면 spec manifest)
|
|
66
|
+
console.log(`📂 라우트 스캔 중...`);
|
|
67
|
+
let manifest: RoutesManifest;
|
|
68
|
+
let enableFsRoutes = false;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
|
|
72
|
+
manifest = resolved.manifest;
|
|
73
|
+
enableFsRoutes = resolved.source === "fs";
|
|
74
|
+
|
|
75
|
+
if (manifest.routes.length === 0) {
|
|
76
|
+
printCLIError(CLI_ERROR_CODES.DEV_NO_ROUTES);
|
|
77
|
+
console.log("💡 app/ 폴더에 page.tsx 파일을 생성하세요:");
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log(" app/page.tsx → /");
|
|
80
|
+
console.log(" app/blog/page.tsx → /blog");
|
|
81
|
+
console.log(" app/api/users/route.ts → /api/users");
|
|
82
|
+
console.log("");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
printCLIError(CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND);
|
|
89
|
+
console.error(error instanceof Error ? error.message : error);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const guardPreset = guardConfigFromFile.preset || "mandu";
|
|
93
|
+
const guardFormat = resolveOutputFormat();
|
|
84
94
|
const guardConfig: GuardConfig | null =
|
|
85
|
-
|
|
95
|
+
guardConfigFromFile.realtime === false
|
|
86
96
|
? null
|
|
87
97
|
: {
|
|
88
98
|
preset: guardPreset,
|
|
89
|
-
srcDir: "src",
|
|
90
|
-
realtime: true,
|
|
99
|
+
srcDir: guardConfigFromFile.srcDir || "src",
|
|
100
|
+
realtime: guardConfigFromFile.realtime ?? true,
|
|
101
|
+
exclude: guardConfigFromFile.exclude,
|
|
91
102
|
realtimeOutput: guardFormat,
|
|
92
103
|
fsRoutes: enableFsRoutes
|
|
93
104
|
? {
|
|
@@ -144,161 +155,231 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
144
155
|
|
|
145
156
|
// Layout 경로 추적 (중복 등록 방지)
|
|
146
157
|
const registeredLayouts = new Set<string>();
|
|
147
|
-
|
|
148
|
-
// 핸들러 등록 함수
|
|
149
|
-
const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
|
|
150
|
-
// 리로드 시 레이아웃 캐시 클리어
|
|
151
|
-
if (isReload) {
|
|
152
|
-
registeredLayouts.clear();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for (const route of manifest.routes) {
|
|
156
|
-
if (route.kind === "api") {
|
|
157
|
-
const modulePath = path.resolve(rootDir, route.module);
|
|
158
|
-
try {
|
|
159
|
-
// 캐시 무효화 (HMR용)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
158
|
+
|
|
159
|
+
// 핸들러 등록 함수
|
|
160
|
+
const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
|
|
161
|
+
// 리로드 시 레이아웃 캐시 클리어
|
|
162
|
+
if (isReload) {
|
|
163
|
+
registeredLayouts.clear();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const route of manifest.routes) {
|
|
167
|
+
if (route.kind === "api") {
|
|
168
|
+
const modulePath = path.resolve(rootDir, route.module);
|
|
169
|
+
try {
|
|
170
|
+
// 캐시 무효화 (HMR용)
|
|
171
|
+
const module = await importFresh(modulePath);
|
|
172
|
+
let handler = module.default || module.handler || module;
|
|
173
|
+
|
|
174
|
+
// ManduFilling 인스턴스를 핸들러 함수로 래핑
|
|
175
|
+
if (handler && typeof handler.handle === 'function') {
|
|
176
|
+
console.log(` 🔄 ManduFilling 래핑: ${route.id}`);
|
|
177
|
+
const filling = handler;
|
|
178
|
+
handler = async (req: Request, params?: Record<string, string>) => {
|
|
179
|
+
return filling.handle(req, params);
|
|
180
|
+
};
|
|
181
|
+
} else {
|
|
182
|
+
console.log(` ⚠️ 핸들러 타입: ${typeof handler}, handle: ${typeof handler?.handle}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
registerApiHandler(route.id, handler);
|
|
186
|
+
console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
|
|
189
|
+
}
|
|
190
|
+
} else if (route.kind === "page" && route.componentModule) {
|
|
191
|
+
const componentPath = path.resolve(rootDir, route.componentModule);
|
|
192
|
+
const isIsland = needsHydration(route);
|
|
193
|
+
const hasLayout = route.layoutChain && route.layoutChain.length > 0;
|
|
194
|
+
|
|
195
|
+
// Layout 로더 등록
|
|
196
|
+
if (route.layoutChain) {
|
|
197
|
+
for (const layoutPath of route.layoutChain) {
|
|
198
|
+
if (!registeredLayouts.has(layoutPath)) {
|
|
199
|
+
const absLayoutPath = path.resolve(rootDir, layoutPath);
|
|
200
|
+
registerLayoutLoader(layoutPath, async () => {
|
|
201
|
+
// 캐시 무효화 (HMR용)
|
|
202
|
+
return importFresh(absLayoutPath);
|
|
203
|
+
});
|
|
204
|
+
registeredLayouts.add(layoutPath);
|
|
205
|
+
console.log(` 🎨 Layout: ${layoutPath}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
|
|
211
|
+
if (route.slotModule) {
|
|
212
|
+
registerPageHandler(route.id, async () => {
|
|
213
|
+
const module = await importFresh(componentPath);
|
|
214
|
+
return module.default;
|
|
215
|
+
});
|
|
216
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
|
|
217
|
+
} else {
|
|
218
|
+
registerPageLoader(route.id, () => importFresh(componentPath));
|
|
219
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// 초기 핸들러 등록
|
|
226
|
+
await registerHandlers(manifest);
|
|
227
|
+
console.log("");
|
|
228
|
+
|
|
229
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
230
|
+
const desiredPort =
|
|
231
|
+
options.port ??
|
|
232
|
+
(envPort && Number.isFinite(envPort) ? envPort : undefined) ??
|
|
233
|
+
serverConfig.port ??
|
|
234
|
+
3333;
|
|
235
|
+
|
|
217
236
|
const hasIslands = manifest.routes.some(
|
|
218
237
|
(r) => r.kind === "page" && r.clientModule && needsHydration(r)
|
|
219
238
|
);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
239
|
+
const hmrEnabled = devConfig.hmr ?? true;
|
|
240
|
+
|
|
241
|
+
const { port } = await resolveAvailablePort(desiredPort, {
|
|
242
|
+
hostname: serverConfig.hostname,
|
|
243
|
+
offsets: hasIslands && hmrEnabled ? [0, HMR_OFFSET] : [0],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (port !== desiredPort) {
|
|
247
|
+
console.warn(`⚠️ Port ${desiredPort} is in use. Using ${port} instead.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// HMR 서버 시작 (클라이언트 슬롯이 있는 경우)
|
|
251
|
+
let hmrServer: ReturnType<typeof createHMRServer> | null = null;
|
|
252
|
+
let devBundler: Awaited<ReturnType<typeof startDevBundler>> | null = null;
|
|
253
|
+
let cssWatcher: CSSWatcher | null = null;
|
|
254
|
+
|
|
255
|
+
// CSS 빌드 시작 (Tailwind v4 감지 시에만)
|
|
256
|
+
const hasTailwind = await isTailwindProject(rootDir);
|
|
257
|
+
if (hasTailwind) {
|
|
258
|
+
cssWatcher = await startCSSWatch({
|
|
259
|
+
rootDir,
|
|
260
|
+
watch: true,
|
|
261
|
+
onBuild: (result) => {
|
|
262
|
+
if (result.success && hmrServer) {
|
|
263
|
+
// cssWatcher.serverPath 사용 (경로 일관성)
|
|
264
|
+
hmrServer.broadcast({
|
|
265
|
+
type: "css-update",
|
|
266
|
+
data: {
|
|
267
|
+
cssPath: cssWatcher?.serverPath || "/.mandu/client/globals.css",
|
|
268
|
+
timestamp: Date.now(),
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
onError: (error) => {
|
|
274
|
+
if (hmrServer) {
|
|
275
|
+
hmrServer.broadcast({
|
|
276
|
+
type: "error",
|
|
277
|
+
data: {
|
|
278
|
+
message: `CSS Error: ${error.message}`,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (hasIslands && hmrEnabled) {
|
|
287
|
+
// HMR 서버 시작
|
|
288
|
+
hmrServer = createHMRServer(port);
|
|
289
|
+
|
|
290
|
+
// Dev 번들러 시작 (파일 감시)
|
|
291
|
+
devBundler = await startDevBundler({
|
|
292
|
+
rootDir,
|
|
293
|
+
manifest,
|
|
294
|
+
watchDirs: devConfig.watchDirs,
|
|
295
|
+
onRebuild: (result) => {
|
|
296
|
+
if (result.success) {
|
|
297
|
+
if (result.routeId === "*") {
|
|
298
|
+
hmrServer?.broadcast({
|
|
299
|
+
type: "reload",
|
|
300
|
+
data: {
|
|
301
|
+
timestamp: Date.now(),
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
hmrServer?.broadcast({
|
|
306
|
+
type: "island-update",
|
|
307
|
+
data: {
|
|
308
|
+
routeId: result.routeId,
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
hmrServer?.broadcast({
|
|
315
|
+
type: "error",
|
|
316
|
+
data: {
|
|
317
|
+
routeId: result.routeId,
|
|
318
|
+
message: result.error,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
onError: (error, routeId) => {
|
|
324
|
+
hmrServer?.broadcast({
|
|
325
|
+
type: "error",
|
|
326
|
+
data: {
|
|
327
|
+
routeId,
|
|
328
|
+
message: error.message,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 메인 서버 시작
|
|
336
|
+
const server = startServer(manifest, {
|
|
337
|
+
port,
|
|
338
|
+
hostname: serverConfig.hostname,
|
|
339
|
+
rootDir,
|
|
340
|
+
isDev: true,
|
|
341
|
+
hmrPort: hmrServer ? port : undefined,
|
|
342
|
+
bundleManifest: devBundler?.initialBuild.manifest,
|
|
343
|
+
cors: serverConfig.cors,
|
|
344
|
+
streaming: serverConfig.streaming,
|
|
345
|
+
// Tailwind 감지 시에만 CSS 링크 주입
|
|
346
|
+
cssPath: hasTailwind ? cssWatcher?.serverPath : false,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const actualPort = server.server.port ?? port;
|
|
350
|
+
if (actualPort !== port) {
|
|
351
|
+
if (hmrServer) {
|
|
352
|
+
hmrServer.close();
|
|
353
|
+
hmrServer = createHMRServer(actualPort);
|
|
354
|
+
server.registry.settings.hmrPort = actualPort;
|
|
355
|
+
console.log(`🔁 HMR port updated: ${actualPort + HMR_OFFSET}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
278
359
|
// FS Routes 실시간 감시
|
|
279
360
|
const routesWatcher = await watchFSRoutes(rootDir, {
|
|
280
361
|
skipLegacy: true,
|
|
281
362
|
onChange: async (result) => {
|
|
282
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
283
|
-
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
284
|
-
|
|
285
|
-
// 레지스트리 클리어 (layout 캐시 포함)
|
|
286
|
-
clearDefaultRegistry();
|
|
287
|
-
|
|
288
|
-
// 새 매니페스트로 서버 업데이트
|
|
289
|
-
manifest = result.manifest;
|
|
290
|
-
console.log(` 📋 라우트: ${manifest.routes.length}개`);
|
|
291
|
-
|
|
292
|
-
// 라우트 재등록 (isReload = true)
|
|
293
|
-
await registerHandlers(manifest, true);
|
|
294
|
-
|
|
295
|
-
// HMR 브로드캐스트 (전체 리로드)
|
|
296
|
-
if (hmrServer) {
|
|
297
|
-
hmrServer.broadcast({
|
|
298
|
-
type: "reload",
|
|
299
|
-
data: { timestamp: Date.now() },
|
|
300
|
-
});
|
|
301
|
-
}
|
|
363
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
364
|
+
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
365
|
+
|
|
366
|
+
// 레지스트리 클리어 (layout 캐시 포함)
|
|
367
|
+
clearDefaultRegistry();
|
|
368
|
+
|
|
369
|
+
// 새 매니페스트로 서버 업데이트
|
|
370
|
+
manifest = result.manifest;
|
|
371
|
+
console.log(` 📋 라우트: ${manifest.routes.length}개`);
|
|
372
|
+
|
|
373
|
+
// 라우트 재등록 (isReload = true)
|
|
374
|
+
await registerHandlers(manifest, true);
|
|
375
|
+
|
|
376
|
+
// HMR 브로드캐스트 (전체 리로드)
|
|
377
|
+
if (hmrServer) {
|
|
378
|
+
hmrServer.broadcast({
|
|
379
|
+
type: "reload",
|
|
380
|
+
data: { timestamp: Date.now() },
|
|
381
|
+
});
|
|
382
|
+
}
|
|
302
383
|
},
|
|
303
384
|
});
|
|
304
385
|
|
|
@@ -312,6 +393,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
312
393
|
server.stop();
|
|
313
394
|
devBundler?.close();
|
|
314
395
|
hmrServer?.close();
|
|
396
|
+
cssWatcher?.close();
|
|
315
397
|
routesWatcher.close();
|
|
316
398
|
archGuardWatcher?.close();
|
|
317
399
|
process.exit(0);
|
package/src/commands/doctor.ts
CHANGED
|
@@ -15,20 +15,22 @@ import {
|
|
|
15
15
|
getBrain,
|
|
16
16
|
} from "../../../core/src/index";
|
|
17
17
|
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
18
|
-
import path from "path";
|
|
18
|
+
import path from "path";
|
|
19
19
|
import fs from "fs/promises";
|
|
20
20
|
|
|
21
|
-
export interface DoctorOptions {
|
|
22
|
-
/** Output format: console, json, or markdown */
|
|
23
|
-
format?: "console" | "json" | "markdown";
|
|
21
|
+
export interface DoctorOptions {
|
|
22
|
+
/** Output format: console, json, or markdown */
|
|
23
|
+
format?: "console" | "json" | "markdown";
|
|
24
24
|
/** Whether to use LLM for enhanced analysis */
|
|
25
25
|
useLLM?: boolean;
|
|
26
26
|
/** Output file path (for json/markdown formats) */
|
|
27
|
-
output?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
31
|
-
const { format
|
|
27
|
+
output?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
31
|
+
const { format, useLLM = true, output } = options;
|
|
32
|
+
const inferredFormat = format ?? (output ? (path.extname(output).toLowerCase() === ".json" ? "json" : "markdown") : undefined);
|
|
33
|
+
const resolvedFormat = inferredFormat ?? "console";
|
|
32
34
|
|
|
33
35
|
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
34
36
|
const rootDir = getRootDir();
|
|
@@ -80,12 +82,12 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
80
82
|
});
|
|
81
83
|
|
|
82
84
|
// Output based on format
|
|
83
|
-
switch (
|
|
84
|
-
case "console":
|
|
85
|
-
printDoctorReport(analysis);
|
|
86
|
-
break;
|
|
87
|
-
|
|
88
|
-
case "json": {
|
|
85
|
+
switch (resolvedFormat) {
|
|
86
|
+
case "console":
|
|
87
|
+
printDoctorReport(analysis);
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case "json": {
|
|
89
91
|
const json = JSON.stringify(
|
|
90
92
|
{
|
|
91
93
|
summary: analysis.summary,
|
|
@@ -104,10 +106,10 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
104
106
|
} else {
|
|
105
107
|
console.log(json);
|
|
106
108
|
}
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case "markdown": {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "markdown": {
|
|
111
113
|
const md = generateDoctorMarkdownReport(analysis);
|
|
112
114
|
|
|
113
115
|
if (output) {
|
|
@@ -116,9 +118,9 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
116
118
|
} else {
|
|
117
119
|
console.log(md);
|
|
118
120
|
}
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|