@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
@@ -5,15 +5,15 @@ import ts from "typescript";
5
5
  import type esbuild from "esbuild";
6
6
  import { consola } from "consola";
7
7
  import { JavaScriptTransformer, Cache as AngularCache } from "@angular/build/private";
8
- import type { AngularSourceFileCache } from "../angular/angular-compiler.js";
9
- import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization.js";
10
- import { SdTsCompiler } from "../ts-compiler/SdTsCompiler.js";
11
- import type { ISdTsCompilerResult } from "../ts-compiler/sd-ts-compiler-result.js";
12
- import { FileReferenceTracker } from "./file-reference-tracker.js";
13
- import { LmdbCacheStore } from "./lmdb-cache-store.js";
14
- import { createCachedLoad, type LoadResultCache } from "./load-result-cache.js";
15
- import { collectHmrCandidates, HMR_MODIFIED_FILE_LIMIT } from "../angular/hmr-candidates.js";
16
- import { createWorkerTransformer } from "../angular/web-worker-transformer.js";
8
+ import type { AngularSourceFileCache } from "../angular/angular-compiler";
9
+ import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
10
+ import { SdTsCompiler } from "../ts-compiler/SdTsCompiler";
11
+ import type { ISdTsCompilerResult } from "../ts-compiler/sd-ts-compiler-result";
12
+ import { FileReferenceTracker } from "./file-reference-tracker";
13
+ import { LmdbCacheStore } from "./lmdb-cache-store";
14
+ import { createCachedLoad, type LoadResultCache } from "./load-result-cache";
15
+ import { collectHmrCandidates, HMR_MODIFIED_FILE_LIMIT } from "../angular/hmr-candidates";
16
+ import { transformWorkerPatterns } from "./esbuild-worker-plugin";
17
17
 
18
18
  const logger = consola.withTag("sd:cli:angular-plugin");
19
19
 
@@ -48,12 +48,6 @@ export interface AngularCompilerPluginOptions {
48
48
  stylesheetErrors?: string[];
49
49
  }
50
50
 
51
- interface AdditionalResult {
52
- outputFiles?: esbuild.OutputFile[];
53
- metafile?: esbuild.Metafile;
54
- errors?: esbuild.PartialMessage[];
55
- }
56
-
57
51
  //#endregion
58
52
 
59
53
  //#region compilerOptions 변환
