refactor(@angular-devkit/build-angular): move dev-server webpack config in a separate file

With this change we remove webpack dev-server logic to a seperate file. We also use the webpack-dev-server API to add live-reload and hmr entry-points and settings.
This commit is contained in:
Alan Agius 2020-10-16 14:31:12 +02:00
parent 710e12dd7d
commit 247b87d40a
10 changed files with 387 additions and 437 deletions

View File

@ -19,7 +19,6 @@ import * as webpack from 'webpack';
import { ExecutionTransformer } from '../transforms';
import {
BuildBrowserFeatures,
NormalizedBrowserBuilderSchema,
deleteOutputDir,
normalizeAssetPatterns,
normalizeOptimization,
@ -51,7 +50,6 @@ import { readTsconfig } from '../utils/read-tsconfig';
import { augmentAppWithServiceWorker } from '../utils/service-worker';
import { assertCompatibleAngularVersion } from '../utils/version';
import {
BrowserWebpackConfigOptions,
generateI18nBrowserWebpackConfigFromContext,
getIndexInputFile,
getIndexOutputFile,
@ -102,32 +100,7 @@ interface ConfigFromContextReturn {
i18n: I18nOptions;
}
export async function buildBrowserWebpackConfigFromContext(
options: BrowserBuilderSchema,
context: BuilderContext,
host: virtualFs.Host<fs.Stats> = new NodeJsSyncHost(),
extraBuildOptions: Partial<NormalizedBrowserBuilderSchema> = {},
): Promise<ConfigFromContextReturn> {
const webpackPartialGenerator = (wco: BrowserWebpackConfigOptions) => [
getCommonConfig(wco),
getBrowserConfig(wco),
getStylesConfig(wco),
getStatsConfig(wco),
getAnalyticsConfig(wco, context),
getCompilerConfig(wco),
wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {},
];
return generateI18nBrowserWebpackConfigFromContext(
options,
context,
webpackPartialGenerator,
host,
extraBuildOptions,
);
}
function getAnalyticsConfig(
export function getAnalyticsConfig(
wco: WebpackConfigOptions,
context: BuilderContext,
): webpack.Configuration {
@ -154,7 +127,7 @@ function getAnalyticsConfig(
return {};
}
function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration {
export function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration {
if (wco.buildOptions.main || wco.buildOptions.polyfills) {
return wco.buildOptions.aot ? getAotConfig(wco) : getNonAotConfig(wco);
}
@ -193,7 +166,21 @@ async function initialize(
projectRoot,
projectSourceRoot,
i18n,
} = await buildBrowserWebpackConfigFromContext(adjustedOptions, context, host, { differentialLoadingMode });
} = await generateI18nBrowserWebpackConfigFromContext(
adjustedOptions,
context,
wco => [
getCommonConfig(wco),
getBrowserConfig(wco),
getStylesConfig(wco),
getStatsConfig(wco),
getAnalyticsConfig(wco, context),
getCompilerConfig(wco),
wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {},
],
host,
{ differentialLoadingMode },
);
// Validate asset option values if processed directly
if (options.assets?.length && !adjustedOptions.assets?.length) {
@ -746,8 +733,8 @@ export function buildWebpackBrowser(
if (options.index) {
await writeIndexHtml({
host,
outputPath: path.join(outputPath, getIndexOutputFile(options)),
indexPath: path.join(context.workspaceRoot, getIndexInputFile(options)),
outputPath: path.join(outputPath, getIndexOutputFile(options.index)),
indexPath: path.join(context.workspaceRoot, getIndexInputFile(options.index)),
files,
noModuleFiles,
moduleFiles,

View File

@ -12,17 +12,16 @@ import {
WebpackLoggingCallback,
runWebpackDevServer,
} from '@angular-devkit/build-webpack';
import { json, logging, tags } from '@angular-devkit/core';
import { json, tags } from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { existsSync, readFileSync } from 'fs';
import * as path from 'path';
import { Observable, from } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import * as ts from 'typescript';
import * as url from 'url';
import * as webpack from 'webpack';
import * as WebpackDevServer from 'webpack-dev-server';
import { buildBrowserWebpackConfigFromContext } from '../browser';
import * as webpackDevServer from 'webpack-dev-server';
import { getAnalyticsConfig, getCompilerConfig } from '../browser';
import { Schema as BrowserBuilderSchema } from '../browser/schema';
import { ExecutionTransformer } from '../transforms';
import { BuildBrowserFeatures, normalizeOptimization } from '../utils';
@ -35,8 +34,10 @@ import { generateEntryPoints } from '../utils/package-chunk-sort';
import { createI18nPlugins } from '../utils/process-bundle';
import { readTsconfig } from '../utils/read-tsconfig';
import { assertCompatibleAngularVersion } from '../utils/version';
import { getIndexInputFile, getIndexOutputFile } from '../utils/webpack-browser-config';
import { generateI18nBrowserWebpackConfigFromContext, getIndexInputFile, getIndexOutputFile } from '../utils/webpack-browser-config';
import { addError, addWarning } from '../utils/webpack-diagnostics';
import { getBrowserConfig, getCommonConfig, getStatsConfig, getStylesConfig, getWorkerConfig } from '../webpack/configs';
import { getDevServerConfig } from '../webpack/configs/dev-server';
import { IndexHtmlWebpackPlugin } from '../webpack/plugins/index-html-webpack-plugin';
import { createWebpackLoggingCallback } from '../webpack/utils/stats';
import { Schema } from './schema';
@ -89,12 +90,12 @@ export function serveWebpackBrowser(
async function setup(): Promise<{
browserOptions: json.JsonObject & BrowserBuilderSchema;
webpackConfig: webpack.Configuration;
webpackDevServerConfig: WebpackDevServer.Configuration;
projectRoot: string;
locale: string | undefined;
}> {
// Get the browser configuration from the target name.
const rawBrowserOptions = await context.getTargetOptions(browserTarget);
options.port = await checkPort(options.port ?? 4200, options.host || 'localhost');
// Override options we need to override, if defined.
const overrides = (Object.keys(options) as (keyof DevServerBuilderOptions)[])
@ -107,6 +108,17 @@ export function serveWebpackBrowser(
{},
);
// Get dev-server only options.
const devServerOptions = (Object.keys(options) as (keyof Schema)[])
.filter(key => !devServerBuildOverriddenKeys.includes(key) && key !== 'browserTarget')
.reduce<Partial<Schema>>(
(previous, key) => ({
...previous,
[key]: options[key],
}),
{},
);
// In dev server we should not have budgets because of extra libs such as socks-js
overrides.budgets = undefined;
@ -116,33 +128,97 @@ export function serveWebpackBrowser(
browserName,
);
const { config, projectRoot, i18n } = await buildBrowserWebpackConfigFromContext(
const { config, projectRoot, i18n } = await generateI18nBrowserWebpackConfigFromContext(
browserOptions,
context,
wco => [
getDevServerConfig(wco),
getCommonConfig(wco),
getBrowserConfig(wco),
getStylesConfig(wco),
getStatsConfig(wco),
getAnalyticsConfig(wco, context),
getCompilerConfig(wco),
browserOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {},
],
host,
{ hmr: options.hmr },
devServerOptions,
);
let webpackConfig = config;
if (!config.devServer) {
throw new Error(
'Webpack Dev Server configuration was not set.',
);
}
if (options.liveReload || options.hmr) {
// This is needed because we cannot use the inline option directly in the config
// because of the SuppressExtractedTextChunksWebpackPlugin
// Consider not using SuppressExtractedTextChunksWebpackPlugin when liveReload is enable.
webpackDevServer.addDevServerEntrypoints(config, {
...config.devServer,
inline: true,
});
// Remove live-reload code from all entrypoints but not main.
// Otherwise this will break SuppressExtractedTextChunksWebpackPlugin because
// 'addDevServerEntrypoints' adds addional entry-points to all entries.
if (!options.hmr && config.entry && typeof config.entry === 'object' && !Array.isArray(config.entry) && config.entry.main) {
for (const [key, value] of Object.entries(config.entry)) {
if (key === 'main' || typeof value === 'string') {
continue;
}
for (let index = 0; index < value.length; index++) {
if (value[index].includes('webpack-dev-server/client/index.js')) {
config.entry[key] = value.splice(index + 1, 1);
}
}
}
}
if (options.hmr) {
context.logger.warn(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.`);
}
}
if (
options.host
&& !/^127\.\d+\.\d+\.\d+/g.test(options.host)
&& options.host !== 'localhost'
) {
context.logger.warn(tags.stripIndent`
Warning: This is a simple server for use in testing or debugging Angular applications
locally. It hasn't been reviewed for security issues.
Binding this server to an open connection can result in compromising your application or
computer. Using a different host than the one passed to the "--host" flag might result in
websocket connection issues. You might need to use "--disableHostCheck" if that's the
case.
`);
}
if (options.disableHostCheck) {
context.logger.warn(tags.oneLine`
Warning: Running a server with --disable-host-check is a security risk.
See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
for more information.
`);
}
let webpackConfig = config;
const tsConfig = readTsconfig(browserOptions.tsConfig, context.workspaceRoot);
if (i18n.shouldInline && tsConfig.options.enableIvy !== false) {
if (i18n.inlineLocales.size > 1) {
throw new Error(
'The development server only supports localizing a single locale per build',
'The development server only supports localizing a single locale per build.',
);
}
await setupLocalize(i18n, browserOptions, webpackConfig);
}
options.port = await checkPort(options.port ?? 4200, options.host || 'localhost');
const webpackDevServerConfig = (webpackConfig.devServer = buildServerConfig(
root,
options,
browserOptions,
context.logger,
));
if (transforms.webpackConfiguration) {
webpackConfig = await transforms.webpackConfiguration(webpackConfig);
}
@ -150,7 +226,6 @@ export function serveWebpackBrowser(
return {
browserOptions,
webpackConfig,
webpackDevServerConfig,
projectRoot,
locale:
browserOptions.i18nLocale || (i18n.shouldInline ? [...i18n.inlineLocales][0] : undefined),
@ -158,40 +233,7 @@ export function serveWebpackBrowser(
}
return from(setup()).pipe(
switchMap(({ browserOptions, webpackConfig, webpackDevServerConfig, projectRoot, locale }) => {
// Resolve public host and client address.
let clientAddress = url.parse(`${options.ssl ? 'https' : 'http'}://0.0.0.0:0`);
if (options.publicHost) {
let publicHost = options.publicHost;
if (!/^\w+:\/\//.test(publicHost)) {
publicHost = `${options.ssl ? 'https' : 'http'}://${publicHost}`;
}
clientAddress = url.parse(publicHost);
options.publicHost = clientAddress.host;
}
// Add live reload config.
if (options.liveReload) {
_addLiveReload(root, options, browserOptions, webpackConfig, clientAddress, context.logger);
} else if (options.hmr) {
context.logger.warn('Live reload is disabled. HMR option ignored.');
}
webpackConfig.plugins = [...(webpackConfig.plugins || [])];
if (!options.watch) {
// There's no option to turn off file watching in webpack-dev-server, but
// we can override the file watcher instead.
webpackConfig.plugins.push({
// tslint:disable-next-line:no-any
apply: (compiler: any) => {
compiler.hooks.afterEnvironment.tap('angular-cli', () => {
compiler.watchFileSystem = { watch: () => {} };
});
},
});
}
switchMap(({ browserOptions, webpackConfig, projectRoot, locale }) => {
const normalizedOptimization = normalizeOptimization(browserOptions.optimization);
if (browserOptions.index) {
@ -205,10 +247,11 @@ export function serveWebpackBrowser(
? generateEntryPoints({ scripts: [], styles })
: [];
webpackConfig.plugins = [...(webpackConfig.plugins || [])];
webpackConfig.plugins.push(
new IndexHtmlWebpackPlugin({
input: path.resolve(root, getIndexInputFile(browserOptions)),
output: getIndexOutputFile(browserOptions),
input: path.resolve(root, getIndexInputFile(browserOptions.index)),
output: getIndexOutputFile(browserOptions.index),
baseHref,
moduleEntrypoints,
entrypoints,
@ -243,7 +286,7 @@ export function serveWebpackBrowser(
{
logging: transforms.logging || createWebpackLoggingCallback(!!options.verbose, context.logger),
webpackFactory: require('webpack') as typeof webpack,
webpackDevServerFactory: require('webpack-dev-server') as typeof WebpackDevServer,
webpackDevServerFactory: require('webpack-dev-server') as typeof webpackDevServer,
},
).pipe(
map(buildEvent => {
@ -251,7 +294,7 @@ export function serveWebpackBrowser(
const serverAddress = url.format({
protocol: options.ssl ? 'https' : 'http',
hostname: options.host === '0.0.0.0' ? 'localhost' : options.host,
pathname: webpackDevServerConfig.publicPath,
pathname: webpackConfig.devServer?.publicPath,
port: buildEvent.port,
});
@ -364,316 +407,4 @@ async function setupLocalize(
});
}
/**
* Create a webpack configuration for the dev server.
* @param workspaceRoot The root of the workspace. This comes from the context.
* @param serverOptions DevServer options, based on the dev server input schema.
* @param browserOptions Browser builder options. See the browser builder from this package.
* @param logger A generic logger to use for showing warnings.
* @returns A webpack dev-server configuration.
*/
export function buildServerConfig(
workspaceRoot: string,
serverOptions: DevServerBuilderOptions,
browserOptions: BrowserBuilderSchema,
logger: logging.LoggerApi,
): WebpackDevServer.Configuration {
// Check that the host is either localhost or prints out a message.
if (
serverOptions.host
&& !/^127\.\d+\.\d+\.\d+/g.test(serverOptions.host)
&& serverOptions.host !== 'localhost'
) {
logger.warn(tags.stripIndent`
Warning: This is a simple server for use in testing or debugging Angular applications
locally. It hasn't been reviewed for security issues.
Binding this server to an open connection can result in compromising your application or
computer. Using a different host than the one passed to the "--host" flag might result in
websocket connection issues. You might need to use "--disableHostCheck" if that's the
case.
`);
}
if (serverOptions.disableHostCheck) {
logger.warn(tags.oneLine`
Warning: Running a server with --disable-host-check is a security risk.
See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
for more information.
`);
}
const servePath = buildServePath(serverOptions, browserOptions, logger);
const { styles, scripts } = normalizeOptimization(browserOptions.optimization);
const config: WebpackDevServer.Configuration&{logLevel: string} = {
host: serverOptions.host,
port: serverOptions.port,
headers: {
'Access-Control-Allow-Origin': '*',
...serverOptions.headers,
},
historyApiFallback: !!browserOptions.index && {
index: `${servePath}/${getIndexOutputFile(browserOptions)}`,
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
rewrites: [
{
from: new RegExp(`^(?!${servePath})/.*`),
to: context => url.format(context.parsedUrl),
},
],
},
stats: false,
compress: styles || scripts,
watchOptions: {
// Using just `--poll` will result in a value of 0 which is very likely not the intention
// A value of 0 is falsy and will disable polling rather then enable
// 500 ms is a sensible default in this case
poll: serverOptions.poll === 0 ? 500 : serverOptions.poll,
ignored: serverOptions.poll === undefined ? undefined : /[\\\/]node_modules[\\\/]/,
},
https: serverOptions.ssl,
overlay: {
errors: !(styles || scripts),
warnings: false,
},
// inline is always false, because we add live reloading scripts in _addLiveReload when needed
inline: false,
public: serverOptions.publicHost,
allowedHosts: serverOptions.allowedHosts,
disableHostCheck: serverOptions.disableHostCheck,
publicPath: servePath,
hot: serverOptions.hmr,
contentBase: false,
logLevel: 'silent',
};
if (serverOptions.ssl) {
_addSslConfig(workspaceRoot, serverOptions, config);
}
if (serverOptions.proxyConfig) {
_addProxyConfig(workspaceRoot, serverOptions, config);
}
return config;
}
/**
* Resolve and build a URL _path_ that will be the root of the server. This resolved base href and
* deploy URL from the browser options and returns a path from the root.
* @param serverOptions The server options that were passed to the server builder.
* @param browserOptions The browser options that were passed to the browser builder.
* @param logger A generic logger to use for showing warnings.
*/
export function buildServePath(
serverOptions: DevServerBuilderOptions,
browserOptions: BrowserBuilderSchema,
logger: logging.LoggerApi,
): string {
let servePath = serverOptions.servePath;
if (!servePath && servePath !== '') {
const defaultPath = _findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl);
if (defaultPath == null) {
logger.warn(tags.oneLine`
Warning: --deploy-url and/or --base-href contain unsupported values for ng serve. Default
serve path of '/' used. Use --serve-path to override.
`);
}
servePath = defaultPath || '';
}
if (servePath.endsWith('/')) {
servePath = servePath.substr(0, servePath.length - 1);
}
if (!servePath.startsWith('/')) {
servePath = `/${servePath}`;
}
return servePath;
}
/**
* Private method to enhance a webpack config with live reload configuration.
* @private
*/
function _addLiveReload(
root: string,
options: DevServerBuilderOptions,
browserOptions: BrowserBuilderSchema,
webpackConfig: webpack.Configuration,
clientAddress: url.UrlWithStringQuery,
logger: logging.LoggerApi,
) {
if (webpackConfig.plugins === undefined) {
webpackConfig.plugins = [];
}
// Workaround node shim hoisting issues with live reload client
// Only needed in dev server mode to support live reload capabilities in all package managers
// Not needed in Webpack 5 - node-libs-browser will not be present in webpack 5
let nodeLibsBrowserPath;
try {
const webpackPath = path.dirname(require.resolve('webpack/package.json'));
nodeLibsBrowserPath = require.resolve('node-libs-browser', { paths: [webpackPath] });
} catch {}
if (nodeLibsBrowserPath) {
const nodeLibsBrowser = require(nodeLibsBrowserPath);
webpackConfig.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/^events|url|querystring$/,
(resource: { issuer?: string; request: string }) => {
if (!resource.issuer) {
return;
}
if (/[\/\\]hot[\/\\]emitter\.js$/.test(resource.issuer)) {
if (resource.request === 'events') {
resource.request = nodeLibsBrowser.events;
}
} else if (
/[\/\\]webpack-dev-server[\/\\]client[\/\\]utils[\/\\]createSocketUrl\.js$/.test(
resource.issuer,
)
) {
switch (resource.request) {
case 'url':
resource.request = nodeLibsBrowser.url;
break;
case 'querystring':
resource.request = nodeLibsBrowser.querystring;
break;
}
}
},
),
);
}
// This allows for live reload of page when changes are made to repo.
// https://webpack.js.org/configuration/dev-server/#devserver-inline
let webpackDevServerPath;
try {
webpackDevServerPath = require.resolve('webpack-dev-server/client');
} catch {
throw new Error('The "webpack-dev-server" package could not be found.');
}
// If a custom path is provided the webpack dev server client drops the sockjs-node segment.
// This adds it back so that behavior is consistent when using a custom URL path
let sockjsPath = '';
if (clientAddress.pathname) {
clientAddress.pathname = path.posix.join(clientAddress.pathname, 'sockjs-node');
sockjsPath = '&sockPath=' + clientAddress.pathname;
}
const entryPoints = [`${webpackDevServerPath}?${url.format(clientAddress)}${sockjsPath}`];
if (options.hmr) {
logger.warn(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.`);
entryPoints.push(
'webpack/hot/dev-server',
);
}
if (typeof webpackConfig.entry !== 'object' || Array.isArray(webpackConfig.entry)) {
webpackConfig.entry = {};
}
if (!Array.isArray(webpackConfig.entry.main)) {
webpackConfig.entry.main = [];
}
webpackConfig.entry.main.unshift(...entryPoints);
}
/**
* Private method to enhance a webpack config with SSL configuration.
* @private
*/
function _addSslConfig(
root: string,
options: DevServerBuilderOptions,
config: WebpackDevServer.Configuration,
) {
let sslKey: string | undefined = undefined;
let sslCert: string | undefined = undefined;
if (options.sslKey) {
const keyPath = path.resolve(root, options.sslKey);
if (existsSync(keyPath)) {
sslKey = readFileSync(keyPath, 'utf-8');
}
}
if (options.sslCert) {
const certPath = path.resolve(root, options.sslCert);
if (existsSync(certPath)) {
sslCert = readFileSync(certPath, 'utf-8');
}
}
config.https = true;
if (sslKey != null && sslCert != null) {
config.https = {
key: sslKey,
cert: sslCert,
};
}
}
/**
* Private method to enhance a webpack config with Proxy configuration.
* @private
*/
function _addProxyConfig(
root: string,
options: DevServerBuilderOptions,
config: WebpackDevServer.Configuration,
) {
let proxyConfig = {};
const proxyPath = path.resolve(root, options.proxyConfig as string);
if (existsSync(proxyPath)) {
proxyConfig = require(proxyPath);
} else {
const message = 'Proxy config file ' + proxyPath + ' does not exist.';
throw new Error(message);
}
config.proxy = proxyConfig;
}
/**
* Find the default server path. We don't want to expose baseHref and deployUrl as arguments, only
* the browser options where needed. This method should stay private (people who want to resolve
* baseHref and deployUrl should use the buildServePath exported function.
* @private
*/
function _findDefaultServePath(baseHref?: string, deployUrl?: string): string | null {
if (!baseHref && !deployUrl) {
return '';
}
if (/^(\w+:)?\/\//.test(baseHref || '') || /^(\w+:)?\/\//.test(deployUrl || '')) {
// If baseHref or deployUrl is absolute, unsupported by ng serve
return null;
}
// normalize baseHref
// for ng serve the starting base is always `/` so a relative
// and root relative value are identical
const baseHrefParts = (baseHref || '').split('/').filter(part => part !== '');
if (baseHref && !baseHref.endsWith('/')) {
baseHrefParts.pop();
}
const normalizedBaseHref = baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`;
if (deployUrl && deployUrl[0] === '/') {
if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) {
// If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve
return null;
}
return deployUrl;
}
// Join together baseHref and deployUrl
return `${normalizedBaseHref}${deployUrl || ''}`;
}
export default createBuilder<DevServerBuilderOptions, DevServerBuilderOutput>(serveWebpackBrowser);

View File

@ -102,8 +102,8 @@ describe('Dev Server Builder ssl', () => {
const overrides = {
ssl: true,
sslKey: '../ssl/server.key',
sslCert: '../ssl/server.crt',
sslKey: 'ssl/server.key',
sslCert: 'ssl/server.crt',
};
const run = await architect.scheduleTarget(target, overrides);

View File

@ -14,10 +14,12 @@ import {
CrossOrigin,
ExtraEntryPoint,
I18NMissingTranslation,
IndexUnion,
Localize,
OptimizationClass,
SourceMapClass,
} from '../browser/schema';
import { Schema as DevServerSchema } from '../dev-server/schema';
import { NormalizedFileReplacement } from './normalize-file-replacements';
export interface BuildOptions {
@ -48,6 +50,7 @@ export interface BuildOptions {
watch?: boolean;
outputHashing?: string;
poll?: number;
index?: IndexUnion;
deleteOutputPath?: boolean;
preserveSymlinks?: boolean;
extractLicenses?: boolean;
@ -61,7 +64,6 @@ export interface BuildOptions {
statsJson: boolean;
forkTypeChecker: boolean;
hmr?: boolean;
main: string;
polyfills?: string;
budgets: Budget[];
@ -87,6 +89,8 @@ export interface WebpackTestOptions extends BuildOptions {
codeCoverageExclude?: string[];
}
export interface WebpackDevServerOptions extends BuildOptions, Omit<DevServerSchema, 'optimization' | 'sourceMap' | 'browserTarget'> { }
export interface WebpackConfigOptions<T = BuildOptions> {
root: string;
logger: logging.Logger;

View File

@ -236,18 +236,18 @@ export async function generateBrowserWebpackConfigFromContext(
};
}
export function getIndexOutputFile(options: BrowserBuilderSchema): string {
if (typeof options.index === 'string') {
return path.basename(options.index);
export function getIndexOutputFile(index: BrowserBuilderSchema['index']): string {
if (typeof index === 'string') {
return path.basename(index);
} else {
return options.index.output || 'index.html';
return index.output || 'index.html';
}
}
export function getIndexInputFile(options: BrowserBuilderSchema): string {
if (typeof options.index === 'string') {
return options.index;
export function getIndexInputFile(index: BrowserBuilderSchema['index']): string {
if (typeof index === 'string') {
return index;
} else {
return options.index.input;
return index.input;
}
}

View File

@ -5,12 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { resolve } from 'path';
import * as webpack from 'webpack';
import { WebpackConfigOptions } from '../../utils/build-options';
import { withWebpackFourOrFive } from '../../utils/webpack-version';
import { CommonJsUsageWarnPlugin } from '../plugins';
import { HmrLoader } from '../plugins/hmr/hmr-loader';
import { getSourceMapDevTool } from '../utils/helpers';
export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configuration {
@ -22,7 +20,6 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati
vendorChunk,
commonChunk,
allowedCommonJsDependencies,
hmr,
} = buildOptions;
const extraPlugins = [];
@ -70,24 +67,11 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati
crossOriginLoading = crossOrigin;
}
const extraRules: webpack.RuleSetRule[] = [];
if (hmr) {
extraRules.push({
loader: HmrLoader,
include: [buildOptions.main].map(p => resolve(wco.root, p)),
});
extraPlugins.push(new webpack.HotModuleReplacementPlugin());
}
return {
devtool: false,
resolve: {
mainFields: ['es2015', 'browser', 'module', 'main'],
},
module: {
rules: extraRules,
},
...withWebpackFourOrFive({}, { target: ['web', 'es5'] }),
output: {
crossOriginLoading,

View File

@ -46,7 +46,7 @@ import {
ScriptsWebpackPlugin,
WebpackRollupLoader,
} from '../plugins';
import { getEsVersionForFileName, getOutputHashFormat, normalizeExtraEntryPoints } from '../utils/helpers';
import { getEsVersionForFileName, getOutputHashFormat, getWatchOptions, normalizeExtraEntryPoints } from '../utils/helpers';
const TerserPlugin = require('terser-webpack-plugin');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
@ -487,13 +487,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
filename: `[name]${targetInFileName}${hashFormat.chunk}.js`,
},
watch: buildOptions.watch,
watchOptions: {
poll: buildOptions.poll,
ignored:
buildOptions.poll === undefined
? undefined
: withWebpackFourOrFive(/[\\\/]node_modules[\\\/]/, 'node_modules/**'),
},
watchOptions: getWatchOptions(buildOptions.poll),
performance: {
hints: false,
},

View File

@ -0,0 +1,241 @@
/**
* @license
* Copyright Google Inc. 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.io/license
*/
import { logging, tags } from '@angular-devkit/core';
import { existsSync, readFileSync } from 'fs';
import { posix, resolve } from 'path';
import * as url from 'url';
import * as webpack from 'webpack';
import { Configuration } from 'webpack-dev-server';
import { normalizeOptimization } from '../../utils';
import { WebpackConfigOptions, WebpackDevServerOptions } from '../../utils/build-options';
import { getIndexOutputFile } from '../../utils/webpack-browser-config';
import { HmrLoader } from '../plugins/hmr/hmr-loader';
import { getWatchOptions } from '../utils/helpers';
export function getDevServerConfig(
wco: WebpackConfigOptions<WebpackDevServerOptions>,
): webpack.Configuration {
const {
buildOptions: {
optimization,
host,
port,
index,
headers,
poll,
ssl,
hmr,
main,
disableHostCheck,
liveReload,
allowedHosts,
watch,
proxyConfig,
},
logger,
root,
} = wco;
const servePath = buildServePath(wco.buildOptions, logger);
const { styles: stylesOptimization, scripts: scriptsOptimization } = normalizeOptimization(optimization);
const extraPlugins = [];
// Resolve public host and client address.
let sockPath: string | undefined;
let publicHost = wco.buildOptions.publicHost;
if (publicHost) {
if (!/^\w+:\/\//.test(publicHost)) {
publicHost = `${ssl ? 'https' : 'http'}://${publicHost}`;
}
const parsedHost = url.parse(publicHost);
publicHost = parsedHost.host;
if (parsedHost.pathname) {
sockPath = posix.join(parsedHost.pathname, 'sockjs-node');
}
}
if (!watch) {
// There's no option to turn off file watching in webpack-dev-server, but
// we can override the file watcher instead.
extraPlugins.push({
// tslint:disable-next-line:no-any
apply: (compiler: any) => {
compiler.hooks.afterEnvironment.tap('angular-cli', () => {
compiler.watchFileSystem = { watch: () => { } };
});
},
});
}
const extraRules: webpack.RuleSetRule[] = [];
if (hmr) {
extraRules.push({
loader: HmrLoader,
include: [main].map(p => resolve(wco.root, p)),
});
}
return {
plugins: extraPlugins,
module: {
rules: extraRules,
},
devServer: {
host,
port,
headers: {
'Access-Control-Allow-Origin': '*',
...headers,
},
historyApiFallback: !!index && {
index: `${servePath}/${getIndexOutputFile(index)}`,
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
rewrites: [
{
from: new RegExp(`^(?!${servePath})/.*`),
to: context => url.format(context.parsedUrl),
},
],
},
sockPath,
stats: false,
compress: stylesOptimization || scriptsOptimization,
watchOptions: getWatchOptions(poll),
https: getSslConfig(root, wco.buildOptions),
overlay: {
errors: !(stylesOptimization || scriptsOptimization),
warnings: false,
},
public: publicHost,
allowedHosts,
disableHostCheck,
inline: false,
publicPath: servePath,
liveReload,
hotOnly: hmr && !liveReload,
hot: hmr,
proxy: addProxyConfig(root, proxyConfig),
contentBase: false,
logLevel: 'silent',
} as Configuration & { logLevel: Configuration['clientLogLevel'] },
};
}
/**
* Resolve and build a URL _path_ that will be the root of the server. This resolved base href and
* deploy URL from the browser options and returns a path from the root.
*/
export function buildServePath(
options: WebpackDevServerOptions,
logger: logging.LoggerApi,
): string {
let servePath = options.servePath;
if (servePath === undefined) {
const defaultPath = findDefaultServePath(options.baseHref, options.deployUrl);
if (defaultPath == null) {
logger.warn(tags.oneLine`
Warning: --deploy-url and/or --base-href contain unsupported values for ng serve. Default
serve path of '/' used. Use --serve-path to override.
`);
}
servePath = defaultPath || '';
}
if (servePath.endsWith('/')) {
servePath = servePath.substr(0, servePath.length - 1);
}
if (!servePath.startsWith('/')) {
servePath = `/${servePath}`;
}
return servePath;
}
/**
* Private method to enhance a webpack config with SSL configuration.
* @private
*/
function getSslConfig(
root: string,
options: WebpackDevServerOptions,
) {
const { ssl, sslCert, sslKey } = options;
if (ssl && sslCert && sslKey) {
return {
key: readFileSync(resolve(root, sslKey), 'utf-8'),
cert: readFileSync(resolve(root, sslCert), 'utf-8'),
};
}
return ssl;
}
/**
* Private method to enhance a webpack config with Proxy configuration.
* @private
*/
function addProxyConfig(
root: string,
proxyConfig: string | undefined,
) {
if (!proxyConfig) {
return undefined;
}
const proxyPath = resolve(root, proxyConfig);
if (existsSync(proxyPath)) {
return require(proxyPath);
}
throw new Error('Proxy config file ' + proxyPath + ' does not exist.');
}
/**
* Find the default server path. We don't want to expose baseHref and deployUrl as arguments, only
* the browser options where needed. This method should stay private (people who want to resolve
* baseHref and deployUrl should use the buildServePath exported function.
* @private
*/
function findDefaultServePath(baseHref?: string, deployUrl?: string): string | null {
if (!baseHref && !deployUrl) {
return '';
}
if (/^(\w+:)?\/\//.test(baseHref || '') || /^(\w+:)?\/\//.test(deployUrl || '')) {
// If baseHref or deployUrl is absolute, unsupported by ng serve
return null;
}
// normalize baseHref
// for ng serve the starting base is always `/` so a relative
// and root relative value are identical
const baseHrefParts = (baseHref || '').split('/').filter(part => part !== '');
if (baseHref && !baseHref.endsWith('/')) {
baseHrefParts.pop();
}
const normalizedBaseHref = baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`;
if (deployUrl && deployUrl[0] === '/') {
if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) {
// If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve
return null;
}
return deployUrl;
}
// Join together baseHref and deployUrl
return `${normalizedBaseHref}${deployUrl || ''}`;
}

View File

@ -39,6 +39,7 @@ export class CommonJsUsageWarnPlugin {
// https://github.com/angular/angular-cli/blob/1e258317b1f6ec1e957ee3559cc3b28ba602f3ba/packages/angular_devkit/build_angular/src/dev-server/index.ts#L605-L638
private allowedDependencies = new Set<string>([
'webpack/hot/dev-server',
'webpack/hot/only-dev-server',
'@angular-devkit/build-angular',
]);

View File

@ -8,8 +8,9 @@
import { basename, normalize } from '@angular-devkit/core';
import { ScriptTarget } from 'typescript';
import { SourceMapDevToolPlugin } from 'webpack';
import { Options, SourceMapDevToolPlugin } from 'webpack';
import { ExtraEntryPoint, ExtraEntryPointClass } from '../../browser/schema';
import { withWebpackFourOrFive } from '../../utils/webpack-version';
export interface HashFormat {
chunk: string;
@ -118,3 +119,10 @@ export function getEsVersionForFileName(
export function isPolyfillsEntry(name: string): boolean {
return name === 'polyfills' || name === 'polyfills-es5';
}
export function getWatchOptions(poll: number | undefined): Options.WatchOptions {
return {
poll,
ignored: poll === undefined ? undefined : withWebpackFourOrFive(/[\\\/]node_modules[\\\/]/, 'node_modules/**'),
};
}