Skip to content

Commit 65894c4

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 7ce76d5 commit 65894c4

File tree

5 files changed

+53
-22
lines changed

5 files changed

+53
-22
lines changed

packages/angular/build/src/builders/application/build-action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,10 @@ function* emitOutputResults(
313313
for (const { source, destination } of assetFiles) {
314314
removedAssetFiles.delete(source);
315315

316-
if (changes.modified.has(source)) {
317-
incrementalResult.modified.push(destination);
318-
} else if (!previousAssetsInfo.has(source)) {
316+
if (!previousAssetsInfo.has(source)) {
319317
incrementalResult.added.push(destination);
318+
} else if (changes.modified.has(source)) {
319+
incrementalResult.modified.push(destination);
320320
} else {
321321
continue;
322322
}

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,
@@ -471,15 +479,26 @@ export async function* serveWithVite(
471479
async function handleUpdate(
472480
normalizePath: (id: string) => string,
473481
generatedFiles: Map<string, OutputFileRecord>,
482+
assetFiles: Map<string, OutputAssetRecord>,
474483
server: ViteDevServer,
475484
serverOptions: NormalizedDevServerOptions,
476485
logger: BuilderContext['logger'],
477486
componentStyles: Map<string, ComponentStyleRecord>,
478487
): Promise<void> {
479488
const updatedFiles: string[] = [];
480-
let destroyAngularServerAppCalled = false;
489+
490+
// Invadate any updated asset
491+
for (const [file, record] of assetFiles) {
492+
if (!record.updated) {
493+
continue;
494+
}
495+
496+
record.updated = false;
497+
updatedFiles.push(file);
498+
}
481499

482500
// Invalidate any updated files
501+
let destroyAngularServerAppCalled = false;
483502
for (const [file, record] of generatedFiles) {
484503
if (!record.updated) {
485504
continue;
@@ -584,10 +603,16 @@ function updateResultRecord(
584603
normalizePath: (id: string) => string,
585604
htmlIndexPath: string,
586605
generatedFiles: Map<string, OutputFileRecord>,
606+
assetFiles: Map<string, OutputAssetRecord>,
587607
componentStyles: Map<string, ComponentStyleRecord>,
588608
initial = false,
589609
): void {
590610
if (file.origin === 'disk') {
611+
assetFiles.set('/' + normalizePath(outputPath), {
612+
source: normalizePath(file.inputPath),
613+
updated: !initial,
614+
});
615+
591616
return;
592617
}
593618

@@ -644,7 +669,7 @@ function updateResultRecord(
644669
export async function setupServer(
645670
serverOptions: NormalizedDevServerOptions,
646671
outputFiles: Map<string, OutputFileRecord>,
647-
assets: Map<string, string>,
672+
assets: Map<string, OutputAssetRecord>,
648673
preserveSymlinks: boolean | undefined,
649674
externalMetadata: DevServerExternalResultMetadata,
650675
ssrMode: ServerSsrMode,
@@ -743,7 +768,11 @@ export async function setupServer(
743768
// The first two are required for Vite to function in prebundling mode (the default) and to load
744769
// the Vite client-side code for browser reloading. These would be available by default but when
745770
// the `allow` option is explicitly configured, they must be included manually.
746-
allow: [cacheDir, join(serverOptions.workspaceRoot, 'node_modules'), ...assets.values()],
771+
allow: [
772+
cacheDir,
773+
join(serverOptions.workspaceRoot, 'node_modules'),
774+
...[...assets.values()].map(({ source }) => source),
775+
],
747776
},
748777
// This is needed when `externalDependencies` is used to prevent Vite load errors.
749778
// 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)