@simplysm/sd-cli 14.0.38 → 14.0.39
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/dist/angular/angular-build-pipeline.d.ts +1 -1
- package/dist/angular/angular-build-pipeline.js +1 -1
- package/dist/angular/client-transform-stylesheet.d.ts +1 -1
- package/dist/angular/client-transform-stylesheet.js +3 -3
- package/dist/esbuild/esbuild-client-config.d.ts +0 -2
- package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +19 -9
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
- package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
- package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
- package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
- package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
- package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +32 -2
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +129 -90
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts +27 -0
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +43 -3
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/package.json +6 -4
- package/src/angular/angular-build-pipeline.ts +2 -2
- package/src/angular/client-transform-stylesheet.ts +4 -4
- package/src/esbuild/esbuild-client-config.ts +21 -12
- package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
- package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
- package/src/workers/client.worker.ts +32 -2
- package/src/workers/server-build.worker.ts +136 -97
- package/src/workers/server-esbuild-context.ts +59 -3
- package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
- package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
- package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
- package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
- package/tests/utils/esbuild-client-config.acc.spec.ts +26 -14
- package/tests/utils/esbuild-client-config.spec.ts +73 -11
- package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
- package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
- package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
- package/tests/workers/server-build-lint.spec.ts +43 -0
- package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
- package/tests/workers/server-build-worker.spec.ts +122 -9
- package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
- package/tests/workers/server-esbuild-context.acc.spec.ts +156 -2
- package/tests/workers/server-esbuild-context.spec.ts +320 -2
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import postcss from "postcss";
|
|
6
|
+
import type esbuild from "esbuild";
|
|
7
|
+
import { createPostcssPlugin } from "../../src/esbuild/esbuild-postcss-plugin";
|
|
8
|
+
|
|
9
|
+
// --- Helpers ---
|
|
10
|
+
|
|
11
|
+
/** PostCSS 플러그인: CSS 앞에 마커 주석을 추가한다 */
|
|
12
|
+
function markerPlugin(): postcss.Plugin {
|
|
13
|
+
return {
|
|
14
|
+
postcssPlugin: "test-marker",
|
|
15
|
+
Once(root) {
|
|
16
|
+
root.prepend(new postcss.Comment({ text: "marker" }));
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** PostCSS 플러그인: 항상 예외를 발생시킨다 */
|
|
22
|
+
function throwingPlugin(): postcss.Plugin {
|
|
23
|
+
return {
|
|
24
|
+
postcssPlugin: "test-throwing",
|
|
25
|
+
Once() {
|
|
26
|
+
throw new Error("postcss-boom");
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** esbuild 플러그인에서 onEnd 콜백을 캡처한다 */
|
|
32
|
+
function captureOnEnd(
|
|
33
|
+
plugin: esbuild.Plugin,
|
|
34
|
+
): (result: esbuild.BuildResult) => Promise<void> | void {
|
|
35
|
+
let cb!: (result: esbuild.BuildResult) => Promise<void> | void;
|
|
36
|
+
void plugin.setup({
|
|
37
|
+
onEnd(fn: (result: esbuild.BuildResult) => Promise<void> | void) {
|
|
38
|
+
cb = fn;
|
|
39
|
+
},
|
|
40
|
+
} as unknown as esbuild.PluginBuild);
|
|
41
|
+
return cb;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 최소한의 esbuild BuildResult를 생성한다 */
|
|
45
|
+
function buildResult(outputs: Record<string, unknown>): esbuild.BuildResult {
|
|
46
|
+
return {
|
|
47
|
+
errors: [] as esbuild.Message[],
|
|
48
|
+
warnings: [] as esbuild.Message[],
|
|
49
|
+
metafile: { inputs: {}, outputs } as esbuild.Metafile,
|
|
50
|
+
outputFiles: [],
|
|
51
|
+
mangleCache: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Tests ---
|
|
56
|
+
|
|
57
|
+
describe("createPostcssPlugin — Acceptance", () => {
|
|
58
|
+
let tmpDir: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Scenario: .css 파일에 PostCSS 적용
|
|
69
|
+
it(".css 파일의 내용이 PostCSS 플러그인으로 변환되어 덮어쓰기된다", async () => {
|
|
70
|
+
const cssFile = path.join(tmpDir, "main.css");
|
|
71
|
+
fs.writeFileSync(cssFile, ".host { display: flex; }");
|
|
72
|
+
|
|
73
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
74
|
+
const onEnd = captureOnEnd(plugin);
|
|
75
|
+
|
|
76
|
+
await onEnd(
|
|
77
|
+
buildResult({
|
|
78
|
+
[cssFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const output = fs.readFileSync(cssFile, "utf-8");
|
|
83
|
+
expect(output).toContain("/* marker */");
|
|
84
|
+
expect(output).toContain("display: flex");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Scenario: .css 확장자가 아닌 파일은 CSS 전체 처리 대상에서 제외
|
|
88
|
+
it(".js 파일은 CSS 전체 적용 로직에서 처리되지 않아 원본이 유지된다", async () => {
|
|
89
|
+
const jsFile = path.join(tmpDir, "main.js");
|
|
90
|
+
const original = 'console.log("hello");';
|
|
91
|
+
fs.writeFileSync(jsFile, original);
|
|
92
|
+
|
|
93
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
94
|
+
const onEnd = captureOnEnd(plugin);
|
|
95
|
+
|
|
96
|
+
await onEnd(
|
|
97
|
+
buildResult({
|
|
98
|
+
[jsFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(original);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Scenario: 빈 PostCSS 플러그인 배열로 생성 시 파일 처리 없음
|
|
106
|
+
it("빈 플러그인 배열이면 .css 파일을 읽지도 수정하지도 않는다", async () => {
|
|
107
|
+
const cssFile = path.join(tmpDir, "main.css");
|
|
108
|
+
const original = ".host { display: flex; }";
|
|
109
|
+
fs.writeFileSync(cssFile, original);
|
|
110
|
+
|
|
111
|
+
const plugin = createPostcssPlugin({ plugins: [] });
|
|
112
|
+
const onEnd = captureOnEnd(plugin);
|
|
113
|
+
|
|
114
|
+
await onEnd(
|
|
115
|
+
buildResult({
|
|
116
|
+
[cssFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(fs.readFileSync(cssFile, "utf-8")).toBe(original);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Scenario: .css 파일 PostCSS 적용 실패 시 esbuild error 추가
|
|
124
|
+
it("PostCSS 실패 시 result.errors에 파일 경로와 에러 메시지가 추가된다", async () => {
|
|
125
|
+
const cssFile = path.join(tmpDir, "broken.css");
|
|
126
|
+
fs.writeFileSync(cssFile, ".host { display: flex; }");
|
|
127
|
+
|
|
128
|
+
const plugin = createPostcssPlugin({ plugins: [throwingPlugin()] });
|
|
129
|
+
const onEnd = captureOnEnd(plugin);
|
|
130
|
+
|
|
131
|
+
const result = buildResult({
|
|
132
|
+
[cssFile]: { bytes: 100, inputs: {}, imports: [], exports: [] },
|
|
133
|
+
});
|
|
134
|
+
await onEnd(result);
|
|
135
|
+
|
|
136
|
+
expect(result.errors).toHaveLength(1);
|
|
137
|
+
expect(result.errors[0].text).toContain("postcss-boom");
|
|
138
|
+
expect(result.errors[0].text).toContain(cssFile);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// --- Slice 2: .js 파일 AST 기반 inline CSS ---
|
|
142
|
+
|
|
143
|
+
// Scenario: ɵɵdefineComponent 내 단일 styles 문자열에 PostCSS 적용
|
|
144
|
+
it("ɵɵdefineComponent 내 styles 문자열에 PostCSS가 적용되어 내용이 변환된다", async () => {
|
|
145
|
+
const jsFile = path.join(tmpDir, "main.js");
|
|
146
|
+
const code = [
|
|
147
|
+
'import * as i0 from "@angular/core";',
|
|
148
|
+
"class MyComp {}",
|
|
149
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
150
|
+
' styles: [".host { display: flex; }"]',
|
|
151
|
+
"});",
|
|
152
|
+
].join("\n");
|
|
153
|
+
fs.writeFileSync(jsFile, code);
|
|
154
|
+
|
|
155
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
156
|
+
const onEnd = captureOnEnd(plugin);
|
|
157
|
+
|
|
158
|
+
await onEnd(
|
|
159
|
+
buildResult({
|
|
160
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
165
|
+
expect(output).toContain("/* marker */");
|
|
166
|
+
expect(output).toContain("display: flex");
|
|
167
|
+
// 원본 JS 구조가 유지되어야 한다
|
|
168
|
+
expect(output).toContain("ɵɵdefineComponent");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Scenario: ɵɵdefineComponent 내 복수 styles 문자열에 각각 PostCSS 적용
|
|
172
|
+
it("복수 styles 문자열에 각각 독립적으로 PostCSS가 적용된다", async () => {
|
|
173
|
+
const jsFile = path.join(tmpDir, "multi.js");
|
|
174
|
+
const code = [
|
|
175
|
+
'import * as i0 from "@angular/core";',
|
|
176
|
+
"class MyComp {}",
|
|
177
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
178
|
+
' styles: [".a { color: red; }", ".b { color: blue; }"]',
|
|
179
|
+
"});",
|
|
180
|
+
].join("\n");
|
|
181
|
+
fs.writeFileSync(jsFile, code);
|
|
182
|
+
|
|
183
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
184
|
+
const onEnd = captureOnEnd(plugin);
|
|
185
|
+
|
|
186
|
+
await onEnd(
|
|
187
|
+
buildResult({
|
|
188
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
193
|
+
// 마커가 각 문자열에 독립적으로 적용됨 → 2회 등장
|
|
194
|
+
const markerCount = (output.match(/\/\* marker \*\//g) ?? []).length;
|
|
195
|
+
expect(markerCount).toBe(2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Scenario: styles 배열이 비어 있으면 파일 수정 안 함
|
|
199
|
+
it("styles 배열이 비어 있으면 .js 파일이 수정되지 않는다", async () => {
|
|
200
|
+
const jsFile = path.join(tmpDir, "empty.js");
|
|
201
|
+
const code = [
|
|
202
|
+
'import * as i0 from "@angular/core";',
|
|
203
|
+
"class MyComp {}",
|
|
204
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
205
|
+
" styles: []",
|
|
206
|
+
"});",
|
|
207
|
+
].join("\n");
|
|
208
|
+
fs.writeFileSync(jsFile, code);
|
|
209
|
+
|
|
210
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
211
|
+
const onEnd = captureOnEnd(plugin);
|
|
212
|
+
|
|
213
|
+
await onEnd(
|
|
214
|
+
buildResult({
|
|
215
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(code);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Scenario: ɵɵdefineComponent 외부의 styles 속성은 무시
|
|
223
|
+
it("ɵɵdefineComponent 외부의 styles 속성은 처리되지 않는다", async () => {
|
|
224
|
+
const jsFile = path.join(tmpDir, "outside.js");
|
|
225
|
+
const code = [
|
|
226
|
+
'const config = { styles: [".host { display: flex; }"] };',
|
|
227
|
+
"export default config;",
|
|
228
|
+
].join("\n");
|
|
229
|
+
fs.writeFileSync(jsFile, code);
|
|
230
|
+
|
|
231
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
232
|
+
const onEnd = captureOnEnd(plugin);
|
|
233
|
+
|
|
234
|
+
await onEnd(
|
|
235
|
+
buildResult({
|
|
236
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(code);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Scenario: 한 .js 파일 내 여러 ɵɵdefineComponent의 styles 모두 처리
|
|
244
|
+
it("한 파일 내 여러 ɵɵdefineComponent의 styles 모두에 PostCSS가 적용된다", async () => {
|
|
245
|
+
const jsFile = path.join(tmpDir, "bundle.js");
|
|
246
|
+
const code = [
|
|
247
|
+
'import * as i0 from "@angular/core";',
|
|
248
|
+
"class CompA {}",
|
|
249
|
+
"CompA.ɵcmp = i0.ɵɵdefineComponent({",
|
|
250
|
+
' styles: [".a { color: red; }"]',
|
|
251
|
+
"});",
|
|
252
|
+
"class CompB {}",
|
|
253
|
+
"CompB.ɵcmp = i0.ɵɵdefineComponent({",
|
|
254
|
+
' styles: [".b { color: blue; }"]',
|
|
255
|
+
"});",
|
|
256
|
+
].join("\n");
|
|
257
|
+
fs.writeFileSync(jsFile, code);
|
|
258
|
+
|
|
259
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
260
|
+
const onEnd = captureOnEnd(plugin);
|
|
261
|
+
|
|
262
|
+
await onEnd(
|
|
263
|
+
buildResult({
|
|
264
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
269
|
+
const markerCount = (output.match(/\/\* marker \*\//g) ?? []).length;
|
|
270
|
+
expect(markerCount).toBe(2);
|
|
271
|
+
expect(output).toContain("color: red");
|
|
272
|
+
expect(output).toContain("color: blue");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Scenario: .js 파일 inline CSS PostCSS 적용 실패 시 esbuild error 추가
|
|
276
|
+
it(".js inline CSS PostCSS 실패 시 result.errors에 에러가 추가된다", async () => {
|
|
277
|
+
const jsFile = path.join(tmpDir, "fail.js");
|
|
278
|
+
const code = [
|
|
279
|
+
'import * as i0 from "@angular/core";',
|
|
280
|
+
"class MyComp {}",
|
|
281
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
282
|
+
' styles: [".host { display: flex; }"]',
|
|
283
|
+
"});",
|
|
284
|
+
].join("\n");
|
|
285
|
+
fs.writeFileSync(jsFile, code);
|
|
286
|
+
|
|
287
|
+
const plugin = createPostcssPlugin({ plugins: [throwingPlugin()] });
|
|
288
|
+
const onEnd = captureOnEnd(plugin);
|
|
289
|
+
|
|
290
|
+
const result = buildResult({
|
|
291
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
292
|
+
});
|
|
293
|
+
await onEnd(result);
|
|
294
|
+
|
|
295
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
|
296
|
+
expect(result.errors[0].text).toContain("postcss-boom");
|
|
297
|
+
expect(result.errors[0].text).toContain(jsFile);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import postcss from "postcss";
|
|
6
|
+
import type esbuild from "esbuild";
|
|
7
|
+
import { createPostcssPlugin } from "../../src/esbuild/esbuild-postcss-plugin";
|
|
8
|
+
|
|
9
|
+
// --- Helpers ---
|
|
10
|
+
|
|
11
|
+
function markerPlugin(): postcss.Plugin {
|
|
12
|
+
return {
|
|
13
|
+
postcssPlugin: "test-marker",
|
|
14
|
+
Once(root) {
|
|
15
|
+
root.prepend(new postcss.Comment({ text: "marker" }));
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function captureOnEnd(
|
|
21
|
+
plugin: esbuild.Plugin,
|
|
22
|
+
): (result: esbuild.BuildResult) => Promise<void> | void {
|
|
23
|
+
let cb!: (result: esbuild.BuildResult) => Promise<void> | void;
|
|
24
|
+
void plugin.setup({
|
|
25
|
+
onEnd(fn: (result: esbuild.BuildResult) => Promise<void> | void) {
|
|
26
|
+
cb = fn;
|
|
27
|
+
},
|
|
28
|
+
} as unknown as esbuild.PluginBuild);
|
|
29
|
+
return cb;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildResult(
|
|
33
|
+
outputs: Record<string, unknown>,
|
|
34
|
+
): esbuild.BuildResult {
|
|
35
|
+
return {
|
|
36
|
+
errors: [] as esbuild.Message[],
|
|
37
|
+
warnings: [] as esbuild.Message[],
|
|
38
|
+
metafile: { inputs: {}, outputs } as esbuild.Metafile,
|
|
39
|
+
outputFiles: [],
|
|
40
|
+
mangleCache: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Tests ---
|
|
45
|
+
|
|
46
|
+
describe("createPostcssPlugin — 플러그인 구조", () => {
|
|
47
|
+
it("플러그인 이름이 sd-postcss이다", () => {
|
|
48
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
49
|
+
expect(plugin.name).toBe("sd-postcss");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("onEnd 콜백이 등록된다", () => {
|
|
53
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
54
|
+
let registered = false;
|
|
55
|
+
void plugin.setup({
|
|
56
|
+
onEnd() {
|
|
57
|
+
registered = true;
|
|
58
|
+
},
|
|
59
|
+
} as unknown as esbuild.PluginBuild);
|
|
60
|
+
expect(registered).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("createPostcssPlugin — metafile 가드", () => {
|
|
65
|
+
let tmpDir: string;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("metafile이 null이면 아무 처리도 하지 않는다", async () => {
|
|
76
|
+
const cssFile = path.join(tmpDir, "main.css");
|
|
77
|
+
const original = ".host { display: flex; }";
|
|
78
|
+
fs.writeFileSync(cssFile, original);
|
|
79
|
+
|
|
80
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
81
|
+
const onEnd = captureOnEnd(plugin);
|
|
82
|
+
|
|
83
|
+
await onEnd({
|
|
84
|
+
errors: [],
|
|
85
|
+
warnings: [],
|
|
86
|
+
metafile: undefined,
|
|
87
|
+
outputFiles: [],
|
|
88
|
+
mangleCache: {},
|
|
89
|
+
} as unknown as esbuild.BuildResult);
|
|
90
|
+
|
|
91
|
+
expect(fs.readFileSync(cssFile, "utf-8")).toBe(original);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("createPostcssPlugin — .css 파일 처리", () => {
|
|
96
|
+
let tmpDir: string;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("여러 .css 파일이 있으면 모두 처리된다", async () => {
|
|
107
|
+
const css1 = path.join(tmpDir, "a.css");
|
|
108
|
+
const css2 = path.join(tmpDir, "b.css");
|
|
109
|
+
fs.writeFileSync(css1, ".a { color: red; }");
|
|
110
|
+
fs.writeFileSync(css2, ".b { color: blue; }");
|
|
111
|
+
|
|
112
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
113
|
+
const onEnd = captureOnEnd(plugin);
|
|
114
|
+
|
|
115
|
+
await onEnd(
|
|
116
|
+
buildResult({
|
|
117
|
+
[css1]: { bytes: 10, inputs: {}, imports: [], exports: [] },
|
|
118
|
+
[css2]: { bytes: 10, inputs: {}, imports: [], exports: [] },
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(fs.readFileSync(css1, "utf-8")).toContain("/* marker */");
|
|
123
|
+
expect(fs.readFileSync(css2, "utf-8")).toContain("/* marker */");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("outputs에 .css 파일이 없으면 파일을 읽지 않는다", async () => {
|
|
127
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
128
|
+
const onEnd = captureOnEnd(plugin);
|
|
129
|
+
|
|
130
|
+
// .js와 .map만 있는 outputs — 에러 없이 정상 완료
|
|
131
|
+
await onEnd(
|
|
132
|
+
buildResult({
|
|
133
|
+
"dist/main.js": { bytes: 10, inputs: {}, imports: [], exports: [] },
|
|
134
|
+
"dist/main.js.map": { bytes: 10, inputs: {}, imports: [], exports: [] },
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// 에러 없이 완료되면 성공
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("PostCSS 에러 메시지에 파일 경로가 포함된다", async () => {
|
|
142
|
+
const cssFile = path.join(tmpDir, "err.css");
|
|
143
|
+
fs.writeFileSync(cssFile, ".host { color: red; }");
|
|
144
|
+
|
|
145
|
+
const throwing: postcss.Plugin = {
|
|
146
|
+
postcssPlugin: "throw-with-detail",
|
|
147
|
+
Once() {
|
|
148
|
+
throw new Error("detailed-error-message");
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const plugin = createPostcssPlugin({ plugins: [throwing] });
|
|
153
|
+
const onEnd = captureOnEnd(plugin);
|
|
154
|
+
|
|
155
|
+
const result = buildResult({
|
|
156
|
+
[cssFile]: { bytes: 10, inputs: {}, imports: [], exports: [] },
|
|
157
|
+
});
|
|
158
|
+
await onEnd(result);
|
|
159
|
+
|
|
160
|
+
expect(result.errors[0].text).toContain(cssFile);
|
|
161
|
+
expect(result.errors[0].text).toContain("detailed-error-message");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("createPostcssPlugin — .js 파일 사전 필터링", () => {
|
|
166
|
+
let tmpDir: string;
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("styles 문자열이 없는 .js 파일은 원본이 유지된다", async () => {
|
|
177
|
+
const jsFile = path.join(tmpDir, "chunk.js");
|
|
178
|
+
const original = 'export const x = "no relevant content";';
|
|
179
|
+
fs.writeFileSync(jsFile, original);
|
|
180
|
+
|
|
181
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
182
|
+
const onEnd = captureOnEnd(plugin);
|
|
183
|
+
|
|
184
|
+
await onEnd(
|
|
185
|
+
buildResult({
|
|
186
|
+
[jsFile]: { bytes: 50, inputs: {}, imports: [], exports: [] },
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(original);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("createPostcssPlugin — .js AST 기반 styles 추출", () => {
|
|
195
|
+
let tmpDir: string;
|
|
196
|
+
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("styles 배열에 Identifier(변수 참조)만 있으면 파일이 수정되지 않는다", async () => {
|
|
206
|
+
const jsFile = path.join(tmpDir, "varref.js");
|
|
207
|
+
const code = [
|
|
208
|
+
'import * as i0 from "@angular/core";',
|
|
209
|
+
"const _c0 = [];",
|
|
210
|
+
"class MyComp {}",
|
|
211
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
212
|
+
" styles: _c0",
|
|
213
|
+
"});",
|
|
214
|
+
].join("\n");
|
|
215
|
+
fs.writeFileSync(jsFile, code);
|
|
216
|
+
|
|
217
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
218
|
+
const onEnd = captureOnEnd(plugin);
|
|
219
|
+
|
|
220
|
+
await onEnd(
|
|
221
|
+
buildResult({
|
|
222
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(fs.readFileSync(jsFile, "utf-8")).toBe(code);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("styles 배열에 문자열과 변수 참조가 혼재하면 문자열만 처리된다", async () => {
|
|
230
|
+
const jsFile = path.join(tmpDir, "mixed.js");
|
|
231
|
+
const code = [
|
|
232
|
+
'import * as i0 from "@angular/core";',
|
|
233
|
+
"const _c0 = '.external { margin: 0 }';",
|
|
234
|
+
"class MyComp {}",
|
|
235
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
236
|
+
' styles: [_c0, ".inline { color: red; }"]',
|
|
237
|
+
"});",
|
|
238
|
+
].join("\n");
|
|
239
|
+
fs.writeFileSync(jsFile, code);
|
|
240
|
+
|
|
241
|
+
const plugin = createPostcssPlugin({ plugins: [markerPlugin()] });
|
|
242
|
+
const onEnd = captureOnEnd(plugin);
|
|
243
|
+
|
|
244
|
+
await onEnd(
|
|
245
|
+
buildResult({
|
|
246
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const output = fs.readFileSync(jsFile, "utf-8");
|
|
251
|
+
// 문자열 리터럴만 처리됨 (마커 1개)
|
|
252
|
+
const markerCount = (output.match(/\/\* marker \*\//g) ?? []).length;
|
|
253
|
+
expect(markerCount).toBe(1);
|
|
254
|
+
// 변수 참조는 원본 유지
|
|
255
|
+
expect(output).toContain("_c0");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it(".js PostCSS 실패 시에도 다른 .css 파일은 정상 처리된다", async () => {
|
|
259
|
+
const jsFile = path.join(tmpDir, "fail.js");
|
|
260
|
+
const cssFile = path.join(tmpDir, "ok.css");
|
|
261
|
+
const jsCode = [
|
|
262
|
+
'import * as i0 from "@angular/core";',
|
|
263
|
+
"class MyComp {}",
|
|
264
|
+
"MyComp.ɵcmp = i0.ɵɵdefineComponent({",
|
|
265
|
+
' styles: [".host { display: flex; }"]',
|
|
266
|
+
"});",
|
|
267
|
+
].join("\n");
|
|
268
|
+
fs.writeFileSync(jsFile, jsCode);
|
|
269
|
+
fs.writeFileSync(cssFile, ".ok { color: green; }");
|
|
270
|
+
|
|
271
|
+
const throwing: postcss.Plugin = {
|
|
272
|
+
postcssPlugin: "test-throwing",
|
|
273
|
+
Once() {
|
|
274
|
+
throw new Error("js-postcss-error");
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const plugin = createPostcssPlugin({ plugins: [throwing] });
|
|
279
|
+
const onEnd = captureOnEnd(plugin);
|
|
280
|
+
|
|
281
|
+
const result = buildResult({
|
|
282
|
+
[cssFile]: { bytes: 20, inputs: {}, imports: [], exports: [] },
|
|
283
|
+
[jsFile]: { bytes: 200, inputs: {}, imports: [], exports: [] },
|
|
284
|
+
});
|
|
285
|
+
await onEnd(result);
|
|
286
|
+
|
|
287
|
+
// 에러가 보고됨
|
|
288
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -32,6 +32,20 @@ vi.mock("../../src/utils/tsc-build", () => ({
|
|
|
32
32
|
runTscPackageBuild: vi.fn(() => mockTscResult),
|
|
33
33
|
}));
|
|
34
34
|
|
|
35
|
+
// tsc plugin mock (build() js=true path)
|
|
36
|
+
const mockTscPlugin = {
|
|
37
|
+
plugin: { name: "sd-tsc", setup: vi.fn() },
|
|
38
|
+
getProgram: vi.fn(),
|
|
39
|
+
getAffectedFiles: vi.fn(),
|
|
40
|
+
getDiagnostics: vi.fn((): unknown[] => []),
|
|
41
|
+
getErrors: vi.fn((): string[] | undefined => undefined),
|
|
42
|
+
resetBuilderProgram: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
vi.mock("../../src/esbuild/esbuild-tsc-plugin", () => ({
|
|
46
|
+
createTscPlugin: vi.fn(() => mockTscPlugin),
|
|
47
|
+
}));
|
|
48
|
+
|
|
35
49
|
vi.mock("../../src/utils/tsconfig", () => ({
|
|
36
50
|
parseTsconfig: vi.fn(() => ({ options: {}, fileNames: [] })),
|
|
37
51
|
getPackageSourceFiles: vi.fn(() => []),
|
|
@@ -107,6 +121,13 @@ beforeEach(() => {
|
|
|
107
121
|
formattedOutput: "",
|
|
108
122
|
});
|
|
109
123
|
mockTscResult.program = { getSourceFiles: () => [] } as any;
|
|
124
|
+
|
|
125
|
+
// Reset tsc plugin mock
|
|
126
|
+
mockTscPlugin.getProgram.mockReset();
|
|
127
|
+
mockTscPlugin.getAffectedFiles.mockReset();
|
|
128
|
+
mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
|
|
129
|
+
mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
|
|
130
|
+
mockTscPlugin.resetBuilderProgram.mockReset();
|
|
110
131
|
});
|
|
111
132
|
|
|
112
133
|
describe("server-build.worker lint integration (Slice 3)", () => {
|
|
@@ -129,6 +150,28 @@ describe("server-build.worker lint integration (Slice 3)", () => {
|
|
|
129
150
|
});
|
|
130
151
|
});
|
|
131
152
|
|
|
153
|
+
describe("Scenario: build js=true runs lint using tscPlugin.getProgram()", () => {
|
|
154
|
+
it("returns lint result using plugin program when js=true and lint enabled", async () => {
|
|
155
|
+
const fakeProgram = { getSourceFiles: () => [] };
|
|
156
|
+
mockTscPlugin.getProgram.mockReturnValue(fakeProgram);
|
|
157
|
+
|
|
158
|
+
const result = await workerMethods["build"]({
|
|
159
|
+
name: "my-server",
|
|
160
|
+
cwd: "/workspace",
|
|
161
|
+
pkgDir: "/workspace/packages/my-server",
|
|
162
|
+
output: { js: true, dts: false, lint: true },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(result).toHaveProperty("lint");
|
|
166
|
+
expect(result.lint).toEqual({
|
|
167
|
+
success: true,
|
|
168
|
+
errorCount: 0,
|
|
169
|
+
warningCount: 0,
|
|
170
|
+
formattedOutput: "",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
132
175
|
describe("Scenario: lint disabled", () => {
|
|
133
176
|
it("does not run lint when output.lint is not set", async () => {
|
|
134
177
|
const result = await workerMethods["build"]({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# server-build.worker.ts 리팩토링 — LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] rebuildAll()이 startWatch() 내부 클로저로 정의: `server-build.worker.ts:272` — startWatch (`:259`) 내부에 `async function rebuildAll()` 정의 확인
|
|
6
|
+
- [x] lastBuilderProgram이 startWatch 로컬 변수: `server-build.worker.ts:264` — `let lastBuilderProgram` startWatch 함수 스코프 내 정의 확인
|
|
7
|
+
- [x] watchLintRunner가 startWatch 로컬 변수: `server-build.worker.ts:265` — `let watchLintRunner` startWatch 함수 스코프 내 정의 확인
|
|
8
|
+
- [x] watchInfo 모듈 스코프 변수 제거: 모듈 스코프에 `watchInfo` 선언 없음 확인, startWatch의 `info` 파라미터를 직접 사용
|
|
9
|
+
- [x] cleanup()에서 lastBuilderProgram 참조 제거: `server-build.worker.ts:108-123` — cleanup 함수에 lastBuilderProgram 참조 없음 확인
|
|
10
|
+
- [x] startWatch에서 createContext에 tsc 옵션 전달: `server-build.worker.ts:348-359` — `tsc: { cwd, output: { dts }, env, includeTests }` 옵션 포함 확인
|
|
11
|
+
- [x] build() js=true에서 createTscPlugin 로컬 생성: `server-build.worker.ts:153-159` — build 함수 내 `const tscPlugin = createTscPlugin({...})` 확인
|
|
12
|
+
- [x] build() js=false에서 runTscPackageBuild 직접 호출: `server-build.worker.ts:192-199` — `runTscPackageBuild({...})` 직접 호출 확인
|
|
13
|
+
- [x] 외부 인터페이스(ServerBuildInfo/WatchInfo/Result/WorkerEvents) 변경 없음: 타입 정의 `:33-96` 변경 없음 확인
|
|
14
|
+
- [x] runTscPackageBuild import 유지: `server-build.worker.ts:19` — import 존재 확인 (js=false fallback용)
|