Files
flipper/desktop/headless/index.tsx
Anton Nikolaev da7449c20b Enable Metro caching
Summary:
Enabling Metro cache for dev mode. For release builds we reset the cache.

Cache is used for faster compilation in dev mode for both main and renderer bundles, as well as for plugins.

Currently we have few side effects based on env vars, so cache is invalidated when they are changed. Also the cache is invalidated when transformations are changed (changed code, bumped dependency etc). Also added a script to reset the cache if something is going wrong.

Reviewed By: mweststrate

Differential Revision: D20691464

fbshipit-source-id: 478947d438bd3090f052dbfa6ad5c649523ecacb
2020-03-30 09:28:35 -07:00

400 lines
12 KiB
TypeScript

/**
* 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 '../app/src/dispatcher/index';
import reducers, {Actions, State} from '../app/src/reducers/index';
import {init as initLogger} from '../app/src/fb-stubs/Logger';
import {exportStore} from '../app/src/utils/exportData';
import {
exportMetricsWithoutTrace,
exportMetricsFromTrace,
} from '../app/src/utils/exportMetrics';
import {listDevices} from '../app/src/utils/listDevices';
import setup from '../static/setup';
import {
getPersistentPlugins,
pluginsClassMap,
} from '../app/src/utils/pluginUtils';
import {serialize} from '../app/src/utils/serialization';
import {getStringFromErrorLike} from '../app/src/utils/index';
import AndroidDevice from '../app/src/devices/AndroidDevice';
import {Store} from 'flipper';
process.env.FLIPPER_HEADLESS = 'true';
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();
}