/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow */ 'use strict'; const GraphNotFoundError = require('./IncrementalBundler/GraphNotFoundError'); const IncrementalBundler = require('./IncrementalBundler'); const RevisionNotFoundError = require('./IncrementalBundler/RevisionNotFoundError'); const debounceAsyncQueue = require('./lib/debounceAsyncQueue'); const formatBundlingError = require('./lib/formatBundlingError'); const getGraphId = require('./lib/getGraphId'); const hmrJSBundle = require('./DeltaBundler/Serializers/hmrJSBundle'); const nullthrows = require('nullthrows'); const parseOptionsFromUrl = require('./lib/parseOptionsFromUrl'); const splitBundleOptions = require('./lib/splitBundleOptions'); const transformHelpers = require('./lib/transformHelpers'); const url = require('url'); const { Logger: {createActionStartEntry, createActionEndEntry, log}, } = require('metro-core'); import type {RevisionId} from './IncrementalBundler'; import type { HmrMessage, HmrUpdateMessage, HmrErrorMessage, } from './lib/bundle-modules/types.flow'; import type {ConfigT} from 'metro-config/src/configTypes.flow'; type Client = {| +sendFn: string => void, revisionId: RevisionId, |}; type ClientGroup = {| +clients: Set<Client>, +unlisten: () => void, revisionId: RevisionId, |}; function send(sendFns: Array<(string) => void>, message: HmrMessage) { const strMessage = JSON.stringify(message); sendFns.forEach(sendFn => sendFn(strMessage)); } /** * The HmrServer (Hot Module Reloading) implements a lightweight interface * to communicate easily to the logic in the React Native repository (which * is the one that handles the Web Socket connections). * * This interface allows the HmrServer to hook its own logic to WS clients * getting connected, disconnected or having errors (through the * `onClientConnect`, `onClientDisconnect` and `onClientError` methods). */ class HmrServer<TClient: Client> { _config: ConfigT; _bundler: IncrementalBundler; _createModuleId: (path: string) => number; _clientGroups: Map<RevisionId, ClientGroup>; constructor( bundler: IncrementalBundler, createModuleId: (path: string) => number, config: ConfigT, ) { this._config = config; this._bundler = bundler; this._createModuleId = createModuleId; this._clientGroups = new Map(); } async onClientConnect( clientUrl: string, sendFn: (data: string) => void, ): Promise<?Client> { const urlObj = nullthrows(url.parse(clientUrl, true)); const query = nullthrows(urlObj.query); let revPromise; if (query.bundleEntry != null) { // TODO(T34760695): Deprecate urlObj.pathname = query.bundleEntry.replace(/\.js$/, '.bundle'); delete query.bundleEntry; const {options} = parseOptionsFromUrl( url.format(urlObj), new Set(this._config.resolver.platforms), ); const {entryFile, transformOptions} = splitBundleOptions(options); /** * `entryFile` is relative to projectRoot, we need to use resolution function * to find the appropriate file with supported extensions. */ const resolutionFn = await transformHelpers.getResolveDependencyFn( this._bundler.getBundler(), transformOptions.platform, ); const resolvedEntryFilePath = resolutionFn( `${this._config.projectRoot}/.`, entryFile, ); const graphId = getGraphId(resolvedEntryFilePath, transformOptions); revPromise = this._bundler.getRevisionByGraphId(graphId); if (!revPromise) { send([sendFn], { type: 'error', body: formatBundlingError(new GraphNotFoundError(graphId)), }); return null; } } else { const revisionId = query.revisionId; revPromise = this._bundler.getRevision(revisionId); if (!revPromise) { send([sendFn], { type: 'error', body: formatBundlingError(new RevisionNotFoundError(revisionId)), }); return null; } } const {graph, id} = await revPromise; const client = { sendFn, revisionId: id, }; let clientGroup = this._clientGroups.get(id); if (clientGroup != null) { clientGroup.clients.add(client); } else { clientGroup = { clients: new Set([client]), unlisten: () => unlisten(), revisionId: id, }; this._clientGroups.set(id, clientGroup); const unlisten = this._bundler .getDeltaBundler() .listen( graph, debounceAsyncQueue( this._handleFileChange.bind(this, clientGroup), 50, ), ); } await this._handleFileChange(clientGroup); return client; } onClientError(client: TClient, e: Error) { this._config.reporter.update({ type: 'hmr_client_error', error: e, }); this.onClientDisconnect(client); } onClientDisconnect(client: TClient) { const group = this._clientGroups.get(client.revisionId); if (group != null) { if (group.clients.size === 1) { this._clientGroups.delete(client.revisionId); group.unlisten(); } else { group.clients.delete(client); } } } async _handleFileChange(group: ClientGroup) { const processingHmrChange = log( createActionStartEntry({action_name: 'Processing HMR change'}), ); const sendFns = [...group.clients].map(client => client.sendFn); send(sendFns, {type: 'update-start'}); const message = await this._prepareMessage(group); send(sendFns, message); send(sendFns, {type: 'update-done'}); log({ ...createActionEndEntry(processingHmrChange), outdated_modules: message.type === 'update' ? message.body.added.length + message.body.modified.length : undefined, }); } async _prepareMessage( group: ClientGroup, ): Promise<HmrUpdateMessage | HmrErrorMessage> { try { const revPromise = this._bundler.getRevision(group.revisionId); if (!revPromise) { return { type: 'error', body: formatBundlingError( new RevisionNotFoundError(group.revisionId), ), }; } const {revision, delta} = await this._bundler.updateGraph( await revPromise, false, ); this._clientGroups.delete(group.revisionId); group.revisionId = revision.id; for (const client of group.clients) { client.revisionId = revision.id; } this._clientGroups.set(group.revisionId, group); const hmrUpdate = hmrJSBundle(delta, revision.graph, { createModuleId: this._createModuleId, projectRoot: this._config.projectRoot, }); return { type: 'update', body: { revisionId: revision.id, ...hmrUpdate, }, }; } catch (error) { const formattedError = formatBundlingError(error); this._config.reporter.update({type: 'bundling_error', error}); return {type: 'error', body: formattedError}; } } } module.exports = HmrServer;