@mandujs/core 0.20.0 → 0.20.2
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 +1 -1
- package/src/bundler/css.ts +323 -353
- package/src/bundler/dev.ts +1 -1
- package/src/devtools/ai/mcp-connector.ts +26 -9
- package/src/runtime/ssr.ts +1 -1
- package/src/runtime/streaming-ssr.ts +1 -1
package/package.json
CHANGED
package/src/bundler/css.ts
CHANGED
|
@@ -1,353 +1,323 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu CSS Builder
|
|
3
|
-
* Tailwind CSS v4 CLI 기반 CSS 빌드 및 감시
|
|
4
|
-
*
|
|
5
|
-
* 특징:
|
|
6
|
-
* - Tailwind v4 Oxide Engine (Rust) 사용
|
|
7
|
-
* - Zero Config: @import "tailwindcss" 자동 감지
|
|
8
|
-
* - 출력: .mandu/client/globals.css
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return [
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ========== Types ==========
|
|
30
|
-
|
|
31
|
-
export interface CSSBuildOptions {
|
|
32
|
-
/** 프로젝트 루트 디렉토리 */
|
|
33
|
-
rootDir: string;
|
|
34
|
-
/** CSS 입력 파일 (기본: "app/globals.css") */
|
|
35
|
-
input?: string;
|
|
36
|
-
/** CSS 출력 파일 (기본: ".mandu/client/globals.css") */
|
|
37
|
-
output?: string;
|
|
38
|
-
/** Watch 모드 활성화 */
|
|
39
|
-
watch?: boolean;
|
|
40
|
-
/** Minify 활성화 (production) */
|
|
41
|
-
minify?: boolean;
|
|
42
|
-
/** 빌드 완료 콜백 */
|
|
43
|
-
onBuild?: (result: CSSBuildResult) => void;
|
|
44
|
-
/** 에러 콜백 */
|
|
45
|
-
onError?: (error: Error) => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface CSSBuildResult {
|
|
49
|
-
success: boolean;
|
|
50
|
-
outputPath: string;
|
|
51
|
-
buildTime?: number;
|
|
52
|
-
error?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface CSSWatcher {
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.log(
|
|
216
|
-
console.log(`
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
process: proc,
|
|
326
|
-
outputPath,
|
|
327
|
-
serverPath: SERVER_CSS_PATH,
|
|
328
|
-
close: () => {
|
|
329
|
-
fsWatcher?.close();
|
|
330
|
-
// Windows에서는 SIGTERM이 무시될 수 있으므로 SIGKILL 사용 (#117)
|
|
331
|
-
if (process.platform === "win32") {
|
|
332
|
-
proc.kill("SIGKILL");
|
|
333
|
-
} else {
|
|
334
|
-
proc.kill();
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* CSS 서버 경로 반환
|
|
342
|
-
*/
|
|
343
|
-
export function getCSSServerPath(): string {
|
|
344
|
-
return SERVER_CSS_PATH;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* CSS 링크 태그 생성
|
|
349
|
-
*/
|
|
350
|
-
export function generateCSSLinkTag(isDev: boolean = false): string {
|
|
351
|
-
const cacheBust = isDev ? `?t=${Date.now()}` : "";
|
|
352
|
-
return `<link rel="stylesheet" href="${SERVER_CSS_PATH}${cacheBust}">`;
|
|
353
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu CSS Builder
|
|
3
|
+
* Tailwind CSS v4 CLI 기반 CSS 빌드 및 감시
|
|
4
|
+
*
|
|
5
|
+
* 특징:
|
|
6
|
+
* - Tailwind v4 Oxide Engine (Rust) 사용
|
|
7
|
+
* - Zero Config: @import "tailwindcss" 자동 감지
|
|
8
|
+
* - 출력: .mandu/client/globals.css
|
|
9
|
+
*
|
|
10
|
+
* #152: Tailwind CLI --watch 모드가 Bun.spawn에서 hang되는 문제 수정
|
|
11
|
+
* - 원인: @tailwindcss/cli v4 --watch 모드가 Bun subprocess에서 파일 미생성
|
|
12
|
+
* - 해결: 자체 파일 감시 + 단발 빌드 반복 방식으로 전환
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "bun";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import fs from "fs/promises";
|
|
18
|
+
import { watch as fsWatch, type FSWatcher } from "fs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tailwind CLI 실행 명령어를 결정한다.
|
|
22
|
+
* Windows에서 Bun.spawn은 PATH 기반 명령어 해석이 불안정하므로 (#152)
|
|
23
|
+
* process.execPath (절대 경로)를 사용해 안정적으로 실행한다.
|
|
24
|
+
*/
|
|
25
|
+
function getTailwindCommand(args: string[]): string[] {
|
|
26
|
+
return [process.execPath, "x", ...args];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ========== Types ==========
|
|
30
|
+
|
|
31
|
+
export interface CSSBuildOptions {
|
|
32
|
+
/** 프로젝트 루트 디렉토리 */
|
|
33
|
+
rootDir: string;
|
|
34
|
+
/** CSS 입력 파일 (기본: "app/globals.css") */
|
|
35
|
+
input?: string;
|
|
36
|
+
/** CSS 출력 파일 (기본: ".mandu/client/globals.css") */
|
|
37
|
+
output?: string;
|
|
38
|
+
/** Watch 모드 활성화 */
|
|
39
|
+
watch?: boolean;
|
|
40
|
+
/** Minify 활성화 (production) */
|
|
41
|
+
minify?: boolean;
|
|
42
|
+
/** 빌드 완료 콜백 */
|
|
43
|
+
onBuild?: (result: CSSBuildResult) => void;
|
|
44
|
+
/** 에러 콜백 */
|
|
45
|
+
onError?: (error: Error) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CSSBuildResult {
|
|
49
|
+
success: boolean;
|
|
50
|
+
outputPath: string;
|
|
51
|
+
buildTime?: number;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CSSWatcher {
|
|
56
|
+
/** 출력 파일 경로 (절대 경로) */
|
|
57
|
+
outputPath: string;
|
|
58
|
+
/** 서버 경로 (/.mandu/client/globals.css) */
|
|
59
|
+
serverPath: string;
|
|
60
|
+
/** 감시 중지 */
|
|
61
|
+
close: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ========== Constants ==========
|
|
65
|
+
|
|
66
|
+
const DEFAULT_INPUT = "app/globals.css";
|
|
67
|
+
const DEFAULT_OUTPUT = ".mandu/client/globals.css";
|
|
68
|
+
const SERVER_CSS_PATH = "/.mandu/client/globals.css";
|
|
69
|
+
const CSS_REBUILD_DEBOUNCE = 150; // ms
|
|
70
|
+
|
|
71
|
+
// ========== Detection ==========
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Tailwind v4 프로젝트인지 감지
|
|
75
|
+
* app/globals.css에 @import "tailwindcss" 포함 여부 확인
|
|
76
|
+
*/
|
|
77
|
+
export async function isTailwindProject(rootDir: string): Promise<boolean> {
|
|
78
|
+
const cssPath = path.join(rootDir, DEFAULT_INPUT);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(cssPath, "utf-8");
|
|
82
|
+
return (
|
|
83
|
+
content.includes('@import "tailwindcss"') ||
|
|
84
|
+
content.includes("@import 'tailwindcss'") ||
|
|
85
|
+
content.includes("@tailwind base")
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* CSS 입력 파일 존재 여부 확인
|
|
94
|
+
*/
|
|
95
|
+
export async function hasCSSEntry(rootDir: string, input?: string): Promise<boolean> {
|
|
96
|
+
const cssPath = path.join(rootDir, input || DEFAULT_INPUT);
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(cssPath);
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ========== Build ==========
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* CSS 단발 빌드 (--watch 없이)
|
|
109
|
+
* Tailwind CLI --watch가 Bun.spawn에서 hang되므로 (#152) 단발 빌드만 사용
|
|
110
|
+
*/
|
|
111
|
+
async function runCSSBuild(
|
|
112
|
+
rootDir: string,
|
|
113
|
+
inputPath: string,
|
|
114
|
+
outputPath: string,
|
|
115
|
+
minify: boolean,
|
|
116
|
+
): Promise<CSSBuildResult> {
|
|
117
|
+
const startTime = performance.now();
|
|
118
|
+
const args = ["@tailwindcss/cli", "-i", inputPath, "-o", outputPath];
|
|
119
|
+
if (minify) args.push("--minify");
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const proc = spawn(getTailwindCommand(args), {
|
|
123
|
+
cwd: rootDir,
|
|
124
|
+
stdout: "pipe",
|
|
125
|
+
stderr: "pipe",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const exitCode = await proc.exited;
|
|
129
|
+
|
|
130
|
+
if (exitCode !== 0) {
|
|
131
|
+
const rawStderr = await new Response(proc.stderr).text();
|
|
132
|
+
// ANSI escape + 환경 경고 필터링
|
|
133
|
+
const stderr = rawStderr
|
|
134
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
135
|
+
.split("\n")
|
|
136
|
+
.filter((l) => l.trim() && !l.includes(".bash_profile") && !l.includes("$'\\377"))
|
|
137
|
+
.join("\n")
|
|
138
|
+
.trim();
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
outputPath,
|
|
142
|
+
error: stderr || `Tailwind CLI exited with code ${exitCode}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
outputPath,
|
|
149
|
+
buildTime: performance.now() - startTime,
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
outputPath,
|
|
155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* CSS 일회성 빌드 (production용)
|
|
162
|
+
*/
|
|
163
|
+
export async function buildCSS(options: CSSBuildOptions): Promise<CSSBuildResult> {
|
|
164
|
+
const {
|
|
165
|
+
rootDir,
|
|
166
|
+
input = DEFAULT_INPUT,
|
|
167
|
+
output = DEFAULT_OUTPUT,
|
|
168
|
+
minify = true,
|
|
169
|
+
} = options;
|
|
170
|
+
|
|
171
|
+
const inputPath = path.join(rootDir, input);
|
|
172
|
+
const outputPath = path.join(rootDir, output);
|
|
173
|
+
|
|
174
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
175
|
+
return runCSSBuild(rootDir, inputPath, outputPath, minify);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ========== Watch ==========
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* CSS 감시 모드 시작 (development용)
|
|
182
|
+
*
|
|
183
|
+
* #152: Tailwind CLI --watch 모드가 Bun.spawn에서 파일을 생성하지 않는 문제로 인해
|
|
184
|
+
* 자체 파일 감시 + 단발 빌드 반복 방식을 사용한다.
|
|
185
|
+
*
|
|
186
|
+
* 동작:
|
|
187
|
+
* 1. 초기 단발 빌드 실행 (await — 서버 시작 전 CSS 준비 보장)
|
|
188
|
+
* 2. app/, src/ 디렉토리 및 입력 CSS 파일 감시
|
|
189
|
+
* 3. 관련 파일 변경 시 debounce 후 단발 빌드 재실행
|
|
190
|
+
*/
|
|
191
|
+
export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatcher> {
|
|
192
|
+
const {
|
|
193
|
+
rootDir,
|
|
194
|
+
input = DEFAULT_INPUT,
|
|
195
|
+
output = DEFAULT_OUTPUT,
|
|
196
|
+
minify = false,
|
|
197
|
+
onBuild,
|
|
198
|
+
onError,
|
|
199
|
+
} = options;
|
|
200
|
+
|
|
201
|
+
const inputPath = path.join(rootDir, input);
|
|
202
|
+
const outputPath = path.join(rootDir, output);
|
|
203
|
+
|
|
204
|
+
// 출력 디렉토리 생성
|
|
205
|
+
try {
|
|
206
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const err = new Error(`CSS 출력 디렉토리 생성 실패: ${error instanceof Error ? error.message : error}`);
|
|
209
|
+
console.error(`❌ ${err.message}`);
|
|
210
|
+
onError?.(err);
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log(`🎨 Tailwind CSS v4 빌드 시작...`);
|
|
215
|
+
console.log(` 입력: ${input}`);
|
|
216
|
+
console.log(` 출력: ${output}`);
|
|
217
|
+
|
|
218
|
+
// 1. 초기 빌드 (await — 서버 시작 전 CSS 준비 보장)
|
|
219
|
+
const initialResult = await runCSSBuild(rootDir, inputPath, outputPath, minify);
|
|
220
|
+
|
|
221
|
+
if (initialResult.success) {
|
|
222
|
+
console.log(` ✅ CSS built (${Math.round(initialResult.buildTime ?? 0)}ms)`);
|
|
223
|
+
} else {
|
|
224
|
+
console.error(` ❌ CSS build failed: ${initialResult.error}`);
|
|
225
|
+
onError?.(new Error(initialResult.error));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 2. 파일 감시 설정 (CSS 소스, app/, src/ 디렉토리)
|
|
229
|
+
const watchers: FSWatcher[] = [];
|
|
230
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
231
|
+
let isBuilding = false;
|
|
232
|
+
let pendingRebuild = false;
|
|
233
|
+
|
|
234
|
+
const triggerRebuild = () => {
|
|
235
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
236
|
+
|
|
237
|
+
debounceTimer = setTimeout(async () => {
|
|
238
|
+
if (isBuilding) {
|
|
239
|
+
pendingRebuild = true;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isBuilding = true;
|
|
244
|
+
try {
|
|
245
|
+
const result = await runCSSBuild(rootDir, inputPath, outputPath, minify);
|
|
246
|
+
if (result.success) {
|
|
247
|
+
console.log(` ✅ CSS rebuilt (${Math.round(result.buildTime ?? 0)}ms)`);
|
|
248
|
+
onBuild?.(result);
|
|
249
|
+
} else {
|
|
250
|
+
console.error(` ❌ CSS rebuild failed: ${result.error}`);
|
|
251
|
+
onError?.(new Error(result.error));
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
255
|
+
console.error(` ❌ CSS rebuild error: ${error.message}`);
|
|
256
|
+
onError?.(error);
|
|
257
|
+
} finally {
|
|
258
|
+
isBuilding = false;
|
|
259
|
+
if (pendingRebuild) {
|
|
260
|
+
pendingRebuild = false;
|
|
261
|
+
triggerRebuild();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}, CSS_REBUILD_DEBOUNCE);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// CSS/TSX/HTML 파일 변경 시 리빌드 트리거
|
|
268
|
+
const isRelevantChange = (filename: string | null): boolean => {
|
|
269
|
+
if (!filename) return false;
|
|
270
|
+
const ext = path.extname(filename).toLowerCase();
|
|
271
|
+
return [".css", ".tsx", ".ts", ".jsx", ".js", ".html"].includes(ext);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// 감시 대상 디렉토리
|
|
275
|
+
const watchTargets = ["app", "src"];
|
|
276
|
+
|
|
277
|
+
for (const dir of watchTargets) {
|
|
278
|
+
const absDir = path.join(rootDir, dir);
|
|
279
|
+
try {
|
|
280
|
+
await fs.access(absDir);
|
|
281
|
+
const watcher = fsWatch(absDir, { recursive: true }, (_event, filename) => {
|
|
282
|
+
if (isRelevantChange(filename)) {
|
|
283
|
+
triggerRebuild();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
watchers.push(watcher);
|
|
287
|
+
} catch {
|
|
288
|
+
// 디렉토리 없으면 무시
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 입력 CSS 파일 직접 감시 (app/ 외부에 있을 수도 있으므로)
|
|
293
|
+
try {
|
|
294
|
+
const cssWatcher = fsWatch(inputPath, () => triggerRebuild());
|
|
295
|
+
watchers.push(cssWatcher);
|
|
296
|
+
} catch {
|
|
297
|
+
// 무시
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
outputPath,
|
|
302
|
+
serverPath: SERVER_CSS_PATH,
|
|
303
|
+
close: () => {
|
|
304
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
305
|
+
for (const w of watchers) w.close();
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* CSS 서버 경로 반환
|
|
312
|
+
*/
|
|
313
|
+
export function getCSSServerPath(): string {
|
|
314
|
+
return SERVER_CSS_PATH;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* CSS 링크 태그 생성
|
|
319
|
+
*/
|
|
320
|
+
export function generateCSSLinkTag(isDev: boolean = false): string {
|
|
321
|
+
const cacheBust = isDev ? `?t=${Date.now()}` : "";
|
|
322
|
+
return `<link rel="stylesheet" href="${SERVER_CSS_PATH}${cacheBust}">`;
|
|
323
|
+
}
|
package/src/bundler/dev.ts
CHANGED
|
@@ -559,7 +559,7 @@ export function generateHMRClientScript(port: number): string {
|
|
|
559
559
|
|
|
560
560
|
function connect() {
|
|
561
561
|
try {
|
|
562
|
-
ws = new WebSocket('ws://
|
|
562
|
+
ws = new WebSocket('ws://' + window.location.hostname + ':' + HMR_PORT);
|
|
563
563
|
|
|
564
564
|
ws.onopen = function() {
|
|
565
565
|
console.log('[Mandu HMR] Connected');
|
|
@@ -85,12 +85,16 @@ export type MCPConnectionStatus =
|
|
|
85
85
|
// ============================================================================
|
|
86
86
|
|
|
87
87
|
const DEFAULT_OPTIONS: Required<MCPConnectorOptions> = {
|
|
88
|
-
|
|
88
|
+
// #176: 포트 하드코딩 제거 — 현재 페이지의 호스트/포트에서 자동 유추
|
|
89
|
+
serverUrl: typeof window !== 'undefined'
|
|
90
|
+
? `ws://${window.location.hostname}:${window.location.port}/mcp`
|
|
91
|
+
: 'ws://localhost:3333/mcp',
|
|
89
92
|
connectionTimeout: 5000,
|
|
90
93
|
requestTimeout: 30000,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// #175: MCP 서버가 없을 수 있으므로 기본 비활성화
|
|
95
|
+
autoReconnect: false,
|
|
96
|
+
reconnectInterval: 1000,
|
|
97
|
+
maxReconnectAttempts: 3,
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
// ============================================================================
|
|
@@ -377,10 +381,11 @@ export class MCPConnector {
|
|
|
377
381
|
private handleDisconnect(): void {
|
|
378
382
|
this.ws = null;
|
|
379
383
|
|
|
380
|
-
|
|
384
|
+
// #175: handleConnectionError에서 이미 reconnect 예약한 경우 중복 방지
|
|
385
|
+
if (this.options.autoReconnect && this.status !== 'disconnected' && !this.reconnectTimer) {
|
|
381
386
|
this.setStatus('error');
|
|
382
387
|
this.scheduleReconnect();
|
|
383
|
-
} else {
|
|
388
|
+
} else if (!this.reconnectTimer) {
|
|
384
389
|
this.setStatus('disconnected');
|
|
385
390
|
}
|
|
386
391
|
}
|
|
@@ -389,12 +394,18 @@ export class MCPConnector {
|
|
|
389
394
|
console.warn('[Mandu Kitchen] MCP connection error:', error.message);
|
|
390
395
|
this.setStatus('error');
|
|
391
396
|
|
|
392
|
-
if (this.options.autoReconnect) {
|
|
397
|
+
if (this.options.autoReconnect && !this.reconnectTimer) {
|
|
393
398
|
this.scheduleReconnect();
|
|
394
399
|
}
|
|
395
400
|
}
|
|
396
401
|
|
|
397
402
|
private scheduleReconnect(): void {
|
|
403
|
+
// #175: 기존 타이머 취소 (중복 방지)
|
|
404
|
+
if (this.reconnectTimer) {
|
|
405
|
+
clearTimeout(this.reconnectTimer);
|
|
406
|
+
this.reconnectTimer = null;
|
|
407
|
+
}
|
|
408
|
+
|
|
398
409
|
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
399
410
|
console.warn('[Mandu Kitchen] Max reconnection attempts reached');
|
|
400
411
|
this.setStatus('disconnected');
|
|
@@ -402,15 +413,21 @@ export class MCPConnector {
|
|
|
402
413
|
}
|
|
403
414
|
|
|
404
415
|
this.reconnectAttempts++;
|
|
416
|
+
// #175: exponential backoff (1s → 2s → 4s → ... → max 30s)
|
|
417
|
+
const delay = Math.min(
|
|
418
|
+
this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
|
|
419
|
+
30_000,
|
|
420
|
+
);
|
|
405
421
|
console.log(
|
|
406
|
-
`[Mandu Kitchen] Reconnecting in ${
|
|
422
|
+
`[Mandu Kitchen] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`
|
|
407
423
|
);
|
|
408
424
|
|
|
409
425
|
this.reconnectTimer = setTimeout(() => {
|
|
426
|
+
this.reconnectTimer = null;
|
|
410
427
|
this.connect().catch(() => {
|
|
411
428
|
// 에러는 handleConnectionError에서 처리됨
|
|
412
429
|
});
|
|
413
|
-
},
|
|
430
|
+
}, delay);
|
|
414
431
|
}
|
|
415
432
|
|
|
416
433
|
private async sendRequest<T>(method: string, params: unknown): Promise<T> {
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -372,7 +372,7 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
|
|
|
372
372
|
|
|
373
373
|
function connect() {
|
|
374
374
|
try {
|
|
375
|
-
ws = new WebSocket('ws://
|
|
375
|
+
ws = new WebSocket('ws://' + window.location.hostname + ':${hmrPort}');
|
|
376
376
|
ws.onopen = function() {
|
|
377
377
|
console.log('[Mandu HMR] Connected');
|
|
378
378
|
reconnectAttempts = 0;
|
|
@@ -618,7 +618,7 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
|
|
|
618
618
|
|
|
619
619
|
function connect() {
|
|
620
620
|
try {
|
|
621
|
-
ws = new WebSocket('ws://
|
|
621
|
+
ws = new WebSocket('ws://' + window.location.hostname + ':${hmrPort}');
|
|
622
622
|
ws.onopen = function() {
|
|
623
623
|
console.log('[Mandu HMR] Connected');
|
|
624
624
|
reconnectAttempts = 0;
|