diff --git a/packages/angular/build/src/builders/dev-server/vite/index.ts b/packages/angular/build/src/builders/dev-server/vite/index.ts index b1f70379125c..75987b2c2cc2 100644 --- a/packages/angular/build/src/builders/dev-server/vite/index.ts +++ b/packages/angular/build/src/builders/dev-server/vite/index.ts @@ -14,7 +14,7 @@ import { join } from 'node:path'; import type { Connect, ViteDevServer } from 'vite'; import type { ComponentStyleRecord } from '../../../tools/vite/middlewares'; import { ServerSsrMode } from '../../../tools/vite/plugins'; -import { EsbuildLoaderOption } from '../../../tools/vite/utils'; +import { EsbuildLoaderOption, updateExternalMetadata } from '../../../tools/vite/utils'; import { normalizeSourceMaps } from '../../../utils'; import { useComponentStyleHmr, useComponentTemplateHmr } from '../../../utils/environment-options'; import { Result, ResultKind } from '../../application/results'; @@ -35,7 +35,6 @@ import { DevServerExternalResultMetadata, OutputAssetRecord, OutputFileRecord, - isAbsoluteUrl, updateResultRecord, } from './utils'; @@ -333,34 +332,7 @@ export async function* serveWithVite( } // To avoid disconnecting the array objects from the option, these arrays need to be mutated instead of replaced. - if (result.detail?.['externalMetadata']) { - const { implicitBrowser, implicitServer, explicit } = result.detail[ - 'externalMetadata' - ] as ExternalResultMetadata; - const implicitServerFiltered = implicitServer.filter( - (m) => !isBuiltin(m) && !isAbsoluteUrl(m), - ); - const implicitBrowserFiltered = implicitBrowser.filter((m) => !isAbsoluteUrl(m)); - - // Empty Arrays to avoid growing unlimited with every re-build. - externalMetadata.explicitBrowser.length = 0; - externalMetadata.explicitServer.length = 0; - externalMetadata.implicitServer.length = 0; - externalMetadata.implicitBrowser.length = 0; - - const externalDeps = browserOptions.externalDependencies ?? []; - externalMetadata.explicitBrowser.push(...explicit, ...externalDeps); - externalMetadata.explicitServer.push(...explicit, ...externalDeps, ...builtinModules); - externalMetadata.implicitServer.push(...implicitServerFiltered); - externalMetadata.implicitBrowser.push(...implicitBrowserFiltered); - - // The below needs to be sorted as Vite uses these options are part of the hashing invalidation algorithm. - // See: https://github.com/vitejs/vite/blob/0873bae0cfe0f0718ad2f5743dd34a17e4ab563d/packages/vite/src/node/optimizer/index.ts#L1203-L1239 - externalMetadata.explicitBrowser.sort(); - externalMetadata.explicitServer.sort(); - externalMetadata.implicitServer.sort(); - externalMetadata.implicitBrowser.sort(); - } + updateExternalMetadata(result, externalMetadata, browserOptions.externalDependencies); if (server) { // Update fs allow list to include any new assets from the build option. diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts index 5df9aafe0d57..007a9a7d4a50 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts @@ -99,11 +99,8 @@ export async function getVitestBuildOptions( entryPoints.set('init-testbed', 'angular:test-bed-init'); const externalDependencies = new Set(['vitest']); - if (!options.browsers?.length) { - // Only add for non-browser setups. - // Comprehensive browser prebundling will be handled separately. - ANGULAR_PACKAGES_TO_EXTERNALIZE.forEach((dep) => externalDependencies.add(dep)); - } + ANGULAR_PACKAGES_TO_EXTERNALIZE.forEach((dep) => externalDependencies.add(dep)); + if (baseBuildOptions.externalDependencies) { baseBuildOptions.externalDependencies.forEach((dep) => externalDependencies.add(dep)); } diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 950a96f2adac..538f005ecdcc 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -9,8 +9,11 @@ import type { BuilderOutput } from '@angular-devkit/architect'; import assert from 'node:assert'; import path from 'node:path'; -import { isMatch } from 'picomatch'; import type { Vitest } from 'vitest/node'; +import { + DevServerExternalResultMetadata, + updateExternalMetadata, +} from '../../../../tools/vite/utils'; import { assertIsError } from '../../../../utils/error'; import { type FullResult, @@ -30,6 +33,12 @@ export class VitestExecutor implements TestExecutor { private readonly projectName: string; private readonly options: NormalizedUnitTestBuilderOptions; private readonly buildResultFiles = new Map(); + private readonly externalMetadata: DevServerExternalResultMetadata = { + implicitBrowser: [], + implicitServer: [], + explicitBrowser: [], + explicitServer: [], + }; // This is a reverse map of the entry points created in `build-options.ts`. // It is used by the in-memory provider plugin to map the requested test file @@ -71,6 +80,8 @@ export class VitestExecutor implements TestExecutor { } } + updateExternalMetadata(buildResult, this.externalMetadata, undefined, true); + // Initialize Vitest if not already present. this.vitest ??= await this.initializeVitest(); const vitest = this.vitest; @@ -220,6 +231,7 @@ export class VitestExecutor implements TestExecutor { coverage, projectName, projectSourceRoot: this.options.projectSourceRoot, + optimizeDepsInclude: this.externalMetadata.explicitBrowser, reporters, setupFiles: testSetupFiles, projectPlugins, diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index e425bdf95e4d..fa3f5f7d3ab9 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -42,6 +42,7 @@ interface VitestConfigPluginOptions { setupFiles: string[]; projectPlugins: Exclude; include: string[]; + optimizeDepsInclude: string[]; } async function findTestEnvironment( @@ -133,6 +134,7 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi }, optimizeDeps: { noDiscovery: true, + include: options.optimizeDepsInclude, }, plugins: projectPlugins, }; diff --git a/packages/angular/build/src/tools/vite/utils.ts b/packages/angular/build/src/tools/vite/utils.ts index 2322eb210bec..2f7cfba84306 100644 --- a/packages/angular/build/src/tools/vite/utils.ts +++ b/packages/angular/build/src/tools/vite/utils.ts @@ -7,8 +7,10 @@ */ import { lookup as lookupMimeType } from 'mrmime'; +import { builtinModules, isBuiltin } from 'node:module'; import { extname } from 'node:path'; import type { DepOptimizationConfig } from 'vite'; +import type { ExternalResultMetadata } from '../esbuild/bundler-execution-result'; import { JavaScriptTransformer } from '../esbuild/javascript-transformer'; import { getFeatureSupport } from '../esbuild/utils'; @@ -109,3 +111,61 @@ export function getDepOptimizationConfig({ }, }; } + +export interface DevServerExternalResultMetadata { + implicitBrowser: string[]; + implicitServer: string[]; + explicitBrowser: string[]; + explicitServer: string[]; +} + +export function isAbsoluteUrl(url: string): boolean { + try { + new URL(url); + + return true; + } catch { + return false; + } +} + +export function updateExternalMetadata( + result: { detail?: { externalMetadata?: ExternalResultMetadata } }, + externalMetadata: DevServerExternalResultMetadata, + externalDependencies: string[] | undefined, + explicitPackagesOnly: boolean = false, +): void { + if (!result.detail?.['externalMetadata']) { + return; + } + + const { implicitBrowser, implicitServer, explicit } = result.detail['externalMetadata']; + const implicitServerFiltered = implicitServer.filter((m) => !isBuiltin(m) && !isAbsoluteUrl(m)); + const implicitBrowserFiltered = implicitBrowser.filter((m) => !isAbsoluteUrl(m)); + const explicitBrowserFiltered = explicitPackagesOnly + ? explicit.filter((m) => !isAbsoluteUrl(m)) + : explicit; + + // Empty Arrays to avoid growing unlimited with every re-build. + externalMetadata.explicitBrowser.length = 0; + externalMetadata.explicitServer.length = 0; + externalMetadata.implicitServer.length = 0; + externalMetadata.implicitBrowser.length = 0; + + const externalDeps = externalDependencies ?? []; + externalMetadata.explicitBrowser.push(...explicitBrowserFiltered, ...externalDeps); + externalMetadata.explicitServer.push( + ...explicitBrowserFiltered, + ...externalDeps, + ...builtinModules, + ); + externalMetadata.implicitServer.push(...implicitServerFiltered); + externalMetadata.implicitBrowser.push(...implicitBrowserFiltered); + + // The below needs to be sorted as Vite uses these options as part of the hashing invalidation algorithm. + // See: https://github.com/vitejs/vite/blob/0873bae0cfe0f0718ad2f5743dd34a17e4ab563d/packages/vite/src/node/optimizer/index.ts#L1203-L1239 + externalMetadata.explicitBrowser.sort(); + externalMetadata.explicitServer.sort(); + externalMetadata.implicitServer.sort(); + externalMetadata.implicitBrowser.sort(); +}