Skip to content

Commit

Permalink
Code clean-ups and createServer migration
Browse files Browse the repository at this point in the history
Summary:
Scope of the diff:

1. Middleware
`react-native-github/local-cli` and `react-native-internal-cli` uses a very similar set of middlewares (internal cli extends github version), so I decided to move it to a standalone file (middleware manager) in order to remove duplications and increase readability.

2. Types
Seems that after Flow upgrade to version 0.68 there were many type issues to resolve, so all of them were auto-mocked. This is fine, but I'd like to see Flow assists me with `Metro.createServer` -> `Metro.runServer` migration. Hence, I decided to resolve flow mocks, related to runServer.

3. `runServer` signature
In `react-native-github` repo I cleaned up `runServer` signature by removing `startCallback` and `readyCallback` from the function parameters and moved them to `runServer` instead.

4. Replace `createServer` by `runServer`
In `react-native-github` repo, `createServer` has been replaced by `runServer`. __Some of arguments are not mapped__.

Note that this diff will partially break argument mapping. This is intentional. @[100000044482482:ives] will fix it with a new config package.

Reviewed By: mjesun

Differential Revision: D8711717

fbshipit-source-id: a843ab576360ff7242099910d8f25a9cb0a388c0
  • Loading branch information
Alexey Kureev authored and facebook-github-bot committed Jul 2, 2018
1 parent ee535fa commit c4a66a8
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 179 deletions.
70 changes: 70 additions & 0 deletions local-cli/server/middleware/MiddlewareManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @strict
* @flow
*/

const compression = require('compression');
const connect = require('connect');
const errorhandler = require('errorhandler');
const morgan = require('morgan');
const path = require('path');
const serveStatic = require('serve-static');
const WebSocketServer = require('ws').Server;

const indexPageMiddleware = require('./indexPage');
const copyToClipBoardMiddleware = require('./copyToClipBoardMiddleware');
const loadRawBodyMiddleware = require('./loadRawBodyMiddleware');
const openStackFrameInEditorMiddleware = require('./openStackFrameInEditorMiddleware');
const statusPageMiddleware = require('./statusPageMiddleware.js');
const systraceProfileMiddleware = require('./systraceProfileMiddleware.js');
const getDevToolsMiddleware = require('./getDevToolsMiddleware');

type Options = {
+watchFolders: $ReadOnlyArray<string>,
+host?: string,
}

type WebSocketProxy = {
server: WebSocketServer,
isChromeConnected: () => boolean,
};
type Connect = any;

