Add `js_file_prod` rule

Reviewed By: matryoshcow

Differential Revision: D3913439

fbshipit-source-id: fe3a30fddb4d8fcabfc88caf9b1b032189812b89
This commit is contained in:
David Aurelio 2016-10-05 09:15:42 -07:00 committed by Facebook Github Bot
parent aef3d8128f
commit 0b2d5317c4
5 changed files with 227 additions and 45 deletions

View File

@ -277,5 +277,30 @@ describe('inline constants', () => {
const {ast} = inline('arbitrary.hs', {ast: toAst(code)}, {dev: false}); const {ast} = inline('arbitrary.hs', {ast: toAst(code)}, {dev: false});
expect(toString(ast)).toEqual(code.replace(/__DEV__/, 'false')); expect(toString(ast)).toEqual(code.replace(/__DEV__/, 'false'));
}); });
});
it('can work with wrapped modules', () => {
const code = `__arbitrary(function() {
var Platform = require('react-native').Platform;
var a = Platform.OS, b = Platform.select({android: 1, ios: 2});
});`;
const {ast} = inline(
'arbitrary', {code}, {dev: true, platform: 'android', isWrapped: true});
expect(toString(ast)).toEqual(
normalize(
code
.replace(/Platform\.OS/, '"android"')
.replace(/Platform\.select[^)]+\)/, 1)
)
);
});
it('can work with transformed require calls', () => {
const code = `__arbitrary(function() {
var a = require(123, 'react-native').Platform.OS;
});`;
const {ast} = inline(
'arbitrary', {code}, {dev: true, platform: 'android', isWrapped: true});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\([^)]+\)\.Platform\.OS/, '"android"')));
});
});

View File

@ -12,40 +12,87 @@
const {traverse, types} = require('babel-core'); const {traverse, types} = require('babel-core');
const isRequireCall = (callee, firstArg) => class Replacement {
callee.type !== 'Identifier' || constructor() {
callee.name !== 'require' || this.nameToIndex = new Map();
!firstArg || this.nextIndex = 0;
firstArg.type !== 'StringLiteral'; }
function collectDependencies(ast, code) { isRequireCall(callee, firstArg) {
let nextIndex = 0; return (
const dependencyIndexes = new Map(); callee.type === 'Identifier' && callee.name === 'require' &&
firstArg && firstArg.type === 'StringLiteral'
);
}
function getIndex(depencyId) { getIndex(name) {
let index = dependencyIndexes.get(depencyId); let index = this.nameToIndex.get(name);
if (index !== undefined) { if (index !== undefined) {
return index; return index;
} }
index = this.nextIndex++;
index = nextIndex++; this.nameToIndex.set(name, index);
dependencyIndexes.set(depencyId, index);
return index; return index;
} }
getNames() {
return Array.from(this.nameToIndex.keys());
}
makeArgs(newId, oldId) {
return [newId, oldId];
}
}
class ProdReplacement {
constructor(names) {
this.replacement = new Replacement();
this.names = names;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' && callee.name === 'require' &&
firstArg && firstArg.type === 'NumericLiteral'
);
}
getIndex(id) {
if (id in this.names) {
return this.replacement.getIndex(this.names[id]);
}
throw new Error(
`${id} is not a known module ID. Existing mappings: ${
this.names.map((n, i) => `${i} => ${n}`).join(', ')}`
);
}
getNames() {
return this.replacement.getNames();
}
makeArgs(newId) {
return [newId];
}
}
function collectDependencies(ast, replacement) {
traverse(ast, { traverse(ast, {
CallExpression(path) { CallExpression(path) {
const node = path.node; const node = path.node;
const arg = node.arguments[0]; const arg = node.arguments[0];
if (isRequireCall(node.callee, arg)) { if (replacement.isRequireCall(node.callee, arg)) {
return; const index = replacement.getIndex(arg.value);
node.arguments = replacement.makeArgs(types.numericLiteral(index), arg);
} }
node.arguments[0] = types.numericLiteral(getIndex(arg.value));
} }
}); });
return Array.from(dependencyIndexes.keys()); return replacement.getNames();
} }
module.exports = collectDependencies; exports = module.exports =
ast => collectDependencies(ast, new Replacement());
exports.forOptimization =
(ast, names) => collectDependencies(ast, new ProdReplacement(names));

