update js-client api (migrate to TS)

Summary:
JS/TS api:
- migrate to TS
- some refactoring (get rid of bridge, make client abstract)

Implementation isn't full yet, things to be implemented:
- let plugins connect on init command from Flipper
- implement Responder

Further plans:
- make fully compatible with react-native api without breaking changes

Reviewed By: mweststrate

Differential Revision: D21839377

fbshipit-source-id: 9e9fe4ad01632f958b59eb255c703c6cbc5fafe2
This commit is contained in:
Timur Valiev
2020-06-11 08:40:07 -07:00
committed by Facebook GitHub Bot
parent f88d707dbb
commit 896a90aa26
22 changed files with 399 additions and 530 deletions

View File

@@ -20,7 +20,7 @@ import {
import React, {Component} from 'react'; import React, {Component} from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {State as Store} from '../reducers'; import {State as Store} from '../reducers';
import {launchJsEmulator} from '../utils/js-client/serverUtils'; import {launchJsEmulator} from '../utils/js-client-server-utils/serverUtils';
import {updateSettings, Action} from '../reducers/settings'; import {updateSettings, Action} from '../reducers/settings';
import {Settings} from '../reducers/settings'; import {Settings} from '../reducers/settings';

View File

@@ -24,12 +24,12 @@ import tls from 'tls';
import net, {Socket} from 'net'; import net, {Socket} from 'net';
import {Responder, Payload, ReactiveSocket} from 'rsocket-types'; import {Responder, Payload, ReactiveSocket} from 'rsocket-types';
import GK from './fb-stubs/GK'; import GK from './fb-stubs/GK';
import {initJsEmulatorIPC} from './utils/js-client/serverUtils'; import {initJsEmulatorIPC} from './utils/js-client-server-utils/serverUtils';
import {buildClientId} from './utils/clientUtils'; import {buildClientId} from './utils/clientUtils';
import {Single} from 'rsocket-flowable'; import {Single} from 'rsocket-flowable';
import WebSocket from 'ws'; import WebSocket from 'ws';
import JSDevice from './devices/JSDevice'; import JSDevice from './devices/JSDevice';
import {WebsocketClientFlipperConnection} from './utils/js-client/websocketClientFlipperConnection'; import {WebsocketClientFlipperConnection} from './utils/js-client-server-utils/websocketClientFlipperConnection';
import querystring from 'querystring'; import querystring from 'querystring';
import {IncomingMessage} from 'http'; import {IncomingMessage} from 'http';
import ws from 'ws'; import ws from 'ws';
@@ -161,7 +161,8 @@ class Server extends EventEmitter {
}) => { }) => {
return ( return (
info.origin.startsWith('chrome-extension://') || info.origin.startsWith('chrome-extension://') ||
info.origin.startsWith('localhost:') info.origin.startsWith('localhost:') ||
info.origin.startsWith('http://localhost:')
); );
}, },
}); });
@@ -240,8 +241,8 @@ class Server extends EventEmitter {
cleanup(); cleanup();
}); });
}); });
wss.on('error', (_ws: WebSocket) => { wss.on('error', (_ws: WebSocket, e: any) => {
console.error('error from wss'); console.error('error from wss' + e);
}); });
} }

View File

