From 4ffe07aa24a0fc9ff48461e9c3664d96e92317cf Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:33:32 -0400 Subject: [PATCH] feat(@angular-devkit/build-angular): move Vite-based dev-server for application builder to new build system package With the `application` builder already within the new `@angular/build` package, the Vite-based `dev-server` builder is now also contained within this package. Only the Vite-based aspects of the `dev-server` have been moved and only the support for the `application` builder. The compatibility builder `browser-esbuild` is not supported with `@angular/build:dev-server`. The existing `dev-server` builder found within `@angular-devkit/build-angular` should continue to be used for both the Webpack-based `browser` builder and the esbuild-based compatibility `browser-esbuild` builder. To maintain backwards compatibility, the existing `@angular-devkit/build-angular:dev-server` builder continues to support builders it has previously. No change to existing applications is required. --- goldens/public-api/angular/build/index.md | 46 +++ .../angular_devkit/build_angular/index.md | 11 +- packages/angular/build/BUILD.bazel | 15 + packages/angular/build/builders.json | 7 +- packages/angular/build/package.json | 3 + .../build/src/builders/dev-server/builder.ts | 117 +++++++ .../build/src/builders/dev-server/index.ts | 18 + .../build/src/builders/dev-server/internal.ts | 20 ++ .../build/src/builders/dev-server/options.ts | 91 +++++ .../build}/src/builders/dev-server/output.ts | 0 .../build/src/builders/dev-server/schema.json | 131 +++++++ .../tests/behavior/build-assets_spec.ts | 125 +++++++ .../tests/behavior/build-base-href_spec.ts | 49 +++ .../tests/behavior/build-budgets_spec.ts | 39 +++ .../build-inline-critical-css_spec.ts | 46 +++ .../build_localize_replaced_watch_spec.ts | 85 +++++ .../behavior/build_translation_watch_spec.ts | 109 ++++++ .../serve-live-reload-proxies_spec.ts | 321 +++++++++++++++++ .../behavior/serve_service-worker_spec.ts | 224 ++++++++++++ .../dev-server/tests/execute-fetch.ts | 43 +++ .../dev-server/tests/jasmine-helpers.ts | 41 +++ .../tests/options/allowed-hosts_spec.ts | 71 ++++ .../tests/options/disable-host-check_spec.ts | 71 ++++ .../tests/options/force-esbuild_spec.ts | 79 +++++ .../dev-server/tests/options/port_spec.ts | 112 ++++++ .../tests/options/prebundle_spec.ts | 99 ++++++ .../tests/options/proxy-config_spec.ts | 323 ++++++++++++++++++ .../tests/options/public-host_spec.ts | 71 ++++ .../tests/options/serve-path_spec.ts | 120 +++++++ .../dev-server/tests/options/watch_spec.ts | 121 +++++++ .../src/builders/dev-server/tests/setup.ts | 104 ++++++ .../src/builders/dev-server/vite-server.ts | 38 +-- packages/angular/build/src/index.ts | 6 + packages/angular/build/src/private.ts | 9 +- .../src/tools/vite/angular-memory-plugin.ts | 2 +- .../src/tools/vite/i18n-locale-plugin.ts | 0 .../build}/src/utils/check-port.ts | 0 packages/angular/build/src/utils/index.ts | 1 + .../build}/src/utils/load-proxy-config.ts | 0 .../src/builders/dev-server/builder.ts | 27 +- .../src/builders/dev-server/index.ts | 2 +- .../src/builders/dev-server/internal.ts | 22 -- .../src/builders/ssr-dev-server/index.ts | 2 +- .../build_angular/src/utils/index.ts | 3 +- yarn.lock | 17 +- 45 files changed, 2762 insertions(+), 79 deletions(-) create mode 100644 packages/angular/build/src/builders/dev-server/builder.ts create mode 100644 packages/angular/build/src/builders/dev-server/index.ts create mode 100644 packages/angular/build/src/builders/dev-server/internal.ts create mode 100644 packages/angular/build/src/builders/dev-server/options.ts rename packages/{angular_devkit/build_angular => angular/build}/src/builders/dev-server/output.ts (100%) create mode 100644 packages/angular/build/src/builders/dev-server/schema.json create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build-base-href_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build-budgets_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build-inline-critical-css_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build_localize_replaced_watch_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/build_translation_watch_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/serve-live-reload-proxies_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/behavior/serve_service-worker_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/jasmine-helpers.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/disable-host-check_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/force-esbuild_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/port_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/prebundle_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/proxy-config_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/public-host_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/serve-path_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/watch_spec.ts create mode 100644 packages/angular/build/src/builders/dev-server/tests/setup.ts rename packages/{angular_devkit/build_angular => angular/build}/src/builders/dev-server/vite-server.ts (96%) rename packages/{angular_devkit/build_angular => angular/build}/src/tools/vite/angular-memory-plugin.ts (99%) rename packages/{angular_devkit/build_angular => angular/build}/src/tools/vite/i18n-locale-plugin.ts (100%) rename packages/{angular_devkit/build_angular => angular/build}/src/utils/check-port.ts (100%) rename packages/{angular_devkit/build_angular => angular/build}/src/utils/load-proxy-config.ts (100%) delete mode 100644 packages/angular_devkit/build_angular/src/builders/dev-server/internal.ts diff --git a/goldens/public-api/angular/build/index.md b/goldens/public-api/angular/build/index.md index 85b23d3d8b..6ca5168f61 100644 --- a/goldens/public-api/angular/build/index.md +++ b/goldens/public-api/angular/build/index.md @@ -8,6 +8,7 @@ import { BuilderContext } from '@angular-devkit/architect'; import { BuilderOutput } from '@angular-devkit/architect'; +import type http from 'node:http'; import { OutputFile } from 'esbuild'; import type { Plugin as Plugin_2 } from 'esbuild'; @@ -107,6 +108,51 @@ export enum BuildOutputFileType { Server = 3 } +// @public +export interface DevServerBuilderOptions { + allowedHosts?: string[]; + // @deprecated + browserTarget?: string; + buildTarget?: string; + disableHostCheck?: boolean; + forceEsbuild?: boolean; + headers?: { + [key: string]: string; + }; + hmr?: boolean; + host?: string; + liveReload?: boolean; + open?: boolean; + poll?: number; + port?: number; + prebundle?: PrebundleUnion; + proxyConfig?: string; + publicHost?: string; + servePath?: string; + ssl?: boolean; + sslCert?: string; + sslKey?: string; + verbose?: boolean; + watch?: boolean; +} + +// @public +export interface DevServerBuilderOutput extends BuilderOutput { + // (undocumented) + address?: string; + // (undocumented) + baseUrl: string; + // (undocumented) + port?: number; +} + +// @public +export function executeDevServerBuilder(options: DevServerBuilderOptions, context: BuilderContext, extensions?: { + buildPlugins?: Plugin_2[]; + middleware?: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[]; + indexHtmlTransformer?: IndexHtmlTransform; +}): AsyncIterable; + // (No @packageDocumentation comment for this package) ``` diff --git a/goldens/public-api/angular_devkit/build_angular/index.md b/goldens/public-api/angular_devkit/build_angular/index.md index ffff9981f8..6083fbeb78 100644 --- a/goldens/public-api/angular_devkit/build_angular/index.md +++ b/goldens/public-api/angular_devkit/build_angular/index.md @@ -12,6 +12,7 @@ import { BuilderContext } from '@angular-devkit/architect'; import { BuilderOutput } from '@angular-devkit/architect'; import type { ConfigOptions } from 'karma'; import { Configuration } from 'webpack'; +import { DevServerBuilderOutput } from '@angular/build'; import type http from 'node:http'; import { IndexHtmlTransform } from '@angular/build/private'; import { json } from '@angular-devkit/core'; @@ -142,15 +143,7 @@ export interface DevServerBuilderOptions { watch?: boolean; } -// @public -export interface DevServerBuilderOutput extends BuilderOutput { - // (undocumented) - address?: string; - // (undocumented) - baseUrl: string; - // (undocumented) - port?: number; -} +export { DevServerBuilderOutput } // @public export function executeBrowserBuilder(options: BrowserBuilderOptions, context: BuilderContext, transforms?: { diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 5ebb308f06..fda717f67f 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -12,6 +12,11 @@ ts_json_schema( src = "src/builders/application/schema.json", ) +ts_json_schema( + name = "dev-server_schema", + src = "src/builders/dev-server/schema.json", +) + ts_library( name = "build", package_name = "@angular/build", @@ -28,6 +33,7 @@ ts_library( ], ) + [ "//packages/angular/build:src/builders/application/schema.ts", + "//packages/angular/build:src/builders/dev-server/schema.ts", ], data = glob( include = [ @@ -57,11 +63,13 @@ ts_library( "@npm//@babel/helper-split-export-declaration", "@npm//@types/babel__core", "@npm//@types/browserslist", + "@npm//@types/inquirer", "@npm//@types/less", "@npm//@types/node", "@npm//@types/picomatch", "@npm//@types/semver", "@npm//@types/watchpack", + "@npm//@vitejs/plugin-basic-ssl", "@npm//ansi-colors", "@npm//autoprefixer", "@npm//browserslist", @@ -69,6 +77,7 @@ ts_library( "@npm//esbuild", "@npm//fast-glob", "@npm//https-proxy-agent", + "@npm//inquirer", "@npm//less", "@npm//magic-string", "@npm//mrmime", @@ -81,6 +90,7 @@ ts_library( "@npm//tslib", "@npm//typescript", "@npm//undici", + "@npm//vite", "@npm//watchpack", ], ) @@ -132,6 +142,11 @@ ts_library( "//packages/angular_devkit/core", "//packages/angular_devkit/core/node", + # dev server only test deps + "@npm//@types/http-proxy", + "@npm//http-proxy", + "@npm//puppeteer", + # Base dependencies for the application in hello-world-app. "@npm//@angular/common", "@npm//@angular/compiler", diff --git a/packages/angular/build/builders.json b/packages/angular/build/builders.json index 6e76ab3bfe..b12954b8cc 100644 --- a/packages/angular/build/builders.json +++ b/packages/angular/build/builders.json @@ -1,9 +1,14 @@ { "builders": { "application": { - "implementation": "./src/builders/application", + "implementation": "./src/builders/application/index", "schema": "./src/builders/application/schema.json", "description": "Build an application." + }, + "dev-server": { + "implementation": "./src/builders/dev-server/index", + "schema": "./src/builders/dev-server/schema.json", + "description": "Execute a development server for an application." } } } diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index f2da77475e..734dcfe794 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -23,11 +23,13 @@ "@babel/core": "7.24.4", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.6", + "@vitejs/plugin-basic-ssl": "1.1.0", "browserslist": "^4.23.0", "critters": "0.0.22", "esbuild": "0.20.2", "fast-glob": "3.3.2", "https-proxy-agent": "7.0.4", + "inquirer": "9.2.19", "less": "4.2.0", "magic-string": "0.30.10", "mrmime": "2.0.0", @@ -38,6 +40,7 @@ "sass": "1.75.0", "semver": "7.6.0", "undici": "6.13.0", + "vite": "5.2.10", "watchpack": "2.4.1" }, "peerDependencies": { diff --git a/packages/angular/build/src/builders/dev-server/builder.ts b/packages/angular/build/src/builders/dev-server/builder.ts new file mode 100644 index 0000000000..353e67b6a4 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/builder.ts @@ -0,0 +1,117 @@ +/** + * @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 type { BuilderContext } from '@angular-devkit/architect'; +import type { Plugin } from 'esbuild'; +import type http from 'node:http'; +import { checkPort } from '../../utils/check-port'; +import { + type IndexHtmlTransform, + buildApplicationInternal, + purgeStaleBuildCache, +} from './internal'; +import { normalizeOptions } from './options'; +import type { DevServerBuilderOutput } from './output'; +import type { Schema as DevServerBuilderOptions } from './schema'; +import { serveWithVite } from './vite-server'; + +/** + * A Builder that executes a development server based on the provided browser target option. + * + * Usage of the `transforms` and/or `extensions` parameters is NOT supported and may cause + * unexpected build output or build failures. + * + * @param options Dev Server options. + * @param context The build context. + * @param extensions An optional object containing an array of build plugins (esbuild-based) + * and/or HTTP request middleware. + * + * @experimental Direct usage of this function is considered experimental. + */ +export async function* execute( + options: DevServerBuilderOptions, + context: BuilderContext, + extensions?: { + buildPlugins?: Plugin[]; + middleware?: (( + req: http.IncomingMessage, + res: http.ServerResponse, + next: (err?: unknown) => void, + ) => void)[]; + indexHtmlTransformer?: IndexHtmlTransform; + }, +): AsyncIterable { + // 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; + } + + const { builderName, normalizedOptions } = await initialize(options, projectName, context); + + // Warn if the initial options provided by the user enable prebundling but caching is disabled + if (options.prebundle && !normalizedOptions.cacheOptions.enabled) { + context.logger.warn( + `Prebundling has been configured but will not be used because caching has been disabled.`, + ); + } + + yield* serveWithVite( + normalizedOptions, + builderName, + (options, context, plugins) => + buildApplicationInternal(options, context, { write: false }, { codePlugins: plugins }), + context, + { indexHtml: extensions?.indexHtmlTransformer }, + extensions, + ); +} + +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.buildTarget); + + 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/build/src/builders/dev-server/index.ts b/packages/angular/build/src/builders/dev-server/index.ts new file mode 100644 index 0000000000..6edd06ef29 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/index.ts @@ -0,0 +1,18 @@ +/** + * @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 { createBuilder } from '@angular-devkit/architect'; +import { execute } from './builder'; +import { DevServerBuilderOutput } from './output'; +import { Schema as DevServerBuilderOptions } from './schema'; + +export { DevServerBuilderOptions, DevServerBuilderOutput, execute as executeDevServerBuilder }; +export default createBuilder(execute); + +// Temporary export to support specs +export { execute as executeDevServer }; diff --git a/packages/angular/build/src/builders/dev-server/internal.ts b/packages/angular/build/src/builders/dev-server/internal.ts new file mode 100644 index 0000000000..97d1206907 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/internal.ts @@ -0,0 +1,20 @@ +/** + * @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 + */ + +export { BuildOutputFile, BuildOutputFileType } from '@angular/build'; +export { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin'; +export { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer'; +export { getFeatureSupport, isZonelessApp } from '../../tools/esbuild/utils'; +export { renderPage } from '../../utils/server-rendering/render-page'; +export { type IndexHtmlTransform } from '../../utils/index-file/index-html-generator'; +export { purgeStaleBuildCache } from '../../utils/purge-cache'; +export { getSupportedBrowsers } from '../../utils/supported-browsers'; +export { transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils'; +export { buildApplicationInternal } from '../../builders/application'; +export { ApplicationBuilderInternalOptions } from '../../builders/application/options'; +export { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result'; diff --git a/packages/angular/build/src/builders/dev-server/options.ts b/packages/angular/build/src/builders/dev-server/options.ts new file mode 100644 index 0000000000..fd3b384e1f --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/options.ts @@ -0,0 +1,91 @@ +/** + * @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); + + // Target specifier defaults to the current project's build target using a development configuration + const buildTargetSpecifier = options.buildTarget ?? options.browserTarget ?? `::development`; + const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build'); + + // Initial options to keep + const { + host, + port, + poll, + open, + verbose, + watch, + allowedHosts, + disableHostCheck, + liveReload, + hmr, + headers, + proxyConfig, + servePath, + publicHost, + ssl, + sslCert, + sslKey, + forceEsbuild, + prebundle, + } = options; + + // Return all the normalized options + return { + buildTarget, + host: host ?? 'localhost', + port: port ?? 4200, + poll, + open, + verbose, + watch, + liveReload, + hmr, + headers, + workspaceRoot, + projectRoot, + cacheOptions, + allowedHosts, + disableHostCheck, + proxyConfig, + servePath, + publicHost, + ssl, + sslCert, + sslKey, + forceEsbuild, + // Prebundling defaults to true but requires caching to function + prebundle: cacheOptions.enabled && (prebundle ?? true), + }; +} diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/output.ts b/packages/angular/build/src/builders/dev-server/output.ts similarity index 100% rename from packages/angular_devkit/build_angular/src/builders/dev-server/output.ts rename to packages/angular/build/src/builders/dev-server/output.ts diff --git a/packages/angular/build/src/builders/dev-server/schema.json b/packages/angular/build/src/builders/dev-server/schema.json new file mode 100644 index 0000000000..f10deb2339 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/schema.json @@ -0,0 +1,131 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Dev Server Target", + "description": "Dev Server target options for Build Facade.", + "type": "object", + "properties": { + "browserTarget": { + "type": "string", + "description": "A browser builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$", + "x-deprecated": "Use 'buildTarget' instead." + }, + "buildTarget": { + "type": "string", + "description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.", + "pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$" + }, + "port": { + "type": "number", + "description": "Port to listen on.", + "default": 4200 + }, + "host": { + "type": "string", + "description": "Host to listen on.", + "default": "localhost" + }, + "proxyConfig": { + "type": "string", + "description": "Proxy configuration file. For more information, see https://angular.io/guide/build#proxying-to-a-backend-server." + }, + "ssl": { + "type": "boolean", + "description": "Serve using HTTPS.", + "default": false + }, + "sslKey": { + "type": "string", + "description": "SSL key to use for serving HTTPS." + }, + "sslCert": { + "type": "string", + "description": "SSL certificate to use for serving HTTPS." + }, + "headers": { + "type": "object", + "description": "Custom HTTP headers to be added to all responses.", + "propertyNames": { + "pattern": "^[-_A-Za-z0-9]+$" + }, + "additionalProperties": { + "type": "string" + } + }, + "open": { + "type": "boolean", + "description": "Opens the url in default browser.", + "default": false, + "alias": "o" + }, + "verbose": { + "type": "boolean", + "description": "Adds more details to output logging." + }, + "liveReload": { + "type": "boolean", + "description": "Whether to reload the page on change, using live-reload.", + "default": true + }, + "publicHost": { + "type": "string", + "description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies. This option has no effect when using the 'application' or other esbuild-based builders." + }, + "allowedHosts": { + "type": "array", + "description": "List of hosts that are allowed to access the dev server. This option has no effect when using the 'application' or other esbuild-based builders.", + "default": [], + "items": { + "type": "string" + } + }, + "servePath": { + "type": "string", + "description": "The pathname where the application will be served." + }, + "disableHostCheck": { + "type": "boolean", + "description": "Don't verify connected clients are part of allowed hosts. This option has no effect when using the 'application' or other esbuild-based builders.", + "default": false + }, + "hmr": { + "type": "boolean", + "description": "Enable hot module replacement.", + "default": false + }, + "watch": { + "type": "boolean", + "description": "Rebuild on change.", + "default": true + }, + "poll": { + "type": "number", + "description": "Enable and define the file watching poll time period in milliseconds." + }, + "forceEsbuild": { + "type": "boolean", + "description": "Force the development server to use the 'browser-esbuild' builder when building. This is a developer preview option for the esbuild-based build system.", + "default": false + }, + "prebundle": { + "description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled. This option has no effect when using the 'browser' or other Webpack-based builders.", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "exclude": { + "description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.", + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false, + "required": ["exclude"] + } + ] + } + }, + "additionalProperties": false, + "anyOf": [{ "required": ["buildTarget"] }, { "required": ["browserTarget"] }] +} diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts new file mode 100644 index 0000000000..11cc2090a1 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-assets_spec.ts @@ -0,0 +1,125 @@ +/** + * @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 { executeDevServer } from '../../index'; +import { executeOnceAndFetch } from '../execute-fetch'; +import { describeServeBuilder } from '../jasmine-helpers'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; + +describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget, isVite) => { + const javascriptFileContent = + "import {foo} from 'unresolved'; /* a comment */const foo = `bar`;\n\n\n"; + + describe('Behavior: "browser builder assets"', () => { + it('serves a project JavaScript asset unmodified', async () => { + await harness.writeFile('src/extra.js', javascriptFileContent); + + setupTarget(harness, { + assets: ['src/extra.js'], + optimization: { + scripts: true, + }, + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'extra.js'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toContain(javascriptFileContent); + }); + + it('serves a project TypeScript asset unmodified', async () => { + await harness.writeFile('src/extra.ts', javascriptFileContent); + + setupTarget(harness, { + assets: ['src/extra.ts'], + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'extra.ts'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toContain(javascriptFileContent); + }); + + it('should return 404 for non existing assets', async () => { + setupTarget(harness, { + assets: [], + optimization: { + scripts: true, + }, + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'does-not-exist.js'); + + expect(result?.success).toBeTrue(); + expect(await response?.status).toBe(404); + }); + + it(`should return the asset that matches 'index.html' when path has a trailing '/'`, async () => { + await harness.writeFile( + 'src/login/index.html', + '

