Repository navigation
refactor(@angular/build): encapsulate library build state in BuilderState and ExecutionResult #34355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
refactor(@angular/build): encapsulate library build state in BuilderState and ExecutionResult #34355
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
240 changes: 240 additions & 0 deletions
240
packages/angular/build/src/builders/library/builder-state.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import { ExecutionResult } from './execution-result'; | ||
| import type { | ||
| BundleResult, | ||
| LibraryCompilationOutput, | ||
| NormalizedLibraryOptions, | ||
| SingleProgramCache, | ||
| } from './types'; | ||
|
|
||
| /** | ||
| * Encapsulates incremental compilation state, watched compilation file tracking, | ||
| * and execution results across watch-mode rebuilds. | ||
| */ | ||
| export class BuilderState { | ||
| readonly #watchedCompilationFiles = new Set<string>(); | ||
| readonly #pendingCompilationFiles = new Set<string>(); | ||
|
|
||
| #executionResult = new ExecutionResult(); | ||
| #singleProgramCache?: SingleProgramCache; | ||
| #compilationMode?: 'partial' | 'full'; | ||
|
|
||
| /** Map of entry point names to their previous bundle results. */ | ||
| readonly previousBundleResults = new Map<string, BundleResult>(); | ||
|
|
||
| /** Set of changed POSIX ESM file paths pending bundling. */ | ||
| readonly pendingChangedEsmFiles = new Set<string>(); | ||
|
|
||
| /** Set of changed POSIX TypeScript declaration file paths pending bundling. */ | ||
| readonly pendingChangedDtsFiles = new Set<string>(); | ||
|
|
||
| /** Whether the previous build iteration encountered a compilation or diagnostic error. */ | ||
| hasCompilationError = false; | ||
|
|
||
| /** Whether package.json and secondary entry point manifests have been emitted. */ | ||
| hasEmittedManifests = false; | ||
|
|
||
| /** Whether the last emitted root package.json included a tslib dependency. */ | ||
| hasEmittedTslib = false; | ||
|
|
||
| /** Whether static assets have been emitted in a prior iteration. */ | ||
| hasEmittedAssets = false; | ||
|
|
||
| /** Whether entry points in package.json changed since the last compilation. */ | ||
| hasEntryPointsChanges = false; | ||
|
|
||
| constructor( | ||
| options?: Partial< | ||
| Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints'> | ||
| >, | ||
| ) { | ||
| if (options?.tsConfigPath) { | ||
| this.#watchedCompilationFiles.add(options.tsConfigPath); | ||
| } | ||
| if (options?.packageJsonPath) { | ||
| this.#watchedCompilationFiles.add(options.packageJsonPath); | ||
| } | ||
| if (options?.entryPoints) { | ||
| for (const entryPoint of options.entryPoints.values()) { | ||
| this.#watchedCompilationFiles.add(entryPoint.entryFilePath); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Cached unified TypeScript and Angular compilation state. */ | ||
| get singleProgramCache(): SingleProgramCache | undefined { | ||
| return this.#singleProgramCache; | ||
| } | ||
|
|
||
| /** Effective compilation mode resolved from builder options and tsconfig. */ | ||
| get compilationMode(): 'partial' | 'full' | undefined { | ||
| return this.#compilationMode; | ||
| } | ||
|
|
||
| /** Whether any active entry point bundle imports `tslib`. */ | ||
| get hasTslibImport(): boolean { | ||
| for (const result of this.previousBundleResults.values()) { | ||
| if (result.hasTslibImport) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Set of POSIX file paths tracked for compilation and configuration | ||
| * (including `tsconfig.json`, `package.json`, entry points, and referenced source, | ||
| * template, and stylesheet files). | ||
| */ | ||
| get watchedCompilationFiles(): Set<string> { | ||
| return this.#watchedCompilationFiles; | ||
| } | ||
|
|
||
| /** | ||
| * Execution result holding errors, warnings, and logs for the current build iteration. | ||
| */ | ||
| get executionResult(): ExecutionResult { | ||
| return this.#executionResult; | ||
| } | ||
|
|
||
| /** | ||
| * Resets the execution result for a new build iteration. | ||
| * | ||
| * @returns The fresh `ExecutionResult` instance for the current build iteration. | ||
| */ | ||
| resetExecutionResult(): ExecutionResult { | ||
| this.#executionResult = new ExecutionResult(); | ||
|
|
||
| return this.#executionResult; | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether compilation and bundling need to run for the current build iteration. | ||
| * Also records incoming modified files so they are retained if the iteration aborts early. | ||
| * | ||
| * @param packageJsonPath Normalized POSIX path to the library's `package.json`. | ||
| * @param modifiedFiles Optional set of modified POSIX file paths in watch mode. | ||
| * @returns `true` if entry points should be compiled; otherwise `false`. | ||
| */ | ||
| shouldCompileEntryPoints(packageJsonPath: string, modifiedFiles?: ReadonlySet<string>): boolean { | ||
| if (modifiedFiles) { | ||
| for (const file of modifiedFiles) { | ||
| if (file !== packageJsonPath) { | ||
| this.#pendingCompilationFiles.add(file); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if ( | ||
| !modifiedFiles || | ||
| !this.#singleProgramCache || | ||
| this.hasCompilationError || | ||
| this.hasEntryPointsChanges || | ||
| this.pendingChangedEsmFiles.size > 0 || | ||
| this.pendingChangedDtsFiles.size > 0 | ||
| ) { | ||
| return true; | ||
| } | ||
|
|
||
| for (const file of this.#pendingCompilationFiles) { | ||
| if (this.#watchedCompilationFiles.has(file)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| this.#pendingCompilationFiles.clear(); | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the effective set of modified POSIX file paths for compilation, | ||
| * merging any pending compilation files from earlier aborted iterations. | ||
| * | ||
| * @param modifiedFiles Optional set of modified POSIX file paths from the current iteration. | ||
| * @returns Merged set of modified POSIX file paths, or `undefined` for a full compilation. | ||
| */ | ||
| getCompilationModifiedFiles(modifiedFiles?: ReadonlySet<string>): Set<string> | undefined { | ||
| if (!modifiedFiles) { | ||
| this.#pendingCompilationFiles.clear(); | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| const files = new Set(modifiedFiles); | ||
| for (const file of this.#pendingCompilationFiles) { | ||
| files.add(file); | ||
| } | ||
|
|
||
| return files; | ||
| } | ||
|
|
||
| /** | ||
| * Records compilation output into the build state, updating the program cache, | ||
| * watched compilation files, and pending changed ESM/DTS file sets. | ||
| * | ||
| * @param output The compilation output produced by `compileLibrary`. | ||
| */ | ||
| recordCompilationOutput(output: LibraryCompilationOutput): void { | ||
| this.#pendingCompilationFiles.clear(); | ||
| this.#singleProgramCache = output.cache; | ||
| if (this.#compilationMode !== output.compilationMode) { | ||
| this.#compilationMode = output.compilationMode; | ||
| this.hasEmittedManifests = false; | ||
| } | ||
|
|
||
| for (const file of output.referencedFiles) { | ||
| this.#watchedCompilationFiles.add(file); | ||
| } | ||
| for (const file of output.changedEsmFiles) { | ||
| this.pendingChangedEsmFiles.add(file); | ||
| } | ||
| for (const file of output.changedDtsFiles) { | ||
| this.pendingChangedDtsFiles.add(file); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Records active bundle results across entry points after compilation and bundling succeed, | ||
| * clearing pending compilation/bundle flags. | ||
| * | ||
| * @param bundleResults Newly bundled results keyed by entry point name. | ||
| */ | ||
| recordBundleOutput(bundleResults: ReadonlyMap<string, BundleResult>): void { | ||
| this.hasCompilationError = false; | ||
| this.hasEntryPointsChanges = false; | ||
| this.pendingChangedEsmFiles.clear(); | ||
| this.pendingChangedDtsFiles.clear(); | ||
|
|
||
| for (const [name, bundleResult] of bundleResults) { | ||
| this.previousBundleResults.set(name, bundleResult); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Disposes active compilation resources and clears cached state. | ||
| */ | ||
| async dispose(): Promise<void> { | ||
| await this.#singleProgramCache?.compilationInstance.close?.(); | ||
| this.#singleProgramCache = undefined; | ||
| this.#compilationMode = undefined; | ||
| this.hasEmittedTslib = false; | ||
| this.hasEmittedAssets = false; | ||
| this.hasCompilationError = false; | ||
| this.hasEmittedManifests = false; | ||
| this.hasEntryPointsChanges = false; | ||
| this.previousBundleResults.clear(); | ||
| this.pendingChangedEsmFiles.clear(); | ||
| this.pendingChangedDtsFiles.clear(); | ||
| this.#pendingCompilationFiles.clear(); | ||
| this.#watchedCompilationFiles.clear(); | ||
| } | ||
| } | ||
155 changes: 155 additions & 0 deletions
155
packages/angular/build/src/builders/library/builder-state_spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import { BuilderState } from './builder-state'; | ||
| import type { LibraryCompilationOutput, SingleProgramCache } from './types'; | ||
|
|
||
| describe('BuilderState', () => { | ||
| const packageJsonPath = '/workspace/projects/my-lib/package.json'; | ||
| const tsConfigPath = '/workspace/projects/my-lib/tsconfig.lib.json'; | ||
| const entryFilePath = '/workspace/projects/my-lib/src/public-api.ts'; | ||
|
|
||
| function createMockCache(closeSpy?: jasmine.Spy): SingleProgramCache { | ||
| return { | ||
| compilationInstance: { | ||
| close: closeSpy, | ||
| } as unknown as SingleProgramCache['compilationInstance'], | ||
| esmFiles: new Map(), | ||
| dtsFiles: new Map(), | ||
| failedFiles: new Set(), | ||
| }; | ||
| } | ||
|
|
||
| function createMockCompilationOutput( | ||
| overrides: Partial<LibraryCompilationOutput> = {}, | ||
| ): LibraryCompilationOutput { | ||
| return { | ||
| esmFiles: new Map(), | ||
| dtsFiles: new Map(), | ||
| changedEsmFiles: new Set(), | ||
| changedDtsFiles: new Set(), | ||
| referencedFiles: new Set(), | ||
| cache: createMockCache(), | ||
| hasStylesheetErrors: false, | ||
| diagnosePromise: Promise.resolve(), | ||
| declarationMap: false, | ||
| compilationMode: 'partial', | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| describe('ExecutionResult', () => { | ||
| it('should track logs, warnings, and errors and reset per iteration', () => { | ||
| const state = new BuilderState(); | ||
| expect(state.executionResult.hasErrors).toBeFalse(); | ||
| expect(state.executionResult.output).toEqual({ success: true }); | ||
|
|
||
| state.executionResult.addLog('Bundling my-lib'); | ||
| state.executionResult.addWarning('Unused import'); | ||
| state.executionResult.addWarnings(['Deprecated API']); | ||
| state.executionResult.addError('Type error'); | ||
| state.executionResult.addErrors(['Syntax error']); | ||
|
|
||
| expect(state.executionResult.logs).toEqual(['Bundling my-lib']); | ||
| expect(state.executionResult.warnings).toEqual(['Unused import', 'Deprecated API']); | ||
| expect(state.executionResult.errors).toEqual(['Type error', 'Syntax error']); | ||
| expect(state.executionResult.hasErrors).toBeTrue(); | ||
| expect(state.executionResult.output).toEqual({ success: false }); | ||
|
|
||
| const freshResult = state.resetExecutionResult(); | ||
| expect(freshResult).toBe(state.executionResult); | ||
| expect(freshResult.hasErrors).toBeFalse(); | ||
| expect(freshResult.logs).toEqual([]); | ||
| expect(freshResult.warnings).toEqual([]); | ||
| expect(freshResult.errors).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('shouldCompileEntryPoints & getCompilationModifiedFiles', () => { | ||
| it('should preserve pending compilation files across aborted iterations', () => { | ||
| const state = new BuilderState({ | ||
| packageJsonPath, | ||
| tsConfigPath, | ||
| entryPoints: new Map([ | ||
| [ | ||
| '.', | ||
| { | ||
| subpath: '.', | ||
| name: '.', | ||
| displayName: 'my-lib', | ||
| bundleName: 'my-lib', | ||
| entryFilePath, | ||
| isPrimary: true, | ||
| }, | ||
| ], | ||
| ]), | ||
| }); | ||
|
|
||
| state.recordCompilationOutput( | ||
| createMockCompilationOutput({ | ||
| referencedFiles: new Set([entryFilePath]), | ||
| }), | ||
| ); | ||
|
|
||
| // Iteration 1: entryFilePath changes, but build aborts before recordCompilationOutput | ||
| expect( | ||
| state.shouldCompileEntryPoints(packageJsonPath, new Set([entryFilePath, packageJsonPath])), | ||
| ).toBeTrue(); | ||
|
|
||
| // Iteration 2: only package.json changes on recovery | ||
| expect( | ||
| state.shouldCompileEntryPoints(packageJsonPath, new Set([packageJsonPath])), | ||
| ).toBeTrue(); | ||
| expect(state.getCompilationModifiedFiles(new Set([packageJsonPath]))).toEqual( | ||
| new Set([packageJsonPath, entryFilePath]), | ||
| ); | ||
|
|
||
| // Once compilation output is recorded, pending compilation files are cleared | ||
| state.recordCompilationOutput(createMockCompilationOutput()); | ||
| expect( | ||
| state.shouldCompileEntryPoints(packageJsonPath, new Set([packageJsonPath])), | ||
| ).toBeFalse(); | ||
| }); | ||
|
|
||
| it('should invalidate hasEmittedManifests when compilationMode changes', () => { | ||
| const state = new BuilderState(); | ||
| state.hasEmittedManifests = true; | ||
| state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' })); | ||
| expect(state.hasEmittedManifests).toBeFalse(); | ||
|
|
||
| state.hasEmittedManifests = true; | ||
| state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' })); | ||
| expect(state.hasEmittedManifests).toBeTrue(); | ||
|
|
||
| state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'full' })); | ||
| expect(state.hasEmittedManifests).toBeFalse(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('dispose', () => { | ||
| it('should close compilationInstance and clear cached state on dispose', async () => { | ||
| const closeSpy = jasmine.createSpy('close').and.resolveTo(); | ||
| const state = new BuilderState({ packageJsonPath }); | ||
| state.recordCompilationOutput( | ||
| createMockCompilationOutput({ | ||
| cache: createMockCache(closeSpy), | ||
| changedEsmFiles: new Set(['/a.js']), | ||
| changedDtsFiles: new Set(['/a.d.ts']), | ||
| }), | ||
| ); | ||
|
|
||
| await state.dispose(); | ||
|
|
||
| expect(closeSpy).toHaveBeenCalledTimes(1); | ||
| expect(state.singleProgramCache).toBeUndefined(); | ||
| expect(state.pendingChangedEsmFiles).toHaveSize(0); | ||
| expect(state.pendingChangedDtsFiles).toHaveSize(0); | ||
| expect(state.watchedCompilationFiles).toHaveSize(0); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.