@@ -1,115 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export type FlipperPluginID = string;
export type FlipperMethodID = string;
export class FlipperBridge {
registerPlugins: (plugins: Array<FlipperPluginID>) => void;
start: (appName: string) => void;
stop: () => void;
sendData: (
plugin: FlipperPluginID,
method: FlipperMethodID,
data: any,
) => void;
subscribe: (
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (any) => void,
) => void;
isAvailable: () => boolean;
}
export class FlipperResponder {
pluginId: FlipperPluginID;
methodId: FlipperMethodID;
_bridge: FlipperBridge;
constructor(
pluginId: FlipperPluginID,
methodId: FlipperMethodID,
bridge: FlipperBridge,
) {
this.pluginId = pluginId;
this.methodId = methodId;
this._bridge = bridge;
}
success(response: any) {}
error(response: any) {}
}
export type FlipperReceiver<T> = (
params: T,
responder: FlipperResponder,
) => void;
export class FlipperConnection {
pluginId: FlipperPluginID;
_bridge: FlipperBridge;
constructor(pluginId: FlipperPluginID, bridge: FlipperBridge) {
this.pluginId = pluginId;
this._bridge = bridge;
}
send(method: FlipperMethodID, data: any) {
this._bridge.sendData(this.pluginId, method, data);
}
receive(method: FlipperMethodID, receiver: FlipperReceiver<*>) {
this._bridge.subscribe(this.pluginId, method, (data) => {
receiver(data, new FlipperResponder(this.pluginId, method, this._bridge));
});
}
}
export class FlipperPlugin {
id: FlipperPluginID;
_connection: ?FlipperConnection;
onConnect(connection: FlipperConnection) {
this._connection = connection;
}
}
export class FlipperClient {
_bridge: FlipperBridge;
plugins: Map<FlipperPluginID, FlipperPlugin> = new Map();
constructor(bridge: FlipperBridge) {
this._bridge = bridge;
}
addPlugin(plugin: FlipperPlugin) {
plugin.onConnect(new FlipperConnection(plugin.id, this._bridge));
this.plugins.set(plugin.id, plugin);
}
getPlugin(id: FlipperPluginID): ?FlipperPlugin {
return this.plugins.get(id);
}
start(appName: string) {
this._bridge.registerPlugins([...this.plugins.keys()]);
this._bridge.start(appName);
}
stop() {
this._bridge.stop();
}
}

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {FlipperClient} from './api';
import {newWebviewClient} from './webviewImpl';
import {AnalyticsLoggingFlipperPlugin} from './plugins/analyticsLogging';
import {FuryPlugin} from './plugins/fury';
class FlipperManager {
flipperClient: FlipperClient;
analyticsPlugin: AnalyticsLoggingFlipperPlugin;
furyPlugin: FuryPlugin;
constructor() {
this.flipperClient = newWebviewClient();
this.analyticsPlugin = new AnalyticsLoggingFlipperPlugin();
this.furyPlugin = new FuryPlugin();
this.flipperClient.addPlugin(this.analyticsPlugin);
this.flipperClient.addPlugin(this.furyPlugin);
this.flipperClient.start('Example JS App');
}
}
let flipperManager: ?FlipperManager;
export function init() {
if (!flipperManager) {
flipperManager = new FlipperManager();
}
}
export function flipper(): ?FlipperManager {
return flipperManager;
}

View File

@@ -1,17 +0,0 @@
{
"name": "flipper-js-client",
"version": "0.46.0",
"private": true,
"main": "index.js",
"license": "MIT",
"title": "Flipper JS Client",
"icon": "apps",
"bugs": {
"email": "timurvaliev@fb.com"
},
"dependencies": {
"rsocket-flowable": "^0.0.14",
"rsocket-types": "^0.0.16",
"ws": "^7.2.3"
}
}

View File

@@ -1,32 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {FlipperPlugin} from '../api';
import type {FlipperPluginID} from '../api';
type EventId = string;
export type AnalyticsEvent = {
id: EventId,
module: string,
name: string,
time: number,
filter: ?string,
highpri: ?boolean,
extras: any,
};
export class AnalyticsLoggingFlipperPlugin extends FlipperPlugin {
id: FlipperPluginID = 'AnalyticsLogging';
sendEvent(event: AnalyticsEvent) {
this._connection && this._connection.send('reportEvent', event);
}
}

View File

