desktop/plugins/public/reactdevtools/serverAddOn.tsx (142 lines of code) (raw):
/**
* 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;