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
This commit is contained in:
Martín Bigio 2016-02-26 15:14:47 -08:00 committed by Facebook Github Bot 4
parent d7cee3a5f9
commit 436db67126
4 changed files with 84 additions and 30 deletions

View File

@ -73,9 +73,12 @@ Error: ${e.message}`
break; break;
} }
case 'update': { case 'update': {
const modules = data.body.modules; const {
const sourceMappingURLs = data.body.sourceMappingURLs; modules,
const sourceURLs = data.body.sourceURLs; sourceMappingURLs,
sourceURLs,
inverseDependencies,
} = data.body;
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
const RCTRedBox = require('NativeModules').RedBox; const RCTRedBox = require('NativeModules').RedBox;
@ -85,7 +88,7 @@ Error: ${e.message}`
RCTExceptionsManager && RCTExceptionsManager.dismissRedbox && RCTExceptionsManager.dismissRedbox(); RCTExceptionsManager && RCTExceptionsManager.dismissRedbox && RCTExceptionsManager.dismissRedbox();
} }
modules.forEach((code, i) => { modules.forEach(({name, code}, i) => {
code = code + '\n\n' + sourceMappingURLs[i]; code = code + '\n\n' + sourceMappingURLs[i];
require('SourceMapsCache').fetch({ require('SourceMapsCache').fetch({
@ -101,6 +104,16 @@ Error: ${e.message}`
? global.nativeInjectHMRUpdate ? global.nativeInjectHMRUpdate
: eval; : 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]); injectFunction(code, sourceURLs[i]);
}); });

View File

@ -8,6 +8,7 @@
*/ */
'use strict'; 'use strict';
const getInverseDependencies = require('node-haste').getInverseDependencies;
const querystring = require('querystring'); const querystring = require('querystring');
const url = require('url'); const url = require('url');
@ -23,9 +24,10 @@ function attachHMRServer({httpServer, path, packagerServer}) {
packagerServer.setHMRFileChangeListener(null); packagerServer.setHMRFileChangeListener(null);
} }
// Returns a promise with the full list of dependencies and the shallow // For the give platform and entry file, returns a promise with:
// dependencies each file on the dependency list has for the give platform // - The full list of dependencies.
// and entry file. // - The shallow dependencies each file on the dependency list has
// - Inverse shallow dependencies map
function getDependencies(platform, bundleEntry) { function getDependencies(platform, bundleEntry) {
return packagerServer.getDependencies({ return packagerServer.getDependencies({
platform: platform, platform: platform,
@ -70,12 +72,16 @@ function attachHMRServer({httpServer, path, packagerServer}) {
dependenciesModulesCache[depName] = dep; dependenciesModulesCache[depName] = dep;
}); });
})).then(() => { })).then(() => {
return { return getInverseDependencies(response)
dependenciesCache, .then(inverseDependenciesCache => {
dependenciesModulesCache, return {
shallowDependencies, dependenciesCache,
resolutionResponse: response, dependenciesModulesCache,
}; shallowDependencies,
inverseDependenciesCache,
resolutionResponse: response,
};
});
}); });
}); });
}); });
@ -97,6 +103,7 @@ function attachHMRServer({httpServer, path, packagerServer}) {
dependenciesCache, dependenciesCache,
dependenciesModulesCache, dependenciesModulesCache,
shallowDependencies, shallowDependencies,
inverseDependenciesCache,
}) => { }) => {
client = { client = {
ws, ws,
@ -105,6 +112,7 @@ function attachHMRServer({httpServer, path, packagerServer}) {
dependenciesCache, dependenciesCache,
dependenciesModulesCache, dependenciesModulesCache,
shallowDependencies, shallowDependencies,
inverseDependenciesCache,
}; };
packagerServer.setHMRFileChangeListener((filename, stat) => { packagerServer.setHMRFileChangeListener((filename, stat) => {
@ -151,6 +159,7 @@ function attachHMRServer({httpServer, path, packagerServer}) {
dependenciesCache, dependenciesCache,
dependenciesModulesCache, dependenciesModulesCache,
shallowDependencies, shallowDependencies,
inverseDependenciesCache,
resolutionResponse, resolutionResponse,
}) => { }) => {
if (!client) { if (!client) {
@ -211,7 +220,8 @@ function attachHMRServer({httpServer, path, packagerServer}) {
return JSON.stringify({ return JSON.stringify({
type: 'update', type: 'update',
body: { body: {
modules: bundle.getModulesCode(), modules: bundle.getModulesNamesAndCode(),
inverseDependencies: inverseDependenciesCache,
sourceURLs: bundle.getSourceURLs(), sourceURLs: bundle.getSourceURLs(),
sourceMappingURLs: bundle.getSourceMappingURLs(), sourceMappingURLs: bundle.getSourceMappingURLs(),
}, },

View File

@ -26,9 +26,6 @@ class HMRBundle extends BundleBase {
module, module,
transformed.code, transformed.code,
).then(({name, 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({ const moduleTransport = new ModuleTransport({
code, code,
name, name,
@ -44,8 +41,13 @@ class HMRBundle extends BundleBase {
}); });
} }
getModulesCode() { getModulesNamesAndCode() {
return this._modules.map(module => module.code); return this._modules.map(module => {
return {
name: JSON.stringify(module.name),
code: module.code,
};
});
} }
getSourceURLs() { getSourceURLs() {

View File

@ -94,12 +94,12 @@ global.__d = define;
global.require = require; global.require = require;
if (__DEV__) { // HMR if (__DEV__) { // HMR
function accept(id, factory) { function accept(id, factory, inverseDependencies) {
var mod = modules[id]; var mod = modules[id];
if (!mod) { if (!mod) {
define(id, factory); 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) { if (!mod.module.hot) {
@ -107,22 +107,51 @@ if (__DEV__) { // HMR
'Cannot accept module because Hot Module Replacement ' + 'Cannot accept module because Hot Module Replacement ' +
'API was not installed.' '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) { if (mod.module.hot.acceptCallback) {
mod.factory = factory;
mod.isInitialized = false;
require(id);
mod.module.hot.acceptCallback(); mod.module.hot.acceptCallback();
return true;
} else { } else {
console.warn( // need to have inverseDependencies to bubble up accept
'[HMR] Module `' + id + '` can\'t be hot reloaded because it\'s not a ' + if (!inverseDependencies) {
'React component. To get the changes reload the JS bundle.' 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; global.__accept = accept;
} }