Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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: '*',
Expand Down Expand Up @@ -431,7 +432,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 @@ -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: '*',
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]));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be clearer if we structure the data differently rather than using true/false/string, as a object to make it easier to follow:

{
    encapsulation?: string;
    componentId?: string;
}

I can foresee that, in the future, this can be a bit confusing whlist debugging that as component ids we store booleans

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping we can revert this workaround sooner rather than later. Post v19 i may also look into patching the Vite client code to add support for shadow DOM with a potential followup Vite PR.

} else {
usedIds.add(componentId);
usedIds.add(trackingId);
}

// Report if there are no changes to avoid reprocessing
Expand Down