@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
@@ -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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"}
@@ -1,117 +0,0 @@
1
- import ts from "typescript";
2
-
3
- /**
4
- * Web Worker/SharedWorker의 `new Worker(new URL('path', import.meta.url))` 패턴을
5
- * 감지하여 fileProcessor로 번들된 경로로 치환하는 TypeScript transformer를 생성한다.
6
- *
7
- * @angular/build의 web-worker-transformer.js 원본을 이식한 구현.
8
- */
9
- export function createWorkerTransformer(
10
- fileProcessor: (workerFile: string, containingFile: string) => string,
11
- ): ts.TransformerFactory<ts.SourceFile> {
12
- return (context) => {
13
- const nodeFactory = context.factory;
14
-
15
- const visitNode = (node: ts.Node): ts.Node => {
16
- // new Worker(...) 또는 new SharedWorker(...) 감지
17
- if (
18
- !ts.isNewExpression(node) ||
19
- !ts.isIdentifier(node.expression) ||
20
- (node.expression.text !== "Worker" && node.expression.text !== "SharedWorker")
21
- ) {
22
- return ts.visitEachChild(node, visitNode, context);
23
- }
24
-
25
- // Worker 인자: 1개 또는 2개
26
- if (node.arguments == null || node.arguments.length < 1 || node.arguments.length > 2) {
27
- return node;
28
- }
29
-
30
- // 첫 인자: new URL(...)
31
- const workerUrlNode = node.arguments[0];
32
- if (
33
- !ts.isNewExpression(workerUrlNode) ||
34
- !ts.isIdentifier(workerUrlNode.expression) ||
35
- workerUrlNode.expression.text !== "URL"
36
- ) {
37
- return node;
38
- }
39
-
40
- // URL 인자: 정확히 2개
41
- if (workerUrlNode.arguments == null || workerUrlNode.arguments.length !== 2) {
42
- return node;
43
- }
44
-
45
- // URL 첫 인자: 문자열 리터럴
46
- if (!ts.isStringLiteralLike(workerUrlNode.arguments[0])) {
47
- return node;
48
- }
49
-
50
- // URL 둘째 인자: import.meta.url
51
- const secondArg = workerUrlNode.arguments[1];
52
- if (
53
- !ts.isPropertyAccessExpression(secondArg) ||
54
- !ts.isMetaProperty(secondArg.expression) ||
55
- secondArg.name.text !== "url"
56
- ) {
57
- return node;
58
- }
59
-
60
- const filePath = workerUrlNode.arguments[0].text;
61
- const importer = node.getSourceFile().fileName;
62
-
63
- // fileProcessor 호출
64
- const replacementPath = fileProcessor(filePath, importer);
65
-
66
- // 경로가 변경되지 않았으면 원본 유지
67
- if (replacementPath === filePath) {
68
- return node;
69
- }
70
-
71
- // AST 치환
72
- return nodeFactory.updateNewExpression(
73
- node,
74
- node.expression,
75
- node.typeArguments,
76
- ts.setTextRange(
77
- nodeFactory.createNodeArray(
78
- [
79
- // URL 인자 치환
80
- nodeFactory.updateNewExpression(
81
- workerUrlNode,
82
- workerUrlNode.expression,
83
- workerUrlNode.typeArguments,
84
- ts.setTextRange(
85
- nodeFactory.createNodeArray(
86
- [nodeFactory.createStringLiteral(replacementPath), workerUrlNode.arguments[1]],
87
- workerUrlNode.arguments.hasTrailingComma,
88
- ),
89
- workerUrlNode.arguments,
90
- ),
91
- ),
92
- // 두 번째 인자: 기존 options가 있으면 유지, 없으면 { type: 'module' } 추가
93
- node.arguments.length > 1
94
- ? node.arguments[1]
95
- : nodeFactory.createObjectLiteralExpression([
96
- nodeFactory.createPropertyAssignment(
97
- "type",
98
- nodeFactory.createStringLiteral("module"),
99
- ),
100
- ]),
101
- ],
102
- node.arguments.hasTrailingComma,
103
- ),
104
- node.arguments,
105
- ),
106
- );
107
- };
108
-
109
- return (sourceFile) => {
110
- // 'Worker' 문자열이 없으면 변환을 건너뛴다
111
- if (!sourceFile.text.includes("Worker")) {
112
- return sourceFile;
113
- }
114
- return ts.visitEachChild(sourceFile, visitNode, context);
115
- };
116
- };
117
- }
@@ -1,154 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import ts from "typescript";
3
-
4
- const { createWorkerTransformer } = await import(
5
- "../../src/angular/web-worker-transformer.js"
6
- );
7
-
8
- //#region 헬퍼
9
-
10
- /**
11
- * TypeScript 소스 코드에 transformer를 적용하고 결과 코드를 반환한다.
12
- */
13
- function transformCode(
14
- code: string,
15
- fileProcessor: (workerFile: string, containingFile: string) => string,
16
- fileName = "test.ts",
17
- ): string {
18
- const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ES2022, true);
19
- const transformer = createWorkerTransformer(fileProcessor);
20
- const result = ts.transform(sourceFile, [transformer]);
21
- const printer = ts.createPrinter();
22
- const output = printer.printFile(result.transformed[0]);
23
- result.dispose();
24
- return output;
25
- }
26
-
27
- //#endregion
28
-
29
- describe("createWorkerTransformer", () => {
30
- // Acceptance: Worker 표준 패턴을 감지하여 번들된 경로로 치환한다
31
- it("Worker 표준 패턴 — URL 치환 + { type: 'module' } 자동 추가", () => {
32
- const fileProcessor = vi.fn().mockReturnValue("worker-ABCD1234.js");
33
- const code = `const w = new Worker(new URL('./my-worker.ts', import.meta.url));`;
34
-
35
- const output = transformCode(code, fileProcessor);
36
-
37
- expect(fileProcessor).toHaveBeenCalledWith("./my-worker.ts", "test.ts");
38
- expect(output).toContain('"worker-ABCD1234.js"');
39
- expect(output).toContain('type');
40
- expect(output).toContain('"module"');
41
- expect(output).not.toContain("./my-worker.ts");
42
- });
43
-
44
- // Acceptance: SharedWorker도 동일하게 처리한다
45
- it("SharedWorker도 동일하게 변환한다", () => {
46
- const fileProcessor = vi.fn().mockReturnValue("worker-SHARED01.js");
47
- const code = `const sw = new SharedWorker(new URL('./shared.ts', import.meta.url));`;
48
-
49
- const output = transformCode(code, fileProcessor);
50
-
51
- expect(fileProcessor).toHaveBeenCalledWith("./shared.ts", "test.ts");
52
- expect(output).toContain('"worker-SHARED01.js"');
53
- });
54
-
55
- // Acceptance: Worker의 기존 options 인자가 있으면 유지한다
56
- it("기존 options 인자가 있으면 유지한다", () => {
57
- const fileProcessor = vi.fn().mockReturnValue("worker-OPT00001.js");
58
- const code = `const w = new Worker(new URL('./w.ts', import.meta.url), { name: 'test' });`;
59
-
60
- const output = transformCode(code, fileProcessor);
61
-
62
- expect(output).toContain('"worker-OPT00001.js"');
63
- expect(output).toContain("name");
64
- // { type: 'module' } 자동 추가 없이 기존 options가 유지됨
65
- expect(output).not.toMatch(/type.*module.*name/);
66
- });
67
- });
68
-
69
- describe("createWorkerTransformer — 변환하지 않는 케이스", () => {
70
- const noopProcessor = vi.fn().mockReturnValue("should-not-appear.js");
71
-
72
- // URL 패턴이 아닌 Worker 생성
73
- it("URL 패턴 없이 문자열 인자만 있으면 변환하지 않는다", () => {
74
- const code = `const w = new Worker('./my-worker.ts');`;
75
- const output = transformCode(code, noopProcessor);
76
-
77
- expect(noopProcessor).not.toHaveBeenCalled();
78
- expect(output).toContain("./my-worker.ts");
79
- });
80
-
81
- // import.meta.url이 아닌 URL
82
- it("import.meta.url이 아닌 URL은 변환하지 않는다", () => {
83
- const code = `const w = new Worker(new URL('./w.ts', 'http://example.com'));`;
84
- const output = transformCode(code, noopProcessor);
85
-
86
- expect(noopProcessor).not.toHaveBeenCalled();
87
- expect(output).toContain("./w.ts");
88
- });
89
-
90
- // Worker 인자 없음
91
- it("Worker 인자가 없으면 변환하지 않는다", () => {
92
- const code = `const w = new Worker();`;
93
- transformCode(code, noopProcessor);
94
-
95
- expect(noopProcessor).not.toHaveBeenCalled();
96
- });
97
-
98
- // Worker 인자 3개 이상
99
- it("Worker 인자가 3개 이상이면 변환하지 않는다", () => {
100
- const code = `const w = new Worker(new URL('./w.ts', import.meta.url), {}, 'extra');`;
101
- transformCode(code, noopProcessor);
102
-
103
- expect(noopProcessor).not.toHaveBeenCalled();
104
- });
105
-
106
- // URL 인자가 1개
107
- it("URL 인자가 1개면 변환하지 않는다", () => {
108
- const code = `const w = new Worker(new URL('./w.ts'));`;
109
- transformCode(code, noopProcessor);
110
-
111
- expect(noopProcessor).not.toHaveBeenCalled();
112
- });
113
-
114
- // URL 인자가 3개
115
- it("URL 인자가 3개면 변환하지 않는다", () => {
116
- const code = `const w = new Worker(new URL('./w.ts', import.meta.url, 'extra'));`;
117
- transformCode(code, noopProcessor);
118
-
119
- expect(noopProcessor).not.toHaveBeenCalled();
120
- });
121
-
122
- // 'Worker' 문자열 없으면 skip
123
- it("소스에 'Worker' 문자열이 없으면 변환을 건너뛴다", () => {
124
- const code = `const x = 1 + 2;`;
125
- const output = transformCode(code, noopProcessor);
126
-
127
- expect(noopProcessor).not.toHaveBeenCalled();
128
- expect(output).toContain("1 + 2");
129
- });
130
- });
131
-
132
- describe("createWorkerTransformer — 추가 경계 케이스", () => {
133
- // fileProcessor가 원본과 동일한 경로를 반환하면 변환하지 않는다
134
- it("fileProcessor가 원본 경로를 그대로 반환하면 AST를 변경하지 않는다", () => {
135
- const fileProcessor = vi.fn().mockReturnValue("./my-worker.ts");
136
- const code = `const w = new Worker(new URL('./my-worker.ts', import.meta.url));`;
137
-
138
- const output = transformCode(code, fileProcessor);
139
-
140
- expect(fileProcessor).toHaveBeenCalledWith("./my-worker.ts", "test.ts");
141
- // 원본 경로가 그대로 유지 (변환 없음)
142
- expect(output).toContain("./my-worker.ts");
143
- });
144
-
145
- // 소스에 Worker 문자열은 있지만 new 표현이 아닌 경우
146
- it("Worker 문자열이 있지만 new 표현이 아니면 변환하지 않는다", () => {
147
- const fileProcessor = vi.fn();
148
- const code = `const WorkerName = "test";`;
149
-
150
- transformCode(code, fileProcessor);
151
-
152
- expect(fileProcessor).not.toHaveBeenCalled();
153
- });
154
- });