JS apps support 1/n
Summary: ### Connecting Flipper with JS apps by using electron's BrowserWindow and IPC 1. UI: there is a menu item in Devices tab which opens JS Emulator Launcher Sheet. Here we can configure URL to open and initial size of the window. 2. BrowserWindow, preloaded js: there is SupportJSClientPreload.js which initialize communication between flipper and app via electron's ipc 3. On flipper's side there is src/utils/js-client/serverUtils.tsx which contains most of JS emulator related code 4. Extracting of FlipperClientConnection: since we don't use RScocket to communicate with JS app I extracted needed methods to FlipperClientConnection (located in Client) and partly implemented them in JSClientFlipperConnection (requestResponse is just send a message now, doesn't return actual result) Reviewed By: jknoxville Differential Revision: D18572882 fbshipit-source-id: 56d1ca1a60ed2e51329b917021a09382cbb1ceec
This commit is contained in:
committed by
Facebook Github Bot
parent
e7ad713df8
commit
c685493db0
@@ -19,6 +19,7 @@ import ShareSheetExportUrl from './chrome/ShareSheetExportUrl';
|
|||||||
import SignInSheet from './chrome/SignInSheet';
|
import SignInSheet from './chrome/SignInSheet';
|
||||||
import ExportDataPluginSheet from './chrome/ExportDataPluginSheet';
|
import ExportDataPluginSheet from './chrome/ExportDataPluginSheet';
|
||||||
import ShareSheetExportFile from './chrome/ShareSheetExportFile';
|
import ShareSheetExportFile from './chrome/ShareSheetExportFile';
|
||||||
|
import JSEmulatorLauncherSheet from './chrome/JSEmulatorLauncherSheet';
|
||||||
import PluginContainer from './PluginContainer';
|
import PluginContainer from './PluginContainer';
|
||||||
import Sheet from './chrome/Sheet';
|
import Sheet from './chrome/Sheet';
|
||||||
import {ipcRenderer, remote} from 'electron';
|
import {ipcRenderer, remote} from 'electron';
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
ACTIVE_SHEET_SHARE_DATA_IN_FILE,
|
ACTIVE_SHEET_SHARE_DATA_IN_FILE,
|
||||||
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
|
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
|
||||||
ACTIVE_SHEET_PLUGIN_SHEET,
|
ACTIVE_SHEET_PLUGIN_SHEET,
|
||||||
|
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
|
||||||
} from './reducers/application';
|
} from './reducers/application';
|
||||||
import {Logger} from './fb-interfaces/Logger';
|
import {Logger} from './fb-interfaces/Logger';
|
||||||
import BugReporter from './fb-stubs/BugReporter';
|
import BugReporter from './fb-stubs/BugReporter';
|
||||||
@@ -122,6 +124,8 @@ export class App extends React.Component<Props> {
|
|||||||
case ACTIVE_SHEET_PLUGIN_SHEET:
|
case ACTIVE_SHEET_PLUGIN_SHEET:
|
||||||
// Currently unused.
|
// Currently unused.
|
||||||
return null;
|
return null;
|
||||||
|
case ACTIVE_SHEET_JS_EMULATOR_LAUNCHER:
|
||||||
|
return <JSEmulatorLauncherSheet onHide={onHide} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {App} from './App.js';
|
|||||||
import {Logger} from './fb-interfaces/Logger';
|
import {Logger} from './fb-interfaces/Logger';
|
||||||
import {Store} from './reducers/index';
|
import {Store} from './reducers/index';
|
||||||
import {setPluginState} from './reducers/pluginStates';
|
import {setPluginState} from './reducers/pluginStates';
|
||||||
import {RSocketClientSocket} from 'rsocket-core/RSocketClient';
|
import {Payload, ConnectionStatus} from 'rsocket-types';
|
||||||
|
import {Flowable, Single} from 'rsocket-flowable';
|
||||||
import {performance} from 'perf_hooks';
|
import {performance} from 'perf_hooks';
|
||||||
import {reportPlatformFailures, reportPluginFailures} from './utils/metrics';
|
import {reportPlatformFailures, reportPluginFailures} from './utils/metrics';
|
||||||
import {notNull} from './utils/typeUtils';
|
import {notNull} from './utils/typeUtils';
|
||||||
@@ -97,6 +98,13 @@ const handleError = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface FlipperClientConnection<D, M> {
|
||||||
|
connectionStatus(): Flowable<ConnectionStatus>;
|
||||||
|
close(): void;
|
||||||
|
fireAndForget(payload: Payload<D, M>): void;
|
||||||
|
requestResponse(payload: Payload<D, M>): Single<Payload<D, M>>;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Client extends EventEmitter {
|
export default class Client extends EventEmitter {
|
||||||
app: App | undefined;
|
app: App | undefined;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@@ -105,7 +113,7 @@ export default class Client extends EventEmitter {
|
|||||||
sdkVersion: number;
|
sdkVersion: number;
|
||||||
messageIdCounter: number;
|
messageIdCounter: number;
|
||||||
plugins: Plugins;
|
plugins: Plugins;
|
||||||
connection: RSocketClientSocket<any, any> | null | undefined;
|
connection: FlipperClientConnection<any, any> | null | undefined;
|
||||||
store: Store;
|
store: Store;
|
||||||
activePlugins: Set<string>;
|
activePlugins: Set<string>;
|
||||||
device: Promise<BaseDevice>;
|
device: Promise<BaseDevice>;
|
||||||
@@ -129,7 +137,7 @@ export default class Client extends EventEmitter {
|
|||||||
constructor(
|
constructor(
|
||||||
id: string,
|
id: string,
|
||||||
query: ClientQuery,
|
query: ClientQuery,
|
||||||
conn: RSocketClientSocket<any, any> | null | undefined,
|
conn: FlipperClientConnection<any, any> | null | undefined,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
store: Store,
|
store: Store,
|
||||||
plugins?: Plugins | null | undefined,
|
plugins?: Plugins | null | undefined,
|
||||||
|
|||||||
@@ -12,11 +12,17 @@ import {connect, ReactReduxContext} from 'react-redux';
|
|||||||
import {spawn} from 'child_process';
|
import {spawn} from 'child_process';
|
||||||
import {dirname} from 'path';
|
import {dirname} from 'path';
|
||||||
import {selectDevice, preferDevice} from '../reducers/connections';
|
import {selectDevice, preferDevice} from '../reducers/connections';
|
||||||
|
import {
|
||||||
|
setActiveSheet,
|
||||||
|
ActiveSheet,
|
||||||
|
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
|
||||||
|
} from '../reducers/application';
|
||||||
import {default as which} from 'which';
|
import {default as which} from 'which';
|
||||||
import {showOpenDialog} from '../utils/exportData';
|
import {showOpenDialog} from '../utils/exportData';
|
||||||
import BaseDevice from '../devices/BaseDevice';
|
import BaseDevice from '../devices/BaseDevice';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {State} from '../reducers';
|
import {State} from '../reducers';
|
||||||
|
import GK from '../fb-stubs/GK';
|
||||||
|
|
||||||
type StateFromProps = {
|
type StateFromProps = {
|
||||||
selectedDevice: BaseDevice | null | undefined;
|
selectedDevice: BaseDevice | null | undefined;
|
||||||
@@ -27,6 +33,7 @@ type StateFromProps = {
|
|||||||
type DispatchFromProps = {
|
type DispatchFromProps = {
|
||||||
selectDevice: (device: BaseDevice) => void;
|
selectDevice: (device: BaseDevice) => void;
|
||||||
preferDevice: (device: string) => void;
|
preferDevice: (device: string) => void;
|
||||||
|
setActiveSheet: (sheet: ActiveSheet) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {};
|
type OwnProps = {};
|
||||||
@@ -154,17 +161,30 @@ class DevicesButton extends Component<Props> {
|
|||||||
label: name,
|
label: name,
|
||||||
click: () => this.launchEmulator(name),
|
click: () => this.launchEmulator(name),
|
||||||
}));
|
}));
|
||||||
if (emulators.length > 0) {
|
|
||||||
|
// Launch JS emulator
|
||||||
|
if (GK.get('flipper_js_client_emulator')) {
|
||||||
|
if (emulators.length > 0) {
|
||||||
|
dropdown.push(
|
||||||
|
{type: 'separator' as 'separator'},
|
||||||
|
{
|
||||||
|
label: 'Launch Android emulators',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
...emulators,
|
||||||
|
);
|
||||||
|
}
|
||||||
dropdown.push(
|
dropdown.push(
|
||||||
{type: 'separator' as 'separator'},
|
{type: 'separator' as 'separator'},
|
||||||
{
|
{
|
||||||
label: 'Launch Android emulators',
|
label: 'Launch JS Web App',
|
||||||
enabled: false,
|
click: () =>
|
||||||
|
this.props.setActiveSheet(ACTIVE_SHEET_JS_EMULATOR_LAUNCHER),
|
||||||
},
|
},
|
||||||
...emulators,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropdown.length > 0) {
|
if (dropdown.length > 0) {
|
||||||
dropdown.push({type: 'separator' as 'separator'});
|
dropdown.push({type: 'separator' as 'separator'});
|
||||||
}
|
}
|
||||||
@@ -196,5 +216,6 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, State>(
|
|||||||
{
|
{
|
||||||
selectDevice,
|
selectDevice,
|
||||||
preferDevice,
|
preferDevice,
|
||||||
|
setActiveSheet,
|
||||||
},
|
},
|
||||||
)(DevicesButton);
|
)(DevicesButton);
|
||||||
|
|||||||
122
src/chrome/JSEmulatorLauncherSheet.tsx
Normal file
122
src/chrome/JSEmulatorLauncherSheet.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
FlexColumn,
|
||||||
|
Button,
|
||||||
|
styled,
|
||||||
|
Text,
|
||||||
|
FlexRow,
|
||||||
|
Spacer,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
} from 'flipper';
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {State as Store} from '../reducers';
|
||||||
|
import {launchJsEmulator} from '../utils/js-client/serverUtils';
|
||||||
|
|
||||||
|
const Container = styled(FlexColumn)({
|
||||||
|
padding: 20,
|
||||||
|
width: 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Title = styled(Text)({
|
||||||
|
marginBottom: 18,
|
||||||
|
marginRight: 10,
|
||||||
|
fontWeight: 100,
|
||||||
|
fontSize: '40px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const textareaStyle = {
|
||||||
|
margin: 0,
|
||||||
|
marginBottom: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TitleInput = styled(Input)({
|
||||||
|
...textareaStyle,
|
||||||
|
height: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
onHide: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateFromProps = {};
|
||||||
|
|
||||||
|
type DispatchFromProps = {};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
url: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = OwnProps & StateFromProps & DispatchFromProps;
|
||||||
|
class JSEmulatorLauncherSheet extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
url: 'http://localhost:8888',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
onUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({url: e.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
onHeightChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({height: Number(e.target.value)});
|
||||||
|
};
|
||||||
|
|
||||||
|
onWidthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({width: Number(e.target.value)});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {url, height, width} = this.state;
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>Launch Web App</Title>
|
||||||
|
<Label>Url</Label>
|
||||||
|
<TitleInput value={url} onChange={this.onUrlChange} />
|
||||||
|
<Label>Height</Label>
|
||||||
|
<TitleInput value={height} onChange={this.onHeightChange} />
|
||||||
|
<Label>Width</Label>
|
||||||
|
<TitleInput value={width} onChange={this.onWidthChange} />
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<FlexRow>
|
||||||
|
<Spacer />
|
||||||
|
<Button compact padded onClick={this.props.onHide}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
compact
|
||||||
|
padded
|
||||||
|
onClick={() => {
|
||||||
|
launchJsEmulator(
|
||||||
|
this.state.url,
|
||||||
|
this.state.height,
|
||||||
|
this.state.width,
|
||||||
|
);
|
||||||
|
this.props.onHide();
|
||||||
|
}}>
|
||||||
|
Launch
|
||||||
|
</Button>
|
||||||
|
</FlexRow>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
|
||||||
|
() => ({}),
|
||||||
|
{},
|
||||||
|
)(JSEmulatorLauncherSheet);
|
||||||
@@ -52,7 +52,7 @@ export type DeviceExport = {
|
|||||||
logs: Array<DeviceLogEntry>;
|
logs: Array<DeviceLogEntry>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS';
|
export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS' | 'JSWebApp';
|
||||||
|
|
||||||
export default class BaseDevice {
|
export default class BaseDevice {
|
||||||
constructor(serial: string, deviceType: DeviceType, title: string, os: OS) {
|
constructor(serial: string, deviceType: DeviceType, title: string, os: OS) {
|
||||||
|
|||||||
20
src/devices/JSDevice.tsx
Normal file
20
src/devices/JSDevice.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* 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 BaseDevice from './BaseDevice';
|
||||||
|
|
||||||
|
export default class JSDevice extends BaseDevice {
|
||||||
|
webContentsId: number;
|
||||||
|
|
||||||
|
constructor(serial: string, title: string, webContentsId: number) {
|
||||||
|
super(serial, 'emulator', title, 'JSWebApp');
|
||||||
|
this.devicePlugins = [];
|
||||||
|
this.webContentsId = webContentsId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' =
|
|||||||
export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' =
|
export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' =
|
||||||
'SET_EXPORT_STATUS_MESSAGE';
|
'SET_EXPORT_STATUS_MESSAGE';
|
||||||
export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE';
|
export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE';
|
||||||
|
export const ACTIVE_SHEET_JS_EMULATOR_LAUNCHER: 'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER' =
|
||||||
|
'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER';
|
||||||
|
|
||||||
export type ActiveSheet =
|
export type ActiveSheet =
|
||||||
| typeof ACTIVE_SHEET_PLUGIN_SHEET
|
| typeof ACTIVE_SHEET_PLUGIN_SHEET
|
||||||
@@ -37,6 +39,7 @@ export type ActiveSheet =
|
|||||||
| typeof ACTIVE_SHEET_DOCTOR
|
| typeof ACTIVE_SHEET_DOCTOR
|
||||||
| typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE
|
| typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE
|
||||||
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
|
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
|
||||||
|
| typeof ACTIVE_SHEET_JS_EMULATOR_LAUNCHER
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
export type LauncherMsg = {
|
export type LauncherMsg = {
|
||||||
|
|||||||
@@ -16,17 +16,19 @@ import {RSocketServer} from 'rsocket-core';
|
|||||||
import RSocketTCPServer from 'rsocket-tcp-server';
|
import RSocketTCPServer from 'rsocket-tcp-server';
|
||||||
import {Single} from 'rsocket-flowable';
|
import {Single} from 'rsocket-flowable';
|
||||||
import Client from './Client';
|
import Client from './Client';
|
||||||
|
import {FlipperClientConnection} from './Client';
|
||||||
import {UninitializedClient} from './UninitializedClient';
|
import {UninitializedClient} from './UninitializedClient';
|
||||||
import {reportPlatformFailures} from './utils/metrics';
|
import {reportPlatformFailures} from './utils/metrics';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import invariant from 'invariant';
|
import invariant from 'invariant';
|
||||||
import tls from 'tls';
|
import tls from 'tls';
|
||||||
import net, {Socket} from 'net';
|
import net, {Socket} from 'net';
|
||||||
import {RSocketClientSocket} from 'rsocket-core/RSocketClient';
|
|
||||||
import {Responder, Payload, ReactiveSocket} from 'rsocket-types';
|
import {Responder, Payload, ReactiveSocket} from 'rsocket-types';
|
||||||
|
import GK from './fb-stubs/GK';
|
||||||
|
import {initJsEmulatorIPC} from './utils/js-client/serverUtils';
|
||||||
|
|
||||||
type ClientInfo = {
|
type ClientInfo = {
|
||||||
connection: RSocketClientSocket<any, any> | null | undefined;
|
connection: FlipperClientConnection<any, any> | null | undefined;
|
||||||
client: Client;
|
client: Client;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,6 +86,11 @@ class Server extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
reportPlatformFailures(this.initialisePromise, 'initializeServer');
|
reportPlatformFailures(this.initialisePromise, 'initializeServer');
|
||||||
|
|
||||||
|
if (GK.get('flipper_js_client_emulator')) {
|
||||||
|
initJsEmulatorIPC(this.store, this.logger, this, this.connections);
|
||||||
|
}
|
||||||
|
|
||||||
return this.initialisePromise;
|
return this.initialisePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +311,7 @@ class Server extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addConnection(
|
async addConnection(
|
||||||
conn: RSocketClientSocket<any, any>,
|
conn: FlipperClientConnection<any, any>,
|
||||||
query: ClientQuery,
|
query: ClientQuery,
|
||||||
csrQuery: ClientCsrQuery,
|
csrQuery: ClientCsrQuery,
|
||||||
): Promise<Client> {
|
): Promise<Client> {
|
||||||
|
|||||||
210
src/utils/js-client/serverUtils.tsx
Normal file
210
src/utils/js-client/serverUtils.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* 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 Client, {ClientQuery} from '../../Client';
|
||||||
|
import {FlipperClientConnection} from '../../Client';
|
||||||
|
import {ipcRenderer, remote} from 'electron';
|
||||||
|
import JSDevice from '../../devices/JSDevice';
|
||||||
|
import {Store} from 'src/reducers';
|
||||||
|
import {Logger} from 'src/fb-interfaces/Logger';
|
||||||
|
|
||||||
|
import {Payload, ConnectionStatus, ISubscriber} from 'rsocket-types';
|
||||||
|
import {Flowable, Single} from 'rsocket-flowable';
|
||||||
|
import Server from 'src/server';
|
||||||
|
|
||||||
|
const connections: Map<number, JSClientFlipperConnection<any>> = new Map();
|
||||||
|
|
||||||
|
const availablePlugins: Map<number, Array<string>> = new Map();
|
||||||
|
|
||||||
|
function jsDeviceId(windowId: number): string {
|
||||||
|
return 'test_js_device' + windowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initJsEmulatorIPC(
|
||||||
|
store: Store,
|
||||||
|
logger: Logger,
|
||||||
|
flipperServer: Server,
|
||||||
|
flipperConnections: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
connection: FlipperClientConnection<any, any> | null | undefined;
|
||||||
|
client: Client;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
ipcRenderer.on('from-js-emulator-init-client', (_event, message) => {
|
||||||
|
const {windowId} = message;
|
||||||
|
const {plugins, appName} = message.payload;
|
||||||
|
store.dispatch({
|
||||||
|
type: 'REGISTER_DEVICE',
|
||||||
|
payload: new JSDevice(jsDeviceId(windowId), 'jsEmulator', windowId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = new JSClientFlipperConnection(windowId);
|
||||||
|
connections.set(windowId, connection);
|
||||||
|
availablePlugins.set(windowId, plugins);
|
||||||
|
|
||||||
|
const query: ClientQuery = {
|
||||||
|
app: appName,
|
||||||
|
os: 'JSWebApp',
|
||||||
|
device: 'jsEmulator',
|
||||||
|
device_id: jsDeviceId(windowId),
|
||||||
|
sdk_version: 2, // hack to bybass callbacks in Client, will be fixed when JS Connection will be fully implemented
|
||||||
|
};
|
||||||
|
const clientId = `${query.app}#${query.os}#${query.device}#${query.device_id}`;
|
||||||
|
|
||||||
|
const client = new Client(
|
||||||
|
clientId,
|
||||||
|
query,
|
||||||
|
connection,
|
||||||
|
logger,
|
||||||
|
store,
|
||||||
|
plugins,
|
||||||
|
);
|
||||||
|
|
||||||
|
flipperConnections.set(clientId, {connection: connection, client: client});
|
||||||
|
|
||||||
|
connection.connectionStatus().subscribe({
|
||||||
|
onNext(payload) {
|
||||||
|
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
|
||||||
|
console.debug(`Device disconnected ${client.id}`, 'server');
|
||||||
|
flipperServer.removeConnection(client.id);
|
||||||
|
const toUnregister = new Set<string>();
|
||||||
|
toUnregister.add(jsDeviceId(windowId));
|
||||||
|
store.dispatch({
|
||||||
|
type: 'UNREGISTER_DEVICES',
|
||||||
|
payload: toUnregister,
|
||||||
|
});
|
||||||
|
connections.delete(windowId);
|
||||||
|
availablePlugins.delete(windowId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubscribe(subscription) {
|
||||||
|
subscription.request(Number.MAX_SAFE_INTEGER);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.init().then(() => {
|
||||||
|
console.log(client);
|
||||||
|
flipperServer.emit('new-client', client);
|
||||||
|
flipperServer.emit('clients-change');
|
||||||
|
client.emit('plugins-change');
|
||||||
|
|
||||||
|
ipcRenderer.on('from-js-emulator', (_event, message) => {
|
||||||
|
const {command, payload} = message;
|
||||||
|
if (command === 'sendFlipperObject') {
|
||||||
|
client.onMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
params: {
|
||||||
|
api: payload.api,
|
||||||
|
method: payload.method,
|
||||||
|
params: JSON.parse(payload.params),
|
||||||
|
},
|
||||||
|
method: 'execute',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function launchJsEmulator(url: string, height: number, width: number) {
|
||||||
|
const BrowserWindow = remote.BrowserWindow;
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
webPreferences: {
|
||||||
|
preload: require('path').join(
|
||||||
|
remote.app.getAppPath(),
|
||||||
|
'SupportJSClientPreload.js',
|
||||||
|
),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: false,
|
||||||
|
allowRunningInsecureContent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
win.webContents.on('preload-error', (_event, path, error) => {
|
||||||
|
console.log(path, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
win.loadURL(url);
|
||||||
|
|
||||||
|
win.webContents.on('did-finish-load', () => {
|
||||||
|
win.webContents.send('parent-window-id', remote.getCurrentWebContents().id);
|
||||||
|
|
||||||
|
const childWindowId = win.webContents.id;
|
||||||
|
win.on('closed', () => {
|
||||||
|
connections.get(childWindowId)?.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JSClientFlipperConnection<M>
|
||||||
|
implements FlipperClientConnection<string, M> {
|
||||||
|
webContentsId: number;
|
||||||
|
connStatusSubscribers: Set<ISubscriber<ConnectionStatus>> = new Set();
|
||||||
|
connStatus: ConnectionStatus;
|
||||||
|
|
||||||
|
constructor(webContentsId: number) {
|
||||||
|
this.webContentsId = webContentsId;
|
||||||
|
this.connStatus = {kind: 'CONNECTED'};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionStatus(): Flowable<ConnectionStatus> {
|
||||||
|
return new Flowable<ConnectionStatus>(subscriber => {
|
||||||
|
subscriber.onSubscribe({
|
||||||
|
cancel: () => {
|
||||||
|
this.connStatusSubscribers.delete(subscriber);
|
||||||
|
},
|
||||||
|
request: _ => {
|
||||||
|
this.connStatusSubscribers.add(subscriber);
|
||||||
|
subscriber.onNext(this.connStatus);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.connStatus = {kind: 'CLOSED'};
|
||||||
|
this.connStatusSubscribers.forEach(subscriber => {
|
||||||
|
subscriber.onNext(this.connStatus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fireAndForget(payload: Payload<string, M>): void {
|
||||||
|
ipcRenderer.sendTo(
|
||||||
|
this.webContentsId,
|
||||||
|
'message-to-plugin',
|
||||||
|
JSON.parse(payload.data != null ? payload.data : '{}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fully implement and return actual result
|
||||||
|
requestResponse(payload: Payload<string, M>): Single<Payload<string, M>> {
|
||||||
|
return new Single(subscriber => {
|
||||||
|
const method =
|
||||||
|
payload.data != null ? JSON.parse(payload.data).method : 'not-defined';
|
||||||
|
if (method != 'getPlugins') {
|
||||||
|
this.fireAndForget(payload);
|
||||||
|
}
|
||||||
|
subscriber.onSubscribe(() => {});
|
||||||
|
subscriber.onComplete(
|
||||||
|
method == 'getPlugins'
|
||||||
|
? {
|
||||||
|
data: JSON.stringify({
|
||||||
|
success: {plugins: availablePlugins.get(this.webContentsId)},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {data: JSON.stringify({success: null})},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
90
static/SupportJSClientPreload.js
Normal file
90
static/SupportJSClientPreload.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==============
|
||||||
|
// Preload script
|
||||||
|
// ==============
|
||||||
|
const {remote, ipcRenderer} = require('electron');
|
||||||
|
|
||||||
|
let FlipperMainWindowId = 0;
|
||||||
|
|
||||||
|
ipcRenderer.on('parent-window-id', (event, message) => {
|
||||||
|
FlipperMainWindowId = message;
|
||||||
|
});
|
||||||
|
|
||||||
|
let FlipperIsClientInit = false;
|
||||||
|
let FlipperMemoizedPlugins;
|
||||||
|
|
||||||
|
function initClient(plugins) {
|
||||||
|
if (FlipperIsClientInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plugins) {
|
||||||
|
FlipperMemoizedPlugins = plugins;
|
||||||
|
}
|
||||||
|
if (FlipperMainWindowId != 0) {
|
||||||
|
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator-init-client', {
|
||||||
|
command: 'initClient',
|
||||||
|
windowId: remote.getCurrentWebContents().id,
|
||||||
|
payload: {
|
||||||
|
plugins: plugins ? plugins : FlipperMemoizedPlugins,
|
||||||
|
appName: 'kite/weblite',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
FlipperIsClientInit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.FlipperWebviewBridge = {
|
||||||
|
registerPlugins: function(plugins) {
|
||||||
|
console.log(plugins);
|
||||||
|
if (FlipperMainWindowId != 0) {
|
||||||
|
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator', {
|
||||||
|
command: 'registerPlugins',
|
||||||
|
payload: plugins,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
start: function() {
|
||||||
|
console.log('start');
|
||||||
|
|
||||||
|
if (FlipperMainWindowId != 0) {
|
||||||
|
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator', {
|
||||||
|
command: 'start',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendFlipperObject: function(plugin, method, data) {
|
||||||
|
console.log(plugin, method, data);
|
||||||
|
initClient();
|
||||||
|
if (FlipperMainWindowId != 0) {
|
||||||
|
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator', {
|
||||||
|
command: 'sendFlipperObject',
|
||||||
|
payload: {
|
||||||
|
api: plugin,
|
||||||
|
method: method,
|
||||||
|
params: data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isFlipperSupported: true,
|
||||||
|
initClient: initClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.on('message-to-plugin', (event, message) => {
|
||||||
|
const flipper = window.flipper;
|
||||||
|
if (!flipper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const receiver = flipper.FlipperWebviewMessageReceiver.receive;
|
||||||
|
const {api, method, params} = message.params;
|
||||||
|
receiver(api, method, JSON.stringify(params));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user