@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 CHANGED
@@ -223,7 +223,7 @@ bun test --watch # 감시 모드
223
223
  ## 요구 사항
224
224
 
225
225
  - Bun >= 1.0.0
226
- - React >= 18.0.0
226
+ - React >= 19.0.0
227
227
 
228
228
  ## 관련 패키지
229
229
 
package/README.md CHANGED
@@ -43,7 +43,9 @@ bun run dev
43
43
  bunx mandu dev
44
44
  ```
45
45
 
46
- Your app is now running at `http://localhost:3000`.
46
+ Your app is now running at `http://localhost:3333`.
47
+
48
+ To change the port, set `PORT` or use `server.port` in `mandu.config`. If the port is in use, Mandu will pick the next available port.
47
49
 
48
50
  ### 3. Create Pages in `app/` Directory
49
51
 
@@ -254,7 +256,6 @@ bunx mandu guard --preset fsd
254
256
 
255
257
  | Option | Description |
256
258
  |--------|-------------|
257
- | `--port <n>` | Server port (default: 3000) |
258
259
  | `--guard` | Enable Guard watching |
259
260
  | `--guard-preset <p>` | Guard preset (default: mandu) |
260
261
 
@@ -307,7 +308,6 @@ bunx mandu guard --preset fsd
307
308
  bunx @mandujs/cli init my-app
308
309
 
309
310
  # Development
310
- bunx mandu dev --port 3000
311
311
  bunx mandu dev --guard
312
312
 
313
313
  # Routes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.9.24",
3
+ "version": "0.9.43",
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.9.39"
35
+ "@mandujs/core": "workspace:*"
36
36
  },
