@simplysm/sd-cli 14.0.37 → 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/angular/vite-angular-plugin.d.ts +2 -5
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +19 -72
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/commands/publish/storage-publisher.js +1 -0
- package/dist/commands/publish/storage-publisher.js.map +1 -1
- package/dist/dev-server/hmr-service.d.ts +2 -0
- package/dist/dev-server/hmr-service.d.ts.map +1 -1
- package/dist/dev-server/hmr-service.js +24 -10
- package/dist/dev-server/hmr-service.js.map +1 -1
- package/dist/electron/electron.js +4 -4
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.d.ts.map +1 -1
- package/dist/engines/BaseEngine.js +10 -1
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
- package/dist/engines/EsbuildClientEngine.js +12 -1
- package/dist/engines/EsbuildClientEngine.js.map +1 -1
- package/dist/engines/engine-factory.d.ts +0 -4
- package/dist/engines/engine-factory.d.ts.map +1 -1
- package/dist/engines/engine-factory.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 +24 -14
- 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/orchestrators/DevOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevOrchestrator.js +0 -5
- package/dist/orchestrators/DevOrchestrator.js.map +1 -1
- package/dist/orchestrators/TypecheckOrchestrator.js +2 -2
- package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -1
- package/dist/sd-cli.js +2 -1
- package/dist/sd-cli.js.map +1 -1
- package/dist/utils/output-utils.d.ts.map +1 -1
- package/dist/utils/output-utils.js +3 -2
- package/dist/utils/output-utils.js.map +1 -1
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +39 -3
- 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/angular/vite-angular-plugin.ts +19 -82
- package/src/commands/publish/storage-publisher.ts +1 -0
- package/src/dev-server/hmr-service.ts +28 -13
- package/src/electron/electron.ts +5 -5
- package/src/engines/BaseEngine.ts +10 -1
- package/src/engines/EsbuildClientEngine.ts +13 -1
- package/src/engines/engine-factory.ts +0 -1
- package/src/esbuild/esbuild-client-config.ts +27 -18
- package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
- package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
- package/src/orchestrators/DevOrchestrator.ts +0 -6
- package/src/orchestrators/TypecheckOrchestrator.ts +2 -2
- package/src/sd-cli.ts +2 -1
- package/src/utils/output-utils.ts +3 -2
- package/src/workers/client.worker.ts +40 -3
- package/src/workers/server-build.worker.ts +136 -97
- package/src/workers/server-esbuild-context.ts +59 -3
- package/tests/angular/_vite-angular-plugin-test-setup.ts +3 -30
- package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
- package/tests/angular/vite-angular-plugin-legacy-watch.spec.ts +14 -31
- package/tests/angular/vite-angular-plugin-vitest.spec.ts +4 -34
- package/tests/angular/vite-angular-plugin.spec.ts +24 -60
- package/tests/commands/typecheck.spec.ts +1 -1
- package/tests/engines/base-engine.spec.ts +25 -0
- package/tests/engines/engine-adapter-isolation.spec.ts +3 -3
- package/tests/engines/esbuild-client-engine.acc.spec.ts +29 -0
- package/tests/engines/esbuild-client-engine.spec.ts +26 -0
- package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
- package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
- package/tests/orchestrators/build-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/dev-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/typecheck-orchestrator.spec.ts +1 -1
- package/tests/orchestrators/watch-orchestrator.spec.ts +1 -1
- package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
- package/tests/utils/esbuild-client-config.acc.spec.ts +30 -15
- 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/utils/hmr-service-dispatcher.acc.spec.ts +70 -0
- package/tests/workers/client-worker-initial-build-error.verify.md +8 -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,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
|
+
});
|
|
@@ -155,6 +155,76 @@ describe("HMR 디스패처 통합", () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
describe("Scenario: 파일 크기 동일하지만 내용 변경 → 변경 감지", () => {
|
|
159
|
+
it("CSS 내용이 변경되었지만 크기가 동일한 경우 css-update를 전송한다", async () => {
|
|
160
|
+
// 임시 dist 디렉토리에 CSS 파일 생성
|
|
161
|
+
const distDir = path.join(tmpDir, "dist-hash-test");
|
|
162
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(distDir, "main.css"), "body { color: red; }");
|
|
164
|
+
fs.writeFileSync(path.join(distDir, "main.js"), "console.log('hello');");
|
|
165
|
+
|
|
166
|
+
// outDir을 전달하는 HMR 서비스 생성
|
|
167
|
+
hmrService.close();
|
|
168
|
+
await new Promise<void>((resolve, reject) => {
|
|
169
|
+
httpServer.close((err) => {
|
|
170
|
+
if (err != null) reject(err);
|
|
171
|
+
else resolve();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
httpServer = http.createServer((_req, res) => { res.writeHead(404); res.end(); });
|
|
176
|
+
hmrService = createHmrService({
|
|
177
|
+
httpServer,
|
|
178
|
+
basePath: "/app/",
|
|
179
|
+
templateUpdates,
|
|
180
|
+
outDir: distDir,
|
|
181
|
+
});
|
|
182
|
+
port = await new Promise<number>((resolve, reject) => {
|
|
183
|
+
httpServer.listen(0, "127.0.0.1", () => {
|
|
184
|
+
const addr = httpServer.address();
|
|
185
|
+
if (typeof addr === "object" && addr != null) resolve(addr.port);
|
|
186
|
+
else reject(new Error("포트 감지 실패"));
|
|
187
|
+
});
|
|
188
|
+
httpServer.on("error", reject);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const cssContent = "body { color: red; }";
|
|
192
|
+
const metafile1: esbuild.Metafile = {
|
|
193
|
+
inputs: {},
|
|
194
|
+
outputs: {
|
|
195
|
+
"main.js": { bytes: 22, inputs: {}, imports: [], exports: [] },
|
|
196
|
+
"main.css": { bytes: cssContent.length, inputs: {}, imports: [], exports: [] },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 첫 빌드 (baseline)
|
|
201
|
+
hmrService.onBuildEnd(metafile1);
|
|
202
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
203
|
+
|
|
204
|
+
// CSS 내용 변경 (같은 크기)
|
|
205
|
+
const newCssContent = "body { color: blu; }"; // 같은 길이
|
|
206
|
+
fs.writeFileSync(path.join(distDir, "main.css"), newCssContent);
|
|
207
|
+
|
|
208
|
+
const ws = await connectWs();
|
|
209
|
+
const msgPromise = waitForMessage(ws);
|
|
210
|
+
|
|
211
|
+
const metafile2: esbuild.Metafile = {
|
|
212
|
+
inputs: {},
|
|
213
|
+
outputs: {
|
|
214
|
+
"main.js": { bytes: 22, inputs: {}, imports: [], exports: [] },
|
|
215
|
+
"main.css": { bytes: newCssContent.length, inputs: {}, imports: [], exports: [] },
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
hmrService.onBuildEnd(metafile2);
|
|
219
|
+
|
|
220
|
+
const msg = await msgPromise;
|
|
221
|
+
expect(msg["type"]).toBe("css-update");
|
|
222
|
+
|
|
223
|
+
ws.close();
|
|
224
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
158
228
|
describe("Scenario: CSS-only 변경 → css-update 메시지", () => {
|
|
159
229
|
it("JS 출력은 동일하고 CSS만 변경되면 css-update를 전송한다", async () => {
|
|
160
230
|
// 초기 빌드
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# 초기 빌드 에러 보고 — LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] client.worker.ts onEnd에서 초기 빌드 시 errors 필드가 포함되는지: `client.worker.ts:285-291` — `initialBuildResolve`에 `errors: result.errors.map((e) => e.text)` 포함 확인
|
|
6
|
+
- [x] EsbuildClientEngine.startWatch에서 반환값의 success 확인 후 에러 로깅하는지: `EsbuildClientEngine.ts:135-137` — `result != null && !result.success` 조건으로 `logger.error` 호출 확인. 테스트 실행 시 `[my-client] 초기 빌드 실패: Module not found: @angular/core; Syntax error in app.ts` 출력 확인
|
|
7
|
+
- [x] esbuild-client-config.ts에서 dev 모드 logLevel이 "warning"인지: `esbuild-client-config.ts:169` — `logLevel: isDev ? "warning" : "silent"` 확인. `isDev = options.mode === "dev"` (line 52)
|
|
8
|
+
- [x] esbuild-client-config.ts에서 build 모드 logLevel이 "silent"인지: 동일 라인 — 삼항 연산자의 false 분기가 `"silent"` 확인
|
|
@@ -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`에 코드 변경 없음: 확인 — 파일 미수정
|