Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(@angular/build): avoid write file logic for internal application build action #28162

Merged
merged 1 commit into from
Aug 6, 2024
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
110 changes: 38 additions & 72 deletions packages/angular/build/src/builders/application/build-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,10 @@
import { BuilderContext } from '@angular-devkit/architect';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import {
logMessages,
withNoProgress,
withSpinner,
writeResultFiles,
} from '../../tools/esbuild/utils';
import { deleteOutputDir } from '../../utils/delete-output-dir';
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
import { shouldWatchRoot } from '../../utils/environment-options';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
Expand Down Expand Up @@ -46,12 +40,9 @@ export async function* runEsBuildBuildAction(
outputOptions: NormalizedOutputOptions;
logger: BuilderContext['logger'];
cacheOptions: NormalizedCachedOptions;
writeToFileSystem: boolean;
writeToFileSystemFilter: ((file: BuildOutputFile) => boolean) | undefined;
watch?: boolean;
verbose?: boolean;
progress?: boolean;
deleteOutputPath?: boolean;
poll?: number;
signal?: AbortSignal;
preserveSymlinks?: boolean;
Expand All @@ -61,13 +52,10 @@ export async function* runEsBuildBuildAction(
},
): AsyncIterable<Result> {
const {
writeToFileSystemFilter,
writeToFileSystem,
watch,
poll,
clearScreen,
logger,
deleteOutputPath,
cacheOptions,
outputOptions,
verbose,
Expand All @@ -79,13 +67,6 @@ export async function* runEsBuildBuildAction(
jsonLogs,
} = options;

if (deleteOutputPath && writeToFileSystem) {
await deleteOutputDir(workspaceRoot, outputOptions.base, [
outputOptions.browser,
outputOptions.server,
]);
}

const withProgress: typeof withSpinner = progress ? withSpinner : withNoProgress;

// Initial build
Expand Down Expand Up @@ -154,7 +135,7 @@ export async function* runEsBuildBuildAction(
// Output the first build results after setting up the watcher to ensure that any code executed
// higher in the iterator call stack will trigger the watcher. This is particularly relevant for
// unit tests which execute the builder and modify the file system programmatically.
yield await writeAndEmitOutput(writeToFileSystem, result, outputOptions, writeToFileSystemFilter);
yield await emitOutputResult(result, outputOptions);

// Finish if watch mode is not enabled
if (!watcher) {
Expand Down Expand Up @@ -207,12 +188,7 @@ export async function* runEsBuildBuildAction(
watcher.remove([...staleWatchFiles]);
}

yield await writeAndEmitOutput(
writeToFileSystem,
result,
outputOptions,
writeToFileSystemFilter,
);
yield await emitOutputResult(result, outputOptions);
}
} finally {
// Stop the watcher and cleanup incremental rebuild state
Expand All @@ -222,65 +198,55 @@ export async function* runEsBuildBuildAction(
}
}

async function writeAndEmitOutput(
writeToFileSystem: boolean,
async function emitOutputResult(
{
outputFiles,
outputWithFiles,
assetFiles,
errors,
warnings,
externalMetadata,
htmlIndexPath,
htmlBaseHref,
}: ExecutionResult,
outputOptions: NormalizedApplicationBuildOptions['outputOptions'],
writeToFileSystemFilter: ((file: BuildOutputFile) => boolean) | undefined,
): Promise<Result> {
if (!outputWithFiles.success) {
if (errors.length > 0) {
return {
kind: ResultKind.Failure,
errors: outputWithFiles.errors as ResultMessage[],
errors: errors as ResultMessage[],
warnings: warnings as ResultMessage[],
detail: {
outputOptions,
},
};
}

if (writeToFileSystem) {
// Write output files
const outputFilesToWrite = writeToFileSystemFilter
? outputFiles.filter(writeToFileSystemFilter)
: outputFiles;

await writeResultFiles(outputFilesToWrite, assetFiles, outputOptions);

// Currently unused other than indicating success if writing to disk.
return {
kind: ResultKind.Full,
files: {},
const result: FullResult = {
kind: ResultKind.Full,
warnings: warnings as ResultMessage[],
files: {},
detail: {
externalMetadata,
htmlIndexPath,
htmlBaseHref,
outputOptions,
},
};
for (const file of assetFiles) {
result.files[file.destination] = {
type: BuildOutputFileType.Browser,
inputPath: file.source,
origin: 'disk',
};
} else {
const result: FullResult = {
kind: ResultKind.Full,
files: {},
detail: {
externalMetadata,
htmlIndexPath,
htmlBaseHref,
},
}
for (const file of outputFiles) {
result.files[file.path] = {
type: file.type,
contents: file.contents,
origin: 'memory',
hash: file.hash,
};
for (const file of outputWithFiles.assetFiles) {
result.files[file.destination] = {
type: BuildOutputFileType.Browser,
inputPath: file.source,
origin: 'disk',
};
}
for (const file of outputWithFiles.outputFiles) {
result.files[file.path] = {
type: file.type,
contents: file.contents,
origin: 'memory',
hash: file.hash,
};
}

return result;
}

return result;
}
104 changes: 83 additions & 21 deletions packages/angular/build/src/builders/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { Plugin } from 'esbuild';
import assert from 'node:assert';
import fs from 'node:fs/promises';
import path from 'node:path';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { createJsonBuildManifest } from '../../tools/esbuild/utils';
import { createJsonBuildManifest, emitFilesToDisk } from '../../tools/esbuild/utils';
import { colors as ansiColors } from '../../utils/color';
import { deleteOutputDir } from '../../utils/delete-output-dir';
import { useJSONBuildLogs } from '../../utils/environment-options';
import { purgeStaleBuildCache } from '../../utils/purge-cache';
import { assertCompatibleAngularVersion } from '../../utils/version';
import { runEsBuildBuildAction } from './build-action';
import { executeBuild } from './execute-build';
import {
ApplicationBuilderExtensions,
ApplicationBuilderInternalOptions,
NormalizedOutputOptions,
normalizeOptions,
} from './options';
import { Result, ResultKind } from './results';
Expand All @@ -29,9 +35,6 @@ export async function* buildApplicationInternal(
options: ApplicationBuilderInternalOptions,
// TODO: Integrate abort signal support into builder system
context: BuilderContext & { signal?: AbortSignal },
infrastructureSettings?: {
write?: boolean;
},
extensions?: ApplicationBuilderExtensions,
): AsyncIterable<Result> {
const { workspaceRoot, logger, target } = context;
Expand All @@ -53,11 +56,8 @@ export async function* buildApplicationInternal(
}

const normalizedOptions = await normalizeOptions(context, projectName, options, extensions);
const writeToFileSystem = infrastructureSettings?.write ?? true;
const writeServerBundles =
writeToFileSystem && !!(normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint);

if (writeServerBundles) {
if (!normalizedOptions.outputOptions.ignoreServer) {
const { browser, server } = normalizedOptions.outputOptions;
if (browser === '') {
context.logger.error(
Expand Down Expand Up @@ -88,7 +88,7 @@ export async function* buildApplicationInternal(

yield* runEsBuildBuildAction(
async (rebuildState) => {
const { prerenderOptions, outputOptions, jsonLogs } = normalizedOptions;
const { prerenderOptions, jsonLogs } = normalizedOptions;

const startTime = process.hrtime.bigint();
const result = await executeBuild(normalizedOptions, context, rebuildState);
Expand All @@ -106,9 +106,6 @@ export async function* buildApplicationInternal(

const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
const hasError = result.errors.length > 0;
if (writeToFileSystem && !hasError) {
result.addLog(`Output location: ${outputOptions.base}\n`);
}

result.addLog(
`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`,
Expand All @@ -121,7 +118,6 @@ export async function* buildApplicationInternal(
watch: normalizedOptions.watch,
preserveSymlinks: normalizedOptions.preserveSymlinks,
poll: normalizedOptions.poll,
deleteOutputPath: normalizedOptions.deleteOutputPath,
cacheOptions: normalizedOptions.cacheOptions,
outputOptions: normalizedOptions.outputOptions,
verbose: normalizedOptions.verbose,
Expand All @@ -131,12 +127,6 @@ export async function* buildApplicationInternal(
clearScreen: normalizedOptions.clearScreen,
colors: normalizedOptions.colors,
jsonLogs: normalizedOptions.jsonLogs,
writeToFileSystem,
// For app-shell and SSG server files are not required by users.
// Omit these when SSR is not enabled.
writeToFileSystemFilter: writeServerBundles
? undefined
: (file) => file.type !== BuildOutputFileType.Server,
logger,
signal,
},
Expand Down Expand Up @@ -202,8 +192,80 @@ export async function* buildApplication(
extensions = pluginsOrExtensions;
}

for await (const result of buildApplicationInternal(options, context, undefined, extensions)) {
yield { success: result.kind !== ResultKind.Failure };
let initial = true;
for await (const result of buildApplicationInternal(options, context, extensions)) {
const outputOptions = result.detail?.['outputOptions'] as NormalizedOutputOptions | undefined;

if (initial) {
initial = false;

// Clean the output location if requested.
// Output options may not be present if the build failed.
if (outputOptions?.clean) {
await deleteOutputDir(context.workspaceRoot, outputOptions.base, [
outputOptions.browser,
outputOptions.server,
]);
}
}

if (result.kind === ResultKind.Failure) {
yield { success: false };
continue;
}

assert(outputOptions, 'Application output options are required for builder usage.');
assert(result.kind === ResultKind.Full, 'Application build did not provide a full output.');

// TODO: Restructure output logging to better handle stdout JSON piping
if (!useJSONBuildLogs) {
context.logger.info(`Output location: ${outputOptions.base}\n`);
}

// Writes the output files to disk and ensures the containing directories are present
const directoryExists = new Set<string>();
await emitFilesToDisk(Object.entries(result.files), async ([filePath, file]) => {
if (outputOptions.ignoreServer && file.type === BuildOutputFileType.Server) {
return;
}

let typeDirectory: string;
switch (file.type) {
case BuildOutputFileType.Browser:
case BuildOutputFileType.Media:
typeDirectory = outputOptions.browser;
break;
case BuildOutputFileType.Server:
typeDirectory = outputOptions.server;
break;
case BuildOutputFileType.Root:
typeDirectory = '';
break;
default:
throw new Error(
`Unhandled write for file "${filePath}" with type "${BuildOutputFileType[file.type]}".`,
);
}
// NOTE: 'base' is a fully resolved path at this point
const fullFilePath = path.join(outputOptions.base, typeDirectory, filePath);

// Ensure output subdirectories exist
const fileBasePath = path.dirname(fullFilePath);
if (fileBasePath && !directoryExists.has(fileBasePath)) {
await fs.mkdir(fileBasePath, { recursive: true });
directoryExists.add(fileBasePath);
}

if (file.origin === 'memory') {
// Write file contents
await fs.writeFile(fullFilePath, file.contents);
} else {
// Copy file contents
await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE);
}
});

yield { success: true };
}
}

Expand Down
Loading