module.exports = class MiddlewareManager {
app: Connect;
options: Options;

constructor(options: Options) {
const debuggerUIFolder = path.join(__dirname, 'util', 'debugger-ui');

this.options = options;
this.app = connect()
.use(loadRawBodyMiddleware)
.use(compression())
.use('/debugger-ui', serveStatic(debuggerUIFolder))
.use(openStackFrameInEditorMiddleware(this.options))
.use(copyToClipBoardMiddleware)
.use(statusPageMiddleware)
.use(systraceProfileMiddleware)
.use(indexPageMiddleware)
.use(morgan('combined'))
.use(errorhandler());
}

serveStatic = (folder: string) => {
this.app.use(serveStatic(folder));
};

getConnectInstance = () => this.app;

attachDevToolsSocket = (socket: WebSocketProxy) => {
this.app.use(
getDevToolsMiddleware(this.options, () => socket.isChromeConnected()),
);
};
};
4 changes: 2 additions & 2 deletions local-cli/server/middleware/getDevToolsMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ function launchDevTools({host, watchFolders}, isChromeConnected) {
// Explicit config always wins
var customDebugger = process.env.REACT_DEBUGGER;
if (customDebugger) {
var projects = watchFolders.map(escapePath).join(' ');
var command = customDebugger + ' ' + projects;
var folders = watchFolders.map(escapePath).join(' ');
var command = customDebugger + ' ' + folders;
console.log('Starting custom debugger by executing: ' + command);
exec(command, function(error, stdout, stderr) {
if (error !== null) {
Expand Down
190 changes: 39 additions & 151 deletions local-cli/server/runServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,132 +14,69 @@ require('../../setupBabel')();

const Metro = require('metro');

const HmrServer = require('metro/src/HmrServer');

const {Terminal} = require('metro-core');

const attachWebsocketServer = require('metro/src/lib/attachWebsocketServer');
const compression = require('compression');
const connect = require('connect');
const copyToClipBoardMiddleware = require('./middleware/copyToClipBoardMiddleware');
const defaultAssetExts = Metro.defaults.assetExts;
const defaultSourceExts = Metro.defaults.sourceExts;
const defaultPlatforms = Metro.defaults.platforms;
const defaultProvidesModuleNodeModules =
Metro.defaults.providesModuleNodeModules;
const errorhandler = require('errorhandler');
const fs = require('fs');
const getDevToolsMiddleware = require('./middleware/getDevToolsMiddleware');
const http = require('http');
const https = require('https');
const indexPageMiddleware = require('./middleware/indexPage');
const loadRawBodyMiddleware = require('./middleware/loadRawBodyMiddleware');
const messageSocket = require('./util/messageSocket.js');
const morgan = require('morgan');
const openStackFrameInEditorMiddleware = require('./middleware/openStackFrameInEditorMiddleware');
const path = require('path');
const serveStatic = require('serve-static');
const statusPageMiddleware = require('./middleware/statusPageMiddleware.js');
const systraceProfileMiddleware = require('./middleware/systraceProfileMiddleware.js');
const webSocketProxy = require('./util/webSocketProxy.js');

const {ASSET_REGISTRY_PATH} = require('../core/Constants');
const MiddlewareManager = require('./middleware/MiddlewareManager');

import type {ConfigT} from 'metro';
/* $FlowFixMe(site=react_native_oss) */
import type {Reporter} from 'metro/src/lib/reporting';

export type Args = {|
+assetExts: $ReadOnlyArray<string>,
+cert: string,
+customLogReporterPath?: string,
+host: string,
+https: boolean,
+maxWorkers: number,
+key: string,
+nonPersistent: boolean,
+platforms: $ReadOnlyArray<string>,
+port: number,
+projectRoot: string,
+providesModuleNodeModules: Array<string>,
+resetCache: boolean,
+sourceExts: $ReadOnlyArray<string>,
+transformer?: string,
+verbose: boolean,
+watchFolders: $ReadOnlyArray<string>,
|};

function runServer(
args: Args,
config: ConfigT,
// FIXME: this is weird design. The top-level should pass down a custom
// reporter rather than passing it up as argument to an event.
startedCallback: (reporter: Reporter) => mixed,
readyCallback: (reporter: Reporter) => mixed,
) {
var wsProxy = null;
var ms = null;

async function runServer(args: Args, config: ConfigT) {
const terminal = new Terminal(process.stdout);
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an error
* found when Flow v0.68 was deployed. To see the error delete this comment
* and run Flow. */
const ReporterImpl = getReporterImpl(args.customLogReporterPath || null);
const reporter = new ReporterImpl(terminal);
const packagerServer = getPackagerServer(args, config, reporter);
startedCallback(reporter);

const app = connect()
.use(loadRawBodyMiddleware)
.use(compression())
.use(
'/debugger-ui',
serveStatic(path.join(__dirname, 'util', 'debugger-ui')),
)
.use(
getDevToolsMiddleware(args, () => wsProxy && wsProxy.isChromeConnected()),
)
.use(getDevToolsMiddleware(args, () => ms && ms.isChromeConnected()))
.use(openStackFrameInEditorMiddleware(args))
.use(copyToClipBoardMiddleware)
.use(statusPageMiddleware)
.use(systraceProfileMiddleware)
.use(indexPageMiddleware)
.use(packagerServer.processRequest.bind(packagerServer));

args.watchFolders.forEach(root => app.use(serveStatic(root)));

app.use(morgan('combined')).use(errorhandler());

/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an error
* found when Flow v0.68 was deployed. To see the error delete this comment
* and run Flow. */
if (args.https && (!args.key || !args.cert)) {
throw new Error('Cannot use https without specifying key and cert options');
}

/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an error
* found when Flow v0.68 was deployed. To see the error delete this comment
* and run Flow. */
const serverInstance = args.https
? https.createServer(
{
key: fs.readFileSync(args.key),
cert: fs.readFileSync(args.cert),
},
app,
)
: http.createServer(app);

serverInstance.listen(args.port, args.host, 511, function() {
attachWebsocketServer({
httpServer: serverInstance,
path: '/hot',
websocketServer: new HmrServer(packagerServer),
});

wsProxy = webSocketProxy.attachToServer(serverInstance, '/debugger-proxy');
ms = messageSocket.attachToServer(serverInstance, '/message');
readyCallback(reporter);
const middlewareManager = new MiddlewareManager(args);

args.watchFolders.forEach(middlewareManager.serveStatic);

const serverInstance = await Metro.runServer({
config: {
...config,
hmrEnabled: true,
maxWorkers: args.maxWorkers,
reporter,
secure: args.https,
secureKey: args.key,
secureCert: args.cert,
transformModulePath: args.transformer
? path.resolve(args.transformer)
: config.getTransformModulePath(),
watch: !args.nonPersistent,
},
});
// Disable any kind of automatic timeout behavior for incoming
// requests in case it takes the packager more than the default
// timeout of 120 seconds to respond to a request.
serverInstance.timeout = 0;

// In Node 8, the default keep-alive for an HTTP connection is 5 seconds. In
// early versions of Node 8, this was implemented in a buggy way which caused
// some HTTP responses (like those containing large JS bundles) to be
// terminated early.
//
// As a workaround, arbitrarily increase the keep-alive from 5 to 30 seconds,
// which should be enough to send even the largest of JS bundles.
//
// For more info: https://github.com/nodejs/node/issues/13391
//
// $FlowFixMe
serverInstance.keepAliveTimeout = 30000;
}

function getReporterImpl(customLogReporterPath: ?string) {
Expand All @@ -162,53 +99,4 @@ function getReporterImpl(customLogReporterPath: ?string) {
}
}

function getPackagerServer(args, config, reporter) {
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an error
* found when Flow v0.68 was deployed. To see the error delete this comment
* and run Flow. */
const transformModulePath = args.transformer
? path.resolve(args.transformer)
: config.getTransformModulePath();

const providesModuleNodeModules =
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.68 was deployed. To see the error delete this
* comment and run Flow. */
args.providesModuleNodeModules || defaultProvidesModuleNodeModules;

return Metro.createServer({
asyncRequireModulePath: config.getAsyncRequireModulePath(),
assetExts: defaultAssetExts.concat(args.assetExts),
assetRegistryPath: ASSET_REGISTRY_PATH,
blacklistRE: config.getBlacklistRE(),
cacheStores: config.cacheStores,
cacheVersion: '3',
enableBabelRCLookup: config.getEnableBabelRCLookup(),
extraNodeModules: config.extraNodeModules,
dynamicDepsInPackages: config.dynamicDepsInPackages,
getModulesRunBeforeMainModule: config.getModulesRunBeforeMainModule,
getPolyfills: config.getPolyfills,
getResolverMainFields: config.getResolverMainFields,
getRunModuleStatement: config.getRunModuleStatement,
getTransformOptions: config.getTransformOptions,
hasteImplModulePath: config.hasteImplModulePath,
maxWorkers: args.maxWorkers,
platforms: defaultPlatforms.concat(args.platforms),
polyfillModuleNames: config.getPolyfillModuleNames(),
postMinifyProcess: config.postMinifyProcess,
postProcessBundleSourcemap: config.postProcessBundleSourcemap,
projectRoot: args.projectRoot,
providesModuleNodeModules: providesModuleNodeModules,
reporter,
resetCache: args.resetCache,
resolveRequest: config.resolveRequest,
sourceExts: args.sourceExts.concat(defaultSourceExts),
transformModulePath: transformModulePath,
verbose: args.verbose,
watch: !args.nonPersistent,
watchFolders: args.watchFolders,
workerPath: config.getWorkerPath(),
});
}

module.exports = runServer;
28 changes: 2 additions & 26 deletions local-cli/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,10 @@ import type {Args as RunServerArgs} from './runServer';
/**
* Starts the React Native Packager Server.
*/
function server(argv: mixed, config: RNConfig, args: Object) {
const startedCallback = logReporter => {
logReporter.update({
type: 'initialize_started',
port: args.port,
projectRoots: args.watchFolders,
});

process.on('uncaughtException', error => {
logReporter.update({
type: 'initialize_failed',
port: args.port,
error,
});

process.exit(11);
});
};

const readyCallback = logReporter => {
logReporter.update({
type: 'initialize_done',

This comment has been minimized.

Copy link
@fson

fson Jul 17, 2018

Contributor

The initialize_done and initialize_failed events don't seem to get triggered anymore after this change. I think runServer needs to be changed to trigger initialize_done.

(I was looking into using initialize_done to detect when the HTTP server is ready.)

});
};
const runServerArgs: RunServerArgs = args;
function server(argv: mixed, config: RNConfig, args: RunServerArgs) {
/* $FlowFixMe(site=react_native_fb) ConfigT shouldn't be extendable. */
const configT: ConfigT = config;
runServer(runServerArgs, configT, startedCallback, readyCallback);
runServer(args, configT);
}

module.exports = {
Expand Down

2 comments on commit c4a66a8

@janicduplessis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kureev This seems to be causing a few issues for me, looks like we're not passing assetRegistryPath anymore which causes an error. Also it looks like we don't add the middlewares anymore, or I don't understand how. middlewareManager isn't passed to the http server started by metro.

I'm currently investigating CI failure and it fails to detect that the packager started because the /status endpoint doesn't exist.

@janicduplessis
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I think I fixed it 🙃 #20162

Please sign in to comment.