Move desktop-related code to "desktop" subfolder (#872)

Summary:
Pull Request resolved: https://github.com/facebook/flipper/pull/872
Move all the JS code related to desktop app to "desktop" subfolder.

The structure of "desktop" folder:
- `src` - JS code of Flipper desktop app executing in Electron Renderer (Chrome) process. This folder also contains all the Flipper plugins in subfolder "src/plugins".
- `static` - JS code of Flipper desktop app bootstrapping executing in Electron Main (Node.js) process
- `pkg` - Flipper packaging lib and CLI tool
- `doctor` - Flipper diagnostics lib and CLI tool
- `scripts` - Build scripts for Flipper desktop app
- `headless` - Headless version of Flipper app
- `headless-tests` - Integration tests running agains Flipper headless version

Reviewed By: passy

Differential Revision: D20249304

fbshipit-source-id: 9a51c63b51b92b758a02fc8ebf7d3d116770efe9
This commit is contained in:
Anton Nikolaev
2020-03-14 14:26:07 -07:00
committed by Facebook GitHub Bot
parent a60e6fee87
commit 85c13bb1f3
607 changed files with 103 additions and 142 deletions

14
desktop/.eslintignore Normal file
View File

@@ -0,0 +1,14 @@
*.d.ts
*.bundle.js
src/fb/plugins/relaydevtools/relay-devtools/*
latest
resources
templates
node_modules
flow-typed
lib
!.eslintrc.js
dist
website/build
react-native/ReactNativeFlipperExample
scripts/generate-changelog.js

75
desktop/.eslintrc.js Normal file
View File

@@ -0,0 +1,75 @@
/**
* 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
*/
const fbjs = require('eslint-config-fbjs');
// enforces copyright header and @format directive to be present in every file
const pattern = /^\*\r?\n[\S\s]*Facebook[\S\s]* \* @format\r?\n/;
const prettierConfig = {
requirePragma: true,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: false,
jsxBracketSameLine: true,
parser: 'flow',
};
module.exports = {
parser: 'babel-eslint',
root: true,
extends: 'fbjs',
plugins: [...fbjs.plugins, 'header', 'prettier', '@typescript-eslint'],
rules: {
// disable rules from eslint-config-fbjs
'react/react-in-jsx-scope': 0, // not needed with our metro implementation
'no-new': 0, // new keyword needed e.g. new Notification
'no-catch-shadow': 0, // only relevant for IE8 and below
'no-bitwise': 0, // bitwise operations needed in some places
'consistent-return': 0,
'no-var': 2,
'prefer-const': [2, {destructuring: 'all'}],
'prefer-spread': 1,
'prefer-rest-params': 1,
'max-len': 0, // lets prettier take care of this
indent: 0, // lets prettier take care of this
'no-console': 0, // we're setting window.console in App.js
'no-multi-spaces': 2,
'prefer-promise-reject-errors': 1,
'no-throw-literal': 'error',
'no-extra-boolean-cast': 2,
'no-extra-semi': 2,
'no-unsafe-negation': 2,
'no-useless-computed-key': 2,
'no-useless-rename': 2,
// additional rules for this project
'header/header': [2, 'block', {pattern}],
'prettier/prettier': [2, prettierConfig],
'flowtype/object-type-delimiter': [0],
},
overrides: [
{
files: ['*.tsx', '*.ts'],
parser: '@typescript-eslint/parser',
rules: {
'prettier/prettier': [2, {...prettierConfig, parser: 'typescript'}],
'@typescript-eslint/no-unused-vars': [
1,
{
ignoreRestSiblings: true,
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
],
};

6
desktop/.npmignore Normal file
View File

@@ -0,0 +1,6 @@
**
!src/**/*
__tests__
.DS_Store
src/plugins/**/*
src/fb/**/*

51
desktop/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,51 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Running Renderer",
"type": "chrome",
"request": "attach",
"port": 9222,
"webRoot": "/",
"url": "http://localhost:3000/index.dev.html"
},
{
"name": "Attach to Running Main",
"type": "node",
"request": "attach",
"port": 9229,
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Launch Current Jest Suite",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "${relativeFile}"]
},
{
"type": "node",
"request": "launch",
"name": "Launch Current Script",
"args": ["${file}"],
"env": {
"TS_NODE_FILES": "true"
},
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart",
"runtimeArgs": [
"--require",
"ts-node/register"
]
}
],
"compounds": [
{
"name": "Attach to All",
"configurations": [
"Attach to Running Main",
"Attach to Running Renderer"
]
}
]
}

1
desktop/.yarnrc Normal file
View File

@@ -0,0 +1 @@
--install.mutex network

View File

@@ -0,0 +1,25 @@
/**
* 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
*/
module.exports = {
remote: {
process: {
env: {},
},
app: {
getPath: (path: string) => `/${path}`,
getAppPath: process.cwd,
getVersion: () => '0.9.99',
relaunch: () => {},
exit: () => {},
},
getCurrentWindow: () => ({isFocused: () => true}),
},
ipcRenderer: {},
};

2
desktop/doctor/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/lib
node_modules/

1
desktop/doctor/.ignore Symbolic link
View File

@@ -0,0 +1 @@
.gitignore

8
desktop/doctor/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Flipper Doctor
This package exists for running checks to diagnose and potentially fix issues affecting the operation of Flipper.
It's designed to be primarily used programmatically but may also expose a CLI interface.
## Usage
`cd doctor`
`yarn run run`

View File

@@ -0,0 +1,7 @@
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}

View File

@@ -0,0 +1,48 @@
{
"name": "flipper-doctor",
"version": "0.7.0",
"description": "Utility for checking for issues with a flipper installation",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"license": "MIT",
"devDependencies": {
"@types/fb-watchman": "2.0.0",
"@types/jest": "^24.0.21",
"@typescript-eslint/eslint-plugin": "^2.8.0",
"eslint": "^6.6.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-flowtype": "^4.5.2",
"eslint-plugin-header": "^3.0.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.16.0",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"ts-jest": "^24.1.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "^3.7.2"
},
"scripts": {
"build": "tsc",
"prepare": "yarn run build",
"prepublishOnly": "yarn test && yarn run lint",
"preversion": "yarn run lint",
"test": "jest --config jestconfig.json --passWithNoTests",
"lint": "eslint -c ../.eslintrc.js src/**/* --ext .js,.ts && tsc --noemit",
"fix": "eslint -c ../.eslintrc.js src/**/* --fix --ext .js,.ts",
"run": "yarn run build && node lib/cli.js"
},
"files": [
"lib/**/*"
],
"keywords": [
"Flipper",
"Doctor"
],
"author": "Facebook, Inc",
"dependencies": {
"@types/node": "^13.7.7",
"envinfo": "^7.4.0",
"fb-watchman": "^2.0.1"
}
}

36
desktop/doctor/src/cli.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* 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 {getHealthchecks} from './index';
import {getEnvInfo} from './environmentInfo';
(async () => {
const environmentInfo = await getEnvInfo();
console.log(JSON.stringify(environmentInfo));
const healthchecks = getHealthchecks();
const results = await Promise.all(
Object.entries(healthchecks).map(async ([key, category]) => [
key,
category.isSkipped
? category
: {
label: category.label,
results: await Promise.all(
category.healthchecks.map(async ({key, label, run}) => ({
key,
label,
result: await run(environmentInfo),
})),
),
},
]),
);
console.log(JSON.stringify(results, null, 2));
})();

View File

@@ -0,0 +1,48 @@
/**
* 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 {run} from 'envinfo';
export type EnvironmentInfo = {
SDKs: {
'iOS SDK': {
Platforms: string[];
};
'Android SDK':
| {
'API Levels': string[] | 'Not Found';
'Build Tools': string[] | 'Not Found';
'System Images': string[] | 'Not Found';
'Android NDK': string | 'Not Found';
}
| 'Not Found';
};
IDEs: {
Xcode: {
version: string;
path: string;
};
};
};
async function retrieveAndParseEnvInfo(): Promise<any> {
return JSON.parse(
await run(
{
SDKs: ['iOS SDK'],
IDEs: ['Xcode'],
},
{json: true, showNotFound: true},
),
);
}
export async function getEnvInfo(): Promise<EnvironmentInfo> {
return await retrieveAndParseEnvInfo();
}

10
desktop/doctor/src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/**
* 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
*/
declare module 'envinfo';

294
desktop/doctor/src/index.ts Normal file
View File

@@ -0,0 +1,294 @@
/**
* 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 {exec} from 'child_process';
import {promisify} from 'util';
import {EnvironmentInfo, getEnvInfo} from './environmentInfo';
export {EnvironmentInfo, getEnvInfo} from './environmentInfo';
import * as watchman from 'fb-watchman';
import * as fs from 'fs';
import * as path from 'path';
export type HealthcheckCategory = {
label: string;
isSkipped: false;
isRequired: boolean;
healthchecks: Healthcheck[];
};
export type SkippedHealthcheckCategory = {
label: string;
isSkipped: true;
skipReason: string;
};
export type Healthchecks = {
common: HealthcheckCategory | SkippedHealthcheckCategory;
android: HealthcheckCategory | SkippedHealthcheckCategory;
ios: HealthcheckCategory | SkippedHealthcheckCategory;
};
export type Healthcheck = {
key: string;
label: string;
isRequired?: boolean;
run: (env: EnvironmentInfo) => Promise<HealthchecRunResult>;
};
export type HealthchecRunResult = {
hasProblem: boolean;
message: string;
};
export type CategoryResult = [
string,
{
label: string;
results: Array<{
key: string;
label: string;
isRequired: boolean;
result: {hasProblem: boolean};
}>;
},
];
export function getHealthchecks(): Healthchecks {
return {
common: {
label: 'Common',
isRequired: true,
isSkipped: false,
healthchecks: [
{
key: 'common.openssl',
label: 'OpenSSL Installed',
run: async (_: EnvironmentInfo) => {
const result = await tryExecuteCommand('openssl version');
const hasProblem = result.hasProblem;
const message = hasProblem
? `OpenSSL (https://wiki.openssl.org/index.php/Binaries) is not installed or not added to PATH. ${result.message}.`
: `OpenSSL (https://wiki.openssl.org/index.php/Binaries) is installed and added to PATH. ${result.message}.`;
return {
hasProblem,
message,
};
},
},
{
key: 'common.watchman',
label: 'Watchman Installed',
run: async (_: EnvironmentInfo) => {
const isAvailable = await isWatchmanAvailable();
return {
hasProblem: !isAvailable,
message: isAvailable
? 'Watchman file watching service (https://facebook.github.io/watchman/) is installed and added to PATH. Live reloading after changes during Flipper plugin development is enabled.'
: 'Watchman file watching service (https://facebook.github.io/watchman/) is not installed or not added to PATH. Live reloading after changes during Flipper plugin development is disabled.',
};
},
},
],
},
android: {
label: 'Android',
isRequired: false,
isSkipped: false,
healthchecks: [
{
key: 'android.sdk',
label: 'SDK Installed',
isRequired: true,
run: async (_: EnvironmentInfo) => {
if (process.env.ANDROID_HOME) {
const androidHome = process.env.ANDROID_HOME;
if (!fs.existsSync(androidHome)) {
return {
hasProblem: true,
message: `ANDROID_HOME points to a folder which does not exist: ${androidHome}.`,
};
}
const platformToolsDir = path.join(androidHome, 'platform-tools');
if (!fs.existsSync(path.join(androidHome, 'platform-tools'))) {
return {
hasProblem: true,
message: `Android SDK Platform Tools not found at the expected location "${platformToolsDir}". Probably they are not installed.`,
};
}
return await tryExecuteCommand(
path.join(platformToolsDir, 'adb') + ' version',
);
}
return await tryExecuteCommand('adb version');
},
},
],
},
ios: {
label: 'iOS',
...(process.platform === 'darwin'
? {
isRequired: false,
isSkipped: false,
healthchecks: [
{
key: 'ios.sdk',
label: 'SDK Installed',
isRequired: true,
run: async (e: EnvironmentInfo) => {
const hasProblem =
!e.SDKs['iOS SDK'] ||
!e.SDKs['iOS SDK'].Platforms ||
!e.SDKs['iOS SDK'].Platforms.length;
const message = hasProblem
? 'iOS SDK is not installed. You can install it using Xcode (https://developer.apple.com/xcode/).'
: `iOS SDK is installed for the following platforms: ${JSON.stringify(
e.SDKs['iOS SDK'].Platforms,
)}.`;
return {
hasProblem,
message,
};
},
},
{
key: 'ios.xcode',
label: 'XCode Installed',
isRequired: true,
run: async (e: EnvironmentInfo) => {
const hasProblem = e.IDEs == null || e.IDEs.Xcode == null;
const message = hasProblem
? 'Xcode (https://developer.apple.com/xcode/) is not installed.'
: `Xcode version ${e.IDEs.Xcode.version} is installed at "${e.IDEs.Xcode.path}".`;
return {
hasProblem,
message,
};
},
},
{
key: 'ios.xcode-select',
label: 'xcode-select set',
isRequired: true,
run: async (_: EnvironmentInfo) => {
const result = await tryExecuteCommand('xcode-select -p');
const hasProblem = result.hasProblem;
const message = hasProblem
? `Xcode version is not selected. You can select it using command "sudo xcode-select -switch <path/to/>Xcode.app". ${result.message}.`
: `Xcode version is selected. ${result.message}.`;
return {
hasProblem,
message,
};
},
},
{
key: 'ios.instruments',
label: 'Instruments exists',
isRequired: true,
run: async (_: EnvironmentInfo) => {
const result = await tryExecuteCommand('which instruments');
const hasProblem = result.hasProblem;
const message = hasProblem
? `Instruments not found. Please try to re-install Xcode (https://developer.apple.com/xcode/). ${result.message}.`
: `Instruments are installed. ${result.message}.`;
return {
hasProblem,
message,
};
},
},
],
}
: {
isSkipped: true,
skipReason: `Healthcheck is skipped, because iOS development is not supported on the current platform "${process.platform}".`,
}),
},
};
}
export async function runHealthchecks(): Promise<
Array<CategoryResult | SkippedHealthcheckCategory>
> {
const environmentInfo = await getEnvInfo();
const healthchecks: Healthchecks = getHealthchecks();
const results: Array<
CategoryResult | SkippedHealthcheckCategory
> = await Promise.all(
Object.entries(healthchecks).map(async ([key, category]) => {
if (category.isSkipped) {
return category;
}
const categoryResult: CategoryResult = [
key,
{
label: category.label,
results: await Promise.all(
category.healthchecks.map(
async ({key, label, run, isRequired}) => ({
key,
label,
isRequired: isRequired ?? true,
result: await run(environmentInfo).catch(e => {
console.error(e);
// TODO Improve result type to be: OK | Problem(message, fix...)
return {
hasProblem: true,
};
}),
}),
),
),
},
];
return categoryResult;
}),
);
return results;
}
async function tryExecuteCommand(
command: string,
): Promise<HealthchecRunResult> {
try {
const output = await promisify(exec)(command);
return {
hasProblem: false,
message: `Command "${command}" successfully executed with output: ${output.stdout}`,
};
} catch (err) {
return {
hasProblem: true,
message: `Command "${command}" failed to execute with output: ${err.message}`,
};
}
}
async function isWatchmanAvailable(): Promise<boolean> {
const client = new watchman.Client();
return new Promise(resolve => {
const complete = (result: boolean) => {
resolve(result);
client.removeAllListeners('error');
client.end();
};
client.once('error', () => complete(false));
client.capabilityCheck(
{optional: [], required: ['relative_root']},
error => {
if (error) {
complete(false);
return;
}
complete(true);
},
);
});
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"lib": ["es7", "dom", "es2017"],
"target": "es5",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"]
}

4335
desktop/doctor/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

10
desktop/flow-typed/TsStub.js vendored Normal file
View File

@@ -0,0 +1,10 @@
/**
* 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.
*/
declare module TsStub {
declare module.exports: any;
}

281
desktop/flow-typed/flipper.js vendored Normal file
View File

@@ -0,0 +1,281 @@
/**
* 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
*/
// This has been taken from the old plugin.js and manually stripped of
// implementation so only the types remain.
// It's not generated using flowgen because the typescript def is slightly weaker
// than the original flow one. (Persisted State generic param being used
// in reducers etc.
import type {KeyboardActions} from './MenuBar.js';
import type {App} from './App.js';
import type {Logger} from './fb-interfaces/Logger.js';
import type Client from './Client.js';
import type {Store, MiddlewareAPI} from './reducers/index.js';
import type {MetricType} from './utils/exportMetrics.tsx';
import type {Node} from 'react';
import type BaseDevice from './devices/BaseDevice.js';
import type AndroidDevice from './devices/AndroidDevice';
import type IOSDevice from './devices/IOSDevice';
declare module 'flipper' {
// This function is intended to be called from outside of the plugin.
// If you want to `call` from the plugin use, this.client.call
declare function callClient(
client: Client,
id: string,
): (string, ?Object) => Promise<Object>;
declare interface PluginClient {
// eslint-disable-next-line
send(method: string, params?: Object): void;
// eslint-disable-next-line
call(method: string, params?: Object): Promise<any>;
// eslint-disable-next-line
subscribe(method: string, callback: (params: any) => void): void;
// eslint-disable-next-line
supportsMethod(method: string): Promise<boolean>;
}
declare type PluginTarget = BaseDevice | Client;
declare type Notification = {|
id: string,
title: string,
message: string | Node,
severity: 'warning' | 'error',
timestamp?: number,
category?: string,
action?: string,
|};
declare type Props<T> = {
logger: Logger,
persistedState: T,
setPersistedState: (state: $Shape<T>) => void,
target: PluginTarget,
deepLinkPayload: ?string,
selectPlugin: (pluginID: string, deepLinkPayload: ?string) => boolean,
isArchivedDevice: boolean,
selectedApp: ?string,
};
declare class FlipperBasePlugin<
State = *,
Actions = *,
PersistedState = *,
> extends React.Component<Props<PersistedState>, State> {
static title: ?string;
static id: string;
static icon: ?string;
static gatekeeper: ?string;
static entry: ?string;
static bugs: ?{
email?: string,
url?: string,
};
static keyboardActions: ?KeyboardActions;
static screenshot: ?string;
static defaultPersistedState: PersistedState;
static persistedStateReducer: ?(
persistedState: PersistedState,
method: string,
data: Object,
) => $Shape<PersistedState>;
static metricsReducer: ?(
persistedState: PersistedState,
) => Promise<MetricType>;
static exportPersistedState: ?(
callClient: (string, ?Object) => Promise<Object>,
persistedState: ?PersistedState,
store: ?MiddlewareAPI,
) => Promise<?PersistedState>;
static serializePersistedState: (
persistedState: PersistedState,
statusUpdate?: (msg: string) => void,
idler?: Idler,
) => Promise<string>;
static deserializePersistedState: (
serializedString: string,
) => PersistedState;
static getActiveNotifications: ?(
persistedState: PersistedState,
) => Array<Notification>;
static onRegisterDevice: ?(
store: Store,
baseDevice: BaseDevice,
setPersistedState: (
pluginKey: string,
newPluginState: ?PersistedState,
) => void,
) => void;
// forbid instance properties that should be static
title: empty;
id: empty;
persist: empty;
icon: empty;
keyboardActions: empty;
screenshot: empty;
reducers: {
[actionName: string]: (state: State, actionData: Object) => $Shape<State>,
};
app: App;
onKeyboardAction: ?(action: string) => void;
toJSON(): string;
init(): void;
teardown(): void;
computeNotifications(props: Props<*>, state: State): Array<Notification>;
// methods to be overridden by subclasses
_init(): void;
_teardown(): void;
dispatchAction(actionData: Actions): void;
}
declare class FlipperDevicePlugin<
S = *,
A = *,
P = *,
> extends FlipperBasePlugin<S, A, P> {
device: BaseDevice;
constructor(props: Props<P>): void;
_init(): void;
_teardown(): void;
static supportsDevice(device: BaseDevice): boolean;
}
declare class FlipperPlugin<S = *, A = *, P = *> extends FlipperBasePlugin<
S,
A,
P,
> {
constructor(props: Props<P>): void;
subscriptions: Array<{
method: string,
callback: Function,
}>;
client: PluginClient;
realClient: Client;
getDevice(): Promise<BaseDevice>;
getAndroidDevice(): AndroidDevice;
getIOSDevice(): IOSDevice;
_teardown(): void;
_init(): void;
}
declare var AndroidDevice: any;
declare var BaseDevice: any;
declare var Block: any;
declare var Box: any;
declare var Button: any;
declare var ButtonGroup: any;
declare var Checkbox: any;
declare var CodeBlock: any;
declare var Component: any;
declare var ContextMenu: any;
declare var DataDescription: any;
declare var DataInspector: any;
declare var DetailSidebar: any;
declare var Device: any;
declare var DeviceLogEntry: any;
declare var Element: any;
declare var ElementID: any;
declare var ElementSearchResultSet: any;
declare var ElementsInspector: any;
declare var ErrorBlock: any;
declare var ErrorBlockContainer: any;
declare var Filter: any;
declare var FlexBox: any;
declare var FlexCenter: any;
declare var FlexColumn: any;
declare var FlexRow: any;
declare var FlipperBasePlugin: any;
declare var FlipperPlugin: any;
declare var GK: any;
declare var Glyph: any;
declare var Heading: any;
declare var HorizontalRule: any;
declare var Input: any;
declare var Label: any;
declare var Link: any;
declare var LoadingIndicator: any;
declare var LogLevel: any;
declare var ManagedDataInspector: any;
declare var ManagedTable: any;
declare var ManagedTable_immutable: any;
declare var MarkerTimeline: any;
declare var MetricType: any;
declare var MiddlewareAPI: any;
declare var OS: any;
declare var Panel: any;
declare var PureComponent: any;
declare var SearchBox: any;
declare var SearchIcon: any;
declare var SearchInput: any;
declare var Searchable: any;
declare var SearchableProps: any;
declare var SearchableTable: any;
declare var SearchableTable_immutable: any;
declare var Select: any;
declare var Sheet: any;
declare var Sidebar: any;
declare var SidebarExtensions: any;
declare var Spacer: any;
declare var StackTrace: any;
declare var StatusIndicator: any;
declare var Store: any;
declare var Tab: any;
declare var TableBodyRow: any;
declare var TableColumnOrder: any;
declare var TableColumnSizes: any;
declare var TableColumns: any;
declare var TableHighlightedRows: any;
declare var TableRowSortOrder: any;
declare var TableRows: any;
declare var TableRows_immutable: any;
declare var Tabs: any;
declare var Text: any;
declare var Textarea: any;
declare var ToggleButton: any;
declare var Toolbar: any;
declare var Tooltip: any;
declare var Value: any;
declare var VerticalRule: any;
declare var View: any;
declare var bufferToBlob: any;
declare var clipboard: any;
declare var colors: any;
declare var constants: any;
declare var createPaste: any;
declare var createTablePlugin: any;
declare var getPersistedState: any;
declare var getPluginKey: any;
declare var getStringFromErrorLike: any;
declare var graphQLQuery: any;
declare var isProduction: any;
declare var keyframes: any;
declare var renderValue: any;
declare var shouldParseAndroidLog: any;
declare var styled: any;
declare var textContent: any;
}

View File

@@ -0,0 +1,507 @@
// flow-typed signature: 7849d9ce4b390afcfc917a08445a20eb
// flow-typed version: <<STUB>>/adbkit-fb_v2.10.1/flow_v0.59.0
/**
* This is an autogenerated libdef stub for:
*
* 'adbkit-fb'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'adbkit-fb' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'adbkit-fb/lib/adb' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/auth' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/client' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/forward' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/getdevicepath' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/getserialno' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/getstate' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/listforwards' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/waitfordevice' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/clear' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/framebuffer' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/getfeatures' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/getpackages' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/getproperties' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/install' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/isinstalled' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/listreverses' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/local' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/log' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/logcat' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/monkey' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/reboot' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/remount' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/reverse' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/root' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/screencap' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/shell' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/startactivity' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/startservice' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/sync' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/tcp' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/tcpip' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/trackjdwp' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/uninstall' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/usb' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/waitbootcomplete' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/connect' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/devices' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/deviceswithpaths' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/disconnect' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/kill' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/trackdevices' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/transport' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/command/host/version' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/connection' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/dump' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/framebuffer/rgbtransform' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/keycode' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/linetransform' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/parser' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/proc/stat' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/protocol' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/sync' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/sync/entry' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/sync/pulltransfer' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/sync/pushtransfer' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/sync/stats' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/packet' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/packetreader' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/rollingcounter' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/server' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/service' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/servicemap' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tcpusb/socket' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/tracker' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/adb/util' {
declare module.exports: any;
}
declare module 'adbkit-fb/lib/cli' {
declare module.exports: any;
}
// Filename aliases
declare module 'adbkit-fb/index' {
declare module.exports: $Exports<'adbkit-fb'>;
}
declare module 'adbkit-fb/index.js' {
declare module.exports: $Exports<'adbkit-fb'>;
}
declare module 'adbkit-fb/lib/adb.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb'>;
}
declare module 'adbkit-fb/lib/adb/auth.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/auth'>;
}
declare module 'adbkit-fb/lib/adb/client.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/client'>;
}
declare module 'adbkit-fb/lib/adb/command.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command'>;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/forward.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-serial/forward'>;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/getdevicepath.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-serial/getdevicepath'>;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/getserialno.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-serial/getserialno'>;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/getstate.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-serial/getstate'>;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/listforwards.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-serial/listforwards'>;
}
declare module 'adbkit-fb/lib/adb/command/host-serial/waitfordevice.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-serial/waitfordevice'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/clear.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/clear'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/framebuffer.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/framebuffer'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/getfeatures.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/getfeatures'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/getpackages.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/getpackages'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/getproperties.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/getproperties'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/install.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/install'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/isinstalled.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/isinstalled'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/listreverses.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/listreverses'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/local.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/local'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/log.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/log'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/logcat.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/logcat'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/monkey.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/monkey'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/reboot.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/reboot'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/remount.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/remount'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/reverse.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/reverse'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/root.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/root'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/screencap.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/screencap'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/shell.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/shell'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/startactivity.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/startactivity'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/startservice.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/startservice'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/sync.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/sync'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/tcp.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/tcp'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/tcpip.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/tcpip'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/trackjdwp.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/trackjdwp'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/uninstall.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/uninstall'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/usb.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/usb'>;
}
declare module 'adbkit-fb/lib/adb/command/host-transport/waitbootcomplete.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host-transport/waitbootcomplete'>;
}
declare module 'adbkit-fb/lib/adb/command/host/connect.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/connect'>;
}
declare module 'adbkit-fb/lib/adb/command/host/devices.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/devices'>;
}
declare module 'adbkit-fb/lib/adb/command/host/deviceswithpaths.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/deviceswithpaths'>;
}
declare module 'adbkit-fb/lib/adb/command/host/disconnect.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/disconnect'>;
}
declare module 'adbkit-fb/lib/adb/command/host/kill.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/kill'>;
}
declare module 'adbkit-fb/lib/adb/command/host/trackdevices.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/trackdevices'>;
}
declare module 'adbkit-fb/lib/adb/command/host/transport.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/transport'>;
}
declare module 'adbkit-fb/lib/adb/command/host/version.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/command/host/version'>;
}
declare module 'adbkit-fb/lib/adb/connection.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/connection'>;
}
declare module 'adbkit-fb/lib/adb/dump.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/dump'>;
}
declare module 'adbkit-fb/lib/adb/framebuffer/rgbtransform.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/framebuffer/rgbtransform'>;
}
declare module 'adbkit-fb/lib/adb/keycode.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/keycode'>;
}
declare module 'adbkit-fb/lib/adb/linetransform.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/linetransform'>;
}
declare module 'adbkit-fb/lib/adb/parser.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/parser'>;
}
declare module 'adbkit-fb/lib/adb/proc/stat.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/proc/stat'>;
}
declare module 'adbkit-fb/lib/adb/protocol.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/protocol'>;
}
declare module 'adbkit-fb/lib/adb/sync.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/sync'>;
}
declare module 'adbkit-fb/lib/adb/sync/entry.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/sync/entry'>;
}
declare module 'adbkit-fb/lib/adb/sync/pulltransfer.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/sync/pulltransfer'>;
}
declare module 'adbkit-fb/lib/adb/sync/pushtransfer.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/sync/pushtransfer'>;
}
declare module 'adbkit-fb/lib/adb/sync/stats.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/sync/stats'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/packet.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/packet'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/packetreader.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/packetreader'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/rollingcounter.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/rollingcounter'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/server.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/server'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/service.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/service'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/servicemap.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/servicemap'>;
}
declare module 'adbkit-fb/lib/adb/tcpusb/socket.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tcpusb/socket'>;
}
declare module 'adbkit-fb/lib/adb/tracker.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/tracker'>;
}
declare module 'adbkit-fb/lib/adb/util.js' {
declare module.exports: $Exports<'adbkit-fb/lib/adb/util'>;
}
declare module 'adbkit-fb/lib/cli.js' {
declare module.exports: $Exports<'adbkit-fb/lib/cli'>;
}

View File

@@ -0,0 +1,32 @@
// flow-typed signature: 3d651fc79523c92efe95077d29cac82f
// flow-typed version: <<STUB>>/dateformat_v3.0.3/flow_v0.102.0
/**
* This is an autogenerated libdef stub for:
*
* 'dateformat'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'dateformat' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'dateformat/lib/dateformat' {
declare module.exports: any;
}
// Filename aliases
declare module 'dateformat/lib/dateformat.js' {
declare module.exports: $Exports<'dateformat/lib/dateformat'>;
}

4282
desktop/flow-typed/npm/electron-v5.0.2.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
* @flow
*
*/
declare module 'electron' {
declare module.exports: any;
}

View File

@@ -0,0 +1,18 @@
// flow-typed signature: 6187fdc45c9567c2e88b8d4399bc1300
// flow-typed version: <<STUB>>/google-palette_vlatest/flow_v0.69.0
/**
* This is an autogenerated libdef stub for:
*
* 'google-palette'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'google-palette' {
declare module.exports: any;
}

633
desktop/flow-typed/npm/graphql_vx.x.x.js vendored Normal file
View File

@@ -0,0 +1,633 @@
// flow-typed signature: 78a3b0630930ae254dcef08544af115d
// flow-typed version: <<STUB>>/graphql_v^0.11.7/flow_v0.59.0
/**
* This is an autogenerated libdef stub for:
*
* 'graphql'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'graphql' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'graphql/error/formatError' {
declare module.exports: any;
}
declare module 'graphql/error/GraphQLError' {
declare module.exports: any;
}
declare module 'graphql/error/index' {
declare module.exports: any;
}
declare module 'graphql/error/locatedError' {
declare module.exports: any;
}
declare module 'graphql/error/syntaxError' {
declare module.exports: any;
}
declare module 'graphql/execution/execute' {
declare module.exports: any;
}
declare module 'graphql/execution/index' {
declare module.exports: any;
}
declare module 'graphql/execution/values' {
declare module.exports: any;
}
declare module 'graphql/graphql' {
declare module.exports: any;
}
declare module 'graphql/jsutils/dedent' {
declare module.exports: any;
}
declare module 'graphql/jsutils/find' {
declare module.exports: any;
}
declare module 'graphql/jsutils/invariant' {
declare module.exports: any;
}
declare module 'graphql/jsutils/isInvalid' {
declare module.exports: any;
}
declare module 'graphql/jsutils/isNullish' {
declare module.exports: any;
}
declare module 'graphql/jsutils/keyMap' {
declare module.exports: any;
}
declare module 'graphql/jsutils/keyValMap' {
declare module.exports: any;
}
declare module 'graphql/jsutils/ObjMap' {
declare module.exports: any;
}
declare module 'graphql/jsutils/quotedOrList' {
declare module.exports: any;
}
declare module 'graphql/jsutils/suggestionList' {
declare module.exports: any;
}
declare module 'graphql/language/ast' {
declare module.exports: any;
}
declare module 'graphql/language/index' {
declare module.exports: any;
}
declare module 'graphql/language/kinds' {
declare module.exports: any;
}
declare module 'graphql/language/lexer' {
declare module.exports: any;
}
declare module 'graphql/language/location' {
declare module.exports: any;
}
declare module 'graphql/language/parser' {
declare module.exports: any;
}
declare module 'graphql/language/printer' {
declare module.exports: any;
}
declare module 'graphql/language/source' {
declare module.exports: any;
}
declare module 'graphql/language/visitor' {
declare module.exports: any;
}
declare module 'graphql/subscription/asyncIteratorReject' {
declare module.exports: any;
}
declare module 'graphql/subscription/index' {
declare module.exports: any;
}
declare module 'graphql/subscription/mapAsyncIterator' {
declare module.exports: any;
}
declare module 'graphql/subscription/subscribe' {
declare module.exports: any;
}
declare module 'graphql/type/definition' {
declare module.exports: any;
}
declare module 'graphql/type/directives' {
declare module.exports: any;
}
declare module 'graphql/type/index' {
declare module.exports: any;
}
declare module 'graphql/type/introspection' {
declare module.exports: any;
}
declare module 'graphql/type/scalars' {
declare module.exports: any;
}
declare module 'graphql/type/schema' {
declare module.exports: any;
}
declare module 'graphql/utilities/assertValidName' {
declare module.exports: any;
}
declare module 'graphql/utilities/astFromValue' {
declare module.exports: any;
}
declare module 'graphql/utilities/buildASTSchema' {
declare module.exports: any;
}
declare module 'graphql/utilities/buildClientSchema' {
declare module.exports: any;
}
declare module 'graphql/utilities/concatAST' {
declare module.exports: any;
}
declare module 'graphql/utilities/extendSchema' {
declare module.exports: any;
}
declare module 'graphql/utilities/findBreakingChanges' {
declare module.exports: any;
}
declare module 'graphql/utilities/findDeprecatedUsages' {
declare module.exports: any;
}
declare module 'graphql/utilities/getOperationAST' {
declare module.exports: any;
}
declare module 'graphql/utilities/index' {
declare module.exports: any;
}
declare module 'graphql/utilities/introspectionQuery' {
declare module.exports: any;
}
declare module 'graphql/utilities/isValidJSValue' {
declare module.exports: any;
}
declare module 'graphql/utilities/isValidLiteralValue' {
declare module.exports: any;
}
declare module 'graphql/utilities/schemaPrinter' {
declare module.exports: any;
}
declare module 'graphql/utilities/separateOperations' {
declare module.exports: any;
}
declare module 'graphql/utilities/typeComparators' {
declare module.exports: any;
}
declare module 'graphql/utilities/typeFromAST' {
declare module.exports: any;
}
declare module 'graphql/utilities/TypeInfo' {
declare module.exports: any;
}
declare module 'graphql/utilities/valueFromAST' {
declare module.exports: any;
}
declare module 'graphql/validation/index' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/ArgumentsOfCorrectType' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/DefaultValuesOfCorrectType' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/FieldsOnCorrectType' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/FragmentsOnCompositeTypes' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/KnownArgumentNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/KnownDirectives' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/KnownFragmentNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/KnownTypeNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/LoneAnonymousOperation' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/NoFragmentCycles' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/NoUndefinedVariables' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/NoUnusedFragments' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/NoUnusedVariables' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/OverlappingFieldsCanBeMerged' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/PossibleFragmentSpreads' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/ProvidedNonNullArguments' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/ScalarLeafs' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/SingleFieldSubscriptions' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/UniqueArgumentNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/UniqueDirectivesPerLocation' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/UniqueFragmentNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/UniqueInputFieldNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/UniqueOperationNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/UniqueVariableNames' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/VariablesAreInputTypes' {
declare module.exports: any;
}
declare module 'graphql/validation/rules/VariablesInAllowedPosition' {
declare module.exports: any;
}
declare module 'graphql/validation/specifiedRules' {
declare module.exports: any;
}
declare module 'graphql/validation/validate' {
declare module.exports: any;
}
// Filename aliases
declare module 'graphql/error/formatError.js' {
declare module.exports: $Exports<'graphql/error/formatError'>;
}
declare module 'graphql/error/GraphQLError.js' {
declare module.exports: $Exports<'graphql/error/GraphQLError'>;
}
declare module 'graphql/error/index.js' {
declare module.exports: $Exports<'graphql/error/index'>;
}
declare module 'graphql/error/locatedError.js' {
declare module.exports: $Exports<'graphql/error/locatedError'>;
}
declare module 'graphql/error/syntaxError.js' {
declare module.exports: $Exports<'graphql/error/syntaxError'>;
}
declare module 'graphql/execution/execute.js' {
declare module.exports: $Exports<'graphql/execution/execute'>;
}
declare module 'graphql/execution/index.js' {
declare module.exports: $Exports<'graphql/execution/index'>;
}
declare module 'graphql/execution/values.js' {
declare module.exports: $Exports<'graphql/execution/values'>;
}
declare module 'graphql/graphql.js' {
declare module.exports: $Exports<'graphql/graphql'>;
}
declare module 'graphql/index' {
declare module.exports: $Exports<'graphql'>;
}
declare module 'graphql/index.js' {
declare module.exports: $Exports<'graphql'>;
}
declare module 'graphql/jsutils/dedent.js' {
declare module.exports: $Exports<'graphql/jsutils/dedent'>;
}
declare module 'graphql/jsutils/find.js' {
declare module.exports: $Exports<'graphql/jsutils/find'>;
}
declare module 'graphql/jsutils/invariant.js' {
declare module.exports: $Exports<'graphql/jsutils/invariant'>;
}
declare module 'graphql/jsutils/isInvalid.js' {
declare module.exports: $Exports<'graphql/jsutils/isInvalid'>;
}
declare module 'graphql/jsutils/isNullish.js' {
declare module.exports: $Exports<'graphql/jsutils/isNullish'>;
}
declare module 'graphql/jsutils/keyMap.js' {
declare module.exports: $Exports<'graphql/jsutils/keyMap'>;
}
declare module 'graphql/jsutils/keyValMap.js' {
declare module.exports: $Exports<'graphql/jsutils/keyValMap'>;
}
declare module 'graphql/jsutils/ObjMap.js' {
declare module.exports: $Exports<'graphql/jsutils/ObjMap'>;
}
declare module 'graphql/jsutils/quotedOrList.js' {
declare module.exports: $Exports<'graphql/jsutils/quotedOrList'>;
}
declare module 'graphql/jsutils/suggestionList.js' {
declare module.exports: $Exports<'graphql/jsutils/suggestionList'>;
}
declare module 'graphql/language/ast.js' {
declare module.exports: $Exports<'graphql/language/ast'>;
}
declare module 'graphql/language/index.js' {
declare module.exports: $Exports<'graphql/language/index'>;
}
declare module 'graphql/language/kinds.js' {
declare module.exports: $Exports<'graphql/language/kinds'>;
}
declare module 'graphql/language/lexer.js' {
declare module.exports: $Exports<'graphql/language/lexer'>;
}
declare module 'graphql/language/location.js' {
declare module.exports: $Exports<'graphql/language/location'>;
}
declare module 'graphql/language/parser.js' {
declare module.exports: $Exports<'graphql/language/parser'>;
}
declare module 'graphql/language/printer.js' {
declare module.exports: $Exports<'graphql/language/printer'>;
}
declare module 'graphql/language/source.js' {
declare module.exports: $Exports<'graphql/language/source'>;
}
declare module 'graphql/language/visitor.js' {
declare module.exports: $Exports<'graphql/language/visitor'>;
}
declare module 'graphql/subscription/asyncIteratorReject.js' {
declare module.exports: $Exports<'graphql/subscription/asyncIteratorReject'>;
}
declare module 'graphql/subscription/index.js' {
declare module.exports: $Exports<'graphql/subscription/index'>;
}
declare module 'graphql/subscription/mapAsyncIterator.js' {
declare module.exports: $Exports<'graphql/subscription/mapAsyncIterator'>;
}
declare module 'graphql/subscription/subscribe.js' {
declare module.exports: $Exports<'graphql/subscription/subscribe'>;
}
declare module 'graphql/type/definition.js' {
declare module.exports: $Exports<'graphql/type/definition'>;
}
declare module 'graphql/type/directives.js' {
declare module.exports: $Exports<'graphql/type/directives'>;
}
declare module 'graphql/type/index.js' {
declare module.exports: $Exports<'graphql/type/index'>;
}
declare module 'graphql/type/introspection.js' {
declare module.exports: $Exports<'graphql/type/introspection'>;
}
declare module 'graphql/type/scalars.js' {
declare module.exports: $Exports<'graphql/type/scalars'>;
}
declare module 'graphql/type/schema.js' {
declare module.exports: $Exports<'graphql/type/schema'>;
}
declare module 'graphql/utilities/assertValidName.js' {
declare module.exports: $Exports<'graphql/utilities/assertValidName'>;
}
declare module 'graphql/utilities/astFromValue.js' {
declare module.exports: $Exports<'graphql/utilities/astFromValue'>;
}
declare module 'graphql/utilities/buildASTSchema.js' {
declare module.exports: $Exports<'graphql/utilities/buildASTSchema'>;
}
declare module 'graphql/utilities/buildClientSchema.js' {
declare module.exports: $Exports<'graphql/utilities/buildClientSchema'>;
}
declare module 'graphql/utilities/concatAST.js' {
declare module.exports: $Exports<'graphql/utilities/concatAST'>;
}
declare module 'graphql/utilities/extendSchema.js' {
declare module.exports: $Exports<'graphql/utilities/extendSchema'>;
}
declare module 'graphql/utilities/findBreakingChanges.js' {
declare module.exports: $Exports<'graphql/utilities/findBreakingChanges'>;
}
declare module 'graphql/utilities/findDeprecatedUsages.js' {
declare module.exports: $Exports<'graphql/utilities/findDeprecatedUsages'>;
}
declare module 'graphql/utilities/getOperationAST.js' {
declare module.exports: $Exports<'graphql/utilities/getOperationAST'>;
}
declare module 'graphql/utilities/index.js' {
declare module.exports: $Exports<'graphql/utilities/index'>;
}
declare module 'graphql/utilities/introspectionQuery.js' {
declare module.exports: $Exports<'graphql/utilities/introspectionQuery'>;
}
declare module 'graphql/utilities/isValidJSValue.js' {
declare module.exports: $Exports<'graphql/utilities/isValidJSValue'>;
}
declare module 'graphql/utilities/isValidLiteralValue.js' {
declare module.exports: $Exports<'graphql/utilities/isValidLiteralValue'>;
}
declare module 'graphql/utilities/schemaPrinter.js' {
declare module.exports: $Exports<'graphql/utilities/schemaPrinter'>;
}
declare module 'graphql/utilities/separateOperations.js' {
declare module.exports: $Exports<'graphql/utilities/separateOperations'>;
}
declare module 'graphql/utilities/typeComparators.js' {
declare module.exports: $Exports<'graphql/utilities/typeComparators'>;
}
declare module 'graphql/utilities/typeFromAST.js' {
declare module.exports: $Exports<'graphql/utilities/typeFromAST'>;
}
declare module 'graphql/utilities/TypeInfo.js' {
declare module.exports: $Exports<'graphql/utilities/TypeInfo'>;
}
declare module 'graphql/utilities/valueFromAST.js' {
declare module.exports: $Exports<'graphql/utilities/valueFromAST'>;
}
declare module 'graphql/validation/index.js' {
declare module.exports: $Exports<'graphql/validation/index'>;
}
declare module 'graphql/validation/rules/ArgumentsOfCorrectType.js' {
declare module.exports: $Exports<'graphql/validation/rules/ArgumentsOfCorrectType'>;
}
declare module 'graphql/validation/rules/DefaultValuesOfCorrectType.js' {
declare module.exports: $Exports<'graphql/validation/rules/DefaultValuesOfCorrectType'>;
}
declare module 'graphql/validation/rules/FieldsOnCorrectType.js' {
declare module.exports: $Exports<'graphql/validation/rules/FieldsOnCorrectType'>;
}
declare module 'graphql/validation/rules/FragmentsOnCompositeTypes.js' {
declare module.exports: $Exports<'graphql/validation/rules/FragmentsOnCompositeTypes'>;
}
declare module 'graphql/validation/rules/KnownArgumentNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/KnownArgumentNames'>;
}
declare module 'graphql/validation/rules/KnownDirectives.js' {
declare module.exports: $Exports<'graphql/validation/rules/KnownDirectives'>;
}
declare module 'graphql/validation/rules/KnownFragmentNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/KnownFragmentNames'>;
}
declare module 'graphql/validation/rules/KnownTypeNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/KnownTypeNames'>;
}
declare module 'graphql/validation/rules/LoneAnonymousOperation.js' {
declare module.exports: $Exports<'graphql/validation/rules/LoneAnonymousOperation'>;
}
declare module 'graphql/validation/rules/NoFragmentCycles.js' {
declare module.exports: $Exports<'graphql/validation/rules/NoFragmentCycles'>;
}
declare module 'graphql/validation/rules/NoUndefinedVariables.js' {
declare module.exports: $Exports<'graphql/validation/rules/NoUndefinedVariables'>;
}
declare module 'graphql/validation/rules/NoUnusedFragments.js' {
declare module.exports: $Exports<'graphql/validation/rules/NoUnusedFragments'>;
}
declare module 'graphql/validation/rules/NoUnusedVariables.js' {
declare module.exports: $Exports<'graphql/validation/rules/NoUnusedVariables'>;
}
declare module 'graphql/validation/rules/OverlappingFieldsCanBeMerged.js' {
declare module.exports: $Exports<'graphql/validation/rules/OverlappingFieldsCanBeMerged'>;
}
declare module 'graphql/validation/rules/PossibleFragmentSpreads.js' {
declare module.exports: $Exports<'graphql/validation/rules/PossibleFragmentSpreads'>;
}
declare module 'graphql/validation/rules/ProvidedNonNullArguments.js' {
declare module.exports: $Exports<'graphql/validation/rules/ProvidedNonNullArguments'>;
}
declare module 'graphql/validation/rules/ScalarLeafs.js' {
declare module.exports: $Exports<'graphql/validation/rules/ScalarLeafs'>;
}
declare module 'graphql/validation/rules/SingleFieldSubscriptions.js' {
declare module.exports: $Exports<'graphql/validation/rules/SingleFieldSubscriptions'>;
}
declare module 'graphql/validation/rules/UniqueArgumentNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/UniqueArgumentNames'>;
}
declare module 'graphql/validation/rules/UniqueDirectivesPerLocation.js' {
declare module.exports: $Exports<'graphql/validation/rules/UniqueDirectivesPerLocation'>;
}
declare module 'graphql/validation/rules/UniqueFragmentNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/UniqueFragmentNames'>;
}
declare module 'graphql/validation/rules/UniqueInputFieldNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/UniqueInputFieldNames'>;
}
declare module 'graphql/validation/rules/UniqueOperationNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/UniqueOperationNames'>;
}
declare module 'graphql/validation/rules/UniqueVariableNames.js' {
declare module.exports: $Exports<'graphql/validation/rules/UniqueVariableNames'>;
}
declare module 'graphql/validation/rules/VariablesAreInputTypes.js' {
declare module.exports: $Exports<'graphql/validation/rules/VariablesAreInputTypes'>;
}
declare module 'graphql/validation/rules/VariablesInAllowedPosition.js' {
declare module.exports: $Exports<'graphql/validation/rules/VariablesInAllowedPosition'>;
}
declare module 'graphql/validation/specifiedRules.js' {
declare module.exports: $Exports<'graphql/validation/specifiedRules'>;
}
declare module 'graphql/validation/validate.js' {
declare module.exports: $Exports<'graphql/validation/validate'>;
}

1248
desktop/flow-typed/npm/immutable_v4.x.x.js vendored Normal file

File diff suppressed because it is too large Load Diff

1185
desktop/flow-typed/npm/jest_v24.x.x.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
// flow-typed signature: 95241cb8305cb4cbda994a489dff963f
// flow-typed version: <<STUB>>/lodash.debounce_v4.0.8/flow_v0.76.0
/**
* This is an autogenerated libdef stub for:
*
* 'lodash.debounce'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'lodash.debounce' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
// Filename aliases
declare module 'lodash.debounce/index' {
declare module.exports: $Exports<'lodash.debounce'>;
}
declare module 'lodash.debounce/index.js' {
declare module.exports: $Exports<'lodash.debounce'>;
}

View File

@@ -0,0 +1,19 @@
// flow-typed signature: 0eceddcdb50aa9bf578c375a12ef9aad
// flow-typed version: <<STUB>>/lodash.memoize_v4.1.2/flow_v0.91.0
/**
* This is an autogenerated libdef stub for:
*
* 'lodash.memoize'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'lodash.memoize' {
declare function memoize<T>(fn: T, resolver?: Function): T;
declare export default typeof memoize;
}

6083
desktop/flow-typed/npm/lodash_v4.x.x.js vendored Normal file

File diff suppressed because it is too large Load Diff

291
desktop/flow-typed/npm/needle_vx.x.x.js vendored Normal file
View File

@@ -0,0 +1,291 @@
// flow-typed signature: 290caff3b97efa4471b18ff001038a13
// flow-typed version: <<STUB>>/needle_v2.2.1/flow_v0.69.0
/**
* This is an autogenerated libdef stub for:
*
* 'needle'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'needle' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'needle/examples/deflated-stream' {
declare module.exports: any;
}
declare module 'needle/examples/digest-auth' {
declare module.exports: any;
}
declare module 'needle/examples/download-to-file' {
declare module.exports: any;
}
declare module 'needle/examples/multipart-stream' {
declare module.exports: any;
}
declare module 'needle/examples/parsed-stream' {
declare module.exports: any;
}
declare module 'needle/examples/parsed-stream2' {
declare module.exports: any;
}
declare module 'needle/examples/stream-events' {
declare module.exports: any;
}
declare module 'needle/examples/stream-to-file' {
declare module.exports: any;
}
declare module 'needle/examples/upload-image' {
declare module.exports: any;
}
declare module 'needle/lib/auth' {
declare module.exports: any;
}
declare module 'needle/lib/cookies' {
declare module.exports: any;
}
declare module 'needle/lib/decoder' {
declare module.exports: any;
}
declare module 'needle/lib/multipart' {
declare module.exports: any;
}
declare module 'needle/lib/needle' {
declare module.exports: any;
}
declare module 'needle/lib/parsers' {
declare module.exports: any;
}
declare module 'needle/lib/querystring' {
declare module.exports: any;
}
declare module 'needle/test/basic_auth_spec' {
declare module.exports: any;
}
declare module 'needle/test/compression_spec' {
declare module.exports: any;
}
declare module 'needle/test/cookies_spec' {
declare module.exports: any;
}
declare module 'needle/test/decoder_spec' {
declare module.exports: any;
}
declare module 'needle/test/errors_spec' {
declare module.exports: any;
}
declare module 'needle/test/headers_spec' {
declare module.exports: any;
}
declare module 'needle/test/helpers' {
declare module.exports: any;
}
declare module 'needle/test/long_string_spec' {
declare module.exports: any;
}
declare module 'needle/test/output_spec' {
declare module.exports: any;
}
declare module 'needle/test/parsing_spec' {
declare module.exports: any;
}
declare module 'needle/test/post_data_spec' {
declare module.exports: any;
}
declare module 'needle/test/proxy_spec' {
declare module.exports: any;
}
declare module 'needle/test/querystring_spec' {
declare module.exports: any;
}
declare module 'needle/test/redirect_spec' {
declare module.exports: any;
}
declare module 'needle/test/redirect_with_timeout' {
declare module.exports: any;
}
declare module 'needle/test/request_stream_spec' {
declare module.exports: any;
}
declare module 'needle/test/response_stream_spec' {
declare module.exports: any;
}
declare module 'needle/test/socket_pool_spec' {
declare module.exports: any;
}
declare module 'needle/test/url_spec' {
declare module.exports: any;
}
declare module 'needle/test/utils/formidable' {
declare module.exports: any;
}
declare module 'needle/test/utils/proxy' {
declare module.exports: any;
}
declare module 'needle/test/utils/test' {
declare module.exports: any;
}
// Filename aliases
declare module 'needle/examples/deflated-stream.js' {
declare module.exports: $Exports<'needle/examples/deflated-stream'>;
}
declare module 'needle/examples/digest-auth.js' {
declare module.exports: $Exports<'needle/examples/digest-auth'>;
}
declare module 'needle/examples/download-to-file.js' {
declare module.exports: $Exports<'needle/examples/download-to-file'>;
}
declare module 'needle/examples/multipart-stream.js' {
declare module.exports: $Exports<'needle/examples/multipart-stream'>;
}
declare module 'needle/examples/parsed-stream.js' {
declare module.exports: $Exports<'needle/examples/parsed-stream'>;
}
declare module 'needle/examples/parsed-stream2.js' {
declare module.exports: $Exports<'needle/examples/parsed-stream2'>;
}
declare module 'needle/examples/stream-events.js' {
declare module.exports: $Exports<'needle/examples/stream-events'>;
}
declare module 'needle/examples/stream-to-file.js' {
declare module.exports: $Exports<'needle/examples/stream-to-file'>;
}
declare module 'needle/examples/upload-image.js' {
declare module.exports: $Exports<'needle/examples/upload-image'>;
}
declare module 'needle/lib/auth.js' {
declare module.exports: $Exports<'needle/lib/auth'>;
}
declare module 'needle/lib/cookies.js' {
declare module.exports: $Exports<'needle/lib/cookies'>;
}
declare module 'needle/lib/decoder.js' {
declare module.exports: $Exports<'needle/lib/decoder'>;
}
declare module 'needle/lib/multipart.js' {
declare module.exports: $Exports<'needle/lib/multipart'>;
}
declare module 'needle/lib/needle.js' {
declare module.exports: $Exports<'needle/lib/needle'>;
}
declare module 'needle/lib/parsers.js' {
declare module.exports: $Exports<'needle/lib/parsers'>;
}
declare module 'needle/lib/querystring.js' {
declare module.exports: $Exports<'needle/lib/querystring'>;
}
declare module 'needle/test/basic_auth_spec.js' {
declare module.exports: $Exports<'needle/test/basic_auth_spec'>;
}
declare module 'needle/test/compression_spec.js' {
declare module.exports: $Exports<'needle/test/compression_spec'>;
}
declare module 'needle/test/cookies_spec.js' {
declare module.exports: $Exports<'needle/test/cookies_spec'>;
}
declare module 'needle/test/decoder_spec.js' {
declare module.exports: $Exports<'needle/test/decoder_spec'>;
}
declare module 'needle/test/errors_spec.js' {
declare module.exports: $Exports<'needle/test/errors_spec'>;
}
declare module 'needle/test/headers_spec.js' {
declare module.exports: $Exports<'needle/test/headers_spec'>;
}
declare module 'needle/test/helpers.js' {
declare module.exports: $Exports<'needle/test/helpers'>;
}
declare module 'needle/test/long_string_spec.js' {
declare module.exports: $Exports<'needle/test/long_string_spec'>;
}
declare module 'needle/test/output_spec.js' {
declare module.exports: $Exports<'needle/test/output_spec'>;
}
declare module 'needle/test/parsing_spec.js' {
declare module.exports: $Exports<'needle/test/parsing_spec'>;
}
declare module 'needle/test/post_data_spec.js' {
declare module.exports: $Exports<'needle/test/post_data_spec'>;
}
declare module 'needle/test/proxy_spec.js' {
declare module.exports: $Exports<'needle/test/proxy_spec'>;
}
declare module 'needle/test/querystring_spec.js' {
declare module.exports: $Exports<'needle/test/querystring_spec'>;
}
declare module 'needle/test/redirect_spec.js' {
declare module.exports: $Exports<'needle/test/redirect_spec'>;
}
declare module 'needle/test/redirect_with_timeout.js' {
declare module.exports: $Exports<'needle/test/redirect_with_timeout'>;
}
declare module 'needle/test/request_stream_spec.js' {
declare module.exports: $Exports<'needle/test/request_stream_spec'>;
}
declare module 'needle/test/response_stream_spec.js' {
declare module.exports: $Exports<'needle/test/response_stream_spec'>;
}
declare module 'needle/test/socket_pool_spec.js' {
declare module.exports: $Exports<'needle/test/socket_pool_spec'>;
}
declare module 'needle/test/url_spec.js' {
declare module.exports: $Exports<'needle/test/url_spec'>;
}
declare module 'needle/test/utils/formidable.js' {
declare module.exports: $Exports<'needle/test/utils/formidable'>;
}
declare module 'needle/test/utils/proxy.js' {
declare module.exports: $Exports<'needle/test/utils/proxy'>;
}
declare module 'needle/test/utils/test.js' {
declare module.exports: $Exports<'needle/test/utils/test'>;
}

View File

@@ -0,0 +1,18 @@
// flow-typed signature: 033193531f49f251b11350a57a58eb12
// flow-typed version: <<STUB>>/react-chartjs-2_vlatest/flow_v0.69.0
/**
* This is an autogenerated libdef stub for:
*
* 'react-chartjs-2'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-chartjs-2' {
declare module.exports: any;
}

View File

@@ -0,0 +1,179 @@
// flow-typed signature: 32f203474854b8a1d378914b9461a972
// flow-typed version: <<STUB>>/react-d3-tree_v1.12.0/flow_v0.86.0
/**
* This is an autogenerated libdef stub for:
*
* 'react-d3-tree'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-d3-tree' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'react-d3-tree/jest/mocks/cssModule' {
declare module.exports: any;
}
declare module 'react-d3-tree/jest/mocks/image' {
declare module.exports: any;
}
declare module 'react-d3-tree/jest/polyfills/raf' {
declare module.exports: any;
}
declare module 'react-d3-tree/jest/setup' {
declare module.exports: any;
}
declare module 'react-d3-tree/lib/react-d3-tree.min' {
declare module.exports: any;
}
declare module 'react-d3-tree/scripts/generateMarkdown' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/index' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Link/index' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Link/tests/index.test' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Node/ForeignObjectElement' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Node/index' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Node/SvgTextElement' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Node/tests/ForeignObjectElement.test' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Node/tests/index.test' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Node/tests/SvgTextElement.test' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Tree/index' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Tree/NodeWrapper' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Tree/tests/index.test' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Tree/tests/mockData' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/Tree/tests/NodeWrapper.test' {
declare module.exports: any;
}
declare module 'react-d3-tree/src/util/index' {
declare module.exports: any;
}
declare module 'react-d3-tree/webpack.config' {
declare module.exports: any;
}
// Filename aliases
declare module 'react-d3-tree/jest/mocks/cssModule.js' {
declare module.exports: $Exports<'react-d3-tree/jest/mocks/cssModule'>;
}
declare module 'react-d3-tree/jest/mocks/image.js' {
declare module.exports: $Exports<'react-d3-tree/jest/mocks/image'>;
}
declare module 'react-d3-tree/jest/polyfills/raf.js' {
declare module.exports: $Exports<'react-d3-tree/jest/polyfills/raf'>;
}
declare module 'react-d3-tree/jest/setup.js' {
declare module.exports: $Exports<'react-d3-tree/jest/setup'>;
}
declare module 'react-d3-tree/lib/react-d3-tree.min.js' {
declare module.exports: $Exports<'react-d3-tree/lib/react-d3-tree.min'>;
}
declare module 'react-d3-tree/scripts/generateMarkdown.js' {
declare module.exports: $Exports<'react-d3-tree/scripts/generateMarkdown'>;
}
declare module 'react-d3-tree/src/index.js' {
declare module.exports: $Exports<'react-d3-tree/src/index'>;
}
declare module 'react-d3-tree/src/Link/index.js' {
declare module.exports: $Exports<'react-d3-tree/src/Link/index'>;
}
declare module 'react-d3-tree/src/Link/tests/index.test.js' {
declare module.exports: $Exports<'react-d3-tree/src/Link/tests/index.test'>;
}
declare module 'react-d3-tree/src/Node/ForeignObjectElement.js' {
declare module.exports: $Exports<'react-d3-tree/src/Node/ForeignObjectElement'>;
}
declare module 'react-d3-tree/src/Node/index.js' {
declare module.exports: $Exports<'react-d3-tree/src/Node/index'>;
}
declare module 'react-d3-tree/src/Node/SvgTextElement.js' {
declare module.exports: $Exports<'react-d3-tree/src/Node/SvgTextElement'>;
}
declare module 'react-d3-tree/src/Node/tests/ForeignObjectElement.test.js' {
declare module.exports: $Exports<'react-d3-tree/src/Node/tests/ForeignObjectElement.test'>;
}
declare module 'react-d3-tree/src/Node/tests/index.test.js' {
declare module.exports: $Exports<'react-d3-tree/src/Node/tests/index.test'>;
}
declare module 'react-d3-tree/src/Node/tests/SvgTextElement.test.js' {
declare module.exports: $Exports<'react-d3-tree/src/Node/tests/SvgTextElement.test'>;
}
declare module 'react-d3-tree/src/Tree/index.js' {
declare module.exports: $Exports<'react-d3-tree/src/Tree/index'>;
}
declare module 'react-d3-tree/src/Tree/NodeWrapper.js' {
declare module.exports: $Exports<'react-d3-tree/src/Tree/NodeWrapper'>;
}
declare module 'react-d3-tree/src/Tree/tests/index.test.js' {
declare module.exports: $Exports<'react-d3-tree/src/Tree/tests/index.test'>;
}
declare module 'react-d3-tree/src/Tree/tests/mockData.js' {
declare module.exports: $Exports<'react-d3-tree/src/Tree/tests/mockData'>;
}
declare module 'react-d3-tree/src/Tree/tests/NodeWrapper.test.js' {
declare module.exports: $Exports<'react-d3-tree/src/Tree/tests/NodeWrapper.test'>;
}
declare module 'react-d3-tree/src/util/index.js' {
declare module.exports: $Exports<'react-d3-tree/src/util/index'>;
}
declare module 'react-d3-tree/webpack.config.js' {
declare module.exports: $Exports<'react-d3-tree/webpack.config'>;
}

View File

@@ -0,0 +1,49 @@
// flow-typed signature: f8556e312538a69cb4b93dca5618b82a
// flow-typed version: <<STUB>>/react-stepper-horizontal_v1.0.11/flow_v0.102.0
/**
* This is an autogenerated libdef stub for:
*
* 'react-stepper-horizontal'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-stepper-horizontal' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'react-stepper-horizontal/lib' {
declare module.exports: any;
}
declare module 'react-stepper-horizontal/lib/Step' {
declare module.exports: any;
}
declare module 'react-stepper-horizontal/lib/Stepper' {
declare module.exports: any;
}
// Filename aliases
declare module 'react-stepper-horizontal/lib/index' {
declare module.exports: $Exports<'react-stepper-horizontal/lib'>;
}
declare module 'react-stepper-horizontal/lib/index.js' {
declare module.exports: $Exports<'react-stepper-horizontal/lib'>;
}
declare module 'react-stepper-horizontal/lib/Step.js' {
declare module.exports: $Exports<'react-stepper-horizontal/lib/Step'>;
}
declare module 'react-stepper-horizontal/lib/Stepper.js' {
declare module.exports: $Exports<'react-stepper-horizontal/lib/Stepper'>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
// flow-typed signature: 0ce56690a743910f5934d0ffc0f26d61
// flow-typed version: <<STUB>>/redux-mock-store_v1.5.3/flow_v0.76.0
/**
* This is an autogenerated libdef stub for:
*
* 'redux-mock-store'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'redux-mock-store' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'redux-mock-store/lib/index' {
declare module.exports: any;
}
declare module 'redux-mock-store/test/index' {
declare module.exports: any;
}
declare module 'redux-mock-store/test/mock/middleware' {
declare module.exports: any;
}
// Filename aliases
declare module 'redux-mock-store/lib/index.js' {
declare module.exports: $Exports<'redux-mock-store/lib/index'>;
}
declare module 'redux-mock-store/test/index.js' {
declare module.exports: $Exports<'redux-mock-store/test/index'>;
}
declare module 'redux-mock-store/test/mock/middleware.js' {
declare module.exports: $Exports<'redux-mock-store/test/mock/middleware'>;
}

View File

@@ -0,0 +1,193 @@
// flow-typed signature: 21bf70a7f73a77579f4cc048adda0919
// flow-typed version: <<STUB>>/sql-formatter_v2.3.3/flow_v0.102.0
/**
* This is an autogenerated libdef stub for:
*
* 'sql-formatter'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'sql-formatter' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'sql-formatter/dist/sql-formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/dist/sql-formatter.min' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Indentation' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/InlineBlock' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Params' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Tokenizer' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/tokenTypes' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/Db2Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/N1qlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/PlSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/StandardSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/sqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Indentation' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/InlineBlock' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Params' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Tokenizer' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/tokenTypes' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/Db2Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/N1qlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/PlSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/StandardSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/sqlFormatter' {
declare module.exports: any;
}
// Filename aliases
declare module 'sql-formatter/dist/sql-formatter.js' {
declare module.exports: $Exports<'sql-formatter/dist/sql-formatter'>;
}
declare module 'sql-formatter/dist/sql-formatter.min.js' {
declare module.exports: $Exports<'sql-formatter/dist/sql-formatter.min'>;
}
declare module 'sql-formatter/lib/core/Formatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Formatter'>;
}
declare module 'sql-formatter/lib/core/Indentation.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Indentation'>;
}
declare module 'sql-formatter/lib/core/InlineBlock.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/InlineBlock'>;
}
declare module 'sql-formatter/lib/core/Params.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Params'>;
}
declare module 'sql-formatter/lib/core/Tokenizer.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Tokenizer'>;
}
declare module 'sql-formatter/lib/core/tokenTypes.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/tokenTypes'>;
}
declare module 'sql-formatter/lib/languages/Db2Formatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/Db2Formatter'>;
}
declare module 'sql-formatter/lib/languages/N1qlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/N1qlFormatter'>;
}
declare module 'sql-formatter/lib/languages/PlSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/PlSqlFormatter'>;
}
declare module 'sql-formatter/lib/languages/StandardSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/StandardSqlFormatter'>;
}
declare module 'sql-formatter/lib/sqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/sqlFormatter'>;
}
declare module 'sql-formatter/src/core/Formatter.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Formatter'>;
}
declare module 'sql-formatter/src/core/Indentation.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Indentation'>;
}
declare module 'sql-formatter/src/core/InlineBlock.js' {
declare module.exports: $Exports<'sql-formatter/src/core/InlineBlock'>;
}
declare module 'sql-formatter/src/core/Params.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Params'>;
}
declare module 'sql-formatter/src/core/Tokenizer.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Tokenizer'>;
}
declare module 'sql-formatter/src/core/tokenTypes.js' {
declare module.exports: $Exports<'sql-formatter/src/core/tokenTypes'>;
}
declare module 'sql-formatter/src/languages/Db2Formatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/Db2Formatter'>;
}
declare module 'sql-formatter/src/languages/N1qlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/N1qlFormatter'>;
}
declare module 'sql-formatter/src/languages/PlSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/PlSqlFormatter'>;
}
declare module 'sql-formatter/src/languages/StandardSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/StandardSqlFormatter'>;
}
declare module 'sql-formatter/src/sqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/sqlFormatter'>;
}

View File

@@ -0,0 +1,17 @@
/**
* 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
* @flow
*/
declare module 'unicode-substring' {
declare export default function unicodeSubstring(
baseString: string,
start: number,
end: number,
): string;
}

10
desktop/flow-typed/react.js vendored Normal file
View File

@@ -0,0 +1,10 @@
/**
* 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
*/
declare var React: $Exports<'react'>;

View File

@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-flow"]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,241 @@
/**
* 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 {spawn} from 'child_process';
import memoize from 'lodash.memoize';
// $FlowFixMe
import stringify from 'canonical-json';
const TEST_TIMEOUT_MS = 30 * 1000;
const layoutPathsToExcludeFromSnapshots = [
'id',
'children.*',
'extraInfo.linkedNode',
'data.View.*.value',
'data.View.*.*.value',
'data.View.*.*.*.value',
'data.Drawable.*.value',
'data.LithoView.mountbounds',
'data.Layout.height',
'data.Layout.width',
'data.*.typeface',
];
const params = {
bin: process.env.FLIPPER_PATH || '/tmp/flipper-macos',
securePort: process.env.SECURE_PORT || '8088',
insecurePort: process.env.INSECURE_PORT || '8089',
device: process.env.DEVICE,
};
if (!params.device) {
console.warn(
'No device specified. Test may fail if more than one is present.',
);
}
const basicArgs = [
'-v',
...(params.device ? ['--device', params.device] : []),
'--secure-port',
params.securePort,
'--insecure-port',
params.insecurePort,
];
const runHeadless = memoize((args: Array<string>): Promise<{
output: Object,
stderr: string,
}> => {
return new Promise((resolve, reject) => {
const stdoutChunks = [];
const stderrChunks = [];
console.info(`Running ${params.bin} ${args.join(' ')}`);
const process = spawn(params.bin, args, {});
process.stdout.setEncoding('utf8');
process.stdout.on('data', chunk => {
stdoutChunks.push(chunk);
});
process.stderr.on('data', chunk => {
stderrChunks.push(chunk);
});
process.stdout.on('end', chunk => {
const stdout = stdoutChunks.join('');
const stderr = stderrChunks.join('');
try {
console.log(stderr);
resolve({output: JSON.parse(stdout), stderr: stderr});
} catch (e) {
console.warn(stderr);
reject(
new Error(
`Failed to parse headless output as JSON (${e.message}): ${stdout}`,
),
);
}
});
setTimeout(() => {
process.kill('SIGINT');
}, 20000);
});
});
function getPluginState(app: string, plugin: string): Promise<string> {
return runHeadless(basicArgs).then(result => {
const pluginStates = result.output.store.pluginStates;
for (const pluginId of Object.keys(pluginStates)) {
const matches = /([^#]+)#([^#]+)#([^#]+)#([^#]+)#([^#]+)/.exec(pluginId);
if (
matches &&
matches.length === 6 &&
matches[1] === app &&
matches[5] === plugin
) {
const id = matches[0];
return pluginStates[id];
}
}
throw new Error(`No matching plugin state for ${app}, ${plugin}`);
});
}
test(
'Flipper app appears in exported clients',
() => {
return runHeadless(basicArgs).then(result => {
expect(result.output.clients.map(c => c.query.app)).toContain('Flipper');
});
},
TEST_TIMEOUT_MS,
);
test(
'Output includes fileVersion',
() => {
return runHeadless(basicArgs).then(result => {
expect(result.output.fileVersion).toMatch(/[0-9]+\.[0-9]+\.[0-9]+/);
});
},
TEST_TIMEOUT_MS,
);
test(
'Output includes device',
() => {
return runHeadless(basicArgs).then(result => {
expect(result.output.device).toBeTruthy();
});
},
TEST_TIMEOUT_MS,
);
test(
'Output includes flipperReleaseRevision',
() => {
return runHeadless(basicArgs).then(result => {
expect(result.output.flipperReleaseRevision).toBeTruthy();
});
},
TEST_TIMEOUT_MS,
);
test(
'Output includes store',
() => {
return runHeadless(basicArgs).then(result => {
expect(result.output.store).toBeTruthy();
});
},
TEST_TIMEOUT_MS,
);
function stripUnstableLayoutAttributes(node: Object): Object {
let newNode = node;
for (const path of layoutPathsToExcludeFromSnapshots) {
const parts = path.split('.');
newNode = stripNode(newNode, parts);
}
return newNode;
}
function stripNode(node: any, path: Array<string>) {
if (path.length === 0) {
return 'PLACEHOLDER';
}
if (path[0] === '*') {
if (Array.isArray(node)) {
return node.map(e => stripNode(e, path.slice(1)));
}
return Object.entries(node).reduce((acc, [key, val]) => {
acc[key] = stripNode(val, path.slice(1));
return acc;
}, {});
}
if (!node[path[0]]) {
return node;
}
return {...node, [path[0]]: stripNode(node[path[0]], path.slice(1))};
}
test('test layout snapshot stripping', () => {
const beforeStripping = {
my: {
test: {
id: 4,
node: 7,
something: 9,
list: [1, 2, 3],
},
},
id: 2,
children: [1, 2, 3],
extraInfo: {
linkedNode: 55,
somethingElse: 44,
},
data: {View: {bounds: {something: {value: 4}}}},
other: 8,
};
const afterStripping = stripUnstableLayoutAttributes(beforeStripping);
expect(afterStripping).toEqual({
my: {
test: {
id: 4,
node: 7,
something: 9,
list: [1, 2, 3],
},
},
id: 'PLACEHOLDER',
children: ['PLACEHOLDER', 'PLACEHOLDER', 'PLACEHOLDER'],
extraInfo: {
linkedNode: 'PLACEHOLDER',
somethingElse: 44,
},
data: {View: {bounds: {something: {value: 'PLACEHOLDER'}}}},
other: 8,
});
});
test('Sample app layout hierarchy matches snapshot', () => {
return getPluginState('Flipper', 'Inspector').then(result => {
const state = JSON.parse(result);
expect(state.rootAXElement).toBe('com.facebook.flipper.sample');
expect(state.rootElement).toBe('com.facebook.flipper.sample');
const canonicalizedElements = Object.values(state.elements)
.map(e => {
const stableizedElements = stripUnstableLayoutAttributes(e);
return stringify(stableizedElements);
})
.sort();
expect(canonicalizedElements).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,25 @@
{
"name": "headless-tests",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "jest",
"test:debug": "node --inspect node_modules/.bin/jest --runInBand"
},
"jest": {
"transform": {
"^.+\\.jsx?$": "babel-jest"
}
},
"devDependencies": {
"jest": "^24.7.1"
},
"license": "MIT",
"dependencies": {
"@babel/preset-env": "^7.8.3",
"@babel/preset-flow": "^7.8.3",
"babel-jest": "^24.8.0",
"canonical-json": "^0.0.4",
"lodash.memoize": "^4.1.2"
}
}

File diff suppressed because it is too large Load Diff

394
desktop/headless/index.tsx Normal file
View File

@@ -0,0 +1,394 @@
/**
* 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 path from 'path';
import {createStore, Dispatch, Middleware, MiddlewareAPI} from 'redux';
import {applyMiddleware} from 'redux';
import yargs, {Argv} from 'yargs';
import dispatcher from '../src/dispatcher/index';
import reducers, {Actions, State} from '../src/reducers/index';
import {init as initLogger} from '../src/fb-stubs/Logger';
import {exportStore} from '../src/utils/exportData';
import {
exportMetricsWithoutTrace,
exportMetricsFromTrace,
} from '../src/utils/exportMetrics';
import {listDevices} from '../src/utils/listDevices';
import setup from '../static/setup';
import {getPersistentPlugins, pluginsClassMap} from '../src/utils/pluginUtils';
import {serialize} from '../src/utils/serialization';
import {getStringFromErrorLike} from '../src/utils/index';
import AndroidDevice from '../src/devices/AndroidDevice';
import {Store} from 'flipper';
type Action = {exit: boolean; result?: string};
type UserArguments = {
securePort: string;
insecurePort: string;
dev: boolean;
exit: 'sigint' | 'disconnect';
verbose: boolean;
metrics: string;
listDevices: boolean;
device: string;
listPlugins: boolean;
selectPlugins: Array<string>;
};
(yargs as Argv<UserArguments>)
.usage('$0 [args]')
.command<UserArguments>(
'*',
'Start a headless Flipper instance',
(yargs: Argv<UserArguments>) => {
yargs.option('secure-port', {
default: '8088',
describe: 'Secure port the Flipper server should run on.',
type: 'string',
});
yargs.option('insecure-port', {
default: '8089',
describe: 'Insecure port the Flipper server should run on.',
type: 'string',
});
yargs.option('dev', {
default: false,
describe:
'Enable redux-devtools. Tries to connect to devtools running on port 8181',
type: 'boolean',
});
yargs.option('exit', {
describe: 'Controls when to exit and dump the store to stdout.',
choices: ['sigint', 'disconnect'],
default: 'sigint',
});
yargs.option('v', {
alias: 'verbose',
default: false,
describe: 'Enable verbose logging',
type: 'boolean',
});
yargs.option('metrics', {
default: undefined,
describe: 'Will export metrics instead of data when flipper terminates',
type: 'string',
});
yargs.option('list-devices', {
default: false,
describe: 'Will print the list of devices in the terminal',
type: 'boolean',
});
yargs.option('list-plugins', {
default: false,
describe: 'Will print the list of supported plugins in the terminal',
type: 'boolean',
});
yargs.option('select-plugins', {
default: [],
describe:
'The data/metrics would be exported only for the selected plugins',
type: 'array',
});
yargs.option('device', {
default: undefined,
describe:
'The identifier passed will be matched against the udid of the available devices and the matched device would be selected',
type: 'string',
});
return yargs;
},
startFlipper,
)
.version(global.__VERSION__)
.help().argv; // http://yargs.js.org/docs/#api-argv
function shouldExportMetric(metrics: string): boolean {
if (!metrics) {
return process.argv.includes('--metrics');
}
return true;
}
function outputAndExit(output: string | null | undefined): void {
output = output || '';
console.log(`Finished. Outputting ${output.length} characters.`);
process.stdout.write(output, () => {
process.exit(0);
});
}
function errorAndExit(error: any): void {
process.stderr.write(getStringFromErrorLike(error), () => {
process.exit(1);
});
}
async function earlyExitActions(
exitClosures: Array<(userArguments: UserArguments) => Promise<Action>>,
userArguments: UserArguments,
_originalConsole?: typeof global.console,
): Promise<void> {
for (const exitAction of exitClosures) {
try {
const action = await exitAction(userArguments);
if (action.exit) {
outputAndExit(action.result);
}
} catch (e) {
errorAndExit(e);
}
}
}
async function exitActions(
exitClosures: Array<
(userArguments: UserArguments, store: Store) => Promise<Action>
>,
userArguments: UserArguments,
store: Store,
): Promise<void> {
const {metrics, exit} = userArguments;
for (const exitAction of exitClosures) {
try {
const action = await exitAction(userArguments, store);
if (action.exit) {
outputAndExit(action.result);
}
} catch (e) {
errorAndExit(e);
}
}
if (exit == 'sigint') {
process.on('SIGINT', async () => {
try {
if (shouldExportMetric(metrics) && !metrics) {
const state = store.getState();
const payload = await exportMetricsWithoutTrace(
store,
state.pluginStates,
);
outputAndExit(payload);
} else {
const {serializedString, errorArray} = await exportStore(store);
errorArray.forEach(console.error);
outputAndExit(serializedString);
}
} catch (e) {
errorAndExit(e);
}
});
}
}
async function storeModifyingActions(
storeModifyingClosures: Array<
(userArguments: UserArguments, store: Store) => Promise<Action>
>,
userArguments: UserArguments,
store: Store,
): Promise<void> {
for (const closure of storeModifyingClosures) {
try {
const action = await closure(userArguments, store);
if (action.exit) {
outputAndExit(action.result);
}
} catch (e) {
errorAndExit(e);
}
}
}
async function startFlipper(userArguments: UserArguments) {
const {verbose, metrics, exit, insecurePort, securePort} = userArguments;
console.error(`
_____ _ _
| __| |_|___ ___ ___ ___
| __| | | . | . | -_| _|
|__| |_|_| _| _|___|_| v${global.__VERSION__}
|_| |_|
`);
// redirect all logging to stderr
const overriddenMethods = ['debug', 'info', 'log', 'warn', 'error'];
for (const method of overriddenMethods) {
(global.console as {[key: string]: any})[method] =
method === 'error' || verbose ? global.console.error : () => {};
}
// Polyfills
global.WebSocket = require('ws'); // used for redux devtools
global.fetch = require('node-fetch/lib/index');
process.env.BUNDLED_PLUGIN_PATH =
process.env.BUNDLED_PLUGIN_PATH ||
path.join(path.dirname(process.execPath), 'plugins');
process.env.FLIPPER_PORTS = `${insecurePort},${securePort}`;
// needs to be required after WebSocket polyfill is loaded
const devToolsEnhancer = require('remote-redux-devtools');
const headlessMiddleware: Middleware<{}, State, any> = (
store: MiddlewareAPI<Dispatch<Actions>, State>,
) => (next: Dispatch<Actions>) => (action: Actions) => {
if (exit == 'disconnect' && action.type == 'CLIENT_REMOVED') {
// TODO(T42325892): Investigate why the export stalls without exiting the
// current eventloop task here.
setTimeout(() => {
if (shouldExportMetric(metrics) && !metrics) {
const state = store.getState();
exportMetricsWithoutTrace(store as Store, state.pluginStates)
.then((payload: string | null) => {
outputAndExit(payload || '');
})
.catch((e: Error) => {
errorAndExit(e);
});
} else {
exportStore(store)
.then(({serializedString}) => {
outputAndExit(serializedString);
})
.catch((e: Error) => {
errorAndExit(e);
});
}
}, 10);
}
return next(action);
};
setup({});
const store = createStore<State, Actions, {}, {}>(
reducers,
devToolsEnhancer.composeWithDevTools(applyMiddleware(headlessMiddleware)),
);
const logger = initLogger(store, {isHeadless: true});
const earlyExitClosures: Array<(
userArguments: UserArguments,
) => Promise<Action>> = [
async (userArguments: UserArguments) => {
if (userArguments.listDevices) {
const devices = await listDevices(store);
const mapped = devices.map(device => {
return {
os: device.os,
title: device.title,
deviceType: device.deviceType,
serial: device.serial,
};
});
return {exit: true, result: await serialize(mapped)};
}
return Promise.resolve({exit: false});
},
];
await earlyExitActions(earlyExitClosures, userArguments);
const cleanupDispatchers = dispatcher(store, logger);
const storeModifyingClosures: Array<(
userArguments: UserArguments,
store: Store,
) => Promise<Action>> = [
async (userArguments: UserArguments, store: Store) => {
const {device: selectedDeviceID} = userArguments;
if (selectedDeviceID) {
const devices = await listDevices(store);
const matchedDevice = devices.find(
device => device.serial === selectedDeviceID,
);
if (matchedDevice) {
if (matchedDevice instanceof AndroidDevice) {
const ports = store.getState().application.serverPorts;
matchedDevice.reverse([ports.secure, ports.insecure]);
}
matchedDevice.loadDevicePlugins(
store.getState().plugins.devicePlugins,
);
store.dispatch({
type: 'REGISTER_DEVICE',
payload: matchedDevice,
});
store.dispatch({
type: 'SELECT_DEVICE',
payload: matchedDevice,
});
return {exit: false};
}
throw new Error(`No device matching the serial ${selectedDeviceID}`);
}
return {
exit: false,
};
},
async (userArguments: UserArguments, store: Store) => {
const {selectPlugins} = userArguments;
const selectedPlugins = selectPlugins.filter(selectPlugin => {
return selectPlugin != undefined;
});
if (selectedPlugins) {
store.dispatch({
type: 'SELECTED_PLUGINS',
payload: selectedPlugins,
});
}
return {
exit: false,
};
},
];
const exitActionClosures: Array<(
userArguments: UserArguments,
store: Store,
) => Promise<Action>> = [
async (userArguments: UserArguments, store: Store) => {
const {listPlugins} = userArguments;
if (listPlugins) {
return Promise.resolve({
exit: true,
result: await serialize(
getPersistentPlugins(store.getState().plugins),
),
});
}
return Promise.resolve({
exit: false,
});
},
async (userArguments: UserArguments, store: Store) => {
const {metrics} = userArguments;
if (shouldExportMetric(metrics) && metrics && metrics.length > 0) {
try {
const payload = await exportMetricsFromTrace(
metrics,
pluginsClassMap(store.getState().plugins),
store.getState().plugins.selectedPlugins,
);
return {exit: true, result: payload ? payload.toString() : ''};
} catch (error) {
return {exit: true, result: error};
}
}
return Promise.resolve({exit: false});
},
];
await storeModifyingActions(storeModifyingClosures, userArguments, store);
await exitActions(exitActionClosures, userArguments, store);
await cleanupDispatchers();
}

259
desktop/package.json Normal file
View File

@@ -0,0 +1,259 @@
{
"name": "flipper",
"version": "0.33.1",
"versionDate": "2018-4-12",
"description": "Mobile development tool",
"productName": "Flipper",
"author": "Facebook Inc",
"main": "src/index.tsx",
"icon": "icon.png",
"category": "facebook-intern",
"privileged": true,
"build": {
"appId": "flipper",
"productName": "Flipper",
"artifactName": "Flipper-${os}.${ext}",
"protocols": {
"name": "flipper",
"schemes": [
"flipper"
]
},
"mac": {
"category": "public.app-category.developer-tools",
"extendInfo": {
"NSUserNotificationAlertStyle": "alert"
}
},
"dmg": {
"background": "dmgBackground.png",
"icon": "icon.icns",
"iconSize": 155,
"window": {
"width": 660,
"height": 400
},
"contents": [
{
"x": 123,
"y": 172
},
{
"x": 539,
"y": 168,
"type": "link",
"path": "/Applications"
}
]
},
"win": {
"publisherName": "Facebook, Inc.",
"sign": null
},
"asar": false,
"fileAssociations": [
{
"ext": [
".flipper"
],
"name": "Flipper Data",
"role": "Viewer",
"icon": "document-icons/document.icns"
}
]
},
"resolutions": {
"@jest-runner/electron/electron": "8.0.1",
"adbkit/adbkit-logcat": "2",
"@types/react": "16.9.17",
"@types/react-dom": "16.9.4"
},
"jest": {
"transform": {
"^.*__tests__/.*\\.tsx?$": "ts-jest",
"\\.(js|tsx?)$": "<rootDir>/static/transforms/index.js"
},
"setupFiles": [
"<rootDir>/static/globalTestSetup.js"
],
"moduleNameMapper": {
"^flipper$": "<rootDir>/src/index.tsx"
},
"clearMocks": true
},
"devDependencies": {
"@jest-runner/electron": "^2.0.2",
"@testing-library/react": "^9.3.0",
"@types/algoliasearch": "^3.30.19",
"@types/babel-code-frame": "^6.20.2",
"@types/decompress": "4.2.3",
"@types/deep-equal": "^1.0.1",
"@types/detect-port": "^1.1.0",
"@types/expand-tilde": "^2.0.0",
"@types/express": "^4.17.2",
"@types/fb-watchman": "^2.0.0",
"@types/fs-extra": "^8.0.0",
"@types/invariant": "^2.2.31",
"@types/jest": "^25.1.0",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.isequal": "^4.5.5",
"@types/mkdirp": "^1.0.0",
"@types/node": "^12.12.20",
"@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4",
"@types/react-redux": "^7.1.5",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.1",
"@types/recursive-readdir": "^2.2.0",
"@types/redux-persist": "^4.3.1",
"@types/requestidlecallback": "^0.3.1",
"@types/rsocket-core": "^0.0.5",
"@types/socket.io": "^2.1.4",
"@types/testing-library__react": "^9.1.2",
"@types/tmp": "^0.1.0",
"@types/uuid": "^7.0.0",
"@types/ws": "^7.2.0",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^2.19.2",
"@typescript-eslint/parser": "^2.19.2",
"babel-code-frame": "^6.26.0",
"babel-eslint": "^10.0.1",
"electron": "8.0.1",
"electron-builder": "^22.3.2",
"eslint": "^6.7.0",
"eslint-config-fbjs": "^3.1.1",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-flowtype": "^4.3.0",
"eslint-plugin-header": "^3.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-relay": "^1.4.1",
"flow-bin": "^0.120.1",
"glob": "^7.1.2",
"jest": "^25.1.0",
"jest-fetch-mock": "^3.0.0",
"prettier": "^1.19.1",
"react-async": "^10.0.0",
"recursive-readdir": "^2.2.2",
"redux-mock-store": "^1.5.3",
"ts-jest": "^25.1.0",
"ts-node": "^8.6.2",
"typescript": "^3.7.2"
},
"dependencies": {
"@emotion/core": "^10.0.22",
"@emotion/styled": "^10.0.23",
"@iarna/toml": "^2.2.3",
"@types/glob": "^7.1.1",
"@types/promise-retry": "^1.1.3",
"@types/react-color": "^3.0.1",
"@types/react-test-renderer": "^16.9.1",
"@types/react-transition-group": "^4.2.2",
"@types/react-virtualized": "^9.21.7",
"@types/redux-devtools-extension": "^2.13.2",
"@types/redux-mock-store": "^1.0.1",
"@types/rsocket-tcp-server": "^0.0.2",
"@types/which": "^1.3.2",
"JSONStream": "^1.3.1",
"adbkit": "^2.11.1",
"adbkit-logcat": "^2.0.1",
"algoliasearch": "^4.0.0",
"ansi-to-html": "^0.6.3",
"async-mutex": "^0.1.3",
"chalk": "^3.0.0",
"codemirror": "^5.25.0",
"cross-env": "^7.0.0",
"dashify": "^2.0.0",
"decompress": "^4.2.0",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"deep-equal": "^2.0.1",
"detect-port": "^1.1.1",
"emotion": "^10.0.23",
"expand-tilde": "^2.0.2",
"express": "^4.15.2",
"fb-watchman": "^2.0.0",
"flipper-doctor": "^0.7.0",
"fs-extra": "^8.0.1",
"immer": "^6.0.0",
"immutable": "^4.0.0-rc.12",
"invariant": "^2.2.2",
"line-replace": "^1.0.2",
"live-plugin-manager": "^0.14.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"mkdirp": "^1.0.0",
"node-fetch": "^2.3.0",
"npm-api": "^1.0.0",
"open": "^7.0.0",
"openssl-wrapper": "^0.3.4",
"p-map": "^4.0.0",
"patch-package": "^6.2.0",
"pkg": "^4.4.1",
"promise-retry": "^1.1.1",
"promisify-child-process": "^3.1.3",
"prop-types": "^15.6.0",
"query-string": "^6.10.1",
"react": "16.13.0",
"react-color": "^2.11.7",
"react-debounce-render": "^6.0.0",
"react-devtools-core": "^4.0.6",
"react-dom": "^16.13.0",
"react-markdown": "^4.2.2",
"react-player": "^1.15.2",
"react-redux": "^7.1.1",
"react-test-renderer": "^16.13.0",
"react-transition-group": "^4.3.0",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.3.1",
"redux": "^4.0.0",
"redux-persist": "^6.0.0",
"remote-redux-devtools": "^0.5.16",
"rsocket-core": "^0.0.19",
"rsocket-tcp-server": "^0.0.19",
"socket.io": "^2.0.4",
"string-natural-compare": "^3.0.0",
"tmp": "0.0.33",
"uuid": "^7.0.1",
"websocket": "^1.0.31",
"which": "^2.0.1",
"ws": "^7.2.0",
"xdg-basedir": "^4.0.0",
"xml2js": "^0.4.19",
"yargs": "^15.0.1",
"yazl": "^2.5.1"
},
"greenkeeper": {
"ignore": [
"tmp",
"flipper-doctor"
]
},
"scripts": {
"preinstall": "node scripts/prepare-watchman-config.js && yarn config set ignore-engines",
"postinstall": "cross-env TS_NODE_FILES=true node --require ts-node/register scripts/yarn-install.ts && patch-package",
"rm-dist": "rimraf dist",
"rm-modules": "rimraf node_modules static/node_modules",
"rm-temp": "rimraf $TMPDIR/jest* $TMPDIR/react-native-packager*",
"rm-bundle": "rimraf static/main.bundle.*",
"reset": "yarn rm-dist && yarn rm-temp && yarn cache clean && yarn rm-bundle && yarn rm-modules",
"start": "cross-env NODE_ENV=development TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts --inspect=9229",
"start:break": "cross-env NODE_ENV=development TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts --inspect-brk=9229",
"start:no-embedded-plugins": "cross-env NODE_ENV=development cross-env FLIPPER_NO_EMBEDDED_PLUGINS=true TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts",
"build": "yarn rm-dist && cross-env NODE_ENV=production TS_NODE_FILES=true node --require ts-node/register scripts/build-release.ts $@",
"build-headless": "yarn rm-dist && mkdir dist && cross-env NODE_ENV=production TS_NODE_FILES=true node --require ts-node/register scripts/build-headless.ts $@",
"fix": "eslint . --fix --ext .js,.ts,.tsx",
"test": "jest --testPathPattern=\"node\\.(js|tsx)$\" --no-cache",
"test:debug": "node --inspect node_modules/.bin/jest --runInBand",
"test-electron": "jest --testPathPattern=\"electron\\.(js|tsx)$\" --testEnvironment=@jest-runner/electron/environment --runner=@jest-runner/electron --no-cache",
"test-with-device": "USE_ELECTRON_STUBS=1 jest --testPathPattern=\"device\\.(js|tsx)$\" --detectOpenHandles --no-cache",
"tsc": "tsc --noemit",
"lint": "eslint . --ext .js,.ts,.tsx && flow check && yarn tsc",
"everything": "yarn reset && yarn install && yarn lint && yarn test && yarn test-electron && yarn build --mac --win --linux && yarn build-headless --mac --linux && yarn start"
},
"optionalDependencies": {
"7zip-bin-mac": "^1.0.1"
}
}

View File

@@ -0,0 +1,47 @@
diff --git a/node_modules/adbkit/lib/adb/parser.js b/node_modules/adbkit/lib/adb/parser.js
index 1b66bda..c41037d 100644
--- a/node_modules/adbkit/lib/adb/parser.js
+++ b/node_modules/adbkit/lib/adb/parser.js
@@ -52,7 +52,7 @@ Parser = (function() {
Parser.prototype.readAll = function() {
var all, endListener, errorListener, resolver, tryRead;
- all = new Buffer(0);
+ all = Buffer.alloc(0);
resolver = Promise.defer();
tryRead = (function(_this) {
return function() {
@@ -108,7 +108,7 @@ Parser = (function() {
return resolver.reject(new Parser.PrematureEOFError(howMany));
}
} else {
- return resolver.resolve(new Buffer(0));
+ return resolver.resolve(Buffer.alloc(0));
}
};
})(this);
@@ -196,7 +196,7 @@ Parser = (function() {
Parser.prototype.readUntil = function(code) {
var read, skipped;
- skipped = new Buffer(0);
+ skipped = Buffer.alloc(0);
read = (function(_this) {
return function() {
return _this.readBytes(1).then(function(chunk) {
diff --git a/node_modules/adbkit/lib/adb/protocol.js b/node_modules/adbkit/lib/adb/protocol.js
index d1377d0..2edd7ba 100644
--- a/node_modules/adbkit/lib/adb/protocol.js
+++ b/node_modules/adbkit/lib/adb/protocol.js
@@ -33,9 +33,9 @@ Protocol = (function() {
Protocol.encodeData = function(data) {
if (!Buffer.isBuffer(data)) {
- data = new Buffer(data);
+ data = Buffer.from(data);
}
- return Buffer.concat([new Buffer(Protocol.encodeLength(data.length)), data]);
+ return Buffer.concat([Buffer.from(Protocol.encodeLength(data.length)), data]);
};
return Protocol;

View File

@@ -0,0 +1,13 @@
diff --git a/node_modules/adbkit-logcat/lib/logcat/parser/binary.js b/node_modules/adbkit-logcat/lib/logcat/parser/binary.js
index 12fa8ec..e122933 100644
--- a/node_modules/adbkit-logcat/lib/logcat/parser/binary.js
+++ b/node_modules/adbkit-logcat/lib/logcat/parser/binary.js
@@ -10,7 +10,7 @@ const HEADER_SIZE_MAX = 100
class Binary extends EventEmitter {
constructor(options) {
super(options)
- this.buffer = new Buffer(0)
+ this.buffer = Buffer.alloc(0)
}
parse(chunk) {

View File

@@ -0,0 +1,51 @@
diff --git a/node_modules/jsonparse/jsonparse.js b/node_modules/jsonparse/jsonparse.js
index 3991060..83f1458 100644
--- a/node_modules/jsonparse/jsonparse.js
+++ b/node_modules/jsonparse/jsonparse.js
@@ -56,7 +56,7 @@ function Parser() {
this.value = undefined;
this.string = undefined; // string data
- this.stringBuffer = Buffer.alloc ? Buffer.alloc(STRING_BUFFER_SIZE) : new Buffer(STRING_BUFFER_SIZE);
+ this.stringBuffer = Buffer.alloc(STRING_BUFFER_SIZE);
this.stringBufferOffset = 0;
this.unicode = undefined; // unicode escapes
this.highSurrogate = undefined;
@@ -67,7 +67,7 @@ function Parser() {
this.state = VALUE;
this.bytes_remaining = 0; // number of bytes remaining in multi byte utf8 char to read after split boundary
this.bytes_in_sequence = 0; // bytes in multi byte utf8 char to read
- this.temp_buffs = { "2": new Buffer(2), "3": new Buffer(3), "4": new Buffer(4) }; // for rebuilding chars split before boundary is reached
+ this.temp_buffs = { "2": Buffer.alloc(2), "3": Buffer.alloc(3), "4": Buffer.alloc(4) }; // for rebuilding chars split before boundary is reached
// Stream offset
this.offset = -1;
@@ -125,7 +125,7 @@ proto.appendStringBuf = function (buf, start, end) {
this.stringBufferOffset += size;
};
proto.write = function (buffer) {
- if (typeof buffer === "string") buffer = new Buffer(buffer);
+ if (typeof buffer === "string") buffer = Buffer.from(buffer);
var n;
for (var i = 0, l = buffer.length; i < l; i++) {
if (this.tState === START){
@@ -221,16 +221,16 @@ proto.write = function (buffer) {
var intVal = parseInt(this.unicode, 16);
this.unicode = undefined;
if (this.highSurrogate !== undefined && intVal >= 0xDC00 && intVal < (0xDFFF + 1)) { //<56320,57343> - lowSurrogate
- this.appendStringBuf(new Buffer(String.fromCharCode(this.highSurrogate, intVal)));
+ this.appendStringBuf(Buffer.from(String.fromCharCode(this.highSurrogate, intVal)));
this.highSurrogate = undefined;
} else if (this.highSurrogate === undefined && intVal >= 0xD800 && intVal < (0xDBFF + 1)) { //<55296,56319> - highSurrogate
this.highSurrogate = intVal;
} else {
if (this.highSurrogate !== undefined) {
- this.appendStringBuf(new Buffer(String.fromCharCode(this.highSurrogate)));
+ this.appendStringBuf(Buffer.from(String.fromCharCode(this.highSurrogate)));
this.highSurrogate = undefined;
}
- this.appendStringBuf(new Buffer(String.fromCharCode(intVal)));
+ this.appendStringBuf(Buffer.from(String.fromCharCode(intVal)));
}
this.tState = STRING1;
}

View File

@@ -0,0 +1 @@
/lib

2
desktop/pkg/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/lib
/node_modules/

21
desktop/pkg/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
desktop/pkg/README.md Normal file
View File

@@ -0,0 +1,70 @@
# flipper-pkg
`flipper-pkg` is a **work-in-progress** tool for bundling and publishing
Flipper plugins.
<!-- toc -->
* [Usage](#usage)
* [Commands](#commands)
<!-- tocstop -->
# Usage
<!-- usage -->
```sh-session
$ npm install -g mycli
$ mycli COMMAND
running command...
$ mycli (-v|--version|version)
mycli/0.0.0 darwin-x64 node-v12.14.0
$ mycli --help [COMMAND]
USAGE
$ mycli COMMAND
...
```
<!-- usagestop -->
# Commands
<!-- commands -->
* [`mycli hello [FILE]`](#mycli-hello-file)
* [`mycli help [COMMAND]`](#mycli-help-command)
## `mycli hello [FILE]`
describe the command here
```
USAGE
$ mycli hello [FILE]
OPTIONS
-f, --force
-h, --help show CLI help
-n, --name=name name to print
EXAMPLE
$ mycli hello
hello world from ./src/hello.ts!
```
_See code: [src/commands/hello.ts](https://github.com/passy/mycli/blob/v0.0.0/src/commands/hello.ts)_
## `mycli help [COMMAND]`
display help for mycli
```
USAGE
$ mycli help [COMMAND]
ARGUMENTS
COMMAND command to show help for
OPTIONS
--all see all commands in CLI
```
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.3/src/commands/help.ts)_
<!-- commandsstop -->
## License
[MIT](LICENSE)

14
desktop/pkg/bin/run Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env node
/**
* 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
*/
require('@oclif/command').run()
.then(require('@oclif/command/flush'))
.catch(require('@oclif/errors/handle'))

8
desktop/pkg/bin/run.cmd Normal file
View File

@@ -0,0 +1,8 @@
@REM Copyright (c) Facebook, Inc. and its affiliates.
@REM
@REM This source code is licensed under the MIT license found in the
@REM LICENSE file in the root directory of this source tree.
@echo off
node "%~dp0run" %*

View File

@@ -0,0 +1,11 @@
{
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": [
"\/node_modules\/",
"\/lib\/"
],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}

