Remove remaining Electron imports from product code: paths & env

Summary:
This diff removes most remaining Electron imports, by storing env and path constants on the RenderHost. As nice side effect those paths are all cached now as well.

To make sure RenderHost is initialised before Flipper itself, forced loading Flipper through a require. Otherwise the setup is super sensitive to circular import statements, since a lot of module initialisation code depends on those paths / env vars.

Reviewed By: nikoant

Differential Revision: D31992230

fbshipit-source-id: 91beb430902272aaf4b051b35cdf12d2fc993347
This commit is contained in:
Michel Weststrate
2021-11-03 07:00:03 -07:00
committed by Facebook GitHub Bot
parent dba09542f9
commit 2e7015388c
20 changed files with 413 additions and 375 deletions

View File

@@ -7,10 +7,20 @@
* @format * @format
*/ */
import {NotificationEvents} from './dispatcher/notifications'; import type {NotificationEvents} from './dispatcher/notifications';
import {PluginNotification} from './reducers/notifications'; import type {PluginNotification} from './reducers/notifications';
import type {NotificationConstructorOptions} from 'electron'; import type {NotificationConstructorOptions} from 'electron';
import {FlipperLib} from 'flipper-plugin'; import type {FlipperLib} from 'flipper-plugin';
import path from 'path';
type ENVIRONMENT_VARIABLES = 'NODE_ENV' | 'DEV_SERVER_URL' | 'CONFIG';
type ENVIRONMENT_PATHS =
| 'appPath'
| 'homePath'
| 'execPath'
| 'staticPath'
| 'tempPath'
| 'desktopPath';
// Events that are emitted from the main.ts ovr the IPC process bridge in Electron // Events that are emitted from the main.ts ovr the IPC process bridge in Electron
type MainProcessEvents = { type MainProcessEvents = {
@@ -45,6 +55,7 @@ type ChildProcessEvents = {
export interface RenderHost { export interface RenderHost {
readonly processId: number; readonly processId: number;
readTextFromClipboard(): string | undefined; readTextFromClipboard(): string | undefined;
writeTextToClipboard(text: string): void;
showSaveDialog?: FlipperLib['showSaveDialog']; showSaveDialog?: FlipperLib['showSaveDialog'];
showOpenDialog?: FlipperLib['showOpenDialog']; showOpenDialog?: FlipperLib['showOpenDialog'];
showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>; showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>;
@@ -60,6 +71,9 @@ export interface RenderHost {
): void; ): void;
shouldUseDarkColors(): boolean; shouldUseDarkColors(): boolean;
restartFlipper(update?: boolean): void; restartFlipper(update?: boolean): void;
env: Partial<Record<ENVIRONMENT_VARIABLES, string>>;
paths: Record<ENVIRONMENT_PATHS, string>;
openLink(url: string): void;
} }
let renderHostInstance: RenderHost | undefined; let renderHostInstance: RenderHost | undefined;
@@ -81,6 +95,7 @@ if (process.env.NODE_ENV === 'test') {
readTextFromClipboard() { readTextFromClipboard() {
return ''; return '';
}, },
writeTextToClipboard() {},
registerShortcut() {}, registerShortcut() {},
hasFocus() { hasFocus() {
return true; return true;
@@ -91,5 +106,15 @@ if (process.env.NODE_ENV === 'test') {
return false; return false;
}, },
restartFlipper() {}, restartFlipper() {},
openLink() {},
env: process.env,
paths: {
appPath: process.cwd(),
homePath: `/dev/null`,
desktopPath: `/dev/null`,
execPath: process.cwd(),
staticPath: path.join(process.cwd(), 'static'),
tempPath: `/tmp/`,
},
}); });
} }

View File

