We know Flipper as an Electron desktop app that serves mobile developers as their debugging companion. Thousands of people use Flipper every day to tinker with their app and get to the bottom of tricky problems.
As announced in the previous roadmap post, we are committed to amplifying how Flipper could improve the quality of our software. We want take Flipper beyond its current role as a complementary debugging tool, provide a powerful API, and allow using Flipper in more than just the GUI context (we call it "headless mode"). Imagine talking to your mobile device (or anything else that runs Flipper Client) from your terminal. Imagine deploying Flipper remotely in the cloud and interacting with it from your laptop. Imagine using your favorite plugins for automated testing.
In this post we cover:
- How Flipper changes to facilitate the headless mode
- How it affects plugins
- A migration guide
How Flipper changesβ
Let us take a look at how it works today as an Electron app.
Here is what happens:
- Flipper starts as an Electron application.
- WebSocket server starts.
- Device discovery starts via adb/idb/metro.
- Electron shows a web view with Flipper UI (React).
- Flipper UI queries the device discovery service for a list of devices.
- At this point, Flipper can already run "device" plugins. These plugins do not receive a connection to a running app. They talk to the device via adb/idb/metro.
- An app starts on the device.
- Flipper Client embedded in the app connects to the WebSocket server.
- Flipper updates the list of known clients and reflects it in the UI.
- Now Flipper can run "client" plugins.
- Client plugins talk to the device application over the WebSocket connection.
You can start Flipper Electron with
yarn start
from the/desktop
folder.
Here is how Flipper Headless works.
- Flipper starts as a Node.js application.
- WebSocket server starts.
- Device discovery starts via adb/idb/metro.
- Web server starts.
- It serves Flipper UI to a browser.
- Flipper UI connects to the WebSocket server.
- Flipper UI sends a message over the WebSocket connection to query the device discovery service for a list of devices.
- At this point, Flipper can already run "device" plugins. These plugins do not receive a connection to a running app. They talk to the device via adb/idb/metro.
- An app starts on the device.
- Flipper Client embedded in the app connects to the WebSocket server.
- Flipper updates the list of known clients. It sends a message over the WebSocket connection to Flipper UI with the information about the new device.
- Now Flipper can run "client" plugins.
- Client plugins talk to the device application over the WebSocket bridge - the connection from Flipper UI to Flipper WebSocket server piped to the connection from the device application to the Flipper WebSocket server.
You can start Flipper Electron with
yarn flipper-server
from the/desktop
folder.
How it affects pluginsβ
Plugins are hosted by Flipper UI. When it was a part of the Electron app, there was no problem. Plugins could access any Node.js APIs thanks to Electron magic. There were no constraints on what plugins could do. After making Flipper UI a proper web app running in a browser, we limited what plugins can do. They no longer can access the network stack or the file system because there are no corresponding browser APIs. Yet, we want to keep Flipper flexible and provide as much freedom to plugin developers as possible. Moreover, we could not leave the existing plugins without a clear migration path.
Since we already have a WebSocket connection between Flipper UI and Flipper Server, we can model almost any request-response and even stream-based Node.js APIs over it. So far, we exposed a curated subset of them:
- child_process
- exec
- fs (and fs-extra)
- constants
- access
- pathExists
- unlink
- mkdir
- rm
- copyFile
- stat
- readlink
- readFile
- writeFile
We also provided a way to download a file or send requests to the internal infrastructure.
Please, find the complete list of available APIs on GitHub. Here are Node.js API abstractions specifically.
As you might have noticed, all exposed APIs are of the request-response nature. They assume a short finite controlled lifespan. Yet, some plugins start additional web servers or spawn long-living child processes. To control their lifetime we need to track them on Flipper Server side and stop them whenever Flipper UI disconnects. Say hello to a new experimental feature - Flipper Server Add-ons!
Now, every flipper plugin could have "server add-on" meta-information. Whenever a Flipper plugin that has a corresponding Server Add-on starts, it sends a command to Flipper Server to start its Server Add-on counterpart. Flipper plugin that lives in a browser inside of Flipper UI talks to its server add-on over the WebSocket connection. Whenever a user disables a plugin, Flipper sends a command to Flipper Server to stop the add-on. At the same time, if Flipper UI crashes or the user just closes the tab, Flipper Server can kill the server add-on on its own.
Flipper plugin can talk to its server add-on companion (see
onServerAddOnMessage
, onServerAddOnUnhandledMessage
, sendToServerAddOn
in
the docs)
and act whenever it starts or stops (see onServerAddOnStart
,
onServerAddOnStop
in
the docs).
Say, you wrote an ultimate library to find primes. You were cautious of the
resource consumption, so you did it in Rust. You created a CLI interface for
your new shiny library. Now, you want your Flipper plugin to use it. It takes a
long time to find a prime and you want to keep track of the progress. You could
use getFlipperLib().remoteServerContext.childProcess.exec
, but it is not
flexible enough to monitor progress updates that your CLI sends to stdout. Here
is how you could approach it:
// contract.tsx
export interface ServerAddOnEvents {
// Server add-on sends "progress" events with the progress updates
progress: number;
}
export interface ServerAddOnMethods {
// Client plugin send "findPrime" messages to the server add-on to start finding primes
findPrime: () => Promise<number>;
}
// index.tsx (your plugin)
import {usePlugin, useValue, createState, PluginClient} from 'flipper-plugin';
import {ServerAddOnEvents, ServerAddOnMethods} from './contract';
export const plugin = (
client: PluginClient<{}, {}, ServerAddOnEvents, ServerAddOnMethods>,
) => {
const prime = createState<number | null>(null);
const progress = createState<number>(0);
client.onServerAddOnStart(async () => {
const newPrime = await client.sendToServerAddOn('findPrime');
prime.set(newPrime);
});
client.onServerAddOnStart(() => {
client.onServerAddOnMessage('progress', progressUpdate => {
progress.set(progressUpdate);
});
});
return {
prime,
progress,
};
};
export const Component = () => {
const pluginInstance = usePlugin(plugin);
const prime = useValue(pluginInstance.prime);
const progress = useValue(pluginInstance.progress);
return <div>{prime ?? `Calculating (${progress}%) done...`}</div>;
};
// serverAddOn.tsx
import {ServerAddOn} from 'flipper-plugin';
import {exec, ChildProcess} from 'child_process';
import {ServerAddOnEvents, ServerAddOnMethods} from './contract';
const serverAddOn: ServerAddOn<ServerAddOnEvents, ServerAddOnMethods> =
async connection => {
let findPrimeChildProcess: ChildProcess | undefined;
connection.receive('findPrime', () => {
if (findPrimeChildProcess) {
// Allow only one findPrime request at a time. Finding primes is expensive!
throw new Error('Too many requests!');
}
// Start our awesome Rust lib
findPrimeChildProcess = exec('/find-prime-cli', {shell: true});
// Return a Promise that resolves when a prime is found.
// Flipper will serialize the value the promise is resolved with and send it oer the wire.
return new Promise(resolve => {
// Listen to stdout of the lib for the progress updates and, eventually, the prime
findPrimeChildProcess.stdout.on('data', data => {
// Say, data is a stringified JSON
const parsed = JSON.parse(data);
if (parsed.type === 'progress') {
connection.send('progress', parsed.value);
return;
}
// Allow new requests to find new primes
findPrimeChildProcess = undefined;
// If it is not a progress update, then a prime is found.
resolve(parsed.value);
});
});
});
};
export default serverAddOn;
Migration guideβ
Examine your plugins for Node.js APIs. Replace them with
getFlipperLib().remoteServerContext.*
calls.// before
import {mkdir} from 'fs/promises';
export const plugin = () => {
const myAwesomeFn = async () => {
await mkdir('/universe/dagobah');
};
return {
myAwesomeFn,
};
};
// after
import {getFlipperLib} from 'flipper-plugin';
export const plugin = () => {
const myAwesomeFn = async () => {
await getFlipperLib().remoteServerContext.mkdir('/universe/dagobah');
};
return {
myAwesomeFn,
};
};If your plugin uses network stack of spawns a subprocess, consider creating a Server Add-on.
In your plugin's folder create a new file -
serverAddOn.tsx
In your plugin's package.json add fields
serverAddOn
andflipperBundlerEntryServerAddOn
{
// ...
"serverAddOn": "dist/serverAddOn.js",
"flipperBundlerEntryServerAddOn": "serverAddOn.tsx",
// ...
}Move your Node.js API calls to
serverAddOn.tsx
Verify your plugin works in a browser environment.
- Clone Flipper repo.
- Navigate to the
desktop
folder. - In your terminal run
yarn
. - Run
yarn flipper-server
. - Load your plugin and make sure it works.
P.S. Flipper needs you!β
Flipper is maintained by a small team at Meta, yet is serving over a hundred plugins and dozens of different targets. Our team's goal is to support Flipper as a plugin-based platform for which we maintain the infrastructure. We don't typically invest in individual plugins, but we do love plugin improvements.
For that reason, we've marked many requests in the issue tracker as
PR Welcome.
Contributing changes should be as simple as cloning the
repository and running
yarn && yarn start
in the desktop/
folder.
Investing in debugging tools, both generic ones or just for specific apps, will benefit iteration speed. And we hope Flipper will make it as hassle free as possible to create your debugging tools. For an overview of Flipper for React Native, and why and how to build your own plugins, we recommend checking out the Flipper: The Extensible DevTool Platform for React Native talk.
Happy debugging!