From 436db67126f1e64d1889c2d084b77c98c8d63f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Fri, 26 Feb 2016 15:14:47 -0800 Subject: [PATCH] Allow parents to accept children modules Summary:In order to be able to Hot Load Redux stores and modules that export functions, we need to build infrastructure to bubble up the HMR updates similar to how webpack does: https://webpack.github.io/docs/hot-module-replacement-with-webpack.html. In here we introduce the minimum of this infrastructure we need to make this work. The Packager server needs to send the inverse dependencies to the HMR runtime that runs on the client so that it can bubble up the patches if they cannot be self accepted by the module that was changed. This diff relies on https://github.com/facebook/node-haste/pull/40/files which adds support for getting the inverse dependencies. Reviewed By: davidaurelio Differential Revision: D2950662 fb-gh-sync-id: 26dcd4aa15da76a727026a9d7ee06e7ae4d22eaa shipit-source-id: 26dcd4aa15da76a727026a9d7ee06e7ae4d22eaa --- Libraries/Utilities/HMRClient.js | 21 ++++++-- local-cli/server/util/attachHMRServer.js | 30 +++++++---- .../react-packager/src/Bundler/HMRBundle.js | 12 +++-- .../src/Resolver/polyfills/require.js | 51 +++++++++++++++---- 4 files changed, 84 insertions(+), 30 deletions(-) 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; }