79
desktop/pkg/package.json Normal file
View File

@@ -0,0 +1,79 @@
{
"name": "flipper-pkg",
"version": "0.0.0",
"description": "Utility for building and publishing Flipper plugins",
"repository": "facebook/flipper",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"license": "MIT",
"bin": {
"flipper-pkg": "./bin/run"
},
"bugs": "https://github.com/facebook/flipper/issues",
"dependencies": {
"@babel/core": "^7.8.6",
"@babel/generator": "^7.8.6",
"@babel/parser": "^7.8.6",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-transform-flow-strip-types": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.8.3",
"@babel/plugin-transform-typescript": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/plugin-help": "^2",
"@types/fs-extra": "^8.1.0",
"@types/inquirer": "^6.5.0",
"@types/node": "^13.7.5",
"cli-ux": "^5.4.5",
"fs-extra": "^8.1.0",
"metro": "^0.58.0",
"inquirer": "^7.0.5",
"tslib": "^1"
},
"devDependencies": {
"@oclif/dev-cli": "^1",
"@types/jest": "^24.0.21",
"globby": "^10",
"jest": "^24.9.0",
"prettier": "^1.19.1",
"ts-jest": "^24.1.0",
"ts-node": "^8",
"typescript": "^3.7.2"
},
"scripts": {
"build": "tsc",
"postpack": "rm -f oclif.manifest.json",
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
"prepare": "yarn run build",
"prepublishOnly": "yarn test && yarn run lint",
"preversion": "yarn run lint",
"test": "jest --config jestconfig.json",
"run": "bin/run",
"version": "oclif-dev readme && git add README.md"
},
"engines": {
"node": ">=8.0.0"
},
"files": [
"/bin",
"/npm-shrinkwrap.json",
"/oclif.manifest.json",
"lib/**/*"
],
"homepage": "https://github.com/facebook/flipper",
"keywords": [
"Flipper"
],
"author": "Facebook, Inc",
"oclif": {
"commands": "./lib/commands",
"bin": "flipper-pkg",
"plugins": [
"@oclif/plugin-help"
]
}
}

