Files
flipper/desktop/plugins/public/reactdevtools/serverAddOn.tsx
Andrey Goncharov ba9a80545d Support globally installed React DevTools
Summary:
- Support loading globally installed DevTools

Background:
1. Initially, I wanted to use react-devtools-core as before. react-devtools-core standalone contains quite a few imports of node.js APIs. After [a conversation with Brian](https://fb.workplace.com/groups/react.devtools/permalink/3131548550392044), I pivoted to react-devtools-inline
2. Technical design doc of react-devtools-inline integration: https://docs.google.com/document/d/1STUSUhXzrW_KkvqSu7Ge-rxjVFF7oU3_NbwzimkO_Z4
3. We support usage of globally installed devtools. Code of react-devtools-inline is not ready to be used by the browser as is. We need to bundle it and substitute React and ReactDOM imports with the globals.
4. As we can't pre-compile what users install globally, we need to bundle global devtools on demand,
5. I tried re-using our Metro bundling pipeline initially, but gave up after fighting it for 2 days. Included, `rollup` instead.
6. Size of a `tgz` archive with a plugin is 2.1MB

allow-large-files

Reviewed By: mweststrate

Differential Revision: D34968770

fbshipit-source-id: 352299964ccc195b8677dbda47db84ffaf38737b
2022-03-31 04:01:33 -07:00

173 lines
4.6 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and 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 {
createControlledPromise,
FlipperServerForServerAddOn,
ServerAddOn,
} from 'flipper-plugin';
import path from 'path';
import {WebSocketServer, WebSocket} from 'ws';
import {rollup} from 'rollup';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import {Events, Methods} from './contract';
const DEV_TOOLS_PORT = 8097; // hardcoded in RN
async function findGlobalDevTools(
flipperServer: FlipperServerForServerAddOn,
): Promise<string | undefined> {
try {
const {stdout: basePath} = await flipperServer.exec(
'node-api-exec',
'npm root -g',
);
console.debug(
'flipper-plugin-react-devtools.findGlobalDevTools -> npm root',
basePath,
);
const devToolsPath = path.join(
basePath.trim(),
'react-devtools-inline',
'frontend.js',
);
await flipperServer.exec('node-api-fs-stat', devToolsPath);
return devToolsPath;
} catch (error) {
console.warn('Failed to find globally installed React DevTools: ' + error);
return undefined;
}
}
const serverAddOn: ServerAddOn<Events, Methods> = async (
connection,
{flipperServer},
) => {
console.debug('flipper-plugin-react-devtools.serverAddOn -> starting');
const startServer = async () => {
console.debug('flipper-plugin-react-devtools.serverAddOn -> startServer');
const wss = new WebSocketServer({port: DEV_TOOLS_PORT});
const startedPromise = createControlledPromise<void>();
wss.on('listening', () => startedPromise.resolve());
wss.on('error', (err) => {
if (startedPromise.state === 'pending') {
startedPromise.reject(err);
return;
}
console.error('flipper-plugin-react-devtools.serverAddOn -> error', err);
});
await startedPromise.promise;
console.debug(
'flipper-plugin-react-devtools.serverAddOn -> started server',
);
wss.on('connection', (ws) => {
connection.send('connected');
console.debug(
'flipper-plugin-react-devtools.serverAddOn -> connected a client',
);
ws.on('message', (data) => {
connection.send('message', JSON.parse(data.toString()));
console.debug(
'flipper-plugin-react-devtools.serverAddOn -> client sent a message',
data.toString(),
);
});
ws.on('error', (err) => {
console.error(
'flipper-plugin-react-devtools.serverAddOn -> client error',
err,
);
});
ws.on('close', () => {
connection.send('disconnected');
console.debug(
'flipper-plugin-react-devtools.serverAddOn -> client left',
);
});
});
connection.receive('message', (data) => {
console.debug(
'flipper-plugin-react-devtools.serverAddOn -> desktop sent a message',
data,
);
wss!.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
});
});
return wss;
};
const wss = await startServer();
connection.receive('globalDevTools', async () => {
const globalDevToolsPath = await findGlobalDevTools(flipperServer);
if (!globalDevToolsPath) {
console.info(
'flipper-plugin-react-devtools.serverAddOn -> not found global React DevTools',
);
return;
}
console.info(
'flipper-plugin-react-devtools.serverAddOn -> found global React DevTools: ',
globalDevToolsPath,
);
const bundle = await rollup({
input: globalDevToolsPath,
plugins: [resolve(), commonjs()],
external: ['react', 'react-is', 'react-dom/client', 'react-dom'],
});
try {
const {output} = await bundle.generate({
format: 'iife',
globals: {
react: 'global.React',
'react-is': 'global.ReactIs',
'react-dom/client': 'global.ReactDOMClient',
'react-dom': 'global.ReactDOM',
},
});
return output[0].code;
} finally {
await bundle.close();
}
});
return async () => {
console.debug('flipper-plugin-react-devtools.serverAddOn -> stopping');
if (wss) {
console.debug(
'flipper-plugin-react-devtools.serverAddOn -> stopping wss',
);
await new Promise<void>((resolve, reject) =>
wss!.close((err) => (err ? reject(err) : resolve())),
);
console.debug('flipper-plugin-react-devtools.serverAddOn -> stopped wss');
}
};
};
export default serverAddOn;