From 0265758e5e756c22b12e0c8d8eed497dad2db362 Mon Sep 17 00:00:00 2001 From: Rafael Oleza Date: Tue, 5 Sep 2017 07:10:43 -0700 Subject: [PATCH] Create brand-new HMR server using the Delta Bundler Reviewed By: mjesun Differential Revision: D5765024 fbshipit-source-id: 3f51ab1564a93b8268e51c0a0a97ea3ef5bd6681 --- .../src/HmrServer/getBundlingOptionsForHmr.js | 52 ++++++++ packages/metro-bundler/src/HmrServer/index.js | 114 ++++++++++++++++++ .../metro-bundler/src/lib/TerminalReporter.js | 12 ++ packages/metro-bundler/src/lib/reporting.js | 4 + 4 files changed, 182 insertions(+) create mode 100644 packages/metro-bundler/src/HmrServer/getBundlingOptionsForHmr.js create mode 100644 packages/metro-bundler/src/HmrServer/index.js diff --git a/packages/metro-bundler/src/HmrServer/getBundlingOptionsForHmr.js b/packages/metro-bundler/src/HmrServer/getBundlingOptionsForHmr.js new file mode 100644 index 00000000..a1a2571a --- /dev/null +++ b/packages/metro-bundler/src/HmrServer/getBundlingOptionsForHmr.js @@ -0,0 +1,52 @@ +/** + * 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. + * + * @format + * @flow + */ + +'use strict'; + +import type {Options as BundleOptions} from '../DeltaBundler'; + +/** + * Module to easily create the needed configuration parameters needed for the + * bundler for HMR (since a lot of params are not relevant in this use case). + */ +module.exports = function getBundlingOptionsForHmr( + entryFile: string, + platform: string, +): BundleOptions { + // These are the really meaningful bundling options. The others below are + // not relevant for HMR. + const mainOptions = { + deltaBundleId: null, + entryFile, + hot: true, + minify: false, + platform, + wrapModules: false, + }; + + return { + ...mainOptions, + assetPlugins: [], + dev: true, + entryModuleOnly: false, + excludeSource: false, + generateSourceMaps: false, + inlineSourceMap: false, + isolateModuleIDs: false, + onProgress: null, + resolutionResponse: null, + runBeforeMainModule: [], + runModule: false, + sourceMapUrl: '', + unbundle: false, + }; +}; diff --git a/packages/metro-bundler/src/HmrServer/index.js b/packages/metro-bundler/src/HmrServer/index.js new file mode 100644 index 00000000..60ee3eb3 --- /dev/null +++ b/packages/metro-bundler/src/HmrServer/index.js @@ -0,0 +1,114 @@ +/** + * 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. + * + * @format + * @flow + */ + +'use strict'; + +const getBundlingOptionsForHmr = require('./getBundlingOptionsForHmr'); +const querystring = require('querystring'); +const url = require('url'); + +import type DeltaTransformer from '../DeltaBundler/DeltaTransformer'; +import type PackagerServer from '../Server'; +import type {Reporter} from '../lib/reporting'; + +type Client = {| + deltaTransformer: DeltaTransformer, + sendFn: (data: string) => mixed, +|}; + +/** + * 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 { + _packagerServer: PackagerServer; + _reporter: Reporter; + + constructor(packagerServer: PackagerServer, reporter: Reporter) { + this._packagerServer = packagerServer; + this._reporter = reporter; + } + + async onClientConnect( + clientUrl: string, + sendFn: (data: string) => mixed, + ): Promise { + const {bundleEntry, platform} = querystring.parse( + /* $FlowFixMe: url might be null */ + url.parse(clientUrl).query, + ); + + // Create a new DeltaTransformer for each client. Once the clients are + // modified to support Delta Bundles, they'll be able to pass the + // DeltaBundleId param through the WS connection and we'll be able to share + // the same DeltaTransformer between the WS connection and the HTTP one. + const deltaBundler = this._packagerServer.getDeltaBundler(); + const {deltaTransformer} = await deltaBundler.getDeltaTransformer( + getBundlingOptionsForHmr(bundleEntry, platform), + ); + + // Trigger an initial build to start up the DeltaTransformer. + await deltaTransformer.getDelta(); + + // Listen to file changes. + const client = {sendFn, deltaTransformer}; + deltaTransformer.on('change', this._handleFileChange.bind(this, client)); + + return client; + } + + onClientError(client: TClient, e: Error) { + this._reporter.update({ + type: 'hmr_client_error', + error: e, + }); + this.onClientDisconnect(client); + } + + onClientDisconnect(client: TClient) { + // We can safely remove all listeners from the delta transformer since the + // transformer is not shared between clients. + client.deltaTransformer.removeAllListeners('change'); + } + + async _handleFileChange(client: Client) { + client.sendFn(JSON.stringify({type: 'update-start'})); + client.sendFn(JSON.stringify(await this._prepareResponse(client))); + client.sendFn(JSON.stringify({type: 'update-done'})); + } + + async _prepareResponse(client: Client): Promise<{type: string, body: {}}> { + const result = await client.deltaTransformer.getDelta(); + const modules = []; + + for (const id in result.delta) { + modules.push({id, code: result.delta[id]}); + } + + return { + type: 'update', + body: { + modules, + inverseDependencies: result.inverseDependencies, + sourceURLs: {}, + sourceMappingURLs: {}, // TODO: handle Source Maps + }, + }; + } +} + +module.exports = HmrServer; diff --git a/packages/metro-bundler/src/lib/TerminalReporter.js b/packages/metro-bundler/src/lib/TerminalReporter.js index 3c73ded4..ce7daa40 100644 --- a/packages/metro-bundler/src/lib/TerminalReporter.js +++ b/packages/metro-bundler/src/lib/TerminalReporter.js @@ -251,6 +251,9 @@ class TerminalReporter { case 'worker_stderr_chunk': this._logWorkerChunk('stderr', event.chunk); break; + case 'hmr_client_error': + this._logHmrClientError(event.error); + break; } } @@ -396,6 +399,15 @@ class TerminalReporter { .join('\n'); } + _logHmrClientError(e: Error): void { + reporting.logError( + this.terminal, + 'A WebSocket client got a connection error. Please reload your device ' + + 'to get HMR working again: %s', + e, + ); + } + /** * Single entry point for reporting events. That allows us to implement the * corresponding JSON reporter easily and have a consistent reporting. diff --git a/packages/metro-bundler/src/lib/reporting.js b/packages/metro-bundler/src/lib/reporting.js index ddbc96be..e1f339cc 100644 --- a/packages/metro-bundler/src/lib/reporting.js +++ b/packages/metro-bundler/src/lib/reporting.js @@ -85,6 +85,10 @@ export type ReportableEvent = | { type: 'worker_stderr_chunk', chunk: string, + } + | { + type: 'hmr_client_error', + error: Error, }; /**