mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-22 06:41:45 +08:00
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:
parent
710e12dd7d
commit
247b87d40a
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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 || ''}`;
|
||||
}
|
@ -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',
|
||||
]);
|
||||
|
||||
|
@ -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/**'),
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user