@simplysm/sd-cli 14.0.44 → 14.0.46

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 (58) 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 +27 -29
  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-config.d.ts.map +1 -1
  10. package/dist/esbuild/esbuild-config.js +2 -5
  11. package/dist/esbuild/esbuild-config.js.map +1 -1
  12. package/dist/esbuild/esbuild-worker-plugin.d.ts +52 -0
  13. package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
  14. package/dist/esbuild/esbuild-worker-plugin.js +319 -0
  15. package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
  16. package/dist/utils/output-path-rewriter.d.ts +5 -2
  17. package/dist/utils/output-path-rewriter.d.ts.map +1 -1
  18. package/dist/utils/output-path-rewriter.js +60 -12
  19. package/dist/utils/output-path-rewriter.js.map +1 -1
  20. package/dist/workers/server-build.worker.d.ts.map +1 -1
  21. package/dist/workers/server-build.worker.js +6 -5
  22. package/dist/workers/server-build.worker.js.map +1 -1
  23. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  24. package/dist/workers/server-esbuild-context.js +3 -1
  25. package/dist/workers/server-esbuild-context.js.map +1 -1
  26. package/dist/workers/server-watch-manager.js +1 -1
  27. package/dist/workers/server-watch-manager.js.map +1 -1
  28. package/package.json +7 -4
  29. package/src/deps/server-externals/server-production-files.ts +31 -31
  30. package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
  31. package/src/esbuild/esbuild-config.ts +2 -7
  32. package/src/esbuild/esbuild-worker-plugin.ts +419 -0
  33. package/src/utils/output-path-rewriter.ts +65 -17
  34. package/src/workers/server-build.worker.ts +6 -5
  35. package/src/workers/server-esbuild-context.ts +3 -1
  36. package/src/workers/server-watch-manager.ts +1 -1
  37. package/tests/deps/server-externals/mise-toml-parse-intent.verify.md +16 -0
  38. package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
  39. package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
  40. package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
  41. package/tests/esbuild/esbuild-worker-plugin.spec.ts +606 -0
  42. package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
  43. package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
  44. package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
  45. package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
  46. package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
  47. package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
  48. package/tests/workers/server-build-worker-plugin.verify.md +9 -0
  49. package/tests/workers/server-build-worker.spec.ts +26 -12
  50. package/tests/workers/server-esbuild-context.spec.ts +13 -5
  51. package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
  52. package/tests/workers/server-watch-manager.spec.ts +2 -2
  53. package/dist/angular/web-worker-transformer.d.ts +0 -9
  54. package/dist/angular/web-worker-transformer.d.ts.map +0 -1
  55. package/dist/angular/web-worker-transformer.js +0 -73
  56. package/dist/angular/web-worker-transformer.js.map +0 -1
  57. package/src/angular/web-worker-transformer.ts +0 -117
  58. package/tests/angular/web-worker-transformer.spec.ts +0 -154