View File

@@ -0,0 +1,12 @@
/**
* 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
*/
test('tests are working', () => {
expect(true).toBeTruthy();
});

View File

@@ -0,0 +1,127 @@
/**
* 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 {Command, flags} from '@oclif/command';
import {promises as fs} from 'fs';
import {mkdirp, pathExists, readJSON, ensureDir} from 'fs-extra';
import * as inquirer from 'inquirer';
import * as path from 'path';
import * as yarn from '../utils/yarn';
import cli from 'cli-ux';
import runBuild from '../utils/runBuild';
async function deriveOutputFileName(inputDirectory: string): Promise<string> {
const packageJson = await readJSON(path.join(inputDirectory, 'package.json'));
return `${packageJson.name || ''}-${packageJson.version}.tgz`;
}
export default class Bundle extends Command {
public static description =
'bundle a plugin folder into a distributable archive';
public static examples = [`$ flipper-pkg bundle path/to/plugin`];
public static flags = {
output: flags.string({
char: 'o',
default: '.',
description:
"Where to output the bundle, file or directory. Defaults to '.'.",
}),
};
public static args = [{name: 'directory', required: true}];
public async run() {
const {args, flags: parsedFlags} = this.parse(Bundle);
const stat = await fs.lstat(args.directory);
if (!stat.isDirectory()) {
this.error(`Plugin source ${args.directory} is not a directory.`);
}
let output;
if (await pathExists(parsedFlags.output)) {
const outputStat = await fs.lstat(parsedFlags.output);
if (outputStat.isDirectory()) {
output = path.resolve(
path.join(
parsedFlags.output,
await deriveOutputFileName(args.directory),
),
);
} else {
output = parsedFlags.output;
}
} else {
let dir;
let file = null;
// Treat this as a
if (parsedFlags.output.slice(-1) === '/') {
dir = parsedFlags.output;
} else {
dir = path.dirname(parsedFlags.output);
file = path.basename(parsedFlags.output);
}
if (!(await pathExists(dir))) {
const answer: {confirm: boolean} = await inquirer.prompt({
default: true,
message: `Output directory '${dir}' doesn't exist. Create it?`,
name: 'confirm',
type: 'confirm',
});
if (answer.confirm) {
mkdirp(dir);
} else {
this.error(`Output directory ${dir} not found.`);
}
}
if (file === null) {
file = await deriveOutputFileName(args.directory);
}
output = path.join(dir, file);
}
const inputDirectory = path.resolve(args.directory);
const outputFile = path.resolve(output);
this.log(`⚙️ Bundling ${inputDirectory} to ${outputFile}...`);
cli.action.start('Installing dependencies');
await yarn.install(inputDirectory);
cli.action.stop();
cli.action.start('Reading package.json');
const packageJson = await readJSON(
path.join(inputDirectory, 'package.json'),
);
const entry =
packageJson.main ??
((await pathExists(path.join(inputDirectory, 'index.tsx')))
? 'index.tsx'
: 'index.jsx');
const bundleMain = packageJson.bundleMain ?? path.join('dist', 'index.js');
const out = path.resolve(inputDirectory, bundleMain);
cli.action.stop(`done. Entry: ${entry}. Bundle main: ${bundleMain}.`);
cli.action.start(`Compiling`);
await ensureDir(path.dirname(out));
await runBuild(inputDirectory, entry, out);
cli.action.stop();
cli.action.start(`Packing to ${outputFile}`);
await yarn.pack(inputDirectory, outputFile);
cli.action.stop();
this.log(`✅ Bundled ${inputDirectory} to ${outputFile}`);
}
}

12
desktop/pkg/src/index.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* 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
*/
export {run} from '@oclif/command';
export const PKG = 'flipper-pkg';
export {default as runBuild} from './utils/runBuild';

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
*/
const {parse} = require('@babel/parser');
const {transformFromAstSync} = require('@babel/core');
const {default: generate} = require('@babel/generator');
const flipperRequires = require('../flipper-requires');
const babelOptions = {
ast: true,
plugins: [flipperRequires],
filename: 'index.js',
};
test('transform react requires to global object', () => {
const src = 'require("react")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.React;');
});
test('transform react-dom requires to global object', () => {
const src = 'require("react-dom")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.ReactDOM;');
});
test('transform flipper requires to global object', () => {
const src = 'require("flipper")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.Flipper;');
});
test('transform React identifier to global.React', () => {
const src = 'React;';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.React;');
});
test.skip('throw error when requiring outside the plugin', () => {
const src = 'require("../test.js")';
const ast = parse(src);
expect(() => {
transformFromAstSync(ast, src, babelOptions);
}).toThrow();
});
test('allow requiring from parent folder as long as we stay in plugin folder', () => {
const src = 'require("../test.js")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, {
...babelOptions,
root: '/path/to/plugin',
filename: '/path/to/plugin/subfolder/index.js',
}).ast;
const {code} = generate(transformed);
expect(code).toBe('require("../test.js");');
});

