diff --git a/.gitignore b/.gitignore index 997efeb79..3c34f29ac 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ !packages/cli/src/commands/init/__fixtures__/editTemplate/node_modules *.tsbuildinfo .cache +.watchmanconfig diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c9c0a7bb..e6e824275 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,7 +94,7 @@ npm config set registry https://registry.npmjs.org/ In order for symlinks to work correctly when running `start` locally, set REACT_NATIVE_APP_ROOT as the root folder of your cli project: ``` -REACT_NATIVE_APP_ROOT=path/to/cli node path/to/cli/packages/cli/build/index.js start +REACT_NATIVE_APP_ROOT=path/to/cli node path/to/cli/packages/cli/build/bin.js start ``` ## Running CLI with React Native from the source diff --git a/packages/cli/package.json b/packages/cli/package.json index 2760990b9..4c1e0cab7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -58,6 +58,7 @@ "open": "^6.2.0", "ora": "^3.4.0", "plist": "^3.0.0", + "pretty-format": "^25.1.0", "semver": "^6.3.0", "serve-static": "^1.13.1", "shell-quote": "1.6.1", @@ -80,6 +81,7 @@ "@types/mkdirp": "^0.5.2", "@types/morgan": "^1.7.37", "@types/node-notifier": "^5.4.0", + "@types/pretty-format": "^24.3.0", "@types/semver": "^6.0.2", "@types/wcwidth": "^1.0.0", "@types/ws": "^6.0.3", diff --git a/packages/cli/src/commands/server/eventsSocket.ts b/packages/cli/src/commands/server/eventsSocket.ts new file mode 100644 index 000000000..def50588c --- /dev/null +++ b/packages/cli/src/commands/server/eventsSocket.ts @@ -0,0 +1,201 @@ +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:'); + }, + }); + + 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 = 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/runServer.ts b/packages/cli/src/commands/server/runServer.ts index c47c5a647..a5cbbf930 100644 --- a/packages/cli/src/commands/server/runServer.ts +++ b/packages/cli/src/commands/server/runServer.ts @@ -15,6 +15,7 @@ import path from 'path'; import {logger} from '@react-native-community/cli-tools'; 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'; @@ -42,9 +43,20 @@ export type Args = { }; async function runServer(_argv: Array, ctx: Config, args: Args) { + let eventsSocket: + | ReturnType + | undefined; const terminal = new Terminal(process.stdout); const ReporterImpl = getReporterImpl(args.customLogReporterPath); - const reporter = new ReporterImpl(terminal); + const terminalReporter = new ReporterImpl(terminal); + const reporter = { + update(event: any) { + terminalReporter.update(event); + if (eventsSocket) { + eventsSocket.reportEvent(event); + } + }, + }; const metroConfig = await loadMetroConfig(ctx, { config: args.config, @@ -108,6 +120,12 @@ async function runServer(_argv: Array, ctx: Config, args: Args) { '/debugger-proxy', ); const ms = messageSocket.attachToServer(serverInstance, '/message'); + eventsSocket = eventsSocketModule.attachToServer( + serverInstance, + '/events', + ms, + ); + middlewareManager.attachDevToolsSocket(wsProxy); middlewareManager.attachDevToolsSocket(ms); @@ -137,7 +155,7 @@ async function runServer(_argv: Array, ctx: Config, args: Args) { await releaseChecker(ctx.root); } -function getReporterImpl(customLogReporterPath: string | void) { +function getReporterImpl(customLogReporterPath: string | undefined) { if (customLogReporterPath === undefined) { return require('metro/src/lib/TerminalReporter'); } diff --git a/yarn.lock b/yarn.lock index e495e91df..3cf79e6fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1857,6 +1857,16 @@ "@types/istanbul-lib-coverage" "^2.0.0" "@types/yargs" "^12.0.9" +"@jest/types@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" + integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@lerna/add@3.18.4": version "3.18.4" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.18.4.tgz#0d97c75b64febc10a9a38546a3019f0f2c24b0e6" @@ -2695,6 +2705,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/command-exists@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/command-exists/-/command-exists-1.2.0.tgz#d97e0ed10097090e4ab0367ed425b0312fad86f3" @@ -2787,11 +2802,31 @@ dependencies: "@types/hapi__joi" "*" +"@types/istanbul-lib-coverage@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + "@types/istanbul-lib-coverage@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz#1eb8c033e98cf4e1a4cedcaf8bcafe8cb7591e85" integrity sha512-eAtOAFZefEnfJiRFQBGw1eYqa5GTLCZ1y86N0XSI/D6EB+E8z6VPV/UL7Gi5UEclFqoQk+6NRqEDsfmDLXn8sg== +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + "@types/jest-diff@*": version "20.0.1" resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" @@ -2892,6 +2927,13 @@ "@types/node" "*" xmlbuilder ">=11.0.1" +"@types/pretty-format@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@types/pretty-format/-/pretty-format-24.3.0.tgz#e88461dcfc0b626778ae5c36a346076933ae84f8" + integrity sha512-y7NafihMfNH0UxgStB00yf76GUGsYROpD7Pg9qa3LD4SkH7Y2UGnGepESRTbpfU0L3zuUBMkSmDJToW6p7f6lA== + dependencies: + pretty-format "*" + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" @@ -2937,11 +2979,23 @@ resolved "https://registry.yarnpkg.com/@types/xmldoc/-/xmldoc-1.1.4.tgz#5867d4e29739719c633bf16413c5a4a4c1c3c802" integrity sha512-a/ONNCf9itbmzEz1ohx0Fv5TLJzXIPQTapxFu+DlYlDtn9UcAa1OhnrOOMwbU8125hFjrkJKL3qllD7vO5Bivw== +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + "@types/yargs@^12.0.2", "@types/yargs@^12.0.9": version "12.0.12" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916" integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw== +"@types/yargs@^15.0.0": + version "15.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.3.tgz#41453a0bc7ab393e995d1f5451455638edbd2baf" + integrity sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^1.5.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.6.0.tgz#a5ff3128c692393fb16efa403ec7c8a5593dab0f" @@ -3124,6 +3178,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -3135,6 +3194,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + ansi-to-html@^0.6.4: version "0.6.12" resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.12.tgz#9dcd1646f17770d02ec065615e97f979f4e313cb" @@ -3936,6 +4003,14 @@ chalk@^2.1.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -4081,12 +4156,19 @@ color-convert@^1.9.0, color-convert@^1.9.1: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -6201,6 +6283,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -9811,6 +9898,16 @@ prettier@1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== +pretty-format@*, 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@^24.0.0, pretty-format@^24.7.0: version "24.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.7.0.tgz#d23106bc2edcd776079c2daa5da02bcb12ed0c10" @@ -10031,6 +10128,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + react-is@^16.8.1: version "16.8.3" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d" @@ -11302,6 +11404,13 @@ supports-color@^6.0.0, supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + svgo@^1.0.0, svgo@^1.2.2: version "1.3.1" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.1.tgz#115c1f9d7e3294dfc66288c8499e65c2a1479729"