@simplysm/sd-cli 14.0.44 → 14.0.45

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.
Files changed (48) hide show
  1. package/dist/deps/server-externals/server-production-files.d.ts +10 -5
  2. package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
  3. package/dist/deps/server-externals/server-production-files.js +22 -26
  4. package/dist/deps/server-externals/server-production-files.js.map +1 -1
  5. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts +3 -8
  6. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
  7. package/dist/esbuild/esbuild-angular-compiler-plugin.js +57 -83
  8. package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
  9. package/dist/esbuild/esbuild-worker-plugin.d.ts +30 -0
  10. package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
  11. package/dist/esbuild/esbuild-worker-plugin.js +197 -0
  12. package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
  13. package/dist/workers/server-build.worker.d.ts.map +1 -1
  14. package/dist/workers/server-build.worker.js +6 -5
  15. package/dist/workers/server-build.worker.js.map +1 -1
  16. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  17. package/dist/workers/server-esbuild-context.js +3 -1
  18. package/dist/workers/server-esbuild-context.js.map +1 -1
  19. package/dist/workers/server-watch-manager.js +1 -1
  20. package/dist/workers/server-watch-manager.js.map +1 -1
  21. package/package.json +5 -4
  22. package/src/deps/server-externals/server-production-files.ts +26 -28
  23. package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
  24. package/src/esbuild/esbuild-worker-plugin.ts +266 -0
  25. package/src/workers/server-build.worker.ts +6 -5
  26. package/src/workers/server-esbuild-context.ts +3 -1
  27. package/src/workers/server-watch-manager.ts +1 -1
  28. package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
  29. package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
  30. package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
  31. package/tests/esbuild/esbuild-worker-plugin.spec.ts +297 -0
  32. package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
  33. package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
  34. package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
  35. package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
  36. package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
  37. package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
  38. package/tests/workers/server-build-worker-plugin.verify.md +9 -0
  39. package/tests/workers/server-build-worker.spec.ts +26 -12
  40. package/tests/workers/server-esbuild-context.spec.ts +13 -5
  41. package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
  42. package/tests/workers/server-watch-manager.spec.ts +2 -2
  43. package/dist/angular/web-worker-transformer.d.ts +0 -9
  44. package/dist/angular/web-worker-transformer.d.ts.map +0 -1
  45. package/dist/angular/web-worker-transformer.js +0 -73
  46. package/dist/angular/web-worker-transformer.js.map +0 -1
  47. package/src/angular/web-worker-transformer.ts +0 -117
  48. package/tests/angular/web-worker-transformer.spec.ts +0 -154
