Skip to content

Commit 3cd0ae9

Browse files
committed
fix(@angular/build): trigger browser reload on asset changes with Vite dev server
Ensures the Vite-based development server automatically reloads the browser when asset files are modified. Closes #26141
1 parent 77d76c7 commit 3cd0ae9

File tree

4 files changed

+50
-19
lines changed

4 files changed

+50
-19
lines changed

packages/angular/build/src/builders/dev-server/vite-server.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ interface OutputFileRecord {
4949
type: BuildOutputFileType;
5050
}
5151

52+
interface OutputAssetRecord {
53+
source: string;
54+
updated: boolean;
55+
}
56+
5257
interface DevServerExternalResultMetadata extends Omit<ExternalResultMetadata, 'explicit'> {
5358
explicitBrowser: string[];
5459
explicitServer: string[];
@@ -168,7 +173,7 @@ export async function* serveWithVite(
168173
let serverUrl: URL | undefined;
169174
let hadError = false;
170175
const generatedFiles = new Map<string, OutputFileRecord>();
171-
const assetFiles = new Map<string, string>();
176+
const assetFiles = new Map<string, OutputAssetRecord>();
172177
const externalMetadata: DevServerExternalResultMetadata = {
173178
implicitBrowser: [],
174179
implicitServer: [],
@@ -229,19 +234,15 @@ export async function* serveWithVite(
229234
assetFiles.clear();
230235
componentStyles.clear();
231236
generatedFiles.clear();
232-
for (const entry of Object.entries(result.files)) {
233-
const [outputPath, file] = entry;
234-
if (file.origin === 'disk') {
235-
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
236-
continue;
237-
}
238237

238+
for (const [outputPath, file] of Object.entries(result.files)) {
239239
updateResultRecord(
240240
outputPath,
241241
file,
242242
normalizePath,
243243
htmlIndexPath,
244244
generatedFiles,
245+
assetFiles,
245246
componentStyles,
246247
// The initial build will not yet have a server setup
247248
!server,
@@ -265,13 +266,15 @@ export async function* serveWithVite(
265266
generatedFiles.delete(filePath);
266267
assetFiles.delete(filePath);
267268
}
269+
268270
for (const modified of result.modified) {
269271
updateResultRecord(
270272
modified,
271273
result.files[modified],
272274
normalizePath,
273275
htmlIndexPath,
274276
generatedFiles,
277+
assetFiles,
275278
componentStyles,
276279
);
277280
}
@@ -282,6 +285,7 @@ export async function* serveWithVite(
282285
normalizePath,
283286
htmlIndexPath,
284287
generatedFiles,
288+
assetFiles,
285289
componentStyles,
286290
);
287291
}
@@ -352,12 +356,16 @@ export async function* serveWithVite(
352356
if (server) {
353357
// Update fs allow list to include any new assets from the build option.
354358
server.config.server.fs.allow = [
355-
...new Set([...server.config.server.fs.allow, ...assetFiles.values()]),
359+
...new Set([
360+
...server.config.server.fs.allow,
361+
...[...assetFiles.values()].map(({ source }) => source),
362+
]),
356363
];
357364

358365
await handleUpdate(
359366
normalizePath,
360367
generatedFiles,
368+
assetFiles,
361369
server,
362370
serverOptions,
363371
context.logger,
@@ -470,15 +478,26 @@ export async function* serveWithVite(
470478
async function handleUpdate(
471479
normalizePath: (id: string) => string,
472480
generatedFiles: Map<string, OutputFileRecord>,
481+
assetFiles: Map<string, OutputAssetRecord>,
473482
server: ViteDevServer,
474483
serverOptions: NormalizedDevServerOptions,
475484
logger: BuilderContext['logger'],
476485
componentStyles: Map<string, ComponentStyleRecord>,
477486
): Promise<void> {
478487
const updatedFiles: string[] = [];
479-
let destroyAngularServerAppCalled = false;
488+
489+
// Invadate any updated asset
490+
for (const [file, record] of assetFiles) {
491+
if (!record.updated) {
492+
continue;
493+
}
494+
495+
record.updated = false;
496+
updatedFiles.push(file);
497+
}
480498

481499
// Invalidate any updated files
500+
let destroyAngularServerAppCalled = false;
482501
for (const [file, record] of generatedFiles) {
483502
if (!record.updated) {
484503
continue;
@@ -583,10 +602,16 @@ function updateResultRecord(
583602
normalizePath: (id: string) => string,
584603
htmlIndexPath: string,
585604
generatedFiles: Map<string, OutputFileRecord>,
605+
assetFiles: Map<string, OutputAssetRecord>,
586606
componentStyles: Map<string, ComponentStyleRecord>,
587607
initial = false,
588608
): void {
589609
if (file.origin === 'disk') {
610+
assetFiles.set('/' + normalizePath(outputPath), {
611+
source: normalizePath(file.inputPath),
612+
updated: !initial,
613+
});
614+
590615
return;
591616
}
592617

@@ -643,7 +668,7 @@ function updateResultRecord(
643668
export async function setupServer(
644669
serverOptions: NormalizedDevServerOptions,
645670
outputFiles: Map<string, OutputFileRecord>,
646-
assets: Map<string, string>,
671+
assets: Map<string, OutputAssetRecord>,
647672
preserveSymlinks: boolean | undefined,
648673
externalMetadata: DevServerExternalResultMetadata,
649674
ssrMode: ServerSsrMode,
@@ -730,7 +755,11 @@ export async function setupServer(
730755
// The first two are required for Vite to function in prebundling mode (the default) and to load
731756
// the Vite client-side code for browser reloading. These would be available by default but when
732757
// the `allow` option is explicitly configured, they must be included manually.
733-
allow: [cacheDir, join(serverOptions.workspaceRoot, 'node_modules'), ...assets.values()],
758+
allow: [
759+
cacheDir,
760+
join(serverOptions.workspaceRoot, 'node_modules'),
761+
...[...assets.values()].map(({ source }) => source),
762+
],
734763
},
735764
// This is needed when `externalDependencies` is used to prevent Vite load errors.
736765
// NOTE: If Vite adds direct support for externals, this can be removed.

packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { lookup as lookupMimeType } from 'mrmime';
1010
import { extname } from 'node:path';
1111
import type { Connect, ViteDevServer } from 'vite';
12-
import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils';
12+
import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils';
1313

1414
export interface ComponentStyleRecord {
1515
rawContent: Uint8Array;
@@ -19,7 +19,7 @@ export interface ComponentStyleRecord {
1919

2020
export function createAngularAssetsMiddleware(
2121
server: ViteDevServer,
22-
assets: Map<string, string>,
22+
assets: AngularOutputAssets,
2323
outputFiles: AngularMemoryOutputFiles,
2424
componentStyles: Map<string, ComponentStyleRecord>,
2525
encapsulateStyle: (style: Uint8Array, componentId: string) => string,
@@ -36,16 +36,16 @@ export function createAngularAssetsMiddleware(
3636
const pathnameHasTrailingSlash = pathname[pathname.length - 1] === '/';
3737

3838
// Rewrite all build assets to a vite raw fs URL
39-
const assetSourcePath = assets.get(pathname);
40-
if (assetSourcePath !== undefined) {
39+
const asset = assets.get(pathname);
40+
if (asset) {
4141
// Workaround to disable Vite transformer middleware.
4242
// See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and
4343
// https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206
4444
req.headers.accept = 'text/html';
4545

4646
// The encoding needs to match what happens in the vite static middleware.
4747
// ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
48-
req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`;
48+
req.url = `${server.config.base}@fs/${encodeURI(asset.source)}`;
4949
next();
5050

5151
return;
@@ -61,7 +61,7 @@ export function createAngularAssetsMiddleware(
6161
assets.get(pathname + '.html');
6262

6363
if (htmlAssetSourcePath) {
64-
req.url = `${server.config.base}@fs/${encodeURI(htmlAssetSourcePath)}`;
64+
req.url = `${server.config.base}@fs/${encodeURI(htmlAssetSourcePath.source)}`;
6565
next();
6666

6767
return;

packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
createAngularSsrExternalMiddleware,
1919
createAngularSsrInternalMiddleware,
2020
} from '../middlewares';
21-
import { AngularMemoryOutputFiles } from '../utils';
21+
import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils';
2222

2323
export enum ServerSsrMode {
2424
/**
@@ -47,7 +47,7 @@ export enum ServerSsrMode {
4747

4848
interface AngularSetupMiddlewaresPluginOptions {
4949
outputFiles: AngularMemoryOutputFiles;
50-
assets: Map<string, string>;
50+
assets: AngularOutputAssets;
5151
extensionMiddleware?: Connect.NextHandleFunction[];
5252
indexHtmlTransformer?: (content: string) => Promise<string>;
5353
componentStyles: Map<string, ComponentStyleRecord>;

packages/angular/build/src/tools/vite/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export type AngularMemoryOutputFiles = Map<
1818
{ contents: Uint8Array; hash: string; servable: boolean }
1919
>;
2020

21+
export type AngularOutputAssets = Map<string, { source: string }>;
22+
2123
export function pathnameWithoutBasePath(url: string, basePath: string): string {
2224
const parsedUrl = new URL(url, 'http://localhost');
2325
const pathname = decodeURIComponent(parsedUrl.pathname);

0 commit comments

Comments
 (0)