diff --git a/desktop/examples/headless-demo/README.md b/desktop/examples/headless-demo/README.md new file mode 100644 index 000000000..80ceae788 --- /dev/null +++ b/desktop/examples/headless-demo/README.md @@ -0,0 +1,20 @@ +# headless-demo + +**Experimental feature!** + +Flipper can run plugins in a headless mode - expose their API over the wire. This is a simple example of how it might look like. + +## Quick start + +0. Run `yarn` from this repo to install dependencies +0. Start Flipper Server: from `desktop` folder run `yarn flipper-server` +0. Start an Android device +0. Run `yarn start` from this repo + +## What happens under the hood + +0. This script connects to Flipper via WebSockets +0. It fetches a list of devices +0. It fetches a list of available headless plugins for the Android device +0. It activates `headless-demo` plugin +0. It sends `increment` command to the plugin diff --git a/desktop/examples/headless-demo/index.js b/desktop/examples/headless-demo/index.js new file mode 100644 index 000000000..5c7df8a91 --- /dev/null +++ b/desktop/examples/headless-demo/index.js @@ -0,0 +1,111 @@ +/** + * 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 + */ + +const WebSocket = require('ws'); + +class FlipperServerClient { + messageId = 0; + wsClient = new WebSocket('ws://localhost:52342?server_companion=true'); + execReqs = new Map(); + + async init() { + await new Promise((resolve) => this.wsClient.on('open', resolve)); + + this.wsClient.on('message', (data) => { + const {event, payload} = JSON.parse(data); + + switch (event) { + case 'exec-response': + case 'exec-response-error': { + const req = this.execReqs.get(payload.id); + + if (!req) { + console.warn('Unknown exec request'); + return; + } + + this.execReqs.delete(payload.id); + + if (event === 'exec-response') { + req.resolve(payload.data); + } else { + req.reject(payload.data); + } + return; + } + } + }); + } + + exec(command, args) { + return new Promise((resolve, reject) => { + const id = this.messageId++; + + this.wsClient.send( + JSON.stringify({ + event: 'exec', + payload: { + id, + command, + args, + }, + }), + ); + + this.execReqs.set(id, {resolve, reject}); + }); + } +} + +const main = async () => { + console.log('main'); + + const client = new FlipperServerClient(); + await client.init(); + + console.log('Initialized client'); + + const devices = await client.exec('device-list', []); + + console.log('Devices', JSON.stringify(devices)); + + const targetDevice = devices.find((device) => !!device.serial); + + const availablePlugins = await client.exec('companion-device-plugin-list', [ + targetDevice.serial, + ]); + + console.log( + 'Available plugins', + targetDevice.serial, + JSON.stringify(availablePlugins), + ); + + console.log('Activating headless-demo plugin for', targetDevice.serial); + + await client.exec('companion-device-plugin-start', [ + targetDevice.serial, + 'headless-demo', + ]); + + console.log('Activated headless-demo plugin'); + + console.log('Using increment api'); + + const res = await client.exec('companion-device-plugin-exec', [ + targetDevice.serial, + 'headless-demo', + 'increment', + [3], + ]); + + console.log('Received a response', JSON.stringify(res)); +}; + +main().catch(console.error); diff --git a/desktop/examples/headless-demo/package.json b/desktop/examples/headless-demo/package.json new file mode 100644 index 000000000..5c67f0b17 --- /dev/null +++ b/desktop/examples/headless-demo/package.json @@ -0,0 +1,12 @@ +{ + "name": "headless-demo", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "ws": "^8.6.0" + } +} diff --git a/desktop/examples/headless-demo/yarn.lock b/desktop/examples/headless-demo/yarn.lock new file mode 100644 index 000000000..22ba9dc32 --- /dev/null +++ b/desktop/examples/headless-demo/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ws@^8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23" + integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw== diff --git a/desktop/plugins/public/headless-demo/index.tsx b/desktop/plugins/public/headless-demo/index.tsx new file mode 100644 index 000000000..2f9f811a0 --- /dev/null +++ b/desktop/plugins/public/headless-demo/index.tsx @@ -0,0 +1,40 @@ +/** + * 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 React from 'react'; + +import {createState, Layout, FlipperPluginInstance} from 'flipper-plugin'; + +export function API( + pluginInstance: FlipperPluginInstance, +) { + return { + increment: pluginInstance.increment, + }; +} + +export function devicePlugin() { + const data = createState(0); + + const increment = (step: number = 1) => { + const newVal = data.get() + step; + data.set(newVal); + return newVal; + }; + + return {increment}; +} + +export function Component() { + return ( + + I am a new shiny headless plugin + + ); +} diff --git a/desktop/plugins/public/headless-demo/package.json b/desktop/plugins/public/headless-demo/package.json new file mode 100644 index 000000000..f15a6d4a6 --- /dev/null +++ b/desktop/plugins/public/headless-demo/package.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://fbflipper.com/schemas/plugin-package/v2.json", + "name": "flipper-plugin-headless-demo", + "id": "headless-demo", + "pluginType": "device", + "version": "0.0.0", + "flipperBundlerEntry": "index.tsx", + "headless": true, + "main": "dist/bundle.js", + "license": "MIT", + "title": "Headless-demo", + "icon": "apps", + "keywords": [ + "flipper-plugin" + ], + "bugs": { + "url": "https://github.com/facebook/flipper/issues" + }, + "peerDependencies": { + "flipper-plugin": "*", + "antd": "*", + "react": "*", + "react-dom": "*", + "@emotion/styled": "*", + "@ant-design/icons": "*", + "@types/react": "*", + "@types/react-dom": "*", + "@types/node": "*" + } +}