Exposes HMR through the Metro API

Reviewed By: rafeca

Differential Revision: D6692912

fbshipit-source-id: 6f119170c40fb99bf2cad83d00edba91bcbbe1c9
This commit is contained in:
Maël Nison 2018-01-12 07:26:12 -08:00 committed by Facebook Github Bot
parent cfe3670a07
commit 1cad201448
3 changed files with 103 additions and 4 deletions

View File

@ -48,7 +48,7 @@ exports.builder = (yargs: Yargs) => {
yargs.option('secure-key', {type: 'string'});
yargs.option('secure-cert', {type: 'string'});
yargs.option('legacy-bundler', {type: 'boolean'});
yargs.option('hmr-enabled', {alias: 'hmr', type: 'boolean'});
yargs.option('config', {alias: 'c', type: 'string'});

View File

@ -16,10 +16,12 @@ const Config = require('./Config');
const Http = require('http');
const Https = require('https');
const MetroBundler = require('./shared/output/bundle');
const MetroHmrServer = require('./HmrServer');
const MetroServer = require('./Server');
const TerminalReporter = require('./lib/TerminalReporter');
const TransformCaching = require('./lib/TransformCaching');
const attachWebsocketServer = require('./lib/attachWebsocketServer');
const defaults = require('./defaults');
const {realpath} = require('fs');
@ -27,6 +29,7 @@ const {readFile} = require('fs-extra');
const {Terminal} = require('metro-core');
import type {ConfigT} from './Config';
import type {Reporter} from './lib/reporting';
import type {RequestOptions, OutputOptions} from './shared/types.flow.js';
import type {Options as ServerOptions} from './shared/types.flow';
import type {IncomingMessage, ServerResponse} from 'http';
@ -40,6 +43,7 @@ type PublicMetroOptions = {|
maxWorkers?: number,
port?: ?number,
projectRoots: Array<string>,
reporter?: Reporter,
// deprecated
resetCache?: boolean,
|};
@ -71,10 +75,9 @@ async function runMetro({
// $FlowFixMe TODO t0 https://github.com/facebook/flow/issues/183
port = null,
projectRoots = [],
reporter = new TerminalReporter(new Terminal(process.stdout)),
watch = false,
}: PrivateMetroOptions): Promise<MetroServer> {
const reporter = new TerminalReporter(new Terminal(process.stdout));
const normalizedConfig = config ? Config.normalize(config) : Config.DEFAULT;
const assetExts = defaults.assetExts.concat(
@ -163,6 +166,7 @@ exports.createConnectMiddleware = async function(
: Config.DEFAULT;
return {
metroServer,
middleware: normalizedConfig.enhanceMiddleware(metroServer.processRequest),
end() {
metroServer.end();
@ -178,21 +182,25 @@ type RunServerOptions = {|
secure?: boolean,
secureKey?: string,
secureCert?: string,
hmrEnabled?: boolean,
|};
exports.runServer = async (options: RunServerOptions) => {
const port = options.port || 8080;
const reporter =
options.reporter || new TerminalReporter(new Terminal(process.stdout));
// Lazy require
const connect = require('connect');
const serverApp = connect();
const {middleware, end} = await exports.createConnectMiddleware({
const {metroServer, middleware, end} = await exports.createConnectMiddleware({
config: options.config,
maxWorkers: options.maxWorkers,
port,
projectRoots: options.projectRoots,
reporter,
resetCache: options.resetCache,
});
@ -212,6 +220,14 @@ exports.runServer = async (options: RunServerOptions) => {
httpServer = Http.createServer(serverApp);
}
if (options.hmrEnabled) {
attachWebsocketServer({
httpServer,
path: '/hot',
websocketServer: new MetroHmrServer(metroServer, reporter),
});
}
httpServer.listen(port, options.host, () => {
options.onReady && options.onReady(httpServer);
});

View File

@ -0,0 +1,83 @@
/**
* 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';
import type {Server as HttpServer} from 'http';
import type {Server as HttpsServer} from 'https';
type WebsocketServiceInterface<T> = {
+onClientConnect: (
url: string,
sendFn: (data: string) => mixed,
) => Promise<T>,
+onClientDisconnect?: (client: T) => mixed,
+onClientError?: (client: T, e: Error) => mixed,
+onClientMessage?: (client: T, message: string) => mixed,
};
type HMROptions<TClient> = {
httpServer: HttpServer | HttpsServer,
websocketServer: WebsocketServiceInterface<TClient>,
path: string,
};
/**
* Attach a websocket server to an already existing HTTP[S] server, and forward
* the received events on the given "websocketServer" parameter. It must be an
* object with the following fields:
*
* - onClientConnect
* - onClientError
* - onClientMessage
* - onClientDisconnect
*/
module.exports = function attachWebsocketServer<TClient: Object>({
httpServer,
websocketServer,
path,
}: HMROptions<TClient>) {
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({
server: httpServer,
path,
});
wss.on('connection', async ws => {
let connected = true;
const url = ws.upgradeReq.url;
const sendFn = (...args) => {
if (connected) {
ws.send(...args);
}
};
const client = await websocketServer.onClientConnect(url, sendFn);
ws.on('error', e => {
websocketServer.onClientError && websocketServer.onClientError(client, e);
});
ws.on('close', () => {
websocketServer.onClientDisconnect &&
websocketServer.onClientDisconnect(client);
connected = false;
});
ws.on('message', message => {
websocketServer.onClientMessage &&
websocketServer.onClientMessage(client, message);
});
});
};