@@ -158,46 +152,6 @@ export function convertSerializedDiagnosticToEsbuild(
158
152
 
159
153
  //#endregion
160
154
 
161
- //#region bundleWebWorker
162
-
163
- /**
164
- * Worker 파일을 esbuild.buildSync()로 별도 ESM 번들로 빌드한다.
165
- * TypeScript transformer 내에서 동기적으로 호출되므로 sync API를 사용한다.
166
- */
167
- export function bundleWebWorker(
168
- build: esbuild.PluginBuild,
169
- sourcemap: boolean,
170
- workerFile: string,
171
- ): esbuild.BuildResult {
172
- try {
173
- return build.esbuild.buildSync({
174
- ...build.initialOptions,
175
- platform: "browser",
176
- write: false,
177
- bundle: true,
178
- metafile: true,
179
- format: "esm",
180
- entryNames: "worker-[hash]",
181
- entryPoints: [workerFile],
182
- sourcemap,
183
- supported: undefined,
184
- plugins: undefined,
185
- });
186
- } catch (error) {
187
- if (
188
- error != null &&
189
- typeof error === "object" &&
190
- "errors" in error &&
191
- "warnings" in error
192
- ) {
193
- return error as esbuild.BuildResult;
194
- }
195
- throw error;
196
- }
197
- }
198
-
199
- //#endregion
200
-
201
155
  //#region onLoad 헬퍼
202
156
 
203
157
  const POTENTIAL_METADATA_REGEX =
@@ -288,7 +242,12 @@ export function createAngularCompilerPlugin(
288
242
  // ── 내부 상태 ──
289
243
  const typeScriptFileCache: Map<string, string | Uint8Array> =
290
244
  pluginOptions.typeScriptFileCache ?? new Map();
291
- const additionalResults = new Map<string, AdditionalResult>();
245
+ // containingFile별 Worker 번들 결과. 증분 빌드에서 변경된 파일만 선택적으로
246
+ // 제거하여, 변경되지 않은 Worker metafile/outputFiles가 유지되도록 함.
247
+ const workerResultsByContainingFile = new Map<
248
+ string,
249
+ { outputFiles?: esbuild.OutputFile[]; metafile?: esbuild.Metafile }
250
+ >();
292
251
  const referencedFileTracker = new FileReferenceTracker();
293
252
  let sdTsCompiler: SdTsCompiler | undefined;
294
253
  let lastResult: ISdTsCompilerResult | undefined;
@@ -308,58 +267,6 @@ export function createAngularCompilerPlugin(
308
267
  return sideEffects;
309
268
  }
310
269
 
311
- // ── 서브함수: WebWorker 프로세서 생성 ──
312
- function createWebWorkerProcessor(
313
- errors: esbuild.PartialMessage[],
314
- warnings: esbuild.PartialMessage[],
315
- ): (workerFile: string, containingFile: string) => string {
316
- return (workerFile: string, containingFile: string): string => {
317
- const fullWorkerPath = path.join(path.dirname(containingFile), workerFile);
318
- const workerResult = bundleWebWorker(build, pluginOptions.sourcemap, fullWorkerPath);
319
-
320
- warnings.push(...workerResult.warnings);
321
-
322
- if (workerResult.errors.length > 0) {
323
- errors.push(...workerResult.errors);
324
- // 에러 파일 경로 추적 (rebuild 허용)
325
- referencedFileTracker.add(
326
- containingFile,
327
- workerResult.errors
328
- .map((e) => e.location?.file)
329
- .filter((f): f is string => f != null)
330
- .map((f) => path.join(cwd, f)),
331
- );
332
- additionalResults.set(fullWorkerPath, { errors: workerResult.errors });
333
- return workerFile;
334
- }
335
-
336
- additionalResults.set(fullWorkerPath, {
337
- outputFiles: workerResult.outputFiles,
338
- metafile: workerResult.metafile,
339
- });
340
-
341
- // metafile.inputs → FileReferenceTracker
342
- if (workerResult.metafile != null) {
343
- referencedFileTracker.add(
344
- containingFile,
345
- Object.keys(workerResult.metafile.inputs).map((input) => path.join(cwd, input)),
346
- );
347
- }
348
-
349
- // worker-[HASH].js 파일 찾기
350
- const workerCodeFile = workerResult.outputFiles?.find((file) =>
351
- /^worker-[A-Z0-9]{8}\.[cm]?js$/.test(path.basename(file.path)),
352
- );
353
- if (workerCodeFile == null) {
354
- errors.push({ text: `Web Worker bundled code file not found: ${fullWorkerPath}`, location: null });
355
- return workerFile;
356
- }
357
- const outdir = build.initialOptions.outdir ?? "";
358
- const workerCodePath = path.relative(outdir, workerCodeFile.path);
359
- return workerCodePath.replaceAll("\\", "/");
360
- };
361
- }
362
-
363
270
  // ── onStart ──
364
271
  build.onStart(async () => {
365
272
  const result: esbuild.OnStartResult = {};
@@ -402,9 +309,10 @@ export function createAngularCompilerPlugin(
402
309
  sourceFileCache.modifiedFiles,
403
310
  );
404
311
 
405
- // stale additionalResults 제거
312
+ // 변경된 파일의 Worker 결과만 선택적 제거 (변경되지 않은 파일의
313
+ // Worker metafile은 유지되어 onEnd에서 재병합됨)
406
314
  for (const file of expandedModifiedFiles) {
407
- additionalResults.delete(file);
315
+ workerResultsByContainingFile.delete(file);
408
316
  }
409
317
  }
410
318
 
@@ -424,19 +332,39 @@ export function createAngularCompilerPlugin(
424
332
  });
425
333
  }
426
334
 
427
- // ── processWebWorker + workerTransformer ──
428
- const processWebWorker = createWebWorkerProcessor(errors, warnings);
429
- const workerTransformer = createWorkerTransformer(processWebWorker);
430
-
431
335
  // ── compileAsync ──
432
336
  const compileResult = await sdTsCompiler.compileAsync(
433
337
  isIncremental ? expandedModifiedFiles : undefined,
434
- { additionalTransformers: { before: [workerTransformer] } },
435
338
  );
436
339
 
437
- // ── emitResults → typeScriptFileCache ──
340
+ // ── emitResults → typeScriptFileCache (Worker 패턴 처리 포함) ──
438
341
  for (const { contents, sourceFileName } of compileResult.emitResults ?? []) {
439
- typeScriptFileCache.set(path.normalize(sourceFileName), contents);
342
+ const normalized = path.normalize(sourceFileName);
343
+ const workerResult = transformWorkerPatterns(contents, normalized, build);
344
+ if (workerResult != null) {
345
+ typeScriptFileCache.set(normalized, workerResult.contents);
346
+ errors.push(...workerResult.errors);
347
+ warnings.push(...workerResult.warnings);
348
+ if (workerResult.workerMetafile != null) {
349
+ referencedFileTracker.add(
350
+ normalized,
351
+ Object.keys(workerResult.workerMetafile.inputs).map((input) =>
352
+ path.join(cwd, input),
353
+ ),
354
+ );
355
+ }
356
+ if (
357
+ workerResult.workerMetafile != null ||
358
+ workerResult.workerOutputFiles != null
359
+ ) {
360
+ workerResultsByContainingFile.set(normalized, {
361
+ outputFiles: workerResult.workerOutputFiles,
362
+ metafile: workerResult.workerMetafile,
363
+ });
364
+ }
365
+ } else {
366
+ typeScriptFileCache.set(normalized, contents);
367
+ }
440
368
  }
441
369
 
442
370
  // ── onLoad 플래그 결정 (첫 빌드에서만) ──
@@ -612,6 +540,37 @@ export function createAngularCompilerPlugin(
612
540
  false, // skipLinker
613
541
  sideEffects,
614
542
  );
543
+
544
+ // Worker 패턴 처리 (D2)
545
+ const textContents = new TextDecoder().decode(contents);
546
+ const workerResult = transformWorkerPatterns(textContents, request, build);
547
+ if (workerResult != null) {
548
+ if (workerResult.workerMetafile != null) {
549
+ referencedFileTracker.add(
550
+ request,
551
+ Object.keys(workerResult.workerMetafile.inputs).map((input) =>
552
+ path.join(cwd, input),
553
+ ),
554
+ );
555
+ }
556
+ if (
557
+ workerResult.workerMetafile != null ||
558
+ workerResult.workerOutputFiles != null
559
+ ) {
560
+ workerResultsByContainingFile.set(request, {
561
+ outputFiles: workerResult.workerOutputFiles,
562
+ metafile: workerResult.workerMetafile,
563
+ });
564
+ }
565
+ return {
566
+ contents: workerResult.contents,
567
+ loader: "js" as const,
568
+ resolveDir: path.dirname(request),
569
+ errors: workerResult.errors.length > 0 ? workerResult.errors : undefined,
570
+ warnings: workerResult.warnings.length > 0 ? workerResult.warnings : undefined,
571
+ };
572
+ }
573
+
615
574
  return {
616
575
  contents,
617
576
  loader: "js" as const,
@@ -622,13 +581,13 @@ export function createAngularCompilerPlugin(
622
581
 
623
582
  // ── onEnd ──
624
583
  build.onEnd((result: esbuild.BuildResult) => {
625
- for (const { outputFiles, metafile } of additionalResults.values()) {
626
- if (outputFiles != null && outputFiles.length > 0) {
627
- result.outputFiles?.push(...outputFiles);
584
+ for (const wr of workerResultsByContainingFile.values()) {
585
+ if (wr.outputFiles != null && wr.outputFiles.length > 0) {
586
+ result.outputFiles?.push(...wr.outputFiles);
628
587
  }
629
- if (result.metafile != null && metafile != null) {
630
- Object.assign(result.metafile.inputs, metafile.inputs);
631
- Object.assign(result.metafile.outputs, metafile.outputs);
588
+ if (result.metafile != null && wr.metafile != null) {
589
+ Object.assign(result.metafile.inputs, wr.metafile.inputs);
590
+ Object.assign(result.metafile.outputs, wr.metafile.outputs);
632
591
  }
633
592
  }
634
593
  });
@@ -0,0 +1,266 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import type esbuild from "esbuild";
4
+
5
+ /**
6
+ * Worker/SharedWorker + new URL + import.meta.url 패턴을 감지하는 정규식.
7
+ *
8
+ * 캡처 그룹:
9
+ * - Group 1: `Worker` 또는 `SharedWorker`
10
+ * - Group 2: URL 경로 (예: `"./worker.ts"`)
11
+ * - Group 3: 옵션 객체 (예: `{ type: "module" }`) — 없으면 undefined
12
+ */
13
+ const WORKER_PATTERN =
14
+ /\bnew\s+(Worker|SharedWorker)\s*\(\s*new\s+URL\s*\(\s*["']([^"']+)["']\s*,\s*import\.meta\.url\s*\)\s*(?:,\s*(\{[^}]*\}))?\s*\)/g;
15
+
16
+ /**
17
+ * Node.js import.meta.resolve 패턴을 감지하는 정규식.
18
+ * 상대 경로(./ 또는 ../)만 감지 — 절대 모듈 경로("some-package")는 무시.
19
+ *
20
+ * 캡처 그룹:
21
+ * - Group 1: 상대 경로 (예: `"../workers/service-protocol.worker"`)
22
+ */
23
+ const NODE_WORKER_PATTERN =
24
+ /\bimport\.meta\.resolve\s*\(\s*["'](\.\.?\/[^"']+)["']\s*\)/g;
25
+
26
+ /**
27
+ * Worker 번들 빌드 결과를 포함하는 transform 결과.
28
+ */
29
+ export interface TransformWorkerResult {
30
+ contents: string;
31
+ errors: esbuild.PartialMessage[];
32
+ warnings: esbuild.PartialMessage[];
33
+ /** write: false일 때 Worker 번들의 outputFiles (onEnd에서 병합용) */
34
+ workerOutputFiles?: esbuild.OutputFile[];
35
+ /** Worker 번들의 metafile (onEnd에서 병합용) */
36
+ workerMetafile?: esbuild.Metafile;
37
+ }
38
+
39
+ /**
40
+ * Worker 파일을 esbuild.buildSync()로 별도 ESM 번들로 빌드한다.
41
+ * esbuild-angular-compiler-plugin.ts의 bundleWebWorker를 기반으로 작성.
42
+ */
43
+ function bundleWorker(
44
+ build: esbuild.PluginBuild,
45
+ workerFile: string,
46
+ platform: esbuild.Platform,
47
+ ): esbuild.BuildResult {
48
+ const sourcemap =
49
+ build.initialOptions.sourcemap != null &&
50
+ build.initialOptions.sourcemap !== false;
51
+ const write = build.initialOptions.write !== false;
52
+
53
+ try {
54
+ return build.esbuild.buildSync({
55
+ ...build.initialOptions,
56
+ platform,
57
+ write,
58
+ bundle: true,
59
+ metafile: true,
60
+ format: "esm",
61
+ entryNames: "worker-[hash]",
62
+ entryPoints: [workerFile],
63
+ sourcemap,
64
+ // 메인 빌드에서 상속하지 않는 옵션
65
+ external: undefined,
66
+ supported: undefined,
67
+ plugins: undefined,
68
+ outbase: undefined,
69
+ inject: undefined,
70
+ });
71
+ } catch (error) {
72
+ if (
73
+ error != null &&
74
+ typeof error === "object" &&
75
+ "errors" in error &&
76
+ "warnings" in error
77
+ ) {
78
+ return error as esbuild.BuildResult;
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 파일 내용에서 Worker/SharedWorker 패턴을 감지하여 Worker 파일을 번들링하고
86
+ * URL 경로를 번들된 파일 경로로 치환한다.
87
+ *
88
+ * Angular 플러그인 등 외부에서 직접 호출할 수 있도록 export한다 (D2).
89
+ *
90
+ * @returns 변환 결과. 패턴이 없으면 undefined.
91
+ */
92
+ export function transformWorkerPatterns(
93
+ content: string,
94
+ filePath: string,
95
+ build: esbuild.PluginBuild,
96
+ ): TransformWorkerResult | undefined {
97
+ // 빠른 사전 필터
98
+ const hasBrowserWorker = content.includes("Worker") && WORKER_PATTERN.test(content);
99
+ if (hasBrowserWorker) WORKER_PATTERN.lastIndex = 0;
100
+
101
+ const hasNodeWorker =
102
+ content.includes("import.meta.resolve") && NODE_WORKER_PATTERN.test(content);
103
+ if (hasNodeWorker) NODE_WORKER_PATTERN.lastIndex = 0;
104
+
105
+ if (!hasBrowserWorker && !hasNodeWorker) {
106
+ return undefined;
107
+ }
108
+
109
+ const errors: esbuild.PartialMessage[] = [];
110
+ const warnings: esbuild.PartialMessage[] = [];
111
+ const allOutputFiles: esbuild.OutputFile[] = [];
112
+ let mergedMetafile: esbuild.Metafile | undefined;
113
+
114
+ const write = build.initialOptions.write !== false;
115
+ const outdir = build.initialOptions.outdir ?? "";
116
+ const containingDir = path.dirname(filePath);
117
+
118
+ /**
119
+ * Worker 번들을 빌드하고, 결과에서 출력 파일 경로를 찾아 반환한다.
120
+ * 에러/경고/outputFiles/metafile을 외부 변수에 수집한다.
121
+ */
122
+ function processWorkerBundle(
123
+ fullWorkerPath: string,
124
+ platform: esbuild.Platform,
125
+ ): string | undefined {
126
+ const workerResult = bundleWorker(build, fullWorkerPath, platform);
127
+
128
+ warnings.push(...workerResult.warnings);
129
+
130
+ if (workerResult.errors.length > 0) {
131
+ errors.push(...workerResult.errors);
132
+ return undefined;
133
+ }
134
+
135
+ if (!write && workerResult.outputFiles != null) {
136
+ allOutputFiles.push(...workerResult.outputFiles);
137
+ }
138
+
139
+ if (workerResult.metafile != null) {
140
+ if (mergedMetafile == null) {
141
+ mergedMetafile = { inputs: {}, outputs: {} };
142
+ }
143
+ Object.assign(mergedMetafile.inputs, workerResult.metafile.inputs);
144
+ Object.assign(mergedMetafile.outputs, workerResult.metafile.outputs);
145
+ }
146
+
147
+ const workerCodeFile =
148
+ workerResult.outputFiles?.find((file) =>
149
+ /^worker-[a-z0-9]+\.[cm]?js$/i.test(path.basename(file.path)),
150
+ ) ??
151
+ (workerResult.metafile != null
152
+ ? (() => {
153
+ const outputKey = Object.keys(workerResult.metafile.outputs).find(
154
+ (key) => /worker-[a-z0-9]+\.[cm]?js$/i.test(path.basename(key)),
155
+ );
156
+ return outputKey != null ? { path: path.resolve(outputKey) } : undefined;
157
+ })()
158
+ : undefined);
159
+
160
+ if (workerCodeFile == null) {
161
+ errors.push({
162
+ text: `Worker 번들 출력 파일을 찾을 수 없습니다: ${fullWorkerPath}`,
163
+ location: null,
164
+ });
165
+ return undefined;
166
+ }
167
+
168
+ // outdir 루트 기준 상대 경로. bundleWorker에서 outbase: undefined를 설정하여
169
+ // Worker 파일이 항상 outdir 루트에 출력됨을 보장한다.
170
+ return path.relative(outdir, workerCodeFile.path).replaceAll("\\", "/");
171
+ }
172
+
173
+ let transformed = content;
174
+
175
+ // 1. 브라우저 Worker 패턴 처리 (new Worker(new URL("path", import.meta.url)))
176
+ if (hasBrowserWorker) {
177
+ transformed = transformed.replace(
178
+ WORKER_PATTERN,
179
+ (match, workerType: string, urlPath: string, existingOpts?: string) => {
180
+ const fullWorkerPath = path.resolve(containingDir, urlPath);
181
+ const workerCodePath = processWorkerBundle(fullWorkerPath, "browser");
182
+ if (workerCodePath == null) return match;
183
+
184
+ const optsStr = existingOpts != null ? existingOpts : '{ type: "module" }';
185
+ return `new ${workerType}(new URL("${workerCodePath}", import.meta.url), ${optsStr})`;
186
+ },
187
+ );
188
+ }
189
+
190
+ // 2. Node.js import.meta.resolve 패턴 처리
191
+ if (hasNodeWorker) {
192
+ transformed = transformed.replace(
193
+ NODE_WORKER_PATTERN,
194
+ (match, resolvePath: string) => {
195
+ const fullWorkerPath = path.resolve(containingDir, resolvePath);
196
+ const workerCodePath = processWorkerBundle(
197
+ fullWorkerPath,
198
+ build.initialOptions.platform ?? "browser",
199
+ );
200
+ if (workerCodePath == null) return match;
201
+
202
+ return `new URL("${workerCodePath}", import.meta.url).href`;
203
+ },
204
+ );
205
+ }
206
+
207
+ return {
208
+ contents: transformed,
209
+ errors,
210
+ warnings,
211
+ workerOutputFiles: allOutputFiles.length > 0 ? allOutputFiles : undefined,
212
+ workerMetafile: mergedMetafile,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * esbuild Worker 번들링 플러그인을 생성한다.
218
+ *
219
+ * onLoad에서 .js/.ts 파일의 Worker/SharedWorker 패턴을 감지하여
220
+ * Worker 파일을 별도 ESM 번들로 빌드하고, URL 경로를 번들된 파일로 치환한다.
221
+ */
222
+ export function createWorkerBundlePlugin(): esbuild.Plugin {
223
+ return {
224
+ name: "sd-worker-bundle",
225
+ setup(build) {
226
+ const pendingWorkerResults: Array<{
227
+ outputFiles?: esbuild.OutputFile[];
228
+ metafile?: esbuild.Metafile;
229
+ }> = [];
230
+
231
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
232
+ const content = fs.readFileSync(args.path, "utf-8");
233
+ const result = transformWorkerPatterns(content, args.path, build);
234
+ if (result == null) return undefined;
235
+
236
+ // write: false일 때 Worker outputFiles를 모아뒀다가 onEnd에서 병합
237
+ if (result.workerOutputFiles != null || result.workerMetafile != null) {
238
+ pendingWorkerResults.push({
239
+ outputFiles: result.workerOutputFiles,
240
+ metafile: result.workerMetafile,
241
+ });
242
+ }
243
+
244
+ return {
245
+ contents: result.contents,
246
+ loader: /\.[cm]?tsx?$/.test(args.path) ? ("ts" as const) : ("js" as const),
247
+ errors: result.errors.length > 0 ? result.errors : undefined,
248
+ warnings: result.warnings.length > 0 ? result.warnings : undefined,
249
+ };
250
+ });
251
+
252
+ build.onEnd((result) => {
253
+ for (const wr of pendingWorkerResults) {
254
+ if (wr.outputFiles != null && wr.outputFiles.length > 0) {
255
+ result.outputFiles?.push(...wr.outputFiles);
256
+ }
257
+ if (result.metafile != null && wr.metafile != null) {
258
+ Object.assign(result.metafile.inputs, wr.metafile.inputs);
259
+ Object.assign(result.metafile.outputs, wr.metafile.outputs);
260
+ }
261
+ }
262
+ pendingWorkerResults.length = 0;
263
+ });
264
+ },
265
+ };
266
+ }
@@ -18,6 +18,7 @@ import {
18
18
  import { collectAllExternals, generateProductionFiles } from "../deps/server-externals/server-production-files";
19
19
  import { SdTsCompiler } from "../ts-compiler/SdTsCompiler";
20
20
  import { createTscPlugin } from "../esbuild/esbuild-tsc-plugin";
21
+ import { createWorkerBundlePlugin } from "../esbuild/esbuild-worker-plugin";
21
22
  import { setupWorkerLifecycle } from "./shared-worker-lifecycle";
22
23
  import { buildWatchPaths } from "./build-watch-paths";
23
24
  import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
@@ -140,7 +141,7 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
140
141
  const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
141
142
 
142
143
  // 외부 모듈 수집
143
- const external = collectAllExternals(info.pkgDir, info.externals);
144
+ const { bundleExternals, prodDependencies } = collectAllExternals(info.pkgDir, info.externals);
144
145
 
145
146
  let jsResult: { success: boolean; errors?: string[]; warnings?: string[] };
146
147
  let tscErrors: string[];
@@ -162,10 +163,10 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
162
163
  pkgDir: info.pkgDir,
163
164
  entryPoints,
164
165
  env: info.env,
165
- external,
166
+ external: bundleExternals,
166
167
  });
167
168
 
168
- jsResult = await esbuild.build({ ...esbuildOptions, plugins: [tscPlugin.plugin] })
169
+ jsResult = await esbuild.build({ ...esbuildOptions, plugins: [createWorkerBundlePlugin(), tscPlugin.plugin] })
169
170
  .then(async (result) => {
170
171
  if (result.outputFiles) {
171
172
  await writeChangedOutputFiles(result.outputFiles);
@@ -212,7 +213,7 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
212
213
 
213
214
  await copyPublicFiles(info.pkgDir, false);
214
215
 
215
- generateProductionFiles(info, external);
216
+ generateProductionFiles(info, prodDependencies);
216
217
  }
217
218
 
218
219
  const allErrors = [...(jsResult.errors ?? []), ...tscErrors];
@@ -310,7 +311,7 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
310
311
  const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
311
312
 
312
313
  // 외부 모듈 수집 (watch 모드용 — watch manager가 자체 캐시를 유지)
313
- const cachedExternal = collectAllExternals(info.pkgDir, info.externals);
314
+ const { bundleExternals: cachedExternal } = collectAllExternals(info.pkgDir, info.externals);
314
315
 
315
316
  // esbuild 컨텍스트 생성 (JS 출력 필요 시, tsc 플러그인 포함)
316
317
  if (info.output.js) {
@@ -10,6 +10,7 @@ import { createTscPlugin, type TscPluginResult } from "../esbuild/esbuild-tsc-pl
10
10
  import type { TypecheckEnv } from "../utils/tsconfig";
11
11
  import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
12
12
  import type { LintWithProgramResult } from "../lint/lint-with-program";
13
+ import { createWorkerBundlePlugin } from "../esbuild/esbuild-worker-plugin";
13
14
 
14
15
  /**
15
16
  * esbuild watch context 생성 옵션
@@ -62,9 +63,10 @@ export async function createContext(options: EsbuildContextOptions): Promise<voi
62
63
  dev: true,
63
64
  });
64
65
 
66
+ const workerPlugin = createWorkerBundlePlugin();
65
67
  context = await esbuild.context({
66
68
  ...baseOptions,
67
- plugins: tscPlugin != null ? [tscPlugin.plugin] : [],
69
+ plugins: tscPlugin != null ? [workerPlugin, tscPlugin.plugin] : [workerPlugin],
68
70
  metafile: true,
69
71
  write: false,
70
72
  });
@@ -59,7 +59,7 @@ export async function startServerWatchLoop(config: ServerWatchLoopConfig): Promi
59
59
  c.path.endsWith("package.json"),
60
60
  );
61
61
  if (hasPackageJsonChange) {
62
- cachedExternal = collectAllExternals(info.pkgDir, info.externals);
62
+ cachedExternal = collectAllExternals(info.pkgDir, info.externals).bundleExternals;
63
63
  }
64
64
 
65
65
  if (info.output.js) {