@simplysm/sd-cli 14.0.43 → 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 (103) hide show
  1. package/dist/angular/ngtsc-build-core.d.ts +12 -3
  2. package/dist/angular/ngtsc-build-core.d.ts.map +1 -1
  3. package/dist/angular/ngtsc-build-core.js +70 -6
  4. package/dist/angular/ngtsc-build-core.js.map +1 -1
  5. package/dist/commands/publish/version-upgrade.d.ts.map +1 -1
  6. package/dist/commands/publish/version-upgrade.js +15 -12
  7. package/dist/commands/publish/version-upgrade.js.map +1 -1
  8. package/dist/deps/replace-deps/replace-deps-resolve.d.ts.map +1 -1
  9. package/dist/deps/replace-deps/replace-deps-resolve.js +6 -7
  10. package/dist/deps/replace-deps/replace-deps-resolve.js.map +1 -1
  11. package/dist/deps/replace-deps/replace-deps.d.ts.map +1 -1
  12. package/dist/deps/replace-deps/replace-deps.js +79 -15
  13. package/dist/deps/replace-deps/replace-deps.js.map +1 -1
  14. package/dist/deps/server-externals/server-production-files.d.ts +10 -5
  15. package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
  16. package/dist/deps/server-externals/server-production-files.js +22 -26
  17. package/dist/deps/server-externals/server-production-files.js.map +1 -1
  18. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts +3 -8
  19. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
  20. package/dist/esbuild/esbuild-angular-compiler-plugin.js +57 -83
  21. package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
  22. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -1
  23. package/dist/esbuild/esbuild-postcss-plugin.js +9 -6
  24. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -1
  25. package/dist/esbuild/esbuild-worker-plugin.d.ts +30 -0
  26. package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
  27. package/dist/esbuild/esbuild-worker-plugin.js +197 -0
  28. package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
  29. package/dist/workers/client.worker.d.ts.map +1 -1
  30. package/dist/workers/client.worker.js +4 -25
  31. package/dist/workers/client.worker.js.map +1 -1
  32. package/dist/workers/incremental-mtime-tracker.d.ts +13 -0
  33. package/dist/workers/incremental-mtime-tracker.d.ts.map +1 -0
  34. package/dist/workers/incremental-mtime-tracker.js +65 -0
  35. package/dist/workers/incremental-mtime-tracker.js.map +1 -0
  36. package/dist/workers/library-build.worker.d.ts.map +1 -1
  37. package/dist/workers/library-build.worker.js +37 -15
  38. package/dist/workers/library-build.worker.js.map +1 -1
  39. package/dist/workers/server-build.worker.d.ts.map +1 -1
  40. package/dist/workers/server-build.worker.js +6 -5
  41. package/dist/workers/server-build.worker.js.map +1 -1
  42. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  43. package/dist/workers/server-esbuild-context.js +3 -1
  44. package/dist/workers/server-esbuild-context.js.map +1 -1
  45. package/dist/workers/server-watch-manager.js +1 -1
  46. package/dist/workers/server-watch-manager.js.map +1 -1
  47. package/package.json +5 -4
  48. package/src/angular/ngtsc-build-core.ts +73 -5
  49. package/src/commands/publish/version-upgrade.ts +43 -34
  50. package/src/deps/replace-deps/replace-deps-resolve.ts +12 -7
  51. package/src/deps/replace-deps/replace-deps.ts +90 -16
  52. package/src/deps/server-externals/server-production-files.ts +26 -28
  53. package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
  54. package/src/esbuild/esbuild-postcss-plugin.ts +9 -6
  55. package/src/esbuild/esbuild-worker-plugin.ts +266 -0
  56. package/src/workers/client.worker.ts +4 -23
  57. package/src/workers/incremental-mtime-tracker.ts +68 -0
  58. package/src/workers/library-build.worker.ts +41 -14
  59. package/src/workers/server-build.worker.ts +6 -5
  60. package/src/workers/server-esbuild-context.ts +3 -1
  61. package/src/workers/server-watch-manager.ts +1 -1
  62. package/tests/angular/ngtsc-build-core.acc.spec.ts +210 -0
  63. package/tests/angular/ngtsc-build-core.spec.ts +52 -0
  64. package/tests/commands/version-upgrade.acc.spec.ts +210 -0
  65. package/tests/commands/version-upgrade.spec.ts +148 -0
  66. package/tests/deps/replace-deps/replace-deps-perf.verify.md +15 -0
  67. package/tests/deps/replace-deps/replace-deps-resolve.acc.spec.ts +124 -0
  68. package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
  69. package/tests/esbuild/esbuild-postcss-plugin-chunking.verify.md +17 -0
  70. package/tests/esbuild/esbuild-postcss-plugin.acc.spec.ts +152 -0
  71. package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
  72. package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
  73. package/tests/esbuild/esbuild-worker-plugin.spec.ts +297 -0
  74. package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
  75. package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
  76. package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
  77. package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
  78. package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
  79. package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
  80. package/tests/utils/ngtsc-build-core-write-emit.spec.ts +124 -0
  81. package/tests/workers/client-worker-mtime-incremental.verify.md +10 -0
  82. package/tests/workers/incremental-mtime-tracker.acc.spec.ts +144 -0
  83. package/tests/workers/incremental-mtime-tracker.spec.ts +102 -0
  84. package/tests/workers/library-build-worker.spec.ts +4 -0
  85. package/tests/workers/server-build-worker-plugin.verify.md +9 -0
  86. package/tests/workers/server-build-worker.spec.ts +26 -12
  87. package/tests/workers/server-esbuild-context.spec.ts +13 -5
  88. package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
  89. package/tests/workers/server-watch-manager.spec.ts +2 -2
  90. package/dist/angular/web-worker-transformer.d.ts +0 -9
  91. package/dist/angular/web-worker-transformer.d.ts.map +0 -1
  92. package/dist/angular/web-worker-transformer.js +0 -73
  93. package/dist/angular/web-worker-transformer.js.map +0 -1
  94. package/src/angular/web-worker-transformer.ts +0 -117
  95. package/tests/angular/web-worker-transformer.spec.ts +0 -154
  96. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.d.ts +0 -2
  97. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.d.ts.map +0 -1
  98. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.js +0 -4
  99. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.js.map +0 -1
  100. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.d.ts +0 -2
  101. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.d.ts.map +0 -1
  102. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.js +0 -4
  103. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.js.map +0 -1
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import type esbuild from "esbuild";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import os from "os";
6
+ import type { AcceptedPlugin } from "postcss";
7
+ import { createPostcssPlugin } from "../../src/esbuild/esbuild-postcss-plugin";
8
+
9
+ /** esbuild 플러그인 lifecycle을 시뮬레이션하는 헬퍼 */
10
+ function setupPlugin(plugin: esbuild.Plugin) {
11
+ let onEndCb:
12
+ | ((
13
+ result: esbuild.BuildResult,
14
+ ) => esbuild.OnEndResult | null | void | Promise<esbuild.OnEndResult | null | void>)
15
+ | undefined;
16
+
17
+ const mockBuild = {
18
+ onEnd(cb: typeof onEndCb) {
19
+ onEndCb = cb;
20
+ },
21
+ } as unknown as esbuild.PluginBuild;
22
+
23
+ void plugin.setup(mockBuild);
24
+
25
+ return {
26
+ async invokeOnEnd(result: Partial<esbuild.BuildResult>) {
27
+ return (
28
+ (await onEndCb?.({
29
+ errors: [],
30
+ warnings: [],
31
+ mangleCache: {},
32
+ outputFiles: [],
33
+ metafile: { inputs: {}, outputs: {} },
34
+ ...result,
35
+ } as esbuild.BuildResult)) ?? null
36
+ );
37
+ },
38
+ };
39
+ }
40
+
41
+ /** 테스트용 PostCSS 플러그인: 모든 CSS 앞에 주석을 추가 */
42
+ function createTestPostcssPlugin(): AcceptedPlugin {
43
+ return {
44
+ postcssPlugin: "test-prefix",
45
+ Once(root) {
46
+ root.prepend({ text: "processed" });
47
+ },
48
+ };
49
+ }
50
+
51
+ /** ɵɵdefineComponent styles 배열을 포함하는 JS 코드를 생성 (실제 Angular 컴파일 출력 패턴) */
52
+ function generateJsWithStyles(styles: string[]): string {
53
+ const stylesLiteral = styles.map((s) => JSON.stringify(s)).join(", ");
54
+ return [
55
+ `import * as i0 from "@angular/core";`,
56
+ `class MyComponent {}`,
57
+ `MyComponent.\u0275cmp = /*@__PURE__*/ i0.\u0275\u0275defineComponent({`,
58
+ ` type: MyComponent,`,
59
+ ` styles: [${stylesLiteral}],`,
60
+ ` template: function() {}`,
61
+ `});`,
62
+ `export { MyComponent };`,
63
+ ].join("\n");
64
+ }
65
+
66
+ describe("createPostcssPlugin — Acceptance Tests", () => {
67
+ let tmpDir: string;
68
+
69
+ beforeEach(() => {
70
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "postcss-test-"));
71
+ });
72
+
73
+ afterEach(() => {
74
+ fs.rmSync(tmpDir, { recursive: true, force: true });
75
+ });
76
+
77
+ it("replacements가 0건이면 파일 내용이 변경되지 않음", async () => {
78
+ // Given: ɵɵdefineComponent styles 배열이 없는 JS 파일
79
+ const jsContent = `const x = 1;\nexport default x;\n`;
80
+ const jsFile = path.join(tmpDir, "no-styles.js");
81
+ fs.writeFileSync(jsFile, jsContent, "utf-8");
82
+
83
+ const plugin = createPostcssPlugin({ plugins: [createTestPostcssPlugin()] });
84
+ const lifecycle = setupPlugin(plugin);
85
+
86
+ // When: PostCSS 처리를 시도
87
+ await lifecycle.invokeOnEnd({
88
+ metafile: { inputs: {}, outputs: { [jsFile]: {} as any } },
89
+ });
90
+
91
+ // Then: 파일 내용이 변경되지 않음
92
+ const result = fs.readFileSync(jsFile, "utf-8");
93
+ expect(result).toBe(jsContent);
94
+ });
95
+
96
+ it("replacements가 1건이면 PostCSS 처리된 CSS로 교체됨", async () => {
97
+ // Given: ɵɵdefineComponent styles 배열에 문자열 리터럴이 1개
98
+ const originalCss = ".host { color: red; }";
99
+ const jsContent = generateJsWithStyles([originalCss]);
100
+ const jsFile = path.join(tmpDir, "one-style.js");
101
+ fs.writeFileSync(jsFile, jsContent, "utf-8");
102
+
103
+ const plugin = createPostcssPlugin({ plugins: [createTestPostcssPlugin()] });
104
+ const lifecycle = setupPlugin(plugin);
105
+
106
+ // When: PostCSS 처리 수행
107
+ await lifecycle.invokeOnEnd({
108
+ metafile: { inputs: {}, outputs: { [jsFile]: {} as any } },
109
+ });
110
+
111
+ // Then: PostCSS 처리된 CSS가 파일에 반영됨
112
+ const result = fs.readFileSync(jsFile, "utf-8");
113
+ expect(result).toContain("/* processed */");
114
+ expect(result).toContain(".host { color: red; }");
115
+ // 파일 앞뒤의 JS 구조가 유지됨
116
+ expect(result).toContain("import");
117
+ expect(result).toContain("export");
118
+ });
119
+
120
+ it("replacements가 3건이면 모든 CSS가 PostCSS 처리됨", async () => {
121
+ // Given: ɵɵdefineComponent styles 배열에 문자열 리터럴이 3개
122
+ const styles = [
123
+ ".a { color: red; }",
124
+ ".b { color: blue; }",
125
+ ".c { color: green; }",
126
+ ];
127
+ const jsContent = generateJsWithStyles(styles);
128
+ const jsFile = path.join(tmpDir, "three-styles.js");
129
+ fs.writeFileSync(jsFile, jsContent, "utf-8");
130
+
131
+ const plugin = createPostcssPlugin({ plugins: [createTestPostcssPlugin()] });
132
+ const lifecycle = setupPlugin(plugin);
133
+
134
+ // When: PostCSS 처리 수행
135
+ await lifecycle.invokeOnEnd({
136
+ metafile: { inputs: {}, outputs: { [jsFile]: {} as any } },
137
+ });
138
+
139
+ // Then: 3개 모두 PostCSS 처리됨
140
+ const result = fs.readFileSync(jsFile, "utf-8");
141
+ // 3번의 "/* processed */" 주석이 삽입됨
142
+ const processedCount = (result.match(/\/\* processed \*\//g) ?? []).length;
143
+ expect(processedCount).toBe(3);
144
+ // 원본 CSS 내용이 모두 유지됨
145
+ expect(result).toContain("color: red");
146
+ expect(result).toContain("color: blue");
147
+ expect(result).toContain("color: green");
148
+ // JS 구조가 유지됨
149
+ expect(result).toContain("import");
150
+ expect(result).toContain("export");
151
+ });
152
+ });
@@ -0,0 +1,11 @@
1
+ # import.meta.resolve Worker 패턴 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] `NODE_WORKER_PATTERN` 정규식이 상대 경로만 감지: `esbuild-worker-plugin.ts:25` — `(\.\.?\/[^"']+)` 패턴으로 `./` 또는 `../`로 시작하는 경로만 캡처. `"some-package"` 같은 절대 모듈 경로는 매칭되지 않음.
6
+ - [x] `bundleWorker`에 platform 파라미터 추가: `esbuild-worker-plugin.ts:42` — `platform: esbuild.Platform` 파라미터 사용. 기존 `"browser"` 하드코딩 제거.
7
+ - [x] 브라우저 Worker 패턴이 `platform: "browser"`로 빌드: `esbuild-worker-plugin.ts:137` — `processWorkerBundle(fullWorkerPath, "browser")` 호출. 기존 동작 유지.
8
+ - [x] Node.js Worker 패턴이 메인 빌드의 platform 계승: `esbuild-worker-plugin.ts:149` — `build.initialOptions.platform ?? "browser"` 사용. 서버 빌드(platform: "node")에서는 "node"로 빌드.
9
+ - [x] 경로 치환이 `new URL("path", import.meta.url).href` 형태: `esbuild-worker-plugin.ts:153` — `new URL("${workerCodePath}", import.meta.url).href` 반환. file:// URL을 반환하므로 core-node Worker 호환.
10
+ - [x] 기존 브라우저 Worker 테스트 22개 모두 통과: `pnpm test --run` 결과 32개 전체 통과 (기존 22 + 신규 10).
11
+ - [x] `external: undefined` 설정 유지: `esbuild-worker-plugin.ts:62` — 메인 빌드의 external이 Worker 번들에 상속되지 않음.
@@ -0,0 +1,318 @@
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 { createWorkerBundlePlugin } = await import(
12
+ "../../src/esbuild/esbuild-worker-plugin.js"
13
+ );
14
+
15
+ /**
16
+ * 임시 디렉토리에 entry 파일을 생성하고 esbuild로 빌드한다.
17
+ * Worker fixture 파일은 fixturesDir에서 참조된다.
18
+ */
19
+ async function buildWithPlugin(
20
+ entryContent: string,
21
+ options?: { write?: boolean; entryExt?: string },
22
+ ): Promise<esbuild.BuildResult & { outdir: string }> {
23
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-plugin-test-"));
24
+ const ext = options?.entryExt ?? ".js";
25
+ const entryFile = path.join(tmpDir, `entry${ext}`);
26
+
27
+ // Worker fixture 파일을 tmpDir에 복사 (entry에서 상대경로 참조 가능하도록)
28
+ for (const f of ["worker.js", "worker2.js", "shared-worker.js", "worker-error.js", "node-worker.js"]) {
29
+ const src = path.join(fixturesDir, f);
30
+ if (fs.existsSync(src)) {
31
+ fs.copyFileSync(src, path.join(tmpDir, f));
32
+ }
33
+ }
34
+
35
+ fs.writeFileSync(entryFile, entryContent);
36
+
37
+ const outdir = path.join(tmpDir, "dist");
38
+ const write = options?.write ?? false;
39
+
40
+ const result = await esbuild.build({
41
+ entryPoints: [entryFile],
42
+ bundle: true,
43
+ write,
44
+ outdir,
45
+ format: "esm",
46
+ platform: "browser",
47
+ metafile: true,
48
+ logLevel: "silent",
49
+ plugins: [createWorkerBundlePlugin()],
50
+ });
51
+
52
+ return { ...result, outdir };
53
+ }
54
+
55
+ /**
56
+ * Node.js platform으로 esbuild 빌드한다 (서버 빌드 시뮬레이션).
57
+ */
58
+ async function buildNodeWithPlugin(
59
+ entryContent: string,
60
+ options?: { write?: boolean },
61
+ ): Promise<esbuild.BuildResult & { outdir: string }> {
62
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-plugin-node-test-"));
63
+ const entryFile = path.join(tmpDir, "entry.js");
64
+
65
+ for (const f of ["worker.js", "worker2.js", "shared-worker.js", "worker-error.js", "node-worker.js"]) {
66
+ const src = path.join(fixturesDir, f);
67
+ if (fs.existsSync(src)) {
68
+ fs.copyFileSync(src, path.join(tmpDir, f));
69
+ }
70
+ }
71
+
72
+ fs.writeFileSync(entryFile, entryContent);
73
+
74
+ const outdir = path.join(tmpDir, "dist");
75
+ const write = options?.write ?? false;
76
+
77
+ const result = await esbuild.build({
78
+ entryPoints: [entryFile],
79
+ bundle: true,
80
+ write,
81
+ outdir,
82
+ format: "esm",
83
+ platform: "node",
84
+ metafile: true,
85
+ logLevel: "silent",
86
+ plugins: [createWorkerBundlePlugin()],
87
+ });
88
+
89
+ return { ...result, outdir };
90
+ }
91
+
92
+ describe("esbuild Worker Bundle Plugin — Acceptance", () => {
93
+ it("JS 파일의 Worker 패턴을 감지하여 번들링하고 URL을 치환한다", async () => {
94
+ const result = await buildWithPlugin(
95
+ 'const w = new Worker(new URL("./worker.js", import.meta.url));',
96
+ );
97
+
98
+ expect(result.errors).toHaveLength(0);
99
+
100
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
101
+ expect(mainOutput).toBeDefined();
102
+ const content = mainOutput!.text;
103
+
104
+ // 원본 경로가 치환됨
105
+ expect(content).not.toContain("./worker.js");
106
+ // worker-[HASH].js 패턴으로 치환
107
+ expect(content).toMatch(/worker-[A-Z0-9]+\.js/i);
108
+ // { type: "module" } 추가됨
109
+ expect(content).toContain("module");
110
+
111
+ // Worker 번들 파일이 outputFiles에 포함됨
112
+ const workerOutput = result.outputFiles!.find((f) =>
113
+ /worker-[a-z0-9]+\.js$/i.test(path.basename(f.path)),
114
+ );
115
+ expect(workerOutput).toBeDefined();
116
+ });
117
+
118
+ it("SharedWorker 패턴을 감지하여 번들링하고 URL을 치환한다", async () => {
119
+ const result = await buildWithPlugin(
120
+ 'const sw = new SharedWorker(new URL("./shared-worker.js", import.meta.url));',
121
+ );
122
+
123
+ expect(result.errors).toHaveLength(0);
124
+
125
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
126
+ const content = mainOutput!.text;
127
+
128
+ expect(content).not.toContain("./shared-worker.js");
129
+ expect(content).toMatch(/worker-[A-Z0-9]+\.js/i);
130
+ });
131
+
132
+ it("TS 파일의 Worker 패턴을 감지하여 번들링한다", async () => {
133
+ const result = await buildWithPlugin(
134
+ 'const w: Worker = new Worker(new URL("./worker.js", import.meta.url));',
135
+ { entryExt: ".ts" },
136
+ );
137
+
138
+ expect(result.errors).toHaveLength(0);
139
+
140
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
141
+ const content = mainOutput!.text;
142
+
143
+ expect(content).not.toContain("./worker.js");
144
+ expect(content).toMatch(/worker-[A-Z0-9]+\.js/i);
145
+ });
146
+
147
+ it("Worker 패턴 없는 파일은 변환 없이 통과한다", async () => {
148
+ const result = await buildWithPlugin(
149
+ 'console.log("no worker");',
150
+ );
151
+
152
+ expect(result.errors).toHaveLength(0);
153
+
154
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
155
+ const content = mainOutput!.text;
156
+
157
+ expect(content).toContain("no worker");
158
+ expect(content).not.toMatch(/worker-[A-Z0-9]+\.js/i);
159
+ });
160
+
161
+ it("한 파일에 복수 Worker 패턴이 있으면 모두 번들링한다", async () => {
162
+ const result = await buildWithPlugin(
163
+ [
164
+ 'const w1 = new Worker(new URL("./worker.js", import.meta.url));',
165
+ 'const w2 = new Worker(new URL("./worker2.js", import.meta.url));',
166
+ ].join("\n"),
167
+ );
168
+
169
+ expect(result.errors).toHaveLength(0);
170
+
171
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
172
+ const content = mainOutput!.text;
173
+
174
+ expect(content).not.toContain("./worker.js");
175
+ expect(content).not.toContain("./worker2.js");
176
+
177
+ // 2개의 서로 다른 worker 번들이 있어야 함
178
+ const workerOutputs = result.outputFiles!.filter((f) =>
179
+ /worker-[a-z0-9]+\.js$/i.test(path.basename(f.path)),
180
+ );
181
+ expect(workerOutputs.length).toBeGreaterThanOrEqual(2);
182
+ });
183
+
184
+ it("기존 옵션이 있으면 유지하고 URL 경로만 치환한다", async () => {
185
+ const result = await buildWithPlugin(
186
+ 'const w = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });',
187
+ );
188
+
189
+ expect(result.errors).toHaveLength(0);
190
+
191
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
192
+ const content = mainOutput!.text;
193
+
194
+ expect(content).not.toContain("./worker.js");
195
+ expect(content).toMatch(/worker-[A-Z0-9]+\.js/i);
196
+ // 기존 { type: "module" }이 유지됨
197
+ expect(content).toContain("module");
198
+ });
199
+
200
+ it("Worker 빌드 에러가 메인 빌드로 전파된다", async () => {
201
+ try {
202
+ await buildWithPlugin(
203
+ 'const w = new Worker(new URL("./worker-error.js", import.meta.url));',
204
+ );
205
+ expect.fail("빌드 에러가 발생해야 한다");
206
+ } catch (e: unknown) {
207
+ // esbuild.build()는 에러 시 errors 프로퍼티를 가진 예외를 throw
208
+ const buildError = e as { errors?: esbuild.Message[] };
209
+ expect(buildError.errors).toBeDefined();
210
+ expect(buildError.errors!.length).toBeGreaterThan(0);
211
+ }
212
+ });
213
+
214
+ it("write: true 빌드에서 Worker 파일이 디스크에 기록된다", async () => {
215
+ const result = await buildWithPlugin(
216
+ 'const w = new Worker(new URL("./worker.js", import.meta.url));',
217
+ { write: true },
218
+ );
219
+
220
+ expect(result.errors).toHaveLength(0);
221
+
222
+ // outdir에 worker-[HASH].js 파일이 존재
223
+ const files = fs.readdirSync(result.outdir);
224
+ const workerFile = files.find((f) => /worker-[a-z0-9]+\.js$/i.test(f));
225
+ expect(workerFile).toBeDefined();
226
+ });
227
+
228
+ it("write: false 빌드에서 Worker 파일이 outputFiles에 포함된다", async () => {
229
+ const result = await buildWithPlugin(
230
+ 'const w = new Worker(new URL("./worker.js", import.meta.url));',
231
+ { write: false },
232
+ );
233
+
234
+ expect(result.errors).toHaveLength(0);
235
+
236
+ const workerOutput = result.outputFiles!.find((f) =>
237
+ /worker-[a-z0-9]+\.js$/i.test(path.basename(f.path)),
238
+ );
239
+ expect(workerOutput).toBeDefined();
240
+ });
241
+ });
242
+
243
+ describe("esbuild Worker Bundle Plugin — Node.js import.meta.resolve Acceptance", () => {
244
+ it("import.meta.resolve 패턴을 감지하여 번들링하고 경로를 치환한다", async () => {
245
+ const result = await buildNodeWithPlugin(
246
+ 'const p = import.meta.resolve("./node-worker.js");',
247
+ );
248
+
249
+ expect(result.errors).toHaveLength(0);
250
+
251
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
252
+ expect(mainOutput).toBeDefined();
253
+ const content = mainOutput!.text;
254
+
255
+ // import.meta.resolve가 치환됨
256
+ expect(content).not.toContain("import.meta.resolve");
257
+ // new URL("worker-HASH.js", import.meta.url).href 패턴으로 치환
258
+ expect(content).toMatch(/new URL\("worker-[A-Z0-9]+\.js",\s*import\.meta\.url\)\.href/i);
259
+
260
+ // Worker 번들 파일이 outputFiles에 포함됨
261
+ const workerOutput = result.outputFiles!.find((f) =>
262
+ /worker-[a-z0-9]+\.js$/i.test(path.basename(f.path)),
263
+ );
264
+ expect(workerOutput).toBeDefined();
265
+ });
266
+
267
+ it("절대 모듈 경로의 import.meta.resolve는 무시한다", async () => {
268
+ const result = await buildNodeWithPlugin(
269
+ 'const p = import.meta.resolve("path");',
270
+ );
271
+
272
+ expect(result.errors).toHaveLength(0);
273
+
274
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
275
+ const content = mainOutput!.text;
276
+
277
+ // import.meta.resolve가 그대로 남음 (치환되지 않음)
278
+ expect(content).toContain("import.meta.resolve");
279
+ });
280
+
281
+ it("브라우저 + Node.js Worker 패턴 공존 시 모두 번들링한다", async () => {
282
+ const result = await buildNodeWithPlugin(
283
+ [
284
+ 'const w = new Worker(new URL("./worker.js", import.meta.url));',
285
+ 'const p = import.meta.resolve("./node-worker.js");',
286
+ ].join("\n"),
287
+ );
288
+
289
+ expect(result.errors).toHaveLength(0);
290
+
291
+ const mainOutput = result.outputFiles!.find((f) => path.basename(f.path).startsWith("entry"));
292
+ const content = mainOutput!.text;
293
+
294
+ // 브라우저 Worker 치환
295
+ expect(content).not.toContain('"./worker.js"');
296
+ // Node.js resolve 치환
297
+ expect(content).not.toContain("import.meta.resolve");
298
+
299
+ // 최소 2개의 worker 번들
300
+ const workerOutputs = result.outputFiles!.filter((f) =>
301
+ /worker-[a-z0-9]+\.js$/i.test(path.basename(f.path)),
302
+ );
303
+ expect(workerOutputs.length).toBeGreaterThanOrEqual(2);
304
+ });
305
+
306
+ it("write: true 빌드에서 Worker 파일이 디스크에 기록된다", async () => {
307
+ const result = await buildNodeWithPlugin(
308
+ 'const p = import.meta.resolve("./node-worker.js");',
309
+ { write: true },
310
+ );
311
+
312
+ expect(result.errors).toHaveLength(0);
313
+
314
+ const files = fs.readdirSync(result.outdir);
315
+ const workerFile = files.find((f) => /worker-[a-z0-9]+\.js$/i.test(f));
316
+ expect(workerFile).toBeDefined();
317
+ });
318
+ });