@simplysm/sd-cli 14.0.47 → 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/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 +46 -18
- 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/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 +4 -4
- 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/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 +60 -19
- package/src/esbuild/esbuild-config.ts +2 -2
- 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 +8 -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
|
|
|
@@ -476,6 +476,14 @@ const x = 1;`,
|
|
|
476
476
|
|
|
477
477
|
expect(matches).toHaveLength(0);
|
|
478
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
|
+
});
|
|
479
487
|
});
|
|
480
488
|
|
|
481
489
|
describe("transformWorkerPatterns — TypeScript 파일 처리", () => {
|
|
@@ -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
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import os from "os";
|
|
5
|
+
import { consola } from "consola";
|
|
5
6
|
import { watchReplaceDeps } from "../../src/deps/replace-deps/replace-deps";
|
|
6
7
|
import type { WatchReplaceDepResult } from "../../src/deps/replace-deps/replace-deps";
|
|
7
8
|
|
|
@@ -12,10 +13,28 @@ import type { WatchReplaceDepResult } from "../../src/deps/replace-deps/replace-
|
|
|
12
13
|
describe("watchReplaceDeps onChanged", () => {
|
|
13
14
|
let tmpDir: string;
|
|
14
15
|
let watchResult: WatchReplaceDepResult | undefined;
|
|
16
|
+
let mockLogger: {
|
|
17
|
+
warn: ReturnType<typeof vi.fn>;
|
|
18
|
+
error: ReturnType<typeof vi.fn>;
|
|
19
|
+
info: ReturnType<typeof vi.fn>;
|
|
20
|
+
start: ReturnType<typeof vi.fn>;
|
|
21
|
+
success: ReturnType<typeof vi.fn>;
|
|
22
|
+
debug: ReturnType<typeof vi.fn>;
|
|
23
|
+
};
|
|
15
24
|
|
|
16
25
|
beforeEach(async () => {
|
|
17
26
|
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "sd-replace-deps-unit-"));
|
|
18
27
|
|
|
28
|
+
mockLogger = {
|
|
29
|
+
warn: vi.fn(),
|
|
30
|
+
error: vi.fn(),
|
|
31
|
+
info: vi.fn(),
|
|
32
|
+
start: vi.fn(),
|
|
33
|
+
success: vi.fn(),
|
|
34
|
+
debug: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
vi.spyOn(consola, "withTag").mockReturnValue(mockLogger as any);
|
|
37
|
+
|
|
19
38
|
// 소스 패키지 생성
|
|
20
39
|
const sourcePkg = path.join(tmpDir, "source-pkg");
|
|
21
40
|
await fs.promises.mkdir(sourcePkg, { recursive: true });
|
|
@@ -43,6 +62,7 @@ describe("watchReplaceDeps onChanged", () => {
|
|
|
43
62
|
watchResult?.dispose();
|
|
44
63
|
watchResult = undefined;
|
|
45
64
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
65
|
+
vi.restoreAllMocks();
|
|
46
66
|
});
|
|
47
67
|
|
|
48
68
|
it("복수 파일 변경 시 300ms 배칭 후 onChanged가 한 번 호출된다", async () => {
|
|
@@ -131,6 +151,58 @@ describe("watchReplaceDeps onChanged", () => {
|
|
|
131
151
|
await changedPromise;
|
|
132
152
|
}, 10_000);
|
|
133
153
|
|
|
154
|
+
it("nested source 경로는 longest-prefix로 소속이 결정된다", async () => {
|
|
155
|
+
// sourceOuter: tmpDir/outer-pkg
|
|
156
|
+
// sourceInner: tmpDir/outer-pkg/inner-pkg (outer의 하위)
|
|
157
|
+
const outerPkg = path.join(tmpDir, "outer-pkg");
|
|
158
|
+
await fs.promises.mkdir(path.join(outerPkg, "src"), { recursive: true });
|
|
159
|
+
await fs.promises.writeFile(
|
|
160
|
+
path.join(outerPkg, "package.json"),
|
|
161
|
+
JSON.stringify({ name: "@test/outer", files: ["src"] }),
|
|
162
|
+
);
|
|
163
|
+
await fs.promises.writeFile(path.join(outerPkg, "src", "o.ts"), "o");
|
|
164
|
+
|
|
165
|
+
const innerPkg = path.join(outerPkg, "inner-pkg");
|
|
166
|
+
await fs.promises.mkdir(path.join(innerPkg, "src"), { recursive: true });
|
|
167
|
+
await fs.promises.writeFile(
|
|
168
|
+
path.join(innerPkg, "package.json"),
|
|
169
|
+
JSON.stringify({ name: "@test/inner", files: ["src"] }),
|
|
170
|
+
);
|
|
171
|
+
await fs.promises.writeFile(path.join(innerPkg, "src", "i.ts"), "i");
|
|
172
|
+
|
|
173
|
+
// target들
|
|
174
|
+
const outerTarget = path.join(
|
|
175
|
+
tmpDir, "project", "node_modules", "@test", "outer", "src",
|
|
176
|
+
);
|
|
177
|
+
const innerTarget = path.join(
|
|
178
|
+
tmpDir, "project", "node_modules", "@test", "inner", "src",
|
|
179
|
+
);
|
|
180
|
+
await fs.promises.mkdir(outerTarget, { recursive: true });
|
|
181
|
+
await fs.promises.mkdir(innerTarget, { recursive: true });
|
|
182
|
+
await fs.promises.writeFile(path.join(outerTarget, "o.ts"), "o");
|
|
183
|
+
await fs.promises.writeFile(path.join(innerTarget, "i.ts"), "i");
|
|
184
|
+
|
|
185
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
186
|
+
|
|
187
|
+
watchResult = await watchReplaceDeps(
|
|
188
|
+
projectRoot,
|
|
189
|
+
{ "@test/outer": outerPkg, "@test/inner": innerPkg },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// inner 파일 변경 — longest-prefix로 inner source에만 매칭되어야 함
|
|
193
|
+
await fs.promises.writeFile(path.join(innerPkg, "src", "i.ts"), "i2");
|
|
194
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
195
|
+
|
|
196
|
+
const innerCopied = await fs.promises.readFile(path.join(innerTarget, "i.ts"), "utf-8");
|
|
197
|
+
expect(innerCopied).toBe("i2");
|
|
198
|
+
|
|
199
|
+
// outer target에는 i.ts가 복사되지 않아야 함 (잘못된 매칭 방지)
|
|
200
|
+
const outerHasI = await fs.promises
|
|
201
|
+
.access(path.join(outerTarget, "inner-pkg", "src", "i.ts"))
|
|
202
|
+
.then(() => true, () => false);
|
|
203
|
+
expect(outerHasI).toBe(false);
|
|
204
|
+
}, 10_000);
|
|
205
|
+
|
|
134
206
|
it("options 파라미터가 undefined일 때 에러가 발생하지 않는다", async () => {
|
|
135
207
|
const projectRoot = path.join(tmpDir, "project");
|
|
136
208
|
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
@@ -144,4 +216,129 @@ describe("watchReplaceDeps onChanged", () => {
|
|
|
144
216
|
await new Promise((r) => setTimeout(r, 1500));
|
|
145
217
|
// 에러 없이 완료됨
|
|
146
218
|
}, 10_000);
|
|
219
|
+
|
|
220
|
+
it("source의 files 필드가 없으면 경고를 출력하고 감시에서 제외한다", async () => {
|
|
221
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
222
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
223
|
+
|
|
224
|
+
// files 필드 제거
|
|
225
|
+
await fs.promises.writeFile(
|
|
226
|
+
path.join(sourcePath, "package.json"),
|
|
227
|
+
JSON.stringify({ name: "@test/pkg" }),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
let callCount = 0;
|
|
231
|
+
watchResult = await watchReplaceDeps(projectRoot, { "@test/pkg": sourcePath }, {
|
|
232
|
+
onChanged: () => {
|
|
233
|
+
callCount++;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
238
|
+
expect.stringContaining("package.json에 files 필드가 없어 감시 건너뜀"),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// 파일을 변경해도 감지되지 않음
|
|
242
|
+
await fs.promises.writeFile(
|
|
243
|
+
path.join(sourcePath, "src", "index.ts"),
|
|
244
|
+
"export const v = 2;",
|
|
245
|
+
);
|
|
246
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
247
|
+
|
|
248
|
+
expect(callCount).toBe(0);
|
|
249
|
+
}, 10_000);
|
|
250
|
+
|
|
251
|
+
it("동일 내용 재저장 시 isFileContentSame 스킵으로 onChanged가 호출되지 않는다", async () => {
|
|
252
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
253
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
254
|
+
|
|
255
|
+
// beforeEach에서 source와 target 모두 "export const v = 1;"로 설정된 상태
|
|
256
|
+
let callCount = 0;
|
|
257
|
+
watchResult = await watchReplaceDeps(projectRoot, { "@test/pkg": sourcePath }, {
|
|
258
|
+
onChanged: () => {
|
|
259
|
+
callCount++;
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 동일 내용으로 재저장 (mtime은 변경되지만 내용은 같음)
|
|
264
|
+
await fs.promises.writeFile(
|
|
265
|
+
path.join(sourcePath, "src", "index.ts"),
|
|
266
|
+
"export const v = 1;",
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
270
|
+
|
|
271
|
+
// isFileContentSame이 true를 반환하여 복사 스킵 → hasActualCopy=false → onChanged 미호출
|
|
272
|
+
expect(callCount).toBe(0);
|
|
273
|
+
}, 10_000);
|
|
274
|
+
|
|
275
|
+
it("dispose 이후의 파일 변경은 감지되지 않는다", async () => {
|
|
276
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
277
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
278
|
+
|
|
279
|
+
let callCount = 0;
|
|
280
|
+
const result = await watchReplaceDeps(projectRoot, { "@test/pkg": sourcePath }, {
|
|
281
|
+
onChanged: () => {
|
|
282
|
+
callCount++;
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
result.dispose();
|
|
287
|
+
// afterEach에서 중복 dispose 방지
|
|
288
|
+
watchResult = undefined;
|
|
289
|
+
|
|
290
|
+
// close 완료 대기
|
|
291
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
292
|
+
|
|
293
|
+
await fs.promises.writeFile(
|
|
294
|
+
path.join(sourcePath, "src", "index.ts"),
|
|
295
|
+
"export const v = 99;",
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
299
|
+
|
|
300
|
+
expect(callCount).toBe(0);
|
|
301
|
+
}, 10_000);
|
|
302
|
+
|
|
303
|
+
it("모든 source의 files 필드가 없으면 감시 대상이 없음 경고를 출력한다", async () => {
|
|
304
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
305
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
306
|
+
|
|
307
|
+
// files 필드 제거
|
|
308
|
+
await fs.promises.writeFile(
|
|
309
|
+
path.join(sourcePath, "package.json"),
|
|
310
|
+
JSON.stringify({ name: "@test/pkg" }),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
watchResult = await watchReplaceDeps(projectRoot, { "@test/pkg": sourcePath });
|
|
314
|
+
|
|
315
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
316
|
+
expect.stringContaining("감시 대상이 없어 워치가 시작되지 않음"),
|
|
317
|
+
);
|
|
318
|
+
}, 10_000);
|
|
319
|
+
|
|
320
|
+
it("files 필드가 없으면 해당 source에 대한 readdir이 호출되지 않는다", async () => {
|
|
321
|
+
const projectRoot = path.join(tmpDir, "project");
|
|
322
|
+
const sourcePath = path.join(tmpDir, "source-pkg");
|
|
323
|
+
|
|
324
|
+
// files 필드 제거
|
|
325
|
+
await fs.promises.writeFile(
|
|
326
|
+
path.join(sourcePath, "package.json"),
|
|
327
|
+
JSON.stringify({ name: "@test/pkg" }),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const readdirSpy = vi.spyOn(fs.promises, "readdir");
|
|
331
|
+
|
|
332
|
+
watchResult = await watchReplaceDeps(projectRoot, { "@test/pkg": sourcePath });
|
|
333
|
+
|
|
334
|
+
// sourcePath에 대한 readdir 호출이 없어야 함 (files null이면 생략되어야 함)
|
|
335
|
+
const sourcePathPosix = path.resolve(sourcePath).replace(/\\/g, "/");
|
|
336
|
+
const calledWithSource = readdirSpy.mock.calls.some((args) => {
|
|
337
|
+
const arg = args[0];
|
|
338
|
+
if (typeof arg !== "string") return false;
|
|
339
|
+
return arg.replace(/\\/g, "/") === sourcePathPosix;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(calledWithSource).toBe(false);
|
|
343
|
+
}, 10_000);
|
|
147
344
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test.fixture.d.ts","sourceRoot":"","sources":["../../../../../../../../tests/angular/fixtures/packages/basic-app/tests/test.fixture.ts"],"names":[],"mappings":"AAEA,qBAKa,gBAAgB;CAAG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test.fixture.js","sourceRoot":"","sources":["../../../../../../../../tests/angular/fixtures/packages/basic-app/tests/test.fixture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,CAAC,SAAS,CAAC;IACT,QAAQ,EAAE,aAAa;IACvB,UAAU,EAAE,IAAI;IAChB,QAAQ,EAAE,sBAAsB;CACjC,CAAC;AACF,MAAM,OAAO,gBAAgB;CAAG"}
|