Skip to content

Commit e6ff801

Browse files
clydinalan-agius4
authored andcommitted
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 a1fa483 commit e6ff801

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

+41-25
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ export async function* serveWithVite(
406406
key: 'r',
407407
description: 'force reload browser',
408408
action(server) {
409+
usedComponentStyles.clear();
409410
server.ws.send({
410411
type: 'full-reload',
411412
path: '*',
@@ -433,7 +434,7 @@ async function handleUpdate(
433434
server: ViteDevServer,
434435
serverOptions: NormalizedDevServerOptions,
435436
logger: BuilderContext['logger'],
436-
usedComponentStyles: Map<string, Set<string>>,
437+
usedComponentStyles: Map<string, Set<string | boolean>>,
437438
): Promise<void> {
438439
const updatedFiles: string[] = [];
439440
let destroyAngularServerAppCalled = false;
@@ -469,42 +470,57 @@ async function handleUpdate(
469470

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

491-
return {
492-
type: 'css-update' as const,
493-
timestamp,
494-
path: filePath,
495-
acceptedPath: filePath,
496-
};
497-
}),
499+
return {
500+
type: 'css-update' as const,
501+
timestamp,
502+
path: filePath,
503+
acceptedPath: filePath,
504+
};
498505
});
499506

500-
logger.info('HMR update sent to client(s).');
507+
if (!requiresReload) {
508+
server.ws.send({
509+
type: 'update',
510+
updates,
511+
});
512+
logger.info('HMR update sent to client(s).');
501513

502-
return;
514+
return;
515+
}
503516
}
504517
}
505518

506519
// Send reload command to clients
507520
if (serverOptions.liveReload) {
521+
// Clear used component tracking on full reload
522+
usedComponentStyles.clear();
523+
508524
server.ws.send({
509525
type: 'full-reload',
510526
path: '*',

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

+10-5
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)