remove window dependency

Summary:
Fixes required to be able to run Flipper in node.js:
* Adds checks if the `window`-object exists before using it, to allow running in node.
* Imports from within Flipper should directly reference the file they are requiring instead of `import from 'flipper'`. This was done in most of the places. Fixed a few occurrences where this wasn't the case. This is to prevent cyclic dependencies in node.
* shared packages (React, ReactDOM and Flipper) were exposed on the `window` before, changed this to `global` as this works in browser and node.
* Adds some missing methods to our electron stubs (used for testing and headless Flipper)

Reviewed By: passy

Differential Revision: D13786577

fbshipit-source-id: 145d560f1446e7d0bdec2acd8dd54dae983d7b36
This commit is contained in:
Daniel Büchele
2019-01-25 12:10:31 -08:00
committed by Facebook Github Bot
parent 7ac6a09af1
commit 771be72b3f
15 changed files with 77 additions and 46 deletions

View File

@@ -7,7 +7,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {Sidebar} from 'flipper'; import Sidebar from '../ui/components/Sidebar';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {toggleRightSidebarAvailable} from '../reducers/application.js'; import {toggleRightSidebarAvailable} from '../reducers/application.js';
@@ -15,7 +15,7 @@ type Props = {
children: any, children: any,
rightSidebarVisible: boolean, rightSidebarVisible: boolean,
rightSidebarAvailable: boolean, rightSidebarAvailable: boolean,
toggleRightSidebarAvailable: (visible: boolean) => void, toggleRightSidebarAvailable: (visible?: boolean) => any,
width?: number, width?: number,
}; };

View File

@@ -11,10 +11,14 @@ import type {
TableColumnSizes, TableColumnSizes,
TableColumns, TableColumns,
} from 'flipper'; } from 'flipper';
import {FlexColumn, Button, DetailSidebar} from 'flipper';
import FlexColumn from './ui/components/FlexColumn';
import Button from './ui/components/Button';
import DetailSidebar from './chrome/DetailSidebar';
import {FlipperPlugin} from './plugin';
import SearchableTable from './ui/components/searchable/SearchableTable';
import textContent from './utils/textContent.js'; import textContent from './utils/textContent.js';
import createPaste from './utils/createPaste.js'; import createPaste from './utils/createPaste.js';
import {FlipperPlugin, SearchableTable} from 'flipper';
type ID = string; type ID = string;

View File

