@simplysm/sd-cli 14.0.38 → 14.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/angular-build-pipeline.d.ts +1 -1
- package/dist/angular/angular-build-pipeline.js +1 -1
- package/dist/angular/client-transform-stylesheet.d.ts +1 -1
- package/dist/angular/client-transform-stylesheet.js +3 -3
- package/dist/esbuild/esbuild-client-config.d.ts +0 -2
- package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +19 -9
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
- package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
- package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
- package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
- package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
- package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +32 -2
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +129 -90
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts +27 -0
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +43 -3
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/package.json +6 -4
- package/src/angular/angular-build-pipeline.ts +2 -2
- package/src/angular/client-transform-stylesheet.ts +4 -4
- package/src/esbuild/esbuild-client-config.ts +21 -12
- package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
- package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
- package/src/workers/client.worker.ts +32 -2
- package/src/workers/server-build.worker.ts +136 -97
- package/src/workers/server-esbuild-context.ts +59 -3
- package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
- package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
- package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
- package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
- package/tests/utils/esbuild-client-config.acc.spec.ts +26 -14
- package/tests/utils/esbuild-client-config.spec.ts +73 -11
- package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
- package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
- package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
- package/tests/workers/server-build-lint.spec.ts +43 -0
- package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
- package/tests/workers/server-build-worker.spec.ts +122 -9
- package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
- package/tests/workers/server-esbuild-context.acc.spec.ts +156 -2
- package/tests/workers/server-esbuild-context.spec.ts +320 -2
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
1
2
|
import esbuild from "esbuild";
|
|
2
3
|
import {
|
|
3
4
|
createServerEsbuildOptions,
|
|
4
5
|
writeChangedOutputFiles,
|
|
5
6
|
} from "../esbuild/esbuild-config";
|
|
7
|
+
import { createTscPlugin, type TscPluginResult } from "../esbuild/esbuild-tsc-plugin";
|
|
8
|
+
import type { TypecheckEnv } from "../utils/tsconfig";
|
|
9
|
+
import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* esbuild watch context 생성 옵션
|
|
@@ -12,6 +16,13 @@ export interface EsbuildContextOptions {
|
|
|
12
16
|
entryPoints: string[];
|
|
13
17
|
env?: Record<string, string>;
|
|
14
18
|
external: string[];
|
|
19
|
+
/** tsc 플러그인 옵션. 제공 시 createTscPlugin으로 플러그인을 생성하여 esbuild context에 포함한다. */
|
|
20
|
+
tsc?: {
|
|
21
|
+
cwd: string;
|
|
22
|
+
output: { dts: boolean };
|
|
23
|
+
env?: TypecheckEnv;
|
|
24
|
+
includeTests?: boolean;
|
|
25
|
+
};
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
/** esbuild watch context (모듈 스코프 상태) */
|
|
@@ -20,11 +31,24 @@ let context: esbuild.BuildContext | undefined;
|
|
|
20
31
|
/** 마지막 빌드의 metafile (변경 필터링용) */
|
|
21
32
|
let lastMetafile: esbuild.Metafile | undefined;
|
|
22
33
|
|
|
34
|
+
/** tsc 플러그인 인스턴스 (모듈 스코프 상태) */
|
|
35
|
+
let tscPlugin: TscPluginResult | undefined;
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* esbuild watch context를 생성한다.
|
|
25
39
|
* dev 모드 전용 (metafile:true, write:false).
|
|
26
40
|
*/
|
|
27
41
|
export async function createContext(options: EsbuildContextOptions): Promise<void> {
|
|
42
|
+
if (options.tsc != null) {
|
|
43
|
+
tscPlugin = createTscPlugin({
|
|
44
|
+
pkgDir: options.pkgDir,
|
|
45
|
+
cwd: options.tsc.cwd,
|
|
46
|
+
output: options.tsc.output,
|
|
47
|
+
env: options.tsc.env,
|
|
48
|
+
includeTests: options.tsc.includeTests,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
const baseOptions = createServerEsbuildOptions({
|
|
29
53
|
pkgDir: options.pkgDir,
|
|
30
54
|
entryPoints: options.entryPoints,
|
|
@@ -35,6 +59,7 @@ export async function createContext(options: EsbuildContextOptions): Promise<voi
|
|
|
35
59
|
|
|
36
60
|
context = await esbuild.context({
|
|
37
61
|
...baseOptions,
|
|
62
|
+
plugins: tscPlugin != null ? [tscPlugin.plugin] : [],
|
|
38
63
|
metafile: true,
|
|
39
64
|
write: false,
|
|
40
65
|
});
|
|
@@ -61,12 +86,14 @@ export async function rebuild(): Promise<{
|
|
|
61
86
|
await writeChangedOutputFiles(result.outputFiles);
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
const
|
|
89
|
+
const esbuildErrors = result.errors.map((e) => e.text);
|
|
90
|
+
const tscErrors = tscPlugin?.getErrors() ?? [];
|
|
91
|
+
const allErrors = [...esbuildErrors, ...tscErrors];
|
|
65
92
|
const warnings = result.warnings.map((w) => w.text);
|
|
66
93
|
|
|
67
94
|
return {
|
|
68
|
-
success:
|
|
69
|
-
errors:
|
|
95
|
+
success: allErrors.length === 0,
|
|
96
|
+
errors: allErrors.length > 0 ? allErrors : undefined,
|
|
70
97
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
71
98
|
};
|
|
72
99
|
}
|
|
@@ -85,6 +112,10 @@ export async function recreateContext(options: EsbuildContextOptions): Promise<v
|
|
|
85
112
|
context = undefined;
|
|
86
113
|
lastMetafile = undefined;
|
|
87
114
|
|
|
115
|
+
if (tscPlugin != null) {
|
|
116
|
+
tscPlugin.resetBuilderProgram();
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
try {
|
|
89
120
|
await createContext(options);
|
|
90
121
|
} finally {
|
|
@@ -101,6 +132,7 @@ export async function dispose(): Promise<void> {
|
|
|
101
132
|
const contextToDispose = context;
|
|
102
133
|
context = undefined;
|
|
103
134
|
lastMetafile = undefined;
|
|
135
|
+
tscPlugin = undefined;
|
|
104
136
|
|
|
105
137
|
if (contextToDispose != null) {
|
|
106
138
|
await contextToDispose.dispose();
|
|
@@ -120,3 +152,27 @@ export function getMetafile(): esbuild.Metafile | undefined {
|
|
|
120
152
|
export function hasContext(): boolean {
|
|
121
153
|
return context != null;
|
|
122
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* tsc 플러그인의 ts.Program을 반환한다.
|
|
158
|
+
* 플러그인이 없으면 undefined를 반환한다.
|
|
159
|
+
*/
|
|
160
|
+
export function getTscProgram(): ts.Program | undefined {
|
|
161
|
+
return tscPlugin?.getProgram();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* tsc 플러그인의 affected files를 반환한다.
|
|
166
|
+
* 플러그인이 없으면 undefined를 반환한다.
|
|
167
|
+
*/
|
|
168
|
+
export function getTscAffectedFiles(): ReadonlySet<string> | undefined {
|
|
169
|
+
return tscPlugin?.getAffectedFiles();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* tsc 플러그인의 diagnostics를 반환한다.
|
|
174
|
+
* 플러그인이 없으면 빈 배열을 반환한다.
|
|
175
|
+
*/
|
|
176
|
+
export function getTscDiagnostics(): SerializedDiagnostic[] {
|
|
177
|
+
return tscPlugin?.getDiagnostics() ?? [];
|
|
178
|
+
}
|
|
@@ -118,7 +118,7 @@ describe("createClientTransformStylesheet", () => {
|
|
|
118
118
|
const deps = new Map<string, Set<string>>();
|
|
119
119
|
const transform = createClientTransformStylesheet({
|
|
120
120
|
loadPaths: [],
|
|
121
|
-
|
|
121
|
+
postcssPlugins: [testPlugin],
|
|
122
122
|
scssErrors: errors,
|
|
123
123
|
scssDependencies: deps,
|
|
124
124
|
});
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type esbuild from "esbuild";
|
|
3
|
+
import type { TscPackageBuildResult } from "../../src/utils/tsc-build";
|
|
4
|
+
|
|
5
|
+
//#region Mocks
|
|
6
|
+
|
|
7
|
+
const mockRunTscPackageBuild = vi.fn<(...args: unknown[]) => TscPackageBuildResult>();
|
|
8
|
+
|
|
9
|
+
vi.mock("../../src/utils/tsc-build", () => ({
|
|
10
|
+
runTscPackageBuild: (...args: unknown[]) => mockRunTscPackageBuild(...args),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockParseTsconfig = vi.fn();
|
|
14
|
+
|
|
15
|
+
vi.mock("../../src/utils/tsconfig", async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal<typeof import("../../src/utils/tsconfig")>();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
parseTsconfig: (...args: unknown[]) => mockParseTsconfig(...args),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
|
|
25
|
+
const { createTscPlugin } = await import("../../src/esbuild/esbuild-tsc-plugin");
|
|
26
|
+
|
|
27
|
+
/** esbuild 플러그인 lifecycle을 시뮬레이션하는 헬퍼 */
|
|
28
|
+
function setupPlugin(plugin: esbuild.Plugin) {
|
|
29
|
+
let onStartCb: (() => esbuild.OnStartResult | null | void | Promise<esbuild.OnStartResult | null | void>) | undefined;
|
|
30
|
+
let onEndCb: ((result: esbuild.BuildResult) => esbuild.OnEndResult | null | void | Promise<esbuild.OnEndResult | null | void>) | undefined;
|
|
31
|
+
|
|
32
|
+
const mockBuild = {
|
|
33
|
+
onStart(cb: typeof onStartCb) { onStartCb = cb; },
|
|
34
|
+
onEnd(cb: typeof onEndCb) { onEndCb = cb; },
|
|
35
|
+
} as unknown as esbuild.PluginBuild;
|
|
36
|
+
|
|
37
|
+
void plugin.setup(mockBuild);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
async invokeOnStart() {
|
|
41
|
+
return (await onStartCb?.()) ?? null;
|
|
42
|
+
},
|
|
43
|
+
async invokeOnEnd(result?: Partial<esbuild.BuildResult>) {
|
|
44
|
+
return (await onEndCb?.({
|
|
45
|
+
errors: [],
|
|
46
|
+
warnings: [],
|
|
47
|
+
mangleCache: {},
|
|
48
|
+
outputFiles: [],
|
|
49
|
+
metafile: { inputs: {}, outputs: {} },
|
|
50
|
+
...result,
|
|
51
|
+
} as esbuild.BuildResult)) ?? null;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const baseOptions = {
|
|
57
|
+
pkgDir: "/workspace/packages/my-server",
|
|
58
|
+
cwd: "/workspace",
|
|
59
|
+
output: { dts: true },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const mockParsedConfig = {
|
|
63
|
+
options: { target: 99 },
|
|
64
|
+
fileNames: [],
|
|
65
|
+
errors: [],
|
|
66
|
+
} as any;
|
|
67
|
+
|
|
68
|
+
function createSuccessTscResult(): TscPackageBuildResult {
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
diagnostics: [],
|
|
72
|
+
errorCount: 0,
|
|
73
|
+
warningCount: 0,
|
|
74
|
+
program: { getSourceFiles: () => [] } as any,
|
|
75
|
+
affectedFiles: new Set(["/workspace/packages/my-server/src/main.ts"]),
|
|
76
|
+
builderProgram: {} as any,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createErrorTscResult(): TscPackageBuildResult {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
errors: ["TS2322: Type 'string' is not assignable to type 'number'"],
|
|
84
|
+
diagnostics: [{ category: 1, code: 2322, messageText: "Type mismatch" }],
|
|
85
|
+
errorCount: 1,
|
|
86
|
+
warningCount: 0,
|
|
87
|
+
program: { getSourceFiles: () => [] } as any,
|
|
88
|
+
affectedFiles: new Set(["/workspace/packages/my-server/src/main.ts"]),
|
|
89
|
+
builderProgram: {} as any,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("createTscPlugin — Acceptance Tests", () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
vi.clearAllMocks();
|
|
96
|
+
mockParseTsconfig.mockReturnValue(mockParsedConfig);
|
|
97
|
+
mockRunTscPackageBuild.mockReturnValue(createSuccessTscResult());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Rule: createTscPlugin은 esbuild.Plugin과 getter 객체를 반환한다
|
|
101
|
+
describe("Scenario: 필수 옵션으로 플러그인 생성", () => {
|
|
102
|
+
it('plugin.name이 "sd-tsc"이고 getter 함수들을 반환한다', () => {
|
|
103
|
+
const result = createTscPlugin(baseOptions);
|
|
104
|
+
|
|
105
|
+
expect(result.plugin.name).toBe("sd-tsc");
|
|
106
|
+
expect(typeof result.plugin.setup).toBe("function");
|
|
107
|
+
expect(typeof result.getProgram).toBe("function");
|
|
108
|
+
expect(typeof result.getAffectedFiles).toBe("function");
|
|
109
|
+
expect(typeof result.getDiagnostics).toBe("function");
|
|
110
|
+
expect(typeof result.getErrors).toBe("function");
|
|
111
|
+
expect(typeof result.resetBuilderProgram).toBe("function");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Scenario: 선택 옵션 포함 플러그인 생성", () => {
|
|
116
|
+
it("env, includeTests 옵션이 runTscPackageBuild에 전달된다", async () => {
|
|
117
|
+
const result = createTscPlugin({
|
|
118
|
+
...baseOptions,
|
|
119
|
+
env: "node",
|
|
120
|
+
includeTests: true,
|
|
121
|
+
});
|
|
122
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
123
|
+
|
|
124
|
+
await lifecycle.invokeOnStart();
|
|
125
|
+
await lifecycle.invokeOnEnd();
|
|
126
|
+
|
|
127
|
+
expect(mockRunTscPackageBuild).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
env: "node",
|
|
130
|
+
includeTests: true,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Rule: onStart에서 tsc를 microtask로 스케줄링하고 await하지 않는다
|
|
137
|
+
describe("Scenario: tsc microtask 스케줄링", () => {
|
|
138
|
+
it("onStart는 tsc 완료를 기다리지 않고 즉시 반환한다", async () => {
|
|
139
|
+
const result = createTscPlugin(baseOptions);
|
|
140
|
+
|
|
141
|
+
// onStart 콜백을 직접 캡처하여 동기적으로 호출
|
|
142
|
+
let onStartCb!: () => void;
|
|
143
|
+
const mockBuild = {
|
|
144
|
+
onStart(cb: () => void) { onStartCb = cb; },
|
|
145
|
+
onEnd() { /* noop */ },
|
|
146
|
+
} as unknown as esbuild.PluginBuild;
|
|
147
|
+
void result.plugin.setup(mockBuild);
|
|
148
|
+
|
|
149
|
+
// onStart 동기 호출 — 반환값이 undefined (await하지 않음)
|
|
150
|
+
const onStartResult = onStartCb();
|
|
151
|
+
expect(onStartResult).toBeUndefined();
|
|
152
|
+
|
|
153
|
+
// onStart 반환 직후, microtask 전이므로 tsc 미호출
|
|
154
|
+
expect(mockRunTscPackageBuild).not.toHaveBeenCalled();
|
|
155
|
+
|
|
156
|
+
// microtask flush 후 tsc 호출됨
|
|
157
|
+
await Promise.resolve();
|
|
158
|
+
expect(mockRunTscPackageBuild).toHaveBeenCalledOnce();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("Scenario: parsedConfig 갱신", () => {
|
|
163
|
+
it("매 onStart마다 parseTsconfig를 호출하여 최신 tsconfig를 반영한다", async () => {
|
|
164
|
+
const result = createTscPlugin(baseOptions);
|
|
165
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
166
|
+
|
|
167
|
+
// 첫 번째 빌드
|
|
168
|
+
await lifecycle.invokeOnStart();
|
|
169
|
+
await lifecycle.invokeOnEnd();
|
|
170
|
+
|
|
171
|
+
// 두 번째 빌드
|
|
172
|
+
const updatedConfig = { ...mockParsedConfig, fileNames: ["/new-file.ts"] };
|
|
173
|
+
mockParseTsconfig.mockReturnValue(updatedConfig);
|
|
174
|
+
|
|
175
|
+
await lifecycle.invokeOnStart();
|
|
176
|
+
await lifecycle.invokeOnEnd();
|
|
177
|
+
|
|
178
|
+
expect(mockParseTsconfig).toHaveBeenCalledTimes(2);
|
|
179
|
+
expect(mockParseTsconfig).toHaveBeenCalledWith(baseOptions.pkgDir);
|
|
180
|
+
expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
|
|
181
|
+
expect.objectContaining({ parsedConfig: updatedConfig }),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Rule: onEnd에서 tsc Promise를 await하여 결과를 내부 상태에 저장한다
|
|
187
|
+
describe("Scenario: tsc 정상 완료", () => {
|
|
188
|
+
it("program, affectedFiles, diagnostics를 저장하고 errors는 undefined", async () => {
|
|
189
|
+
const tscResult = createSuccessTscResult();
|
|
190
|
+
mockRunTscPackageBuild.mockReturnValue(tscResult);
|
|
191
|
+
|
|
192
|
+
const result = createTscPlugin(baseOptions);
|
|
193
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
194
|
+
|
|
195
|
+
await lifecycle.invokeOnStart();
|
|
196
|
+
await lifecycle.invokeOnEnd();
|
|
197
|
+
|
|
198
|
+
expect(result.getProgram()).toBe(tscResult.program);
|
|
199
|
+
expect(result.getAffectedFiles()).toBe(tscResult.affectedFiles);
|
|
200
|
+
expect(result.getDiagnostics()).toEqual([]);
|
|
201
|
+
expect(result.getErrors()).toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("Scenario: tsc 타입 에러 발생", () => {
|
|
206
|
+
it("errors를 string[]로, diagnostics를 SerializedDiagnostic[]로 저장한다", async () => {
|
|
207
|
+
const tscResult = createErrorTscResult();
|
|
208
|
+
mockRunTscPackageBuild.mockReturnValue(tscResult);
|
|
209
|
+
|
|
210
|
+
const result = createTscPlugin(baseOptions);
|
|
211
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
212
|
+
|
|
213
|
+
await lifecycle.invokeOnStart();
|
|
214
|
+
await lifecycle.invokeOnEnd();
|
|
215
|
+
|
|
216
|
+
expect(result.getErrors()).toEqual(["TS2322: Type 'string' is not assignable to type 'number'"]);
|
|
217
|
+
expect(result.getDiagnostics()).toEqual([{ category: 1, code: 2322, messageText: "Type mismatch" }]);
|
|
218
|
+
expect(result.getProgram()).toBe(tscResult.program);
|
|
219
|
+
expect(result.getAffectedFiles()).toBe(tscResult.affectedFiles);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Scenario: tsc 예외 발생", () => {
|
|
224
|
+
it("try-catch로 포착하여 errors에 메시지 저장, program/affectedFiles는 undefined", async () => {
|
|
225
|
+
mockRunTscPackageBuild.mockImplementation(() => {
|
|
226
|
+
throw new Error("tsconfig parse failed");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const result = createTscPlugin(baseOptions);
|
|
230
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
231
|
+
|
|
232
|
+
await lifecycle.invokeOnStart();
|
|
233
|
+
await lifecycle.invokeOnEnd();
|
|
234
|
+
|
|
235
|
+
expect(result.getErrors()).toEqual(["tsconfig parse failed"]);
|
|
236
|
+
expect(result.getDiagnostics()).toEqual([]);
|
|
237
|
+
expect(result.getProgram()).toBeUndefined();
|
|
238
|
+
expect(result.getAffectedFiles()).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Rule: result.errors에 push하지 않는다
|
|
243
|
+
describe("Scenario: tsc 에러가 있어도 result.errors는 변경하지 않음", () => {
|
|
244
|
+
it("onEnd의 result.errors에 tsc 에러를 push하지 않는다", async () => {
|
|
245
|
+
mockRunTscPackageBuild.mockReturnValue(createErrorTscResult());
|
|
246
|
+
|
|
247
|
+
const result = createTscPlugin(baseOptions);
|
|
248
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
249
|
+
|
|
250
|
+
await lifecycle.invokeOnStart();
|
|
251
|
+
|
|
252
|
+
const buildResult = {
|
|
253
|
+
errors: [] as esbuild.Message[],
|
|
254
|
+
warnings: [] as esbuild.Message[],
|
|
255
|
+
};
|
|
256
|
+
await lifecycle.invokeOnEnd(buildResult);
|
|
257
|
+
|
|
258
|
+
// result.errors는 비어있어야 함 (tsc 에러가 push되지 않음)
|
|
259
|
+
expect(buildResult.errors).toEqual([]);
|
|
260
|
+
// tsc 에러는 getErrors()로만 조회 가능
|
|
261
|
+
expect(result.getErrors()).toBeDefined();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Rule: getter는 마지막 빌드 결과를 반환한다
|
|
266
|
+
describe("Scenario: 빌드 전 getter 호출", () => {
|
|
267
|
+
it("빌드 실행 전 기본값을 반환한다", () => {
|
|
268
|
+
const result = createTscPlugin(baseOptions);
|
|
269
|
+
|
|
270
|
+
expect(result.getProgram()).toBeUndefined();
|
|
271
|
+
expect(result.getAffectedFiles()).toBeUndefined();
|
|
272
|
+
expect(result.getDiagnostics()).toEqual([]);
|
|
273
|
+
expect(result.getErrors()).toBeUndefined();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("Scenario: 빌드 후 getter 호출", () => {
|
|
278
|
+
it("tsc 빌드 성공 후 program과 affectedFiles를 반환한다", async () => {
|
|
279
|
+
const tscResult = createSuccessTscResult();
|
|
280
|
+
mockRunTscPackageBuild.mockReturnValue(tscResult);
|
|
281
|
+
|
|
282
|
+
const result = createTscPlugin(baseOptions);
|
|
283
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
284
|
+
|
|
285
|
+
await lifecycle.invokeOnStart();
|
|
286
|
+
await lifecycle.invokeOnEnd();
|
|
287
|
+
|
|
288
|
+
expect(result.getProgram()).toBe(tscResult.program);
|
|
289
|
+
expect(result.getAffectedFiles()).toEqual(new Set(["/workspace/packages/my-server/src/main.ts"]));
|
|
290
|
+
expect(result.getDiagnostics()).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Rule: lastBuilderProgram을 캐싱하여 watch 모드 증분 빌드를 지원한다
|
|
295
|
+
describe("Scenario: 증분 빌드 — builderProgram 재사용", () => {
|
|
296
|
+
it("첫 번째 빌드 후 캐싱된 builderProgram을 두 번째 빌드에 oldBuilderProgram으로 전달한다", async () => {
|
|
297
|
+
const firstBuilderProgram = { kind: "first" } as any;
|
|
298
|
+
mockRunTscPackageBuild.mockReturnValue({
|
|
299
|
+
...createSuccessTscResult(),
|
|
300
|
+
builderProgram: firstBuilderProgram,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = createTscPlugin(baseOptions);
|
|
304
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
305
|
+
|
|
306
|
+
// 첫 번째 빌드
|
|
307
|
+
await lifecycle.invokeOnStart();
|
|
308
|
+
await lifecycle.invokeOnEnd();
|
|
309
|
+
|
|
310
|
+
// 두 번째 빌드
|
|
311
|
+
await lifecycle.invokeOnStart();
|
|
312
|
+
await lifecycle.invokeOnEnd();
|
|
313
|
+
|
|
314
|
+
// 두 번째 호출에서 oldBuilderProgram이 첫 번째 결과의 builderProgram
|
|
315
|
+
expect(mockRunTscPackageBuild).toHaveBeenCalledTimes(2);
|
|
316
|
+
expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
|
|
317
|
+
expect.objectContaining({ oldBuilderProgram: firstBuilderProgram }),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("Scenario: builderProgram 리셋", () => {
|
|
323
|
+
it("resetBuilderProgram 호출 후 다음 빌드는 fresh build로 실행된다", async () => {
|
|
324
|
+
const cachedBuilderProgram = { kind: "cached" } as any;
|
|
325
|
+
mockRunTscPackageBuild.mockReturnValue({
|
|
326
|
+
...createSuccessTscResult(),
|
|
327
|
+
builderProgram: cachedBuilderProgram,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = createTscPlugin(baseOptions);
|
|
331
|
+
const lifecycle = setupPlugin(result.plugin);
|
|
332
|
+
|
|
333
|
+
// 첫 번째 빌드 — builderProgram 캐싱
|
|
334
|
+
await lifecycle.invokeOnStart();
|
|
335
|
+
await lifecycle.invokeOnEnd();
|
|
336
|
+
|
|
337
|
+
// 리셋
|
|
338
|
+
result.resetBuilderProgram();
|
|
339
|
+
|
|
340
|
+
// 다음 빌드 — oldBuilderProgram이 undefined (fresh build)
|
|
341
|
+
await lifecycle.invokeOnStart();
|
|
342
|
+
await lifecycle.invokeOnEnd();
|
|
343
|
+
|
|
344
|
+
expect(mockRunTscPackageBuild).toHaveBeenLastCalledWith(
|
|
345
|
+
expect.objectContaining({ oldBuilderProgram: undefined }),
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|