UNPKG

@angular-devkit/build-angular

Version:
343 lines (342 loc) 17.5 kB
"use strict"; /** * @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);