From 5d25503536f281512037306cde93c3e34f33b047 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:24:44 -0500 Subject: [PATCH] refactor(@angular/build): create shared utility for external metadata The logic to process external metadata from build results was previously implemented directly within the dev-server. This commit extracts this logic into a shared utility function, `updateExternalMetadata`, and moves it to a common location. The dev-server is updated to use this new function. Additionally, the Vitest unit test builder is modified to leverage this utility, allowing it to correctly track external dependencies for Vite's dependency optimization. --- .../src/builders/dev-server/vite/index.ts | 32 +--------- .../unit-test/runners/vitest/build-options.ts | 7 +-- .../unit-test/runners/vitest/executor.ts | 14 ++++- .../unit-test/runners/vitest/plugins.ts | 2 + .../angular/build/src/tools/vite/utils.ts | 60 +++++++++++++++++++ 5 files changed, 79 insertions(+), 36 deletions(-) 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(); +}