diff --git a/lib/HotModuleReplacement.runtime.js b/lib/HotModuleReplacement.runtime.js index 8427189b30a..02b3e8c9976 100644 --- a/lib/HotModuleReplacement.runtime.js +++ b/lib/HotModuleReplacement.runtime.js @@ -109,6 +109,7 @@ module.exports = function() { _declinedDependencies: {}, _selfAccepted: false, _selfDeclined: false, + _selfInvalidated: false, _disposeHandlers: [], _main: hotCurrentChildModule !== moduleId, @@ -139,6 +140,29 @@ module.exports = function() { var idx = hot._disposeHandlers.indexOf(callback); if (idx >= 0) hot._disposeHandlers.splice(idx, 1); }, + invalidate: function() { + this._selfInvalidated = true; + switch (hotStatus) { + case "idle": + hotUpdate = {}; + hotUpdate[moduleId] = modules[moduleId]; + hotSetStatus("ready"); + break; + case "ready": + hotApplyInvalidatedModule(moduleId); + break; + case "prepare": + case "check": + case "dispose": + case "apply": + (hotQueuedInvalidatedModules = + hotQueuedInvalidatedModules || []).push(moduleId); + break; + default: + // ignore requests in error states + break; + } + }, // Management API check: hotCheck, @@ -180,7 +204,7 @@ module.exports = function() { var hotDeferred; // The update info - var hotUpdate, hotUpdateNewHash; + var hotUpdate, hotUpdateNewHash, hotQueuedInvalidatedModules; function toModuleId(id) { var isNumber = +id + "" === id; @@ -195,7 +219,7 @@ module.exports = function() { hotSetStatus("check"); return hotDownloadManifest(hotRequestTimeout).then(function(update) { if (!update) { - hotSetStatus("idle"); + hotSetStatus(hotApplyInvalidatedModules() ? "ready" : "idle"); return null; } hotRequestedFilesMap = {}; @@ -288,6 +312,11 @@ module.exports = function() { if (hotStatus !== "ready") throw new Error("apply() is only allowed in ready status"); options = options || {}; + return hotApplyInternal(options); + } + + function hotApplyInternal(options) { + hotApplyInvalidatedModules(); var cb; var i; @@ -310,7 +339,11 @@ module.exports = function() { var moduleId = queueItem.id; var chain = queueItem.chain; module = installedModules[moduleId]; - if (!module || module.hot._selfAccepted) continue; + if ( + !module || + (module.hot._selfAccepted && !module.hot._selfInvalidated) + ) + continue; if (module.hot._selfDeclined) { return { type: "self-declined", @@ -478,10 +511,13 @@ module.exports = function() { installedModules[moduleId] && installedModules[moduleId].hot._selfAccepted && // removed self-accepted modules should not be required - appliedUpdate[moduleId] !== warnUnexpectedRequire + appliedUpdate[moduleId] !== warnUnexpectedRequire && + // when called invalidate self-accepting is not possible + !installedModules[moduleId].hot._selfInvalidated ) { outdatedSelfAcceptedModules.push({ module: moduleId, + parents: installedModules[moduleId].parents.slice(), errorHandler: installedModules[moduleId].hot._selfAccepted }); } @@ -554,7 +590,11 @@ module.exports = function() { // Now in "apply" phase hotSetStatus("apply"); - hotCurrentHash = hotUpdateNewHash; + if (hotUpdateNewHash !== undefined) { + hotCurrentHash = hotUpdateNewHash; + hotUpdateNewHash = undefined; + } + hotUpdate = undefined; // insert new code for (moduleId in appliedUpdate) { @@ -607,7 +647,8 @@ module.exports = function() { for (i = 0; i < outdatedSelfAcceptedModules.length; i++) { var item = outdatedSelfAcceptedModules[i]; moduleId = item.module; - hotCurrentParents = [moduleId]; + hotCurrentParents = item.parents; + hotCurrentChildModule = moduleId; try { $require$(moduleId); } catch (err) { @@ -649,9 +690,32 @@ module.exports = function() { return Promise.reject(error); } + if (hotQueuedInvalidatedModules) { + return hotApplyInternal(options).then(function(list) { + outdatedModules.forEach(function(moduleId) { + if (list.indexOf(moduleId) < 0) list.push(moduleId); + }); + return list; + }); + } + hotSetStatus("idle"); return new Promise(function(resolve) { resolve(outdatedModules); }); } + + function hotApplyInvalidatedModules() { + if (hotQueuedInvalidatedModules) { + if (!hotUpdate) hotUpdate = {}; + hotQueuedInvalidatedModules.forEach(hotApplyInvalidatedModule); + hotQueuedInvalidatedModules = undefined; + return true; + } + } + + function hotApplyInvalidatedModule(moduleId) { + if (!Object.prototype.hasOwnProperty.call(hotUpdate, moduleId)) + hotUpdate[moduleId] = modules[moduleId]; + } }; diff --git a/test/hotCases/invalidate/conditional-accept/data.json b/test/hotCases/invalidate/conditional-accept/data.json new file mode 100644 index 00000000000..a99a38c6966 --- /dev/null +++ b/test/hotCases/invalidate/conditional-accept/data.json @@ -0,0 +1,7 @@ +{ "a": 1, "b": 1 } +--- +{ "a": 2, "b": 1 } +--- +{ "a": 2, "b": 2 } +--- +{ "a": 3, "b": 3 } diff --git a/test/hotCases/invalidate/conditional-accept/index.js b/test/hotCases/invalidate/conditional-accept/index.js new file mode 100644 index 00000000000..e7925928189 --- /dev/null +++ b/test/hotCases/invalidate/conditional-accept/index.js @@ -0,0 +1,48 @@ +import "./data.json"; +import mod1 from "./module1"; +import mod2 from "./module2"; +import { value1, value2 } from "./store"; + +it("should invalidate a self-accepted module", function(done) { + expect(mod1).toBe(1); + expect(mod2).toBe(1); + expect(value1).toBe(1); + expect(value2).toBe(1); + let step = 0; + module.hot.accept("./module1"); + module.hot.accept("./module2"); + module.hot.accept("./data.json", () => + setTimeout(() => { + switch (step) { + case 0: + step++; + expect(mod1).toBe(1); + expect(mod2).toBe(1); + expect(value1).toBe(2); + expect(value2).toBe(2); + NEXT(require("../../update")(done)); + break; + case 1: + step++; + expect(mod1).toBe(2); + expect(mod2).toBe(2); + expect(value1).toBe(2); + expect(value2).toBe(2); + NEXT(require("../../update")(done)); + break; + case 2: + step++; + expect(mod1).toBe(3); + expect(mod2).toBe(3); + expect(value1).toBe(3); + expect(value2).toBe(3); + done(); + break; + default: + done(new Error("should not happen")); + break; + } + }, 100) + ); + NEXT(require("../../update")(done)); +}); diff --git a/test/hotCases/invalidate/conditional-accept/module1.js b/test/hotCases/invalidate/conditional-accept/module1.js new file mode 100644 index 00000000000..e478012e71e --- /dev/null +++ b/test/hotCases/invalidate/conditional-accept/module1.js @@ -0,0 +1,16 @@ +import data from "./data.json"; +import { setValue1 } from "./store"; + +setValue1(data.a); + +export default data.b; + +if (module.hot.data && module.hot.data.ok && module.hot.data.b !== data.b) { + module.hot.invalidate(); +} else { + module.hot.dispose(d => { + d.ok = true; + d.b = data.b; + }); + module.hot.accept(); +} diff --git a/test/hotCases/invalidate/conditional-accept/module2.js b/test/hotCases/invalidate/conditional-accept/module2.js new file mode 100644 index 00000000000..0538d7e44c1 --- /dev/null +++ b/test/hotCases/invalidate/conditional-accept/module2.js @@ -0,0 +1,16 @@ +import data from "./data.json"; +import { setValue2 } from "./store"; + +setValue2(data.a); + +export default data.b; + +const b = data.b; + +module.hot.accept(["./data.json"], () => { + if (data.b !== b) { + module.hot.invalidate(); + return; + } + setValue2(data.a); +}); diff --git a/test/hotCases/invalidate/conditional-accept/store.js b/test/hotCases/invalidate/conditional-accept/store.js new file mode 100644 index 00000000000..bc8c9c68f6a --- /dev/null +++ b/test/hotCases/invalidate/conditional-accept/store.js @@ -0,0 +1,9 @@ +export let value1 = 0; +export function setValue1(v) { + value1 = v; +} + +export let value2 = 0; +export function setValue2(v) { + value2 = v; +} diff --git a/test/hotCases/invalidate/during-idle/a.js b/test/hotCases/invalidate/during-idle/a.js new file mode 100644 index 00000000000..df594c6c21b --- /dev/null +++ b/test/hotCases/invalidate/during-idle/a.js @@ -0,0 +1,5 @@ +export function invalidate() { + module.hot.invalidate(); +} + +export const value = {}; diff --git a/test/hotCases/invalidate/during-idle/b.js b/test/hotCases/invalidate/during-idle/b.js new file mode 100644 index 00000000000..70b8f861b4e --- /dev/null +++ b/test/hotCases/invalidate/during-idle/b.js @@ -0,0 +1,7 @@ +export function invalidate() { + module.hot.invalidate(); +} + +export const value = {}; + +module.hot.accept(); diff --git a/test/hotCases/invalidate/during-idle/c.js b/test/hotCases/invalidate/during-idle/c.js new file mode 100644 index 00000000000..424b691d927 --- /dev/null +++ b/test/hotCases/invalidate/during-idle/c.js @@ -0,0 +1,11 @@ +export function invalidate() { + module.hot.invalidate(); +} + +export const value = module.hot.data ? module.hot.data.value : {}; + +module.hot.dispose(data => { + data.value = value; +}); + +module.hot.accept(); diff --git a/test/hotCases/invalidate/during-idle/index.js b/test/hotCases/invalidate/during-idle/index.js new file mode 100644 index 00000000000..1a406401b66 --- /dev/null +++ b/test/hotCases/invalidate/during-idle/index.js @@ -0,0 +1,19 @@ +import { a, b, c } from "./module"; + +it("should allow to invalidate and reload a file", () => { + const oldA = a.value; + const oldB = b.value; + const oldC = c.value; + expect(module.hot.status()).toBe("idle"); + a.invalidate(); + expect(module.hot.status()).toBe("ready"); + b.invalidate(); + expect(module.hot.status()).toBe("ready"); + c.invalidate(); + expect(module.hot.status()).toBe("ready"); + module.hot.apply(); + expect(module.hot.status()).toBe("idle"); + expect(a.value).not.toBe(oldA); + expect(b.value).not.toBe(oldB); + expect(c.value).toBe(oldC); +}); diff --git a/test/hotCases/invalidate/during-idle/module.js b/test/hotCases/invalidate/during-idle/module.js new file mode 100644 index 00000000000..62a44c6d05d --- /dev/null +++ b/test/hotCases/invalidate/during-idle/module.js @@ -0,0 +1,7 @@ +import * as a from "./a"; +import * as b from "./b"; +import * as c from "./c"; + +export { a, b, c }; + +module.hot.accept(["./a", "./b", "./c"]);