View File

@@ -0,0 +1,34 @@
/**
* 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
*/
function isDynamicRequire(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
(node.arguments.length !== 1 || node.arguments[0].type !== 'StringLiteral')
);
}
module.exports = function(babel) {
const t = babel.types;
return {
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path) {
if (!isDynamicRequire(path.node)) {
return;
}
path.replaceWith(t.identifier('triggerDynamicRequireError'));
},
},
};
};

View File

@@ -0,0 +1,94 @@
/**
* 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
*/
const BUILTINS = [
'electron',
'buffer',
'child_process',
'crypto',
'dgram',
'dns',
'fs',
'http',
'https',
'net',
'os',
'readline',
'stream',
'string_decoder',
'tls',
'tty',
'zlib',
'constants',
'events',
'url',
'assert',
'util',
'path',
'perf_hooks',
'punycode',
'querystring',
'cluster',
'console',
'module',
'process',
'vm',
'domain',
'v8',
'repl',
'timers',
'node-fetch',
];
const IGNORED_MODULES = [
'bufferutil',
'utf-8-validate',
'spawn-sync',
'./src/logcat',
'./src/monkey',
'./src/adb',
];
function isRequire(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'StringLiteral'
);
}
module.exports = function(babel) {
const t = babel.types;
return {
name: 'infinity-import-react',
visitor: {
CallExpression(path) {
if (!isRequire(path.node)) {
return;
}
const source = path.node.arguments[0].value;
if (
BUILTINS.includes(source) ||
BUILTINS.some(moduleName => source.startsWith(`${moduleName}/`))
) {
path.node.callee.name = 'electronRequire';
}
if (IGNORED_MODULES.includes(source)) {
path.replaceWith(t.identifier('triggerReferenceError'));
}
},
},
};
};

View File

@@ -0,0 +1,35 @@
/**
* 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
*/
const babylon = require('@babel/parser');
const fs = require('fs');
const electronStubs = babylon.parseExpression(
fs.readFileSync('static/electron-stubs.notjs').toString(),
);
module.exports = function(babel) {
return {
name: 'replace-electron-requires-with-stubs',
visitor: {
CallExpression(path) {
if (
path.node.type === 'CallExpression' &&
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'require' &&
path.node.arguments.length > 0
) {
if (path.node.arguments[0].value === 'electron') {
path.replaceWith(electronStubs);
}
}
},
},
};
};

View File

@@ -0,0 +1,47 @@
/**
* 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
*/
const fs = require('fs');
const path = require('path');
const replaceFBStubs = fs.existsSync(
path.join(__dirname, '..', '..', 'src', 'fb'),
);
const requireFromFolder = (folder, path) =>
new RegExp(folder + '/[A-Za-z0-9.-_]+(.js)?$', 'g').test(path);
module.exports = function(babel) {
return {
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path) {
if (
replaceFBStubs &&
path.node.type === 'CallExpression' &&
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'require' &&
path.node.arguments.length > 0
) {
if (requireFromFolder('fb', path.node.arguments[0].value)) {
throw new Error(
'Do not require directly from fb/, but rather from fb-stubs/ to not break flow-typing and make sure stubs are up-to-date.',
);
} else if (
requireFromFolder('fb-stubs', path.node.arguments[0].value)
) {
path.node.arguments[0].value = path.node.arguments[0].value.replace(
'/fb-stubs/',
'/fb/',
);
}
}
},
},
};
};

View File

@@ -0,0 +1,81 @@
/**
* 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
*/
const {resolve, dirname} = require('path');
// do not apply this transform for these paths
const EXCLUDE_PATHS = [
'/node_modules/react-devtools-core/',
'relay-devtools/DevtoolsUI',
];
function isExcludedPath(path) {
for (const epath of EXCLUDE_PATHS) {
if (path.indexOf(epath) > -1) {
return true;
}
}
return false;
} // $FlowFixMe
module.exports = ({types: t}) => ({
visitor: {
// $FlowFixMe
CallExpression(path, state) {
if (isExcludedPath(state.file.opts.filename)) {
return;
}
const node = path.node;
const args = node.arguments || [];
if (
node.callee.name === 'require' &&
args.length === 1 &&
t.isStringLiteral(args[0])
) {
if (args[0].value === 'flipper') {
path.replaceWith(t.identifier('global.Flipper'));
} else if (args[0].value === 'react') {
path.replaceWith(t.identifier('global.React'));
} else if (args[0].value === 'react-dom') {
path.replaceWith(t.identifier('global.ReactDOM'));
} else if (args[0].value === 'adbkit') {
path.replaceWith(t.identifier('global.adbkit'));
} else if (
// require a file not a pacakge
args[0].value.indexOf('/') > -1 &&
// in the plugin itself and not inside one of its dependencies
state.file.opts.filename.indexOf('node_modules') === -1 &&
// the resolved path for this file is outside the plugins root
!resolve(dirname(state.file.opts.filename), args[0].value).startsWith(
state.file.opts.root,
) &&
!resolve(dirname(state.file.opts.filename), args[0].value).indexOf(
'/static/',
) < 0
) {
throw new Error(
`Plugins cannot require files from outside their folder. Attempted to require ${resolve(
dirname(state.file.opts.filename),
args[0].value,
)} which isn't inside ${state.file.opts.root}`,
);
}
}
},
Identifier(path, state) {
if (
path.node.name === 'React' &&
path.parentPath.node.id !== path.node &&
!isExcludedPath(state.file.opts.filename)
) {
path.replaceWith(t.identifier('global.React'));
}
},
},
});

View File

@@ -0,0 +1,124 @@
/**
* 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
*/
const generate = require('@babel/generator').default;
const babylon = require('@babel/parser');
const babel = require('@babel/core');
const fs = require('fs');
const path = require('path');
function transform({filename, options, src}) {
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
const presets = [require('@babel/preset-react')];
const ast = babylon.parse(src, {
filename,
plugins: isTypeScript
? [
'jsx',
'typescript',
'classProperties',
'optionalChaining',
'nullishCoalescingOperator',
]
: [
'jsx',
['flow', {all: true}],
'classProperties',
'objectRestSpread',
'optionalChaining',
'nullishCoalescingOperator',
],
sourceType: 'module',
});
// run babel
const plugins = [];
if (!isTypeScript) {
plugins.push(
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-proposal-object-rest-spread'),
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-transform-flow-strip-types'),
require('@babel/plugin-proposal-optional-chaining'),
require('@babel/plugin-proposal-nullish-coalescing-operator'),
require('./dynamic-requires.js'),
);
} else {
plugins.push(
require('@babel/plugin-transform-typescript'),
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-proposal-optional-chaining'),
require('@babel/plugin-proposal-nullish-coalescing-operator'),
);
}
if (
fs.existsSync(
path.resolve(path.dirname(path.dirname(__dirname)), 'src', 'fb'),
)
) {
plugins.push(require('./fb-stubs.js'));
}
if (!options.isTestRunner) {
// Replacing require statements with electronRequire to prevent metro from
// resolving them. electronRequire are resolved during runtime by electron.
// As the tests are not bundled by metro and run in @jest-runner/electron,
// electron imports are working out of the box.
plugins.push(require('./electron-requires'));
}
plugins.push(require('./flipper-requires.js'));
const transformed = babel.transformFromAst(ast, src, {
ast: true,
babelrc: !filename.includes('node_modules'),
code: false,
comments: false,
compact: false,
root: options.projectRoot,
filename,
plugins,
presets,
sourceMaps: true,
retainLines: !!options.isTestRunner,
});
const result = generate(
transformed.ast,
{
filename,
sourceFileName: filename,
sourceMaps: true,
retainLines: !!options.isTestRunner,
},
src,
);
return {
ast: transformed.ast,
code: result.code,
filename,
map: result.map,
};
}
module.exports = {
transform,
// Disable caching of babel transforms all together. We haven't found a good
// way to cache our transforms, as they rely on side effects like env vars or
// the existence of folders in the file system.
getCacheKey: () => Math.random().toString(36),
process(src, filename, config, options) {
return transform({
src,
filename,
config,
options: {...options, isTestRunner: true},
});
},
};

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
*/
const Metro = require('metro'); // no typings :(
import * as path from 'path';
function hash(string: string) {
let hash = 0;
if (string.length === 0) {
return hash;
}
let chr;
for (let i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0;
}
return hash;
}
const fileToIdMap = new Map();
const createModuleIdFactory = () => (filePath: string) => {
if (filePath === '__prelude__') {
return 0;
}
let id = fileToIdMap.get(filePath);
if (typeof id !== 'number') {
id = hash(filePath);
fileToIdMap.set(filePath, id);
}
return id;
};
export default async function runBuild(
inputDirectory: string,
entry: string,
out: string,
) {
await Metro.runBuild(
{
reporter: {update: () => {}},
projectRoot: inputDirectory,
watchFolders: [inputDirectory, path.resolve(__dirname, '..', '..')],
serializer: {
getRunModuleStatement: (moduleID: string) =>
`module.exports = global.__r(${moduleID}).default;`,
createModuleIdFactory,
},
transformer: {
babelTransformerPath: path.resolve(
__dirname,
'..',
'transforms',
'index.js',
),
},
},
{
dev: false,
minify: false,
resetCache: true,
sourceMap: true,
entry,
out,
},
);
}

View File

@@ -0,0 +1,36 @@
/**
* 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 util from 'util';
import {exec as execImport} from 'child_process';
const exec = util.promisify(execImport);
const WINDOWS = /^win/.test(process.platform);
const YARN_PATH = 'yarn' + (WINDOWS ? '.cmd' : '');
export async function install(pkgDir: string) {
const {stderr} = await exec(YARN_PATH, {
cwd: pkgDir,
});
if (stderr) {
console.warn(stderr);
}
}
export async function pack(pkgDir: string, out: string) {
const {stderr} = await exec(
[YARN_PATH, 'pack', '--filename', out].join(' '),
{
cwd: pkgDir,
},
);
if (stderr) {
console.warn(stderr);
}
}

16
desktop/pkg/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"declaration": true,
"outDir": "lib",
"rootDir": "src",
"strict": true,
"importHelpers": true,
"esModuleInterop": true,
"allowJs": true
},
"include": [
"src/**/*"
]
}

7
desktop/pkg/tslint.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"],
"rules": {
"interface-name": false,
"variable-name": false
}
}

