@angular/build
Version:
Official build system for Angular
354 lines (353 loc) • 15 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runEsBuildBuildAction = runEsBuildBuildAction;
const node_fs_1 = require("node:fs");
const node_path_1 = __importDefault(require("node:path"));
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
const sass_language_1 = require("../../tools/esbuild/stylesheets/sass-language");
const utils_1 = require("../../tools/esbuild/utils");
const environment_options_1 = require("../../utils/environment-options");
const results_1 = require("./results");
// Watch workspace for package manager changes
const packageWatchFiles = [
// manifest can affect module resolution
'package.json',
// npm lock file
'package-lock.json',
// pnpm lock file
'pnpm-lock.yaml',
// yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/)
'yarn.lock',
'.pnp.cjs',
'.pnp.data.json',
];
async function* runEsBuildBuildAction(action, options) {
const { watch, poll, clearScreen, logger, cacheOptions, outputOptions, verbose, projectRoot, workspaceRoot, progress, preserveSymlinks, colors, jsonLogs, incrementalResults, } = options;
const withProgress = progress ? utils_1.withSpinner : utils_1.withNoProgress;
// Initial build
let result;
try {
// Perform the build action
result = await withProgress('Building...', () => action());
// Log all diagnostic (error/warning/logs) messages
await (0, utils_1.logMessages)(logger, result, colors, jsonLogs);
}
finally {
// Ensure Sass workers are shutdown if not watching
if (!watch) {
(0, sass_language_1.shutdownSassWorkerPool)();
}
}
// Setup watcher if watch mode enabled
let watcher;
if (watch) {
if (progress) {
logger.info('Watch mode enabled. Watching for file changes...');
}
const ignored = [
// Ignore the output and cache paths to avoid infinite rebuild cycles
outputOptions.base,
cacheOptions.basePath,
`${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`,
];
// Setup a watcher
const { createWatcher } = await Promise.resolve().then(() => __importStar(require('../../tools/esbuild/watcher')));
watcher = createWatcher({
polling: typeof poll === 'number',
interval: poll,
followSymlinks: preserveSymlinks,
ignored,
});
// Setup abort support
options.signal?.addEventListener('abort', () => void watcher?.close());
// Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
if (environment_options_1.shouldWatchRoot) {
if (!preserveSymlinks) {
// Ignore all node modules directories to avoid excessive file watchers.
// Package changes are handled below by watching manifest and lock files.
// NOTE: this is not enable when preserveSymlinks is true as this would break `npm link` usages.
ignored.push('**/node_modules/**');
watcher.add(packageWatchFiles
.map((file) => node_path_1.default.join(workspaceRoot, file))
.filter((file) => (0, node_fs_1.existsSync)(file)));
}
watcher.add(projectRoot);
}
// Watch locations provided by the initial build result
watcher.add(result.watchFiles);
}
// 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* emitOutputResults(result, outputOptions);
// Finish if watch mode is not enabled
if (!watcher) {
return;
}
// Used to force a full result on next rebuild if there were initial errors.
// This ensures at least one full result is emitted.
let hasInitialErrors = result.errors.length > 0;
// Wait for changes and rebuild as needed
const currentWatchFiles = new Set(result.watchFiles);
try {
for await (const changes of watcher) {
if (options.signal?.aborted) {
break;
}
if (clearScreen) {
// eslint-disable-next-line no-console
console.clear();
}
if (verbose) {
logger.info(changes.toDebugString());
}
// Clear removed files from current watch files
changes.removed.forEach((removedPath) => currentWatchFiles.delete(removedPath));
const rebuildState = result.createRebuildState(changes);
result = await withProgress('Changes detected. Rebuilding...', () => action(rebuildState));
// Log all diagnostic (error/warning/logs) messages
await (0, utils_1.logMessages)(logger, result, colors, jsonLogs);
// Update watched locations provided by the new build result.
// Keep watching all previous files if there are any errors; otherwise consider all
// files stale until confirmed present in the new result's watch files.
const staleWatchFiles = result.errors.length > 0 ? undefined : new Set(currentWatchFiles);
for (const watchFile of result.watchFiles) {
if (!currentWatchFiles.has(watchFile)) {
// Add new watch location
watcher.add(watchFile);
currentWatchFiles.add(watchFile);
}
// Present so remove from stale locations
staleWatchFiles?.delete(watchFile);
}
// Remove any stale locations if the build was successful
if (staleWatchFiles?.size) {
watcher.remove([...staleWatchFiles]);
}
for (const outputResult of emitOutputResults(result, outputOptions, changes, incrementalResults && !hasInitialErrors ? rebuildState : undefined)) {
yield outputResult;
}
// Clear initial build errors flag if no errors are now present
hasInitialErrors &&= result.errors.length > 0;
}
}
finally {
// Stop the watcher and cleanup incremental rebuild state
await Promise.allSettled([watcher.close(), result.dispose()]);
(0, sass_language_1.shutdownSassWorkerPool)();
}
}
function* emitOutputResults({ outputFiles, assetFiles, errors, warnings, externalMetadata, htmlIndexPath, htmlBaseHref, templateUpdates, }, outputOptions, changes, rebuildState) {
if (errors.length > 0) {
yield {
kind: results_1.ResultKind.Failure,
errors: errors,
warnings: warnings,
detail: {
outputOptions,
},
};
return;
}
// Use a full result if there is no rebuild state (no prior build result)
if (!rebuildState || !changes) {
const result = {
kind: results_1.ResultKind.Full,
warnings: warnings,
files: {},
detail: {
externalMetadata,
htmlIndexPath,
htmlBaseHref,
outputOptions,
},
};
for (const file of assetFiles) {
result.files[file.destination] = {
type: bundler_context_1.BuildOutputFileType.Browser,
inputPath: file.source,
origin: 'disk',
};
}
for (const file of outputFiles) {
result.files[file.path] = {
type: file.type,
contents: file.contents,
origin: 'memory',
hash: file.hash,
};
}
yield result;
return;
}
// Template updates only exist if no other JS changes have occurred.
// A full page reload may be required based on the following incremental output change analysis.
const hasTemplateUpdates = !!templateUpdates?.size;
// Use an incremental result if previous output information is available
const { previousAssetsInfo, previousOutputInfo } = rebuildState;
const incrementalResult = {
kind: results_1.ResultKind.Incremental,
warnings: warnings,
// Initially attempt to use a background update of files to support component updates.
background: hasTemplateUpdates,
added: [],
removed: [],
modified: [],
files: {},
detail: {
externalMetadata,
htmlIndexPath,
htmlBaseHref,
outputOptions,
},
};
let hasCssUpdates = false;
// Initially assume all previous output files have been removed
const removedOutputFiles = new Map(previousOutputInfo);
for (const file of outputFiles) {
removedOutputFiles.delete(file.path);
const previousHash = previousOutputInfo.get(file.path)?.hash;
let needFile = false;
if (previousHash === undefined) {
needFile = true;
incrementalResult.added.push(file.path);
}
else if (previousHash !== file.hash) {
needFile = true;
incrementalResult.modified.push(file.path);
}
if (needFile) {
if (file.path.endsWith('.css')) {
hasCssUpdates = true;
}
else if (!canBackgroundUpdate(file)) {
incrementalResult.background = false;
}
incrementalResult.files[file.path] = {
type: file.type,
contents: file.contents,
origin: 'memory',
hash: file.hash,
};
}
}
// Initially assume all previous assets files have been removed
const removedAssetFiles = new Map(previousAssetsInfo);
for (const { source, destination } of assetFiles) {
removedAssetFiles.delete(source);
if (!previousAssetsInfo.has(source)) {
incrementalResult.added.push(destination);
incrementalResult.background = false;
}
else if (changes.modified.has(source)) {
incrementalResult.modified.push(destination);
incrementalResult.background = false;
}
else {
continue;
}
hasCssUpdates ||= destination.endsWith('.css');
incrementalResult.files[destination] = {
type: bundler_context_1.BuildOutputFileType.Browser,
inputPath: source,
origin: 'disk',
};
}
// Do not remove stale files yet if there are template updates.
// Component chunk files may still be referenced in running browser code.
// Module evaluation time component updates will update any of these files.
// This typically occurs when a lazy component is changed that has not yet
// been accessed at runtime.
if (hasTemplateUpdates && incrementalResult.background) {
removedOutputFiles.clear();
}
// Include the removed output and asset files
incrementalResult.removed.push(...Array.from(removedOutputFiles, ([file, { type }]) => ({
path: file,
type,
})), ...Array.from(removedAssetFiles.values(), (file) => ({
path: file,
type: bundler_context_1.BuildOutputFileType.Browser,
})));
yield incrementalResult;
// If there are template updates and the incremental update was background only, a component
// update is possible.
if (hasTemplateUpdates && incrementalResult.background) {
// Template changes may be accompanied by stylesheet changes and these should also be updated hot when possible.
if (hasCssUpdates) {
const styleResult = {
kind: results_1.ResultKind.Incremental,
added: incrementalResult.added.filter(isCssFilePath),
removed: incrementalResult.removed.filter(({ path }) => isCssFilePath(path)),
modified: incrementalResult.modified.filter(isCssFilePath),
files: Object.fromEntries(Object.entries(incrementalResult.files).filter(([path]) => isCssFilePath(path))),
};
yield styleResult;
}
const updateResult = {
kind: results_1.ResultKind.ComponentUpdate,
updates: Array.from(templateUpdates, ([id, content]) => ({
type: 'template',
id,
content,
})),
};
yield updateResult;
}
}
function isCssFilePath(filePath) {
return /\.css(?:\.map)?$/i.test(filePath);
}
function canBackgroundUpdate(file) {
// Files in the output root are not served and do not affect the
// application available with the development server.
if (file.type === bundler_context_1.BuildOutputFileType.Root) {
return true;
}
// Updates to non-JS files must signal an update with the dev server
// except the service worker configuration which is special cased.
return /(?:\.m?js|\.map)$/.test(file.path) || file.path === 'ngsw.json';
}