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

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

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

Reviewed By: passy

Differential Revision: D20249304

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

View File

@@ -0,0 +1,66 @@
/**
* 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 {transform} from '@babel/core';
import electronProcess from '../electron-process';
const babelOptions = {
ast: true,
plugins: [electronProcess],
filename: 'index.js',
};
test('transform "process.exit(0);"', () => {
const src = 'process.exit(0);';
const code = transform(src, babelOptions).code;
expect(code).toMatchInlineSnapshot(`"electronProcess.exit(0);"`);
});
test('transform "global.process.exit(0);"', () => {
const src = 'global.process.exit(0);';
const code = transform(src, babelOptions).code;
expect(code).toMatchInlineSnapshot(`"global.electronProcess.exit(0);"`);
});
test('transform "process.ENV.TEST = "true";"', () => {
const src = 'process.ENV.TEST = "true";';
const code = transform(src, babelOptions).code;
expect(code).toMatchInlineSnapshot(
`"electronProcess.ENV.TEST = \\"true\\";"`,
);
});
test('do not transform if process bound in an upper scope', () => {
const src = `
const process = {};
for (const i=0; i<10; i++) {
process.ENV[i] = i;
}
`;
const code = transform(src, babelOptions).code;
expect(code).toMatchInlineSnapshot(`
"const process = {};
for (const i = 0; i < 10; i++) {
process.ENV[i] = i;
}"
`);
});
test('do not transform if process bound to the current scope', () => {
const src = `
const process = {};
process.ENV.TEST = "true";
`;
const code = transform(src, babelOptions).code;
expect(code).toMatchInlineSnapshot(`
"const process = {};
process.ENV.TEST = \\"true\\";"
`);
});

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {transform} from '@babel/core';
import electronStubs from '../electron-stubs';
const babelOptions = {
ast: true,
plugins: [electronStubs],
filename: 'index.js',
};
test('transform electron requires to inlined stubs', () => {
const src = 'require("electron")';
const transformed = transform(src, babelOptions).ast;
const body = transformed.program.body[0];
expect(body.type).toBe('ExpressionStatement');
expect(body.expression.properties.map(p => p.key.name)).toContain('remote');
});

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {parse} from '@babel/parser';
import {transformFromAstSync} from '@babel/core';
import generate from '@babel/generator';
import flipperRequires from '../flipper-requires';
const babelOptions = {
ast: true,
plugins: [flipperRequires],
filename: 'index.js',
};
test('transform react requires to global object', () => {
const src = 'require("react")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.React;');
});
test('transform react-dom requires to global object', () => {
const src = 'require("react-dom")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.ReactDOM;');
});
test('transform flipper requires to global object', () => {
const src = 'require("flipper")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.Flipper;');
});
test('transform React identifier to global.React', () => {
const src = 'React;';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions).ast;
const {code} = generate(transformed);
expect(code).toBe('global.React;');
});
test.skip('throw error when requiring outside the plugin', () => {
const src = 'require("../test.js")';
const ast = parse(src);
expect(() => {
transformFromAstSync(ast, src, babelOptions);
}).toThrow();
});
test('allow requiring from parent folder as long as we stay in plugin folder', () => {
const src = 'require("../test.js")';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, {
...babelOptions,
root: '/path/to/plugin',
filename: '/path/to/plugin/subfolder/index.js',
}).ast;
const {code} = generate(transformed);
expect(code).toBe('require("../test.js");');
});

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
function isDynamicRequire(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
(node.arguments.length !== 1 || node.arguments[0].type !== 'StringLiteral')
);
}
module.exports = function(babel) {
const t = babel.types;
return {
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path) {
if (!isDynamicRequire(path.node)) {
return;
}
path.replaceWith(t.identifier('triggerDynamicRequireError'));
},
},
};
};

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
module.exports = function(babel, options) {
return {
name: 'change-process-to-electronProcess',
visitor: {
MemberExpression(path) {
if (
path.node.object.type === 'Identifier' &&
path.node.object.name === 'process' &&
!path.scope.hasBinding('process')
) {
path.node.object.name = 'electronProcess';
} else if (
path.node.object.type === 'MemberExpression' &&
path.node.object.object.type === 'Identifier' &&
path.node.object.object.name === 'global' &&
path.node.object.property.type === 'Identifier' &&
path.node.object.property.name === 'process'
) {
path.node.object.property.name = 'electronProcess';
}
},
},
};
};

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
function isRequire(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'StringLiteral'
);
}
module.exports = function(babel, options) {
return {
name: 'change-electron-to-electronRequire-in-main',
visitor: {
CallExpression(path) {
if (!isRequire(path.node)) {
return;
}
const source = path.node.arguments[0].value;
if (!source.startsWith('./')) {
path.node.callee.name = 'electronRequire';
}
},
},
};
};

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
const BUILTINS = [
'electron',
'buffer',
'child_process',
'crypto',
'dgram',
'dns',
'fs',
'http',
'https',
'net',
'os',
'readline',
'stream',
'string_decoder',
'tls',
'tty',
'zlib',
'constants',
'events',
'url',
'assert',
'util',
'path',
'perf_hooks',
'punycode',
'querystring',
'cluster',
'console',
'module',
'process',
'vm',
'domain',
'v8',
'repl',
'timers',
'node-fetch',
];
const IGNORED_MODULES = [
'bufferutil',
'utf-8-validate',
'spawn-sync',
'./src/logcat',
'./src/monkey',
'./src/adb',
];
function isRequire(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'StringLiteral'
);
}
module.exports = function(babel) {
const t = babel.types;
return {
name: 'infinity-import-react',
visitor: {
CallExpression(path) {
if (!isRequire(path.node)) {
return;
}
const source = path.node.arguments[0].value;
if (
BUILTINS.includes(source) ||
BUILTINS.some(moduleName => source.startsWith(`${moduleName}/`))
) {
path.node.callee.name = 'electronRequire';
}
if (IGNORED_MODULES.includes(source)) {
path.replaceWith(t.identifier('triggerReferenceError'));
}
},
},
};
};

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
const babylon = require('@babel/parser');
const fs = require('fs');
const electronStubs = babylon.parseExpression(
fs.readFileSync('static/electron-stubs.notjs').toString(),
);
module.exports = function(babel) {
return {
name: 'replace-electron-requires-with-stubs',
visitor: {
CallExpression(path) {
if (
path.node.type === 'CallExpression' &&
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'require' &&
path.node.arguments.length > 0
) {
if (path.node.arguments[0].value === 'electron') {
path.replaceWith(electronStubs);
}
}
},
},
};
};

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
const fs = require('fs');
const path = require('path');
const replaceFBStubs = fs.existsSync(
path.join(__dirname, '..', '..', 'src', 'fb'),
);
const requireFromFolder = (folder, path) =>
new RegExp(folder + '/[A-Za-z0-9.-_]+(.js)?$', 'g').test(path);
module.exports = function(babel) {
return {
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path) {
if (
replaceFBStubs &&
path.node.type === 'CallExpression' &&
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'require' &&
path.node.arguments.length > 0
) {
if (requireFromFolder('fb', path.node.arguments[0].value)) {
throw new Error(
'Do not require directly from fb/, but rather from fb-stubs/ to not break flow-typing and make sure stubs are up-to-date.',
);
} else if (
requireFromFolder('fb-stubs', path.node.arguments[0].value)
) {
path.node.arguments[0].value = path.node.arguments[0].value.replace(
'/fb-stubs/',
'/fb/',
);
}
}
},
},
};
};

View File

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

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
module.exports = function(babel) {
const t = babel.types;
return {
name: 'infinity-import-react',
visitor: {
Program: {
exit(path, state) {
if (state.get('NEEDS_REACT')) {
path.unshiftContainer('body', [
t.variableDeclaration('var', [
t.variableDeclarator(
t.identifier('React'),
t.callExpression(t.identifier('require'), [
t.stringLiteral('react'),
]),
),
]),
]);
}
},
},
ReferencedIdentifier(path, state) {
// mark react as needing to be imported
if (path.node.name === 'React' && !path.scope.getBinding('React')) {
state.set('NEEDS_REACT', true);
}
// replace Buffer with require('buffer')
if (path.node.name === 'Buffer' && !path.scope.getBinding('Buffer')) {
path.replaceWith(
t.memberExpression(
t.callExpression(t.identifier('require'), [
t.stringLiteral('buffer'),
]),
t.identifier('Buffer'),
),
);
}
},
},
};
};

View File

@@ -0,0 +1,157 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
const generate = require('@babel/generator').default;
const babylon = require('@babel/parser');
const babel = require('@babel/core');
const fs = require('fs');
const path = require('path');
const staticDir = path.resolve(__dirname, '..');
function transform({filename, options, src}) {
const isPlugin =
options.projectRoot && !__dirname.startsWith(options.projectRoot);
const isMain =
options.projectRoot &&
options.projectRoot === staticDir &&
!options.isTestRunner;
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
const presets = [
isMain
? [
require('../node_modules/@babel/preset-env'),
{targets: {electron: require('electron/package.json').version}},
]
: require('../node_modules/@babel/preset-react'),
];
const ast = babylon.parse(src, {
filename,
plugins: isTypeScript
? [
'jsx',
'typescript',
'classProperties',
'optionalChaining',
'nullishCoalescingOperator',
]
: [
'jsx',
['flow', {all: true}],
'classProperties',
'objectRestSpread',
'optionalChaining',
'nullishCoalescingOperator',
],
sourceType: 'module',
});
// run babel
const plugins = [];
if (!isTypeScript) {
plugins.push(
require('../node_modules/@babel/plugin-transform-modules-commonjs'),
require('../node_modules/@babel/plugin-proposal-object-rest-spread'),
require('../node_modules/@babel/plugin-proposal-class-properties'),
require('../node_modules/@babel/plugin-transform-flow-strip-types'),
require('../node_modules/@babel/plugin-proposal-optional-chaining'),
require('../node_modules/@babel/plugin-proposal-nullish-coalescing-operator'),
require('./dynamic-requires.js'),
);
} else {
plugins.push(
require('../node_modules/@babel/plugin-transform-typescript'),
require('../node_modules/@babel/plugin-proposal-class-properties'),
require('../node_modules/@babel/plugin-transform-modules-commonjs'),
require('../node_modules/@babel/plugin-proposal-optional-chaining'),
require('../node_modules/@babel/plugin-proposal-nullish-coalescing-operator'),
);
}
if (
fs.existsSync(
path.resolve(path.dirname(path.dirname(__dirname)), 'src', 'fb'),
)
) {
plugins.push(require('./fb-stubs.js'));
}
if (process.env.BUILD_HEADLESS) {
plugins.push(require('./electron-stubs.js'));
}
if (!options.isTestRunner) {
if (isMain) {
// For the main Electron process ("static" folder), to avoid issues with
// native node modules, we prevent Metro from resolving any installed modules.
// Instead all of them are just resolved from "node_modules" as usual.
plugins.push(require('./electron-requires-main'));
// Metro bundler messes up "global.process", so we're changing all its occurrences to "global.electronProcess" instead.
// https://github.com/facebook/metro/blob/7e6b3114fc4a9b07a8c0dd3797b1e0c55a4c32ad/packages/metro/src/lib/getPreludeCode.js#L24
plugins.push(require('./electron-process'));
} else {
// Replacing require statements with electronRequire to prevent metro from
// resolving them. electronRequire are resolved during runtime by electron.
// As the tests are not bundled by metro and run in @jest-runner/electron,
// electron imports are working out of the box.
plugins.push(require('./electron-requires'));
}
}
if (isPlugin) {
plugins.push(require('./flipper-requires.js'));
} else {
plugins.push(require('./import-react.js'));
}
const transformed = babel.transformFromAst(ast, src, {
ast: true,
babelrc: !filename.includes('node_modules'),
code: false,
comments: false,
compact: false,
root: options.projectRoot,
filename,
plugins,
presets,
sourceMaps: true,
retainLines: !!options.isTestRunner,
});
const result = generate(
transformed.ast,
{
filename,
sourceFileName: filename,
sourceMaps: true,
retainLines: !!options.isTestRunner,
},
src,
);
return {
ast: transformed.ast,
code: result.code,
filename,
map: result.map,
};
}
module.exports = {
transform,
// Disable caching of babel transforms all together. We haven't found a good
// way to cache our transforms, as they rely on side effects like env vars or
// the existence of folders in the file system.
getCacheKey: () => Math.random().toString(36),
process(src, filename, config, options) {
return transform({
src,
filename,
config,
options: {...options, isTestRunner: true},
});
},
};