Skip to content
11 changes: 7 additions & 4 deletions packages/angular/build/src/builders/library/builder-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class BuilderState {
readonly #watchedCompilationFiles = new Set<string>();
readonly #pendingCompilationFiles = new Set<string>();
readonly #pendingAssetFiles = new Set<string>();
readonly #assetMatchers?: readonly AssetMatcher[];
#assetMatchers?: readonly AssetMatcher[];

#emittedBundleFiles = new Set<string>();
#emittedManifestFiles = new Set<string>();
Expand Down Expand Up @@ -66,7 +66,7 @@ export class BuilderState {

constructor(
options?: Partial<
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints' | 'assets'>
Pick<NormalizedLibraryOptions, 'tsConfigPath' | 'packageJsonPath' | 'entryPoints'>
>,
) {
if (options?.tsConfigPath) {
Expand All @@ -80,8 +80,6 @@ export class BuilderState {
this.#watchedCompilationFiles.add(entryPoint.entryFilePath);
}
}

this.#assetMatchers = options?.assets ? createAssetMatchers(options.assets) : undefined;
}

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

if (effectiveModifiedFiles?.size && options.assets.length > 0) {
this.#assetMatchers ??= createAssetMatchers(options.assets);
}

return collectAssetsToEmit(
options.assets,
options.workspaceRoot,
Expand Down Expand Up @@ -460,6 +462,7 @@ export class BuilderState {
await this.#singleProgramCache?.compilationInstance.close?.();
this.#singleProgramCache = undefined;
this.#compilationMode = undefined;
this.#assetMatchers = undefined;
this.#hasEmittedTslib = false;
this.#hasEmittedAssets = false;
this.hasCompilationError = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ describe('BuilderState', () => {
workspaceRoot: tempRoot,
assets: [{ glob: 'README.md', input: tempRoot, output: '.' }],
};
const state = new BuilderState(options);
const state = new BuilderState();

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

it('should lazily create asset matchers only on incremental watch asset collection', async () => {
const baseTmpDir = process.env['TEST_TMPDIR'];
assert(baseTmpDir, 'TEST_TMPDIR is not set');
const tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-builder-state-lazy-'));

try {
const readmePath = join(tempRoot, 'README.md');
await writeFile(readmePath, '# Initial');

let globAccessCount = 0;
const asset = {
get glob() {
globAccessCount++;

return 'README.md';
},
input: tempRoot,
output: '.',
};
const options = {
workspaceRoot: tempRoot,
assets: [asset],
};

// Constructing BuilderState should not compile asset matchers
const state = new BuilderState();
expect(globAccessCount).toBe(0);

state.recordEmittedOutputs({ filesToEmit: [], filesToDelete: [] });
expect(globAccessCount).toBe(0);

// First incremental watch collection initializes asset matchers once
const firstCollect = await state.collectAssets(options, new Set([readmePath]));
expect(firstCollect.filesToEmit).toHaveSize(1);
expect(globAccessCount).toBe(1);

// Subsequent incremental watch collections reuse cached asset matchers
const secondCollect = await state.collectAssets(options, new Set([readmePath]));
expect(secondCollect.filesToEmit).toHaveSize(1);
expect(globAccessCount).toBe(1);
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
});

it('should close compilationInstance and clear cached state on dispose', async () => {
const closeSpy = jasmine.createSpy('close').and.resolveTo();
const state = new BuilderState({ packageJsonPath });
Expand Down
10 changes: 7 additions & 3 deletions packages/angular/build/src/builders/library/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export async function* executeLibraryBuilder(
signal,
watchFiles: [
...buildState.watchedCompilationFiles,
...normalizedOptions.styleIncludePaths,
Comment thread
alan-agius4 marked this conversation as resolved.
...normalizedOptions.assets.map((asset) => asset.input),
],
});
Expand Down Expand Up @@ -243,6 +244,7 @@ async function* runWatchLoop(
signal: AbortSignal,
): AsyncIterableIterator<BuilderOutput> {
const { packageJsonPath, clearScreen } = options;
let hasPackageJsonError = false;

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

for (const posixFile of changedFiles) {
if (/\.(?:scss|sass)$/i.test(posixFile)) {
if (/\.(?:scss|sass|css)$/i.test(posixFile)) {
resetSassWorkerPoolCaches();
break;
}
}

// Check if package.json was modified
if (changedFiles.has(packageJsonPath)) {
// Check if package.json was modified or failed to reload on a previous iteration
if (hasPackageJsonError || changedFiles.has(packageJsonPath)) {
buildState.hasEmittedManifests = false;
try {
const packageJson = await loadPackageJson(packageJsonPath);
updateWatchedEntryPoints(packageJson, options, buildState);
hasPackageJsonError = false;
} catch (error) {
assertIsError(error);
hasPackageJsonError = true;
await buildState.singleProgramCache?.compilationInstance.update?.(changedFiles);
if (buildState.shouldCompileEntryPoints(packageJsonPath, changedFiles)) {
buildState.hasCompilationError = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -847,5 +847,63 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
},
]);
});

it('should retry reloading broken package.json when another file changes and recover once fixed', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
watch: true,
});

await harness.executeWithCases([
async ({ result }) => {
expect(result?.success).toBeTrue();

// Break package.json with invalid JSON
await harness.writeFile('projects/lib/package.json', '{ invalid json');
},
async ({ result }) => {
expect(result?.success).toBeFalse();
expect(result?.error).toContain("Failed to reload 'package.json'");

// Modify a source file while package.json is still broken
await harness.writeFile(
'projects/lib/src/lib/lib.component.ts',
`
import { Component } from '@angular/core';

@Component({
selector: 'lib-after-pkg-error',
template: '<span>After Pkg Error</span>',
})
export class LibComponent {}
`,
);
},
async ({ result }) => {
expect(result?.success).toBeFalse();
expect(result?.error).toContain("Failed to reload 'package.json'");

// Fix package.json
await harness.writeFile(
'projects/lib/package.json',
JSON.stringify(
{
name: 'lib',
version: '0.0.1',
exports: {
'.': './src/public-api.ts',
},
},
null,
2,
),
);
},
({ result }) => {
expect(result?.success).toBeTrue();
expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('lib-after-pkg-error');
},
]);
});
});
});
Loading