mirror of https://github.com/status-im/metro.git
Add `js_file_prod` rule
Reviewed By: matryoshcow Differential Revision: D3913439 fbshipit-source-id: fe3a30fddb4d8fcabfc88caf9b1b032189812b89
This commit is contained in:
parent
85b74ab639
commit
47a7a9ee80
|
@ -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"')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue