Re-use babel transformations

Summary:
SORRY FOR BIG DIFF, but it's really hard to split it as all these changes are cross-dependent and should be made at once:
1. Moved transformations to separate package "flipper-babel-transformer" and linked it using yarn workspaces to "static" and "pkg" packages where they are re-used. Removed double copies of transformations we had before int these two packages.
2. Converted transformations to typescript
3. Refactored transformations to avoid relying on file system paths for customisation (FB stubs and Electron stubs for headless build)
4. As babel transformations must be built before other builds - enabled incremental build for them and changed scripts to invoke the transformations build before other build scripts
5. As we need to deploy all the dependencies including the fresh "flipper-babel-transformer" as a part of "static" - implemented script which copies package with all the dependencies taking in account yarn workspaces (hoisting and symlinks)

Reviewed By: passy, mweststrate

Differential Revision: D20690662

fbshipit-source-id: 38a275b60d3c91e01ec21d1dbd72d03c05cfac0b
This commit is contained in:
Anton Nikolaev
2020-03-27 03:21:58 -07:00
committed by Facebook GitHub Bot
parent 07a6a3b87d
commit c1bb656a0d
59 changed files with 1168 additions and 2690 deletions

View File

@@ -10,6 +10,7 @@
<PROJECT_ROOT>/desktop/static/.* <PROJECT_ROOT>/desktop/static/.*
<PROJECT_ROOT>/desktop/pkg/.* <PROJECT_ROOT>/desktop/pkg/.*
<PROJECT_ROOT>/desktop/doctor/.* <PROJECT_ROOT>/desktop/doctor/.*
<PROJECT_ROOT>/desktop/babel-transformer/.*
<PROJECT_ROOT>/desktop/plugins/fb/relaydevtools/relay-devtools/DevtoolsUI.js$ <PROJECT_ROOT>/desktop/plugins/fb/relaydevtools/relay-devtools/DevtoolsUI.js$
<PROJECT_ROOT>/website/.* <PROJECT_ROOT>/website/.*
<PROJECT_ROOT>/desktop/plugins/sections/d3/d3.js$ <PROJECT_ROOT>/desktop/plugins/sections/d3/d3.js$

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,8 @@
{
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": ["/node_modules/", "/lib/"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}

View File

@@ -0,0 +1,55 @@
{
"name": "flipper-babel-transformer",
"version": "0.2.0",
"description": "Babel transformer for Flipper plugins",
"repository": "facebook/flipper",
"main": "lib/index.js",
"flipper:source": "src",
"types": "lib/index.d.ts",
"license": "MIT",
"bugs": "https://github.com/facebook/flipper/issues",
"dependencies": {
"@babel/core": "^7.9.0",
"@babel/generator": "^7.9.0",
"@babel/parser": "^7.9.0",
"@babel/types": "^7.9.0",
"@babel/traverse": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.9.0",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/plugin-transform-flow-strip-types": "^7.9.0",
"@babel/plugin-transform-modules-commonjs": "^7.9.0",
"@babel/plugin-transform-typescript": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/preset-react": "^7.9.1",
"@types/fs-extra": "^8.1.0",
"@types/node": "^13.7.5",
"fs-extra": "^8.1.0",
"metro": "^0.58.0",
"tslib": "^1"
},
"devDependencies": {
"@types/jest": "25.1.4",
"jest": "^25.1.0",
"prettier": "^2.0.0",
"ts-jest": "^25.2.1",
"ts-node": "^8",
"typescript": "^3.7.2"
},
"scripts": {
"build": "tsc -b",
"prepack": "rm -rf lib && tsc -b",
"prepublishOnly": "yarn test",
"test": "jest --config jestconfig.json"
},
"files": [
"lib/**/*",
"src/**/*"
],
"homepage": "https://github.com/facebook/flipper",
"keywords": [
"Flipper"
],
"author": "Facebook, Inc"
}

View File

@@ -8,7 +8,7 @@
*/ */
import {transform} from '@babel/core'; import {transform} from '@babel/core';
import electronProcess from '../electron-process'; const electronProcess = require('../electron-process');
const babelOptions = { const babelOptions = {
ast: true, ast: true,
@@ -18,19 +18,19 @@ const babelOptions = {
test('transform "process.exit(0);"', () => { test('transform "process.exit(0);"', () => {
const src = 'process.exit(0);'; const src = 'process.exit(0);';
const code = transform(src, babelOptions).code; const code = transform(src, babelOptions)!.code;
expect(code).toMatchInlineSnapshot(`"electronProcess.exit(0);"`); expect(code).toMatchInlineSnapshot(`"electronProcess.exit(0);"`);
}); });
test('transform "global.process.exit(0);"', () => { test('transform "global.process.exit(0);"', () => {
const src = 'global.process.exit(0);'; const src = 'global.process.exit(0);';
const code = transform(src, babelOptions).code; const code = transform(src, babelOptions)!.code;
expect(code).toMatchInlineSnapshot(`"global.electronProcess.exit(0);"`); expect(code).toMatchInlineSnapshot(`"global.electronProcess.exit(0);"`);
}); });
test('transform "process.ENV.TEST = "true";"', () => { test('transform "process.ENV.TEST = "true";"', () => {
const src = 'process.ENV.TEST = "true";'; const src = 'process.ENV.TEST = "true";';
const code = transform(src, babelOptions).code; const code = transform(src, babelOptions)!.code;
expect(code).toMatchInlineSnapshot( expect(code).toMatchInlineSnapshot(
`"electronProcess.ENV.TEST = \\"true\\";"`, `"electronProcess.ENV.TEST = \\"true\\";"`,
); );
@@ -43,7 +43,7 @@ test('do not transform if process bound in an upper scope', () => {
process.ENV[i] = i; process.ENV[i] = i;
} }
`; `;
const code = transform(src, babelOptions).code; const code = transform(src, babelOptions)!.code;
expect(code).toMatchInlineSnapshot(` expect(code).toMatchInlineSnapshot(`
"const process = {}; "const process = {};
@@ -58,7 +58,7 @@ test('do not transform if process bound to the current scope', () => {
const process = {}; const process = {};
process.ENV.TEST = "true"; process.ENV.TEST = "true";
`; `;
const code = transform(src, babelOptions).code; const code = transform(src, babelOptions)!.code;
expect(code).toMatchInlineSnapshot(` expect(code).toMatchInlineSnapshot(`
"const process = {}; "const process = {};
process.ENV.TEST = \\"true\\";" process.ENV.TEST = \\"true\\";"

View File

@@ -8,7 +8,7 @@
*/ */
import {transform} from '@babel/core'; import {transform} from '@babel/core';
import electronStubs from '../electron-stubs'; const electronStubs = require('../electron-stubs');
const babelOptions = { const babelOptions = {
ast: true, ast: true,
@@ -18,8 +18,17 @@ const babelOptions = {
test('transform electron requires to inlined stubs', () => { test('transform electron requires to inlined stubs', () => {
const src = 'require("electron")'; const src = 'require("electron")';
const transformed = transform(src, babelOptions).ast; const transformed = transform(src, babelOptions)!.ast;
const body = transformed.program.body[0]; const body = transformed!.program.body[0];
expect(body.type).toBe('ExpressionStatement'); expect(body.type).toBe('ExpressionStatement');
expect(body.expression.properties.map((p) => p.key.name)).toContain('remote'); if (body.type !== 'ExpressionStatement') {
return;
}
expect(body.expression.type).toBe('ObjectExpression');
if (body.expression.type !== 'ObjectExpression') {
return;
}
expect(body.expression.properties.map((p) => (p as any).key.name)).toContain(
'remote',
);
}); });

View File

@@ -9,9 +9,8 @@
import {parse} from '@babel/parser'; import {parse} from '@babel/parser';
import {transformFromAstSync} from '@babel/core'; import {transformFromAstSync} from '@babel/core';
import generate from '@babel/generator'; import {default as generate} from '@babel/generator';
const flipperRequires = require('../flipper-requires');
import flipperRequires from '../flipper-requires';
const babelOptions = { const babelOptions = {
ast: true, ast: true,
@@ -22,36 +21,36 @@ const babelOptions = {
test('transform react requires to global object', () => { 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('global.React;'); expect(code).toBe('global.React;');
}); });
test('transform react-dom requires to global object', () => { 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('global.ReactDOM;'); expect(code).toBe('global.ReactDOM;');
}); });
test('transform flipper requires to global object', () => { 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('global.Flipper;'); expect(code).toBe('global.Flipper;');
}); });
test('transform React identifier to global.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('global.React;'); expect(code).toBe('global.React;');
}); });
test.skip('throw error when requiring outside the plugin', () => { test('throw error when requiring outside the plugin', () => {
const src = 'require("../test.js")'; const src = 'require("../test.js")';
const ast = parse(src); const ast = parse(src);
expect(() => { expect(() => {
@@ -66,7 +65,7 @@ test('allow requiring from parent folder as long as we stay in plugin folder', (
...babelOptions, ...babelOptions,
root: '/path/to/plugin', root: '/path/to/plugin',
filename: '/path/to/plugin/subfolder/index.js', filename: '/path/to/plugin/subfolder/index.js',
}).ast; })!.ast;
const {code} = generate(transformed); const {code} = generate(transformed!);
expect(code).toBe('require("../test.js");'); expect(code).toBe('require("../test.js");');
}); });

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
*/
import {CallExpression, identifier} from '@babel/types';
import {NodePath} from '@babel/traverse';
function isDynamicRequire(node: CallExpression) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
(node.arguments.length !== 1 || node.arguments[0].type !== 'StringLiteral')
);
}
module.exports = () => ({
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path: NodePath<CallExpression>) {
path.node;
if (!isDynamicRequire(path.node)) {
return;
}
path.replaceWith(identifier('triggerDynamicRequireError'));
},
},
});

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
*/
import {MemberExpression} from '@babel/types';
import {NodePath} from '@babel/traverse';
module.exports = () => ({
name: 'change-process-to-electronProcess',
visitor: {
MemberExpression(path: NodePath<MemberExpression>) {
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,32 @@
/**
* 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 {CallExpression} from '@babel/types';
import {NodePath} from '@babel/traverse';
module.exports = () => ({
name: 'change-electron-to-electronRequire-in-main',
visitor: {
CallExpression(path: NodePath<CallExpression>) {
const node = path.node;
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'StringLiteral'
) {
const source = node.arguments[0].value;
if (!source.startsWith('./')) {
node.callee.name = 'electronRequire';
}
}
},
},
});

View File

@@ -7,6 +7,9 @@
* @format * @format
*/ */
import {CallExpression, identifier} from '@babel/types';
import {NodePath} from '@babel/traverse';
const BUILTINS = [ const BUILTINS = [
'electron', 'electron',
'buffer', 'buffer',
@@ -55,40 +58,30 @@ const IGNORED_MODULES = [
'./src/adb', './src/adb',
]; ];
function isRequire(node) { module.exports = () => ({
return ( name: 'infinity-import-react',
visitor: {
CallExpression(path: NodePath<CallExpression>) {
const node = path.node;
if (
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.type === 'Identifier' && node.callee.type === 'Identifier' &&
node.callee.name === 'require' && node.callee.name === 'require' &&
node.arguments.length === 1 && node.arguments.length === 1 &&
node.arguments[0].type === 'StringLiteral' node.arguments[0].type === 'StringLiteral'
); ) {
} const source = node.arguments[0].value;
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 ( if (
BUILTINS.includes(source) || BUILTINS.includes(source) ||
BUILTINS.some((moduleName) => source.startsWith(`${moduleName}/`)) BUILTINS.some((moduleName) => source.startsWith(`${moduleName}/`))
) { ) {
path.node.callee.name = 'electronRequire'; node.callee.name = 'electronRequire';
} }
if (IGNORED_MODULES.includes(source)) { if (IGNORED_MODULES.includes(source)) {
path.replaceWith(t.identifier('triggerReferenceError')); path.replaceWith(identifier('triggerReferenceError'));
}
} }
}, },
}, },
}; });
};

View File

@@ -0,0 +1,65 @@
/**
* 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 {parseExpression} from '@babel/parser';
import {CallExpression} from '@babel/types';
import {NodePath} from '@babel/traverse';
const electronStubs = parseExpression(
`{
remote: {
process: {
env: {},
},
getCurrentWindow: function() {
return {
isFocused: function() {return true;},
on: function() {return true;}
};
},
app: {
getVersion: function() {return global.__VERSION__ || '1';},
getName: function() {return '';},
getAppPath: function() {return process.cwd();}
},
shell: {
openExternal: function() {}
},
Menu: {
buildFromTemplate: function() {
return {items: []}
},
setApplicationMenu: function() {}
}
},
ipcRenderer: {
on: function() {return true;}
},
}
`,
);
module.exports = () => ({
name: 'replace-electron-requires-with-stubs',
visitor: {
CallExpression(path: NodePath<CallExpression>) {
const node = path.node;
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length > 0 &&
node.arguments[0].type === 'StringLiteral' &&
node.arguments[0].value === 'electron'
) {
path.replaceWith(electronStubs);
}
},
},
});

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import path from 'path';
import {CallExpression} from '@babel/types';
import {NodePath} from '@babel/traverse';
const isFBFile = (filePath: string) =>
filePath.includes(`${path.sep}fb${path.sep}`);
const requireFromFolder = (folder: string, path: string) =>
new RegExp(folder + '/[A-Za-z0-9.-_]+(.js)?$', 'g').test(path);
module.exports = () => ({
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path: NodePath<CallExpression>, state: any) {
if (
path.node.type === 'CallExpression' &&
path.node.callee.type === 'Identifier' &&
path.node.callee.name === 'require' &&
path.node.arguments.length > 0 &&
path.node.arguments[0].type === 'StringLiteral'
) {
if (
requireFromFolder('fb', path.node.arguments[0].value) &&
!isFBFile(state.file.opts.filename)
) {
throw new Error(
'For files which are not under fb/ 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

@@ -7,7 +7,15 @@
* @format * @format
*/ */
const {resolve, dirname} = require('path'); import {
CallExpression,
Identifier,
isStringLiteral,
identifier,
} from '@babel/types';
import {NodePath} from '@babel/traverse';
import {resolve, dirname, relative} from 'path';
// do not apply this transform for these paths // do not apply this transform for these paths
const EXCLUDE_PATHS = [ const EXCLUDE_PATHS = [
@@ -15,18 +23,17 @@ const EXCLUDE_PATHS = [
'relay-devtools/DevtoolsUI', 'relay-devtools/DevtoolsUI',
]; ];
function isExcludedPath(path) { function isExcludedPath(path: string) {
for (const epath of EXCLUDE_PATHS) { for (const epath of EXCLUDE_PATHS) {
if (path.indexOf(epath) > -1) { if (path.indexOf(epath) > -1) {
return true; return true;
} }
} }
return false; return false;
} // $FlowFixMe }
module.exports = ({types: t}) => ({ module.exports = () => ({
visitor: { visitor: {
// $FlowFixMe CallExpression(path: NodePath<CallExpression>, state: any) {
CallExpression(path, state) {
if (isExcludedPath(state.file.opts.filename)) { if (isExcludedPath(state.file.opts.filename)) {
return; return;
} }
@@ -34,47 +41,48 @@ module.exports = ({types: t}) => ({
const args = node.arguments || []; const args = node.arguments || [];
if ( if (
node.callee.type === 'Identifier' &&
node.callee.name === 'require' && node.callee.name === 'require' &&
args.length === 1 && args.length === 1 &&
t.isStringLiteral(args[0]) isStringLiteral(args[0])
) { ) {
if (args[0].value === 'flipper') { if (args[0].value === 'flipper') {
path.replaceWith(t.identifier('global.Flipper')); path.replaceWith(identifier('global.Flipper'));
} else if (args[0].value === 'react') { } else if (args[0].value === 'react') {
path.replaceWith(t.identifier('global.React')); path.replaceWith(identifier('global.React'));
} else if (args[0].value === 'react-dom') { } else if (args[0].value === 'react-dom') {
path.replaceWith(t.identifier('global.ReactDOM')); path.replaceWith(identifier('global.ReactDOM'));
} else if (args[0].value === 'adbkit') { } else if (args[0].value === 'adbkit') {
path.replaceWith(t.identifier('global.adbkit')); path.replaceWith(identifier('global.adbkit'));
} 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 &&
// in the plugin itself and not inside one of its dependencies // in the plugin itself and not inside one of its dependencies
state.file.opts.filename.indexOf('node_modules') === -1 && state.file.opts.filename.indexOf('node_modules') === -1 &&
// the resolved path for this file is outside the plugins root // the resolved path for this file is outside the plugins root
!resolve(dirname(state.file.opts.filename), args[0].value).startsWith( !resolve(
state.file.opts.root, state.file.opts.root,
) && relative(state.file.opts.cwd, dirname(state.file.opts.filename)),
!resolve(dirname(state.file.opts.filename), args[0].value).indexOf( args[0].value,
'/static/', ).startsWith(state.file.opts.root)
) < 0
) { ) {
throw new Error( throw new Error(
`Plugins cannot require files from outside their folder. Attempted to require ${resolve( `Plugins cannot require files from outside their folder. Attempted to require ${resolve(
dirname(state.file.opts.filename), state.file.opts.root,
relative(state.file.opts.cwd, dirname(state.file.opts.filename)),
args[0].value, args[0].value,
)} which isn't inside ${state.file.opts.root}`, )} which isn't inside ${state.file.opts.root}`,
); );
} }
} }
}, },
Identifier(path, state) { Identifier(path: NodePath<Identifier>, state: any) {
if ( if (
path.node.name === 'React' && path.node.name === 'React' &&
path.parentPath.node.id !== path.node && (path.parentPath.node as any).id !== path.node &&
!isExcludedPath(state.file.opts.filename) !isExcludedPath(state.file.opts.filename)
) { ) {
path.replaceWith(t.identifier('global.React')); path.replaceWith(identifier('global.React'));
} }
}, },
}, },

View File

@@ -0,0 +1,15 @@
/**
* 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
*/
// 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.
export default function getCacheKey() {
return Math.random().toString(36);
}

View File

@@ -0,0 +1,57 @@
/**
* 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 {
variableDeclarator,
variableDeclaration,
identifier,
callExpression,
stringLiteral,
memberExpression,
Identifier,
Program,
} from '@babel/types';
import {NodePath} from '@babel/traverse';
module.exports = () => ({
name: 'infinity-import-react',
visitor: {
Program: {
exit(path: NodePath<Program>, state: any) {
if (state.get('NEEDS_REACT')) {
path.unshiftContainer('body', [
variableDeclaration('var', [
variableDeclarator(
identifier('React'),
callExpression(identifier('require'), [stringLiteral('react')]),
),
]),
]);
}
},
},
ReferencedIdentifier(path: NodePath<Identifier>, state: any) {
// 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(
memberExpression(
callExpression(identifier('require'), [stringLiteral('buffer')]),
identifier('Buffer'),
),
);
}
},
},
});

View File

@@ -0,0 +1,18 @@
/**
* 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 {default as transform} from './transform-plugin';
import {default as getCacheKey} from './get-cache-key';
module.exports = {
transform,
getCacheKey,
};
export default transform;

View File

@@ -0,0 +1,38 @@
/**
* 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 {default as doTransform} from './transform';
import {default as getCacheKey} from './get-cache-key';
module.exports = {
transform,
getCacheKey,
};
function transform({
filename,
options,
src,
}: {
filename: string;
options: any;
src: string;
}) {
const presets = [require('@babel/preset-react')];
const plugins = [];
if (process.env.FLIPPER_FB) {
plugins.push(require('./fb-stubs'));
}
if (process.env.BUILD_HEADLESS) {
plugins.push(require('./electron-stubs'));
}
plugins.push(require('./electron-requires'));
plugins.push(require('./import-react'));
return doTransform({filename, options, src, presets, plugins});
}

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
*/
import {default as doTransform} from './transform';
import {default as getCacheKey} from './get-cache-key';
module.exports = {
transform,
getCacheKey,
};
function transform({
filename,
options,
src,
}: {
filename: string;
options: any;
src: string;
}) {
const presets = [require('@babel/preset-react')];
const plugins = [];
if (process.env.FLIPPER_FB) {
plugins.push(require('./fb-stubs'));
}
if (process.env.BUILD_HEADLESS) {
plugins.push(require('./electron-stubs'));
}
plugins.push(require('./import-react'));
return doTransform({filename, options, src, presets, plugins});
}

View File

@@ -0,0 +1,40 @@
/**
* 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 {default as doTransform} from './transform';
import {default as getCacheKey} from './get-cache-key';
module.exports = {
transform,
getCacheKey,
};
function transform({
filename,
options,
src,
}: {
filename: string;
options: any;
src: string;
}) {
const presets = [
[
require('@babel/preset-env'),
{targets: {electron: process.env.FLIPPER_ELECTRON_VERSION}},
],
];
const plugins = [];
if (process.env.FLIPPER_FB) {
plugins.push(require('./fb-stubs'));
}
plugins.push(require('./electron-requires-main'));
plugins.push(require('./electron-process'));
return doTransform({filename, options, src, presets, plugins});
}

View File

@@ -0,0 +1,36 @@
/**
* 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 {default as doTransform} from './transform';
export default function transform({
filename,
options,
src,
presets,
plugins,
}: {
filename: string;
options: any;
src: string;
presets: any[];
plugins: any[];
}) {
presets = presets ?? [require('@babel/preset-react')];
plugins = plugins ?? [];
if (process.env.FLIPPER_FB) {
plugins.push(require('./fb-stubs'));
}
if (process.env.BUILD_HEADLESS) {
plugins.push(require('./electron-stubs'));
}
plugins.push(require('./electron-requires'));
plugins.push(require('./flipper-requires'));
return doTransform({filename, options, src, presets, plugins});
}

View File

@@ -7,18 +7,47 @@
* @format * @format
*/ */
const generate = require('@babel/generator').default; import {default as generate} from '@babel/generator';
const babylon = require('@babel/parser'); import {parse} from '@babel/parser';
const babel = require('@babel/core'); import {transformFromAstSync} from '@babel/core';
const fs = require('fs');
const path = require('path');
function transform({filename, options, src}) { export default function transform({
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
const presets = [require('@babel/preset-react')];
const ast = babylon.parse(src, {
filename, filename,
options,
src,
presets,
plugins,
}: {
filename: string;
options: any;
src: string;
presets?: any[];
plugins?: any[];
}) {
presets = presets ?? [require('@babel/preset-react')];
plugins = plugins ?? [];
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
if (!isTypeScript) {
plugins.unshift(
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-proposal-object-rest-spread'),
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-transform-flow-strip-types'),
require('@babel/plugin-proposal-optional-chaining'),
require('@babel/plugin-proposal-nullish-coalescing-operator'),
require('./dynamic-requires'),
);
} else {
plugins.unshift(
require('@babel/plugin-transform-typescript'),
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-proposal-optional-chaining'),
require('@babel/plugin-proposal-nullish-coalescing-operator'),
);
}
const ast = parse(src, {
sourceFilename: filename,
plugins: isTypeScript plugins: isTypeScript
? [ ? [
'jsx', 'jsx',
@@ -37,46 +66,7 @@ function transform({filename, options, src}) {
], ],
sourceType: 'module', sourceType: 'module',
}); });
const transformed = transformFromAstSync(ast, src, {
// run babel
const plugins = [];
if (!isTypeScript) {
plugins.push(
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-proposal-object-rest-spread'),
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-transform-flow-strip-types'),
require('@babel/plugin-proposal-optional-chaining'),
require('@babel/plugin-proposal-nullish-coalescing-operator'),
require('./dynamic-requires.js'),
);
} else {
plugins.push(
require('@babel/plugin-transform-typescript'),
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-transform-modules-commonjs'),
require('@babel/plugin-proposal-optional-chaining'),
require('@babel/plugin-proposal-nullish-coalescing-operator'),
);
}
if (
fs.existsSync(
path.resolve(path.dirname(path.dirname(__dirname)), 'app', 'src', 'fb'),
)
) {
plugins.push(require('./fb-stubs.js'));
}
if (!options.isTestRunner) {
// 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'));
}
plugins.push(require('./flipper-requires.js'));
const transformed = babel.transformFromAst(ast, src, {
ast: true, ast: true,
babelrc: !filename.includes('node_modules'), babelrc: !filename.includes('node_modules'),
code: false, code: false,
@@ -89,8 +79,11 @@ function transform({filename, options, src}) {
sourceMaps: true, sourceMaps: true,
retainLines: !!options.isTestRunner, retainLines: !!options.isTestRunner,
}); });
if (!transformed) {
throw new Error('Failed to transform');
}
const result = generate( const result = generate(
transformed.ast, transformed.ast!,
{ {
filename, filename,
sourceFileName: filename, sourceFileName: filename,
@@ -106,19 +99,3 @@ function transform({filename, options, src}) {
map: result.map, 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},
});
},
};

View File

@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"allowJs": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/*"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"],
"rules": {
"interface-name": false,
"variable-name": false
}
}

View File

@@ -5,13 +5,11 @@
"workspaces": { "workspaces": {
"packages": [ "packages": [
"app", "app",
"babel-transformer",
"doctor", "doctor",
"headless-tests",
"pkg", "pkg",
"static", "static"
"headless-tests"
],
"nohoist": [
"flipper-static/**"
] ]
}, },
"build": { "build": {
@@ -73,15 +71,17 @@
"@types/react": "16.9.17", "@types/react": "16.9.17",
"@types/react-dom": "16.9.4", "@types/react-dom": "16.9.4",
"acorn": "7.1.1", "acorn": "7.1.1",
"minimist": "1.2.2" "minimist": "1.2.2",
"metro/temp": "0.9.0",
"ws": "7.2.0"
}, },
"jest": { "jest": {
"transform": { "transform": {
"^.*__tests__/.*\\.tsx?$": "ts-jest", "^.*__tests__/.*\\.tsx?$": "ts-jest",
"\\.(js|tsx?)$": "<rootDir>/static/transforms/index.js" "\\.(js|tsx?)$": "<rootDir>/scripts/jest-transform.js"
}, },
"setupFiles": [ "setupFiles": [
"<rootDir>/static/globalTestSetup.js" "<rootDir>/scripts/jest-setup.js"
], ],
"moduleNameMapper": { "moduleNameMapper": {
"^flipper$": "<rootDir>/app/src", "^flipper$": "<rootDir>/app/src",
@@ -91,13 +91,18 @@
"clearMocks": true "clearMocks": true
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.8.3",
"@jest-runner/electron": "^2.0.2", "@jest-runner/electron": "^2.0.2",
"@testing-library/react": "^9.3.0", "@testing-library/react": "^9.3.0",
"@types/algoliasearch": "^3.30.19", "@types/algoliasearch": "^3.30.19",
"@types/babel-code-frame": "^6.20.2", "@types/babel__code-frame": "^7.0.0",
"@types/babel__core": "^7.1.6",
"@types/babel__generator": "^7.6.1",
"@types/babel__traverse": "^7.0.9",
"@types/decompress": "4.2.3", "@types/decompress": "4.2.3",
"@types/deep-equal": "^1.0.1", "@types/deep-equal": "^1.0.1",
"@types/detect-port": "^1.1.0", "@types/detect-port": "^1.1.0",
"@types/electron-devtools-installer": "^2.2.0",
"@types/expand-tilde": "^2.0.0", "@types/expand-tilde": "^2.0.0",
"@types/express": "^4.17.2", "@types/express": "^4.17.2",
"@types/fb-watchman": "^2.0.0", "@types/fb-watchman": "^2.0.0",
@@ -135,7 +140,6 @@
"@types/yazl": "^2.4.2", "@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^2.19.2", "@typescript-eslint/eslint-plugin": "^2.19.2",
"@typescript-eslint/parser": "^2.19.2", "@typescript-eslint/parser": "^2.19.2",
"babel-code-frame": "^6.26.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"electron": "8.0.1", "electron": "8.0.1",
"electron-builder": "^22.3.2", "electron-builder": "^22.3.2",
@@ -162,6 +166,7 @@
"react-async": "^10.0.0", "react-async": "^10.0.0",
"recursive-readdir": "^2.2.2", "recursive-readdir": "^2.2.2",
"redux-mock-store": "^1.5.3", "redux-mock-store": "^1.5.3",
"rimraf": "^3.0.2",
"ts-jest": "^25.1.0", "ts-jest": "^25.1.0",
"ts-node": "^8.8.1", "ts-node": "^8.8.1",
"typescript": "^3.7.2" "typescript": "^3.7.2"
@@ -170,25 +175,32 @@
"preinstall": "node scripts/prepare-watchman-config.js && yarn config set ignore-engines", "preinstall": "node scripts/prepare-watchman-config.js && yarn config set ignore-engines",
"postinstall": "cross-env TS_NODE_FILES=true node --require ts-node/register scripts/yarn-install.ts && patch-package", "postinstall": "cross-env TS_NODE_FILES=true node --require ts-node/register scripts/yarn-install.ts && patch-package",
"rm-dist": "rimraf ../dist", "rm-dist": "rimraf ../dist",
"rm-modules": "rimraf plugins/**/node_modules pkg/node_modules doctor/node_modules static/node_modules node_modules", "rm-modules": "rimraf **/node_modules node_modules",
"rm-temp": "rimraf $TMPDIR/jest* $TMPDIR/react-native-packager*", "rm-temp": "rimraf $TMPDIR/jest* $TMPDIR/react-native-packager*",
"rm-bundle": "rimraf static/main.bundle.* pkg/lib doctor/lib", "rm-bundle": "rimraf static/main.bundle.* **/lib **/*.tsbuildinfo",
"reset": "yarn rm-dist && yarn rm-temp && yarn cache clean && yarn rm-bundle && yarn rm-modules", "reset": "yarn rm-dist && yarn rm-temp && yarn cache clean && yarn rm-bundle && yarn rm-modules",
"start": "cross-env NODE_ENV=development TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts --inspect=9229", "dev-server": "yarn build:babel-transformer && cross-env NODE_ENV=development TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts",
"start:break": "cross-env NODE_ENV=development TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts --inspect-brk=9229", "start": "yarn dev-server --inspect=9229",
"start:no-embedded-plugins": "cross-env NODE_ENV=development cross-env FLIPPER_NO_EMBEDDED_PLUGINS=true TS_NODE_FILES=true node --require ts-node/register scripts/start-dev-server.ts", "start:break": "yarn dev-server --inspect-brk=9229",
"build": "yarn rm-dist && cross-env NODE_ENV=production TS_NODE_FILES=true node --require ts-node/register scripts/build-release.ts $@", "start:no-embedded-plugins": "yarn start --no-embedded-plugins",
"build-headless": "yarn rm-dist && mkdir ../dist && cross-env NODE_ENV=production TS_NODE_FILES=true node --require ts-node/register scripts/build-headless.ts $@", "build:babel-transformer": "cd babel-transformer && yarn build",
"prebuild": "yarn build:babel-transformer && yarn rm-dist",
"build": "cross-env NODE_ENV=production TS_NODE_FILES=true node --require ts-node/register scripts/build-release.ts $@",
"prebuild-headless": "yarn build:babel-transformer",
"build-headless": "cross-env NODE_ENV=production TS_NODE_FILES=true node --require ts-node/register scripts/build-headless.ts $@",
"open-dist": "open ../dist/mac/Flipper.app --args --launcher=false",
"test-dist": "yarn build --mac && yarn open-dist --inspect=9229",
"test-dist:no-embedded-plugins": "yarn build --mac --no-embedded-plugins && yarn open-dist --inspect=9229",
"fix": "eslint . --fix --ext .js,.ts,.tsx", "fix": "eslint . --fix --ext .js,.ts,.tsx",
"test": "jest --testPathPattern=\"node\\.(js|ts|tsx)$\" --no-cache", "test": "yarn build:babel-transformer && jest --testPathPattern=\"node\\.(js|ts|tsx)$\" --no-cache",
"test:debug": "node --inspect node_modules/.bin/jest --runInBand", "test:debug": "yarn build:babel-transformer && node --inspect node_modules/.bin/jest --runInBand",
"test-electron": "jest --testPathPattern=\"electron\\.(js|ts|tsx)$\" --testEnvironment=@jest-runner/electron/environment --runner=@jest-runner/electron --no-cache", "test-electron": "yarn build:babel-transformer && jest --testPathPattern=\"electron\\.(js|ts|tsx)$\" --testEnvironment=@jest-runner/electron/environment --runner=@jest-runner/electron --no-cache",
"test-with-device": "USE_ELECTRON_STUBS=1 jest --testPathPattern=\"device\\.(js|ts|tsx)$\" --detectOpenHandles --no-cache", "test-with-device": "yarn build:babel-transformer && USE_ELECTRON_STUBS=1 jest --testPathPattern=\"device\\.(js|ts|tsx)$\" --detectOpenHandles --no-cache",
"lint:tsc": "tsc --noemit", "lint:tsc": "tsc --noemit",
"lint:eslint": "eslint . --ext .js,.ts,.tsx", "lint:eslint": "eslint . --ext .js,.ts,.tsx",
"lint:flow": "flow check", "lint:flow": "flow check",
"lint": "yarn lint:eslint && yarn lint:flow && yarn lint:tsc", "lint": "yarn lint:eslint && yarn lint:flow && yarn lint:tsc",
"everything": "yarn reset && yarn install && yarn lint && yarn test && yarn test-electron && yarn build --mac --win --linux && yarn build-headless --mac --linux && yarn start" "everything": "yarn reset && yarn install && yarn lint && yarn test && yarn test-electron && yarn build --mac --mac-dmg --win --linux && yarn build-headless --mac --linux && yarn start"
}, },
"optionalDependencies": { "optionalDependencies": {
"7zip-bin-mac": "^1.0.1" "7zip-bin-mac": "^1.0.1"

View File

@@ -12,17 +12,6 @@
}, },
"bugs": "https://github.com/facebook/flipper/issues", "bugs": "https://github.com/facebook/flipper/issues",
"dependencies": { "dependencies": {
"@babel/core": "^7.9.0",
"@babel/generator": "^7.9.3",
"@babel/parser": "^7.9.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.9.0",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/plugin-transform-flow-strip-types": "^7.9.0",
"@babel/plugin-transform-modules-commonjs": "^7.9.0",
"@babel/plugin-transform-typescript": "^7.9.0",
"@babel/preset-react": "^7.9.1",
"@oclif/command": "^1", "@oclif/command": "^1",
"@oclif/config": "^1", "@oclif/config": "^1",
"@oclif/plugin-help": "^2", "@oclif/plugin-help": "^2",
@@ -31,24 +20,26 @@
"@types/node": "^13.7.5", "@types/node": "^13.7.5",
"cli-ux": "^5.4.5", "cli-ux": "^5.4.5",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"metro": "^0.58.0", "flipper-babel-transformer": "0.2.0",
"inquirer": "^7.0.5", "inquirer": "^7.0.5",
"metro": "^0.58.0",
"tslib": "^1" "tslib": "^1"
}, },
"devDependencies": { "devDependencies": {
"@oclif/dev-cli": "^1", "@oclif/dev-cli": "^1",
"@types/jest": "^24.0.21", "@types/jest": "25.1.4",
"globby": "^10", "globby": "^10",
"jest": "^24.9.0", "jest": "^25.1.0",
"prettier": "^2.0.0", "prettier": "^2.0.0",
"ts-jest": "^24.1.0", "ts-jest": "^25.2.1",
"ts-node": "^8", "ts-node": "^8",
"typescript": "^3.7.2" "typescript": "^3.7.2"
}, },
"scripts": { "scripts": {
"prebuild": "cd ../babel-transformer && yarn build",
"build": "tsc -b", "build": "tsc -b",
"postpack": "rm -f oclif.manifest.json", "postpack": "rm -f oclif.manifest.json",
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme", "prepack": "rm -rf lib && yarn build && oclif-dev manifest && oclif-dev readme",
"prepare": "yarn run build", "prepare": "yarn run build",
"prepublishOnly": "yarn test && yarn run lint", "prepublishOnly": "yarn test && yarn run lint",
"preversion": "yarn run lint", "preversion": "yarn run lint",

View File

@@ -1,72 +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
*/
const {parse} = require('@babel/parser');
const {transformFromAstSync} = require('@babel/core');
const {default: generate} = require('@babel/generator');
const flipperRequires = require('../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

@@ -1,38 +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
*/
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, state) {
const t = babel.types;
return {
name: 'replace-dynamic-requires',
visitor: {
CallExpression(path) {
if (!isDynamicRequire(path.node)) {
return;
}
path.replaceWith(
t.identifier(
'triggerDynamicRequireError_' + state.file.opts.filename,
),
);
},
},
};
};

View File

@@ -1,94 +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
*/
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

@@ -1,35 +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
*/
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

@@ -1,51 +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
*/
const fs = require('fs');
const path = require('path');
const replaceFBStubs = fs.existsSync(
path.join(__dirname, '..', '..', 'app', 'src', 'fb'),
);
const isFBFile = (filePath) => filePath.includes(`${path.sep}fb${path.sep}`);
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, state) {
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) &&
!isFBFile(state.file.opts.filename)
) {
throw new Error(
'For files which are not under fb/ 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

@@ -1,81 +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
*/
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

@@ -52,12 +52,7 @@ export default async function runBuild(
createModuleIdFactory, createModuleIdFactory,
}, },
transformer: { transformer: {
babelTransformerPath: path.resolve( babelTransformerPath: require.resolve('flipper-babel-transformer'),
__dirname,
'..',
'transforms',
'index.js',
),
}, },
}, },
{ {

View File

@@ -7,7 +7,7 @@
* @format * @format
*/ */
import fs from 'fs'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import lineReplace from 'line-replace'; import lineReplace from 'line-replace';
import yazl from 'yazl'; import yazl from 'yazl';
@@ -19,6 +19,8 @@ import {
getVersionNumber, getVersionNumber,
genMercurialRevision, genMercurialRevision,
} from './build-utils'; } from './build-utils';
import isFB from './isFB';
import {distDir} from './paths';
const PLUGINS_FOLDER_NAME = 'plugins'; const PLUGINS_FOLDER_NAME = 'plugins';
@@ -68,6 +70,9 @@ async function createZip(buildDir: string, distDir: string, targets: string[]) {
} }
(async () => { (async () => {
if (isFB) {
process.env.FLIPPER_FB = 'true';
}
const targets: {mac?: string; linux?: string; win?: string} = {}; const targets: {mac?: string; linux?: string; win?: string} = {};
let platformPostfix: string = ''; let platformPostfix: string = '';
if (process.argv.indexOf('--mac') > -1) { if (process.argv.indexOf('--mac') > -1) {
@@ -95,7 +100,6 @@ async function createZip(buildDir: string, distDir: string, targets: string[]) {
process.env.BUILD_HEADLESS = 'true'; process.env.BUILD_HEADLESS = 'true';
const buildDir = await buildFolder(); const buildDir = await buildFolder();
const distDir = path.join(__dirname, '..', '..', 'dist');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('Created build directory', buildDir); console.log('Created build directory', buildDir);
await compileHeadless(buildDir); await compileHeadless(buildDir);
@@ -114,6 +118,7 @@ async function createZip(buildDir: string, distDir: string, targets: string[]) {
Object.values(targets).join(','), Object.values(targets).join(','),
'--debug', '--debug',
]); ]);
await fs.ensureDir(distDir);
await createZip(buildDir, distDir, Object.keys(targets)); await createZip(buildDir, distDir, Object.keys(targets));
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('✨ Done'); console.log('✨ Done');

View File

@@ -22,14 +22,13 @@ import {
} from './build-utils'; } from './build-utils';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import {getIcons, buildLocalIconPath, getIconURL} from '../app/src/utils/icons'; import {getIcons, buildLocalIconPath, getIconURL} from '../app/src/utils/icons';
import isFB from './isFB';
import copyPackageWithDependencies from './copy-package-with-dependencies';
import {staticDir, distDir} from './paths';
function generateManifest(versionNumber: string) { async function generateManifest(versionNumber: string) {
const filePath = path.join(__dirname, '..', '..', 'dist'); await fs.writeFile(
if (!fs.existsSync(filePath)) { path.join(distDir, 'manifest.json'),
fs.mkdirSync(filePath);
}
fs.writeFileSync(
path.join(__dirname, '..', '..', 'dist', 'manifest.json'),
JSON.stringify({ JSON.stringify({
package: 'com.facebook.sonar', package: 'com.facebook.sonar',
version_name: versionNumber, version_name: versionNumber,
@@ -37,7 +36,7 @@ function generateManifest(versionNumber: string) {
); );
} }
function modifyPackageManifest( async function modifyPackageManifest(
buildFolder: string, buildFolder: string,
versionNumber: string, versionNumber: string,
hgRevision: string | null, hgRevision: string | null,
@@ -56,7 +55,7 @@ function modifyPackageManifest(
if (hgRevision != null) { if (hgRevision != null) {
manifest.revision = hgRevision; manifest.revision = hgRevision;
} }
fs.writeFileSync( await fs.writeFile(
path.join(buildFolder, 'package.json'), path.join(buildFolder, 'package.json'),
JSON.stringify(manifest, null, ' '), JSON.stringify(manifest, null, ' '),
); );
@@ -74,7 +73,7 @@ async function buildDist(buildFolder: string) {
} }
postBuildCallbacks.push(() => postBuildCallbacks.push(() =>
spawn('zip', ['-qyr9', '../Flipper-mac.zip', 'Flipper.app'], { spawn('zip', ['-qyr9', '../Flipper-mac.zip', 'Flipper.app'], {
cwd: path.join(__dirname, '..', '..', 'dist', 'mac'), cwd: path.join(distDir, 'mac'),
encoding: 'utf-8', encoding: 'utf-8',
}), }),
); );
@@ -107,8 +106,8 @@ async function buildDist(buildFolder: string) {
config: { config: {
appId: `com.facebook.sonar`, appId: `com.facebook.sonar`,
directories: { directories: {
buildResources: path.join(__dirname, '..', 'static'), buildResources: buildFolder,
output: path.join(__dirname, '..', '..', 'dist'), output: distDir,
}, },
electronDownload: electronDownloadOptions, electronDownload: electronDownloadOptions,
npmRebuild: false, npmRebuild: false,
@@ -122,10 +121,8 @@ async function buildDist(buildFolder: string) {
} }
} }
function copyStaticFolder(buildFolder: string) { async function copyStaticFolder(buildFolder: string) {
fs.copySync(path.join(__dirname, '..', 'static'), buildFolder, { await copyPackageWithDependencies(staticDir, buildFolder);
dereference: true,
});
} }
function downloadIcons(buildFolder: string) { function downloadIcons(buildFolder: string) {
@@ -175,18 +172,25 @@ function downloadIcons(buildFolder: string) {
} }
(async () => { (async () => {
if (isFB) {
process.env.FLIPPER_FB = 'true';
}
const dir = await buildFolder(); const dir = await buildFolder();
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('Created build directory', dir); console.log('Created build directory', dir);
await compileMain({dev: false}); await compileMain({dev: false});
copyStaticFolder(dir); await copyStaticFolder(dir);
await downloadIcons(dir); await downloadIcons(dir);
if (!process.argv.includes('--no-embedded-plugins')) {
await compileDefaultPlugins(path.join(dir, 'defaultPlugins')); await compileDefaultPlugins(path.join(dir, 'defaultPlugins'));
}
await compileRenderer(dir); await compileRenderer(dir);
const versionNumber = getVersionNumber(); const versionNumber = getVersionNumber();
const hgRevision = await genMercurialRevision(); const hgRevision = await genMercurialRevision();
modifyPackageManifest(dir, versionNumber, hgRevision); await modifyPackageManifest(dir, versionNumber, hgRevision);
generateManifest(versionNumber); await fs.ensureDir(distDir);
await generateManifest(versionNumber);
await buildDist(dir); await buildDist(dir);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('✨ Done'); console.log('✨ Done');

View File

@@ -16,7 +16,13 @@ import fs from 'fs-extra';
import {spawn} from 'promisify-child-process'; import {spawn} from 'promisify-child-process';
import recursiveReaddir from 'recursive-readdir'; import recursiveReaddir from 'recursive-readdir';
import {default as getWatchFolders} from '../static/get-watch-folders'; import {default as getWatchFolders} from '../static/get-watch-folders';
import {appDir, staticDir, pluginsDir, headlessDir} from './paths'; import {
appDir,
staticDir,
pluginsDir,
headlessDir,
babelTransformationsDir,
} from './paths';
async function mostRecentlyChanged( async function mostRecentlyChanged(
dir: string, dir: string,
@@ -72,7 +78,10 @@ async function compile(
watchFolders, watchFolders,
serializer: {}, serializer: {},
transformer: { transformer: {
babelTransformerPath: path.join(staticDir, 'transforms', 'index.js'), babelTransformerPath: path.join(
babelTransformationsDir,
'transform-app',
),
}, },
resolver: { resolver: {
resolverMainFields: ['flipper:source', 'module', 'main'], resolverMainFields: ['flipper:source', 'module', 'main'],
@@ -138,6 +147,7 @@ export async function compileRenderer(buildFolder: string) {
export async function compileMain({dev}: {dev: boolean}) { export async function compileMain({dev}: {dev: boolean}) {
const out = path.join(staticDir, 'main.bundle.js'); const out = path.join(staticDir, 'main.bundle.js');
process.env.FLIPPER_ELECTRON_VERSION = require('electron/package.json').version;
// check if main needs to be compiled // check if main needs to be compiled
if (await fs.pathExists(out)) { if (await fs.pathExists(out)) {
const staticDirCtime = await mostRecentlyChanged(staticDir, ['*.bundle.*']); const staticDirCtime = await mostRecentlyChanged(staticDir, ['*.bundle.*']);
@@ -147,14 +157,17 @@ export async function compileMain({dev}: {dev: boolean}) {
return; return;
} }
} }
console.log(`⚙️ Compiling main bundle... ${staticDir}`); console.log('⚙️ Compiling main bundle...');
try { try {
const config = Object.assign({}, await Metro.loadConfig(), { const config = Object.assign({}, await Metro.loadConfig(), {
reporter: {update: () => {}}, reporter: {update: () => {}},
projectRoot: staticDir, projectRoot: staticDir,
watchFolders: await getWatchFolders(staticDir), watchFolders: await getWatchFolders(staticDir),
transformer: { transformer: {
babelTransformerPath: path.join(staticDir, 'transforms', 'index.js'), babelTransformerPath: path.join(
babelTransformationsDir,
'transform-main',
),
}, },
resolver: { resolver: {
sourceExts: ['tsx', 'ts', 'js'], sourceExts: ['tsx', 'ts', 'js'],

View File

@@ -0,0 +1,110 @@
/**
* 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 fs from 'fs-extra';
import path from 'path';
/**
* This function copies package into the specified target dir with all its dependencies:
* 1) Both direct and transitive dependencies are copied.
* 2) Symlinks are dereferenced and copied to the target dir as normal dirs.
* 3) Hoisting is supported, so the function scans node_modules up the file tree until dependency is resolved.
* 4) All the dependencies keep their scopes, e.g. dependency from <packageDir>/node_modules/package1/node_modules/package2
* is copied to <targetDir>/node_modules/package1/node_modules/package2.
* 5) Prints informative error and fails fast if a dependency is not resolved.
*/
export default async function copyPackageWithDependencies(
packageDir: string,
targetDir: string,
) {
await fs.remove(targetDir);
await copyPackageWithDependenciesRecursive(packageDir, targetDir, targetDir);
}
async function copyPackageWithDependenciesRecursive(
packageDir: string,
targetDir: string,
rootTargetDir: string,
) {
if (await fs.pathExists(targetDir)) {
return;
}
await fs.mkdirp(targetDir);
if ((await fs.stat(packageDir)).isSymbolicLink()) {
packageDir = await fs.readlink(packageDir);
}
await fs.copy(packageDir, targetDir, {
dereference: true,
recursive: true,
filter: (src) => !src.startsWith(path.join(packageDir, 'node_modules')),
});
const pkg = await fs.readJson(path.join(packageDir, 'package.json'));
const dependencies = (pkg.dependencies ?? {}) as {[key: string]: string};
let unresolvedCount = Object.keys(dependencies).length;
let curPackageDir = packageDir;
let curTargetDir = targetDir;
while (unresolvedCount > 0) {
const curPackageModulesDir = path.join(curPackageDir, 'node_modules');
if (await fs.pathExists(curPackageModulesDir)) {
for (const moduleName of Object.keys(dependencies)) {
const curModulePath = path.join(
curPackageModulesDir,
...moduleName.split('/'),
);
const targetModulePath = path.join(
curTargetDir,
'node_modules',
...moduleName.split('/'),
);
if (await fs.pathExists(curModulePath)) {
await copyPackageWithDependenciesRecursive(
curModulePath,
targetModulePath,
rootTargetDir,
);
delete dependencies[moduleName];
unresolvedCount--;
}
}
}
const parentPackageDir = getParentPackageDir(curPackageDir);
if (
!parentPackageDir ||
parentPackageDir === '' ||
parentPackageDir === curPackageDir
) {
break;
}
curPackageDir = parentPackageDir;
curTargetDir = getParentPackageDir(curTargetDir);
if (!curTargetDir || curTargetDir.length < rootTargetDir.length) {
curTargetDir = rootTargetDir;
}
}
if (unresolvedCount > 0) {
for (const unresolvedDependency of Object.keys(dependencies)) {
console.error(`Cannot resolve ${unresolvedDependency} in ${packageDir}`);
}
process.exit(1);
}
}
function getParentPackageDir(packageDir: string) {
packageDir = path.dirname(packageDir);
while (
path.basename(packageDir) === 'node_modules' ||
path.basename(packageDir).startsWith('@')
) {
packageDir = path.dirname(packageDir);
}
return packageDir;
}

11
desktop/scripts/isFB.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/**
* 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 isFB: boolean;
export default isFB;

16
desktop/scripts/isFB.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* 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
* @ts-check
*/
const fs = require('fs-extra');
const path = require('path');
const isFB = fs.pathExistsSync(path.resolve(__dirname, '..', 'static', 'fb'));
module.exports = isFB;

View File

@@ -0,0 +1,27 @@
/**
* 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 import/no-unresolved
const {transform} = require('../babel-transformer/lib/transform-jest');
const isFB = require('./isFB');
if (isFB && process.env.FLIPPER_FB === undefined) {
process.env.FLIPPER_FB = 'true';
}
module.exports = {
process(src, filename, config, options) {
return transform({
src,
filename,
config,
options: {...options, isTestRunner: true},
});
},
};

View File

@@ -14,3 +14,9 @@ export const appDir = path.join(rootDir, 'app');
export const staticDir = path.join(rootDir, 'static'); export const staticDir = path.join(rootDir, 'static');
export const pluginsDir = path.join(rootDir, 'plugins'); export const pluginsDir = path.join(rootDir, 'plugins');
export const headlessDir = path.join(rootDir, 'headless'); export const headlessDir = path.join(rootDir, 'headless');
export const distDir = path.resolve(rootDir, '..', 'dist');
export const babelTransformationsDir = path.resolve(
rootDir,
'babel-transformer',
'src',
);

View File

@@ -8,7 +8,7 @@
*/ */
const electronBinary: string = require('electron') as any; const electronBinary: string = require('electron') as any;
import codeFrame from 'babel-code-frame'; import codeFrame from '@babel/code-frame';
import socketIo from 'socket.io'; import socketIo from 'socket.io';
import express, {Express} from 'express'; import express, {Express} from 'express';
import detect from 'detect-port'; import detect from 'detect-port';
@@ -23,7 +23,8 @@ import Watchman from '../static/watchman';
import Metro from 'metro'; import Metro from 'metro';
import MetroResolver from 'metro-resolver'; import MetroResolver from 'metro-resolver';
import {default as getWatchFolders} from '../static/get-watch-folders'; import {default as getWatchFolders} from '../static/get-watch-folders';
import {staticDir, pluginsDir, appDir} from './paths'; import {staticDir, pluginsDir, appDir, babelTransformationsDir} from './paths';
import isFB from './isFB';
const ansiToHtmlConverter = new AnsiToHtmlConverter(); const ansiToHtmlConverter = new AnsiToHtmlConverter();
@@ -40,6 +41,9 @@ function launchElectron({
bundleURL: string; bundleURL: string;
electronURL: string; electronURL: string;
}) { }) {
if (process.argv.includes('--no-embedded-plugins')) {
process.env.FLIPPER_NO_EMBEDDED_PLUGINS = 'true';
}
const args = [ const args = [
path.join(staticDir, 'index.js'), path.join(staticDir, 'index.js'),
'--remote-debugging-port=9222', '--remote-debugging-port=9222',
@@ -88,7 +92,7 @@ async function startMetroServer(app: Express) {
projectRoot: appDir, projectRoot: appDir,
watchFolders, watchFolders,
transformer: { transformer: {
babelTransformerPath: path.join(staticDir, 'transforms', 'index.js'), babelTransformerPath: path.join(babelTransformationsDir, 'transform-app'),
}, },
resolver: { resolver: {
resolverMainFields: ['flipper:source', 'module', 'main'], resolverMainFields: ['flipper:source', 'module', 'main'],
@@ -251,6 +255,9 @@ function outputScreen(socket?: socketIo.Server) {
} }
(async () => { (async () => {
if (isFB && process.env.FLIPPER_FB === undefined) {
process.env.FLIPPER_FB = 'true';
}
const port = await detect(DEFAULT_PORT); const port = await detect(DEFAULT_PORT);
const {app, server} = await startAssetServer(port); const {app, server} = await startAssetServer(port);
const socket = await addWebsocket(server); const socket = await addWebsocket(server);

View File

@@ -270,7 +270,8 @@ async function compilePlugin(
console.log(`🥫 Using cached version of ${name}...`); console.log(`🥫 Using cached version of ${name}...`);
return result; return result;
} else { } else {
console.log(`⚙️ Compiling ${name}...`); // eslint-disable-line no-console // eslint-disable-line no-console
console.log(`⚙️ Compiling ${name}...`);
try { try {
await Metro.runBuild( await Metro.runBuild(
{ {
@@ -283,11 +284,9 @@ async function compilePlugin(
createModuleIdFactory, createModuleIdFactory,
}, },
transformer: { transformer: {
babelTransformerPath: path.join( babelTransformerPath: global.electronResolve
__dirname, ? global.electronResolve('flipper-babel-transformer') // when compilation is executing in Electron main process
'transforms', : require.resolve('flipper-babel-transformer'), // when compilation is is executing in Node.js script
'index.js',
),
}, },
resolver: { resolver: {
sourceExts: ['tsx', 'ts', 'js'], sourceExts: ['tsx', 'ts', 'js'],

View File

@@ -0,0 +1,12 @@
/**
* 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 isFB = false;
export default isFB;

View File

@@ -8,6 +8,7 @@
*/ */
global.electronRequire = require; global.electronRequire = require;
global.electronResolve = require.resolve;
global.electronProcess = process; global.electronProcess = process;
require('./main.bundle.js'); require('./main.bundle.js');

View File

@@ -25,6 +25,7 @@ import fixPath from 'fix-path';
import {exec} from 'child_process'; import {exec} from 'child_process';
import compilePlugins from './compilePlugins'; import compilePlugins from './compilePlugins';
import setup from './setup'; import setup from './setup';
import isFB from './fb-stubs/isFB';
import delegateToLauncher from './launcher'; import delegateToLauncher from './launcher';
import expandTilde from 'expand-tilde'; import expandTilde from 'expand-tilde';
import yargs from 'yargs'; import yargs from 'yargs';
@@ -85,6 +86,10 @@ const argv = yargs
const {config, configPath, flipperDir} = setup(argv); const {config, configPath, flipperDir} = setup(argv);
if (isFB && process.env.FLIPPER_FB === undefined) {
process.env.FLIPPER_FB = 'true';
}
const skipLoadingEmbeddedPlugins = process.env.FLIPPER_NO_EMBEDDED_PLUGINS; const skipLoadingEmbeddedPlugins = process.env.FLIPPER_NO_EMBEDDED_PLUGINS;
const pluginPaths = (config.pluginPaths ?? []) const pluginPaths = (config.pluginPaths ?? [])

View File

@@ -2,24 +2,15 @@
"name": "flipper-static", "name": "flipper-static",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"private": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.8.3",
"@babel/generator": "^7.8.3",
"@babel/parser": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-transform-flow-strip-types": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.8.3",
"@babel/plugin-transform-typescript": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"electron-devtools-installer": "^2.2.4", "electron-devtools-installer": "^2.2.4",
"expand-tilde": "^2.0.2", "expand-tilde": "^2.0.2",
"fb-watchman": "^2.0.0", "fb-watchman": "^2.0.0",
"fix-path": "^3.0.0", "fix-path": "^3.0.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"flipper-babel-transformer": "0.2.0",
"mem": "^6.0.0", "mem": "^6.0.0",
"metro": "^0.58.0", "metro": "^0.58.0",
"mkdirp": "^1.0.0", "mkdirp": "^1.0.0",
@@ -28,13 +19,5 @@
"uuid": "^7.0.1", "uuid": "^7.0.1",
"xdg-basedir": "^4.0.0", "xdg-basedir": "^4.0.0",
"yargs": "^15.0.1" "yargs": "^15.0.1"
},
"resolutions": {
"metro/temp": "0.9.0",
"ws": "7.2.0"
},
"devDependencies": {
"@babel/preset-env": "^7.8.3",
"@types/electron-devtools-installer": "^2.2.0"
} }
} }

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
*/
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

@@ -1,33 +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
*/
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

@@ -1,37 +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
*/
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

@@ -1,35 +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
*/
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

@@ -1,51 +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
*/
const fs = require('fs');
const path = require('path');
const replaceFBStubs = fs.existsSync(
path.join(__dirname, '..', '..', 'app', 'src', 'fb'),
);
const isFBFile = (filePath) => filePath.includes(`${path.sep}fb${path.sep}`);
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, state) {
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) &&
!isFBFile(state.file.opts.filename)
) {
throw new Error(
'For files which are not under fb/ 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

@@ -1,53 +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
*/
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

@@ -1,158 +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
*/
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 rootDir = path.resolve(__dirname, '..', '..');
const staticDir = path.join(rootDir, 'static');
const appDir = path.join(rootDir, 'app');
const headlessDir = path.join(rootDir, 'headless');
function transform({filename, options, src}) {
const isMain = options.projectRoot && options.projectRoot === staticDir;
const isPlugin =
options.projectRoot &&
![staticDir, appDir, headlessDir].includes(options.projectRoot);
const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts');
const presets = [
isMain && !options.isTestRunner
? [
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)), 'app', '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},
});
},
};

View File

@@ -7,6 +7,7 @@
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"sourceMap": true, "sourceMap": true,
"declaration": true,
"jsx": "react", "jsx": "react",
"moduleResolution": "node", "moduleResolution": "node",
"skipLibCheck": true, "skipLibCheck": true,

View File

@@ -6,7 +6,8 @@
"flipper": ["./app/src"], "flipper": ["./app/src"],
"flipper-doctor": ["./doctor/src"], "flipper-doctor": ["./doctor/src"],
"flipper-pkg": ["./pkg/src"], "flipper-pkg": ["./pkg/src"],
"live-plugin-manager": ["./types/live-plugin-manager.d.tsx"] "live-plugin-manager": ["./types/live-plugin-manager.d.tsx"],
"flipper-babel-transformer": ["./babel-transformer/src"]
} }
}, },
"include": [ "include": [

View File

@@ -12,6 +12,7 @@ declare module NodeJS {
__REVISION__: string | undefined; __REVISION__: string | undefined;
__VERSION__: string; __VERSION__: string;
electronRequire: (name: string) => any; electronRequire: (name: string) => any;
electronResolve: (name: string) => string;
window: Window | undefined; window: Window | undefined;
WebSocket: any; WebSocket: any;
fetch: any; fetch: any;

File diff suppressed because it is too large Load Diff