diff --git a/packages/metro/src/commands/serve.js b/packages/metro/src/commands/serve.js index 6d056796..1f274f97 100644 --- a/packages/metro/src/commands/serve.js +++ b/packages/metro/src/commands/serve.js @@ -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'}); diff --git a/packages/metro/src/index.js b/packages/metro/src/index.js index 6df0b7f2..f6f84dea 100644 --- a/packages/metro/src/index.js +++ b/packages/metro/src/index.js @@ -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, + 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 { - 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); }); diff --git a/packages/metro/src/lib/attachWebsocketServer.js b/packages/metro/src/lib/attachWebsocketServer.js new file mode 100644 index 00000000..8cbbdffa --- /dev/null +++ b/packages/metro/src/lib/attachWebsocketServer.js @@ -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 = { + +onClientConnect: ( + url: string, + sendFn: (data: string) => mixed, + ) => Promise, + +onClientDisconnect?: (client: T) => mixed, + +onClientError?: (client: T, e: Error) => mixed, + +onClientMessage?: (client: T, message: string) => mixed, +}; + +type HMROptions = { + httpServer: HttpServer | HttpsServer, + websocketServer: WebsocketServiceInterface, + 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({ + httpServer, + websocketServer, + path, +}: HMROptions) { + 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); + }); + }); +};