5742
desktop/pkg/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
/**
* 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 fs from 'fs';
import path from 'path';
import lineReplace from 'line-replace';
import yazl from 'yazl';
const {exec: createBinary} = require('pkg');
import {
buildFolder,
compile,
compileDefaultPlugins,
getVersionNumber,
genMercurialRevision,
} from './build-utils';
const PLUGINS_FOLDER_NAME = 'plugins';
function preludeBundle(
dir: string,
versionNumber: string,
buildRevision: string | null,
) {
const revisionStr =
buildRevision == null ? '' : `global.__REVISION__="${buildRevision}";`;
return new Promise(resolve =>
lineReplace({
file: path.join(dir, 'bundle.js'),
line: 1,
text: `var __DEV__=false; global.electronRequire = require; global.performance = require("perf_hooks").performance;global.__VERSION__="${versionNumber}";${revisionStr}`,
addNewLine: true,
callback: resolve,
}),
);
}
async function createZip(buildDir: string, distDir: string, targets: string[]) {
return new Promise(resolve => {
const zip = new yazl.ZipFile();
// add binaries for each target
targets.forEach(target => {
const binary = `flipper-${target === 'mac' ? 'macos' : target}`;
zip.addFile(path.join(buildDir, binary), binary);
});
// add plugins
const pluginDir = path.join(buildDir, PLUGINS_FOLDER_NAME);
fs.readdirSync(pluginDir).forEach(file => {
zip.addFile(
path.join(pluginDir, file),
path.join(PLUGINS_FOLDER_NAME, file),
);
});
// write zip file
zip.outputStream
.pipe(fs.createWriteStream(path.join(distDir, 'Flipper-headless.zip')))
.on('close', resolve);
zip.end();
});
}
(async () => {
const targets: {mac?: string; linux?: string; win?: string} = {};
let platformPostfix: string = '';
if (process.argv.indexOf('--mac') > -1) {
targets.mac = 'node10-macos-x64';
platformPostfix = '-macos';
}
if (process.argv.indexOf('--linux') > -1) {
targets.linux = 'node10-linux-x64';
platformPostfix = '-linux';
}
if (process.argv.indexOf('--win') > -1) {
targets.win = 'node10-win-x64';
platformPostfix = '-win';
}
const length = Object.keys(targets).length;
if (length === 0) {
throw new Error('No targets specified. eg. --mac, --win, or --linux');
} else if (length > 1) {
// platformPostfix is automatically added by pkg
platformPostfix = '';
}
// Compiling all plugins takes a long time. Use this flag for quicker
// developement iteration by not including any plugins.
const skipPlugins = process.argv.indexOf('--no-plugins') > -1;
process.env.BUILD_HEADLESS = 'true';
const buildDir = await buildFolder();
const distDir = path.join(__dirname, '..', '..', 'dist');
// eslint-disable-next-line no-console
console.log('Created build directory', buildDir);
await compile(buildDir, path.join(__dirname, '..', 'headless', 'index.tsx'));
const versionNumber = getVersionNumber();
const buildRevision = await genMercurialRevision();
await preludeBundle(buildDir, versionNumber, buildRevision);
await compileDefaultPlugins(
path.join(buildDir, PLUGINS_FOLDER_NAME),
skipPlugins,
);
await createBinary([
path.join(buildDir, 'bundle.js'),
'--output',
path.join(buildDir, `flipper${platformPostfix}`),
'--targets',
Object.values(targets).join(','),
'--debug',
]);
await createZip(buildDir, distDir, Object.keys(targets));
// eslint-disable-next-line no-console
console.log('✨ Done');
process.exit();
})();

194
desktop/scripts/build-release.ts Executable file
View File

@@ -0,0 +1,194 @@
/**
* 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 path from 'path';
import fs from 'fs-extra';
import {Platform, Arch, ElectronDownloadOptions, build} from 'electron-builder';
import {spawn} from 'promisify-child-process';
import {
buildFolder,
compile,
compileMain,
die,
compileDefaultPlugins,
getVersionNumber,
genMercurialRevision,
} from './build-utils';
import fetch from 'node-fetch';
import {getIcons, buildLocalIconPath, getIconURL} from '../src/utils/icons';
function generateManifest(versionNumber: string) {
const filePath = path.join(__dirname, '..', '..', 'dist');
if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath);
}
fs.writeFileSync(
path.join(__dirname, '..', '..', 'dist', 'manifest.json'),
JSON.stringify({
package: 'com.facebook.sonar',
version_name: versionNumber,
}),
);
}
function modifyPackageManifest(
buildFolder: string,
versionNumber: string,
hgRevision: string | null,
) {
// eslint-disable-next-line no-console
console.log('Creating package.json manifest');
const manifest = require('../package.json');
const manifestStatic = require('../static/package.json');
// The manifest's dependencies are bundled with the final app by
// electron-builder. We want to bundle the dependencies from the static-folder
// because all dependencies from the root-folder are already bundled by metro.
manifest.dependencies = manifestStatic.dependencies;
manifest.main = 'index.js';
manifest.version = versionNumber;
if (hgRevision != null) {
manifest.revision = hgRevision;
}
fs.writeFileSync(
path.join(buildFolder, 'package.json'),
JSON.stringify(manifest, null, ' '),
);
}
async function buildDist(buildFolder: string) {
const targetsRaw: Map<Platform, Map<Arch, string[]>>[] = [];
const postBuildCallbacks: (() => void)[] = [];
if (process.argv.indexOf('--mac') > -1) {
targetsRaw.push(Platform.MAC.createTarget(['dir']));
// You can build mac apps on Linux but can't build dmgs, so we separate those.
if (process.argv.indexOf('--mac-dmg') > -1) {
targetsRaw.push(Platform.MAC.createTarget(['dmg']));
}
postBuildCallbacks.push(() =>
spawn('zip', ['-qyr9', '../Flipper-mac.zip', 'Flipper.app'], {
cwd: path.join(__dirname, '..', '..', 'dist', 'mac'),
encoding: 'utf-8',
}),
);
}
if (process.argv.indexOf('--linux') > -1) {
targetsRaw.push(Platform.LINUX.createTarget(['zip']));
}
if (process.argv.indexOf('--win') > -1) {
targetsRaw.push(Platform.WINDOWS.createTarget(['zip']));
}
if (!targetsRaw.length) {
throw new Error('No targets specified. eg. --mac, --win, or --linux');
}
// merge all target maps into a single map
let targetsMerged: [Platform, Map<Arch, string[]>][] = [];
for (const target of targetsRaw) {
targetsMerged = targetsMerged.concat(Array.from(target));
}
const targets = new Map(targetsMerged);
const electronDownloadOptions: ElectronDownloadOptions = {};
if (process.env.electron_config_cache) {
electronDownloadOptions.cache = process.env.electron_config_cache;
}
try {
await build({
publish: 'never',
config: {
appId: `com.facebook.sonar`,
directories: {
buildResources: path.join(__dirname, '..', 'static'),
output: path.join(__dirname, '..', '..', 'dist'),
},
electronDownload: electronDownloadOptions,
npmRebuild: false,
},
projectDir: buildFolder,
targets,
});
return await Promise.all(postBuildCallbacks.map(p => p()));
} catch (err) {
return die(err);
}
}
function copyStaticFolder(buildFolder: string) {
fs.copySync(path.join(__dirname, '..', 'static'), buildFolder, {
dereference: true,
});
}
function downloadIcons(buildFolder: string) {
const iconURLs = Object.entries(getIcons()).reduce<
{
name: string;
size: number;
density: number;
}[]
>((acc, [name, sizes]) => {
acc.push(
// get icons in @1x and @2x
...sizes.map(size => ({name, size, density: 1})),
...sizes.map(size => ({name, size, density: 2})),
);
return acc;
}, []);
return Promise.all(
iconURLs.map(({name, size, density}) => {
const url = getIconURL(name, size, density);
return fetch(url)
.then(res => {
if (res.status !== 200) {
throw new Error(
// eslint-disable-next-line prettier/prettier
`Could not download the icon ${name} from ${url}: got status ${
res.status
}`,
);
}
return res;
})
.then(
res =>
new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(
path.join(buildFolder, buildLocalIconPath(name, size, density)),
);
res.body.pipe(fileStream);
res.body.on('error', reject);
fileStream.on('finish', resolve);
}),
);
}),
);
}
(async () => {
const dir = await buildFolder();
// eslint-disable-next-line no-console
console.log('Created build directory', dir);
await compileMain({dev: false});
copyStaticFolder(dir);
await downloadIcons(dir);
await compileDefaultPlugins(path.join(dir, 'defaultPlugins'));
await compile(dir, path.join(__dirname, '..', 'src', 'init.tsx'));
const versionNumber = getVersionNumber();
const hgRevision = await genMercurialRevision();
modifyPackageManifest(dir, versionNumber, hgRevision);
generateManifest(versionNumber);
await buildDist(dir);
// eslint-disable-next-line no-console
console.log('✨ Done');
process.exit();
})();

View File

@@ -0,0 +1,183 @@
/**
* 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
*/
const Metro = require('../static/node_modules/metro');
import compilePlugins from '../static/compilePlugins';
import util from 'util';
import tmp from 'tmp';
import path from 'path';
import fs from 'fs-extra';
import {spawn} from 'promisify-child-process';
import recursiveReaddir from 'recursive-readdir';
async function mostRecentlyChanged(
dir: string,
ignores: string[],
): Promise<Date> {
const files = await util.promisify<string, string[], string[]>(
recursiveReaddir,
)(dir, ignores);
return files
.map(f => fs.lstatSync(f).ctime)
.reduce((a, b) => (a > b ? a : b), new Date(0));
}
export function die(err: Error) {
console.error(err.stack);
process.exit(1);
}
export function compileDefaultPlugins(
defaultPluginDir: string,
skipAll: boolean = false,
) {
return compilePlugins(
null,
skipAll
? []
: [
path.join(__dirname, '..', 'src', 'plugins'),
path.join(__dirname, '..', 'src', 'fb', 'plugins'),
],
defaultPluginDir,
{force: true, failSilently: false, recompileOnChanges: false},
)
.then(defaultPlugins =>
fs.writeFileSync(
path.join(defaultPluginDir, 'index.json'),
JSON.stringify(
defaultPlugins.map(({entry, rootDir, out, ...plugin}) => ({
...plugin,
out: path.parse(out).base,
})),
),
),
)
.catch(die);
}
export function compile(buildFolder: string, entry: string) {
console.log(`⚙️ Compiling renderer bundle...`);
const projectRoots = path.join(__dirname, '..');
return Metro.runBuild(
{
reporter: {update: () => {}},
projectRoot: projectRoots,
watchFolders: [projectRoots],
serializer: {},
transformer: {
babelTransformerPath: path.join(
__dirname,
'..',
'static',
'transforms',
'index.js',
),
},
resolver: {
blacklistRE: /(\/|\\)(sonar|flipper|flipper-public)(\/|\\)(desktop)(\/|\\)(dist|doctor)(\/|\\)|(\.native\.js$)/,
},
},
{
dev: false,
minify: false,
resetCache: true,
sourceMap: true,
entry,
out: path.join(buildFolder, 'bundle.js'),
},
)
.then(() => console.log('✅ Compiled renderer bundle.'))
.catch(die);
}
export async function compileMain({dev}: {dev: boolean}) {
const staticDir = path.resolve(__dirname, '..', 'static');
const out = path.join(staticDir, 'main.bundle.js');
// check if main needs to be compiled
if (await fs.pathExists(out)) {
const staticDirCtime = await mostRecentlyChanged(staticDir, ['*.bundle.*']);
const bundleCtime = (await fs.lstat(out)).ctime;
if (staticDirCtime < bundleCtime) {
console.log(`🥫 Using cached version of main bundle...`);
return;
}
}
console.log(`⚙️ Compiling main bundle...`);
try {
const config = Object.assign({}, await Metro.loadConfig(), {
reporter: {update: () => {}},
projectRoot: staticDir,
watchFolders: [staticDir],
transformer: {
babelTransformerPath: path.join(
__dirname,
'..',
'static',
'transforms',
'index.js',
),
},
resolver: {
sourceExts: ['tsx', 'ts', 'js'],
blacklistRE: /(\/|\\)(sonar|flipper|flipper-public)(\/|\\)(desktop)(\/|\\)(dist|doctor)(\/|\\)|(\.native\.js$)/,
},
});
await Metro.runBuild(config, {
platform: 'web',
entry: path.join(staticDir, 'main.ts'),
out,
dev,
minify: false,
sourceMap: true,
resetCache: true,
});
console.log('✅ Compiled main bundle.');
} catch (err) {
die(err);
}
}
export function buildFolder(): Promise<string> {
// eslint-disable-next-line no-console
console.log('Creating build directory');
return new Promise<string>((resolve, reject) => {
tmp.dir({prefix: 'flipper-build-'}, (err, buildFolder) => {
if (err) {
reject(err);
} else {
resolve(buildFolder);
}
});
}).catch(e => {
die(e);
return '';
});
}
export function getVersionNumber() {
let {version} = require('../package.json');
const buildNumber = process.argv.join(' ').match(/--version=(\d+)/);
if (buildNumber && buildNumber.length > 0) {
version = [...version.split('.').slice(0, 2), buildNumber[1]].join('.');
}
return version;
}
// Asynchronously determine current mercurial revision as string or `null` in case of any error.
export function genMercurialRevision(): Promise<string | null> {
return spawn('hg', ['log', '-r', '.', '-T', '{node}'], {encoding: 'utf8'})
.then(
res =>
(res &&
(typeof res.stdout === 'string'
? res.stdout
: res.stdout?.toString())) ||
null,
)
.catch(() => null);
}

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* 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.
*
* @noformat
*/
/**
* WARNING: this file should be able to run on node v6.16.0, which is used at SandCastle.
* Please run `nvm use 6.16.0` before testing changes in this file!
*/
const path = require('path');
const fs = require('fs');
const cp = require('child_process');
const desktopRoot = path.resolve(__dirname, '..');
const root = path.resolve(desktopRoot, '..');
const version = JSON.parse(fs.readFileSync(path.join(desktopRoot, 'package.json'), 'utf8')).version;
const now = new Date();
const date = `${now.getDate()}/${now.getMonth() + 1}/${now.getFullYear()}`;
const newlineMarker = '__NEWLINE_MARKER__';
const fChangelog = path.resolve(root, 'CHANGELOG.md');
const lastCommit = cp
.execSync(`hg log --limit 1 --template '{node}'`, {cwd: root})
.toString();
const firstCommit = cp
.execSync(
`hg log --limit 1 --template '{node}' --keyword 'Flipper Release: v'`,
{cwd: root}
)
.toString();
console.log(
`Generating changelog for version ${version} based on ${firstCommit}..${lastCommit}`
);
// # get all commit summaries since last release | find all changelog entries, but make sure there is only one line per commit by temporarily replacing newlines
const hgLogCommand = `hg log -r "${firstCommit}::${lastCommit} and file('../*')" --template "{phabdiff} - {sub('\n','${newlineMarker}', desc)}\n"`;
const hgLog = cp.execSync(hgLogCommand, {cwd: __dirname}).toString();
const diffRe = /^D\d+/;
const changeLogLineRe = /(^changelog:\s*?)(.*?)$/i;
let contents = `# ${version} (${date})\n\n`;
let changes = 0;
hgLog
.split('\n')
.filter(line => diffRe.test(line))
.forEach(line => {
// Grab the diff nr from every line in the output
const diff = line.trim().match(diffRe)[0];
// unfold the lines generated by hg log again
line.split(newlineMarker).forEach(diffline => {
// if a line starts with changelog:, grab the rest of the text and add it to the changelog
const match = diffline.match(changeLogLineRe);
if (match) {
changes++;
contents += ` * ${diff} - ${match[2]}\n`;
}
});
});
if (!changes) {
console.log('No diffs with changelog items found in this release');
} else {
contents += '\n\n' + fs.readFileSync(fChangelog, 'utf8');
fs.writeFileSync(fChangelog, contents);
}

View File

@@ -0,0 +1,65 @@
/**
* 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
*/
const generate = require('babel-generator').default;
const babylon = require('babylon');
const babel = require('babel-core');
const metro = require('metro');
exports.transform = function({
filename,
options,
src,
plugins: defaultPlugins,
}) {
const presets = [];
let ast = babylon.parse(src, {
filename,
plugins: ['jsx', 'flow', 'classProperties', 'objectRestSpread'],
sourceType: filename.includes('node_modules') ? 'script' : 'module',
});
// run babel
const plugins = [
...defaultPlugins,
require('./babel-plugins/electron-requires.js'),
require('./babel-plugins/dynamic-requires.js'),
];
if (!filename.includes('node_modules')) {
plugins.unshift(require('babel-plugin-transform-es2015-modules-commonjs'));
}
ast = babel.transformFromAst(ast, src, {
babelrc: !filename.includes('node_modules'),
code: false,
comments: false,
compact: false,
filename,
plugins,
presets,
sourceMaps: true,
}).ast;
const result = generate(
ast,
{
filename,
sourceFileName: filename,
sourceMaps: true,
},
src,
);
return {
ast,
code: result.code,
filename,
map: result.rawMappings.map(metro.sourceMaps.compactMapping),
};
};

View File

@@ -0,0 +1,31 @@
/**
* 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
*/
const path = require('path');
const util = require('util');
const {exists: existsImport, copyFile} = require('fs');
const exists = util.promisify(existsImport);
const desktopRootDir = path.resolve(__dirname, '..');
const rootDir = path.resolve(desktopRootDir, '..');
const hasGit = exists(path.join(rootDir, '.git'));
async function prepareWatchmanConfig(dir) {
const hasWatchmanConfig = exists(path.join(dir, '.watchmanconfig'));
if ((await hasGit) && !(await hasWatchmanConfig)) {
console.log(`Creating .watchmanconfig in ${dir}`);
await util.promisify(copyFile)(
path.join(dir, '_watchmanconfig'),
path.join(dir, '.watchmanconfig'),
);
}
}
prepareWatchmanConfig(rootDir);
prepareWatchmanConfig(path.join(desktopRootDir, 'static'));

View File

@@ -0,0 +1,265 @@
/**
* 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
*/
const electronBinary: string = require('electron') as any;
import codeFrame from 'babel-code-frame';
import socketIo from 'socket.io';
import express, {Express} from 'express';
import detect from 'detect-port';
import child from 'child_process';
import AnsiToHtmlConverter from 'ansi-to-html';
import chalk from 'chalk';
import http from 'http';
import path from 'path';
import fs from 'fs';
import {compileMain} from './build-utils';
import Watchman from '../static/watchman';
const Metro = require('../static/node_modules/metro');
const MetroResolver = require('../static/node_modules/metro-resolver');
const ansiToHtmlConverter = new AnsiToHtmlConverter();
const DEFAULT_PORT = (process.env.PORT || 3000) as number;
const STATIC_DIR = path.join(__dirname, '..', 'static');
let shutdownElectron: (() => void) | undefined = undefined;
function launchElectron({
devServerURL,
bundleURL,
electronURL,
}: {
devServerURL: string;
bundleURL: string;
electronURL: string;
}) {
const args = [
path.join(STATIC_DIR, 'index.js'),
'--remote-debugging-port=9222',
...process.argv,
];
const proc = child.spawn(electronBinary, args, {
cwd: STATIC_DIR,
env: {
...process.env,
SONAR_ROOT: process.cwd(),
BUNDLE_URL: bundleURL,
ELECTRON_URL: electronURL,
DEV_SERVER_URL: devServerURL,
},
stdio: 'inherit',
});
const electronCloseListener = () => {
process.exit();
};
const processExitListener = () => {
proc.kill();
};
proc.on('close', electronCloseListener);
process.on('exit', processExitListener);
return () => {
proc.off('close', electronCloseListener);
process.off('exit', processExitListener);
proc.kill();
};
}
function startMetroServer(app: Express) {
const projectRoot = path.join(__dirname, '..');
return Metro.runMetro({
projectRoot,
watchFolders: [projectRoot],
transformer: {
babelTransformerPath: path.join(
__dirname,
'..',
'static',
'transforms',
'index.js',
),
},
resolver: {
blacklistRE: /(\/|\\)(sonar|flipper|flipper-public)(\/|\\)(desktop)(\/|\\)(dist|doctor)(\/|\\)|(\.native\.js$)/,
resolveRequest: (context: any, moduleName: string, platform: string) => {
if (moduleName.startsWith('./localhost:3000')) {
moduleName = moduleName.replace('./localhost:3000', '.');
}
return MetroResolver.resolve(
{...context, resolveRequest: null},
moduleName,
platform,
);
},
},
watch: true,
}).then((metroBundlerServer: any) => {
app.use(metroBundlerServer.processRequest.bind(metroBundlerServer));
});
}
function startAssetServer(
port: number,
): Promise<{app: Express; server: http.Server}> {
const app = express();
app.use((req, res, next) => {
if (knownErrors[req.url] != null) {
delete knownErrors[req.url];
outputScreen();
}
next();
});
app.use((req, res, next) => {
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
next();
});
app.post('/_restartElectron', (req, res) => {
if (shutdownElectron) {
shutdownElectron();
}
shutdownElectron = launchElectron({
devServerURL: `http://localhost:${port}`,
bundleURL: `http://localhost:${port}/src/init.bundle?dev=true&platform=web&minify=false&excludeSource=false`,
electronURL: `http://localhost:${port}/index.dev.html`,
});
res.end();
});
app.get('/', (req, res) => {
fs.readFile(path.join(STATIC_DIR, 'index.dev.html'), (err, content) => {
res.end(content);
});
});
app.use(express.static(STATIC_DIR));
app.use(function(err: any, req: any, res: any, _next: any) {
knownErrors[req.url] = err;
outputScreen();
res.status(500).send('Something broke, check the console!');
});
const server = http.createServer(app);
return new Promise(resolve => {
server.listen(port, 'localhost', () => resolve({app, server}));
});
}
async function addWebsocket(server: http.Server) {
const io = socketIo(server);
// notify connected clients that there's errors in the console
io.on('connection', client => {
if (hasErrors()) {
client.emit('hasErrors', ansiToHtmlConverter.toHtml(buildErrorScreen()));
}
});
// refresh the app on changes to the src folder
// this can be removed once metroServer notifies us about file changes
try {
const watchman = new Watchman(path.resolve(__dirname, '..', 'src'));
await watchman.initialize();
await watchman.startWatchFiles(
'',
() => {
io.emit('refresh');
},
{
excludes: [
'**/__tests__/**/*',
'**/node_modules/**/*',
'**/.*',
'plugins/**/*', // plugin changes are tracked separately, so exlcuding them here to avoid double reloading.
],
},
);
} catch (err) {
console.error(
'Failed to start watching for changes using Watchman, continue without hot reloading',
err,
);
}
return io;
}
const knownErrors: {[key: string]: any} = {};
function hasErrors() {
return Object.keys(knownErrors).length > 0;
}
function buildErrorScreen() {
const lines = [
chalk.red(`✖ Found ${Object.keys(knownErrors).length} errors`),
'',
];
for (const url in knownErrors) {
const err = knownErrors[url];
if (err.filename != null && err.lineNumber != null && err.column != null) {
lines.push(chalk.inverse(err.filename));
lines.push();
lines.push(err.message);
lines.push(
codeFrame(
fs.readFileSync(err.filename, 'utf8'),
err.lineNumber,
err.column,
),
);
} else {
lines.push(err.stack);
}
lines.push('');
}
return lines.join('\n');
}
function outputScreen(socket?: socketIo.Server) {
// output screen
if (hasErrors()) {
const errorScreen = buildErrorScreen();
console.error(errorScreen);
// notify live clients of errors
socket?.emit('hasErrors', ansiToHtmlConverter.toHtml(errorScreen));
} else {
// eslint-disable-next-line no-console
console.log(chalk.green('✔ No known errors'));
}
}
(async () => {
const port = await detect(DEFAULT_PORT);
const {app, server} = await startAssetServer(port);
const socket = await addWebsocket(server);
await startMetroServer(app);
outputScreen(socket);
await compileMain({dev: true});
shutdownElectron = launchElectron({
devServerURL: `http://localhost:${port}`,
bundleURL: `http://localhost:${port}/src/init.bundle`,
electronURL: `http://localhost:${port}/index.dev.html`,
});
})();

View File

@@ -0,0 +1,65 @@
/**
* 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 path from 'path';
import util from 'util';
import globImport from 'glob';
import {exec as execImport} from 'child_process';
const glob = util.promisify(globImport);
const exec = util.promisify(execImport);
const PACKAGES = [
'headless-tests',
'static',
'src/plugins/*',
'src/fb/plugins/*',
'src/fb/plugins/layout/*',
];
const WINDOWS = /^win/.test(process.platform);
const YARN_PATH =
process.argv.length > 2
? path.join(__dirname, process.argv[2])
: 'yarn' + (WINDOWS ? '.cmd' : '');
Promise.all(
PACKAGES.map(pattern =>
glob(path.join(__dirname, '..', pattern, 'package.json')),
),
)
.then(async packages => {
const flattenPackages = packages.reduce((acc, cv) => acc.concat(cv), []);
console.log(
`Installing dependencies for ${flattenPackages.length} plugins`,
);
for (const pkg of flattenPackages) {
const {stderr} = await exec(
// This script is itself executed by yarn (as postinstall script),
// therefore another yarn instance is running, while we are trying to
// install the plugin dependencies. We are setting a different port
// for the mutex of this yarn instance to make sure, it is not blocked
// by the yarn instance which is executing this script. Otherwise this
// will cause a deadlock.
[YARN_PATH, '--mutex', 'network:30330'].join(' '),
{
cwd: pkg.replace('/package.json', ''),
},
);
if (stderr) {
console.warn(stderr);
} else {
console.log(`Installed dependencies for ${pkg}`);
}
}
})
// eslint-disable-next-line
.then(() => console.log('📦 Installed all plugin dependencies!'))
.catch(err => {
console.error('❌ Installing plugin dependencies failed.');
console.error(err);
process.exit(1);
});

22
desktop/src/.eslintrc.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* 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
*/
// Eslint rules cascade. Rules defined here are merged with the rules
// defined in the parent directory.
// Somewhat ironically, this particular file is not actually transformed by Babel
// and can't have ES6 imports/exports.
// eslint-disable-line import/no-commonjs
module.exports = {
plugins: ['import'],
rules: {
// Import rules from https://www.npmjs.com/package/eslint-plugin-import
'import/no-commonjs': 1,
},
};

173
desktop/src/App.tsx Normal file
View File

