Skip to content

Commit f993eef

Browse files
committed
fixup! fix(@angular/build): support incremental asset updates and output cleanup in library watch mode
1 parent 78c5a6a commit f993eef

7 files changed

Lines changed: 100 additions & 71 deletions

File tree

‎packages/angular/build/src/builders/application/index.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88

99
import { Builder, BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
1010
import assert from 'node:assert';
11-
import fs from 'node:fs/promises';
1211
import path from 'node:path';
1312
import { BuildOutputFileType } from '../../tools/esbuild/bundler-files';
1413
import { createJsonBuildManifest } from '../../tools/esbuild/utils';
1514
import { colors as ansiColors } from '../../utils/color';
1615
import { deleteOutputDir } from '../../utils/delete-output-dir';
17-
import { emitFilesToDisk } from '../../utils/emit-files-to-disk';
16+
import { deleteFilesFromDisk, emitFilesToDisk } from '../../utils/disk-files';
1817
import { bazelEsbuildPluginPath, useJSONBuildLogs } from '../../utils/environment-options';
1918
import { isSubDirectory } from '../../utils/path';
2019
import { purgeStaleBuildCache } from '../../utils/purge-cache';
@@ -215,13 +214,10 @@ export async function* buildApplication(
215214
// Delete any removed files if incremental
216215
if (result.kind === ResultKind.Incremental && result.removed?.length) {
217216
try {
218-
await Promise.all(
219-
result.removed.map((file) => {
220-
const fullFilePath = generateFullPath(file.path, file.type, outputOptions);
221-
222-
return fs.rm(fullFilePath, { force: true, maxRetries: 3 });
223-
}),
217+
const filesToDelete = result.removed.map((file) =>
218+
generateFullPath(file.path, file.type, outputOptions),
224219
);
220+
await deleteFilesFromDisk(filesToDelete);
225221
} catch (error) {
226222
context.logger.error(error instanceof Error ? error.message : String(error));
227223
yield { success: false };

‎packages/angular/build/src/builders/library/pipeline/build-action.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
*/
88

99
import type { BuilderContext } from '@angular-devkit/architect';
10-
import { rm } from 'node:fs/promises';
1110
import path from 'node:path';
1211
import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets';
13-
import { emitFilesToDisk } from '../../../utils/emit-files-to-disk';
12+
import { deleteFilesFromDisk, emitFilesToDisk } from '../../../utils/disk-files';
1413
import { assertIsError } from '../../../utils/error';
1514
import type { BundleResult, NormalizedLibraryOptions, SingleBuildState } from '../types';
1615
import { type CollectedAssets, collectAssetsToEmit } from './assets';
@@ -274,9 +273,9 @@ export async function buildAction(actionContext: BuildActionContext): Promise<Bu
274273

275274
updateEmittedAssetState(buildState, collectedAssets, filesToDelete);
276275

277-
if (filesToDelete.size > 0) {
278-
await Promise.allSettled(
279-
[...filesToDelete].map((file) => rm(path.join(options.outputPath, file), { force: true })),
276+
if (filesToDelete.size) {
277+
await deleteFilesFromDisk(
278+
Array.from(filesToDelete, (file) => path.join(options.outputPath, file)),
280279
);
281280
}
282281

‎packages/angular/build/src/private.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export { ComponentStylesheetBundler } from './tools/esbuild/angular/component-st
7272
export * from './utils/bundle-calculator';
7373
export { checkPort } from './utils/check-port';
7474
export { deleteOutputDir } from './utils/delete-output-dir';
75-
export { emitFilesToDisk } from './utils/emit-files-to-disk';
75+
export { emitFilesToDisk } from './utils/disk-files';
7676
export { type I18nOptions, createI18nOptions, loadTranslations } from './utils/i18n-options';
7777
export {
7878
IndexHtmlGenerator,

packages/angular/build/src/utils/emit-files-to-disk.ts renamed to packages/angular/build/src/utils/disk-files.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { constants, cp, mkdir, writeFile } from 'node:fs/promises';
9+
import { constants, cp, mkdir, rm, writeFile } from 'node:fs/promises';
1010
import { dirname } from 'node:path';
1111
import { runConcurrent } from './concurrency';
1212

1313
/**
1414
* Maximum number of concurrent file system write/copy operations to avoid open file descriptor exhaustion.
1515
*/
16-
const MAX_CONCURRENT_WRITES = 64;
16+
const MAX_CONCURRENT_OPS = 64;
1717

1818
/**
1919
* Represents a file that can be emitted to disk from memory or copied from an existing path on disk.
@@ -38,7 +38,7 @@ export async function emitFilesToDisk<T extends EmittableFile = EmittableFile>(
3838

3939
const directoryCreationPromises = new Map<string, Promise<string | undefined>>();
4040

41-
await runConcurrent(files, MAX_CONCURRENT_WRITES, async (file) => {
41+
await runConcurrent(files, MAX_CONCURRENT_OPS, async (file) => {
4242
const fullFilePath = resolveDestinationPath(file);
4343
if (!fullFilePath) {
4444
return;
@@ -69,3 +69,15 @@ export async function emitFilesToDisk<T extends EmittableFile = EmittableFile>(
6969
});
7070
});
7171
}
72+
73+
/**
74+
* Deletes a collection of files from disk with bounded concurrency.
75+
* @param files The collection of files to delete.
76+
*/
77+
export async function deleteFilesFromDisk(files: readonly string[]): Promise<void> {
78+
if (files.length === 0) {
79+
return;
80+
}
81+
82+
await runConcurrent(files, MAX_CONCURRENT_OPS, (file) => rm(file, { force: true }));
83+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 assert from 'node:assert';
10+
import { existsSync } from 'node:fs';
11+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
12+
import { join } from 'node:path';
13+
import { type EmittableFile, deleteFilesFromDisk, emitFilesToDisk } from './disk-files';
14+
15+
describe('disk-files', () => {
16+
let tempRoot: string;
17+
18+
beforeEach(async () => {
19+
const baseTmpDir = process.env['TEST_TMPDIR'];
20+
assert(baseTmpDir, 'TEST_TMPDIR is not set');
21+
tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-disk-files-test-'));
22+
});
23+
24+
afterEach(async () => {
25+
if (tempRoot) {
26+
await rm(tempRoot, { recursive: true, force: true });
27+
}
28+
});
29+
30+
describe('emitFilesToDisk', () => {
31+
it('should write memory files and copy disk files while creating nested directories', async () => {
32+
const sourceFile = join(tempRoot, 'source-asset.txt');
33+
await writeFile(sourceFile, 'from-disk');
34+
35+
const files: (EmittableFile & { path: string })[] = [
36+
{
37+
origin: 'memory',
38+
path: 'nested/a/mem1.txt',
39+
contents: new TextEncoder().encode('mem-1'),
40+
},
41+
{ origin: 'memory', path: 'nested/a/mem2.txt', contents: 'mem-2' },
42+
{ origin: 'disk', path: 'nested/b/disk1.txt', source: sourceFile },
43+
{ origin: 'disk', path: 'nested/c/disk2.txt', source: sourceFile },
44+
{ origin: 'memory', path: 'skipped/file.txt', contents: 'should-not-exist' },
45+
];
46+
47+
const outDir = join(tempRoot, 'out');
48+
await emitFilesToDisk(files, (file) =>
49+
file.path.startsWith('skipped/') ? undefined : join(outDir, file.path),
50+
);
51+
52+
expect(await readFile(join(outDir, 'nested/a/mem1.txt'), 'utf-8')).toBe('mem-1');
53+
expect(await readFile(join(outDir, 'nested/a/mem2.txt'), 'utf-8')).toBe('mem-2');
54+
expect(await readFile(join(outDir, 'nested/b/disk1.txt'), 'utf-8')).toBe('from-disk');
55+
expect(await readFile(join(outDir, 'nested/c/disk2.txt'), 'utf-8')).toBe('from-disk');
56+
expect(existsSync(join(outDir, 'skipped/file.txt'))).toBeFalse();
57+
});
58+
});
59+
60+
describe('deleteFilesFromDisk', () => {
61+
it('should delete existing files and ignore non-existent files', async () => {
62+
const fileA = join(tempRoot, 'a.txt');
63+
const fileB = join(tempRoot, 'b.txt');
64+
const nonExistent = join(tempRoot, 'missing.txt');
65+
66+
await writeFile(fileA, 'a');
67+
await writeFile(fileB, 'b');
68+
69+
await deleteFilesFromDisk([fileA, nonExistent]);
70+
71+
expect(existsSync(fileA)).toBeFalse();
72+
expect(existsSync(fileB)).toBeTrue();
73+
});
74+
});
75+
});

‎packages/angular/build/src/utils/emit-files-to-disk_spec.ts‎

Lines changed: 0 additions & 53 deletions
This file was deleted.

‎packages/angular/build/src/utils/test-files.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import path from 'node:path';
1010
import { ResultFile } from '../builders/application/results';
1111
import { BuildOutputFileType } from '../tools/esbuild/bundler-files';
12-
import { emitFilesToDisk } from './emit-files-to-disk';
12+
import { emitFilesToDisk } from './disk-files';
1313

1414
/**
1515
* Writes a collection of build result files to a specified directory.

0 commit comments

Comments
 (0)