@mandujs/cli 0.9.45 β†’ 0.10.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.
@@ -1,440 +1,486 @@
1
- import {
2
- startServer,
3
- registerApiHandler,
4
- registerPageLoader,
5
- registerPageHandler,
6
- registerLayoutLoader,
7
- startDevBundler,
8
- createHMRServer,
9
- needsHydration,
10
- loadEnv,
11
- watchFSRoutes,
12
- clearDefaultRegistry,
13
- createGuardWatcher,
14
- checkDirectory,
15
- printReport,
16
- formatReportForAgent,
17
- formatReportAsAgentJSON,
18
- getPreset,
19
- validateAndReport,
20
- isTailwindProject,
21
- startCSSWatch,
22
- type RoutesManifest,
23
- type GuardConfig,
24
- type Violation,
25
- type CSSWatcher,
26
- } from "@mandujs/core";
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
-
35
- export interface DevOptions {
36
- port?: number;
37
- }
38
-
39
- export async function dev(options: DevOptions = {}): Promise<void> {
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
- }
64
-
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();
94
- const guardConfig: GuardConfig | null =
95
- guardConfigFromFile.realtime === false
96
- ? null
97
- : {
98
- preset: guardPreset,
99
- srcDir: guardConfigFromFile.srcDir || "src",
100
- realtime: guardConfigFromFile.realtime ?? true,
101
- exclude: guardConfigFromFile.exclude,
102
- realtimeOutput: guardFormat,
103
- fsRoutes: enableFsRoutes
104
- ? {
105
- noPageToPage: true,
106
- pageCanImport: [
107
- "client/pages",
108
- "client/widgets",
109
- "client/features",
110
- "client/entities",
111
- "client/shared",
112
- "shared/contracts",
113
- "shared/types",
114
- "shared/utils/client",
115
- ],
116
- layoutCanImport: [
117
- "client/app",
118
- "client/widgets",
119
- "client/shared",
120
- "shared/contracts",
121
- "shared/types",
122
- "shared/utils/client",
123
- ],
124
- routeCanImport: [
125
- "server/api",
126
- "server/application",
127
- "server/domain",
128
- "server/infra",
129
- "server/core",
130
- "shared/contracts",
131
- "shared/schema",
132
- "shared/types",
133
- "shared/utils/client",
134
- "shared/utils/server",
135
- "shared/env",
136
- ],
137
- }
138
- : undefined,
139
- };
140
-
141
- if (guardConfig) {
142
- const preflightReport = await checkDirectory(guardConfig, rootDir);
143
- if (preflightReport.bySeverity.error > 0) {
144
- if (guardFormat === "json") {
145
- console.log(formatReportAsAgentJSON(preflightReport, guardPreset));
146
- } else if (guardFormat === "agent") {
147
- console.log(formatReportForAgent(preflightReport, guardPreset));
148
- } else {
149
- printReport(preflightReport, getPreset(guardPreset).hierarchy);
150
- }
151
- console.error("\n❌ Architecture Guard failed. Fix errors before starting dev server.");
152
- process.exit(1);
153
- }
154
- }
155
-
156
- // Layout 경둜 좔적 (쀑볡 등둝 λ°©μ§€)
157
- const registeredLayouts = new Set<string>();
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
-
236
- const hasIslands = manifest.routes.some(
237
- (r) => r.kind === "page" && r.clientModule && needsHydration(r)
238
- );
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
-
359
- // FS Routes μ‹€μ‹œκ°„ κ°μ‹œ
360
- const routesWatcher = await watchFSRoutes(rootDir, {
361
- skipLegacy: true,
362
- onChange: async (result) => {
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
- }
383
- },
384
- });
385
-
386
- // Architecture Guard μ‹€μ‹œκ°„ κ°μ‹œ (선택적)
387
- let archGuardWatcher: ReturnType<typeof createGuardWatcher> | null = null;
388
- let guardFailed = false;
389
-
390
- // 정리 ν•¨μˆ˜
391
- const cleanup = () => {
392
- console.log("\nπŸ›‘ μ„œλ²„ μ’…λ£Œ 쀑...");
393
- server.stop();
394
- devBundler?.close();
395
- hmrServer?.close();
396
- cssWatcher?.close();
397
- routesWatcher.close();
398
- archGuardWatcher?.close();
399
- process.exit(0);
400
- };
401
-
402
- const stopOnGuardError = (violation: Violation) => {
403
- if (violation.severity !== "error" || guardFailed) {
404
- return;
405
- }
406
- guardFailed = true;
407
- console.error("\n❌ Architecture Guard violation detected. Stopping dev server.");
408
- cleanup();
409
- };
410
-
411
- if (guardConfig) {
412
- console.log(`πŸ›‘οΈ Architecture Guard ν™œμ„±ν™” (${guardPreset})`);
413
-
414
- archGuardWatcher = createGuardWatcher({
415
- config: guardConfig,
416
- rootDir,
417
- onViolation: stopOnGuardError,
418
- onFileAnalyzed: (analysis, violations) => {
419
- if (violations.length > 0) {
420
- // HMR μ—λŸ¬λ‘œ λΈŒλ‘œλ“œμΊμŠ€νŠΈ
421
- hmrServer?.broadcast({
422
- type: "guard-violation",
423
- data: {
424
- file: analysis.filePath,
425
- violations: violations.map((v) => ({
426
- line: v.line,
427
- message: `${v.fromLayer} β†’ ${v.toLayer}: ${v.ruleDescription}`,
428
- })),
429
- },
430
- });
431
- }
432
- },
433
- });
434
-
435
- archGuardWatcher.start();
436
- }
437
-
438
- process.on("SIGINT", cleanup);
439
- process.on("SIGTERM", cleanup);
440
- }
1
+ import {
2
+ startServer,
3
+ registerApiHandler,
4
+ registerPageLoader,
5
+ registerPageHandler,
6
+ registerLayoutLoader,
7
+ startDevBundler,
8
+ createHMRServer,
9
+ needsHydration,
10
+ loadEnv,
11
+ watchFSRoutes,
12
+ clearDefaultRegistry,
13
+ createGuardWatcher,
14
+ checkDirectory,
15
+ printReport,
16
+ formatReportForAgent,
17
+ formatReportAsAgentJSON,
18
+ getPreset,
19
+ validateAndReport,
20
+ isTailwindProject,
21
+ startCSSWatch,
22
+ readLockfile,
23
+ readMcpConfig,
24
+ validateWithPolicy,
25
+ detectMode,
26
+ formatPolicyAction,
27
+ formatValidationResult,
28
+ type RoutesManifest,
29
+ type GuardConfig,
30
+ type Violation,
31
+ type CSSWatcher,
32
+ } from "@mandujs/core";
33
+ import { resolveFromCwd } from "../util/fs";
34
+ import { resolveOutputFormat } from "../util/output";
35
+ import { CLI_ERROR_CODES, printCLIError } from "../errors";
36
+ import { importFresh } from "../util/bun";
37
+ import { resolveManifest } from "../util/manifest";
38
+ import { resolveAvailablePort } from "../util/port";
39
+ import path from "path";
40
+
41
+ export interface DevOptions {
42
+ port?: number;
43
+ }
44
+
45
+ export async function dev(options: DevOptions = {}): Promise<void> {
46
+ const rootDir = resolveFromCwd(".");
47
+ const config = await validateAndReport(rootDir);
48
+
49
+ if (!config) {
50
+ printCLIError(CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED);
51
+ process.exit(1);
52
+ }
53
+
54
+ // Lockfile 검증 (μ„€μ • 무결성)
55
+ const lockfile = await readLockfile(rootDir);
56
+ let mcpConfig: Record<string, unknown> | null = null;
57
+ try {
58
+ mcpConfig = await readMcpConfig(rootDir);
59
+ } catch (error) {
60
+ console.warn(
61
+ `⚠️ MCP μ„€μ • λ‘œλ“œ μ‹€νŒ¨: ${error instanceof Error ? error.message : String(error)}`
62
+ );
63
+ }
64
+ const { result: lockResult, action, bypassed } = validateWithPolicy(
65
+ config,
66
+ lockfile,
67
+ detectMode(),
68
+ mcpConfig
69
+ );
70
+
71
+ if (action === "block") {
72
+ console.error("πŸ›‘ μ„œλ²„ μ‹œμž‘ 차단: Lockfile 뢈일치");
73
+ console.error(" 섀정이 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ˜λ„ν•œ 변경이라면:");
74
+ console.error(" $ mandu lock");
75
+ console.error("");
76
+ console.error(" λ³€κ²½ 사항 확인:");
77
+ console.error(" $ mandu lock --diff");
78
+ if (lockResult) {
79
+ console.error("");
80
+ console.error(formatValidationResult(lockResult));
81
+ }
82
+ process.exit(1);
83
+ }
84
+
85
+ const serverConfig = config.server ?? {};
86
+ const devConfig = config.dev ?? {};
87
+ const guardConfigFromFile = config.guard ?? {};
88
+ const HMR_OFFSET = 1;
89
+
90
+ console.log(`πŸ₯Ÿ Mandu Dev Server`);
91
+
92
+ // Lockfile μƒνƒœ 좜λ ₯
93
+ if (action === "warn") {
94
+ console.log(`⚠️ ${formatPolicyAction(action, bypassed)}`);
95
+ } else if (lockfile && lockResult?.valid) {
96
+ console.log(`πŸ”’ μ„€μ • 무결성 확인됨 (${lockResult.currentHash?.slice(0, 8)})`);
97
+ } else if (!lockfile) {
98
+ console.log(`πŸ’‘ Lockfile μ—†μŒ - 'mandu lock'으둜 생성 ꢌμž₯`);
99
+ }
100
+
101
+ // .env 파일 λ‘œλ“œ
102
+ const envResult = await loadEnv({
103
+ rootDir,
104
+ env: "development",
105
+ });
106
+
107
+ if (envResult.loaded.length > 0) {
108
+ console.log(`πŸ” ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ: ${envResult.loaded.join(", ")}`);
109
+ }
110
+
111
+ // 라우트 μŠ€μΊ” (FS Routes μš°μ„ , μ—†μœΌλ©΄ spec manifest)
112
+ console.log(`πŸ“‚ 라우트 μŠ€μΊ” 쀑...`);
113
+ let manifest: RoutesManifest;
114
+ let enableFsRoutes = false;
115
+
116
+ try {
117
+ const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
118
+ manifest = resolved.manifest;
119
+ enableFsRoutes = resolved.source === "fs";
120
+
121
+ if (manifest.routes.length === 0) {
122
+ printCLIError(CLI_ERROR_CODES.DEV_NO_ROUTES);
123
+ console.log("πŸ’‘ app/ 폴더에 page.tsx νŒŒμΌμ„ μƒμ„±ν•˜μ„Έμš”:");
124
+ console.log("");
125
+ console.log(" app/page.tsx β†’ /");
126
+ console.log(" app/blog/page.tsx β†’ /blog");
127
+ console.log(" app/api/users/route.ts β†’ /api/users");
128
+ console.log("");
129
+ process.exit(1);
130
+ }
131
+
132
+ console.log(`βœ… ${manifest.routes.length}개 라우트 발견\n`);
133
+ } catch (error) {
134
+ printCLIError(CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND);
135
+ console.error(error instanceof Error ? error.message : error);
136
+ process.exit(1);
137
+ }
138
+ const guardPreset = guardConfigFromFile.preset || "mandu";
139
+ const guardFormat = resolveOutputFormat();
140
+ const guardConfig: GuardConfig | null =
141
+ guardConfigFromFile.realtime === false
142
+ ? null
143
+ : {
144
+ preset: guardPreset,
145
+ srcDir: guardConfigFromFile.srcDir || "src",
146
+ realtime: guardConfigFromFile.realtime ?? true,
147
+ exclude: guardConfigFromFile.exclude,
148
+ realtimeOutput: guardFormat,
149
+ fsRoutes: enableFsRoutes
150
+ ? {
151
+ noPageToPage: true,
152
+ pageCanImport: [
153
+ "client/pages",
154
+ "client/widgets",
155
+ "client/features",
156
+ "client/entities",
157
+ "client/shared",
158
+ "shared/contracts",
159
+ "shared/types",
160
+ "shared/utils/client",
161
+ ],
162
+ layoutCanImport: [
163
+ "client/app",
164
+ "client/widgets",
165
+ "client/shared",
166
+ "shared/contracts",
167
+ "shared/types",
168
+ "shared/utils/client",
169
+ ],
170
+ routeCanImport: [
171
+ "server/api",
172
+ "server/application",
173
+ "server/domain",
174
+ "server/infra",
175
+ "server/core",
176
+ "shared/contracts",
177
+ "shared/schema",
178
+ "shared/types",
179
+ "shared/utils/client",
180
+ "shared/utils/server",
181
+ "shared/env",
182
+ ],
183
+ }
184
+ : undefined,
185
+ };
186
+
187
+ if (guardConfig) {
188
+ const preflightReport = await checkDirectory(guardConfig, rootDir);
189
+ if (preflightReport.bySeverity.error > 0) {
190
+ if (guardFormat === "json") {
191
+ console.log(formatReportAsAgentJSON(preflightReport, guardPreset));
192
+ } else if (guardFormat === "agent") {
193
+ console.log(formatReportForAgent(preflightReport, guardPreset));
194
+ } else {
195
+ printReport(preflightReport, getPreset(guardPreset).hierarchy);
196
+ }
197
+ console.error("\n❌ Architecture Guard failed. Fix errors before starting dev server.");
198
+ process.exit(1);
199
+ }
200
+ }
201
+
202
+ // Layout 경둜 좔적 (쀑볡 등둝 λ°©μ§€)
203
+ const registeredLayouts = new Set<string>();
204
+
205
+ // ν•Έλ“€λŸ¬ 등둝 ν•¨μˆ˜
206
+ const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
207
+ // λ¦¬λ‘œλ“œ μ‹œ λ ˆμ΄μ•„μ›ƒ μΊμ‹œ 클리어
208
+ if (isReload) {
209
+ registeredLayouts.clear();
210
+ }
211
+
212
+ for (const route of manifest.routes) {
213
+ if (route.kind === "api") {
214
+ const modulePath = path.resolve(rootDir, route.module);
215
+ try {
216
+ // μΊμ‹œ λ¬΄νš¨ν™” (HMR용)
217
+ const module = await importFresh(modulePath);
218
+ let handler = module.default || module.handler || module;
219
+
220
+ // ManduFilling μΈμŠ€ν„΄μŠ€λ₯Ό ν•Έλ“€λŸ¬ ν•¨μˆ˜λ‘œ λž˜ν•‘
221
+ if (handler && typeof handler.handle === 'function') {
222
+ console.log(` πŸ”„ ManduFilling λž˜ν•‘: ${route.id}`);
223
+ const filling = handler;
224
+ handler = async (req: Request, params?: Record<string, string>) => {
225
+ return filling.handle(req, params);
226
+ };
227
+ } else {
228
+ console.log(` ⚠️ ν•Έλ“€λŸ¬ νƒ€μž…: ${typeof handler}, handle: ${typeof handler?.handle}`);
229
+ }
230
+
231
+ registerApiHandler(route.id, handler);
232
+ console.log(` πŸ“‘ API: ${route.pattern} -> ${route.id}`);
233
+ } catch (error) {
234
+ console.error(` ❌ API ν•Έλ“€λŸ¬ λ‘œλ“œ μ‹€νŒ¨: ${route.id}`, error);
235
+ }
236
+ } else if (route.kind === "page" && route.componentModule) {
237
+ const componentPath = path.resolve(rootDir, route.componentModule);
238
+ const isIsland = needsHydration(route);
239
+ const hasLayout = route.layoutChain && route.layoutChain.length > 0;
240
+
241
+ // Layout λ‘œλ” 등둝
242
+ if (route.layoutChain) {
243
+ for (const layoutPath of route.layoutChain) {
244
+ if (!registeredLayouts.has(layoutPath)) {
245
+ const absLayoutPath = path.resolve(rootDir, layoutPath);
246
+ registerLayoutLoader(layoutPath, async () => {
247
+ // μΊμ‹œ λ¬΄νš¨ν™” (HMR용)
248
+ return importFresh(absLayoutPath);
249
+ });
250
+ registeredLayouts.add(layoutPath);
251
+ console.log(` 🎨 Layout: ${layoutPath}`);
252
+ }
253
+ }
254
+ }
255
+
256
+ // slotModule이 있으면 PageHandler μ‚¬μš© (filling.loader 지원)
257
+ if (route.slotModule) {
258
+ registerPageHandler(route.id, async () => {
259
+ const module = await importFresh(componentPath);
260
+ return module.default;
261
+ });
262
+ console.log(` πŸ“„ Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
263
+ } else {
264
+ registerPageLoader(route.id, () => importFresh(componentPath));
265
+ console.log(` πŸ“„ Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
266
+ }
267
+ }
268
+ }
269
+ };
270
+
271
+ // 초기 ν•Έλ“€λŸ¬ 등둝
272
+ await registerHandlers(manifest);
273
+ console.log("");
274
+
275
+ const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
276
+ const desiredPort =
277
+ options.port ??
278
+ (envPort && Number.isFinite(envPort) ? envPort : undefined) ??
279
+ serverConfig.port ??
280
+ 3333;
281
+
282
+ const hasIslands = manifest.routes.some(
283
+ (r) => r.kind === "page" && r.clientModule && needsHydration(r)
284
+ );
285
+ const hmrEnabled = devConfig.hmr ?? true;
286
+
287
+ const { port } = await resolveAvailablePort(desiredPort, {
288
+ hostname: serverConfig.hostname,
289
+ offsets: hasIslands && hmrEnabled ? [0, HMR_OFFSET] : [0],
290
+ });
291
+
292
+ if (port !== desiredPort) {
293
+ console.warn(`⚠️ Port ${desiredPort} is in use. Using ${port} instead.`);
294
+ }
295
+
296
+ // HMR μ„œλ²„ μ‹œμž‘ (ν΄λΌμ΄μ–ΈνŠΈ 슬둯이 μžˆλŠ” 경우)
297
+ let hmrServer: ReturnType<typeof createHMRServer> | null = null;
298
+ let devBundler: Awaited<ReturnType<typeof startDevBundler>> | null = null;
299
+ let cssWatcher: CSSWatcher | null = null;
300
+
301
+ // CSS λΉŒλ“œ μ‹œμž‘ (Tailwind v4 감지 μ‹œμ—λ§Œ)
302
+ const hasTailwind = await isTailwindProject(rootDir);
303
+ if (hasTailwind) {
304
+ cssWatcher = await startCSSWatch({
305
+ rootDir,
306
+ watch: true,
307
+ onBuild: (result) => {
308
+ if (result.success && hmrServer) {
309
+ // cssWatcher.serverPath μ‚¬μš© (경둜 일관성)
310
+ hmrServer.broadcast({
311
+ type: "css-update",
312
+ data: {
313
+ cssPath: cssWatcher?.serverPath || "/.mandu/client/globals.css",
314
+ timestamp: Date.now(),
315
+ },
316
+ });
317
+ }
318
+ },
319
+ onError: (error) => {
320
+ if (hmrServer) {
321
+ hmrServer.broadcast({
322
+ type: "error",
323
+ data: {
324
+ message: `CSS Error: ${error.message}`,
325
+ },
326
+ });
327
+ }
328
+ },
329
+ });
330
+ }
331
+
332
+ if (hasIslands && hmrEnabled) {
333
+ // HMR μ„œλ²„ μ‹œμž‘
334
+ hmrServer = createHMRServer(port);
335
+
336
+ // Dev λ²ˆλ“€λŸ¬ μ‹œμž‘ (파일 κ°μ‹œ)
337
+ devBundler = await startDevBundler({
338
+ rootDir,
339
+ manifest,
340
+ watchDirs: devConfig.watchDirs,
341
+ onRebuild: (result) => {
342
+ if (result.success) {
343
+ if (result.routeId === "*") {
344
+ hmrServer?.broadcast({
345
+ type: "reload",
346
+ data: {
347
+ timestamp: Date.now(),
348
+ },
349
+ });
350
+ } else {
351
+ hmrServer?.broadcast({
352
+ type: "island-update",
353
+ data: {
354
+ routeId: result.routeId,
355
+ timestamp: Date.now(),
356
+ },
357
+ });
358
+ }
359
+ } else {
360
+ hmrServer?.broadcast({
361
+ type: "error",
362
+ data: {
363
+ routeId: result.routeId,
364
+ message: result.error,
365
+ },
366
+ });
367
+ }
368
+ },
369
+ onError: (error, routeId) => {
370
+ hmrServer?.broadcast({
371
+ type: "error",
372
+ data: {
373
+ routeId,
374
+ message: error.message,
375
+ },
376
+ });
377
+ },
378
+ });
379
+ }
380
+
381
+ // 메인 μ„œλ²„ μ‹œμž‘
382
+ const server = startServer(manifest, {
383
+ port,
384
+ hostname: serverConfig.hostname,
385
+ rootDir,
386
+ isDev: true,
387
+ hmrPort: hmrServer ? port : undefined,
388
+ bundleManifest: devBundler?.initialBuild.manifest,
389
+ cors: serverConfig.cors,
390
+ streaming: serverConfig.streaming,
391
+ // Tailwind 감지 μ‹œμ—λ§Œ CSS 링크 μ£Όμž…
392
+ cssPath: hasTailwind ? cssWatcher?.serverPath : false,
393
+ });
394
+
395
+ const actualPort = server.server.port ?? port;
396
+ if (actualPort !== port) {
397
+ if (hmrServer) {
398
+ hmrServer.close();
399
+ hmrServer = createHMRServer(actualPort);
400
+ server.registry.settings.hmrPort = actualPort;
401
+ console.log(`πŸ” HMR port updated: ${actualPort + HMR_OFFSET}`);
402
+ }
403
+ }
404
+
405
+ // FS Routes μ‹€μ‹œκ°„ κ°μ‹œ
406
+ const routesWatcher = await watchFSRoutes(rootDir, {
407
+ skipLegacy: true,
408
+ onChange: async (result) => {
409
+ const timestamp = new Date().toLocaleTimeString();
410
+ console.log(`\nπŸ”„ [${timestamp}] 라우트 λ³€κ²½ 감지`);
411
+
412
+ // λ ˆμ§€μŠ€νŠΈλ¦¬ 클리어 (layout μΊμ‹œ 포함)
413
+ clearDefaultRegistry();
414
+
415
+ // μƒˆ λ§€λ‹ˆνŽ˜μŠ€νŠΈλ‘œ μ„œλ²„ μ—…λ°μ΄νŠΈ
416
+ manifest = result.manifest;
417
+ console.log(` πŸ“‹ 라우트: ${manifest.routes.length}개`);
418
+
419
+ // 라우트 μž¬λ“±λ‘ (isReload = true)
420
+ await registerHandlers(manifest, true);
421
+
422
+ // HMR λΈŒλ‘œλ“œμΊμŠ€νŠΈ (전체 λ¦¬λ‘œλ“œ)
423
+ if (hmrServer) {
424
+ hmrServer.broadcast({
425
+ type: "reload",
426
+ data: { timestamp: Date.now() },
427
+ });
428
+ }
429
+ },
430
+ });
431
+
432
+ // Architecture Guard μ‹€μ‹œκ°„ κ°μ‹œ (선택적)
433
+ let archGuardWatcher: ReturnType<typeof createGuardWatcher> | null = null;
434
+ let guardFailed = false;
435
+
436
+ // 정리 ν•¨μˆ˜
437
+ const cleanup = () => {
438
+ console.log("\nπŸ›‘ μ„œλ²„ μ’…λ£Œ 쀑...");
439
+ server.stop();
440
+ devBundler?.close();
441
+ hmrServer?.close();
442
+ cssWatcher?.close();
443
+ routesWatcher.close();
444
+ archGuardWatcher?.close();
445
+ process.exit(0);
446
+ };
447
+
448
+ const stopOnGuardError = (violation: Violation) => {
449
+ if (violation.severity !== "error" || guardFailed) {
450
+ return;
451
+ }
452
+ guardFailed = true;
453
+ console.error("\n❌ Architecture Guard violation detected. Stopping dev server.");
454
+ cleanup();
455
+ };
456
+
457
+ if (guardConfig) {
458
+ console.log(`πŸ›‘οΈ Architecture Guard ν™œμ„±ν™” (${guardPreset})`);
459
+
460
+ archGuardWatcher = createGuardWatcher({
461
+ config: guardConfig,
462
+ rootDir,
463
+ onViolation: stopOnGuardError,
464
+ onFileAnalyzed: (analysis, violations) => {
465
+ if (violations.length > 0) {
466
+ // HMR μ—λŸ¬λ‘œ λΈŒλ‘œλ“œμΊμŠ€νŠΈ
467
+ hmrServer?.broadcast({
468
+ type: "guard-violation",
469
+ data: {
470
+ file: analysis.filePath,
471
+ violations: violations.map((v) => ({
472
+ line: v.line,
473
+ message: `${v.fromLayer} β†’ ${v.toLayer}: ${v.ruleDescription}`,
474
+ })),
475
+ },
476
+ });
477
+ }
478
+ },
479
+ });
480
+
481
+ archGuardWatcher.start();
482
+ }
483
+
484
+ process.on("SIGINT", cleanup);
485
+ process.on("SIGTERM", cleanup);
486
+ }