diff --git a/Libraries/Utilities/HMRClient.js b/Libraries/Utilities/HMRClient.js index d9d8b79bdd6..887bab88000 100644 --- a/Libraries/Utilities/HMRClient.js +++ b/Libraries/Utilities/HMRClient.js @@ -73,9 +73,12 @@ Error: ${e.message}` break; } case 'update': { - const modules = data.body.modules; - const sourceMappingURLs = data.body.sourceMappingURLs; - const sourceURLs = data.body.sourceURLs; + const { + modules, + sourceMappingURLs, + sourceURLs, + inverseDependencies, + } = data.body; if (Platform.OS === 'ios') { const RCTRedBox = require('NativeModules').RedBox; @@ -85,7 +88,7 @@ Error: ${e.message}` RCTExceptionsManager && RCTExceptionsManager.dismissRedbox && RCTExceptionsManager.dismissRedbox(); } - modules.forEach((code, i) => { + modules.forEach(({name, code}, i) => { code = code + '\n\n' + sourceMappingURLs[i]; require('SourceMapsCache').fetch({ @@ -101,6 +104,16 @@ Error: ${e.message}` ? global.nativeInjectHMRUpdate : eval; + // TODO: (martinb) yellow box if cannot accept module + code = ` + __accept( + ${name}, + function(global, require, module, exports) { + ${code} + }, + ${JSON.stringify(inverseDependencies)} + );`; + injectFunction(code, sourceURLs[i]); }); diff --git a/local-cli/server/util/attachHMRServer.js b/local-cli/server/util/attachHMRServer.js index e0495e3d700..22323d071e2 100644 --- a/local-cli/server/util/attachHMRServer.js +++ b/local-cli/server/util/attachHMRServer.js @@ -8,6 +8,7 @@ */ 'use strict'; +const getInverseDependencies = require('node-haste').getInverseDependencies; const querystring = require('querystring'); const url = require('url'); @@ -23,9 +24,10 @@ function attachHMRServer({httpServer, path, packagerServer}) { packagerServer.setHMRFileChangeListener(null); } - // Returns a promise with the full list of dependencies and the shallow - // dependencies each file on the dependency list has for the give platform - // and entry file. + // For the give platform and entry file, returns a promise with: + // - The full list of dependencies. + // - The shallow dependencies each file on the dependency list has + // - Inverse shallow dependencies map function getDependencies(platform, bundleEntry) { return packagerServer.getDependencies({ platform: platform, @@ -70,12 +72,16 @@ function attachHMRServer({httpServer, path, packagerServer}) { dependenciesModulesCache[depName] = dep; }); })).then(() => { - return { - dependenciesCache, - dependenciesModulesCache, - shallowDependencies, - resolutionResponse: response, - }; + return getInverseDependencies(response) + .then(inverseDependenciesCache => { + return { + dependenciesCache, + dependenciesModulesCache, + shallowDependencies, + inverseDependenciesCache, + resolutionResponse: response, + }; + }); }); }); }); @@ -97,6 +103,7 @@ function attachHMRServer({httpServer, path, packagerServer}) { dependenciesCache, dependenciesModulesCache, shallowDependencies, + inverseDependenciesCache, }) => { client = { ws, @@ -105,6 +112,7 @@ function attachHMRServer({httpServer, path, packagerServer}) { dependenciesCache, dependenciesModulesCache, shallowDependencies, + inverseDependenciesCache, }; packagerServer.setHMRFileChangeListener((filename, stat) => { @@ -151,6 +159,7 @@ function attachHMRServer({httpServer, path, packagerServer}) { dependenciesCache, dependenciesModulesCache, shallowDependencies, + inverseDependenciesCache, resolutionResponse, }) => { if (!client) { @@ -211,7 +220,8 @@ function attachHMRServer({httpServer, path, packagerServer}) { return JSON.stringify({ type: 'update', body: { - modules: bundle.getModulesCode(), + modules: bundle.getModulesNamesAndCode(), + inverseDependencies: inverseDependenciesCache, sourceURLs: bundle.getSourceURLs(), sourceMappingURLs: bundle.getSourceMappingURLs(), }, diff --git a/packager/react-packager/src/Bundler/HMRBundle.js b/packager/react-packager/src/Bundler/HMRBundle.js index 49a21dfd8bc..72850778846 100644 --- a/packager/react-packager/src/Bundler/HMRBundle.js +++ b/packager/react-packager/src/Bundler/HMRBundle.js @@ -26,9 +26,6 @@ class HMRBundle extends BundleBase { module, transformed.code, ).then(({name, code}) => { - // need to be in single line so that lines match on sourcemaps - code = `__accept(${JSON.stringify(name)}, function(global, require, module, exports) { ${code} });`; - const moduleTransport = new ModuleTransport({ code, name, @@ -44,8 +41,13 @@ class HMRBundle extends BundleBase { }); } - getModulesCode() { - return this._modules.map(module => module.code); + getModulesNamesAndCode() { + return this._modules.map(module => { + return { + name: JSON.stringify(module.name), + code: module.code, + }; + }); } getSourceURLs() { diff --git a/packager/react-packager/src/Resolver/polyfills/require.js b/packager/react-packager/src/Resolver/polyfills/require.js index 764e4dd1e2e..d5b0cd81a2f 100644 --- a/packager/react-packager/src/Resolver/polyfills/require.js +++ b/packager/react-packager/src/Resolver/polyfills/require.js @@ -94,12 +94,12 @@ global.__d = define; global.require = require; if (__DEV__) { // HMR - function accept(id, factory) { + function accept(id, factory, inverseDependencies) { var mod = modules[id]; if (!mod) { define(id, factory); - return; // new modules don't need to be accepted + return true; // new modules don't need to be accepted } if (!mod.module.hot) { @@ -107,22 +107,51 @@ if (__DEV__) { // HMR 'Cannot accept module because Hot Module Replacement ' + 'API was not installed.' ); - return; + return false; } + // replace and initialize factory + if (factory) { + mod.factory = factory; + } + mod.isInitialized = false; + require(id); + if (mod.module.hot.acceptCallback) { - mod.factory = factory; - mod.isInitialized = false; - require(id); - mod.module.hot.acceptCallback(); + return true; } else { - console.warn( - '[HMR] Module `' + id + '` can\'t be hot reloaded because it\'s not a ' + - 'React component. To get the changes reload the JS bundle.' - ); + // need to have inverseDependencies to bubble up accept + if (!inverseDependencies) { + throw new Error('Undefined `inverseDependencies`'); + } + + // accept parent modules recursively up until all siblings are accepted + return acceptAll(inverseDependencies[id], inverseDependencies); } } + function acceptAll(modules, inverseDependencies) { + if (modules.length === 0) { + return true; + } + + var notAccepted = modules.filter(function(module) { + return !accept(module, /*factory*/ undefined, inverseDependencies); + }); + + var parents = []; + for (var i = 0; i < notAccepted.length; i++) { + // if this the module has no parents then the change cannot be hot loaded + if (inverseDependencies[notAccepted[i]].length === 0) { + return false; + } + + parents.pushAll(inverseDependencies[notAccepted[i]]); + } + + return acceptAll(parents, inverseDependencies); + } + global.__accept = accept; }