From c7876627f9862157b23ce0e32f7065f8761a14df Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Mon, 4 Nov 2024 11:29:51 -0500
Subject: [PATCH] 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 ``
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.
---
.../src/builders/dev-server/vite-server.ts | 66 ++++++++++++-------
.../vite/middlewares/assets-middleware.ts | 15 +++--
2 files changed, 51 insertions(+), 30 deletions(-)
diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts
index 3223a9280fc2..ab3e16935f08 100644
--- a/packages/angular/build/src/builders/dev-server/vite-server.ts
+++ b/packages/angular/build/src/builders/dev-server/vite-server.ts
@@ -404,6 +404,7 @@ export async function* serveWithVite(
key: 'r',
description: 'force reload browser',
action(server) {
+ usedComponentStyles.clear();
server.ws.send({
type: 'full-reload',
path: '*',
@@ -431,7 +432,7 @@ async function handleUpdate(
server: ViteDevServer,
serverOptions: NormalizedDevServerOptions,
logger: BuilderContext['logger'],
- usedComponentStyles: Map>,
+ usedComponentStyles: Map>,
): Promise {
const updatedFiles: string[] = [];
let destroyAngularServerAppCalled = false;
@@ -467,42 +468,57 @@ async function handleUpdate(
if (serverOptions.liveReload || serverOptions.hmr) {
if (updatedFiles.every((f) => f.endsWith('.css'))) {
+ let requiresReload = false;
const timestamp = Date.now();
- server.ws.send({
- type: 'update',
- updates: updatedFiles.flatMap((filePath) => {
- // For component styles, an HMR update must be sent for each one with the corresponding
- // component identifier search parameter (`ngcomp`). The Vite client code will not keep
- // the existing search parameters when it performs an update and each one must be
- // specified explicitly. Typically, there is only one each though as specific style files
- // are not typically reused across components.
- const componentIds = usedComponentStyles.get(filePath);
- if (componentIds) {
- return Array.from(componentIds).map((id) => ({
- type: 'css-update',
+ const updates = updatedFiles.flatMap((filePath) => {
+ // For component styles, an HMR update must be sent for each one with the corresponding
+ // component identifier search parameter (`ngcomp`). The Vite client code will not keep
+ // the existing search parameters when it performs an update and each one must be
+ // specified explicitly. Typically, there is only one each though as specific style files
+ // are not typically reused across components.
+ const componentIds = usedComponentStyles.get(filePath);
+ if (componentIds) {
+ return Array.from(componentIds).map((id) => {
+ if (id === true) {
+ // Shadow DOM components currently require a full reload.
+ // Vite's CSS hot replacement does not support shadow root searching.
+ requiresReload = true;
+ }
+
+ return {
+ type: 'css-update' as const,
timestamp,
- path: `${filePath}?ngcomp` + (id ? `=${id}` : ''),
+ path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''),
acceptedPath: filePath,
- }));
- }
+ };
+ });
+ }
- return {
- type: 'css-update' as const,
- timestamp,
- path: filePath,
- acceptedPath: filePath,
- };
- }),
+ return {
+ type: 'css-update' as const,
+ timestamp,
+ path: filePath,
+ acceptedPath: filePath,
+ };
});
- logger.info('HMR update sent to client(s).');
+ if (!requiresReload) {
+ server.ws.send({
+ type: 'update',
+ updates,
+ });
+ logger.info('HMR update sent to client(s).');
- return;
+ return;
+ }
}
}
// Send reload command to clients
if (serverOptions.liveReload) {
+ // Clear used component tracking on full reload
+ usedComponentStyles.clear();
+
server.ws.send({
type: 'full-reload',
path: '*',
diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts
index aefb18df229b..76a6f6c5359f 100644
--- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts
+++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts
@@ -15,7 +15,7 @@ export function createAngularAssetsMiddleware(
server: ViteDevServer,
assets: Map,
outputFiles: AngularMemoryOutputFiles,
- usedComponentStyles: Map>,
+ usedComponentStyles: Map>,
encapsulateStyle: (style: Uint8Array, componentId: string) => string,
): Connect.NextHandleFunction {
return function angularAssetsMiddleware(req, res, next) {
@@ -76,14 +76,19 @@ export function createAngularAssetsMiddleware(
let data: Uint8Array | string = outputFile.contents;
if (extension === '.css') {
// Inject component ID for view encapsulation if requested
- const componentId = new URL(req.url, 'http://localhost').searchParams.get('ngcomp');
+ const searchParams = new URL(req.url, 'http://localhost').searchParams;
+ const componentId = searchParams.get('ngcomp');
if (componentId !== null) {
- // Record the component style usage for HMR updates
+ // Track if the component uses ShadowDOM encapsulation (3 = ViewEncapsulation.ShadowDom)
+ const shadow = searchParams.get('e') === '3';
+
+ // Record the component style usage for HMR updates (true = shadow; false = none; string = emulated)
const usedIds = usedComponentStyles.get(pathname);
+ const trackingId = componentId || shadow;
if (usedIds === undefined) {
- usedComponentStyles.set(pathname, new Set([componentId]));
+ usedComponentStyles.set(pathname, new Set([trackingId]));
} else {
- usedIds.add(componentId);
+ usedIds.add(trackingId);
}
// Report if there are no changes to avoid reprocessing