Skip to content

Commit 4951994

Browse files
authored
refactor(@angular/build): unify and deduplicate emitFilesToDisk across builders
Relocate emitFilesToDisk into src/utils/emit-files-to-disk.ts and unify the file emission descriptor shape across the application and library builders around origin ('memory' | 'disk') and source.
1 parent 1f31103 commit 4951994

18 files changed

Lines changed: 196 additions & 115 deletions

File tree

‎packages/angular/build/src/builders/application/build-action.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ function* emitOutputResults(
333333
incrementalResult.files.push({
334334
path: destination,
335335
type: BuildOutputFileType.Browser,
336-
inputPath: source,
336+
source,
337337
origin: 'disk',
338338
});
339339
}
@@ -406,7 +406,7 @@ function createFullResult(
406406
...assetFiles.map(({ source, destination }) => ({
407407
path: destination,
408408
type: BuildOutputFileType.Browser,
409-
inputPath: source,
409+
source,
410410
origin: 'disk' as const,
411411
})),
412412
...outputFiles.map((file) => ({

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

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import assert from 'node:assert';
1111
import fs from 'node:fs/promises';
1212
import path from 'node:path';
1313
import { BuildOutputFileType } from '../../tools/esbuild/bundler-files';
14-
import { createJsonBuildManifest, emitFilesToDisk } from '../../tools/esbuild/utils';
14+
import { createJsonBuildManifest } from '../../tools/esbuild/utils';
1515
import { colors as ansiColors } from '../../utils/color';
1616
import { deleteOutputDir } from '../../utils/delete-output-dir';
17+
import { emitFilesToDisk } from '../../utils/emit-files-to-disk';
1718
import { bazelEsbuildPluginPath, useJSONBuildLogs } from '../../utils/environment-options';
1819
import { isSubDirectory } from '../../utils/path';
1920
import { purgeStaleBuildCache } from '../../utils/purge-cache';
@@ -197,37 +198,14 @@ export async function* buildApplication(
197198
}
198199

199200
// Writes the output files to disk and ensures the containing directories are present
200-
const directoryExists = new Set<string>();
201201
try {
202-
await emitFilesToDisk(result.files, async (file) => {
203-
if (
204-
outputOptions.ignoreServer &&
205-
(file.type === BuildOutputFileType.ServerApplication ||
206-
file.type === BuildOutputFileType.ServerRoot)
207-
) {
208-
return;
209-
}
210-
211-
const fullFilePath = generateFullPath(file.path, file.type, outputOptions);
212-
213-
// Ensure output subdirectories exist
214-
const fileBasePath = path.dirname(fullFilePath);
215-
if (fileBasePath && !directoryExists.has(fileBasePath)) {
216-
await fs.mkdir(fileBasePath, { recursive: true });
217-
directoryExists.add(fileBasePath);
218-
}
219-
220-
if (file.origin === 'memory') {
221-
// Write file contents
222-
await fs.writeFile(fullFilePath, file.contents);
223-
} else {
224-
// Copy file contents
225-
await fs.cp(file.inputPath, fullFilePath, {
226-
mode: fs.constants.COPYFILE_FICLONE,
227-
preserveTimestamps: true,
228-
});
229-
}
230-
});
202+
await emitFilesToDisk(result.files, (file) =>
203+
outputOptions.ignoreServer &&
204+
(file.type === BuildOutputFileType.ServerApplication ||
205+
file.type === BuildOutputFileType.ServerRoot)
206+
? undefined
207+
: generateFullPath(file.path, file.type, outputOptions),
208+
);
231209
} catch (error) {
232210
context.logger.error(error instanceof Error ? error.message : String(error));
233211
yield { success: false };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface BaseResultFile {
5353

5454
export interface DiskFile extends BaseResultFile {
5555
origin: 'disk';
56-
inputPath: string;
56+
source: string;
5757
}
5858

5959
export interface MemoryFile extends BaseResultFile {

‎packages/angular/build/src/builders/dev-server/vite/utils.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function updateResultRecord(
4040
): void {
4141
if (file.origin === 'disk') {
4242
assetFiles.set('/' + normalizePath(file.path), {
43-
source: normalizePath(file.inputPath),
43+
source: normalizePath(file.source),
4444
updated: !initial,
4545
});
4646

‎packages/angular/build/src/builders/extract-i18n/application-extraction.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ function setupLocalizeExtractor(
107107
if (file?.origin === 'memory') {
108108
content = textDecoder.decode(file.contents);
109109
} else if (file?.origin === 'disk') {
110-
content = readFileSync(file.inputPath, 'utf-8');
110+
content = readFileSync(file.source, 'utf-8');
111111
}
112112
if (content === undefined) {
113113
throw new Error('Unknown file requested: ' + requestedPath);

‎packages/angular/build/src/builders/karma/assets-middleware.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class AngularAssetsMiddleware {
6060
// https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10
6161
switch (file.origin) {
6262
case 'disk':
63-
this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true);
63+
this.serveFile(file.source, undefined, res, undefined, undefined, /* doNotCache */ true);
6464
break;
6565
case 'memory':
6666
// Include pathname to help with Content-Type headers.

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

Lines changed: 3 additions & 18 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 { constants, copyFile, mkdir, writeFile } from 'node:fs/promises';
1110
import path from 'node:path';
1211
import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets';
13-
import { emitFilesToDisk } from '../../../tools/esbuild/utils';
12+
import { emitFilesToDisk } from '../../../utils/emit-files-to-disk';
1413
import { toPosixPath } from '../../../utils/path';
1514
import type { NormalizedLibraryOptions, SingleBuildState } from '../types';
1615
import { collectAssetsToEmit } from './assets';
@@ -27,7 +26,6 @@ export function createSingleBuildState(): SingleBuildState {
2726
previousBundleResults: new Map(),
2827
pendingChangedEsmFiles: new Set(),
2928
pendingChangedDtsFiles: new Set(),
30-
directoryExists: new Set(),
3129
};
3230
}
3331

@@ -90,7 +88,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise<vo
9088
} = actionContext;
9189

9290
const posixPackageJsonPath = toPosixPath(options.packageJsonPath);
93-
const { pendingChangedEsmFiles, pendingChangedDtsFiles, directoryExists } = buildState;
91+
const { pendingChangedEsmFiles, pendingChangedDtsFiles } = buildState;
9492

9593
if (!modifiedFiles || modifiedFiles.has(posixPackageJsonPath)) {
9694
buildState.hasEmittedManifests = false;
@@ -231,20 +229,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise<vo
231229
));
232230
filesToEmit.push(...resolvedAssetsToEmit);
233231

234-
await emitFilesToDisk<OutputFile>(filesToEmit, async (file) => {
235-
const fullFilePath = path.join(options.outputPath, file.path);
236-
const fileBasePath = path.dirname(fullFilePath);
237-
if (fileBasePath && !directoryExists.has(fileBasePath)) {
238-
await mkdir(fileBasePath, { recursive: true });
239-
directoryExists.add(fileBasePath);
240-
}
241-
242-
if (file.type === 'memory') {
243-
await writeFile(fullFilePath, file.contents);
244-
} else {
245-
await copyFile(file.source, fullFilePath, constants.COPYFILE_FICLONE);
246-
}
247-
});
232+
await emitFilesToDisk(filesToEmit, (file) => path.join(options.outputPath, file.path));
248233

249234
buildState.hasEmittedManifests = true;
250235
buildState.hasEmittedTslib = hasTslibImport;

‎packages/angular/build/src/builders/library/pipeline/utils.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const TYPES_OUTPUT_DIR = 'types';
2626
* Represents an in-memory file to be emitted to disk.
2727
*/
2828
export interface MemoryOutputFile {
29-
type: 'memory';
29+
origin: 'memory';
3030

3131
/** The destination path where the file should be written. */
3232
path: string;
@@ -39,7 +39,7 @@ export interface MemoryOutputFile {
3939
* Represents an existing file on disk to be copied to a destination path.
4040
*/
4141
export interface DiskOutputFile {
42-
type: 'disk';
42+
origin: 'disk';
4343

4444
/** The path to the source file on disk. */
4545
source: string;
@@ -62,7 +62,7 @@ export type OutputFile = MemoryOutputFile | DiskOutputFile;
6262
*/
6363
export function createDiskOutputFile(source: string, path: string): DiskOutputFile {
6464
return {
65-
type: 'disk',
65+
origin: 'disk',
6666
source,
6767
path,
6868
};
@@ -80,7 +80,7 @@ export function createMemoryOutputFile(
8080
contents: string | Uint8Array | Record<string, unknown>,
8181
): MemoryOutputFile {
8282
return {
83-
type: 'memory',
83+
origin: 'memory',
8484
path,
8585
contents:
8686
typeof contents === 'string' || contents instanceof Uint8Array

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

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

9+
import { rm } from 'node:fs/promises';
910
import { executeLibraryBuilder } from '../../builder';
1011
import { BASE_OPTIONS, LIBRARY_BUILDER_INFO, describeLibraryBuilder } from '../setup';
1112

@@ -563,5 +564,42 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) =>
563564
},
564565
]);
565566
});
567+
568+
it('should recreate deleted output directories on subsequent watch rebuilds', async () => {
569+
harness.useTarget('build', {
570+
...BASE_OPTIONS,
571+
watch: true,
572+
});
573+
574+
await harness.executeWithCases([
575+
async ({ result }) => {
576+
expect(result?.success).toBeTrue();
577+
expect(harness.hasFile('dist/lib/fesm2022/lib.mjs')).toBeTrue();
578+
579+
// Remove output subdirectory externally between rebuilds
580+
await rm(harness.resolvePath('dist/lib/fesm2022'), { recursive: true, force: true });
581+
expect(harness.hasFile('dist/lib/fesm2022/lib.mjs')).toBeFalse();
582+
583+
// Trigger a rebuild
584+
await harness.writeFile(
585+
'projects/lib/src/lib/lib.component.ts',
586+
`
587+
import { Component } from '@angular/core';
588+
589+
@Component({
590+
selector: 'lib-recreated',
591+
template: '<span>Recreated</span>',
592+
})
593+
export class LibComponent {}
594+
`,
595+
);
596+
},
597+
({ result }) => {
598+
expect(result?.success).toBeTrue();
599+
const content = harness.readFile('dist/lib/fesm2022/lib.mjs');
600+
expect(content).toContain('lib-recreated');
601+
},
602+
]);
603+
});
566604
});
567605
});

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,4 @@ export interface SingleBuildState {
153153

154154
/** Whether entry points in package.json changed since the last compilation. */
155155
hasEntryPointsChanges?: boolean;
156-
157-
/** Cache of output directory paths known to exist on disk. */
158-
directoryExists: Set<string>;
159156
}

0 commit comments

Comments
 (0)