From 298b554a7a40465444b4c508e2250ecbf459ea47 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:30:18 -0500 Subject: [PATCH] feat(@angular/build): enable component template hot replacement by default When using the `application` builder (default for new projects) with the development server, component template only changes will now automatically replace the template within the running application without a full reload of the page. No application code changes are necessary and both file-based (`templateUrl`) and inline (`template`) component templates are supported. Additionally, changing a components styles in combination with a template change is also supported for hot replacement. This includes both inline and file-based changes. If any issues are encountered or it is preferred to not hot replace component templates, the `NG_HMR_TEMPLATES=0` environment variable can be used to disable the feature. Setting the `liveReload` option or `hmr` option to false will also disable all updates. --- .../build/src/builders/dev-server/vite-server.ts | 13 +++++-------- .../angular/build/src/utils/environment-options.ts | 2 +- tests/legacy-cli/e2e/tests/basic/rebuild.ts | 9 ++------- .../legacy-cli/e2e/tests/vite/ssr-entry-express.ts | 6 +++++- .../legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts | 6 +++++- tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts | 6 +++++- tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts | 8 ++++++-- 7 files changed, 29 insertions(+), 21 deletions(-) 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 2447f02f5305..fb70e6f964d5 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -138,17 +138,14 @@ export async function* serveWithVite( process.setSourceMapsEnabled(true); } - // Enable to support component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively) + // Enable to support link-based component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively) browserOptions.externalRuntimeStyles = serverOptions.liveReload && serverOptions.hmr && useComponentStyleHmr; - // Enable to support component template hot replacement (`NG_HMR_TEMPLATE=1` can be used to enable) - browserOptions.templateUpdates = !!serverOptions.liveReload && useComponentTemplateHmr; - if (browserOptions.templateUpdates) { - context.logger.warn( - 'Experimental support for component template hot replacement has been enabled via the "NG_HMR_TEMPLATE" environment variable.', - ); - } + // Enable to support component template hot replacement (`NG_HMR_TEMPLATE=0` can be used to disable selectively) + // This will also replace file-based/inline styles as code if external runtime styles are not enabled. + browserOptions.templateUpdates = + serverOptions.liveReload && serverOptions.hmr && useComponentTemplateHmr; // Setup the prebundling transformer that will be shared across Vite prebundling requests const prebundleTransformer = new JavaScriptTransformer( diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index 40de16a535e3..63abd82af46e 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -107,7 +107,7 @@ export const useComponentStyleHmr = const hmrComponentTemplateVariable = process.env['NG_HMR_TEMPLATES']; export const useComponentTemplateHmr = - isPresent(hmrComponentTemplateVariable) && isEnabled(hmrComponentTemplateVariable); + !isPresent(hmrComponentTemplateVariable) || !isDisabled(hmrComponentTemplateVariable); 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 f512961366bc..d289587d78b8 100644 --- a/tests/legacy-cli/e2e/tests/basic/rebuild.ts +++ b/tests/legacy-cli/e2e/tests/basic/rebuild.ts @@ -9,13 +9,8 @@ export default async function () { const validBundleRegEx = esbuild ? /sent to client/ : /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; + // Disable HMR to support page reload based rebuild testing. + const port = await ngServe('--no-hmr'); // Add a lazy route. await silentNg('generate', 'component', 'lazy'); diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts index 5337ad5e5cc5..a8fbbea83503 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts @@ -90,6 +90,7 @@ export default async function () { 'src/app/home/home.component.html', 'home works', 'yay home works!!!', + true, ); await validateResponse('/api/test', /foo/); await validateResponse('/home', /yay home works/); @@ -111,9 +112,12 @@ async function modifyFileAndWaitUntilUpdated( filePath: string, searchValue: string, replaceValue: string, + hmr = false, ): Promise { await Promise.all([ - waitForAnyProcessOutputToMatch(/Page reload sent to client/), + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), ]); } diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts index 87c84ad0010f..aca0251b27db 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts @@ -90,6 +90,7 @@ export default async function () { 'src/app/home/home.component.html', 'home works', 'yay home works!!!', + true, ); await validateResponse('/api/test', /foo/); await validateResponse('/home', /yay home works/); @@ -111,9 +112,12 @@ async function modifyFileAndWaitUntilUpdated( filePath: string, searchValue: string, replaceValue: string, + hmr = false, ): Promise { await Promise.all([ - waitForAnyProcessOutputToMatch(/Page reload sent to client/), + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), ]); } diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts index 0027190395f4..88e03d34aea9 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts @@ -81,6 +81,7 @@ export default async function () { 'src/app/home/home.component.html', 'home works', 'yay home works!!!', + true, ); await validateResponse('/api/test', /foo/); await validateResponse('/home', /yay home works/); @@ -102,9 +103,12 @@ async function modifyFileAndWaitUntilUpdated( filePath: string, searchValue: string, replaceValue: string, + hmr = false, ): Promise { await Promise.all([ - waitForAnyProcessOutputToMatch(/Page reload sent to client/), + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), ]); } diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts index 353a6cf5b855..f36b36eec332 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts @@ -3,7 +3,7 @@ import { setTimeout } from 'node:timers/promises'; import { replaceInFile, writeMultipleFiles } from '../../utils/fs'; import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process'; import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages'; -import { ngServe, updateJsonFile, useSha } from '../../utils/project'; +import { ngServe, useSha } from '../../utils/project'; import { getGlobalVariable } from '../../utils/env'; export default async function () { @@ -73,6 +73,7 @@ export default async function () { 'src/app/home/home.component.html', 'home works', 'yay home works!!!', + true, ); await validateResponse('/api/test', /foo/); await validateResponse('/home', /yay home works/); @@ -94,9 +95,12 @@ async function modifyFileAndWaitUntilUpdated( filePath: string, searchValue: string, replaceValue: string, + hmr = false, ): Promise { await Promise.all([ - waitForAnyProcessOutputToMatch(/Page reload sent to client/), + waitForAnyProcessOutputToMatch( + hmr ? /Component update sent to client/ : /Page reload sent to client/, + ), setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)), ]); }