@@ -0,0 +1,606 @@
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, findWorkerPatterns } = 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 패턴은 무시한다", () => {
63
+ const result = transformWorkerPatterns(
64
+ `// new Worker(new URL("./worker.js", import.meta.url))
65
+ const x = 1;`,
66
+ "/test/entry.js",
67
+ createMockBuild(),
68
+ );
69
+
70
+ expect(result).toBeUndefined();
71
+ });
72
+
73
+ it("문자열 리터럴 내 Worker 패턴은 무시한다", () => {
74
+ const result = transformWorkerPatterns(
75
+ `const s = "new Worker(new URL(\\"./worker.js\\", import.meta.url))";`,
76
+ "/test/entry.js",
77
+ createMockBuild(),
78
+ );
79
+
80
+ expect(result).toBeUndefined();
81
+ });
82
+
83
+ it("Worker + new URL + import.meta.url 패턴을 감지하여 치환한다", () => {
84
+ const entryPath = path.join(fixturesDir, "entry.js");
85
+
86
+ const result = transformWorkerPatterns(
87
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
88
+ entryPath,
89
+ createMockBuild(),
90
+ );
91
+
92
+ expect(result).toBeDefined();
93
+ expect(result!.contents).not.toContain("./worker.js");
94
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
95
+ expect(result!.errors).toHaveLength(0);
96
+ });
97
+
98
+ it("SharedWorker + new URL + import.meta.url 패턴을 감지하여 치환한다", () => {
99
+ const entryPath = path.join(fixturesDir, "entry.js");
100
+
101
+ const result = transformWorkerPatterns(
102
+ `const sw = new SharedWorker(new URL("./shared-worker.js", import.meta.url));`,
103
+ entryPath,
104
+ createMockBuild(),
105
+ );
106
+
107
+ expect(result).toBeDefined();
108
+ expect(result!.contents).not.toContain("./shared-worker.js");
109
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
110
+ });
111
+
112
+ it("복수 Worker 패턴을 모두 치환한다", () => {
113
+ const entryPath = path.join(fixturesDir, "entry.js");
114
+
115
+ const result = transformWorkerPatterns(
116
+ [
117
+ `const w1 = new Worker(new URL("./worker.js", import.meta.url));`,
118
+ `const w2 = new Worker(new URL("./worker2.js", import.meta.url));`,
119
+ ].join("\n"),
120
+ entryPath,
121
+ createMockBuild(),
122
+ );
123
+
124
+ expect(result).toBeDefined();
125
+ expect(result!.contents).not.toContain("./worker.js");
126
+ expect(result!.contents).not.toContain("./worker2.js");
127
+ });
128
+ });
129
+
130
+ describe("transformWorkerPatterns — type: module 처리", () => {
131
+ it("옵션 없는 Worker에 { type: 'module' }을 추가한다", () => {
132
+ const entryPath = path.join(fixturesDir, "entry.js");
133
+
134
+ const result = transformWorkerPatterns(
135
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
136
+ entryPath,
137
+ createMockBuild(),
138
+ );
139
+
140
+ expect(result).toBeDefined();
141
+ // { type: "module" } 추가됨
142
+ expect(result!.contents).toMatch(/\{\s*type:\s*"module"\s*\}/);
143
+ });
144
+
145
+ it("기존 옵션이 있으면 그대로 유지한다", () => {
146
+ const entryPath = path.join(fixturesDir, "entry.js");
147
+
148
+ const result = transformWorkerPatterns(
149
+ `const w = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });`,
150
+ entryPath,
151
+ createMockBuild(),
152
+ );
153
+
154
+ expect(result).toBeDefined();
155
+ expect(result!.contents).toContain('{ type: "module" }');
156
+ });
157
+ });
158
+
159
+ describe("transformWorkerPatterns — 에러 처리", () => {
160
+ it("Worker 빌드 에러를 errors에 포함하여 반환한다", () => {
161
+ const entryPath = path.join(fixturesDir, "entry.js");
162
+
163
+ const result = transformWorkerPatterns(
164
+ `const w = new Worker(new URL("./worker-error.js", import.meta.url));`,
165
+ entryPath,
166
+ createMockBuild(),
167
+ );
168
+
169
+ expect(result).toBeDefined();
170
+ expect(result!.errors.length).toBeGreaterThan(0);
171
+ });
172
+ });
173
+
174
+ describe("transformWorkerPatterns — write 옵션", () => {
175
+ it("write: false일 때 workerOutputFiles를 반환한다", () => {
176
+ const entryPath = path.join(fixturesDir, "entry.js");
177
+
178
+ const result = transformWorkerPatterns(
179
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
180
+ entryPath,
181
+ createMockBuild({ write: false }),
182
+ );
183
+
184
+ expect(result).toBeDefined();
185
+ expect(result!.workerOutputFiles).toBeDefined();
186
+ expect(result!.workerOutputFiles!.length).toBeGreaterThan(0);
187
+ });
188
+
189
+ it("write: true일 때 workerOutputFiles가 undefined이다", () => {
190
+ const entryPath = path.join(fixturesDir, "entry.js");
191
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-write-"));
192
+
193
+ const result = transformWorkerPatterns(
194
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
195
+ entryPath,
196
+ createMockBuild({ write: true, outdir: tmpDir }),
197
+ );
198
+
199
+ expect(result).toBeDefined();
200
+ expect(result!.workerOutputFiles).toBeUndefined();
201
+
202
+ // 대신 디스크에 파일이 기록됨
203
+ const files = fs.readdirSync(tmpDir);
204
+ const workerFile = files.find((f) => /worker-[a-z0-9]+\.js$/i.test(f));
205
+ expect(workerFile).toBeDefined();
206
+ });
207
+ });
208
+
209
+ describe("transformWorkerPatterns — import.meta.resolve 패턴", () => {
210
+ it("import.meta.resolve 상대 경로 패턴을 감지하여 치환한다", () => {
211
+ const entryPath = path.join(fixturesDir, "entry.js");
212
+
213
+ const result = transformWorkerPatterns(
214
+ `const p = import.meta.resolve("./node-worker.js");`,
215
+ entryPath,
216
+ createMockBuild({ platform: "node" }),
217
+ );
218
+
219
+ expect(result).toBeDefined();
220
+ expect(result!.contents).not.toContain("./node-worker.js");
221
+ expect(result!.contents).toMatch(/new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i);
222
+ expect(result!.errors).toHaveLength(0);
223
+ });
224
+
225
+ it("절대 모듈 경로의 import.meta.resolve는 무시한다", () => {
226
+ const result = transformWorkerPatterns(
227
+ `const p = import.meta.resolve("some-package");`,
228
+ "/test/entry.js",
229
+ createMockBuild({ platform: "node" }),
230
+ );
231
+
232
+ expect(result).toBeUndefined();
233
+ });
234
+
235
+ it("import.meta.resolve 패턴 없는 파일은 undefined를 반환한다", () => {
236
+ const result = transformWorkerPatterns(
237
+ `console.log("no resolve");`,
238
+ "/test/entry.js",
239
+ createMockBuild({ platform: "node" }),
240
+ );
241
+
242
+ expect(result).toBeUndefined();
243
+ });
244
+
245
+ it("브라우저 + Node.js Worker 패턴이 공존하면 모두 치환한다", () => {
246
+ const entryPath = path.join(fixturesDir, "entry.js");
247
+
248
+ const result = transformWorkerPatterns(
249
+ [
250
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
251
+ `const p = import.meta.resolve("./node-worker.js");`,
252
+ ].join("\n"),
253
+ entryPath,
254
+ createMockBuild({ platform: "node" }),
255
+ );
256
+
257
+ expect(result).toBeDefined();
258
+ // 브라우저 Worker 치환
259
+ expect(result!.contents).not.toContain('"./worker.js"');
260
+ // Node.js resolve 치환
261
+ expect(result!.contents).not.toContain("import.meta.resolve");
262
+ expect(result!.contents).toMatch(/new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i);
263
+ });
264
+
265
+ it("import.meta.resolve Worker 빌드 에러를 errors에 포함한다", () => {
266
+ const entryPath = path.join(fixturesDir, "entry.js");
267
+
268
+ const result = transformWorkerPatterns(
269
+ `const p = import.meta.resolve("./worker-error.js");`,
270
+ entryPath,
271
+ createMockBuild({ platform: "node" }),
272
+ );
273
+
274
+ expect(result).toBeDefined();
275
+ expect(result!.errors.length).toBeGreaterThan(0);
276
+ });
277
+
278
+ it("write: false일 때 workerOutputFiles를 반환한다", () => {
279
+ const entryPath = path.join(fixturesDir, "entry.js");
280
+
281
+ const result = transformWorkerPatterns(
282
+ `const p = import.meta.resolve("./node-worker.js");`,
283
+ entryPath,
284
+ createMockBuild({ write: false, platform: "node" }),
285
+ );
286
+
287
+ expect(result).toBeDefined();
288
+ expect(result!.workerOutputFiles).toBeDefined();
289
+ expect(result!.workerOutputFiles!.length).toBeGreaterThan(0);
290
+ });
291
+ });
292
+
293
+ describe("createWorkerBundlePlugin — 플러그인 구조", () => {
294
+ it("esbuild Plugin 프로토콜을 따르는 객체를 반환한다", () => {
295
+ const plugin = createWorkerBundlePlugin();
296
+
297
+ expect(plugin.name).toBe("sd-worker-bundle");
298
+ expect(typeof plugin.setup).toBe("function");
299
+ });
300
+
301
+ it("setup에서 onLoad/onEnd 훅이 등록된다", async () => {
302
+ const plugin = createWorkerBundlePlugin();
303
+
304
+ let hasOnLoad = false;
305
+ let hasOnEnd = false;
306
+
307
+ const mockBuild = {
308
+ initialOptions: {},
309
+ onLoad(_opts: unknown, _cb: unknown) { hasOnLoad = true; },
310
+ onEnd(_cb: unknown) { hasOnEnd = true; },
311
+ } as unknown as esbuild.PluginBuild;
312
+
313
+ await plugin.setup(mockBuild);
314
+
315
+ expect(hasOnLoad).toBe(true);
316
+ expect(hasOnEnd).toBe(true);
317
+ });
318
+ });
319
+
320
+ describe("findWorkerPatterns — AST 기반 패턴 탐지", () => {
321
+ it("Worker + new URL + import.meta.url 패턴을 탐지한다", () => {
322
+ const matches = findWorkerPatterns(
323
+ `const w = new Worker(new URL("./worker.js", import.meta.url));`,
324
+ );
325
+
326
+ expect(matches).toHaveLength(1);
327
+ expect(matches[0].type).toBe("browser");
328
+ expect(matches[0].urlPath).toBe("./worker.js");
329
+ expect(matches[0].workerType).toBe("Worker");
330
+ expect(matches[0].existingOpts).toBeUndefined();
331
+ });
332
+
333
+ it("SharedWorker 패턴을 탐지한다", () => {
334
+ const matches = findWorkerPatterns(
335
+ `const sw = new SharedWorker(new URL("./sw.js", import.meta.url));`,
336
+ );
337
+
338
+ expect(matches).toHaveLength(1);
339
+ expect(matches[0].workerType).toBe("SharedWorker");
340
+ expect(matches[0].urlPath).toBe("./sw.js");
341
+ });
342
+
343
+ it("옵션 객체가 있는 Worker 패턴을 탐지하고 원본 텍스트를 보존한다", () => {
344
+ const content = `const w = new Worker(new URL("./w.js", import.meta.url), { type: "module" });`;
345
+ const matches = findWorkerPatterns(content);
346
+
347
+ expect(matches).toHaveLength(1);
348
+ expect(matches[0].existingOpts).toBe('{ type: "module" }');
349
+ });
350
+
351
+ it("import.meta.resolve 상대 경로 패턴을 탐지한다", () => {
352
+ const matches = findWorkerPatterns(
353
+ `const p = import.meta.resolve("./node-worker.js");`,
354
+ );
355
+
356
+ expect(matches).toHaveLength(1);
357
+ expect(matches[0].type).toBe("node");
358
+ expect(matches[0].urlPath).toBe("./node-worker.js");
359
+ });
360
+
361
+ it("import.meta.resolve 절대 모듈 경로는 무시한다", () => {
362
+ const matches = findWorkerPatterns(
363
+ `const p = import.meta.resolve("some-package");`,
364
+ );
365
+
366
+ expect(matches).toHaveLength(0);
367
+ });
368
+
369
+ it("new URL 없는 Worker는 무시한다", () => {
370
+ const matches = findWorkerPatterns(
371
+ `const w = new Worker("./worker.js");`,
372
+ );
373
+
374
+ expect(matches).toHaveLength(0);
375
+ });
376
+
377
+ it("import.meta.url이 아닌 URL 생성은 무시한다", () => {
378
+ const matches = findWorkerPatterns(
379
+ `const w = new Worker(new URL("./w.js", location.href));`,
380
+ );
381
+
382
+ expect(matches).toHaveLength(0);
383
+ });
384
+
385
+ it("복수 패턴을 모두 탐지한다", () => {
386
+ const matches = findWorkerPatterns(
387
+ [
388
+ `const w1 = new Worker(new URL("./w1.js", import.meta.url));`,
389
+ `const w2 = new Worker(new URL("./w2.js", import.meta.url));`,
390
+ ].join("\n"),
391
+ );
392
+
393
+ expect(matches).toHaveLength(2);
394
+ expect(matches[0].urlPath).toBe("./w1.js");
395
+ expect(matches[1].urlPath).toBe("./w2.js");
396
+ });
397
+
398
+ it("start/end 위치가 정확하다", () => {
399
+ const content = `const w = new Worker(new URL("./w.js", import.meta.url));`;
400
+ const matches = findWorkerPatterns(content);
401
+
402
+ expect(matches).toHaveLength(1);
403
+ const matched = content.slice(matches[0].start, matches[0].end);
404
+ expect(matched).toBe(`new Worker(new URL("./w.js", import.meta.url))`);
405
+ });
406
+
407
+ it("주석 내 Worker 패턴은 무시한다", () => {
408
+ const matches = findWorkerPatterns(
409
+ `// new Worker(new URL("./w.js", import.meta.url))
410
+ const x = 1;`,
411
+ );
412
+
413
+ expect(matches).toHaveLength(0);
414
+ });
415
+
416
+ it("문자열 리터럴 내 Worker 패턴은 무시한다", () => {
417
+ const matches = findWorkerPatterns(
418
+ `const s = "new Worker(new URL(\\"./w.js\\", import.meta.url))";`,
419
+ );
420
+
421
+ expect(matches).toHaveLength(0);
422
+ });
423
+
424
+ it("블록 주석 내 Worker 패턴은 무시한다", () => {
425
+ const matches = findWorkerPatterns(
426
+ `/* new Worker(new URL("./w.js", import.meta.url)) */
427
+ const x = 1;`,
428
+ );
429
+
430
+ expect(matches).toHaveLength(0);
431
+ });
432
+
433
+ it("주석 내 import.meta.resolve는 무시한다", () => {
434
+ const matches = findWorkerPatterns(
435
+ `// import.meta.resolve("./w.js")
436
+ const x = 1;`,
437
+ );
438
+
439
+ expect(matches).toHaveLength(0);
440
+ });
441
+
442
+ it("파싱 불가능한 코드는 빈 배열을 반환한다", () => {
443
+ const matches = findWorkerPatterns(
444
+ `this is not valid javascript }{][`,
445
+ );
446
+
447
+ expect(matches).toHaveLength(0);
448
+ });
449
+ });
450
+
451
+ describe("transformWorkerPatterns — TypeScript 파일 처리", () => {
452
+ it("import type이 포함된 .ts 파일에서 Worker 패턴을 탐지하여 치환한다", () => {
453
+ const entryPath = path.join(fixturesDir, "entry.ts");
454
+ const result = transformWorkerPatterns(
455
+ `import type { T } from "pkg";
456
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
457
+ entryPath,
458
+ createMockBuild(),
459
+ );
460
+
461
+ expect(result).toBeDefined();
462
+ expect(result!.contents).not.toContain("./worker.js");
463
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
464
+ expect(result!.errors).toHaveLength(0);
465
+ });
466
+
467
+ it("import type이 포함된 .ts 파일에서 import.meta.resolve 패턴을 탐지하여 치환한다", () => {
468
+ const entryPath = path.join(fixturesDir, "entry.ts");
469
+ const result = transformWorkerPatterns(
470
+ `import type { T } from "pkg";
471
+ const p = import.meta.resolve("./node-worker.js");`,
472
+ entryPath,
473
+ createMockBuild({ platform: "node" }),
474
+ );
475
+
476
+ expect(result).toBeDefined();
477
+ expect(result!.contents).not.toContain("./node-worker.js");
478
+ expect(result!.contents).toMatch(
479
+ /new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i,
480
+ );
481
+ expect(result!.errors).toHaveLength(0);
482
+ });
483
+
484
+ it("타입 어노테이션이 있는 변수 선언(const w: Worker = ...)에서 Worker를 탐지한다", () => {
485
+ const entryPath = path.join(fixturesDir, "entry.ts");
486
+ const result = transformWorkerPatterns(
487
+ `const w: Worker = new Worker(new URL("./worker.js", import.meta.url));`,
488
+ entryPath,
489
+ createMockBuild(),
490
+ );
491
+
492
+ expect(result).toBeDefined();
493
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
494
+ });
495
+
496
+ it(".mts 확장자의 TS 파일도 변환 후 처리한다", () => {
497
+ const entryPath = path.join(fixturesDir, "entry.mts");
498
+ const result = transformWorkerPatterns(
499
+ `import type { T } from "pkg";
500
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
501
+ entryPath,
502
+ createMockBuild(),
503
+ );
504
+
505
+ expect(result).toBeDefined();
506
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
507
+ });
508
+
509
+ it(".cts 확장자의 TS 파일도 변환 후 처리한다", () => {
510
+ const entryPath = path.join(fixturesDir, "entry.cts");
511
+ const result = transformWorkerPatterns(
512
+ `import type { T } from "pkg";
513
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
514
+ entryPath,
515
+ createMockBuild(),
516
+ );
517
+
518
+ expect(result).toBeDefined();
519
+ expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
520
+ });
521
+
522
+ it("TS 파일의 주석 내 Worker 패턴은 무시한다", () => {
523
+ const entryPath = path.join(fixturesDir, "entry.ts");
524
+ const result = transformWorkerPatterns(
525
+ `import type { T } from "pkg";
526
+ // new Worker(new URL("./worker.js", import.meta.url))
527
+ const x: number = 1;`,
528
+ entryPath,
529
+ createMockBuild(),
530
+ );
531
+
532
+ expect(result).toBeUndefined();
533
+ });
534
+
535
+ it("TS 파일의 문자열 리터럴 내 Worker 패턴은 무시한다", () => {
536
+ const entryPath = path.join(fixturesDir, "entry.ts");
537
+ const result = transformWorkerPatterns(
538
+ `import type { T } from "pkg";
539
+ const s: string = "new Worker(new URL(\\"./worker.js\\", import.meta.url))";`,
540
+ entryPath,
541
+ createMockBuild(),
542
+ );
543
+
544
+ expect(result).toBeUndefined();
545
+ });
546
+
547
+ it("사전 필터를 통과하지 못하는 TS 파일(Worker 키워드 없음)은 undefined를 반환한다", () => {
548
+ const entryPath = path.join(fixturesDir, "entry.ts");
549
+ const result = transformWorkerPatterns(
550
+ `import type { T } from "pkg";
551
+ const x: number = 1;`,
552
+ entryPath,
553
+ createMockBuild(),
554
+ );
555
+
556
+ expect(result).toBeUndefined();
557
+ });
558
+
559
+ it("TS 변환 실패(문법 오류) 시 errors에 에러를 포함하여 반환한다", () => {
560
+ const entryPath = path.join(fixturesDir, "entry.ts");
561
+ const result = transformWorkerPatterns(
562
+ `const w = new Worker(new URL("./worker.js", import.meta.url)); const x: =`,
563
+ entryPath,
564
+ createMockBuild(),
565
+ );
566
+
567
+ expect(result).toBeDefined();
568
+ expect(result!.errors.length).toBeGreaterThan(0);
569
+ });
570
+ });
571
+
572
+ describe("createWorkerBundlePlugin — TS 파일 onLoad 반환", () => {
573
+ it("TS 파일에서 Worker 감지 시 loader로 'js'를 반환한다", async () => {
574
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-onload-"));
575
+ const tsFile = path.join(tmpDir, "entry.ts");
576
+ fs.copyFileSync(
577
+ path.join(fixturesDir, "worker.js"),
578
+ path.join(tmpDir, "worker.js"),
579
+ );
580
+ fs.writeFileSync(
581
+ tsFile,
582
+ `import type { T } from "pkg";
583
+ const w = new Worker(new URL("./worker.js", import.meta.url));`,
584
+ );
585
+
586
+ const plugin = createWorkerBundlePlugin();
587
+
588
+ let onLoadCallback: ((args: { path: string }) => Promise<any> | any) | null = null;
589
+ const mockBuild = {
590
+ esbuild,
591
+ initialOptions: { outdir: tmpDir, write: false },
592
+ onLoad: (_filter: unknown, cb: (args: { path: string }) => Promise<any> | any) => {
593
+ onLoadCallback = cb;
594
+ },
595
+ onEnd: () => { /* noop */ },
596
+ } as unknown as esbuild.PluginBuild;
597
+
598
+ await plugin.setup(mockBuild);
599
+ expect(onLoadCallback).not.toBeNull();
600
+
601
+ const result = await onLoadCallback!({ path: tsFile });
602
+ expect(result).toBeDefined();
603
+ expect(result.loader).toBe("js");
604
+ expect(result.contents).toMatch(/worker-[a-z0-9]+\.js/i);
605
+ });
606
+ });
@@ -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"` (확장자 없음)