@@ -0,0 +1,173 @@
/**
* 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 {FlexColumn, FlexRow} from 'flipper';
import {connect} from 'react-redux';
import TitleBar from './chrome/TitleBar';
import MainSidebar2 from './chrome/mainsidebar/MainSidebar2';
import BugReporterDialog from './chrome/BugReporterDialog';
import ErrorBar from './chrome/ErrorBar';
import DoctorBar from './chrome/DoctorBar';
import ShareSheetExportUrl from './chrome/ShareSheetExportUrl';
import SignInSheet from './chrome/SignInSheet';
import ExportDataPluginSheet from './chrome/ExportDataPluginSheet';
import ShareSheetExportFile from './chrome/ShareSheetExportFile';
import JSEmulatorLauncherSheet from './chrome/JSEmulatorLauncherSheet';
import PluginContainer from './PluginContainer';
import Sheet from './chrome/Sheet';
import {ipcRenderer, remote} from 'electron';
import {
ActiveSheet,
ShareType,
ACTIVE_SHEET_BUG_REPORTER,
ACTIVE_SHEET_PLUGINS,
ACTIVE_SHEET_SHARE_DATA,
ACTIVE_SHEET_SIGN_IN,
ACTIVE_SHEET_SETTINGS,
ACTIVE_SHEET_DOCTOR,
ACTIVE_SHEET_SHARE_DATA_IN_FILE,
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
ACTIVE_SHEET_PLUGIN_SHEET,
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
} from './reducers/application';
import {Logger} from './fb-interfaces/Logger';
import BugReporter from './fb-stubs/BugReporter';
import {State as Store} from './reducers/index';
import {StaticView, FlipperError} from './reducers/connections';
import PluginManager from './chrome/plugin-manager/PluginManager';
import StatusBar from './chrome/StatusBar';
import SettingsSheet from './chrome/SettingsSheet';
import DoctorSheet from './chrome/DoctorSheet';
const version = remote.app.getVersion();
type OwnProps = {
logger: Logger;
bugReporter: BugReporter;
};
type StateFromProps = {
leftSidebarVisible: boolean;
errors: FlipperError[];
activeSheet: ActiveSheet;
share: ShareType | null;
staticView: StaticView;
};
type Props = StateFromProps & OwnProps;
export class App extends React.Component<Props> {
componentDidMount() {
// track time since launch
const [s, ns] = process.hrtime();
const launchEndTime = s * 1e3 + ns / 1e6;
ipcRenderer.on('getLaunchTime', (_: any, launchStartTime: number) => {
this.props.logger.track(
'performance',
'launchTime',
launchEndTime - launchStartTime,
);
});
ipcRenderer.send('getLaunchTime');
ipcRenderer.send('componentDidMount');
}
getSheet = (onHide: () => any) => {
const {activeSheet} = this.props;
switch (activeSheet) {
case ACTIVE_SHEET_BUG_REPORTER:
return (
<BugReporterDialog
bugReporter={this.props.bugReporter}
onHide={onHide}
/>
);
case ACTIVE_SHEET_PLUGINS:
return <PluginManager onHide={onHide} />;
case ACTIVE_SHEET_SIGN_IN:
return <SignInSheet onHide={onHide} />;
case ACTIVE_SHEET_SETTINGS:
return <SettingsSheet platform={process.platform} onHide={onHide} />;
case ACTIVE_SHEET_DOCTOR:
return <DoctorSheet onHide={onHide} />;
case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT:
return <ExportDataPluginSheet onHide={onHide} />;
case ACTIVE_SHEET_SHARE_DATA:
return (
<ShareSheetExportUrl
onHide={onHide}
logger={this.props.logger}
closeOnFinish={
this.props.share != null && this.props.share.closeOnFinish
}
/>
);
case ACTIVE_SHEET_SHARE_DATA_IN_FILE:
return this.props.share && this.props.share.type === 'file' ? (
<ShareSheetExportFile
onHide={onHide}
file={this.props.share.file}
logger={this.props.logger}
/>
) : (
(() => {
console.error('No file provided when calling share sheet.');
return null;
})()
);
case ACTIVE_SHEET_PLUGIN_SHEET:
// Currently unused.
return null;
case ACTIVE_SHEET_JS_EMULATOR_LAUNCHER:
return <JSEmulatorLauncherSheet onHide={onHide} />;
default:
return null;
}
};
render() {
return (
<FlexColumn grow={true}>
<TitleBar version={version} />
<DoctorBar />
<ErrorBar />
<Sheet>{this.getSheet}</Sheet>
<FlexRow grow={true}>
{this.props.leftSidebarVisible && <MainSidebar2 />}
{this.props.staticView != null ? (
React.createElement(this.props.staticView, {
logger: this.props.logger,
})
) : (
<PluginContainer logger={this.props.logger} />
)}
<div
id="flipper-out-of-contents-container"
style={{width: '100%', display: 'none'}}
/>
</FlexRow>
<StatusBar />
</FlexColumn>
);
}
}
export default connect<StateFromProps, {}, OwnProps, Store>(
({
application: {leftSidebarVisible, activeSheet, share},
connections: {errors, staticView},
}) => ({
leftSidebarVisible,
activeSheet,
share: share,
errors,
staticView,
}),
)(App);

599
desktop/src/Client.tsx Normal file
View File

@@ -0,0 +1,599 @@
/**
* 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 {FlipperPlugin, FlipperDevicePlugin} from './plugin';
import BaseDevice, {OS} from './devices/BaseDevice';
import {App} from './App.js';
import {Logger} from './fb-interfaces/Logger';
import {Store} from './reducers/index';
import {setPluginState} from './reducers/pluginStates';
import {Payload, ConnectionStatus} from 'rsocket-types';
import {Flowable, Single} from 'rsocket-flowable';
import {performance} from 'perf_hooks';
import {reportPlatformFailures, reportPluginFailures} from './utils/metrics';
import {notNull} from './utils/typeUtils';
import {default as isProduction} from './utils/isProduction';
import {registerPlugins} from './reducers/plugins';
import createTableNativePlugin from './plugins/TableNativePlugin';
import EventEmitter from 'events';
import invariant from 'invariant';
import {flipperRecorderAddEvent} from './utils/pluginStateRecorder';
import {getPluginKey} from './utils/pluginUtils';
import {
processMessageImmediately,
processMessageLater,
} from './utils/messageQueue';
import GK from './fb-stubs/GK';
type Plugins = Array<string>;
export type ClientQuery = {
app: string;
os: OS;
device: string;
device_id: string;
sdk_version?: number;
};
export type ClientExport = {
id: string;
query: ClientQuery;
};
type ErrorType = {message: string; stacktrace: string; name: string};
type Params = {
api: string;
method: string;
params?: Object;
};
type RequestMetadata = {method: string; id: number; params: Params | undefined};
const handleError = (store: Store, device: BaseDevice, error: ErrorType) => {
if (isProduction()) {
return;
}
const crashReporterPlugin = store
.getState()
.plugins.devicePlugins.get('CrashReporter');
if (!crashReporterPlugin) {
return;
}
const pluginKey = getPluginKey(null, device, 'CrashReporter');
const persistedState = {
...crashReporterPlugin.defaultPersistedState,
...store.getState().pluginStates[pluginKey],
};
const isCrashReport: boolean = Boolean(error.name || error.message);
const payload = isCrashReport
? {
name: error.name,
reason: error.message,
callstack: error.stacktrace,
}
: {
name: 'Plugin Error',
reason: JSON.stringify(error),
};
const newPluginState =
crashReporterPlugin.persistedStateReducer == null
? persistedState
: crashReporterPlugin.persistedStateReducer(
persistedState,
'flipper-crash-report',
payload,
);
if (persistedState !== newPluginState) {
store.dispatch(
setPluginState({
pluginKey,
state: newPluginState,
}),
);
}
};
export interface FlipperClientConnection<D, M> {
connectionStatus(): Flowable<ConnectionStatus>;
close(): void;
fireAndForget(payload: Payload<D, M>): void;
requestResponse(payload: Payload<D, M>): Single<Payload<D, M>>;
}
export default class Client extends EventEmitter {
app: App | undefined;
connected: boolean;
id: string;
query: ClientQuery;
sdkVersion: number;
messageIdCounter: number;
plugins: Plugins;
connection: FlipperClientConnection<any, any> | null | undefined;
store: Store;
activePlugins: Set<string>;
device: Promise<BaseDevice>;
_deviceResolve: (device: BaseDevice) => void = _ => {};
_deviceSet: false | BaseDevice = false;
logger: Logger;
lastSeenDeviceList: Array<BaseDevice>;
broadcastCallbacks: Map<string, Map<string, Set<Function>>>;
rIC: any;
requestCallbacks: Map<
number,
{
resolve: (data: any) => void;
reject: (err: Error) => void;
metadata: RequestMetadata;
// eslint-disable-next-line prettier/prettier
}
>;
constructor(
id: string,
query: ClientQuery,
conn: FlipperClientConnection<any, any> | null | undefined,
logger: Logger,
store: Store,
plugins?: Plugins | null | undefined,
device?: BaseDevice,
) {
super();
this.connected = true;
this.plugins = plugins ? plugins : [];
this.connection = conn;
this.id = id;
this.query = query;
this.sdkVersion = query.sdk_version || 0;
this.messageIdCounter = 0;
this.logger = logger;
this.store = store;
this.broadcastCallbacks = new Map();
this.requestCallbacks = new Map();
this.activePlugins = new Set();
this.lastSeenDeviceList = [];
this.device = device
? Promise.resolve(device)
: new Promise((resolve, _reject) => {
this._deviceResolve = resolve;
});
if (device) {
this._deviceSet = device;
}
const client = this;
// node.js doesn't support requestIdleCallback
this.rIC =
typeof window === 'undefined' || !window.requestIdleCallback
? (cb: Function, _: any) => {
cb();
}
: window.requestIdleCallback.bind(window);
if (conn) {
conn.connectionStatus().subscribe({
onNext(payload) {
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
client.connected = false;
}
},
onSubscribe(subscription) {
subscription.request(Number.MAX_SAFE_INTEGER);
},
});
}
}
/* All clients should have a corresponding Device in the store.
However, clients can connect before a device is registered, so wait a
while for the device to be registered if it isn't already. */
setMatchingDevice(): void {
if (this._deviceSet) {
return;
}
reportPlatformFailures(
new Promise<BaseDevice>((resolve, reject) => {
let unsubscribe: () => void = () => {};
const device = this.store
.getState()
.connections.devices.find(
device => device.serial === this.query.device_id,
);
if (device) {
resolve(device);
return;
}
const timeout = setTimeout(() => {
unsubscribe();
const error = `Timed out waiting for device for client ${this.id}`;
console.error(error);
reject(error);
}, 5000);
unsubscribe = this.store.subscribe(() => {
const newDeviceList = this.store.getState().connections.devices;
if (newDeviceList === this.lastSeenDeviceList) {
return;
}
this.lastSeenDeviceList = this.store.getState().connections.devices;
const matchingDevice = newDeviceList.find(
device => device.serial === this.query.device_id,
);
if (matchingDevice) {
clearTimeout(timeout);
resolve(matchingDevice);
unsubscribe();
}
});
}),
'client-setMatchingDevice',
).then(device => {
this._deviceSet = device;
this._deviceResolve(device);
});
}
supportsPlugin(Plugin: typeof FlipperPlugin): boolean {
return this.plugins.includes(Plugin.id);
}
async init() {
this.setMatchingDevice();
await this.getPlugins();
}
// get the supported plugins
async getPlugins(): Promise<Plugins> {
const plugins = await this.rawCall<{plugins: Plugins}>(
'getPlugins',
false,
).then(data => data.plugins);
this.plugins = plugins;
const nativeplugins = plugins
.map(plugin => /_nativeplugin_([^_]+)_([^_]+)/.exec(plugin))
.filter(notNull)
.map(([id, type, title]) => {
// TODO put this in another component, and make the "types" registerable
switch (type) {
case 'Table':
return createTableNativePlugin(id, title);
default: {
return null;
}
}
})
.filter(Boolean);
this.store.dispatch(registerPlugins(nativeplugins as any));
return plugins;
}
// get the plugins, and update the UI
async refreshPlugins() {
await this.getPlugins();
this.emit('plugins-change');
}
async deviceSerial(): Promise<string> {
try {
const device = await this.device;
if (!device) {
console.error('Using "" for deviceId device is not ready');
return '';
}
return device.serial;
} catch (e) {
console.error(
'Using "" for deviceId because client has no matching device',
);
return '';
}
}
onMessage(msg: string) {
if (typeof msg !== 'string') {
return;
}
let rawData;
try {
rawData = JSON.parse(msg);
} catch (err) {
console.error(`Invalid JSON: ${msg}`, 'clientMessage');
return;
}
const data: {
id?: number;
method?: string;
params?: Params;
success?: Object;
error?: ErrorType;
} = rawData;
const {id, method} = data;
if (id == null) {
const {error} = data;
if (error != null) {
console.error(
`Error received from device ${
method ? `when calling ${method}` : ''
}: ${error.message} + \nDevice Stack Trace: ${error.stacktrace}`,
'deviceError',
);
this.device.then(device => handleError(this.store, device, error));
} else if (method === 'refreshPlugins') {
this.refreshPlugins();
} else if (method === 'execute') {
invariant(data.params, 'expected params');
const params: Params = data.params;
const device = this.getDeviceSync();
if (device) {
const persistingPlugin:
| typeof FlipperPlugin
| typeof FlipperDevicePlugin
| undefined =
this.store.getState().plugins.clientPlugins.get(params.api) ||
this.store.getState().plugins.devicePlugins.get(params.api);
if (persistingPlugin && persistingPlugin.persistedStateReducer) {
const pluginKey = getPluginKey(this.id, device, params.api);
flipperRecorderAddEvent(pluginKey, params.method, params.params);
if (GK.get('flipper_event_queue')) {
processMessageLater(
this.store,
pluginKey,
persistingPlugin,
params,
);
} else {
processMessageImmediately(
this.store,
pluginKey,
persistingPlugin,
params,
);
}
}
} else {
console.warn(
`Received a message for plugin ${params.api}.${params.method}, which will be ignored because the device has not connected yet`,
);
}
const apiCallbacks = this.broadcastCallbacks.get(params.api);
if (!apiCallbacks) {
return;
}
const methodCallbacks = apiCallbacks.get(params.method);
if (methodCallbacks) {
for (const callback of methodCallbacks) {
callback(params.params);
}
}
}
return; // method === 'execute'
}
if (this.sdkVersion < 1) {
const callbacks = this.requestCallbacks.get(id);
if (!callbacks) {
return;
}
this.requestCallbacks.delete(id);
this.finishTimingRequestResponse(callbacks.metadata);
this.onResponse(data, callbacks.resolve, callbacks.reject);
}
}
onResponse(
data: {
success?: Object;
error?: ErrorType;
},
resolve: ((a: any) => any) | undefined,
reject: (error: ErrorType) => any,
) {
if (data.success) {
resolve && resolve(data.success);
} else if (data.error) {
reject(data.error);
const {error} = data;
if (error) {
this.device.then(device => handleError(this.store, device, error));
}
} else {
// ???
}
}
toJSON(): ClientExport {
return {id: this.id, query: this.query};
}
subscribe(api: string, method: string, callback: (params: Object) => void) {
let apiCallbacks = this.broadcastCallbacks.get(api);
if (!apiCallbacks) {
apiCallbacks = new Map();
this.broadcastCallbacks.set(api, apiCallbacks);
}
let methodCallbacks = apiCallbacks.get(method);
if (!methodCallbacks) {
methodCallbacks = new Set();
apiCallbacks.set(method, methodCallbacks);
}
methodCallbacks.add(callback);
}
unsubscribe(api: string, method: string, callback: Function) {
const apiCallbacks = this.broadcastCallbacks.get(api);
if (!apiCallbacks) {
return;
}
const methodCallbacks = apiCallbacks.get(method);
if (!methodCallbacks) {
return;
}
methodCallbacks.delete(callback);
}
rawCall<T>(method: string, fromPlugin: boolean, params?: Params): Promise<T> {
return new Promise((resolve, reject) => {
const id = this.messageIdCounter++;
const metadata: RequestMetadata = {
method,
id,
params,
};
if (this.sdkVersion < 1) {
this.requestCallbacks.set(id, {reject, resolve, metadata});
}
const data = {
id,
method,
params,
};
const plugin = params ? params.api : undefined;
console.debug(data, 'message:call');
if (this.sdkVersion < 1) {
this.startTimingRequestResponse({method, id, params});
if (this.connection) {
this.connection.fireAndForget({data: JSON.stringify(data)});
}
return;
}
const mark = this.getPerformanceMark(metadata);
performance.mark(mark);
if (!fromPlugin || this.isAcceptingMessagesFromPlugin(plugin)) {
this.connection &&
this.connection
.requestResponse({data: JSON.stringify(data)})
.subscribe({
onComplete: payload => {
if (!fromPlugin || this.isAcceptingMessagesFromPlugin(plugin)) {
const logEventName = this.getLogEventName(data);
this.logger.trackTimeSince(mark, logEventName);
const response: {
success?: Object;
error?: ErrorType;
} = JSON.parse(payload.data);
this.onResponse(response, resolve, reject);
}
},
// Open fresco then layout and you get errors because responses come back after deinit.
onError: e => {
if (this.isAcceptingMessagesFromPlugin(plugin)) {
reject(e);
}
},
});
}
});
}
getDeviceSync(): BaseDevice | undefined {
return this._deviceSet || undefined;
}
startTimingRequestResponse(data: RequestMetadata) {
performance.mark(this.getPerformanceMark(data));
}
finishTimingRequestResponse(data: RequestMetadata) {
const mark = this.getPerformanceMark(data);
const logEventName = this.getLogEventName(data);
this.logger.trackTimeSince(mark, logEventName);
}
isAcceptingMessagesFromPlugin(plugin: string | null | undefined) {
return this.connection && (!plugin || this.activePlugins.has(plugin));
}
getPerformanceMark(data: RequestMetadata): string {
const {method, id} = data;
return `request_response_${method}_${id}`;
}
getLogEventName(data: RequestMetadata): string {
const {method, params} = data;
return params && params.api && params.method
? `request_response_${method}_${params.api}_${params.method}`
: `request_response_${method}`;
}
initPlugin(pluginId: string) {
this.activePlugins.add(pluginId);
this.rawSend('init', {plugin: pluginId});
}
deinitPlugin(pluginId: string) {
this.activePlugins.delete(pluginId);
this.rawSend('deinit', {plugin: pluginId});
}
rawSend(method: string, params?: Object): void {
const data = {
method,
params,
};
console.debug(data, 'message:send');
if (this.connection) {
this.connection.fireAndForget({data: JSON.stringify(data)});
}
}
call(
api: string,
method: string,
fromPlugin: boolean,
params?: Object,
): Promise<Object> {
return reportPluginFailures(
this.rawCall('execute', fromPlugin, {api, method, params}),
`Call-${method}`,
api,
);
}
send(api: string, method: string, params?: Object): void {
if (!isProduction()) {
console.warn(
`${api}:${method ||
''} client.send() is deprecated. Please use call() instead so you can handle errors.`,
);
}
return this.rawSend('execute', {api, method, params});
}
async supportsMethod(api: string, method: string): Promise<boolean> {
if (this.sdkVersion < 2) {
return Promise.resolve(false);
}
const response = await this.rawCall<{
isSupported: boolean;
}>('isMethodSupported', true, {
api,
method,
});
return response.isSupported;
}
}

446
desktop/src/MenuBar.tsx Normal file
View File

