Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions packages/angular/build/src/builders/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@

import { Builder, BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import assert from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';
import { BuildOutputFileType } from '../../tools/esbuild/bundler-files';
import { createJsonBuildManifest } from '../../tools/esbuild/utils';
import { colors as ansiColors } from '../../utils/color';
import { deleteOutputDir } from '../../utils/delete-output-dir';
import { emitFilesToDisk } from '../../utils/emit-files-to-disk';
import { deleteFilesFromDisk, emitFilesToDisk } from '../../utils/disk-files';
import { bazelEsbuildPluginPath, useJSONBuildLogs } from '../../utils/environment-options';
import { isSubDirectory } from '../../utils/path';
import { purgeStaleBuildCache } from '../../utils/purge-cache';
Expand Down Expand Up @@ -215,13 +214,10 @@ export async function* buildApplication(
// Delete any removed files if incremental
if (result.kind === ResultKind.Incremental && result.removed?.length) {
try {
await Promise.all(
result.removed.map((file) => {
const fullFilePath = generateFullPath(file.path, file.type, outputOptions);

return fs.rm(fullFilePath, { force: true, maxRetries: 3 });
}),
const filesToDelete = result.removed.map((file) =>
generateFullPath(file.path, file.type, outputOptions),
);
await deleteFilesFromDisk(filesToDelete);
} catch (error) {
context.logger.error(error instanceof Error ? error.message : String(error));
yield { success: false };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import type { BuilderContext } from '@angular-devkit/architect';
import path from 'node:path';
import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets';
import { emitFilesToDisk } from '../../../utils/emit-files-to-disk';
import { emitFilesToDisk } from '../../../utils/disk-files';
import { assertIsError } from '../../../utils/error';
import { toPosixPath } from '../../../utils/path';
import type { NormalizedLibraryOptions, SingleBuildState } from '../types';
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export { ComponentStylesheetBundler } from './tools/esbuild/angular/component-st
export * from './utils/bundle-calculator';
export { checkPort } from './utils/check-port';
export { deleteOutputDir } from './utils/delete-output-dir';
export { emitFilesToDisk } from './utils/emit-files-to-disk';
export { emitFilesToDisk } from './utils/disk-files';
export { type I18nOptions, createI18nOptions, loadTranslations } from './utils/i18n-options';
export {
IndexHtmlGenerator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { constants, cp, mkdir, writeFile } from 'node:fs/promises';
import { constants, cp, mkdir, rm, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { runConcurrent } from './concurrency';

/**
* Maximum number of concurrent file system write/copy operations to avoid open file descriptor exhaustion.
*/
const MAX_CONCURRENT_WRITES = 64;
const MAX_CONCURRENT_OPS = 64;

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

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

await runConcurrent(files, MAX_CONCURRENT_WRITES, async (file) => {
await runConcurrent(files, MAX_CONCURRENT_OPS, async (file) => {
const fullFilePath = resolveDestinationPath(file);
if (!fullFilePath) {
return;
Expand Down Expand Up @@ -69,3 +69,15 @@ export async function emitFilesToDisk<T extends EmittableFile = EmittableFile>(
});
});
}

/**
* Deletes a collection of files from disk with bounded concurrency.
* @param files The collection of files to delete.
*/
export async function deleteFilesFromDisk(files: readonly string[]): Promise<void> {
if (files.length === 0) {
return;
}

await runConcurrent(files, MAX_CONCURRENT_OPS, (file) => rm(file, { force: true }));
Comment thread
clydin marked this conversation as resolved.
}
75 changes: 75 additions & 0 deletions packages/angular/build/src/utils/disk-files_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @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 assert from 'node:assert';
import { existsSync } from 'node:fs';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { type EmittableFile, deleteFilesFromDisk, emitFilesToDisk } from './disk-files';

describe('disk-files', () => {
let tempRoot: string;

beforeEach(async () => {
const baseTmpDir = process.env['TEST_TMPDIR'];
assert(baseTmpDir, 'TEST_TMPDIR is not set');
tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-disk-files-test-'));
});

afterEach(async () => {
if (tempRoot) {
await rm(tempRoot, { recursive: true, force: true });
}
});

describe('emitFilesToDisk', () => {
it('should write memory files and copy disk files while creating nested directories', async () => {
const sourceFile = join(tempRoot, 'source-asset.txt');
await writeFile(sourceFile, 'from-disk');

const files: (EmittableFile & { path: string })[] = [
{
origin: 'memory',
path: 'nested/a/mem1.txt',
contents: new TextEncoder().encode('mem-1'),
},
{ origin: 'memory', path: 'nested/a/mem2.txt', contents: 'mem-2' },
{ origin: 'disk', path: 'nested/b/disk1.txt', source: sourceFile },
{ origin: 'disk', path: 'nested/c/disk2.txt', source: sourceFile },
{ origin: 'memory', path: 'skipped/file.txt', contents: 'should-not-exist' },
];

const outDir = join(tempRoot, 'out');
await emitFilesToDisk(files, (file) =>
file.path.startsWith('skipped/') ? undefined : join(outDir, file.path),
);

expect(await readFile(join(outDir, 'nested/a/mem1.txt'), 'utf-8')).toBe('mem-1');
expect(await readFile(join(outDir, 'nested/a/mem2.txt'), 'utf-8')).toBe('mem-2');
expect(await readFile(join(outDir, 'nested/b/disk1.txt'), 'utf-8')).toBe('from-disk');
expect(await readFile(join(outDir, 'nested/c/disk2.txt'), 'utf-8')).toBe('from-disk');
expect(existsSync(join(outDir, 'skipped/file.txt'))).toBeFalse();
});
});

describe('deleteFilesFromDisk', () => {
it('should delete existing files and ignore non-existent files', async () => {
const fileA = join(tempRoot, 'a.txt');
const fileB = join(tempRoot, 'b.txt');
const nonExistent = join(tempRoot, 'missing.txt');

await writeFile(fileA, 'a');
await writeFile(fileB, 'b');

await deleteFilesFromDisk([fileA, nonExistent]);

expect(existsSync(fileA)).toBeFalse();
expect(existsSync(fileB)).toBeTrue();
});
});
});
53 changes: 0 additions & 53 deletions packages/angular/build/src/utils/emit-files-to-disk_spec.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/angular/build/src/utils/test-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import path from 'node:path';
import { ResultFile } from '../builders/application/results';
import { BuildOutputFileType } from '../tools/esbuild/bundler-files';
import { emitFilesToDisk } from './emit-files-to-disk';
import { emitFilesToDisk } from './disk-files';

/**
* Writes a collection of build result files to a specified directory.
Expand Down
Loading