@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
@@ -1,6 +1,7 @@
1
1
  import type { ServerBuildInfo } from "../../workers/server-build.worker";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
+ import YAML from "yaml";
4
5
  import { cpx } from "@simplysm/core-node";
5
6
  import { consola } from "consola";
6
7
  import { collectAllDependencyExternals } from "../../esbuild/esbuild-config";
@@ -8,21 +9,29 @@ import { collectAllDependencyExternals } from "../../esbuild/esbuild-config";
8
9
  const logger = consola.withTag("sd:cli:server-production-files");
9
10
 
10
11
  /**
11
- * 가지 소스에서 외부 모듈을 수집하고 병합한다.
12
- * collectAllDependencyExternals를 통한 단일 패스 의존성 트리 순회를 사용한다.
12
+ * 외부 모듈을 용도로 분리하여 수집한다.
13
+ * - bundleExternals: esbuild external 번들에서 제외할 패키지
14
+ * - prodDependencies: dist/package.json dependencies — 런타임에 실제 설치되어야 하는 패키지
15
+ *
16
+ * 미설치 optional peer는 번들 제외만 필요할 뿐 런타임에 쓰이지 않으므로 prodDependencies에 포함하지 않는다.
13
17
  */
14
- export function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
18
+ export function collectAllExternals(
19
+ pkgDir: string,
20
+ manualExternals?: string[],
21
+ ): { bundleExternals: string[]; prodDependencies: string[] } {
15
22
  logger.debug("의존성 트리 스캔 중...");
16
23
  const { optionalPeerDeps, nativeModules } = collectAllDependencyExternals(pkgDir);
17
24
 
18
25
  const manual = manualExternals ?? [];
19
- return [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
26
+ return {
27
+ bundleExternals: [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])],
28
+ prodDependencies: [...new Set([...nativeModules, ...manual])],
29
+ };
20
30
  }
21
31
 
22
32
  /**
23
33
  * pnpm-lock.yaml의 packages 섹션을 파싱하여 name→version 맵을 생성한다.
24
- * Lockfile v9 형식: `packages:` 섹션의 `'name@version':` 키를 파싱한다.
25
- * YAML 파서 의존성을 피하기 위해 단순 라인 기반 파싱을 사용한다.
34
+ * 형태: "name@version" · "@scope/name@version" · "name@version(peer@ver)..."
26
35
  */
27
36
  export function parseLockfileVersions(cwd: string): Map<string, string> {
28
37
  const lockfilePath = path.join(cwd, "pnpm-lock.yaml");
@@ -31,30 +40,19 @@ export function parseLockfileVersions(cwd: string): Map<string, string> {
31
40
  }
32
41
 
33
42
  const content = fs.readFileSync(lockfilePath, "utf-8");
43
+ const parsed = YAML.parse(content) as { packages?: Record<string, unknown> };
34
44
  const map = new Map<string, string>();
35
45
 
36
- // "packages:" 섹션을 찾고 "'@scope/name@1.2.3':" 또는 "'name@1.2.3':" 형태의 항목을 파싱
37
- const lines = content.split("\n");
38
- let inPackages = false;
39
- for (const line of lines) {
40
- if (line === "packages:") {
41
- inPackages = true;
42
- continue;
43
- }
44
- if (inPackages && line.length > 0 && !line.startsWith(" ") && !line.startsWith("'")) {
45
- break; // 다음 최상위 섹션
46
- }
47
- if (!inPackages) continue;
48
-
49
- // "'@scope/name@version':" 또는 "'name@version':" 매칭
50
- const match = /^\s{2}'(.+)@(\d[^']*)':\s*$/.exec(line);
51
- if (match != null) {
52
- const name = match[1];
53
- const version = match[2];
54
- // 첫 번째 항목 유지 (lockfile은 각 버전을 한 번만 기록)
55
- if (!map.has(name)) {
56
- map.set(name, version);
57
- }
46
+ for (const key of Object.keys(parsed.packages ?? {})) {
47
+ // 번째 @숫자 기준으로 name / version 분리 (scope 패키지의 선두 @ 보존)
48
+ const m = /^(.+?)@(\d.+)$/.exec(key);
49
+ if (m == null) continue;
50
+ const name = m[1];
51
+ // peerDep suffix "(peer@ver)..." 제거
52
+ const parenIdx = m[2].indexOf("(");
53
+ const version = parenIdx === -1 ? m[2] : m[2].substring(0, parenIdx);
54
+ if (!map.has(name)) {
55
+ map.set(name, version);
58
56
  }
59
57
  }
60
58
 
@@ -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
  });
@@ -88,16 +88,19 @@ export function createPostcssPlugin(options: CreatePostcssPluginOptions): esbuil
88
88
 
89
89
  if (replacements.length === 0) continue;
90
90
 
91
- // PostCSS 적용 역순 교체
92
- let modified = code;
93
- // 끝에서 시작 방향으로 교체하여 offset 유지
94
- const sorted = replacements.sort((a, b) => b.start - a.start);
91
+ // PostCSS 적용 정방향 청크 배열 + join
92
+ const sorted = replacements.sort((a, b) => a.start - b.start);
93
+ const chunks: string[] = [];
94
+ let cursor = 0;
95
95
 
96
96
  for (const rep of sorted) {
97
+ chunks.push(code.slice(cursor, rep.start));
97
98
  const processed = await processor.process(rep.text, { from: file });
98
- const escaped = JSON.stringify(processed.css);
99
- modified = modified.slice(0, rep.start) + escaped + modified.slice(rep.end);
99
+ chunks.push(JSON.stringify(processed.css));
100
+ cursor = rep.end;
100
101
  }
102
+ chunks.push(code.slice(cursor));
103
+ const modified = chunks.join("");
101
104
 
102
105
  await fs.promises.writeFile(file, modified);
103
106
  } catch (e: unknown) {
@@ -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
+ }