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 f8d0dfe8fba9..dfbbc8454b53 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -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, @@ -175,7 +176,7 @@ export async function* serveWithVite( explicitBrowser: [], explicitServer: [], }; - const usedComponentStyles = new Map>(); + const componentStyles = new Map(); const templateUpdates = new Map(); // Add cleanup logic via a builder teardown. @@ -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.'); @@ -321,7 +328,7 @@ export async function* serveWithVite( server, serverOptions, context.logger, - usedComponentStyles, + componentStyles, ); } } else { @@ -380,7 +387,7 @@ export async function* serveWithVite( prebundleTransformer, target, isZonelessApp(polyfills), - usedComponentStyles, + componentStyles, templateUpdates, browserOptions.loader as EsbuildLoaderOption | undefined, extensions?.middleware, @@ -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: '*', @@ -434,7 +441,7 @@ async function handleUpdate( server: ViteDevServer, serverOptions: NormalizedDevServerOptions, logger: BuilderContext['logger'], - usedComponentStyles: Map>, + componentStyles: Map, ): Promise { const updatedFiles: string[] = []; let destroyAngularServerAppCalled = false; @@ -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, @@ -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', @@ -535,6 +544,7 @@ function analyzeResultFiles( htmlIndexPath: string, resultFiles: Record, generatedFiles: Map, + componentStyles: Map, ) { const seen = new Set(['/index.html']); for (const [outputPath, file] of Object.entries(resultFiles)) { @@ -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); } } } @@ -609,7 +632,7 @@ export async function setupServer( prebundleTransformer: JavaScriptTransformer, target: string[], zoneless: boolean, - usedComponentStyles: Map>, + componentStyles: Map, templateUpdates: Map, prebundleLoaderExtensions: EsbuildLoaderOption | undefined, extensionMiddleware?: Connect.NextHandleFunction[], @@ -719,7 +742,7 @@ export async function setupServer( assets, indexHtmlTransformer, extensionMiddleware, - usedComponentStyles, + componentStyles, templateUpdates, ssrMode, }), 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 76a6f6c5359f..b712744e8f33 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -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; + reload?: boolean; +} + export function createAngularAssetsMiddleware( server: ViteDevServer, assets: Map, outputFiles: AngularMemoryOutputFiles, - usedComponentStyles: Map>, + componentStyles: Map, encapsulateStyle: (style: Uint8Array, componentId: string) => string, ): Connect.NextHandleFunction { return function angularAssetsMiddleware(req, res, next) { @@ -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 diff --git a/packages/angular/build/src/tools/vite/middlewares/index.ts b/packages/angular/build/src/tools/vite/middlewares/index.ts index fb5416c07e7e..2981e9912081 100644 --- a/packages/angular/build/src/tools/vite/middlewares/index.ts +++ b/packages/angular/build/src/tools/vite/middlewares/index.ts @@ -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 { diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index 07983c011878..1118cf3ac797 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -9,6 +9,7 @@ import type { Connect, Plugin } from 'vite'; import { loadEsmModule } from '../../../utils/load-esm'; import { + ComponentStyleRecord, angularHtmlFallbackMiddleware, createAngularAssetsMiddleware, createAngularComponentMiddleware, @@ -49,7 +50,7 @@ interface AngularSetupMiddlewaresPluginOptions { assets: Map; extensionMiddleware?: Connect.NextHandleFunction[]; indexHtmlTransformer?: (content: string) => Promise; - usedComponentStyles: Map>; + componentStyles: Map; templateUpdates: Map; ssrMode: ServerSsrMode; } @@ -78,7 +79,7 @@ export function createAngularSetupMiddlewaresPlugin( outputFiles, extensionMiddleware, assets, - usedComponentStyles, + componentStyles, templateUpdates, ssrMode, } = options; @@ -91,7 +92,7 @@ export function createAngularSetupMiddlewaresPlugin( server, assets, outputFiles, - usedComponentStyles, + componentStyles, await createEncapsulateStyle(), ), );