Login page

', + ); + + setupTarget(harness, { + assets: ['src/login'], + optimization: { + scripts: true, + }, + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'login/'); + + expect(result?.success).toBeTrue(); + expect(await response?.status).toBe(200); + expect(await response?.text()).toContain('

Login page

'); + }); + + (isVite ? it : xit)( + `should return the asset that matches '.html' when path has no trailing '/'`, + async () => { + await harness.writeFile( + 'src/login/new.html', + '

Login page

', + ); + + setupTarget(harness, { + assets: ['src/login'], + optimization: { + scripts: true, + }, + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'login/new'); + + expect(result?.success).toBeTrue(); + expect(await response?.status).toBe(200); + expect(await response?.text()).toContain('

Login page

'); + }, + ); + }); +}); diff --git a/packages/angular/build/src/builders/dev-server/tests/behavior/build-base-href_spec.ts b/packages/angular/build/src/builders/dev-server/tests/behavior/build-base-href_spec.ts new file mode 100644 index 0000000000..a8063c10ae --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/tests/behavior/build-base-href_spec.ts @@ -0,0 +1,49 @@ +/** + * @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 { executeDevServer } from '../../index'; +import { executeOnceAndFetch } from '../execute-fetch'; +import { describeServeBuilder } from '../jasmine-helpers'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; + +describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { + describe('Behavior: "buildTarget baseHref"', () => { + beforeEach(async () => { + setupTarget(harness, { + baseHref: '/test/', + }); + + // Application code is not needed for these tests + await harness.writeFile('src/main.ts', 'console.log("foo");'); + }); + + it('uses the baseHref defined in the "buildTarget" options as the serve path', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, '/test/main.js'); + + expect(result?.success).toBeTrue(); + const baseUrl = new URL(`${result?.baseUrl}/`); + expect(baseUrl.pathname).toBe('/test/'); + expect(await response?.text()).toContain('console.log'); + }); + + it('serves the application from baseHref location without trailing slash', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, '/test'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toContain('