@simplysm/sd-cli 14.0.38 → 14.0.40
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/dev-server/hmr-client-script.d.ts +2 -2
- package/dist/dev-server/hmr-client-script.d.ts.map +1 -1
- package/dist/dev-server/hmr-client-script.js +4 -4
- package/dist/dev-server/hmr-client-script.js.map +1 -1
- 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 +20 -10
- 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 +94 -26
- 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 +57 -4
- 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/dev-server/hmr-client-script.ts +4 -4
- package/src/esbuild/esbuild-client-config.ts +22 -13
- package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
- package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
- package/src/workers/client.worker.ts +96 -29
- package/src/workers/server-build.worker.ts +136 -97
- package/src/workers/server-esbuild-context.ts +72 -4
- 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 +34 -20
- package/tests/utils/esbuild-client-config.spec.ts +79 -16
- 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/utils/hmr-client-script.acc.spec.ts +8 -8
- package/tests/utils/hmr-client-script.spec.ts +5 -5
- 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 +188 -2
- package/tests/workers/server-esbuild-context.spec.ts +401 -2
|
@@ -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
|
+
});
|
|
@@ -5,7 +5,7 @@ import { getHmrClientScript, createHmrPostTransform } from "../../src/dev-server
|
|
|
5
5
|
describe("HMR 클라이언트 스크립트 통합", () => {
|
|
6
6
|
describe("Scenario: HMR 클라이언트 문법 호환성", () => {
|
|
7
7
|
it("Chrome 61 비호환 문법을 사용하지 않는다", () => {
|
|
8
|
-
const script = getHmrClientScript("/app/");
|
|
8
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
9
9
|
|
|
10
10
|
// optional chaining (?.) 미사용
|
|
11
11
|
expect(script).not.toMatch(/\?\./);
|
|
@@ -16,13 +16,13 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("WebSocket 연결 코드를 포함한다", () => {
|
|
19
|
-
const script = getHmrClientScript("/app/");
|
|
19
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
20
20
|
expect(script).toContain("WebSocket");
|
|
21
21
|
expect(script).toContain("ws://");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("component-update, css-update, full-reload 메시지 핸들러를 포함한다", () => {
|
|
25
|
-
const script = getHmrClientScript("/app/");
|
|
25
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
26
26
|
expect(script).toContain("component-update");
|
|
27
27
|
expect(script).toContain("css-update");
|
|
28
28
|
expect(script).toContain("full-reload");
|
|
@@ -31,7 +31,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it("자동 재연결 로직을 포함한다", () => {
|
|
34
|
-
const script = getHmrClientScript("/app/");
|
|
34
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
35
35
|
expect(script).toContain("setTimeout");
|
|
36
36
|
expect(script).toContain("connect");
|
|
37
37
|
});
|
|
@@ -77,7 +77,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
it("css-update 시 msg.files와 매칭되는 link만 cache-busting 적용", () => {
|
|
80
|
-
const script = getHmrClientScript("/app/");
|
|
80
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
81
81
|
const { sandbox, triggerMessage, mainLink, vendorLink } = createScriptEnv();
|
|
82
82
|
|
|
83
83
|
runInNewContext(script, sandbox);
|
|
@@ -89,7 +89,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it("css-update 시 files에 여러 파일이 있으면 매칭되는 모든 link를 업데이트", () => {
|
|
92
|
-
const script = getHmrClientScript("/app/");
|
|
92
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
93
93
|
const { sandbox, triggerMessage, mainLink, vendorLink } = createScriptEnv();
|
|
94
94
|
|
|
95
95
|
runInNewContext(script, sandbox);
|
|
@@ -107,7 +107,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
107
107
|
|
|
108
108
|
describe("Scenario: 스크립트 주입", () => {
|
|
109
109
|
it("postTransform이 </body> 직전에 script 태그를 삽입한다", async () => {
|
|
110
|
-
const transform = createHmrPostTransform("/app/");
|
|
110
|
+
const transform = createHmrPostTransform("/app/", 4200);
|
|
111
111
|
const html = "<html><body><div>content</div></body></html>";
|
|
112
112
|
const result = await transform(html);
|
|
113
113
|
|
|
@@ -117,7 +117,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it("</body>가 없는 HTML에서도 스크립트를 추가한다", async () => {
|
|
120
|
-
const transform = createHmrPostTransform("/app/");
|
|
120
|
+
const transform = createHmrPostTransform("/app/", 4200);
|
|
121
121
|
const html = "<html><body><div>content</div>";
|
|
122
122
|
const result = await transform(html);
|
|
123
123
|
|
|
@@ -3,19 +3,19 @@ import { getHmrClientScript, createHmrPostTransform } from "../../src/dev-server
|
|
|
3
3
|
|
|
4
4
|
describe("getHmrClientScript", () => {
|
|
5
5
|
it("유효한 JavaScript를 생성한다", () => {
|
|
6
|
-
const script = getHmrClientScript("/app/");
|
|
6
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
7
7
|
// new Function으로 구문 오류 확인
|
|
8
8
|
expect(() => new Function(script)).not.toThrow();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it("IIFE로 감싸져 있다", () => {
|
|
12
|
-
const script = getHmrClientScript("/app/");
|
|
12
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
13
13
|
expect(script.trimStart()).toMatch(/^\(function\(\)/);
|
|
14
14
|
expect(script.trimEnd()).toMatch(/\}\)\(\);$/);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("const/let 대신 var를 사용한다", () => {
|
|
18
|
-
const script = getHmrClientScript("/app/");
|
|
18
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
19
19
|
// var 사용 확인
|
|
20
20
|
expect(script).toContain("var ws");
|
|
21
21
|
// const/let 미사용 확인
|
|
@@ -26,7 +26,7 @@ describe("getHmrClientScript", () => {
|
|
|
26
26
|
|
|
27
27
|
describe("createHmrPostTransform", () => {
|
|
28
28
|
it("여러 </body> 태그가 있으면 마지막 것 앞에 주입한다", async () => {
|
|
29
|
-
const transform = createHmrPostTransform("/app/");
|
|
29
|
+
const transform = createHmrPostTransform("/app/", 4200);
|
|
30
30
|
const html = "<body>first</body><body>second</body>";
|
|
31
31
|
const result = await transform(html);
|
|
32
32
|
|
|
@@ -37,7 +37,7 @@ describe("createHmrPostTransform", () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it("빈 HTML에도 스크립트를 추가한다", async () => {
|
|
40
|
-
const transform = createHmrPostTransform("/");
|
|
40
|
+
const transform = createHmrPostTransform("/", 4200);
|
|
41
41
|
const result = await transform("");
|
|
42
42
|
expect(result).toContain("<script>");
|
|
43
43
|
});
|
|
@@ -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용)
|
|
@@ -29,8 +29,8 @@ let mockMetafileInputs: Record<string, unknown> = {};
|
|
|
29
29
|
// tsc build mock
|
|
30
30
|
const mockRunTscPackageBuild = vi.fn(() => ({
|
|
31
31
|
success: true,
|
|
32
|
-
errors: undefined,
|
|
33
|
-
diagnostics: [],
|
|
32
|
+
errors: undefined as string[] | undefined,
|
|
33
|
+
diagnostics: [] as unknown[],
|
|
34
34
|
errorCount: 0,
|
|
35
35
|
warningCount: 0,
|
|
36
36
|
}));
|
|
@@ -116,6 +116,20 @@ vi.mock("../../src/utils/tsc-build", () => ({
|
|
|
116
116
|
runTscPackageBuild: mockRunTscPackageBuild,
|
|
117
117
|
}));
|
|
118
118
|
|
|
119
|
+
// tsc plugin mock (build() js=true path uses createTscPlugin)
|
|
120
|
+
const mockTscPlugin = {
|
|
121
|
+
plugin: { name: "sd-tsc", setup: vi.fn() },
|
|
122
|
+
getProgram: vi.fn(),
|
|
123
|
+
getAffectedFiles: vi.fn(),
|
|
124
|
+
getDiagnostics: vi.fn((): unknown[] => []),
|
|
125
|
+
getErrors: vi.fn((): string[] | undefined => undefined),
|
|
126
|
+
resetBuilderProgram: vi.fn(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
vi.mock("../../src/esbuild/esbuild-tsc-plugin", () => ({
|
|
130
|
+
createTscPlugin: vi.fn(() => mockTscPlugin),
|
|
131
|
+
}));
|
|
132
|
+
|
|
119
133
|
vi.mock("../../src/workers/shared-worker-lifecycle", () => {
|
|
120
134
|
let guardCalled = false;
|
|
121
135
|
resetGuard = () => { guardCalled = false; };
|
|
@@ -175,7 +189,14 @@ describe("server-build.worker build()", () => {
|
|
|
175
189
|
diagnostics: [],
|
|
176
190
|
errorCount: 0,
|
|
177
191
|
warningCount: 0,
|
|
178
|
-
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Reset tsc plugin mock (used for js=true path)
|
|
195
|
+
mockTscPlugin.getProgram.mockReset();
|
|
196
|
+
mockTscPlugin.getAffectedFiles.mockReset();
|
|
197
|
+
mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
|
|
198
|
+
mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
|
|
199
|
+
mockTscPlugin.resetBuilderProgram.mockReset();
|
|
179
200
|
|
|
180
201
|
// Reset lockfile content and cache
|
|
181
202
|
mockLockfileContent = "";
|
|
@@ -212,21 +233,65 @@ describe("server-build.worker build()", () => {
|
|
|
212
233
|
expect(result.mainJsPath).toBe(path.resolve(baseBuildInfo.pkgDir, "dist", "main.js").replace(/\\/g, "/"));
|
|
213
234
|
});
|
|
214
235
|
|
|
215
|
-
// Acceptance: type error detected
|
|
236
|
+
// Acceptance: type error detected via tsc plugin (js=true)
|
|
216
237
|
it("reports typecheck error in build field", async () => {
|
|
238
|
+
mockTscPlugin.getErrors.mockReturnValue(["TS2345: type error"]);
|
|
239
|
+
mockTscPlugin.getDiagnostics.mockReturnValue([{ code: 2345, category: 1 }]);
|
|
240
|
+
|
|
241
|
+
const result = await workerFns["build"](baseBuildInfo);
|
|
242
|
+
|
|
243
|
+
expect(result.build.success).toBe(false);
|
|
244
|
+
expect(result.build.errors).toContain("TS2345: type error");
|
|
245
|
+
expect(result.build.diagnostics).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Acceptance: esbuild + tsc both error — merged
|
|
249
|
+
it("merges esbuild and tsc errors when both fail", async () => {
|
|
250
|
+
vi.mocked(esbuild.build).mockResolvedValueOnce({
|
|
251
|
+
errors: [{ text: "esbuild syntax error" }],
|
|
252
|
+
warnings: [],
|
|
253
|
+
outputFiles: [],
|
|
254
|
+
} as any);
|
|
255
|
+
mockTscPlugin.getErrors.mockReturnValue(["TS2322: type mismatch"]);
|
|
256
|
+
|
|
257
|
+
const result = await workerFns["build"](baseBuildInfo);
|
|
258
|
+
|
|
259
|
+
expect(result.build.success).toBe(false);
|
|
260
|
+
expect(result.build.errors).toContain("esbuild syntax error");
|
|
261
|
+
expect(result.build.errors).toContain("TS2322: type mismatch");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Acceptance: diagnostics from tsc plugin (js=true)
|
|
265
|
+
it("includes diagnostics from tsc plugin in build result", async () => {
|
|
266
|
+
mockTscPlugin.getDiagnostics.mockReturnValue([
|
|
267
|
+
{ code: 2322, category: 1, messageText: "Type mismatch" },
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
const result = await workerFns["build"](baseBuildInfo);
|
|
271
|
+
|
|
272
|
+
expect(result.build.diagnostics).toHaveLength(1);
|
|
273
|
+
expect(result.build.diagnostics[0]).toEqual(
|
|
274
|
+
expect.objectContaining({ code: 2322 }),
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Acceptance: js=false uses runTscPackageBuild directly
|
|
279
|
+
it("uses runTscPackageBuild directly when output.js=false", async () => {
|
|
217
280
|
mockRunTscPackageBuild.mockReturnValueOnce({
|
|
218
281
|
success: false,
|
|
219
|
-
errors: ["TS2345: type error"]
|
|
282
|
+
errors: ["TS2345: type error"],
|
|
220
283
|
diagnostics: [{ code: 2345, category: 1 }] as any,
|
|
221
284
|
errorCount: 1,
|
|
222
285
|
warningCount: 0,
|
|
223
|
-
|
|
286
|
+
});
|
|
224
287
|
|
|
225
|
-
const result = await workerFns["build"](
|
|
288
|
+
const result = await workerFns["build"]({
|
|
289
|
+
...baseBuildInfo,
|
|
290
|
+
output: { js: false, dts: true },
|
|
291
|
+
});
|
|
226
292
|
|
|
227
293
|
expect(result.build.success).toBe(false);
|
|
228
294
|
expect(result.build.errors).toContain("TS2345: type error");
|
|
229
|
-
expect(result.build.diagnostics).toHaveLength(1);
|
|
230
295
|
});
|
|
231
296
|
|
|
232
297
|
// Acceptance: esbuild error detected
|
|
@@ -466,7 +531,14 @@ describe("server-build.worker startWatch()", () => {
|
|
|
466
531
|
diagnostics: [],
|
|
467
532
|
errorCount: 0,
|
|
468
533
|
warningCount: 0,
|
|
469
|
-
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Reset tsc plugin mock (used for watch mode rebuild)
|
|
537
|
+
mockTscPlugin.getProgram.mockReset();
|
|
538
|
+
mockTscPlugin.getAffectedFiles.mockReset();
|
|
539
|
+
mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
|
|
540
|
+
mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
|
|
541
|
+
mockTscPlugin.resetBuilderProgram.mockReset();
|
|
470
542
|
|
|
471
543
|
mockReadFileSync.mockImplementation((filePath: string) => {
|
|
472
544
|
if (String(filePath).endsWith("package.json")) {
|
|
@@ -567,6 +639,47 @@ describe("server-build.worker startWatch()", () => {
|
|
|
567
639
|
const buildCalls = mockSend.mock.calls.filter((c) => c[0] === "build");
|
|
568
640
|
expect(buildCalls.length).toBeGreaterThanOrEqual(1);
|
|
569
641
|
});
|
|
642
|
+
|
|
643
|
+
// Acceptance: rebuildAll js=true — single esbuildCtx.rebuild() call, tsc not called directly
|
|
644
|
+
it("uses esbuildCtx.rebuild() without direct tsc call in watch mode rebuild", async () => {
|
|
645
|
+
mockMetafileInputs = { "packages/my-server/src/main.ts": {} };
|
|
646
|
+
|
|
647
|
+
await workerFns["startWatch"](watchInfo);
|
|
648
|
+
|
|
649
|
+
const onChangeHandler = mockOnChange.mock.calls[0][1] as (
|
|
650
|
+
changes: Array<{ event: string; path: string }>,
|
|
651
|
+
) => Promise<void>;
|
|
652
|
+
|
|
653
|
+
mockRebuild.mockClear();
|
|
654
|
+
mockRunTscPackageBuild.mockClear();
|
|
655
|
+
mockSend.mockClear();
|
|
656
|
+
|
|
657
|
+
const absPath = path.resolve("/workspace", "packages/my-server/src/main.ts").replace(/\\/g, "/");
|
|
658
|
+
await onChangeHandler([{ event: "change", path: absPath }]);
|
|
659
|
+
|
|
660
|
+
// esbuild rebuild should have been called (tsc triggered by plugin inside)
|
|
661
|
+
expect(mockRebuild).toHaveBeenCalled();
|
|
662
|
+
// runTscPackageBuild should NOT be called directly for js=true
|
|
663
|
+
expect(mockRunTscPackageBuild).not.toHaveBeenCalled();
|
|
664
|
+
// Build event should be sent
|
|
665
|
+
expect(mockSend).toHaveBeenCalledWith("build", expect.objectContaining({
|
|
666
|
+
build: expect.objectContaining({ success: true }),
|
|
667
|
+
}));
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Acceptance: startWatch passes tsc options to createContext
|
|
671
|
+
it("passes tsc options to esbuildCtx.createContext", async () => {
|
|
672
|
+
await workerFns["startWatch"]({
|
|
673
|
+
...watchInfo,
|
|
674
|
+
output: { js: true, dts: true, env: "node" as any, includeTests: true },
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
expect(esbuild.context).toHaveBeenCalledWith(
|
|
678
|
+
expect.objectContaining({
|
|
679
|
+
plugins: [mockTscPlugin.plugin],
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
});
|
|
570
683
|
});
|
|
571
684
|
|
|
572
685
|
describe("server-build.worker stopWatch()", () => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# server-esbuild-context tsc 통합 — LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] `EsbuildContextOptions.tsc`가 optional이므로 `server-watch-manager.ts:66-71`의 기존 `recreateContext()` 호출이 타입 에러 없이 동작: 확인 — `tsc` 미전달 시 기존 플러그인 재사용 로직 정상
|
|
6
|
+
- [x] `server-watch-manager.spec.ts` 8개 테스트 전부 통과: 확인 — 회귀 없음
|
|
7
|
+
- [x] `server-watch-manager.ts`에 코드 변경 없음: 확인 — 파일 미수정
|