Skip to content

Commit 1765294

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular/build): use structured component stylesheet tracking for hot replacement
When using the development server with the application builder, the internal state of any external component stylesheets is now more comprehensively tracked. This allows for more flexibility in both debugging potential problems as well as supporting additional stylesheet preprocessing steps including deferred component stylesheet processing. (cherry picked from commit a2f5ca9)
1 parent e04b891 commit 1765294

File tree

4 files changed

+64
-31
lines changed

4 files changed

+64
-31
lines changed

packages/angular/build/src/builders/dev-server/vite-server.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { readFile } from 'node:fs/promises';
1414
import { builtinModules, isBuiltin } from 'node:module';
1515
import { join } from 'node:path';
1616
import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from 'vite';
17+
import type { ComponentStyleRecord } from '../../tools/vite/middlewares';
1718
import {
1819
ServerSsrMode,
1920
createAngularLocaleDataPlugin,
@@ -175,7 +176,7 @@ export async function* serveWithVite(
175176
explicitBrowser: [],
176177
explicitServer: [],
177178
};
178-
const usedComponentStyles = new Map<string, Set<string>>();
179+
const componentStyles = new Map<string, ComponentStyleRecord>();
179180
const templateUpdates = new Map<string, string>();
180181

181182
// Add cleanup logic via a builder teardown.
@@ -232,11 +233,17 @@ export async function* serveWithVite(
232233
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
233234
}
234235
}
235-
// Clear stale template updates on a code rebuilds
236+
// Clear stale template updates on code rebuilds
236237
templateUpdates.clear();
237238

