Move app/src (mostly) to flipper-ui-core/src

Summary:
This diff moves all UI code from app/src to app/flipper-ui-core. That is now slightly too much (e.g. node deps are not removed yet), but from here it should be easier to move things out again, as I don't want this diff to be open for too long to avoid too much merge conflicts.

* But at least flipper-ui-core is Electron free :)
* Killed all cross module imports as well, as they where now even more in the way
* Some unit test needed some changes, most not too big (but emotion hashes got renumbered in the snapshots, feel free to ignore that)
* Found some files that were actually meaningless (tsconfig in plugins, WatchTools files, that start generating compile errors, removed those

Follow up work:
* make flipper-ui-core configurable, and wire up flipper-server-core in Electron instead of here
* remove node deps (aigoncharov)
* figure out correct place to load GKs, plugins, make intern requests etc., and move to the correct module
* clean up deps

Reviewed By: aigoncharov

Differential Revision: D32427722

fbshipit-source-id: 14fe92e1ceb15b9dcf7bece367c8ab92df927a70
This commit is contained in:
Michel Weststrate
2021-11-16 05:25:40 -08:00
committed by Facebook GitHub Bot
parent 54b7ce9308
commit 7e50c0466a
293 changed files with 483 additions and 497 deletions

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import BaseDevice from './BaseDevice';
import type {DeviceOS, DeviceType} from 'flipper-plugin';
import {DeviceShell} from './BaseDevice';
import {SupportFormRequestDetailsState} from '../reducers/supportForm';
export default class ArchivedDevice extends BaseDevice {
isArchived = true;
constructor(options: {
serial: string;
deviceType: DeviceType;
title: string;
os: DeviceOS;
screenshotHandle?: string | null;
source?: string;
supportRequestDetails?: SupportFormRequestDetailsState;
}) {
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,
icon: 'box',
},
);
this.connected.set(false);
this.source = options.source || '';
this.supportRequestDetails = options.supportRequestDetails;
this.archivedScreenshotHandle = options.screenshotHandle ?? null;
}
archivedScreenshotHandle: string | null;
displayTitle(): string {
return `${this.title} ${this.source ? '(Imported)' : '(Offline)'}`;
}
supportRequestDetails?: SupportFormRequestDetailsState;
spawnShell(): DeviceShell | undefined | null {
return null;
}
getArchivedScreenshotHandle(): string | null {
return this.archivedScreenshotHandle;
}
/**
* @override
*/
async startLogging() {
// No-op
}
/**
* @override
*/
async stopLogging() {
// No-op
}
}

View File

