From 2d838efd4d938038e8cf29127afb950ff8718904 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 22 Sep 2021 09:01:29 -0700 Subject: [PATCH] Separate device in server and client version [2/n] Summary: This stack takes care of handling care of moving all device interactions over the (possible) async channel FlipperServer. The FlipperServer interface (see previous diff) allows listening to specific server events using `on`, and emit commands to be executed by the server by using `exec` (e.g. `exec('take-screenshot', serial) => Promise`). FlipperServerImpl implements this interface on the server side. The device implementations are split as follows ``` server / backend process: ServerDevice - iOSDevice - AndroidDevice - MetroDevice - DummyDevice - Mac/Windows Device frontend / ui: BaseDevice: a normal connected, device, implements device apis as they already existed - ArchivedDevice (note that this doesn't have a server counterpart) - TestDevice (for unit tests, with stubbed backend communication) ``` All features of devices are for simplicity unified (since the deviations are small), where specific device types might not implement certain features like taking screenshots or running shell commands. To avoid making this diff unnecessarily big, some open Todo's will be addressed later in this stack, and it shouldn't be landed alone. Reviewed By: timur-valiev Differential Revision: D30909346 fbshipit-source-id: cce0bee94fdd5db59bebe3577a6084219a038719 --- desktop/app/package.json | 1 + desktop/app/src/Client.tsx | 2 +- desktop/app/src/PluginContainer.tsx | 2 +- desktop/app/src/__tests__/disconnect.node.tsx | 9 +- .../app/src/chrome/VideoRecordingButton.tsx | 2 +- .../{server => }/devices/ArchivedDevice.tsx | 25 +- .../src/{server => }/devices/BaseDevice.tsx | 127 ++++--- .../__tests__/pluginManager.node.tsx | 2 +- desktop/app/src/dispatcher/flipperServer.tsx | 21 +- .../dispatcher/handleOpenPluginDeeplink.tsx | 2 +- desktop/app/src/dispatcher/reactNative.tsx | 11 +- desktop/app/src/dispatcher/tracking.tsx | 2 +- desktop/app/src/index.tsx | 10 +- desktop/app/src/plugin.tsx | 2 +- .../reducers/__tests__/connections.node.tsx | 37 +- .../__tests__/sandydeviceplugins.node.tsx | 14 +- desktop/app/src/reducers/connections.tsx | 72 +--- .../sandy-chrome/appinspect/AppInspect.tsx | 2 +- .../sandy-chrome/appinspect/AppSelector.tsx | 2 +- .../sandy-chrome/appinspect/PluginList.tsx | 5 +- .../appinspect/__tests__/PluginList.spec.tsx | 14 +- desktop/app/src/selectors/connections.tsx | 6 +- ...lipperServer.tsx => FlipperServerImpl.tsx} | 93 +++-- .../app/src/server/comms/ServerController.tsx | 9 +- .../server/comms/ServerWebSocketBrowser.tsx | 5 - .../app/src/server/devices/DummyDevice.tsx | 19 +- .../app/src/server/devices/ServerDevice.tsx | 72 ++++ .../devices/__tests__/BaseDevice.node.tsx | 17 +- .../server/devices/android/AndroidDevice.tsx | 43 ++- .../server/devices/android/KaiOSDevice.tsx | 6 +- .../devices/android/androidDeviceManager.tsx | 14 +- .../src/server/devices/desktop/MacDevice.tsx | 18 +- .../server/devices/desktop/WindowsDevice.tsx | 18 +- .../devices/desktop/desktopDeviceManager.tsx | 8 +- .../app/src/server/devices/ios/IOSDevice.tsx | 31 +- .../devices/ios/__tests__/iOSDevice.node.tsx | 6 +- .../server/devices/ios/iOSDeviceManager.tsx | 15 +- .../src/server/devices/metro/MetroDevice.tsx | 18 +- .../devices/metro/metroDeviceManager.tsx | 8 +- desktop/app/src/test-utils/MockFlipper.tsx | 7 +- desktop/app/src/test-utils/TestDevice.tsx | 38 ++ .../createMockFlipperWithPlugin.tsx | 2 +- .../src/utils/__tests__/exportData.node.tsx | 20 +- desktop/app/src/utils/clientUtils.tsx | 2 +- desktop/app/src/utils/exportData.tsx | 6 +- .../src/utils/flipperLibImplementation.tsx | 2 +- desktop/app/src/utils/pluginUtils.tsx | 2 +- desktop/app/src/utils/reduxDevToolsConfig.tsx | 2 +- desktop/app/src/utils/screenshot.tsx | 2 +- .../src/plugin/DevicePlugin.tsx | 1 - .../src/test-utils/test-utils.tsx | 4 +- .../flipper-plugin/src/types/server-types.tsx | 39 +- desktop/plugins/public/cpu/index.tsx | 1 + .../testCrashReporterPlugin.node.tsx | 10 +- .../public/kaios-allocations/index.tsx | 4 +- desktop/plugins/public/kaios-ram/index.tsx | 13 +- .../navigation/util/appMatchPatterns.tsx | 6 +- desktop/yarn.lock | 334 +++++++++++++++--- 58 files changed, 869 insertions(+), 396 deletions(-) rename desktop/app/src/{server => }/devices/ArchivedDevice.tsx (67%) rename desktop/app/src/{server => }/devices/BaseDevice.tsx (71%) rename desktop/app/src/server/{FlipperServer.tsx => FlipperServerImpl.tsx} (72%) create mode 100644 desktop/app/src/server/devices/ServerDevice.tsx create mode 100644 desktop/app/src/test-utils/TestDevice.tsx diff --git a/desktop/app/package.json b/desktop/app/package.json index 82d2b5d99..03be343cf 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -41,6 +41,7 @@ "fs-extra": "^10.0.0", "immer": "^9.0.6", "invariant": "^2.2.2", + "js-base64": "^3.7.0", "lodash": "^4.17.21", "lodash.memoize": "^4.1.2", "open": "^8.2.1", diff --git a/desktop/app/src/Client.tsx b/desktop/app/src/Client.tsx index f5e9c55c8..bd5120fa7 100644 --- a/desktop/app/src/Client.tsx +++ b/desktop/app/src/Client.tsx @@ -11,7 +11,7 @@ /* eslint-disable node/no-sync */ import {PluginDefinition} from './plugin'; -import BaseDevice from './server/devices/BaseDevice'; +import BaseDevice from './devices/BaseDevice'; import {Logger} from './fb-interfaces/Logger'; import {Store} from './reducers/index'; import {performance} from 'perf_hooks'; diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index 7e4a44d00..6feceb9c8 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -9,7 +9,7 @@ import {FlipperPlugin, FlipperDevicePlugin} from './plugin'; import {Logger} from './fb-interfaces/Logger'; -import BaseDevice from './server/devices/BaseDevice'; +import BaseDevice from './devices/BaseDevice'; import {pluginKey as getPluginKey} from './utils/pluginKey'; import Client from './Client'; import { diff --git a/desktop/app/src/__tests__/disconnect.node.tsx b/desktop/app/src/__tests__/disconnect.node.tsx index c3a42bb88..ae986c822 100644 --- a/desktop/app/src/__tests__/disconnect.node.tsx +++ b/desktop/app/src/__tests__/disconnect.node.tsx @@ -7,7 +7,6 @@ * @format */ -import {default as BaseDevice} from '../server/devices/BaseDevice'; import {createMockFlipperWithPlugin} from '../test-utils/createMockFlipperWithPlugin'; import { TestUtils, @@ -17,7 +16,7 @@ import { PluginClient, } from 'flipper-plugin'; import {handleClientConnected} from '../dispatcher/flipperServer'; -import {destroyDevice} from '../reducers/connections'; +import {TestDevice} from '../test-utils/TestDevice'; test('Devices can disconnect', async () => { const deviceplugin = new _SandyPluginDefinition( @@ -101,12 +100,8 @@ test('New device with same serial removes & cleans the old one', async () => { expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(store.getState().connections.devices).toEqual([device]); - // calling destroy explicitly defeats the point of this test a bit, - // but we now print an error rather than proactively destroying the device, - // see https://github.com/facebook/flipper/issues/1989 - destroyDevice(store, logger, device.serial); // submit a new device with same serial - const device2 = new BaseDevice( + const device2 = new TestDevice( device.serial, 'physical', 'MockAndroidDevice', diff --git a/desktop/app/src/chrome/VideoRecordingButton.tsx b/desktop/app/src/chrome/VideoRecordingButton.tsx index e6e0266f6..5d899238f 100644 --- a/desktop/app/src/chrome/VideoRecordingButton.tsx +++ b/desktop/app/src/chrome/VideoRecordingButton.tsx @@ -8,7 +8,7 @@ */ import React, {Component} from 'react'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; import {Button, Glyph, colors} from '../ui'; import path from 'path'; import os from 'os'; diff --git a/desktop/app/src/server/devices/ArchivedDevice.tsx b/desktop/app/src/devices/ArchivedDevice.tsx similarity index 67% rename from desktop/app/src/server/devices/ArchivedDevice.tsx rename to desktop/app/src/devices/ArchivedDevice.tsx index 6118c920c..0be1e4965 100644 --- a/desktop/app/src/server/devices/ArchivedDevice.tsx +++ b/desktop/app/src/devices/ArchivedDevice.tsx @@ -10,7 +10,7 @@ import BaseDevice from './BaseDevice'; import type {DeviceOS, DeviceType} from 'flipper-plugin'; import {DeviceShell} from './BaseDevice'; -import {SupportFormRequestDetailsState} from '../../reducers/supportForm'; +import {SupportFormRequestDetailsState} from '../reducers/supportForm'; export default class ArchivedDevice extends BaseDevice { isArchived = true; @@ -24,7 +24,28 @@ export default class ArchivedDevice extends BaseDevice { source?: string; supportRequestDetails?: SupportFormRequestDetailsState; }) { - super(options.serial, options.deviceType, options.title, options.os); + super( + { + close() {}, + exec(command, ..._args: any[]) { + throw new Error( + `[Archived device] Cannot invoke command ${command} on an archived device`, + ); + }, + on(event) { + console.warn( + `Cannot subscribe to server events from an Archived device: ${event}`, + ); + }, + off() {}, + }, + { + deviceType: options.deviceType, + title: options.title, + os: options.os, + serial: options.serial, + }, + ); this.icon = 'box'; this.connected.set(false); this.source = options.source || ''; diff --git a/desktop/app/src/server/devices/BaseDevice.tsx b/desktop/app/src/devices/BaseDevice.tsx similarity index 71% rename from desktop/app/src/server/devices/BaseDevice.tsx rename to desktop/app/src/devices/BaseDevice.tsx index e53338aa4..44ba664ae 100644 --- a/desktop/app/src/server/devices/BaseDevice.tsx +++ b/desktop/app/src/devices/BaseDevice.tsx @@ -18,9 +18,12 @@ import { createState, getFlipperLib, DeviceOS, + DeviceDescription, + FlipperServer, } from 'flipper-plugin'; import {DeviceSpec, PluginDetails} from 'flipper-plugin-lib'; -import {getPluginKey} from '../../utils/pluginKey'; +import {getPluginKey} from '../utils/pluginKey'; +import {Base64} from 'js-base64'; export type DeviceShell = { stdout: stream.Readable; @@ -40,37 +43,40 @@ export type DeviceExport = { }; export default class BaseDevice { + description: DeviceDescription; + flipperServer: FlipperServer; isArchived = false; hasDevicePlugins = false; // true if there are device plugins for this device (not necessarily enabled) - constructor( - serial: string, - deviceType: DeviceType, - title: string, - os: DeviceOS, - specs: DeviceSpec[] = [], - ) { - this.serial = serial; - this.title = title; - this.deviceType = deviceType; - this.os = os; - this.specs = specs; + constructor(flipperServer: FlipperServer, description: DeviceDescription) { + this.flipperServer = flipperServer; + this.description = description; } // operating system of this device - os: DeviceOS; + get os() { + return this.description.os; + } // human readable name for this device - title: string; + get title(): string { + return this.description.title; + } // type of this device - deviceType: DeviceType; + get deviceType() { + return this.description.deviceType; + } // serial number for this device - serial: string; + get serial() { + return this.description.serial; + } // additional device specs used for plugin compatibility checks - specs: DeviceSpec[]; + get specs(): DeviceSpec[] { + return this.description.specs ?? []; + } // possible src of icon to display next to the device title icon: string | null | undefined; @@ -127,12 +133,34 @@ export default class BaseDevice { }; } - startLogging() { - // to be subclassed + private deviceLogEventHandler = (payload: { + serial: string; + entry: DeviceLogEntry; + }) => { + if (payload.serial === this.serial && this.logListeners.size > 0) { + this.addLogEntry(payload.entry); + } + }; + + addLogEntry(entry: DeviceLogEntry) { + this.logListeners.forEach((listener) => { + // prevent breaking other listeners, if one listener doesn't work. + try { + listener(entry); + } catch (e) { + console.error(`Log listener exception:`, e); + } + }); + } + + async startLogging() { + await this.flipperServer.exec('device-start-logging', this.serial); + this.flipperServer.on('device-log', this.deviceLogEventHandler); } stopLogging() { - // to be subclassed + this.flipperServer.off('device-log', this.deviceLogEventHandler); + return this.flipperServer.exec('device-stop-logging', this.serial); } addLogListener(callback: DeviceLogListener): Symbol { @@ -144,23 +172,6 @@ export default class BaseDevice { return id; } - _notifyLogListeners(entry: DeviceLogEntry) { - if (this.logListeners.size > 0) { - this.logListeners.forEach((listener) => { - // prevent breaking other listeners, if one listener doesn't work. - try { - listener(entry); - } catch (e) { - console.error(`Log listener exception:`, e); - } - }); - } - } - - addLogEntry(entry: DeviceLogEntry) { - this._notifyLogListeners(entry); - } - removeLogListener(id: Symbol) { this.logListeners.delete(id); if (this.logListeners.size === 0) { @@ -172,22 +183,48 @@ export default class BaseDevice { throw new Error('unimplemented'); } - screenshot(): Promise { - return Promise.reject( - new Error('No screenshot support for current device'), + async screenshotAvailable(): Promise { + if (this.isArchived) { + return false; + } + return this.flipperServer.exec('device-supports-screenshot', this.serial); + } + + async screenshot(): Promise { + if (this.isArchived) { + return Buffer.from([]); + } + return Buffer.from( + Base64.toUint8Array( + await this.flipperServer.exec('device-take-screenshot', this.serial), + ), ); } async screenCaptureAvailable(): Promise { - return false; + if (this.isArchived) { + return false; + } + return this.flipperServer.exec( + 'device-supports-screencapture', + this.serial, + ); } - async startScreenCapture(_destination: string): Promise { - throw new Error('startScreenCapture not implemented on BaseDevice '); + async startScreenCapture(destination: string): Promise { + return this.flipperServer.exec( + 'device-start-screencapture', + this.serial, + destination, + ); } async stopScreenCapture(): Promise { - return null; + return this.flipperServer.exec('device-stop-screencapture', this.serial); + } + + async executeShell(command: string): Promise { + return this.flipperServer.exec('device-shell-exec', this.serial, command); } supportsPlugin(plugin: PluginDefinition | PluginDetails) { diff --git a/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx b/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx index 53e9f2557..4b95402f4 100644 --- a/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx @@ -22,7 +22,7 @@ import {_SandyPluginDefinition as SandyPluginDefinition} from 'flipper-plugin'; import MockFlipper from '../../test-utils/MockFlipper'; import Client from '../../Client'; import React from 'react'; -import BaseDevice from '../../server/devices/BaseDevice'; +import BaseDevice from '../../devices/BaseDevice'; const pluginDetails1 = TestUtils.createMockPluginDetails({ id: 'plugin1', diff --git a/desktop/app/src/dispatcher/flipperServer.tsx b/desktop/app/src/dispatcher/flipperServer.tsx index 37bd55c08..4b983a3e8 100644 --- a/desktop/app/src/dispatcher/flipperServer.tsx +++ b/desktop/app/src/dispatcher/flipperServer.tsx @@ -10,16 +10,16 @@ import React from 'react'; import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; -import {FlipperServer} from '../server/FlipperServer'; +import {FlipperServerImpl} from '../server/FlipperServerImpl'; import {selectClient, selectDevice} from '../reducers/connections'; import Client from '../Client'; import {notification} from 'antd'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; export default async (store: Store, logger: Logger) => { const {enableAndroid, androidHome, idbPath, enableIOS, enablePhysicalIOS} = store.getState().settingsState; - const server = new FlipperServer( + const server = new FlipperServerImpl( { enableAndroid, androidHome, @@ -69,23 +69,22 @@ export default async (store: Store, logger: Logger) => { }); }); - server.on('device-connected', (device) => { + server.on('device-connected', (deviceInfo) => { logger.track('usage', 'register-device', { - os: device.os, - name: device.title, - serial: device.serial, + os: deviceInfo.os, + name: deviceInfo.title, + serial: deviceInfo.serial, }); - // TODO: fixed later in this stack - (device as BaseDevice).loadDevicePlugins( + const device = new BaseDevice(server, deviceInfo); + device.loadDevicePlugins( store.getState().plugins.devicePlugins, store.getState().connections.enabledDevicePlugins, ); store.dispatch({ type: 'REGISTER_DEVICE', - // TODO: fixed later in this stack - payload: device as any, + payload: device, }); }); diff --git a/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx b/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx index e178118df..09df93c7f 100644 --- a/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx +++ b/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx @@ -24,7 +24,7 @@ import {loadPlugin, switchPlugin} from '../reducers/pluginManager'; import {startPluginDownload} from '../reducers/pluginDownloads'; import isProduction, {isTest} from '../utils/isProduction'; import restart from '../utils/restartFlipper'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; import Client from '../Client'; import {RocketOutlined} from '@ant-design/icons'; import {showEmulatorLauncher} from '../sandy-chrome/appinspect/LaunchEmulator'; diff --git a/desktop/app/src/dispatcher/reactNative.tsx b/desktop/app/src/dispatcher/reactNative.tsx index c6223936a..989124988 100644 --- a/desktop/app/src/dispatcher/reactNative.tsx +++ b/desktop/app/src/dispatcher/reactNative.tsx @@ -10,7 +10,6 @@ // Used to register a shortcut. Don't have an alternative for that. // eslint-disable-next-line flipper/no-electron-remote-imports import {remote} from 'electron'; -import MetroDevice from '../server/devices/metro/MetroDevice'; import {Store} from '../reducers'; type ShortcutEventCommand = @@ -47,9 +46,15 @@ export default (store: Store) => { .getState() .connections.devices.filter( (device) => device.os === 'Metro' && !device.isArchived, - ) as MetroDevice[]; + ); - devices.forEach((device) => device.sendCommand(shortcut.command)); + devices.forEach((device) => + device.flipperServer.exec( + 'metro-command', + device.serial, + shortcut.command, + ), + ); }), ); }; diff --git a/desktop/app/src/dispatcher/tracking.tsx b/desktop/app/src/dispatcher/tracking.tsx index a5568a0db..0b7b493a0 100644 --- a/desktop/app/src/dispatcher/tracking.tsx +++ b/desktop/app/src/dispatcher/tracking.tsx @@ -27,7 +27,7 @@ import { selectionChanged, } from '../reducers/usageTracking'; import produce from 'immer'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; import {deconstructClientId} from '../utils/clientUtils'; import {getCPUUsage} from 'process'; import {sideEffect} from '../utils/sideEffect'; diff --git a/desktop/app/src/index.tsx b/desktop/app/src/index.tsx index 1343d5671..b36fcd9d3 100644 --- a/desktop/app/src/index.tsx +++ b/desktop/app/src/index.tsx @@ -34,14 +34,11 @@ export {getPluginKey} from './utils/pluginKey'; export {Notification, Idler} from 'flipper-plugin'; export {IdlerImpl} from './utils/Idler'; export {Store, State as ReduxState} from './reducers/index'; -export {default as BaseDevice} from './server/devices/BaseDevice'; +export {default as BaseDevice} from './devices/BaseDevice'; export {default as isProduction} from './utils/isProduction'; export {DetailSidebar} from 'flipper-plugin'; -export {default as Device} from './server/devices/BaseDevice'; -export {default as AndroidDevice} from './server/devices/android/AndroidDevice'; -export {default as ArchivedDevice} from './server/devices/ArchivedDevice'; -export {default as IOSDevice} from './server/devices/ios/IOSDevice'; -export {default as KaiOSDevice} from './server/devices/android/KaiOSDevice'; +export {default as Device} from './devices/BaseDevice'; +export {default as ArchivedDevice} from './devices/ArchivedDevice'; export {DeviceOS as OS} from 'flipper-plugin'; export {default as Button} from './ui/components/Button'; export {default as ToggleButton} from './ui/components/ToggleSwitch'; @@ -135,3 +132,4 @@ export {IDEFileResolver, IDEType} from './fb-stubs/IDEFileResolver'; export {renderMockFlipperWithPlugin} from './test-utils/createMockFlipperWithPlugin'; export {Tracked} from 'flipper-plugin'; // To be able to use it in legacy plugins export {RequireLogin} from './ui/components/RequireLogin'; +export {TestDevice} from './test-utils/TestDevice'; diff --git a/desktop/app/src/plugin.tsx b/desktop/app/src/plugin.tsx index eb8e269df..e473f3ff3 100644 --- a/desktop/app/src/plugin.tsx +++ b/desktop/app/src/plugin.tsx @@ -11,7 +11,7 @@ import {KeyboardActions} from './MenuBar'; import {Logger} from './fb-interfaces/Logger'; import Client from './Client'; import {Component} from 'react'; -import BaseDevice from './server/devices/BaseDevice'; +import BaseDevice from './devices/BaseDevice'; import {StaticView} from './reducers/connections'; import {State as ReduxState} from './reducers'; import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue'; diff --git a/desktop/app/src/reducers/__tests__/connections.node.tsx b/desktop/app/src/reducers/__tests__/connections.node.tsx index 8495e382a..0304e3df2 100644 --- a/desktop/app/src/reducers/__tests__/connections.node.tsx +++ b/desktop/app/src/reducers/__tests__/connections.node.tsx @@ -9,10 +9,9 @@ import reducer from '../connections'; import {State, selectPlugin} from '../connections'; -import BaseDevice from '../../server/devices/BaseDevice'; -import MetroDevice from '../../server/devices/metro/MetroDevice'; import {_setFlipperLibImplementation} from 'flipper-plugin'; import {createMockFlipperLib} from 'flipper-plugin/src/test-utils/test-utils'; +import {TestDevice} from '../../test-utils/TestDevice'; beforeEach(() => { _setFlipperLibImplementation(createMockFlipperLib()); @@ -23,8 +22,8 @@ afterEach(() => { }); test('doing a double REGISTER_DEVICE keeps the last', () => { - const device1 = new BaseDevice('serial', 'physical', 'title', 'Android'); - const device2 = new BaseDevice('serial', 'physical', 'title2', 'Android'); + const device1 = new TestDevice('serial', 'physical', 'title', 'Android'); + const device2 = new TestDevice('serial', 'physical', 'title2', 'Android'); const initialState: State = reducer(undefined, { type: 'REGISTER_DEVICE', payload: device1, @@ -41,7 +40,12 @@ test('doing a double REGISTER_DEVICE keeps the last', () => { }); test('register, remove, re-register a metro device works correctly', () => { - const device1 = new MetroDevice('http://localhost:8081', undefined); + const device1 = new TestDevice( + 'http://localhost:8081', + 'emulator', + 'React Native', + 'Metro', + ); let state: State = reducer(undefined, { type: 'REGISTER_DEVICE', payload: device1, @@ -56,7 +60,12 @@ test('register, remove, re-register a metro device works correctly', () => { state = reducer(state, { type: 'REGISTER_DEVICE', - payload: new MetroDevice('http://localhost:8081', undefined), + payload: new TestDevice( + 'http://localhost:8081', + 'emulator', + 'React Native', + 'Metro', + ), }); expect(state.devices.length).toBe(1); expect(state.devices[0].displayTitle()).toBe('React Native'); @@ -70,19 +79,3 @@ test('selectPlugin sets deepLinkPayload correctly', () => { ); expect(state.deepLinkPayload).toBe('myPayload'); }); - -test('UNREGISTER_DEVICE removes device', () => { - const device = new BaseDevice('serial', 'physical', 'title', 'Android'); - const initialState: State = reducer(undefined, { - type: 'REGISTER_DEVICE', - payload: new BaseDevice('serial', 'physical', 'title', 'Android'), - }); - - expect(initialState.devices).toEqual([device]); - const endState = reducer(initialState, { - type: 'UNREGISTER_DEVICES', - payload: new Set(['serial']), - }); - - expect(endState.devices).toEqual([]); -}); diff --git a/desktop/app/src/reducers/__tests__/sandydeviceplugins.node.tsx b/desktop/app/src/reducers/__tests__/sandydeviceplugins.node.tsx index 985a989a0..47658279c 100644 --- a/desktop/app/src/reducers/__tests__/sandydeviceplugins.node.tsx +++ b/desktop/app/src/reducers/__tests__/sandydeviceplugins.node.tsx @@ -9,7 +9,7 @@ import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin'; import {Store} from '../../'; -import {destroyDevice, selectPlugin} from '../../reducers/connections'; +import {selectPlugin} from '../../reducers/connections'; import { _SandyPluginDefinition, _SandyDevicePluginInstance, @@ -86,15 +86,3 @@ test('it should initialize device sandy plugins', async () => { expect(instanceApi.deactivateStub).toBeCalledTimes(1); expect(instanceApi.destroyStub).toBeCalledTimes(0); }); - -test('it should cleanup if device is removed', async () => { - const {device, store, logger} = await createMockFlipperWithPlugin(TestPlugin); - const pluginInstance = device.sandyPluginStates.get(TestPlugin.id)!; - expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0); - - // close device - destroyDevice(store, logger, device.serial); - expect( - (pluginInstance.instanceApi as PluginApi).destroyStub, - ).toHaveBeenCalledTimes(1); -}); diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index 5b827f081..2f3d48da3 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -10,21 +10,19 @@ import {ComponentType} from 'react'; import {produce} from 'immer'; -import type BaseDevice from '../server/devices/BaseDevice'; -import MacDevice from '../server/devices/desktop/MacDevice'; +import type BaseDevice from '../devices/BaseDevice'; import type Client from '../Client'; import type {UninitializedClient} from '../server/UninitializedClient'; import {performance} from 'perf_hooks'; -import type {Actions, Store} from '.'; +import type {Actions} from '.'; import {WelcomeScreenStaticView} from '../sandy-chrome/WelcomeScreen'; import {isDevicePluginDefinition} from '../utils/pluginUtils'; import {getPluginKey} from '../utils/pluginKey'; import {deconstructClientId} from '../utils/clientUtils'; import type {RegisterPluginAction} from './plugins'; -import MetroDevice from '../server/devices/metro/MetroDevice'; -import {Logger} from 'flipper-plugin'; -import {FlipperServer} from '../server/FlipperServer'; +import {DeviceOS, Logger} from 'flipper-plugin'; +import {FlipperServerImpl} from '../server/FlipperServerImpl'; import {shallowEqual} from 'react-redux'; export type StaticViewProps = {logger: Logger}; @@ -77,7 +75,7 @@ type StateV2 = { deepLinkPayload: unknown; staticView: StaticView; selectedAppPluginListRevision: number; - flipperServer: FlipperServer | undefined; + flipperServer: FlipperServerImpl | undefined; }; type StateV1 = Omit & { @@ -91,10 +89,6 @@ type StateV0 = Omit & { }; export type Action = - | { - type: 'UNREGISTER_DEVICES'; - payload: Set; - } | { type: 'REGISTER_DEVICE'; payload: BaseDevice; @@ -173,12 +167,12 @@ export type Action = } | { type: 'SET_FLIPPER_SERVER'; - payload: FlipperServer; + payload: FlipperServerImpl; } | RegisterPluginAction; const DEFAULT_PLUGIN = 'DeviceLogs'; -const DEFAULT_DEVICE_BLACKLIST = [MacDevice, MetroDevice]; +const DEFAULT_DEVICE_BLACKLIST: DeviceOS[] = ['MacOS', 'Metro', 'Windows']; const INITAL_STATE: State = { devices: [], selectedDevice: null, @@ -250,7 +244,13 @@ export default (state: State = INITAL_STATE, action: Actions): State => { (device) => device.serial === payload.serial, ); if (existing !== -1) { - newDevices[existing].destroy(); + const d = newDevices[existing]; + if (d.connected.get()) { + console.warn( + `Tried to replace still connected device '${d.serial}' with a new instance`, + ); + } + d.destroy(); newDevices[existing] = payload; } else { newDevices.push(payload); @@ -262,28 +262,6 @@ export default (state: State = INITAL_STATE, action: Actions): State => { }); } - // TODO: remove - case 'UNREGISTER_DEVICES': { - const deviceSerials = action.payload; - - return updateSelection( - produce(state, (draft) => { - draft.devices = draft.devices.filter((device) => { - if (!deviceSerials.has(device.serial)) { - return true; - } else { - if (device.connected.get()) { - console.warn( - 'Tried to unregister a device before it was destroyed', - ); - } - return false; - } - }); - }), - ); - } - case 'SELECT_PLUGIN': { const {payload} = action; const {selectedPlugin, selectedApp, deepLinkPayload} = payload; @@ -581,9 +559,7 @@ export function getClientById( } export function canBeDefaultDevice(device: BaseDevice) { - return !DEFAULT_DEVICE_BLACKLIST.some( - (blacklistedDevice) => device instanceof blacklistedDevice, - ); + return !DEFAULT_DEVICE_BLACKLIST.includes(device.os); } /** @@ -669,21 +645,3 @@ export function isPluginEnabled( const enabledAppPlugins = enabledPlugins[appInfo.app]; return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1; } - -// TODO: remove! -export function destroyDevice(store: Store, logger: Logger, serial: string) { - const device = store - .getState() - .connections.devices.find((device) => device.serial === serial); - if (device) { - device.destroy(); - logger.track('usage', 'unregister-device', { - os: device.os, - serial, - }); - store.dispatch({ - type: 'UNREGISTER_DEVICES', - payload: new Set([serial]), - }); - } -} diff --git a/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx b/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx index fc7c26652..bed93e91b 100644 --- a/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx @@ -18,7 +18,7 @@ import ScreenCaptureButtons from '../../chrome/ScreenCaptureButtons'; import MetroButton from '../../chrome/MetroButton'; import {BookmarkSection} from './BookmarkSection'; import Client from '../../Client'; -import BaseDevice from '../../server/devices/BaseDevice'; +import BaseDevice from '../../devices/BaseDevice'; import {ExclamationCircleOutlined, FieldTimeOutlined} from '@ant-design/icons'; import {useSelector} from 'react-redux'; import { diff --git a/desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx b/desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx index 418b8d4eb..115fe0a79 100644 --- a/desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/AppSelector.tsx @@ -25,7 +25,7 @@ import { selectClient, selectDevice, } from '../../reducers/connections'; -import BaseDevice from '../../server/devices/BaseDevice'; +import BaseDevice from '../../devices/BaseDevice'; import Client from '../../Client'; import {State} from '../../reducers'; import {brandColors, brandIcons, colors} from '../../ui/components/colors'; diff --git a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx index 21cc693ec..365d89227 100644 --- a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx @@ -23,9 +23,8 @@ import {useDispatch, useStore} from '../../utils/useStore'; import {getPluginTitle, getPluginTooltip} from '../../utils/pluginUtils'; import {selectPlugin} from '../../reducers/connections'; import Client from '../../Client'; -import BaseDevice from '../../server/devices/BaseDevice'; +import BaseDevice from '../../devices/BaseDevice'; import {DownloadablePluginDetails} from 'flipper-plugin-lib'; -import MetroDevice from '../../server/devices/metro/MetroDevice'; import { DownloadablePluginState, PluginDownloadStatus, @@ -52,7 +51,7 @@ export const PluginList = memo(function PluginList({ }: { client: Client | null; activeDevice: BaseDevice | null; - metroDevice: MetroDevice | null; + metroDevice: BaseDevice | null; }) { const dispatch = useDispatch(); const connections = useStore((state) => state.connections); diff --git a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx index 973a77edc..55c6d1f60 100644 --- a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx @@ -13,7 +13,7 @@ import { } from '../../../test-utils/createMockFlipperWithPlugin'; import {FlipperPlugin} from '../../../plugin'; import MetroDevice from '../../../server/devices/metro/MetroDevice'; -import BaseDevice from '../../../server/devices/BaseDevice'; +import BaseDevice from '../../../devices/BaseDevice'; import {_SandyPluginDefinition} from 'flipper-plugin'; import {TestUtils} from 'flipper-plugin'; import {selectPlugin} from '../../../reducers/connections'; @@ -33,6 +33,7 @@ import { getMetroDevice, getPluginLists, } from '../../../selectors/connections'; +import {TestDevice} from '../../../test-utils/TestDevice'; const createMockPluginDetails = TestUtils.createMockPluginDetails; @@ -67,7 +68,7 @@ describe('basic getActiveDevice', () => { describe('basic getActiveDevice with metro present', () => { let flipper: MockFlipperResult; - let metro: MetroDevice; + let metro: BaseDevice; let testDevice: BaseDevice; beforeEach(async () => { @@ -79,7 +80,12 @@ describe('basic getActiveDevice with metro present', () => { // flipper.store.dispatch(registerPlugins([LogsPlugin])) flipper.store.dispatch({ type: 'REGISTER_DEVICE', - payload: new MetroDevice('http://localhost:8081', undefined), + payload: new TestDevice( + 'http://localhost:8081', + 'physical', + 'metro', + 'Metro', + ), }); metro = getMetroDevice(flipper.store.getState())!; metro.supportsPlugin = (p) => { @@ -88,7 +94,7 @@ describe('basic getActiveDevice with metro present', () => { }); test('findMetroDevice', () => { - expect(metro).toBeInstanceOf(MetroDevice); + expect(metro.os).toBe('Metro'); }); test('correct base selection state', () => { diff --git a/desktop/app/src/selectors/connections.tsx b/desktop/app/src/selectors/connections.tsx index c7496d5af..5246a01f4 100644 --- a/desktop/app/src/selectors/connections.tsx +++ b/desktop/app/src/selectors/connections.tsx @@ -7,7 +7,6 @@ * @format */ -import MetroDevice from '../server/devices/metro/MetroDevice'; import {State} from '../reducers'; import { computePluginLists, @@ -36,9 +35,8 @@ export const getActiveClient = createSelector( export const getMetroDevice = createSelector(getDevices, (devices) => { return ( - (devices.find((device) => device.os === 'Metro' && !device.isArchived) as - | MetroDevice - | undefined) ?? null + devices.find((device) => device.os === 'Metro' && !device.isArchived) ?? + null ); }); diff --git a/desktop/app/src/server/FlipperServer.tsx b/desktop/app/src/server/FlipperServerImpl.tsx similarity index 72% rename from desktop/app/src/server/FlipperServer.tsx rename to desktop/app/src/server/FlipperServerImpl.tsx index 2b23ad949..f412e021b 100644 --- a/desktop/app/src/server/FlipperServer.tsx +++ b/desktop/app/src/server/FlipperServerImpl.tsx @@ -14,7 +14,7 @@ import {Logger} from '../fb-interfaces/Logger'; import ServerController from './comms/ServerController'; import {UninitializedClient} from './UninitializedClient'; import {addErrorNotification} from '../reducers/notifications'; -import {CertificateExchangeMedium} from '../server/utils/CertificateProvider'; +import {CertificateExchangeMedium} from './utils/CertificateProvider'; import {isLoggedIn} from '../fb-stubs/user'; import React from 'react'; import {Typography} from 'antd'; @@ -27,8 +27,15 @@ import {AndroidDeviceManager} from './devices/android/androidDeviceManager'; import {IOSDeviceManager} from './devices/ios/iOSDeviceManager'; import metroDevice from './devices/metro/metroDeviceManager'; import desktopDevice from './devices/desktop/desktopDeviceManager'; -import BaseDevice from './devices/BaseDevice'; -import {FlipperServerEvents, FlipperServerState} from 'flipper-plugin'; +import { + FlipperServerEvents, + FlipperServerState, + FlipperServerCommands, + FlipperServer, +} from 'flipper-plugin'; +import {ServerDevice} from './devices/ServerDevice'; +import {Base64} from 'js-base64'; +import MetroDevice from './devices/metro/MetroDevice'; export interface FlipperServerConfig { enableAndroid: boolean; @@ -60,14 +67,14 @@ const defaultConfig: FlipperServerConfig = { * The server should be largely treated as event emitter, by listening to the relevant events * using '.on'. All events are strongly typed. */ -export class FlipperServer { +export class FlipperServerImpl implements FlipperServer { public config: FlipperServerConfig; private readonly events = new EventEmitter(); // server handles the incoming RSocket / WebSocket connections from Flipper clients readonly server: ServerController; readonly disposers: ((() => void) | void)[] = []; - private readonly devices = new Map(); + private readonly devices = new Map(); state: FlipperServerState = 'pending'; android: AndroidDeviceManager; ios: IOSDeviceManager; @@ -197,6 +204,13 @@ export class FlipperServer { this.events.on(event, callback); } + off( + event: Event, + callback: (payload: FlipperServerEvents[Event]) => void, + ): void { + this.events.off(event, callback); + } + /** * @internal */ @@ -207,31 +221,64 @@ export class FlipperServer { this.events.emit(event, payload); } - registerDevice(device: BaseDevice) { + exec( + event: Event, + ...args: Parameters + ): ReturnType { + console.debug(`[FlipperServer] command ${event}: `, args); + const handler: (...args: any[]) => Promise = + this.commandHandler[event]; + if (!handler) { + throw new Error(`Unimplemented server command: ${event}`); + } + return handler(...args) as any; + } + + private commandHandler: FlipperServerCommands = { + 'device-start-logging': async (serial: string) => + this.getDevice(serial).startLogging(), + 'device-stop-logging': async (serial: string) => + this.getDevice(serial).stopLogging(), + 'device-supports-screenshot': async (serial: string) => + this.getDevice(serial).screenshotAvailable(), + 'device-supports-screencapture': async (serial: string) => + this.getDevice(serial).screenCaptureAvailable(), + 'device-take-screenshot': async (serial: string) => + Base64.fromUint8Array(await this.getDevice(serial).screenshot()), + 'device-start-screencapture': async (serial, destination) => + this.getDevice(serial).startScreenCapture(destination), + 'device-stop-screencapture': async (serial: string) => + this.getDevice(serial).stopScreenCapture(), + 'device-shell-exec': async (serial: string, command: string) => + this.getDevice(serial).executeShell(command), + 'metro-command': async (serial: string, command: string) => { + const device = this.getDevice(serial); + if (!(device instanceof MetroDevice)) { + throw new Error('Not a Metro device: ' + serial); + } + device.sendCommand(command); + }, + }; + + registerDevice(device: ServerDevice) { // destroy existing device - const existing = this.devices.get(device.serial); + const {serial} = device.info; + const existing = this.devices.get(serial); if (existing) { // assert different kind of devices aren't accidentally reusing the same serial if (Object.getPrototypeOf(existing) !== Object.getPrototypeOf(device)) { throw new Error( - `Tried to register a new device type for existing serial '${ - device.serial - }': Trying to replace existing '${ + `Tried to register a new device type for existing serial '${serial}': Trying to replace existing '${ Object.getPrototypeOf(existing).constructor.name }' with a new '${Object.getPrototypeOf(device).constructor.name}`, ); } - // devices should be recycled, unless they have lost connection - if (existing.connected.get()) { - throw new Error( - `Tried to replace still connected device '${device.serial}' with a new instance`, - ); - } - existing.destroy(); + // clean up connection + existing.disconnect(); } // register new device - this.devices.set(device.serial, device); - this.emit('device-connected', device); + this.devices.set(device.info.serial, device); + this.emit('device-connected', device.info); } unregisterDevice(serial: string) { @@ -240,10 +287,10 @@ export class FlipperServer { return; } device.disconnect(); // we'll only destroy upon replacement - this.emit('device-disconnected', device); + this.emit('device-disconnected', device.info); } - getDevice(serial: string): BaseDevice { + getDevice(serial: string): ServerDevice { const device = this.devices.get(serial); if (!device) { throw new Error('No device with serial: ' + serial); @@ -255,14 +302,14 @@ export class FlipperServer { return Array.from(this.devices.keys()); } - getDevices(): BaseDevice[] { + getDevices(): ServerDevice[] { return Array.from(this.devices.values()); } public async close() { this.server.close(); for (const device of this.devices.values()) { - device.destroy(); + device.disconnect(); } this.disposers.forEach((f) => f?.()); this.setServerState('closed'); diff --git a/desktop/app/src/server/comms/ServerController.tsx b/desktop/app/src/server/comms/ServerController.tsx index 56db0f3b0..b4f14e471 100644 --- a/desktop/app/src/server/comms/ServerController.tsx +++ b/desktop/app/src/server/comms/ServerController.tsx @@ -21,7 +21,7 @@ import invariant from 'invariant'; import GK from '../../fb-stubs/GK'; import {buildClientId} from '../../utils/clientUtils'; import DummyDevice from '../../server/devices/DummyDevice'; -import BaseDevice from '../../server/devices/BaseDevice'; +import BaseDevice from '../../devices/BaseDevice'; import {sideEffect} from '../../utils/sideEffect'; import { appNameWithUpdateHint, @@ -36,7 +36,7 @@ import { createServer, TransportType, } from './ServerFactory'; -import {FlipperServer} from '../FlipperServer'; +import {FlipperServerImpl} from '../FlipperServerImpl'; import {isTest} from '../../utils/isProduction'; import {timeout} from 'flipper-plugin'; @@ -77,11 +77,11 @@ class ServerController extends EventEmitter implements ServerEventsListener { certificateProvider: CertificateProvider; connectionTracker: ConnectionTracker; - flipperServer: FlipperServer; + flipperServer: FlipperServerImpl; timeHandlers: Map = new Map(); - constructor(flipperServer: FlipperServer) { + constructor(flipperServer: FlipperServerImpl) { super(); this.flipperServer = flipperServer; this.connections = new Map(); @@ -247,6 +247,7 @@ class ServerController extends EventEmitter implements ServerEventsListener { if (transformedMedium === 'WWW' || transformedMedium === 'NONE') { this.flipperServer.registerDevice( new DummyDevice( + this.flipperServer, clientQuery.device_id, clientQuery.app + (transformedMedium === 'WWW' ? ' Server Exchanged' : ''), diff --git a/desktop/app/src/server/comms/ServerWebSocketBrowser.tsx b/desktop/app/src/server/comms/ServerWebSocketBrowser.tsx index f4031802f..2407c41ad 100644 --- a/desktop/app/src/server/comms/ServerWebSocketBrowser.tsx +++ b/desktop/app/src/server/comms/ServerWebSocketBrowser.tsx @@ -76,11 +76,6 @@ class ServerWebSocketBrowser extends ServerWebSocketBase { Object.values(clients).map((p) => p.then((c) => this.listener.onConnectionClosed(c.id)), ); - // TODO: destroy device. - // This seems to be the only case in which a device gets destroyed when there's a disconnection - // or error on the transport layer. - // - // destroyDevice(this.store, this.logger, deviceId); }; /** diff --git a/desktop/app/src/server/devices/DummyDevice.tsx b/desktop/app/src/server/devices/DummyDevice.tsx index 2b2ee129a..59804cdfa 100644 --- a/desktop/app/src/server/devices/DummyDevice.tsx +++ b/desktop/app/src/server/devices/DummyDevice.tsx @@ -8,13 +8,24 @@ */ import {DeviceOS} from 'flipper-plugin'; -import BaseDevice from './BaseDevice'; +import {FlipperServerImpl} from '../FlipperServerImpl'; +import {ServerDevice} from './ServerDevice'; /** * Use this device when you do not have the actual uuid of the device. For example, it is currently used in the case when, we do certificate exchange through WWW mode. In this mode we do not know the device id of the app and we generate a fake one. */ -export default class DummyDevice extends BaseDevice { - constructor(serial: string, title: string, os: DeviceOS) { - super(serial, 'dummy', title, os); +export default class DummyDevice extends ServerDevice { + constructor( + flipperServer: FlipperServerImpl, + serial: string, + title: string, + os: DeviceOS, + ) { + super(flipperServer, { + serial, + deviceType: 'dummy', + title, + os, + }); } } diff --git a/desktop/app/src/server/devices/ServerDevice.tsx b/desktop/app/src/server/devices/ServerDevice.tsx new file mode 100644 index 000000000..c9d48bd92 --- /dev/null +++ b/desktop/app/src/server/devices/ServerDevice.tsx @@ -0,0 +1,72 @@ +/** + * 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 {createState, DeviceDescription, DeviceLogEntry} from 'flipper-plugin'; +import {FlipperServerImpl} from '../FlipperServerImpl'; + +export abstract class ServerDevice { + readonly info: DeviceDescription; + readonly connected = createState(true); + readonly flipperServer: FlipperServerImpl; + + constructor(flipperServer: FlipperServerImpl, info: DeviceDescription) { + this.flipperServer = flipperServer; + this.info = info; + } + + get serial(): string { + return this.info.serial; + } + + addLogEntry(entry: DeviceLogEntry) { + this.flipperServer.emit('device-log', { + serial: this.serial, + entry, + }); + } + + /** + * The device might have no active connection + */ + disconnect(): void {} + + startLogging() { + // to be subclassed + } + + stopLogging() { + // to be subclassed + } + + async screenshotAvailable(): Promise { + return false; + } + + screenshot(): Promise { + return Promise.reject( + new Error('No screenshot support for current device'), + ); + } + + async screenCaptureAvailable(): Promise { + return false; + } + + async startScreenCapture(_destination: string): Promise { + throw new Error('startScreenCapture not implemented on BaseDevice '); + } + + async stopScreenCapture(): Promise { + throw new Error('stopScreenCapture not implemented on BaseDevice '); + } + + async executeShell(_command: string): Promise { + throw new Error('executeShell not implemented on BaseDevice'); + } +} diff --git a/desktop/app/src/server/devices/__tests__/BaseDevice.node.tsx b/desktop/app/src/server/devices/__tests__/BaseDevice.node.tsx index 0a524474c..863f48536 100644 --- a/desktop/app/src/server/devices/__tests__/BaseDevice.node.tsx +++ b/desktop/app/src/server/devices/__tests__/BaseDevice.node.tsx @@ -7,12 +7,11 @@ * @format */ -import BaseDevice from '../BaseDevice'; import * as DeviceTestPluginModule from '../../../test-utils/DeviceTestPlugin'; import {TestUtils, _SandyPluginDefinition} from 'flipper-plugin'; -import ArchivedDevice from '../ArchivedDevice'; -import DummyDevice from '../DummyDevice'; import {createMockFlipperWithPlugin} from '../../../test-utils/createMockFlipperWithPlugin'; +import {TestDevice} from '../../../test-utils/TestDevice'; +import ArchivedDevice from '../../../devices/ArchivedDevice'; const physicalDevicePluginDetails = TestUtils.createMockPluginDetails({ id: 'physicalDevicePlugin', @@ -122,7 +121,7 @@ const androidOnlyDevicePlugin = new _SandyPluginDefinition( ); test('ios physical device compatibility', () => { - const device = new BaseDevice('serial', 'physical', 'test device', 'iOS'); + const device = new TestDevice('serial', 'physical', 'test device', 'iOS'); expect(device.supportsPlugin(physicalDevicePlugin)).toBeTruthy(); expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeTruthy(); expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy(); @@ -146,7 +145,7 @@ test('archived device compatibility', () => { }); test('android emulator device compatibility', () => { - const device = new BaseDevice('serial', 'emulator', 'test device', 'Android'); + const device = new TestDevice('serial', 'emulator', 'test device', 'Android'); expect(device.supportsPlugin(physicalDevicePlugin)).toBeFalsy(); expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeFalsy(); expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy(); @@ -155,7 +154,7 @@ test('android emulator device compatibility', () => { }); test('android KaiOS device compatibility', () => { - const device = new BaseDevice( + const device = new TestDevice( 'serial', 'physical', 'test device', @@ -170,7 +169,7 @@ test('android KaiOS device compatibility', () => { }); test('android dummy device compatibility', () => { - const device = new DummyDevice('serial', 'test device', 'Android'); + const device = new TestDevice('serial', 'dummy', 'test device', 'Android'); expect(device.supportsPlugin(physicalDevicePlugin)).toBeFalsy(); expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeFalsy(); expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy(); @@ -188,7 +187,7 @@ test('log listeners are resumed and suspended automatically - 1', async () => { type: 'info', tag: 'tag', } as const; - const device = new BaseDevice('serial', 'physical', 'test device', 'Android'); + const device = new TestDevice('serial', 'physical', 'test device', 'Android'); device.startLogging = jest.fn(); device.stopLogging = jest.fn(); @@ -251,7 +250,7 @@ test('log listeners are resumed and suspended automatically - 2', async () => { type: 'info', tag: 'tag', } as const; - const device = new BaseDevice('serial', 'physical', 'test device', 'Android'); + const device = new TestDevice('serial', 'physical', 'test device', 'Android'); device.startLogging = jest.fn(); device.stopLogging = jest.fn(); diff --git a/desktop/app/src/server/devices/android/AndroidDevice.tsx b/desktop/app/src/server/devices/android/AndroidDevice.tsx index de7072f2f..d56ac0030 100644 --- a/desktop/app/src/server/devices/android/AndroidDevice.tsx +++ b/desktop/app/src/server/devices/android/AndroidDevice.tsx @@ -7,7 +7,6 @@ * @format */ -import BaseDevice from '../BaseDevice'; import adb, {Client as ADBClient, PullTransfer} from 'adbkit'; import {Priority, Reader} from 'adbkit-logcat'; import {createWriteStream} from 'fs'; @@ -16,18 +15,19 @@ import which from 'which'; import {spawn} from 'child_process'; import {dirname, join} from 'path'; import {DeviceSpec} from 'flipper-plugin-lib'; +import {ServerDevice} from '../ServerDevice'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder'; -export default class AndroidDevice extends BaseDevice { +export default class AndroidDevice extends ServerDevice { adb: ADBClient; - abiList: Array = []; - sdkVersion: string | undefined = undefined; pidAppMapping: {[key: number]: string} = {}; private recordingProcess?: Promise; reader?: Reader; constructor( + flipperServer: FlipperServerImpl, serial: string, deviceType: DeviceType, title: string, @@ -36,11 +36,17 @@ export default class AndroidDevice extends BaseDevice { sdkVersion: string, specs: DeviceSpec[] = [], ) { - super(serial, deviceType, title, 'Android', specs); + super(flipperServer, { + serial, + deviceType, + title, + os: 'Android', + specs, + abiList, + sdkVersion, + }); this.adb = adb; - this.icon = 'mobile'; - this.abiList = abiList; - this.sdkVersion = sdkVersion; + // TODO: this.icon = 'mobile'; } startLogging() { @@ -117,7 +123,7 @@ export default class AndroidDevice extends BaseDevice { } clearLogs(): Promise { - return this.executeShell(['logcat', '-c']); + return this.executeShellOrDie(['logcat', '-c']); } navigateToLocation(location: string) { @@ -126,9 +132,6 @@ export default class AndroidDevice extends BaseDevice { } async screenshot(): Promise { - if (this.isArchived) { - return Buffer.from([]); - } return new Promise((resolve, reject) => { this.adb .screencap(this.serial) @@ -148,11 +151,8 @@ export default class AndroidDevice extends BaseDevice { } async screenCaptureAvailable(): Promise { - if (this.isArchived) { - return false; - } try { - await this.executeShell( + await this.executeShellOrDie( `[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`, ); return true; @@ -161,7 +161,14 @@ export default class AndroidDevice extends BaseDevice { } } - private async executeShell(command: string | string[]): Promise { + async executeShell(command: string): Promise { + return await this.adb + .shell(this.serial, command) + .then(adb.util.readAll) + .then((output: Buffer) => output.toString().trim()); + } + + private async executeShellOrDie(command: string | string[]): Promise { const output = await this.adb .shell(this.serial, command) .then(adb.util.readAll) @@ -191,7 +198,7 @@ export default class AndroidDevice extends BaseDevice { } async startScreenCapture(destination: string) { - await this.executeShell( + await this.executeShellOrDie( `mkdir -p "${DEVICE_RECORDING_DIR}" && echo -n > "${DEVICE_RECORDING_DIR}/.nomedia"`, ); const recordingLocation = `${DEVICE_RECORDING_DIR}/video.mp4`; diff --git a/desktop/app/src/server/devices/android/KaiOSDevice.tsx b/desktop/app/src/server/devices/android/KaiOSDevice.tsx index cc4c6f1b8..8c4241889 100644 --- a/desktop/app/src/server/devices/android/KaiOSDevice.tsx +++ b/desktop/app/src/server/devices/android/KaiOSDevice.tsx @@ -10,9 +10,11 @@ import {DeviceType} from 'flipper-plugin-lib'; import AndroidDevice from './AndroidDevice'; import {Client as ADBClient} from 'adbkit'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; export default class KaiOSDevice extends AndroidDevice { constructor( + flipperServer: FlipperServerImpl, serial: string, deviceType: DeviceType, title: string, @@ -20,7 +22,9 @@ export default class KaiOSDevice extends AndroidDevice { abiList: Array, sdkVersion: string, ) { - super(serial, deviceType, title, adb, abiList, sdkVersion, ['KaiOS']); + super(flipperServer, serial, deviceType, title, adb, abiList, sdkVersion, [ + 'KaiOS', + ]); } async screenCaptureAvailable() { diff --git a/desktop/app/src/server/devices/android/androidDeviceManager.tsx b/desktop/app/src/server/devices/android/androidDeviceManager.tsx index 5dc3ec719..dec13f790 100644 --- a/desktop/app/src/server/devices/android/androidDeviceManager.tsx +++ b/desktop/app/src/server/devices/android/androidDeviceManager.tsx @@ -15,14 +15,14 @@ import which from 'which'; import {promisify} from 'util'; import {Client as ADBClient, Device} from 'adbkit'; import {join} from 'path'; -import {FlipperServer} from '../../FlipperServer'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; import {notNull} from '../../utils/typeUtils'; export class AndroidDeviceManager { // cache emulator path private emulatorPath: string | undefined; - constructor(public flipperServer: FlipperServer) {} + constructor(public flipperServer: FlipperServerImpl) {} private createDevice( adbClient: ADBClient, @@ -49,7 +49,15 @@ export class AndroidDeviceManager { ); const androidLikeDevice = new ( isKaiOSDevice ? KaiOSDevice : AndroidDevice - )(device.id, type, name, adbClient, abiList, sdkVersion); + )( + this.flipperServer, + device.id, + type, + name, + adbClient, + abiList, + sdkVersion, + ); if (this.flipperServer.config.serverPorts) { await androidLikeDevice .reverse([ diff --git a/desktop/app/src/server/devices/desktop/MacDevice.tsx b/desktop/app/src/server/devices/desktop/MacDevice.tsx index 402c1bb45..7ddb6bf96 100644 --- a/desktop/app/src/server/devices/desktop/MacDevice.tsx +++ b/desktop/app/src/server/devices/desktop/MacDevice.tsx @@ -7,13 +7,17 @@ * @format */ -import BaseDevice from '../BaseDevice'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; +import {ServerDevice} from '../ServerDevice'; -export default class MacDevice extends BaseDevice { - constructor() { - super('', 'physical', 'Mac', 'MacOS'); - this.icon = 'app-apple'; +export default class MacDevice extends ServerDevice { + constructor(flipperServer: FlipperServerImpl) { + super(flipperServer, { + serial: '', + deviceType: 'physical', + title: 'Mac', + os: 'MacOS', + }); + // TODO: this.icon = 'app-apple'; } - - teardown() {} } diff --git a/desktop/app/src/server/devices/desktop/WindowsDevice.tsx b/desktop/app/src/server/devices/desktop/WindowsDevice.tsx index c3c8e8be8..8e0d83722 100644 --- a/desktop/app/src/server/devices/desktop/WindowsDevice.tsx +++ b/desktop/app/src/server/devices/desktop/WindowsDevice.tsx @@ -7,13 +7,17 @@ * @format */ -import BaseDevice from '../BaseDevice'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; +import {ServerDevice} from '../ServerDevice'; -export default class WindowsDevice extends BaseDevice { - constructor() { - super('', 'physical', 'Windows', 'Windows'); - this.icon = 'app-microsoft-windows'; +export default class WindowsDevice extends ServerDevice { + constructor(flipperServer: FlipperServerImpl) { + super(flipperServer, { + serial: '', + deviceType: 'physical', + title: 'Windows', + os: 'Windows', + }); + // TODO: this.icon = 'app-microsoft-windows'; } - - teardown() {} } diff --git a/desktop/app/src/server/devices/desktop/desktopDeviceManager.tsx b/desktop/app/src/server/devices/desktop/desktopDeviceManager.tsx index 85e832e9a..cca51118f 100644 --- a/desktop/app/src/server/devices/desktop/desktopDeviceManager.tsx +++ b/desktop/app/src/server/devices/desktop/desktopDeviceManager.tsx @@ -9,14 +9,14 @@ import MacDevice from './MacDevice'; import WindowsDevice from './WindowsDevice'; -import {FlipperServer} from '../../FlipperServer'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; -export default (flipperServer: FlipperServer) => { +export default (flipperServer: FlipperServerImpl) => { let device; if (process.platform === 'darwin') { - device = new MacDevice(); + device = new MacDevice(flipperServer); } else if (process.platform === 'win32') { - device = new WindowsDevice(); + device = new WindowsDevice(flipperServer); } else { return; } diff --git a/desktop/app/src/server/devices/ios/IOSDevice.tsx b/desktop/app/src/server/devices/ios/IOSDevice.tsx index 3b816fd13..4f5beac14 100644 --- a/desktop/app/src/server/devices/ios/IOSDevice.tsx +++ b/desktop/app/src/server/devices/ios/IOSDevice.tsx @@ -9,11 +9,12 @@ import {LogLevel, DeviceLogEntry, DeviceType, timeout} from 'flipper-plugin'; import child_process, {ChildProcess} from 'child_process'; -import BaseDevice from '../BaseDevice'; import JSONStream from 'JSONStream'; import {Transform} from 'stream'; import {ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB, IOSBridge} from './IOSBridge'; import split2 from 'split2'; +import {ServerDevice} from '../ServerDevice'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault'; @@ -38,7 +39,7 @@ type RawLogEntry = { // Mar 25 17:06:38 iPhone symptomsd(SymptomEvaluator)[125] : Stuff const logRegex = /(^.{15}) ([^ ]+?) ([^\[]+?)\[(\d+?)\] <(\w+?)>: (.*)$/s; -export default class IOSDevice extends BaseDevice { +export default class IOSDevice extends ServerDevice { log?: child_process.ChildProcessWithoutNullStreams; buffer: string; private recordingProcess?: ChildProcess; @@ -46,13 +47,14 @@ export default class IOSDevice extends BaseDevice { private iOSBridge: IOSBridge; constructor( + flipperServer: FlipperServerImpl, iOSBridge: IOSBridge, serial: string, deviceType: DeviceType, title: string, ) { - super(serial, deviceType, title, 'iOS'); - this.icon = 'mobile'; + super(flipperServer, {serial, deviceType, title, os: 'iOS'}); + // TODO: this.icon = 'mobile'; this.buffer = ''; this.iOSBridge = iOSBridge; } @@ -87,7 +89,10 @@ export default class IOSDevice extends BaseDevice { if (!this.log) { try { - this.log = iOSBridge.startLogListener(this.serial, this.deviceType); + this.log = iOSBridge.startLogListener( + this.serial, + this.info.deviceType, + ); } catch (e) { if (e.message === ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB) { console.warn(e); @@ -110,7 +115,7 @@ export default class IOSDevice extends BaseDevice { }); try { - if (this.deviceType === 'physical') { + if (this.info.deviceType === 'physical') { this.log.stdout.pipe(split2('\0')).on('data', (line: string) => { const parsed = IOSDevice.parseLogLine(line); if (parsed) { @@ -205,7 +210,7 @@ export default class IOSDevice extends BaseDevice { } async screenCaptureAvailable() { - return this.deviceType === 'emulator' && this.connected.get(); + return this.info.deviceType === 'emulator' && this.connected.get(); } async startScreenCapture(destination: string) { @@ -216,7 +221,7 @@ export default class IOSDevice extends BaseDevice { this.recordingLocation = destination; } - async stopScreenCapture(): Promise { + async stopScreenCapture(): Promise { if (this.recordingProcess && this.recordingLocation) { const prom = new Promise((resolve, _reject) => { this.recordingProcess!.on( @@ -228,7 +233,7 @@ export default class IOSDevice extends BaseDevice { this.recordingProcess!.kill('SIGINT'); }); - const output: string | null = await timeout( + const output: string = await timeout( 5000, prom, 'Timed out to stop a screen capture.', @@ -241,15 +246,17 @@ export default class IOSDevice extends BaseDevice { .catch((e) => { this.recordingLocation = undefined; console.warn('Failed to terminate iOS screen recording:', e); - return null; + throw e; }); return output; } - return null; + throw new Error('No recording in progress'); } disconnect() { - this.stopScreenCapture(); + if (this.recordingProcess && this.recordingLocation) { + this.stopScreenCapture(); + } super.disconnect(); } } diff --git a/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx b/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx index 6034b65b2..f9092f5bd 100644 --- a/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx +++ b/desktop/app/src/server/devices/ios/__tests__/iOSDevice.node.tsx @@ -12,7 +12,7 @@ import configureStore from 'redux-mock-store'; import {State, createRootReducer} from '../../../../reducers/index'; import {getInstance} from '../../../../fb-stubs/Logger'; import {IOSBridge} from '../IOSBridge'; -import {FlipperServer} from '../../../FlipperServer'; +import {FlipperServerImpl} from '../../../FlipperServerImpl'; const mockStore = configureStore([])( createRootReducer()(undefined, {type: 'INIT'}), @@ -59,14 +59,14 @@ test('test parseXcodeFromCoreSimPath from standard locations', () => { }); test('test getAllPromisesForQueryingDevices when xcode detected', () => { - const flipperServer = new FlipperServer({}, mockStore, getInstance()); + const flipperServer = new FlipperServerImpl({}, mockStore, getInstance()); flipperServer.ios.iosBridge = {} as IOSBridge; const promises = flipperServer.ios.getAllPromisesForQueryingDevices(true); expect(promises.length).toEqual(3); }); test('test getAllPromisesForQueryingDevices when xcode is not detected', () => { - const flipperServer = new FlipperServer({}, mockStore, getInstance()); + const flipperServer = new FlipperServerImpl({}, mockStore, getInstance()); flipperServer.ios.iosBridge = {} as IOSBridge; const promises = flipperServer.ios.getAllPromisesForQueryingDevices(false); expect(promises.length).toEqual(1); diff --git a/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx b/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx index 4b8f8d513..1668eec82 100644 --- a/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx +++ b/desktop/app/src/server/devices/ios/iOSDeviceManager.tsx @@ -21,7 +21,7 @@ import { IOSBridge, makeIOSBridge, } from './IOSBridge'; -import {FlipperServer} from '../../FlipperServer'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; type iOSSimulatorDevice = { state: 'Booted' | 'Shutdown' | 'Shutting Down'; @@ -67,7 +67,7 @@ export class IOSDeviceManager { private xcodeVersionMismatchFound = false; public xcodeCommandLineToolsDetected = false; - constructor(private flipperServer: FlipperServer) { + constructor(private flipperServer: FlipperServerImpl) { if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { this.portForwarders.forEach((process) => process.kill()); @@ -152,7 +152,8 @@ export class IOSDeviceManager { this.flipperServer .getDevices() .filter( - (device) => device.os === 'iOS' && device.deviceType === targetType, + (device) => + device.info.os === 'iOS' && device.info.deviceType === targetType, ) .map((device) => device.serial), ); @@ -162,7 +163,13 @@ export class IOSDeviceManager { currentDeviceIDs.delete(udid); } else if (targetType === type) { console.log(`[conn] detected new iOS device ${targetType} ${udid}`); - const iOSDevice = new IOSDevice(this.iosBridge, udid, type, name); + const iOSDevice = new IOSDevice( + this.flipperServer, + this.iosBridge, + udid, + type, + name, + ); this.flipperServer.registerDevice(iOSDevice); } } diff --git a/desktop/app/src/server/devices/metro/MetroDevice.tsx b/desktop/app/src/server/devices/metro/MetroDevice.tsx index c3d44dd30..8132fe41e 100644 --- a/desktop/app/src/server/devices/metro/MetroDevice.tsx +++ b/desktop/app/src/server/devices/metro/MetroDevice.tsx @@ -8,9 +8,10 @@ */ import {LogLevel} from 'flipper-plugin'; -import BaseDevice from '../BaseDevice'; import {EventEmitter} from 'events'; import util from 'util'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; +import {ServerDevice} from '../ServerDevice'; // From xplat/js/metro/packages/metro/src/lib/reporting.js export type BundleDetails = { @@ -139,12 +140,21 @@ function getLoglevelFromMessageType( } } -export default class MetroDevice extends BaseDevice { +export default class MetroDevice extends ServerDevice { ws?: WebSocket; metroEventEmitter = new EventEmitter(); - constructor(serial: string, ws: WebSocket | undefined) { - super(serial, 'emulator', 'React Native', 'Metro'); + constructor( + flipperServer: FlipperServerImpl, + serial: string, + ws: WebSocket | undefined, + ) { + super(flipperServer, { + serial, + deviceType: 'emulator', + title: 'React Native', + os: 'Metro', + }); if (ws) { this.ws = ws; ws.onmessage = this._handleWSMessage; diff --git a/desktop/app/src/server/devices/metro/metroDeviceManager.tsx b/desktop/app/src/server/devices/metro/metroDeviceManager.tsx index f268289e6..beae98973 100644 --- a/desktop/app/src/server/devices/metro/metroDeviceManager.tsx +++ b/desktop/app/src/server/devices/metro/metroDeviceManager.tsx @@ -10,7 +10,7 @@ import MetroDevice from './MetroDevice'; import http from 'http'; import {parseEnvironmentVariableAsNumber} from '../../utils/environmentVariables'; -import {FlipperServer} from '../../FlipperServer'; +import {FlipperServerImpl} from '../../FlipperServerImpl'; const METRO_HOST = 'localhost'; const METRO_PORT = parseEnvironmentVariableAsNumber('METRO_SERVER_PORT', 8081); @@ -46,13 +46,13 @@ async function isMetroRunning(): Promise { async function registerMetroDevice( ws: WebSocket | undefined, - flipperServer: FlipperServer, + flipperServer: FlipperServerImpl, ) { - const metroDevice = new MetroDevice(METRO_URL, ws); + const metroDevice = new MetroDevice(flipperServer, METRO_URL, ws); flipperServer.registerDevice(metroDevice); } -export default (flipperServer: FlipperServer) => { +export default (flipperServer: FlipperServerImpl) => { let timeoutHandle: NodeJS.Timeout; let ws: WebSocket | undefined; let unregistered = false; diff --git a/desktop/app/src/test-utils/MockFlipper.tsx b/desktop/app/src/test-utils/MockFlipper.tsx index b5f308775..3e2e2e8c6 100644 --- a/desktop/app/src/test-utils/MockFlipper.tsx +++ b/desktop/app/src/test-utils/MockFlipper.tsx @@ -8,7 +8,7 @@ */ import {createStore} from 'redux'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; import {createRootReducer} from '../reducers'; import {Store} from '../reducers/index'; import Client from '../Client'; @@ -24,8 +24,9 @@ import {getInstance} from '../fb-stubs/Logger'; import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation'; import pluginManager from '../dispatcher/pluginManager'; import {PluginDetails} from 'flipper-plugin-lib'; -import ArchivedDevice from '../server/devices/ArchivedDevice'; +import ArchivedDevice from '../devices/ArchivedDevice'; import {ClientQuery, DeviceOS} from 'flipper-plugin'; +import {TestDevice} from './TestDevice'; export interface AppOptions { plugins?: PluginDefinition[]; @@ -129,7 +130,7 @@ export default class MockFlipper { title: 'archived device', os: 'Android', }) - : new BaseDevice(s, 'physical', 'MockAndroidDevice', os ?? 'Android'); + : new TestDevice(s, 'physical', 'MockAndroidDevice', os ?? 'Android'); device.supportsPlugin = !isSupportedByPlugin ? () => true : isSupportedByPlugin; diff --git a/desktop/app/src/test-utils/TestDevice.tsx b/desktop/app/src/test-utils/TestDevice.tsx new file mode 100644 index 000000000..f8b9d6c56 --- /dev/null +++ b/desktop/app/src/test-utils/TestDevice.tsx @@ -0,0 +1,38 @@ +/** + * 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 {DeviceOS, DeviceType} from 'flipper-plugin'; +import {DeviceSpec} from 'flipper-plugin-lib'; +import BaseDevice from '../devices/BaseDevice'; + +export class TestDevice extends BaseDevice { + constructor( + serial: string, + deviceType: DeviceType, + title: string, + os: DeviceOS, + specs?: DeviceSpec[], + ) { + super( + { + on: jest.fn(), + off: jest.fn(), + exec: jest.fn(), + close: jest.fn(), + }, + { + serial, + deviceType, + title, + os, + specs, + }, + ); + } +} diff --git a/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx b/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx index cc130385d..53aa09271 100644 --- a/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx +++ b/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx @@ -21,7 +21,7 @@ import { selectDevice, selectClient, } from '../reducers/connections'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; import {Store} from '../reducers/index'; import Client from '../Client'; diff --git a/desktop/app/src/utils/__tests__/exportData.node.tsx b/desktop/app/src/utils/__tests__/exportData.node.tsx index 73422101c..f91ab6982 100644 --- a/desktop/app/src/utils/__tests__/exportData.node.tsx +++ b/desktop/app/src/utils/__tests__/exportData.node.tsx @@ -9,8 +9,7 @@ import {State} from '../../reducers/index'; import configureStore from 'redux-mock-store'; -import {default as BaseDevice} from '../../server/devices/BaseDevice'; -import {default as ArchivedDevice} from '../../server/devices/ArchivedDevice'; +import {default as ArchivedDevice} from '../../devices/ArchivedDevice'; import { processStore, determinePluginsToProcess, @@ -35,6 +34,7 @@ import { } from 'flipper-plugin'; import {selectPlugin} from '../../reducers/connections'; import {TestIdler} from '../Idler'; +import {TestDevice} from '../..'; const testIdler = new TestIdler(); @@ -71,7 +71,7 @@ function generateNotifications( return {id, title, message, severity}; } -function generateClientIdentifier(device: BaseDevice, app: string): string { +function generateClientIdentifier(device: TestDevice, app: string): string { const {os, deviceType, serial} = device; const identifier = `${app}#${os}#${deviceType}#${serial}`; return identifier; @@ -98,7 +98,7 @@ function generateClientFromClientWithSalt( }; } function generateClientFromDevice( - device: BaseDevice, + device: TestDevice, app: string, ): ClientExport { const {os, deviceType, serial} = device; @@ -710,7 +710,7 @@ test('test processStore function for no selected plugins', async () => { }); test('test determinePluginsToProcess for mutilple clients having plugins present', async () => { - const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); + const device1 = new TestDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); const client1 = new Client( generateClientIdentifier(device1, 'app'), { @@ -797,7 +797,7 @@ test('test determinePluginsToProcess for mutilple clients having plugins present }); test('test determinePluginsToProcess for no selected plugin present in any clients', async () => { - const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); + const device1 = new TestDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); const client1 = new Client( generateClientIdentifier(device1, 'app'), { @@ -853,7 +853,7 @@ test('test determinePluginsToProcess for no selected plugin present in any clien }); test('test determinePluginsToProcess for multiple clients on same device', async () => { - const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); + const device1 = new TestDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); const client1 = new Client( generateClientIdentifier(device1, 'app'), { @@ -913,8 +913,8 @@ test('test determinePluginsToProcess for multiple clients on same device', async }); test('test determinePluginsToProcess for multiple clients on different device', async () => { - const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); - const device2 = new BaseDevice('serial2', 'emulator', 'TestiPhone', 'iOS'); + const device1 = new TestDevice('serial1', 'emulator', 'TestiPhone', 'iOS'); + const device2 = new TestDevice('serial2', 'emulator', 'TestiPhone', 'iOS'); const client1Device1 = new Client( generateClientIdentifier(device1, 'app'), { @@ -1006,7 +1006,7 @@ test('test determinePluginsToProcess for multiple clients on different device', }); test('test determinePluginsToProcess to ignore archived clients', async () => { - const selectedDevice = new BaseDevice( + const selectedDevice = new TestDevice( 'serial', 'emulator', 'TestiPhone', diff --git a/desktop/app/src/utils/clientUtils.tsx b/desktop/app/src/utils/clientUtils.tsx index 9775d97c8..8ad37d379 100644 --- a/desktop/app/src/utils/clientUtils.tsx +++ b/desktop/app/src/utils/clientUtils.tsx @@ -8,7 +8,7 @@ */ import type Client from '../Client'; -import type BaseDevice from '../server/devices/BaseDevice'; +import type BaseDevice from '../devices/BaseDevice'; /* A Client uniuely identifies an app running on some device. diff --git a/desktop/app/src/utils/exportData.tsx b/desktop/app/src/utils/exportData.tsx index a3fe6aeb9..984728cc3 100644 --- a/desktop/app/src/utils/exportData.tsx +++ b/desktop/app/src/utils/exportData.tsx @@ -12,15 +12,15 @@ import path from 'path'; import electron from 'electron'; import {getInstance as getLogger} from '../fb-stubs/Logger'; import {Store, MiddlewareAPI} from '../reducers'; -import {DeviceExport} from '../server/devices/BaseDevice'; +import {DeviceExport} from '../devices/BaseDevice'; import {State as PluginsState} from '../reducers/plugins'; import {PluginNotification} from '../reducers/notifications'; import Client, {ClientExport} from '../Client'; import {getAppVersion} from './info'; import {pluginKey} from '../utils/pluginKey'; import {DevicePluginMap, ClientPluginMap} from '../plugin'; -import {default as BaseDevice} from '../server/devices/BaseDevice'; -import {default as ArchivedDevice} from '../server/devices/ArchivedDevice'; +import {default as BaseDevice} from '../devices/BaseDevice'; +import {default as ArchivedDevice} from '../devices/ArchivedDevice'; import fs from 'fs'; import {v4 as uuidv4} from 'uuid'; import {remote, OpenDialogOptions} from 'electron'; diff --git a/desktop/app/src/utils/flipperLibImplementation.tsx b/desktop/app/src/utils/flipperLibImplementation.tsx index f62e20545..00d547c4a 100644 --- a/desktop/app/src/utils/flipperLibImplementation.tsx +++ b/desktop/app/src/utils/flipperLibImplementation.tsx @@ -12,7 +12,7 @@ import type {Logger} from '../fb-interfaces/Logger'; import type {Store} from '../reducers'; import createPaste from '../fb-stubs/createPaste'; import GK from '../fb-stubs/GK'; -import type BaseDevice from '../server/devices/BaseDevice'; +import type BaseDevice from '../devices/BaseDevice'; import {clipboard, shell} from 'electron'; import constants from '../fb-stubs/constants'; import {addNotification} from '../reducers/notifications'; diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx index 658ebd4be..7d7fb1189 100644 --- a/desktop/app/src/utils/pluginUtils.tsx +++ b/desktop/app/src/utils/pluginUtils.tsx @@ -10,7 +10,7 @@ import type {PluginDefinition} from '../plugin'; import type {State, Store} from '../reducers'; import type {State as PluginsState} from '../reducers/plugins'; -import type BaseDevice from '../server/devices/BaseDevice'; +import type BaseDevice from '../devices/BaseDevice'; import type Client from '../Client'; import type { ActivatablePluginDetails, diff --git a/desktop/app/src/utils/reduxDevToolsConfig.tsx b/desktop/app/src/utils/reduxDevToolsConfig.tsx index 251b10259..0d8e446c1 100644 --- a/desktop/app/src/utils/reduxDevToolsConfig.tsx +++ b/desktop/app/src/utils/reduxDevToolsConfig.tsx @@ -8,7 +8,7 @@ */ import {State} from '../reducers/index'; -import {DeviceExport} from '../server/devices/BaseDevice'; +import {DeviceExport} from '../devices/BaseDevice'; export const stateSanitizer = (state: State) => { if (state.connections && state.connections.devices) { diff --git a/desktop/app/src/utils/screenshot.tsx b/desktop/app/src/utils/screenshot.tsx index d0de9a1a3..0b07b8297 100644 --- a/desktop/app/src/utils/screenshot.tsx +++ b/desktop/app/src/utils/screenshot.tsx @@ -9,7 +9,7 @@ import fs from 'fs'; import path from 'path'; -import BaseDevice from '../server/devices/BaseDevice'; +import BaseDevice from '../devices/BaseDevice'; import {reportPlatformFailures} from './metrics'; import expandTilde from 'expand-tilde'; import {remote} from 'electron'; diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index 195a24146..17c82b807 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -69,7 +69,6 @@ export interface RealFlipperDevice { deviceType: DeviceType; addLogListener(callback: DeviceLogListener): Symbol; removeLogListener(id: Symbol): void; - addLogEntry(entry: DeviceLogEntry): void; } export class SandyDevicePluginInstance extends BasePluginInstance { diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 858695c10..3ea699e1f 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -486,7 +486,9 @@ export function createMockBundledPluginDetails( }; } -function createMockDevice(options?: StartPluginOptions): RealFlipperDevice { +function createMockDevice(options?: StartPluginOptions): RealFlipperDevice & { + addLogEntry(entry: DeviceLogEntry): void; +} { const logListeners: (undefined | DeviceLogListener)[] = []; return { os: 'Android', diff --git a/desktop/flipper-plugin/src/types/server-types.tsx b/desktop/flipper-plugin/src/types/server-types.tsx index bf78d5045..313f2190b 100644 --- a/desktop/flipper-plugin/src/types/server-types.tsx +++ b/desktop/flipper-plugin/src/types/server-types.tsx @@ -8,9 +8,11 @@ */ import { + DeviceSpec, DeviceType as PluginDeviceType, OS as PluginOS, } from 'flipper-plugin-lib'; +import {DeviceLogEntry} from '../plugin/DevicePlugin'; // In the future, this file would deserve it's own package, as it doesn't really relate to plugins. // Since flipper-plugin however is currently shared among server, client and defines a lot of base types, leaving it here for now. @@ -31,6 +33,10 @@ export type DeviceDescription = { readonly title: string; readonly deviceType: DeviceType; readonly serial: string; + // Android specific information + readonly specs?: DeviceSpec[]; + readonly abiList?: string[]; + readonly sdkVersion?: string; }; export type ClientQuery = { @@ -58,8 +64,39 @@ export type FlipperServerEvents = { 'device-connected': DeviceDescription; 'device-disconnected': DeviceDescription; 'client-connected': ClientDescription; + 'device-log': { + serial: string; + entry: DeviceLogEntry; + }; }; export type FlipperServerCommands = { - // TODO + 'device-start-logging': (serial: string) => Promise; + 'device-stop-logging': (serial: string) => Promise; + 'device-supports-screenshot': (serial: string) => Promise; + 'device-supports-screencapture': (serial: string) => Promise; + 'device-take-screenshot': (serial: string) => Promise; // base64 encoded buffer + 'device-start-screencapture': ( + serial: string, + destination: string, + ) => Promise; + 'device-stop-screencapture': (serial: string) => Promise; // file path + 'device-shell-exec': (serial: string, command: string) => Promise; + 'metro-command': (serial: string, command: string) => Promise; }; + +export interface FlipperServer { + on( + event: Event, + callback: (payload: FlipperServerEvents[Event]) => void, + ): void; + off( + event: Event, + callback: (payload: FlipperServerEvents[Event]) => void, + ): void; + exec( + event: Event, + ...args: Parameters + ): ReturnType; + close(): void; +} diff --git a/desktop/plugins/public/cpu/index.tsx b/desktop/plugins/public/cpu/index.tsx index 210cb93f0..73a99b1ca 100644 --- a/desktop/plugins/public/cpu/index.tsx +++ b/desktop/plugins/public/cpu/index.tsx @@ -75,6 +75,7 @@ export function devicePlugin(client: PluginClient<{}, {}>) { const device = client.device; const executeShell = async (command: string) => { + // TODO: fix return new Promise((resolve, reject) => { (device.realDevice as any).adb .shell(device.serial, command) diff --git a/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx b/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx index fd146ffc4..e5d3e65e9 100644 --- a/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx +++ b/desktop/plugins/public/crash_reporter/__tests__/testCrashReporterPlugin.node.tsx @@ -7,7 +7,7 @@ * @format */ -import {BaseDevice} from 'flipper'; +import {TestDevice} from 'flipper'; import {Crash, CrashLog} from '../index'; import {TestUtils} from 'flipper-plugin'; import {getPluginKey} from 'flipper'; @@ -121,7 +121,7 @@ test('test the parsing of the Android crash log with os being iOS', () => { expect(crash.name).toEqual('Cannot figure out the cause'); }); test('test the getter of pluginKey with proper input', () => { - const device = new BaseDevice('serial', 'emulator', 'test device', 'iOS'); + const device = new TestDevice('serial', 'emulator', 'test device', 'iOS'); const pluginKey = getPluginKey(null, device, 'CrashReporter'); expect(pluginKey).toEqual('serial#CrashReporter'); }); @@ -134,7 +134,7 @@ test('test the getter of pluginKey with defined selected app', () => { expect(pluginKey).toEqual('selectedApp#CrashReporter'); }); test('test the getter of pluginKey with defined selected app and defined base device', () => { - const device = new BaseDevice('serial', 'emulator', 'test device', 'iOS'); + const device = new TestDevice('serial', 'emulator', 'test device', 'iOS'); const pluginKey = getPluginKey('selectedApp', device, 'CrashReporter'); expect(pluginKey).toEqual('selectedApp#CrashReporter'); }); @@ -216,7 +216,7 @@ test('test parsing of path when a regex is not present', () => { }); test('test shouldShowCrashNotification function for all correct inputs', () => { - const device = new BaseDevice( + const device = new TestDevice( 'TH1S-15DEV1CE-1D', 'emulator', 'test device', @@ -231,7 +231,7 @@ test('test shouldShowCrashNotification function for all correct inputs', () => { expect(shouldShowNotification).toEqual(true); }); test('test shouldShowiOSCrashNotification function for all correct inputs but incorrect id', () => { - const device = new BaseDevice( + const device = new TestDevice( 'TH1S-15DEV1CE-1D', 'emulator', 'test device', diff --git a/desktop/plugins/public/kaios-allocations/index.tsx b/desktop/plugins/public/kaios-allocations/index.tsx index 9fe7490cf..678b571bc 100644 --- a/desktop/plugins/public/kaios-allocations/index.tsx +++ b/desktop/plugins/public/kaios-allocations/index.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import {FlipperDevicePlugin, Device, KaiOSDevice} from 'flipper'; +import {FlipperDevicePlugin, Device} from 'flipper'; import { Button, @@ -186,7 +186,7 @@ export default class AllocationsPlugin extends FlipperDevicePlugin< }; static supportsDevice(device: Device) { - return device instanceof KaiOSDevice; + return device.description.specs?.includes('KaiOS') ?? false; } onStartMonitor = async () => { diff --git a/desktop/plugins/public/kaios-ram/index.tsx b/desktop/plugins/public/kaios-ram/index.tsx index cc98a4a9a..e5b149970 100644 --- a/desktop/plugins/public/kaios-ram/index.tsx +++ b/desktop/plugins/public/kaios-ram/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; -import {FlipperDevicePlugin, Device, KaiOSDevice, sleep} from 'flipper'; +import {FlipperDevicePlugin, Device, sleep} from 'flipper'; import {FlexColumn, Button, Toolbar, Panel} from 'flipper'; @@ -76,7 +76,7 @@ export default class KaiOSGraphs extends FlipperDevicePlugin { }; static supportsDevice(device: Device) { - return device instanceof KaiOSDevice; + return device.description.specs?.includes('KaiOS') ?? false; } async init() { @@ -116,13 +116,8 @@ export default class KaiOSGraphs extends FlipperDevicePlugin { } }; - executeShell = (command: string) => { - return (this.device as KaiOSDevice).adb - .shell(this.device.serial, command) - .then(adb.util.readAll) - .then((output) => { - return output.toString().trim(); - }); + executeShell = async (command: string): Promise => { + return this.device.executeShell(command); }; getMemory = () => { diff --git a/desktop/plugins/public/navigation/util/appMatchPatterns.tsx b/desktop/plugins/public/navigation/util/appMatchPatterns.tsx index 770e39b5b..59c14d0e9 100644 --- a/desktop/plugins/public/navigation/util/appMatchPatterns.tsx +++ b/desktop/plugins/public/navigation/util/appMatchPatterns.tsx @@ -9,7 +9,7 @@ import fs from 'fs'; import path from 'path'; -import {BaseDevice, AndroidDevice, IOSDevice, getAppPath} from 'flipper'; +import {BaseDevice, getAppPath} from 'flipper'; import {AppMatchPattern} from '../types'; let patternsPath: string | undefined; @@ -34,9 +34,9 @@ export const getAppMatchPatterns = ( const appName = extractAppNameFromSelectedApp(selectedApp); if (appName === 'Facebook') { let filename: string; - if (device instanceof AndroidDevice) { + if (device.os === 'Android') { filename = 'facebook-match-patterns-android.json'; - } else if (device instanceof IOSDevice) { + } else if (device.os === 'iOS') { filename = 'facebook-match-patterns-ios.json'; } else { return; diff --git a/desktop/yarn.lock b/desktop/yarn.lock index c296c04be..5cad40778 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -3461,6 +3461,11 @@ "@typescript-eslint/types" "4.31.0" eslint-visitor-keys "^2.0.0" +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -3654,7 +3659,7 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@^4.1.1: +ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== @@ -3786,6 +3791,14 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + app-builder-bin@3.5.13: version "3.5.13" resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-3.5.13.tgz#6dd7f4de34a4e408806f99b8c7d6ef1601305b7e" @@ -4058,11 +4071,11 @@ axe-core@^4.0.2: integrity sha512-1uIESzroqpaTzt9uX48HO+6gfnKu3RwvWdCcWSrX4csMInJfCo1yvKPNXCwXFRpJqRW25tiASb6No0YH57PXqg== axios@^0.18.0, axios@^0.21.1, axios@^0.21.2: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.10.0" axobject-query@^2.2.0: version "2.2.0" @@ -4332,6 +4345,11 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + binaryextensions@^4.15.0: version "4.15.0" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-4.15.0.tgz#c63a502e0078ff1b0e9b00a9f74d3c2b0f8bd32e" @@ -4422,7 +4440,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -4441,6 +4459,11 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + browserslist@^4.16.5, browserslist@^4.16.6: version "4.17.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.0.tgz#1fcd81ec75b41d6d4994fb0831b92ac18c01649c" @@ -4693,6 +4716,21 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chokidar@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.3.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -5279,9 +5317,9 @@ cssstyle@^2.3.0: cssom "~0.3.6" csstype@^2.5.7, csstype@^2.6.7, csstype@^3.0.2, csstype@^3.0.5: - version "3.0.8" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" - integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + version "3.0.9" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" + integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== d3-array@2, d3-array@2.3.3, d3-array@^2.3.0: version "2.3.3" @@ -5375,6 +5413,13 @@ debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, d dependencies: ms "2.1.2" +debug@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -5387,6 +5432,11 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + decimal.js-light@^2.4.1: version "2.5.1" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -5602,6 +5652,11 @@ diff-sequences@^26.6.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -6728,6 +6783,14 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -6772,6 +6835,11 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" @@ -6787,10 +6855,10 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.110.0.tgz#fa56d1fdc4bbeb488db8b9d0a685cb91939ab0ff" integrity sha512-IVHejqogTgZL2e206twVsdfX5he6mXS5F0AY315ao+6rEifbElEoVWKLYdEBsVl7QMp4buPbMe5FqXSdYQMUSQ== -follow-redirects@^1.14.0: - version "1.14.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" - integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== +follow-redirects@^1.10.0: + version "1.14.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" + integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== for-in@^1.0.2: version "1.0.2" @@ -6905,6 +6973,11 @@ fsevents@^2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -6978,13 +7051,25 @@ github-slugger@^1.2.1: dependencies: emoji-regex ">=6.0.0 <=6.1.1" -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" @@ -7125,6 +7210,11 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1. resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -7193,6 +7283,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hermes-parser@0.4.7: version "0.4.7" resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.4.7.tgz#410f5129d57183784d205a0538e6fbdcf614c9ea" @@ -7530,6 +7625,13 @@ is-bigint@^1.0.1: resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" @@ -7667,7 +7769,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -7744,7 +7846,7 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^2.0.0: +is-plain-obj@^2.0.0, is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== @@ -8673,6 +8775,13 @@ jest@^26.6.3: import-local "^3.0.2" jest-cli "^26.6.3" +js-base64@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.0.tgz#74979859ac07f999d28280b0697e82715f7e36ab" + integrity sha512-hJiXqoqZKdNx7PNuqHx3ZOgwcvgCprV0cs9ZMeqERshhVZ3cmXc3HGR60mKsHHqVK18PCwGXnmPiPDbao7SOMQ== + dependencies: + mocha "^8.4.0" + js-message@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.5.tgz#2300d24b1af08e89dd095bc1a4c9c9cfcb892d15" @@ -8690,6 +8799,13 @@ js-queue@2.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.0.0, js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" @@ -8698,13 +8814,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" - integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== - dependencies: - argparse "^2.0.1" - jscodeshift@^0.6.3: version "0.6.4" resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.6.4.tgz#e19ab86214edac86a75c4557fc88b3937d558a8e" @@ -9071,6 +9180,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lockfile@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609" @@ -9173,6 +9289,13 @@ lodash@4.x, lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lo resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" @@ -9748,6 +9871,37 @@ mkdirp@^0.5.1, mkdirp@^0.5.4: dependencies: minimist "^1.2.5" +mocha@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff" + integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.1" + debug "4.3.1" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.1.6" + growl "1.10.5" + he "1.2.0" + js-yaml "4.0.0" + log-symbols "4.0.0" + minimatch "3.0.4" + ms "2.1.3" + nanoid "3.1.20" + serialize-javascript "5.0.1" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + wide-align "1.1.3" + workerpool "6.1.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mock-fs@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.0.0.tgz#5574520ac824c01a10091bf951c66f677c71acaa" @@ -9773,7 +9927,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -9783,6 +9937,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nanoid@3.1.20: + version "3.1.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9919,7 +10078,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -10227,6 +10386,13 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -10248,6 +10414,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -10443,7 +10616,7 @@ picomatch@^2.0.4, picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -picomatch@^2.2.3: +picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -10749,6 +10922,13 @@ raf@^3.4.0: dependencies: performance-now "^2.1.0" +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -11502,6 +11682,13 @@ readdir-glob@^1.0.0: dependencies: minimatch "^3.0.4" +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== + dependencies: + picomatch "^2.2.1" + realpath-native@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866" @@ -11949,7 +12136,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -12087,6 +12274,13 @@ serialize-error@^5.0.0: dependencies: type-fest "^0.8.0" +serialize-javascript@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" @@ -12486,7 +12680,7 @@ string-natural-compare@^3.0.0, string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -string-width@^2.0.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -12611,7 +12805,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -12647,6 +12841,13 @@ sumchecker@^3.0.1: dependencies: debug "^4.1.0" +supports-color@8.1.1, supports-color@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -12661,13 +12862,6 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-hyperlinks@^2.0.0, supports-hyperlinks@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" @@ -13523,6 +13717,13 @@ which-typed-array@^1.1.2: has-symbols "^1.0.1" is-typed-array "^1.1.3" +which@2.0.2, which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -13530,12 +13731,12 @@ which@^1.2.9: dependencies: isexe "^2.0.0" -which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== dependencies: - isexe "^2.0.0" + string-width "^1.0.2 || 2" widest-line@^3.1.0: version "3.1.0" @@ -13549,6 +13750,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +workerpool@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b" + integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg== + wrap-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131" @@ -13689,6 +13895,11 @@ yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@20.x: version "20.2.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" @@ -13707,6 +13918,29 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -13724,19 +13958,6 @@ yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb" @@ -13770,6 +13991,11 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zip-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79"