diff --git a/local-cli/server/util/attachHMRServer.js b/local-cli/server/util/attachHMRServer.js index 30c86f8f6..572d3792a 100644 --- a/local-cli/server/util/attachHMRServer.js +++ b/local-cli/server/util/attachHMRServer.js @@ -22,16 +22,76 @@ function attachHMRServer({httpServer, path, packagerServer}) { client = 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. + function getDependencies(platform, bundleEntry) { + return packagerServer.getDependencies({ + platform: platform, + dev: true, + entryFile: bundleEntry, + }).then(response => { + // for each dependency builds the object: + // `{path: '/a/b/c.js', deps: ['modA', 'modB', ...]}` + return Promise.all(Object.values(response.dependencies).map(dep => { + if (dep.isAsset() || dep.isAsset_DEPRECATED() || dep.isJSON()) { + return Promise.resolve({path: dep.path, deps: []}); + } + return packagerServer.getShallowDependencies(dep.path) + .then(deps => { + return { + path: dep.path, + deps, + }; + }); + })) + .then(deps => { + // list with all the dependencies the bundle entry has + const dependenciesCache = response.dependencies.map(dep => dep.path); + + // map that indicates the shallow dependency each file included on the + // bundle has + const shallowDependencies = {}; + deps.forEach(dep => shallowDependencies[dep.path] = dep.deps); + + return {dependenciesCache, shallowDependencies}; + }); + }); + } + packagerServer.addFileChangeListener(filename => { if (!client) { return; } - packagerServer.buildBundleForHMR({ - entryFile: filename, - platform: client.platform, - }) - .then(bundle => client.ws.send(bundle)); + packagerServer.getShallowDependencies(filename) + .then(deps => { + // if the file dependencies have change we need to invalidate the + // dependencies caches because the list of files we need to send to the + // client may have changed + if (arrayEquals(deps, client.shallowDependencies[filename])) { + return Promise.resolve(); + } + return getDependencies(client.platform, client.bundleEntry) + .then(({dependenciesCache, shallowDependencies}) => { + // invalidate caches + client.dependenciesCache = dependenciesCache; + client.shallowDependencies = shallowDependencies; + }); + }) + .then(() => { + // make sure the file was modified is part of the bundle + if (!client.shallowDependencies[filename]) { + return; + } + + return packagerServer.buildBundleForHMR({ + platform: client.platform, + entryFile: filename, + }) + .then(bundle => client.ws.send(bundle)); + }) + .done(); }); const WebSocketServer = require('ws').Server; @@ -44,19 +104,37 @@ function attachHMRServer({httpServer, path, packagerServer}) { wss.on('connection', ws => { console.log('[Hot Module Replacement] Client connected'); const params = querystring.parse(url.parse(ws.upgradeReq.url).query); - client = { - ws, - platform: params.platform, - bundleEntry: params.bundleEntry, - }; - client.ws.on('error', e => { - console.error('[Hot Module Replacement] Unexpected error', e); - disconnect(); - }); + getDependencies(params.platform, params.bundleEntry) + .then(({dependenciesCache, shallowDependencies}) => { + client = { + ws, + platform: params.platform, + bundleEntry: params.bundleEntry, + dependenciesCache, + shallowDependencies, + }; - client.ws.on('close', () => disconnect()); + client.ws.on('error', e => { + console.error('[Hot Module Replacement] Unexpected error', e); + disconnect(); + }); + + client.ws.on('close', () => disconnect()); + }) + .done(); }); } +function arrayEquals(arrayA, arrayB) { + arrayA = arrayA || []; + arrayB = arrayB || []; + return ( + arrayA.length === arrayB.length && + arrayA.every((element, index) => { + return element === arrayB[index]; + }) + ); +} + module.exports = attachHMRServer; diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index a2fd93be4..dc76a267f 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -310,6 +310,10 @@ class Bundler { this._transformer.invalidateFile(filePath); } + getShallowDependencies(entryFile) { + return this._resolver.getShallowDependencies(entryFile); + } + getDependencies(main, isDev, platform) { return this._resolver.getDependencies(main, { dev: isDev, platform }); } diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js index 69072339f..ac88a2e37 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -133,6 +133,14 @@ class DependencyGraph { return this._loading; } + /** + * Returns a promise with the direct dependencies the module associated to + * the given entryPath has. + */ + getShallowDependencies(entryPath) { + return this._moduleCache.getModule(entryPath).getDependencies(); + } + getDependencies(entryPath, platform) { return this.load().then(() => { platform = this._getRequestPlatform(entryPath, platform); diff --git a/packager/react-packager/src/Resolver/index.js b/packager/react-packager/src/Resolver/index.js index be7e8a5eb..d0f922856 100644 --- a/packager/react-packager/src/Resolver/index.js +++ b/packager/react-packager/src/Resolver/index.js @@ -99,6 +99,10 @@ class Resolver { this._polyfillModuleNames = opts.polyfillModuleNames || []; } + getShallowDependencies(entryFile) { + return this._depGraph.getShallowDependencies(entryFile); + } + getDependencies(main, options) { const opts = getDependenciesValidateOpts(options); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 915203958..a6bb6b344 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -246,6 +246,10 @@ class Server { }); } + getShallowDependencies(entryFile) { + return this._bundler.getShallowDependencies(entryFile); + } + getDependencies(options) { return Promise.resolve().then(() => { if (!options.platform) {