@@ -46,9 +46,12 @@ function forwardPort(port: number, multiplexChannelPort: number) {
const portForwarders: Array<ChildProcess> = GK.get('flipper_ios_device_support') const portForwarders: Array<ChildProcess> = GK.get('flipper_ios_device_support')
? [forwardPort(8089, 8079), forwardPort(8088, 8078)] ? [forwardPort(8089, 8079), forwardPort(8088, 8078)]
: []; : [];
window.addEventListener('beforeunload', () => {
portForwarders.forEach(process => process.kill()); if (typeof window !== 'undefined') {
}); window.addEventListener('beforeunload', () => {
portForwarders.forEach(process => process.kill());
});
}
function queryDevices(store: Store, logger: Logger): Promise<void> { function queryDevices(store: Store, logger: Logger): Promise<void> {
const {connections} = store.getState(); const {connections} = store.getState();

View File

@@ -12,7 +12,7 @@ import type {State} from '../reducers/plugins';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import * as Flipper from 'flipper'; import * as Flipper from '../index.js';
import { import {
registerPlugins, registerPlugins,
addGatekeepedPlugins, addGatekeepedPlugins,
@@ -20,11 +20,9 @@ import {
addFailedPlugins, addFailedPlugins,
} from '../reducers/plugins'; } from '../reducers/plugins';
import {remote} from 'electron'; import {remote} from 'electron';
import {GK} from 'flipper'; import GK from '../fb-stubs/GK';
import {FlipperBasePlugin} from '../plugin.js'; import {FlipperBasePlugin} from '../plugin.js';
import {setupMenuBar} from '../MenuBar.js'; import {setupMenuBar} from '../MenuBar.js';
import {setPluginState} from '../reducers/pluginStates.js';
import {getPersistedState} from '../utils/pluginUtils.js';
export type PluginDefinition = { export type PluginDefinition = {
name: string, name: string,
@@ -35,9 +33,10 @@ export type PluginDefinition = {
export default (store: Store, logger: Logger) => { export default (store: Store, logger: Logger) => {
// expose Flipper and exact globally for dynamically loaded plugins // expose Flipper and exact globally for dynamically loaded plugins
window.React = React; const globalObject = typeof window === 'undefined' ? global : window;
window.ReactDOM = ReactDOM; globalObject.React = React;
window.Flipper = Flipper; globalObject.ReactDOM = ReactDOM;
globalObject.Flipper = Flipper;
const gatekeepedPlugins: Array<PluginDefinition> = []; const gatekeepedPlugins: Array<PluginDefinition> = [];
const disabledPlugins: Array<PluginDefinition> = []; const disabledPlugins: Array<PluginDefinition> = [];
@@ -74,7 +73,7 @@ function getBundledPlugins(): Array<PluginDefinition> {
// List of defaultPlugins is written at build time // List of defaultPlugins is written at build time
let bundledPlugins: Array<PluginDefinition> = []; let bundledPlugins: Array<PluginDefinition> = [];
try { try {
bundledPlugins = window.electronRequire('./defaultPlugins/index.json'); bundledPlugins = global.electronRequire('./defaultPlugins/index.json');
} catch (e) {} } catch (e) {}
return bundledPlugins.map(plugin => ({ return bundledPlugins.map(plugin => ({
@@ -86,8 +85,10 @@ function getBundledPlugins(): Array<PluginDefinition> {
export function getDynamicPlugins() { export function getDynamicPlugins() {
let dynamicPlugins: Array<PluginDefinition> = []; let dynamicPlugins: Array<PluginDefinition> = [];
try { try {
// $FlowFixMe process.env not defined in electron API spec dynamicPlugins = JSON.parse(
dynamicPlugins = JSON.parse(remote?.process.env.PLUGINS || '[]'); // $FlowFixMe process.env not defined in electron API spec
remote?.process.env.PLUGINS || process.env.PLUGINS || '[]',
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@@ -119,7 +120,8 @@ export const checkDisabled = (disabledPlugins: Array<PluginDefinition>) => (
try { try {
disabledList = new Set( disabledList = new Set(
// $FlowFixMe process.env not defined in electron API spec // $FlowFixMe process.env not defined in electron API spec
JSON.parse(remote?.process.env.CONFIG || '{}').disabledPlugins || [], JSON.parse(remote?.process.env.CONFIG || process.env.CONFIG || '{}')
.disabledPlugins || [],
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -134,13 +136,13 @@ export const checkDisabled = (disabledPlugins: Array<PluginDefinition>) => (
export const requirePlugin = ( export const requirePlugin = (
failedPlugins: Array<[PluginDefinition, string]>, failedPlugins: Array<[PluginDefinition, string]>,
requireFunction: Function = window.electronRequire, reqFn: Function = global.electronRequire,
) => { ) => {
return ( return (
pluginDefinition: PluginDefinition, pluginDefinition: PluginDefinition,
): ?Class<FlipperPlugin<> | FlipperDevicePlugin<>> => { ): ?Class<FlipperPlugin<> | FlipperDevicePlugin<>> => {
try { try {
let plugin = requireFunction(pluginDefinition.out); let plugin = reqFn(pluginDefinition.out);
if (plugin.default) { if (plugin.default) {
plugin = plugin.default; plugin = plugin.default;
} }

View File

@@ -78,7 +78,9 @@ export default (store: Store, logger: Logger) => {
}, },
); );
window.addEventListener('beforeunload', () => { if (typeof window !== 'undefined') {
server.close(); window.addEventListener('beforeunload', () => {
}); server.close();
});
}
}; };

View File

@@ -29,10 +29,12 @@ export default (store: Store, logger: Logger) => {
} }
} }
droppedFrameDetection( if (typeof window !== 'undefined') {
performance.now(), droppedFrameDetection(
() => store.getState().application.windowIsFocused, performance.now(),
); () => store.getState().application.windowIsFocused,
);
}
ipcRenderer.on('trackUsage', () => { ipcRenderer.on('trackUsage', () => {
const { const {

View File

@@ -14,7 +14,8 @@ import type {Store} from './reducers/index.js';
import React from 'react'; import React from 'react';
import type {Node} from 'react'; import type {Node} from 'react';
import BaseDevice from './devices/BaseDevice.js'; import BaseDevice from './devices/BaseDevice.js';
import {AndroidDevice, IOSDevice} from 'flipper'; import AndroidDevice from './devices/AndroidDevice';
import IOSDevice from './devices/IOSDevice';
const invariant = require('invariant'); const invariant = require('invariant');

View File

@@ -13,7 +13,7 @@ import {
maybeSnapTop, maybeSnapTop,
SNAP_SIZE, SNAP_SIZE,
} from '../../utils/snap.js'; } from '../../utils/snap.js';
import {styled} from '../../ui'; import styled from '../styled/index.js';
const invariant = require('invariant'); const invariant = require('invariant');
const React = require('react'); const React = require('react');

View File

@@ -4,7 +4,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
* @format * @format
*/ */
import {Link} from 'flipper'; import Link from '../Link';
import type {DataInspectorSetValue} from './DataInspector.js'; import type {DataInspectorSetValue} from './DataInspector.js';
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import styled from '../../styled/index.js'; import styled from '../../styled/index.js';

View File

@@ -13,7 +13,7 @@ import Panel from '../Panel.js';
import ManagedDataInspector from '../data-inspector/ManagedDataInspector.js'; import ManagedDataInspector from '../data-inspector/ManagedDataInspector.js';
import {Component} from 'react'; import {Component} from 'react';
import {Console} from '../console'; import {Console} from '../console';
import {GK} from 'flipper'; import GK from '../../../fb-stubs/GK';
const deepEqual = require('deep-equal'); const deepEqual = require('deep-equal');

View File

@@ -96,7 +96,9 @@ export function getIconUrl(
} }
} }
let requestedScale: number = window.devicePixelRatio; let requestedScale: number =
typeof window !== 'undefined' ? window.devicePixelRatio : 1;
if (!SCALE.includes(requestedScale)) { if (!SCALE.includes(requestedScale)) {
// find the next largest size // find the next largest size
const possibleScale: ?number = SCALE.find(scale => { const possibleScale: ?number = SCALE.find(scale => {

View File

@@ -5,11 +5,25 @@
}, },
getCurrentWindow: function() { getCurrentWindow: function() {
return { return {
isFocused: function() {return true;} isFocused: function() {return true;},
on: function() {return true;}
}; };
}, },
app: { app: {
getVersion: function() {return '1';} getVersion: function() {return '1';},
getName: function() {return '';}
},
shell: {
openExternal: function() {}
},
Menu: {
buildFromTemplate: function() {
return {items: []}
},
setApplicationMenu: function() {}
} }
}, },
ipcRenderer: {
on: function() {return true;}
},
} }

View File

@@ -17,36 +17,36 @@ const babelOptions = {
filename: 'index.js', filename: 'index.js',
}; };
test('transform react requires to global window', () => { test('transform react requires to global object', () => {
const src = 'require("react")'; const src = 'require("react")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast; const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed); const {code} = generate(transformed);
expect(code).toBe('window.React;'); expect(code).toBe('global.React;');
}); });
test('transform react-dom requires to global window', () => { test('transform react-dom requires to global object', () => {
const src = 'require("react-dom")'; const src = 'require("react-dom")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast; const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed); const {code} = generate(transformed);
expect(code).toBe('window.ReactDOM;'); expect(code).toBe('global.ReactDOM;');
}); });
test('transform flipper requires to global window', () => { test('transform flipper requires to global object', () => {
const src = 'require("flipper")'; const src = 'require("flipper")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast; const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed); const {code} = generate(transformed);
expect(code).toBe('window.Flipper;'); expect(code).toBe('global.Flipper;');
}); });
test('transform React identifier to window.React', () => { test('transform React identifier to global.React', () => {
const src = 'React;'; const src = 'React;';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast; const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed); const {code} = generate(transformed);
expect(code).toBe('window.React;'); expect(code).toBe('global.React;');
}); });
test.skip('throw error when requiring outside the plugin', () => { test.skip('throw error when requiring outside the plugin', () => {

View File

@@ -29,6 +29,7 @@ const BUILTINS = [
'assert', 'assert',
'util', 'util',
'path', 'path',
'perf_hooks',
'punycode', 'punycode',
'querystring', 'querystring',
'cluster', 'cluster',

View File

@@ -37,11 +37,11 @@ module.exports = ({types: t}) => ({
t.isStringLiteral(args[0]) t.isStringLiteral(args[0])
) { ) {
if (args[0].value === 'flipper') { if (args[0].value === 'flipper') {
path.replaceWith(t.identifier('window.Flipper')); path.replaceWith(t.identifier('global.Flipper'));
} else if (args[0].value === 'react') { } else if (args[0].value === 'react') {
path.replaceWith(t.identifier('window.React')); path.replaceWith(t.identifier('global.React'));
} else if (args[0].value === 'react-dom') { } else if (args[0].value === 'react-dom') {
path.replaceWith(t.identifier('window.ReactDOM')); path.replaceWith(t.identifier('global.ReactDOM'));
} else if ( } else if (
// require a file not a pacakge // require a file not a pacakge
args[0].value.indexOf('/') > -1 && args[0].value.indexOf('/') > -1 &&
@@ -70,7 +70,7 @@ module.exports = ({types: t}) => ({
path.parentPath.node.id !== path.node && path.parentPath.node.id !== path.node &&
!isExcludedPath(state.file.opts.filename) !isExcludedPath(state.file.opts.filename)
) { ) {
path.replaceWith(t.identifier('window.React')); path.replaceWith(t.identifier('global.React'));
} }
}, },
}, },