@@ -1,106 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
import {FlipperPlugin, FlipperResponder, FlipperConnection} from '../api';
import type {FlipperPluginID} from '../api';
type EventId = string;
type EventType = number;
type ThreadID = number;
type SeqID = number;
type StackTraceElement = {
className: string,
methodName: string,
fileName: string,
lineNumber: number,
};
type FuryTaskEvent = {
id: EventId,
time: number,
eventType: EventType,
callStack: StackTraceElement[],
extras: any,
tag: string,
parentTid: ThreadID,
currentTid: ThreadID,
parentSeqId: SeqID,
currentSeqId: SeqID,
isDirect: boolean,
isPoint: boolean,
isOnActivated: boolean,
};
export type ReqContext = {
tag: string,
parentSeqId: SeqID,
currentSeqId: SeqID,
isDirect: boolean,
isPoint: boolean,
};
const structuredTraceFun = (error, structuredStackTrace) => {
return structuredStackTrace;
};
function getStackTrace(): CallSite[] {
const oldPrep = Error.prepareStackTrace;
Error.prepareStackTrace = structuredTraceFun;
const error = {};
Error.captureStackTrace(error, getStackTrace);
const stack = error.stack;
Error.prepareStackTrace = oldPrep;
return stack;
}
export class FuryPlugin extends FlipperPlugin {
id: FlipperPluginID = 'Fury';
onConnect(connection: FlipperConnection) {
super.onConnect(connection);
connection.receive(
'toggleRecording',
(data: any, responder: FlipperResponder) => {
window.console.log(data);
},
);
}
processEvent(reqContext: ReqContext, isOnActivated: boolean) {
const stack = getStackTrace();
const eventType = reqContext.isPoint ? 2 : isOnActivated ? 0 : 1;
const event: FuryTaskEvent = {
id: reqContext.currentSeqId + '/' + eventType,
time: new Date().getTime(),
eventType: eventType,
callStack: stack.map((frame) => {
return {
className: frame.getTypeName() || '',
methodName: frame.getFunctionName() || '',
fileName: frame.getFileName() || '',
lineNumber: frame.getLineNumber() || 0,
};
}),
extras: {},
tag: reqContext.tag,
parentTid: -1,
currentTid: -1,
parentSeqId: reqContext.parentSeqId,
currentSeqId: reqContext.currentSeqId,
isDirect: reqContext.isDirect,
isPoint: reqContext.isPoint,
isOnActivated: isOnActivated,
};
this._connection && this._connection.send('reportEvent', event);
}
}

View File

@@ -1,63 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {FlipperClient, FlipperBridge} from './api';
import type {FlipperPluginID, FlipperMethodID} from './api';
class FlipperWebviewBridgeImpl extends FlipperBridge {
_subscriptions: Map<string, (any) => void> = new Map();
registerPlugins = (plugins: Array<FlipperPluginID>) => {
window.FlipperWebviewBridge &&
window.FlipperWebviewBridge.registerPlugins(plugins);
};
start = (appName: string) => {
window.FlipperWebviewBridge && window.FlipperWebviewBridge.start(appName);
};
stop = () => {
window.FlipperWebviewBridge && window.FlipperWebviewBridge.stop();
};
sendData = (plugin: FlipperPluginID, method: FlipperMethodID, data: any) => {
window.FlipperWebviewBridge &&
window.FlipperWebviewBridge.sendFlipperObject(
plugin,
method,
JSON.stringify(data),
);
};
subscribe = (
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (any) => void,
) => {
this._subscriptions.set(plugin + method, handler);
};
isAvailable = () => {
return window.FlipperWebviewBridge != null;
};
receive(plugin: FlipperPluginID, method: FlipperMethodID, data: string) {
const handler = this._subscriptions.get(plugin + method);
handler && handler(JSON.parse(data));
}
}
export function newWebviewClient(): FlipperClient {
const bridge = new FlipperWebviewBridgeImpl();
window.flipper = {
FlipperWebviewMessageReceiver: bridge,
};
return new FlipperClient(bridge);
}

View File

@@ -1,150 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
base64-js@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
buffer@^5.0.6:
version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
core-js@^2.4.1:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
encoding@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
dependencies:
iconv-lite "~0.4.13"
fbjs-css-vars@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==
fbjs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-1.0.0.tgz#52c215e0883a3c86af2a7a776ed51525ae8e0a5a"
integrity sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==
dependencies:
core-js "^2.4.1"
fbjs-css-vars "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
rsocket-flowable@^0.0.14:
version "0.0.14"
resolved "https://registry.yarnpkg.com/rsocket-flowable/-/rsocket-flowable-0.0.14.tgz#5195dbd2972a98afe4a4ee082904fbc5890bc630"
integrity sha512-vK85yDj0DRpoTV1jQZF0JmSuQz4qj2DHMyV744I9h/8AP5jVJhuN1D+ArKQvP0VWXjFoRK23ObVMnoSdSkk4pg==
dependencies:
fbjs "^1.0.0"
rsocket-types@^0.0.16:
version "0.0.16"
resolved "https://registry.yarnpkg.com/rsocket-types/-/rsocket-types-0.0.16.tgz#2113dcf8e25478d6764f1e3e84ad4a97d16a6fde"
integrity sha512-zOJ2u5bgooj8QewqoIoaeEn1FHDZSyHp7VuDSiEJAAjTTQpYyEqKpLTvqi+tazRCBOTfulMBijootTI18VVeIg==
dependencies:
buffer "^5.0.6"
fbjs "^1.0.0"
rsocket-flowable "^0.0.14"
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
ua-parser-js@^0.7.18:
version "0.7.21"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
whatwg-fetch@>=0.10.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
ws@^7.2.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==