238239
// Analyze result files for changes
239-
analyzeResultFiles(normalizePath, htmlIndexPath, result.files, generatedFiles);
240+
analyzeResultFiles(
241+
normalizePath,
242+
htmlIndexPath,
243+
result.files,
244+
generatedFiles,
245+
componentStyles,
246+
);
240247
break;
241248
case ResultKind.Incremental:
242249
assert(server, 'Builder must provide an initial full build before incremental results.');
@@ -321,7 +328,7 @@ export async function* serveWithVite(
321328
server,
322329
serverOptions,
323330
context.logger,
324-
usedComponentStyles,
331+
componentStyles,
325332
);
326333
}
327334
} else {
@@ -380,7 +387,7 @@ export async function* serveWithVite(
380387
prebundleTransformer,
381388
target,
382389
isZonelessApp(polyfills),
383-
usedComponentStyles,
390+
componentStyles,
384391
templateUpdates,
385392
browserOptions.loader as EsbuildLoaderOption | undefined,
386393
extensions?.middleware,
@@ -406,7 +413,7 @@ export async function* serveWithVite(
406413
key: 'r',
407414
description: 'force reload browser',
408415
action(server) {
409-
usedComponentStyles.clear();
416+
componentStyles.forEach((record) => record.used?.clear());
410417
server.ws.send({
411418
type: 'full-reload',
412419
path: '*',
@@ -434,7 +441,7 @@ async function handleUpdate(
434441
server: ViteDevServer,
435442
serverOptions: NormalizedDevServerOptions,
436443
logger: BuilderContext['logger'],
437-
usedComponentStyles: Map<string, Set<string | boolean>>,
444+
componentStyles: Map<string, ComponentStyleRecord>,
438445
): Promise<void> {
439446
const updatedFiles: string[] = [];
440447
let destroyAngularServerAppCalled = false;
@@ -478,15 +485,17 @@ async function handleUpdate(
478485
// the existing search parameters when it performs an update and each one must be
479486
// specified explicitly. Typically, there is only one each though as specific style files
480487
// 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-
}
488+
const record = componentStyles.get(filePath);
489+
if (record) {
490+
if (record.reload) {
491+
// Shadow DOM components currently require a full reload.
492+
// Vite's CSS hot replacement does not support shadow root searching.
493+
requiresReload = true;
494+
495+
return [];
496+
}
489497

498+
return Array.from(record.used ?? []).map((id) => {
490499
return {
491500
type: 'css-update' as const,
492501
timestamp,
@@ -519,7 +528,7 @@ async function handleUpdate(
519528
// Send reload command to clients
520529
if (serverOptions.liveReload) {
521530
// Clear used component tracking on full reload
522-
usedComponentStyles.clear();
531+
componentStyles.forEach((record) => record.used?.clear());
523532

524533
server.ws.send({
525534
type: 'full-reload',
@@ -535,6 +544,7 @@ function analyzeResultFiles(
535544
htmlIndexPath: string,
536545
resultFiles: Record<string, ResultFile>,
537546
generatedFiles: Map<string, OutputFileRecord>,
547+
componentStyles: Map<string, ComponentStyleRecord>,
538548
) {
539549
const seen = new Set<string>(['/index.html']);
540550
for (const [outputPath, file] of Object.entries(resultFiles)) {
@@ -589,12 +599,25 @@ function analyzeResultFiles(
589599
type: file.type,
590600
servable,
591601
});
602+
603+
// Record any external component styles
604+
if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) {
605+
const componentStyle = componentStyles.get(filePath);
606+
if (componentStyle) {
607+
componentStyle.rawContent = file.contents;
608+
} else {
609+
componentStyles.set(filePath, {
610+
rawContent: file.contents,
611+
});
612+
}
613+
}
592614
}
593615

594616
// Clear stale output files
595617
for (const file of generatedFiles.keys()) {
596618
if (!seen.has(file)) {
597619
generatedFiles.delete(file);
620+
componentStyles.delete(file);
598621
}
599622
}
600623
}
@@ -609,7 +632,7 @@ export async function setupServer(
609632
prebundleTransformer: JavaScriptTransformer,
610633
target: string[],
611634
zoneless: boolean,
612-
usedComponentStyles: Map<string, Set<string>>,
635+
componentStyles: Map<string, ComponentStyleRecord>,
613636
templateUpdates: Map<string, string>,
614637
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
615638
extensionMiddleware?: Connect.NextHandleFunction[],
@@ -719,7 +742,7 @@ export async function setupServer(
719742
assets,
720743
indexHtmlTransformer,
721744
extensionMiddleware,
722-
usedComponentStyles,
745+
componentStyles,
723746
templateUpdates,
724747
ssrMode,
725748
}),

packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@ import { extname } from 'node:path';
1111
import type { Connect, ViteDevServer } from 'vite';
1212
import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils';
1313

14+
export interface ComponentStyleRecord {
15+
rawContent: Uint8Array;
16+
used?: Set<string>;
17+
reload?: boolean;
18+
}
19+
1420
export function createAngularAssetsMiddleware(
1521
server: ViteDevServer,
1622
assets: Map<string, string>,
1723
outputFiles: AngularMemoryOutputFiles,
18-
usedComponentStyles: Map<string, Set<string | boolean>>,
24+
componentStyles: Map<string, ComponentStyleRecord>,
1925
encapsulateStyle: (style: Uint8Array, componentId: string) => string,
2026
): Connect.NextHandleFunction {
2127
return function angularAssetsMiddleware(req, res, next) {
@@ -74,21 +80,24 @@ export function createAngularAssetsMiddleware(
7480
const outputFile = outputFiles.get(pathname);
7581
if (outputFile?.servable) {
7682
let data: Uint8Array | string = outputFile.contents;
77-
if (extension === '.css') {
83+
const componentStyle = componentStyles.get(pathname);
84+
if (componentStyle) {
7885
// Inject component ID for view encapsulation if requested
7986
const searchParams = new URL(req.url, 'http://localhost').searchParams;
8087
const componentId = searchParams.get('ngcomp');
8188
if (componentId !== null) {
8289
// Track if the component uses ShadowDOM encapsulation (3 = ViewEncapsulation.ShadowDom)
83-
const shadow = searchParams.get('e') === '3';
90+
// Shadow DOM components currently require a full reload.
91+
// Vite's CSS hot replacement does not support shadow root searching.
92+
if (searchParams.get('e') === '3') {
93+
componentStyle.reload = true;
94+
}
8495

85-
// Record the component style usage for HMR updates (true = shadow; false = none; string = emulated)
86-
const usedIds = usedComponentStyles.get(pathname);
87-
const trackingId = componentId || shadow;
88-
if (usedIds === undefined) {
89-
usedComponentStyles.set(pathname, new Set([trackingId]));
96+
// Record the component style usage for HMR updates
97+
if (componentStyle.used === undefined) {
98+
componentStyle.used = new Set([componentId]);
9099
} else {
91-
usedIds.add(trackingId);
100+
componentStyle.used.add(componentId);
92101
}
93102

94103
// Report if there are no changes to avoid reprocessing

packages/angular/build/src/tools/vite/middlewares/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export { createAngularAssetsMiddleware } from './assets-middleware';
9+
export { type ComponentStyleRecord, createAngularAssetsMiddleware } from './assets-middleware';
1010
export { angularHtmlFallbackMiddleware } from './html-fallback-middleware';
1111
export { createAngularIndexHtmlMiddleware } from './index-html-middleware';
1212
export {

packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type { Connect, Plugin } from 'vite';
1010
import { loadEsmModule } from '../../../utils/load-esm';
1111
import {
12+
ComponentStyleRecord,
1213
angularHtmlFallbackMiddleware,
1314
createAngularAssetsMiddleware,
1415
createAngularComponentMiddleware,
@@ -49,7 +50,7 @@ interface AngularSetupMiddlewaresPluginOptions {
4950
assets: Map<string, string>;
5051
extensionMiddleware?: Connect.NextHandleFunction[];
5152
indexHtmlTransformer?: (content: string) => Promise<string>;
52-
usedComponentStyles: Map<string, Set<string>>;
53+
componentStyles: Map<string, ComponentStyleRecord>;
5354
templateUpdates: Map<string, string>;
5455
ssrMode: ServerSsrMode;
5556
}
@@ -78,7 +79,7 @@ export function createAngularSetupMiddlewaresPlugin(
7879
outputFiles,
7980
extensionMiddleware,
8081
assets,
81-
usedComponentStyles,
82+
componentStyles,
8283
templateUpdates,
8384
ssrMode,
8485
} = options;
@@ -91,7 +92,7 @@ export function createAngularSetupMiddlewaresPlugin(
9192
server,
9293
assets,
9394
outputFiles,
94-
usedComponentStyles,
95+
componentStyles,
9596
await createEncapsulateStyle(),
9697
),
9798
);

0 commit comments

Comments
 (0)