diff --git a/packages/core/integration-tests/test/hmr.js b/packages/core/integration-tests/test/hmr.js
index 66794f361db..4818bd97400 100644
--- a/packages/core/integration-tests/test/hmr.js
+++ b/packages/core/integration-tests/test/hmr.js
@@ -79,10 +79,17 @@ describe('hmr', function () {
ws = await openSocket('ws://localhost:' + port);
let outputs = [];
+ let reloaded = false;
await run(bundleGraph, {
output(o) {
outputs.push(o);
},
+ location: {
+ reload() {
+ reloaded = true;
+ outputs = [];
+ },
+ },
});
for (let update of [].concat(updates)) {
@@ -94,13 +101,16 @@ describe('hmr', function () {
fsUpdates[f],
);
}
- }
- await nextWSMessage(nullthrows(ws));
- await sleep(100);
+ await nextWSMessage(nullthrows(ws));
+ await sleep(100);
+ }
// Fixup the prototypes so that strict assertions work
- return JSON.parse(JSON.stringify(outputs));
+ return {
+ outputs: JSON.parse(JSON.stringify(outputs)),
+ reloaded,
+ };
}
afterEach(async () => {
@@ -330,7 +340,7 @@ describe('hmr', function () {
});
it('should support self accepting', async function () {
- let outputs = await testHMRClient('hmr-accept-self', outputs => {
+ let {outputs} = await testHMRClient('hmr-accept-self', outputs => {
assert.deepStrictEqual(outputs, [
['other', 1],
['local', 1],
@@ -352,7 +362,7 @@ describe('hmr', function () {
});
it('should bubble through parents', async function () {
- let outputs = await testHMRClient('hmr-bubble', outputs => {
+ let {outputs} = await testHMRClient('hmr-bubble', outputs => {
assert.deepStrictEqual(outputs, [
['other', 1],
['local', 1],
@@ -375,7 +385,7 @@ describe('hmr', function () {
});
it('should call dispose callbacks', async function () {
- let outputs = await testHMRClient('hmr-dispose', outputs => {
+ let {outputs} = await testHMRClient('hmr-dispose', outputs => {
assert.deepStrictEqual(outputs, [
['eval:other', 1, null],
['eval:local', 1, null],
@@ -417,7 +427,7 @@ module.hot.dispose((data) => {
});
it('should work with circular dependencies', async function () {
- let outputs = await testHMRClient('hmr-circular', outputs => {
+ let {outputs} = await testHMRClient('hmr-circular', outputs => {
assert.deepEqual(outputs, [3]);
return {
@@ -429,6 +439,115 @@ module.hot.dispose((data) => {
assert.deepEqual(outputs, [3, 10]);
});
+ it('should reload if not accepted', async function () {
+ let {reloaded} = await testHMRClient('hmr-reload', outputs => {
+ assert.deepEqual(outputs, [3]);
+ return {
+ 'local.js': 'exports.a = 5; exports.b = 5;',
+ };
+ });
+
+ assert(reloaded);
+ });
+
+ it('should reload when modifying the entry', async function () {
+ let {reloaded} = await testHMRClient('hmr-reload', outputs => {
+ assert.deepEqual(outputs, [3]);
+ return {
+ 'index.js': 'output(5)',
+ };
+ });
+
+ assert(reloaded);
+ });
+
+ it('should work with multiple parents', async function () {
+ let {outputs} = await testHMRClient('hmr-multiple-parents', outputs => {
+ assert.deepEqual(outputs, ['a: fn1 b: fn2']);
+ return {
+ 'fn2.js': 'export function fn2() { return "UPDATED"; }',
+ };
+ });
+
+ assert.deepEqual(outputs, ['a: fn1 b: fn2', 'a: fn1 b: UPDATED']);
+ });
+
+ it('should reload if only one parent accepts', async function () {
+ let {reloaded} = await testHMRClient(
+ 'hmr-multiple-parents-reload',
+ outputs => {
+ assert.deepEqual(outputs, ['a: fn1', 'b: fn2']);
+ return {
+ 'fn2.js': 'export function fn2() { return "UPDATED"; }',
+ };
+ },
+ );
+
+ assert(reloaded);
+ });
+
+ it('should work across bundles', async function () {
+ let {reloaded} = await testHMRClient('hmr-dynamic', outputs => {
+ assert.deepEqual(outputs, [3]);
+ return {
+ 'local.js': 'exports.a = 5; exports.b = 5;',
+ };
+ });
+
+ // assert.deepEqual(outputs, [3, 10]);
+ assert(reloaded); // TODO: this should eventually not reload...
+ });
+
+ it('should work with urls', async function () {
+ let search;
+ let {outputs} = await testHMRClient('hmr-url', outputs => {
+ assert.equal(outputs.length, 1);
+ let url = new URL(outputs[0]);
+ assert(/test\.[0-9a-f]+\.txt/, url.pathname);
+ assert(!isNaN(url.search.slice(1)));
+ search = url.search;
+ return {
+ 'test.txt': 'yo',
+ };
+ });
+
+ assert.equal(outputs.length, 2);
+ let url = new URL(outputs[1]);
+ assert(/test\.[0-9a-f]+\.txt/, url.pathname);
+ assert(!isNaN(url.search.slice(1)));
+ assert.notEqual(url.search, search);
+ });
+
+ it('should clean up orphaned assets when deleting a dependency', async function () {
+ let search;
+ let {outputs} = await testHMRClient('hmr-url', [
+ outputs => {
+ assert.equal(outputs.length, 1);
+ let url = new URL(outputs[0]);
+ assert(/test\.[0-9a-f]+\.txt/, url.pathname);
+ assert(!isNaN(url.search.slice(1)));
+ search = url.search;
+ return {
+ 'index.js': 'output("yo"); module.hot.accept();',
+ };
+ },
+ outputs => {
+ assert.equal(outputs.length, 2);
+ assert.equal(outputs[1], 'yo');
+ return {
+ 'index.js':
+ 'output(new URL("test.txt", import.meta.url)); module.hot.accept();',
+ };
+ },
+ ]);
+
+ assert.equal(outputs.length, 3);
+ let url = new URL(outputs[2]);
+ assert(/test\.[0-9a-f]+\.txt/, url.pathname);
+ assert(!isNaN(url.search.slice(1)));
+ assert.notEqual(url.search, search);
+ });
+
/*
it.skip('should accept HMR updates in the runtime after an initial error', async function() {
await fs.mkdirp(path.join(__dirname, '/input'));
@@ -530,90 +649,6 @@ module.hot.dispose((data) => {
]);
});
- it.skip('should work across bundles', async function() {
- await ncp(
- path.join(__dirname, '/integration/hmr-dynamic'),
- path.join(__dirname, '/input'),
- );
-
- let port = await getPort();
- let b = await bundle(path.join(__dirname, '/input/index.js'), {
- hmrOptions: {
- https: false,
- port,
- host: 'localhost',
- },
- env: {
- HMR_HOSTNAME: 'localhost',
- HMR_PORT: port,
- },
- watch: true,
- });
-
- let outputs = [];
-
- await run(b, {
- output(o) {
- outputs.push(o);
- },
- });
-
- await sleep(50);
- assert.deepEqual(outputs, [3]);
-
- let ws = new WebSocket('ws://localhost:' + port);
-
- await sleep(50);
- fs.writeFile(
- path.join(__dirname, '/input/local.js'),
- 'exports.a = 5; exports.b = 5;',
- );
-
- await nextWSMessage(ws);
- await sleep(50);
-
- assert.deepEqual(outputs, [3, 10]);
- });
-
- it.skip('should bubble up HMR events to a page reload', async function() {
- await ncp(
- path.join(__dirname, '/integration/hmr-reload'),
- path.join(__dirname, '/input'),
- );
-
- let b = bundler(path.join(__dirname, '/input/index.js'), {
- watch: true,
- hmr: true,
- });
- let bundle = await b.bundle();
-
- let outputs = [];
- let ctx = await run(
- bundle,
- {
- output(o) {
- outputs.push(o);
- },
- },
- {require: false},
- );
- let spy = sinon.spy(ctx.location, 'reload');
-
- await sleep(50);
- assert.deepEqual(outputs, [3]);
- assert(spy.notCalled);
-
- await sleep(100);
- fs.writeFile(
- path.join(__dirname, '/input/local.js'),
- 'exports.a = 5; exports.b = 5;',
- );
-
- // await nextEvent(b, 'bundled');
- assert.deepEqual(outputs, [3]);
- assert(spy.calledOnce);
- });
-
it.skip('should trigger a page reload when a new bundle is created', async function() {
await ncp(
path.join(__dirname, '/integration/hmr-new-bundle'),
diff --git a/packages/core/integration-tests/test/integration/hmr-css-modules/index.jsx b/packages/core/integration-tests/test/integration/hmr-css-modules/index.jsx
index 9890fe63081..e1e7b14ba06 100644
--- a/packages/core/integration-tests/test/integration/hmr-css-modules/index.jsx
+++ b/packages/core/integration-tests/test/integration/hmr-css-modules/index.jsx
@@ -1,3 +1,4 @@
import * as styles from "./index.module.css";
+import React from 'react';
-const Hello = () =>
hello
;
+export const Hello = () => hello
;
diff --git a/packages/core/integration-tests/test/integration/hmr-css-modules/package.json b/packages/core/integration-tests/test/integration/hmr-css-modules/package.json
new file mode 100644
index 00000000000..60b3c8a2fdf
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-css-modules/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "react": "^16"
+ }
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/a.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/a.js
new file mode 100644
index 00000000000..e9c59a904f7
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/a.js
@@ -0,0 +1,4 @@
+import {fn1} from './utils';
+
+output('a: ' + fn1());
+module.hot.accept();
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/b.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/b.js
new file mode 100644
index 00000000000..f0df5255a02
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/b.js
@@ -0,0 +1,3 @@
+import {fn2} from './utils';
+
+output('b: ' + fn2());
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/fn1.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/fn1.js
new file mode 100644
index 00000000000..0c96cbec6c3
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/fn1.js
@@ -0,0 +1,3 @@
+export function fn1() {
+ return 'fn1';
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/fn2.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/fn2.js
new file mode 100644
index 00000000000..313ad438042
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/fn2.js
@@ -0,0 +1,3 @@
+export function fn2() {
+ return 'fn2';
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/index.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/index.js
new file mode 100644
index 00000000000..3e895565614
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/index.js
@@ -0,0 +1,2 @@
+import './a';
+import './b';
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/utils.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/utils.js
new file mode 100644
index 00000000000..dc225d142a7
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents-reload/utils.js
@@ -0,0 +1,2 @@
+export * from './fn1';
+export * from './fn2';
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents/a.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents/a.js
new file mode 100644
index 00000000000..b6eb820723c
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents/a.js
@@ -0,0 +1,5 @@
+import {fn1} from './utils';
+
+export function a() {
+ return 'a: ' + fn1();
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents/b.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents/b.js
new file mode 100644
index 00000000000..9cd1bb186b5
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents/b.js
@@ -0,0 +1,5 @@
+import {fn2} from './utils';
+
+export function b() {
+ return 'b: ' + fn2();
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents/fn1.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents/fn1.js
new file mode 100644
index 00000000000..0c96cbec6c3
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents/fn1.js
@@ -0,0 +1,3 @@
+export function fn1() {
+ return 'fn1';
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents/fn2.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents/fn2.js
new file mode 100644
index 00000000000..313ad438042
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents/fn2.js
@@ -0,0 +1,3 @@
+export function fn2() {
+ return 'fn2';
+}
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents/index.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents/index.js
new file mode 100644
index 00000000000..7ee423ae600
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents/index.js
@@ -0,0 +1,6 @@
+import {a} from './a';
+import {b} from './b';
+
+output(a() + ' ' + b());
+
+module.hot.accept();
diff --git a/packages/core/integration-tests/test/integration/hmr-multiple-parents/utils.js b/packages/core/integration-tests/test/integration/hmr-multiple-parents/utils.js
new file mode 100644
index 00000000000..dc225d142a7
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-multiple-parents/utils.js
@@ -0,0 +1,2 @@
+export * from './fn1';
+export * from './fn2';
diff --git a/packages/core/integration-tests/test/integration/hmr-url/index.js b/packages/core/integration-tests/test/integration/hmr-url/index.js
new file mode 100644
index 00000000000..bd75a9c761f
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-url/index.js
@@ -0,0 +1,3 @@
+let url = new URL('test.txt', import.meta.url);
+output(url);
+module.hot.accept();
diff --git a/packages/core/integration-tests/test/integration/hmr-url/test.txt b/packages/core/integration-tests/test/integration/hmr-url/test.txt
new file mode 100644
index 00000000000..32f95c0d124
--- /dev/null
+++ b/packages/core/integration-tests/test/integration/hmr-url/test.txt
@@ -0,0 +1 @@
+hi
\ No newline at end of file
diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js
index a32105f80d1..a3d6fc0c04f 100644
--- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js
+++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js
@@ -292,6 +292,21 @@ function hmrApply(bundle /*: ParcelRequire */, asset /*: HMRAsset */) {
} else if (asset.type === 'js') {
let deps = asset.depsByBundle[bundle.HMR_BUNDLE_ID];
if (deps) {
+ if (modules[asset.id]) {
+ // Remove dependencies that are removed and will become orphaned.
+ // This is necessary so that if the asset is added back again, the cache is gone, and we prevent a full page reload.
+ let oldDeps = modules[asset.id][1];
+ for (let dep in oldDeps) {
+ if (!deps[dep] || deps[dep] !== oldDeps[dep]) {
+ let id = oldDeps[dep];
+ let parents = getParents(module.bundle.root, id);
+ if (parents.length === 1) {
+ hmrDelete(module.bundle.root, id);
+ }
+ }
+ }
+ }
+
var fn = new Function('require', 'module', 'exports', asset.output);
modules[asset.id] = [fn, deps];
} else if (bundle.parent) {
@@ -300,10 +315,73 @@ function hmrApply(bundle /*: ParcelRequire */, asset /*: HMRAsset */) {
}
}
+function hmrDelete(bundle, id) {
+ let modules = bundle.modules;
+ if (!modules) {
+ return;
+ }
+
+ if (modules[id]) {
+ // Collect dependencies that will become orphaned when this module is deleted.
+ let deps = modules[id][1];
+ let orphans = [];
+ for (let dep in deps) {
+ let parents = getParents(module.bundle.root, deps[dep]);
+ if (parents.length === 1) {
+ orphans.push(deps[dep]);
+ }
+ }
+
+ // Delete the module. This must be done before deleting dependencies in case of circular dependencies.
+ delete modules[id];
+ delete bundle.cache[id];
+
+ // Now delete the orphans.
+ orphans.forEach(id => {
+ hmrDelete(module.bundle.root, id);
+ });
+ } else if (bundle.parent) {
+ hmrDelete(bundle.parent, id);
+ }
+}
+
function hmrAcceptCheck(
bundle /*: ParcelRequire */,
id /*: string */,
depsByBundle /*: ?{ [string]: { [string]: string } }*/,
+) {
+ if (hmrAcceptCheckOne(bundle, id, depsByBundle)) {
+ return true;
+ }
+
+ // Traverse parents breadth first. All possible ancestries must accept the HMR update, or we'll reload.
+ let parents = getParents(module.bundle.root, id);
+ let accepted = false;
+ while (parents.length > 0) {
+ let v = parents.shift();
+ let a = hmrAcceptCheckOne(v[0], v[1], null);
+ if (a) {
+ // If this parent accepts, stop traversing upward, but still consider siblings.
+ accepted = true;
+ } else {
+ // Otherwise, queue the parents in the next level upward.
+ let p = getParents(module.bundle.root, v[1]);
+ if (p.length === 0) {
+ // If there are no parents, then we've reached an entry without accepting. Reload.
+ accepted = false;
+ break;
+ }
+ parents.push(...p);
+ }
+ }
+
+ return accepted;
+}
+
+function hmrAcceptCheckOne(
+ bundle /*: ParcelRequire */,
+ id /*: string */,
+ depsByBundle /*: ?{ [string]: { [string]: string } }*/,
) {
var modules = bundle.modules;
if (!modules) {
@@ -330,20 +408,9 @@ function hmrAcceptCheck(
assetsToAccept.push([bundle, id]);
- if (cached && cached.hot && cached.hot._acceptCallbacks.length) {
+ if (!cached || (cached.hot && cached.hot._acceptCallbacks.length)) {
return true;
}
-
- let parents = getParents(module.bundle.root, id);
-
- // If no parents, the asset is new. Prevent reloading the page.
- if (!parents.length) {
- return true;
- }
-
- return parents.some(function (v) {
- return hmrAcceptCheck(v[0], v[1], null);
- });
}
function hmrAcceptRun(bundle /*: ParcelRequire */, id /*: string */) {