View File

@@ -16,7 +16,6 @@
"workspaces": { "workspaces": {
"packages": [ "packages": [
"app", "app",
"app/src/utils/js-client",
"babel-transformer", "babel-transformer",
"doctor", "doctor",
"headless", "headless",

4
flipper-js-client-sdk/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
lib/
node_modules/
*.tsbuildinfo
/coverage

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,34 @@
# flipper-sdk-api
SDK to build Flipper clients for JS based apps
## Installation
`yarn add flipper-client-sdk`
## Usage
## Example
```TypeScript
class SeaMammalPlugin extends AbsctractFlipperPlugin {
getId(): string {
return 'sea-mammals';
}
runInBackground(): boolean {
return true;
}
newRow(row: {id: string, url: string, title: string}) {
this.connection?.send("newRow", row)
}
}
const flipperClient = newWebviewClient();
cosnt plugin = new SeaMammalPlugin();
flipperClient.addPlugin();
flipperClient.start('Example JS App');
plugin.newRow({id: '1', title: 'Dolphin', url: 'example.com'})
```

View File

@@ -0,0 +1,28 @@
{
"name": "flipper-client-sdk",
"version": "0.0.1",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"title": "Flipper SDK API",
"scripts": {
"reset": "rimraf lib *.tsbuildinfo",
"build": "tsc -b"
},
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/flipper.git",
"baseUrl": "https://github.com/facebook/flipper/tree/master/flipper-js-client-sdk"
},
"keywords": [
"flipper"
],
"author": {
"name": "Facebook Inc"
},
"license": "MIT",
"licenseFilename": "LICENSE",
"readmeFilename": "README.md",
"devDependencies": {
"typescript": "^3.9.2"
}
}

View File

@@ -0,0 +1,147 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export type FlipperPluginID = string;
export type FlipperMethodID = string;
export class FlipperResponder {
pluginId: FlipperPluginID;
methodId: FlipperMethodID;
private _client: FlipperClient;
constructor(
pluginId: FlipperPluginID,
methodId: FlipperMethodID,
client: FlipperClient
) {
this.pluginId = pluginId;
this.methodId = methodId;
this._client = client;
}
success(_response: any) {}
error(_response: any) {}
}
export type FlipperReceiver<T> = (
params: T,
responder: FlipperResponder,
) => void;
export class FlipperConnection {
pluginId: FlipperPluginID;
private client: FlipperClient;
constructor(pluginId: FlipperPluginID, client: FlipperClient) {
this.pluginId = pluginId;
this.client = client;
}
send(method: FlipperMethodID, data: any) {
this.client.sendData(this.pluginId, method, data);
}
receive<T>(method: FlipperMethodID, receiver: FlipperReceiver<T>) {
this.client.subscribe(this.pluginId, method, (data: T) => {
receiver(data, new FlipperResponder(this.pluginId, method, this.client));
});
}
}
export interface FlipperPlugin {
/**
* @return The id of this plugin. This is the namespace which Flipper desktop plugins will call
* methods on to route them to your plugin. This should match the id specified in your React
* plugin.
*/
getId(): string;
/**
* Called when a connection has been established. The connection passed to this method is valid
* until {@link FlipperPlugin#onDisconnect()} is called.
*/
onConnect(connection: FlipperConnection): void;
/**
* Called when the connection passed to `FlipperPlugin#onConnect(FlipperConnection)` is no
* longer valid. Do not try to use the connection in or after this method has been called.
*/
onDisconnect(): void;
/**
* Returns true if the plugin is meant to be run in background too, otherwise it returns false.
*/
runInBackground(): boolean;
}
export abstract class AbstractFlipperPlugin implements FlipperPlugin{
protected connection: FlipperConnection | null | undefined;
onConnect(connection: FlipperConnection): void {
this.connection = connection;
}
onDisconnect(): void {
this.connection = null;
}
abstract getId(): string;
abstract runInBackground(): boolean;
}
export abstract class FlipperClient {
_isConnected: boolean = false;
plugins: Map<FlipperPluginID, FlipperPlugin> = new Map();
addPlugin(plugin: FlipperPlugin) {
if (this._isConnected) {
plugin.onConnect(new FlipperConnection(plugin.getId(), this));
}
this.plugins.set(plugin.getId(), plugin);
}
getPlugin(id: FlipperPluginID): FlipperPlugin | undefined {
return this.plugins.get(id);
}
onConnect() {
if (this._isConnected) {
return;
}
this._isConnected = true;
Array.from(this.plugins.values()).map((plugin) =>
plugin.onConnect(new FlipperConnection(plugin.getId(), this)),
);
}
onDisconnect() {
this._isConnected = false;
Array.from(this.plugins.values()).map((plugin) => plugin.onDisconnect());
}
abstract start: (appName: string) => void;
abstract stop: () => void;
abstract sendData: (
plugin: FlipperPluginID,
method: FlipperMethodID,
data: any,
) => void;
abstract subscribe: <T>(
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (message: T) => void,
) => void;
abstract isAvailable: () => boolean;
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {FlipperClient, AbstractFlipperPlugin} from './api';
import {newWebviewClient} from './webviewImpl';
class SeaMammalPlugin extends AbstractFlipperPlugin {
getId(): string {
return 'sea-mammals';
}
runInBackground(): boolean {
return true;
}
newRow(row: {id: string, url: string, title: string}) {
this.connection?.send("newRow", row)
}
}
class FlipperManager {
flipperClient: FlipperClient;
seaMammalPlugin: SeaMammalPlugin;
constructor() {
this.flipperClient = newWebviewClient();
this.seaMammalPlugin = new SeaMammalPlugin();
this.flipperClient.addPlugin(this.seaMammalPlugin);
this.flipperClient.start('Example JS App');
}
}
let flipperManager: FlipperManager | undefined;
export function init() {
if (!flipperManager) {
flipperManager = new FlipperManager();
}
}
export function flipper(): FlipperManager | undefined {
return flipperManager;
}

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export * from './api';

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {FlipperClient} from './api';
import type {FlipperPluginID, FlipperMethodID} from './api';
class FlipperWebviewClient extends FlipperClient {
_subscriptions: Map<string, (message: any) => void> = new Map();
_client: FlipperClient | null = null;
start = (appName: string) => {
const bridge = (window as any).FlipperWebviewBridge;
bridge?.registerPlugins(this.plugins);
bridge?.start(appName);
};
stop = () => {
const bridge = (window as any).FlipperWebviewBridge;
bridge?.FlipperWebviewBridge.stop();
};
sendData = (plugin: FlipperPluginID, method: FlipperMethodID, data: any) => {
const bridge = (window as any).FlipperWebviewBridge;
bridge && bridge.sendFlipperObject(plugin, method, JSON.stringify(data));
};
subscribe = (
plugin: FlipperPluginID,
method: FlipperMethodID,
handler: (msg: any) => void,
) => {
this._subscriptions.set(plugin + method, handler);
};
isAvailable = () => {
return (window as any).FlipperWebviewBridge != null;
};
receive(plugin: FlipperPluginID, method: FlipperMethodID, data: string) {
const handler = this._subscriptions.get(plugin + method);
handler && handler(JSON.parse(data));
}
setClient(client: FlipperClient) {
this._client = client;
}
}
export function newWebviewClient(): FlipperClient {
return new FlipperWebviewClient();
}

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"esModuleInterop": true,
"target": "ES2017",
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"declaration": true,
"moduleResolution": "node",
"skipLibCheck": true,
"strict": true,
"downlevelIteration": true,
"module": "commonjs",
"lib": [
"es7",
"dom",
"es2017"
],
"composite": true,
"baseUrl": ".",
"allowJs": true
},
"include": [
"src"
],
"exclude": [
"node_modules",
"**/__tests__/*"
]
}

View File

@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
typescript@^3.9.2:
version "3.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==