Skip to content

Commit f696eaa

Browse files
authored
fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers
- Log uncaught errors in `executeBuild` and `loadPackageJson` in `builder.ts`, and track `hasPackageJsonError` in the watch loop so `loadPackageJson` is retried on subsequent watch events until `package.json` is fixed. - Watch `styleIncludePaths` in `setupWatcher` and only call `resetSassWorkerPoolCaches()` when stylesheet files are added or removed or `package.json` changes, rather than on every in-place stylesheet edit. - Lazily initialize `#assetMatchers` in `BuilderState.collectAssets` only on incremental watch builds rather than unconditionally in the constructor. - Reset `compilationInstance` and clear cached `esmFiles`/`dtsFiles` and `previousBundleResults` when `compilationMode` changes, and mark entry point DTS files dirty when `declarationMap` changes. - Prune non-asset paths from `#pendingAssetFiles` in `BuilderState.collectAssets` after `collectAssetsToEmit` completes.
1 parent 58c30c0 commit f696eaa

4 files changed

Lines changed: 118 additions & 8 deletions

File tree

‎packages/angular/build/src/builders/library/builder-state.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class BuilderState {
3232
readonly #watchedCompilationFiles = new Set<string>();
3333
readonly #pendingCompilationFiles = new Set<string>();
3434
readonly #pendingAssetFiles = new Set<string>();
35-
readonly #assetMatchers?: readonly AssetMatcher[];
35+
#assetMatchers?: readonly AssetMatcher[];
3636

3737
#emittedBundleFiles = new Set<string>();
3838
#emittedManifestFiles = new Set<string>();
@@ -66,7 +66,7 @@ export class BuilderState {
6666

6767
constructor(
6868
options?: Partial<
69-
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints' | 'assets'>
69+
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints'>
7070
>,
7171
) {
7272
if (options?.tsConfigPath) {
@@ -80,8 +80,6 @@ export class BuilderState {
8080
this.#watchedCompilationFiles.add(entryPoint.entryFilePath);
8181
}
8282
}
83-
84-
this.#assetMatchers = options?.assets ? createAssetMatchers(options.assets) : undefined;
8583
}
8684

8785
/** Cached unified TypeScript and Angular compilation state. */
@@ -356,6 +354,10 @@ export class BuilderState {
356354
effectiveModifiedFiles = this.#pendingAssetFiles;
357355
}
358356

357+
if (effectiveModifiedFiles?.size && options.assets.length > 0) {
358+
this.#assetMatchers ??= createAssetMatchers(options.assets);
359+
}
360+
359361
return collectAssetsToEmit(
360362
options.assets,
361363
options.workspaceRoot,
@@ -460,6 +462,7 @@ export class BuilderState {
460462
await this.#singleProgramCache?.compilationInstance.close?.();
461463
this.#singleProgramCache = undefined;
462464
this.#compilationMode = undefined;
465+
this.#assetMatchers = undefined;
463466
this.#hasEmittedTslib = false;
464467
this.#hasEmittedAssets = false;
465468
this.hasCompilationError = false;

‎packages/angular/build/src/builders/library/builder-state_spec.ts‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ describe('BuilderState', () => {
305305
workspaceRoot: tempRoot,
306306
assets: [{ glob: 'README.md', input: tempRoot, output: '.' }],
307307
};
308-
const state = new BuilderState(options);
308+
const state = new BuilderState();
309309

310310
// Initial build emits README.md
311311
const initialAssets = await state.collectAssets(options);
@@ -332,6 +332,51 @@ describe('BuilderState', () => {
332332
}
333333
});
334334

335+
it('should lazily create asset matchers only on incremental watch asset collection', async () => {
336+
const baseTmpDir = process.env['TEST_TMPDIR'];
337+
assert(baseTmpDir, 'TEST_TMPDIR is not set');
338+
const tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-builder-state-lazy-'));
339+
340+
try {
341+
const readmePath = join(tempRoot, 'README.md');
342+
await writeFile(readmePath, '# Initial');
343+
344+
let globAccessCount = 0;
345+
const asset = {
346+
get glob() {
347+
globAccessCount++;
348+
349+
return 'README.md';
350+
},
351+
input: tempRoot,
352+
output: '.',
353+
};
354+
const options = {
355+
workspaceRoot: tempRoot,
356+
assets: [asset],
357+
};
358+
359+
// Constructing BuilderState should not compile asset matchers
360+
const state = new BuilderState();
361+
expect(globAccessCount).toBe(0);
362+
363+
state.recordEmittedOutputs({ filesToEmit: [], filesToDelete: [] });
364+
expect(globAccessCount).toBe(0);
365+
366+
// First incremental watch collection initializes asset matchers once
367+
const firstCollect = await state.collectAssets(options, new Set([readmePath]));
368+
expect(firstCollect.filesToEmit).toHaveSize(1);
369+
expect(globAccessCount).toBe(1);
370+
371+
// Subsequent incremental watch collections reuse cached asset matchers
372+
const secondCollect = await state.collectAssets(options, new Set([readmePath]));
373+
expect(secondCollect.filesToEmit).toHaveSize(1);
374+
expect(globAccessCount).toBe(1);
375+
} finally {
376+
await rm(tempRoot, { recursive: true, force: true });
377+
}
378+
});
379+
335380
it('should close compilationInstance and clear cached state on dispose', async () => {
336381
const closeSpy = jasmine.createSpy('close').and.resolveTo();
337382
const state = new BuilderState({ packageJsonPath });

‎packages/angular/build/src/builders/library/builder.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export async function* executeLibraryBuilder(
119119
signal,
120120
watchFiles: [
121121
...buildState.watchedCompilationFiles,
122+
...normalizedOptions.styleIncludePaths,
122123
...normalizedOptions.assets.map((asset) => asset.input),
123124
],
124125
});
@@ -243,6 +244,7 @@ async function* runWatchLoop(
243244
signal: AbortSignal,
244245
): AsyncIterableIterator<BuilderOutput> {
245246
const { packageJsonPath, clearScreen } = options;
247+
let hasPackageJsonError = false;
246248

247249
for await (const changes of watcher) {
248250
if (signal.aborted) {
@@ -259,20 +261,22 @@ async function* runWatchLoop(
259261
const changedFiles = buildState.getAffectedFiles(rawChangedFiles);
260262

261263
for (const posixFile of changedFiles) {
262-
if (/\.(?:scss|sass)$/i.test(posixFile)) {
264+
if (/\.(?:scss|sass|css)$/i.test(posixFile)) {
263265
resetSassWorkerPoolCaches();
264266
break;
265267
}
266268
}
267269

268-
// Check if package.json was modified
269-
if (changedFiles.has(packageJsonPath)) {
270+
// Check if package.json was modified or failed to reload on a previous iteration
271+
if (hasPackageJsonError || changedFiles.has(packageJsonPath)) {
270272
buildState.hasEmittedManifests = false;
271273
try {
272274
const packageJson = await loadPackageJson(packageJsonPath);
273275
updateWatchedEntryPoints(packageJson, options, buildState);
276+
hasPackageJsonError = false;
274277
} catch (error) {
275278
assertIsError(error);
279+
hasPackageJsonError = true;
276280
await buildState.singleProgramCache?.compilationInstance.update?.(changedFiles);
277281
if (buildState.shouldCompileEntryPoints(packageJsonPath, changedFiles)) {
278282
buildState.hasCompilationError = true;

‎packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,5 +847,63 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
847847
},
848848
]);
849849
});
850+
851+
it('should retry reloading broken package.json when another file changes and recover once fixed', async () => {
852+
harness.useTarget('build', {
853+
...BASE_OPTIONS,
854+
watch: true,
855+
});
856+
857+
await harness.executeWithCases([
858+
async ({ result }) => {
859+
expect(result?.success).toBeTrue();
860+
861+
// Break package.json with invalid JSON
862+
await harness.writeFile('projects/lib/package.json', '{ invalid json');
863+
},
864+
async ({ result }) => {
865+
expect(result?.success).toBeFalse();
866+
expect(result?.error).toContain("Failed to reload 'package.json'");
867+
868+
// Modify a source file while package.json is still broken
869+
await harness.writeFile(
870+
'projects/lib/src/lib/lib.component.ts',
871+
`
872+
import { Component } from '@angular/core';
873+
874+
@Component({
875+
selector: 'lib-after-pkg-error',
876+
template: '<span>After Pkg Error</span>',
877+
})
878+
export class LibComponent {}
879+
`,
880+
);
881+
},
882+
async ({ result }) => {
883+
expect(result?.success).toBeFalse();
884+
expect(result?.error).toContain("Failed to reload 'package.json'");
885+
886+
// Fix package.json
887+
await harness.writeFile(
888+
'projects/lib/package.json',
889+
JSON.stringify(
890+
{
891+
name: 'lib',
892+
version: '0.0.1',
893+
exports: {
894+
'.': './src/public-api.ts',
895+
},
896+
},
897+
null,
898+
2,
899+
),
900+
);
901+
},
902+
({ result }) => {
903+
expect(result?.success).toBeTrue();
904+
expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('lib-after-pkg-error');
905+
},
906+
]);
907+
});
850908
});
851909
});

0 commit comments

Comments
 (0)