@@ -12,7 +12,7 @@ import React, {useState, useEffect, useCallback} from 'react';
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import open from 'open'; import open from 'open';
import {capture, CAPTURE_LOCATION, getFileName} from '../utils/screenshot'; import {capture, getCaptureLocation, getFileName} from '../utils/screenshot';
import {CameraOutlined, VideoCameraOutlined} from '@ant-design/icons'; import {CameraOutlined, VideoCameraOutlined} from '@ant-design/icons';
import {useStore} from '../utils/useStore'; import {useStore} from '../utils/useStore';
@@ -83,7 +83,7 @@ export default function ScreenCaptureButtons() {
} }
if (!isRecording) { if (!isRecording) {
setIsRecording(true); setIsRecording(true);
const videoPath = path.join(CAPTURE_LOCATION, getFileName('mp4')); const videoPath = path.join(getCaptureLocation(), getFileName('mp4'));
return selectedDevice.startScreenCapture(videoPath).catch((e) => { return selectedDevice.startScreenCapture(videoPath).catch((e) => {
console.error('Failed to start recording', e); console.error('Failed to start recording', e);
message.error('Failed to start recording' + e); message.error('Failed to start recording' + e);

View File

@@ -18,8 +18,9 @@ import BaseDevice from '../devices/BaseDevice';
import {ClientDescription, timeout} from 'flipper-common'; import {ClientDescription, timeout} from 'flipper-common';
import {reportPlatformFailures} from 'flipper-common'; import {reportPlatformFailures} from 'flipper-common';
import {sideEffect} from '../utils/sideEffect'; import {sideEffect} from '../utils/sideEffect';
import {getAppTempPath, getStaticPath} from '../utils/pathUtils'; import {getStaticPath} from '../utils/pathUtils';
import constants from '../fb-stubs/constants'; import constants from '../fb-stubs/constants';
import {getRenderHostInstance} from '../RenderHost';
export default async (store: Store, logger: Logger) => { export default async (store: Store, logger: Logger) => {
const {enableAndroid, androidHome, idbPath, enableIOS, enablePhysicalIOS} = const {enableAndroid, androidHome, idbPath, enableIOS, enablePhysicalIOS} =
@@ -33,7 +34,7 @@ export default async (store: Store, logger: Logger) => {
enableIOS, enableIOS,
enablePhysicalIOS, enablePhysicalIOS,
staticPath: getStaticPath(), staticPath: getStaticPath(),
tmpPath: getAppTempPath(), tmpPath: getRenderHostInstance().paths.tempPath,
validWebSocketOrigins: constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES, validWebSocketOrigins: constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES,
}, },
logger, logger,

View File

@@ -15,17 +15,27 @@ import {
_LoggerContext, _LoggerContext,
} from 'flipper-plugin'; } from 'flipper-plugin';
// eslint-disable-next-line flipper/no-electron-remote-imports // eslint-disable-next-line flipper/no-electron-remote-imports
import {ipcRenderer, remote, SaveDialogReturnValue} from 'electron'; import {
import {setRenderHostInstance} from '../RenderHost'; ipcRenderer,
import {clipboard} from 'electron'; remote,
import restart from './restartFlipper'; SaveDialogReturnValue,
clipboard,
shell,
} from 'electron';
import {getRenderHostInstance, setRenderHostInstance} from '../RenderHost';
import isProduction from '../utils/isProduction';
import fs from 'fs';
export function initializeElectron() { export function initializeElectron() {
const app = remote.app;
setRenderHostInstance({ setRenderHostInstance({
processId: remote.process.pid, processId: remote.process.pid,
readTextFromClipboard() { readTextFromClipboard() {
return clipboard.readText(); return clipboard.readText();
}, },
writeTextToClipboard(text: string) {
clipboard.writeText(text);
},
async showSaveDialog(options) { async showSaveDialog(options) {
return (await remote.dialog.showSaveDialog(options))?.filePath; return (await remote.dialog.showSaveDialog(options))?.filePath;
}, },
@@ -56,6 +66,9 @@ export function initializeElectron() {
return undefined; return undefined;
}); });
}, },
openLink(url: string) {
shell.openExternal(url);
},
registerShortcut(shortcut, callback) { registerShortcut(shortcut, callback) {
remote.globalShortcut.register(shortcut, callback); remote.globalShortcut.register(shortcut, callback);
}, },
@@ -76,5 +89,50 @@ export function initializeElectron() {
restartFlipper() { restartFlipper() {
restart(); restart();
}, },
env: process.env,
paths: {
appPath: app.getAppPath(),
homePath: app.getPath('home'),
execPath: process.execPath || remote.process.execPath,
staticPath: getStaticDir(),
tempPath: app.getPath('temp'),
desktopPath: app.getPath('desktop'),
},
}); });
} }
function getStaticDir() {
let _staticPath = path.resolve(__dirname, '..', '..', '..', 'static');
if (fs.existsSync(_staticPath)) {
return _staticPath;
}
if (remote && fs.existsSync(remote.app.getAppPath())) {
_staticPath = path.join(remote.app.getAppPath());
}
if (!fs.existsSync(_staticPath)) {
throw new Error('Static path does not exist: ' + _staticPath);
}
return _staticPath;
}
function restart(update: boolean = false) {
if (isProduction()) {
if (update) {
const options = {
args: process.argv
.splice(0, 1)
.filter((arg) => arg !== '--no-launcher' && arg !== '--no-updater'),
};
remote.app.relaunch(options);
} else {
remote.app.relaunch();
}
remote.app.exit();
} else {
// Relaunching the process with the standard way doesn't work in dev mode.
// So instead we're sending a signal to dev server to kill the current instance of electron and launch new.
fetch(`${getRenderHostInstance().env.DEV_SERVER_URL}/_restartElectron`, {
method: 'POST',
});
}
}

View File

@@ -1,34 +0,0 @@
/**
* 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-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import isProduction from '../utils/isProduction';
export default function restart(update: boolean = false) {
if (isProduction()) {
if (update) {
const options = {
args: process.argv
.splice(0, 1)
.filter((arg) => arg !== '--no-launcher' && arg !== '--no-updater'),
};
remote.app.relaunch(options);
} else {
remote.app.relaunch();
}
remote.app.exit();
} else {
// Relaunching the process with the standard way doesn't work in dev mode.
// So instead we're sending a signal to dev server to kill the current instance of electron and launch new.
fetch(`${remote.process.env.DEV_SERVER_URL}/_restartElectron`, {
method: 'POST',
});
}
}

View File

@@ -7,55 +7,13 @@
* @format * @format
*/ */
import {Provider} from 'react-redux'; import {initializeElectron} from './electron/initializeElectron';
import ReactDOM from 'react-dom';
import GK from './fb-stubs/GK'; import GK from './fb-stubs/GK';
import {init as initLogger} from './fb-stubs/Logger';
import {SandyApp} from './sandy-chrome/SandyApp';
import setupPrefetcher from './fb-stubs/Prefetcher';
import {Persistor, persistStore} from 'redux-persist';
import {Store} from './reducers/index';
import dispatcher from './dispatcher/index';
import TooltipProvider from './ui/components/TooltipProvider';
import config from './utils/processConfig';
import {initLauncherHooks} from './utils/launcher';
import {setPersistor} from './utils/persistor';
import React from 'react';
import path from 'path';
import {getStore} from './store';
import {cache} from '@emotion/css';
import {CacheProvider} from '@emotion/react';
import {enableMapSet} from 'immer'; import {enableMapSet} from 'immer';
import os from 'os'; import os from 'os';
import {initializeFlipperLibImplementation} from './utils/flipperLibImplementation';
import {enableConsoleHook} from './chrome/ConsoleLogs'; initializeElectron();
import {sideEffect} from './utils/sideEffect';
import {
_NuxManagerContext,
_createNuxManager,
_setGlobalInteractionReporter,
Logger,
_LoggerContext,
Layout,
theme,
getFlipperLib,
} from 'flipper-plugin';
import isProduction from './utils/isProduction';
import {Button, Input, Result, Typography} from 'antd';
import constants from './fb-stubs/constants';
import styled from '@emotion/styled';
import {CopyOutlined} from '@ant-design/icons';
import {getVersionString} from './utils/versionString';
import {PersistGate} from 'redux-persist/integration/react';
import {
setLoggerInstance,
setUserSessionManagerInstance,
GK as flipperCommonGK,
} from 'flipper-common';
import {internGraphPOSTAPIRequest} from './fb-stubs/user';
import {getRenderHostInstance} from './RenderHost';
import {initializeElectron} from './electron/initializeElectron';
if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') {
// By default Node.JS has its internal certificate storage and doesn't use // By default Node.JS has its internal certificate storage and doesn't use
@@ -70,187 +28,9 @@ enableMapSet();
GK.init(); GK.init();
class AppFrame extends React.Component< // By turning this in a require, we force the JS that the body of this module (init) has completed (initializeElectron),
{logger: Logger; persistor: Persistor}, // before starting the rest of the Flipper process.
{error: any; errorInfo: any} // This prevent issues where the render host is referred at module initialisation level,
> { // but not set yet, which might happen when using normal imports.
state = {error: undefined as any, errorInfo: undefined as any}; // eslint-disable-next-line import/no-commonjs
require('./startFlipperDesktop');
getError() {
return this.state.error
? `${
this.state.error
}\n\nFlipper version: ${getVersionString()}\n\nComponent stack:\n${
this.state.errorInfo?.componentStack
}\n\nError stacktrace:\n${this.state.error?.stack}`
: '';
}
render() {
const {logger, persistor} = this.props;
return this.state.error ? (
<Layout.Container grow center pad={80} style={{height: '100%'}}>
<Layout.Top style={{maxWidth: 800, height: '100%'}}>
<Result
status="error"
title="Detected a Flipper crash"
subTitle={
<p>
A crash was detected in the Flipper chrome. Filing a{' '}
<Typography.Link
href={
constants.IS_PUBLIC_BUILD
? 'https://github.com/facebook/flipper/issues/new/choose'
: constants.FEEDBACK_GROUP_LINK
}>
bug report
</Typography.Link>{' '}
would be appreciated! Please include the details below.
</p>
}
extra={[
<Button
key="copy_error"
icon={<CopyOutlined />}
onClick={() => {
getFlipperLib().writeTextToClipboard(this.getError());
}}>
Copy error
</Button>,
<Button
key="retry_error"
type="primary"
onClick={() => {
this.setState({error: undefined, errorInfo: undefined});
}}>
Retry
</Button>,
]}
/>
<CodeBlock value={this.getError()} readOnly />
</Layout.Top>
</Layout.Container>
) : (
<_LoggerContext.Provider value={logger}>
<Provider store={getStore()}>
<PersistGate persistor={persistor}>
<CacheProvider value={cache}>
<TooltipProvider>
<_NuxManagerContext.Provider value={_createNuxManager()}>
<SandyApp />
</_NuxManagerContext.Provider>
</TooltipProvider>
</CacheProvider>
</PersistGate>
</Provider>
</_LoggerContext.Provider>
);
}
componentDidCatch(error: any, errorInfo: any) {
console.error(
`Flipper chrome crash: ${error}`,
error,
'\nComponents: ' + errorInfo?.componentStack,
);
this.setState({
error,
errorInfo,
});
}
}
function setProcessState(store: Store) {
const settings = store.getState().settingsState;
const androidHome = settings.androidHome;
const idbPath = settings.idbPath;
if (!process.env.ANDROID_HOME && !process.env.ANDROID_SDK_ROOT) {
process.env.ANDROID_HOME = androidHome;
}
// emulator/emulator is more reliable than tools/emulator, so prefer it if
// it exists
process.env.PATH =
['emulator', 'tools', 'platform-tools']
.map((directory) => path.resolve(androidHome, directory))
.join(':') +
`:${idbPath}` +
`:${process.env.PATH}`;
window.requestIdleCallback(() => {
setupPrefetcher(settings);
});
}
function init() {
initializeElectron();
// TODO: centralise all those initialisations in a single configuration call
flipperCommonGK.get = (name) => GK.get(name);
const store = getStore();
const logger = initLogger(store);
setLoggerInstance(logger);
// rehydrate app state before exposing init
const persistor = persistStore(store, undefined, () => {
// Make sure process state is set before dispatchers run
setProcessState(store);
dispatcher(store, logger);
});
setPersistor(persistor);
initializeFlipperLibImplementation(getRenderHostInstance(), store, logger);
_setGlobalInteractionReporter((r) => {
logger.track('usage', 'interaction', r);
if (!isProduction()) {
const msg = `[interaction] ${r.scope}:${r.action} in ${r.duration}ms`;
if (r.success) console.debug(msg);
else console.warn(msg, r.error);
}
});
setUserSessionManagerInstance({
internGraphPOSTAPIRequest,
});
ReactDOM.render(
<AppFrame logger={logger} persistor={persistor} />,
document.getElementById('root'),
);
initLauncherHooks(config(), store);
enableConsoleHook();
window.flipperGlobalStoreDispatch = store.dispatch;
// listen to settings and load the right theme
sideEffect(
store,
{name: 'loadTheme', fireImmediately: false, throttleMs: 500},
(state) => state.settingsState.darkMode,
(theme) => {
let shouldUseDarkMode = false;
if (theme === 'dark') {
shouldUseDarkMode = true;
} else if (theme === 'light') {
shouldUseDarkMode = false;
} else if (theme === 'system') {
shouldUseDarkMode = getRenderHostInstance().shouldUseDarkColors();
}
(
document.getElementById('flipper-theme-import') as HTMLLinkElement
).href = `themes/${shouldUseDarkMode ? 'dark' : 'light'}.css`;
getRenderHostInstance().sendIpcEvent('setTheme', theme);
},
);
}
setImmediate(() => {
// make sure all modules are loaded
// @ts-ignore
window.flipperInit = init;
window.dispatchEvent(new Event('flipper-store-ready'));
});
const CodeBlock = styled(Input.TextArea)({
...theme.monospace,
color: theme.textColorSecondary,
});

View File

@@ -9,7 +9,7 @@
import {Actions} from './index'; import {Actions} from './index';
import os from 'os'; import os from 'os';
import electron from 'electron'; import {getRenderHostInstance} from '../RenderHost';
export enum Tristate { export enum Tristate {
True, True,
@@ -105,6 +105,7 @@ function getDefaultAndroidSdkPath() {
} }
function getWindowsSdkPath() { function getWindowsSdkPath() {
const app = electron.app || electron.remote.app; return `${
return `${app.getPath('home')}\\AppData\\Local\\android\\sdk`; getRenderHostInstance().paths.homePath
}\\AppData\\Local\\android\\sdk`;
} }

View File

@@ -0,0 +1,239 @@
/**
* 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 {Provider} from 'react-redux';
import ReactDOM from 'react-dom';
import GK from './fb-stubs/GK';
import {init as initLogger} from './fb-stubs/Logger';
import {SandyApp} from './sandy-chrome/SandyApp';
import setupPrefetcher from './fb-stubs/Prefetcher';
import {Persistor, persistStore} from 'redux-persist';
import {Store} from './reducers/index';
import dispatcher from './dispatcher/index';
import TooltipProvider from './ui/components/TooltipProvider';
import config from './utils/processConfig';
import {initLauncherHooks} from './utils/launcher';
import {setPersistor} from './utils/persistor';
import React from 'react';
import path from 'path';
import {getStore} from './store';
import {cache} from '@emotion/css';
import {CacheProvider} from '@emotion/react';
import {initializeFlipperLibImplementation} from './utils/flipperLibImplementation';
import {enableConsoleHook} from './chrome/ConsoleLogs';
import {sideEffect} from './utils/sideEffect';
import {
_NuxManagerContext,
_createNuxManager,
_setGlobalInteractionReporter,
Logger,
_LoggerContext,
Layout,
theme,
getFlipperLib,
} from 'flipper-plugin';
import isProduction from './utils/isProduction';
import {Button, Input, Result, Typography} from 'antd';
import constants from './fb-stubs/constants';
import styled from '@emotion/styled';
import {CopyOutlined} from '@ant-design/icons';
import {getVersionString} from './utils/versionString';
import {PersistGate} from 'redux-persist/integration/react';
import {
setLoggerInstance,
setUserSessionManagerInstance,
GK as flipperCommonGK,
} from 'flipper-common';
import {internGraphPOSTAPIRequest} from './fb-stubs/user';
import {getRenderHostInstance} from './RenderHost';
class AppFrame extends React.Component<
{logger: Logger; persistor: Persistor},
{error: any; errorInfo: any}
> {
state = {error: undefined as any, errorInfo: undefined as any};
getError() {
return this.state.error
? `${
this.state.error
}\n\nFlipper version: ${getVersionString()}\n\nComponent stack:\n${
this.state.errorInfo?.componentStack
}\n\nError stacktrace:\n${this.state.error?.stack}`
: '';
}
render() {
const {logger, persistor} = this.props;
return this.state.error ? (
<Layout.Container grow center pad={80} style={{height: '100%'}}>
<Layout.Top style={{maxWidth: 800, height: '100%'}}>
<Result
status="error"
title="Detected a Flipper crash"
subTitle={
<p>
A crash was detected in the Flipper chrome. Filing a{' '}
<Typography.Link
href={
constants.IS_PUBLIC_BUILD
? 'https://github.com/facebook/flipper/issues/new/choose'
: constants.FEEDBACK_GROUP_LINK
}>
bug report
</Typography.Link>{' '}
would be appreciated! Please include the details below.
</p>
}
extra={[
<Button
key="copy_error"
icon={<CopyOutlined />}
onClick={() => {
getFlipperLib().writeTextToClipboard(this.getError());
}}>
Copy error
</Button>,
<Button
key="retry_error"
type="primary"
onClick={() => {
this.setState({error: undefined, errorInfo: undefined});
}}>
Retry
</Button>,
]}
/>
<CodeBlock value={this.getError()} readOnly />
</Layout.Top>
</Layout.Container>
) : (
<_LoggerContext.Provider value={logger}>
<Provider store={getStore()}>
<PersistGate persistor={persistor}>
<CacheProvider value={cache}>
<TooltipProvider>
<_NuxManagerContext.Provider value={_createNuxManager()}>
<SandyApp />
</_NuxManagerContext.Provider>
</TooltipProvider>
</CacheProvider>
</PersistGate>
</Provider>
</_LoggerContext.Provider>
);
}
componentDidCatch(error: any, errorInfo: any) {
console.error(
`Flipper chrome crash: ${error}`,
error,
'\nComponents: ' + errorInfo?.componentStack,
);
this.setState({
error,
errorInfo,
});
}
}
function setProcessState(store: Store) {
const settings = store.getState().settingsState;
const androidHome = settings.androidHome;
const idbPath = settings.idbPath;
if (!process.env.ANDROID_HOME && !process.env.ANDROID_SDK_ROOT) {
process.env.ANDROID_HOME = androidHome;
}
// emulator/emulator is more reliable than tools/emulator, so prefer it if
// it exists
process.env.PATH =
['emulator', 'tools', 'platform-tools']
.map((directory) => path.resolve(androidHome, directory))
.join(':') +
`:${idbPath}` +
`:${process.env.PATH}`;
window.requestIdleCallback(() => {
setupPrefetcher(settings);
});
}
function init() {
// TODO: centralise all those initialisations in a single configuration call
flipperCommonGK.get = (name) => GK.get(name);
const store = getStore();
const logger = initLogger(store);
setLoggerInstance(logger);
// rehydrate app state before exposing init
const persistor = persistStore(store, undefined, () => {
// Make sure process state is set before dispatchers run
setProcessState(store);
dispatcher(store, logger);
});
setPersistor(persistor);
initializeFlipperLibImplementation(getRenderHostInstance(), store, logger);
_setGlobalInteractionReporter((r) => {
logger.track('usage', 'interaction', r);
if (!isProduction()) {
const msg = `[interaction] ${r.scope}:${r.action} in ${r.duration}ms`;
if (r.success) console.debug(msg);
else console.warn(msg, r.error);
}
});
setUserSessionManagerInstance({
internGraphPOSTAPIRequest,
});
ReactDOM.render(
<AppFrame logger={logger} persistor={persistor} />,
document.getElementById('root'),
);
initLauncherHooks(config(), store);
enableConsoleHook();
window.flipperGlobalStoreDispatch = store.dispatch;
// listen to settings and load the right theme
sideEffect(
store,
{name: 'loadTheme', fireImmediately: false, throttleMs: 500},
(state) => state.settingsState.darkMode,
(theme) => {
let shouldUseDarkMode = false;
if (theme === 'dark') {
shouldUseDarkMode = true;
} else if (theme === 'light') {
shouldUseDarkMode = false;
} else if (theme === 'system') {
shouldUseDarkMode = getRenderHostInstance().shouldUseDarkColors();
}
(
document.getElementById('flipper-theme-import') as HTMLLinkElement
).href = `themes/${shouldUseDarkMode ? 'dark' : 'light'}.css`;
getRenderHostInstance().sendIpcEvent('setTheme', theme);
},
);
}
setImmediate(() => {
// make sure all modules are loaded
// @ts-ignore
window.flipperInit = init;
window.dispatchEvent(new Event('flipper-store-ready'));
});
const CodeBlock = styled(Input.TextArea)({
...theme.monospace,
color: theme.textColorSecondary,
});

View File

@@ -13,7 +13,6 @@ import type {Store} from '../reducers';
import createPaste from '../fb-stubs/createPaste'; import createPaste from '../fb-stubs/createPaste';
import GK from '../fb-stubs/GK'; import GK from '../fb-stubs/GK';
import type BaseDevice from '../devices/BaseDevice'; import type BaseDevice from '../devices/BaseDevice';
import {clipboard, shell} from 'electron';
import constants from '../fb-stubs/constants'; import constants from '../fb-stubs/constants';
import {addNotification} from '../reducers/notifications'; import {addNotification} from '../reducers/notifications';
import {deconstructPluginKey} from 'flipper-common'; import {deconstructPluginKey} from 'flipper-common';
@@ -49,12 +48,8 @@ export function initializeFlipperLibImplementation(
}, },
}); });
}, },
writeTextToClipboard(text: string) { writeTextToClipboard: renderHost.writeTextToClipboard,
clipboard.writeText(text); openLink: renderHost.openLink,
},
openLink(url: string) {
shell.openExternal(url);
},
showNotification(pluginId, notification) { showNotification(pluginId, notification) {
const parts = deconstructPluginKey(pluginId); const parts = deconstructPluginKey(pluginId);
store.dispatch( store.dispatch(
@@ -69,5 +64,9 @@ export function initializeFlipperLibImplementation(
showSaveDialog: renderHost.showSaveDialog, showSaveDialog: renderHost.showSaveDialog,
showOpenDialog: renderHost.showOpenDialog, showOpenDialog: renderHost.showOpenDialog,
showSelectDirectoryDialog: renderHost.showSelectDirectoryDialog, showSelectDirectoryDialog: renderHost.showSelectDirectoryDialog,
paths: {
appPath: renderHost.paths.appPath,
homePath: renderHost.paths.homePath,
},
}); });
} }

View File

@@ -13,8 +13,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
// eslint-disable-next-line flipper/no-electron-remote-imports import {getRenderHostInstance} from '../RenderHost';
import {remote} from 'electron';
import {getStaticPath} from './pathUtils'; import {getStaticPath} from './pathUtils';
const AVAILABLE_SIZES = [8, 10, 12, 16, 18, 20, 24, 32]; const AVAILABLE_SIZES = [8, 10, 12, 16, 18, 20, 24, 32];
@@ -85,7 +84,7 @@ export function buildIconURLSync(name: string, size: number, density: number) {
) { ) {
// From utils/isProduction // From utils/isProduction
const isProduction = !/node_modules[\\/]electron[\\/]/.test( const isProduction = !/node_modules[\\/]electron[\\/]/.test(
process.execPath || remote.process.execPath, getRenderHostInstance().paths.execPath,
); );
if (!isProduction) { if (!isProduction) {
@@ -126,7 +125,12 @@ export function buildIconURLSync(name: string, size: number, density: number) {
return url; return url;
} }
export function getIconURLSync(name: string, size: number, density: number) { export function getIconURLSync(
name: string,
size: number,
density: number,
basePath: string = getRenderHostInstance().paths.appPath,
) {
if (name.indexOf('/') > -1) { if (name.indexOf('/') > -1) {
return name; return name;
} }
@@ -161,15 +165,8 @@ export function getIconURLSync(name: string, size: number, density: number) {
} }
// resolve icon locally if possible // resolve icon locally if possible
if ( const iconPath = path.join(basePath, buildLocalIconPath(name, size, density));
remote && if (fs.existsSync(iconPath)) {
fs.existsSync(
path.join(
remote.app.getAppPath(),
buildLocalIconPath(name, size, density),
),
)
) {
return buildLocalIconURL(name, size, density); return buildLocalIconURL(name, size, density);
} }
return buildIconURLSync(name, requestedSize, density); return buildIconURLSync(name, requestedSize, density);

View File

@@ -7,14 +7,15 @@
* @format * @format
*/ */
import electron from 'electron'; import {getRenderHostInstance} from '../RenderHost';
const _isProduction = !/node_modules[\\/]electron[\\/]/.test( let _isProduction: boolean | undefined;
// We only run this once and cache the output so this slow access is okay.
// eslint-disable-next-line no-restricted-properties
process.execPath || electron.remote.process.execPath,
);
export default function isProduction(): boolean { export default function isProduction(): boolean {
if (_isProduction === undefined) {
_isProduction = !/node_modules[\\/]electron[\\/]/.test(
getRenderHostInstance().paths.execPath,
);
}
return _isProduction; return _isProduction;
} }

View File

@@ -7,18 +7,14 @@
* @format * @format
*/ */
import electron from 'electron';
import lodash from 'lodash'; import lodash from 'lodash';
import isProduction from './isProduction';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import {promisify} from 'util'; import {promisify} from 'util';
import {getRenderHostInstance} from '../RenderHost';
const getPackageJSON = async () => { const getPackageJSON = async () => {
const base = const base = getRenderHostInstance().paths.appPath;
isProduction() && electron.remote
? electron.remote.app.getAppPath()
: process.cwd();
const content = await promisify(fs.readFile)( const content = await promisify(fs.readFile)(
path.join(base, 'package.json'), path.join(base, 'package.json'),
'utf-8', 'utf-8',

View File

@@ -12,35 +12,15 @@
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
// In utils this is fine when used with caching.
// eslint-disable-next-line flipper/no-electron-remote-imports
import {default as electron, remote} from 'electron';
import config from '../fb-stubs/config'; import config from '../fb-stubs/config';
import {getRenderHostInstance} from '../RenderHost';
let _staticPath = '';
function getStaticDir() {
if (_staticPath) {
return _staticPath;
}
_staticPath = path.resolve(__dirname, '..', '..', '..', 'static');
if (fs.existsSync(_staticPath)) {
return _staticPath;
}
if (remote && fs.existsSync(remote.app.getAppPath())) {
_staticPath = path.join(remote.app.getAppPath());
}
if (!fs.existsSync(_staticPath)) {
throw new Error('Static path does not exist: ' + _staticPath);
}
return _staticPath;
}
export function getStaticPath( export function getStaticPath(
relativePath: string = '.', relativePath: string = '.',
{asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false}, {asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false},
) { ) {
const staticDir = getStaticDir(); const staticDir = getRenderHostInstance().paths.staticPath;
const absolutePath = path.resolve(staticDir, relativePath); const absolutePath = path.resolve(staticDir, relativePath);
// Unfortunately, path.resolve, fs.pathExists, fs.read etc do not automatically work with asarUnpacked files. // Unfortunately, path.resolve, fs.pathExists, fs.read etc do not automatically work with asarUnpacked files.
// All these functions still look for files in "app.asar" even if they are unpacked. // All these functions still look for files in "app.asar" even if they are unpacked.
@@ -51,26 +31,6 @@ export function getStaticPath(
: absolutePath; : absolutePath;
} }
let _appPath: string | undefined = undefined;
export function getAppPath() {
if (!_appPath) {
_appPath = getStaticPath('..');
}
return _appPath;
}
let _tempPath: string | undefined = undefined;
export function getAppTempPath() {
if (!_tempPath) {
// We cache this.
// eslint-disable-next-line no-restricted-properties
_tempPath = (electron.app || electron.remote.app).getPath('temp');
}
return _tempPath;
}
export function getChangelogPath() { export function getChangelogPath() {
const changelogPath = getStaticPath(config.isFBBuild ? 'facebook' : '.'); const changelogPath = getStaticPath(config.isFBBuild ? 'facebook' : '.');
if (fs.existsSync(changelogPath)) { if (fs.existsSync(changelogPath)) {

View File

@@ -7,8 +7,7 @@
* @format * @format
*/ */
// eslint-disable-next-line flipper/no-electron-remote-imports import {getRenderHostInstance} from '../RenderHost';
import {remote} from 'electron';
export type ProcessConfig = { export type ProcessConfig = {
disabledPlugins: Set<string>; disabledPlugins: Set<string>;
@@ -27,9 +26,7 @@ export type ProcessConfig = {
let configObj: ProcessConfig | null = null; let configObj: ProcessConfig | null = null;
export default function config(): ProcessConfig { export default function config(): ProcessConfig {
if (configObj === null) { if (configObj === null) {
const json = JSON.parse( const json = JSON.parse(getRenderHostInstance().env.CONFIG || '{}');
(remote && remote.process.env.CONFIG) || process.env.CONFIG || '{}',
);
configObj = { configObj = {
disabledPlugins: new Set(json.disabledPlugins || []), disabledPlugins: new Set(json.disabledPlugins || []),
lastWindowPosition: json.lastWindowPosition, lastWindowPosition: json.lastWindowPosition,

View File

@@ -12,14 +12,14 @@ import path from 'path';
import BaseDevice from '../devices/BaseDevice'; import BaseDevice from '../devices/BaseDevice';
import {reportPlatformFailures} from 'flipper-common'; import {reportPlatformFailures} from 'flipper-common';
import expandTilde from 'expand-tilde'; import expandTilde from 'expand-tilde';
// eslint-disable-next-line flipper/no-electron-remote-imports
import {remote} from 'electron';
import config from '../utils/processConfig'; import config from '../utils/processConfig';
import {getRenderHostInstance} from '../RenderHost';
// TODO: refactor so this doesn't need to be exported export function getCaptureLocation() {
export const CAPTURE_LOCATION = expandTilde( return expandTilde(
config().screenCapturePath || remote.app.getPath('desktop'), config().screenCapturePath || getRenderHostInstance().paths.desktopPath,
); );
}
// TODO: refactor so this doesn't need to be exported // TODO: refactor so this doesn't need to be exported
export function getFileName(extension: 'png' | 'mp4'): string { export function getFileName(extension: 'png' | 'mp4'): string {
@@ -32,7 +32,7 @@ export async function capture(device: BaseDevice): Promise<string> {
console.log('Skipping screenshot for disconnected device'); console.log('Skipping screenshot for disconnected device');
return ''; return '';
} }
const pngPath = path.join(CAPTURE_LOCATION, getFileName('png')); const pngPath = path.join(getCaptureLocation(), getFileName('png'));
return reportPlatformFailures( return reportPlatformFailures(
device.screenshot().then((buffer) => writeBufferToFile(pngPath, buffer)), device.screenshot().then((buffer) => writeBufferToFile(pngPath, buffer)),
'captureScreenshot', 'captureScreenshot',

View File

@@ -48,6 +48,10 @@ export interface FlipperLib {
}; };
}): Promise<string | undefined>; }): Promise<string | undefined>;
showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>; showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>;
paths: {
homePath: string;
appPath: string;
};
} }
export let flipperLibInstance: FlipperLib | undefined; export let flipperLibInstance: FlipperLib | undefined;

View File

@@ -379,6 +379,10 @@ export function createMockFlipperLib(options?: StartPluginOptions): FlipperLib {
writeTextToClipboard: jest.fn(), writeTextToClipboard: jest.fn(),
openLink: jest.fn(), openLink: jest.fn(),
showNotification: jest.fn(), showNotification: jest.fn(),
paths: {
appPath: process.cwd(),
homePath: `/dev/null`,
},
}; };
} }

View File

@@ -10,8 +10,6 @@
import React from 'react'; import React from 'react';
import {styled, colors, FlexColumn} from 'flipper'; import {styled, colors, FlexColumn} from 'flipper';
import electron from 'electron';
const devToolsNodeId = (url: string) => const devToolsNodeId = (url: string) =>
`hermes-chromedevtools-out-of-react-node-${url.replace(/\W+/g, '-')}`; `hermes-chromedevtools-out-of-react-node-${url.replace(/\W+/g, '-')}`;
@@ -28,10 +26,16 @@ function createDevToolsNode(
return existing; return existing;
} }
// It is necessary to activate chrome devtools in electron // It is necessary to deactivate chrome devtools in electron
electron.remote.getCurrentWindow().webContents.toggleDevTools(); try {
electron.remote.getCurrentWindow().webContents.closeDevTools(); const electron = require('electron');
if (electron.default) {
electron.default.remote.getCurrentWindow().webContents.toggleDevTools();
electron.default.remote.getCurrentWindow().webContents.closeDevTools();
}
} catch (e) {
console.warn('Failed to close Electron devtools: ', e);
}
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.id = devToolsNodeId(url); wrapper.id = devToolsNodeId(url);
wrapper.style.height = '100%'; wrapper.style.height = '100%';

View File

@@ -9,14 +9,14 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {getAppPath} from 'flipper';
import {AppMatchPattern} from '../types'; import {AppMatchPattern} from '../types';
import {Device} from 'flipper-plugin'; import {Device, getFlipperLib} from 'flipper-plugin';
let patternsPath: string | undefined; let patternsPath: string | undefined;
function getPatternsBasePath() { function getPatternsBasePath() {
return (patternsPath = patternsPath ?? path.join(getAppPath(), 'facebook')); return (patternsPath =
patternsPath ?? path.join(getFlipperLib().paths.appPath, 'facebook'));
} }
const extractAppNameFromSelectedApp = (selectedApp: string | null) => { const extractAppNameFromSelectedApp = (selectedApp: string | null) => {

View File

@@ -33,6 +33,7 @@ import {
getIconsSync, getIconsSync,
buildLocalIconPath, buildLocalIconPath,
getIconURLSync, getIconURLSync,
Icons,
} from '../app/src/utils/icons'; } from '../app/src/utils/icons';
import isFB from './isFB'; import isFB from './isFB';
import copyPackageWithDependencies from './copy-package-with-dependencies'; import copyPackageWithDependencies from './copy-package-with-dependencies';
@@ -309,7 +310,12 @@ async function copyStaticFolder(buildFolder: string) {
} }
function downloadIcons(buildFolder: string) { function downloadIcons(buildFolder: string) {
const iconURLs = Object.entries(getIconsSync()).reduce< const icons: Icons = JSON.parse(
fs.readFileSync(path.join(buildFolder, 'icons.json'), {
encoding: 'utf8',
}),
);
const iconURLs = Object.entries(icons).reduce<
{ {
name: string; name: string;
size: number; size: number;
@@ -326,7 +332,7 @@ function downloadIcons(buildFolder: string) {
return Promise.all( return Promise.all(
iconURLs.map(({name, size, density}) => { iconURLs.map(({name, size, density}) => {
const url = getIconURLSync(name, size, density); const url = getIconURLSync(name, size, density, buildFolder);
return fetch(url, { return fetch(url, {
retryOptions: { retryOptions: {
// Be default, only 5xx are retried but we're getting the odd 404 // Be default, only 5xx are retried but we're getting the odd 404