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';