@mandujs/cli 0.12.2 → 0.13.1
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 +234 -234
- package/README.md +354 -354
- package/package.json +2 -2
- package/src/commands/contract.ts +173 -173
- package/src/commands/dev.ts +8 -68
- package/src/commands/doctor.ts +27 -27
- package/src/commands/guard-arch.ts +303 -303
- package/src/commands/guard-check.ts +3 -3
- package/src/commands/monitor.ts +300 -300
- package/src/commands/openapi.ts +107 -107
- package/src/commands/registry.ts +367 -357
- package/src/commands/routes.ts +228 -228
- package/src/commands/start.ts +184 -0
- package/src/errors/codes.ts +35 -35
- package/src/errors/index.ts +2 -2
- package/src/errors/messages.ts +143 -143
- package/src/hooks/index.ts +17 -17
- package/src/hooks/preaction.ts +256 -256
- package/src/main.ts +37 -34
- package/src/terminal/banner.ts +166 -166
- package/src/terminal/help.ts +306 -306
- package/src/terminal/index.ts +71 -71
- package/src/terminal/output.ts +295 -295
- package/src/terminal/palette.ts +30 -30
- package/src/terminal/progress.ts +327 -327
- package/src/terminal/stream-writer.ts +214 -214
- package/src/terminal/table.ts +354 -354
- package/src/terminal/theme.ts +142 -142
- package/src/util/bun.ts +6 -6
- package/src/util/fs.ts +23 -23
- package/src/util/handlers.ts +96 -0
- package/src/util/manifest.ts +52 -52
- package/src/util/output.ts +22 -22
- package/src/util/port.ts +71 -71
- package/templates/default/AGENTS.md +96 -96
- package/templates/default/app/api/health/route.ts +13 -13
- package/templates/default/app/globals.css +49 -49
- package/templates/default/app/layout.tsx +27 -27
- package/templates/default/app/page.tsx +38 -38
- package/templates/default/package.json +1 -0
- package/templates/default/src/client/shared/lib/utils.ts +16 -16
- package/templates/default/src/client/shared/ui/button.tsx +57 -57
- package/templates/default/src/client/shared/ui/card.tsx +78 -78
- package/templates/default/src/client/shared/ui/index.ts +21 -21
- package/templates/default/src/client/shared/ui/input.tsx +24 -24
- package/templates/default/tests/example.test.ts +58 -58
- package/templates/default/tests/helpers.ts +52 -52
- package/templates/default/tests/setup.ts +9 -9
- package/templates/default/tsconfig.json +12 -14
- package/templates/default/apps/server/main.ts +0 -67
- package/templates/default/apps/web/entry.tsx +0 -35
package/src/commands/routes.ts
CHANGED
|
@@ -1,228 +1,228 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FS Routes CLI Commands
|
|
3
|
-
*
|
|
4
|
-
* 파일 시스템 기반 라우트 관리 명령어
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
scanRoutes,
|
|
9
|
-
generateManifest,
|
|
10
|
-
formatRoutesForCLI,
|
|
11
|
-
watchFSRoutes,
|
|
12
|
-
validateAndReport,
|
|
13
|
-
type GenerateOptions,
|
|
14
|
-
type FSScannerConfig,
|
|
15
|
-
} from "@mandujs/core";
|
|
16
|
-
import { resolveFromCwd } from "../util/fs";
|
|
17
|
-
|
|
18
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
-
// Types
|
|
20
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
-
|
|
22
|
-
export interface RoutesGenerateOptions {
|
|
23
|
-
/** 출력 파일 경로 */
|
|
24
|
-
output?: string;
|
|
25
|
-
/** 상세 출력 */
|
|
26
|
-
verbose?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface RoutesListOptions {
|
|
30
|
-
/** 상세 출력 */
|
|
31
|
-
verbose?: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface RoutesWatchOptions {
|
|
35
|
-
/** 출력 파일 경로 */
|
|
36
|
-
output?: string;
|
|
37
|
-
/** 상세 출력 */
|
|
38
|
-
verbose?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
-
// Commands
|
|
43
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* routes generate - FS Routes 스캔 및 매니페스트 생성
|
|
47
|
-
*/
|
|
48
|
-
export async function routesGenerate(options: RoutesGenerateOptions = {}): Promise<boolean> {
|
|
49
|
-
const rootDir = resolveFromCwd(".");
|
|
50
|
-
const config = await validateAndReport(rootDir);
|
|
51
|
-
if (!config) return false;
|
|
52
|
-
|
|
53
|
-
console.log("🥟 Mandu FS Routes Generate\n");
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const generateOptions: GenerateOptions = {
|
|
57
|
-
scanner: config.fsRoutes,
|
|
58
|
-
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
59
|
-
skipLegacy: true, // 레거시 병합 비활성화
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const result = await generateManifest(rootDir, generateOptions);
|
|
63
|
-
|
|
64
|
-
// 결과 출력
|
|
65
|
-
console.log(`✅ FS Routes 스캔 완료`);
|
|
66
|
-
console.log(` 📋 라우트: ${result.manifest.routes.length}개\n`);
|
|
67
|
-
|
|
68
|
-
// 경고 출력
|
|
69
|
-
if (result.warnings.length > 0) {
|
|
70
|
-
console.log("⚠️ 경고:");
|
|
71
|
-
for (const warning of result.warnings) {
|
|
72
|
-
console.log(` - ${warning}`);
|
|
73
|
-
}
|
|
74
|
-
console.log("");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 라우트 목록 출력
|
|
78
|
-
if (options.verbose) {
|
|
79
|
-
console.log(formatRoutesForCLI(result.manifest));
|
|
80
|
-
console.log("");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 출력 파일 경로
|
|
84
|
-
if (generateOptions.outputPath) {
|
|
85
|
-
console.log(`📁 매니페스트 저장: ${generateOptions.outputPath}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return true;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
console.error("❌ FS Routes 생성 실패:", error instanceof Error ? error.message : error);
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* routes list - 현재 라우트 목록 출력
|
|
97
|
-
*/
|
|
98
|
-
export async function routesList(options: RoutesListOptions = {}): Promise<boolean> {
|
|
99
|
-
const rootDir = resolveFromCwd(".");
|
|
100
|
-
const config = await validateAndReport(rootDir);
|
|
101
|
-
if (!config) return false;
|
|
102
|
-
|
|
103
|
-
console.log("🥟 Mandu Routes List\n");
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const result = await scanRoutes(rootDir, config.fsRoutes);
|
|
107
|
-
|
|
108
|
-
if (result.errors.length > 0) {
|
|
109
|
-
console.log("⚠️ 스캔 경고:");
|
|
110
|
-
for (const error of result.errors) {
|
|
111
|
-
console.log(` - ${error.type}: ${error.message}`);
|
|
112
|
-
}
|
|
113
|
-
console.log("");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (result.routes.length === 0) {
|
|
117
|
-
console.log("📭 라우트가 없습니다.");
|
|
118
|
-
console.log("");
|
|
119
|
-
console.log("💡 app/ 폴더에 page.tsx 또는 route.ts 파일을 생성하세요.");
|
|
120
|
-
console.log("");
|
|
121
|
-
console.log("예시:");
|
|
122
|
-
console.log(" app/page.tsx → /");
|
|
123
|
-
console.log(" app/blog/page.tsx → /blog");
|
|
124
|
-
console.log(" app/api/users/route.ts → /api/users");
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 라우트 목록 출력
|
|
129
|
-
console.log(`📋 라우트 (${result.routes.length}개)`);
|
|
130
|
-
console.log("─".repeat(70));
|
|
131
|
-
|
|
132
|
-
for (const route of result.routes) {
|
|
133
|
-
const icon = route.kind === "page" ? "📄" : "📡";
|
|
134
|
-
const hydration = route.clientModule ? " 🏝️" : "";
|
|
135
|
-
const pattern = route.pattern.padEnd(35);
|
|
136
|
-
const id = route.id;
|
|
137
|
-
|
|
138
|
-
console.log(`${icon} ${pattern} → ${id}${hydration}`);
|
|
139
|
-
|
|
140
|
-
if (options.verbose) {
|
|
141
|
-
console.log(` 📁 ${route.sourceFile}`);
|
|
142
|
-
if (route.clientModule) {
|
|
143
|
-
console.log(` 🏝️ ${route.clientModule}`);
|
|
144
|
-
}
|
|
145
|
-
if (route.layoutChain.length > 0) {
|
|
146
|
-
console.log(` 📐 layouts: ${route.layoutChain.join(" → ")}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log("");
|
|
152
|
-
|
|
153
|
-
// 통계
|
|
154
|
-
console.log("📊 통계");
|
|
155
|
-
console.log(` 페이지: ${result.stats.pageCount}개`);
|
|
156
|
-
console.log(` API: ${result.stats.apiCount}개`);
|
|
157
|
-
console.log(` 레이아웃: ${result.stats.layoutCount}개`);
|
|
158
|
-
console.log(` Island: ${result.stats.islandCount}개`);
|
|
159
|
-
console.log(` 스캔 시간: ${result.stats.scanTime}ms`);
|
|
160
|
-
|
|
161
|
-
return true;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error("❌ 라우트 목록 조회 실패:", error instanceof Error ? error.message : error);
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* routes watch - 실시간 라우트 감시
|
|
170
|
-
*/
|
|
171
|
-
export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boolean> {
|
|
172
|
-
const rootDir = resolveFromCwd(".");
|
|
173
|
-
const config = await validateAndReport(rootDir);
|
|
174
|
-
if (!config) return false;
|
|
175
|
-
|
|
176
|
-
console.log("🥟 Mandu FS Routes Watch\n");
|
|
177
|
-
console.log("👀 라우트 변경 감시 중... (Ctrl+C로 종료)\n");
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
// 초기 스캔
|
|
181
|
-
const initialResult = await generateManifest(rootDir, {
|
|
182
|
-
scanner: config.fsRoutes,
|
|
183
|
-
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
console.log(`✅ 초기 스캔: ${initialResult.manifest.routes.length}개 라우트\n`);
|
|
187
|
-
|
|
188
|
-
// 감시 시작
|
|
189
|
-
const watcher = await watchFSRoutes(rootDir, {
|
|
190
|
-
scanner: config.fsRoutes,
|
|
191
|
-
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
192
|
-
onChange: (result) => {
|
|
193
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
194
|
-
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
195
|
-
console.log(` 📋 총 라우트: ${result.manifest.routes.length}개`);
|
|
196
|
-
|
|
197
|
-
if (result.warnings.length > 0) {
|
|
198
|
-
for (const warning of result.warnings) {
|
|
199
|
-
console.log(` ⚠️ ${warning}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (options.verbose) {
|
|
204
|
-
console.log("");
|
|
205
|
-
console.log(formatRoutesForCLI(result.manifest));
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// 종료 시그널 처리
|
|
211
|
-
const cleanup = () => {
|
|
212
|
-
console.log("\n\n🛑 감시 종료");
|
|
213
|
-
watcher.close();
|
|
214
|
-
process.exit(0);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
process.on("SIGINT", cleanup);
|
|
218
|
-
process.on("SIGTERM", cleanup);
|
|
219
|
-
|
|
220
|
-
// 무한 대기
|
|
221
|
-
await new Promise(() => {});
|
|
222
|
-
|
|
223
|
-
return true;
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error("❌ 라우트 감시 실패:", error instanceof Error ? error.message : error);
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* FS Routes CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* 파일 시스템 기반 라우트 관리 명령어
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
scanRoutes,
|
|
9
|
+
generateManifest,
|
|
10
|
+
formatRoutesForCLI,
|
|
11
|
+
watchFSRoutes,
|
|
12
|
+
validateAndReport,
|
|
13
|
+
type GenerateOptions,
|
|
14
|
+
type FSScannerConfig,
|
|
15
|
+
} from "@mandujs/core";
|
|
16
|
+
import { resolveFromCwd } from "../util/fs";
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Types
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
export interface RoutesGenerateOptions {
|
|
23
|
+
/** 출력 파일 경로 */
|
|
24
|
+
output?: string;
|
|
25
|
+
/** 상세 출력 */
|
|
26
|
+
verbose?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RoutesListOptions {
|
|
30
|
+
/** 상세 출력 */
|
|
31
|
+
verbose?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RoutesWatchOptions {
|
|
35
|
+
/** 출력 파일 경로 */
|
|
36
|
+
output?: string;
|
|
37
|
+
/** 상세 출력 */
|
|
38
|
+
verbose?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
// Commands
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* routes generate - FS Routes 스캔 및 매니페스트 생성
|
|
47
|
+
*/
|
|
48
|
+
export async function routesGenerate(options: RoutesGenerateOptions = {}): Promise<boolean> {
|
|
49
|
+
const rootDir = resolveFromCwd(".");
|
|
50
|
+
const config = await validateAndReport(rootDir);
|
|
51
|
+
if (!config) return false;
|
|
52
|
+
|
|
53
|
+
console.log("🥟 Mandu FS Routes Generate\n");
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const generateOptions: GenerateOptions = {
|
|
57
|
+
scanner: config.fsRoutes,
|
|
58
|
+
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
59
|
+
skipLegacy: true, // 레거시 병합 비활성화
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = await generateManifest(rootDir, generateOptions);
|
|
63
|
+
|
|
64
|
+
// 결과 출력
|
|
65
|
+
console.log(`✅ FS Routes 스캔 완료`);
|
|
66
|
+
console.log(` 📋 라우트: ${result.manifest.routes.length}개\n`);
|
|
67
|
+
|
|
68
|
+
// 경고 출력
|
|
69
|
+
if (result.warnings.length > 0) {
|
|
70
|
+
console.log("⚠️ 경고:");
|
|
71
|
+
for (const warning of result.warnings) {
|
|
72
|
+
console.log(` - ${warning}`);
|
|
73
|
+
}
|
|
74
|
+
console.log("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 라우트 목록 출력
|
|
78
|
+
if (options.verbose) {
|
|
79
|
+
console.log(formatRoutesForCLI(result.manifest));
|
|
80
|
+
console.log("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 출력 파일 경로
|
|
84
|
+
if (generateOptions.outputPath) {
|
|
85
|
+
console.log(`📁 매니페스트 저장: ${generateOptions.outputPath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("❌ FS Routes 생성 실패:", error instanceof Error ? error.message : error);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* routes list - 현재 라우트 목록 출력
|
|
97
|
+
*/
|
|
98
|
+
export async function routesList(options: RoutesListOptions = {}): Promise<boolean> {
|
|
99
|
+
const rootDir = resolveFromCwd(".");
|
|
100
|
+
const config = await validateAndReport(rootDir);
|
|
101
|
+
if (!config) return false;
|
|
102
|
+
|
|
103
|
+
console.log("🥟 Mandu Routes List\n");
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const result = await scanRoutes(rootDir, config.fsRoutes);
|
|
107
|
+
|
|
108
|
+
if (result.errors.length > 0) {
|
|
109
|
+
console.log("⚠️ 스캔 경고:");
|
|
110
|
+
for (const error of result.errors) {
|
|
111
|
+
console.log(` - ${error.type}: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
console.log("");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (result.routes.length === 0) {
|
|
117
|
+
console.log("📭 라우트가 없습니다.");
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log("💡 app/ 폴더에 page.tsx 또는 route.ts 파일을 생성하세요.");
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log("예시:");
|
|
122
|
+
console.log(" app/page.tsx → /");
|
|
123
|
+
console.log(" app/blog/page.tsx → /blog");
|
|
124
|
+
console.log(" app/api/users/route.ts → /api/users");
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 라우트 목록 출력
|
|
129
|
+
console.log(`📋 라우트 (${result.routes.length}개)`);
|
|
130
|
+
console.log("─".repeat(70));
|
|
131
|
+
|
|
132
|
+
for (const route of result.routes) {
|
|
133
|
+
const icon = route.kind === "page" ? "📄" : "📡";
|
|
134
|
+
const hydration = route.clientModule ? " 🏝️" : "";
|
|
135
|
+
const pattern = route.pattern.padEnd(35);
|
|
136
|
+
const id = route.id;
|
|
137
|
+
|
|
138
|
+
console.log(`${icon} ${pattern} → ${id}${hydration}`);
|
|
139
|
+
|
|
140
|
+
if (options.verbose) {
|
|
141
|
+
console.log(` 📁 ${route.sourceFile}`);
|
|
142
|
+
if (route.clientModule) {
|
|
143
|
+
console.log(` 🏝️ ${route.clientModule}`);
|
|
144
|
+
}
|
|
145
|
+
if (route.layoutChain.length > 0) {
|
|
146
|
+
console.log(` 📐 layouts: ${route.layoutChain.join(" → ")}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log("");
|
|
152
|
+
|
|
153
|
+
// 통계
|
|
154
|
+
console.log("📊 통계");
|
|
155
|
+
console.log(` 페이지: ${result.stats.pageCount}개`);
|
|
156
|
+
console.log(` API: ${result.stats.apiCount}개`);
|
|
157
|
+
console.log(` 레이아웃: ${result.stats.layoutCount}개`);
|
|
158
|
+
console.log(` Island: ${result.stats.islandCount}개`);
|
|
159
|
+
console.log(` 스캔 시간: ${result.stats.scanTime}ms`);
|
|
160
|
+
|
|
161
|
+
return true;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error("❌ 라우트 목록 조회 실패:", error instanceof Error ? error.message : error);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* routes watch - 실시간 라우트 감시
|
|
170
|
+
*/
|
|
171
|
+
export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boolean> {
|
|
172
|
+
const rootDir = resolveFromCwd(".");
|
|
173
|
+
const config = await validateAndReport(rootDir);
|
|
174
|
+
if (!config) return false;
|
|
175
|
+
|
|
176
|
+
console.log("🥟 Mandu FS Routes Watch\n");
|
|
177
|
+
console.log("👀 라우트 변경 감시 중... (Ctrl+C로 종료)\n");
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// 초기 스캔
|
|
181
|
+
const initialResult = await generateManifest(rootDir, {
|
|
182
|
+
scanner: config.fsRoutes,
|
|
183
|
+
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
console.log(`✅ 초기 스캔: ${initialResult.manifest.routes.length}개 라우트\n`);
|
|
187
|
+
|
|
188
|
+
// 감시 시작
|
|
189
|
+
const watcher = await watchFSRoutes(rootDir, {
|
|
190
|
+
scanner: config.fsRoutes,
|
|
191
|
+
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
192
|
+
onChange: (result) => {
|
|
193
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
194
|
+
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
|
195
|
+
console.log(` 📋 총 라우트: ${result.manifest.routes.length}개`);
|
|
196
|
+
|
|
197
|
+
if (result.warnings.length > 0) {
|
|
198
|
+
for (const warning of result.warnings) {
|
|
199
|
+
console.log(` ⚠️ ${warning}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options.verbose) {
|
|
204
|
+
console.log("");
|
|
205
|
+
console.log(formatRoutesForCLI(result.manifest));
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 종료 시그널 처리
|
|
211
|
+
const cleanup = () => {
|
|
212
|
+
console.log("\n\n🛑 감시 종료");
|
|
213
|
+
watcher.close();
|
|
214
|
+
process.exit(0);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
process.on("SIGINT", cleanup);
|
|
218
|
+
process.on("SIGTERM", cleanup);
|
|
219
|
+
|
|
220
|
+
// 무한 대기
|
|
221
|
+
await new Promise(() => {});
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error("❌ 라우트 감시 실패:", error instanceof Error ? error.message : error);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mandu start - 프로덕션 서버 실행
|
|
3
|
+
*
|
|
4
|
+
* dev.ts에서 개발 전용 기능(HMR, 파일 감시, Guard)을 제거한 프로덕션 버전.
|
|
5
|
+
* 반드시 mandu build 이후에 실행해야 합니다.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
startServer,
|
|
9
|
+
loadEnv,
|
|
10
|
+
validateAndReport,
|
|
11
|
+
readLockfile,
|
|
12
|
+
readMcpConfig,
|
|
13
|
+
validateWithPolicy,
|
|
14
|
+
detectMode,
|
|
15
|
+
formatPolicyAction,
|
|
16
|
+
formatValidationResult,
|
|
17
|
+
type RoutesManifest,
|
|
18
|
+
type BundleManifest,
|
|
19
|
+
} from "@mandujs/core";
|
|
20
|
+
import { resolveFromCwd } from "../util/fs";
|
|
21
|
+
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
22
|
+
import { resolveManifest } from "../util/manifest";
|
|
23
|
+
import { resolveAvailablePort } from "../util/port";
|
|
24
|
+
import { registerManifestHandlers } from "../util/handlers";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import fs from "fs";
|
|
27
|
+
|
|
28
|
+
export interface StartOptions {
|
|
29
|
+
port?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function start(options: StartOptions = {}): Promise<void> {
|
|
33
|
+
const rootDir = resolveFromCwd(".");
|
|
34
|
+
const config = await validateAndReport(rootDir);
|
|
35
|
+
|
|
36
|
+
if (!config) {
|
|
37
|
+
printCLIError(CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 빌드 결과물 확인
|
|
42
|
+
const manifestJsonPath = path.join(rootDir, ".mandu/manifest.json");
|
|
43
|
+
if (!fs.existsSync(manifestJsonPath)) {
|
|
44
|
+
console.error("❌ 빌드 결과물이 없습니다. 먼저 'mandu build'를 실행하세요.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 번들 매니페스트 로드
|
|
49
|
+
let bundleManifest: BundleManifest | undefined;
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(manifestJsonPath, "utf-8");
|
|
52
|
+
bundleManifest = JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
console.warn("⚠️ 번들 매니페스트 파싱 실패. Island hydration이 비활성됩니다.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Lockfile 검증 (strict: block 정책)
|
|
58
|
+
const lockfile = await readLockfile(rootDir);
|
|
59
|
+
let mcpConfig: Record<string, unknown> | null = null;
|
|
60
|
+
try {
|
|
61
|
+
mcpConfig = await readMcpConfig(rootDir);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`⚠️ MCP 설정 로드 실패: ${error instanceof Error ? error.message : String(error)}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const { result: lockResult, action, bypassed } = validateWithPolicy(
|
|
68
|
+
config,
|
|
69
|
+
lockfile,
|
|
70
|
+
detectMode(),
|
|
71
|
+
mcpConfig
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (action === "block") {
|
|
75
|
+
console.error("🛑 서버 시작 차단: Lockfile 불일치");
|
|
76
|
+
console.error(" 설정이 변경되었습니다. 의도한 변경이라면:");
|
|
77
|
+
console.error(" $ mandu lock");
|
|
78
|
+
console.error("");
|
|
79
|
+
console.error(" 변경 사항 확인:");
|
|
80
|
+
console.error(" $ mandu lock --diff");
|
|
81
|
+
if (lockResult) {
|
|
82
|
+
console.error("");
|
|
83
|
+
console.error(formatValidationResult(lockResult));
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const serverConfig = config.server ?? {};
|
|
89
|
+
|
|
90
|
+
console.log(`🥟 Mandu Production 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 파일 로드 (production 모드)
|
|
102
|
+
const envResult = await loadEnv({
|
|
103
|
+
rootDir,
|
|
104
|
+
env: "production",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (envResult.loaded.length > 0) {
|
|
108
|
+
console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 라우트 스캔
|
|
112
|
+
console.log(`📂 라우트 스캔 중...`);
|
|
113
|
+
let manifest: RoutesManifest;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
|
|
117
|
+
manifest = resolved.manifest;
|
|
118
|
+
|
|
119
|
+
if (manifest.routes.length === 0) {
|
|
120
|
+
printCLIError(CLI_ERROR_CODES.DEV_NO_ROUTES);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
printCLIError(CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND);
|
|
127
|
+
console.error(error instanceof Error ? error.message : error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 핸들러 등록 (표준 import — 캐시 무효화 없음)
|
|
132
|
+
const registeredLayouts = new Set<string>();
|
|
133
|
+
const productionImport = async (modulePath: string) => {
|
|
134
|
+
const url = Bun.pathToFileURL(modulePath);
|
|
135
|
+
return import(url.href);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
await registerManifestHandlers(manifest, rootDir, {
|
|
139
|
+
importFn: productionImport,
|
|
140
|
+
registeredLayouts,
|
|
141
|
+
});
|
|
142
|
+
console.log("");
|
|
143
|
+
|
|
144
|
+
// 포트 결정
|
|
145
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
146
|
+
const desiredPort =
|
|
147
|
+
options.port ??
|
|
148
|
+
(envPort && Number.isFinite(envPort) ? envPort : undefined) ??
|
|
149
|
+
serverConfig.port ??
|
|
150
|
+
3333;
|
|
151
|
+
|
|
152
|
+
const { port } = await resolveAvailablePort(desiredPort, {
|
|
153
|
+
hostname: serverConfig.hostname,
|
|
154
|
+
offsets: [0],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (port !== desiredPort) {
|
|
158
|
+
console.warn(`⚠️ Port ${desiredPort} is in use. Using ${port} instead.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 메인 서버 시작 (프로덕션 모드)
|
|
162
|
+
const server = startServer(manifest, {
|
|
163
|
+
port,
|
|
164
|
+
hostname: serverConfig.hostname,
|
|
165
|
+
rootDir,
|
|
166
|
+
isDev: false,
|
|
167
|
+
bundleManifest,
|
|
168
|
+
cors: serverConfig.cors,
|
|
169
|
+
streaming: serverConfig.streaming,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const actualPort = server.server.port ?? port;
|
|
173
|
+
console.log(`\n🚀 Production server running on http://${serverConfig.hostname || "localhost"}:${actualPort}`);
|
|
174
|
+
|
|
175
|
+
// Graceful shutdown
|
|
176
|
+
const cleanup = () => {
|
|
177
|
+
console.log("\n🛑 서버 종료 중...");
|
|
178
|
+
server.stop();
|
|
179
|
+
process.exit(0);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
process.on("SIGINT", cleanup);
|
|
183
|
+
process.on("SIGTERM", cleanup);
|
|
184
|
+
}
|