@simplysm/sd-cli 14.0.16 → 14.0.17
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 +2 -1
- package/dist/angular/client-transform-stylesheet.d.ts +2 -0
- package/dist/angular/client-transform-stylesheet.d.ts.map +1 -1
- package/dist/angular/client-transform-stylesheet.js +88 -2
- package/dist/angular/client-transform-stylesheet.js.map +1 -1
- package/dist/angular/vite-angular-plugin.d.ts +7 -0
- package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
- package/dist/angular/vite-angular-plugin.js +78 -16
- package/dist/angular/vite-angular-plugin.js.map +1 -1
- package/dist/capacitor/capacitor.d.ts.map +1 -1
- package/dist/capacitor/capacitor.js +9 -13
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +8 -9
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/device.d.ts.map +1 -1
- package/dist/commands/device.js +33 -1
- package/dist/commands/device.js.map +1 -1
- package/dist/commands/lint.d.ts +0 -1
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +2 -3
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/publish.js +2 -2
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/typecheck.d.ts.map +1 -1
- package/dist/commands/typecheck.js +0 -1
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/electron/electron.d.ts +3 -2
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +54 -31
- package/dist/electron/electron.js.map +1 -1
- package/dist/engines/BaseEngine.js +1 -1
- package/dist/engines/BaseEngine.js.map +1 -1
- package/dist/engines/NgtscEngine.d.ts.map +1 -1
- package/dist/engines/NgtscEngine.js +0 -1
- package/dist/engines/NgtscEngine.js.map +1 -1
- package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
- package/dist/engines/ServerEsbuildEngine.js +0 -1
- package/dist/engines/ServerEsbuildEngine.js.map +1 -1
- package/dist/engines/TscEngine.d.ts.map +1 -1
- package/dist/engines/TscEngine.js +0 -1
- package/dist/engines/TscEngine.js.map +1 -1
- package/dist/engines/ViteEngine.d.ts.map +1 -1
- package/dist/engines/ViteEngine.js +8 -1
- package/dist/engines/ViteEngine.js.map +1 -1
- package/dist/engines/index.d.ts +0 -10
- package/dist/engines/index.d.ts.map +1 -1
- package/dist/engines/index.js +0 -5
- package/dist/engines/index.js.map +1 -1
- package/dist/engines/types.d.ts +0 -1
- package/dist/engines/types.d.ts.map +1 -1
- package/dist/infra/SignalHandler.d.ts +1 -6
- package/dist/infra/SignalHandler.d.ts.map +1 -1
- package/dist/infra/SignalHandler.js +4 -13
- package/dist/infra/SignalHandler.js.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/BuildOrchestrator.js +7 -12
- package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
- package/dist/orchestrators/DevWatchOrchestrator.js +17 -10
- package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
- package/dist/sd-cli-entry.d.ts +0 -1
- package/dist/sd-cli-entry.d.ts.map +1 -1
- package/dist/sd-cli-entry.js +7 -8
- package/dist/sd-cli-entry.js.map +1 -1
- package/dist/sd-config.types.d.ts +11 -1
- package/dist/sd-config.types.d.ts.map +1 -1
- package/dist/utils/angular-compiler.d.ts.map +1 -1
- package/dist/utils/angular-compiler.js +20 -13
- package/dist/utils/angular-compiler.js.map +1 -1
- package/dist/utils/esbuild-config.d.ts +1 -1
- package/dist/utils/esbuild-config.d.ts.map +1 -1
- package/dist/utils/esbuild-config.js +1 -4
- package/dist/utils/esbuild-config.js.map +1 -1
- package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
- package/dist/utils/ngtsc-build-core.js +3 -0
- package/dist/utils/ngtsc-build-core.js.map +1 -1
- package/dist/utils/tsc-build.d.ts +5 -0
- package/dist/utils/tsc-build.d.ts.map +1 -1
- package/dist/utils/tsc-build.js +2 -1
- package/dist/utils/tsc-build.js.map +1 -1
- package/dist/utils/vite-config.d.ts +1 -1
- package/dist/utils/vite-config.d.ts.map +1 -1
- package/dist/utils/vite-config.js +22 -53
- package/dist/utils/vite-config.js.map +1 -1
- package/dist/utils/vite-pwa-plugin.d.ts +9 -0
- package/dist/utils/vite-pwa-plugin.d.ts.map +1 -0
- package/dist/utils/vite-pwa-plugin.js +139 -0
- package/dist/utils/vite-pwa-plugin.js.map +1 -0
- package/dist/utils/worker-utils.d.ts +2 -5
- package/dist/utils/worker-utils.d.ts.map +1 -1
- package/dist/utils/worker-utils.js +5 -11
- package/dist/utils/worker-utils.js.map +1 -1
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +9 -3
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/library-build.worker.d.ts.map +1 -1
- package/dist/workers/library-build.worker.js +6 -2
- package/dist/workers/library-build.worker.js.map +1 -1
- package/dist/workers/ngtsc-build.worker.js +2 -2
- package/dist/workers/ngtsc-build.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +6 -2
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-runtime.worker.js +4 -4
- package/dist/workers/server-runtime.worker.js.map +1 -1
- package/docs/config.md +26 -0
- package/docs/pwa-configuration-types.md +1 -1
- package/package.json +8 -10
- package/src/angular/client-transform-stylesheet.ts +104 -2
- package/src/angular/vite-angular-plugin.ts +92 -31
- package/src/capacitor/capacitor.ts +10 -26
- package/src/commands/check.ts +8 -11
- package/src/commands/device.ts +38 -3
- package/src/commands/lint.ts +2 -3
- package/src/commands/publish.ts +2 -2
- package/src/commands/typecheck.ts +0 -1
- package/src/electron/electron.ts +62 -43
- package/src/engines/BaseEngine.ts +1 -1
- package/src/engines/NgtscEngine.ts +0 -1
- package/src/engines/ServerEsbuildEngine.ts +0 -1
- package/src/engines/TscEngine.ts +0 -1
- package/src/engines/ViteEngine.ts +7 -1
- package/src/engines/index.ts +0 -10
- package/src/engines/types.ts +0 -1
- package/src/infra/SignalHandler.ts +4 -14
- package/src/orchestrators/BuildOrchestrator.ts +7 -9
- package/src/orchestrators/DevWatchOrchestrator.ts +21 -9
- package/src/sd-cli-entry.ts +11 -16
- package/src/sd-config.types.ts +12 -1
- package/src/utils/angular-compiler.ts +21 -21
- package/src/utils/esbuild-config.ts +2 -5
- package/src/utils/ngtsc-build-core.ts +7 -0
- package/src/utils/tsc-build.ts +7 -0
- package/src/utils/vite-config.ts +23 -55
- package/src/utils/vite-pwa-plugin.ts +168 -0
- package/src/utils/worker-utils.ts +5 -11
- package/src/workers/client.worker.ts +11 -3
- package/src/workers/library-build.worker.ts +6 -2
- package/src/workers/ngtsc-build.worker.ts +2 -2
- package/src/workers/server-build.worker.ts +7 -2
- package/src/workers/server-runtime.worker.ts +4 -4
- package/tests/angular/client-transform-stylesheet.spec.ts +43 -0
- package/tests/angular/find-affected-by-scss.spec.ts +37 -0
- package/tests/angular/fixtures/basic-app/scss/_colors.scss +1 -0
- package/tests/angular/fixtures/basic-app/scss/_variables.scss +3 -0
- package/tests/angular/fixtures/basic-app/src/styled.component.ts +14 -0
- package/tests/angular/linker-disk-cache.spec.ts +158 -0
- package/tests/angular/scss-disk-cache.spec.ts +162 -0
- package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +15 -15
- package/tests/angular/vite-angular-plugin-hmr.spec.ts +9 -9
- package/tests/angular/vite-angular-plugin-lint.spec.ts +4 -4
- package/tests/angular/vite-angular-plugin-scss-hmr.spec.ts +87 -0
- package/tests/angular/vite-angular-plugin.spec.ts +15 -15
- package/tests/capacitor/capacitor-icon.spec.ts +2 -4
- package/tests/capacitor/capacitor-init.spec.ts +2 -4
- package/tests/capacitor/capacitor-workspace.spec.ts +2 -4
- package/tests/commands/device.spec.ts +100 -0
- package/tests/electron/electron.spec.ts +24 -17
- package/tests/engines/ngtsc-engine.spec.ts +0 -3
- package/tests/engines/server-esbuild-engine.spec.ts +0 -3
- package/tests/engines/tsc-engine.spec.ts +1 -2
- package/tests/engines/vite-engine.spec.ts +0 -2
- package/tests/infra/signal-handler.spec.ts +1 -12
- package/tests/orchestrators/build-orchestrator.spec.ts +0 -6
- package/tests/orchestrators/dev-watch-orchestrator.spec.ts +24 -66
- package/tests/utils/angular-compiler.spec.ts +1396 -32
- package/tests/utils/esbuild-config.spec.ts +4 -7
- package/tests/utils/{ngtsc-build-core-angular-compiler.spec.ts → ngtsc-build-core.spec.ts} +142 -11
- package/tests/utils/tsc-build.spec.ts +4 -1
- package/tests/utils/vite-config.spec.ts +130 -261
- package/tests/utils/vite-pwa-plugin.acc.spec.ts +143 -0
- package/tests/utils/vite-pwa-plugin.spec.ts +350 -0
- package/tests/utils/worker-utils.spec.ts +8 -7
- package/tests/workers/client-worker.spec.ts +50 -1
- package/tests/workers/dev-port-file.verify.md +6 -0
- package/tests/workers/library-build-lint.spec.ts +1 -1
- package/tests/workers/library-build-worker.spec.ts +1 -1
- package/tests/workers/ngtsc-build-lint.spec.ts +1 -1
- package/tests/workers/server-build-lint.spec.ts +1 -1
- package/tests/workers/server-build-worker.spec.ts +1 -1
- package/tests/workers/server-runtime-worker.spec.ts +8 -1
- package/dist/infra/WorkerManager.d.ts +0 -40
- package/dist/infra/WorkerManager.d.ts.map +0 -1
- package/dist/infra/WorkerManager.js +0 -59
- package/dist/infra/WorkerManager.js.map +0 -1
- package/dist/utils/SdCliReporter.d.ts +0 -18
- package/dist/utils/SdCliReporter.d.ts.map +0 -1
- package/dist/utils/SdCliReporter.js +0 -144
- package/dist/utils/SdCliReporter.js.map +0 -1
- package/src/infra/WorkerManager.ts +0 -65
- package/src/utils/SdCliReporter.ts +0 -177
- package/tests/angular/scss-compiler-async.spec.ts +0 -54
- package/tests/commands/dev.spec.ts +0 -53
- package/tests/commands/watch.spec.ts +0 -53
- package/tests/infra/worker-manager.spec.ts +0 -63
- package/tests/utils/angular-compiler-emit.spec.ts +0 -570
- package/tests/utils/angular-compiler-init.spec.ts +0 -705
- package/tests/utils/angular-compiler-update.spec.ts +0 -293
- package/tests/utils/build-env.spec.ts +0 -33
- package/tests/utils/ngtsc-build-core-transform-stylesheet.spec.ts +0 -124
- package/tests/utils/ngtsc-scss-refactor.spec.ts +0 -47
|
@@ -1,22 +1,116 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import ts from "typescript";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
3
6
|
|
|
4
|
-
// ---
|
|
7
|
+
// --- Mock Setup ---
|
|
8
|
+
|
|
9
|
+
const mockAnalyzeAsync = vi.fn().mockResolvedValue(undefined);
|
|
10
|
+
const mockGetDiagnosticsForFile = vi.fn().mockReturnValue([]);
|
|
11
|
+
const mockGetOptionDiagnostics = vi.fn().mockReturnValue([]);
|
|
12
|
+
const mockGetResourceDependencies = vi.fn().mockReturnValue([]);
|
|
13
|
+
const mockIgnoreForDiagnostics = new Set<ts.SourceFile>();
|
|
14
|
+
const mockIgnoreForEmit = new Set<ts.SourceFile>();
|
|
15
|
+
const mockSafeToSkipEmit = vi.fn().mockReturnValue(false);
|
|
16
|
+
const mockRecordSuccessfulEmit = vi.fn();
|
|
17
|
+
const mockPrepareEmit = vi.fn().mockReturnValue({
|
|
18
|
+
transformers: { before: [], after: [] },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const ngtscConstructorSpy = vi.fn();
|
|
22
|
+
|
|
23
|
+
function createRealTsProgram(
|
|
24
|
+
files: Record<string, string> = { "index.ts": "export const x = 1;" },
|
|
25
|
+
extraOptions: ts.CompilerOptions = {},
|
|
26
|
+
): { program: ts.Program; dir: string; rootNames: string[] } {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "angular-compiler-test-"));
|
|
28
|
+
const rootNames: string[] = [];
|
|
29
|
+
for (const [name, content] of Object.entries(files)) {
|
|
30
|
+
const filePath = path.join(dir, name);
|
|
31
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
32
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
33
|
+
rootNames.push(filePath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options: ts.CompilerOptions = {
|
|
37
|
+
target: ts.ScriptTarget.ESNext,
|
|
38
|
+
module: ts.ModuleKind.ESNext,
|
|
39
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
40
|
+
strict: false,
|
|
41
|
+
skipLibCheck: true,
|
|
42
|
+
types: [],
|
|
43
|
+
outDir: path.join(dir, "out"),
|
|
44
|
+
...extraOptions,
|
|
45
|
+
};
|
|
46
|
+
const host = ts.createCompilerHost(options);
|
|
47
|
+
const program = ts.createProgram(rootNames, options, host);
|
|
48
|
+
return { program, dir, rootNames };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let realProgram: { program: ts.Program; dir: string; rootNames: string[] };
|
|
52
|
+
|
|
53
|
+
vi.mock("../../src/utils/angular-build", () => {
|
|
54
|
+
class NgtscProgram {
|
|
55
|
+
compiler = {
|
|
56
|
+
analyzeAsync: mockAnalyzeAsync,
|
|
57
|
+
getDiagnosticsForFile: mockGetDiagnosticsForFile,
|
|
58
|
+
getOptionDiagnostics: mockGetOptionDiagnostics,
|
|
59
|
+
getResourceDependencies: mockGetResourceDependencies,
|
|
60
|
+
ignoreForDiagnostics: mockIgnoreForDiagnostics,
|
|
61
|
+
ignoreForEmit: mockIgnoreForEmit,
|
|
62
|
+
incrementalCompilation: {
|
|
63
|
+
safeToSkipEmit: mockSafeToSkipEmit,
|
|
64
|
+
recordSuccessfulEmit: mockRecordSuccessfulEmit,
|
|
65
|
+
},
|
|
66
|
+
prepareEmit: mockPrepareEmit,
|
|
67
|
+
};
|
|
68
|
+
constructor(...args: unknown[]) {
|
|
69
|
+
ngtscConstructorSpy(...args);
|
|
70
|
+
}
|
|
71
|
+
getTsProgram() {
|
|
72
|
+
return realProgram.program;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
NgtscProgram,
|
|
77
|
+
OptimizeFor: { WholeProgram: 0, SingleFile: 1 },
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const { AngularCompiler, AngularSourceFileCache, augmentHostWithCaching } = await import(
|
|
82
|
+
"../../src/utils/angular-compiler"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// --- Common beforeEach ---
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.clearAllMocks();
|
|
89
|
+
mockIgnoreForDiagnostics.clear();
|
|
90
|
+
mockIgnoreForEmit.clear();
|
|
91
|
+
mockGetResourceDependencies.mockReturnValue([]);
|
|
92
|
+
mockGetDiagnosticsForFile.mockReturnValue([]);
|
|
93
|
+
mockSafeToSkipEmit.mockReturnValue(false);
|
|
94
|
+
mockRecordSuccessfulEmit.mockReset();
|
|
95
|
+
mockPrepareEmit.mockReturnValue({
|
|
96
|
+
transformers: { before: [], after: [] },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
realProgram = createRealTsProgram();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// AngularSourceFileCache
|
|
104
|
+
// =============================================================================
|
|
5
105
|
|
|
6
106
|
describe("AngularSourceFileCache — Unit Tests", () => {
|
|
7
|
-
it("modifiedFiles는 빈 Set으로 초기화된다",
|
|
8
|
-
const { AngularSourceFileCache } = await import(
|
|
9
|
-
"../../src/utils/angular-compiler"
|
|
10
|
-
);
|
|
107
|
+
it("modifiedFiles는 빈 Set으로 초기화된다", () => {
|
|
11
108
|
const cache = new AngularSourceFileCache();
|
|
12
109
|
expect(cache.modifiedFiles).toBeInstanceOf(Set);
|
|
13
110
|
expect(cache.modifiedFiles.size).toBe(0);
|
|
14
111
|
});
|
|
15
112
|
|
|
16
|
-
it("invalidate는 여러 파일을 한번에 처리한다",
|
|
17
|
-
const { AngularSourceFileCache } = await import(
|
|
18
|
-
"../../src/utils/angular-compiler"
|
|
19
|
-
);
|
|
113
|
+
it("invalidate는 여러 파일을 한번에 처리한다", () => {
|
|
20
114
|
const cache = new AngularSourceFileCache();
|
|
21
115
|
const sf1 = ts.createSourceFile("a.ts", "", ts.ScriptTarget.ESNext);
|
|
22
116
|
const sf2 = ts.createSourceFile("b.ts", "", ts.ScriptTarget.ESNext);
|
|
@@ -31,11 +125,7 @@ describe("AngularSourceFileCache — Unit Tests", () => {
|
|
|
31
125
|
expect(cache.modifiedFiles.has("src/b.ts")).toBe(true);
|
|
32
126
|
});
|
|
33
127
|
|
|
34
|
-
it("augmentHostWithCaching — 캐시 미스 시 원본 호출 후 캐시 저장",
|
|
35
|
-
const { AngularSourceFileCache, augmentHostWithCaching } = await import(
|
|
36
|
-
"../../src/utils/angular-compiler"
|
|
37
|
-
);
|
|
38
|
-
|
|
128
|
+
it("augmentHostWithCaching — 캐시 미스 시 원본 호출 후 캐시 저장", () => {
|
|
39
129
|
const cache = new AngularSourceFileCache();
|
|
40
130
|
const fakeSourceFile = ts.createSourceFile(
|
|
41
131
|
"miss.ts",
|
|
@@ -56,11 +146,7 @@ describe("AngularSourceFileCache — Unit Tests", () => {
|
|
|
56
146
|
expect(cache.get("miss.ts")).toBe(fakeSourceFile);
|
|
57
147
|
});
|
|
58
148
|
|
|
59
|
-
it("augmentHostWithCaching — shouldCreateNewSourceFile=true이면 캐시 무시",
|
|
60
|
-
const { AngularSourceFileCache, augmentHostWithCaching } = await import(
|
|
61
|
-
"../../src/utils/angular-compiler"
|
|
62
|
-
);
|
|
63
|
-
|
|
149
|
+
it("augmentHostWithCaching — shouldCreateNewSourceFile=true이면 캐시 무시", () => {
|
|
64
150
|
const cache = new AngularSourceFileCache();
|
|
65
151
|
const cachedFile = ts.createSourceFile("cached.ts", "old", ts.ScriptTarget.ESNext);
|
|
66
152
|
const freshFile = ts.createSourceFile("cached.ts", "new", ts.ScriptTarget.ESNext);
|
|
@@ -79,16 +165,10 @@ describe("AngularSourceFileCache — Unit Tests", () => {
|
|
|
79
165
|
});
|
|
80
166
|
});
|
|
81
167
|
|
|
82
|
-
// --- Acceptance Tests: Slice 1 — AngularSourceFileCache ---
|
|
83
|
-
|
|
84
168
|
describe("AngularSourceFileCache", () => {
|
|
85
169
|
// Scenario: SourceFileCache 통합
|
|
86
170
|
describe("augmentHostWithCaching으로 호스트에 캐시를 통합한다", () => {
|
|
87
|
-
it("캐시에 있는 파일은 재파싱 없이 반환되고, 미스 시 원본 호출 후 캐시 저장",
|
|
88
|
-
const { AngularSourceFileCache, augmentHostWithCaching } = await import(
|
|
89
|
-
"../../src/utils/angular-compiler"
|
|
90
|
-
);
|
|
91
|
-
|
|
171
|
+
it("캐시에 있는 파일은 재파싱 없이 반환되고, 미스 시 원본 호출 후 캐시 저장", () => {
|
|
92
172
|
const cache = new AngularSourceFileCache();
|
|
93
173
|
const compilerOptions: ts.CompilerOptions = {
|
|
94
174
|
target: ts.ScriptTarget.ESNext,
|
|
@@ -125,11 +205,7 @@ describe("AngularSourceFileCache", () => {
|
|
|
125
205
|
|
|
126
206
|
// Scenario: SourceFileCache invalidate
|
|
127
207
|
describe("invalidate로 파일을 캐시에서 삭제하고 modifiedFiles에 추가한다", () => {
|
|
128
|
-
it("invalidate 후 캐시에서 삭제되고 modifiedFiles에 추가된다",
|
|
129
|
-
const { AngularSourceFileCache } = await import(
|
|
130
|
-
"../../src/utils/angular-compiler"
|
|
131
|
-
);
|
|
132
|
-
|
|
208
|
+
it("invalidate 후 캐시에서 삭제되고 modifiedFiles에 추가된다", () => {
|
|
133
209
|
const cache = new AngularSourceFileCache();
|
|
134
210
|
const fakeSourceFile = ts.createSourceFile(
|
|
135
211
|
"component.ts",
|
|
@@ -149,3 +225,1291 @@ describe("AngularSourceFileCache", () => {
|
|
|
149
225
|
});
|
|
150
226
|
});
|
|
151
227
|
});
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// AngularCompiler — initialize
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
233
|
+
describe("AngularCompiler — Unit Tests", () => {
|
|
234
|
+
it("initialize() 전 getTsProgram()은 에러를 던진다", () => {
|
|
235
|
+
const compiler = new AngularCompiler({
|
|
236
|
+
rootNames: ["src/main.ts"],
|
|
237
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(() => compiler.getTsProgram()).toThrow("initialize()");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("initialize() 전 compiler getter는 에러를 던진다", () => {
|
|
244
|
+
const compiler = new AngularCompiler({
|
|
245
|
+
rootNames: ["src/main.ts"],
|
|
246
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(() => compiler.compiler).toThrow("initialize()");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("initialize() 전 ngtscProgram은 undefined이다", () => {
|
|
253
|
+
const compiler = new AngularCompiler({
|
|
254
|
+
rootNames: ["src/main.ts"],
|
|
255
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(compiler.ngtscProgram).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("host.readResource가 파일 내용을 반환한다", async () => {
|
|
262
|
+
const compiler = new AngularCompiler({
|
|
263
|
+
rootNames: realProgram.rootNames,
|
|
264
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await compiler.initialize();
|
|
268
|
+
|
|
269
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
270
|
+
// readResource는 host.readFile을 래핑한다
|
|
271
|
+
expect(typeof hostArg["readResource"]).toBe("function");
|
|
272
|
+
const content = (hostArg["readResource"])(realProgram.rootNames[0]);
|
|
273
|
+
expect(content).toContain("export const x = 1");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("host.transformResource — style이 아닌 type은 null을 반환한다", async () => {
|
|
277
|
+
const transformStylesheet = vi.fn().mockResolvedValue("css");
|
|
278
|
+
const compiler = new AngularCompiler({
|
|
279
|
+
rootNames: realProgram.rootNames,
|
|
280
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
281
|
+
transformStylesheet,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await compiler.initialize();
|
|
285
|
+
|
|
286
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
287
|
+
const result = await (hostArg["transformResource"])("data", {
|
|
288
|
+
type: "template",
|
|
289
|
+
containingFile: "/app/comp.ts",
|
|
290
|
+
resourceFile: null,
|
|
291
|
+
});
|
|
292
|
+
expect(result).toBeNull();
|
|
293
|
+
expect(transformStylesheet).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("host.transformResource — 빈 스타일은 빈 content를 반환한다", async () => {
|
|
297
|
+
const transformStylesheet = vi.fn().mockResolvedValue("css");
|
|
298
|
+
const compiler = new AngularCompiler({
|
|
299
|
+
rootNames: realProgram.rootNames,
|
|
300
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
301
|
+
transformStylesheet,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await compiler.initialize();
|
|
305
|
+
|
|
306
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
307
|
+
const result = await (hostArg["transformResource"])(" ", {
|
|
308
|
+
type: "style",
|
|
309
|
+
containingFile: "/app/comp.ts",
|
|
310
|
+
resourceFile: null,
|
|
311
|
+
});
|
|
312
|
+
expect(result).toEqual({ content: "" });
|
|
313
|
+
expect(transformStylesheet).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("host.getModifiedResourceFiles는 sourceFileCache.modifiedFiles를 반환한다", async () => {
|
|
317
|
+
const cache = new AngularSourceFileCache();
|
|
318
|
+
cache.modifiedFiles.add("src/styles.scss");
|
|
319
|
+
|
|
320
|
+
const compiler = new AngularCompiler({
|
|
321
|
+
rootNames: realProgram.rootNames,
|
|
322
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
323
|
+
sourceFileCache: cache,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await compiler.initialize();
|
|
327
|
+
|
|
328
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
329
|
+
expect(typeof hostArg["getModifiedResourceFiles"]).toBe("function");
|
|
330
|
+
const modifiedFiles = (hostArg["getModifiedResourceFiles"])();
|
|
331
|
+
expect(modifiedFiles.has("src/styles.scss")).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("resourceNameToFileName — 존재하지 않는 파일은 null 반환", async () => {
|
|
335
|
+
const compiler = new AngularCompiler({
|
|
336
|
+
rootNames: realProgram.rootNames,
|
|
337
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await compiler.initialize();
|
|
341
|
+
|
|
342
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
343
|
+
const result = (hostArg["resourceNameToFileName"])(
|
|
344
|
+
"./nonexistent.html",
|
|
345
|
+
realProgram.rootNames[0],
|
|
346
|
+
);
|
|
347
|
+
expect(result).toBeNull();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("resourceNameToFileName — 존재하는 템플릿 파일은 resolvedPath 반환", async () => {
|
|
351
|
+
const htmlPath = path.join(realProgram.dir, "comp.html");
|
|
352
|
+
fs.writeFileSync(htmlPath, "<div></div>", "utf-8");
|
|
353
|
+
|
|
354
|
+
const compiler = new AngularCompiler({
|
|
355
|
+
rootNames: realProgram.rootNames,
|
|
356
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await compiler.initialize();
|
|
360
|
+
|
|
361
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
362
|
+
const result = (hostArg["resourceNameToFileName"])(
|
|
363
|
+
"./comp.html",
|
|
364
|
+
realProgram.rootNames[0],
|
|
365
|
+
);
|
|
366
|
+
expect(result).toBe(htmlPath);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("resourceNameToFileName — externalStylesheets와 stylesheet일 때 SHA256 ID 반환", async () => {
|
|
370
|
+
const scssPath = path.join(realProgram.dir, "comp.scss");
|
|
371
|
+
fs.writeFileSync(scssPath, ".x{}", "utf-8");
|
|
372
|
+
|
|
373
|
+
const externalStylesheets = new Map<string, string>();
|
|
374
|
+
const compiler = new AngularCompiler({
|
|
375
|
+
rootNames: realProgram.rootNames,
|
|
376
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
377
|
+
externalStylesheets,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await compiler.initialize();
|
|
381
|
+
|
|
382
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
383
|
+
const result = (hostArg["resourceNameToFileName"])(
|
|
384
|
+
"./comp.scss",
|
|
385
|
+
realProgram.rootNames[0],
|
|
386
|
+
);
|
|
387
|
+
// SHA256 ID + .css가 반환되어야 한다
|
|
388
|
+
expect(result).toMatch(/^[a-f0-9]{64}\.css$/);
|
|
389
|
+
// externalStylesheets에 매핑이 저장되어야 한다
|
|
390
|
+
expect(externalStylesheets.has(scssPath)).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("angularCompilerOptions가 compilerOptions에 병합된다", async () => {
|
|
394
|
+
const compiler = new AngularCompiler({
|
|
395
|
+
rootNames: realProgram.rootNames,
|
|
396
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
397
|
+
angularCompilerOptions: { strictTemplates: true },
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await compiler.initialize();
|
|
401
|
+
|
|
402
|
+
const passedOptions = ngtscConstructorSpy.mock.calls[0][1] as Record<string, unknown>;
|
|
403
|
+
expect(passedOptions["strictTemplates"]).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("collectDiagnostics() — initialize() 전 호출 시 에러", () => {
|
|
407
|
+
const compiler = new AngularCompiler({
|
|
408
|
+
rootNames: ["src/main.ts"],
|
|
409
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(() => [...compiler.collectDiagnostics()]).toThrow("initialize()");
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("AngularCompiler — 초기화", () => {
|
|
417
|
+
// Scenario: 최초 초기화
|
|
418
|
+
it("initialize()로 호스트, NgtscProgram, BuilderProgram이 생성되고 analyzeAsync가 호출된다", async () => {
|
|
419
|
+
const compiler = new AngularCompiler({
|
|
420
|
+
rootNames: ["src/main.ts"],
|
|
421
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const result = await compiler.initialize();
|
|
425
|
+
|
|
426
|
+
// NgtscProgram이 생성되었다
|
|
427
|
+
expect(ngtscConstructorSpy).toHaveBeenCalledTimes(1);
|
|
428
|
+
// oldProgram은 undefined (최초)
|
|
429
|
+
expect(ngtscConstructorSpy.mock.calls[0][3]).toBeUndefined();
|
|
430
|
+
// analyzeAsync가 호출되었다
|
|
431
|
+
expect(mockAnalyzeAsync).toHaveBeenCalledTimes(1);
|
|
432
|
+
// affectedFiles가 반환된다
|
|
433
|
+
expect(result.affectedFiles).toBeInstanceOf(Set);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Scenario: 재초기화 (incremental)
|
|
437
|
+
it("두번째 initialize()에서 이전 NgtscProgram이 oldProgram으로 전달된다", async () => {
|
|
438
|
+
const compiler = new AngularCompiler({
|
|
439
|
+
rootNames: ["src/main.ts"],
|
|
440
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await compiler.initialize();
|
|
444
|
+
const firstProgram = compiler.ngtscProgram;
|
|
445
|
+
|
|
446
|
+
await compiler.initialize();
|
|
447
|
+
|
|
448
|
+
// 두번째 호출에서 oldProgram으로 첫번째 NgtscProgram이 전달됨
|
|
449
|
+
expect(ngtscConstructorSpy).toHaveBeenCalledTimes(2);
|
|
450
|
+
expect(ngtscConstructorSpy.mock.calls[1][3]).toBe(firstProgram);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Scenario: transformStylesheet 콜백 주입 (library)
|
|
454
|
+
it("transformStylesheet 콜백이 host.transformResource를 통해 호출된다", async () => {
|
|
455
|
+
const transformStylesheet = vi.fn().mockResolvedValue("body { color: red; }");
|
|
456
|
+
|
|
457
|
+
const compiler = new AngularCompiler({
|
|
458
|
+
rootNames: ["src/main.ts"],
|
|
459
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
460
|
+
transformStylesheet,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await compiler.initialize();
|
|
464
|
+
|
|
465
|
+
// NgtscProgram 생성 시 전달된 host를 가져온다
|
|
466
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, unknown>;
|
|
467
|
+
expect(typeof hostArg["transformResource"]).toBe("function");
|
|
468
|
+
|
|
469
|
+
// transformResource를 호출하면 transformStylesheet가 호출된다
|
|
470
|
+
const result = await (hostArg["transformResource"] as Function)(
|
|
471
|
+
".button { color: blue; }",
|
|
472
|
+
{ type: "style", containingFile: "/app/comp.ts", resourceFile: "/app/comp.scss" },
|
|
473
|
+
);
|
|
474
|
+
expect(transformStylesheet).toHaveBeenCalledWith(
|
|
475
|
+
".button { color: blue; }",
|
|
476
|
+
"/app/comp.ts",
|
|
477
|
+
"/app/comp.scss",
|
|
478
|
+
);
|
|
479
|
+
expect(result).toEqual({ content: "body { color: red; }" });
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Scenario: compilerOptionsTransformer 적용
|
|
483
|
+
it("compilerOptionsTransformer가 NgtscProgram 생성에 사용된다", async () => {
|
|
484
|
+
const transformer = vi.fn((opts: ts.CompilerOptions) => ({
|
|
485
|
+
...opts,
|
|
486
|
+
noEmit: false,
|
|
487
|
+
declaration: false,
|
|
488
|
+
}));
|
|
489
|
+
|
|
490
|
+
const compiler = new AngularCompiler({
|
|
491
|
+
rootNames: ["src/main.ts"],
|
|
492
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext, noEmit: true },
|
|
493
|
+
compilerOptionsTransformer: transformer,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await compiler.initialize();
|
|
497
|
+
|
|
498
|
+
expect(transformer).toHaveBeenCalledTimes(1);
|
|
499
|
+
// NgtscProgram에 전달된 options에 변환이 적용되었는지 확인
|
|
500
|
+
const passedOptions = ngtscConstructorSpy.mock.calls[0][1] as ts.CompilerOptions;
|
|
501
|
+
expect(passedOptions.noEmit).toBe(false);
|
|
502
|
+
expect(passedOptions.declaration).toBe(false);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Scenario: resourceNameToFileName으로 외부 스타일시트 경로 해석
|
|
506
|
+
it("host.resourceNameToFileName이 절대 경로를 반환한다", async () => {
|
|
507
|
+
const compiler = new AngularCompiler({
|
|
508
|
+
rootNames: ["src/main.ts"],
|
|
509
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await compiler.initialize();
|
|
513
|
+
|
|
514
|
+
const hostArg = ngtscConstructorSpy.mock.calls[0][2] as Record<string, Function>;
|
|
515
|
+
expect(typeof hostArg["resourceNameToFileName"]).toBe("function");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Scenario: declaration true 설정 (library)
|
|
519
|
+
it("compilerOptionsTransformer로 declaration: true 설정", async () => {
|
|
520
|
+
const compiler = new AngularCompiler({
|
|
521
|
+
rootNames: ["src/main.ts"],
|
|
522
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
523
|
+
compilerOptionsTransformer: (opts) => ({
|
|
524
|
+
...opts,
|
|
525
|
+
declaration: true,
|
|
526
|
+
declarationMap: true,
|
|
527
|
+
}),
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await compiler.initialize();
|
|
531
|
+
|
|
532
|
+
const passedOptions = ngtscConstructorSpy.mock.calls[0][1] as ts.CompilerOptions;
|
|
533
|
+
expect(passedOptions.declaration).toBe(true);
|
|
534
|
+
expect(passedOptions.declarationMap).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Scenario: declaration false 설정 (client)
|
|
538
|
+
it("compilerOptionsTransformer로 declaration: false 설정", async () => {
|
|
539
|
+
const compiler = new AngularCompiler({
|
|
540
|
+
rootNames: ["src/main.ts"],
|
|
541
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
542
|
+
compilerOptionsTransformer: (opts) => ({
|
|
543
|
+
...opts,
|
|
544
|
+
declaration: false,
|
|
545
|
+
}),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await compiler.initialize();
|
|
549
|
+
|
|
550
|
+
const passedOptions = ngtscConstructorSpy.mock.calls[0][1] as ts.CompilerOptions;
|
|
551
|
+
expect(passedOptions.declaration).toBe(false);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Scenario: 초기화 후 ts.Program 획득
|
|
555
|
+
it("getTsProgram()으로 ts.Program을 반환한다", async () => {
|
|
556
|
+
const compiler = new AngularCompiler({
|
|
557
|
+
rootNames: ["src/main.ts"],
|
|
558
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await compiler.initialize();
|
|
562
|
+
|
|
563
|
+
const program = compiler.getTsProgram();
|
|
564
|
+
expect(program).toBe(realProgram.program);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Scenario: ESLint에 Program 주입 가능
|
|
568
|
+
it("getTsProgram()의 결과가 ESLint parserOptions.programs에 주입 가능한 형태이다", async () => {
|
|
569
|
+
const compiler = new AngularCompiler({
|
|
570
|
+
rootNames: ["src/main.ts"],
|
|
571
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
await compiler.initialize();
|
|
575
|
+
|
|
576
|
+
const program = compiler.getTsProgram();
|
|
577
|
+
// ESLint parserOptions.programs는 ts.Program 배열을 받는다
|
|
578
|
+
const parserOptions = { programs: [program], project: null };
|
|
579
|
+
expect(parserOptions.programs).toHaveLength(1);
|
|
580
|
+
expect(parserOptions.programs[0]).toBe(realProgram.program);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Scenario: SourceFileCache 통합
|
|
584
|
+
it("sourceFileCache 제공 시 augmentHostWithCaching이 적용된다", async () => {
|
|
585
|
+
const cache = new AngularSourceFileCache();
|
|
586
|
+
|
|
587
|
+
const compiler = new AngularCompiler({
|
|
588
|
+
rootNames: ["src/main.ts"],
|
|
589
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
590
|
+
sourceFileCache: cache,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await compiler.initialize();
|
|
594
|
+
|
|
595
|
+
// host가 캐시를 사용하도록 래핑되었는지 간접 확인:
|
|
596
|
+
// NgtscProgram이 성공적으로 생성되었으면 호스트가 정상적으로 구성된 것
|
|
597
|
+
expect(ngtscConstructorSpy).toHaveBeenCalledTimes(1);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Scenario: packageJsonCache — node_modules 변경 시 clear
|
|
601
|
+
it("modifiedFiles에 node_modules 파일이 있으면 packageJsonCache가 clear된다", async () => {
|
|
602
|
+
const cache = new AngularSourceFileCache();
|
|
603
|
+
|
|
604
|
+
const compiler = new AngularCompiler({
|
|
605
|
+
rootNames: ["src/main.ts"],
|
|
606
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
607
|
+
sourceFileCache: cache,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// 첫 초기화 — packageJsonCache 생성
|
|
611
|
+
await compiler.initialize();
|
|
612
|
+
|
|
613
|
+
// node_modules 파일 변경
|
|
614
|
+
cache.modifiedFiles.add("node_modules/some-pkg/index.js");
|
|
615
|
+
|
|
616
|
+
// 재초기화 — packageJsonCache.clear() 호출되어야 함
|
|
617
|
+
// 에러 없이 완료되면 성공
|
|
618
|
+
await compiler.initialize();
|
|
619
|
+
|
|
620
|
+
expect(ngtscConstructorSpy).toHaveBeenCalledTimes(2);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Scenario: packageJsonCache — node_modules 미변경 시 재사용
|
|
624
|
+
it("modifiedFiles에 node_modules 파일이 없으면 packageJsonCache가 재사용된다", async () => {
|
|
625
|
+
const cache = new AngularSourceFileCache();
|
|
626
|
+
|
|
627
|
+
const compiler = new AngularCompiler({
|
|
628
|
+
rootNames: ["src/main.ts"],
|
|
629
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
630
|
+
sourceFileCache: cache,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// 첫 초기화
|
|
634
|
+
await compiler.initialize();
|
|
635
|
+
|
|
636
|
+
// 일반 파일만 변경
|
|
637
|
+
cache.modifiedFiles.add("src/app/component.ts");
|
|
638
|
+
|
|
639
|
+
// 재초기화
|
|
640
|
+
await compiler.initialize();
|
|
641
|
+
|
|
642
|
+
// 에러 없이 완료
|
|
643
|
+
expect(ngtscConstructorSpy).toHaveBeenCalledTimes(2);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// =============================================================================
|
|
648
|
+
// AngularCompiler — affected 파일
|
|
649
|
+
// =============================================================================
|
|
650
|
+
|
|
651
|
+
describe("AngularCompiler — affected 파일", () => {
|
|
652
|
+
// Scenario: 소스 파일 변경 시 affected 파일 수집
|
|
653
|
+
it("initialize()가 BuilderProgram 기반으로 affected 파일을 반환한다", async () => {
|
|
654
|
+
const compiler = new AngularCompiler({
|
|
655
|
+
rootNames: realProgram.rootNames,
|
|
656
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const result = await compiler.initialize();
|
|
660
|
+
// affected 파일이 Set으로 반환된다
|
|
661
|
+
expect(result.affectedFiles).toBeInstanceOf(Set);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Scenario: 리소스 변경 시 해당 .ts 파일이 affected에 추가
|
|
665
|
+
it("리소스 변경 시 해당 .ts 파일이 affected에 추가된다", async () => {
|
|
666
|
+
// realProgram has index.ts — mock its resource dependencies
|
|
667
|
+
const sourceFiles = realProgram.program.getSourceFiles();
|
|
668
|
+
const indexFile = sourceFiles.find((sf) => sf.fileName.includes("index.ts"));
|
|
669
|
+
const scssPath = path.join(realProgram.dir, "component.scss");
|
|
670
|
+
fs.writeFileSync(scssPath, ".x { }", "utf-8");
|
|
671
|
+
|
|
672
|
+
mockGetResourceDependencies.mockImplementation((sf: ts.SourceFile) => {
|
|
673
|
+
if (sf === indexFile) {
|
|
674
|
+
return [scssPath];
|
|
675
|
+
}
|
|
676
|
+
return [];
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const cache = new AngularSourceFileCache();
|
|
680
|
+
cache.modifiedFiles.add(scssPath);
|
|
681
|
+
|
|
682
|
+
const compiler = new AngularCompiler({
|
|
683
|
+
rootNames: realProgram.rootNames,
|
|
684
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
685
|
+
sourceFileCache: cache,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const result = await compiler.initialize();
|
|
689
|
+
// index.ts가 affected에 추가되어야 한다 (리소스 변경으로 인해)
|
|
690
|
+
const affectedFileNames = [...result.affectedFiles].map((sf) => sf.fileName);
|
|
691
|
+
expect(affectedFileNames.some((f) => f.includes("index.ts"))).toBe(true);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// =============================================================================
|
|
696
|
+
// AngularCompiler — collectDiagnostics
|
|
697
|
+
// =============================================================================
|
|
698
|
+
|
|
699
|
+
describe("AngularCompiler — collectDiagnostics", () => {
|
|
700
|
+
// Scenario: 4종 진단 수집
|
|
701
|
+
it("collectDiagnostics가 Option, Syntactic, Semantic, Angular 진단을 수집한다", async () => {
|
|
702
|
+
const compiler = new AngularCompiler({
|
|
703
|
+
rootNames: realProgram.rootNames,
|
|
704
|
+
compilerOptions: {
|
|
705
|
+
target: ts.ScriptTarget.ESNext,
|
|
706
|
+
module: ts.ModuleKind.ESNext,
|
|
707
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
708
|
+
skipLibCheck: true,
|
|
709
|
+
types: [],
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
await compiler.initialize();
|
|
714
|
+
|
|
715
|
+
// collectDiagnostics가 에러 없이 실행된다
|
|
716
|
+
const diagnostics = [...compiler.collectDiagnostics()];
|
|
717
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Scenario: affected 파일의 Angular 진단은 재계산 후 캐시
|
|
721
|
+
it("affected 파일의 Angular 진단은 getDiagnosticsForFile로 재계산된다", async () => {
|
|
722
|
+
const indexFile = realProgram.program.getSourceFiles()
|
|
723
|
+
.find((sf) => sf.fileName.includes("index.ts"));
|
|
724
|
+
const scssPath = path.join(realProgram.dir, "component.scss");
|
|
725
|
+
fs.writeFileSync(scssPath, ".x { }", "utf-8");
|
|
726
|
+
|
|
727
|
+
const mockDiag = {
|
|
728
|
+
category: ts.DiagnosticCategory.Error,
|
|
729
|
+
code: 1000,
|
|
730
|
+
messageText: "test error",
|
|
731
|
+
file: indexFile,
|
|
732
|
+
start: 0,
|
|
733
|
+
length: 5,
|
|
734
|
+
} as ts.Diagnostic;
|
|
735
|
+
mockGetDiagnosticsForFile.mockReturnValue([mockDiag]);
|
|
736
|
+
mockGetResourceDependencies.mockImplementation((sf: ts.SourceFile) => {
|
|
737
|
+
if (sf.fileName.includes("index.ts")) {
|
|
738
|
+
return [scssPath];
|
|
739
|
+
}
|
|
740
|
+
return [];
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const cache = new AngularSourceFileCache();
|
|
744
|
+
cache.modifiedFiles.add(scssPath);
|
|
745
|
+
|
|
746
|
+
const compiler = new AngularCompiler({
|
|
747
|
+
rootNames: realProgram.rootNames,
|
|
748
|
+
compilerOptions: {
|
|
749
|
+
target: ts.ScriptTarget.ESNext,
|
|
750
|
+
module: ts.ModuleKind.ESNext,
|
|
751
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
752
|
+
skipLibCheck: true,
|
|
753
|
+
types: [],
|
|
754
|
+
},
|
|
755
|
+
sourceFileCache: cache,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
await compiler.initialize();
|
|
759
|
+
|
|
760
|
+
const diagnostics = [...compiler.collectDiagnostics()];
|
|
761
|
+
// getDiagnosticsForFile이 호출되었다
|
|
762
|
+
expect(mockGetDiagnosticsForFile).toHaveBeenCalled();
|
|
763
|
+
// 진단이 수집되었다
|
|
764
|
+
expect(diagnostics.some((d) => d.code === 1000)).toBe(true);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Scenario: non-affected 파일의 Angular 진단은 캐시 반환
|
|
768
|
+
it("non-affected 파일의 Angular 진단은 캐시에서 반환된다 (첫 빌드에서 모든 파일이 affected)", async () => {
|
|
769
|
+
const mockDiag = {
|
|
770
|
+
category: ts.DiagnosticCategory.Warning,
|
|
771
|
+
code: 2000,
|
|
772
|
+
messageText: "cached warning",
|
|
773
|
+
} as ts.Diagnostic;
|
|
774
|
+
mockGetDiagnosticsForFile.mockReturnValue([mockDiag]);
|
|
775
|
+
|
|
776
|
+
const compiler = new AngularCompiler({
|
|
777
|
+
rootNames: realProgram.rootNames,
|
|
778
|
+
compilerOptions: {
|
|
779
|
+
target: ts.ScriptTarget.ESNext,
|
|
780
|
+
module: ts.ModuleKind.ESNext,
|
|
781
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
782
|
+
skipLibCheck: true,
|
|
783
|
+
types: [],
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
await compiler.initialize();
|
|
788
|
+
|
|
789
|
+
// 첫 collectDiagnostics — 초기 빌드에서 모든 파일이 affected → getDiagnosticsForFile 호출됨
|
|
790
|
+
const diags1 = [...compiler.collectDiagnostics()];
|
|
791
|
+
const callCount1 = mockGetDiagnosticsForFile.mock.calls.length;
|
|
792
|
+
expect(callCount1).toBeGreaterThan(0);
|
|
793
|
+
expect(diags1.some((d) => d.code === 2000)).toBe(true);
|
|
794
|
+
|
|
795
|
+
// 두번째 initialize + collectDiagnostics — 변경 없으면 캐시 사용
|
|
796
|
+
mockGetDiagnosticsForFile.mockClear();
|
|
797
|
+
await compiler.initialize();
|
|
798
|
+
const diags2 = [...compiler.collectDiagnostics()];
|
|
799
|
+
// 두번째에서는 변경이 없으므로 affected가 비어 있어야 하고, 캐시에서 반환
|
|
800
|
+
// (하지만 BuilderProgram은 재생성되므로 affected가 다시 계산될 수 있다)
|
|
801
|
+
// 최소한 에러 없이 동작해야 한다
|
|
802
|
+
expect(Array.isArray(diags2)).toBe(true);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
// Scenario: 리소스 변경 시 해당 .ts 파일의 캐시 무효화
|
|
806
|
+
it("리소스 변경 시 해당 .ts 파일의 diagnosticCache가 무효화된다", async () => {
|
|
807
|
+
const scssPath = path.join(realProgram.dir, "styles.scss");
|
|
808
|
+
fs.writeFileSync(scssPath, ".y { }", "utf-8");
|
|
809
|
+
|
|
810
|
+
const mockDiag = {
|
|
811
|
+
category: ts.DiagnosticCategory.Error,
|
|
812
|
+
code: 3000,
|
|
813
|
+
messageText: "resource error",
|
|
814
|
+
} as ts.Diagnostic;
|
|
815
|
+
mockGetDiagnosticsForFile.mockReturnValue([mockDiag]);
|
|
816
|
+
mockGetResourceDependencies.mockImplementation((sf: ts.SourceFile) => {
|
|
817
|
+
if (sf.fileName.includes("index.ts")) {
|
|
818
|
+
return [scssPath];
|
|
819
|
+
}
|
|
820
|
+
return [];
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
const cache = new AngularSourceFileCache();
|
|
824
|
+
|
|
825
|
+
const compiler = new AngularCompiler({
|
|
826
|
+
rootNames: realProgram.rootNames,
|
|
827
|
+
compilerOptions: {
|
|
828
|
+
target: ts.ScriptTarget.ESNext,
|
|
829
|
+
module: ts.ModuleKind.ESNext,
|
|
830
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
831
|
+
skipLibCheck: true,
|
|
832
|
+
types: [],
|
|
833
|
+
},
|
|
834
|
+
sourceFileCache: cache,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// 첫 빌드
|
|
838
|
+
await compiler.initialize();
|
|
839
|
+
void [...compiler.collectDiagnostics()];
|
|
840
|
+
expect(mockGetDiagnosticsForFile).toHaveBeenCalled();
|
|
841
|
+
|
|
842
|
+
// 리소스 변경 후 재빌드
|
|
843
|
+
mockGetDiagnosticsForFile.mockClear();
|
|
844
|
+
cache.modifiedFiles.add(scssPath);
|
|
845
|
+
|
|
846
|
+
await compiler.initialize();
|
|
847
|
+
void [...compiler.collectDiagnostics()];
|
|
848
|
+
|
|
849
|
+
// 리소스 변경으로 캐시 무효화 → getDiagnosticsForFile이 다시 호출되어야 한다
|
|
850
|
+
expect(mockGetDiagnosticsForFile).toHaveBeenCalled();
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// =============================================================================
|
|
855
|
+
// AngularCompiler — emitAffectedFiles
|
|
856
|
+
// =============================================================================
|
|
857
|
+
|
|
858
|
+
describe("AngularCompiler.emitAffectedFiles — Unit Tests", () => {
|
|
859
|
+
it("initialize() 전 호출 시 에러를 던진다", () => {
|
|
860
|
+
const compiler = new AngularCompiler({
|
|
861
|
+
rootNames: ["src/main.ts"],
|
|
862
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(() => [...compiler.emitAffectedFiles()]).toThrow("initialize()");
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it("noEmit=true이면 빈 Iterator를 반환한다", async () => {
|
|
869
|
+
realProgram = createRealTsProgram(
|
|
870
|
+
{ "a.ts": "export const a = 1;" },
|
|
871
|
+
{ noEmit: true },
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const compiler = new AngularCompiler({
|
|
875
|
+
rootNames: realProgram.rootNames,
|
|
876
|
+
compilerOptions: {
|
|
877
|
+
target: ts.ScriptTarget.ESNext,
|
|
878
|
+
module: ts.ModuleKind.ESNext,
|
|
879
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
880
|
+
skipLibCheck: true,
|
|
881
|
+
types: [],
|
|
882
|
+
noEmit: true,
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
await compiler.initialize();
|
|
887
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
888
|
+
expect(results).toEqual([]);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it("ignoreForEmit에 포함된 파일은 emit되지 않는다", async () => {
|
|
892
|
+
realProgram = createRealTsProgram({ "a.ts": "export const a = 1;" });
|
|
893
|
+
|
|
894
|
+
const compiler = new AngularCompiler({
|
|
895
|
+
rootNames: realProgram.rootNames,
|
|
896
|
+
compilerOptions: {
|
|
897
|
+
target: ts.ScriptTarget.ESNext,
|
|
898
|
+
module: ts.ModuleKind.ESNext,
|
|
899
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
900
|
+
skipLibCheck: true,
|
|
901
|
+
types: [],
|
|
902
|
+
},
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
await compiler.initialize();
|
|
906
|
+
|
|
907
|
+
// ignoreForEmit에 모든 소스 파일 추가
|
|
908
|
+
const tsProgram = compiler.getTsProgram();
|
|
909
|
+
for (const sf of tsProgram.getSourceFiles()) {
|
|
910
|
+
mockIgnoreForEmit.add(sf);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
914
|
+
expect(results).toEqual([]);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it("declaration file은 2차 루프에서 skip된다", async () => {
|
|
918
|
+
realProgram = createRealTsProgram({ "a.ts": "export const a = 1;" });
|
|
919
|
+
|
|
920
|
+
const compiler = new AngularCompiler({
|
|
921
|
+
rootNames: realProgram.rootNames,
|
|
922
|
+
compilerOptions: {
|
|
923
|
+
target: ts.ScriptTarget.ESNext,
|
|
924
|
+
module: ts.ModuleKind.ESNext,
|
|
925
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
926
|
+
skipLibCheck: true,
|
|
927
|
+
types: [],
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
await compiler.initialize();
|
|
932
|
+
// 이미 emit된 파일이 없는 상태에서도 .d.ts 파일은 2차 루프에서 skip
|
|
933
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
934
|
+
// .d.ts 입력 파일이 결과에 포함되지 않아야 한다
|
|
935
|
+
for (const r of results) {
|
|
936
|
+
// emit된 결과의 원본 소스가 declaration file이면 안 됨 (입력 .d.ts 파일)
|
|
937
|
+
expect((r as { filename: string }).filename).not.toMatch(/node_modules/);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("sourceFilter가 적용되면 필터를 통과하지 못한 파일은 결과에 포함되지 않는다", async () => {
|
|
942
|
+
realProgram = createRealTsProgram({
|
|
943
|
+
"a.ts": "export const a = 1;",
|
|
944
|
+
"b.ts": "export const b = 2;",
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const compiler = new AngularCompiler({
|
|
948
|
+
rootNames: realProgram.rootNames,
|
|
949
|
+
compilerOptions: {
|
|
950
|
+
target: ts.ScriptTarget.ESNext,
|
|
951
|
+
module: ts.ModuleKind.ESNext,
|
|
952
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
953
|
+
skipLibCheck: true,
|
|
954
|
+
types: [],
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
await compiler.initialize();
|
|
959
|
+
|
|
960
|
+
// 파일 a만 허용 (sourceFilter는 소스 파일명 기준)
|
|
961
|
+
const results = [...compiler.emitAffectedFiles({
|
|
962
|
+
sourceFilter: (fileName: string) => fileName.includes("a.ts"),
|
|
963
|
+
})];
|
|
964
|
+
|
|
965
|
+
// a.ts에서 나온 출력만 포함 (b.ts 출력은 제외)
|
|
966
|
+
expect(results.length).toBeGreaterThan(0);
|
|
967
|
+
for (const r of results) {
|
|
968
|
+
expect((r as { filename: string }).filename).toMatch(/[/\\]a\./);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it("EmitResult는 filename과 contents를 포함한다", async () => {
|
|
973
|
+
realProgram = createRealTsProgram({ "a.ts": "export const a = 1;" });
|
|
974
|
+
|
|
975
|
+
const compiler = new AngularCompiler({
|
|
976
|
+
rootNames: realProgram.rootNames,
|
|
977
|
+
compilerOptions: {
|
|
978
|
+
target: ts.ScriptTarget.ESNext,
|
|
979
|
+
module: ts.ModuleKind.ESNext,
|
|
980
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
981
|
+
skipLibCheck: true,
|
|
982
|
+
types: [],
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
await compiler.initialize();
|
|
987
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
988
|
+
|
|
989
|
+
expect(results.length).toBeGreaterThan(0);
|
|
990
|
+
for (const r of results) {
|
|
991
|
+
expect(r).toHaveProperty("filename");
|
|
992
|
+
expect(r).toHaveProperty("contents");
|
|
993
|
+
expect(typeof (r as { filename: string }).filename).toBe("string");
|
|
994
|
+
expect(typeof (r as { contents: string }).contents).toBe("string");
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
describe("AngularCompiler — emitAffectedFiles", () => {
|
|
1000
|
+
// Scenario: 변경된 파일만 emit
|
|
1001
|
+
it("변경된 파일만 emit하고 변경되지 않은 파일은 emit하지 않는다", async () => {
|
|
1002
|
+
// 파일 A, B, C로 프로그램 구성
|
|
1003
|
+
realProgram = createRealTsProgram({
|
|
1004
|
+
"a.ts": "export const a = 1;",
|
|
1005
|
+
"b.ts": "export const b = 2;",
|
|
1006
|
+
"c.ts": "export const c = 3;",
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
const compiler = new AngularCompiler({
|
|
1010
|
+
rootNames: realProgram.rootNames,
|
|
1011
|
+
compilerOptions: {
|
|
1012
|
+
target: ts.ScriptTarget.ESNext,
|
|
1013
|
+
module: ts.ModuleKind.ESNext,
|
|
1014
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1015
|
+
skipLibCheck: true,
|
|
1016
|
+
types: [],
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
await compiler.initialize();
|
|
1021
|
+
|
|
1022
|
+
// 첫 빌드에서 모든 파일이 affected → 모두 emit
|
|
1023
|
+
const firstResults = [...compiler.emitAffectedFiles()];
|
|
1024
|
+
const firstEmittedFiles = firstResults.map((r: { filename: string }) => r.filename);
|
|
1025
|
+
expect(firstEmittedFiles.length).toBeGreaterThan(0);
|
|
1026
|
+
|
|
1027
|
+
// 두번째 빌드: safeToSkipEmit이 true이면 skip
|
|
1028
|
+
mockSafeToSkipEmit.mockReturnValue(true);
|
|
1029
|
+
await compiler.initialize();
|
|
1030
|
+
|
|
1031
|
+
const secondResults = [...compiler.emitAffectedFiles()];
|
|
1032
|
+
// safeToSkipEmit=true + affectedFiles에 없는 파일은 emit하지 않는다
|
|
1033
|
+
expect(secondResults.length).toBe(0);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// Scenario: 첫 빌드에서 모든 파일 emit
|
|
1037
|
+
it("첫 빌드에서 모든 소스 파일이 emit된다", async () => {
|
|
1038
|
+
realProgram = createRealTsProgram({
|
|
1039
|
+
"a.ts": "export const a = 1;",
|
|
1040
|
+
"b.ts": "export const b = 2;",
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const compiler = new AngularCompiler({
|
|
1044
|
+
rootNames: realProgram.rootNames,
|
|
1045
|
+
compilerOptions: {
|
|
1046
|
+
target: ts.ScriptTarget.ESNext,
|
|
1047
|
+
module: ts.ModuleKind.ESNext,
|
|
1048
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1049
|
+
skipLibCheck: true,
|
|
1050
|
+
types: [],
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
await compiler.initialize();
|
|
1055
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1056
|
+
|
|
1057
|
+
// 모든 소스 파일이 emit됨 (declaration file 제외)
|
|
1058
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// Scenario: Angular가 affected로 판단하지만 TypeScript가 판단하지 않은 파일
|
|
1062
|
+
it("TypeScript가 emit하지 않았지만 safeToSkipEmit이 false인 파일이 emit된다", async () => {
|
|
1063
|
+
realProgram = createRealTsProgram({
|
|
1064
|
+
"a.ts": "export const a = 1;",
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// safeToSkipEmit이 false이면 2차 루프에서 emit해야 한다
|
|
1068
|
+
mockSafeToSkipEmit.mockReturnValue(false);
|
|
1069
|
+
|
|
1070
|
+
const compiler = new AngularCompiler({
|
|
1071
|
+
rootNames: realProgram.rootNames,
|
|
1072
|
+
compilerOptions: {
|
|
1073
|
+
target: ts.ScriptTarget.ESNext,
|
|
1074
|
+
module: ts.ModuleKind.ESNext,
|
|
1075
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1076
|
+
skipLibCheck: true,
|
|
1077
|
+
types: [],
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
await compiler.initialize();
|
|
1082
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1083
|
+
|
|
1084
|
+
// 파일이 emit됨
|
|
1085
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1086
|
+
// recordSuccessfulEmit이 호출됨
|
|
1087
|
+
expect(mockRecordSuccessfulEmit).toHaveBeenCalled();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// Scenario: safeToSkipEmit이 true이고 affectedFiles에 없는 파일은 skip
|
|
1091
|
+
it("safeToSkipEmit이 true이고 affectedFiles에 없으면 emit하지 않는다", async () => {
|
|
1092
|
+
realProgram = createRealTsProgram({
|
|
1093
|
+
"a.ts": "export const a = 1;",
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
const compiler = new AngularCompiler({
|
|
1097
|
+
rootNames: realProgram.rootNames,
|
|
1098
|
+
compilerOptions: {
|
|
1099
|
+
target: ts.ScriptTarget.ESNext,
|
|
1100
|
+
module: ts.ModuleKind.ESNext,
|
|
1101
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1102
|
+
skipLibCheck: true,
|
|
1103
|
+
types: [],
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// 첫 빌드
|
|
1108
|
+
await compiler.initialize();
|
|
1109
|
+
void [...compiler.emitAffectedFiles()];
|
|
1110
|
+
|
|
1111
|
+
// 두번째 빌드: safeToSkipEmit=true + 변경 없음
|
|
1112
|
+
mockSafeToSkipEmit.mockReturnValue(true);
|
|
1113
|
+
await compiler.initialize();
|
|
1114
|
+
|
|
1115
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1116
|
+
expect(results.length).toBe(0);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Scenario: emit 성공 시 incremental 추적 기록
|
|
1120
|
+
it("emit 성공 시 recordSuccessfulEmit이 호출된다", async () => {
|
|
1121
|
+
realProgram = createRealTsProgram({
|
|
1122
|
+
"a.ts": "export const a = 1;",
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
const compiler = new AngularCompiler({
|
|
1126
|
+
rootNames: realProgram.rootNames,
|
|
1127
|
+
compilerOptions: {
|
|
1128
|
+
target: ts.ScriptTarget.ESNext,
|
|
1129
|
+
module: ts.ModuleKind.ESNext,
|
|
1130
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1131
|
+
skipLibCheck: true,
|
|
1132
|
+
types: [],
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
await compiler.initialize();
|
|
1137
|
+
void [...compiler.emitAffectedFiles()];
|
|
1138
|
+
|
|
1139
|
+
expect(mockRecordSuccessfulEmit).toHaveBeenCalled();
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Scenario: prepareEmit()의 transformers가 emit에 적용
|
|
1143
|
+
it("prepareEmit()의 transformers가 emit에 적용된다", async () => {
|
|
1144
|
+
realProgram = createRealTsProgram({
|
|
1145
|
+
"a.ts": "export const a = 1;",
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
const mockTransformerBefore = vi.fn(
|
|
1149
|
+
(_ctx: ts.TransformationContext) => (sf: ts.SourceFile) => sf,
|
|
1150
|
+
);
|
|
1151
|
+
mockPrepareEmit.mockReturnValue({
|
|
1152
|
+
transformers: { before: [mockTransformerBefore], after: [] },
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
const compiler = new AngularCompiler({
|
|
1156
|
+
rootNames: realProgram.rootNames,
|
|
1157
|
+
compilerOptions: {
|
|
1158
|
+
target: ts.ScriptTarget.ESNext,
|
|
1159
|
+
module: ts.ModuleKind.ESNext,
|
|
1160
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1161
|
+
skipLibCheck: true,
|
|
1162
|
+
types: [],
|
|
1163
|
+
},
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
await compiler.initialize();
|
|
1167
|
+
void [...compiler.emitAffectedFiles()];
|
|
1168
|
+
|
|
1169
|
+
// prepareEmit이 호출되었다
|
|
1170
|
+
expect(mockPrepareEmit).toHaveBeenCalled();
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// Scenario: 추가 transformers를 외부에서 주입 가능
|
|
1174
|
+
it("additionalTransformers가 emit 시 적용된다", async () => {
|
|
1175
|
+
realProgram = createRealTsProgram({
|
|
1176
|
+
"a.ts": "export const a = 1;",
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
const additionalBefore = vi.fn(
|
|
1180
|
+
(_ctx: ts.TransformationContext) => (sf: ts.SourceFile) => sf,
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
const compiler = new AngularCompiler({
|
|
1184
|
+
rootNames: realProgram.rootNames,
|
|
1185
|
+
compilerOptions: {
|
|
1186
|
+
target: ts.ScriptTarget.ESNext,
|
|
1187
|
+
module: ts.ModuleKind.ESNext,
|
|
1188
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1189
|
+
skipLibCheck: true,
|
|
1190
|
+
types: [],
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
await compiler.initialize();
|
|
1195
|
+
const results = [...compiler.emitAffectedFiles({
|
|
1196
|
+
additionalTransformers: {
|
|
1197
|
+
before: [additionalBefore],
|
|
1198
|
+
},
|
|
1199
|
+
})];
|
|
1200
|
+
|
|
1201
|
+
// emit이 실행되었다
|
|
1202
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// Scenario: JS와 .d.ts 모두 emit
|
|
1206
|
+
it("declaration=true, noEmit=false일 때 .js와 .d.ts 모두 emit된다", async () => {
|
|
1207
|
+
realProgram = createRealTsProgram(
|
|
1208
|
+
{ "a.ts": "export const a = 1;" },
|
|
1209
|
+
{ declaration: true, noEmit: false },
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
const compiler = new AngularCompiler({
|
|
1213
|
+
rootNames: realProgram.rootNames,
|
|
1214
|
+
compilerOptions: {
|
|
1215
|
+
target: ts.ScriptTarget.ESNext,
|
|
1216
|
+
module: ts.ModuleKind.ESNext,
|
|
1217
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1218
|
+
skipLibCheck: true,
|
|
1219
|
+
types: [],
|
|
1220
|
+
declaration: true,
|
|
1221
|
+
noEmit: false,
|
|
1222
|
+
outDir: path.join(realProgram.dir, "out"),
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
await compiler.initialize();
|
|
1227
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1228
|
+
|
|
1229
|
+
const filenames = results.map((r: { filename: string }) => r.filename);
|
|
1230
|
+
const hasJs = filenames.some((f: string) => f.endsWith(".js"));
|
|
1231
|
+
const hasDts = filenames.some((f: string) => f.endsWith(".d.ts"));
|
|
1232
|
+
expect(hasJs).toBe(true);
|
|
1233
|
+
expect(hasDts).toBe(true);
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// Scenario: JS만 emit
|
|
1237
|
+
it("declaration=false일 때 .js만 emit되고 .d.ts는 생성되지 않는다", async () => {
|
|
1238
|
+
realProgram = createRealTsProgram(
|
|
1239
|
+
{ "a.ts": "export const a = 1;" },
|
|
1240
|
+
{ declaration: false, noEmit: false },
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
const compiler = new AngularCompiler({
|
|
1244
|
+
rootNames: realProgram.rootNames,
|
|
1245
|
+
compilerOptions: {
|
|
1246
|
+
target: ts.ScriptTarget.ESNext,
|
|
1247
|
+
module: ts.ModuleKind.ESNext,
|
|
1248
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1249
|
+
skipLibCheck: true,
|
|
1250
|
+
types: [],
|
|
1251
|
+
declaration: false,
|
|
1252
|
+
noEmit: false,
|
|
1253
|
+
outDir: path.join(realProgram.dir, "out"),
|
|
1254
|
+
},
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
await compiler.initialize();
|
|
1258
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1259
|
+
|
|
1260
|
+
const filenames = results.map((r: { filename: string }) => r.filename);
|
|
1261
|
+
const hasJs = filenames.some((f: string) => f.endsWith(".js"));
|
|
1262
|
+
const hasDts = filenames.some((f: string) => f.endsWith(".d.ts"));
|
|
1263
|
+
expect(hasJs).toBe(true);
|
|
1264
|
+
expect(hasDts).toBe(false);
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
// Scenario: .d.ts만 emit
|
|
1268
|
+
it("emitDeclarationOnly=true일 때 .d.ts만 emit되고 .js는 생성되지 않는다", async () => {
|
|
1269
|
+
realProgram = createRealTsProgram(
|
|
1270
|
+
{ "a.ts": "export const a = 1;" },
|
|
1271
|
+
{ declaration: true, emitDeclarationOnly: true },
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
const compiler = new AngularCompiler({
|
|
1275
|
+
rootNames: realProgram.rootNames,
|
|
1276
|
+
compilerOptions: {
|
|
1277
|
+
target: ts.ScriptTarget.ESNext,
|
|
1278
|
+
module: ts.ModuleKind.ESNext,
|
|
1279
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1280
|
+
skipLibCheck: true,
|
|
1281
|
+
types: [],
|
|
1282
|
+
declaration: true,
|
|
1283
|
+
emitDeclarationOnly: true,
|
|
1284
|
+
outDir: path.join(realProgram.dir, "out"),
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
await compiler.initialize();
|
|
1289
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1290
|
+
|
|
1291
|
+
const filenames = results.map((r: { filename: string }) => r.filename);
|
|
1292
|
+
const hasJs = filenames.some((f: string) => f.endsWith(".js"));
|
|
1293
|
+
const hasDts = filenames.some((f: string) => f.endsWith(".d.ts"));
|
|
1294
|
+
expect(hasJs).toBe(false);
|
|
1295
|
+
expect(hasDts).toBe(true);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// Scenario: emit 없음 (typecheck 전용)
|
|
1299
|
+
it("noEmit=true일 때 어떤 파일도 emit되지 않는다", async () => {
|
|
1300
|
+
realProgram = createRealTsProgram(
|
|
1301
|
+
{ "a.ts": "export const a = 1;" },
|
|
1302
|
+
{ noEmit: true },
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
const compiler = new AngularCompiler({
|
|
1306
|
+
rootNames: realProgram.rootNames,
|
|
1307
|
+
compilerOptions: {
|
|
1308
|
+
target: ts.ScriptTarget.ESNext,
|
|
1309
|
+
module: ts.ModuleKind.ESNext,
|
|
1310
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1311
|
+
skipLibCheck: true,
|
|
1312
|
+
types: [],
|
|
1313
|
+
noEmit: true,
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
await compiler.initialize();
|
|
1318
|
+
const results = [...compiler.emitAffectedFiles()];
|
|
1319
|
+
|
|
1320
|
+
expect(results.length).toBe(0);
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// =============================================================================
|
|
1325
|
+
// AngularCompiler — update
|
|
1326
|
+
// =============================================================================
|
|
1327
|
+
|
|
1328
|
+
describe("AngularCompiler.update — Unit Tests", () => {
|
|
1329
|
+
it("sourceFileCache가 없으면 에러를 던진다", async () => {
|
|
1330
|
+
const compiler = new AngularCompiler({
|
|
1331
|
+
rootNames: realProgram.rootNames,
|
|
1332
|
+
compilerOptions: {
|
|
1333
|
+
target: ts.ScriptTarget.ESNext,
|
|
1334
|
+
module: ts.ModuleKind.ESNext,
|
|
1335
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1336
|
+
skipLibCheck: true,
|
|
1337
|
+
types: [],
|
|
1338
|
+
},
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
await compiler.initialize();
|
|
1342
|
+
await expect(compiler.update(new Set(["a.ts"]))).rejects.toThrow();
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it("update() 후 affectedFiles가 반환된다", async () => {
|
|
1346
|
+
const cache = new AngularSourceFileCache();
|
|
1347
|
+
|
|
1348
|
+
const compiler = new AngularCompiler({
|
|
1349
|
+
rootNames: realProgram.rootNames,
|
|
1350
|
+
compilerOptions: {
|
|
1351
|
+
target: ts.ScriptTarget.ESNext,
|
|
1352
|
+
module: ts.ModuleKind.ESNext,
|
|
1353
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1354
|
+
skipLibCheck: true,
|
|
1355
|
+
types: [],
|
|
1356
|
+
},
|
|
1357
|
+
sourceFileCache: cache,
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
await compiler.initialize();
|
|
1361
|
+
const result = await compiler.update(new Set(["nonexistent.ts"]));
|
|
1362
|
+
|
|
1363
|
+
expect(result).toHaveProperty("affectedFiles");
|
|
1364
|
+
expect(result.affectedFiles).toBeInstanceOf(Set);
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
describe("AngularCompiler — update", () => {
|
|
1369
|
+
// Scenario: 파일 변경 후 incremental rebuild
|
|
1370
|
+
it("update() 후 initialize()가 재호출되어 affectedFiles가 갱신된다", async () => {
|
|
1371
|
+
const cache = new AngularSourceFileCache();
|
|
1372
|
+
|
|
1373
|
+
const compiler = new AngularCompiler({
|
|
1374
|
+
rootNames: realProgram.rootNames,
|
|
1375
|
+
compilerOptions: {
|
|
1376
|
+
target: ts.ScriptTarget.ESNext,
|
|
1377
|
+
module: ts.ModuleKind.ESNext,
|
|
1378
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1379
|
+
skipLibCheck: true,
|
|
1380
|
+
types: [],
|
|
1381
|
+
},
|
|
1382
|
+
sourceFileCache: cache,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// 초기 빌드
|
|
1386
|
+
await compiler.initialize();
|
|
1387
|
+
const initCallCount = ngtscConstructorSpy.mock.calls.length;
|
|
1388
|
+
|
|
1389
|
+
// 파일 변경 후 update
|
|
1390
|
+
const aPath = realProgram.rootNames[0];
|
|
1391
|
+
const result = await compiler.update(new Set([aPath]));
|
|
1392
|
+
|
|
1393
|
+
// initialize()가 재호출됨
|
|
1394
|
+
expect(ngtscConstructorSpy.mock.calls.length).toBe(initCallCount + 1);
|
|
1395
|
+
// sourceFileCache에서 invalidate 됨
|
|
1396
|
+
const normalizedPath = aPath.replace(/\\/g, "/");
|
|
1397
|
+
expect(cache.has(normalizedPath)).toBe(false);
|
|
1398
|
+
expect(cache.modifiedFiles.has(normalizedPath)).toBe(true);
|
|
1399
|
+
// affectedFiles가 반환됨
|
|
1400
|
+
expect(result.affectedFiles).toBeInstanceOf(Set);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
// Scenario: node_modules 파일 변경 시 packageJsonCache clear
|
|
1404
|
+
it("node_modules 파일이 변경되면 packageJsonCache가 clear된다", async () => {
|
|
1405
|
+
const cache = new AngularSourceFileCache();
|
|
1406
|
+
|
|
1407
|
+
const compiler = new AngularCompiler({
|
|
1408
|
+
rootNames: realProgram.rootNames,
|
|
1409
|
+
compilerOptions: {
|
|
1410
|
+
target: ts.ScriptTarget.ESNext,
|
|
1411
|
+
module: ts.ModuleKind.ESNext,
|
|
1412
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1413
|
+
skipLibCheck: true,
|
|
1414
|
+
types: [],
|
|
1415
|
+
},
|
|
1416
|
+
sourceFileCache: cache,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
// 초기 빌드
|
|
1420
|
+
await compiler.initialize();
|
|
1421
|
+
|
|
1422
|
+
// node_modules 파일 변경으로 update
|
|
1423
|
+
const result = await compiler.update(
|
|
1424
|
+
new Set(["node_modules/some-package/index.js"]),
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
// packageJsonCache.clear()가 호출됨 (에러 없이 완료)
|
|
1428
|
+
expect(result.affectedFiles).toBeInstanceOf(Set);
|
|
1429
|
+
// initialize()가 재호출됨
|
|
1430
|
+
expect(ngtscConstructorSpy.mock.calls.length).toBe(2);
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
// Scenario: SCSS 파일 변경 시 관련 컴포넌트가 affected에 포함
|
|
1434
|
+
it("SCSS 파일 변경 시 관련 컴포넌트가 affected에 포함된다", async () => {
|
|
1435
|
+
realProgram = createRealTsProgram({
|
|
1436
|
+
"component.ts": "export class Comp {}",
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
const scssPath = path.join(realProgram.dir, "component.scss");
|
|
1440
|
+
fs.writeFileSync(scssPath, ".x { }", "utf-8");
|
|
1441
|
+
|
|
1442
|
+
const sourceFiles = realProgram.program.getSourceFiles();
|
|
1443
|
+
const compFile = sourceFiles.find((sf) => sf.fileName.includes("component.ts"));
|
|
1444
|
+
|
|
1445
|
+
mockGetResourceDependencies.mockImplementation((sf: ts.SourceFile) => {
|
|
1446
|
+
if (sf === compFile) {
|
|
1447
|
+
return [scssPath];
|
|
1448
|
+
}
|
|
1449
|
+
return [];
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const cache = new AngularSourceFileCache();
|
|
1453
|
+
|
|
1454
|
+
const compiler = new AngularCompiler({
|
|
1455
|
+
rootNames: realProgram.rootNames,
|
|
1456
|
+
compilerOptions: {
|
|
1457
|
+
target: ts.ScriptTarget.ESNext,
|
|
1458
|
+
module: ts.ModuleKind.ESNext,
|
|
1459
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1460
|
+
skipLibCheck: true,
|
|
1461
|
+
types: [],
|
|
1462
|
+
},
|
|
1463
|
+
sourceFileCache: cache,
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// 초기 빌드
|
|
1467
|
+
await compiler.initialize();
|
|
1468
|
+
|
|
1469
|
+
// SCSS 변경 후 update
|
|
1470
|
+
const result = await compiler.update(new Set([scssPath]));
|
|
1471
|
+
|
|
1472
|
+
// component.ts가 affected에 포함
|
|
1473
|
+
const affectedFileNames = [...result.affectedFiles].map(
|
|
1474
|
+
(sf: ts.SourceFile) => sf.fileName,
|
|
1475
|
+
);
|
|
1476
|
+
expect(affectedFileNames.some((f: string) => f.includes("component.ts"))).toBe(true);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// Scenario: 리소스 변경 없으면 diagnosticCache 재사용
|
|
1480
|
+
it("리소스 변경 없으면 diagnosticCache가 재사용된다", async () => {
|
|
1481
|
+
const mockDiag = {
|
|
1482
|
+
category: ts.DiagnosticCategory.Error,
|
|
1483
|
+
code: 5000,
|
|
1484
|
+
messageText: "cached diagnostic",
|
|
1485
|
+
} as ts.Diagnostic;
|
|
1486
|
+
mockGetDiagnosticsForFile.mockReturnValue([mockDiag]);
|
|
1487
|
+
|
|
1488
|
+
const cache = new AngularSourceFileCache();
|
|
1489
|
+
|
|
1490
|
+
const compiler = new AngularCompiler({
|
|
1491
|
+
rootNames: realProgram.rootNames,
|
|
1492
|
+
compilerOptions: {
|
|
1493
|
+
target: ts.ScriptTarget.ESNext,
|
|
1494
|
+
module: ts.ModuleKind.ESNext,
|
|
1495
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1496
|
+
skipLibCheck: true,
|
|
1497
|
+
types: [],
|
|
1498
|
+
},
|
|
1499
|
+
sourceFileCache: cache,
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// 초기 빌드 + diagnostics
|
|
1503
|
+
await compiler.initialize();
|
|
1504
|
+
void [...compiler.collectDiagnostics()];
|
|
1505
|
+
expect(mockGetDiagnosticsForFile).toHaveBeenCalled();
|
|
1506
|
+
|
|
1507
|
+
// 변경 없이 update → diagnosticCache 재사용
|
|
1508
|
+
mockGetDiagnosticsForFile.mockClear();
|
|
1509
|
+
await compiler.update(new Set([]));
|
|
1510
|
+
const diags2 = [...compiler.collectDiagnostics()];
|
|
1511
|
+
|
|
1512
|
+
// diagnostics가 반환된다 (에러 없이 동작)
|
|
1513
|
+
expect(Array.isArray(diags2)).toBe(true);
|
|
1514
|
+
});
|
|
1515
|
+
});
|