@@ -0,0 +1,297 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import esbuild from "esbuild";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import { fileURLToPath } from "url";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const fixturesDir = path.join(__dirname, "fixtures", "worker-plugin");
10
+
11
+ const { transformWorkerPatterns, createWorkerBundlePlugin } = await import(
12
+ "../../src/esbuild/esbuild-worker-plugin.js"
13
+ );
14
+
15
+ /**
16
+ * transformWorkerPatterns 테스트용 최소 PluginBuild mock 생성.
17
+ * 실제 esbuild 모듈을 사용하여 Worker 번들링이 동작한다.
18
+ */
19
+ function createMockBuild(overrides?: Partial<esbuild.BuildOptions>): esbuild.PluginBuild {
20
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-unit-"));
21
+ return {
22
+ esbuild,
23
+ initialOptions: {
24
+ outdir: tmpDir,
25
+ write: false,
26
+ ...overrides,
27
+ },
28
+ } as unknown as esbuild.PluginBuild;
29
+ }
30
+
31
+ describe("transformWorkerPatterns — 패턴 감지", () => {
32
+ it("Worker 패턴이 없는 content에 대해 undefined를 반환한다", () => {
33
+ const result = transformWorkerPatterns(
34
+ 'console.log("hello");',
35
+ "/test/entry.js",
36
+ createMockBuild(),
37
+ );
38
+
39
+ expect(result).toBeUndefined();
40
+ });
41
+
42
+ it("'Worker' 문자열이 있지만 new URL 패턴이 아니면 undefined를 반환한다", () => {
43
+ const result = transformWorkerPatterns(
44
+ 'const w = new Worker("./worker.js");',
45
+ "/test/entry.js",
46
+ createMockBuild(),
47
+ );
48
+
49
+ expect(result).toBeUndefined();
50
+ });
51
+
52
+ it("import.meta.url이 아닌 URL 생성은 undefined를 반환한다", () => {
53
+ const result = transformWorkerPatterns(
54
+ 'const w = new Worker(new URL("./worker.js", location.href));',
55
+ "/test/entry.js",
56
+ createMockBuild(),
57
+ );
58
+
59
+ expect(result).toBeUndefined();
60
+ });
61
+
62
+ it("Worker + new URL + import.meta.url 패턴을 감지하여 치환한다", () => {
63
+ const entryPath = path.join(fixturesDir, "entry.js");
64
+
65
+ const result = transformWorkerPatterns(
66
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
67
+ entryPath,
68
+ createMockBuild(),
69
+ );
70
+
71
+ expect(result).toBeDefined();
72
+ expect(result!.contents).not.toContain("./worker.js");
73
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
74
+ expect(result!.errors).toHaveLength(0);
75
+ });
76
+
77
+ it("SharedWorker + new URL + import.meta.url 패턴을 감지하여 치환한다", () => {
78
+ const entryPath = path.join(fixturesDir, "entry.js");
79
+
80
+ const result = transformWorkerPatterns(
81
+ `const sw = new SharedWorker(new URL("./shared-worker.js", import.meta.url));`,
82
+ entryPath,
83
+ createMockBuild(),
84
+ );
85
+
86
+ expect(result).toBeDefined();
87
+ expect(result!.contents).not.toContain("./shared-worker.js");
88
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
89
+ });
90
+
91
+ it("복수 Worker 패턴을 모두 치환한다", () => {
92
+ const entryPath = path.join(fixturesDir, "entry.js");
93
+
94
+ const result = transformWorkerPatterns(
95
+ [
96
+ `const w1 = new Worker(new URL("./worker.js", import.meta.url));`,
97
+ `const w2 = new Worker(new URL("./worker2.js", import.meta.url));`,
98
+ ].join("\n"),
99
+ entryPath,
100
+ createMockBuild(),
101
+ );
102
+
103
+ expect(result).toBeDefined();
104
+ expect(result!.contents).not.toContain("./worker.js");
105
+ expect(result!.contents).not.toContain("./worker2.js");
106
+ });
107
+ });
108
+
109
+ describe("transformWorkerPatterns — type: module 처리", () => {
110
+ it("옵션 없는 Worker에 { type: 'module' }을 추가한다", () => {
111
+ const entryPath = path.join(fixturesDir, "entry.js");
112
+
113
+ const result = transformWorkerPatterns(
114
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
115
+ entryPath,
116
+ createMockBuild(),
117
+ );
118
+
119
+ expect(result).toBeDefined();
120
+ // { type: "module" } 추가됨
121
+ expect(result!.contents).toMatch(/\{\s*type:\s*"module"\s*\}/);
122
+ });
123
+
124
+ it("기존 옵션이 있으면 그대로 유지한다", () => {
125
+ const entryPath = path.join(fixturesDir, "entry.js");
126
+
127
+ const result = transformWorkerPatterns(
128
+ `const w = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });`,
129
+ entryPath,
130
+ createMockBuild(),
131
+ );
132
+
133
+ expect(result).toBeDefined();
134
+ expect(result!.contents).toContain('{ type: "module" }');
135
+ });
136
+ });
137
+
138
+ describe("transformWorkerPatterns — 에러 처리", () => {
139
+ it("Worker 빌드 에러를 errors에 포함하여 반환한다", () => {
140
+ const entryPath = path.join(fixturesDir, "entry.js");
141
+
142
+ const result = transformWorkerPatterns(
143
+ `const w = new Worker(new URL("./worker-error.js", import.meta.url));`,
144
+ entryPath,
145
+ createMockBuild(),
146
+ );
147
+
148
+ expect(result).toBeDefined();
149
+ expect(result!.errors.length).toBeGreaterThan(0);
150
+ });
151
+ });
152
+
153
+ describe("transformWorkerPatterns — write 옵션", () => {
154
+ it("write: false일 때 workerOutputFiles를 반환한다", () => {
155
+ const entryPath = path.join(fixturesDir, "entry.js");
156
+
157
+ const result = transformWorkerPatterns(
158
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
159
+ entryPath,
160
+ createMockBuild({ write: false }),
161
+ );
162
+
163
+ expect(result).toBeDefined();
164
+ expect(result!.workerOutputFiles).toBeDefined();
165
+ expect(result!.workerOutputFiles!.length).toBeGreaterThan(0);
166
+ });
167
+
168
+ it("write: true일 때 workerOutputFiles가 undefined이다", () => {
169
+ const entryPath = path.join(fixturesDir, "entry.js");
170
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-write-"));
171
+
172
+ const result = transformWorkerPatterns(
173
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
174
+ entryPath,
175
+ createMockBuild({ write: true, outdir: tmpDir }),
176
+ );
177
+
178
+ expect(result).toBeDefined();
179
+ expect(result!.workerOutputFiles).toBeUndefined();
180
+
181
+ // 대신 디스크에 파일이 기록됨
182
+ const files = fs.readdirSync(tmpDir);
183
+ const workerFile = files.find((f) => /worker-[a-z0-9]+\.js$/i.test(f));
184
+ expect(workerFile).toBeDefined();
185
+ });
186
+ });
187
+
188
+ describe("transformWorkerPatterns — import.meta.resolve 패턴", () => {
189
+ it("import.meta.resolve 상대 경로 패턴을 감지하여 치환한다", () => {
190
+ const entryPath = path.join(fixturesDir, "entry.js");
191
+
192
+ const result = transformWorkerPatterns(
193
+ `const p = import.meta.resolve("./node-worker.js");`,
194
+ entryPath,
195
+ createMockBuild({ platform: "node" }),
196
+ );
197
+
198
+ expect(result).toBeDefined();
199
+ expect(result!.contents).not.toContain("./node-worker.js");
200
+ expect(result!.contents).toMatch(/new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i);
201
+ expect(result!.errors).toHaveLength(0);
202
+ });
203
+
204
+ it("절대 모듈 경로의 import.meta.resolve는 무시한다", () => {
205
+ const result = transformWorkerPatterns(
206
+ `const p = import.meta.resolve("some-package");`,
207
+ "/test/entry.js",
208
+ createMockBuild({ platform: "node" }),
209
+ );
210
+
211
+ expect(result).toBeUndefined();
212
+ });
213
+
214
+ it("import.meta.resolve 패턴 없는 파일은 undefined를 반환한다", () => {
215
+ const result = transformWorkerPatterns(
216
+ `console.log("no resolve");`,
217
+ "/test/entry.js",
218
+ createMockBuild({ platform: "node" }),
219
+ );
220
+
221
+ expect(result).toBeUndefined();
222
+ });
223
+
224
+ it("브라우저 + Node.js Worker 패턴이 공존하면 모두 치환한다", () => {
225
+ const entryPath = path.join(fixturesDir, "entry.js");
226
+
227
+ const result = transformWorkerPatterns(
228
+ [
229
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
230
+ `const p = import.meta.resolve("./node-worker.js");`,
231
+ ].join("\n"),
232
+ entryPath,
233
+ createMockBuild({ platform: "node" }),
234
+ );
235
+
236
+ expect(result).toBeDefined();
237
+ // 브라우저 Worker 치환
238
+ expect(result!.contents).not.toContain('"./worker.js"');
239
+ // Node.js resolve 치환
240
+ expect(result!.contents).not.toContain("import.meta.resolve");
241
+ expect(result!.contents).toMatch(/new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i);
242
+ });
243
+
244
+ it("import.meta.resolve Worker 빌드 에러를 errors에 포함한다", () => {
245
+ const entryPath = path.join(fixturesDir, "entry.js");
246
+
247
+ const result = transformWorkerPatterns(
248
+ `const p = import.meta.resolve("./worker-error.js");`,
249
+ entryPath,
250
+ createMockBuild({ platform: "node" }),
251
+ );
252
+
253
+ expect(result).toBeDefined();
254
+ expect(result!.errors.length).toBeGreaterThan(0);
255
+ });
256
+
257
+ it("write: false일 때 workerOutputFiles를 반환한다", () => {
258
+ const entryPath = path.join(fixturesDir, "entry.js");
259
+
260
+ const result = transformWorkerPatterns(
261
+ `const p = import.meta.resolve("./node-worker.js");`,
262
+ entryPath,
263
+ createMockBuild({ write: false, platform: "node" }),
264
+ );
265
+
266
+ expect(result).toBeDefined();
267
+ expect(result!.workerOutputFiles).toBeDefined();
268
+ expect(result!.workerOutputFiles!.length).toBeGreaterThan(0);
269
+ });
270
+ });
271
+
272
+ describe("createWorkerBundlePlugin — 플러그인 구조", () => {
273
+ it("esbuild Plugin 프로토콜을 따르는 객체를 반환한다", () => {
274
+ const plugin = createWorkerBundlePlugin();
275
+
276
+ expect(plugin.name).toBe("sd-worker-bundle");
277
+ expect(typeof plugin.setup).toBe("function");
278
+ });
279
+
280
+ it("setup에서 onLoad/onEnd 훅이 등록된다", async () => {
281
+ const plugin = createWorkerBundlePlugin();
282
+
283
+ let hasOnLoad = false;
284
+ let hasOnEnd = false;
285
+
286
+ const mockBuild = {
287
+ initialOptions: {},
288
+ onLoad(_opts: unknown, _cb: unknown) { hasOnLoad = true; },
289
+ onEnd(_cb: unknown) { hasOnEnd = true; },
290
+ } as unknown as esbuild.PluginBuild;
291
+
292
+ await plugin.setup(mockBuild);
293
+
294
+ expect(hasOnLoad).toBe(true);
295
+ expect(hasOnEnd).toBe(true);
296
+ });
297
+ });
@@ -0,0 +1,7 @@
1
+ # Worker 빌드 경고가 메인 빌드로 전파됨 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] `transformWorkerPatterns`에서 `workerResult.warnings`를 warnings 배열에 push: `esbuild-worker-plugin.ts:109` — `warnings.push(...workerResult.warnings)` 확인. esbuild.buildSync의 BuildResult.warnings가 그대로 전달됨.
6
+ - [x] 반환 결과의 `warnings` 필드에 수집된 경고가 포함됨: `esbuild-worker-plugin.ts:172` — `warnings` 배열이 결과 객체에 포함됨.
7
+ - [x] 플러그인의 onLoad에서 warnings를 esbuild에 전달: `esbuild-worker-plugin.ts:210` — `warnings: result.warnings.length > 0 ? result.warnings : undefined` — onLoad 결과에 warnings를 포함하여 esbuild가 빌드 경고로 처리.
@@ -0,0 +1,2 @@
1
+ const path = require("path");
2
+ process.stdout.write("node-worker:" + path.resolve("."));
@@ -0,0 +1,6 @@
1
+ self.onconnect = (e) => {
2
+ const port = e.ports[0];
3
+ port.onmessage = (ev) => {
4
+ port.postMessage(ev.data);
5
+ };
6
+ };
@@ -0,0 +1 @@
1
+ this is not valid javascript }{][
@@ -0,0 +1,3 @@
1
+ self.onmessage = (e) => {
2
+ self.postMessage(e.data);
3
+ };
@@ -0,0 +1,3 @@
1
+ self.onmessage = (e) => {
2
+ self.postMessage("worker2:" + e.data);
3
+ };
@@ -0,0 +1,9 @@
1
+ # Feature 1.3 서버 빌드에 Worker 플러그인 적용 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] 프로덕션 빌드에 Worker 플러그인 포함: `server-build.worker.ts:169`에서 `plugins: [createWorkerBundlePlugin(), tscPlugin.plugin]`로 설정됨. Worker 플러그인이 tsc 플러그인보다 앞에 위치.
6
+ - [x] Watch/dev 빌드에 Worker 플러그인 포함: `server-esbuild-context.ts:66-69`에서 `workerPlugin` 변수로 생성 후 tsc 유무에 따라 `[workerPlugin, tscPlugin.plugin]` 또는 `[workerPlugin]`으로 설정됨.
7
+ - [x] Worker 패턴 없는 서버 코드 투명 통과: `esbuild-worker-plugin.ts:83-85`에서 `content.includes("Worker")`가 false면 즉시 `undefined` 반환. `esbuild-worker-plugin.ts:197`의 onLoad에서 `result == null`이면 `undefined` 반환하여 다음 핸들러에 위임.
8
+ - [x] 기존 tsc 플러그인/external 처리 미변경: `createServerEsbuildOptions` 함수 미수정. tsc 플러그인 생성 로직 미변경. external 처리 미변경.
9
+ - [x] import 경로에 `.js` 확장자 미사용: 두 파일 모두 `from "../esbuild/esbuild-worker-plugin"` (확장자 없음)
@@ -246,6 +246,20 @@ describe("server-build.worker build()", () => {
246
246
  });
247
247
  });
