From da19b37803f9b15dc8bb25b1c621f9f1e72af974 Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Tue, 16 Sep 2025 16:25:19 -0400
Subject: [PATCH] fix(@angular/build): serve build assets and styles in vitest
Adds a Vite middleware to the Vitest runner to serve assets and global stylesheets that are part of the build output.
Previously, when running component tests with Vitest, any referenced assets (such as images in an `
` tag) or component stylesheets would result in a 404 error because the Vite server was not configured to serve them.
This change introduces a middleware that intercepts requests for non-script files. It checks against the build's output files and serves the corresponding asset directly from memory or disk. This ensures that the test environment accurately reflects the assets available during a real build, allowing components to render correctly for testing.
---
.../unit-test/runners/vitest/plugins.ts | 6 +++
.../vite/middlewares/assets-middleware.ts | 43 +++++++++++++++++++
2 files changed, 49 insertions(+)
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 166a40ded6b3..df7d2e06449f 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
@@ -10,6 +10,7 @@ import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import type { VitestPlugin } from 'vitest/node';
+import { createBuildAssetsMiddleware } from '../../../../tools/vite/middlewares/assets-middleware';
import { toPosixPath } from '../../../../utils/path';
import type { ResultFile } from '../../../application/results';
import type { NormalizedUnitTestBuilderOptions } from '../../options';
@@ -129,6 +130,11 @@ export function createVitestPlugins(
};
}
},
+ configureServer: (server) => {
+ server.middlewares.use(
+ createBuildAssetsMiddleware(server.config.base, buildResultFiles),
+ );
+ },
},
{
name: 'angular:html-index',
diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts
index a9fe69ffca15..414ec5ca13d1 100644
--- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts
+++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts
@@ -12,6 +12,7 @@ import { readFileSync } from 'node:fs';
import type { ServerResponse } from 'node:http';
import { extname } from 'node:path';
import type { Connect, ViteDevServer } from 'vite';
+import { ResultFile } from '../../../builders/application/results';
import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils';
export interface ComponentStyleRecord {
@@ -214,3 +215,45 @@ function checkAndHandleEtag(
return false;
}
+
+export function createBuildAssetsMiddleware(
+ basePath: string,
+ buildResultFiles: ReadonlyMap,
+ readHandler: (path: string) => Buffer = readFileSync,
+): Connect.NextHandleFunction {
+ return function buildAssetsMiddleware(req, res, next) {
+ if (req.url === undefined || res.writableEnded) {
+ return;
+ }
+
+ // Parse the incoming request.
+ // The base of the URL is unused but required to parse the URL.
+ const pathname = pathnameWithoutBasePath(req.url, basePath);
+ const extension = extname(pathname);
+ if (extension && !/\.[mc]?[jt]s(?:\.map)?$/.test(extension)) {
+ const outputFile = buildResultFiles.get(pathname.slice(1));
+ if (outputFile) {
+ const contents =
+ outputFile.origin === 'memory' ? outputFile.contents : readHandler(outputFile.inputPath);
+
+ const etag = `W/${createHash('sha256').update(contents).digest('hex')}`;
+ if (checkAndHandleEtag(req, res, etag)) {
+ return;
+ }
+
+ const mimeType = lookupMimeType(extension);
+ if (mimeType) {
+ res.setHeader('Content-Type', mimeType);
+ }
+
+ res.setHeader('ETag', etag);
+ res.setHeader('Cache-Control', 'no-cache');
+ res.end(contents);
+
+ return;
+ }
+ }
+
+ next();
+ };
+}