Skip to content

Commit f68f56c

Browse files
committed
refactor(@angular/build): encapsulate library build state in BuilderState and ExecutionResult
Introduce `BuilderState` and `ExecutionResult` to replace `SingleBuildState`, `BuildActionResult`, `LibraryCompilationDiagnostics`, and manual `watchedCompilationFiles` parameter threading across the library builder pipeline. `BuilderState` manages incremental compilation state, watched compilation files, pending compilation modifications across aborted watch iterations, and `ExecutionResult` diagnostic collection.
1 parent 2b5a9a6 commit f68f56c

11 files changed

Lines changed: 883 additions & 494 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { ExecutionResult } from './execution-result';
10+
import type {
11+
BundleResult,
12+
LibraryCompilationOutput,
13+
NormalizedLibraryOptions,
14+
SingleProgramCache,
15+
} from './types';
16+
17+
/**
18+
* Encapsulates incremental compilation state, watched compilation file tracking,
19+
* and execution results across watch-mode rebuilds.
20+
*/
21+
export class BuilderState {
22+
readonly #watchedCompilationFiles = new Set<string>();
23+
readonly #pendingCompilationFiles = new Set<string>();
24+
25+
#executionResult = new ExecutionResult();
26+
#singleProgramCache?: SingleProgramCache;
27+
#compilationMode?: 'partial' | 'full';
28+
29+
/** Map of entry point names to their previous bundle results. */
30+
readonly previousBundleResults = new Map<string, BundleResult>();
31+
32+
/** Set of changed POSIX ESM file paths pending bundling. */
33+
readonly pendingChangedEsmFiles = new Set<string>();
34+
35+
/** Set of changed POSIX TypeScript declaration file paths pending bundling. */
36+
readonly pendingChangedDtsFiles = new Set<string>();
37+
38+
/** Whether the previous build iteration encountered a compilation or diagnostic error. */
39+
hasCompilationError = false;
40+
41+
/** Whether package.json and secondary entry point manifests have been emitted. */
42+
hasEmittedManifests = false;
43+
44+
/** Whether the last emitted root package.json included a tslib dependency. */
45+
hasEmittedTslib = false;
46+
47+
/** Whether static assets have been emitted in a prior iteration. */
48+
hasEmittedAssets = false;
49+
50+
/** Whether entry points in package.json changed since the last compilation. */
51+
hasEntryPointsChanges = false;
52+
53+
constructor(
54+
options?: Partial<
55+
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints'>
56+
>,
57+
) {
58+
if (options?.tsConfigPath) {
59+
this.#watchedCompilationFiles.add(options.tsConfigPath);
60+
}
61+
if (options?.packageJsonPath) {
62+
this.#watchedCompilationFiles.add(options.packageJsonPath);
63+
}
64+
if (options?.entryPoints) {
65+
for (const entryPoint of options.entryPoints.values()) {
66+
this.#watchedCompilationFiles.add(entryPoint.entryFilePath);
67+
}
68+
}
69+
}
70+
71+
/** Cached unified TypeScript and Angular compilation state. */
72+
get singleProgramCache(): SingleProgramCache | undefined {
73+
return this.#singleProgramCache;
74+
}
75+
76+
/** Effective compilation mode resolved from builder options and tsconfig. */
77+
get compilationMode(): 'partial' | 'full' | undefined {
78+
return this.#compilationMode;
79+
}
80+
81+
/** Whether any active entry point bundle imports `tslib`. */
82+
get hasTslibImport(): boolean {
83+
for (const result of this.previousBundleResults.values()) {
84+
if (result.hasTslibImport) {
85+
return true;
86+
}
87+
}
88+
89+
return false;
90+
}
91+
92+
/**
93+
* Set of POSIX file paths tracked for compilation and configuration
94+
* (including `tsconfig.json`, `package.json`, entry points, and referenced source,
95+
* template, and stylesheet files).
96+
*/
97+
get watchedCompilationFiles(): Set<string> {
98+
return this.#watchedCompilationFiles;
99+
}
100+
101+
/**
102+
* Execution result holding errors, warnings, and logs for the current build iteration.
103+
*/
104+
get executionResult(): ExecutionResult {
105+
return this.#executionResult;
106+
}
107+
108+
/**
109+
* Resets the execution result for a new build iteration.
110+
*
111+
* @returns The fresh `ExecutionResult` instance for the current build iteration.
112+
*/
113+
resetExecutionResult(): ExecutionResult {
114+
this.#executionResult = new ExecutionResult();
115+
116+
return this.#executionResult;
117+
}
118+
119+
/**
120+
* Determines whether compilation and bundling need to run for the current build iteration.
121+
* Also records incoming modified files so they are retained if the iteration aborts early.
122+
*
123+
* @param packageJsonPath Normalized POSIX path to the library's `package.json`.
124+
* @param modifiedFiles Optional set of modified POSIX file paths in watch mode.
125+
* @returns `true` if entry points should be compiled; otherwise `false`.
126+
*/
127+
shouldCompileEntryPoints(packageJsonPath: string, modifiedFiles?: ReadonlySet<string>): boolean {
128+
if (modifiedFiles) {
129+
for (const file of modifiedFiles) {
130+
if (file !== packageJsonPath) {
131+
this.#pendingCompilationFiles.add(file);
132+
}
133+
}
134+
}
135+
136+
if (
137+
!modifiedFiles ||
138+
!this.#singleProgramCache ||
139+
this.hasCompilationError ||
140+
this.hasEntryPointsChanges ||
141+
this.pendingChangedEsmFiles.size > 0 ||
142+
this.pendingChangedDtsFiles.size > 0
143+
) {
144+
return true;
145+
}
146+
147+
for (const file of this.#pendingCompilationFiles) {
148+
if (this.#watchedCompilationFiles.has(file)) {
149+
return true;
150+
}
151+
}
152+
153+
this.#pendingCompilationFiles.clear();
154+
155+
return false;
156+
}
157+
158+
/**
159+
* Returns the effective set of modified POSIX file paths for compilation,
160+
* merging any pending compilation files from earlier aborted iterations.
161+
*
162+
* @param modifiedFiles Optional set of modified POSIX file paths from the current iteration.
163+
* @returns Merged set of modified POSIX file paths, or `undefined` for a full compilation.
164+
*/
165+
getCompilationModifiedFiles(modifiedFiles?: ReadonlySet<string>): Set<string> | undefined {
166+
if (!modifiedFiles) {
167+
this.#pendingCompilationFiles.clear();
168+
169+
return undefined;
170+
}
171+
172+
const files = new Set(modifiedFiles);
173+
for (const file of this.#pendingCompilationFiles) {
174+
files.add(file);
175+
}
176+
177+
return files;
178+
}
179+
180+
/**
181+
* Records compilation output into the build state, updating the program cache,
182+
* watched compilation files, and pending changed ESM/DTS file sets.
183+
*
184+
* @param output The compilation output produced by `compileLibrary`.
185+
*/
186+
recordCompilationOutput(output: LibraryCompilationOutput): void {
187+
this.#pendingCompilationFiles.clear();
188+
this.#singleProgramCache = output.cache;
189+
if (this.#compilationMode !== output.compilationMode) {
190+
this.#compilationMode = output.compilationMode;
191+
this.hasEmittedManifests = false;
192+
}
193+
194+
for (const file of output.referencedFiles) {
195+
this.#watchedCompilationFiles.add(file);
196+
}
197+
for (const file of output.changedEsmFiles) {
198+
this.pendingChangedEsmFiles.add(file);
199+
}
200+
for (const file of output.changedDtsFiles) {
201+
this.pendingChangedDtsFiles.add(file);
202+
}
203+
}
204+
205+
/**
206+
* Records active bundle results across entry points after compilation and bundling succeed,
207+
* clearing pending compilation/bundle flags.
208+
*
209+
* @param bundleResults Newly bundled results keyed by entry point name.
210+
*/
211+
recordBundleOutput(bundleResults: ReadonlyMap<string, BundleResult>): void {
212+
this.hasCompilationError = false;
213+
this.hasEntryPointsChanges = false;
214+
this.pendingChangedEsmFiles.clear();
215+
this.pendingChangedDtsFiles.clear();
216+
217+
for (const [name, bundleResult] of bundleResults) {
218+
this.previousBundleResults.set(name, bundleResult);
219+
}
220+
}
221+
222+
/**
223+
* Disposes active compilation resources and clears cached state.
224+
*/
225+
async dispose(): Promise<void> {
226+
await this.#singleProgramCache?.compilationInstance.close?.();
227+
this.#singleProgramCache = undefined;
228+
this.#compilationMode = undefined;
229+
this.hasEmittedTslib = false;
230+
this.hasEmittedAssets = false;
231+
this.hasCompilationError = false;
232+
this.hasEmittedManifests = false;
233+
this.hasEntryPointsChanges = false;
234+
this.previousBundleResults.clear();
235+
this.pendingChangedEsmFiles.clear();
236+
this.pendingChangedDtsFiles.clear();
237+
this.#pendingCompilationFiles.clear();
238+
this.#watchedCompilationFiles.clear();
239+
}
240+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { BuilderState } from './builder-state';
10+
import type { LibraryCompilationOutput, SingleProgramCache } from './types';
11+
12+
describe('BuilderState', () => {
13+
const packageJsonPath = '/workspace/projects/my-lib/package.json';
14+
const tsConfigPath = '/workspace/projects/my-lib/tsconfig.lib.json';
15+
const entryFilePath = '/workspace/projects/my-lib/src/public-api.ts';
16+
17+
function createMockCache(closeSpy?: jasmine.Spy): SingleProgramCache {
18+
return {
19+
compilationInstance: {
20+
close: closeSpy,
21+
} as unknown as SingleProgramCache['compilationInstance'],
22+
esmFiles: new Map(),
23+
dtsFiles: new Map(),
24+
failedFiles: new Set(),
25+
};
26+
}
27+
28+
function createMockCompilationOutput(
29+
overrides: Partial<LibraryCompilationOutput> = {},
30+
): LibraryCompilationOutput {
31+
return {
32+
esmFiles: new Map(),
33+
dtsFiles: new Map(),
34+
changedEsmFiles: new Set(),
35+
changedDtsFiles: new Set(),
36+
referencedFiles: new Set(),
37+
cache: createMockCache(),
38+
hasStylesheetErrors: false,
39+
diagnosePromise: Promise.resolve(),
40+
declarationMap: false,
41+
compilationMode: 'partial',
42+
...overrides,
43+
};
44+
}
45+
46+
describe('ExecutionResult', () => {
47+
it('should track logs, warnings, and errors and reset per iteration', () => {
48+
const state = new BuilderState();
49+
expect(state.executionResult.hasErrors).toBeFalse();
50+
expect(state.executionResult.output).toEqual({ success: true });
51+
52+
state.executionResult.addLog('Bundling my-lib');
53+
state.executionResult.addWarning('Unused import');
54+
state.executionResult.addWarnings(['Deprecated API']);
55+
state.executionResult.addError('Type error');
56+
state.executionResult.addErrors(['Syntax error']);
57+
58+
expect(state.executionResult.logs).toEqual(['Bundling my-lib']);
59+
expect(state.executionResult.warnings).toEqual(['Unused import', 'Deprecated API']);
60+
expect(state.executionResult.errors).toEqual(['Type error', 'Syntax error']);
61+
expect(state.executionResult.hasErrors).toBeTrue();
62+
expect(state.executionResult.output).toEqual({ success: false });
63+
64+
const freshResult = state.resetExecutionResult();
65+
expect(freshResult).toBe(state.executionResult);
66+
expect(freshResult.hasErrors).toBeFalse();
67+
expect(freshResult.logs).toEqual([]);
68+
expect(freshResult.warnings).toEqual([]);
69+
expect(freshResult.errors).toEqual([]);
70+
});
71+
});
72+
73+
describe('shouldCompileEntryPoints & getCompilationModifiedFiles', () => {
74+
it('should preserve pending compilation files across aborted iterations', () => {
75+
const state = new BuilderState({
76+
packageJsonPath,
77+
tsConfigPath,
78+
entryPoints: new Map([
79+
[
80+
'.',
81+
{
82+
subpath: '.',
83+
name: '.',
84+
displayName: 'my-lib',
85+
bundleName: 'my-lib',
86+
entryFilePath,
87+
isPrimary: true,
88+
},
89+
],
90+
]),
91+
});
92+
93+
state.recordCompilationOutput(
94+
createMockCompilationOutput({
95+
referencedFiles: new Set([entryFilePath]),
96+
}),
97+
);
98+
99+
// Iteration 1: entryFilePath changes, but build aborts before recordCompilationOutput
100+
expect(
101+
state.shouldCompileEntryPoints(packageJsonPath, new Set([entryFilePath, packageJsonPath])),
102+
).toBeTrue();
103+
104+
// Iteration 2: only package.json changes on recovery
105+
expect(
106+
state.shouldCompileEntryPoints(packageJsonPath, new Set([packageJsonPath])),
107+
).toBeTrue();
108+
expect(state.getCompilationModifiedFiles(new Set([packageJsonPath]))).toEqual(
109+
new Set([packageJsonPath, entryFilePath]),
110+
);
111+
112+
// Once compilation output is recorded, pending compilation files are cleared
113+
state.recordCompilationOutput(createMockCompilationOutput());
114+
expect(
115+
state.shouldCompileEntryPoints(packageJsonPath, new Set([packageJsonPath])),
116+
).toBeFalse();
117+
});
118+
119+
it('should invalidate hasEmittedManifests when compilationMode changes', () => {
120+
const state = new BuilderState();
121+
state.hasEmittedManifests = true;
122+
state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' }));
123+
expect(state.hasEmittedManifests).toBeFalse();
124+
125+
state.hasEmittedManifests = true;
126+
state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' }));
127+
expect(state.hasEmittedManifests).toBeTrue();
128+
129+
state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'full' }));
130+
expect(state.hasEmittedManifests).toBeFalse();
131+
});
132+
});
133+
134+
describe('dispose', () => {
135+
it('should close compilationInstance and clear cached state on dispose', async () => {
136+
const closeSpy = jasmine.createSpy('close').and.resolveTo();
137+
const state = new BuilderState({ packageJsonPath });
138+
state.recordCompilationOutput(
139+
createMockCompilationOutput({
140+
cache: createMockCache(closeSpy),
141+
changedEsmFiles: new Set(['/a.js']),
142+
changedDtsFiles: new Set(['/a.d.ts']),
143+
}),
144+
);
145+
146+
await state.dispose();
147+
148+
expect(closeSpy).toHaveBeenCalledTimes(1);
149+
expect(state.singleProgramCache).toBeUndefined();
150+
expect(state.pendingChangedEsmFiles).toHaveSize(0);
151+
expect(state.pendingChangedDtsFiles).toHaveSize(0);
152+
expect(state.watchedCompilationFiles).toHaveSize(0);
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)