@@ -0,0 +1,350 @@
/**
* 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 stream from 'stream';
import {
Device,
_SandyDevicePluginInstance,
_SandyPluginDefinition,
DeviceLogListener,
Idler,
createState,
getFlipperLib,
} from 'flipper-plugin';
import {
DeviceLogEntry,
DeviceOS,
DeviceType,
DeviceDescription,
FlipperServer,
} from 'flipper-common';
import {DeviceSpec, PluginDetails} from 'flipper-plugin-lib';
import {getPluginKey} from '../utils/pluginKey';
import {Base64} from 'js-base64';
export type DeviceShell = {
stdout: stream.Readable;
stderr: stream.Readable;
stdin: stream.Writable;
};
type PluginDefinition = _SandyPluginDefinition;
type PluginMap = Map<string, PluginDefinition>;
export type DeviceExport = {
os: DeviceOS;
title: string;
deviceType: DeviceType;
serial: string;
pluginStates: Record<string, any>;
};
export default class BaseDevice implements Device {
description: DeviceDescription;
flipperServer: FlipperServer;
isArchived = false;
hasDevicePlugins = false; // true if there are device plugins for this device (not necessarily enabled)
constructor(flipperServer: FlipperServer, description: DeviceDescription) {
this.flipperServer = flipperServer;
this.description = description;
}
get isConnected(): boolean {
return this.connected.get();
}
// operating system of this device
get os() {
return this.description.os;
}
// human readable name for this device
get title(): string {
return this.description.title;
}
// type of this device
get deviceType() {
return this.description.deviceType;
}
// serial number for this device
get serial() {
return this.description.serial;
}
// additional device specs used for plugin compatibility checks
get specs(): DeviceSpec[] {
return this.description.specs ?? [];
}
// possible src of icon to display next to the device title
get icon() {
return this.description.icon;
}
logListeners: Map<Symbol, DeviceLogListener> = new Map();
readonly connected = createState(true);
// if imported, stores the original source location
source = '';
// TODO: ideally we don't want BasePlugin to know about the concept of plugins
sandyPluginStates: Map<string, _SandyDevicePluginInstance> = new Map<
string,
_SandyDevicePluginInstance
>();
supportsOS(os: DeviceOS) {
return os.toLowerCase() === this.os.toLowerCase();
}
displayTitle(): string {
return this.connected.get() ? this.title : `${this.title} (Offline)`;
}
async exportState(
idler: Idler,
onStatusMessage: (msg: string) => void,
selectedPlugins: string[],
): Promise<Record<string, any>> {
const pluginStates: Record<string, any> = {};
for (const instance of this.sandyPluginStates.values()) {
if (
selectedPlugins.includes(instance.definition.id) &&
instance.isPersistable()
) {
pluginStates[instance.definition.id] = await instance.exportState(
idler,
onStatusMessage,
);
}
}
return pluginStates;
}
toJSON() {
return {
os: this.os,
title: this.title,
deviceType: this.deviceType,
serial: this.serial,
};
}
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() {
this.flipperServer.off('device-log', this.deviceLogEventHandler);
return this.flipperServer.exec('device-stop-logging', this.serial);
}
addLogListener(callback: DeviceLogListener): Symbol {
if (this.logListeners.size === 0) {
this.startLogging();
}
const id = Symbol();
this.logListeners.set(id, callback);
return id;
}
removeLogListener(id: Symbol) {
this.logListeners.delete(id);
if (this.logListeners.size === 0) {
this.stopLogging();
}
}
async navigateToLocation(location: string) {
return this.flipperServer.exec('device-navigate', this.serial, location);
}
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> {
if (this.isArchived) {
return false;
}
return this.flipperServer.exec(
'device-supports-screencapture',
this.serial,
);
}
async startScreenCapture(destination: string): Promise<void> {
return this.flipperServer.exec(
'device-start-screencapture',
this.serial,
destination,
);
}
async stopScreenCapture(): Promise<string | 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);
}
async sendMetroCommand(command: string): Promise<void> {
return this.flipperServer.exec('metro-command', this.serial, command);
}
async forwardPort(local: string, remote: string): Promise<boolean> {
return this.flipperServer.exec(
'device-forward-port',
this.serial,
local,
remote,
);
}
async clearLogs() {
return this.flipperServer.exec('device-clear-logs', this.serial);
}
supportsPlugin(plugin: PluginDefinition | PluginDetails) {
let pluginDetails: PluginDetails;
if (plugin instanceof _SandyPluginDefinition) {
pluginDetails = plugin.details;
if (!pluginDetails.pluginType && !pluginDetails.supportedDevices) {
// TODO T84453692: this branch is to support plugins defined with the legacy approach. Need to remove this branch after some transition period when
// all the plugins will be migrated to the new approach with static compatibility metadata in package.json.
if (plugin instanceof _SandyPluginDefinition) {
return (
plugin.isDevicePlugin &&
(plugin.asDevicePluginModule().supportsDevice?.(this as any) ??
false)
);
} else {
return (plugin as any).supportsDevice(this);
}
}
} else {
pluginDetails = plugin;
}
return (
pluginDetails.pluginType === 'device' &&
(!pluginDetails.supportedDevices ||
pluginDetails.supportedDevices?.some(
(d) =>
(!d.os || d.os === this.os) &&
(!d.type || d.type === this.deviceType) &&
(d.archived === undefined || d.archived === this.isArchived) &&
(!d.specs || d.specs.every((spec) => this.specs.includes(spec))),
))
);
}
loadDevicePlugins(
devicePlugins: PluginMap,
enabledDevicePlugins: Set<string>,
pluginStates?: Record<string, any>,
) {
if (!devicePlugins) {
return;
}
const plugins = Array.from(devicePlugins.values()).filter((p) =>
enabledDevicePlugins?.has(p.id),
);
for (const plugin of plugins) {
this.loadDevicePlugin(plugin, pluginStates?.[plugin.id]);
}
}
loadDevicePlugin(plugin: PluginDefinition, initialState?: any) {
if (!this.supportsPlugin(plugin)) {
return;
}
this.hasDevicePlugins = true;
if (plugin instanceof _SandyPluginDefinition) {
try {
this.sandyPluginStates.set(
plugin.id,
new _SandyDevicePluginInstance(
getFlipperLib(),
plugin,
this,
// break circular dep, one of those days again...
getPluginKey(undefined, {serial: this.serial}, plugin.id),
initialState,
),
);
} catch (e) {
console.error(`Failed to start device plugin '${plugin.id}': `, e);
}
}
}
unloadDevicePlugin(pluginId: string) {
const instance = this.sandyPluginStates.get(pluginId);
if (instance) {
instance.destroy();
this.sandyPluginStates.delete(pluginId);
}
}
disconnect() {
this.logListeners.clear();
this.stopLogging();
this.connected.set(false);
}
destroy() {
this.disconnect();
this.sandyPluginStates.forEach((instance) => {
instance.destroy();
});
this.sandyPluginStates.clear();
}
}

View File

@@ -0,0 +1,312 @@
/**
* 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 * as DeviceTestPluginModule from '../../test-utils/DeviceTestPlugin';
import {TestUtils, _SandyPluginDefinition} from 'flipper-plugin';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {TestDevice} from '../../test-utils/TestDevice';
import ArchivedDevice from '../../devices/ArchivedDevice';
const physicalDevicePluginDetails = TestUtils.createMockPluginDetails({
id: 'physicalDevicePlugin',
name: 'flipper-plugin-physical-device',
version: '0.0.1',
pluginType: 'device',
supportedDevices: [
{
os: 'iOS',
type: 'physical',
archived: false,
},
{
os: 'Android',
type: 'physical',
},
],
});
const physicalDevicePlugin = new _SandyPluginDefinition(
physicalDevicePluginDetails,
DeviceTestPluginModule,
);
const iosPhysicalDevicePluginDetails = TestUtils.createMockPluginDetails({
id: 'iosPhysicalDevicePlugin',
name: 'flipper-plugin-ios-physical-device',
version: '0.0.1',
pluginType: 'device',
supportedDevices: [
{
os: 'iOS',
type: 'physical',
},
],
});
const iosPhysicalDevicePlugin = new _SandyPluginDefinition(
iosPhysicalDevicePluginDetails,
DeviceTestPluginModule,
);
const iosEmulatorlDevicePluginDetails = TestUtils.createMockPluginDetails({
id: 'iosEmulatorDevicePlugin',
name: 'flipper-plugin-ios-emulator-device',
version: '0.0.1',
pluginType: 'device',
supportedDevices: [
{
os: 'iOS',
type: 'emulator',
},
],
});
const iosEmulatorDevicePlugin = new _SandyPluginDefinition(
iosEmulatorlDevicePluginDetails,
DeviceTestPluginModule,
);
const androiKaiosPhysicalDevicePluginDetails =
TestUtils.createMockPluginDetails({
id: 'androidPhysicalDevicePlugin',
name: 'flipper-plugin-android-physical-device',
version: '0.0.1',
pluginType: 'device',
supportedDevices: [
{
os: 'Android',
type: 'physical',
specs: ['KaiOS'],
},
],
});
const androidKaiosPhysicalDevicePlugin = new _SandyPluginDefinition(
androiKaiosPhysicalDevicePluginDetails,
DeviceTestPluginModule,
);
const androidEmulatorlDevicePluginDetails = TestUtils.createMockPluginDetails({
id: 'androidEmulatorDevicePlugin',
name: 'flipper-plugin-android-emulator-device',
version: '0.0.1',
pluginType: 'device',
supportedDevices: [
{
os: 'Android',
type: 'emulator',
},
],
});
const androidEmulatorDevicePlugin = new _SandyPluginDefinition(
androidEmulatorlDevicePluginDetails,
DeviceTestPluginModule,
);
const androidOnlyDevicePluginDetails = TestUtils.createMockPluginDetails({
id: 'androidEmulatorDevicePlugin',
name: 'flipper-plugin-android-emulator-device',
version: '0.0.1',
pluginType: 'device',
supportedDevices: [
{
os: 'Android',
},
],
});
const androidOnlyDevicePlugin = new _SandyPluginDefinition(
androidOnlyDevicePluginDetails,
DeviceTestPluginModule,
);
test('ios physical device compatibility', () => {
const device = new TestDevice('serial', 'physical', 'test device', 'iOS');
expect(device.supportsPlugin(physicalDevicePlugin)).toBeTruthy();
expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeTruthy();
expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidKaiosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidEmulatorDevicePlugin)).toBeFalsy();
});
test('archived device compatibility', () => {
const device = new ArchivedDevice({
serial: 'serial',
deviceType: 'physical',
title: 'test device',
os: 'iOS',
screenshotHandle: null,
});
expect(device.supportsPlugin(physicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeTruthy();
expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidKaiosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidEmulatorDevicePlugin)).toBeFalsy();
});
test('android emulator device compatibility', () => {
const device = new TestDevice('serial', 'emulator', 'test device', 'Android');
expect(device.supportsPlugin(physicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidKaiosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidEmulatorDevicePlugin)).toBeTruthy();
});
test('android KaiOS device compatibility', () => {
const device = new TestDevice(
'serial',
'physical',
'test device',
'Android',
['KaiOS'],
);
expect(device.supportsPlugin(physicalDevicePlugin)).toBeTruthy();
expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidKaiosPhysicalDevicePlugin)).toBeTruthy();
expect(device.supportsPlugin(androidEmulatorDevicePlugin)).toBeFalsy();
});
test('android dummy device compatibility', () => {
const device = new TestDevice('serial', 'dummy', 'test device', 'Android');
expect(device.supportsPlugin(physicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(iosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(iosEmulatorDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidKaiosPhysicalDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidEmulatorDevicePlugin)).toBeFalsy();
expect(device.supportsPlugin(androidOnlyDevicePlugin)).toBeTruthy();
});
test('log listeners are resumed and suspended automatically - 1', async () => {
const message = {
date: new Date(),
message: 'test',
pid: 0,
tid: 1,
type: 'info',
tag: 'tag',
} as const;
const device = new TestDevice('serial', 'physical', 'test device', 'Android');
device.startLogging = jest.fn();
device.stopLogging = jest.fn();
const DevicePlugin = TestUtils.createTestDevicePlugin({
devicePlugin(client) {
const entries: any[] = [];
let disposer: any;
function start() {
disposer = client.onDeviceLogEntry((entry) => {
entries.push(entry);
});
}
function stop() {
disposer?.();
}
start();
return {start, stop, entries};
},
});
await createMockFlipperWithPlugin(DevicePlugin, {
device,
});
const instance = device.sandyPluginStates.get(DevicePlugin.id);
expect(instance).toBeDefined();
const entries = instance?.instanceApi.entries as any[];
// logging set up, messages arrive
expect(device.startLogging).toBeCalledTimes(1);
device.addLogEntry(message);
expect(entries.length).toBe(1);
// stop, messages don't arrive
instance?.instanceApi.stop();
expect(device.stopLogging).toBeCalledTimes(1);
device.addLogEntry(message);
expect(entries.length).toBe(1);
// resume, messsages arrive again
instance?.instanceApi.start();
expect(device.startLogging).toBeCalledTimes(2);
expect(device.stopLogging).toBeCalledTimes(1);
device.addLogEntry(message);
expect(entries.length).toBe(2);
// device disconnects, loggers are disposed
device.disconnect();
expect(device.stopLogging).toBeCalledTimes(2);
});
test('log listeners are resumed and suspended automatically - 2', async () => {
const message = {
date: new Date(),
message: 'test',
pid: 0,
tid: 1,
type: 'info',
tag: 'tag',
} as const;
const device = new TestDevice('serial', 'physical', 'test device', 'Android');
device.startLogging = jest.fn();
device.stopLogging = jest.fn();
const entries: any[] = [];
const DevicePlugin = TestUtils.createTestDevicePlugin({
devicePlugin(client) {
client.onDeviceLogEntry((entry) => {
entries.push(entry);
});
return {};
},
});
const Plugin = TestUtils.createTestPlugin(
{
plugin(client) {
client.onDeviceLogEntry((entry) => {
entries.push(entry);
});
return {};
},
},
{
id: 'AnotherPlugin',
},
);
const flipper = await createMockFlipperWithPlugin(DevicePlugin, {
device,
additionalPlugins: [Plugin],
});
const instance = device.sandyPluginStates.get(DevicePlugin.id);
expect(instance).toBeDefined();
// logging set up, messages arrives in both
expect(device.startLogging).toBeCalledTimes(1);
device.addLogEntry(message);
expect(entries.length).toBe(2);
// disable one plugin
flipper.togglePlugin(Plugin.id);
expect(device.stopLogging).toBeCalledTimes(0);
device.addLogEntry(message);
expect(entries.length).toBe(3);
// disable the other plugin
flipper.togglePlugin(DevicePlugin.id);
expect(device.stopLogging).toBeCalledTimes(1);
device.addLogEntry(message);
expect(entries.length).toBe(3);
// re-enable plugn
flipper.togglePlugin(Plugin.id);
expect(device.startLogging).toBeCalledTimes(2);
device.addLogEntry(message);
expect(entries.length).toBe(4);
});