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

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

View File

@@ -7,6 +7,6 @@
* @format
*/
global.fetch = require('jest-fetch-mock');
const isFB = false;
require('immer').enableMapSet();
export default isFB;

View File

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

View File

@@ -25,6 +25,7 @@ import fixPath from 'fix-path';
import {exec} from 'child_process';
import compilePlugins from './compilePlugins';
import setup from './setup';
import isFB from './fb-stubs/isFB';
import delegateToLauncher from './launcher';
import expandTilde from 'expand-tilde';
import yargs from 'yargs';
@@ -85,6 +86,10 @@ const argv = yargs
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 pluginPaths = (config.pluginPaths ?? [])

View File

@@ -2,24 +2,15 @@
"name": "flipper-static",
"version": "1.0.0",
"main": "index.js",
"private": true,
"license": "MIT",
"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",
"expand-tilde": "^2.0.2",
"fb-watchman": "^2.0.0",
"fix-path": "^3.0.0",
"fs-extra": "^8.1.0",
"flipper-babel-transformer": "0.2.0",
"mem": "^6.0.0",
"metro": "^0.58.0",
"mkdirp": "^1.0.0",
@@ -28,13 +19,5 @@
"uuid": "^7.0.1",
"xdg-basedir": "^4.0.0",
"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,66 +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
*/
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

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

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

@@ -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,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

@@ -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},
});
},
};