diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index f6fdeaf84dcc..c9089eed4ede 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -16,7 +16,7 @@ import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/ut import { shouldWatchRoot } from '../../utils/environment-options'; import { NormalizedCachedOptions } from '../../utils/normalize-cache'; import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; -import { FullResult, Result, ResultKind, ResultMessage } from './results'; +import { ComponentUpdateResult, FullResult, Result, ResultKind, ResultMessage } from './results'; // Watch workspace for package manager changes const packageWatchFiles = [ @@ -207,6 +207,7 @@ async function emitOutputResult( externalMetadata, htmlIndexPath, htmlBaseHref, + templateUpdates, }: ExecutionResult, outputOptions: NormalizedApplicationBuildOptions['outputOptions'], ): Promise { @@ -221,6 +222,20 @@ async function emitOutputResult( }; } + // Template updates only exist if no other changes have occurred + if (templateUpdates?.size) { + const updateResult: ComponentUpdateResult = { + kind: ResultKind.ComponentUpdate, + updates: Array.from(templateUpdates).map(([id, content]) => ({ + type: 'template', + id, + content, + })), + }; + + return updateResult; + } + const result: FullResult = { kind: ResultKind.Full, warnings: warnings as ResultMessage[], diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 690262004b73..b5ca83a76405 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -49,7 +49,6 @@ export async function executeBuild( i18nOptions, optimizationOptions, assets, - outputMode, cacheOptions, serverEntryPoint, baseHref, @@ -73,10 +72,15 @@ export async function executeBuild( let componentStyleBundler; let codeBundleCache; let bundlingResult: BundleContextResult; + let templateUpdates: Map | undefined; if (rebuildState) { bundlerContexts = rebuildState.rebuildContexts; componentStyleBundler = rebuildState.componentStyleBundler; codeBundleCache = rebuildState.codeBundleCache; + templateUpdates = rebuildState.templateUpdates; + // Reset template updates for new rebuild + templateUpdates?.clear(); + const allFileChanges = rebuildState.fileChanges.all; // Bundle all contexts that do not require TypeScript changed file checks. @@ -85,7 +89,6 @@ export async function executeBuild( // Check the TypeScript code bundling cache for changes. If invalid, force a rebundle of // all TypeScript related contexts. - // TODO: Enable cached bundling for the typescript contexts const forceTypeScriptRebuild = codeBundleCache?.invalidate(allFileChanges); const typescriptResults: BundleContextResult[] = []; for (const typescriptContext of bundlerContexts.typescriptContexts) { @@ -98,7 +101,16 @@ export async function executeBuild( const target = transformSupportedBrowsersToTargets(browsers); codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); componentStyleBundler = createComponentStyleBundler(options, target); - bundlerContexts = setupBundlerContexts(options, target, codeBundleCache, componentStyleBundler); + if (options.templateUpdates) { + templateUpdates = new Map(); + } + bundlerContexts = setupBundlerContexts( + options, + target, + codeBundleCache, + componentStyleBundler, + templateUpdates, + ); // Bundle everything on initial build bundlingResult = await BundlerContext.bundleAll([ @@ -129,6 +141,7 @@ export async function executeBuild( bundlerContexts, componentStyleBundler, codeBundleCache, + templateUpdates, ); executionResult.addWarnings(bundlingResult.warnings); diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts index b969253e703f..c0ef33ab2ff8 100644 --- a/packages/angular/build/src/builders/application/setup-bundling.ts +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -34,6 +34,7 @@ export function setupBundlerContexts( target: string[], codeBundleCache: SourceFileCache, stylesheetBundler: ComponentStylesheetBundler, + templateUpdates: Map | undefined, ): { typescriptContexts: BundlerContext[]; otherContexts: BundlerContext[]; @@ -55,7 +56,13 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, watch, - createBrowserCodeBundleOptions(options, target, codeBundleCache, stylesheetBundler), + createBrowserCodeBundleOptions( + options, + target, + codeBundleCache, + stylesheetBundler, + templateUpdates, + ), ), ); 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 b4f5db4435c5..34133ab8e283 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -51,7 +51,7 @@ export interface CompilerPluginOptions { incremental: boolean; externalRuntimeStyles?: boolean; instrumentForCoverage?: (request: string) => boolean; - templateUpdates?: boolean; + templateUpdates?: Map; } // eslint-disable-next-line max-lines-per-function @@ -303,6 +303,12 @@ export function createCompilerPlugin( !!initializationResult.compilerOptions.inlineSourceMap; referencedFiles = initializationResult.referencedFiles; externalStylesheets = initializationResult.externalStylesheets; + if (initializationResult.templateUpdates) { + // Propagate any template updates + initializationResult.templateUpdates.forEach((value, key) => + pluginOptions.templateUpdates?.set(key, value), + ); + } } catch (error) { (result.errors ??= []).push({ text: 'Angular compilation initialization failed.', @@ -657,7 +663,7 @@ function createCompilerOptionsTransformer( sourceRoot: undefined, preserveSymlinks, externalRuntimeStyles: pluginOptions.externalRuntimeStyles, - _enableHmr: pluginOptions.templateUpdates, + _enableHmr: !!pluginOptions.templateUpdates, }; }; } diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index a14c13706607..ceb60c644949 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -38,11 +38,17 @@ export function createBrowserCodeBundleOptions( target: string[], sourceFileCache: SourceFileCache, stylesheetBundler: ComponentStylesheetBundler, + templateUpdates: Map | undefined, ): BundlerOptionsFactory { return (loadCache) => { const { entryPoints, outputNames, polyfills } = options; - const pluginOptions = createCompilerPluginOptions(options, sourceFileCache, loadCache); + const pluginOptions = createCompilerPluginOptions( + options, + sourceFileCache, + loadCache, + templateUpdates, + ); const zoneless = isZonelessApp(polyfills); diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index 03a82172b967..d6d2d2a01fd8 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -28,6 +28,7 @@ export interface RebuildState { codeBundleCache?: SourceFileCache; fileChanges: ChangedFiles; previousOutputHashes: Map; + templateUpdates?: Map; } export interface ExternalResultMetadata { @@ -60,6 +61,7 @@ export class ExecutionResult { }, private componentStyleBundler: ComponentStylesheetBundler, private codeBundleCache?: SourceFileCache, + readonly templateUpdates?: Map, ) {} addOutputFile(path: string, content: string | Uint8Array, type: BuildOutputFileType): void { @@ -166,6 +168,7 @@ export class ExecutionResult { componentStyleBundler: this.componentStyleBundler, fileChanges, previousOutputHashes: new Map(this.outputFiles.map((file) => [file.path, file.hash])), + templateUpdates: this.templateUpdates, }; } diff --git a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts index f29c71e0be2b..355bbad228ff 100644 --- a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts +++ b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts @@ -17,6 +17,7 @@ export function createCompilerPluginOptions( options: NormalizedApplicationBuildOptions, sourceFileCache: SourceFileCache, loadResultCache?: LoadResultCache, + templateUpdates?: Map, ): CreateCompilerPluginParameters[0] { const { sourcemapOptions, @@ -26,7 +27,6 @@ export function createCompilerPluginOptions( jit, externalRuntimeStyles, instrumentForCoverage, - templateUpdates, } = options; const incremental = !!options.watch; diff --git a/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts index abfd330dec90..6110316773b3 100644 --- a/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/component-middleware.ts @@ -33,7 +33,7 @@ export function createAngularComponentMiddleware( return; } - const updateCode = templateUpdates.get(componentId) ?? ''; + const updateCode = templateUpdates.get(encodeURIComponent(componentId)) ?? ''; res.setHeader('Content-Type', 'text/javascript'); res.setHeader('Cache-Control', 'no-cache');