diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index ed204bc0a9d6..aa3978f7197b 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -48,11 +48,11 @@ "lmdb": "3.1.3" }, "peerDependencies": { - "@angular/compiler": "^19.0.0-next.0", - "@angular/compiler-cli": "^19.0.0-next.0", - "@angular/localize": "^19.0.0-next.0", - "@angular/platform-server": "^19.0.0-next.0", - "@angular/service-worker": "^19.0.0-next.0", + "@angular/compiler": "^19.0.0-next.9", + "@angular/compiler-cli": "^19.0.0-next.9", + "@angular/localize": "^19.0.0-next.9", + "@angular/platform-server": "^19.0.0-next.9", + "@angular/service-worker": "^19.0.0-next.9", "@angular/ssr": "^0.0.0-PLACEHOLDER", "less": "^4.2.0", "postcss": "^8.4.0", 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 d65ca93ff450..7a4acb7a00d1 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -137,8 +137,13 @@ export async function* serveWithVite( process.setSourceMapsEnabled(true); } - // TODO: Enable by default once full support across CLI and FW is integrated - browserOptions.externalRuntimeStyles = useComponentStyleHmr; + // Enable to support component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable) + browserOptions.externalRuntimeStyles = !!serverOptions.liveReload && useComponentStyleHmr; + if (browserOptions.externalRuntimeStyles) { + // Preload the @angular/compiler package to avoid first stylesheet request delays. + // Once @angular/build is native ESM, this should be re-evaluated. + void loadEsmModule('@angular/compiler'); + } // Setup the prebundling transformer that will be shared across Vite prebundling requests const prebundleTransformer = new JavaScriptTransformer( @@ -166,7 +171,7 @@ export async function* serveWithVite( explicitBrowser: [], explicitServer: [], }; - const usedComponentStyles = new Map(); + const usedComponentStyles = new Map>(); const templateUpdates = new Map(); // Add cleanup logic via a builder teardown. @@ -423,7 +428,7 @@ async function handleUpdate( server: ViteDevServer, serverOptions: NormalizedDevServerOptions, logger: BuilderContext['logger'], - usedComponentStyles: Map, + usedComponentStyles: Map>, ): Promise { const updatedFiles: string[] = []; let destroyAngularServerAppCalled = false; @@ -470,7 +475,7 @@ async function handleUpdate( // are not typically reused across components. const componentIds = usedComponentStyles.get(filePath); if (componentIds) { - return componentIds.map((id) => ({ + return Array.from(componentIds).map((id) => ({ type: 'css-update', timestamp, path: `${filePath}?ngcomp` + (id ? `=${id}` : ''), @@ -582,7 +587,7 @@ export async function setupServer( prebundleTransformer: JavaScriptTransformer, target: string[], zoneless: boolean, - usedComponentStyles: Map, + usedComponentStyles: Map>, templateUpdates: Map, prebundleLoaderExtensions: EsbuildLoaderOption | undefined, extensionMiddleware?: Connect.NextHandleFunction[], diff --git a/packages/angular/build/src/tools/angular/angular-host.ts b/packages/angular/build/src/tools/angular/angular-host.ts index cb7616424f03..a446e525af1d 100644 --- a/packages/angular/build/src/tools/angular/angular-host.ts +++ b/packages/angular/build/src/tools/angular/angular-host.ts @@ -25,6 +25,7 @@ export interface AngularHostOptions { containingFile: string, stylesheetFile?: string, order?: number, + className?: string, ): Promise; processWebWorker(workerFile: string, containingFile: string): string; } @@ -197,9 +198,8 @@ export function createAngularCompilerHost( data, context.containingFile, context.resourceFile ?? undefined, - // TODO: Remove once available in compiler-cli types - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (context as any).order, + context.order, + context.className, ); return typeof result === 'string' ? { content: result } : null; diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts index 817c4081ee21..f3b3503f6988 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -51,12 +51,15 @@ export class ParallelCompilation extends AngularCompilation { }> { const stylesheetChannel = new MessageChannel(); // The request identifier is required because Angular can issue multiple concurrent requests - stylesheetChannel.port1.on('message', ({ requestId, data, containingFile, stylesheetFile }) => { - hostOptions - .transformStylesheet(data, containingFile, stylesheetFile) - .then((value) => stylesheetChannel.port1.postMessage({ requestId, value })) - .catch((error) => stylesheetChannel.port1.postMessage({ requestId, error })); - }); + stylesheetChannel.port1.on( + 'message', + ({ requestId, data, containingFile, stylesheetFile, order, className }) => { + hostOptions + .transformStylesheet(data, containingFile, stylesheetFile, order, className) + .then((value) => stylesheetChannel.port1.postMessage({ requestId, value })) + .catch((error) => stylesheetChannel.port1.postMessage({ requestId, error })); + }, + ); // The web worker processing is a synchronous operation and uses shared memory combined with // the Atomics API to block execution here until a response is received. diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts index 38014bc670f9..a9d1816ac76d 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts @@ -48,7 +48,7 @@ export async function initialize(request: InitRequest) { fileReplacements: request.fileReplacements, sourceFileCache, modifiedFiles: sourceFileCache.modifiedFiles, - transformStylesheet(data, containingFile, stylesheetFile) { + transformStylesheet(data, containingFile, stylesheetFile, order, className) { const requestId = randomUUID(); const resultPromise = new Promise((resolve, reject) => stylesheetRequests.set(requestId, [resolve, reject]), @@ -59,6 +59,8 @@ export async function initialize(request: InitRequest) { data, containingFile, stylesheetFile, + order, + className, }); return resultPromise; diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index f4ff8a5de1d9..541b0e073e35 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -182,7 +182,7 @@ export function createCompilerPlugin( fileReplacements: pluginOptions.fileReplacements, modifiedFiles, sourceFileCache: pluginOptions.sourceFileCache, - async transformStylesheet(data, containingFile, stylesheetFile, order) { + async transformStylesheet(data, containingFile, stylesheetFile, order, className) { let stylesheetResult; // Stylesheet file only exists for external stylesheets @@ -202,6 +202,7 @@ export function createCompilerPlugin( ? createHash('sha-256') .update(containingFile) .update((order ?? 0).toString()) + .update(className ?? '') .digest('hex') : undefined, ); diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts index 11c2c64b26e5..21ea46ff55d1 100644 --- a/packages/angular/build/src/tools/esbuild/utils.ts +++ b/packages/angular/build/src/tools/esbuild/utils.ts @@ -43,6 +43,7 @@ export function logBuildStats( const browserStats: BundleStats[] = []; const serverStats: BundleStats[] = []; let unchangedCount = 0; + let componentStyleChange = false; for (const { path: file, size, type } of outputFiles) { // Only display JavaScript and CSS files @@ -63,6 +64,12 @@ export function logBuildStats( continue; } + // Skip logging external component stylesheets used for HMR + if (metafile.outputs[file] && 'ng-component' in metafile.outputs[file]) { + componentStyleChange = true; + continue; + } + const name = initial.get(file)?.name ?? getChunkNameFromMetafile(metafile, file); const stat: BundleStats = { initial: initial.has(file), @@ -88,7 +95,11 @@ export function logBuildStats( return tableText + '\n'; } else if (changedFiles !== undefined) { - return '\nNo output file changes.\n'; + if (componentStyleChange) { + return '\nComponent stylesheet(s) changed.\n'; + } else { + return '\nNo output file changes.\n'; + } } if (unchangedCount > 0) { return `Unchanged output files: ${unchangedCount}`; 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 1e3962049a18..e5d04887aa44 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -16,7 +16,7 @@ export function createAngularAssetsMiddleware( server: ViteDevServer, assets: Map, outputFiles: AngularMemoryOutputFiles, - usedComponentStyles: Map, + usedComponentStyles: Map>, ): Connect.NextHandleFunction { return function angularAssetsMiddleware(req, res, next) { if (req.url === undefined || res.writableEnded) { @@ -81,9 +81,9 @@ export function createAngularAssetsMiddleware( // Record the component style usage for HMR updates const usedIds = usedComponentStyles.get(pathname); if (usedIds === undefined) { - usedComponentStyles.set(pathname, [componentId]); + usedComponentStyles.set(pathname, new Set([componentId])); } else { - usedIds.push(componentId); + usedIds.add(componentId); } // Shim the stylesheet if a component ID is provided if (componentId.length > 0) { 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 81459aff4312..0b3c998886e2 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 @@ -48,7 +48,7 @@ interface AngularSetupMiddlewaresPluginOptions { assets: Map; extensionMiddleware?: Connect.NextHandleFunction[]; indexHtmlTransformer?: (content: string) => Promise; - usedComponentStyles: Map; + usedComponentStyles: Map>; templateUpdates: Map; ssrMode: ServerSsrMode; } diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index 8412b8215fed..73e403b7f171 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -103,7 +103,7 @@ export const shouldOptimizeChunks = const hmrComponentStylesVariable = process.env['NG_HMR_CSTYLES']; export const useComponentStyleHmr = - isPresent(hmrComponentStylesVariable) && isEnabled(hmrComponentStylesVariable); + !isPresent(hmrComponentStylesVariable) || !isDisabled(hmrComponentStylesVariable); const partialSsrBuildVariable = process.env['NG_BUILD_PARTIAL_SSR']; export const usePartialSsrBuild = diff --git a/tests/legacy-cli/e2e/tests/basic/rebuild.ts b/tests/legacy-cli/e2e/tests/basic/rebuild.ts index ce90caefb7ba..787fe1acfb19 100644 --- a/tests/legacy-cli/e2e/tests/basic/rebuild.ts +++ b/tests/legacy-cli/e2e/tests/basic/rebuild.ts @@ -9,7 +9,14 @@ export default async function () { const validBundleRegEx = esbuild ? /complete\./ : /Compiled successfully\./; const lazyBundleRegEx = esbuild ? /chunk-/ : /src_app_lazy_lazy_component_ts\.js/; + // Disable component stylesheet HMR to support page reload based rebuild testing. + // Ideally this environment variable would be passed directly to the new serve process + // but this would require signficant test changes due to the existing `ngServe` signature. + const oldHMRValue = process.env['NG_HMR_CSTYLES']; + process.env['NG_HMR_CSTYLES'] = '0'; const port = await ngServe(); + process.env['NG_HMR_CSTYLES'] = oldHMRValue; + // Add a lazy route. await silentNg('generate', 'component', 'lazy'); diff --git a/yarn.lock b/yarn.lock index 9825cd936fbf..e6918c9cb077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -401,11 +401,11 @@ __metadata: vite: "npm:5.4.9" watchpack: "npm:2.4.2" peerDependencies: - "@angular/compiler": ^19.0.0-next.0 - "@angular/compiler-cli": ^19.0.0-next.0 - "@angular/localize": ^19.0.0-next.0 - "@angular/platform-server": ^19.0.0-next.0 - "@angular/service-worker": ^19.0.0-next.0 + "@angular/compiler": ^19.0.0-next.9 + "@angular/compiler-cli": ^19.0.0-next.9 + "@angular/localize": ^19.0.0-next.9 + "@angular/platform-server": ^19.0.0-next.9 + "@angular/service-worker": ^19.0.0-next.9 "@angular/ssr": ^0.0.0-PLACEHOLDER less: ^4.2.0 postcss: ^8.4.0