diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx index e5cfbf6da28d..98229e11039a 100755 --- a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx +++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx @@ -2,7 +2,7 @@ # Input hashes for repository rule npm_translate_lock(name = "npm2", pnpm_lock = "@//:pnpm-lock.yaml"). # This file should be checked into version control along with the pnpm-lock.yaml file. .npmrc=-2023857461 -package.json=-681275834 +package.json=688000741 packages/angular/cli/package.json=349838588 packages/angular/pwa/package.json=-1352285148 packages/angular_devkit/architect/package.json=-1496633956 @@ -13,6 +13,6 @@ packages/angular_devkit/schematics/package.json=673943597 packages/angular_devkit/schematics_cli/package.json=-356386813 packages/ngtools/webpack/package.json=-942726894 packages/schematics/angular/package.json=251715148 -pnpm-lock.yaml=-2120244736 +pnpm-lock.yaml=926343104 pnpm-workspace.yaml=1732591250 -yarn.lock=1185228888 +yarn.lock=969972397 diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index 8936ba852ac9..79240b301679 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -108,6 +108,7 @@ export enum BuildOutputFileType { // @public export type DevServerBuilderOptions = { + allowedHosts?: AllowedHosts; buildTarget: string; headers?: { [key: string]: string; diff --git a/package.json b/package.json index 3fd5690413f6..2172432b3f02 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,7 @@ "unenv": "^1.10.0", "verdaccio": "6.0.5", "verdaccio-auth-memory": "^10.0.0", - "vite": "6.0.7", + "vite": "6.0.11", "watchpack": "2.4.2", "webpack": "5.97.1", "webpack-dev-middleware": "7.4.2", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 11d8b27a4c1f..ac2ba4996adf 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -41,7 +41,7 @@ "rollup": "4.30.1", "sass": "1.83.1", "semver": "7.6.3", - "vite": "6.0.7", + "vite": "6.0.11", "watchpack": "2.4.2" }, "optionalDependencies": { diff --git a/packages/angular/build/src/builders/dev-server/options.ts b/packages/angular/build/src/builders/dev-server/options.ts index 237c26b8c2f6..3e0f59319117 100644 --- a/packages/angular/build/src/builders/dev-server/options.ts +++ b/packages/angular/build/src/builders/dev-server/options.ts @@ -103,6 +103,7 @@ export async function normalizeOptions( sslCert, sslKey, prebundle, + allowedHosts, } = options; // Return all the normalized options @@ -128,5 +129,6 @@ export async function normalizeOptions( // Prebundling defaults to true but requires caching to function prebundle: cacheOptions.enabled && !optimization.scripts && prebundle, inspect, + allowedHosts: allowedHosts ? allowedHosts : [], }; } diff --git a/packages/angular/build/src/builders/dev-server/schema.json b/packages/angular/build/src/builders/dev-server/schema.json index 2eb16987b8ca..3d5cf155aae2 100644 --- a/packages/angular/build/src/builders/dev-server/schema.json +++ b/packages/angular/build/src/builders/dev-server/schema.json @@ -36,6 +36,23 @@ "type": "string", "description": "SSL certificate to use for serving HTTPS." }, + "allowedHosts": { + "description": "The hosts that can access the development server. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts", + "default": [], + "oneOf": [ + { + "type": "array", + "description": "List of hosts that are allowed to access the development server.", + "items": { + "type": "string" + } + }, + { + "type": "boolean", + "description": "Indicates that all hosts are allowed. This is not recommended and a security risk." + } + ] + }, "headers": { "type": "object", "description": "Custom HTTP headers to be added to all responses.", diff --git a/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts b/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts index 3bb731a6c6b3..a36196da14be 100644 --- a/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts +++ b/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts @@ -7,7 +7,8 @@ */ import { lastValueFrom, mergeMap, take, timeout } from 'rxjs'; -import { URL } from 'url'; +import { get, IncomingMessage, RequestOptions } from 'node:http'; +import { text } from 'node:stream/consumers'; import { BuilderHarness, BuilderHarnessExecutionOptions, @@ -41,3 +42,48 @@ export async function executeOnceAndFetch( ), ); } + +/** + * Executes the builder and then immediately performs a GET request + * via the Node.js `http` builtin module. This is useful for cases + * where the `fetch` API is limited such as testing different `Host` + * header values with the development server. + * The `fetch` based alternative is preferred otherwise. + * + * @param harness A builder harness instance. + * @param url The URL string to get. + * @param options An options object. + */ +export async function executeOnceAndGet( + harness: BuilderHarness, + url: string, + options?: Partial & { request?: RequestOptions }, +): Promise { + return lastValueFrom( + harness.execute().pipe( + timeout(30_000), + mergeMap(async (executionResult) => { + let response = undefined; + let content = undefined; + if (executionResult.result?.success) { + let baseUrl = `${executionResult.result.baseUrl}`; + baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`; + const resolvedUrl = new URL(url, baseUrl); + + response = await new Promise((resolve) => + get(resolvedUrl, options?.request ?? {}, resolve), + ); + + if (response.statusCode === 200) { + content = await text(response); + } + + response.resume(); + } + + return { ...executionResult, response, content }; + }), + take(1), + ), + ); +} diff --git a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts new file mode 100644 index 000000000000..8e96c7b4b4b0 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts @@ -0,0 +1,80 @@ +/** + * @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.dev/license + */ + +import { executeDevServer } from '../../index'; +import { executeOnceAndGet } from '../execute-fetch'; +import { describeServeBuilder } from '../jasmine-helpers'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; + +const FETCH_HEADERS = Object.freeze({ Host: 'example.com' }); + +describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { + describe('option: "allowedHosts"', () => { + beforeEach(async () => { + setupTarget(harness); + + // Application code is not needed for these tests + await harness.writeFile('src/main.ts', ''); + }); + + it('does not allow an invalid host when option is not present', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(response?.statusCode).toBe(403); + }); + + it('does not allow an invalid host when option is an empty array', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + allowedHosts: [], + }); + + const { result, response } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(response?.statusCode).toBe(403); + }); + + it('allows a host when specified in the option', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + allowedHosts: ['example.com'], + }); + + const { result, content } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(content).toContain(''); + }); + + it('allows a host when option is true', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + allowedHosts: true, + }); + + const { result, content } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(content).toContain('<title>'); + }); + }); +}); diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 217e0af011e3..c27423985aeb 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -758,6 +758,7 @@ export async function setupServer( strictPort: true, host: serverOptions.host, open: serverOptions.open, + allowedHosts: serverOptions.allowedHosts, headers: serverOptions.headers, // Disable the websocket if live reload is disabled (false/undefined are the only valid values) ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined, 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 index 76f8e282e8e4..e27413816904 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts @@ -88,12 +88,22 @@ export function execute( // New build system defaults hmr option to the value of liveReload normalizedOptions.hmr ??= normalizedOptions.liveReload; + // New build system uses Vite's allowedHost option convention of true for disabling host checks + if (normalizedOptions.disableHostCheck) { + (normalizedOptions as unknown as { allowedHosts: true }).allowedHosts = true; + } else { + normalizedOptions.allowedHosts ??= []; + } + return defer(() => Promise.all([import('@angular/build/private'), import('../browser-esbuild')]), ).pipe( switchMap(([{ serveWithVite, buildApplicationInternal }, { convertBrowserOptions }]) => serveWithVite( - normalizedOptions as typeof normalizedOptions & { hmr: boolean }, + normalizedOptions as typeof normalizedOptions & { + hmr: boolean; + allowedHosts: true | string[]; + }, builderName, (options, context, codePlugins) => { return builderName === '@angular-devkit/build-angular:browser-esbuild' diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json b/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json index a1001107f86f..bb7a87a5d2f7 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json @@ -67,7 +67,7 @@ }, "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.", + "description": "List of hosts that are allowed to access the dev server.", "default": [], "items": { "type": "string" @@ -79,7 +79,7 @@ }, "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.", + "description": "Don't verify connected clients are part of allowed hosts.", "default": false }, "hmr": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa01853718cd..fdde87abf10a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,7 +219,7 @@ importers: version: 8.19.1(eslint@8.57.0)(typescript@5.7.3) '@vitejs/plugin-basic-ssl': specifier: 1.2.0 - version: 1.2.0(vite@6.0.7) + version: 1.2.0(vite@6.0.11) '@web/test-runner': specifier: ^0.19.0 version: 0.19.0 @@ -491,8 +491,8 @@ importers: specifier: ^10.0.0 version: 10.2.2 vite: - specifier: 6.0.7 - version: 6.0.7(@types/node@18.19.70)(less@4.2.1)(sass@1.83.1)(terser@5.37.0) + specifier: 6.0.11 + version: 6.0.11(@types/node@18.19.70)(less@4.2.1)(sass@1.83.1)(terser@5.37.0) watchpack: specifier: 2.4.2 version: 2.4.2 @@ -2360,6 +2360,7 @@ packages: /@bazel/typescript@5.8.1(typescript@5.7.3): resolution: {integrity: sha512-NAJ8WQHZL1WE1YmRoCrq/1hhG15Mvy/viWh6TkvFnBeEhNUiQUsA5GYyhU1ztnBIYW03nATO3vwhAEfO7Q0U5g==} + deprecated: No longer maintained, https://github.com/aspect-build/rules_ts is the recommended replacement hasBin: true peerDependencies: typescript: 5.7.3 @@ -5605,22 +5606,22 @@ packages: semver: 7.6.3 dev: true - /@vitejs/plugin-basic-ssl@1.2.0(vite@6.0.3): + /@vitejs/plugin-basic-ssl@1.2.0(vite@6.0.11): resolution: {integrity: sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==} engines: {node: '>=14.21.3'} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 dependencies: - vite: 6.0.3(@types/node@18.19.70)(less@4.2.1)(sass@1.83.0)(terser@5.37.0) + vite: 6.0.11(@types/node@18.19.70)(less@4.2.1)(sass@1.83.1)(terser@5.37.0) dev: true - /@vitejs/plugin-basic-ssl@1.2.0(vite@6.0.7): + /@vitejs/plugin-basic-ssl@1.2.0(vite@6.0.3): resolution: {integrity: sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==} engines: {node: '>=14.21.3'} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 dependencies: - vite: 6.0.7(@types/node@18.19.70)(less@4.2.1)(sass@1.83.1)(terser@5.37.0) + vite: 6.0.3(@types/node@18.19.70)(less@4.2.1)(sass@1.83.0)(terser@5.37.0) dev: true /@web/browser-logs@0.4.1: @@ -12336,7 +12337,7 @@ packages: /puppeteer@18.2.1: resolution: {integrity: sha512-7+UhmYa7wxPh2oMRwA++k8UGVDxh3YdWFB52r9C3tM81T6BU7cuusUSxImz0GEYSOYUKk/YzIhkQ6+vc0gHbxQ==} engines: {node: '>=14.1.0'} - deprecated: < 19.4.0 is no longer supported + deprecated: < 22.8.2 is no longer supported dependencies: https-proxy-agent: 5.0.1(supports-color@10.0.0) progress: 2.0.3 @@ -14497,8 +14498,8 @@ packages: extsprintf: 1.4.1 dev: true - /vite@6.0.3(@types/node@18.19.70)(less@4.2.1)(sass@1.83.0)(terser@5.37.0): - resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==} + /vite@6.0.11(@types/node@18.19.70)(less@4.2.1)(sass@1.83.1)(terser@5.37.0): + resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -14542,14 +14543,14 @@ packages: less: 4.2.1 postcss: 8.4.49 rollup: 4.30.1 - sass: 1.83.0 + sass: 1.83.1 terser: 5.37.0 optionalDependencies: fsevents: 2.3.3 dev: true - /vite@6.0.7(@types/node@18.19.70)(less@4.2.1)(sass@1.83.1)(terser@5.37.0): - resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==} + /vite@6.0.3(@types/node@18.19.70)(less@4.2.1)(sass@1.83.0)(terser@5.37.0): + resolution: {integrity: sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -14593,7 +14594,7 @@ packages: less: 4.2.1 postcss: 8.4.49 rollup: 4.30.1 - sass: 1.83.1 + sass: 1.83.0 terser: 5.37.0 optionalDependencies: fsevents: 2.3.3 diff --git a/yarn.lock b/yarn.lock index 25b196053f28..fa4cb96054f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -467,7 +467,7 @@ __metadata: unenv: "npm:^1.10.0" verdaccio: "npm:6.0.5" verdaccio-auth-memory: "npm:^10.0.0" - vite: "npm:6.0.7" + vite: "npm:6.0.11" watchpack: "npm:2.4.2" webpack: "npm:5.97.1" webpack-dev-middleware: "npm:7.4.2" @@ -18519,11 +18519,11 @@ __metadata: languageName: node linkType: hard -"vite@npm:6.0.3": - version: 6.0.3 - resolution: "vite@npm:6.0.3" +"vite@npm:6.0.11": + version: 6.0.11 + resolution: "vite@npm:6.0.11" dependencies: - esbuild: "npm:^0.24.0" + esbuild: "npm:^0.24.2" fsevents: "npm:~2.3.3" postcss: "npm:^8.4.49" rollup: "npm:^4.23.0" @@ -18567,15 +18567,15 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/764ebed14770426a638575b23a51127c630ace873999ab896b0184484d8107e7255cdf64cfb36c65c1ef1d583e44b70a1d14c0f05b89612e834a5806e3964475 + checksum: 10c0/a0537f9bf8d6ded740646a4aa44b8dbf442d3005e75f7b27e981ef6011f22d4759f5eb643a393c0ffb8d21e2f50fb5f774d3a53108fb96a10b0f83697e8efe84 languageName: node linkType: hard -"vite@npm:6.0.7": - version: 6.0.7 - resolution: "vite@npm:6.0.7" +"vite@npm:6.0.3": + version: 6.0.3 + resolution: "vite@npm:6.0.3" dependencies: - esbuild: "npm:^0.24.2" + esbuild: "npm:^0.24.0" fsevents: "npm:~2.3.3" postcss: "npm:^8.4.49" rollup: "npm:^4.23.0" @@ -18619,7 +18619,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/ae81047b4290a7206b9394a39a782d509e9610462e7946422ba22d5bc615b5a322c07e33d7bf9dd0b3312ec3f5c63353b725913d1519324bfdf539b4f1e03f52 + checksum: 10c0/764ebed14770426a638575b23a51127c630ace873999ab896b0184484d8107e7255cdf64cfb36c65c1ef1d583e44b70a1d14c0f05b89612e834a5806e3964475 languageName: node linkType: hard