@@ -0,0 +1,446 @@
/**
* 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 {FlipperPlugin, FlipperDevicePlugin} from './plugin';
import {
showOpenDialog,
startFileExport,
startLinkExport,
} from './utils/exportData';
import {
setActiveSheet,
ACTIVE_SHEET_PLUGINS,
ACTIVE_SHEET_SETTINGS,
} from './reducers/application';
import {setStaticView} from './reducers/connections';
import SupportRequestFormV2 from './fb-stubs/SupportRequestFormV2';
import {Store} from './reducers/';
import electron, {MenuItemConstructorOptions} from 'electron';
import {notNull} from './utils/typeUtils';
import constants from './fb-stubs/constants';
export type DefaultKeyboardAction = 'clear' | 'goToBottom' | 'createPaste';
export type TopLevelMenu = 'Edit' | 'View' | 'Window' | 'Help';
export type KeyboardAction = {
action: string;
label: string;
accelerator?: string;
topLevelMenu: TopLevelMenu;
};
const defaultKeyboardActions: Array<KeyboardAction> = [
{
label: 'Clear',
accelerator: 'CmdOrCtrl+K',
topLevelMenu: 'View',
action: 'clear',
},
{
label: 'Go To Bottom',
accelerator: 'CmdOrCtrl+B',
topLevelMenu: 'View',
action: 'goToBottom',
},
{
label: 'Create Paste',
topLevelMenu: 'Edit',
action: 'createPaste',
},
];
export type KeyboardActions = Array<DefaultKeyboardAction | KeyboardAction>;
const menuItems: Map<string, electron.MenuItem> = new Map();
let pluginActionHandler: ((action: string) => void) | null;
function actionHandler(action: string) {
if (pluginActionHandler) {
pluginActionHandler(action);
} else {
console.warn(`Unhandled keyboard action "${action}".`);
}
}
export function setupMenuBar(
plugins: Array<typeof FlipperPlugin | typeof FlipperDevicePlugin>,
store: Store,
) {
const template = getTemplate(
electron.remote.app,
electron.remote.shell,
store,
);
// collect all keyboard actions from all plugins
const registeredActions: Set<KeyboardAction> = new Set(
plugins
.map(plugin => plugin.keyboardActions || [])
.reduce((acc: KeyboardActions, cv) => acc.concat(cv), [])
.map((action: DefaultKeyboardAction | KeyboardAction) =>
typeof action === 'string'
? defaultKeyboardActions.find(a => a.action === action)
: action,
)
.filter(notNull),
);
// add keyboard actions to
registeredActions.forEach(keyboardAction => {
if (keyboardAction != null) {
appendMenuItem(template, actionHandler, keyboardAction);
}
});
// create actual menu instance
const applicationMenu = electron.remote.Menu.buildFromTemplate(template);
// add menu items to map, so we can modify them easily later
registeredActions.forEach(keyboardAction => {
if (keyboardAction != null) {
const {topLevelMenu, label, action} = keyboardAction;
const menu = applicationMenu.items.find(
menuItem => menuItem.label === topLevelMenu,
);
if (menu && menu.submenu) {
const menuItem = menu.submenu.items.find(
menuItem => menuItem.label === label,
);
menuItem && menuItems.set(action, menuItem);
}
}
});
// update menubar
electron.remote.Menu.setApplicationMenu(applicationMenu);
}
function appendMenuItem(
template: Array<MenuItemConstructorOptions>,
actionHandler: (action: string) => void,
item: KeyboardAction,
) {
const keyboardAction = item;
if (keyboardAction == null) {
return;
}
const itemIndex = template.findIndex(
menu => menu.label === keyboardAction.topLevelMenu,
);
if (itemIndex > -1 && template[itemIndex].submenu != null) {
(template[itemIndex].submenu as MenuItemConstructorOptions[]).push({
click: () => actionHandler(keyboardAction.action),
label: keyboardAction.label,
accelerator: keyboardAction.accelerator,
enabled: false,
});
}
}
export function activateMenuItems(
activePlugin:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>,
) {
// disable all keyboard actions
for (const item of menuItems) {
item[1].enabled = false;
}
// set plugin action handler
if (activePlugin.onKeyboardAction) {
pluginActionHandler = activePlugin.onKeyboardAction;
}
// enable keyboard actions for the current plugin
if (activePlugin.constructor.keyboardActions != null) {
(activePlugin.constructor.keyboardActions || []).forEach(keyboardAction => {
const action =
typeof keyboardAction === 'string'
? keyboardAction
: keyboardAction.action;
const item = menuItems.get(action);
if (item != null) {
item.enabled = true;
}
});
}
// set the application menu again to make sure it updates
electron.remote.Menu.setApplicationMenu(
electron.remote.Menu.getApplicationMenu(),
);
}
function getTemplate(
app: electron.App,
shell: electron.Shell,
store: Store,
): Array<MenuItemConstructorOptions> {
const exportSubmenu = [
{
label: 'File...',
accelerator: 'CommandOrControl+E',
click: () => startFileExport(store.dispatch),
},
];
if (constants.ENABLE_SHAREABLE_LINK) {
exportSubmenu.push({
label: 'Shareable Link',
accelerator: 'CommandOrControl+Shift+E',
click: () => startLinkExport(store.dispatch),
});
}
const fileSubmenu: MenuItemConstructorOptions[] = [
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => store.dispatch(setActiveSheet(ACTIVE_SHEET_SETTINGS)),
},
{
label: 'Import Flipper File...',
accelerator: 'CommandOrControl+O',
click: function() {
showOpenDialog(store);
},
},
{
label: 'Export',
submenu: exportSubmenu,
},
];
const supportRequestSubmenu = [
{
label: 'Create...',
click: function() {
// Dispatch an action to open the export screen of Support Request form
store.dispatch(setStaticView(SupportRequestFormV2));
},
},
];
fileSubmenu.push({
label: 'Support Requests',
submenu: supportRequestSubmenu,
});
const template: MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: fileSubmenu,
},
{
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
role: 'undo',
},
{
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
role: 'redo',
},
{
type: 'separator',
},
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut',
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy',
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste',
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectAll',
},
],
},
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: function(
_,
focusedWindow: electron.BrowserWindow | undefined,
) {
if (focusedWindow) {
focusedWindow.reload();
}
},
},
{
label: 'Toggle Full Screen',
accelerator: (function() {
if (process.platform === 'darwin') {
return 'Ctrl+Command+F';
} else {
return 'F11';
}
})(),
click: function(
_,
focusedWindow: electron.BrowserWindow | undefined,
) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
},
},
{
label: 'Manage Plugins...',
click: function() {
store.dispatch(setActiveSheet(ACTIVE_SHEET_PLUGINS));
},
},
{
label: 'Toggle Developer Tools',
accelerator: (function() {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
} else {
return 'Ctrl+Shift+I';
}
})(),
click: function(
_,
focusedWindow: electron.BrowserWindow | undefined,
) {
if (focusedWindow) {
// @ts-ignore: https://github.com/electron/electron/issues/7832
focusedWindow.toggleDevTools();
}
},
},
{
type: 'separator',
},
],
},
{
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize',
},
{
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close',
},
],
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Getting started',
click: function() {
shell.openExternal(
'https://fbflipper.com/docs/getting-started.html',
);
},
},
{
label: 'Create plugins',
click: function() {
shell.openExternal(
'https://fbflipper.com/docs/tutorial/intro.html',
);
},
},
{
label: 'Report problems',
click: function() {
shell.openExternal('https://github.com/facebook/flipper/issues');
},
},
],
},
];
if (process.platform === 'darwin') {
const name = app.name;
template.unshift({
label: name,
submenu: [
{
label: 'About ' + name,
role: 'about',
},
{
type: 'separator',
},
{
label: 'Services',
role: 'services',
submenu: [],
},
{
type: 'separator',
},
{
label: 'Hide ' + name,
accelerator: 'Command+H',
role: 'hide',
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
role: 'hideOthers',
},
{
label: 'Show All',
role: 'unhide',
},
{
type: 'separator',
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: function() {
app.quit();
},
},
],
});
const windowMenu = template.find(function(m) {
return m.role === 'window';
});
if (windowMenu) {
(windowMenu.submenu as MenuItemConstructorOptions[]).push(
{
type: 'separator',
},
{
label: 'Bring All to Front',
role: 'front',
},
);
}
}
return template;
}

View File

@@ -0,0 +1,581 @@
/**
* 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 {SearchableProps, FlipperBasePlugin, FlipperPlugin} from 'flipper';
import {Logger} from './fb-interfaces/Logger';
import {
Searchable,
Button,
ButtonGroup,
FlexBox,
FlexColumn,
FlexRow,
Glyph,
ContextMenu,
styled,
colors,
} from 'flipper';
import {FlipperDevicePlugin} from './plugin';
import {connect} from 'react-redux';
import React, {Component, Fragment} from 'react';
import {clipboard} from 'electron';
import {
PluginNotification,
updatePluginBlacklist,
updateCategoryBlacklist,
} from './reducers/notifications';
import {selectPlugin} from './reducers/connections';
import {State as StoreState} from './reducers/index';
import textContent from './utils/textContent';
import createPaste from './fb-stubs/createPaste';
import {getPluginTitle} from './utils/pluginUtils';
type OwnProps = {
onClear: () => void;
selectedID: string | null | undefined;
logger: Logger;
} & Partial<SearchableProps>;
type StateFromProps = {
activeNotifications: Array<PluginNotification>;
invalidatedNotifications: Array<PluginNotification>;
blacklistedPlugins: Array<string>;
blacklistedCategories: Array<string>;
devicePlugins: Map<string, typeof FlipperDevicePlugin>;
clientPlugins: Map<string, typeof FlipperPlugin>;
};
type DispatchFromProps = {
selectPlugin: (payload: {
selectedPlugin: string | null;
selectedApp: string | null;
deepLinkPayload: string | null;
}) => any;
updatePluginBlacklist: (blacklist: Array<string>) => any;
updateCategoryBlacklist: (blacklist: Array<string>) => any;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
type State = {
selectedNotification: string | null | undefined;
};
const Content = styled(FlexColumn)({
padding: '0 10px',
backgroundColor: colors.light02,
overflow: 'scroll',
flexGrow: 1,
});
const Heading = styled(FlexBox)({
display: 'block',
alignItems: 'center',
marginTop: 15,
marginBottom: 5,
color: colors.macOSSidebarSectionTitle,
fontSize: 11,
fontWeight: 500,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
flexShrink: 0,
});
const NoContent = styled(FlexColumn)({
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
flexGrow: 1,
fontWeight: 500,
lineHeight: 2.5,
color: colors.light30,
});
class NotificationsTable extends Component<Props & SearchableProps, State> {
contextMenuItems = [{label: 'Clear all', click: this.props.onClear}];
state: State = {
selectedNotification: this.props.selectedID,
};
componentDidUpdate(prevProps: Props & SearchableProps) {
if (this.props.filters.length !== prevProps.filters.length) {
this.props.updatePluginBlacklist(
this.props.filters
.filter(f => f.type === 'exclude' && f.key.toLowerCase() === 'plugin')
.map(f => String(f.value)),
);
this.props.updateCategoryBlacklist(
this.props.filters
.filter(
f => f.type === 'exclude' && f.key.toLowerCase() === 'category',
)
.map(f => String(f.value)),
);
}
if (
this.props.selectedID &&
prevProps.selectedID !== this.props.selectedID
) {
this.setState({
selectedNotification: this.props.selectedID,
});
}
}
onHidePlugin = (pluginId: string) => {
// add filter to searchbar
this.props.addFilter({
value: pluginId,
type: 'exclude',
key: 'plugin',
});
this.props.updatePluginBlacklist(
this.props.blacklistedPlugins.concat(pluginId),
);
};
onHideCategory = (category: string) => {
// add filter to searchbar
this.props.addFilter({
value: category,
type: 'exclude',
key: 'category',
});
this.props.updatePluginBlacklist(
this.props.blacklistedCategories.concat(category),
);
};
getFilter = (): ((n: PluginNotification) => boolean) => (
n: PluginNotification,
) => {
const searchTerm = this.props.searchTerm.toLowerCase();
// filter plugins
const blacklistedPlugins = new Set(
this.props.blacklistedPlugins.map(p => p.toLowerCase()),
);
if (blacklistedPlugins.has(n.pluginId.toLowerCase())) {
return false;
}
// filter categories
const {category} = n.notification;
if (category) {
const blacklistedCategories = new Set(
this.props.blacklistedCategories.map(p => p.toLowerCase()),
);
if (blacklistedCategories.has(category.toLowerCase())) {
return false;
}
}
if (searchTerm.length === 0) {
return true;
} else if (n.notification.title.toLowerCase().indexOf(searchTerm) > -1) {
return true;
} else if (
typeof n.notification.message === 'string' &&
n.notification.message.toLowerCase().indexOf(searchTerm) > -1
) {
return true;
}
return false;
};
getPlugin = (id: string) =>
this.props.clientPlugins.get(id) || this.props.devicePlugins.get(id);
render() {
const activeNotifications = this.props.activeNotifications
.filter(this.getFilter())
.map((n: PluginNotification) => {
const {category} = n.notification;
return (
<NotificationItem
key={n.notification.id}
{...n}
plugin={this.getPlugin(n.pluginId)}
isSelected={this.state.selectedNotification === n.notification.id}
onHighlight={() =>
this.setState({selectedNotification: n.notification.id})
}
onClear={this.props.onClear}
onHidePlugin={() => this.onHidePlugin(n.pluginId)}
onHideCategory={
category ? () => this.onHideCategory(category) : undefined
}
selectPlugin={this.props.selectPlugin}
logger={this.props.logger}
/>
);
})
.reverse();
const invalidatedNotifications = this.props.invalidatedNotifications
.filter(this.getFilter())
.map((n: PluginNotification) => (
<NotificationItem
key={n.notification.id}
{...n}
plugin={this.getPlugin(n.pluginId)}
onClear={this.props.onClear}
inactive
/>
))
.reverse();
return (
<ContextMenu items={this.contextMenuItems} component={Content}>
{activeNotifications.length > 0 && (
<Fragment>
<Heading>Active notifications</Heading>
<FlexColumn shrink={false}>{activeNotifications}</FlexColumn>
</Fragment>
)}
{invalidatedNotifications.length > 0 && (
<Fragment>
<Heading>Past notifications</Heading>
<FlexColumn shrink={false}>{invalidatedNotifications}</FlexColumn>
</Fragment>
)}
{activeNotifications.length + invalidatedNotifications.length === 0 && (
<NoContent>
<Glyph
name="bell-null"
size={24}
variant="outline"
color={colors.light30}
/>
No Notifications
</NoContent>
)}
</ContextMenu>
);
}
}
export const ConnectedNotificationsTable = connect<
StateFromProps,
DispatchFromProps,
OwnProps,
StoreState
>(
({
notifications: {
activeNotifications,
invalidatedNotifications,
blacklistedPlugins,
blacklistedCategories,
},
plugins: {devicePlugins, clientPlugins},
}) => ({
activeNotifications,
invalidatedNotifications,
blacklistedPlugins,
blacklistedCategories,
devicePlugins,
clientPlugins,
}),
{
updatePluginBlacklist,
updateCategoryBlacklist,
selectPlugin,
},
)(Searchable(NotificationsTable));
const shadow = (
props: {isSelected?: boolean; inactive?: boolean},
_hover?: boolean,
) => {
if (props.inactive) {
return `inset 0 0 0 1px ${colors.light10}`;
}
const shadow = ['1px 1px 5px rgba(0,0,0,0.1)'];
if (props.isSelected) {
shadow.push(`inset 0 0 0 2px ${colors.macOSTitleBarIconSelected}`);
}
return shadow.join(',');
};
const SEVERITY_COLOR_MAP = {
warning: colors.yellow,
error: colors.red,
};
type NotificationBoxProps = {
inactive?: boolean;
isSelected?: boolean;
severity: keyof typeof SEVERITY_COLOR_MAP;
};
const NotificationBox = styled(FlexRow)<NotificationBoxProps>(props => ({
backgroundColor: props.inactive ? 'transparent' : colors.white,
opacity: props.inactive ? 0.5 : 1,
alignItems: 'flex-start',
borderRadius: 5,
padding: 10,
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
marginBottom: 10,
boxShadow: shadow(props),
'::before': {
content: '""',
display: !props.inactive && !props.isSelected ? 'block' : 'none',
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 3,
backgroundColor: SEVERITY_COLOR_MAP[props.severity] || colors.info,
},
':hover': {
boxShadow: shadow(props, true),
'& > *': {
opacity: 1,
},
},
}));
const Title = styled.div({
minWidth: 150,
color: colors.light80,
flexShrink: 0,
marginBottom: 6,
fontWeight: 500,
lineHeight: 1,
fontSize: '1.1em',
});
const NotificationContent = styled(FlexColumn)<{isSelected?: boolean}>(
props => ({
marginLeft: 6,
marginRight: 10,
flexGrow: 1,
overflow: 'hidden',
maxHeight: props.isSelected ? 'none' : 56,
lineHeight: 1.4,
color: props.isSelected ? colors.light50 : colors.light30,
}),
);
const Actions = styled(FlexRow)({
alignItems: 'center',
justifyContent: 'space-between',
color: colors.light20,
marginTop: 12,
borderTop: `1px solid ${colors.light05}`,
paddingTop: 8,
});
const NotificationButton = styled.div({
border: `1px solid ${colors.light20}`,
color: colors.light50,
borderRadius: 4,
textAlign: 'center',
padding: 4,
width: 80,
marginBottom: 4,
opacity: 0,
transition: '0.15s opacity',
'[data-role="notification"]:hover &': {
opacity: 0.5,
},
':last-child': {
marginBottom: 0,
},
'[data-role="notification"] &:hover': {
opacity: 1,
},
});
type ItemProps = {
onHighlight?: () => any;
onHidePlugin?: () => any;
onHideCategory?: () => any;
onClear?: () => any;
isSelected?: boolean;
inactive?: boolean;
selectPlugin?: (payload: {
selectedPlugin: string | null;
selectedApp: string | null;
deepLinkPayload: string | null;
}) => any;
logger?: Logger;
plugin: typeof FlipperBasePlugin | null | undefined;
};
type ItemState = {
reportedNotHelpful: boolean;
};
class NotificationItem extends Component<
ItemProps & PluginNotification,
ItemState
> {
constructor(props: ItemProps & PluginNotification) {
super(props);
const items = [];
if (props.onHidePlugin && props.plugin) {
items.push({
label: `Hide ${getPluginTitle(props.plugin)} plugin`,
click: this.props.onHidePlugin,
});
}
if (props.onHideCategory) {
items.push({
label: 'Hide Similar',
click: this.props.onHideCategory,
});
}
items.push(
{label: 'Copy', click: this.copy},
{label: 'Create Paste', click: this.createPaste},
);
this.contextMenuItems = items;
}
state = {reportedNotHelpful: false};
contextMenuItems: Array<{label: string; click: (() => any) | undefined}>;
deepLinkButton = React.createRef();
createPaste = () => {
createPaste(this.getContent());
};
copy = () => clipboard.writeText(this.getContent());
getContent = (): string =>
[
this.props.notification.timestamp,
`[${this.props.notification.severity}] ${this.props.notification.title}`,
this.props.notification.action,
this.props.notification.category,
textContent(this.props.notification.message),
]
.filter(Boolean)
.join('\n');
openDeeplink = () => {
const {notification, pluginId, client} = this.props;
if (this.props.selectPlugin && notification.action) {
this.props.selectPlugin({
selectedPlugin: pluginId,
selectedApp: client,
deepLinkPayload: notification.action,
});
}
};
reportNotUseful = (e: React.MouseEvent<any>) => {
e.preventDefault();
e.stopPropagation();
if (this.props.logger) {
this.props.logger.track(
'usage',
'notification-not-useful',
this.props.notification,
);
}
this.setState({reportedNotHelpful: true});
};
onHide = (e: React.MouseEvent<any>) => {
e.preventDefault();
e.stopPropagation();
if (this.props.onHideCategory) {
this.props.onHideCategory();
} else if (this.props.onHidePlugin) {
this.props.onHidePlugin();
}
};
render() {
const {
notification,
isSelected,
inactive,
onHidePlugin,
onHideCategory,
plugin,
} = this.props;
const {action} = notification;
return (
<ContextMenu<React.ComponentProps<typeof NotificationBox>>
data-role="notification"
component={NotificationBox}
severity={notification.severity}
onClick={this.props.onHighlight}
isSelected={isSelected}
inactive={inactive}
items={this.contextMenuItems}>
<Glyph name={(plugin ? plugin.icon : 'bell') || 'bell'} size={12} />
<NotificationContent isSelected={isSelected}>
<Title>{notification.title}</Title>
{notification.message}
{!inactive &&
isSelected &&
plugin &&
(action || onHidePlugin || onHideCategory) && (
<Actions>
<FlexRow>
{action && (
<Button onClick={this.openDeeplink}>
Open in {getPluginTitle(plugin)}
</Button>
)}
<ButtonGroup>
{onHideCategory && (
<Button onClick={onHideCategory}>Hide similar</Button>
)}
{onHidePlugin && (
<Button onClick={onHidePlugin}>
Hide {getPluginTitle(plugin)}
</Button>
)}
</ButtonGroup>
</FlexRow>
<span>
{notification.timestamp
? new Date(notification.timestamp).toTimeString()
: ''}
</span>
</Actions>
)}
</NotificationContent>
{action && !inactive && !isSelected && (
<FlexColumn style={{alignSelf: 'center'}}>
{action && (
<NotificationButton onClick={this.openDeeplink}>
Open
</NotificationButton>
)}
{this.state.reportedNotHelpful ? (
<NotificationButton onClick={this.onHide}>
Hide
</NotificationButton>
) : (
<NotificationButton onClick={this.reportNotUseful}>
Not helpful
</NotificationButton>
)}
</FlexColumn>
)}
</ContextMenu>
);
}
}

View File

@@ -0,0 +1,457 @@
/**
* 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 {
FlipperPlugin,
FlipperDevicePlugin,
Props as PluginProps,
} from './plugin';
import {Logger} from './fb-interfaces/Logger';
import BaseDevice from './devices/BaseDevice';
import {pluginKey as getPluginKey} from './reducers/pluginStates';
import Client from './Client';
import {
ErrorBoundary,
FlexColumn,
FlexRow,
colors,
styled,
ArchivedDevice,
Glyph,
Label,
VBox,
View,
} from 'flipper';
import {
StaticView,
setStaticView,
pluginIsStarred,
starPlugin,
} from './reducers/connections';
import React, {PureComponent} from 'react';
import {connect, ReactReduxContext} from 'react-redux';
import {setPluginState} from './reducers/pluginStates';
import {selectPlugin} from './reducers/connections';
import {State as Store, MiddlewareAPI} from './reducers/index';
import {activateMenuItems} from './MenuBar';
import {Message} from './reducers/pluginMessageQueue';
import {Idler} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue';
import {ToggleButton, SmallText} from './ui';
const Container = styled(FlexColumn)({
width: 0,
flexGrow: 1,
flexShrink: 1,
backgroundColor: colors.white,
});
export const SidebarContainer = styled(FlexRow)({
backgroundColor: colors.light02,
height: '100%',
overflow: 'scroll',
});
const Waiting = styled(FlexColumn)({
width: '100%',
height: '100%',
flexGrow: 1,
background: colors.light02,
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
});
function ProgressBar({progress}: {progress: number}) {
return (
<ProgressBarContainer>
<ProgressBarBar progress={progress} />
</ProgressBarContainer>
);
}
const ProgressBarContainer = styled.div({
border: `1px solid ${colors.cyan}`,
borderRadius: 4,
width: 300,
});
const ProgressBarBar = styled.div<{progress: number}>(({progress}) => ({
background: colors.cyan,
width: `${Math.min(100, Math.round(progress * 100))}%`,
height: 8,
}));
type OwnProps = {
logger: Logger;
};
type StateFromProps = {
pluginState: Object;
activePlugin: typeof FlipperPlugin | typeof FlipperDevicePlugin | null;
target: Client | BaseDevice | null;
pluginKey: string | null;
deepLinkPayload: string | null;
selectedApp: string | null;
isArchivedDevice: boolean;
pendingMessages: Message[] | undefined;
pluginIsEnabled: boolean;
};
type DispatchFromProps = {
selectPlugin: (payload: {
selectedPlugin: string | null;
selectedApp?: string | null;
deepLinkPayload: string | null;
}) => any;
setPluginState: (payload: {pluginKey: string; state: any}) => void;
setStaticView: (payload: StaticView) => void;
starPlugin: typeof starPlugin;
};
type Props = StateFromProps & DispatchFromProps & OwnProps;
type State = {
progress: {current: number; total: number};
};
class PluginContainer extends PureComponent<Props, State> {
static contextType = ReactReduxContext;
plugin:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined;
refChanged = (
ref:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined,
) => {
if (this.plugin) {
this.plugin._teardown();
this.plugin = null;
}
if (ref && this.props.target) {
activateMenuItems(ref);
ref._init();
this.props.logger.trackTimeSince(`activePlugin-${ref.constructor.id}`);
this.plugin = ref;
}
};
idler?: Idler;
pluginBeingProcessed: string | null = null;
state = {progress: {current: 0, total: 0}};
get store(): MiddlewareAPI {
return this.context.store;
}
componentWillUnmount() {
if (this.plugin) {
this.plugin._teardown();
this.plugin = null;
}
this.cancelCurrentQueue();
}
componentDidMount() {
this.processMessageQueue();
}
componentDidUpdate() {
this.processMessageQueue();
}
processMessageQueue() {
const {
pluginKey,
pendingMessages,
activePlugin,
pluginIsEnabled,
} = this.props;
if (pluginKey !== this.pluginBeingProcessed) {
this.pluginBeingProcessed = pluginKey;
this.cancelCurrentQueue();
this.setState({progress: {current: 0, total: 0}});
if (
pluginIsEnabled &&
activePlugin &&
activePlugin.persistedStateReducer &&
pluginKey &&
pendingMessages?.length
) {
const start = Date.now();
this.idler = new Idler();
processMessageQueue(
activePlugin,
pluginKey,
this.store,
progress => {
this.setState({progress});
},
this.idler,
).then(completed => {
const duration = Date.now() - start;
this.props.logger.track(
'duration',
'queue-processing-before-plugin-open',
{
completed,
duration,
},
activePlugin.id,
);
});
}
}
}
cancelCurrentQueue() {
if (this.idler && !this.idler.isCancelled()) {
this.idler.cancel();
}
}
render() {
const {
activePlugin,
pluginKey,
target,
pendingMessages,
pluginIsEnabled,
} = this.props;
if (!activePlugin || !target || !pluginKey) {
console.warn(`No selected plugin. Rendering empty!`);
return null;
}
if (!pluginIsEnabled) {
return this.renderPluginEnabler();
}
if (!pendingMessages || pendingMessages.length === 0) {
return this.renderPlugin();
}
return this.renderPluginLoader();
}
renderPluginEnabler() {
const activePlugin = this.props.activePlugin!;
return (
<View grow>
<Waiting>
<VBox>
<FlexRow>
<Label
style={{
fontSize: '16px',
color: colors.light30,
textTransform: 'uppercase',
}}>
{activePlugin.title}
</Label>
</FlexRow>
</VBox>
<VBox>
<ToggleButton
toggled={false}
onClick={() => {
this.props.starPlugin({
selectedPlugin: activePlugin.id,
selectedApp: (this.props.target as Client).query.app,
});
}}
large
/>
</VBox>
<VBox>
<SmallText>Click to enable this plugin</SmallText>
</VBox>
</Waiting>
</View>
);
}
renderPluginLoader() {
return (
<View grow>
<Waiting>
<VBox>
<Glyph
name="dashboard"
variant="outline"
size={24}
color={colors.light30}
/>
</VBox>
<VBox>
<Label>
Processing {this.state.progress.total} events for{' '}
{this.props.activePlugin?.id ?? 'plugin'}
</Label>
</VBox>
<VBox>
<ProgressBar
progress={this.state.progress.current / this.state.progress.total}
/>
</VBox>
</Waiting>
</View>
);
}
renderPlugin() {
const {
pluginState,
setPluginState,
activePlugin,
pluginKey,
target,
isArchivedDevice,
selectedApp,
} = this.props;
if (!activePlugin || !target || !pluginKey) {
console.warn(`No selected plugin. Rendering empty!`);
return null;
}
const props: PluginProps<Object> & {
key: string;
ref: (
ref:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined,
) => void;
} = {
key: pluginKey,
logger: this.props.logger,
selectedApp,
persistedState: activePlugin.defaultPersistedState
? {
...activePlugin.defaultPersistedState,
...pluginState,
}
: pluginState,
setStaticView: (payload: StaticView) => this.props.setStaticView(payload),
setPersistedState: state => setPluginState({pluginKey, state}),
target,
deepLinkPayload: this.props.deepLinkPayload,
selectPlugin: (pluginID: string, deepLinkPayload: string | null) => {
const {target} = this.props;
// check if plugin will be available
if (
target instanceof Client &&
target.plugins.some(p => p === pluginID)
) {
this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload});
return true;
} else if (target instanceof BaseDevice) {
this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload});
return true;
} else {
return false;
}
},
ref: this.refChanged,
isArchivedDevice,
};
return (
<React.Fragment>
<Container key="plugin">
<ErrorBoundary
heading={`Plugin "${activePlugin.title ||
'Unknown'}" encountered an error during render`}>
{React.createElement(activePlugin, props)}
</ErrorBoundary>
</Container>
<SidebarContainer id="detailsSidebar" />
</React.Fragment>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
connections: {
selectedPlugin,
selectedDevice,
selectedApp,
clients,
deepLinkPayload,
userStarredPlugins,
},
pluginStates,
plugins: {devicePlugins, clientPlugins},
pluginMessageQueue,
}) => {
let pluginKey = null;
let target = null;
let activePlugin:
| typeof FlipperDevicePlugin
| typeof FlipperPlugin
| null = null;
let pluginIsEnabled = false;
if (selectedPlugin) {
activePlugin = devicePlugins.get(selectedPlugin) || null;
target = selectedDevice;
if (selectedDevice && activePlugin) {
pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id);
pluginIsEnabled = true;
} else {
target =
clients.find((client: Client) => client.id === selectedApp) || null;
activePlugin = clientPlugins.get(selectedPlugin) || null;
if (activePlugin && target) {
pluginKey = getPluginKey(target.id, activePlugin.id);
pluginIsEnabled = pluginIsStarred(
userStarredPlugins,
selectedApp,
activePlugin.id,
);
}
}
}
const isArchivedDevice = !selectedDevice
? false
: selectedDevice instanceof ArchivedDevice;
if (isArchivedDevice) {
pluginIsEnabled = true;
}
const pendingMessages = pluginKey
? pluginMessageQueue[pluginKey]
: undefined;
const s: StateFromProps = {
pluginState: pluginStates[pluginKey as string],
activePlugin: activePlugin,
target,
deepLinkPayload,
pluginKey,
isArchivedDevice,
selectedApp: selectedApp || null,
pendingMessages,
pluginIsEnabled,
};
return s;
},
{
setPluginState,
selectPlugin,
setStaticView,
starPlugin,
},
)(PluginContainer);

View File

@@ -0,0 +1,14 @@
/**
* 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
*/
export type UninitializedClient = {
os: string;
deviceName: string;
appName: string;
};

View File

@@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can create a Fake flipper 1`] = `
Object {
"androidEmulators": Array [],
"clients": Array [
Object {
"id": "TestApp#Android#MockAndroidDevice#serial",
"query": Object {
"app": "TestApp",
"device": "MockAndroidDevice",
"device_id": "serial",
"os": "Android",
},
},
],
"deepLinkPayload": null,
"devices": Array [
Object {
"deviceType": "physical",
"logs": Array [],
"os": "Android",
"serial": "serial",
"title": "MockAndroidDevice",
},
],
"errors": Array [],
"selectedApp": "TestApp#Android#MockAndroidDevice#serial",
"selectedDevice": Object {
"deviceType": "physical",
"logs": Array [],
"os": "Android",
"serial": "serial",
"title": "MockAndroidDevice",
},
"selectedPlugin": "TestPlugin",
"staticView": null,
"uninitializedClients": Array [],
"userPreferredApp": "TestApp#Android#MockAndroidDevice#serial",
"userPreferredDevice": "MockAndroidDevice",
"userPreferredPlugin": "TestPlugin",
"userStarredPlugins": Object {
"TestApp": Array [
"TestPlugin",
],
},
}
`;
exports[`can create a Fake flipper 2`] = `
Object {
"clientPlugins": Map {
"TestPlugin" => [Function],
},
"devicePlugins": Map {},
"disabledPlugins": Array [],
"failedPlugins": Array [],
"gatekeepedPlugins": Array [],
"selectedPlugins": Array [],
}
`;

View File

@@ -0,0 +1,63 @@
/**
* 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 {createMockFlipperWithPlugin} from '../test-utils/createMockFlipperWithPlugin';
import {FlipperPlugin} from '../plugin';
interface PersistedState {
count: 1;
}
class TestPlugin extends FlipperPlugin<any, any, any> {
static id = 'TestPlugin';
static defaultPersistedState = {
count: 0,
};
static persistedStateReducer(
persistedState: PersistedState,
method: string,
_payload: {},
) {
if (method === 'inc') {
return Object.assign({}, persistedState, {
count: persistedState.count + 1,
});
}
return persistedState;
}
render() {
return null;
}
}
test('can create a Fake flipper', async () => {
await createMockFlipperWithPlugin(
TestPlugin,
async ({client, device, store, sendMessage}) => {
expect(client).toBeTruthy();
expect(device).toBeTruthy();
expect(store).toBeTruthy();
expect(sendMessage).toBeTruthy();
expect(client.plugins.includes(TestPlugin.id)).toBe(true);
expect(store.getState().connections).toMatchSnapshot();
expect(store.getState().plugins).toMatchSnapshot();
sendMessage('inc', {});
expect(store.getState().pluginStates).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
"count": 1,
},
}
`);
},
);
});

View File

