Skip to content

Commit dbdd3fc

Browse files
committed
feat(@angular-devkit/build-angular): out of the box hot module replacement (HMR)
With this change we configure HMR internally and therefore users which want to use basic HMR functionality will not longer be required to change their application code. This is important because previously a lot of users missed out on HMR or reported a broken behaviour. This also gives novice users a better chance to appreciate HMR and Angular because of the zero effort required to use HMR. Closes #17324
1 parent c6aeb60 commit dbdd3fc

File tree

2 files changed

+220
-16
lines changed
  • packages/angular_devkit/build_angular/src

2 files changed

+220
-16
lines changed

packages/angular_devkit/build_angular/src/dev-server/index.ts

+8-16
Original file line numberDiff line numberDiff line change
@@ -557,22 +557,14 @@ function _addLiveReload(
557557

558558
const entryPoints = [`${webpackDevServerPath}?${url.format(clientAddress)}${sockjsPath}`];
559559
if (options.hmr) {
560-
const webpackHmrLink = 'https://webpack.js.org/guides/hot-module-replacement';
561-
logger.warn(tags.oneLine`NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.`);
562-
563-
const showWarning = options.hmrWarning;
564-
if (showWarning) {
565-
logger.info(tags.stripIndents`
566-
The project will still live reload when HMR is enabled, but to take full advantage of HMR
567-
additional application code which is not included by default in an Angular CLI project is required.
568-
569-
See ${webpackHmrLink} for information on working with HMR for Webpack.`);
570-
logger.warn(
571-
tags.oneLine`To disable this warning use "hmrWarning: false" under "serve"
572-
options in "angular.json".`,
573-
);
574-
}
575-
entryPoints.push('webpack/hot/dev-server');
560+
logger.warn(tags.stripIndents`NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.
561+
See https://webpack.js.org/guides/hot-module-replacement for information on working with HMR for Webpack.`);
562+
563+
entryPoints.push(
564+
'webpack/hot/dev-server',
565+
path.join(__dirname, '../webpack/hmr.js'),
566+
);
567+
576568
if (browserOptions.styles?.length) {
577569
// When HMR is enabled we need to add the css paths as part of the entrypoints
578570
// because otherwise no JS bundle will contain the HMR accept code.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
// TODO: change the file to TypeScript and build soley using Bazel.
10+
11+
import { ApplicationRef, PlatformRef, ɵresetCompiledComponents } from '@angular/core';
12+
import { filter, take } from 'rxjs/operators';
13+
14+
if (module['hot']) {
15+
module['hot'].accept();
16+
module['hot'].dispose(() => {
17+
if (typeof ng === 'undefined') {
18+
console.warn(`[NG HMR] Cannot find global 'ng'. Likely this is caused because scripts optimization is enabled.`);
19+
20+
return;
21+
}
22+
23+
if (!ng.getInjector) {
24+
// View Engine
25+
return;
26+
}
27+
28+
// Reset JIT compiled components cache
29+
ɵresetCompiledComponents();
30+
const appRoot = getAppRoot();
31+
if (!appRoot) {
32+
return;
33+
}
34+
35+
const appRef = getApplicationRef(appRoot);
36+
if (!appRef) {
37+
return;
38+
}
39+
40+
const oldInputs = document.querySelectorAll('input, textarea');
41+
const oldOptions = document.querySelectorAll('option');
42+
43+
// Create new application
44+
appRef.components
45+
.forEach(cp => {
46+
const element = cp.location.nativeElement;
47+
const parentNode = element.parentNode;
48+
parentNode.insertBefore(
49+
document.createElement(element.tagName),
50+
element,
51+
);
52+
53+
parentNode.removeChild(element);
54+
});
55+
56+
// Destroy old application, injectors, <style..., etc..
57+
const platformRef = getPlatformRef(appRoot);
58+
if (platformRef) {
59+
platformRef.destroy();
60+
}
61+
62+
// Restore all inputs and options
63+
const bodyElement = document.body;
64+
if ((oldInputs.length + oldOptions.length) === 0 || !bodyElement) {
65+
return;
66+
}
67+
68+
// Use a `MutationObserver` to wait until the app-root element has been bootstrapped.
69+
// ie: when the ng-version attribute is added.
70+
new MutationObserver((_mutationsList, observer) => {
71+
observer.disconnect();
72+
73+
const newAppRoot = getAppRoot();
74+
if (!newAppRoot) {
75+
return;
76+
}
77+
78+
const newAppRef = getApplicationRef(newAppRoot);
79+
if (!newAppRef) {
80+
return;
81+
}
82+
83+
// Wait until the application isStable to restore the form values
84+
newAppRef.isStable
85+
.pipe(
86+
filter(isStable => !!isStable),
87+
take(1),
88+
)
89+
.subscribe(() => restoreFormValues(oldInputs, oldOptions));
90+
})
91+
.observe(bodyElement, {
92+
attributes: true,
93+
subtree: true,
94+
attributeFilter: ['ng-version'],
95+
});
96+
});
97+
}
98+
99+
function getAppRoot() {
100+
const appRoot = document.querySelector('[ng-version]');
101+
if (!appRoot) {
102+
console.warn('[NG HMR] Cannot find the application root component.');
103+
104+
return undefined;
105+
}
106+
107+
return appRoot;
108+
}
109+
110+
function getToken(appRoot, token) {
111+
return typeof ng === 'object' && ng.getInjector(appRoot).get(token) || undefined;
112+
}
113+
114+
function getApplicationRef(appRoot) {
115+
const appRef = getToken(appRoot, ApplicationRef);
116+
if (!appRef) {
117+
console.warn(`[NG HMR] Cannot get 'ApplicationRef'.`);
118+
119+
return undefined;
120+
}
121+
122+
return appRef;
123+
}
124+
125+
function getPlatformRef(appRoot) {
126+
const platformRef = getToken(appRoot, PlatformRef);
127+
if (!platformRef) {
128+
console.warn(`[NG HMR] Cannot get 'PlatformRef'.`);
129+
130+
return undefined;
131+
}
132+
133+
return platformRef;
134+
}
135+
136+
function dispatchEvents(element) {
137+
element.dispatchEvent(new Event('input', {
138+
bubbles: true,
139+
cancelable: true,
140+
}));
141+
142+
element.blur();
143+
144+
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
145+
}
146+
147+
function restoreFormValues(oldInputs, oldOptions) {
148+
// Restore input
149+
const newInputs = document.querySelectorAll('input, textarea');
150+
if (newInputs.length && newInputs.length === oldInputs.length) {
151+
console.log('[NG HMR] Restoring input/textarea values.');
152+
for (let index = 0; index < newInputs.length; index++) {
153+
const newElement = newInputs[index];
154+
const oldElement = oldInputs[index];
155+
156+
switch (oldElement.type) {
157+
case 'button':
158+
case 'image':
159+
case 'submit':
160+
case 'reset':
161+
// These types don't need any value change.
162+
continue;
163+
case 'radio':
164+
case 'checkbox':
165+
newElement.checked = oldElement.checked;
166+
break;
167+
case 'color':
168+
case 'date':
169+
case 'datetime-local':
170+
case 'email':
171+
case 'file':
172+
case 'hidden':
173+
case 'image':
174+
case 'month':
175+
case 'number':
176+
case 'password':
177+
case 'radio':
178+
case 'range':
179+
case 'search':
180+
case 'tel':
181+
case 'text':
182+
case 'textarea':
183+
case 'time':
184+
case 'url':
185+
case 'week':
186+
newElement.value = oldElement.value;
187+
break;
188+
default:
189+
console.warn('[NG HMR] Unknown input type ' + oldElement.type + '.');
190+
continue;
191+
}
192+
193+
dispatchEvents(newElement);
194+
}
195+
} else {
196+
console.warn('[NG HMR] Cannot restore input/textarea values.');
197+
}
198+
199+
// Restore option
200+
const newOptions = document.querySelectorAll('option');
201+
if (newOptions.length && newOptions.length === oldOptions.length) {
202+
console.log('[NG HMR] Restoring selected options.');
203+
for (let index = 0; index < newOptions.length; index++) {
204+
const newElement = newOptions[index];
205+
newElement.selected = oldOptions[index].selected;
206+
207+
dispatchEvents(newElement);
208+
}
209+
} else {
210+
console.warn('[NG HMR] Cannot restore selected options.');
211+
}
212+
}

0 commit comments

Comments
 (0)