From 47a7a9ee8045bbdaae62c01e8c21de1eb55cf740 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Wed, 5 Oct 2016 09:15:42 -0700 Subject: [PATCH] Add `js_file_prod` rule Reviewed By: matryoshcow Differential Revision: D3913439 fbshipit-source-id: fe3a30fddb4d8fcabfc88caf9b1b032189812b89 --- .../worker/__tests__/inline-test.js | 27 +++++- .../worker/collect-dependencies.js | 85 ++++++++++++---- .../JSTransformer/worker/constant-folding.js | 5 +- .../src/JSTransformer/worker/inline.js | 59 ++++++++---- react-packager/src/ModuleGraph/worker.js | 96 ++++++++++++++++++- 5 files changed, 227 insertions(+), 45 deletions(-) diff --git a/react-packager/src/JSTransformer/worker/__tests__/inline-test.js b/react-packager/src/JSTransformer/worker/__tests__/inline-test.js index b52e07c0..2278a157 100644 --- a/react-packager/src/JSTransformer/worker/__tests__/inline-test.js +++ b/react-packager/src/JSTransformer/worker/__tests__/inline-test.js @@ -277,5 +277,30 @@ describe('inline constants', () => { const {ast} = inline('arbitrary.hs', {ast: toAst(code)}, {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"'))); + }); +}); diff --git a/react-packager/src/JSTransformer/worker/collect-dependencies.js b/react-packager/src/JSTransformer/worker/collect-dependencies.js index 1a9eac1c..1ae9d1d8 100644 --- a/react-packager/src/JSTransformer/worker/collect-dependencies.js +++ b/react-packager/src/JSTransformer/worker/collect-dependencies.js @@ -12,40 +12,87 @@ const {traverse, types} = require('babel-core'); -const isRequireCall = (callee, firstArg) => - callee.type !== 'Identifier' || - callee.name !== 'require' || - !firstArg || - firstArg.type !== 'StringLiteral'; +class Replacement { + constructor() { + this.nameToIndex = new Map(); + this.nextIndex = 0; + } -function collectDependencies(ast, code) { - let nextIndex = 0; - const dependencyIndexes = new Map(); + isRequireCall(callee, firstArg) { + return ( + callee.type === 'Identifier' && callee.name === 'require' && + firstArg && firstArg.type === 'StringLiteral' + ); + } - function getIndex(depencyId) { - let index = dependencyIndexes.get(depencyId); + getIndex(name) { + let index = this.nameToIndex.get(name); if (index !== undefined) { return index; } - - index = nextIndex++; - dependencyIndexes.set(depencyId, index); + index = this.nextIndex++; + this.nameToIndex.set(name, 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, { CallExpression(path) { const node = path.node; const arg = node.arguments[0]; - if (isRequireCall(node.callee, arg)) { - return; + if (replacement.isRequireCall(node.callee, arg)) { + 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)); diff --git a/react-packager/src/JSTransformer/worker/constant-folding.js b/react-packager/src/JSTransformer/worker/constant-folding.js index f896430f..9304aab2 100644 --- a/react-packager/src/JSTransformer/worker/constant-folding.js +++ b/react-packager/src/JSTransformer/worker/constant-folding.js @@ -11,9 +11,6 @@ const babel = require('babel-core'); const t = babel.types; -const isLiteral = binaryExpression => - t.isLiteral(binaryExpression.left) && t.isLiteral(binaryExpression.right); - const Conditional = { exit(path) { const node = path.node; @@ -81,5 +78,5 @@ function constantFolding(filename, transformResult) { }); } +constantFolding.plugin = plugin; module.exports = constantFolding; - diff --git a/react-packager/src/JSTransformer/worker/inline.js b/react-packager/src/JSTransformer/worker/inline.js index adc1f71d..3ec50a63 100644 --- a/react-packager/src/JSTransformer/worker/inline.js +++ b/react-packager/src/JSTransformer/worker/inline.js @@ -28,12 +28,15 @@ const importMap = new Map([['ReactNative', 'react-native']]); 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) => t.isCallExpression(node) && t.isIdentifier(node.callee, requirePattern) && - t.isStringLiteral(node.arguments[0], t.stringLiteral(dependencyId)); + checkRequireArgs(node.arguments, dependencyId); const isImport = (node, scope, patterns) => patterns.some(pattern => { @@ -41,21 +44,25 @@ const isImport = (node, scope, patterns) => 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)); - return identifier && isToplevelBinding(scope.getBinding(identifier.name)) || - isImport(node, scope, patterns); + return ( + 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) && - 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.isMemberExpression(node.object) && 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) => t.isIdentifier(node.property, nodeEnv) && @@ -64,18 +71,19 @@ const isProcessEnvNodeEnv = (node, scope) => t.isIdentifier(node.object.object, processId) && isGlobal(scope.getBinding(processId.name)); -const isPlatformSelect = (node, scope) => +const isPlatformSelect = (node, scope, isWrappedModule) => t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object, platform) && 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.isIdentifier(node.callee.property, select) && t.isMemberExpression(node.callee.object) && 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) => t.isIdentifier(node, dev) && @@ -97,22 +105,30 @@ const inlinePlugin = { MemberExpression(path, state) { const node = path.node; const scope = path.scope; + const opts = state.opts; - if (isPlatformOS(node, scope) || isReactPlatformOS(node, scope)) { - path.replaceWith(t.stringLiteral(state.opts.platform)); + if ( + isPlatformOS(node, scope, opts.isWrapped) || + isReactPlatformOS(node, scope, opts.isWrapped) + ) { + path.replaceWith(t.stringLiteral(opts.platform)); } else if (isProcessEnvNodeEnv(node, scope)) { path.replaceWith( - t.stringLiteral(state.opts.dev ? 'development' : 'production')); + t.stringLiteral(opts.dev ? 'development' : 'production')); } }, CallExpression(path, state) { const node = path.node; const scope = path.scope; 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) - ? findProperty(arg, state.opts.platform) + ? findProperty(arg, opts.platform) : node; path.replaceWith(replacement); @@ -123,6 +139,12 @@ const 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) { const code = transformResult.code; const babelOptions = { @@ -141,4 +163,5 @@ function inline(filename, transformResult, options) { : babel.transform(code, babelOptions); } +inline.plugin = inlinePlugin; module.exports = inline; diff --git a/react-packager/src/ModuleGraph/worker.js b/react-packager/src/ModuleGraph/worker.js index 7b7f7b53..c56f2f01 100644 --- a/react-packager/src/ModuleGraph/worker.js +++ b/react-packager/src/ModuleGraph/worker.js @@ -15,10 +15,16 @@ const dirname = require('path').dirname; const babel = require('babel-core'); const generate = require('babel-generator').default; -const series = require('async/series'); + const mkdirp = require('mkdirp'); +const series = require('async/series'); +const sourceMap = require('source-map'); 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'); function transformModule(infile, options, outfile, callback) { @@ -63,8 +69,7 @@ function transformModule(infile, options, outfile, callback) { }; try { - mkdirp.sync(dirname(outfile)); - fs.writeFileSync(outfile, JSON.stringify(result), 'utf8'); + writeResult(outfile, result); } catch (writeError) { callback(writeError); 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) { const dependencies = collectDependencies(ast); const file = wrapModule(ast); @@ -101,4 +135,60 @@ function wrapModule(file) { 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.optimizeModule = optimizeModule;