@angular-devkit/build-angular
Version:
Angular Webpack Build Facade
343 lines (342 loc) • 17.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 __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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.BUILD_TIMEOUT = void 0;
exports.buildWebpackBrowser = buildWebpackBrowser;
const private_1 = require("@angular/build/private");
const architect_1 = require("@angular-devkit/architect");
const build_webpack_1 = require("@angular-devkit/build-webpack");
const webpack_1 = require("@ngtools/webpack");
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const rxjs_1 = require("rxjs");
const configs_1 = require("../../tools/webpack/configs");
const async_chunks_1 = require("../../tools/webpack/utils/async-chunks");
const helpers_1 = require("../../tools/webpack/utils/helpers");
const stats_1 = require("../../tools/webpack/utils/stats");
const utils_1 = require("../../utils");
const color_1 = require("../../utils/color");
const copy_assets_1 = require("../../utils/copy-assets");
const error_1 = require("../../utils/error");
const i18n_inlining_1 = require("../../utils/i18n-inlining");
const normalize_cache_1 = require("../../utils/normalize-cache");
const output_paths_1 = require("../../utils/output-paths");
const package_chunk_sort_1 = require("../../utils/package-chunk-sort");
const spinner_1 = require("../../utils/spinner");
const webpack_browser_config_1 = require("../../utils/webpack-browser-config");
/**
* Maximum time in milliseconds for single build/rebuild
* This accounts for CI variability.
*/
exports.BUILD_TIMEOUT = 30_000;
async function initialize(options, context, webpackConfigurationTransform) {
const originalOutputPath = options.outputPath;
// Assets are processed directly by the builder except when watching
const adjustedOptions = options.watch ? options : { ...options, assets: [] };
const { config, projectRoot, projectSourceRoot, i18n } = await (0, webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext)(adjustedOptions, context, (wco) => [
(0, configs_1.getCommonConfig)(wco),
(0, configs_1.getStylesConfig)(wco),
]);
let transformedConfig;
if (webpackConfigurationTransform) {
transformedConfig = await webpackConfigurationTransform(config);
}
if (options.deleteOutputPath) {
await (0, utils_1.deleteOutputDir)(context.workspaceRoot, originalOutputPath);
}
return { config: transformedConfig || config, projectRoot, projectSourceRoot, i18n };
}
/**
* @experimental Direct usage of this function is considered experimental.
*/
// eslint-disable-next-line max-lines-per-function
function buildWebpackBrowser(options, context, transforms = {}) {
const projectName = context.target?.project;
if (!projectName) {
throw new Error('The builder requires a target.');
}
const baseOutputPath = path.resolve(context.workspaceRoot, options.outputPath);
let outputPaths;
// Check Angular version.
(0, private_1.assertCompatibleAngularVersion)(context.workspaceRoot);
return (0, rxjs_1.from)(context.getProjectMetadata(projectName)).pipe((0, rxjs_1.switchMap)(async (projectMetadata) => {
// Purge old build disk cache.
await (0, private_1.purgeStaleBuildCache)(context);
// Initialize builder
const initialization = await initialize(options, context, transforms.webpackConfiguration);
// Add index file to watched files.
if (options.watch) {
const indexInputFile = path.join(context.workspaceRoot, (0, webpack_browser_config_1.getIndexInputFile)(options.index));
initialization.config.plugins ??= [];
initialization.config.plugins.push({
apply: (compiler) => {
compiler.hooks.thisCompilation.tap('build-angular', (compilation) => {
compilation.fileDependencies.add(indexInputFile);
});
},
});
}
return {
...initialization,
cacheOptions: (0, normalize_cache_1.normalizeCacheOptions)(projectMetadata, context.workspaceRoot),
};
}), (0, rxjs_1.switchMap)(
// eslint-disable-next-line max-lines-per-function
({ config, projectRoot, projectSourceRoot, i18n, cacheOptions }) => {
const normalizedOptimization = (0, utils_1.normalizeOptimization)(options.optimization);
return (0, build_webpack_1.runWebpack)(config, context, {
webpackFactory: require('webpack'),
logging: transforms.logging ||
((stats, config) => {
if (options.verbose && config.stats !== false) {
const statsOptions = config.stats === true ? undefined : config.stats;
context.logger.info(stats.toString(statsOptions));
}
}),
}).pipe((0, rxjs_1.concatMap)(
// eslint-disable-next-line max-lines-per-function
async (buildEvent) => {
const spinner = new spinner_1.Spinner();
spinner.enabled = options.progress !== false;
const { success, emittedFiles = [], outputPath: webpackOutputPath } = buildEvent;
const webpackRawStats = buildEvent.webpackStats;
if (!webpackRawStats) {
throw new Error('Webpack stats build result is required.');
}
// Fix incorrectly set `initial` value on chunks.
const extraEntryPoints = [
...(0, helpers_1.normalizeExtraEntryPoints)(options.styles || [], 'styles'),
...(0, helpers_1.normalizeExtraEntryPoints)(options.scripts || [], 'scripts'),
];
const webpackStats = {
...webpackRawStats,
chunks: (0, async_chunks_1.markAsyncChunksNonInitial)(webpackRawStats, extraEntryPoints),
};
if (!success) {
// If using bundle downleveling then there is only one build
// If it fails show any diagnostic messages and bail
if ((0, stats_1.statsHasWarnings)(webpackStats)) {
context.logger.warn((0, stats_1.statsWarningsToString)(webpackStats, { colors: true }));
}
if ((0, stats_1.statsHasErrors)(webpackStats)) {
context.logger.error((0, stats_1.statsErrorsToString)(webpackStats, { colors: true }));
}
return {
webpackStats: webpackRawStats,
output: { success: false },
};
}
else {
outputPaths = (0, output_paths_1.ensureOutputPaths)(baseOutputPath, i18n);
const scriptsEntryPointName = (0, helpers_1.normalizeExtraEntryPoints)(options.scripts || [], 'scripts').map((x) => x.bundleName);
if (i18n.shouldInline) {
const success = await (0, i18n_inlining_1.i18nInlineEmittedFiles)(context, emittedFiles, i18n, baseOutputPath, Array.from(outputPaths.values()), scriptsEntryPointName, webpackOutputPath, options.i18nMissingTranslation);
if (!success) {
return {
webpackStats: webpackRawStats,
output: { success: false },
};
}
}
// Check for budget errors and display them to the user.
const budgets = options.budgets;
let budgetFailures;
if (budgets?.length) {
budgetFailures = [...(0, private_1.checkBudgets)(budgets, webpackStats)];
for (const { severity, message } of budgetFailures) {
switch (severity) {
case private_1.ThresholdSeverity.Warning:
webpackStats.warnings?.push({ message });
break;
case private_1.ThresholdSeverity.Error:
webpackStats.errors?.push({ message });
break;
default:
assertNever(severity);
}
}
}
const buildSuccess = success && !(0, stats_1.statsHasErrors)(webpackStats);
if (buildSuccess) {
// Copy assets
if (!options.watch && options.assets?.length) {
spinner.start('Copying assets...');
try {
await (0, copy_assets_1.copyAssets)((0, utils_1.normalizeAssetPatterns)(options.assets, context.workspaceRoot, projectRoot, projectSourceRoot), Array.from(outputPaths.values()), context.workspaceRoot);
spinner.succeed('Copying assets complete.');
}
catch (err) {
spinner.fail(color_1.colors.redBright('Copying of assets failed.'));
(0, error_1.assertIsError)(err);
return {
output: {
success: false,
error: 'Unable to copy assets: ' + err.message,
},
webpackStats: webpackRawStats,
};
}
}
if (options.index) {
spinner.start('Generating index html...');
const entrypoints = (0, package_chunk_sort_1.generateEntryPoints)({
scripts: options.scripts ?? [],
styles: options.styles ?? [],
});
const indexHtmlGenerator = new private_1.IndexHtmlGenerator({
cache: cacheOptions,
indexPath: path.join(context.workspaceRoot, (0, webpack_browser_config_1.getIndexInputFile)(options.index)),
entrypoints,
deployUrl: options.deployUrl,
sri: options.subresourceIntegrity,
optimization: normalizedOptimization,
crossOrigin: options.crossOrigin,
postTransform: transforms.indexHtml,
imageDomains: Array.from(webpack_1.imageDomains),
});
let hasErrors = false;
for (const [locale, outputPath] of outputPaths.entries()) {
try {
const { csrContent: content, warnings, errors, } = await indexHtmlGenerator.process({
baseHref: getLocaleBaseHref(i18n, locale) ?? options.baseHref,
// i18nLocale is used when Ivy is disabled
lang: locale || undefined,
outputPath,
files: mapEmittedFilesToFileInfo(emittedFiles),
});
if (warnings.length || errors.length) {
spinner.stop();
warnings.forEach((m) => context.logger.warn(m));
errors.forEach((m) => {
context.logger.error(m);
hasErrors = true;
});
spinner.start();
}
const indexOutput = path.join(outputPath, (0, webpack_browser_config_1.getIndexOutputFile)(options.index));
await fs.promises.mkdir(path.dirname(indexOutput), { recursive: true });
await fs.promises.writeFile(indexOutput, content);
}
catch (error) {
spinner.fail('Index html generation failed.');
(0, error_1.assertIsError)(error);
return {
webpackStats: webpackRawStats,
output: { success: false, error: error.message },
};
}
}
if (hasErrors) {
spinner.fail('Index html generation failed.');
return {
webpackStats: webpackRawStats,
output: { success: false },
};
}
else {
spinner.succeed('Index html generation complete.');
}
}
if (options.serviceWorker) {
spinner.start('Generating service worker...');
for (const [locale, outputPath] of outputPaths.entries()) {
try {
await (0, private_1.augmentAppWithServiceWorker)(projectRoot, context.workspaceRoot, outputPath, getLocaleBaseHref(i18n, locale) ?? options.baseHref ?? '/', options.ngswConfigPath);
}
catch (error) {
spinner.fail('Service worker generation failed.');
(0, error_1.assertIsError)(error);
return {
webpackStats: webpackRawStats,
output: { success: false, error: error.message },
};
}
}
spinner.succeed('Service worker generation complete.');
}
}
(0, stats_1.webpackStatsLogger)(context.logger, webpackStats, config, budgetFailures);
return {
webpackStats: webpackRawStats,
output: { success: buildSuccess },
};
}
}), (0, rxjs_1.map)(({ output: event, webpackStats }) => ({
...event,
stats: (0, stats_1.generateBuildEventStats)(webpackStats, options),
baseOutputPath,
outputs: (outputPaths &&
[...outputPaths.entries()].map(([locale, path]) => ({
locale,
path,
baseHref: getLocaleBaseHref(i18n, locale) ?? options.baseHref,
}))) || {
path: baseOutputPath,
baseHref: options.baseHref,
},
})));
}));
function getLocaleBaseHref(i18n, locale) {
if (i18n.flatOutput) {
return undefined;
}
const localeData = i18n.locales[locale];
if (!localeData) {
return undefined;
}
const baseHrefSuffix = localeData.baseHref ?? localeData.subPath + '/';
return baseHrefSuffix !== '' ? (0, utils_1.urlJoin)(options.baseHref || '', baseHrefSuffix) : undefined;
}
}
function assertNever(input) {
throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`);
}
function mapEmittedFilesToFileInfo(files = []) {
const filteredFiles = [];
for (const { file, name, extension, initial } of files) {
if (name && initial) {
filteredFiles.push({ file, extension, name });
}
}
return filteredFiles;
}
exports.default = (0, architect_1.createBuilder)(buildWebpackBrowser);
;