@mandujs/cli 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/commands/build.ts +156 -0
- package/src/main.ts +16 -1
- package/templates/default/package.json +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mandujs/core": "^0.
|
|
35
|
+
"@mandujs/core": "^0.4.0"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"bun": ">=1.0.0"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mandu build - 클라이언트 번들 빌드
|
|
3
|
+
*
|
|
4
|
+
* Hydration이 필요한 Island들을 번들링합니다.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadManifest, buildClientBundles, printBundleStats } from "@mandujs/core";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
|
|
11
|
+
export interface BuildOptions {
|
|
12
|
+
/** 코드 압축 (기본: production에서 true) */
|
|
13
|
+
minify?: boolean;
|
|
14
|
+
/** 소스맵 생성 */
|
|
15
|
+
sourcemap?: boolean;
|
|
16
|
+
/** 감시 모드 */
|
|
17
|
+
watch?: boolean;
|
|
18
|
+
/** 출력 디렉토리 */
|
|
19
|
+
outDir?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const specPath = path.join(cwd, "spec", "routes.manifest.json");
|
|
25
|
+
|
|
26
|
+
console.log("📦 Mandu Build - Client Bundle Builder\n");
|
|
27
|
+
|
|
28
|
+
// 1. Spec 로드
|
|
29
|
+
const specResult = await loadManifest(specPath);
|
|
30
|
+
if (!specResult.success) {
|
|
31
|
+
console.error("❌ Spec 로드 실패:");
|
|
32
|
+
for (const error of specResult.errors) {
|
|
33
|
+
console.error(` ${error}`);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manifest = specResult.manifest;
|
|
39
|
+
console.log(`✅ Spec 로드 완료: ${manifest.routes.length}개 라우트`);
|
|
40
|
+
|
|
41
|
+
// 2. Hydration이 필요한 라우트 확인
|
|
42
|
+
const hydratedRoutes = manifest.routes.filter(
|
|
43
|
+
(route) =>
|
|
44
|
+
route.kind === "page" &&
|
|
45
|
+
route.clientModule &&
|
|
46
|
+
(!route.hydration || route.hydration.strategy !== "none")
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (hydratedRoutes.length === 0) {
|
|
50
|
+
console.log("\n📭 Hydration이 필요한 라우트가 없습니다.");
|
|
51
|
+
console.log(" (clientModule이 없거나 hydration.strategy: none)");
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`\n🏝️ ${hydratedRoutes.length}개 Island 빌드 중...`);
|
|
56
|
+
for (const route of hydratedRoutes) {
|
|
57
|
+
const hydration = route.hydration || { strategy: "island", priority: "visible" };
|
|
58
|
+
console.log(` - ${route.id} (${hydration.strategy}, ${hydration.priority || "visible"})`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. 번들 빌드
|
|
62
|
+
const startTime = performance.now();
|
|
63
|
+
const result = await buildClientBundles(manifest, cwd, {
|
|
64
|
+
minify: options.minify,
|
|
65
|
+
sourcemap: options.sourcemap,
|
|
66
|
+
outDir: options.outDir,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 4. 결과 출력
|
|
70
|
+
console.log("");
|
|
71
|
+
printBundleStats(result);
|
|
72
|
+
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
console.error("\n❌ 빌드 실패");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
79
|
+
console.log(`\n✅ 빌드 완료 (${elapsed}ms)`);
|
|
80
|
+
console.log(` 출력: .mandu/client/`);
|
|
81
|
+
|
|
82
|
+
// 5. 감시 모드
|
|
83
|
+
if (options.watch) {
|
|
84
|
+
console.log("\n👀 파일 감시 모드...");
|
|
85
|
+
console.log(" Ctrl+C로 종료\n");
|
|
86
|
+
|
|
87
|
+
await watchAndRebuild(manifest, cwd, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 파일 감시 및 재빌드
|
|
95
|
+
*/
|
|
96
|
+
async function watchAndRebuild(
|
|
97
|
+
manifest: Awaited<ReturnType<typeof loadManifest>>["manifest"],
|
|
98
|
+
rootDir: string,
|
|
99
|
+
options: BuildOptions
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const slotsDir = path.join(rootDir, "spec", "slots");
|
|
102
|
+
|
|
103
|
+
// 디렉토리 존재 확인
|
|
104
|
+
try {
|
|
105
|
+
await fs.access(slotsDir);
|
|
106
|
+
} catch {
|
|
107
|
+
console.warn(`⚠️ 슬롯 디렉토리가 없습니다: ${slotsDir}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { watch } = await import("fs");
|
|
112
|
+
|
|
113
|
+
const watcher = watch(slotsDir, { recursive: true }, async (event, filename) => {
|
|
114
|
+
if (!filename) return;
|
|
115
|
+
|
|
116
|
+
// .client.ts 파일만 감시
|
|
117
|
+
if (!filename.endsWith(".client.ts")) return;
|
|
118
|
+
|
|
119
|
+
const routeId = filename.replace(".client.ts", "").replace(/\\/g, "/").split("/").pop();
|
|
120
|
+
if (!routeId) return;
|
|
121
|
+
|
|
122
|
+
const route = manifest!.routes.find((r) => r.id === routeId);
|
|
123
|
+
if (!route || !route.clientModule) return;
|
|
124
|
+
|
|
125
|
+
console.log(`\n🔄 변경 감지: ${routeId}`);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const result = await buildClientBundles(manifest!, rootDir, {
|
|
129
|
+
minify: options.minify,
|
|
130
|
+
sourcemap: options.sourcemap,
|
|
131
|
+
outDir: options.outDir,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.success) {
|
|
135
|
+
console.log(`✅ 재빌드 완료: ${routeId}`);
|
|
136
|
+
} else {
|
|
137
|
+
console.error(`❌ 재빌드 실패: ${routeId}`);
|
|
138
|
+
for (const error of result.errors) {
|
|
139
|
+
console.error(` ${error}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`❌ 재빌드 오류: ${error}`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 종료 시 정리
|
|
148
|
+
process.on("SIGINT", () => {
|
|
149
|
+
console.log("\n\n👋 빌드 감시 종료");
|
|
150
|
+
watcher.close();
|
|
151
|
+
process.exit(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 무한 대기
|
|
155
|
+
await new Promise(() => {});
|
|
156
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { generateApply } from "./commands/generate-apply";
|
|
|
5
5
|
import { guardCheck } from "./commands/guard-check";
|
|
6
6
|
import { dev } from "./commands/dev";
|
|
7
7
|
import { init } from "./commands/init";
|
|
8
|
+
import { build } from "./commands/build";
|
|
8
9
|
import {
|
|
9
10
|
changeBegin,
|
|
10
11
|
changeCommit,
|
|
@@ -24,6 +25,7 @@ Commands:
|
|
|
24
25
|
spec-upsert Spec 파일 검증 및 lock 갱신
|
|
25
26
|
generate Spec에서 코드 생성
|
|
26
27
|
guard Guard 규칙 검사
|
|
28
|
+
build 클라이언트 번들 빌드 (Hydration)
|
|
27
29
|
dev 개발 서버 실행
|
|
28
30
|
|
|
29
31
|
change begin 변경 트랜잭션 시작 (스냅샷 생성)
|
|
@@ -38,6 +40,9 @@ Options:
|
|
|
38
40
|
--file <path> spec-upsert 시 사용할 spec 파일 경로
|
|
39
41
|
--port <port> dev 서버 포트 (기본: 3000)
|
|
40
42
|
--no-auto-correct guard 시 자동 수정 비활성화
|
|
43
|
+
--minify build 시 코드 압축
|
|
44
|
+
--sourcemap build 시 소스맵 생성
|
|
45
|
+
--watch build 시 파일 감시 모드
|
|
41
46
|
--message <msg> change begin 시 설명 메시지
|
|
42
47
|
--id <id> change rollback 시 특정 변경 ID
|
|
43
48
|
--keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
|
|
@@ -48,13 +53,15 @@ Examples:
|
|
|
48
53
|
bunx mandu spec-upsert
|
|
49
54
|
bunx mandu generate
|
|
50
55
|
bunx mandu guard
|
|
56
|
+
bunx mandu build --minify
|
|
57
|
+
bunx mandu build --watch
|
|
51
58
|
bunx mandu dev --port 3000
|
|
52
59
|
bunx mandu change begin --message "Add new route"
|
|
53
60
|
bunx mandu change commit
|
|
54
61
|
bunx mandu change rollback
|
|
55
62
|
|
|
56
63
|
Workflow:
|
|
57
|
-
1. init → 2. spec-upsert → 3. generate → 4.
|
|
64
|
+
1. init → 2. spec-upsert → 3. generate → 4. build → 5. guard → 6. dev
|
|
58
65
|
`;
|
|
59
66
|
|
|
60
67
|
function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
|
|
@@ -110,6 +117,14 @@ async function main(): Promise<void> {
|
|
|
110
117
|
});
|
|
111
118
|
break;
|
|
112
119
|
|
|
120
|
+
case "build":
|
|
121
|
+
success = await build({
|
|
122
|
+
minify: options.minify === "true",
|
|
123
|
+
sourcemap: options.sourcemap === "true",
|
|
124
|
+
watch: options.watch === "true",
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
|
|
113
128
|
case "dev":
|
|
114
129
|
await dev({ port: options.port ? Number(options.port) : undefined });
|
|
115
130
|
break;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "mandu dev",
|
|
7
|
+
"build": "mandu build",
|
|
7
8
|
"generate": "mandu generate",
|
|
8
9
|
"guard": "mandu guard",
|
|
9
10
|
"spec": "mandu spec-upsert",
|
|
@@ -11,12 +12,12 @@
|
|
|
11
12
|
"test:watch": "bun test --watch"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@mandujs/core": "^0.
|
|
15
|
+
"@mandujs/core": "^0.4.0",
|
|
15
16
|
"react": "^18.2.0",
|
|
16
17
|
"react-dom": "^18.2.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
|
-
"@mandujs/cli": "^0.
|
|
20
|
+
"@mandujs/cli": "^0.4.0",
|
|
20
21
|
"@types/react": "^18.2.0",
|
|
21
22
|
"@types/react-dom": "^18.2.0",
|
|
22
23
|
"typescript": "^5.0.0"
|