@angular/build
Version:
Official build system for Angular
213 lines (212 loc) • 10.5 kB
JavaScript
;
/**
* @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
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildApplicationInternal = buildApplicationInternal;
exports.buildApplication = buildApplication;
const architect_1 = require("@angular-devkit/architect");
const node_assert_1 = __importDefault(require("node:assert"));
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
const utils_1 = require("../../tools/esbuild/utils");
const color_1 = require("../../utils/color");
const delete_output_dir_1 = require("../../utils/delete-output-dir");
const environment_options_1 = require("../../utils/environment-options");
const purge_cache_1 = require("../../utils/purge-cache");
const version_1 = require("../../utils/version");
const build_action_1 = require("./build-action");
const execute_build_1 = require("./execute-build");
const options_1 = require("./options");
const results_1 = require("./results");
const isNodeV22orHigher = Number(process.versions.node.split('.', 1)[0]) >= 22;
async function* buildApplicationInternal(options,
// TODO: Integrate abort signal support into builder system
context, extensions) {
const { workspaceRoot, logger, target } = context;
// Check Angular version.
(0, version_1.assertCompatibleAngularVersion)(workspaceRoot);
// Purge old build disk cache.
await (0, purge_cache_1.purgeStaleBuildCache)(context);
// Determine project name from builder context target
const projectName = target?.project;
if (!projectName) {
context.logger.error(`The 'application' builder requires a target to be specified.`);
// Only the vite-based dev server current uses the errors value
yield { kind: results_1.ResultKind.Failure, errors: [] };
return;
}
const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options, extensions);
if (!normalizedOptions.outputOptions.ignoreServer) {
const { browser, server } = normalizedOptions.outputOptions;
if (browser === '') {
context.logger.error(`'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`);
yield { kind: results_1.ResultKind.Failure, errors: [] };
return;
}
if (browser === server) {
context.logger.error(`'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`);
yield { kind: results_1.ResultKind.Failure, errors: [] };
return;
}
}
// Setup an abort controller with a builder teardown if no signal is present
let signal = context.signal;
if (!signal) {
const controller = new AbortController();
signal = controller.signal;
context.addTeardown(() => controller.abort('builder-teardown'));
}
yield* (0, build_action_1.runEsBuildBuildAction)(async (rebuildState) => {
const { serverEntryPoint, jsonLogs, partialSSRBuild } = normalizedOptions;
const startTime = process.hrtime.bigint();
const result = await (0, execute_build_1.executeBuild)(normalizedOptions, context, rebuildState);
if (jsonLogs) {
result.addLog(await (0, utils_1.createJsonBuildManifest)(result, normalizedOptions));
}
else {
if (serverEntryPoint && !partialSSRBuild) {
const prerenderedRoutesLength = Object.keys(result.prerenderedRoutes).length;
let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';
result.addLog(color_1.colors.magenta(prerenderMsg));
}
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
const hasError = result.errors.length > 0;
result.addLog(`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`);
}
return result;
}, {
watch: normalizedOptions.watch,
preserveSymlinks: normalizedOptions.preserveSymlinks,
poll: normalizedOptions.poll,
cacheOptions: normalizedOptions.cacheOptions,
outputOptions: normalizedOptions.outputOptions,
verbose: normalizedOptions.verbose,
projectRoot: normalizedOptions.projectRoot,
workspaceRoot: normalizedOptions.workspaceRoot,
progress: normalizedOptions.progress,
clearScreen: normalizedOptions.clearScreen,
colors: normalizedOptions.colors,
jsonLogs: normalizedOptions.jsonLogs,
incrementalResults: normalizedOptions.incrementalResults,
logger,
signal,
});
}
/**
* Builds an application using the `application` builder with the provided
* options.
*
* Usage of the `extensions` parameter is NOT supported and may cause unexpected
* build output or build failures.
*
* @experimental Direct usage of this function is considered experimental.
*
* @param options The options defined by the builder's schema to use.
* @param context An Architect builder context instance.
* @param extensions An object contain extension points for the build.
* @returns The build output results of the build.
*/
async function* buildApplication(options, context, extensions) {
let initial = true;
const internalOptions = { ...options, incrementalResults: true };
for await (const result of buildApplicationInternal(internalOptions, context, extensions)) {
const outputOptions = result.detail?.['outputOptions'];
if (initial) {
initial = false;
// Clean the output location if requested.
// Output options may not be present if the build failed.
if (outputOptions?.clean) {
await (0, delete_output_dir_1.deleteOutputDir)(context.workspaceRoot, outputOptions.base, [
outputOptions.browser,
outputOptions.server,
]);
}
}
if (result.kind === results_1.ResultKind.Failure) {
yield { success: false };
continue;
}
(0, node_assert_1.default)(outputOptions, 'Application output options are required for builder usage.');
(0, node_assert_1.default)(result.kind === results_1.ResultKind.Full || result.kind === results_1.ResultKind.Incremental, 'Application build did not provide a file result output.');
// TODO: Restructure output logging to better handle stdout JSON piping
if (!environment_options_1.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();
await (0, utils_1.emitFilesToDisk)(Object.entries(result.files), async ([filePath, file]) => {
if (outputOptions.ignoreServer &&
(file.type === bundler_context_1.BuildOutputFileType.ServerApplication ||
file.type === bundler_context_1.BuildOutputFileType.ServerRoot)) {
return;
}
const fullFilePath = generateFullPath(filePath, file.type, outputOptions);
// Ensure output subdirectories exist
const fileBasePath = node_path_1.default.dirname(fullFilePath);
if (fileBasePath && !directoryExists.has(fileBasePath)) {
await promises_1.default.mkdir(fileBasePath, { recursive: true });
directoryExists.add(fileBasePath);
}
if (file.origin === 'memory') {
// Write file contents
await promises_1.default.writeFile(fullFilePath, file.contents);
}
else {
// Copy file contents
if (isNodeV22orHigher) {
// Use newer `cp` API on Node.js 22+ (minimum v22 for CLI is 22.11)
await promises_1.default.cp(file.inputPath, fullFilePath, {
mode: promises_1.default.constants.COPYFILE_FICLONE,
preserveTimestamps: true,
});
}
else {
// For Node.js 20 use `copyFile` (`cp` is not stable for v20)
// TODO: Remove when Node.js 20 is no longer supported
await promises_1.default.copyFile(file.inputPath, fullFilePath, promises_1.default.constants.COPYFILE_FICLONE);
}
}
});
// Delete any removed files if incremental
if (result.kind === results_1.ResultKind.Incremental && result.removed?.length) {
await Promise.all(result.removed.map((file) => {
const fullFilePath = generateFullPath(file.path, file.type, outputOptions);
return promises_1.default.rm(fullFilePath, { force: true, maxRetries: 3 });
}));
}
yield { success: true };
}
}
function generateFullPath(filePath, type, outputOptions) {
let typeDirectory;
switch (type) {
case bundler_context_1.BuildOutputFileType.Browser:
case bundler_context_1.BuildOutputFileType.Media:
typeDirectory = outputOptions.browser;
break;
case bundler_context_1.BuildOutputFileType.ServerApplication:
case bundler_context_1.BuildOutputFileType.ServerRoot:
typeDirectory = outputOptions.server;
break;
case bundler_context_1.BuildOutputFileType.Root:
typeDirectory = '';
break;
default:
throw new Error(`Unhandled write for file "${filePath}" with type "${bundler_context_1.BuildOutputFileType[type]}".`);
}
// NOTE: 'base' is a fully resolved path at this point
const fullFilePath = node_path_1.default.join(outputOptions.base, typeDirectory, filePath);
return fullFilePath;
}
const builder = (0, architect_1.createBuilder)(buildApplication);
exports.default = builder;