From f32d0eed17b185b5db024bf671097163ca89764e Mon Sep 17 00:00:00 2001 From: Rafael Oleza Date: Fri, 11 Aug 2017 12:17:13 -0700 Subject: [PATCH] Refactor attachHMRServer to use async/await Reviewed By: davidaurelio Differential Revision: D5603552 fbshipit-source-id: 8fc5f9bcd35a6adf3e3cdf5de47384d8420601cd --- local-cli/server/util/attachHMRServer.js | 541 ++++++++++++----------- 1 file changed, 277 insertions(+), 264 deletions(-) diff --git a/local-cli/server/util/attachHMRServer.js b/local-cli/server/util/attachHMRServer.js index e24489180..7fb8aedb6 100644 --- a/local-cli/server/util/attachHMRServer.js +++ b/local-cli/server/util/attachHMRServer.js @@ -88,89 +88,253 @@ function attachHMRServer( // - The full list of dependencies. // - The shallow dependencies each file on the dependency list has // - Inverse shallow dependencies map - function getDependencies(platform: string, bundleEntry: string): Promise<{ + async function getDependencies(platform: string, bundleEntry: string): Promise<{ dependenciesCache: Array, dependenciesModulesCache: {[mixed]: TModule}, shallowDependencies: {[string]: Array}, inverseDependenciesCache: mixed, resolutionResponse: ResolutionResponse, }> { - return packagerServer.getDependencies({ + const response = await packagerServer.getDependencies({ dev: true, entryFile: bundleEntry, hot: true, minify: false, platform: platform, recursive: true, - }).then(response => { - /* $FlowFixMe: getModuleId might be null */ - const {getModuleId}: {getModuleId: () => number} = response; - - // for each dependency builds the object: - // `{path: '/a/b/c.js', deps: ['modA', 'modB', ...]}` - return Promise.all(response.dependencies.map((dep: TModule) => { - return dep.getName().then(depName => { - if (dep.isAsset() || dep.isJSON()) { - return Promise.resolve({path: dep.path, deps: []}); - } - return packagerServer.getShallowDependencies({ - dev: true, - entryFile: dep.path, - hot: true, - minify: false, - platform: platform, - recursive: true, - }).then(deps => { - return { - path: dep.path, - name: depName, - deps, - }; - }); - }); - })) - .then((deps: Array<{path: string, name?: string, deps: Array}>) => { - // list with all the dependencies' filenames the bundle entry has - const dependenciesCache = response.dependencies.map(dep => dep.path); - - // map from module name to path - const moduleToFilenameCache = Object.create(null); - deps.forEach(dep => { - /* $FlowFixMe: `name` could be null, but `deps` would be as well. */ - moduleToFilenameCache[dep.name] = dep.path; - }); - - // map that indicates the shallow dependency each file included on the - // bundle has - const shallowDependencies = Object.create(null); - deps.forEach(dep => { - shallowDependencies[dep.path] = dep.deps; - }); - - // map from module name to the modules' dependencies the bundle entry - // has - const dependenciesModulesCache = Object.create(null); - response.dependencies.forEach(dep => { - dependenciesModulesCache[getModuleId(dep)] = dep; - }); - - - const inverseDependenciesCache = Object.create(null); - const inverseDependencies = getInverseDependencies(response); - for (const [module, dependents] of inverseDependencies) { - inverseDependenciesCache[getModuleId(module)] = - Array.from(dependents).map(getModuleId); - } - - return { - dependenciesCache, - dependenciesModulesCache, - shallowDependencies, - inverseDependenciesCache, - resolutionResponse: response, - }; - }); }); + + /* $FlowFixMe: getModuleId might be null */ + const {getModuleId}: {getModuleId: () => number} = response; + + // for each dependency builds the object: + // `{path: '/a/b/c.js', deps: ['modA', 'modB', ...]}` + const deps: Array<{ + path: string, + name?: string, + deps: Array, + }> = await Promise.all(response.dependencies.map(async (dep: TModule) => { + const depName = await dep.getName(); + + if (dep.isAsset() || dep.isJSON()) { + return {path: dep.path, deps: []}; + } + const dependencies = await packagerServer.getShallowDependencies({ + dev: true, + entryFile: dep.path, + hot: true, + minify: false, + platform: platform, + recursive: true, + }); + + return { + path: dep.path, + name: depName, + deps: dependencies, + }; + })); + + // list with all the dependencies' filenames the bundle entry has + const dependenciesCache = response.dependencies.map(dep => dep.path); + + // map from module name to path + const moduleToFilenameCache = Object.create(null); + deps.forEach(dep => { + /* $FlowFixMe: `name` could be null, but `deps` would be as well. */ + moduleToFilenameCache[dep.name] = dep.path; + }); + + // map that indicates the shallow dependency each file included on the + // bundle has + const shallowDependencies = Object.create(null); + deps.forEach(dep => { + shallowDependencies[dep.path] = dep.deps; + }); + + // map from module name to the modules' dependencies the bundle entry + // has + const dependenciesModulesCache = Object.create(null); + response.dependencies.forEach(dep => { + dependenciesModulesCache[getModuleId(dep)] = dep; + }); + + const inverseDependenciesCache = Object.create(null); + const inverseDependencies = getInverseDependencies(response); + for (const [module, dependents] of inverseDependencies) { + inverseDependenciesCache[getModuleId(module)] = + Array.from(dependents).map(getModuleId); + } + + return { + dependenciesCache, + dependenciesModulesCache, + shallowDependencies, + inverseDependenciesCache, + resolutionResponse: response, + }; + } + + async function prepareResponse(filename): Object { + try { + const bundle = await generateBundle(filename); + + if (!client || !bundle || bundle.isEmpty()) { + return; + } + + return { + type: 'update', + body: { + modules: bundle.getModulesIdsAndCode(), + inverseDependencies: client.inverseDependenciesCache, + sourceURLs: bundle.getSourceURLs(), + sourceMappingURLs: bundle.getSourceMappingURLs(), + }, + }; + } catch (error) { + // send errors to the client instead of killing packager server + let body; + if (error.type === 'TransformError' || + error.type === 'NotFoundError' || + error.type === 'UnableToResolveError') { + body = { + type: error.type, + description: error.description, + filename: error.filename, + lineNumber: error.lineNumber, + }; + } else { + console.error(error.stack || error); + body = { + type: 'InternalError', + description: 'react-packager has encountered an internal error, ' + + 'please check your terminal error output for more details', + }; + } + + return {type: 'error', body}; + } + } + + async function generateBundle(filename) { + if (client === null) { + return; + } + + const deps = await packagerServer.getShallowDependencies({ + dev: true, + minify: false, + entryFile: filename, + hot: true, + platform: client.platform, + recursive: true, + }); + + if (client === null) { + return; + } + + // 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 + const oldDependencies = client.shallowDependencies[filename]; + + let resolutionResponse; + + if (arrayEquals(deps, oldDependencies)) { + // Need to create a resolution response to pass to the bundler + // to process requires after transform. By providing a + // specific response we can compute a non recursive one which + // is the least we need and improve performance. + const response = await packagerServer.getDependencies({ + dev: true, + entryFile: filename, + hot: true, + minify: false, + platform: client.platform, + recursive: true, + }); + + const module = await packagerServer.getModuleForPath(filename); + + resolutionResponse = await response.copy({dependencies: [module]}); + } else { + // if there're new dependencies compare the full list of + // dependencies we used to have with the one we now have + const { + dependenciesCache: depsCache, + dependenciesModulesCache: depsModulesCache, + shallowDependencies: shallowDeps, + inverseDependenciesCache: inverseDepsCache, + resolutionResponse: myResolutionReponse, + } = await getDependencies(client.platform, client.bundleEntry); + + if (client === null) { + return; + } + + const moduleToUpdate = await packagerServer.getModuleForPath(filename); + + if (client === null) { + return; + } + + // build list of modules for which we'll send HMR updates + const modulesToUpdate = [moduleToUpdate]; + Object.keys(depsModulesCache).forEach(module => { + if (!client || !client.dependenciesModulesCache[module]) { + modulesToUpdate.push(depsModulesCache[module]); + } + }); + + // Need to send modules to the client in an order it can + // process them: if a new dependency graph was uncovered + // because a new dependency was added, the file that was + // changed, which is the root of the dependency tree that + // will be sent, needs to be the last module that gets + // processed. Reversing the new modules makes sense + // because we get them through the resolver which returns + // a BFS ordered list. + modulesToUpdate.reverse(); + + // invalidate caches + client.dependenciesCache = depsCache; + client.dependenciesModulesCache = depsModulesCache; + client.shallowDependencies = shallowDeps; + client.inverseDependenciesCache = inverseDepsCache; + + resolutionResponse = await myResolutionReponse.copy({ + dependencies: modulesToUpdate, + }); + } + + // make sure the file was modified is part of the bundle + if (!client || !client.shallowDependencies[filename]) { + return; + } + + const httpServerAddress = httpServer.address(); + + // Sanitize the value from the HTTP server + let packagerHost = 'localhost'; + if (httpServer.address().address && + httpServer.address().address !== '::' && + httpServer.address().address !== '') { + packagerHost = httpServerAddress.address; + } + + const bundle: HMRBundle = await packagerServer.buildBundleForHMR( + { + entryFile: client.bundleEntry, + platform: client.platform, + resolutionResponse, + }, + packagerHost, + httpServerAddress.port, + ); + + return bundle; } const WebSocketServer = require('ws').Server; @@ -179,213 +343,62 @@ function attachHMRServer( path: path, }); - wss.on('connection', ws => { + wss.on('connection', async ws => { /* $FlowFixMe: url might be null */ const params = querystring.parse(url.parse(ws.upgradeReq.url).query); - getDependencies(params.platform, params.bundleEntry) - .then(({ + try { + const { dependenciesCache, dependenciesModulesCache, shallowDependencies, inverseDependenciesCache, - }) => { - client = { - ws, - platform: params.platform, - bundleEntry: params.bundleEntry, - dependenciesCache, - dependenciesModulesCache, - shallowDependencies, - inverseDependenciesCache, - }; + } = await getDependencies(params.platform, params.bundleEntry); - packagerServer.setHMRFileChangeListener((type, filename) => { - if (!client) { - return; + client = { + ws, + platform: params.platform, + bundleEntry: params.bundleEntry, + dependenciesCache, + dependenciesModulesCache, + shallowDependencies, + inverseDependenciesCache, + }; + + packagerServer.setHMRFileChangeListener(async (type, filename) => { + if (client === null) { + return; + } + + const blacklisted = blacklist.find( + blacklistedPath => filename.indexOf(blacklistedPath) !== -1, + ); + if (blacklisted) { + return; + } + + client.ws.send(JSON.stringify({type: 'update-start'})); + + if (type !== 'delete') { + const response = await prepareResponse(filename); + + if (client && response) { + client.ws.send(JSON.stringify(response)); } + } - const blacklisted = blacklist.find(blacklistedPath => - filename.indexOf(blacklistedPath) !== -1 - ); + client.ws.send(JSON.stringify({type: 'update-done'})); + }); - if (blacklisted) { - return; - } + client.ws.on('error', e => { + console.error('[Hot Module Replacement] Unexpected error', e); + disconnect(); + }); - client.ws.send(JSON.stringify({type: 'update-start'})); - const promise = type === 'delete' - ? Promise.resolve() - : packagerServer.getShallowDependencies({ - dev: true, - minify: false, - entryFile: filename, - hot: true, - platform: client.platform, - recursive: true, - }).then(deps => { - if (!client) { - return []; - } - - // 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 - const oldDependencies = client.shallowDependencies[filename]; - if (arrayEquals(deps, oldDependencies)) { - // Need to create a resolution response to pass to the bundler - // to process requires after transform. By providing a - // specific response we can compute a non recursive one which - // is the least we need and improve performance. - return packagerServer.getDependencies({ - dev: true, - entryFile: filename, - hot: true, - minify: false, - platform: client.platform, - recursive: true, - }).then(response => { - return packagerServer.getModuleForPath(filename).then(module => { - return response.copy({dependencies: [module]}); - }); - }); - } - - // if there're new dependencies compare the full list of - // dependencies we used to have with the one we now have - return getDependencies(client.platform, client.bundleEntry) - .then(({ - dependenciesCache: depsCache, - dependenciesModulesCache: depsModulesCache, - shallowDependencies: shallowDeps, - inverseDependenciesCache: inverseDepsCache, - resolutionResponse, - }) => { - if (!client) { - return {}; - } - - const nonNullClient = client; - - return packagerServer.getModuleForPath(filename).then(moduleToUpdate => { - // build list of modules for which we'll send HMR updates - const modulesToUpdate = [moduleToUpdate]; - Object.keys(depsModulesCache).forEach(module => { - if (!nonNullClient.dependenciesModulesCache[module]) { - modulesToUpdate.push(depsModulesCache[module]); - } - }); - - // Need to send modules to the client in an order it can - // process them: if a new dependency graph was uncovered - // because a new dependency was added, the file that was - // changed, which is the root of the dependency tree that - // will be sent, needs to be the last module that gets - // processed. Reversing the new modules makes sense - // because we get them through the resolver which returns - // a BFS ordered list. - modulesToUpdate.reverse(); - - // invalidate caches - nonNullClient.dependenciesCache = depsCache; - nonNullClient.dependenciesModulesCache = depsModulesCache; - nonNullClient.shallowDependencies = shallowDeps; - nonNullClient.inverseDependenciesCache = inverseDepsCache; - - return resolutionResponse.copy({ - dependencies: modulesToUpdate - }); - }); - }); - }) - .then((resolutionResponse) => { - if (!client) { - return; - } - - // make sure the file was modified is part of the bundle - if (!client.shallowDependencies[filename]) { - return; - } - - const httpServerAddress = httpServer.address(); - - // Sanitize the value from the HTTP server - let packagerHost = 'localhost'; - if (httpServer.address().address && - httpServer.address().address !== '::' && - httpServer.address().address !== '') { - packagerHost = httpServerAddress.address; - } - - return packagerServer.buildBundleForHMR({ - entryFile: client.bundleEntry, - platform: client.platform, - resolutionResponse, - }, packagerHost, httpServerAddress.port); - }) - .then((bundle: HMRBundle) => { - if (!client || !bundle || bundle.isEmpty()) { - return; - } - - return JSON.stringify({ - type: 'update', - body: { - modules: bundle.getModulesIdsAndCode(), - inverseDependencies: client.inverseDependenciesCache, - sourceURLs: bundle.getSourceURLs(), - sourceMappingURLs: bundle.getSourceMappingURLs(), - }, - }); - }) - .catch(error => { - // send errors to the client instead of killing packager server - let body; - if (error.type === 'TransformError' || - error.type === 'NotFoundError' || - error.type === 'UnableToResolveError') { - body = { - type: error.type, - description: error.description, - filename: error.filename, - lineNumber: error.lineNumber, - }; - } else { - console.error(error.stack || error); - body = { - type: 'InternalError', - description: 'react-packager has encountered an internal error, ' + - 'please check your terminal error output for more details', - }; - } - - return JSON.stringify({type: 'error', body}); - }) - .then(update => { - if (!client || !update) { - return; - } - - client.ws.send(update); - }); - - promise.then(() => { - /* $FlowFixMe: assume `client` non-null */ - client.ws.send(JSON.stringify({type: 'update-done'})); - }); - }); - - client.ws.on('error', e => { - console.error('[Hot Module Replacement] Unexpected error', e); - disconnect(); - }); - - client.ws.on('close', () => disconnect()); - }) - .catch(err => { + client.ws.on('close', () => disconnect()); + } catch (err) { throw err; - }); + } }); }