@simplysm/sd-cli 14.0.46 → 14.0.48
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/README.md +782 -0
- package/dist/angular/ngtsc-build-core.js +2 -2
- package/dist/angular/ngtsc-build-core.js.map +1 -1
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +3 -2
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/capacitor/capacitor-android.js +2 -2
- package/dist/capacitor/capacitor-android.js.map +1 -1
- package/dist/capacitor/capacitor-build.d.ts.map +1 -1
- package/dist/capacitor/capacitor-build.js +2 -1
- package/dist/capacitor/capacitor-build.js.map +1 -1
- package/dist/capacitor/capacitor-icon.d.ts.map +1 -1
- package/dist/capacitor/capacitor-icon.js +2 -1
- package/dist/capacitor/capacitor-icon.js.map +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
- package/dist/capacitor/capacitor-npm-config.js +2 -1
- package/dist/capacitor/capacitor-npm-config.js.map +1 -1
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +2 -1
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/device.js +2 -2
- package/dist/commands/device.js.map +1 -1
- package/dist/commands/publish/git-phase.js +1 -1
- package/dist/commands/publish/git-phase.js.map +1 -1
- package/dist/commands/replace-deps.js +2 -2
- package/dist/commands/replace-deps.js.map +1 -1
- package/dist/deps/replace-deps/collect-deps.js +2 -2
- package/dist/deps/replace-deps/collect-deps.js.map +1 -1
- package/dist/deps/replace-deps/replace-deps.d.ts.map +1 -1
- package/dist/deps/replace-deps/replace-deps.js +108 -81
- package/dist/deps/replace-deps/replace-deps.js.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +2 -2
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +2 -1
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.d.ts.map +1 -1
- package/dist/engines/BaseEngine.js +2 -2
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
- package/dist/engines/EsbuildClientEngine.js +2 -2
- package/dist/engines/EsbuildClientEngine.js.map +1 -1
- package/dist/engines/NgtscEngine.js +2 -2
- package/dist/engines/NgtscEngine.js.map +1 -1
- package/dist/engines/ServerEsbuildEngine.js +2 -2
- package/dist/engines/ServerEsbuildEngine.js.map +1 -1
- package/dist/engines/TscEngine.js +2 -2
- package/dist/engines/TscEngine.js.map +1 -1
- package/dist/engines/engine-factory.d.ts.map +1 -1
- package/dist/engines/engine-factory.js +2 -2
- package/dist/engines/engine-factory.js.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.js +52 -20
- package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
- package/dist/esbuild/esbuild-config.js +2 -2
- package/dist/esbuild/esbuild-config.js.map +1 -1
- package/dist/esbuild/esbuild-worker-plugin.d.ts +14 -2
- package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-worker-plugin.js +13 -5
- package/dist/esbuild/esbuild-worker-plugin.js.map +1 -1
- package/dist/lint/lint-with-program.js +2 -2
- package/dist/lint/lint-with-program.js.map +1 -1
- package/dist/runtime/lazy-logger.d.ts +14 -0
- package/dist/runtime/lazy-logger.d.ts.map +1 -0
- package/dist/runtime/lazy-logger.js +23 -0
- package/dist/runtime/lazy-logger.js.map +1 -0
- package/dist/sd-cli-entry.js +2 -2
- package/dist/sd-cli-entry.js.map +1 -1
- package/dist/sd-cli.js +2 -2
- package/dist/sd-cli.js.map +1 -1
- package/dist/ts-compiler/SdTsCompiler.d.ts +11 -0
- package/dist/ts-compiler/SdTsCompiler.d.ts.map +1 -1
- package/dist/ts-compiler/SdTsCompiler.js +223 -116
- package/dist/ts-compiler/SdTsCompiler.js.map +1 -1
- package/dist/typecheck/typecheck-non-package.js +2 -2
- package/dist/typecheck/typecheck-non-package.js.map +1 -1
- package/dist/typecheck/typecheck-serialization.d.ts +31 -9
- package/dist/typecheck/typecheck-serialization.d.ts.map +1 -1
- package/dist/typecheck/typecheck-serialization.js +62 -22
- package/dist/typecheck/typecheck-serialization.js.map +1 -1
- package/dist/utils/output-utils.js +2 -2
- package/dist/utils/output-utils.js.map +1 -1
- package/dist/utils/package-classify.js +2 -2
- package/dist/utils/package-classify.js.map +1 -1
- package/dist/utils/package-utils.js +2 -2
- package/dist/utils/package-utils.js.map +1 -1
- package/dist/utils/sd-config.js +2 -2
- package/dist/utils/sd-config.js.map +1 -1
- package/dist/utils/tsconfig.d.ts.map +1 -1
- package/dist/utils/tsconfig.js +3 -5
- package/dist/utils/tsconfig.js.map +1 -1
- package/package.json +8 -8
- package/src/angular/ngtsc-build-core.ts +3 -3
- package/src/angular/vite-angular-plugin.ts +3 -2
- package/src/capacitor/capacitor-android.ts +2 -2
- package/src/capacitor/capacitor-build.ts +2 -1
- package/src/capacitor/capacitor-icon.ts +2 -1
- package/src/capacitor/capacitor-npm-config.ts +2 -1
- package/src/capacitor/capacitor.ts +2 -1
- package/src/commands/device.ts +2 -2
- package/src/commands/publish/git-phase.ts +1 -1
- package/src/commands/replace-deps.ts +2 -2
- package/src/deps/replace-deps/collect-deps.ts +2 -2
- package/src/deps/replace-deps/replace-deps.ts +119 -85
- package/src/deps/server-externals/server-production-files.ts +2 -2
- package/src/electron/electron.ts +2 -1
- package/src/engines/BaseEngine.ts +2 -2
- package/src/engines/EsbuildClientEngine.ts +2 -2
- package/src/engines/NgtscEngine.ts +2 -2
- package/src/engines/ServerEsbuildEngine.ts +2 -2
- package/src/engines/TscEngine.ts +2 -2
- package/src/engines/engine-factory.ts +2 -2
- package/src/esbuild/esbuild-angular-compiler-plugin.ts +66 -21
- package/src/esbuild/esbuild-config.ts +2 -2
- package/src/esbuild/esbuild-worker-plugin.ts +24 -4
- package/src/lint/lint-with-program.ts +2 -2
- package/src/runtime/lazy-logger.ts +23 -0
- package/src/sd-cli-entry.ts +2 -2
- package/src/sd-cli.ts +2 -2
- package/src/ts-compiler/SdTsCompiler.ts +280 -138
- package/src/typecheck/typecheck-non-package.ts +2 -2
- package/src/typecheck/typecheck-serialization.ts +100 -26
- package/src/utils/output-utils.ts +2 -2
- package/src/utils/package-classify.ts +2 -2
- package/src/utils/package-utils.ts +2 -2
- package/src/utils/sd-config.ts +2 -2
- package/src/utils/tsconfig.ts +3 -4
- package/tests/esbuild/esbuild-worker-plugin.spec.ts +245 -0
- package/tests/utils/replace-deps-watch.acc.spec.ts +85 -0
- package/tests/utils/replace-deps-watch.spec.ts +198 -1
- package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.d.ts +0 -3
- package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.d.ts.map +0 -1
- package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.js +0 -9
- package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.js.map +0 -1
|
@@ -2,38 +2,87 @@ import ts from "typescript";
|
|
|
2
2
|
import { fsx } from "@simplysm/core-node";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* DiagnosticMessageChain을 worker 경계로 넘기기 위한 직렬화 구조.
|
|
6
|
+
* (chain 구조를 그대로 보존하여 formatter가 원본 들여쓰기/순서대로 출력하게 함)
|
|
7
|
+
*/
|
|
8
|
+
export interface SerializedMessageChain {
|
|
9
|
+
messageText: string;
|
|
10
|
+
category: number;
|
|
11
|
+
code: number;
|
|
12
|
+
next?: SerializedMessageChain[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Worker로 전달할 수 있는 직렬화된 Diagnostic.
|
|
17
|
+
* ts.Diagnostic의 모든 사용자 가시 필드(detail/relatedInformation/flag)를 보존한다.
|
|
6
18
|
*/
|
|
7
19
|
export interface SerializedDiagnostic {
|
|
8
20
|
category: number;
|
|
9
21
|
code: number;
|
|
10
|
-
messageText
|
|
22
|
+
/** messageText는 chain(overload 에러 등)이거나 단순 문자열. chain이면 구조 그대로 보존 */
|
|
23
|
+
messageText: string | SerializedMessageChain;
|
|
11
24
|
file?: {
|
|
12
25
|
fileName: string;
|
|
13
26
|
};
|
|
14
27
|
start?: number;
|
|
15
28
|
length?: number;
|
|
29
|
+
relatedInformation?: SerializedDiagnosticRelatedInformation[];
|
|
30
|
+
/** true 또는 ts가 넘기는 (빈) 객체. formatter가 인식. */
|
|
31
|
+
reportsUnnecessary?: boolean;
|
|
32
|
+
reportsDeprecated?: boolean;
|
|
33
|
+
source?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** ts.DiagnosticRelatedInformation에 대응. file/start/length와 messageText만 가진 축약 구조. */
|
|
37
|
+
export interface SerializedDiagnosticRelatedInformation {
|
|
38
|
+
category: number;
|
|
39
|
+
code: number;
|
|
40
|
+
messageText: string | SerializedMessageChain;
|
|
41
|
+
file?: { fileName: string };
|
|
42
|
+
start?: number;
|
|
43
|
+
length?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function serializeMessageChain(chain: ts.DiagnosticMessageChain): SerializedMessageChain {
|
|
47
|
+
return {
|
|
48
|
+
messageText: chain.messageText,
|
|
49
|
+
category: chain.category,
|
|
50
|
+
code: chain.code,
|
|
51
|
+
next: chain.next?.map(serializeMessageChain),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function serializeMessageText(
|
|
56
|
+
messageText: string | ts.DiagnosticMessageChain,
|
|
57
|
+
): string | SerializedMessageChain {
|
|
58
|
+
if (typeof messageText === "string") return messageText;
|
|
59
|
+
return serializeMessageChain(messageText);
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
/**
|
|
19
63
|
* Diagnostic을 직렬화 가능한 형태로 변환한다.
|
|
20
64
|
* (Worker 스레드 간 structured clone 통신을 위해 순환 참조/함수를 제거)
|
|
65
|
+
* messageText chain, relatedInformation, reportsUnnecessary/Deprecated, source 등 모든 detail 보존.
|
|
21
66
|
*/
|
|
22
67
|
export function serializeDiagnostic(diagnostic: ts.Diagnostic): SerializedDiagnostic {
|
|
23
|
-
// DiagnosticMessageChain인 경우 모든 컨텍스트 정보를 보존하기 위해 전체 체인을 평탄화
|
|
24
|
-
const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
25
|
-
|
|
26
68
|
return {
|
|
27
69
|
category: diagnostic.category,
|
|
28
70
|
code: diagnostic.code,
|
|
29
|
-
messageText,
|
|
30
|
-
file: diagnostic.file
|
|
31
|
-
? {
|
|
32
|
-
fileName: diagnostic.file.fileName,
|
|
33
|
-
}
|
|
34
|
-
: undefined,
|
|
71
|
+
messageText: serializeMessageText(diagnostic.messageText),
|
|
72
|
+
file: diagnostic.file != null ? { fileName: diagnostic.file.fileName } : undefined,
|
|
35
73
|
start: diagnostic.start,
|
|
36
74
|
length: diagnostic.length,
|
|
75
|
+
relatedInformation: diagnostic.relatedInformation?.map((ri) => ({
|
|
76
|
+
category: ri.category,
|
|
77
|
+
code: ri.code,
|
|
78
|
+
messageText: serializeMessageText(ri.messageText),
|
|
79
|
+
file: ri.file != null ? { fileName: ri.file.fileName } : undefined,
|
|
80
|
+
start: ri.start,
|
|
81
|
+
length: ri.length,
|
|
82
|
+
})),
|
|
83
|
+
reportsUnnecessary: diagnostic.reportsUnnecessary != null ? true : undefined,
|
|
84
|
+
reportsDeprecated: diagnostic.reportsDeprecated != null ? true : undefined,
|
|
85
|
+
source: diagnostic.source,
|
|
37
86
|
};
|
|
38
87
|
}
|
|
39
88
|
|
|
@@ -53,32 +102,57 @@ function getScriptKind(fileName: string): ts.ScriptKind {
|
|
|
53
102
|
* @param fileCache - 파일 내용 캐시 (같은 파일의 중복 읽기 방지)
|
|
54
103
|
* @returns 복원된 ts.Diagnostic 객체
|
|
55
104
|
*/
|
|
105
|
+
function deserializeMessageChain(chain: SerializedMessageChain): ts.DiagnosticMessageChain {
|
|
106
|
+
return {
|
|
107
|
+
messageText: chain.messageText,
|
|
108
|
+
category: chain.category,
|
|
109
|
+
code: chain.code,
|
|
110
|
+
next: chain.next?.map(deserializeMessageChain),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function deserializeMessageText(
|
|
115
|
+
messageText: string | SerializedMessageChain,
|
|
116
|
+
): string | ts.DiagnosticMessageChain {
|
|
117
|
+
if (typeof messageText === "string") return messageText;
|
|
118
|
+
return deserializeMessageChain(messageText);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function loadFile(fileName: string, fileCache: Map<string, string>): ts.SourceFile {
|
|
122
|
+
if (!fileCache.has(fileName)) {
|
|
123
|
+
fileCache.set(fileName, fsx.existsSync(fileName) ? fsx.readSync(fileName) : "");
|
|
124
|
+
}
|
|
125
|
+
const content = fileCache.get(fileName)!;
|
|
126
|
+
const scriptKind = getScriptKind(fileName);
|
|
127
|
+
return ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false, scriptKind);
|
|
128
|
+
}
|
|
129
|
+
|
|
56
130
|
export function deserializeDiagnostic(
|
|
57
131
|
serialized: SerializedDiagnostic,
|
|
58
132
|
fileCache: Map<string, string>,
|
|
59
133
|
): ts.Diagnostic {
|
|
60
|
-
|
|
61
|
-
if (serialized.file != null) {
|
|
62
|
-
const fileName = serialized.file.fileName;
|
|
134
|
+
const file = serialized.file != null ? loadFile(serialized.file.fileName, fileCache) : undefined;
|
|
63
135
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
file = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false, scriptKind);
|
|
74
|
-
}
|
|
136
|
+
const relatedInformation: ts.DiagnosticRelatedInformation[] | undefined =
|
|
137
|
+
serialized.relatedInformation?.map((ri) => ({
|
|
138
|
+
category: ri.category,
|
|
139
|
+
code: ri.code,
|
|
140
|
+
messageText: deserializeMessageText(ri.messageText),
|
|
141
|
+
file: ri.file != null ? loadFile(ri.file.fileName, fileCache) : undefined,
|
|
142
|
+
start: ri.start,
|
|
143
|
+
length: ri.length,
|
|
144
|
+
}));
|
|
75
145
|
|
|
76
146
|
return {
|
|
77
147
|
category: serialized.category,
|
|
78
148
|
code: serialized.code,
|
|
79
|
-
messageText: serialized.messageText,
|
|
149
|
+
messageText: deserializeMessageText(serialized.messageText),
|
|
80
150
|
file,
|
|
81
151
|
start: serialized.start,
|
|
82
152
|
length: serialized.length,
|
|
153
|
+
relatedInformation,
|
|
154
|
+
reportsUnnecessary: serialized.reportsUnnecessary === true ? {} : undefined,
|
|
155
|
+
reportsDeprecated: serialized.reportsDeprecated === true ? {} : undefined,
|
|
156
|
+
source: serialized.source,
|
|
83
157
|
};
|
|
84
158
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { formatMessagesSync, type PartialMessage } from "esbuild";
|
|
2
|
-
import {
|
|
2
|
+
import { createLazyLogger } from "../runtime/lazy-logger";
|
|
3
3
|
import type { BuildResult } from "../runtime/ResultCollector";
|
|
4
4
|
|
|
5
|
-
const logger =
|
|
5
|
+
const logger = createLazyLogger("sd:cli:output");
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* esbuild Message 배열을 포맷된 문자열 배열로 변환한다.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { consola } from "consola";
|
|
3
2
|
import { pathx } from "@simplysm/core-node";
|
|
3
|
+
import { createLazyLogger } from "../runtime/lazy-logger";
|
|
4
4
|
import type {
|
|
5
5
|
BuildTarget,
|
|
6
6
|
SdBuildPackageConfig,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
SdServerPackageConfig,
|
|
11
11
|
} from "../sd-config.types";
|
|
12
12
|
|
|
13
|
-
const logger =
|
|
13
|
+
const logger = createLazyLogger("sd:cli:package-classify");
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* 패키지 config를 순회하며 null 필터링 + target 필터링을 수행한다.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import { consola } from "consola";
|
|
4
3
|
import { SdError } from "@simplysm/core-common";
|
|
5
4
|
import { pathx } from "@simplysm/core-node";
|
|
5
|
+
import { createLazyLogger } from "../runtime/lazy-logger";
|
|
6
6
|
import type {
|
|
7
7
|
SdBuildPackageConfig,
|
|
8
8
|
SdPackageConfig,
|
|
9
9
|
} from "../sd-config.types";
|
|
10
10
|
|
|
11
|
-
const logger =
|
|
11
|
+
const logger = createLazyLogger("sd:cli:package-utils");
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* import.meta.dirname에서 위로 탐색하여 package.json을 찾고 패키지 루트를 반환한다.
|
package/src/utils/sd-config.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createJiti } from "jiti";
|
|
2
2
|
import { SdError } from "@simplysm/core-common";
|
|
3
3
|
import { fsx, pathx } from "@simplysm/core-node";
|
|
4
|
-
import {
|
|
4
|
+
import { createLazyLogger } from "../runtime/lazy-logger";
|
|
5
5
|
import type { SdConfig, SdConfigParams } from "../sd-config.types";
|
|
6
6
|
|
|
7
|
-
const logger =
|
|
7
|
+
const logger = createLazyLogger("sd:cli:sd-config");
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* sd.config.ts를 로드한다.
|
package/src/utils/tsconfig.ts
CHANGED
|
@@ -2,9 +2,9 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import ts from "typescript";
|
|
4
4
|
import { pathx } from "@simplysm/core-node";
|
|
5
|
-
import {
|
|
5
|
+
import { createLazyLogger } from "../runtime/lazy-logger";
|
|
6
6
|
|
|
7
|
-
const logger =
|
|
7
|
+
const logger = createLazyLogger("sd:cli:tsconfig");
|
|
8
8
|
|
|
9
9
|
//#region TypecheckEnv
|
|
10
10
|
|
|
@@ -110,10 +110,9 @@ export function getPackageSourceFiles(
|
|
|
110
110
|
const srcDir = path.join(pkgDir, "src");
|
|
111
111
|
const files = parsedConfig.fileNames.filter((f) => {
|
|
112
112
|
if (pathx.isChildPath(f, srcDir)) return true;
|
|
113
|
-
if (f.endsWith(".fixture.ts")) return true;
|
|
114
113
|
return false;
|
|
115
114
|
});
|
|
116
|
-
logger.debug(`소스 파일 필터링: ${parsedConfig.fileNames.length}개 중 ${files.length}개 (src/
|
|
115
|
+
logger.debug(`소스 파일 필터링: ${parsedConfig.fileNames.length}개 중 ${files.length}개 (src/ only)`);
|
|
117
116
|
return files;
|
|
118
117
|
}
|
|
119
118
|
|
|
@@ -28,6 +28,36 @@ function createMockBuild(overrides?: Partial<esbuild.BuildOptions>): esbuild.Plu
|
|
|
28
28
|
} as unknown as esbuild.PluginBuild;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* transformSync 호출을 추적할 수 있도록 esbuild 네임스페이스를 wrap한다.
|
|
33
|
+
* vi.spyOn이 ESM namespace frozen property에서 실패하므로 대안으로 사용한다.
|
|
34
|
+
*/
|
|
35
|
+
function createTrackedBuild(overrides?: Partial<esbuild.BuildOptions>): {
|
|
36
|
+
build: esbuild.PluginBuild;
|
|
37
|
+
transformSyncCalls: Array<Parameters<typeof esbuild.transformSync>>;
|
|
38
|
+
} {
|
|
39
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "worker-unit-"));
|
|
40
|
+
const transformSyncCalls: Array<Parameters<typeof esbuild.transformSync>> = [];
|
|
41
|
+
const trackedEsbuild = {
|
|
42
|
+
...esbuild,
|
|
43
|
+
transformSync: (...args: Parameters<typeof esbuild.transformSync>) => {
|
|
44
|
+
transformSyncCalls.push(args);
|
|
45
|
+
return esbuild.transformSync(...args);
|
|
46
|
+
},
|
|
47
|
+
} as typeof esbuild;
|
|
48
|
+
|
|
49
|
+
const build = {
|
|
50
|
+
esbuild: trackedEsbuild,
|
|
51
|
+
initialOptions: {
|
|
52
|
+
outdir: tmpDir,
|
|
53
|
+
write: false,
|
|
54
|
+
...overrides,
|
|
55
|
+
},
|
|
56
|
+
} as unknown as esbuild.PluginBuild;
|
|
57
|
+
|
|
58
|
+
return { build, transformSyncCalls };
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
describe("transformWorkerPatterns — 패턴 감지", () => {
|
|
32
62
|
it("Worker 패턴이 없는 content에 대해 undefined를 반환한다", () => {
|
|
33
63
|
const result = transformWorkerPatterns(
|
|
@@ -446,6 +476,14 @@ const x = 1;`,
|
|
|
446
476
|
|
|
447
477
|
expect(matches).toHaveLength(0);
|
|
448
478
|
});
|
|
479
|
+
|
|
480
|
+
it("래퍼 함수를 통한 Worker 생성은 탐지하지 않는다", () => {
|
|
481
|
+
const matches = findWorkerPatterns(
|
|
482
|
+
`const w = createBrowserWorker(new URL("./worker.js", import.meta.url), { type: "module" });`,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(matches).toHaveLength(0);
|
|
486
|
+
});
|
|
449
487
|
});
|
|
450
488
|
|
|
451
489
|
describe("transformWorkerPatterns — TypeScript 파일 처리", () => {
|
|
@@ -604,3 +642,210 @@ const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
|
604
642
|
expect(result.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
605
643
|
});
|
|
606
644
|
});
|
|
645
|
+
|
|
646
|
+
describe("transformWorkerPatterns — skipTsTransform 옵션", () => {
|
|
647
|
+
it("skipTsTransform: true + .ts 경로 + JS content → transformSync 호출 없이 정상 치환", () => {
|
|
648
|
+
const entryPath = path.join(fixturesDir, "entry.ts");
|
|
649
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
650
|
+
|
|
651
|
+
// ngtsc emit 결과를 흉내: 파일 경로는 .ts이지만 content는 이미 JS
|
|
652
|
+
const result = transformWorkerPatterns(
|
|
653
|
+
`const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
654
|
+
entryPath,
|
|
655
|
+
build,
|
|
656
|
+
{ skipTsTransform: true },
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
660
|
+
expect(result).toBeDefined();
|
|
661
|
+
expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
662
|
+
expect(result!.errors).toHaveLength(0);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("skipTsTransform: true + .ts 경로 + 실제 TS 구문 → 계약 위반으로 undefined (조용한 누락)", () => {
|
|
666
|
+
const entryPath = path.join(fixturesDir, "entry.ts");
|
|
667
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
668
|
+
|
|
669
|
+
// 호출자가 계약을 위반하여 실제 TS 구문을 넘긴 경우:
|
|
670
|
+
// transformSync가 스킵되므로 acorn이 TS 구문 파싱 실패 → 빈 matches → undefined
|
|
671
|
+
const result = transformWorkerPatterns(
|
|
672
|
+
`import type { T } from "pkg";
|
|
673
|
+
const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
674
|
+
entryPath,
|
|
675
|
+
build,
|
|
676
|
+
{ skipTsTransform: true },
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
680
|
+
expect(result).toBeUndefined();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("옵션 생략 + .ts + import type + Worker → transformSync 호출 후 정상 감지 (후방 호환)", () => {
|
|
684
|
+
const entryPath = path.join(fixturesDir, "entry.ts");
|
|
685
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
686
|
+
|
|
687
|
+
const result = transformWorkerPatterns(
|
|
688
|
+
`import type { T } from "pkg";
|
|
689
|
+
const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
690
|
+
entryPath,
|
|
691
|
+
build,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
expect(transformSyncCalls).toHaveLength(1);
|
|
695
|
+
expect(transformSyncCalls[0][1]).toMatchObject({ loader: "ts" });
|
|
696
|
+
expect(result).toBeDefined();
|
|
697
|
+
expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("옵션 { skipTsTransform: false } → 기본 동작 (transformSync 호출)", () => {
|
|
701
|
+
const entryPath = path.join(fixturesDir, "entry.ts");
|
|
702
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
703
|
+
|
|
704
|
+
const result = transformWorkerPatterns(
|
|
705
|
+
`import type { T } from "pkg";
|
|
706
|
+
const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
707
|
+
entryPath,
|
|
708
|
+
build,
|
|
709
|
+
{ skipTsTransform: false },
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
expect(transformSyncCalls).toHaveLength(1);
|
|
713
|
+
expect(result).toBeDefined();
|
|
714
|
+
expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("빈 객체 {} → skipTsTransform undefined → 기본 동작 (transformSync 호출)", () => {
|
|
718
|
+
const entryPath = path.join(fixturesDir, "entry.ts");
|
|
719
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
720
|
+
|
|
721
|
+
const result = transformWorkerPatterns(
|
|
722
|
+
`import type { T } from "pkg";
|
|
723
|
+
const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
724
|
+
entryPath,
|
|
725
|
+
build,
|
|
726
|
+
{},
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
expect(transformSyncCalls).toHaveLength(1);
|
|
730
|
+
expect(result).toBeDefined();
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("skipTsTransform: true + .js 경로 → 동작 동일 (확장자 분기 진입 안 함)", () => {
|
|
734
|
+
const entryPath = path.join(fixturesDir, "entry.js");
|
|
735
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
736
|
+
|
|
737
|
+
const result = transformWorkerPatterns(
|
|
738
|
+
`const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
739
|
+
entryPath,
|
|
740
|
+
build,
|
|
741
|
+
{ skipTsTransform: true },
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
745
|
+
expect(result).toBeDefined();
|
|
746
|
+
expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe("transformWorkerPatterns — 사전 필터 정규식 경계", () => {
|
|
751
|
+
it("타입 어노테이션만 등장한 Worker 키워드 → 사전 필터 차단", () => {
|
|
752
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
753
|
+
|
|
754
|
+
const result = transformWorkerPatterns(
|
|
755
|
+
`const x: Worker = 1 as any;`,
|
|
756
|
+
path.join(fixturesDir, "entry.ts"),
|
|
757
|
+
build,
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
761
|
+
expect(result).toBeUndefined();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("interface 선언에만 등장한 Worker → 사전 필터 차단", () => {
|
|
765
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
766
|
+
|
|
767
|
+
const result = transformWorkerPatterns(
|
|
768
|
+
`interface WorkerLike { run(): void; }`,
|
|
769
|
+
path.join(fixturesDir, "entry.ts"),
|
|
770
|
+
build,
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
774
|
+
expect(result).toBeUndefined();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it("import type에만 등장한 Worker → 사전 필터 차단", () => {
|
|
778
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
779
|
+
|
|
780
|
+
const result = transformWorkerPatterns(
|
|
781
|
+
`import type { Worker } from "./types";`,
|
|
782
|
+
path.join(fixturesDir, "entry.ts"),
|
|
783
|
+
build,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
787
|
+
expect(result).toBeUndefined();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("new Worker 호출 → 사전 필터 통과, AST 감지", () => {
|
|
791
|
+
const entryPath = path.join(fixturesDir, "entry.js");
|
|
792
|
+
const result = transformWorkerPatterns(
|
|
793
|
+
`const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
794
|
+
entryPath,
|
|
795
|
+
createMockBuild(),
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
expect(result).toBeDefined();
|
|
799
|
+
expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it("new SharedWorker 호출 → 사전 필터 통과, AST 감지", () => {
|
|
803
|
+
const entryPath = path.join(fixturesDir, "entry.js");
|
|
804
|
+
const result = transformWorkerPatterns(
|
|
805
|
+
`const sw = new SharedWorker(new URL("./shared-worker.js", import.meta.url));`,
|
|
806
|
+
entryPath,
|
|
807
|
+
createMockBuild(),
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
expect(result).toBeDefined();
|
|
811
|
+
expect(result!.contents).toMatch(/worker-[a-z0-9]+\.js/i);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it("import.meta.resolve 호출 → 사전 필터 통과, AST 감지", () => {
|
|
815
|
+
const entryPath = path.join(fixturesDir, "entry.js");
|
|
816
|
+
const result = transformWorkerPatterns(
|
|
817
|
+
`const p = import.meta.resolve("./node-worker.js");`,
|
|
818
|
+
entryPath,
|
|
819
|
+
createMockBuild({ platform: "node" }),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
expect(result).toBeDefined();
|
|
823
|
+
expect(result!.contents).toMatch(
|
|
824
|
+
/new URL\("worker-[a-z0-9]+\.js", import\.meta\.url\)\.href/i,
|
|
825
|
+
);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it("new Worker (연속 공백) → 사전 필터 통과", () => {
|
|
829
|
+
const entryPath = path.join(fixturesDir, "entry.js");
|
|
830
|
+
const result = transformWorkerPatterns(
|
|
831
|
+
`const w = new Worker(new URL("./worker.js", import.meta.url));`,
|
|
832
|
+
entryPath,
|
|
833
|
+
createMockBuild(),
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
expect(result).toBeDefined();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("WorkerSubClass 식별자 (단어 경계 위반) → 사전 필터 차단", () => {
|
|
840
|
+
const { build, transformSyncCalls } = createTrackedBuild();
|
|
841
|
+
|
|
842
|
+
const result = transformWorkerPatterns(
|
|
843
|
+
`class WorkerSubClass {}\nconst x = new WorkerSubClass();`,
|
|
844
|
+
path.join(fixturesDir, "entry.ts"),
|
|
845
|
+
build,
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
expect(transformSyncCalls).toHaveLength(0);
|
|
849
|
+
expect(result).toBeUndefined();
|
|
850
|
+
});
|
|
851
|
+
});
|
|
@@ -134,4 +134,89 @@ describe("watchReplaceDeps onChanged 콜백", () => {
|
|
|
134
134
|
// 콜백 호출 대기
|
|
135
135
|
await changedPromise;
|
|
136
136
|
}, 10_000);
|
|
137
|
+
|
|
138
|
+
it("동일 source를 참조하는 복수 entry가 있으면 모든 target에 복사된다", async () => {
|
|
139
|
+
// 추가 target 디렉토리 생성 (@test/pkgB)
|
|
140
|
+
const targetDirB = path.join(tmpDir, "project", "node_modules", "@test", "pkgB", "src");
|
|
141
|
+
await fs.promises.mkdir(targetDirB, { recursive: true });
|
|
142
|
+
await fs.promises.writeFile(path.join(targetDirB, "index.ts"), "export const a = 1;");
|
|
143
|
+
|
|
144
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
145
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
146
|
+
|
|
147
|
+
// 두 패턴이 동일 sourcePath를 참조
|
|
148
|
+
const replaceDeps = {
|
|
149
|
+
"@test/pkg": sourcePath,
|
|
150
|
+
"@test/pkgB": sourcePath,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let callCount = 0;
|
|
154
|
+
watchResult = await watchReplaceDeps(projectRoot, replaceDeps, {
|
|
155
|
+
onChanged: () => {
|
|
156
|
+
callCount++;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// 소스 파일 변경
|
|
161
|
+
await fs.promises.writeFile(
|
|
162
|
+
path.join(sourcePath, "src", "index.ts"),
|
|
163
|
+
"export const a = 9;",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// 배칭 + 복사 완료 대기
|
|
167
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
168
|
+
|
|
169
|
+
// 두 target 모두 업데이트
|
|
170
|
+
const targetAContent = await fs.promises.readFile(
|
|
171
|
+
path.join(tmpDir, "project", "node_modules", "@test", "pkg", "src", "index.ts"),
|
|
172
|
+
"utf-8",
|
|
173
|
+
);
|
|
174
|
+
const targetBContent = await fs.promises.readFile(
|
|
175
|
+
path.join(targetDirB, "index.ts"),
|
|
176
|
+
"utf-8",
|
|
177
|
+
);
|
|
178
|
+
expect(targetAContent).toBe("export const a = 9;");
|
|
179
|
+
expect(targetBContent).toBe("export const a = 9;");
|
|
180
|
+
|
|
181
|
+
// 동일 source의 중복 watchPath는 하나로 통합되므로 이벤트가 1회만 배칭됨 → onChanged 1회
|
|
182
|
+
expect(callCount).toBe(1);
|
|
183
|
+
}, 10_000);
|
|
184
|
+
|
|
185
|
+
it("소스 파일이 삭제되면 대상 파일도 삭제된다", async () => {
|
|
186
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
187
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
188
|
+
const replaceDeps = { "@test/pkg": sourcePath };
|
|
189
|
+
|
|
190
|
+
let resolveChanged: () => void;
|
|
191
|
+
const changedPromise = new Promise<void>((resolve) => {
|
|
192
|
+
resolveChanged = resolve;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
watchResult = await watchReplaceDeps(projectRoot, replaceDeps, {
|
|
196
|
+
onChanged: () => {
|
|
197
|
+
resolveChanged();
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const destPath = path.join(
|
|
202
|
+
tmpDir, "project", "node_modules", "@test", "pkg", "src", "index.ts",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// 삭제 전 target 파일 존재 확인
|
|
206
|
+
await expect(fs.promises.access(destPath)).resolves.toBeUndefined();
|
|
207
|
+
|
|
208
|
+
// 소스 삭제
|
|
209
|
+
await fs.promises.unlink(path.join(sourcePath, "src", "index.ts"));
|
|
210
|
+
|
|
211
|
+
await changedPromise;
|
|
212
|
+
|
|
213
|
+
// 삭제 반영까지 여유 대기
|
|
214
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
215
|
+
|
|
216
|
+
// target도 삭제됨
|
|
217
|
+
const targetExists = await fs.promises
|
|
218
|
+
.access(destPath)
|
|
219
|
+
.then(() => true, () => false);
|
|
220
|
+
expect(targetExists).toBe(false);
|
|
221
|
+
}, 10_000);
|
|
137
222
|
});
|