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<buffer>`).

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
This commit is contained in:
Michel Weststrate
2021-09-22 09:01:29 -07:00
committed by Facebook GitHub Bot
parent 845d0755f1
commit 2d838efd4d
58 changed files with 869 additions and 396 deletions

View File

@@ -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",

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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';

View File

@@ -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 || '';

View File

@@ -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<Buffer> {
return Promise.reject(
new Error('No screenshot support for current device'),
async screenshotAvailable(): Promise<boolean> {
if (this.isArchived) {
return false;
}
return this.flipperServer.exec('device-supports-screenshot', this.serial);
}
async screenshot(): Promise<Buffer> {
if (this.isArchived) {
return Buffer.from([]);
}
return Buffer.from(
Base64.toUint8Array(
await this.flipperServer.exec('device-take-screenshot', this.serial),
),
);
}
async screenCaptureAvailable(): Promise<boolean> {
return false;
if (this.isArchived) {
return false;
}
return this.flipperServer.exec(
'device-supports-screencapture',
this.serial,
);
}
async startScreenCapture(_destination: string): Promise<void> {
throw new Error('startScreenCapture not implemented on BaseDevice ');
async startScreenCapture(destination: string): Promise<void> {
return this.flipperServer.exec(
'device-start-screencapture',
this.serial,
destination,
);
}
async stopScreenCapture(): Promise<string | null> {
return null;
return this.flipperServer.exec('device-stop-screencapture', this.serial);
}
async executeShell(command: string): Promise<string> {
return this.flipperServer.exec('device-shell-exec', this.serial, command);
}
supportsPlugin(plugin: PluginDefinition | PluginDetails) {

View File

@@ -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',

View File

@@ -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,
});
});

View File

@@ -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';

View File

@@ -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,
),
);
}),
);
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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([]);
});

View File

@@ -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);
});

View File

@@ -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<StateV2, 'enabledPlugins' | 'enabledDevicePlugins'> & {
@@ -91,10 +89,6 @@ type StateV0 = Omit<StateV1, 'enabledPlugins' | 'enabledDevicePlugins'> & {
};
export type Action =
| {
type: 'UNREGISTER_DEVICES';
payload: Set<string>;
}
| {
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]),
});
}
}

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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);

View File

@@ -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', () => {

View File

@@ -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
);
});

View File

@@ -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<string, BaseDevice>();
private readonly devices = new Map<string, ServerDevice>();
state: FlipperServerState = 'pending';
android: AndroidDeviceManager;
ios: IOSDeviceManager;
@@ -197,6 +204,13 @@ export class FlipperServer {
this.events.on(event, callback);
}
off<Event extends keyof FlipperServerEvents>(
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 extends keyof FlipperServerCommands>(
event: Event,
...args: Parameters<FlipperServerCommands[Event]>
): ReturnType<FlipperServerCommands[Event]> {
console.debug(`[FlipperServer] command ${event}: `, args);
const handler: (...args: any[]) => Promise<any> =
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');

View File

@@ -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<string, NodeJS.Timeout> = 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' : ''),

View File

@@ -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);
};
/**

View File

@@ -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,
});
}
}

View File

@@ -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<boolean> {
return false;
}
screenshot(): Promise<Buffer> {
return Promise.reject(
new Error('No screenshot support for current device'),
);
}
async screenCaptureAvailable(): Promise<boolean> {
return false;
}
async startScreenCapture(_destination: string): Promise<void> {
throw new Error('startScreenCapture not implemented on BaseDevice ');
}
async stopScreenCapture(): Promise<string> {
throw new Error('stopScreenCapture not implemented on BaseDevice ');
}
async executeShell(_command: string): Promise<string> {
throw new Error('executeShell not implemented on BaseDevice');
}
}

View File

@@ -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();

View File

@@ -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<string> = [];
sdkVersion: string | undefined = undefined;
pidAppMapping: {[key: number]: string} = {};
private recordingProcess?: Promise<string>;
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<void> {
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<Buffer> {
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<boolean> {
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<void> {
async executeShell(command: string): Promise<string> {
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<void> {
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`;

View File

@@ -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<string>,
sdkVersion: string,
) {
super(serial, deviceType, title, adb, abiList, sdkVersion, ['KaiOS']);
super(flipperServer, serial, deviceType, title, adb, abiList, sdkVersion, [
'KaiOS',
]);
}
async screenCaptureAvailable() {

View File

@@ -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([

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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;
}

View File

@@ -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] <Notice>: 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<string | null> {
async stopScreenCapture(): Promise<string> {
if (this.recordingProcess && this.recordingLocation) {
const prom = new Promise<void>((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<void>(
const output: string = await timeout<void>(
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();
}
}

View File

@@ -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<State, {}>([])(
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);

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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<boolean> {
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;

View File

@@ -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;

View File

@@ -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,
},
);
}
}

View File

@@ -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';

View File

@@ -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',

View File

@@ -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.

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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<void>;
'device-stop-logging': (serial: string) => Promise<void>;
'device-supports-screenshot': (serial: string) => Promise<boolean>;
'device-supports-screencapture': (serial: string) => Promise<boolean>;
'device-take-screenshot': (serial: string) => Promise<string>; // base64 encoded buffer
'device-start-screencapture': (
serial: string,
destination: string,
) => Promise<void>;
'device-stop-screencapture': (serial: string) => Promise<string>; // file path
'device-shell-exec': (serial: string, command: string) => Promise<string>;
'metro-command': (serial: string, command: string) => Promise<void>;
};
export interface FlipperServer {
on<Event extends keyof FlipperServerEvents>(
event: Event,
callback: (payload: FlipperServerEvents[Event]) => void,
): void;
off<Event extends keyof FlipperServerEvents>(
event: Event,
callback: (payload: FlipperServerEvents[Event]) => void,
): void;
exec<Event extends keyof FlipperServerCommands>(
event: Event,
...args: Parameters<FlipperServerCommands[Event]>
): ReturnType<FlipperServerCommands[Event]>;
close(): void;
}

View File

@@ -75,6 +75,7 @@ export function devicePlugin(client: PluginClient<{}, {}>) {
const device = client.device;
const executeShell = async (command: string) => {
// TODO: fix
return new Promise<string>((resolve, reject) => {
(device.realDevice as any).adb
.shell(device.serial, command)

View File

@@ -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',

View File

@@ -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 () => {

View File

@@ -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<State, any, any> {
};
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<State, any, any> {
}
};
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<string> => {
return this.device.executeShell(command);
};
getMemory = () => {

View File

@@ -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;

View File

@@ -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"