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 */) {