Skip to content

Commit c8e1521

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. (cherry picked from commit e6ff801)
1 parent c41529c commit c8e1521

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)