From 1bfb7c1b85f044c59e1cca571183ee487e7843d2 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 8 Mar 2023 15:06:36 -0500 Subject: [PATCH] refactor(@angular-devkit/build-angular): extract option processing for dev server builder To provide support for additional development server integration, the `dev-server` builder's option processing has been reorganized into separate files. The main builder bootstrapping logic has also been separated into another file. This additionally helps reduce the overall size of the main Webpack-based development server file. --- .../angular_devkit/build_angular/index.md | 4 +- .../src/builders/dev-server/builder.ts | 98 +++++++++++++++++++ .../src/builders/dev-server/index.ts | 10 +- .../src/builders/dev-server/options.ts | 84 ++++++++++++++++ .../src/builders/dev-server/webpack-server.ts | 90 ++++------------- .../angular_devkit/build_angular/src/index.ts | 2 +- 6 files changed, 213 insertions(+), 75 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts create mode 100644 packages/angular_devkit/build_angular/src/builders/dev-server/options.ts diff --git a/goldens/public-api/angular_devkit/build_angular/index.md b/goldens/public-api/angular_devkit/build_angular/index.md index b409e36172a4..a9830bd16214 100644 --- a/goldens/public-api/angular_devkit/build_angular/index.md +++ b/goldens/public-api/angular_devkit/build_angular/index.md @@ -143,8 +143,8 @@ export function executeBrowserBuilder(options: BrowserBuilderOptions, context: B }): Observable; // @public -export function executeDevServerBuilder(options: DevServerBuilderOptions_2, context: BuilderContext, transforms?: { - webpackConfiguration?: ExecutionTransformer; +export function executeDevServerBuilder(options: DevServerBuilderOptions, context: BuilderContext, transforms?: { + webpackConfiguration?: ExecutionTransformer; logging?: WebpackLoggingCallback; indexHtml?: IndexHtmlTransform; }): Observable; diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts new file mode 100644 index 000000000000..b42c74e3955c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import { EMPTY, Observable, defer, switchMap } from 'rxjs'; +import { ExecutionTransformer } from '../../transforms'; +import { checkPort } from '../../utils/check-port'; +import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator'; +import { purgeStaleBuildCache } from '../../utils/purge-cache'; +import { normalizeOptions } from './options'; +import { Schema as DevServerBuilderOptions } from './schema'; +import { DevServerBuilderOutput, serveWebpackBrowser } from './webpack-server'; + +/** + * A Builder that executes a development server based on the provided browser target option. + * @param options Dev Server options. + * @param context The build context. + * @param transforms A map of transforms that can be used to hook into some logic (such as + * transforming webpack configuration before passing it to webpack). + * + * @experimental Direct usage of this function is considered experimental. + */ +export function execute( + options: DevServerBuilderOptions, + context: BuilderContext, + transforms: { + webpackConfiguration?: ExecutionTransformer; + logging?: import('@angular-devkit/build-webpack').WebpackLoggingCallback; + indexHtml?: IndexHtmlTransform; + } = {}, +): Observable { + // Determine project name from builder context target + const projectName = context.target?.project; + if (!projectName) { + context.logger.error(`The 'dev-server' builder requires a target to be specified.`); + + return EMPTY; + } + + return defer(() => initialize(options, projectName, context)).pipe( + switchMap(({ builderName, normalizedOptions }) => { + // Issue a warning that the dev-server does not currently support the experimental esbuild- + // based builder and will use Webpack. + if (builderName === '@angular-devkit/build-angular:browser-esbuild') { + context.logger.warn( + 'WARNING: The experimental esbuild-based builder is not currently supported ' + + 'by the dev-server. The stable Webpack-based builder will be used instead.', + ); + } + + return serveWebpackBrowser(normalizedOptions, builderName, context, transforms); + }), + ); +} + +async function initialize( + initialOptions: DevServerBuilderOptions, + projectName: string, + context: BuilderContext, +) { + // Purge old build disk cache. + await purgeStaleBuildCache(context); + + const normalizedOptions = await normalizeOptions(context, projectName, initialOptions); + const builderName = await context.getBuilderNameForTarget(normalizedOptions.browserTarget); + + if ( + !normalizedOptions.disableHostCheck && + !/^127\.\d+\.\d+\.\d+/g.test(normalizedOptions.host) && + normalizedOptions.host !== 'localhost' + ) { + context.logger.warn(` +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 "--disable-host-check" if that's the +case. + `); + } + + if (normalizedOptions.disableHostCheck) { + context.logger.warn( + '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.', + ); + } + + normalizedOptions.port = await checkPort(normalizedOptions.port, normalizedOptions.host); + + return { builderName, normalizedOptions }; +} diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts index 0ae945de23a9..e8fb4bfdc166 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/index.ts @@ -7,8 +7,12 @@ */ import { createBuilder } from '@angular-devkit/architect'; +import { execute } from './builder'; import { Schema as DevServerBuilderOptions } from './schema'; -import { DevServerBuilderOutput, serveWebpackBrowser } from './webpack-server'; +import { DevServerBuilderOutput } from './webpack-server'; -export { DevServerBuilderOptions, DevServerBuilderOutput, serveWebpackBrowser }; -export default createBuilder(serveWebpackBrowser); +export { DevServerBuilderOptions, DevServerBuilderOutput, execute as executeDevServerBuilder }; +export default createBuilder(execute); + +// Temporary export to support specs +export { execute as serveWebpackBrowser }; diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts new file mode 100644 index 000000000000..4d06e3be1cc9 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/options.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; +import path from 'node:path'; +import { normalizeCacheOptions } from '../../utils/normalize-cache'; +import { Schema as DevServerOptions } from './schema'; + +export type NormalizedDevServerOptions = Awaited>; + +/** + * Normalize the user provided options by creating full paths for all path based options + * and converting multi-form options into a single form that can be directly used + * by the build process. + * + * @param context The context for current builder execution. + * @param projectName The name of the project for the current execution. + * @param options An object containing the options to use for the build. + * @returns An object containing normalized options required to perform the build. + */ +export async function normalizeOptions( + context: BuilderContext, + projectName: string, + options: DevServerOptions, +) { + const workspaceRoot = context.workspaceRoot; + const projectMetadata = await context.getProjectMetadata(projectName); + const projectRoot = path.join(workspaceRoot, (projectMetadata.root as string | undefined) ?? ''); + + const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot); + + const browserTarget = targetFromTargetString(options.browserTarget); + + // Initial options to keep + const { + host, + port, + poll, + open, + verbose, + watch, + allowedHosts, + disableHostCheck, + liveReload, + hmr, + headers, + proxyConfig, + servePath, + publicHost, + ssl, + sslCert, + sslKey, + } = options; + + // Return all the normalized options + return { + browserTarget, + host: host ?? 'localhost', + port: port ?? 4200, + poll, + open, + verbose, + watch, + liveReload, + hmr, + headers, + workspaceRoot, + projectRoot, + cacheOptions, + allowedHosts, + disableHostCheck, + proxyConfig, + servePath, + publicHost, + ssl, + sslCert, + sslKey, + }; +} diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts index 087c2a6406d1..781ac9350a49 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; +import { BuilderContext } from '@angular-devkit/architect'; import { DevServerBuildOutput, WebpackLoggingCallback, @@ -20,14 +20,12 @@ import webpack from 'webpack'; import webpackDevServer from 'webpack-dev-server'; import { ExecutionTransformer } from '../../transforms'; import { normalizeOptimization } from '../../utils'; -import { checkPort } from '../../utils/check-port'; import { colors } from '../../utils/color'; import { I18nOptions, loadTranslations } from '../../utils/i18n-options'; import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator'; import { createTranslationLoader } from '../../utils/load-translations'; -import { NormalizedCachedOptions, normalizeCacheOptions } from '../../utils/normalize-cache'; +import { NormalizedCachedOptions } from '../../utils/normalize-cache'; import { generateEntryPoints } from '../../utils/package-chunk-sort'; -import { purgeStaleBuildCache } from '../../utils/purge-cache'; import { assertCompatibleAngularVersion } from '../../utils/version'; import { generateI18nBrowserWebpackConfigFromContext, @@ -44,9 +42,7 @@ import { generateBuildEventStats, } from '../../webpack/utils/stats'; import { Schema as BrowserBuilderSchema, OutputHashing } from '../browser/schema'; -import { Schema } from './schema'; - -export type DevServerBuilderOptions = Schema; +import { NormalizedDevServerOptions } from './options'; /** * @experimental Direct usage of this type is considered experimental. @@ -59,15 +55,15 @@ export type DevServerBuilderOutput = DevServerBuildOutput & { /** * Reusable implementation of the Angular Webpack development server builder. * @param options Dev Server options. + * @param builderName The name of the builder used to build the application. * @param context The build context. * @param transforms A map of transforms that can be used to hook into some logic (such as * transforming webpack configuration before passing it to webpack). - * - * @experimental Direct usage of this function is considered experimental. */ // eslint-disable-next-line max-lines-per-function export function serveWebpackBrowser( - options: DevServerBuilderOptions, + options: NormalizedDevServerOptions, + builderName: string, context: BuilderContext, transforms: { webpackConfiguration?: ExecutionTransformer; @@ -79,55 +75,19 @@ export function serveWebpackBrowser( const { logger, workspaceRoot } = context; assertCompatibleAngularVersion(workspaceRoot); - const browserTarget = targetFromTargetString(options.browserTarget); - async function setup(): Promise<{ browserOptions: BrowserBuilderSchema; webpackConfig: webpack.Configuration; - projectRoot: string; }> { - const projectName = context.target?.project; - if (!projectName) { - throw new Error('The builder requires a target.'); - } - - // Purge old build disk cache. - await purgeStaleBuildCache(context); - - options.port = await checkPort(options.port ?? 4200, options.host || 'localhost'); - 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.`); } - if ( - !options.disableHostCheck && - options.host && - !/^127\.\d+\.\d+\.\d+/g.test(options.host) && - options.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 "--disable-host-check" if that's the - case. - `); - } - - if (options.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. - `); - } // Get the browser configuration from the target name. - const rawBrowserOptions = (await context.getTargetOptions(browserTarget)) as json.JsonObject & - BrowserBuilderSchema; + const rawBrowserOptions = (await context.getTargetOptions( + options.browserTarget, + )) as json.JsonObject & BrowserBuilderSchema; if (rawBrowserOptions.outputHashing && rawBrowserOptions.outputHashing !== OutputHashing.None) { // Disable output hashing for dev build as this can cause memory leaks @@ -136,20 +96,6 @@ export function serveWebpackBrowser( logger.warn(`Warning: 'outputHashing' option is disabled when using the dev-server.`); } - const metadata = await context.getProjectMetadata(projectName); - const cacheOptions = normalizeCacheOptions(metadata, context.workspaceRoot); - - const browserName = await context.getBuilderNameForTarget(browserTarget); - - // Issue a warning that the dev-server does not currently support the experimental esbuild- - // based builder and will use Webpack. - if (browserName === '@angular-devkit/build-angular:browser-esbuild') { - logger.warn( - 'WARNING: The experimental esbuild-based builder is not currently supported ' + - 'by the dev-server. The stable Webpack-based builder will be used instead.', - ); - } - const browserOptions = (await context.validateOptions( { ...rawBrowserOptions, @@ -158,7 +104,7 @@ export function serveWebpackBrowser( // In dev server we should not have budgets because of extra libs such as socks-js budgets: undefined, } as json.JsonObject & BrowserBuilderSchema, - browserName, + builderName, )) as json.JsonObject & BrowserBuilderSchema; const { styles, scripts } = normalizeOptimization(browserOptions.optimization); @@ -173,7 +119,7 @@ export function serveWebpackBrowser( `); } - const { config, projectRoot, i18n } = await generateI18nBrowserWebpackConfigFromContext( + const { config, i18n } = await generateI18nBrowserWebpackConfigFromContext( browserOptions, context, (wco) => [getDevServerConfig(wco), getCommonConfig(wco), getStylesConfig(wco)], @@ -203,7 +149,14 @@ export function serveWebpackBrowser( ); } - await setupLocalize(locale, i18n, browserOptions, webpackConfig, cacheOptions, context); + await setupLocalize( + locale, + i18n, + browserOptions, + webpackConfig, + options.cacheOptions, + context, + ); } if (transforms.webpackConfiguration) { @@ -231,7 +184,7 @@ export function serveWebpackBrowser( entrypoints, deployUrl: browserOptions.deployUrl, sri: browserOptions.subresourceIntegrity, - cache: cacheOptions, + cache: options.cacheOptions, postTransform: transforms.indexHtml, optimization: normalizeOptimization(browserOptions.optimization), crossOrigin: browserOptions.crossOrigin, @@ -245,7 +198,7 @@ export function serveWebpackBrowser( new ServiceWorkerPlugin({ baseHref: browserOptions.baseHref, root: context.workspaceRoot, - projectRoot, + projectRoot: options.projectRoot, ngswConfigPath: browserOptions.ngswConfigPath, }), ); @@ -254,7 +207,6 @@ export function serveWebpackBrowser( return { browserOptions, webpackConfig, - projectRoot, }; } diff --git a/packages/angular_devkit/build_angular/src/index.ts b/packages/angular_devkit/build_angular/src/index.ts index 5e9829236c5f..37aa4d24d4d2 100644 --- a/packages/angular_devkit/build_angular/src/index.ts +++ b/packages/angular_devkit/build_angular/src/index.ts @@ -30,7 +30,7 @@ export { } from './builders/browser'; export { - serveWebpackBrowser as executeDevServerBuilder, + executeDevServerBuilder, DevServerBuilderOptions, DevServerBuilderOutput, } from './builders/dev-server';