37
37
  "engines": {
38
38
  "bun": ">=1.0.0"
@@ -2,11 +2,13 @@
2
2
  * mandu build - 클라이언트 번들 빌드
3
3
  *
4
4
  * Hydration이 필요한 Island들을 번들링합니다.
5
+ * Tailwind v4 프로젝트는 CSS도 함께 빌드합니다.
5
6
  */
6
7
 
7
- import { loadManifest, buildClientBundles, printBundleStats } from "@mandujs/core";
8
+ import { buildClientBundles, printBundleStats, validateAndReport, isTailwindProject, buildCSS, type RoutesManifest } from "@mandujs/core";
8
9
  import path from "path";
9
10
  import fs from "fs/promises";
11
+ import { resolveManifest } from "../util/manifest";
10
12
 
11
13
  export interface BuildOptions {
12
14
  /** 코드 압축 (기본: production에서 true) */
@@ -21,22 +23,26 @@ export interface BuildOptions {
21
23
 
22
24
  export async function build(options: BuildOptions = {}): Promise<boolean> {
23
25
  const cwd = process.cwd();
24
- const specPath = path.join(cwd, "spec", "routes.manifest.json");
25
26
 
26
27
  console.log("📦 Mandu Build - Client Bundle Builder\n");
27
28
 
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
- }
29
+ const config = await validateAndReport(cwd);
30
+ if (!config) {
35
31
  return false;
36
32
  }
33
+ const buildConfig = config.build ?? {};
37
34
 
38
- const manifest = specResult.data!;
39
- console.log(`✅ Spec 로드 완료: ${manifest.routes.length}개 라우트`);
35
+ // 1. 라우트 매니페스트 로드 (FS Routes 우선)
36
+ let manifest: Awaited<ReturnType<typeof resolveManifest>>["manifest"];
37
+ try {
38
+ const resolved = await resolveManifest(cwd, { fsRoutes: config.fsRoutes });
39
+ manifest = resolved.manifest;
40
+ console.log(`✅ 라우트 로드 완료 (${resolved.source}): ${manifest.routes.length}개 라우트`);
41
+ } catch (error) {
42
+ console.error("❌ 라우트 로드 실패:");
43
+ console.error(` ${error instanceof Error ? error.message : error}`);
44
+ return false;
45
+ }
40
46
 
41
47
  // 2. Hydration이 필요한 라우트 확인
42
48
  const hydratedRoutes = manifest.routes.filter(
@@ -58,15 +64,34 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
58
64
  console.log(` - ${route.id} (${hydration.strategy}, ${hydration.priority || "visible"})`);
59
65
  }
60
66
 
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
- });
67
+ // 3. Tailwind CSS 빌드 (감지 시에만)
68
+ const hasTailwind = await isTailwindProject(cwd);
69
+ if (hasTailwind) {
70
+ console.log(`\n🎨 Tailwind CSS v4 빌드 중...`);
71
+ const cssResult = await buildCSS({
72
+ rootDir: cwd,
73
+ minify: options.minify ?? true,
74
+ });
75
+
76
+ if (!cssResult.success) {
77
+ console.error(`\n❌ CSS 빌드 실패: ${cssResult.error}`);
78
+ return false;
79
+ }
80
+
81
+ console.log(` ✅ CSS 빌드 완료 (${cssResult.buildTime?.toFixed(0)}ms)`);
82
+ console.log(` 출력: ${cssResult.outputPath}`);
83
+ }
68
84
 
69
- // 4. 결과 출력
85
+ // 4. 번들 빌드
86
+ const startTime = performance.now();
87
+ const resolvedBuildOptions: BuildOptions = {
88
+ minify: options.minify ?? buildConfig.minify,
89
+ sourcemap: options.sourcemap ?? buildConfig.sourcemap,
90
+ outDir: options.outDir ?? buildConfig.outDir,
91
+ };
92
+ const result = await buildClientBundles(manifest, cwd, resolvedBuildOptions);
93
+
94
+ // 5. 결과 출력
70
95
  console.log("");
71
96
  printBundleStats(result);
72
97
 
@@ -78,13 +103,16 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
78
103
  const elapsed = (performance.now() - startTime).toFixed(0);
79
104
  console.log(`\n✅ 빌드 완료 (${elapsed}ms)`);
80
105
  console.log(` 출력: .mandu/client/`);
106
+ if (hasTailwind) {
107
+ console.log(` CSS: .mandu/client/globals.css`);
108
+ }
81
109
 
82
- // 5. 감시 모드
110
+ // 6. 감시 모드
83
111
  if (options.watch) {
84
112
  console.log("\n👀 파일 감시 모드...");
85
113
  console.log(" Ctrl+C로 종료\n");
86
114
 
87
- await watchAndRebuild(manifest, cwd, options);
115
+ await watchAndRebuild(manifest, cwd, resolvedBuildOptions);
88
116
  }
89
117
 
90
118
  return true;
@@ -94,7 +122,7 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
94
122
  * 파일 감시 및 재빌드
95
123
  */
96
124
  async function watchAndRebuild(
97
- manifest: Awaited<ReturnType<typeof loadManifest>>["manifest"],
125
+ manifest: RoutesManifest,
98
126
  rootDir: string,
99
127
  options: BuildOptions
100
128
  ): Promise<void> {
@@ -10,32 +10,28 @@ import {
10
10
  checkDirectory,
11
11
  printReport,
12
12
  getPreset,
13
+ validateAndReport,
13
14
  loadManifest,
14
15
  runGuardCheck,
15
16
  buildGuardReport,
16
17
  printReportSummary,
17
18
  type GuardConfig,
18
- type GuardPreset,
19
19
  } from "@mandujs/core";
20
20
  import path from "path";
21
21
  import { resolveFromCwd, isDirectory, pathExists } from "../util/fs";
22
- import { resolveOutputFormat, type OutputFormat } from "../util/output";
23
-
24
- export interface CheckOptions {
25
- preset?: GuardPreset;
26
- format?: OutputFormat;
27
- ci?: boolean;
28
- quiet?: boolean;
29
- legacy?: boolean;
30
- }
22
+ import { resolveOutputFormat } from "../util/output";
31
23
 
32
- export async function check(options: CheckOptions = {}): Promise<boolean> {
24
+ export async function check(): Promise<boolean> {
33
25
  const rootDir = resolveFromCwd(".");
34
- const preset = options.preset ?? "mandu";
35
- const format = resolveOutputFormat(options.format);
36
- const quiet = options.quiet === true;
37
- const strictWarnings = options.ci === true;
38
- const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
26
+ const config = await validateAndReport(rootDir);
27
+ if (!config) return false;
28
+
29
+ const guardConfigFromFile = config.guard ?? {};
30
+ const preset = guardConfigFromFile.preset ?? "mandu";
31
+ const format = resolveOutputFormat();
32
+ const quiet = false;
33
+ const strictWarnings = process.env.CI === "true";
34
+ const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
39
35
  const specPath = resolveFromCwd("spec/routes.manifest.json");
40
36
  const hasSpec = await pathExists(specPath);
41
37
 
@@ -70,6 +66,7 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
70
66
  try {
71
67
  if (format === "console") {
72
68
  const result = await generateManifest(rootDir, {
69
+ scanner: config.fsRoutes,
73
70
  outputPath: ".mandu/routes.manifest.json",
74
71
  skipLegacy: true,
75
72
  });
@@ -95,7 +92,7 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
95
92
  log("");
96
93
  }
97
94
  } else {
98
- const scan = await scanRoutes(rootDir);
95
+ const scan = await scanRoutes(rootDir, config.fsRoutes);
99
96
  routesSummary.count = scan.routes.length;
100
97
  routesSummary.warnings = scan.errors.map((e) => `${e.type}: ${e.message}`);
101
98
  }
@@ -119,7 +116,8 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
119
116
  // 2) Architecture Guard 검사
120
117
  const guardConfig: GuardConfig = {
121
118
  preset,
122
- srcDir: "src",
119
+ srcDir: guardConfigFromFile.srcDir ?? "src",
120
+ exclude: guardConfigFromFile.exclude,
123
121
  fsRoutes: enableFsRoutes
124
122
  ? {
125
123
  noPageToPage: true,
@@ -4,16 +4,17 @@
4
4
  */
5
5
 
6
6
  import {
7
- loadManifest,
8
7
  runContractGuardCheck,
9
8
  generateContractTemplate,
10
9
  buildContractRegistry,
11
10
  writeContractRegistry,
12
11
  readContractRegistry,
13
12
  diffContractRegistry,
13
+ validateAndReport,
14
14
  } from "@mandujs/core";
15
15
  import path from "path";
16
16
  import fs from "fs/promises";
17
+ import { resolveManifest } from "../util/manifest";
17
18
 
18
19
  interface ContractCreateOptions {
19
20
  routeId: string;
@@ -33,24 +34,32 @@ interface ContractDiffOptions {
33
34
  output?: string;
34
35
  json?: boolean;
35
36
  }
37
+
38
+ async function loadRoutesManifest(rootDir: string) {
39
+ const config = await validateAndReport(rootDir);
40
+ if (!config) {
41
+ throw new Error("Invalid mandu.config");
42
+ }
43
+ const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
44
+ return resolved.manifest;
45
+ }
36
46
 
37
47
  /**
38
48
  * Create a new contract file for a route
39
49
  */
40
- export async function contractCreate(options: ContractCreateOptions): Promise<boolean> {
41
- const rootDir = process.cwd();
42
- const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
43
-
44
- console.log(`\n📜 Creating contract for route: ${options.routeId}\n`);
45
-
46
- // Load manifest
47
- const manifestResult = await loadManifest(manifestPath);
48
- if (!manifestResult.success) {
49
- console.error("❌ Failed to load manifest:", manifestResult.errors);
50
- return false;
51
- }
52
-
53
- const manifest = manifestResult.data!;
50
+ export async function contractCreate(options: ContractCreateOptions): Promise<boolean> {
51
+ const rootDir = process.cwd();
52
+
53
+ console.log(`\n📜 Creating contract for route: ${options.routeId}\n`);
54
+
55
+ // Load manifest
56
+ let manifest;
57
+ try {
58
+ manifest = await loadRoutesManifest(rootDir);
59
+ } catch (error) {
60
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
61
+ return false;
62
+ }
54
63
 
55
64
  // Find the route
56
65
  const route = manifest.routes.find((r) => r.id === options.routeId);
@@ -105,22 +114,21 @@ export async function contractCreate(options: ContractCreateOptions): Promise<bo
105
114
  * Validate all contracts against their slot implementations
106
115
  */
107
116
  export async function contractValidate(options: ContractValidateOptions = {}): Promise<boolean> {
108
- const rootDir = process.cwd();
109
- const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
110
-
111
- console.log(`\n🔍 Validating contracts...\n`);
112
-
113
- // Load manifest
114
- const manifestResult = await loadManifest(manifestPath);
115
- if (!manifestResult.success) {
116
- console.error("❌ Failed to load manifest:", manifestResult.errors);
117
- return false;
118
- }
119
-
120
- const manifest = manifestResult.data!;
121
-
122
- // Run contract guard check
123
- const violations = await runContractGuardCheck(manifest, rootDir);
117
+ const rootDir = process.cwd();
118
+
119
+ console.log(`\n🔍 Validating contracts...\n`);
120
+
121
+ // Load manifest
122
+ let manifest;
123
+ try {
124
+ manifest = await loadRoutesManifest(rootDir);
125
+ } catch (error) {
126
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
127
+ return false;
128
+ }
129
+
130
+ // Run contract guard check
131
+ const violations = await runContractGuardCheck(manifest, rootDir);
124
132
 
125
133
  if (violations.length === 0) {
126
134
  console.log(`✅ All contracts are valid!\n`);
@@ -175,18 +183,17 @@ export async function contractValidate(options: ContractValidateOptions = {}): P
175
183
  */
176
184
  export async function contractBuild(options: ContractBuildOptions = {}): Promise<boolean> {
177
185
  const rootDir = process.cwd();
178
- const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
179
186
  const outputPath = options.output || path.join(rootDir, ".mandu", "contracts.json");
180
187
 
181
188
  console.log(`\n📦 Building contract registry...\n`);
182
189
 
183
- const manifestResult = await loadManifest(manifestPath);
184
- if (!manifestResult.success) {
185
- console.error("❌ Failed to load manifest:", manifestResult.errors);
190
+ let manifest;
191
+ try {
192
+ manifest = await loadRoutesManifest(rootDir);
193
+ } catch (error) {
194
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
186
195
  return false;
187
196
  }
188
-
189
- const manifest = manifestResult.data!;
190
197
  const { registry, warnings } = await buildContractRegistry(manifest, rootDir);
191
198
 
192
199
  if (warnings.length > 0) {
@@ -211,7 +218,6 @@ export async function contractBuild(options: ContractBuildOptions = {}): Promise
211
218
  */
212
219
  export async function contractDiff(options: ContractDiffOptions = {}): Promise<boolean> {
213
220
  const rootDir = process.cwd();
214
- const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
215
221
  const fromPath = options.from || path.join(rootDir, ".mandu", "contracts.json");
216
222
 
217
223
  console.log(`\n🔍 Diffing contracts...\n`);
@@ -226,12 +232,14 @@ export async function contractDiff(options: ContractDiffOptions = {}): Promise<b
226
232
  let toRegistry = options.to ? await readContractRegistry(options.to) : null;
227
233
 
228
234
  if (!toRegistry) {
229
- const manifestResult = await loadManifest(manifestPath);
230
- if (!manifestResult.success) {
231
- console.error("❌ Failed to load manifest:", manifestResult.errors);
235
+ let manifest;
236
+ try {
237
+ manifest = await loadRoutesManifest(rootDir);
238
+ } catch (error) {
239
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
232
240
  return false;
233
241
  }
234
- const { registry } = await buildContractRegistry(manifestResult.data!, rootDir);
242
+ const { registry } = await buildContractRegistry(manifest, rootDir);
235
243
  toRegistry = registry;
236
244
  }
237
245