@mandujs/cli 0.3.6 → 0.4.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/package.json CHANGED
@@ -1,40 +1,40 @@
1
- {
2
- "name": "@mandujs/cli",
3
- "version": "0.3.6",
4
- "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
- "type": "module",
6
- "main": "./src/main.ts",
7
- "bin": {
8
- "mandu": "./src/main.ts"
9
- },
10
- "files": [
11
- "src/**/*",
12
- "templates/**/*"
13
- ],
14
- "keywords": [
15
- "ai",
16
- "agent",
17
- "framework",
18
- "fullstack",
19
- "bun",
20
- "typescript",
21
- "react",
22
- "ssr",
23
- "code-generation"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "https://github.com/konamgil/mandu.git"
28
- },
29
- "author": "konamgil",
30
- "license": "MIT",
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "dependencies": {
35
- "@mandujs/core": "^0.3.4"
36
- },
37
- "engines": {
38
- "bun": ">=1.0.0"
39
- }
40
- }
1
+ {
2
+ "name": "@mandujs/cli",
3
+ "version": "0.4.1",
4
+ "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
+ "type": "module",
6
+ "main": "./src/main.ts",
7
+ "bin": {
8
+ "mandu": "./src/main.ts"
9
+ },
10
+ "files": [
11
+ "src/**/*",
12
+ "templates/**/*"
13
+ ],
14
+ "keywords": [
15
+ "ai",
16
+ "agent",
17
+ "framework",
18
+ "fullstack",
19
+ "bun",
20
+ "typescript",
21
+ "react",
22
+ "ssr",
23
+ "code-generation"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/konamgil/mandu.git"
28
+ },
29
+ "author": "konamgil",
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@mandujs/core": "^0.4.1"
36
+ },
37
+ "engines": {
38
+ "bun": ">=1.0.0"
39
+ }
40
+ }
@@ -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
+ }
@@ -1,9 +1,19 @@
1
- import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandujs/core";
1
+ import {
2
+ loadManifest,
3
+ startServer,
4
+ registerApiHandler,
5
+ registerPageLoader,
6
+ startDevBundler,
7
+ createHMRServer,
8
+ needsHydration,
9
+ } from "@mandujs/core";
2
10
  import { resolveFromCwd } from "../util/fs";
3
11
  import path from "path";
4
12
 
5
13
  export interface DevOptions {
6
14
  port?: number;
15
+ /** HMR 비활성화 */
16
+ noHmr?: boolean;
7
17
  }
8
18
 
9
19
  export async function dev(options: DevOptions = {}): Promise<void> {
@@ -21,9 +31,11 @@ export async function dev(options: DevOptions = {}): Promise<void> {
21
31
  process.exit(1);
22
32
  }
23
33
 
24
- console.log(`✅ Spec 로드 완료: ${result.data.routes.length}개 라우트`);
34
+ const manifest = result.data;
35
+ console.log(`✅ Spec 로드 완료: ${manifest.routes.length}개 라우트`);
25
36
 
26
- for (const route of result.data.routes) {
37
+ // 핸들러 등록
38
+ for (const route of manifest.routes) {
27
39
  if (route.kind === "api") {
28
40
  const modulePath = path.resolve(rootDir, route.module);
29
41
  try {
@@ -36,7 +48,8 @@ export async function dev(options: DevOptions = {}): Promise<void> {
36
48
  } else if (route.kind === "page" && route.componentModule) {
37
49
  const componentPath = path.resolve(rootDir, route.componentModule);
38
50
  registerPageLoader(route.id, () => import(componentPath));
39
- console.log(` 📄 Page: ${route.pattern} -> ${route.id}`);
51
+ const isIsland = needsHydration(route);
52
+ console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}`);
40
53
  }
41
54
  }
42
55
 
@@ -44,17 +57,69 @@ export async function dev(options: DevOptions = {}): Promise<void> {
44
57
 
45
58
  const port = options.port || Number(process.env.PORT) || 3000;
46
59
 
47
- const server = startServer(result.data, { port });
60
+ // HMR 서버 시작 (클라이언트 슬롯이 있는 경우)
61
+ let hmrServer: ReturnType<typeof createHMRServer> | null = null;
62
+ let devBundler: Awaited<ReturnType<typeof startDevBundler>> | null = null;
48
63
 
49
- process.on("SIGINT", () => {
50
- console.log("\n🛑 서버 종료 중...");
51
- server.stop();
52
- process.exit(0);
64
+ const hasIslands = manifest.routes.some(
65
+ (r) => r.kind === "page" && r.clientModule && needsHydration(r)
66
+ );
67
+
68
+ if (hasIslands && !options.noHmr) {
69
+ // HMR 서버 시작
70
+ hmrServer = createHMRServer(port);
71
+
72
+ // Dev 번들러 시작 (파일 감시)
73
+ devBundler = await startDevBundler({
74
+ rootDir,
75
+ manifest,
76
+ onRebuild: (result) => {
77
+ if (result.success) {
78
+ hmrServer?.broadcast({
79
+ type: "island-update",
80
+ data: {
81
+ routeId: result.routeId,
82
+ timestamp: Date.now(),
83
+ },
84
+ });
85
+ } else {
86
+ hmrServer?.broadcast({
87
+ type: "error",
88
+ data: {
89
+ routeId: result.routeId,
90
+ message: result.error,
91
+ },
92
+ });
93
+ }
94
+ },
95
+ onError: (error, routeId) => {
96
+ hmrServer?.broadcast({
97
+ type: "error",
98
+ data: {
99
+ routeId,
100
+ message: error.message,
101
+ },
102
+ });
103
+ },
104
+ });
105
+ }
106
+
107
+ // 메인 서버 시작
108
+ const server = startServer(manifest, {
109
+ port,
110
+ isDev: true,
111
+ hmrPort: hmrServer ? port : undefined,
53
112
  });
54
113
 
55
- process.on("SIGTERM", () => {
114
+ // 정리 함수
115
+ const cleanup = () => {
56
116
  console.log("\n🛑 서버 종료 중...");
57
117
  server.stop();
118
+ devBundler?.close();
119
+ hmrServer?.close();
58
120
  process.exit(0);
59
- });
121
+ };
122
+
123
+ process.on("SIGINT", cleanup);
124
+ process.on("SIGTERM", cleanup);
60
125
  }
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. guard → 5. dev
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.3.4",
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.3.6",
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"