248
248
 
249
+ // Acceptance: production build includes worker bundle plugin
250
+ it("includes worker bundle plugin in esbuild.build() plugins", async () => {
251
+ await workerFns["build"](baseBuildInfo);
252
+
253
+ expect(esbuild.build).toHaveBeenCalledWith(
254
+ expect.objectContaining({
255
+ plugins: [
256
+ expect.objectContaining({ name: "sd-worker-bundle" }),
257
+ mockTscPlugin.plugin,
258
+ ],
259
+ }),
260
+ );
261
+ });
262
+
249
263
  // Acceptance: esbuild + typecheck parallel execution
250
264
  it("runs esbuild and tsc in parallel for server build", async () => {
251
265
  const result = await workerFns["build"](baseBuildInfo);
@@ -442,7 +456,7 @@ describe("server-build.worker build()", () => {
442
456
  expect(pkg.volta.node).toBe("v20.11.0");
443
457
  });
444
458
 
445
- it("collects externals from three sources", async () => {
459
+ it("includes nativeModules and manual externals in dist/package.json but excludes missing optional peer deps", async () => {
446
460
  vi.mocked(collectAllDependencyExternals).mockReturnValue({
447
461
  optionalPeerDeps: ["opt-dep"],
448
462
  nativeModules: ["native-mod"],
@@ -451,9 +465,6 @@ describe("server-build.worker build()", () => {
451
465
  mockLockfileContent = [
452
466
  "packages:",
453
467
  "",
454
- " 'opt-dep@1.2.3':",
455
- " resolution: {integrity: sha512-a}",
456
- "",
457
468
  " 'native-mod@2.4.0':",
458
469
  " resolution: {integrity: sha512-b}",
459
470
  "",
@@ -473,16 +484,16 @@ describe("server-build.worker build()", () => {
473
484
 
474
485
  const pkgJsonPath = path.join(baseBuildInfo.pkgDir, "dist", "package.json");
475
486
  const pkg = JSON.parse(writtenFiles.get(pkgJsonPath)!);
476
- expect(pkg.dependencies["opt-dep"]).toBe("1.2.3");
487
+ expect(pkg.dependencies["opt-dep"]).toBeUndefined();
477
488
  expect(pkg.dependencies["native-mod"]).toBe("2.4.0");
478
489
  expect(pkg.dependencies["manual-ext"]).toBe("3.1.0");
479
490
  });
480
491
 
481
- // Unit: reports error for externals not found in pnpm-lock.yaml
492
+ // Unit: reports error when a nativeModule/manual external is missing from lockfile
482
493
  it("reports error for external dependency not in lockfile", async () => {
483
494
  vi.mocked(collectAllDependencyExternals).mockReturnValue({
484
- optionalPeerDeps: ["unknown-dep"],
485
- nativeModules: [],
495
+ optionalPeerDeps: [],
496
+ nativeModules: ["unknown-native"],
486
497
  });
487
498
 
488
499
  mockLockfileContent = [
@@ -499,7 +510,7 @@ describe("server-build.worker build()", () => {
499
510
 
500
511
  const result = await workerFns["build"](baseBuildInfo);
501
512
  expect(result.build.success).toBe(false);
502
- expect(result.build.errors[0]).toContain("unknown-dep");
513
+ expect(result.build.errors[0]).toContain("unknown-native");
503
514
  expect(result.build.errors[0]).toContain("not found in pnpm-lock.yaml");
504
515
  });
505
516
 
@@ -699,8 +710,8 @@ describe("server-build.worker startWatch()", () => {
699
710
  }));
700
711
  });
701
712
 
702
- // Acceptance: startWatch passes tsc options to createContext
703
- it("passes tsc options to esbuildCtx.createContext", async () => {
713
+ // Acceptance: startWatch passes tsc options to createContext with worker plugin
714
+ it("passes worker bundle plugin and tsc plugin to esbuildCtx.createContext", async () => {
704
715
  await workerFns["startWatch"]({
705
716
  ...watchInfo,
706
717
  output: { js: true, dts: true, env: "node" as any, includeTests: true },
@@ -708,7 +719,10 @@ describe("server-build.worker startWatch()", () => {
708
719
 
709
720
  expect(esbuild.context).toHaveBeenCalledWith(
710
721
  expect.objectContaining({
711
- plugins: [mockTscPlugin.plugin],
722
+ plugins: [
723
+ expect.objectContaining({ name: "sd-worker-bundle" }),
724
+ mockTscPlugin.plugin,
725
+ ],
712
726
  }),
713
727
  );
714
728
  });
@@ -351,7 +351,7 @@ describe("createContext — tsc plugin", () => {
351
351
  await dispose();
352
352
  });
353
353
 
354
- it("includes tsc plugin in esbuild context when tsc options provided", async () => {
354
+ it("includes worker bundle plugin and tsc plugin when tsc options provided", async () => {
355
355
  await createContext({
356
356
  ...baseOptions,
357
357
  tsc: { cwd: "/workspace", output: { dts: true } },
@@ -359,17 +359,22 @@ describe("createContext — tsc plugin", () => {
359
359
 
360
360
  expect(esbuild.context).toHaveBeenCalledWith(
361
361
  expect.objectContaining({
362
- plugins: [mockTscPlugin.plugin],
362
+ plugins: [
363
+ expect.objectContaining({ name: "sd-worker-bundle" }),
364
+ mockTscPlugin.plugin,
365
+ ],
363
366
  }),
364
367
  );
365
368
  });
366
369
 
367
- it("creates esbuild context without plugins when no tsc options and no existing plugin", async () => {
370
+ it("includes worker bundle plugin only when no tsc options", async () => {
368
371
  await createContext(baseOptions);
369
372
 
370
373
  expect(esbuild.context).toHaveBeenCalledWith(
371
374
  expect.objectContaining({
372
- plugins: [],
375
+ plugins: [
376
+ expect.objectContaining({ name: "sd-worker-bundle" }),
377
+ ],
373
378
  }),
374
379
  );
375
380
  });
@@ -389,7 +394,10 @@ describe("createContext — tsc plugin", () => {
389
394
 
390
395
  expect(esbuild.context).toHaveBeenCalledWith(
391
396
  expect.objectContaining({
392
- plugins: [mockTscPlugin.plugin],
397
+ plugins: [
398
+ expect.objectContaining({ name: "sd-worker-bundle" }),
399
+ mockTscPlugin.plugin,
400
+ ],
393
401
  }),
394
402
  );
395
403
  });
@@ -48,7 +48,7 @@ vi.mock("../../src/utils/tsconfig", () => ({
48
48
  }));
49
49
 
50
50
  vi.mock("../../src/deps/server-externals/server-production-files", () => ({
51
- collectAllExternals: vi.fn(() => []),
51
+ collectAllExternals: vi.fn(() => ({ bundleExternals: [], prodDependencies: [] })),
52
52
  }));
53
53
 
54
54
  //#endregion
@@ -93,7 +93,7 @@ describe("startServerWatchLoop", () => {
93
93
  vi.mocked(esbuildCtx.hasContext).mockReturnValue(true);
94
94
  vi.mocked(esbuildCtx.getMetafile).mockReturnValue(undefined);
95
95
  vi.mocked(esbuildCtx.recreateContext).mockResolvedValue();
96
- vi.mocked(collectAllExternals).mockReturnValue([]);
96
+ vi.mocked(collectAllExternals).mockReturnValue({ bundleExternals: [], prodDependencies: [] });
97
97
  (mockLogger.debug as unknown as ReturnType<typeof vi.fn>).mockClear();
98
98
  });
99
99
 
@@ -45,7 +45,7 @@ vi.mock("../../src/utils/tsconfig", () => ({
45
45
  }));
46
46
 
47
47
  vi.mock("../../src/deps/server-externals/server-production-files", () => ({
48
- collectAllExternals: vi.fn(() => []),
48
+ collectAllExternals: vi.fn(() => ({ bundleExternals: [], prodDependencies: [] })),
49
49
  }));
50
50
 
51
51
  //#endregion
@@ -89,7 +89,7 @@ describe("startServerWatchLoop", () => {
89
89
  vi.mocked(esbuildCtx.hasContext).mockReset().mockReturnValue(true);
90
90
  vi.mocked(esbuildCtx.getMetafile).mockReset().mockReturnValue(undefined);
91
91
  vi.mocked(esbuildCtx.recreateContext).mockReset().mockResolvedValue();
92
- vi.mocked(collectAllExternals).mockReset().mockReturnValue([]);
92
+ vi.mocked(collectAllExternals).mockReset().mockReturnValue({ bundleExternals: [], prodDependencies: [] });
93
93
  (mockLogger.debug as unknown as ReturnType<typeof vi.fn>).mockClear();
94
94
  });
95
95
 
@@ -1,9 +0,0 @@
1
- import ts from "typescript";
2
- /**
3
- * Web Worker/SharedWorker의 `new Worker(new URL('path', import.meta.url))` 패턴을
4
- * 감지하여 fileProcessor로 번들된 경로로 치환하는 TypeScript transformer를 생성한다.
5
- *
6
- * @angular/build의 web-worker-transformer.js 원본을 이식한 구현.
7
- */
8
- export declare function createWorkerTransformer(fileProcessor: (workerFile: string, containingFile: string) => string): ts.TransformerFactory<ts.SourceFile>;
9
- //# sourceMappingURL=web-worker-transformer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"web-worker-transformer.d.ts","sourceRoot":"","sources":["../../src/angular/web-worker-transformer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,MAAM,GACpE,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC,UAAU,CAAC,CA0GtC"}
@@ -1,73 +0,0 @@
1
- import ts from "typescript";
2
- /**
3
- * Web Worker/SharedWorker의 `new Worker(new URL('path', import.meta.url))` 패턴을
4
- * 감지하여 fileProcessor로 번들된 경로로 치환하는 TypeScript transformer를 생성한다.
5
- *
6
- * @angular/build의 web-worker-transformer.js 원본을 이식한 구현.
7
- */
8
- export function createWorkerTransformer(fileProcessor) {
9
- return (context) => {
10
- const nodeFactory = context.factory;
11
- const visitNode = (node) => {
12
- // new Worker(...) 또는 new SharedWorker(...) 감지
13
- if (!ts.isNewExpression(node) ||
14
- !ts.isIdentifier(node.expression) ||
15
- (node.expression.text !== "Worker" && node.expression.text !== "SharedWorker")) {
16
- return ts.visitEachChild(node, visitNode, context);
17
- }
18
- // Worker 인자: 1개 또는 2개
19
- if (node.arguments == null || node.arguments.length < 1 || node.arguments.length > 2) {
20
- return node;
21
- }
22
- // 첫 인자: new URL(...)
23
- const workerUrlNode = node.arguments[0];
24
- if (!ts.isNewExpression(workerUrlNode) ||
25
- !ts.isIdentifier(workerUrlNode.expression) ||
26
- workerUrlNode.expression.text !== "URL") {
27
- return node;
28
- }
29
- // URL 인자: 정확히 2개
30
- if (workerUrlNode.arguments == null || workerUrlNode.arguments.length !== 2) {
31
- return node;
32
- }
33
- // URL 첫 인자: 문자열 리터럴
34
- if (!ts.isStringLiteralLike(workerUrlNode.arguments[0])) {
35
- return node;
36
- }
37
- // URL 둘째 인자: import.meta.url
38
- const secondArg = workerUrlNode.arguments[1];
39
- if (!ts.isPropertyAccessExpression(secondArg) ||
40
- !ts.isMetaProperty(secondArg.expression) ||
41
- secondArg.name.text !== "url") {
42
- return node;
43
- }
44
- const filePath = workerUrlNode.arguments[0].text;
45
- const importer = node.getSourceFile().fileName;
46
- // fileProcessor 호출
47
- const replacementPath = fileProcessor(filePath, importer);
48
- // 경로가 변경되지 않았으면 원본 유지
49
- if (replacementPath === filePath) {
50
- return node;
51
- }
52
- // AST 치환
53
- return nodeFactory.updateNewExpression(node, node.expression, node.typeArguments, ts.setTextRange(nodeFactory.createNodeArray([
54
- // URL 인자 치환
55
- nodeFactory.updateNewExpression(workerUrlNode, workerUrlNode.expression, workerUrlNode.typeArguments, ts.setTextRange(nodeFactory.createNodeArray([nodeFactory.createStringLiteral(replacementPath), workerUrlNode.arguments[1]], workerUrlNode.arguments.hasTrailingComma), workerUrlNode.arguments)),
56
- // 두 번째 인자: 기존 options가 있으면 유지, 없으면 { type: 'module' } 추가
57
- node.arguments.length > 1
58
- ? node.arguments[1]
59
- : nodeFactory.createObjectLiteralExpression([
60
- nodeFactory.createPropertyAssignment("type", nodeFactory.createStringLiteral("module")),
61
- ]),
62
- ], node.arguments.hasTrailingComma), node.arguments));
63
- };
64
- return (sourceFile) => {
65
- // 'Worker' 문자열이 없으면 변환을 건너뛴다
66
- if (!sourceFile.text.includes("Worker")) {
67
- return sourceFile;
68
- }
69
- return ts.visitEachChild(sourceFile, visitNode, context);
70
- };
71
- };
72
- }
73
- //# sourceMappingURL=web-worker-transformer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"web-worker-transformer.js","sourceRoot":"","sources":["../../src/angular/web-worker-transformer.ts"],"names":[],"mappings":"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"}