Skip to content

Commit

Permalink
fix(@angular/build): workaround Vite CSS ShadowDOM hot replacement
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
clydin authored and alan-agius4 committed Nov 5, 2024
1 parent a1fa483 commit e6ff801
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 30 deletions.
66 changes: 41 additions & 25 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ export async function* serveWithVite(
key: 'r',
description: 'force reload browser',
action(server) {
usedComponentStyles.clear();
server.ws.send({
type: 'full-reload',
path: '*',
Expand Down Expand Up @@ -433,7 +434,7 @@ async function handleUpdate(
server: ViteDevServer,
serverOptions: NormalizedDevServerOptions,
logger: BuilderContext['logger'],
usedComponentStyles: Map<string, Set<string>>,
usedComponentStyles: Map<string, Set<string | boolean>>,
): Promise<void> {
const updatedFiles: string[] = [];
let destroyAngularServerAppCalled = false;
Expand Down Expand Up @@ -469,42 +470,57 @@ async function handleUpdate(

if (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: '*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function createAngularAssetsMiddleware(
server: ViteDevServer,
assets: Map<string, string>,
outputFiles: AngularMemoryOutputFiles,
usedComponentStyles: Map<string, Set<string>>,
usedComponentStyles: Map<string, Set<string | boolean>>,
encapsulateStyle: (style: Uint8Array, componentId: string) => string,
): Connect.NextHandleFunction {
return function angularAssetsMiddleware(req, res, next) {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit e6ff801

Please sign in to comment.