Skip to content

Commit c787662

Browse files
committed
fix(@angular/build): workaround Vite CSS ShadowDOM hot replacement
When using the development server with the application builder (default for new projects), Angular components using ShadowDOM view encapsulation will now cause a full page reload. This ensures that these components styles are correctly updated during watch mode. Vite's CSS hot replacement client code currently does not support searching and replacing `<link>` elements inside shadow roots. When support is available within Vite, an HMR based update for ShadowDOM components can be supported as other view encapsulation modes are now.
1 parent 416c9aa commit c787662

File tree

2 files changed

+51
-30
lines changed

2 files changed

+51
-30
lines changed

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

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ export async function* serveWithVite(
404404
key: 'r',
405405
description: 'force reload browser',
406406
action(server) {
407+
usedComponentStyles.clear();
407408
server.ws.send({
408409
type: 'full-reload',
409410
path: '*',
@@ -431,7 +432,7 @@ async function handleUpdate(
431432
server: ViteDevServer,
432433
serverOptions: NormalizedDevServerOptions,
433434
logger: BuilderContext['logger'],
434-
usedComponentStyles: Map<string, Set<string>>,
435+
usedComponentStyles: Map<string, Set<string | boolean>>,
435436
): Promise<void> {
436437
const updatedFiles: string[] = [];
437438
let destroyAngularServerAppCalled = false;
@@ -467,42 +468,57 @@ async function handleUpdate(
467468

468469
if (serverOptions.liveReload || serverOptions.hmr) {
469470
if (updatedFiles.every((f) => f.endsWith('.css'))) {
471+
let requiresReload = false;
470472
const timestamp = Date.now();
471-
server.ws.send({
472-
type: 'update',
473-
updates: updatedFiles.flatMap((filePath) => {
474-
// For component styles, an HMR update must be sent for each one with the corresponding
475-
// component identifier search parameter (`ngcomp`). The Vite client code will not keep
476-
// the existing search parameters when it performs an update and each one must be
477-
// specified explicitly. Typically, there is only one each though as specific style files
478-
// are not typically reused across components.
479-
const componentIds = usedComponentStyles.get(filePath);
480-
if (componentIds) {
481-
return Array.from(componentIds).map((id) => ({
482-
type: 'css-update',
473+
const updates = updatedFiles.flatMap((filePath) => {
474+
// For component styles, an HMR update must be sent for each one with the corresponding
475+
// component identifier search parameter (`ngcomp`). The Vite client code will not keep
476+
// the existing search parameters when it performs an update and each one must be
477+
// specified explicitly. Typically, there is only one each though as specific style files
478+
// are not typically reused across components.
479+
const componentIds = usedComponentStyles.get(filePath);
480+
if (componentIds) {
481+
return Array.from(componentIds).map((id) => {
482+
if (id === true) {
483+
// Shadow DOM components currently require a full reload.
484+
// Vite's CSS hot replacement does not support shadow root searching.
485+
requiresReload = true;
486+
}
487+
488+
return {
489+
type: 'css-update' as const,
483490
timestamp,
484-
path: `${filePath}?ngcomp` + (id ? `=${id}` : ''),
491+
path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''),
485492
acceptedPath: filePath,
486-
}));
487-
}
493+
};
494+
});
495+
}
488496

489-
return {
490-
type: 'css-update' as const,
491-
timestamp,
492-
path: filePath,
493-
acceptedPath: filePath,
494-
};
495-
}),
497+
return {
498+
type: 'css-update' as const,
499+
timestamp,
500+
path: filePath,
501+
acceptedPath: filePath,
502+
};
496503
});
497504

498-
logger.info('HMR update sent to client(s).');
505+
if (!requiresReload) {
506+
server.ws.send({
507+
type: 'update',
508+
updates,
509+
});
510+
logger.info('HMR update sent to client(s).');
499511

500-
return;
512+
return;
513+
}
501514
}
502515
}
503516

504517
// Send reload command to clients
505518
if (serverOptions.liveReload) {
519+
// Clear used component tracking on full reload
520+
usedComponentStyles.clear();
521+
506522
server.ws.send({
507523
type: 'full-reload',
508524
path: '*',

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function createAngularAssetsMiddleware(
1515
server: ViteDevServer,
1616
assets: Map<string, string>,
1717
outputFiles: AngularMemoryOutputFiles,
18-
usedComponentStyles: Map<string, Set<string>>,
18+
usedComponentStyles: Map<string, Set<string | boolean>>,
1919
encapsulateStyle: (style: Uint8Array, componentId: string) => string,
2020
): Connect.NextHandleFunction {
2121
return function angularAssetsMiddleware(req, res, next) {
@@ -76,14 +76,19 @@ export function createAngularAssetsMiddleware(
7676
let data: Uint8Array | string = outputFile.contents;
7777
if (extension === '.css') {
7878
// Inject component ID for view encapsulation if requested
79-
const componentId = new URL(req.url, 'http://localhost').searchParams.get('ngcomp');
79+
const searchParams = new URL(req.url, 'http://localhost').searchParams;
80+
const componentId = searchParams.get('ngcomp');
8081
if (componentId !== null) {
81-
// Record the component style usage for HMR updates
82+
// Track if the component uses ShadowDOM encapsulation (3 = ViewEncapsulation.ShadowDom)
83+
const shadow = searchParams.get('e') === '3';
84+
85+
// Record the component style usage for HMR updates (true = shadow; false = none; string = emulated)
8286
const usedIds = usedComponentStyles.get(pathname);
87+
const trackingId = componentId || shadow;
8388
if (usedIds === undefined) {
84-
usedComponentStyles.set(pathname, new Set([componentId]));
89+
usedComponentStyles.set(pathname, new Set([trackingId]));
8590
} else {
86-
usedIds.add(componentId);
91+
usedIds.add(trackingId);
8792
}
8893

8994
// Report if there are no changes to avoid reprocessing

0 commit comments

Comments
 (0)