Skip to content

Commit

Permalink
fix(@angular/build): trigger browser reload on asset changes with Vit…
Browse files Browse the repository at this point in the history
…e dev server

Ensures the Vite-based development server automatically reloads the browser when asset files are modified.

Closes angular#26141
  • Loading branch information
alan-agius4 committed Jan 8, 2025
1 parent 8425d01 commit b020af1
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 19 deletions.
51 changes: 40 additions & 11 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ interface OutputFileRecord {
type: BuildOutputFileType;
}

interface OutputAssetRecord {
source: string;
updated: boolean;
}

interface DevServerExternalResultMetadata extends Omit<ExternalResultMetadata, 'explicit'> {
explicitBrowser: string[];
explicitServer: string[];
Expand Down Expand Up @@ -168,7 +173,7 @@ export async function* serveWithVite(
let serverUrl: URL | undefined;
let hadError = false;
const generatedFiles = new Map<string, OutputFileRecord>();
const assetFiles = new Map<string, string>();
const assetFiles = new Map<string, OutputAssetRecord>();
const externalMetadata: DevServerExternalResultMetadata = {
implicitBrowser: [],
implicitServer: [],
Expand Down Expand Up @@ -229,19 +234,15 @@ export async function* serveWithVite(
assetFiles.clear();
componentStyles.clear();
generatedFiles.clear();
for (const entry of Object.entries(result.files)) {
const [outputPath, file] = entry;
if (file.origin === 'disk') {
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
continue;
}

for (const [outputPath, file] of Object.entries(result.files)) {
updateResultRecord(
outputPath,
file,
normalizePath,
htmlIndexPath,
generatedFiles,
assetFiles,
componentStyles,
// The initial build will not yet have a server setup
!server,
Expand All @@ -265,13 +266,15 @@ export async function* serveWithVite(
generatedFiles.delete(filePath);
assetFiles.delete(filePath);
}

for (const modified of result.modified) {
updateResultRecord(
modified,
result.files[modified],
normalizePath,
htmlIndexPath,
generatedFiles,
assetFiles,
componentStyles,
);
}
Expand All @@ -282,6 +285,7 @@ export async function* serveWithVite(
normalizePath,
htmlIndexPath,
generatedFiles,
assetFiles,
componentStyles,
);
}
Expand Down Expand Up @@ -352,12 +356,16 @@ export async function* serveWithVite(
if (server) {
// Update fs allow list to include any new assets from the build option.
server.config.server.fs.allow = [
...new Set([...server.config.server.fs.allow, ...assetFiles.values()]),
...new Set([
...server.config.server.fs.allow,
...[...assetFiles.values()].map(({ source }) => source),
]),
];

await handleUpdate(
normalizePath,
generatedFiles,
assetFiles,
server,
serverOptions,
context.logger,
Expand Down Expand Up @@ -470,15 +478,26 @@ export async function* serveWithVite(
async function handleUpdate(
normalizePath: (id: string) => string,
generatedFiles: Map<string, OutputFileRecord>,
assetFiles: Map<string, OutputAssetRecord>,
server: ViteDevServer,
serverOptions: NormalizedDevServerOptions,
logger: BuilderContext['logger'],
componentStyles: Map<string, ComponentStyleRecord>,
): Promise<void> {
const updatedFiles: string[] = [];
let destroyAngularServerAppCalled = false;

// Invadate any updated asset
for (const [file, record] of assetFiles) {
if (!record.updated) {
continue;
}

record.updated = false;
updatedFiles.push(file);
}

// Invalidate any updated files
let destroyAngularServerAppCalled = false;
for (const [file, record] of generatedFiles) {
if (!record.updated) {
continue;
Expand Down Expand Up @@ -583,10 +602,16 @@ function updateResultRecord(
normalizePath: (id: string) => string,
htmlIndexPath: string,
generatedFiles: Map<string, OutputFileRecord>,
assetFiles: Map<string, OutputAssetRecord>,
componentStyles: Map<string, ComponentStyleRecord>,
initial = false,
): void {
if (file.origin === 'disk') {
assetFiles.set('/' + normalizePath(outputPath), {
source: normalizePath(file.inputPath),
updated: !initial,
});

return;
}

Expand Down Expand Up @@ -643,7 +668,7 @@ function updateResultRecord(
export async function setupServer(
serverOptions: NormalizedDevServerOptions,
outputFiles: Map<string, OutputFileRecord>,
assets: Map<string, string>,
assets: Map<string, OutputAssetRecord>,
preserveSymlinks: boolean | undefined,
externalMetadata: DevServerExternalResultMetadata,
ssrMode: ServerSsrMode,
Expand Down Expand Up @@ -730,7 +755,11 @@ export async function setupServer(
// The first two are required for Vite to function in prebundling mode (the default) and to load
// the Vite client-side code for browser reloading. These would be available by default but when
// the `allow` option is explicitly configured, they must be included manually.
allow: [cacheDir, join(serverOptions.workspaceRoot, 'node_modules'), ...assets.values()],
allow: [
cacheDir,
join(serverOptions.workspaceRoot, 'node_modules'),
...[...assets.values()].map(({ source }) => source),
],
},
// This is needed when `externalDependencies` is used to prevent Vite load errors.
// NOTE: If Vite adds direct support for externals, this can be removed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { lookup as lookupMimeType } from 'mrmime';
import { extname } from 'node:path';
import type { Connect, ViteDevServer } from 'vite';
import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils';
import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils';

export interface ComponentStyleRecord {
rawContent: Uint8Array;
Expand All @@ -19,7 +19,7 @@ export interface ComponentStyleRecord {

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

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

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

return;
Expand All @@ -61,7 +61,7 @@ export function createAngularAssetsMiddleware(
assets.get(pathname + '.html');

if (htmlAssetSourcePath) {
req.url = `${server.config.base}@fs/${encodeURI(htmlAssetSourcePath)}`;
req.url = `${server.config.base}@fs/${encodeURI(htmlAssetSourcePath.source)}`;
next();

return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
createAngularSsrExternalMiddleware,
createAngularSsrInternalMiddleware,
} from '../middlewares';
import { AngularMemoryOutputFiles } from '../utils';
import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils';

export enum ServerSsrMode {
/**
Expand Down Expand Up @@ -47,7 +47,7 @@ export enum ServerSsrMode {

interface AngularSetupMiddlewaresPluginOptions {
outputFiles: AngularMemoryOutputFiles;
assets: Map<string, string>;
assets: AngularOutputAssets;
extensionMiddleware?: Connect.NextHandleFunction[];
indexHtmlTransformer?: (content: string) => Promise<string>;
componentStyles: Map<string, ComponentStyleRecord>;
Expand Down
2 changes: 2 additions & 0 deletions packages/angular/build/src/tools/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type AngularMemoryOutputFiles = Map<
{ contents: Uint8Array; hash: string; servable: boolean }
>;

export type AngularOutputAssets = Map<string, { source: string }>;

export function pathnameWithoutBasePath(url: string, basePath: string): string {
const parsedUrl = new URL(url, 'http://localhost');
const pathname = decodeURIComponent(parsedUrl.pathname);
Expand Down

0 comments on commit b020af1

Please sign in to comment.