From 247b87d40a919c144d4fdcf3e55a563c3e006e34 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 16 Oct 2020 14:31:12 +0200 Subject: [PATCH] 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. --- .../build_angular/src/browser/index.ts | 51 +- .../build_angular/src/dev-server/index.ts | 469 ++++-------------- .../build_angular/src/dev-server/ssl_spec.ts | 4 +- .../build_angular/src/utils/build-options.ts | 6 +- .../src/utils/webpack-browser-config.ts | 16 +- .../src/webpack/configs/browser.ts | 16 - .../src/webpack/configs/common.ts | 10 +- .../src/webpack/configs/dev-server.ts | 241 +++++++++ .../plugins/common-js-usage-warn-plugin.ts | 1 + .../src/webpack/utils/helpers.ts | 10 +- 10 files changed, 387 insertions(+), 437 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/webpack/configs/dev-server.ts diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index f71185d464..7c6711efd3 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -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 = new NodeJsSyncHost(), - extraBuildOptions: Partial = {}, -): Promise { - 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, diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index 1c13473af3..b5462d6ab5 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -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>( + (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(serveWebpackBrowser); diff --git a/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts index ca3ec505d3..9b2a39f3bc 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts @@ -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); diff --git a/packages/angular_devkit/build_angular/src/utils/build-options.ts b/packages/angular_devkit/build_angular/src/utils/build-options.ts index 70954cb64a..386c3a8943 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-options.ts @@ -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 { } + export interface WebpackConfigOptions { root: string; logger: logging.Logger; diff --git a/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts b/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts index abda3c9974..65699db228 100644 --- a/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts +++ b/packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts @@ -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; } } diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts index 0ac41b9882..d0a48c74fb 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/browser.ts @@ -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, diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index 905488765e..a1df69728e 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -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, }, diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/dev-server.ts b/packages/angular_devkit/build_angular/src/webpack/configs/dev-server.ts new file mode 100644 index 0000000000..9f0e1a284c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/webpack/configs/dev-server.ts @@ -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, +): 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 || ''}`; +} diff --git a/packages/angular_devkit/build_angular/src/webpack/plugins/common-js-usage-warn-plugin.ts b/packages/angular_devkit/build_angular/src/webpack/plugins/common-js-usage-warn-plugin.ts index daa3ba386c..43fc57d596 100644 --- a/packages/angular_devkit/build_angular/src/webpack/plugins/common-js-usage-warn-plugin.ts +++ b/packages/angular_devkit/build_angular/src/webpack/plugins/common-js-usage-warn-plugin.ts @@ -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([ 'webpack/hot/dev-server', + 'webpack/hot/only-dev-server', '@angular-devkit/build-angular', ]); diff --git a/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts b/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts index 73e9742d8a..f6ad3988cc 100644 --- a/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts +++ b/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts @@ -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/**'), + }; +}