diff --git a/desktop/app/src/RenderHost.tsx b/desktop/app/src/RenderHost.tsx index 663edca4a..c503fe3bd 100644 --- a/desktop/app/src/RenderHost.tsx +++ b/desktop/app/src/RenderHost.tsx @@ -7,10 +7,20 @@ * @format */ -import {NotificationEvents} from './dispatcher/notifications'; -import {PluginNotification} from './reducers/notifications'; +import type {NotificationEvents} from './dispatcher/notifications'; +import type {PluginNotification} from './reducers/notifications'; 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 type MainProcessEvents = { @@ -45,6 +55,7 @@ type ChildProcessEvents = { export interface RenderHost { readonly processId: number; readTextFromClipboard(): string | undefined; + writeTextToClipboard(text: string): void; showSaveDialog?: FlipperLib['showSaveDialog']; showOpenDialog?: FlipperLib['showOpenDialog']; showSelectDirectoryDialog?(defaultPath?: string): Promise; @@ -60,6 +71,9 @@ export interface RenderHost { ): void; shouldUseDarkColors(): boolean; restartFlipper(update?: boolean): void; + env: Partial>; + paths: Record; + openLink(url: string): void; } let renderHostInstance: RenderHost | undefined; @@ -81,6 +95,7 @@ if (process.env.NODE_ENV === 'test') { readTextFromClipboard() { return ''; }, + writeTextToClipboard() {}, registerShortcut() {}, hasFocus() { return true; @@ -91,5 +106,15 @@ if (process.env.NODE_ENV === 'test') { return false; }, 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/`, + }, }); } diff --git a/desktop/app/src/chrome/ScreenCaptureButtons.tsx b/desktop/app/src/chrome/ScreenCaptureButtons.tsx index bf1b70279..3ba3c4333 100644 --- a/desktop/app/src/chrome/ScreenCaptureButtons.tsx +++ b/desktop/app/src/chrome/ScreenCaptureButtons.tsx @@ -12,7 +12,7 @@ import React, {useState, useEffect, useCallback} from 'react'; import path from 'path'; import fs from 'fs-extra'; 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 {useStore} from '../utils/useStore'; @@ -83,7 +83,7 @@ export default function ScreenCaptureButtons() { } if (!isRecording) { setIsRecording(true); - const videoPath = path.join(CAPTURE_LOCATION, getFileName('mp4')); + const videoPath = path.join(getCaptureLocation(), getFileName('mp4')); return selectedDevice.startScreenCapture(videoPath).catch((e) => { console.error('Failed to start recording', e); message.error('Failed to start recording' + e); diff --git a/desktop/app/src/dispatcher/flipperServer.tsx b/desktop/app/src/dispatcher/flipperServer.tsx index 9fd4c0a3d..688ce5869 100644 --- a/desktop/app/src/dispatcher/flipperServer.tsx +++ b/desktop/app/src/dispatcher/flipperServer.tsx @@ -18,8 +18,9 @@ import BaseDevice from '../devices/BaseDevice'; import {ClientDescription, timeout} from 'flipper-common'; import {reportPlatformFailures} from 'flipper-common'; import {sideEffect} from '../utils/sideEffect'; -import {getAppTempPath, getStaticPath} from '../utils/pathUtils'; +import {getStaticPath} from '../utils/pathUtils'; import constants from '../fb-stubs/constants'; +import {getRenderHostInstance} from '../RenderHost'; export default async (store: Store, logger: Logger) => { const {enableAndroid, androidHome, idbPath, enableIOS, enablePhysicalIOS} = @@ -33,7 +34,7 @@ export default async (store: Store, logger: Logger) => { enableIOS, enablePhysicalIOS, staticPath: getStaticPath(), - tmpPath: getAppTempPath(), + tmpPath: getRenderHostInstance().paths.tempPath, validWebSocketOrigins: constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES, }, logger, diff --git a/desktop/app/src/electron/initializeElectron.tsx b/desktop/app/src/electron/initializeElectron.tsx index c91dbfdfc..6ac7b2708 100644 --- a/desktop/app/src/electron/initializeElectron.tsx +++ b/desktop/app/src/electron/initializeElectron.tsx @@ -15,17 +15,27 @@ import { _LoggerContext, } from 'flipper-plugin'; // eslint-disable-next-line flipper/no-electron-remote-imports -import {ipcRenderer, remote, SaveDialogReturnValue} from 'electron'; -import {setRenderHostInstance} from '../RenderHost'; -import {clipboard} from 'electron'; -import restart from './restartFlipper'; +import { + ipcRenderer, + remote, + SaveDialogReturnValue, + clipboard, + shell, +} from 'electron'; +import {getRenderHostInstance, setRenderHostInstance} from '../RenderHost'; +import isProduction from '../utils/isProduction'; +import fs from 'fs'; export function initializeElectron() { + const app = remote.app; setRenderHostInstance({ processId: remote.process.pid, readTextFromClipboard() { return clipboard.readText(); }, + writeTextToClipboard(text: string) { + clipboard.writeText(text); + }, async showSaveDialog(options) { return (await remote.dialog.showSaveDialog(options))?.filePath; }, @@ -56,6 +66,9 @@ export function initializeElectron() { return undefined; }); }, + openLink(url: string) { + shell.openExternal(url); + }, registerShortcut(shortcut, callback) { remote.globalShortcut.register(shortcut, callback); }, @@ -76,5 +89,50 @@ export function initializeElectron() { restartFlipper() { 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', + }); + } +} diff --git a/desktop/app/src/electron/restartFlipper.tsx b/desktop/app/src/electron/restartFlipper.tsx deleted file mode 100644 index f4677531c..000000000 --- a/desktop/app/src/electron/restartFlipper.tsx +++ /dev/null @@ -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', - }); - } -} diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index 3bfc6cd20..f28227fc1 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -7,55 +7,13 @@ * @format */ -import {Provider} from 'react-redux'; -import ReactDOM from 'react-dom'; +import {initializeElectron} from './electron/initializeElectron'; 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 os from 'os'; -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'; -import {initializeElectron} from './electron/initializeElectron'; + +initializeElectron(); if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { // By default Node.JS has its internal certificate storage and doesn't use @@ -70,187 +28,9 @@ enableMapSet(); GK.init(); -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 ? ( - - - - A crash was detected in the Flipper chrome. Filing a{' '} - - bug report - {' '} - would be appreciated! Please include the details below. -

- } - extra={[ - , - , - ]} - /> - -
-
- ) : ( - <_LoggerContext.Provider value={logger}> - - - - - <_NuxManagerContext.Provider value={_createNuxManager()}> - - - - - - - - ); - } - - 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( - , - 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, -}); +// By turning this in a require, we force the JS that the body of this module (init) has completed (initializeElectron), +// before starting the rest of the Flipper process. +// This prevent issues where the render host is referred at module initialisation level, +// but not set yet, which might happen when using normal imports. +// eslint-disable-next-line import/no-commonjs +require('./startFlipperDesktop'); diff --git a/desktop/app/src/reducers/settings.tsx b/desktop/app/src/reducers/settings.tsx index 665d3bb24..29944fd81 100644 --- a/desktop/app/src/reducers/settings.tsx +++ b/desktop/app/src/reducers/settings.tsx @@ -9,7 +9,7 @@ import {Actions} from './index'; import os from 'os'; -import electron from 'electron'; +import {getRenderHostInstance} from '../RenderHost'; export enum Tristate { True, @@ -105,6 +105,7 @@ function getDefaultAndroidSdkPath() { } function getWindowsSdkPath() { - const app = electron.app || electron.remote.app; - return `${app.getPath('home')}\\AppData\\Local\\android\\sdk`; + return `${ + getRenderHostInstance().paths.homePath + }\\AppData\\Local\\android\\sdk`; } diff --git a/desktop/app/src/startFlipperDesktop.tsx b/desktop/app/src/startFlipperDesktop.tsx new file mode 100644 index 000000000..1100cec3b --- /dev/null +++ b/desktop/app/src/startFlipperDesktop.tsx @@ -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 ? ( + + + + A crash was detected in the Flipper chrome. Filing a{' '} + + bug report + {' '} + would be appreciated! Please include the details below. +

+ } + extra={[ + , + , + ]} + /> + +
+
+ ) : ( + <_LoggerContext.Provider value={logger}> + + + + + <_NuxManagerContext.Provider value={_createNuxManager()}> + + + + + + + + ); + } + + 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( + , + 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, +}); diff --git a/desktop/app/src/utils/flipperLibImplementation.tsx b/desktop/app/src/utils/flipperLibImplementation.tsx index 69280aa6d..6ee718565 100644 --- a/desktop/app/src/utils/flipperLibImplementation.tsx +++ b/desktop/app/src/utils/flipperLibImplementation.tsx @@ -13,7 +13,6 @@ import type {Store} from '../reducers'; import createPaste from '../fb-stubs/createPaste'; import GK from '../fb-stubs/GK'; import type BaseDevice from '../devices/BaseDevice'; -import {clipboard, shell} from 'electron'; import constants from '../fb-stubs/constants'; import {addNotification} from '../reducers/notifications'; import {deconstructPluginKey} from 'flipper-common'; @@ -49,12 +48,8 @@ export function initializeFlipperLibImplementation( }, }); }, - writeTextToClipboard(text: string) { - clipboard.writeText(text); - }, - openLink(url: string) { - shell.openExternal(url); - }, + writeTextToClipboard: renderHost.writeTextToClipboard, + openLink: renderHost.openLink, showNotification(pluginId, notification) { const parts = deconstructPluginKey(pluginId); store.dispatch( @@ -69,5 +64,9 @@ export function initializeFlipperLibImplementation( showSaveDialog: renderHost.showSaveDialog, showOpenDialog: renderHost.showOpenDialog, showSelectDirectoryDialog: renderHost.showSelectDirectoryDialog, + paths: { + appPath: renderHost.paths.appPath, + homePath: renderHost.paths.homePath, + }, }); } diff --git a/desktop/app/src/utils/icons.ts b/desktop/app/src/utils/icons.ts index 24b5cdadd..730d19e89 100644 --- a/desktop/app/src/utils/icons.ts +++ b/desktop/app/src/utils/icons.ts @@ -13,8 +13,7 @@ import fs from 'fs'; import path from 'path'; -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; +import {getRenderHostInstance} from '../RenderHost'; import {getStaticPath} from './pathUtils'; 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 const isProduction = !/node_modules[\\/]electron[\\/]/.test( - process.execPath || remote.process.execPath, + getRenderHostInstance().paths.execPath, ); if (!isProduction) { @@ -126,7 +125,12 @@ export function buildIconURLSync(name: string, size: number, density: number) { 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) { return name; } @@ -161,15 +165,8 @@ export function getIconURLSync(name: string, size: number, density: number) { } // resolve icon locally if possible - if ( - remote && - fs.existsSync( - path.join( - remote.app.getAppPath(), - buildLocalIconPath(name, size, density), - ), - ) - ) { + const iconPath = path.join(basePath, buildLocalIconPath(name, size, density)); + if (fs.existsSync(iconPath)) { return buildLocalIconURL(name, size, density); } return buildIconURLSync(name, requestedSize, density); diff --git a/desktop/app/src/utils/isProduction.tsx b/desktop/app/src/utils/isProduction.tsx index 72e69c09f..301fa790b 100644 --- a/desktop/app/src/utils/isProduction.tsx +++ b/desktop/app/src/utils/isProduction.tsx @@ -7,14 +7,15 @@ * @format */ -import electron from 'electron'; +import {getRenderHostInstance} from '../RenderHost'; -const _isProduction = !/node_modules[\\/]electron[\\/]/.test( - // 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, -); +let _isProduction: boolean | undefined; export default function isProduction(): boolean { + if (_isProduction === undefined) { + _isProduction = !/node_modules[\\/]electron[\\/]/.test( + getRenderHostInstance().paths.execPath, + ); + } return _isProduction; } diff --git a/desktop/app/src/utils/packageMetadata.tsx b/desktop/app/src/utils/packageMetadata.tsx index b4d3aaa7b..9c3c4e559 100644 --- a/desktop/app/src/utils/packageMetadata.tsx +++ b/desktop/app/src/utils/packageMetadata.tsx @@ -7,18 +7,14 @@ * @format */ -import electron from 'electron'; import lodash from 'lodash'; -import isProduction from './isProduction'; import path from 'path'; import fs from 'fs'; import {promisify} from 'util'; +import {getRenderHostInstance} from '../RenderHost'; const getPackageJSON = async () => { - const base = - isProduction() && electron.remote - ? electron.remote.app.getAppPath() - : process.cwd(); + const base = getRenderHostInstance().paths.appPath; const content = await promisify(fs.readFile)( path.join(base, 'package.json'), 'utf-8', diff --git a/desktop/app/src/utils/pathUtils.tsx b/desktop/app/src/utils/pathUtils.tsx index 6807590e3..32a4da8d7 100644 --- a/desktop/app/src/utils/pathUtils.tsx +++ b/desktop/app/src/utils/pathUtils.tsx @@ -12,35 +12,15 @@ import path from 'path'; 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'; - -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; -} +import {getRenderHostInstance} from '../RenderHost'; export function getStaticPath( relativePath: string = '.', {asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false}, ) { - const staticDir = getStaticDir(); + const staticDir = getRenderHostInstance().paths.staticPath; const absolutePath = path.resolve(staticDir, relativePath); // 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. @@ -51,26 +31,6 @@ export function getStaticPath( : 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() { const changelogPath = getStaticPath(config.isFBBuild ? 'facebook' : '.'); if (fs.existsSync(changelogPath)) { diff --git a/desktop/app/src/utils/processConfig.tsx b/desktop/app/src/utils/processConfig.tsx index 7cf0f9f03..b611c07fa 100644 --- a/desktop/app/src/utils/processConfig.tsx +++ b/desktop/app/src/utils/processConfig.tsx @@ -7,8 +7,7 @@ * @format */ -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; +import {getRenderHostInstance} from '../RenderHost'; export type ProcessConfig = { disabledPlugins: Set; @@ -27,9 +26,7 @@ export type ProcessConfig = { let configObj: ProcessConfig | null = null; export default function config(): ProcessConfig { if (configObj === null) { - const json = JSON.parse( - (remote && remote.process.env.CONFIG) || process.env.CONFIG || '{}', - ); + const json = JSON.parse(getRenderHostInstance().env.CONFIG || '{}'); configObj = { disabledPlugins: new Set(json.disabledPlugins || []), lastWindowPosition: json.lastWindowPosition, diff --git a/desktop/app/src/utils/screenshot.tsx b/desktop/app/src/utils/screenshot.tsx index ad9a19e55..2305654cf 100644 --- a/desktop/app/src/utils/screenshot.tsx +++ b/desktop/app/src/utils/screenshot.tsx @@ -12,14 +12,14 @@ import path from 'path'; import BaseDevice from '../devices/BaseDevice'; import {reportPlatformFailures} from 'flipper-common'; import expandTilde from 'expand-tilde'; -// eslint-disable-next-line flipper/no-electron-remote-imports -import {remote} from 'electron'; import config from '../utils/processConfig'; +import {getRenderHostInstance} from '../RenderHost'; -// TODO: refactor so this doesn't need to be exported -export const CAPTURE_LOCATION = expandTilde( - config().screenCapturePath || remote.app.getPath('desktop'), -); +export function getCaptureLocation() { + return expandTilde( + config().screenCapturePath || getRenderHostInstance().paths.desktopPath, + ); +} // TODO: refactor so this doesn't need to be exported export function getFileName(extension: 'png' | 'mp4'): string { @@ -32,7 +32,7 @@ export async function capture(device: BaseDevice): Promise { console.log('Skipping screenshot for disconnected device'); return ''; } - const pngPath = path.join(CAPTURE_LOCATION, getFileName('png')); + const pngPath = path.join(getCaptureLocation(), getFileName('png')); return reportPlatformFailures( device.screenshot().then((buffer) => writeBufferToFile(pngPath, buffer)), 'captureScreenshot', diff --git a/desktop/flipper-plugin/src/plugin/FlipperLib.tsx b/desktop/flipper-plugin/src/plugin/FlipperLib.tsx index a76df8e8b..062bc967f 100644 --- a/desktop/flipper-plugin/src/plugin/FlipperLib.tsx +++ b/desktop/flipper-plugin/src/plugin/FlipperLib.tsx @@ -48,6 +48,10 @@ export interface FlipperLib { }; }): Promise; showSelectDirectoryDialog?(defaultPath?: string): Promise; + paths: { + homePath: string; + appPath: string; + }; } export let flipperLibInstance: FlipperLib | undefined; diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 74364fc72..85e1e410a 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -379,6 +379,10 @@ export function createMockFlipperLib(options?: StartPluginOptions): FlipperLib { writeTextToClipboard: jest.fn(), openLink: jest.fn(), showNotification: jest.fn(), + paths: { + appPath: process.cwd(), + homePath: `/dev/null`, + }, }; } diff --git a/desktop/plugins/public/hermesdebuggerrn/ChromeDevTools.tsx b/desktop/plugins/public/hermesdebuggerrn/ChromeDevTools.tsx index 4776a9dcc..d6859fb22 100644 --- a/desktop/plugins/public/hermesdebuggerrn/ChromeDevTools.tsx +++ b/desktop/plugins/public/hermesdebuggerrn/ChromeDevTools.tsx @@ -10,8 +10,6 @@ import React from 'react'; import {styled, colors, FlexColumn} from 'flipper'; -import electron from 'electron'; - const devToolsNodeId = (url: string) => `hermes-chromedevtools-out-of-react-node-${url.replace(/\W+/g, '-')}`; @@ -28,10 +26,16 @@ function createDevToolsNode( return existing; } - // It is necessary to activate chrome devtools in electron - electron.remote.getCurrentWindow().webContents.toggleDevTools(); - electron.remote.getCurrentWindow().webContents.closeDevTools(); - + // It is necessary to deactivate chrome devtools in electron + try { + 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'); wrapper.id = devToolsNodeId(url); wrapper.style.height = '100%'; diff --git a/desktop/plugins/public/navigation/util/appMatchPatterns.tsx b/desktop/plugins/public/navigation/util/appMatchPatterns.tsx index 9298b6900..53004ae02 100644 --- a/desktop/plugins/public/navigation/util/appMatchPatterns.tsx +++ b/desktop/plugins/public/navigation/util/appMatchPatterns.tsx @@ -9,14 +9,14 @@ import fs from 'fs'; import path from 'path'; -import {getAppPath} from 'flipper'; import {AppMatchPattern} from '../types'; -import {Device} from 'flipper-plugin'; +import {Device, getFlipperLib} from 'flipper-plugin'; let patternsPath: string | undefined; function getPatternsBasePath() { - return (patternsPath = patternsPath ?? path.join(getAppPath(), 'facebook')); + return (patternsPath = + patternsPath ?? path.join(getFlipperLib().paths.appPath, 'facebook')); } const extractAppNameFromSelectedApp = (selectedApp: string | null) => { diff --git a/desktop/scripts/build-release.ts b/desktop/scripts/build-release.ts index 6f30e0490..9ba47e329 100755 --- a/desktop/scripts/build-release.ts +++ b/desktop/scripts/build-release.ts @@ -33,6 +33,7 @@ import { getIconsSync, buildLocalIconPath, getIconURLSync, + Icons, } from '../app/src/utils/icons'; import isFB from './isFB'; import copyPackageWithDependencies from './copy-package-with-dependencies'; @@ -309,7 +310,12 @@ async function copyStaticFolder(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; size: number; @@ -326,7 +332,7 @@ function downloadIcons(buildFolder: string) { return Promise.all( iconURLs.map(({name, size, density}) => { - const url = getIconURLSync(name, size, density); + const url = getIconURLSync(name, size, density, buildFolder); return fetch(url, { retryOptions: { // Be default, only 5xx are retried but we're getting the odd 404