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