Skip to content

Commit bd1d05a

Browse files
committed
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.
1 parent a56c55c commit bd1d05a

File tree

7 files changed

+51
-31
lines changed

7 files changed

+51
-31
lines changed

packages/angular/build/src/builders/dev-server/vite-server.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,14 @@ export async function* serveWithVite(
138138
process.setSourceMapsEnabled(true);
139139
}
140140

141-
// Enable to support component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively)
141+
// Enable to support link-based component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively)
142142
browserOptions.externalRuntimeStyles =
143143
serverOptions.liveReload && serverOptions.hmr && useComponentStyleHmr;
144144

145-
// Enable to support component template hot replacement (`NG_HMR_TEMPLATE=1` can be used to enable)
146-
browserOptions.templateUpdates = !!serverOptions.liveReload && useComponentTemplateHmr;
147-
if (browserOptions.templateUpdates) {
148-
context.logger.warn(
149-
'Experimental support for component template hot replacement has been enabled via the "NG_HMR_TEMPLATE" environment variable.',
150-
);
151-
}
145+
// Enable to support component template hot replacement (`NG_HMR_TEMPLATE=0` can be used to disable selectively)
146+
// This will also replace file-based/inline styles as code if external runtime styles are not enabled.
147+
browserOptions.templateUpdates =
148+
serverOptions.liveReload && serverOptions.hmr && useComponentTemplateHmr;
152149