View File

@ -11,9 +11,6 @@
const babel = require('babel-core'); const babel = require('babel-core');
const t = babel.types; const t = babel.types;
const isLiteral = binaryExpression =>
t.isLiteral(binaryExpression.left) && t.isLiteral(binaryExpression.right);
const Conditional = { const Conditional = {
exit(path) { exit(path) {
const node = path.node; const node = path.node;
@ -81,5 +78,5 @@ function constantFolding(filename, transformResult) {
}); });
} }
constantFolding.plugin = plugin;
module.exports = constantFolding; module.exports = constantFolding;

View File

@ -28,12 +28,15 @@ const importMap = new Map([['ReactNative', 'react-native']]);
const isGlobal = (binding) => !binding; const isGlobal = (binding) => !binding;
const isToplevelBinding = (binding) => isGlobal(binding) || !binding.scope.parent; const isToplevelBinding = (binding, isWrappedModule) =>
isGlobal(binding) ||
!binding.scope.parent ||
isWrappedModule && !binding.scope.parent.parent;
const isRequireCall = (node, dependencyId, scope) => const isRequireCall = (node, dependencyId, scope) =>
t.isCallExpression(node) && t.isCallExpression(node) &&
t.isIdentifier(node.callee, requirePattern) && t.isIdentifier(node.callee, requirePattern) &&
t.isStringLiteral(node.arguments[0], t.stringLiteral(dependencyId)); checkRequireArgs(node.arguments, dependencyId);
const isImport = (node, scope, patterns) => const isImport = (node, scope, patterns) =>
patterns.some(pattern => { patterns.some(pattern => {
@ -41,21 +44,25 @@ const isImport = (node, scope, patterns) =>
return isRequireCall(node, importName, scope); return isRequireCall(node, importName, scope);
}); });
function isImportOrGlobal(node, scope, patterns) { function isImportOrGlobal(node, scope, patterns, isWrappedModule) {
const identifier = patterns.find(pattern => t.isIdentifier(node, pattern)); const identifier = patterns.find(pattern => t.isIdentifier(node, pattern));
return identifier && isToplevelBinding(scope.getBinding(identifier.name)) || return (
isImport(node, scope, patterns); identifier &&
isToplevelBinding(scope.getBinding(identifier.name), isWrappedModule) ||
isImport(node, scope, patterns)
);
} }
const isPlatformOS = (node, scope) => const isPlatformOS = (node, scope, isWrappedModule) =>
t.isIdentifier(node.property, os) && t.isIdentifier(node.property, os) &&
isImportOrGlobal(node.object, scope, [platform]); isImportOrGlobal(node.object, scope, [platform], isWrappedModule);
const isReactPlatformOS = (node, scope) => const isReactPlatformOS = (node, scope, isWrappedModule) =>
t.isIdentifier(node.property, os) && t.isIdentifier(node.property, os) &&
t.isMemberExpression(node.object) && t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, platform) && t.isIdentifier(node.object.property, platform) &&
isImportOrGlobal(node.object.object, scope, [React, ReactNative]); isImportOrGlobal(
node.object.object, scope, [React, ReactNative], isWrappedModule);
const isProcessEnvNodeEnv = (node, scope) => const isProcessEnvNodeEnv = (node, scope) =>
t.isIdentifier(node.property, nodeEnv) && t.isIdentifier(node.property, nodeEnv) &&
@ -64,18 +71,19 @@ const isProcessEnvNodeEnv = (node, scope) =>
t.isIdentifier(node.object.object, processId) && t.isIdentifier(node.object.object, processId) &&
isGlobal(scope.getBinding(processId.name)); isGlobal(scope.getBinding(processId.name));
const isPlatformSelect = (node, scope) => const isPlatformSelect = (node, scope, isWrappedModule) =>
t.isMemberExpression(node.callee) && t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, platform) && t.isIdentifier(node.callee.object, platform) &&
t.isIdentifier(node.callee.property, select) && t.isIdentifier(node.callee.property, select) &&
isImportOrGlobal(node.callee.object, scope, [platform]); isImportOrGlobal(node.callee.object, scope, [platform], isWrappedModule);
const isReactPlatformSelect = (node, scope) => const isReactPlatformSelect = (node, scope, isWrappedModule) =>
t.isMemberExpression(node.callee) && t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, select) && t.isIdentifier(node.callee.property, select) &&
t.isMemberExpression(node.callee.object) && t.isMemberExpression(node.callee.object) &&
t.isIdentifier(node.callee.object.property, platform) && t.isIdentifier(node.callee.object.property, platform) &&
isImportOrGlobal(node.callee.object.object, scope, [React, ReactNative]); isImportOrGlobal(
node.callee.object.object, scope, [React, ReactNative], isWrappedModule);
const isDev = (node, parent, scope) => const isDev = (node, parent, scope) =>
t.isIdentifier(node, dev) && t.isIdentifier(node, dev) &&
@ -97,22 +105,30 @@ const inlinePlugin = {
MemberExpression(path, state) { MemberExpression(path, state) {
const node = path.node; const node = path.node;
const scope = path.scope; const scope = path.scope;
const opts = state.opts;
if (isPlatformOS(node, scope) || isReactPlatformOS(node, scope)) { if (
path.replaceWith(t.stringLiteral(state.opts.platform)); isPlatformOS(node, scope, opts.isWrapped) ||
isReactPlatformOS(node, scope, opts.isWrapped)
) {
path.replaceWith(t.stringLiteral(opts.platform));
} else if (isProcessEnvNodeEnv(node, scope)) { } else if (isProcessEnvNodeEnv(node, scope)) {
path.replaceWith( path.replaceWith(
t.stringLiteral(state.opts.dev ? 'development' : 'production')); t.stringLiteral(opts.dev ? 'development' : 'production'));
} }
}, },
CallExpression(path, state) { CallExpression(path, state) {
const node = path.node; const node = path.node;
const scope = path.scope; const scope = path.scope;
const arg = node.arguments[0]; const arg = node.arguments[0];
const opts = state.opts;
if (isPlatformSelect(node, scope) || isReactPlatformSelect(node, scope)) { if (
isPlatformSelect(node, scope, opts.isWrapped) ||
isReactPlatformSelect(node, scope, opts.isWrapped)
) {
const replacement = t.isObjectExpression(arg) const replacement = t.isObjectExpression(arg)
? findProperty(arg, state.opts.platform) ? findProperty(arg, opts.platform)
: node; : node;
path.replaceWith(replacement); path.replaceWith(replacement);
@ -123,6 +139,12 @@ const inlinePlugin = {
const plugin = () => inlinePlugin; const plugin = () => inlinePlugin;
function checkRequireArgs(args, dependencyId) {
const pattern = t.stringLiteral(dependencyId);
return t.isStringLiteral(args[0], pattern) ||
t.isNumericLiteral(args[0]) && t.isStringLiteral(args[1], pattern);
}
function inline(filename, transformResult, options) { function inline(filename, transformResult, options) {
const code = transformResult.code; const code = transformResult.code;
const babelOptions = { const babelOptions = {
@ -141,4 +163,5 @@ function inline(filename, transformResult, options) {
: babel.transform(code, babelOptions); : babel.transform(code, babelOptions);
} }
inline.plugin = inlinePlugin;
module.exports = inline; module.exports = inline;

View File

@ -15,10 +15,16 @@ const dirname = require('path').dirname;
const babel = require('babel-core'); const babel = require('babel-core');
const generate = require('babel-generator').default; const generate = require('babel-generator').default;
const series = require('async/series');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const series = require('async/series');
const sourceMap = require('source-map');
const collectDependencies = require('../JSTransformer/worker/collect-dependencies'); const collectDependencies = require('../JSTransformer/worker/collect-dependencies');
const constantFolding = require('../JSTransformer/worker/constant-folding').plugin;
const inline = require('../JSTransformer/worker/inline').plugin;
const minify = require('../JSTransformer/worker/minify');
const docblock = require('../node-haste/DependencyGraph/docblock'); const docblock = require('../node-haste/DependencyGraph/docblock');
function transformModule(infile, options, outfile, callback) { function transformModule(infile, options, outfile, callback) {
@ -63,8 +69,7 @@ function transformModule(infile, options, outfile, callback) {
}; };
try { try {
mkdirp.sync(dirname(outfile)); writeResult(outfile, result);
fs.writeFileSync(outfile, JSON.stringify(result), 'utf8');
} catch (writeError) { } catch (writeError) {
callback(writeError); callback(writeError);
return; return;
@ -73,6 +78,35 @@ function transformModule(infile, options, outfile, callback) {
}); });
} }
function optimizeModule(infile, outfile, options, callback) {
let data;
try {
data = JSON.parse(fs.readFileSync(infile, 'utf8'));
} catch (readError) {
callback(readError);
return;
}
const transformed = data.transformed;
const result = Object.assign({}, data);
result.transformed = {};
const file = data.file;
const code = data.code;
try {
Object.keys(transformed).forEach(key => {
result.transformed[key] = optimize(transformed[key], file, code, options);
});
writeResult(outfile, result);
} catch (error) {
callback(error);
return;
}
callback(null);
}
function makeResult(ast, filename, sourceCode) { function makeResult(ast, filename, sourceCode) {
const dependencies = collectDependencies(ast); const dependencies = collectDependencies(ast);
const file = wrapModule(ast); const file = wrapModule(ast);
@ -101,4 +135,60 @@ function wrapModule(file) {
return t.file(t.program([t.expressionStatement(def)])); return t.file(t.program([t.expressionStatement(def)]));
} }
function optimize(transformed, file, originalCode, options) {
const optimized =
optimizeCode(transformed.code, transformed.map, file, options);
const dependencies = collectDependencies.forOptimization(
optimized.ast, transformed.dependencies);
const gen = generate(optimized.ast, {
comments: false,
compact: true,
filename: file,
sourceMaps: true,
sourceMapTarget: file,
sourceFileName: file,
}, originalCode);
const merged = new sourceMap.SourceMapGenerator();
const inputMap = new sourceMap.SourceMapConsumer(transformed.map);
new sourceMap.SourceMapConsumer(gen.map)
.eachMapping(mapping => {
const original = inputMap.originalPositionFor({
line: mapping.originalLine,
column: mapping.originalColumn,
});
if (original.line == null) {
return;
}
merged.addMapping({
generated: {line: mapping.generatedLine, column: mapping.generatedColumn},
original: {line: original.line, column: original.column || 0},
source: file,
name: original.name || mapping.name,
});
});
const min = minify(file, gen.code, merged.toJSON());
return {code: min.code, map: min.map, dependencies};
}
function optimizeCode(code, map, filename, options) {
const inlineOptions = Object.assign({isWrapped: true}, options);
return babel.transform(code, {
plugins: [[constantFolding], [inline, inlineOptions]],
babelrc: false,
code: false,
filename,
});
}
function writeResult(outfile, result) {
mkdirp.sync(dirname(outfile));
fs.writeFileSync(outfile, JSON.stringify(result), 'utf8');
}
exports.transformModule = transformModule; exports.transformModule = transformModule;
exports.optimizeModule = optimizeModule;