diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6266eb855..f1d58c46e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,13 +33,13 @@ And then: ```sh cd /my/new/react-native/project/ -yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" +yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" npx react-native start --watchFolders /path/to/cloned/cli/ npx react-native run-android ``` -*Note: you must use the `--watchFolders` flag with the `start` command when testing the CLI with `yarn link` like this. Otherwise Metro can't find the symlinked folder and this may result in errors such as `ReferenceError: SHA-1 for file ... is not computed`.* +_Note: you must use the `--watchFolders` flag with the `start` command when testing the CLI with `yarn link` like this. Otherwise Metro can't find the symlinked folder and this may result in errors such as `ReferenceError: SHA-1 for file ... is not computed`._ Once you're done with testing and you'd like to get back to regular setup, run `yarn unlink` instead of `yarn link` from above command. Then `yarn install --force`. diff --git a/packages/cli-server-api/src/index.ts b/packages/cli-server-api/src/index.ts index a31d9d46f..4507dbbaa 100644 --- a/packages/cli-server-api/src/index.ts +++ b/packages/cli-server-api/src/index.ts @@ -79,6 +79,7 @@ export function createDevServerMiddleware(options: MiddlewareOptions) { '/message', ); broadcast = messageSocket.broadcast; + isDebuggerConnected = debuggerProxy.isDebuggerConnected; const eventsSocket = eventsSocketServer.attachToServer( server, '/events', diff --git a/packages/cli/package.json b/packages/cli/package.json index 31d29623c..1bdb40e9d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,17 +30,15 @@ "dependencies": { "@hapi/joi": "^15.0.3", "@react-native-community/cli-debugger-ui": "^4.8.0", + "@react-native-community/cli-server-api": "^4.8.0", "@react-native-community/cli-tools": "^4.8.0", "@react-native-community/cli-types": "^4.8.0", "chalk": "^3.0.0", "command-exists": "^1.2.8", "commander": "^2.19.0", - "compression": "^1.7.1", - "connect": "^3.6.5", "cosmiconfig": "^5.1.0", "deepmerge": "^3.2.0", "envinfo": "^7.1.0", - "errorhandler": "^1.5.0", "execa": "^1.0.0", "find-up": "^4.1.0", "fs-extra": "^8.1.0", @@ -63,17 +61,14 @@ "serve-static": "^1.13.1", "strip-ansi": "^5.2.0", "sudo-prompt": "^9.0.0", - "wcwidth": "^1.0.1", - "ws": "^1.1.0" + "wcwidth": "^1.0.1" }, "peerDependencies": { "react-native": "^0.62.0-rc.0" }, "devDependencies": { "@types/command-exists": "^1.2.0", - "@types/compression": "^1.0.1", "@types/cosmiconfig": "^5.0.3", - "@types/errorhandler": "^0.0.32", "@types/fs-extra": "^8.1.0", "@types/glob": "^7.1.1", "@types/graceful-fs": "^4.1.3", @@ -83,7 +78,6 @@ "@types/mkdirp": "^0.5.2", "@types/semver": "^6.0.2", "@types/wcwidth": "^1.0.0", - "@types/ws": "^6.0.3", "slash": "^3.0.0", "snapshot-diff": "^0.7.0" } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 89e9feac2..24c1adfa6 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,7 +1,7 @@ import {Command, DetachedCommand} from '@react-native-community/cli-types'; // @ts-ignore - JS file -import server from './server/server'; +import start from './start/start'; import bundle from './bundle/bundle'; import ramBundle from './bundle/ramBundle'; import link from './link/link'; @@ -16,7 +16,7 @@ import init from './init'; import doctor from './doctor'; export const projectCommands = [ - server, + start, bundle, ramBundle, link, diff --git a/packages/cli/src/commands/server/eventsSocket.ts b/packages/cli/src/commands/server/eventsSocket.ts deleted file mode 100644 index e02c6b1ff..000000000 --- a/packages/cli/src/commands/server/eventsSocket.ts +++ /dev/null @@ -1,203 +0,0 @@ -import {Server as WebSocketServer} from 'ws'; -import {logger} from '@react-native-community/cli-tools'; -import prettyFormat from 'pretty-format'; -import {Server as HttpServer} from 'http'; -import {Server as HttpsServer} from 'https'; -import messageSocketModule from './messageSocket'; - -/** - * The eventsSocket websocket listens at the 'events/` for websocket - * connections, on which all Metro reports will be emitted. - * - * This is mostly useful for developer tools (clients) that wants to monitor Metro, - * and the apps connected to Metro. - * - * The eventsSocket provides the following features: - * - it reports any Metro event (that is reported through a reporter) to all clients - * - it reports any console.log's (and friends) from the connected app to all clients - * (as client_log event) - * - it allows connected clients to send commands through Metro to the connected app. - * This reuses the generic command mechanism. - * Two useful commands are 'reload' and 'devmenu'. - */ - -type Server = HttpServer | HttpsServer; - -type Command = { - version: number; - type: 'command'; - command: string; - params?: any; -}; - -/** - * This number is used to version the communication protocol between - * Dev tooling like Flipper and Metro, so that in the future we can recognize - * messages coming from old clients, so that it will be simpler to implement - * backward compatibility. - * - * We start at 2 as the protocol is currently the same as used internally at FB, - * which happens to be at version 2 as well. - */ -const PROTOCOL_VERSION = 2; - -function parseMessage(data: string): T | undefined { - try { - const message = JSON.parse(data); - if (message.version === PROTOCOL_VERSION) { - return message; - } - logger.error( - 'Received message had wrong protocol version: ' + message.version, - ); - } catch { - logger.error('Failed to parse the message as JSON:\n' + data); - } - return undefined; -} - -/** - * Two types of messages will arrive in this function, - * 1) messages generated by Metro itself (through the reporter abstraction) - * those are yet to be serialized, and can contain any kind of data structure - * 2) a specific event generated by Metro is `client_log`, which describes - * console.* calls in the app. - * The arguments send to the console are pretty printed so that they can be - * displayed in a nicer way in dev tools - * - * @param message - */ -function serializeMessage(message: any) { - // We do want to send Metro report messages, but their contents is not guaranteed to be serializable. - // For some known types we will pretty print otherwise not serializable parts first: - let toSerialize = message; - if (message && message.error && message.error instanceof Error) { - toSerialize = { - ...message, - error: prettyFormat(message.error, { - escapeString: true, - highlight: true, - maxDepth: 3, - min: true, - }), - }; - } else if (message && message.type === 'client_log') { - toSerialize = { - ...message, - data: message.data.map((item: any) => - typeof item === 'string' - ? item - : prettyFormat(item, { - escapeString: true, - highlight: true, - maxDepth: 3, - min: true, - plugins: [prettyFormat.plugins.ReactElement], - }), - ), - }; - } - try { - return JSON.stringify(toSerialize); - } catch (e) { - logger.error('Failed to serialize: ' + e); - return null; - } -} - -type MessageSocket = ReturnType; - -/** - * Starts the eventsSocket at the given path - * - * @param server - * @param path typically: 'events/' - * @param messageSocket: webSocket to which all connected RN apps are listening - */ -function attachToServer( - server: Server, - path: string, - messageSocket: MessageSocket, -) { - const wss = new WebSocketServer({ - server: server, - path: path, - verifyClient({origin}: {origin: string}) { - // This exposes the full JS logs and enables issuing commands like reload - // so let's make sure only locally running stuff can connect to it - return ( - origin.startsWith('http://localhost:') || origin.startsWith('file:') - ); - }, - }); - - const clients = new Map(); - let nextClientId = 0; - - /** - * broadCastEvent is called by reportEvent (below), which is called by the - * default reporter of this server, to make sure that all Metro events are - * broadcasted to all connected clients - * (that is, all devtools such as Flipper, _not_: connected apps) - * - * @param message - */ - function broadCastEvent(message: any) { - if (!clients.size) { - return; - } - const serialized = serializeMessage(message); - if (!serialized) { - return; - } - for (const ws of clients.values()) { - try { - ws.send(serialized); - } catch (e) { - logger.error( - `Failed to send broadcast to client due to:\n ${e.toString()}`, - ); - } - } - } - - wss.on('connection', function(clientWs) { - const clientId = `client#${nextClientId++}`; - - clients.set(clientId, clientWs); - - clientWs.onclose = clientWs.onerror = () => { - clients.delete(clientId); - }; - - clientWs.onmessage = event => { - const message: Command | undefined = parseMessage(event.data.toString()); - if (message == null) { - return; - } - if (message.type === 'command') { - try { - /** - * messageSocket.broadcast (not to be confused with our own broadcast above) - * forwards a command to all connected React Native applications. - */ - messageSocket.broadcast(message.command, message.params); - } catch (e) { - logger.error('Failed to forward message to clients: ', e); - } - } else { - logger.error('Unknown message type: ', message.type); - } - }; - }); - - return { - reportEvent: (event: any) => { - broadCastEvent(event); - }, - }; -} - -export default { - attachToServer, -}; diff --git a/packages/cli/src/commands/server/messageSocket.ts b/packages/cli/src/commands/server/messageSocket.ts deleted file mode 100644 index e458daf78..000000000 --- a/packages/cli/src/commands/server/messageSocket.ts +++ /dev/null @@ -1,257 +0,0 @@ -/** - * 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. - */ - -import url from 'url'; -import {Server as WebSocketServer} from 'ws'; -import {logger} from '@react-native-community/cli-tools'; -import {Server as HttpServer} from 'http'; -import {Server as HttpsServer} from 'https'; - -const PROTOCOL_VERSION = 2; - -type IdObject = { - requestId: string; - clientId: string; -}; - -type Message = { - version?: string; - id?: IdObject; - method?: string; - target: string; - result?: any; - error?: Error; - params?: Record; -}; - -function parseMessage(data: string, binary: any) { - if (binary) { - logger.error('Expected text message, got binary!'); - return undefined; - } - try { - const message = JSON.parse(data); - if (message.version === PROTOCOL_VERSION) { - return message; - } - logger.error( - `Received message had wrong protocol version: ${message.version}`, - ); - } catch (e) { - logger.error(`Failed to parse the message as JSON:\n${data}`); - } - return undefined; -} - -function isBroadcast(message: Message) { - return ( - typeof message.method === 'string' && - message.id === undefined && - message.target === undefined - ); -} - -function isRequest(message: Message) { - return ( - typeof message.method === 'string' && typeof message.target === 'string' - ); -} - -function isResponse(message: Message) { - return ( - typeof message.id === 'object' && - typeof message.id.requestId !== 'undefined' && - typeof message.id.clientId === 'string' && - (message.result !== undefined || message.error !== undefined) - ); -} - -type Server = HttpServer | HttpsServer; -function attachToServer(server: Server, path: string) { - const wss = new WebSocketServer({ - server, - path, - }); - const clients = new Map(); - let nextClientId = 0; - - function getClientWs(clientId: string) { - const clientWs = clients.get(clientId); - if (clientWs === undefined) { - throw new Error( - `could not find id "${clientId}" while forwarding request`, - ); - } - return clientWs; - } - - function handleSendBroadcast( - broadcasterId: string | null, - message: Partial, - ) { - const forwarded = { - version: PROTOCOL_VERSION, - method: message.method, - params: message.params, - }; - if (clients.size === 0) { - logger.warn( - `No apps connected. Sending "${ - message.method - }" to all React Native apps failed. Make sure your app is running in the simulator or on a phone connected via USB.`, - ); - } - for (const [otherId, otherWs] of clients) { - if (otherId !== broadcasterId) { - try { - otherWs.send(JSON.stringify(forwarded)); - } catch (e) { - logger.error( - `Failed to send broadcast to client: '${otherId}' ` + - `due to:\n ${e.toString()}`, - ); - } - } - } - } - - wss.on('connection', clientWs => { - const clientId = `client#${nextClientId++}`; - - function handleCaughtError(message: Message, error: Error) { - const errorMessage = { - id: message.id, - method: message.method, - target: message.target, - error: message.error === undefined ? 'undefined' : 'defined', - params: message.params === undefined ? 'undefined' : 'defined', - result: message.result === undefined ? 'undefined' : 'defined', - }; - - if (message.id === undefined) { - logger.error( - `Handling message from ${clientId} failed with:\n${error}\n` + - `message:\n${JSON.stringify(errorMessage)}`, - ); - } else { - try { - clientWs.send( - JSON.stringify({ - version: PROTOCOL_VERSION, - error, - id: message.id, - }), - ); - } catch (e) { - logger.error( - `Failed to reply to ${clientId} with error:\n${error}` + - `\nmessage:\n${JSON.stringify(errorMessage)}` + - `\ndue to error: ${e.toString()}`, - ); - } - } - } - - function handleServerRequest(message: Message) { - let result = null; - switch (message.method) { - case 'getid': - result = clientId; - break; - case 'getpeers': - result = {}; - clients.forEach((otherWs, otherId) => { - if (clientId !== otherId) { - result[otherId] = url.parse(otherWs.upgradeReq.url, true).query; - } - }); - break; - default: - throw new Error(`unknown method: ${message.method}`); - } - - clientWs.send( - JSON.stringify({ - version: PROTOCOL_VERSION, - result, - id: message.id, - }), - ); - } - - function forwardRequest(message: Message) { - getClientWs(message.target).send( - JSON.stringify({ - version: PROTOCOL_VERSION, - method: message.method, - params: message.params, - id: - message.id === undefined - ? undefined - : {requestId: message.id, clientId}, - }), - ); - } - - function forwardResponse(message: Message) { - if (!message.id) { - return; - } - getClientWs(message.id.clientId).send( - JSON.stringify({ - version: PROTOCOL_VERSION, - result: message.result, - error: message.error, - id: message.id.requestId, - }), - ); - } - - clients.set(clientId, clientWs); - const onCloseHandler = () => { - // @ts-ignore - clientWs.onmessage = null; - clients.delete(clientId); - }; - clientWs.onclose = onCloseHandler; - clientWs.onerror = onCloseHandler; - clientWs.onmessage = (event: any) => { - const message = parseMessage(event.data, event.binary); - if (message === undefined) { - logger.error('Received message not matching protocol'); - return; - } - - try { - if (isBroadcast(message)) { - handleSendBroadcast(clientId, message); - } else if (isRequest(message)) { - if (message.target === 'server') { - handleServerRequest(message); - } else { - forwardRequest(message); - } - } else if (isResponse(message)) { - forwardResponse(message); - } else { - throw new Error('Invalid message, did not match the protocol'); - } - } catch (e) { - handleCaughtError(message, e.toString()); - } - }; - }); - - return { - isDebuggerConnected: () => true, - broadcast: (method: string, params?: Record) => { - handleSendBroadcast(null, {method, params}); - }, - }; -} - -export default {attachToServer, parseMessage}; diff --git a/packages/cli/src/commands/server/middleware/MiddlewareManager.ts b/packages/cli/src/commands/server/middleware/MiddlewareManager.ts deleted file mode 100644 index 6a2e21da7..000000000 --- a/packages/cli/src/commands/server/middleware/MiddlewareManager.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * 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. - */ - -import compression from 'compression'; -import connect from 'connect'; -import errorhandler from 'errorhandler'; -import {Server as WebSocketServer} from 'ws'; -import serveStatic from 'serve-static'; -import {debuggerUIMiddleware} from '@react-native-community/cli-debugger-ui'; -import indexPageMiddleware from './indexPage'; -import getSecurityHeadersMiddleware from './getSecurityHeadersMiddleware'; -import loadRawBodyMiddleware from './loadRawBodyMiddleware'; -import openStackFrameInEditorMiddleware from './openStackFrameInEditorMiddleware'; -import openURLMiddleware from './openURLMiddleware'; -import statusPageMiddleware from './statusPageMiddleware'; -import systraceProfileMiddleware from './systraceProfileMiddleware'; -import getDevToolsMiddleware from './getDevToolsMiddleware'; - -type Options = { - host?: string; - watchFolders: ReadonlyArray; - port: number; -}; - -type WebSocketProxy = { - server?: WebSocketServer; - isDebuggerConnected: () => boolean; -}; - -export default class MiddlewareManager { - app: connect.Server; - - options: Options; - - constructor(options: Options) { - this.options = options; - this.app = connect() - .use(getSecurityHeadersMiddleware) - .use(loadRawBodyMiddleware) - // @ts-ignore compression and connect types mismatch - .use(compression()) - .use('/debugger-ui', debuggerUIMiddleware()) - .use(openStackFrameInEditorMiddleware(this.options)) - .use(openURLMiddleware) - .use(statusPageMiddleware) - .use(systraceProfileMiddleware) - .use(indexPageMiddleware) - .use(errorhandler()); - } - - serveStatic(folder: string) { - // @ts-ignore serveStatic and connect types mismatch - this.app.use(serveStatic(folder)); - } - - getConnectInstance() { - return this.app; - } - - attachDevToolsSocket(socket: WebSocketProxy) { - this.app.use( - getDevToolsMiddleware(this.options, () => socket.isDebuggerConnected()), - ); - } -} diff --git a/packages/cli/src/commands/server/middleware/getDevToolsMiddleware.ts b/packages/cli/src/commands/server/middleware/getDevToolsMiddleware.ts deleted file mode 100644 index d5a136106..000000000 --- a/packages/cli/src/commands/server/middleware/getDevToolsMiddleware.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * 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. - */ -import http from 'http'; -import {launchDebugger, logger} from '@react-native-community/cli-tools'; -import {exec} from 'child_process'; - -function launchDefaultDebugger( - host: string | undefined, - port: number, - args = '', -) { - const hostname = host || 'localhost'; - const debuggerURL = `http://${hostname}:${port}/debugger-ui${args}`; - logger.info('Launching Dev Tools...'); - launchDebugger(debuggerURL); -} - -function escapePath(pathname: string) { - // " Can escape paths with spaces in OS X, Windows, and *nix - return `"${pathname}"`; -} - -type LaunchDevToolsOptions = { - host?: string; - port: number; - watchFolders: ReadonlyArray; -}; - -function launchDevTools( - {host, port, watchFolders}: LaunchDevToolsOptions, - isDebuggerConnected: () => boolean, -) { - // Explicit config always wins - const customDebugger = process.env.REACT_DEBUGGER; - if (customDebugger) { - startCustomDebugger({watchFolders, customDebugger}); - } else if (!isDebuggerConnected()) { - // Debugger is not yet open; we need to open a session - launchDefaultDebugger(host, port); - } -} - -function startCustomDebugger({ - watchFolders, - customDebugger, -}: { - watchFolders: ReadonlyArray; - customDebugger: string; -}) { - const folders = watchFolders.map(escapePath).join(' '); - const command = `${customDebugger} ${folders}`; - logger.info('Starting custom debugger by executing:', command); - exec(command, function(error) { - if (error !== null) { - logger.error('Error while starting custom debugger:', error.stack || ''); - } - }); -} - -export default function getDevToolsMiddleware( - options: LaunchDevToolsOptions, - isDebuggerConnected: () => boolean, -) { - return function devToolsMiddleware( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (err?: any) => void, - ) { - if (req.url === '/launch-js-devtools') { - launchDevTools(options, isDebuggerConnected); - res.end('OK'); - } else { - next(); - } - }; -} diff --git a/packages/cli/src/commands/server/middleware/getSecurityHeadersMiddleware.ts b/packages/cli/src/commands/server/middleware/getSecurityHeadersMiddleware.ts deleted file mode 100644 index 5fedf4898..000000000 --- a/packages/cli/src/commands/server/middleware/getSecurityHeadersMiddleware.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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. - */ -import http from 'http'; - -export default function getSecurityHeadersMiddleware( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (err?: any) => void, -) { - // Block any cross origin request. - if ( - typeof req.headers.origin === 'string' && - !req.headers.origin.match(/^https?:\/\/localhost:/) - ) { - next( - new Error( - 'Unauthorized request from ' + - req.headers.origin + - '. This may happen because of a conflicting browser extension. Please try to disable it and try again.', - ), - ); - return; - } - - // Block MIME-type sniffing. - res.setHeader('X-Content-Type-Options', 'nosniff'); - - next(); -} diff --git a/packages/cli/src/commands/server/middleware/index.html b/packages/cli/src/commands/server/middleware/index.html deleted file mode 100644 index 0173e96d4..000000000 --- a/packages/cli/src/commands/server/middleware/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - React Native - - -

React Native packager is running.

-

Visit documentation

- - diff --git a/packages/cli/src/commands/server/middleware/indexPage.ts b/packages/cli/src/commands/server/middleware/indexPage.ts deleted file mode 100644 index 403c8e922..000000000 --- a/packages/cli/src/commands/server/middleware/indexPage.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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. - */ -import http from 'http'; -import fs from 'fs'; -import path from 'path'; - -export default function indexPageMiddleware( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (err?: any) => void, -) { - if (req.url === '/') { - res.setHeader('Content-Type', 'text/html'); - res.end(fs.readFileSync(path.join(__dirname, 'index.html'))); - } else { - next(); - } -} diff --git a/packages/cli/src/commands/server/middleware/loadRawBodyMiddleware.ts b/packages/cli/src/commands/server/middleware/loadRawBodyMiddleware.ts deleted file mode 100644 index 2a9746387..000000000 --- a/packages/cli/src/commands/server/middleware/loadRawBodyMiddleware.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * 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. - */ -import http from 'http'; - -export default function rawBodyMiddleware( - req: http.IncomingMessage, - _res: http.ServerResponse, - next: (err?: any) => void, -) { - (req as http.IncomingMessage & {rawBody: string}).rawBody = ''; - req.setEncoding('utf8'); - - req.on('data', (chunk: string) => { - (req as http.IncomingMessage & {rawBody: string}).rawBody += chunk; - }); - - req.on('end', () => { - next(); - }); -} diff --git a/packages/cli/src/commands/server/middleware/openStackFrameInEditorMiddleware.ts b/packages/cli/src/commands/server/middleware/openStackFrameInEditorMiddleware.ts deleted file mode 100644 index 8fdbc8c62..000000000 --- a/packages/cli/src/commands/server/middleware/openStackFrameInEditorMiddleware.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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. - */ -import http from 'http'; -import {launchEditor} from '@react-native-community/cli-tools'; - -export default function getOpenStackFrameInEditorMiddleware({ - watchFolders, -}: { - watchFolders: ReadonlyArray; -}) { - return ( - req: http.IncomingMessage & {rawBody: string}, - res: http.ServerResponse, - next: (err?: any) => void, - ) => { - if (req.url === '/open-stack-frame') { - const frame = JSON.parse(req.rawBody); - launchEditor(frame.file, frame.lineNumber, watchFolders); - res.end('OK'); - } else { - next(); - } - }; -} diff --git a/packages/cli/src/commands/server/middleware/openURLMiddleware.ts b/packages/cli/src/commands/server/middleware/openURLMiddleware.ts deleted file mode 100644 index ce11e4fa3..000000000 --- a/packages/cli/src/commands/server/middleware/openURLMiddleware.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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. - */ -import http from 'http'; -import {launchDefaultBrowser, logger} from '@react-native-community/cli-tools'; - -/** - * Handle request from JS to open an arbitrary URL in Chrome - */ -export default function openURLMiddleware( - req: http.IncomingMessage & {rawBody: string}, - res: http.ServerResponse, - next: (err?: any) => void, -) { - if (req.url === '/open-url') { - const {url} = JSON.parse(req.rawBody); - logger.info(`Opening ${url}...`); - launchDefaultBrowser(url); - res.end('OK'); - } else { - next(); - } -} diff --git a/packages/cli/src/commands/server/middleware/statusPageMiddleware.ts b/packages/cli/src/commands/server/middleware/statusPageMiddleware.ts deleted file mode 100644 index 215bf5c3c..000000000 --- a/packages/cli/src/commands/server/middleware/statusPageMiddleware.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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. - */ -import http from 'http'; - -/** - * Status page so that anyone who needs to can verify that the packager is - * running on 8081 and not another program / service. - */ -export default function statusPageMiddleware( - req: http.IncomingMessage, - res: http.ServerResponse, - next: (err?: any) => void, -) { - if (req.url === '/status') { - res.end('packager-status:running'); - } else { - next(); - } -} diff --git a/packages/cli/src/commands/server/middleware/systraceProfileMiddleware.ts b/packages/cli/src/commands/server/middleware/systraceProfileMiddleware.ts deleted file mode 100644 index 35557aa81..000000000 --- a/packages/cli/src/commands/server/middleware/systraceProfileMiddleware.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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. - */ -import http from 'http'; -import fs from 'fs'; -import {logger} from '@react-native-community/cli-tools'; - -export default function systraceProfileMiddleware( - req: http.IncomingMessage & {rawBody: string}, - res: http.ServerResponse, - next: (err?: any) => void, -) { - if (req.url !== '/systrace') { - next(); - return; - } - - logger.info('Dumping profile information...'); - const dumpName = `/tmp/dump_${Date.now()}.json`; - fs.writeFileSync(dumpName, req.rawBody); - const response = - `Your profile was saved at:\n${dumpName}\n\n` + - 'On Google Chrome navigate to chrome://tracing and then click on "load" ' + - 'to load and visualise your profile.\n\n' + - 'This message is also printed to your console by the packager so you can copy it :)'; - logger.info(response); - res.end(response); -} diff --git a/packages/cli/src/commands/server/webSocketProxy.ts b/packages/cli/src/commands/server/webSocketProxy.ts deleted file mode 100644 index a182b3532..000000000 --- a/packages/cli/src/commands/server/webSocketProxy.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * 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 - */ - -import ws from 'ws'; -import {logger} from '@react-native-community/cli-tools'; -import {Server as HttpServer} from 'http'; -import {Server as HttpsServer} from 'https'; - -type Server = HttpServer | HttpsServer; -function attachToServer(server: Server, path: string) { - const WebSocketServer = ws.Server; - const wss = new WebSocketServer({ - server, - path, - }); - - let debuggerSocket: ws | null; - let clientSocket: ws | null; - - function send(dest: ws | null, message: ws.Data) { - if (!dest) { - return; - } - - try { - dest.send(message); - } catch (e) { - logger.warn(e); - // Sometimes this call throws 'not opened' - } - } - - const debuggerSocketCloseHandler = () => { - debuggerSocket = null; - if (clientSocket) { - clientSocket.close(1011, 'Debugger was disconnected'); - } - }; - - const clientSocketCloseHandler = () => { - clientSocket = null; - send(debuggerSocket, JSON.stringify({method: '$disconnected'})); - }; - - wss.on('connection', (connection: ws) => { - // @ts-ignore current definition of ws does not have upgradeReq type - const {url} = connection.upgradeReq; - - if (url.indexOf('role=debugger') > -1) { - if (debuggerSocket) { - connection.close(1011, 'Another debugger is already connected'); - return; - } - debuggerSocket = connection; - if (debuggerSocket) { - debuggerSocket.onerror = debuggerSocketCloseHandler; - debuggerSocket.onclose = debuggerSocketCloseHandler; - debuggerSocket.onmessage = ({data}) => send(clientSocket, data); - } - } else if (url.indexOf('role=client') > -1) { - if (clientSocket) { - // @ts-ignore not nullable with current type definition of ws - clientSocket.onerror = null; - // @ts-ignore not nullable with current type definition of ws - clientSocket.onclose = null; - // @ts-ignore not nullable with current type definition of ws - clientSocket.onmessage = null; - clientSocket.close(1011, 'Another client connected'); - } - clientSocket = connection; - clientSocket.onerror = clientSocketCloseHandler; - clientSocket.onclose = clientSocketCloseHandler; - clientSocket.onmessage = ({data}) => send(debuggerSocket, data); - } else { - connection.close(1011, 'Missing role param'); - } - }); - - return { - server: wss, - isDebuggerConnected() { - return !!debuggerSocket; - }, - }; -} - -export default { - attachToServer, -}; diff --git a/packages/cli/src/commands/server/runServer.ts b/packages/cli/src/commands/start/runServer.ts similarity index 71% rename from packages/cli/src/commands/server/runServer.ts rename to packages/cli/src/commands/start/runServer.ts index d15b3ab02..36940150c 100644 --- a/packages/cli/src/commands/server/runServer.ts +++ b/packages/cli/src/commands/start/runServer.ts @@ -9,13 +9,13 @@ import Metro from 'metro'; // @ts-ignore untyped metro import {Terminal} from 'metro-core'; -import http from 'http'; import path from 'path'; +import { + createDevServerMiddleware, + indexPageMiddleware, +} from '@react-native-community/cli-server-api'; import {Config} from '@react-native-community/cli-types'; -import messageSocket from './messageSocket'; -import eventsSocketModule from './eventsSocket'; -import webSocketProxy from './webSocketProxy'; -import MiddlewareManager from './middleware/MiddlewareManager'; + import loadMetroConfig from '../../tools/loadMetroConfig'; import releaseChecker from '../../tools/releaseChecker'; import enableWatchMode from './watchMode'; @@ -41,17 +41,15 @@ export type Args = { }; async function runServer(_argv: Array, ctx: Config, args: Args) { - let eventsSocket: - | ReturnType - | undefined; + let reportEvent: ((event: any) => void) | undefined; const terminal = new Terminal(process.stdout); const ReporterImpl = getReporterImpl(args.customLogReporterPath); const terminalReporter = new ReporterImpl(terminal); const reporter = { update(event: any) { terminalReporter.update(event); - if (eventsSocket) { - eventsSocket.reportEvent(event); + if (reportEvent) { + reportEvent(event); } }, }; @@ -73,24 +71,22 @@ async function runServer(_argv: Array, ctx: Config, args: Args) { ); } - const middlewareManager = new MiddlewareManager({ + const {middleware, attachToServer} = createDevServerMiddleware({ host: args.host, port: metroConfig.server.port, watchFolders: metroConfig.watchFolders, }); - - metroConfig.watchFolders.forEach( - middlewareManager.serveStatic.bind(middlewareManager), - ); + middleware.use(indexPageMiddleware); const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware; - - metroConfig.server.enhanceMiddleware = (middleware: any, server: unknown) => { + metroConfig.server.enhanceMiddleware = ( + metroMiddleware: any, + server: unknown, + ) => { if (customEnhanceMiddleware) { - middleware = customEnhanceMiddleware(middleware, server); + metroMiddleware = customEnhanceMiddleware(metroMiddleware, server); } - - return middlewareManager.getConnectInstance().use(middleware); + return middleware.use(metroMiddleware); }; const serverInstance = await Metro.runServer(metroConfig, { @@ -101,31 +97,14 @@ async function runServer(_argv: Array, ctx: Config, args: Args) { hmrEnabled: true, }); - const wsProxy = webSocketProxy.attachToServer( - serverInstance, - '/debugger-proxy', - ); - const ms = messageSocket.attachToServer(serverInstance, '/message'); - eventsSocket = eventsSocketModule.attachToServer( - serverInstance, - '/events', - ms, - ); + const {messageSocket, eventsSocket} = attachToServer(serverInstance); - middlewareManager.attachDevToolsSocket(wsProxy); - middlewareManager.attachDevToolsSocket(ms); + reportEvent = eventsSocket.reportEvent; if (args.interactive) { - enableWatchMode(ms); + enableWatchMode(messageSocket); } - middlewareManager - .getConnectInstance() - .use('/reload', (_req: http.IncomingMessage, res: http.ServerResponse) => { - ms.broadcast('reload'); - res.end('OK'); - }); - // 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 diff --git a/packages/cli/src/commands/server/server.ts b/packages/cli/src/commands/start/start.ts similarity index 100% rename from packages/cli/src/commands/server/server.ts rename to packages/cli/src/commands/start/start.ts diff --git a/packages/cli/src/commands/server/watchMode.ts b/packages/cli/src/commands/start/watchMode.ts similarity index 100% rename from packages/cli/src/commands/server/watchMode.ts rename to packages/cli/src/commands/start/watchMode.ts diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 5c3901517..daf679bae 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -7,6 +7,7 @@ "references": [ {"path": "../tools"}, {"path": "../cli-types"}, - {"path": "../debugger-ui"} + {"path": "../debugger-ui"}, + {"path": "../cli-server-api"} ] } diff --git a/yarn.lock b/yarn.lock index f948ca7f1..8cc92d55f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1252,6 +1252,16 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" +"@jest/types@^25.1.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.3.0.tgz#88f94b277a1d028fd7117bc1f74451e0fc2131e7" + integrity sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@jest/types@^25.2.3": version "25.2.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.2.3.tgz#035c4fb94e2da472f359ff9a211915d59987f6b6" @@ -9448,7 +9458,17 @@ pretty-format@^24.7.0, pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" -pretty-format@^25.1.0, pretty-format@^25.2.0, pretty-format@^25.2.3: +pretty-format@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" + integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== + dependencies: + "@jest/types" "^25.1.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + +pretty-format@^25.2.0, pretty-format@^25.2.3: version "25.2.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.3.tgz#ba6e9603a0d80fa2e470b1fed55de1f9bfd81421" integrity sha512-IP4+5UOAVGoyqC/DiomOeHBUKN6q00gfyT2qpAsRH64tgOKB2yF7FHJXC18OCiU0/YFierACup/zdCOWw0F/0w==