@mandujs/core 0.18.3 → 0.18.6
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 +8 -2
- package/src/bundler/build.ts +53 -16
- package/src/bundler/css.ts +337 -302
- package/src/bundler/dev.ts +63 -6
- package/src/bundler/types.ts +6 -0
- package/src/config/mandu.ts +1 -1
- package/src/config/validate.ts +1 -1
- package/src/contract/registry.ts +591 -568
- package/src/resource/generator.ts +5 -4
- package/src/router/fs-scanner.ts +4 -4
- package/src/runtime/escape.ts +12 -0
- package/src/runtime/server.ts +1 -1
- package/src/runtime/ssr.ts +27 -10
- package/src/runtime/streaming-ssr.ts +34 -7
package/src/bundler/css.ts
CHANGED
|
@@ -1,302 +1,337 @@
|
|
|
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
|
-
import { spawn, type Subprocess } from "bun";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import fs from "fs/promises";
|
|
14
|
-
|
|
15
|
-
// ========== Types ==========
|
|
16
|
-
|
|
17
|
-
export interface CSSBuildOptions {
|
|
18
|
-
/** 프로젝트 루트 디렉토리 */
|
|
19
|
-
rootDir: string;
|
|
20
|
-
/** CSS 입력 파일 (기본: "app/globals.css") */
|
|
21
|
-
input?: string;
|
|
22
|
-
/** CSS 출력 파일 (기본: ".mandu/client/globals.css") */
|
|
23
|
-
output?: string;
|
|
24
|
-
/** Watch 모드 활성화 */
|
|
25
|
-
watch?: boolean;
|
|
26
|
-
/** Minify 활성화 (production) */
|
|
27
|
-
minify?: boolean;
|
|
28
|
-
/** 빌드 완료 콜백 */
|
|
29
|
-
onBuild?: (result: CSSBuildResult) => void;
|
|
30
|
-
/** 에러 콜백 */
|
|
31
|
-
onError?: (error: Error) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface CSSBuildResult {
|
|
35
|
-
success: boolean;
|
|
36
|
-
outputPath: string;
|
|
37
|
-
buildTime?: number;
|
|
38
|
-
error?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface CSSWatcher {
|
|
42
|
-
/** Tailwind CLI 프로세스 */
|
|
43
|
-
process: Subprocess;
|
|
44
|
-
/** 출력 파일 경로 (절대 경로) */
|
|
45
|
-
outputPath: string;
|
|
46
|
-
/** 서버 경로 (/.mandu/client/globals.css) */
|
|
47
|
-
serverPath: string;
|
|
48
|
-
/** 프로세스 종료 */
|
|
49
|
-
close: () => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ========== Constants ==========
|
|
53
|
-
|
|
54
|
-
const DEFAULT_INPUT = "app/globals.css";
|
|
55
|
-
const DEFAULT_OUTPUT = ".mandu/client/globals.css";
|
|
56
|
-
const SERVER_CSS_PATH = "/.mandu/client/globals.css";
|
|
57
|
-
|
|
58
|
-
// ========== Detection ==========
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Tailwind v4 프로젝트인지 감지
|
|
62
|
-
* app/globals.css에 @import "tailwindcss" 포함 여부 확인
|
|
63
|
-
*/
|
|
64
|
-
export async function isTailwindProject(rootDir: string): Promise<boolean> {
|
|
65
|
-
const cssPath = path.join(rootDir, DEFAULT_INPUT);
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const content = await fs.readFile(cssPath, "utf-8");
|
|
69
|
-
// Tailwind v4: @import "tailwindcss"
|
|
70
|
-
// Tailwind v3: @tailwind base; @tailwind components; @tailwind utilities;
|
|
71
|
-
return (
|
|
72
|
-
content.includes('@import "tailwindcss"') ||
|
|
73
|
-
content.includes("@import 'tailwindcss'") ||
|
|
74
|
-
content.includes("@tailwind base")
|
|
75
|
-
);
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* CSS 입력 파일 존재 여부 확인
|
|
83
|
-
*/
|
|
84
|
-
export async function hasCSSEntry(rootDir: string, input?: string): Promise<boolean> {
|
|
85
|
-
const cssPath = path.join(rootDir, input || DEFAULT_INPUT);
|
|
86
|
-
try {
|
|
87
|
-
await fs.access(cssPath);
|
|
88
|
-
return true;
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ========== Build ==========
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* CSS 일회성 빌드 (production용)
|
|
98
|
-
*/
|
|
99
|
-
export async function buildCSS(options: CSSBuildOptions): Promise<CSSBuildResult> {
|
|
100
|
-
const {
|
|
101
|
-
rootDir,
|
|
102
|
-
input = DEFAULT_INPUT,
|
|
103
|
-
output = DEFAULT_OUTPUT,
|
|
104
|
-
minify = true,
|
|
105
|
-
} = options;
|
|
106
|
-
|
|
107
|
-
const inputPath = path.join(rootDir, input);
|
|
108
|
-
const outputPath = path.join(rootDir, output);
|
|
109
|
-
const startTime = performance.now();
|
|
110
|
-
|
|
111
|
-
// 출력 디렉토리 생성
|
|
112
|
-
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
113
|
-
|
|
114
|
-
// Tailwind CLI 실행
|
|
115
|
-
const args = [
|
|
116
|
-
"@tailwindcss/cli",
|
|
117
|
-
"-i", inputPath,
|
|
118
|
-
"-o", outputPath,
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
if (minify) {
|
|
122
|
-
args.push("--minify");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const proc = spawn(["bunx", ...args], {
|
|
127
|
-
cwd: rootDir,
|
|
128
|
-
stdout: "pipe",
|
|
129
|
-
stderr: "pipe",
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// 프로세스 완료 대기
|
|
133
|
-
const exitCode = await proc.exited;
|
|
134
|
-
|
|
135
|
-
if (exitCode !== 0) {
|
|
136
|
-
const stderr = await new Response(proc.stderr).text();
|
|
137
|
-
return {
|
|
138
|
-
success: false,
|
|
139
|
-
outputPath,
|
|
140
|
-
error: stderr || `Tailwind CLI exited with code ${exitCode}`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const buildTime = performance.now() - startTime;
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
success: true,
|
|
148
|
-
outputPath,
|
|
149
|
-
buildTime,
|
|
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
|
-
// ========== Watch ==========
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* CSS 감시 모드 시작 (development용)
|
|
164
|
-
* Tailwind CLI --watch 모드로 실행
|
|
165
|
-
*/
|
|
166
|
-
export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatcher> {
|
|
167
|
-
const {
|
|
168
|
-
rootDir,
|
|
169
|
-
input = DEFAULT_INPUT,
|
|
170
|
-
output = DEFAULT_OUTPUT,
|
|
171
|
-
minify = false,
|
|
172
|
-
onBuild,
|
|
173
|
-
onError,
|
|
174
|
-
} = options;
|
|
175
|
-
|
|
176
|
-
const inputPath = path.join(rootDir, input);
|
|
177
|
-
const outputPath = path.join(rootDir, output);
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
// 출력 디렉토리 생성
|
|
181
|
-
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
182
|
-
} catch (error) {
|
|
183
|
-
const err = new Error(`CSS 출력 디렉토리 생성 실패: ${error instanceof Error ? error.message : error}`);
|
|
184
|
-
console.error(`❌ ${err.message}`);
|
|
185
|
-
onError?.(err);
|
|
186
|
-
throw err;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Tailwind CLI 인자 구성
|
|
190
|
-
const args = [
|
|
191
|
-
"@tailwindcss/cli",
|
|
192
|
-
"-i", inputPath,
|
|
193
|
-
"-o", outputPath,
|
|
194
|
-
"--watch",
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
if (minify) {
|
|
198
|
-
args.push("--minify");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
console.log(`🎨 Tailwind CSS v4 빌드 시작...`);
|
|
202
|
-
console.log(` 입력: ${input}`);
|
|
203
|
-
console.log(` 출력: ${output}`);
|
|
204
|
-
|
|
205
|
-
// Bun subprocess로 Tailwind CLI 실행
|
|
206
|
-
let proc;
|
|
207
|
-
try {
|
|
208
|
-
proc = spawn(["bunx", ...args], {
|
|
209
|
-
cwd: rootDir,
|
|
210
|
-
stdout: "pipe",
|
|
211
|
-
stderr: "pipe",
|
|
212
|
-
});
|
|
213
|
-
} catch (error) {
|
|
214
|
-
const err = new Error(
|
|
215
|
-
`Tailwind CLI 실행 실패. @tailwindcss/cli가 설치되어 있는지 확인하세요.\n` +
|
|
216
|
-
`설치: bun add -d @tailwindcss/cli tailwindcss\n` +
|
|
217
|
-
`원인: ${error instanceof Error ? error.message : error}`
|
|
218
|
-
);
|
|
219
|
-
console.error(`❌ ${err.message}`);
|
|
220
|
-
onError?.(err);
|
|
221
|
-
throw err;
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
+
import { spawn, type Subprocess } from "bun";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
|
|
15
|
+
// ========== Types ==========
|
|
16
|
+
|
|
17
|
+
export interface CSSBuildOptions {
|
|
18
|
+
/** 프로젝트 루트 디렉토리 */
|
|
19
|
+
rootDir: string;
|
|
20
|
+
/** CSS 입력 파일 (기본: "app/globals.css") */
|
|
21
|
+
input?: string;
|
|
22
|
+
/** CSS 출력 파일 (기본: ".mandu/client/globals.css") */
|
|
23
|
+
output?: string;
|
|
24
|
+
/** Watch 모드 활성화 */
|
|
25
|
+
watch?: boolean;
|
|
26
|
+
/** Minify 활성화 (production) */
|
|
27
|
+
minify?: boolean;
|
|
28
|
+
/** 빌드 완료 콜백 */
|
|
29
|
+
onBuild?: (result: CSSBuildResult) => void;
|
|
30
|
+
/** 에러 콜백 */
|
|
31
|
+
onError?: (error: Error) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CSSBuildResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
outputPath: string;
|
|
37
|
+
buildTime?: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CSSWatcher {
|
|
42
|
+
/** Tailwind CLI 프로세스 */
|
|
43
|
+
process: Subprocess;
|
|
44
|
+
/** 출력 파일 경로 (절대 경로) */
|
|
45
|
+
outputPath: string;
|
|
46
|
+
/** 서버 경로 (/.mandu/client/globals.css) */
|
|
47
|
+
serverPath: string;
|
|
48
|
+
/** 프로세스 종료 */
|
|
49
|
+
close: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ========== Constants ==========
|
|
53
|
+
|
|
54
|
+
const DEFAULT_INPUT = "app/globals.css";
|
|
55
|
+
const DEFAULT_OUTPUT = ".mandu/client/globals.css";
|
|
56
|
+
const SERVER_CSS_PATH = "/.mandu/client/globals.css";
|
|
57
|
+
|
|
58
|
+
// ========== Detection ==========
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Tailwind v4 프로젝트인지 감지
|
|
62
|
+
* app/globals.css에 @import "tailwindcss" 포함 여부 확인
|
|
63
|
+
*/
|
|
64
|
+
export async function isTailwindProject(rootDir: string): Promise<boolean> {
|
|
65
|
+
const cssPath = path.join(rootDir, DEFAULT_INPUT);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = await fs.readFile(cssPath, "utf-8");
|
|
69
|
+
// Tailwind v4: @import "tailwindcss"
|
|
70
|
+
// Tailwind v3: @tailwind base; @tailwind components; @tailwind utilities;
|
|
71
|
+
return (
|
|
72
|
+
content.includes('@import "tailwindcss"') ||
|
|
73
|
+
content.includes("@import 'tailwindcss'") ||
|
|
74
|
+
content.includes("@tailwind base")
|
|
75
|
+
);
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* CSS 입력 파일 존재 여부 확인
|
|
83
|
+
*/
|
|
84
|
+
export async function hasCSSEntry(rootDir: string, input?: string): Promise<boolean> {
|
|
85
|
+
const cssPath = path.join(rootDir, input || DEFAULT_INPUT);
|
|
86
|
+
try {
|
|
87
|
+
await fs.access(cssPath);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ========== Build ==========
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* CSS 일회성 빌드 (production용)
|
|
98
|
+
*/
|
|
99
|
+
export async function buildCSS(options: CSSBuildOptions): Promise<CSSBuildResult> {
|
|
100
|
+
const {
|
|
101
|
+
rootDir,
|
|
102
|
+
input = DEFAULT_INPUT,
|
|
103
|
+
output = DEFAULT_OUTPUT,
|
|
104
|
+
minify = true,
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const inputPath = path.join(rootDir, input);
|
|
108
|
+
const outputPath = path.join(rootDir, output);
|
|
109
|
+
const startTime = performance.now();
|
|
110
|
+
|
|
111
|
+
// 출력 디렉토리 생성
|
|
112
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
113
|
+
|
|
114
|
+
// Tailwind CLI 실행
|
|
115
|
+
const args = [
|
|
116
|
+
"@tailwindcss/cli",
|
|
117
|
+
"-i", inputPath,
|
|
118
|
+
"-o", outputPath,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
if (minify) {
|
|
122
|
+
args.push("--minify");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const proc = spawn(["bunx", ...args], {
|
|
127
|
+
cwd: rootDir,
|
|
128
|
+
stdout: "pipe",
|
|
129
|
+
stderr: "pipe",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 프로세스 완료 대기
|
|
133
|
+
const exitCode = await proc.exited;
|
|
134
|
+
|
|
135
|
+
if (exitCode !== 0) {
|
|
136
|
+
const stderr = await new Response(proc.stderr).text();
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
outputPath,
|
|
140
|
+
error: stderr || `Tailwind CLI exited with code ${exitCode}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const buildTime = performance.now() - startTime;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
outputPath,
|
|
149
|
+
buildTime,
|
|
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
|
+
// ========== Watch ==========
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* CSS 감시 모드 시작 (development용)
|
|
164
|
+
* Tailwind CLI --watch 모드로 실행
|
|
165
|
+
*/
|
|
166
|
+
export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatcher> {
|
|
167
|
+
const {
|
|
168
|
+
rootDir,
|
|
169
|
+
input = DEFAULT_INPUT,
|
|
170
|
+
output = DEFAULT_OUTPUT,
|
|
171
|
+
minify = false,
|
|
172
|
+
onBuild,
|
|
173
|
+
onError,
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
const inputPath = path.join(rootDir, input);
|
|
177
|
+
const outputPath = path.join(rootDir, output);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
// 출력 디렉토리 생성
|
|
181
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const err = new Error(`CSS 출력 디렉토리 생성 실패: ${error instanceof Error ? error.message : error}`);
|
|
184
|
+
console.error(`❌ ${err.message}`);
|
|
185
|
+
onError?.(err);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Tailwind CLI 인자 구성
|
|
190
|
+
const args = [
|
|
191
|
+
"@tailwindcss/cli",
|
|
192
|
+
"-i", inputPath,
|
|
193
|
+
"-o", outputPath,
|
|
194
|
+
"--watch",
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
if (minify) {
|
|
198
|
+
args.push("--minify");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`🎨 Tailwind CSS v4 빌드 시작...`);
|
|
202
|
+
console.log(` 입력: ${input}`);
|
|
203
|
+
console.log(` 출력: ${output}`);
|
|
204
|
+
|
|
205
|
+
// Bun subprocess로 Tailwind CLI 실행
|
|
206
|
+
let proc;
|
|
207
|
+
try {
|
|
208
|
+
proc = spawn(["bunx", ...args], {
|
|
209
|
+
cwd: rootDir,
|
|
210
|
+
stdout: "pipe",
|
|
211
|
+
stderr: "pipe",
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const err = new Error(
|
|
215
|
+
`Tailwind CLI 실행 실패. @tailwindcss/cli가 설치되어 있는지 확인하세요.\n` +
|
|
216
|
+
`설치: bun add -d @tailwindcss/cli tailwindcss\n` +
|
|
217
|
+
`원인: ${error instanceof Error ? error.message : error}`
|
|
218
|
+
);
|
|
219
|
+
console.error(`❌ ${err.message}`);
|
|
220
|
+
onError?.(err);
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 출력 파일 워처로 빌드 완료 감지 (stdout 패턴보다 신뢰성 높음, #111)
|
|
225
|
+
// Tailwind CLI stdout 출력 형식은 버전마다 달라질 수 있으므로 파일 변경으로 감지
|
|
226
|
+
let fsWatcher: ReturnType<typeof fs.watch> | null = null;
|
|
227
|
+
let lastMtime = 0;
|
|
228
|
+
|
|
229
|
+
const startFileWatcher = () => {
|
|
230
|
+
try {
|
|
231
|
+
fsWatcher = fs.watch(outputPath, () => {
|
|
232
|
+
// 연속 이벤트 중복 방지 (50ms 이내 재발생 무시)
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
if (now - lastMtime < 50) return;
|
|
235
|
+
lastMtime = now;
|
|
236
|
+
console.log(` ✅ CSS rebuilt`);
|
|
237
|
+
onBuild?.({ success: true, outputPath });
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
// 파일이 아직 없으면 500ms 후 재시도
|
|
241
|
+
setTimeout(startFileWatcher, 500);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// stdout 로그용 (빌드 시작/완료 메시지 표시)
|
|
246
|
+
(async () => {
|
|
247
|
+
const reader = proc.stdout.getReader();
|
|
248
|
+
const decoder = new TextDecoder();
|
|
249
|
+
|
|
250
|
+
while (true) {
|
|
251
|
+
const { done, value } = await reader.read();
|
|
252
|
+
if (done) break;
|
|
253
|
+
|
|
254
|
+
const text = decoder.decode(value);
|
|
255
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
256
|
+
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
if (line.includes("warn") || line.includes("Warning")) {
|
|
259
|
+
console.log(` ⚠️ CSS ${line.trim()}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
})();
|
|
264
|
+
|
|
265
|
+
// 초기 빌드 완료 후 파일 워처 시작
|
|
266
|
+
startFileWatcher();
|
|
267
|
+
|
|
268
|
+
// stderr 모니터링 (에러 감지)
|
|
269
|
+
(async () => {
|
|
270
|
+
const reader = proc.stderr.getReader();
|
|
271
|
+
const decoder = new TextDecoder();
|
|
272
|
+
|
|
273
|
+
while (true) {
|
|
274
|
+
const { done, value } = await reader.read();
|
|
275
|
+
if (done) break;
|
|
276
|
+
|
|
277
|
+
const text = decoder.decode(value).trim();
|
|
278
|
+
if (text) {
|
|
279
|
+
// 환경 경고 무시
|
|
280
|
+
if (text.includes(".bash_profile") || text.includes("$'\\377")) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// Tailwind CLI 정상 진행 메시지는 info 레벨로 처리
|
|
284
|
+
// (패키지 해석, 다운로드, 잠금 파일 등은 정상 동작)
|
|
285
|
+
if (
|
|
286
|
+
text.includes("Resolving dependencies") ||
|
|
287
|
+
text.includes("Resolved, downloaded") ||
|
|
288
|
+
text.includes("Saved lockfile") ||
|
|
289
|
+
text.includes("≈ tailwindcss") ||
|
|
290
|
+
text.match(/^v?\d+\.\d+\.\d+/) // 버전 출력
|
|
291
|
+
) {
|
|
292
|
+
if (text) console.log(` ℹ️ CSS: ${text}`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
console.error(` ❌ CSS Error: ${text}`);
|
|
296
|
+
onError?.(new Error(text));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
})();
|
|
300
|
+
|
|
301
|
+
// 프로세스 종료 감지
|
|
302
|
+
proc.exited.then((code) => {
|
|
303
|
+
if (code !== 0 && code !== null) {
|
|
304
|
+
console.error(` ❌ Tailwind CLI exited with code ${code}`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
process: proc,
|
|
310
|
+
outputPath,
|
|
311
|
+
serverPath: SERVER_CSS_PATH,
|
|
312
|
+
close: () => {
|
|
313
|
+
fsWatcher?.close();
|
|
314
|
+
// Windows에서는 SIGTERM이 무시될 수 있으므로 SIGKILL 사용 (#117)
|
|
315
|
+
if (process.platform === "win32") {
|
|
316
|
+
proc.kill("SIGKILL");
|
|
317
|
+
} else {
|
|
318
|
+
proc.kill();
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* CSS 서버 경로 반환
|
|
326
|
+
*/
|
|
327
|
+
export function getCSSServerPath(): string {
|
|
328
|
+
return SERVER_CSS_PATH;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* CSS 링크 태그 생성
|
|
333
|
+
*/
|
|
334
|
+
export function generateCSSLinkTag(isDev: boolean = false): string {
|
|
335
|
+
const cacheBust = isDev ? `?t=${Date.now()}` : "";
|
|
336
|
+
return `<link rel="stylesheet" href="${SERVER_CSS_PATH}${cacheBust}">`;
|
|
337
|
+
}
|