153150
// Setup the prebundling transformer that will be shared across Vite prebundling requests
154151
const prebundleTransformer = new JavaScriptTransformer(

packages/angular/build/src/utils/environment-options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const useComponentStyleHmr =
107107

108108
const hmrComponentTemplateVariable = process.env['NG_HMR_TEMPLATES'];
109109
export const useComponentTemplateHmr =
110-
isPresent(hmrComponentTemplateVariable) && isEnabled(hmrComponentTemplateVariable);
110+
!isPresent(hmrComponentTemplateVariable) || !isDisabled(hmrComponentTemplateVariable);
111111

112112
const partialSsrBuildVariable = process.env['NG_BUILD_PARTIAL_SSR'];
113113
export const usePartialSsrBuild =

tests/legacy-cli/e2e/tests/basic/rebuild.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,8 @@ export default async function () {
99
const validBundleRegEx = esbuild ? /sent to client/ : /Compiled successfully\./;
1010
const lazyBundleRegEx = esbuild ? /chunk-/ : /src_app_lazy_lazy_component_ts\.js/;
1111

12-
// Disable component stylesheet HMR to support page reload based rebuild testing.
13-
// Ideally this environment variable would be passed directly to the new serve process
14-
// but this would require signficant test changes due to the existing `ngServe` signature.
15-
const oldHMRValue = process.env['NG_HMR_CSTYLES'];
16-
process.env['NG_HMR_CSTYLES'] = '0';
17-
const port = await ngServe();
18-
process.env['NG_HMR_CSTYLES'] = oldHMRValue;
12+
// Disable HMR to support page reload based rebuild testing.
13+
const port = await ngServe('--no-hmr');
1914

2015
// Add a lazy route.
2116
await silentNg('generate', 'component', 'lazy');

tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,19 @@ export default async function () {
9292
'yay home works!!!',
9393
);
9494
await validateResponse('/api/test', /foo/);
95-
await validateResponse('/home', /yay home works/);
95+
await validateResponse('/home', /yay home works/, true);
9696

9797
// Modify the API response and validate the change.
9898
await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
9999
await validateResponse('/api/test', /bar/);
100100
await validateResponse('/home', /yay home works/);
101101

102-
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
103-
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
102+
async function validateResponse(pathname: string, match: RegExp, twice = false): Promise<void> {
103+
let response = await fetch(new URL(pathname, `http://localhost:${port}`));
104+
// TODO: Investigate why component template HMR requires a second manual fetch but it is updated when using browser.
105+
if (twice) {
106+
response = await fetch(new URL(pathname, `http://localhost:${port}`));
107+
}
104108
const text = await response.text();
105109
assert.match(text, match);
106110
assert.equal(response.status, 200);

tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,22 @@ export default async function () {
9090
'src/app/home/home.component.html',
9191
'home works',
9292
'yay home works!!!',
93+
true,
9394
);
9495
await validateResponse('/api/test', /foo/);
95-
await validateResponse('/home', /yay home works/);
96+
await validateResponse('/home', /yay home works/, true);
9697

9798
// Modify the API response and validate the change.
9899
await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
99100
await validateResponse('/api/test', /bar/);
100101
await validateResponse('/home', /yay home works/);
101102

102-
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
103-
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
103+
async function validateResponse(pathname: string, match: RegExp, twice = false): Promise<void> {
104+
let response = await fetch(new URL(pathname, `http://localhost:${port}`));
105+
// TODO: Investigate why component template HMR requires a second manual fetch but it is updated when using browser.
106+
if (twice) {
107+
response = await fetch(new URL(pathname, `http://localhost:${port}`));
108+
}
104109
const text = await response.text();
105110
assert.match(text, match);
106111
assert.equal(response.status, 200);
@@ -111,9 +116,12 @@ async function modifyFileAndWaitUntilUpdated(
111116
filePath: string,
112117
searchValue: string,
113118
replaceValue: string,
119+
hmr = false,
114120
): Promise<void> {
115121
await Promise.all([
116-
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
122+
waitForAnyProcessOutputToMatch(
123+
hmr ? /Component update sent to client/ : /Page reload sent to client/,
124+
),
117125
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
118126
]);
119127
}

tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,22 @@ export default async function () {
8181
'src/app/home/home.component.html',
8282
'home works',
8383
'yay home works!!!',
84+
true,
8485
);
8586
await validateResponse('/api/test', /foo/);
86-
await validateResponse('/home', /yay home works/);
87+
await validateResponse('/home', /yay home works/, true);
8788

8889
// Modify the API response and validate the change.
8990
await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
9091
await validateResponse('/api/test', /bar/);
9192
await validateResponse('/home', /yay home works/);
9293

93-
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
94-
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
94+
async function validateResponse(pathname: string, match: RegExp, twice = false): Promise<void> {
95+
let response = await fetch(new URL(pathname, `http://localhost:${port}`));
96+
// TODO: Investigate why component template HMR requires a second manual fetch but it is updated when using browser.
97+
if (twice) {
98+
response = await fetch(new URL(pathname, `http://localhost:${port}`));
99+
}
95100
const text = await response.text();
96101
assert.match(text, match);
97102
assert.equal(response.status, 200);
@@ -102,9 +107,12 @@ async function modifyFileAndWaitUntilUpdated(
102107
filePath: string,
103108
searchValue: string,
104109
replaceValue: string,
110+
hmr = false,
105111
): Promise<void> {
106112
await Promise.all([
107-
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
113+
waitForAnyProcessOutputToMatch(
114+
hmr ? /Component update sent to client/ : /Page reload sent to client/,
115+
),
108116
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
109117
]);
110118
}

tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,22 @@ export default async function () {
7373
'src/app/home/home.component.html',
7474
'home works',
7575
'yay home works!!!',
76+
true,
7677
);
7778
await validateResponse('/api/test', /foo/);
78-
await validateResponse('/home', /yay home works/);
79+
await validateResponse('/home', /yay home works/, true);
7980

8081
// Modify the API response and validate the change.
8182
await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
8283
await validateResponse('/api/test', /bar/);
8384
await validateResponse('/home', /yay home works/);
8485

85-
async function validateResponse(pathname: string, match: RegExp): Promise<void> {
86-
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
86+
async function validateResponse(pathname: string, match: RegExp, twice = false): Promise<void> {
87+
let response = await fetch(new URL(pathname, `http://localhost:${port}`));
88+
// TODO: Investigate why component template HMR requires a second manual fetch but it is updated when using browser.
89+
if (twice) {
90+
response = await fetch(new URL(pathname, `http://localhost:${port}`));
91+
}
8792
const text = await response.text();
8893
assert.match(text, match);
8994
assert.equal(response.status, 200);
@@ -94,9 +99,12 @@ async function modifyFileAndWaitUntilUpdated(
9499
filePath: string,
95100
searchValue: string,
96101
replaceValue: string,
102+
hmr = false,
97103
): Promise<void> {
98104
await Promise.all([
99-
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
105+
waitForAnyProcessOutputToMatch(
106+
hmr ? /Component update sent to client/ : /Page reload sent to client/,
107+
),
100108
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
101109
]);
102110
}

0 commit comments

Comments
 (0)