@angular-devkit/build-angular
Version:
Angular Webpack Build Facade
297 lines (295 loc) • 14.4 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.serveWebpackBrowser = serveWebpackBrowser;
const private_1 = require("@angular/build/private");
const build_webpack_1 = require("@angular-devkit/build-webpack");
const core_1 = require("@angular-devkit/core");
const path = __importStar(require("node:path"));
const url = __importStar(require("node:url"));
const rxjs_1 = require("rxjs");
const configs_1 = require("../../tools/webpack/configs");
const index_html_webpack_plugin_1 = require("../../tools/webpack/plugins/index-html-webpack-plugin");
const service_worker_plugin_1 = require("../../tools/webpack/plugins/service-worker-plugin");
const stats_1 = require("../../tools/webpack/utils/stats");
const utils_1 = require("../../utils");
const color_1 = require("../../utils/color");
const i18n_webpack_1 = require("../../utils/i18n-webpack");
const load_esm_1 = require("../../utils/load-esm");
const package_chunk_sort_1 = require("../../utils/package-chunk-sort");
const webpack_browser_config_1 = require("../../utils/webpack-browser-config");
const webpack_diagnostics_1 = require("../../utils/webpack-diagnostics");
const schema_1 = require("../browser/schema");
/**
* Reusable implementation of the Angular Webpack development server builder.
* @param options Dev Server options.
* @param builderName The name of the builder used to build the application.
* @param context The build context.
* @param transforms A map of transforms that can be used to hook into some logic (such as
* transforming webpack configuration before passing it to webpack).
*/
// eslint-disable-next-line max-lines-per-function
function serveWebpackBrowser(options, builderName, context, transforms = {}) {
// Check Angular version.
const { logger, workspaceRoot } = context;
(0, private_1.assertCompatibleAngularVersion)(workspaceRoot);
async function setup() {
if (options.hmr) {
logger.warn(core_1.tags.stripIndents `NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.
See https://webpack.js.org/guides/hot-module-replacement for information on working with HMR for Webpack.`);
}
// Get the browser configuration from the target name.
const rawBrowserOptions = await context.getTargetOptions(options.buildTarget);
if (rawBrowserOptions.outputHashing && rawBrowserOptions.outputHashing !== schema_1.OutputHashing.None) {
// Disable output hashing for dev build as this can cause memory leaks
// See: https://github.com/webpack/webpack-dev-server/issues/377#issuecomment-241258405
rawBrowserOptions.outputHashing = schema_1.OutputHashing.None;
logger.warn(`Warning: 'outputHashing' option is disabled when using the dev-server.`);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const browserOptions = (await context.validateOptions({
...rawBrowserOptions,
watch: options.watch,
verbose: options.verbose,
// In dev server we should not have budgets because of extra libs such as socks-js
budgets: undefined,
}, builderName));
const { styles, scripts } = (0, utils_1.normalizeOptimization)(browserOptions.optimization);
if (scripts || styles.minify) {
logger.error(core_1.tags.stripIndents `
****************************************************************************************
This is a simple server for use in testing or debugging Angular applications locally.
It hasn't been reviewed for security issues.
DON'T USE IT FOR PRODUCTION!
****************************************************************************************
`);
}
const { config, i18n } = await (0, webpack_browser_config_1.generateI18nBrowserWebpackConfigFromContext)(browserOptions, context, (wco) => [(0, configs_1.getDevServerConfig)(wco), (0, configs_1.getCommonConfig)(wco), (0, configs_1.getStylesConfig)(wco)], options);
if (!config.devServer) {
throw new Error('Webpack Dev Server configuration was not set.');
}
let locale;
if (i18n.shouldInline) {
// Dev-server only supports one locale
locale = [...i18n.inlineLocales][0];
}
else if (i18n.hasDefinedSourceLocale) {
// use source locale if not localizing
locale = i18n.sourceLocale;
}
let webpackConfig = config;
// If a locale is defined, setup localization
if (locale) {
if (i18n.inlineLocales.size > 1) {
throw new Error('The development server only supports localizing a single locale per build.');
}
await setupLocalize(locale, i18n, browserOptions, webpackConfig, options.cacheOptions, context);
}
if (transforms.webpackConfiguration) {
webpackConfig = await transforms.webpackConfiguration(webpackConfig);
}
webpackConfig.plugins ??= [];
if (browserOptions.index) {
const { scripts = [], styles = [], baseHref } = browserOptions;
const entrypoints = (0, package_chunk_sort_1.generateEntryPoints)({
scripts,
styles,
// The below is needed as otherwise HMR for CSS will break.
// styles.js and runtime.js needs to be loaded as a non-module scripts as otherwise `document.currentScript` will be null.
// https://github.com/webpack-contrib/mini-css-extract-plugin/blob/90445dd1d81da0c10b9b0e8a17b417d0651816b8/src/hmr/hotModuleReplacement.js#L39
isHMREnabled: !!webpackConfig.devServer?.hot,
});
webpackConfig.plugins.push(new index_html_webpack_plugin_1.IndexHtmlWebpackPlugin({
indexPath: path.resolve(workspaceRoot, (0, webpack_browser_config_1.getIndexInputFile)(browserOptions.index)),
outputPath: (0, webpack_browser_config_1.getIndexOutputFile)(browserOptions.index),
baseHref,
entrypoints,
deployUrl: browserOptions.deployUrl,
sri: browserOptions.subresourceIntegrity,
cache: options.cacheOptions,
postTransform: transforms.indexHtml,
optimization: (0, utils_1.normalizeOptimization)(browserOptions.optimization),
crossOrigin: browserOptions.crossOrigin,
lang: locale,
}));
}
if (browserOptions.serviceWorker) {
webpackConfig.plugins.push(new service_worker_plugin_1.ServiceWorkerPlugin({
baseHref: browserOptions.baseHref,
root: context.workspaceRoot,
projectRoot: options.projectRoot,
ngswConfigPath: browserOptions.ngswConfigPath,
}));
}
return {
browserOptions,
webpackConfig,
};
}
return (0, rxjs_1.from)(setup()).pipe((0, rxjs_1.switchMap)(({ browserOptions, webpackConfig }) => {
return (0, build_webpack_1.runWebpackDevServer)(webpackConfig, context, {
logging: transforms.logging || (0, stats_1.createWebpackLoggingCallback)(browserOptions, logger),
webpackFactory: require('webpack'),
webpackDevServerFactory: require('webpack-dev-server'),
}).pipe((0, rxjs_1.concatMap)(async (buildEvent, index) => {
const webpackRawStats = buildEvent.webpackStats;
if (!webpackRawStats) {
throw new Error('Webpack stats build result is required.');
}
// Resolve serve address.
const publicPath = webpackConfig.devServer?.devMiddleware?.publicPath;
const serverAddress = url.format({
protocol: options.ssl ? 'https' : 'http',
hostname: options.host === '0.0.0.0' ? 'localhost' : options.host,
port: buildEvent.port,
pathname: typeof publicPath === 'string' ? publicPath : undefined,
});
if (index === 0) {
logger.info('\n' +
core_1.tags.oneLine `
**
Angular Live Development Server is listening on ${options.host}:${buildEvent.port},
open your browser on ${serverAddress}
**
` +
'\n');
if (options.open) {
const open = (await (0, load_esm_1.loadEsmModule)('open')).default;
await open(serverAddress);
}
}
if (buildEvent.success) {
logger.info(`\n${color_1.colors.greenBright(color_1.colors.symbols.check)} Compiled successfully.`);
}
else {
logger.info(`\n${color_1.colors.redBright(color_1.colors.symbols.cross)} Failed to compile.`);
}
return {
...buildEvent,
baseUrl: serverAddress,
stats: (0, stats_1.generateBuildEventStats)(webpackRawStats, browserOptions),
};
}));
}));
}
async function setupLocalize(locale, i18n, browserOptions, webpackConfig, cacheOptions, context) {
const localeDescription = i18n.locales[locale];
// Modify main entrypoint to include locale data
if (localeDescription?.dataPath &&
typeof webpackConfig.entry === 'object' &&
!Array.isArray(webpackConfig.entry) &&
webpackConfig.entry['main']) {
if (Array.isArray(webpackConfig.entry['main'])) {
webpackConfig.entry['main'].unshift(localeDescription.dataPath);
}
else {
webpackConfig.entry['main'] = [
localeDescription.dataPath,
webpackConfig.entry['main'],
];
}
}
let missingTranslationBehavior = browserOptions.i18nMissingTranslation || 'ignore';
let translation = localeDescription?.translation || {};
if (locale === i18n.sourceLocale) {
missingTranslationBehavior = 'ignore';
translation = {};
}
const i18nLoaderOptions = {
locale,
missingTranslationBehavior,
translation: i18n.shouldInline ? translation : undefined,
translationFiles: localeDescription?.files.map((file) => path.resolve(context.workspaceRoot, file.path)),
};
const i18nRule = {
test: /\.[cm]?[tj]sx?$/,
enforce: 'post',
use: [
{
loader: require.resolve('../../tools/babel/webpack-loader'),
options: {
cacheDirectory: (cacheOptions.enabled && path.join(cacheOptions.path, 'babel-dev-server-i18n')) ||
false,
cacheIdentifier: JSON.stringify({
locale,
translationIntegrity: localeDescription?.files.map((file) => file.integrity),
}),
i18n: i18nLoaderOptions,
},
},
],
};
// Get the rules and ensure the Webpack configuration is setup properly
const rules = webpackConfig.module?.rules || [];
if (!webpackConfig.module) {
webpackConfig.module = { rules };
}
else if (!webpackConfig.module.rules) {
webpackConfig.module.rules = rules;
}
rules.push(i18nRule);
// Add a plugin to reload translation files on rebuilds
const loader = await (0, private_1.createTranslationLoader)();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
webpackConfig.plugins.push({
apply: (compiler) => {
compiler.hooks.thisCompilation.tap('build-angular', (compilation) => {
if (i18n.shouldInline && i18nLoaderOptions.translation === undefined) {
// Reload translations
(0, i18n_webpack_1.loadTranslations)(locale, localeDescription, context.workspaceRoot, loader, {
warn(message) {
(0, webpack_diagnostics_1.addWarning)(compilation, message);
},
error(message) {
(0, webpack_diagnostics_1.addError)(compilation, message);
},
}, undefined, browserOptions.i18nDuplicateTranslation);
i18nLoaderOptions.translation = localeDescription.translation ?? {};
}
compilation.hooks.finishModules.tap('build-angular', () => {
// After loaders are finished, clear out the now unneeded translations
i18nLoaderOptions.translation = undefined;
});
});
},
});
}
;