@simplysm/sd-cli 12.16.46 → 12.16.52
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/pkg-builders/client/SdNgBundler.js +1 -3
- package/dist/pkg-builders/client/createSdNgPlugin.d.ts +1 -1
- package/dist/pkg-builders/client/createSdNgPlugin.js +16 -2
- package/dist/pkg-builders/commons/SdWorkerPathPlugin.d.ts +1 -0
- package/dist/pkg-builders/commons/SdWorkerPathPlugin.js +44 -52
- package/package.json +5 -5
- package/src/pkg-builders/client/SdNgBundler.ts +1 -3
- package/src/pkg-builders/client/createSdNgPlugin.ts +18 -1
- package/src/pkg-builders/commons/SdWorkerPathPlugin.ts +67 -69
- package/tests/client/create-sd-ng-plugin-js-handler-unit.spec.ts +86 -0
- package/tests/client/create-sd-ng-plugin-js-handler.spec.ts +151 -0
- package/tests/commons/sd-worker-path-angular-integration.spec.md +28 -0
- package/tests/commons/sd-worker-path-plugin.spec.ts +141 -0
|
@@ -21,7 +21,6 @@ import { resolveAssets } from "@angular/build/src/utils/resolve-assets";
|
|
|
21
21
|
import { createSdNgPlugin } from "./createSdNgPlugin";
|
|
22
22
|
import { SdCliPerformanceTimer } from "../../utils/SdCliPerformanceTimer";
|
|
23
23
|
import nodeModule from "module";
|
|
24
|
-
import { SdWorkerPathPlugin } from "../commons/SdWorkerPathPlugin";
|
|
25
24
|
import { SdPolyfillPlugin } from "./SdPolyfillPlugin";
|
|
26
25
|
export class SdNgBundler {
|
|
27
26
|
constructor(_opt, _conf) {
|
|
@@ -436,11 +435,10 @@ export class SdNgBundler {
|
|
|
436
435
|
plugins: [
|
|
437
436
|
createSourcemapIgnorelistPlugin(),
|
|
438
437
|
SdPolyfillPlugin(["Chrome >= 61"]),
|
|
439
|
-
createSdNgPlugin(this._opt, this._modifiedFileSet, this._ngResultCache),
|
|
438
|
+
createSdNgPlugin(this._opt, this._modifiedFileSet, this._ngResultCache, this._outputPath),
|
|
440
439
|
...(this._conf.builderType === "electron"
|
|
441
440
|
? []
|
|
442
441
|
: [nodeStdLibBrowserPlugin(nodeStdLibBrowser)]),
|
|
443
|
-
SdWorkerPathPlugin(this._outputPath),
|
|
444
442
|
// {
|
|
445
443
|
// name: "log-circular",
|
|
446
444
|
// setup(build) {
|
|
@@ -2,4 +2,4 @@ import esbuild from "esbuild";
|
|
|
2
2
|
import { TNormPath } from "@simplysm/sd-core-node";
|
|
3
3
|
import { ISdCliNgPluginResultCache } from "../../types/plugin/ISdCliNgPluginResultCache";
|
|
4
4
|
import { ISdTsCompilerOptions } from "../../types/build/ISdTsCompilerOptions";
|
|
5
|
-
export declare function createSdNgPlugin(opt: ISdTsCompilerOptions, modifiedFileSet: Set<TNormPath>, resultCache: ISdCliNgPluginResultCache): esbuild.Plugin;
|
|
5
|
+
export declare function createSdNgPlugin(opt: ISdTsCompilerOptions, modifiedFileSet: Set<TNormPath>, resultCache: ISdCliNgPluginResultCache, workerOutdir?: string): esbuild.Plugin;
|
|
@@ -5,7 +5,8 @@ import { FsUtils, PathUtils, SdLogger } from "@simplysm/sd-core-node";
|
|
|
5
5
|
import { SdCliPerformanceTimer } from "../../utils/SdCliPerformanceTimer";
|
|
6
6
|
import { SdCliConvertMessageUtils } from "../../utils/SdCliConvertMessageUtils";
|
|
7
7
|
import { SdTsCompiler } from "../../ts-compiler/SdTsCompiler";
|
|
8
|
-
|
|
8
|
+
import { transformWorkerPaths } from "../commons/SdWorkerPathPlugin";
|
|
9
|
+
export function createSdNgPlugin(opt, modifiedFileSet, resultCache, workerOutdir) {
|
|
9
10
|
let perf;
|
|
10
11
|
const logger = SdLogger.get(["simplysm", "sd-cli", "createSdNgPlugin"]);
|
|
11
12
|
const debug = (...msg) => {
|
|
@@ -60,7 +61,11 @@ export function createSdNgPlugin(opt, modifiedFileSet, resultCache) {
|
|
|
60
61
|
if (output != null) {
|
|
61
62
|
return { contents: output, loader: "js" };
|
|
62
63
|
}
|
|
63
|
-
const
|
|
64
|
+
const rawContents = emittedFiles?.last()?.text ?? "";
|
|
65
|
+
// 워커 경로 변환 (import.meta.resolve → string literal)
|
|
66
|
+
const contents = workerOutdir != null
|
|
67
|
+
? await transformWorkerPaths(rawContents, args.path, workerOutdir, build.initialOptions, "document.baseURI")
|
|
68
|
+
: rawContents;
|
|
64
69
|
const { sideEffects } = await build.resolve(args.path, {
|
|
65
70
|
kind: "import-statement",
|
|
66
71
|
resolveDir: build.initialOptions.absWorkingDir ?? "",
|
|
@@ -96,6 +101,15 @@ export function createSdNgPlugin(opt, modifiedFileSet, resultCache) {
|
|
|
96
101
|
return { errors: [{ text: err?.message ?? String(err) }] };
|
|
97
102
|
}
|
|
98
103
|
});
|
|
104
|
+
if (workerOutdir != null) {
|
|
105
|
+
build.onLoad({ filter: /\.js$/ }, async (args) => {
|
|
106
|
+
const source = await FsUtils.readFileAsync(args.path);
|
|
107
|
+
const transformed = await transformWorkerPaths(source, args.path, workerOutdir, build.initialOptions, "document.baseURI");
|
|
108
|
+
if (transformed === source)
|
|
109
|
+
return null;
|
|
110
|
+
return { contents: transformed, loader: "js" };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
99
113
|
const otherLoaderFilter = new RegExp("(" +
|
|
100
114
|
Object.keys(build.initialOptions.loader ?? {})
|
|
101
115
|
.map((ext) => "\\" + ext)
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import esbuild from "esbuild";
|
|
2
|
+
export declare function transformWorkerPaths(source: string, filePath: string, outdir: string, buildOptions: esbuild.BuildOptions, baseUrlExpr?: string): Promise<string>;
|
|
2
3
|
export declare function SdWorkerPathPlugin(outdir: string): esbuild.Plugin;
|
|
@@ -1,67 +1,59 @@
|
|
|
1
1
|
import esbuild from "esbuild";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { FsUtils, HashUtils } from "@simplysm/sd-core-node";
|
|
4
|
+
export async function transformWorkerPaths(source, filePath, outdir, buildOptions, baseUrlExpr = "import.meta.url") {
|
|
5
|
+
// g flag regex는 함수 내에서 생성해야 lastIndex 상태 누출 방지
|
|
6
|
+
const regex = /import\.meta\.resolve\(\s*(['"])([^'"]+?\.worker)(?:\.[a-z]+)?\1\s*\)/g;
|
|
7
|
+
if (!regex.test(source)) {
|
|
8
|
+
return source;
|
|
9
|
+
}
|
|
10
|
+
return await replaceAsync(source, regex, async (_match, quote, relPath) => {
|
|
11
|
+
// 1. 실제 워커 파일 경로 계산
|
|
12
|
+
const workerSourcePath = path.resolve(path.dirname(filePath), relPath);
|
|
13
|
+
// 확장자가 없을 경우 자동 탐색 (js, ts 등)
|
|
14
|
+
const resolvedWorkerPath = resolveFile(workerSourcePath);
|
|
15
|
+
if (resolvedWorkerPath == null) {
|
|
16
|
+
// 파일이 없으면 건드리지 않음 (런타임 에러로 넘김)
|
|
17
|
+
return _match;
|
|
18
|
+
}
|
|
19
|
+
// 2. 출력될 워커 파일명 결정 (해시 사용)
|
|
20
|
+
const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
|
|
21
|
+
const hash = HashUtils.get(fileContent).substring(0, 8);
|
|
22
|
+
const workerBaseName = path.basename(resolvedWorkerPath, path.extname(resolvedWorkerPath));
|
|
23
|
+
const outputFileName = `${workerBaseName}-${hash}.js`;
|
|
24
|
+
const outputFilePath = path.join(outdir, "workers", outputFileName);
|
|
25
|
+
// 3. 워커 파일 빌드
|
|
26
|
+
await esbuild.build({
|
|
27
|
+
...buildOptions,
|
|
28
|
+
plugins: buildOptions.plugins?.filter((item) => item.name !== "sd-worker-path-plugin" &&
|
|
29
|
+
item.name !== "sd-ng-plugin" &&
|
|
30
|
+
item.name !== "sd-server-plugin") ?? [],
|
|
31
|
+
outdir: undefined,
|
|
32
|
+
entryPoints: [resolvedWorkerPath],
|
|
33
|
+
bundle: true,
|
|
34
|
+
write: true,
|
|
35
|
+
splitting: false,
|
|
36
|
+
outfile: outputFilePath,
|
|
37
|
+
});
|
|
38
|
+
// 4. new URL(..., baseUrlExpr).href로 치환
|
|
39
|
+
// 클라이언트: document.baseURI (esbuild가 import.meta를 빈 객체로 대체하는 문제 회피)
|
|
40
|
+
// 서버: import.meta.url (Node.js에서 정상 동작)
|
|
41
|
+
return `new URL(${quote}./workers/${outputFileName}${quote}, ${baseUrlExpr}).href`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
4
44
|
export function SdWorkerPathPlugin(outdir) {
|
|
5
45
|
return {
|
|
6
46
|
name: "sd-worker-path-plugin",
|
|
7
47
|
setup(build) {
|
|
8
48
|
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
|
|
9
49
|
const originalSource = await FsUtils.readFileAsync(args.path);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const regex = /import\.meta\.resolve\(\s*(['"])([^'"]+?\.worker)(?:\.[a-z]+)?\1\s*\)/g;
|
|
13
|
-
if (!regex.test(originalSource)) {
|
|
50
|
+
const transformed = await transformWorkerPaths(originalSource, args.path, outdir, build.initialOptions);
|
|
51
|
+
if (transformed === originalSource) {
|
|
14
52
|
return null;
|
|
15
53
|
}
|
|
16
|
-
// 매칭되는 모든 import.meta.resolve를 찾아서 처리
|
|
17
|
-
const newSource = await replaceAsync(originalSource, regex, async (match, quote, relPath) => {
|
|
18
|
-
// 1. 실제 워커 파일 경로 계산
|
|
19
|
-
const workerSourcePath = path.resolve(path.dirname(args.path), relPath);
|
|
20
|
-
// 확장자가 없을 경우 자동 탐색 (js, ts 등)
|
|
21
|
-
const resolvedWorkerPath = resolveFile(workerSourcePath);
|
|
22
|
-
if (resolvedWorkerPath == null) {
|
|
23
|
-
// 파일이 없으면 건드리지 않음 (런타임 에러로 넘김)
|
|
24
|
-
return match;
|
|
25
|
-
}
|
|
26
|
-
// 2. 출력될 워커 파일명 결정 (캐싱 및 중복 방지를 위해 해시 사용 권장)
|
|
27
|
-
const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
|
|
28
|
-
const hash = HashUtils.get(fileContent).substring(0, 8);
|
|
29
|
-
const workerBaseName = path.basename(resolvedWorkerPath, path.extname(resolvedWorkerPath));
|
|
30
|
-
const outputFileName = `${workerBaseName}-${hash}.js`;
|
|
31
|
-
const outputFilePath = path.join(outdir, "workers", outputFileName);
|
|
32
|
-
// 3. 워커 파일 빌드 (존재하지 않거나 변경되었을 때만)
|
|
33
|
-
// (간단하게 하기 위해 매번 빌드 시도하거나, 해시로 체크 가능. 여기선 esbuild 증분 빌드에 맡김)
|
|
34
|
-
// *중요*: 워커도 번들링해야 함.
|
|
35
|
-
await esbuild.build({
|
|
36
|
-
...build.initialOptions,
|
|
37
|
-
plugins: build.initialOptions.plugins?.filter((item) => item.name !== "sd-worker-path-plugin" &&
|
|
38
|
-
item.name !== "sd-ng-plugin" &&
|
|
39
|
-
item.name !== "sd-server-plugin") ?? [],
|
|
40
|
-
outdir: undefined,
|
|
41
|
-
entryPoints: [resolvedWorkerPath],
|
|
42
|
-
bundle: true,
|
|
43
|
-
write: true,
|
|
44
|
-
splitting: false,
|
|
45
|
-
outfile: outputFilePath,
|
|
46
|
-
// platform: build.initialOptions.platform,
|
|
47
|
-
// target: build.initialOptions.target,
|
|
48
|
-
// format: build.initialOptions.format,
|
|
49
|
-
// minify: build.initialOptions.minify,
|
|
50
|
-
// banner: build.initialOptions.banner,
|
|
51
|
-
// sourcemap: build.initialOptions.sourcemap,
|
|
52
|
-
// external: build.initialOptions.external, // 외부 의존성 설정 상속
|
|
53
|
-
// 플러그인 상속 주의: 무한 루프 방지를 위해 이 플러그인은 제외해야 함
|
|
54
|
-
});
|
|
55
|
-
// 4. 경로 치환
|
|
56
|
-
// 번들링된 메인 파일(dist/main.js) 기준으로 workers 폴더는 ./workers/ 임
|
|
57
|
-
return `import.meta.resolve(${quote}./workers/${outputFileName}${quote})`;
|
|
58
|
-
});
|
|
59
54
|
return {
|
|
60
|
-
contents:
|
|
61
|
-
loader: "ts",
|
|
62
|
-
// 워커 파일이 변경되면 이 파일도 다시 빌드되어야 함을 알림
|
|
63
|
-
// (정확히 하려면 워커의 의존성까지 다 넣어야 하지만, 최소한 워커 엔트리는 넣음)
|
|
64
|
-
// watchFiles: ... (esbuild가 내부적으로 처리해주길 기대)
|
|
55
|
+
contents: transformed,
|
|
56
|
+
loader: "ts",
|
|
65
57
|
};
|
|
66
58
|
});
|
|
67
59
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/sd-cli",
|
|
3
|
-
"version": "12.16.
|
|
3
|
+
"version": "12.16.52",
|
|
4
4
|
"description": "심플리즘 패키지 - CLI",
|
|
5
5
|
"author": "김석래",
|
|
6
6
|
"repository": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"@angular/compiler-cli": "^20.3.18",
|
|
18
18
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
19
19
|
"@electron/rebuild": "^4.0.3",
|
|
20
|
-
"@simplysm/sd-core-common": "12.16.
|
|
21
|
-
"@simplysm/sd-core-node": "12.16.
|
|
22
|
-
"@simplysm/sd-service-server": "12.16.
|
|
23
|
-
"@simplysm/sd-storage": "12.16.
|
|
20
|
+
"@simplysm/sd-core-common": "12.16.52",
|
|
21
|
+
"@simplysm/sd-core-node": "12.16.52",
|
|
22
|
+
"@simplysm/sd-service-server": "12.16.52",
|
|
23
|
+
"@simplysm/sd-storage": "12.16.52",
|
|
24
24
|
"abortcontroller-polyfill": "^1.7.8",
|
|
25
25
|
"browserslist": "^4.28.1",
|
|
26
26
|
"cordova": "^13.0.0",
|
|
@@ -40,7 +40,6 @@ import { ISdCliNgPluginResultCache } from "../../types/plugin/ISdCliNgPluginResu
|
|
|
40
40
|
import { INpmConfig } from "../../types/common-config/INpmConfig";
|
|
41
41
|
import { ISdBuildResult } from "../../types/build/ISdBuildResult";
|
|
42
42
|
import { ISdTsCompilerOptions } from "../../types/build/ISdTsCompilerOptions";
|
|
43
|
-
import { SdWorkerPathPlugin } from "../commons/SdWorkerPathPlugin";
|
|
44
43
|
import { SdPolyfillPlugin } from "./SdPolyfillPlugin";
|
|
45
44
|
|
|
46
45
|
export class SdNgBundler {
|
|
@@ -561,11 +560,10 @@ export class SdNgBundler {
|
|
|
561
560
|
plugins: [
|
|
562
561
|
createSourcemapIgnorelistPlugin(),
|
|
563
562
|
SdPolyfillPlugin(["Chrome >= 61"]),
|
|
564
|
-
createSdNgPlugin(this._opt, this._modifiedFileSet, this._ngResultCache),
|
|
563
|
+
createSdNgPlugin(this._opt, this._modifiedFileSet, this._ngResultCache, this._outputPath),
|
|
565
564
|
...(this._conf.builderType === "electron"
|
|
566
565
|
? []
|
|
567
566
|
: [nodeStdLibBrowserPlugin(nodeStdLibBrowser)]),
|
|
568
|
-
SdWorkerPathPlugin(this._outputPath),
|
|
569
567
|
// {
|
|
570
568
|
// name: "log-circular",
|
|
571
569
|
// setup(build) {
|
|
@@ -9,11 +9,13 @@ import { SdTsCompiler } from "../../ts-compiler/SdTsCompiler";
|
|
|
9
9
|
import { ISdCliNgPluginResultCache } from "../../types/plugin/ISdCliNgPluginResultCache";
|
|
10
10
|
import { ISdTsCompilerResult } from "../../types/build/ISdTsCompilerResult";
|
|
11
11
|
import { ISdTsCompilerOptions } from "../../types/build/ISdTsCompilerOptions";
|
|
12
|
+
import { transformWorkerPaths } from "../commons/SdWorkerPathPlugin";
|
|
12
13
|
|
|
13
14
|
export function createSdNgPlugin(
|
|
14
15
|
opt: ISdTsCompilerOptions,
|
|
15
16
|
modifiedFileSet: Set<TNormPath>,
|
|
16
17
|
resultCache: ISdCliNgPluginResultCache,
|
|
18
|
+
workerOutdir?: string,
|
|
17
19
|
): esbuild.Plugin {
|
|
18
20
|
let perf: SdCliPerformanceTimer;
|
|
19
21
|
const logger = SdLogger.get(["simplysm", "sd-cli", "createSdNgPlugin"]);
|
|
@@ -90,7 +92,13 @@ export function createSdNgPlugin(
|
|
|
90
92
|
return { contents: output, loader: "js" };
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
const
|
|
95
|
+
const rawContents = emittedFiles?.last()?.text ?? "";
|
|
96
|
+
|
|
97
|
+
// 워커 경로 변환 (import.meta.resolve → string literal)
|
|
98
|
+
const contents =
|
|
99
|
+
workerOutdir != null
|
|
100
|
+
? await transformWorkerPaths(rawContents, args.path, workerOutdir, build.initialOptions, "document.baseURI")
|
|
101
|
+
: rawContents;
|
|
94
102
|
|
|
95
103
|
const { sideEffects } = await build.resolve(args.path, {
|
|
96
104
|
kind: "import-statement",
|
|
@@ -142,6 +150,15 @@ export function createSdNgPlugin(
|
|
|
142
150
|
}
|
|
143
151
|
});
|
|
144
152
|
|
|
153
|
+
if (workerOutdir != null) {
|
|
154
|
+
build.onLoad({ filter: /\.js$/ }, async (args) => {
|
|
155
|
+
const source = await FsUtils.readFileAsync(args.path);
|
|
156
|
+
const transformed = await transformWorkerPaths(source, args.path, workerOutdir, build.initialOptions, "document.baseURI");
|
|
157
|
+
if (transformed === source) return null;
|
|
158
|
+
return { contents: transformed, loader: "js" };
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
145
162
|
const otherLoaderFilter = new RegExp(
|
|
146
163
|
"(" +
|
|
147
164
|
Object.keys(build.initialOptions.loader ?? {})
|
|
@@ -2,6 +2,64 @@ import esbuild from "esbuild";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { FsUtils, HashUtils } from "@simplysm/sd-core-node";
|
|
4
4
|
|
|
5
|
+
export async function transformWorkerPaths(
|
|
6
|
+
source: string,
|
|
7
|
+
filePath: string,
|
|
8
|
+
outdir: string,
|
|
9
|
+
buildOptions: esbuild.BuildOptions,
|
|
10
|
+
baseUrlExpr: string = "import.meta.url",
|
|
11
|
+
): Promise<string> {
|
|
12
|
+
// g flag regex는 함수 내에서 생성해야 lastIndex 상태 누출 방지
|
|
13
|
+
const regex = /import\.meta\.resolve\(\s*(['"])([^'"]+?\.worker)(?:\.[a-z]+)?\1\s*\)/g;
|
|
14
|
+
|
|
15
|
+
if (!regex.test(source)) {
|
|
16
|
+
return source;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return await replaceAsync(source, regex, async (_match, quote, relPath) => {
|
|
20
|
+
// 1. 실제 워커 파일 경로 계산
|
|
21
|
+
const workerSourcePath = path.resolve(path.dirname(filePath), relPath);
|
|
22
|
+
|
|
23
|
+
// 확장자가 없을 경우 자동 탐색 (js, ts 등)
|
|
24
|
+
const resolvedWorkerPath = resolveFile(workerSourcePath);
|
|
25
|
+
if (resolvedWorkerPath == null) {
|
|
26
|
+
// 파일이 없으면 건드리지 않음 (런타임 에러로 넘김)
|
|
27
|
+
return _match;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. 출력될 워커 파일명 결정 (해시 사용)
|
|
31
|
+
const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
|
|
32
|
+
const hash = HashUtils.get(fileContent).substring(0, 8);
|
|
33
|
+
const workerBaseName = path.basename(resolvedWorkerPath, path.extname(resolvedWorkerPath));
|
|
34
|
+
const outputFileName = `${workerBaseName}-${hash}.js`;
|
|
35
|
+
const outputFilePath = path.join(outdir, "workers", outputFileName);
|
|
36
|
+
|
|
37
|
+
// 3. 워커 파일 빌드
|
|
38
|
+
await esbuild.build({
|
|
39
|
+
...buildOptions,
|
|
40
|
+
plugins:
|
|
41
|
+
buildOptions.plugins?.filter(
|
|
42
|
+
(item) =>
|
|
43
|
+
item.name !== "sd-worker-path-plugin" &&
|
|
44
|
+
item.name !== "sd-ng-plugin" &&
|
|
45
|
+
item.name !== "sd-server-plugin",
|
|
46
|
+
) ?? [],
|
|
47
|
+
outdir: undefined,
|
|
48
|
+
|
|
49
|
+
entryPoints: [resolvedWorkerPath],
|
|
50
|
+
bundle: true,
|
|
51
|
+
write: true,
|
|
52
|
+
splitting: false,
|
|
53
|
+
outfile: outputFilePath,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 4. new URL(..., baseUrlExpr).href로 치환
|
|
57
|
+
// 클라이언트: document.baseURI (esbuild가 import.meta를 빈 객체로 대체하는 문제 회피)
|
|
58
|
+
// 서버: import.meta.url (Node.js에서 정상 동작)
|
|
59
|
+
return `new URL(${quote}./workers/${outputFileName}${quote}, ${baseUrlExpr}).href`;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
5
63
|
export function SdWorkerPathPlugin(outdir: string): esbuild.Plugin {
|
|
6
64
|
return {
|
|
7
65
|
name: "sd-worker-path-plugin",
|
|
@@ -9,80 +67,20 @@ export function SdWorkerPathPlugin(outdir: string): esbuild.Plugin {
|
|
|
9
67
|
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => {
|
|
10
68
|
const originalSource = await FsUtils.readFileAsync(args.path);
|
|
11
69
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
70
|
+
const transformed = await transformWorkerPaths(
|
|
71
|
+
originalSource,
|
|
72
|
+
args.path,
|
|
73
|
+
outdir,
|
|
74
|
+
build.initialOptions,
|
|
75
|
+
);
|
|
15
76
|
|
|
16
|
-
if (
|
|
77
|
+
if (transformed === originalSource) {
|
|
17
78
|
return null;
|
|
18
79
|
}
|
|
19
80
|
|
|
20
|
-
// 매칭되는 모든 import.meta.resolve를 찾아서 처리
|
|
21
|
-
const newSource = await replaceAsync(
|
|
22
|
-
originalSource,
|
|
23
|
-
regex,
|
|
24
|
-
async (match, quote, relPath) => {
|
|
25
|
-
// 1. 실제 워커 파일 경로 계산
|
|
26
|
-
const workerSourcePath = path.resolve(path.dirname(args.path), relPath);
|
|
27
|
-
|
|
28
|
-
// 확장자가 없을 경우 자동 탐색 (js, ts 등)
|
|
29
|
-
const resolvedWorkerPath = resolveFile(workerSourcePath);
|
|
30
|
-
if (resolvedWorkerPath == null) {
|
|
31
|
-
// 파일이 없으면 건드리지 않음 (런타임 에러로 넘김)
|
|
32
|
-
return match;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 2. 출력될 워커 파일명 결정 (캐싱 및 중복 방지를 위해 해시 사용 권장)
|
|
36
|
-
const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
|
|
37
|
-
const hash = HashUtils.get(fileContent).substring(0, 8);
|
|
38
|
-
const workerBaseName = path.basename(
|
|
39
|
-
resolvedWorkerPath,
|
|
40
|
-
path.extname(resolvedWorkerPath),
|
|
41
|
-
);
|
|
42
|
-
const outputFileName = `${workerBaseName}-${hash}.js`;
|
|
43
|
-
const outputFilePath = path.join(outdir, "workers", outputFileName);
|
|
44
|
-
|
|
45
|
-
// 3. 워커 파일 빌드 (존재하지 않거나 변경되었을 때만)
|
|
46
|
-
// (간단하게 하기 위해 매번 빌드 시도하거나, 해시로 체크 가능. 여기선 esbuild 증분 빌드에 맡김)
|
|
47
|
-
// *중요*: 워커도 번들링해야 함.
|
|
48
|
-
await esbuild.build({
|
|
49
|
-
...build.initialOptions,
|
|
50
|
-
plugins:
|
|
51
|
-
build.initialOptions.plugins?.filter(
|
|
52
|
-
(item) =>
|
|
53
|
-
item.name !== "sd-worker-path-plugin" &&
|
|
54
|
-
item.name !== "sd-ng-plugin" &&
|
|
55
|
-
item.name !== "sd-server-plugin",
|
|
56
|
-
) ?? [],
|
|
57
|
-
outdir: undefined,
|
|
58
|
-
|
|
59
|
-
entryPoints: [resolvedWorkerPath],
|
|
60
|
-
bundle: true,
|
|
61
|
-
write: true,
|
|
62
|
-
splitting: false,
|
|
63
|
-
outfile: outputFilePath,
|
|
64
|
-
// platform: build.initialOptions.platform,
|
|
65
|
-
// target: build.initialOptions.target,
|
|
66
|
-
// format: build.initialOptions.format,
|
|
67
|
-
// minify: build.initialOptions.minify,
|
|
68
|
-
// banner: build.initialOptions.banner,
|
|
69
|
-
// sourcemap: build.initialOptions.sourcemap,
|
|
70
|
-
// external: build.initialOptions.external, // 외부 의존성 설정 상속
|
|
71
|
-
// 플러그인 상속 주의: 무한 루프 방지를 위해 이 플러그인은 제외해야 함
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// 4. 경로 치환
|
|
75
|
-
// 번들링된 메인 파일(dist/main.js) 기준으로 workers 폴더는 ./workers/ 임
|
|
76
|
-
return `import.meta.resolve(${quote}./workers/${outputFileName}${quote})`;
|
|
77
|
-
},
|
|
78
|
-
);
|
|
79
|
-
|
|
80
81
|
return {
|
|
81
|
-
contents:
|
|
82
|
-
loader: "ts",
|
|
83
|
-
// 워커 파일이 변경되면 이 파일도 다시 빌드되어야 함을 알림
|
|
84
|
-
// (정확히 하려면 워커의 의존성까지 다 넣어야 하지만, 최소한 워커 엔트리는 넣음)
|
|
85
|
-
// watchFiles: ... (esbuild가 내부적으로 처리해주길 기대)
|
|
82
|
+
contents: transformed,
|
|
83
|
+
loader: "ts",
|
|
86
84
|
};
|
|
87
85
|
});
|
|
88
86
|
},
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { transformWorkerPaths } from "../../src/pkg-builders/commons/SdWorkerPathPlugin";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* .js 핸들러가 사용하는 transformWorkerPaths의 .js 파일 처리를 검증하는 Unit Test.
|
|
9
|
+
* transformWorkerPaths는 이미 .ts 파일에 대해 테스트되어 있으므로,
|
|
10
|
+
* .js 파일에서도 동일하게 동작하는지 검증한다.
|
|
11
|
+
*/
|
|
12
|
+
describe("transformWorkerPaths .js 파일 지원", () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-worker-js-unit-"));
|
|
17
|
+
|
|
18
|
+
const workersDir = path.join(tmpDir, "src", "workers");
|
|
19
|
+
fs.mkdirSync(workersDir, { recursive: true });
|
|
20
|
+
fs.writeFileSync(
|
|
21
|
+
path.join(workersDir, "client-protocol.worker.js"),
|
|
22
|
+
"self.onmessage = (e) => { self.postMessage('ok'); };",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it(".js 파일 경로에서 import.meta.resolve 워커 패턴이 정상 변환된다", async () => {
|
|
31
|
+
const source = `const url = import.meta.resolve("./workers/client-protocol.worker");`;
|
|
32
|
+
const filePath = path.join(tmpDir, "src", "protocol.js");
|
|
33
|
+
const outdir = path.join(tmpDir, "dist-js");
|
|
34
|
+
|
|
35
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
36
|
+
format: "esm",
|
|
37
|
+
platform: "browser",
|
|
38
|
+
logLevel: "silent",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(result).toMatch(
|
|
42
|
+
/new URL\(["']\.\/workers\/client-protocol\.worker-[a-f0-9]+\.js["'], import\.meta\.url\)\.href/,
|
|
43
|
+
);
|
|
44
|
+
expect(result).not.toContain("import.meta.resolve");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it(".js 파일에서 워커 패턴이 없으면 원본 그대로 반환된다", async () => {
|
|
48
|
+
const source = `const x = 42; export default x;`;
|
|
49
|
+
const filePath = path.join(tmpDir, "src", "normal.js");
|
|
50
|
+
const outdir = path.join(tmpDir, "dist-js-no-pattern");
|
|
51
|
+
|
|
52
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
53
|
+
format: "esm",
|
|
54
|
+
platform: "browser",
|
|
55
|
+
logLevel: "silent",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result).toBe(source);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it(".js 파일에서 여러 워커 패턴이 동시에 변환된다", async () => {
|
|
62
|
+
// 두 번째 워커 파일 추가
|
|
63
|
+
const workersDir = path.join(tmpDir, "src", "workers");
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
path.join(workersDir, "decode.worker.js"),
|
|
66
|
+
"self.onmessage = (e) => { self.postMessage('decode'); };",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const source = [
|
|
70
|
+
`const url1 = import.meta.resolve("./workers/client-protocol.worker");`,
|
|
71
|
+
`const url2 = import.meta.resolve("./workers/decode.worker");`,
|
|
72
|
+
].join("\n");
|
|
73
|
+
const filePath = path.join(tmpDir, "src", "multi.js");
|
|
74
|
+
const outdir = path.join(tmpDir, "dist-js-multi");
|
|
75
|
+
|
|
76
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
77
|
+
format: "esm",
|
|
78
|
+
platform: "browser",
|
|
79
|
+
logLevel: "silent",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result).not.toContain("import.meta.resolve");
|
|
83
|
+
expect(result).toMatch(/client-protocol\.worker-[a-f0-9]+\.js/);
|
|
84
|
+
expect(result).toMatch(/decode\.worker-[a-f0-9]+\.js/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createSdNgPlugin } from "../../src/pkg-builders/client/createSdNgPlugin";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import esbuild from "esbuild";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* createSdNgPlugin의 .js onLoad 핸들러가
|
|
10
|
+
* transformWorkerPaths를 적용하는지 검증하는 Acceptance Test.
|
|
11
|
+
*
|
|
12
|
+
* 플러그인의 setup()을 호출하여 등록된 핸들러를 캡처한 뒤,
|
|
13
|
+
* .js 파일에 대해 핸들러를 직접 호출하여 변환 결과를 검증한다.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface CapturedHandler {
|
|
17
|
+
filter: RegExp;
|
|
18
|
+
handler: (args: esbuild.OnLoadArgs) => Promise<esbuild.OnLoadResult | null>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function captureOnLoadHandlers(workerOutdir?: string): CapturedHandler[] {
|
|
22
|
+
const captureTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-ng-capture-"));
|
|
23
|
+
|
|
24
|
+
// SdTsCompiler가 tsconfig.json을 읽으므로 최소한의 파일을 제공
|
|
25
|
+
fs.writeFileSync(
|
|
26
|
+
path.join(captureTmpDir, "tsconfig.json"),
|
|
27
|
+
JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext" } }),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const plugin = createSdNgPlugin(
|
|
31
|
+
{
|
|
32
|
+
pkgPath: captureTmpDir as any,
|
|
33
|
+
tsConfigPath: path.join(captureTmpDir, "tsconfig.json") as any,
|
|
34
|
+
},
|
|
35
|
+
new Set(),
|
|
36
|
+
{ affectedFileSet: new Set(), watchFileSet: new Set() },
|
|
37
|
+
workerOutdir,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const handlers: CapturedHandler[] = [];
|
|
41
|
+
const mockBuild = {
|
|
42
|
+
initialOptions: {
|
|
43
|
+
format: "esm" as const,
|
|
44
|
+
platform: "browser" as const,
|
|
45
|
+
logLevel: "silent" as const,
|
|
46
|
+
loader: {},
|
|
47
|
+
},
|
|
48
|
+
onStart: () => {},
|
|
49
|
+
onLoad: (opts: { filter: RegExp }, handler: any) => {
|
|
50
|
+
handlers.push({ filter: opts.filter, handler });
|
|
51
|
+
},
|
|
52
|
+
onEnd: () => {},
|
|
53
|
+
resolve: async () => ({ sideEffects: false }),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
plugin.setup(mockBuild as any);
|
|
57
|
+
fs.rmSync(captureTmpDir, { recursive: true, force: true });
|
|
58
|
+
return handlers;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("createSdNgPlugin .js onLoad 핸들러", () => {
|
|
62
|
+
let tmpDir: string;
|
|
63
|
+
|
|
64
|
+
beforeAll(() => {
|
|
65
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-ng-js-handler-"));
|
|
66
|
+
|
|
67
|
+
// 워커 파일 생성
|
|
68
|
+
const workersDir = path.join(tmpDir, "src", "workers");
|
|
69
|
+
fs.mkdirSync(workersDir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
path.join(workersDir, "client-protocol.worker.js"),
|
|
72
|
+
"self.onmessage = (e) => { self.postMessage('ok'); };",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterAll(() => {
|
|
77
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("npm 패키지의 .js 파일에서 import.meta.resolve 워커 패턴이 변환된다", async () => {
|
|
81
|
+
const workerOutdir = path.join(tmpDir, "dist");
|
|
82
|
+
const handlers = captureOnLoadHandlers(workerOutdir);
|
|
83
|
+
|
|
84
|
+
// .js 필터를 가진 핸들러가 등록되어야 한다
|
|
85
|
+
const jsHandler = handlers.find((h) => h.filter.test("test.js") && !h.filter.test("test.mjs"));
|
|
86
|
+
expect(jsHandler).toBeDefined();
|
|
87
|
+
|
|
88
|
+
// .js 파일에 import.meta.resolve 워커 패턴이 있는 경우
|
|
89
|
+
const jsFilePath = path.join(tmpDir, "src", "protocol.js");
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
jsFilePath,
|
|
92
|
+
`const url = import.meta.resolve("./workers/client-protocol.worker");`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const result = await jsHandler!.handler({
|
|
96
|
+
path: jsFilePath,
|
|
97
|
+
namespace: "file",
|
|
98
|
+
suffix: "",
|
|
99
|
+
pluginData: undefined,
|
|
100
|
+
with: {},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result).not.toBeNull();
|
|
104
|
+
expect(result!.contents).toBeDefined();
|
|
105
|
+
expect(String(result!.contents)).toMatch(
|
|
106
|
+
/new URL\(["']\.\/workers\/client-protocol\.worker-[a-f0-9]+\.js["'], import\.meta\.url\)\.href/,
|
|
107
|
+
);
|
|
108
|
+
expect(String(result!.contents)).not.toContain("import.meta.resolve");
|
|
109
|
+
expect(result!.loader).toBe("js");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("워커 패턴이 없는 .js 파일은 null을 반환한다", async () => {
|
|
113
|
+
const workerOutdir = path.join(tmpDir, "dist-no-pattern");
|
|
114
|
+
const handlers = captureOnLoadHandlers(workerOutdir);
|
|
115
|
+
|
|
116
|
+
const jsHandler = handlers.find((h) => h.filter.test("test.js") && !h.filter.test("test.mjs"));
|
|
117
|
+
expect(jsHandler).toBeDefined();
|
|
118
|
+
|
|
119
|
+
// 워커 패턴이 없는 .js 파일
|
|
120
|
+
const jsFilePath = path.join(tmpDir, "src", "normal.js");
|
|
121
|
+
fs.writeFileSync(jsFilePath, `const x = 42; export default x;`);
|
|
122
|
+
|
|
123
|
+
const result = await jsHandler!.handler({
|
|
124
|
+
path: jsFilePath,
|
|
125
|
+
namespace: "file",
|
|
126
|
+
suffix: "",
|
|
127
|
+
pluginData: undefined,
|
|
128
|
+
with: {},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("workerOutdir가 없으면 .js 핸들러가 등록되지 않는다", () => {
|
|
135
|
+
const handlers = captureOnLoadHandlers(undefined);
|
|
136
|
+
|
|
137
|
+
// .js 전용 핸들러가 없어야 한다 (기존 .ts, .mjs, otherLoader 필터만 있음)
|
|
138
|
+
const jsOnlyHandler = handlers.find(
|
|
139
|
+
(h) => h.filter.test("test.js") && !h.filter.test("test.ts") && !h.filter.test("test.mjs"),
|
|
140
|
+
);
|
|
141
|
+
expect(jsOnlyHandler).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("기존 .ts 핸들러가 여전히 등록되어 있다", () => {
|
|
145
|
+
const workerOutdir = path.join(tmpDir, "dist-ts-check");
|
|
146
|
+
const handlers = captureOnLoadHandlers(workerOutdir);
|
|
147
|
+
|
|
148
|
+
const tsHandler = handlers.find((h) => h.filter.test("test.ts"));
|
|
149
|
+
expect(tsHandler).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Worker 경로 변환 Angular 빌드 통합 검증
|
|
2
|
+
|
|
3
|
+
## 전제 조건
|
|
4
|
+
- Angular 클라이언트 패키지가 `sd-service-client`를 의존하고 있다
|
|
5
|
+
- 서버가 실행 중이며 WebSocket 통신이 가능하다
|
|
6
|
+
|
|
7
|
+
## 수행 절차
|
|
8
|
+
|
|
9
|
+
### 1. Angular 클라이언트 빌드
|
|
10
|
+
1. `yarn build --packages {angular-client-package}` 실행
|
|
11
|
+
2. 빌드 출력 디렉토리에서 chunk 파일 검색
|
|
12
|
+
|
|
13
|
+
### 2. 번들 출력 검증
|
|
14
|
+
1. 빌드된 chunk 파일에서 `import_meta.resolve` 또는 `import.meta.resolve` 검색
|
|
15
|
+
2. 해당 패턴이 없어야 한다
|
|
16
|
+
3. `./workers/client-protocol.worker-` 패턴이 string literal로 존재해야 한다
|
|
17
|
+
4. `dist/workers/` 하위에 `client-protocol.worker-{hash}.js` 파일이 생성되어야 한다
|
|
18
|
+
|
|
19
|
+
### 3. 런타임 검증
|
|
20
|
+
1. Angular 앱을 브라우저(Chrome)에서 실행
|
|
21
|
+
2. 서버로부터 30KB 이상의 WebSocket 응답을 발생시키는 작업 수행
|
|
22
|
+
3. 콘솔에 `TypeError: import_meta.resolve is not a function` 에러가 발생하지 않아야 한다
|
|
23
|
+
4. 응답이 정상적으로 디코딩되어 화면에 표시되어야 한다
|
|
24
|
+
|
|
25
|
+
## 기대 결과
|
|
26
|
+
- 빌드 출력에 `import.meta.resolve` 런타임 호출이 없다
|
|
27
|
+
- 워커 파일이 별도 빌드되어 `workers/` 하위에 존재한다
|
|
28
|
+
- 30KB 이상 응답에서 Worker가 정상 동작한다
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { transformWorkerPaths } from "../../src/pkg-builders/commons/SdWorkerPathPlugin";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
|
|
7
|
+
describe("transformWorkerPaths", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-worker-test-"));
|
|
12
|
+
|
|
13
|
+
const workersDir = path.join(tmpDir, "src", "workers");
|
|
14
|
+
fs.mkdirSync(workersDir, { recursive: true });
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(workersDir, "test.worker.ts"),
|
|
17
|
+
"self.onmessage = (e: MessageEvent) => { self.postMessage('ok'); };",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("서버 패턴 -- import.meta.resolve() 직접 사용", () => {
|
|
26
|
+
it("치환 결과는 new URL(..., import.meta.url).href 형태이다", async () => {
|
|
27
|
+
const source = `const url = import.meta.resolve("./workers/test.worker");`;
|
|
28
|
+
const filePath = path.join(tmpDir, "src", "main.ts");
|
|
29
|
+
const outdir = path.join(tmpDir, "dist-server");
|
|
30
|
+
|
|
31
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
32
|
+
format: "esm",
|
|
33
|
+
platform: "node",
|
|
34
|
+
logLevel: "silent",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// new URL("./workers/test.worker-{hash}.js", import.meta.url).href 형태여야 함
|
|
38
|
+
expect(result).toMatch(
|
|
39
|
+
/new URL\(["']\.\/workers\/test\.worker-[a-f0-9]+\.js["'], import\.meta\.url\)\.href/,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("클라이언트 패턴 -- new URL(import.meta.resolve(...), import.meta.url) 사용", () => {
|
|
45
|
+
it("import.meta.resolve() 부분만 치환되어 이중 new URL() 래핑이 된다", async () => {
|
|
46
|
+
const source = `const w = new Worker(new URL(import.meta.resolve("./workers/test.worker"), import.meta.url));`;
|
|
47
|
+
const filePath = path.join(tmpDir, "src", "main.ts");
|
|
48
|
+
const outdir = path.join(tmpDir, "dist-client");
|
|
49
|
+
|
|
50
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
51
|
+
format: "esm",
|
|
52
|
+
platform: "browser",
|
|
53
|
+
logLevel: "silent",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// import.meta.resolve() 부분이 new URL(...).href로 치환
|
|
57
|
+
expect(result).toMatch(
|
|
58
|
+
/new URL\(new URL\(["']\.\/workers\/test\.worker-[a-f0-9]+\.js["'], import\.meta\.url\)\.href, import\.meta\.url\)/,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("공통 계약", () => {
|
|
64
|
+
it("변환 후 import.meta.resolve 호출이 제거된다", async () => {
|
|
65
|
+
const source = `const url = import.meta.resolve("./workers/test.worker");`;
|
|
66
|
+
const filePath = path.join(tmpDir, "src", "main.ts");
|
|
67
|
+
const outdir = path.join(tmpDir, "dist-common1");
|
|
68
|
+
|
|
69
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
70
|
+
format: "esm",
|
|
71
|
+
platform: "node",
|
|
72
|
+
logLevel: "silent",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).not.toContain("import.meta.resolve");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("변환 후 워커 파일명이 해시 포함 string literal로 존재한다", async () => {
|
|
79
|
+
const source = `const url = import.meta.resolve("./workers/test.worker");`;
|
|
80
|
+
const filePath = path.join(tmpDir, "src", "main.ts");
|
|
81
|
+
const outdir = path.join(tmpDir, "dist-common2");
|
|
82
|
+
|
|
83
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
84
|
+
format: "esm",
|
|
85
|
+
platform: "node",
|
|
86
|
+
logLevel: "silent",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result).toMatch(/["']\.\/workers\/test\.worker-[a-f0-9]+\.js["']/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("변환 후 import.meta.url이 유지된다", async () => {
|
|
93
|
+
const source = `const url = import.meta.resolve("./workers/test.worker");`;
|
|
94
|
+
const filePath = path.join(tmpDir, "src", "main.ts");
|
|
95
|
+
const outdir = path.join(tmpDir, "dist-common3");
|
|
96
|
+
|
|
97
|
+
const result = await transformWorkerPaths(source, filePath, outdir, {
|
|
98
|
+
format: "esm",
|
|
99
|
+
platform: "node",
|
|
100
|
+
logLevel: "silent",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result).toContain("import.meta.url");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("매칭 없으면 원본을 그대로 반환한다", async () => {
|
|
108
|
+
const source = `const x = 42;`;
|
|
109
|
+
const result = await transformWorkerPaths(source, "/tmp/main.ts", "/tmp/dist", {});
|
|
110
|
+
expect(result).toBe(source);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("워커 내용 변경 시 다른 hash가 생성된다", async () => {
|
|
114
|
+
const source = `import.meta.resolve("./workers/test.worker")`;
|
|
115
|
+
const filePath = path.join(tmpDir, "src", "main.ts");
|
|
116
|
+
const outdir = path.join(tmpDir, "dist-hash-test");
|
|
117
|
+
const workerPath = path.join(tmpDir, "src", "workers", "test.worker.ts");
|
|
118
|
+
|
|
119
|
+
// 첫 번째 변환
|
|
120
|
+
fs.writeFileSync(workerPath, "self.onmessage = () => { self.postMessage('v1'); };");
|
|
121
|
+
const result1 = await transformWorkerPaths(source, filePath, outdir, {
|
|
122
|
+
format: "esm",
|
|
123
|
+
platform: "browser",
|
|
124
|
+
logLevel: "silent",
|
|
125
|
+
});
|
|
126
|
+
const hash1 = result1.match(/test\.worker-([a-f0-9]+)\.js/)?.[1];
|
|
127
|
+
|
|
128
|
+
// 워커 내용 변경 후 두 번째 변환
|
|
129
|
+
fs.writeFileSync(workerPath, "self.onmessage = () => { self.postMessage('v2-changed'); };");
|
|
130
|
+
const result2 = await transformWorkerPaths(source, filePath, outdir, {
|
|
131
|
+
format: "esm",
|
|
132
|
+
platform: "browser",
|
|
133
|
+
logLevel: "silent",
|
|
134
|
+
});
|
|
135
|
+
const hash2 = result2.match(/test\.worker-([a-f0-9]+)\.js/)?.[1];
|
|
136
|
+
|
|
137
|
+
expect(hash1).toBeDefined();
|
|
138
|
+
expect(hash2).toBeDefined();
|
|
139
|
+
expect(hash1).not.toBe(hash2);
|
|
140
|
+
});
|
|
141
|
+
});
|