Skip to content

Commit

Permalink
refactor(@angular/build): use structured component stylesheet trackin…
Browse files Browse the repository at this point in the history
…g 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)
  • Loading branch information
clydin authored and alan-agius4 committed Nov 6, 2024
1 parent e04b891 commit 1765294
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 31 deletions.
59 changes: 41 additions & 18 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { readFile } from 'node:fs/promises';
import { builtinModules, isBuiltin } from 'node:module';
import { join } from 'node:path';
import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from 'vite';
import type { ComponentStyleRecord } from '../../tools/vite/middlewares';
import {
ServerSsrMode,
createAngularLocaleDataPlugin,
Expand Down Expand Up @@ -175,7 +176,7 @@ export async function* serveWithVite(
explicitBrowser: [],
explicitServer: [],
};
const usedComponentStyles = new Map<string, Set<string>>();
const componentStyles = new Map<string, ComponentStyleRecord>();
const templateUpdates = new Map<string, string>();

// Add cleanup logic via a builder teardown.
Expand Down Expand Up @@ -232,11 +233,17 @@ export async function* serveWithVite(
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
}
}
// Clear stale template updates on a code rebuilds
// Clear stale template updates on code rebuilds
templateUpdates.clear();

// Analyze result files for changes
analyzeResultFiles(normalizePath, htmlIndexPath, result.files, generatedFiles);
analyzeResultFiles(
normalizePath,
htmlIndexPath,
result.files,
generatedFiles,
componentStyles,
);
break;
case ResultKind.Incremental:
assert(server, 'Builder must provide an initial full build before incremental results.');
Expand Down Expand Up @@ -321,7 +328,7 @@ export async function* serveWithVite(
server,
serverOptions,
context.logger,
usedComponentStyles,
componentStyles,
);
}
} else {
Expand Down Expand Up @@ -380,7 +387,7 @@ export async function* serveWithVite(
prebundleTransformer,
target,
isZonelessApp(polyfills),
usedComponentStyles,
componentStyles,
templateUpdates,
browserOptions.loader as EsbuildLoaderOption | undefined,
extensions?.middleware,
Expand All @@ -406,7 +413,7 @@ export async function* serveWithVite(
key: 'r',
description: 'force reload browser',
action(server) {
usedComponentStyles.clear();
componentStyles.forEach((record) => record.used?.clear());
server.ws.send({
type: 'full-reload',
path: '*',
Expand Down Expand Up @@ -434,7 +441,7 @@ async function handleUpdate(
server: ViteDevServer,
serverOptions: NormalizedDevServerOptions,
logger: BuilderContext['logger'],
usedComponentStyles: Map<string, Set<string | boolean>>,
componentStyles: Map<string, ComponentStyleRecord>,
): Promise<void> {
const updatedFiles: string[] = [];
let destroyAngularServerAppCalled = false;
Expand Down Expand Up @@ -478,15 +485,17 @@ async function handleUpdate(
// 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;
}
const record = componentStyles.get(filePath);
if (record) {
if (record.reload) {
// Shadow DOM components currently require a full reload.
// Vite's CSS hot replacement does not support shadow root searching.
requiresReload = true;

return [];
}

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

server.ws.send({
type: 'full-reload',
Expand All @@ -535,6 +544,7 @@ function analyzeResultFiles(
htmlIndexPath: string,
resultFiles: Record<string, ResultFile>,
generatedFiles: Map<string, OutputFileRecord>,
componentStyles: Map<string, ComponentStyleRecord>,
) {
const seen = new Set<string>(['/index.html']);
for (const [outputPath, file] of Object.entries(resultFiles)) {
Expand Down Expand Up @@ -589,12 +599,25 @@ function analyzeResultFiles(
type: file.type,
servable,
});

// Record any external component styles
if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) {
const componentStyle = componentStyles.get(filePath);
if (componentStyle) {
componentStyle.rawContent = file.contents;
} else {
componentStyles.set(filePath, {
rawContent: file.contents,
});
}
}
}

// Clear stale output files
for (const file of generatedFiles.keys()) {
if (!seen.has(file)) {
generatedFiles.delete(file);
componentStyles.delete(file);
}
}
}
Expand All @@ -609,7 +632,7 @@ export async function setupServer(
prebundleTransformer: JavaScriptTransformer,
target: string[],
zoneless: boolean,
usedComponentStyles: Map<string, Set<string>>,
componentStyles: Map<string, ComponentStyleRecord>,
templateUpdates: Map<string, string>,
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
extensionMiddleware?: Connect.NextHandleFunction[],
Expand Down Expand Up @@ -719,7 +742,7 @@ export async function setupServer(
assets,
indexHtmlTransformer,
extensionMiddleware,
usedComponentStyles,
componentStyles,
templateUpdates,
ssrMode,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import { extname } from 'node:path';
import type { Connect, ViteDevServer } from 'vite';
import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils';

export interface ComponentStyleRecord {
rawContent: Uint8Array;
used?: Set<string>;
reload?: boolean;
}

export function createAngularAssetsMiddleware(
server: ViteDevServer,
assets: Map<string, string>,
outputFiles: AngularMemoryOutputFiles,
usedComponentStyles: Map<string, Set<string | boolean>>,
componentStyles: Map<string, ComponentStyleRecord>,
encapsulateStyle: (style: Uint8Array, componentId: string) => string,
): Connect.NextHandleFunction {
return function angularAssetsMiddleware(req, res, next) {
Expand Down Expand Up @@ -74,21 +80,24 @@ export function createAngularAssetsMiddleware(
const outputFile = outputFiles.get(pathname);
if (outputFile?.servable) {
let data: Uint8Array | string = outputFile.contents;
if (extension === '.css') {
const componentStyle = componentStyles.get(pathname);
if (componentStyle) {
// Inject component ID for view encapsulation if requested
const searchParams = new URL(req.url, 'http://localhost').searchParams;
const componentId = searchParams.get('ngcomp');
if (componentId !== null) {
// Track if the component uses ShadowDOM encapsulation (3 = ViewEncapsulation.ShadowDom)
const shadow = searchParams.get('e') === '3';
// Shadow DOM components currently require a full reload.
// Vite's CSS hot replacement does not support shadow root searching.
if (searchParams.get('e') === '3') {
componentStyle.reload = true;
}

// 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([trackingId]));
// Record the component style usage for HMR updates
if (componentStyle.used === undefined) {
componentStyle.used = new Set([componentId]);
} else {
usedIds.add(trackingId);
componentStyle.used.add(componentId);
}

// Report if there are no changes to avoid reprocessing
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/tools/vite/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

export { createAngularAssetsMiddleware } from './assets-middleware';
export { type ComponentStyleRecord, createAngularAssetsMiddleware } from './assets-middleware';
export { angularHtmlFallbackMiddleware } from './html-fallback-middleware';
export { createAngularIndexHtmlMiddleware } from './index-html-middleware';
export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { Connect, Plugin } from 'vite';
import { loadEsmModule } from '../../../utils/load-esm';
import {
ComponentStyleRecord,
angularHtmlFallbackMiddleware,
createAngularAssetsMiddleware,
createAngularComponentMiddleware,
Expand Down Expand Up @@ -49,7 +50,7 @@ interface AngularSetupMiddlewaresPluginOptions {
assets: Map<string, string>;
extensionMiddleware?: Connect.NextHandleFunction[];
indexHtmlTransformer?: (content: string) => Promise<string>;
usedComponentStyles: Map<string, Set<string>>;
componentStyles: Map<string, ComponentStyleRecord>;
templateUpdates: Map<string, string>;
ssrMode: ServerSsrMode;
}
Expand Down Expand Up @@ -78,7 +79,7 @@ export function createAngularSetupMiddlewaresPlugin(
outputFiles,
extensionMiddleware,
assets,
usedComponentStyles,
componentStyles,
templateUpdates,
ssrMode,
} = options;
Expand All @@ -91,7 +92,7 @@ export function createAngularSetupMiddlewaresPlugin(
server,
assets,
outputFiles,
usedComponentStyles,
componentStyles,
await createEncapsulateStyle(),
),
);
Expand Down

0 comments on commit 1765294

Please sign in to comment.