diff --git a/local-cli/server/util/__tests__/getInverseDependencies-test.js b/local-cli/server/util/__tests__/getInverseDependencies-test.js deleted file mode 100644 index 5936f3e113b8dd..00000000000000 --- a/local-cli/server/util/__tests__/getInverseDependencies-test.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @emails oncall+javascript_foundation - */ -'use strict'; - -jest.dontMock('../getInverseDependencies'); - -const getInverseDependencies = require('../getInverseDependencies'); - -describe('getInverseDependencies', () => { - it('', () => { - const module1 = createModule('module1', ['module2', 'module3']); - const module2 = createModule('module2', ['module3', 'module4']); - const module3 = createModule('module3', ['module4']); - const module4 = createModule('module4', []); - - const modulePairs = { - 'module1': [['module2', module2], ['module3', module3]], - 'module2': [['module3', module3], ['module4', module4]], - 'module3': [['module4', module4]], - 'module4': [], - }; - - const resolutionResponse = { - dependencies: [module1, module2, module3, module4], - getResolvedDependencyPairs: module => { - return modulePairs[module.hash()]; - }, - }; - - const dependencies = getInverseDependencies(resolutionResponse); - const actual = // jest can't compare maps and sets - Array.from(dependencies.entries()) - .map(([key, value]) => [key, Array.from(value)]); - - expect(actual).toEqual([ - [module2, [module1]], - [module3, [module1, module2]], - [module4, [module2, module3]], - ]); - }); -}); - -function createModule(name, dependencies) { - return { - hash: () => name, - getName: () => Promise.resolve(name), - getDependencies: () => Promise.resolve(dependencies), - }; -} diff --git a/local-cli/server/util/attachHMRServer.js b/local-cli/server/util/attachHMRServer.js deleted file mode 100644 index 184eb78febded2..00000000000000 --- a/local-cli/server/util/attachHMRServer.js +++ /dev/null @@ -1,459 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow - */ - -'use strict'; - -const getInverseDependencies = require('./getInverseDependencies'); -const querystring = require('querystring'); -const url = require('url'); - -import type {ResolutionResponse} from './getInverseDependencies'; -import type {Server as HTTPServer} from 'http'; -import type {Server as HTTPSServer} from 'https'; -import type {Client as WebSocketClient} from 'ws'; - -const blacklist = [ - 'Libraries/Utilities/HMRClient.js', -]; - -type HMRBundle = { - getModulesIdsAndCode(): Array<{id: string, code: string}>, - getSourceMappingURLs(): Array, - getSourceURLs(): Array, - isEmpty(): boolean, -}; - -type DependencyOptions = {| - +dev: boolean, - +entryFile: string, - +hot: boolean, - +minify: boolean, - +platform: ?string, - +recursive: boolean, - +rootEntryFile: string, - +bundlingOptions?: Object, -|}; - -/** - * This is a subset of the actual `metro-bundler`'s `Server` class, - * without all the stuff we don't need to know about. This allows us to use - * `attachHMRServer` with different versions of `metro-bundler` as long as - * this specific contract is maintained. - */ -type PackagerServer = { - buildBundleForHMR( - options: {platform: ?string}, - host: string, - port: number, - ): Promise, - getDependencies(options: DependencyOptions): Promise>, - getModuleForPath(entryFile: string): Promise, - getShallowDependencies(options: DependencyOptions): Promise>, - setHMRFileChangeListener(listener: ?(type: string, filePath: string) => mixed): void, -}; - -type HMROptions = { - httpServer: HTTPServer | HTTPSServer, - packagerServer: PackagerServer, - path: string, -}; - -type Moduleish = { - getName(): string, - isAsset(): boolean, - isJSON(): boolean, - path: string, -}; - -/** - * Attaches a WebSocket based connection to the Packager to expose - * Hot Module Replacement updates to the simulator. - */ -function attachHMRServer( - {httpServer, path, packagerServer}: HMROptions, -) { - type Client = {| - ws: WebSocketClient, - platform: string, - bundleEntry: string, - dependenciesCache: Array, - dependenciesModulesCache: {[mixed]: TModule}, - shallowDependencies: {[string]: Array}, - inverseDependenciesCache: mixed, - |}; - - const clients: Set = new Set(); - - function disconnect(client: Client) { - clients.delete(client); - - // If there are no clients connected, stop listenig for file changes - if (clients.size === 0) { - packagerServer.setHMRFileChangeListener(null); - } - } - - // 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 - async function getDependencies(platform: string, bundleEntry: string): Promise<{ - dependenciesCache: Array, - dependenciesModulesCache: {[mixed]: TModule}, - shallowDependencies: {[string]: Array}, - inverseDependenciesCache: mixed, - /* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error found when Flow v0.54 was deployed. To see the error - * delete this comment and run Flow. */ - resolutionResponse: ResolutionResponse, - }> { - const response = await packagerServer.getDependencies({ - dev: true, - entryFile: bundleEntry, - rootEntryFile: bundleEntry, - hot: true, - minify: false, - platform: platform, - recursive: true, - }); - - /* $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 = dep.getName(); - - if (dep.isAsset() || dep.isJSON()) { - return {path: dep.path, deps: []}; - } - const dependencies = await packagerServer.getShallowDependencies({ - dev: true, - entryFile: dep.path, - rootEntryFile: bundleEntry, - hot: true, - minify: false, - platform: platform, - recursive: true, - bundlingOptions: response.options, - }); - - 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); - } - - /* $FlowFixMe(>=0.56.0 site=react_native_oss) This comment suppresses an - * error found when Flow v0.56 was deployed. To see the error delete this - * comment and run Flow. */ - /* $FlowFixMe(>=0.56.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error found when Flow v0.56 was deployed. To see the error - * delete this comment and run Flow. */ - return { - dependenciesCache, - dependenciesModulesCache, - shallowDependencies, - inverseDependenciesCache, - resolutionResponse: response, - }; - } - - async function prepareResponse( - client: Client, - filename: string, - ): Promise { - try { - const bundle = await generateBundle(client, filename); - - if (!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( - client: Client, - filename: string, - ): Promise { - // If the main file is an asset, do not generate a bundle. - const moduleToUpdate = await packagerServer.getModuleForPath(filename); - if (moduleToUpdate.isAsset()) { - return; - } - - const deps = await packagerServer.getShallowDependencies({ - dev: true, - minify: false, - entryFile: filename, - rootEntryFile: client.bundleEntry, - hot: true, - platform: client.platform, - recursive: true, - }); - - // 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, - rootEntryFile: client.bundleEntry, - hot: true, - minify: false, - platform: client.platform, - recursive: true, - }); - - resolutionResponse = await response.copy({ - dependencies: [moduleToUpdate]}, - ); - } 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); - - // build list of modules for which we'll send HMR updates - const modulesToUpdate = [moduleToUpdate]; - Object.keys(depsModulesCache).forEach(module => { - if (!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.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; - } - - function handleFileChange( - type: string, - filename: string, - ): void { - clients.forEach( - client => sendFileChangeToClient(client, type, filename), - ); - } - - async function sendFileChangeToClient( - client: Client, - type: string, - filename: string, - ): Promise { - const blacklisted = blacklist.find( - blacklistedPath => filename.indexOf(blacklistedPath) !== -1, - ); - if (blacklisted) { - return; - } - - if (clients.has(client)) { - client.ws.send(JSON.stringify({type: 'update-start'})); - } - - if (type !== 'delete') { - const response = await prepareResponse(client, filename); - - if (response && clients.has(client)) { - client.ws.send(JSON.stringify(response)); - } - } - - if (clients.has(client)) { - client.ws.send(JSON.stringify({type: 'update-done'})); - } - } - - /* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an - * error found when Flow v0.54 was deployed. To see the error delete this - * comment and run Flow. */ - const WebSocketServer = require('ws').Server; - const wss = new WebSocketServer({ - server: httpServer, - path: path, - }); - - wss.on('connection', async ws => { - /* $FlowFixMe: url might be null */ - const params = querystring.parse(url.parse(ws.upgradeReq.url).query); - - const { - dependenciesCache, - dependenciesModulesCache, - shallowDependencies, - inverseDependenciesCache, - } = await getDependencies(params.platform, params.bundleEntry); - - const client = { - ws, - platform: params.platform, - bundleEntry: params.bundleEntry, - dependenciesCache, - dependenciesModulesCache, - shallowDependencies, - inverseDependenciesCache, - }; - clients.add(client); - - // If this is the first client connecting, start listening to file changes - if (clients.size === 1) { - packagerServer.setHMRFileChangeListener(handleFileChange); - } - - client.ws.on('error', e => { - console.error('[Hot Module Replacement] Unexpected error', e); - disconnect(client); - }); - - client.ws.on('close', () => disconnect(client)); - }); -} - -function arrayEquals(arrayA: Array, arrayB: Array): boolean { - arrayA = arrayA || []; - arrayB = arrayB || []; - return ( - arrayA.length === arrayB.length && - arrayA.every((element, index) => { - return element === arrayB[index]; - }) - ); -} - -module.exports = attachHMRServer; diff --git a/local-cli/server/util/getInverseDependencies.js b/local-cli/server/util/getInverseDependencies.js deleted file mode 100644 index 927af924672998..00000000000000 --- a/local-cli/server/util/getInverseDependencies.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @flow - * @format - */ - -'use strict'; - -/** - * This is a subset of the actual `metro-bundler`'s `ResolutionResponse` class, - * without all the stuff we don't need to know about. This allows us to use - * `getInverseDependencies` with different versions of `metro-bundler`. - */ -export type ResolutionResponse = { - copy(data: { - dependencies?: Array, - mainModuleId?: number, - mocks?: mixed, - }): ResolutionResponse, - dependencies: Array, - getResolvedDependencyPairs( - module: TModule, - ): $ReadOnlyArray<[string, TModule]>, - options: Object, -}; - -function resolveModuleRequires( - resolutionResponse: ResolutionResponse, - module: TModule, -): Array { - const pairs = resolutionResponse.getResolvedDependencyPairs(module); - return pairs ? pairs.map(([, dependencyModule]) => dependencyModule) : []; -} - -function getModuleDependents( - cache: Map>, - module: TModule, -): Set { - let dependents = cache.get(module); - if (!dependents) { - dependents = new Set(); - cache.set(module, dependents); - } - return dependents; -} - -/** - * Returns an object that indicates in which module each module is required. - */ -function getInverseDependencies( - resolutionResponse: ResolutionResponse, -): Map> { - const cache = new Map(); - - resolutionResponse.dependencies.forEach(module => { - resolveModuleRequires(resolutionResponse, module).forEach(dependency => { - getModuleDependents(cache, dependency).add(module); - }); - }); - - return cache; -} - -module.exports = getInverseDependencies;