@mandujs/cli 0.9.42 → 0.9.44
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 +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/commands/build.ts +55 -33
- package/src/commands/check.ts +16 -18
- package/src/commands/contract.ts +50 -42
- package/src/commands/dev.ts +95 -42
- package/src/commands/doctor.ts +27 -25
- package/src/commands/guard-arch.ts +12 -3
- package/src/commands/init.ts +53 -52
- package/src/commands/monitor.ts +2 -3
- package/src/commands/openapi.ts +107 -48
- package/src/errors/messages.ts +2 -2
- package/src/main.ts +75 -145
- package/src/util/manifest.ts +52 -0
- package/src/util/port.ts +71 -0
- package/templates/default/AGENTS.md +96 -0
- package/templates/default/app/globals.css +45 -33
- package/templates/default/package.json +15 -12
- package/templates/default/postcss.config.js +0 -6
- package/templates/default/tailwind.config.ts +0 -64
package/README.ko.md
CHANGED
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:
|
|
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.
|
|
3
|
+
"version": "0.9.44",
|
|
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": "
|
|
35
|
+
"@mandujs/core": "workspace:*"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"bun": ">=1.0.0"
|
package/src/commands/build.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* mandu build - 클라이언트 번들 빌드
|
|
3
3
|
*
|
|
4
4
|
* Hydration이 필요한 Island들을 번들링합니다.
|
|
5
|
+
* Tailwind v4 프로젝트는 CSS도 함께 빌드합니다.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import {
|
|
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) */
|
|
@@ -19,30 +21,28 @@ export interface BuildOptions {
|
|
|
19
21
|
outDir?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
23
|
-
const cwd = process.cwd();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!config) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
const buildConfig = config.build ?? {};
|
|
33
|
-
|
|
34
|
-
// 1. Spec 로드
|
|
35
|
-
const specResult = await loadManifest(specPath);
|
|
36
|
-
if (!specResult.success) {
|
|
37
|
-
console.error("❌ Spec 로드 실패:");
|
|
38
|
-
for (const error of specResult.errors) {
|
|
39
|
-
console.error(` ${error}`);
|
|
40
|
-
}
|
|
24
|
+
export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
|
|
27
|
+
console.log("📦 Mandu Build - Client Bundle Builder\n");
|
|
28
|
+
|
|
29
|
+
const config = await validateAndReport(cwd);
|
|
30
|
+
if (!config) {
|
|
41
31
|
return false;
|
|
42
32
|
}
|
|
33
|
+
const buildConfig = config.build ?? {};
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
46
|
|
|
47
47
|
// 2. Hydration이 필요한 라우트 확인
|
|
48
48
|
const hydratedRoutes = manifest.routes.filter(
|
|
@@ -64,15 +64,34 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
|
64
64
|
console.log(` - ${route.id} (${hydration.strategy}, ${hydration.priority || "visible"})`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// 3.
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
84
|
|
|
75
|
-
// 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. 결과 출력
|
|
76
95
|
console.log("");
|
|
77
96
|
printBundleStats(result);
|
|
78
97
|
|
|
@@ -84,13 +103,16 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
|
84
103
|
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
85
104
|
console.log(`\n✅ 빌드 완료 (${elapsed}ms)`);
|
|
86
105
|
console.log(` 출력: .mandu/client/`);
|
|
106
|
+
if (hasTailwind) {
|
|
107
|
+
console.log(` CSS: .mandu/client/globals.css`);
|
|
108
|
+
}
|
|
87
109
|
|
|
88
|
-
//
|
|
110
|
+
// 6. 감시 모드
|
|
89
111
|
if (options.watch) {
|
|
90
112
|
console.log("\n👀 파일 감시 모드...");
|
|
91
113
|
console.log(" Ctrl+C로 종료\n");
|
|
92
114
|
|
|
93
|
-
await watchAndRebuild(manifest, cwd,
|
|
115
|
+
await watchAndRebuild(manifest, cwd, resolvedBuildOptions);
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
return true;
|
|
@@ -100,7 +122,7 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
|
|
|
100
122
|
* 파일 감시 및 재빌드
|
|
101
123
|
*/
|
|
102
124
|
async function watchAndRebuild(
|
|
103
|
-
manifest:
|
|
125
|
+
manifest: RoutesManifest,
|
|
104
126
|
rootDir: string,
|
|
105
127
|
options: BuildOptions
|
|
106
128
|
): Promise<void> {
|
package/src/commands/check.ts
CHANGED
|
@@ -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
|
|
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(
|
|
24
|
+
export async function check(): Promise<boolean> {
|
|
33
25
|
const rootDir = resolveFromCwd(".");
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
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,
|
package/src/commands/contract.ts
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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(
|
|
242
|
+
const { registry } = await buildContractRegistry(manifest, rootDir);
|
|
235
243
|
toRegistry = registry;
|
|
236
244
|
}
|
|
237
245
|
|
package/src/commands/dev.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
createHMRServer,
|
|
9
9
|
needsHydration,
|
|
10
10
|
loadEnv,
|
|
11
|
-
generateManifest,
|
|
12
11
|
watchFSRoutes,
|
|
13
12
|
clearDefaultRegistry,
|
|
14
13
|
createGuardWatcher,
|
|
@@ -18,29 +17,23 @@ import {
|
|
|
18
17
|
formatReportAsAgentJSON,
|
|
19
18
|
getPreset,
|
|
20
19
|
validateAndReport,
|
|
20
|
+
isTailwindProject,
|
|
21
|
+
startCSSWatch,
|
|
21
22
|
type RoutesManifest,
|
|
22
23
|
type GuardConfig,
|
|
23
|
-
type GuardPreset,
|
|
24
24
|
type Violation,
|
|
25
|
+
type CSSWatcher,
|
|
25
26
|
} from "@mandujs/core";
|
|
26
|
-
import {
|
|
27
|
-
import { resolveOutputFormat
|
|
27
|
+
import { resolveFromCwd } from "../util/fs";
|
|
28
|
+
import { resolveOutputFormat } from "../util/output";
|
|
28
29
|
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
29
30
|
import { importFresh } from "../util/bun";
|
|
31
|
+
import { resolveManifest } from "../util/manifest";
|
|
32
|
+
import { resolveAvailablePort } from "../util/port";
|
|
30
33
|
import path from "path";
|
|
31
34
|
|
|
32
35
|
export interface DevOptions {
|
|
33
36
|
port?: number;
|
|
34
|
-
/** HMR 비활성화 */
|
|
35
|
-
noHmr?: boolean;
|
|
36
|
-
/** FS Routes 비활성화 (레거시 모드) */
|
|
37
|
-
legacy?: boolean;
|
|
38
|
-
/** Architecture Guard 활성화 */
|
|
39
|
-
guard?: boolean;
|
|
40
|
-
/** Guard 프리셋 */
|
|
41
|
-
guardPreset?: GuardPreset;
|
|
42
|
-
/** Guard 출력 형식 */
|
|
43
|
-
guardFormat?: OutputFormat;
|
|
44
37
|
}
|
|
45
38
|
|
|
46
39
|
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
@@ -55,8 +48,9 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
55
48
|
const serverConfig = config.server ?? {};
|
|
56
49
|
const devConfig = config.dev ?? {};
|
|
57
50
|
const guardConfigFromFile = config.guard ?? {};
|
|
51
|
+
const HMR_OFFSET = 1;
|
|
58
52
|
|
|
59
|
-
console.log(`🥟 Mandu Dev Server
|
|
53
|
+
console.log(`🥟 Mandu Dev Server`);
|
|
60
54
|
|
|
61
55
|
// .env 파일 로드
|
|
62
56
|
const envResult = await loadEnv({
|
|
@@ -68,32 +62,37 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
68
62
|
console.log(`🔐 환경 변수 로드: ${envResult.loaded.join(", ")}`);
|
|
69
63
|
}
|
|
70
64
|
|
|
71
|
-
// FS Routes
|
|
72
|
-
console.log(`📂
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
}
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.
|
|
82
|
-
console.log(" app/page.tsx → /");
|
|
83
|
-
console.log(" app/blog/page.tsx → /blog");
|
|
84
|
-
console.log(" app/api/users/route.ts → /api/users");
|
|
85
|
-
console.log("");
|
|
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);
|
|
86
90
|
process.exit(1);
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.log(`✅ ${manifest.routes.length}개 라우트 발견\n`);
|
|
91
|
-
|
|
92
|
-
const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
|
|
93
|
-
const guardPreset = options.guardPreset || guardConfigFromFile.preset || "mandu";
|
|
94
|
-
const guardFormat = resolveOutputFormat(options.guardFormat);
|
|
92
|
+
const guardPreset = guardConfigFromFile.preset || "mandu";
|
|
93
|
+
const guardFormat = resolveOutputFormat();
|
|
95
94
|
const guardConfig: GuardConfig | null =
|
|
96
|
-
|
|
95
|
+
guardConfigFromFile.realtime === false
|
|
97
96
|
? null
|
|
98
97
|
: {
|
|
99
98
|
preset: guardPreset,
|
|
@@ -228,21 +227,62 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
228
227
|
console.log("");
|
|
229
228
|
|
|
230
229
|
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
231
|
-
const
|
|
230
|
+
const desiredPort =
|
|
232
231
|
options.port ??
|
|
233
232
|
(envPort && Number.isFinite(envPort) ? envPort : undefined) ??
|
|
234
233
|
serverConfig.port ??
|
|
235
234
|
3333;
|
|
236
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
|
+
|
|
237
250
|
// HMR 서버 시작 (클라이언트 슬롯이 있는 경우)
|
|
238
251
|
let hmrServer: ReturnType<typeof createHMRServer> | null = null;
|
|
239
252
|
let devBundler: Awaited<ReturnType<typeof startDevBundler>> | null = null;
|
|
253
|
+
let cssWatcher: CSSWatcher | null = null;
|
|
240
254
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
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
|
+
}
|
|
244
285
|
|
|
245
|
-
const hmrEnabled = options.noHmr ? false : (devConfig.hmr ?? true);
|
|
246
286
|
if (hasIslands && hmrEnabled) {
|
|
247
287
|
// HMR 서버 시작
|
|
248
288
|
hmrServer = createHMRServer(port);
|
|
@@ -302,8 +342,20 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
302
342
|
bundleManifest: devBundler?.initialBuild.manifest,
|
|
303
343
|
cors: serverConfig.cors,
|
|
304
344
|
streaming: serverConfig.streaming,
|
|
345
|
+
// Tailwind 감지 시에만 CSS 링크 주입
|
|
346
|
+
cssPath: hasTailwind ? cssWatcher?.serverPath : false,
|
|
305
347
|
});
|
|
306
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
|
+
|
|
307
359
|
// FS Routes 실시간 감시
|
|
308
360
|
const routesWatcher = await watchFSRoutes(rootDir, {
|
|
309
361
|
skipLegacy: true,
|
|
@@ -341,6 +393,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
341
393
|
server.stop();
|
|
342
394
|
devBundler?.close();
|
|
343
395
|
hmrServer?.close();
|
|
396
|
+
cssWatcher?.close();
|
|
344
397
|
routesWatcher.close();
|
|
345
398
|
archGuardWatcher?.close();
|
|
346
399
|
process.exit(0);
|