Introduce Metro device and plugin

Summary: Introduced Metro device and the possibility to directly connect to running Metro instances

Reviewed By: jknoxville

Differential Revision: D19445623

fbshipit-source-id: 31978d966a56007c48f795076d6651e23de0e38d
This commit is contained in:
Michel Weststrate
2020-02-11 07:27:00 -08:00
committed by Facebook Github Bot
parent 014524ec26
commit 437ec11ca7
8 changed files with 382 additions and 1 deletions

View File

@@ -52,7 +52,7 @@ export type DeviceExport = {
logs: Array<DeviceLogEntry>;
};
export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS' | 'JSWebApp';
export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS' | 'JSWebApp' | 'Metro';
export default class BaseDevice {
constructor(serial: string, deviceType: DeviceType, title: string, os: OS) {

View File

@@ -0,0 +1,32 @@
/**
* 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 ArchivedDevice from './ArchivedDevice';
import {v4} from 'uuid';
export default class MetroDevice extends BaseDevice {
ws: WebSocket;
constructor(serial: string, ws: WebSocket) {
super(serial, 'emulator', 'React Native', 'Metro');
this.ws = ws;
this.devicePlugins = [];
}
archive() {
return new ArchivedDevice(
this.serial + v4(),
this.deviceType,
this.title,
this.os,
[...this.logEntries],
);
}
}

View File

@@ -8,6 +8,7 @@
*/
import androidDevice from './androidDevice';
import metroDevice from './metroDevice';
import iOSDevice from './iOSDevice';
import desktopDevice from './desktopDevice';
import application from './application';
@@ -28,6 +29,7 @@ export default function(store: Store, logger: Logger): () => Promise<void> {
application,
store.getState().settingsState.enableAndroid ? androidDevice : null,
iOSDevice,
metroDevice,
desktopDevice,
tracking,
server,

View File

@@ -0,0 +1,141 @@
/**
* 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 {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger';
import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
import MetroDevice from '../devices/MetroDevice';
import {ArchivedDevice} from 'flipper';
const METRO_PORT = 8081;
const METRO_HOST = 'localhost';
const METRO_URL = `http://${METRO_HOST}:${METRO_PORT}`;
const METRO_LOGS_ENDPOINT = `ws://${METRO_HOST}:${METRO_PORT}/events`;
const METRO_MESSAGE = ['React Native packager is running', 'Metro is running'];
const QUERY_INTERVAL = 5000;
const METRO_DEVICE_ID = 'metro'; // there is always only one activve
async function isMetroRunning(): Promise<boolean> {
try {
const contents = await (await global.fetch(METRO_URL)).text();
return METRO_MESSAGE.some(msg => contents.includes(msg));
} catch (e) {
return false;
}
}
async function registerDevice(ws: WebSocket, store: Store, logger: Logger) {
const metroDevice = new MetroDevice(METRO_DEVICE_ID, ws);
logger.track('usage', 'register-device', {
os: 'Metro',
name: metroDevice.title,
});
metroDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({
type: 'REGISTER_DEVICE',
payload: metroDevice,
serial: METRO_DEVICE_ID,
});
registerDeviceCallbackOnPlugins(
store,
store.getState().plugins.devicePlugins,
store.getState().plugins.clientPlugins,
metroDevice,
);
}
async function unregisterDevices(store: Store, logger: Logger) {
logger.track('usage', 'unregister-device', {
os: 'Metro',
serial: METRO_DEVICE_ID,
});
let archivedDevice: ArchivedDevice | undefined = undefined;
const device = store
.getState()
.connections.devices.find(device => device.serial === METRO_DEVICE_ID);
if (device && !device.isArchived) {
archivedDevice = device.archive();
}
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: new Set([METRO_DEVICE_ID]),
});
if (archivedDevice) {
archivedDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({
type: 'REGISTER_DEVICE',
payload: archivedDevice,
});
}
}
export default (store: Store, logger: Logger) => {
let timeoutHandle: NodeJS.Timeout;
let ws: WebSocket | undefined;
async function tryConnectToMetro() {
if (ws) {
return;
}
if (await isMetroRunning()) {
const _ws = new WebSocket(METRO_LOGS_ENDPOINT);
_ws.onopen = () => {
clearTimeout(guard);
ws = _ws;
registerDevice(ws, store, logger);
};
_ws.onclose = _ws.onerror = () => {
clearTimeout(guard);
ws = undefined;
unregisterDevices(store, logger);
scheduleNext();
};
const guard = setTimeout(() => {
// Metro is running, but didn't respond to /events endpoint
store.dispatch({
type: 'SERVER_ERROR',
payload: {
message:
"Found a running Metro instance, but couldn't connect to the logs. Probably your React Native version is too old to support Flipper.",
details: `Failed to get a connection to ${METRO_LOGS_ENDPOINT} in a timely fashion`,
urgent: true,
},
});
// Note: no scheduleNext, we won't retry until restart
}, 5000);
} else {
scheduleNext();
}
}
function scheduleNext() {
timeoutHandle = setTimeout(tryConnectToMetro, QUERY_INTERVAL);
}
tryConnectToMetro();
// cleanup method
return () => {
if (ws) {
ws.close();
}
if (timeoutHandle) {
clearInterval(timeoutHandle);
}
};
};

View File

@@ -50,6 +50,7 @@ export {createTablePlugin} from './createTablePlugin';
export {default as DetailSidebar} from './chrome/DetailSidebar';
export {default as Device} from './devices/BaseDevice';
export {default as AndroidDevice} from './devices/AndroidDevice';
export {default as MetroDevice} from './devices/MetroDevice';
export {default as ArchivedDevice} from './devices/ArchivedDevice';
export {default as IOSDevice} from './devices/IOSDevice';
export {default as KaiOSDevice} from './devices/KaiOSDevice';

171
src/plugins/metro/index.tsx Normal file
View File

@@ -0,0 +1,171 @@
/**
* 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 React from 'react';
import {
FlipperDevicePlugin,
Device,
View,
Button,
Toolbar,
ButtonGroup,
MetroDevice,
} from 'flipper';
type LogEntry = {};
export type PersistedState = {
logs: LogEntry[];
};
type State = {};
/*
Flow types for events
/
A tagged union of all the actions that may happen and we may want to
report to the tool user.
/
export type ReportableEvent =
| {
port: number,
projectRoots: $ReadOnlyArray<string>,
type: 'initialize_started',
...
}
| {type: 'initialize_done', ...}
| {
type: 'initialize_failed',
port: number,
error: Error,
...
}
| {
buildID: string,
type: 'bundle_build_done',
...
}
| {
buildID: string,
type: 'bundle_build_failed',
...
}
| {
buildID: string,
bundleDetails: BundleDetails,
type: 'bundle_build_started',
...
}
| {
error: Error,
type: 'bundling_error',
...
}
| {type: 'dep_graph_loading', ...}
| {type: 'dep_graph_loaded', ...}
| {
buildID: string,
type: 'bundle_transform_progressed',
transformedFileCount: number,
totalFileCount: number,
...
}
| {
type: 'global_cache_error',
error: Error,
...
}
| {
type: 'global_cache_disabled',
reason: GlobalCacheDisabledReason,
...
}
| {type: 'transform_cache_reset', ...}
| {
type: 'worker_stdout_chunk',
chunk: string,
...
}
| {
type: 'worker_stderr_chunk',
chunk: string,
...
}
| {
type: 'hmr_client_error',
error: Error,
...
}
| {
type: 'client_log',
level:
| 'trace'
| 'info'
| 'warn'
| 'error'
| 'log'
| 'group'
| 'groupCollapsed'
| 'groupEnd'
| 'debug',
data: Array<mixed>,
...
};
*/
export default class MetroPlugin extends FlipperDevicePlugin<
State,
any,
PersistedState
> {
static supportsDevice(device: Device) {
return device.os === 'Metro';
}
get ws(): WebSocket {
return (this.device as MetroDevice).ws;
}
sendCommand(command: string) {
if (this.ws) {
this.ws.send(
JSON.stringify({
version: 2,
type: 'command',
command,
}),
);
}
}
render() {
return (
<View>
<Toolbar>
<ButtonGroup>
Work-in-progress
<Button
onClick={() => {
this.sendCommand('reload');
}}>
Reload RN
</Button>
<Button
onClick={() => {
this.sendCommand('devMenu');
}}>
Dev Menu
</Button>
</ButtonGroup>
</Toolbar>
</View>
);
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "Metro",
"version": "0.1.0",
"description": "A plugin to manage React Native applications served trough Metro",
"main": "index.tsx",
"repository": "https://github.com/facebook/flipper",
"license": "MIT",
"keywords": [
"flipper-plugin",
"react-native",
"metro"
],
"title": "Metro Bundler",
"bugs": {
"email": "mweststrate@fb.com"
},
"dependencies": {}
}

View File

@@ -127,3 +127,19 @@ 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([]);
});