@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.
- package/dist/deps/server-externals/server-production-files.d.ts +10 -5
- package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +22 -26
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts +3 -8
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.js +57 -83
- package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
- package/dist/esbuild/esbuild-worker-plugin.d.ts +30 -0
- package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-worker-plugin.js +197 -0
- package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +6 -5
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +3 -1
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/dist/workers/server-watch-manager.js +1 -1
- package/dist/workers/server-watch-manager.js.map +1 -1
- package/package.json +5 -4
- package/src/deps/server-externals/server-production-files.ts +26 -28
- package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
- package/src/esbuild/esbuild-worker-plugin.ts +266 -0
- package/src/workers/server-build.worker.ts +6 -5
- package/src/workers/server-esbuild-context.ts +3 -1
- package/src/workers/server-watch-manager.ts +1 -1
- package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
- package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
- package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
- package/tests/esbuild/esbuild-worker-plugin.spec.ts +297 -0
- package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
- package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
- package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
- package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
- package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
- package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
- package/tests/workers/server-build-worker-plugin.verify.md +9 -0
- package/tests/workers/server-build-worker.spec.ts +26 -12
- package/tests/workers/server-esbuild-context.spec.ts +13 -5
- package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
- package/tests/workers/server-watch-manager.spec.ts +2 -2
- package/dist/angular/web-worker-transformer.d.ts +0 -9
- package/dist/angular/web-worker-transformer.d.ts.map +0 -1
- package/dist/angular/web-worker-transformer.js +0 -73
- package/dist/angular/web-worker-transformer.js.map +0 -1
- package/src/angular/web-worker-transformer.ts +0 -117
- 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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
312
|
+
// 변경된 파일의 Worker 결과만 선택적 제거 (변경되지 않은 파일의
|
|
313
|
+
// Worker metafile은 유지되어 onEnd에서 재병합됨)
|
|
406
314
|
for (const file of expandedModifiedFiles) {
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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) {
|