@@ -0,0 +1,80 @@
/**
* 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 {createTablePlugin} from '../createTablePlugin';
import {FlipperPlugin} from '../plugin';
import {List, Map as ImmutableMap} from 'immutable';
import {TableRows_immutable} from '../ui/components/table/types.js';
const PROPS = {
method: 'method',
resetMethod: 'resetMethod',
columns: {},
columnSizes: {},
renderSidebar: () => {},
buildRow: () => {},
};
type PersistedState<T> = {
rows: TableRows_immutable;
datas: ImmutableMap<string, T>;
};
type RowData = {
id: string;
};
test('createTablePlugin returns FlipperPlugin', () => {
const tablePlugin = createTablePlugin({...PROPS});
expect(tablePlugin.prototype).toBeInstanceOf(FlipperPlugin);
});
test('persistedStateReducer is resetting data', () => {
const resetMethod = 'resetMethod';
const tablePlugin = createTablePlugin<RowData>({...PROPS, resetMethod});
const ps: PersistedState<RowData> = {
datas: ImmutableMap({'1': {id: '1'}}),
rows: List([
{
key: '1',
columns: {
id: {
value: '1',
},
},
},
]),
};
const {rows, datas} = tablePlugin.persistedStateReducer(ps, resetMethod, {
id: '0',
});
expect(datas!.toJSON()).toEqual({});
expect(rows!.size).toBe(0);
});
test('persistedStateReducer is adding data', () => {
const method = 'method';
const tablePlugin = createTablePlugin({...PROPS, method});
const id = '1';
const {
rows,
datas,
} = tablePlugin.persistedStateReducer(
tablePlugin.defaultPersistedState,
method,
{id},
);
expect(rows!.size).toBe(1);
expect([...datas!.keys()]).toEqual([id]);
});

View File

@@ -0,0 +1,105 @@
/**
* 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 {FlexRow, colors, LoadingIndicator, Glyph, styled} from 'flipper';
import {remote} from 'electron';
import isProduction from '../utils/isProduction';
import React, {Component} from 'react';
import config from '../fb-stubs/config';
const Container = styled(FlexRow)({
alignItems: 'center',
});
type State = {
updater:
| 'error'
| 'checking-for-update'
| 'update-available'
| 'update-not-available'
| 'update-downloaded';
error?: string;
};
type Props = {
version: string;
};
export default class AutoUpdateVersion extends Component<Props, State> {
state: State = {
updater: 'update-not-available',
};
componentDidMount() {
if (isProduction()) {
// this will fail, if the app is not code signed
try {
remote.autoUpdater.setFeedURL({
url: `${config.updateServer}?version=${this.props.version}`,
});
} catch (e) {
console.error(e);
}
remote.autoUpdater.on('update-downloaded', () => {
this.setState({updater: 'update-downloaded'});
const notification = new Notification('Update available', {
body: 'Restart Flipper to update to the latest version.',
requireInteraction: true,
});
notification.onclick = remote.autoUpdater.quitAndInstall;
});
remote.autoUpdater.on('error', error => {
this.setState({updater: 'error', error: error.toString()});
});
remote.autoUpdater.on('checking-for-update', () => {
this.setState({updater: 'checking-for-update'});
});
remote.autoUpdater.on('update-available', () => {
this.setState({updater: 'update-available'});
});
remote.autoUpdater.on('update-not-available', () => {
this.setState({updater: 'update-not-available'});
});
remote.autoUpdater.checkForUpdates();
}
}
render() {
return (
<Container>
{this.state.updater === 'update-available' && (
<span title="Downloading new version">
<LoadingIndicator size={16} />
</span>
)}
{this.state.updater === 'error' && (
<span title={`Error fetching update: ${this.state.error || ''}`}>
<Glyph color={colors.light30} name="caution-triangle" />
</span>
)}
{this.state.updater === 'update-downloaded' && (
<span
tabIndex={-1}
role="button"
title="Update available. Restart Flipper."
onClick={remote.autoUpdater.quitAndInstall}>
<Glyph color={colors.light30} name="breaking-news" />
</span>
)}
</Container>
);
}
}

View File

@@ -0,0 +1,323 @@
/**
* 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 BugReporter from '../fb-stubs/BugReporter';
import {FlipperPlugin, FlipperDevicePlugin} from '../plugin';
import React, {Fragment, Component} from 'react';
import {connect} from 'react-redux';
import {
Button,
colors,
Link,
Input,
FlexColumn,
FlexRow,
FlexCenter,
Textarea,
Text,
Glyph,
styled,
} from 'flipper';
import {State as Store} from '../reducers';
const Container = styled(FlexColumn)({
padding: 10,
width: 400,
height: 300,
});
const Icon = styled(Glyph)({
marginRight: 8,
marginLeft: 3,
});
const Center = styled(Text)({
textAlign: 'center',
lineHeight: '130%',
paddingLeft: 20,
paddingRight: 20,
});
const Title = styled.div({
fontWeight: 500,
marginTop: 8,
marginLeft: 2,
marginBottom: 8,
});
const textareaStyle = {
margin: 0,
marginBottom: 10,
};
const TitleInput = styled(Input)({
...textareaStyle,
height: 30,
});
const DescriptionTextarea = styled(Textarea)({
...textareaStyle,
flexGrow: 1,
});
const SubmitButtonContainer = styled.div({
marginLeft: 'auto',
});
const Footer = styled(FlexRow)({
lineHeight: '24px',
});
const CloseDoneButton = styled(Button)({
marginTop: 20,
marginLeft: 'auto !important',
marginRight: 'auto',
});
const InfoBox = styled(FlexRow)({
marginBottom: 20,
lineHeight: '130%',
});
type State = {
description: string;
title: string;
submitting: boolean;
success: number | null | undefined;
error: string | null | undefined;
};
type OwnProps = {
bugReporter: BugReporter;
onHide: () => any;
};
type DispatchFromProps = {};
type StateFromProps = {
activePlugin:
| typeof FlipperPlugin
| typeof FlipperDevicePlugin
| null
| undefined;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class BugReporterDialog extends Component<Props, State> {
state = {
description: '',
title: '',
submitting: false,
success: null,
error: null,
};
titleRef?: HTMLElement | null;
descriptionRef?: HTMLElement | null;
onDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({description: e.target.value});
};
onTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({title: e.target.value});
};
onSubmit = () => {
// validate fields
const {title, description} = this.state;
if (!title) {
this.setState({
error: 'Title required.',
});
if (this.titleRef) {
this.titleRef.focus();
}
return;
}
if (!description) {
this.setState({
error: 'Description required.',
});
if (this.descriptionRef) {
this.descriptionRef.focus();
}
return;
}
this.setState(
{
error: null,
submitting: true,
},
() => {
// this will be called before the next repaint
requestAnimationFrame(() => {
// we have to call this again to ensure a repaint has actually happened
// as requestAnimationFrame is called BEFORE a repaint, not after which
// means we have to queue up twice to actually ensure a repaint has
// happened
requestAnimationFrame(() => {
this.props.bugReporter
.report(title, description)
.then((id: number) => {
this.setState({
submitting: false,
success: id,
});
})
.catch(err => {
this.setState({
error: err.message,
submitting: false,
});
});
});
});
},
);
};
setTitleRef = (ref: HTMLElement | null) => {
this.titleRef = ref;
};
setDescriptionRef = (ref: HTMLElement | null) => {
this.descriptionRef = ref;
};
onCancel = () => {
this.setState({
error: null,
title: '',
description: '',
});
this.props.onHide();
};
render() {
let content;
const {title, success, error, description, submitting} = this.state;
const {activePlugin} = this.props;
if (success) {
content = (
<FlexCenter grow={true}>
<FlexColumn>
<Center>
<Glyph
name="checkmark-circle"
size={24}
variant="outline"
color={colors.light30}
/>
<br />
<Title>Bug Report created</Title>
The bug report{' '}
<Link
href={`https://our.intern.facebook.com/intern/bug/${success}`}>
{success}
</Link>{' '}
was successfully created. Thank you for your help making Flipper
better!
</Center>
<CloseDoneButton onClick={this.onCancel} compact type="primary">
Close
</CloseDoneButton>
</FlexColumn>
</FlexCenter>
);
} else {
content = (
<Fragment>
<Title>Report a bug in Flipper</Title>
<TitleInput
placeholder="Title"
value={title}
ref={this.setTitleRef}
onChange={this.onTitleChange}
disabled={submitting}
/>
<DescriptionTextarea
placeholder="Describe your problem in as much detail as possible."
value={description}
ref={this.setDescriptionRef}
onChange={this.onDescriptionChange}
disabled={submitting}
/>
{activePlugin && activePlugin.bugs && (
<InfoBox>
<Icon color={colors.light50} name="info-circle" />
<span>
If your bug is related to the{' '}
<strong>
{(activePlugin && activePlugin.title) || activePlugin.id}{' '}
plugin
</strong>
{activePlugin && activePlugin.bugs && activePlugin.bugs.url && (
<span>
, you might find useful information about it here:{' '}
<Link href={activePlugin.bugs.url || ''}>
{activePlugin.bugs.url}
</Link>
</span>
)}
{activePlugin && activePlugin.bugs && activePlugin.bugs.email && (
<span>
, you might also want contact{' '}
<Link href={'mailto:' + String(activePlugin.bugs.email)}>
{activePlugin.bugs.email}
</Link>
, the author/oncall of this plugin, directly
</span>
)}
.
</span>
</InfoBox>
)}
<Footer>
{error != null && <Text color={colors.red}>{error}</Text>}
<SubmitButtonContainer>
<Button
onClick={this.onCancel}
disabled={submitting}
compact
padded>
Cancel
</Button>
<Button
type="primary"
onClick={this.onSubmit}
disabled={submitting}
compact
padded>
Submit Report
</Button>
</SubmitButtonContainer>
</Footer>
</Fragment>
);
}
return <Container>{content}</Container>;
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
plugins: {devicePlugins, clientPlugins},
connections: {selectedPlugin},
}) => ({
activePlugin: selectedPlugin
? devicePlugins.get(selectedPlugin) || clientPlugins.get(selectedPlugin)
: null,
}),
)(BugReporterDialog);

View File

@@ -0,0 +1,39 @@
/**
* 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 {FlexRow, Button} from '../ui/index';
import {styled, LoadingIndicator, Text} from 'flipper';
import React, {Component} from 'react';
import {colors} from '../ui/components/colors';
const Wrapper = styled(FlexRow)({
color: colors.light50,
alignItems: 'center',
marginLeft: 10,
});
type Props = {
msg: string;
onCancel: () => void;
};
export default class CancellableExportStatus extends Component<Props> {
render() {
const {msg, onCancel} = this.props;
return (
<Wrapper>
<LoadingIndicator size={16} />
&nbsp;
<Text>{msg}</Text>
&nbsp;
<Button onClick={onCancel}> Cancel </Button>
</Wrapper>
);
}
}

View File

@@ -0,0 +1,77 @@
/**
* 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 ReactDOM from 'react-dom';
import Sidebar from '../ui/components/Sidebar';
import {connect} from 'react-redux';
import {toggleRightSidebarAvailable} from '../reducers/application';
type OwnProps = {
children: any;
width?: number;
minWidth?: number;
};
type StateFromProps = {
rightSidebarVisible: boolean;
rightSidebarAvailable: boolean;
};
type DispatchFromProps = {
toggleRightSidebarAvailable: (visible?: boolean) => any;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class DetailSidebar extends React.Component<Props> {
componentDidMount() {
this.updateSidebarAvailablility();
}
componentDidUpdate() {
this.updateSidebarAvailablility();
}
updateSidebarAvailablility() {
const available = Boolean(this.props.children);
if (available !== this.props.rightSidebarAvailable) {
this.props.toggleRightSidebarAvailable(available);
}
}
render() {
const domNode = document.getElementById('detailsSidebar');
return (
this.props.children &&
this.props.rightSidebarVisible &&
domNode &&
ReactDOM.createPortal(
<Sidebar
minWidth={this.props.minWidth}
width={this.props.width || 300}
position="right">
{this.props.children}
</Sidebar>,
domNode,
)
);
}
}
// @TODO: TS_MIGRATION
type Store = any;
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({application: {rightSidebarVisible, rightSidebarAvailable}}) => ({
rightSidebarVisible,
rightSidebarAvailable,
}),
{
toggleRightSidebarAvailable,
},
)(DetailSidebar);

View File

@@ -0,0 +1,217 @@
/**
* 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 {Button, styled} from 'flipper';
import {connect, ReactReduxContext} from 'react-redux';
import {spawn} from 'child_process';
import {dirname} from 'path';
import {selectDevice, preferDevice} from '../reducers/connections';
import {
setActiveSheet,
ActiveSheet,
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
} from '../reducers/application';
import {default as which} from 'which';
import {showOpenDialog} from '../utils/exportData';
import BaseDevice from '../devices/BaseDevice';
import React, {Component} from 'react';
import {State} from '../reducers';
import GK from '../fb-stubs/GK';
type StateFromProps = {
selectedDevice: BaseDevice | null | undefined;
androidEmulators: Array<string>;
devices: Array<BaseDevice>;
};
type DispatchFromProps = {
selectDevice: (device: BaseDevice) => void;
preferDevice: (device: string) => void;
setActiveSheet: (sheet: ActiveSheet) => void;
};
type OwnProps = {};
type Props = OwnProps & StateFromProps & DispatchFromProps;
const DropdownButton = styled(Button)({
fontSize: 11,
});
class DevicesButton extends Component<Props> {
launchEmulator = (name: string) => {
// On Linux, you must run the emulator from the directory it's in because
// reasons ...
which('emulator')
.then(emulatorPath => {
if (emulatorPath) {
const child = spawn(emulatorPath, [`@${name}`], {
detached: true,
cwd: dirname(emulatorPath),
});
child.stderr.on('data', data => {
console.error(`Android emulator error: ${data}`);
});
child.on('error', console.error);
} else {
throw new Error('Could not get emulator path');
}
})
.catch(console.error);
this.props.preferDevice(name);
};
render() {
const {
devices,
androidEmulators,
selectedDevice,
selectDevice,
} = this.props;
let buttonLabel = 'No device selected';
let icon = 'minus-circle';
if (selectedDevice && selectedDevice.isArchived) {
buttonLabel = `${selectedDevice.displayTitle() || 'Unknown device'}`;
icon = 'box';
} else if (selectedDevice && selectedDevice.deviceType === 'physical') {
buttonLabel = selectedDevice.displayTitle() || 'Unknown device';
icon = 'mobile';
} else if (selectedDevice && selectedDevice.deviceType === 'emulator') {
buttonLabel = selectedDevice.displayTitle() || 'Unknown emulator';
icon = 'desktop';
}
const dropdown: any[] = [];
// Physical devices
const connectedDevices = [
{
label: 'Connected Devices',
enabled: false,
},
...devices
.filter(device => device.deviceType === 'physical')
.map((device: BaseDevice) => ({
click: () => selectDevice(device),
checked: device === selectedDevice,
label: `📱 ${device.displayTitle()}`,
type: 'checkbox',
})),
];
if (connectedDevices.length > 1) {
dropdown.push(...connectedDevices);
}
// Emulators
const runningEmulators = [
{
label: 'Running Emulators',
enabled: false,
},
...devices
.filter(device => device.deviceType === 'emulator')
.map((device: BaseDevice) => ({
click: () => selectDevice(device),
checked: device === selectedDevice,
label: device.displayTitle(),
type: 'checkbox',
})),
];
if (runningEmulators.length > 1) {
dropdown.push(...runningEmulators);
}
// Archived
const importedFiles = [
{
label: 'Disconnected Devices',
enabled: false,
},
...devices
.filter(device => device.isArchived)
.map((device: BaseDevice) => ({
click: () => selectDevice(device),
checked: device === selectedDevice,
label: `📦 ${device.displayTitle()}`,
type: 'checkbox',
})),
];
if (importedFiles.length > 1) {
dropdown.push(...importedFiles);
}
// Launch JS emulator
if (GK.get('flipper_js_client_emulator')) {
dropdown.push(
{type: 'separator' as 'separator'},
{
label: 'Launch JS Web App',
click: () =>
this.props.setActiveSheet(ACTIVE_SHEET_JS_EMULATOR_LAUNCHER),
},
);
}
// Launch Android emulators
if (androidEmulators.length > 0) {
const emulators = Array.from(androidEmulators)
.filter(
(name: string) =>
devices.findIndex(
(device: BaseDevice) =>
device.title === name && !device.isArchived,
) === -1,
)
.map((name: string) => ({
label: name,
click: () => this.launchEmulator(name),
}));
if (emulators.length > 0) {
dropdown.push(
{type: 'separator' as 'separator'},
{
label: 'Launch Android emulators',
enabled: false,
},
...emulators,
);
}
}
if (dropdown.length > 0) {
dropdown.push({type: 'separator' as 'separator'});
}
return (
<ReactReduxContext.Consumer>
{({store}) => {
dropdown.push({
label: 'Open File...',
click: () => {
showOpenDialog(store);
},
});
return (
<DropdownButton compact={true} icon={icon} dropdown={dropdown}>
{buttonLabel}
</DropdownButton>
);
}}
</ReactReduxContext.Consumer>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, State>(
({connections: {devices, androidEmulators, selectedDevice}}) => ({
devices,
androidEmulators,
selectedDevice,
}),
{
selectDevice,
preferDevice,
setActiveSheet,
},
)(DevicesButton);

View File

@@ -0,0 +1,189 @@
/**
* 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 {styled, colors} from 'flipper';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {
setActiveSheet,
ActiveSheet,
ACTIVE_SHEET_DOCTOR,
ACTIVE_SHEET_SETTINGS,
} from '../reducers/application';
import {State as Store} from '../reducers/index';
import {ButtonGroup, Button} from 'flipper';
import {FlexColumn, FlexRow} from 'flipper';
import runHealthchecks, {
HealthcheckSettings,
HealthcheckEventsHandler,
} from '../utils/runHealthchecks';
import {
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
HealthcheckReport,
HealthcheckResult,
} from '../reducers/healthchecks';
import {reportUsage} from '../utils/metrics';
type StateFromProps = {
healthcheckReport: HealthcheckReport;
} & HealthcheckSettings;
type DispatchFromProps = {
setActiveSheet: (payload: ActiveSheet) => void;
} & HealthcheckEventsHandler;
type State = {visible: boolean; message: string; showSettingsButton: boolean};
type Props = DispatchFromProps & StateFromProps;
class DoctorBar extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
visible: false,
message: '',
showSettingsButton: false,
};
}
componentDidMount() {
this.showMessageIfChecksFailed();
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
const failedCategories = Object.values(
props.healthcheckReport.categories,
).filter(cat => hasProblems(cat.result));
if (failedCategories.length == 1) {
const failedCat = failedCategories[0];
if (failedCat.key === 'ios' || failedCat.key === 'android') {
return {
...state,
message: `Doctor has discovered problems with your ${failedCat.label} setup. If you are not interested in ${failedCat.label} development you can disable it in Settings.`,
showSettingsButton: true,
};
}
}
if (failedCategories.length) {
return {
...state,
message: 'Doctor has discovered problems with your installation.',
showSettingsButton: false,
};
}
return null;
}
async showMessageIfChecksFailed() {
await runHealthchecks(this.props);
const result = this.props.healthcheckReport.result;
if (hasProblems(result)) {
if (result.isAcknowledged) {
reportUsage('doctor:warning:suppressed');
} else {
this.setVisible(true);
reportUsage('doctor:warning:shown');
}
}
}
render() {
return (
this.state.visible && (
<Container>
<WarningContainer>
<FlexRow style={{flexDirection: 'row-reverse'}}>
<ButtonSection>
<ButtonGroup>
<Button
onClick={() => {
reportUsage('doctor:report:opened:fromWarningBar');
this.props.setActiveSheet(ACTIVE_SHEET_DOCTOR);
this.setVisible(false);
}}>
Show Problems
</Button>
{this.state.showSettingsButton && (
<Button
onClick={() => {
reportUsage('settings:opened:fromWarningBar');
this.props.setActiveSheet(ACTIVE_SHEET_SETTINGS);
}}>
Show Settings
</Button>
)}
<Button onClick={() => this.setVisible(false)}>
Dismiss
</Button>
</ButtonGroup>
</ButtonSection>
<FlexColumn style={{flexGrow: 1}}>
{this.state.message}
</FlexColumn>
</FlexRow>
</WarningContainer>
</Container>
)
);
}
setVisible(visible: boolean) {
this.setState(prevState => {
return {
...prevState,
visible,
};
});
}
}
export default connect<StateFromProps, DispatchFromProps, {}, Store>(
({
settingsState: {enableAndroid, enableIOS},
healthchecks: {healthcheckReport},
}) => ({
enableAndroid,
enableIOS,
healthcheckReport,
}),
{
setActiveSheet,
updateHealthcheckResult,
startHealthchecks,
finishHealthchecks,
},
)(DoctorBar);
const Container = styled.div({
boxShadow: '2px 2px 2px #ccc',
userSelect: 'text',
});
const WarningContainer = styled.div({
backgroundColor: colors.orange,
color: '#fff',
maxHeight: '600px',
overflowY: 'auto',
overflowX: 'hidden',
transition: 'max-height 0.3s ease',
'&.collapsed': {
maxHeight: '0px',
},
padding: '4px 12px',
borderBottom: '1px solid ' + colors.orangeDark3,
verticalAlign: 'middle',
lineHeight: '28px',
});
const ButtonSection = styled(FlexColumn)({
marginLeft: '8px',
flexShrink: 0,
flexGrow: 0,
});
function hasProblems(result: HealthcheckResult) {
return result.status === 'WARNING' || result.status === 'FAILED';
}

View File

@@ -0,0 +1,422 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
FlexColumn,
styled,
Text,
FlexRow,
Glyph,
LoadingIndicator,
colors,
Spacer,
Button,
FlexBox,
Checkbox,
} from 'flipper';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
import {
HealthcheckResult,
HealthcheckReportCategory,
HealthcheckReport,
startHealthchecks,
finishHealthchecks,
updateHealthcheckResult,
acknowledgeProblems,
resetAcknowledgedProblems,
} from '../reducers/healthchecks';
import runHealthchecks, {
HealthcheckSettings,
HealthcheckEventsHandler,
} from '../utils/runHealthchecks';
import {shell} from 'electron';
import {reportUsage} from '../utils/metrics';
type StateFromProps = {
healthcheckReport: HealthcheckReport;
} & HealthcheckSettings;
type DispatchFromProps = {
acknowledgeProblems: () => void;
resetAcknowledgedProblems: () => void;
} & HealthcheckEventsHandler;
const Container = styled(FlexColumn)({
padding: 20,
width: 600,
});
const HealthcheckDisplayContainer = styled(FlexRow)({
alignItems: 'center',
marginBottom: 5,
});
const HealthcheckListContainer = styled(FlexColumn)({
marginBottom: 20,
width: 300,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
const CategoryContainer = styled(FlexColumn)({
marginBottom: 5,
marginLeft: 20,
marginRight: 20,
});
const SideContainer = styled(FlexBox)({
marginBottom: 20,
padding: 20,
backgroundColor: colors.highlightBackground,
border: '1px solid #b3b3b3',
width: 250,
});
const SideContainerText = styled(Text)({
display: 'block',
wordWrap: 'break-word',
overflow: 'auto',
});
const HealthcheckLabel = styled(Text)({
paddingLeft: 5,
});
const SkipReasonLabel = styled(Text)({
paddingLeft: 21,
fontStyle: 'italic',
});
const CenteredContainer = styled.label({
display: 'flex',
alignItems: 'center',
});
type OwnProps = {
onHide: () => void;
};
function CenteredCheckbox(props: {
checked: boolean;
text: string;
onChange: (checked: boolean) => void;
}) {
const {checked, onChange, text} = props;
return (
<CenteredContainer>
<Checkbox checked={checked} onChange={onChange} />
{text}
</CenteredContainer>
);
}
function HealthcheckIcon(props: {checkResult: HealthcheckResult}) {
const {checkResult: check} = props;
switch (props.checkResult.status) {
case 'IN_PROGRESS':
return <LoadingIndicator size={16} title={props.checkResult.message} />;
case 'SKIPPED':
return (
<Glyph
size={16}
name={'question'}
color={colors.grey}
title={props.checkResult.message}
/>
);
case 'SUCCESS':
return (
<Glyph
size={16}
name={'checkmark'}
color={colors.green}
title={props.checkResult.message}
/>
);
case 'FAILED':
return (
<Glyph
size={16}
name={'cross'}
color={colors.red}
title={props.checkResult.message}
variant={check.isAcknowledged ? 'outline' : 'filled'}
/>
);
default:
return (
<Glyph
size={16}
name={'caution'}
color={colors.yellow}
title={props.checkResult.message}
/>
);
}
}
function HealthcheckDisplay(props: {
label: string;
result: HealthcheckResult;
selected?: boolean;
onClick?: () => void;
}) {
return (
<FlexColumn shrink>
<HealthcheckDisplayContainer shrink title={props.result.message}>
<HealthcheckIcon checkResult={props.result} />
<HealthcheckLabel
bold={props.selected}
underline={!!props.onClick}
cursor={props.onClick && 'pointer'}
onClick={props.onClick}>
{props.label}
</HealthcheckLabel>
</HealthcheckDisplayContainer>
</FlexColumn>
);
}
function SideMessageDisplay(props: {children: React.ReactNode}) {
return <SideContainerText selectable>{props.children}</SideContainerText>;
}
function ResultMessage(props: {result: HealthcheckResult}) {
if (status === 'IN_PROGRESS') {
return <p>Doctor is running healthchecks...</p>;
} else if (hasProblems(props.result)) {
return (
<p>
Doctor has discovered problems with your installation. Please click to
an item to get its details.
</p>
);
} else {
return (
<p>
All good! Doctor has not discovered any issues with your installation.
</p>
);
}
}
function hasProblems(result: HealthcheckResult) {
const {status} = result;
return status === 'FAILED' || status === 'WARNING';
}
function hasNewProblems(result: HealthcheckResult) {
return hasProblems(result) && !result.isAcknowledged;
}
export type State = {
acknowledgeCheckboxVisible: boolean;
acknowledgeOnClose?: boolean;
selectedCheckKey?: string;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class DoctorSheet extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
acknowledgeCheckboxVisible: false,
};
}
componentDidMount() {
reportUsage('doctor:report:opened');
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
if (
!state.acknowledgeCheckboxVisible &&
hasProblems(props.healthcheckReport.result)
) {
return {
...state,
acknowledgeCheckboxVisible: true,
acknowledgeOnClose:
state.acknowledgeOnClose === undefined
? !hasNewProblems(props.healthcheckReport.result)
: state.acknowledgeOnClose,
};
}
if (
state.acknowledgeCheckboxVisible &&
!hasProblems(props.healthcheckReport.result)
) {
return {
...state,
acknowledgeCheckboxVisible: false,
};
}
return null;
}
componentWillUnmount(): void {
if (this.state.acknowledgeOnClose) {
if (hasNewProblems(this.props.healthcheckReport.result)) {
reportUsage('doctor:report:closed:newProblems:acknowledged');
}
reportUsage('doctor:report:closed:acknowleged');
this.props.acknowledgeProblems();
} else {
if (hasNewProblems(this.props.healthcheckReport.result)) {
reportUsage('doctor:report:closed:newProblems:notAcknowledged');
}
reportUsage('doctor:report:closed:notAcknowledged');
this.props.resetAcknowledgedProblems();
}
}
onAcknowledgeOnCloseChanged(acknowledge: boolean): void {
this.setState(prevState => {
return {
...prevState,
acknowledgeOnClose: acknowledge,
};
});
}
openHelpUrl(helpUrl?: string): void {
helpUrl && shell.openExternal(helpUrl);
}
async runHealthchecks(): Promise<void> {
await runHealthchecks(this.props);
}
getCheckMessage(checkKey: string): string {
for (const cat of Object.values(this.props.healthcheckReport.categories)) {
const check = Object.values(cat.checks).find(chk => chk.key === checkKey);
if (check) {
return check.result.message || '';
}
}
return '';
}
render() {
return (
<Container>
<Title>Doctor</Title>
<FlexRow>
<HealthcheckListContainer>
{Object.values(this.props.healthcheckReport.categories).map(
(category: HealthcheckReportCategory) => {
return (
<CategoryContainer key={category.key}>
<HealthcheckDisplay
label={category.label}
result={category.result}
/>
{category.result.status !== 'SKIPPED' && (
<CategoryContainer>
{Object.values(category.checks).map(check => (
<HealthcheckDisplay
key={check.key}
selected={check.key === this.state.selectedCheckKey}
label={check.label}
result={check.result}
onClick={() =>
this.setState({
...this.state,
selectedCheckKey:
this.state.selectedCheckKey === check.key
? undefined
: check.key,
})
}
/>
))}
</CategoryContainer>
)}
{category.result.status === 'SKIPPED' && (
<CategoryContainer>
<SkipReasonLabel>
{category.result.message}
</SkipReasonLabel>
</CategoryContainer>
)}
</CategoryContainer>
);
},
)}
</HealthcheckListContainer>
<Spacer />
<SideContainer shrink>
<SideMessageDisplay>
<SideContainerText selectable>
{this.state.selectedCheckKey && (
<p>{this.getCheckMessage(this.state.selectedCheckKey)}</p>
)}
{!this.state.selectedCheckKey && (
<ResultMessage result={this.props.healthcheckReport.result} />
)}
</SideContainerText>
</SideMessageDisplay>
</SideContainer>
</FlexRow>
<FlexRow>
<Spacer />
{this.state.acknowledgeCheckboxVisible && (
<CenteredCheckbox
checked={!!this.state.acknowledgeOnClose}
onChange={this.onAcknowledgeOnCloseChanged.bind(this)}
text={
'Do not show warning about these problems on Flipper startup'
}
/>
)}
<Button compact padded onClick={this.props.onHide}>
Close
</Button>
<Button
disabled={
this.props.healthcheckReport.result.status === 'IN_PROGRESS'
}
type="primary"
compact
padded
onClick={() => this.runHealthchecks()}>
Re-run
</Button>
</FlexRow>
</Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
healthchecks: {healthcheckReport},
settingsState: {enableAndroid, enableIOS},
}) => ({
healthcheckReport,
enableAndroid,
enableIOS,
}),
{
startHealthchecks,
finishHealthchecks,
updateHealthcheckResult,
acknowledgeProblems,
resetAcknowledgedProblems,
},
)(DoctorSheet);

View File

@@ -0,0 +1,200 @@
/**
* 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 {styled, colors, Glyph} from 'flipper';
import React, {useState, memo} from 'react';
import {connect} from 'react-redux';
import {FlipperError, dismissError} from '../reducers/connections';
import {State as Store} from '../reducers/index';
import {ErrorBlock, ButtonGroup, Button} from 'flipper';
import {FlexColumn, FlexRow} from 'flipper';
type StateFromProps = {
errors: FlipperError[];
};
type DispatchFromProps = {
dismissError: typeof dismissError;
};
type Props = DispatchFromProps & StateFromProps;
const ErrorBar = memo(function ErrorBar(props: Props) {
const [collapsed, setCollapsed] = useState(true);
if (!props.errors.length) {
return null;
}
const errorCount = props.errors.reduce(
(sum, error) => sum + (error.occurrences || 1),
0,
);
const urgentErrors = props.errors.filter(e => e.urgent);
return (
<ErrorBarContainer>
<ErrorRows
className={collapsed && urgentErrors.length === 0 ? 'collapsed' : ''}>
{(collapsed ? urgentErrors : props.errors).map((error, index) => (
<ErrorTile
onDismiss={() => props.dismissError(index)}
key={index}
error={error}
/>
))}
</ErrorRows>
<DismissAllErrors
onClick={() => setCollapsed(c => !c)}
title="Show / hide errors">
<Glyph
color={colors.white}
size={8}
name={collapsed ? 'chevron-down' : 'chevron-up'}
style={{marginRight: 4}}
/>
{collapsed && errorCount}
</DismissAllErrors>
</ErrorBarContainer>
);
});
export default connect<StateFromProps, DispatchFromProps, {}, Store>(
({connections: {errors}}) => ({
errors,
}),
{
dismissError,
},
)(ErrorBar);
function ErrorTile({
onDismiss,
error,
}: {
onDismiss: () => void;
error: FlipperError;
}) {
const [collapsed, setCollapsed] = useState(true);
return (
<ErrorRow className={`${error.urgent ? 'urgent' : ''}`}>
<FlexRow style={{flexDirection: 'row-reverse'}}>
<ButtonSection>
<ButtonGroup>
{(error.details || error.error) && (
<Button
onClick={() => setCollapsed(s => !s)}
icon={collapsed ? `chevron-down` : 'chevron-up'}
iconSize={12}
/>
)}
<Button onClick={onDismiss} icon="cross-circle" iconSize={12} />
</ButtonGroup>
</ButtonSection>
{error.occurrences! > 1 && (
<ErrorCounter title="Nr of times this error occurred">
{error.occurrences}
</ErrorCounter>
)}
<FlexColumn
style={
collapsed
? {overflow: 'hidden', whiteSpace: 'nowrap', flexGrow: 1}
: {flexGrow: 1}
}
title={error.message}>
{error.message}
</FlexColumn>
</FlexRow>
{!collapsed && (
<FlexRow>
<ErrorDetails>
{error.details}
{error.error && <ErrorBlock error={error.error} />}
</ErrorDetails>
</FlexRow>
)}
</ErrorRow>
);
}
const ErrorBarContainer = styled.div({
boxShadow: '2px 2px 2px #ccc',
userSelect: 'text',
});
const DismissAllErrors = styled.div({
boxShadow: '2px 2px 2px #ccc',
backgroundColor: colors.cherryDark3,
color: '#fff',
textAlign: 'center',
borderBottomLeftRadius: '4px',
borderBottomRightRadius: '4px',
position: 'absolute',
width: '48px',
height: '16px',
zIndex: 2,
right: '20px',
fontSize: '6pt',
lineHeight: '16px',
cursor: 'pointer',
alignItems: 'center',
});
const ErrorDetails = styled.div({
width: '100%',
marginTop: 4,
});
const ErrorRows = styled.div({
color: '#fff',
maxHeight: '600px',
overflowY: 'auto',
overflowX: 'hidden',
transition: 'max-height 0.3s ease',
borderBottom: '1px solid #b3b3b3',
'&.collapsed': {
maxHeight: '0px',
borderBottom: 'none',
},
});
const ErrorRow = styled.div({
padding: '4px 12px',
verticalAlign: 'middle',
lineHeight: '28px',
backgroundColor: colors.yellowTint,
color: colors.yellow,
'&.urgent': {
backgroundColor: colors.redTint,
color: colors.red,
},
});
const ButtonSection = styled(FlexColumn)({
marginLeft: '8px',
flexShrink: 0,
flexGrow: 0,
});
const ErrorCounter = styled(FlexColumn)({
border: `1px solid ${colors.light20}`,
color: colors.light20,
width: 20,
height: 20,
borderRadius: 20,
marginTop: 4,
lineHeight: '18px',
textAlign: 'center',
flexShrink: 0,
flexGrow: 0,
marginLeft: '8px',
fontSize: '10px',
});

View File

@@ -0,0 +1,164 @@
/**
* 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 {connect} from 'react-redux';
import React, {Component} from 'react';
import {ShareType} from '../reducers/application';
import {State as PluginState} from '../reducers/plugins';
import {State as PluginStatesState} from '../reducers/pluginStates';
import {State as Store} from '../reducers';
import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue';
import {ActiveSheet} from '../reducers/application';
import {selectedPlugins as actionForSelectedPlugins} from '../reducers/plugins';
import {getActivePersistentPlugins} from '../utils/pluginUtils';
import {
ACTIVE_SHEET_SHARE_DATA,
setActiveSheet as getActiveSheetAction,
setExportDataToFileActiveSheet as getExportDataToFileActiveSheetAction,
} from '../reducers/application';
import ListView from './ListView';
import {Dispatch, Action} from 'redux';
import {unsetShare} from '../reducers/application';
import {FlexColumn, styled} from 'flipper';
import Client from '../Client';
type OwnProps = {
onHide: () => void;
};
type StateFromProps = {
share: ShareType | null;
plugins: PluginState;
pluginStates: PluginStatesState;
pluginMessageQueue: PluginMessageQueueState;
selectedClient: Client | undefined;
};
type DispatchFromProps = {
setSelectedPlugins: (payload: Array<string>) => void;
setActiveSheet: (payload: ActiveSheet) => void;
setExportDataToFileActiveSheet: (payload: {
file: string;
closeOnFinish: boolean;
}) => void;
unsetShare: () => void;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
const Container = styled(FlexColumn)({
width: 700,
maxHeight: 700,
padding: 8,
});
type State = {
availablePluginsToExport: Array<{id: string; label: string}>;
};
class ExportDataPluginSheet extends Component<Props, State> {
state: State = {availablePluginsToExport: []};
static getDerivedStateFromProps(props: Props, _state: State): State {
const {plugins, pluginStates, pluginMessageQueue, selectedClient} = props;
const availablePluginsToExport = getActivePersistentPlugins(
pluginStates,
pluginMessageQueue,
plugins,
selectedClient,
);
return {
availablePluginsToExport,
};
}
render() {
const {onHide} = this.props;
const onHideWithUnsettingShare = () => {
this.props.unsetShare();
onHide();
};
return (
<Container>
<ListView
type="multiple"
title="Select the plugins for which you want to export the data"
onSubmit={() => {
const {share} = this.props;
if (!share) {
console.error(
'applications.share is undefined, whereas it was expected to be defined',
);
} else {
switch (share.type) {
case 'link':
this.props.setActiveSheet(ACTIVE_SHEET_SHARE_DATA);
break;
case 'file': {
const file = share.file;
if (file) {
this.props.setExportDataToFileActiveSheet({
file,
closeOnFinish: true,
});
} else {
console.error('share.file is undefined');
}
}
}
}
}}
onChange={selectedArray => {
this.props.setSelectedPlugins(selectedArray);
}}
elements={this.state.availablePluginsToExport}
selectedElements={new Set(this.props.plugins.selectedPlugins)}
onHide={onHideWithUnsettingShare}
/>
</Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
application: {share},
plugins,
pluginStates,
pluginMessageQueue,
connections: {selectedApp, clients},
}) => {
const selectedClient = clients.find(o => {
return o.id === selectedApp;
});
return {
share,
plugins,
pluginStates,
pluginMessageQueue,
selectedClient,
};
},
(dispatch: Dispatch<Action<any>>) => ({
setSelectedPlugins: (plugins: Array<string>) => {
dispatch(actionForSelectedPlugins(plugins));
},
setActiveSheet: (payload: ActiveSheet) => {
dispatch(getActiveSheetAction(payload));
},
setExportDataToFileActiveSheet: (payload: {
file: string;
closeOnFinish: boolean;
}) => {
dispatch(getExportDataToFileActiveSheetAction(payload));
},
unsetShare: () => {
dispatch(unsetShare());
},
}),
)(ExportDataPluginSheet);

Some files were not shown because too many files have changed in this diff Show More