Add new worker for code transform, optimization, and dependency extraction

Summary:This adds a new worker implementation that

- uses the existing transforms to transform code
- optionally inline `__DEV__`, `process.env.NODE_ENV`, and `Platform.OS`
- optionally eliminate branches of conditionals with constant conditions
- extracts dependencies
- optionally minifies

This will land as part of a multi-commit stack, not in isolation

Reviewed By: martinbigio

Differential Revision: D2976677

fb-gh-sync-id: 38e317f90b6948b28ef2e3fe8b66fc0b9c75aa38
shipit-source-id: 38e317f90b6948b28ef2e3fe8b66fc0b9c75aa38
This commit is contained in:
David Aurelio 2016-03-01 04:40:40 -08:00 committed by Facebook Github Bot 9
parent bf0806b803
commit 5309991ba6
10 changed files with 1173 additions and 0 deletions

View File

@ -0,0 +1,112 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.autoMockOff();
const babel = require('babel-core');
const constantFolding = require('../constant-folding');
function parse(code) {
return babel.transform(code, {code: false, babelrc: false, compact: true});
}
describe('constant expressions', () => {
it('can optimize conditional expressions with constant conditions', () => {
const code = `
a(
'production'=="production",
'production'!=='development',
false && 1 || 0 || 2,
true || 3,
'android'==='ios' ? null : {},
'android'==='android' ? {a:1} : {a:0},
'foo'==='bar' ? b : c,
f() ? g() : h()
);`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(`a(true,true,2,true,{},{a:1},c,f()?g():h());`);
});
it('can optimize ternary expressions with constant conditions', () => {
const code =
`var a = true ? 1 : 2;
var b = 'android' == 'android'
? ('production' != 'production' ? 'a' : 'A')
: 'i';`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(`var a=1;var b='A';`);
});
it('can optimize logical operator expressions with constant conditions', () => {
const code = `
var a = true || 1;
var b = 'android' == 'android' &&
'production' != 'production' || null || "A";`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(`var a=true;var b="A";`);
});
it('can optimize logical operators with partly constant operands', () => {
const code = `
var a = "truthy" || z();
var b = "truthy" && z();
var c = null && z();
var d = null || z();
var e = !1 && z();
`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(`var a="truthy";var b=z();var c=null;var d=z();var e=false;`);
});
it('can remode an if statement with a falsy constant test', () => {
const code = `
if ('production' === 'development' || false) {
var a = 1;
}
`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(``);
});
it('can optimize if-else-branches with constant conditions', () => {
const code = `
if ('production' == 'development') {
var a = 1;
var b = a + 2;
} else if ('development' == 'development') {
var a = 3;
var b = a + 4;
} else {
var a = 'b';
}
`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(`{var a=3;var b=a+4;}`);
});
it('can optimize nested if-else constructs', () => {
const code = `
if ('ios' === "android") {
if (true) {
require('a');
} else {
require('b');
}
} else if ('android' === 'android') {
if (true) {
require('c');
} else {
require('d');
}
}
`;
expect(constantFolding('arbitrary.js', parse(code)).code)
.toEqual(`{{require('c');}}`);
});
});

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.autoMockOff();
const extractDependencies = require('../extract-dependencies');
describe('Dependency extraction:', () => {
it('can extract calls to require', () => {
const code = `require('foo/bar');
var React = require("React");
var A = React.createClass({
render: function() {
return require ( "Component" );
}
});
require
('more');`
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies)
.toEqual(['foo/bar', 'React', 'Component', 'more']);
expect(dependencyOffsets).toEqual([8, 46, 147, 203]);
});
it('does not extract require method calls', () => {
const code = `
require('a');
foo.require('b');
bar.
require ( 'c').require('d')require('e')`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['a', 'e']);
expect(dependencyOffsets).toEqual([15, 97]);
});
it('does not extract require calls from strings', () => {
const code = `require('foo');
var React = '\\'require("React")';
var a = ' // require("yadda")';
var a = ' /* require("yadda") */';
var A = React.createClass({
render: function() {
return require ( "Component" );
}
});
" \\" require('more')";`
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['foo', 'Component']);
expect(dependencyOffsets).toEqual([8, 226]);
});
it('does not extract require calls in comments', () => {
const code = `require('foo')//require("not/this")
/* A comment here with a require('call') that should not be extracted */require('bar')
// ending comment without newline require("baz")`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['foo', 'bar']);
expect(dependencyOffsets).toEqual([8, 122]);
});
it('deduplicates dependencies', () => {
const code = `require('foo');require( "foo" );
require("foo");`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['foo']);
expect(dependencyOffsets).toEqual([8, 24, 47]);
});
it('does not extract calls to function with names that start with "require"', () => {
const code = `arbitraryrequire('foo');`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
});
it('does not get confused by previous states', () => {
// yes, this was a bug
const code = `require("a");/* a comment */ var a = /[a]/.test('a');`
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['a']);
expect(dependencyOffsets).toEqual([8]);
});
});

View File

@ -0,0 +1,133 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.autoMockOff();
const inline = require('../inline');
const {transform, transformFromAst} = require('babel-core');
const babelOptions = {
babelrc: false,
compact: true,
};
function toString(ast) {
return normalize(transformFromAst(ast, babelOptions).code);
}
function normalize(code) {
return transform(code, babelOptions).code;
}
function toAst(code) {
return transform(code, {...babelOptions, code: false}).ast;
}
describe('inline constants', () => {
it('replaces __DEV__ in the code', () => {
const code = `function a() {
var a = __DEV__ ? 1 : 2;
var b = a.__DEV__;
var c = function __DEV__(__DEV__) {};
}`
const {ast} = inline('arbitrary.js', {code}, {dev: true});
expect(toString(ast)).toEqual(normalize(code.replace(/__DEV__/, 'true')));
});
it('replaces Platform.OS in the code if Platform is a global', () => {
const code = `function a() {
var a = Platform.OS;
var b = a.Platform.OS;
}`
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.OS/, '"ios"')));
});
it('replaces Platform.OS in the code if Platform is a top level import', () => {
const code = `
var Platform = require('Platform');
function a() {
if (Platform.OS === 'android') a = function() {};
var b = a.Platform.OS;
}`
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.OS/, '"ios"')));
});
it('replaces require("Platform").OS in the code', () => {
const code = `function a() {
var a = require('Platform').OS;
var b = a.require('Platform').OS;
}`
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('Platform'\)\.OS/, '"android"')));
});
it('replaces React.Platform.OS in the code if React is a global', () => {
const code = `function a() {
var a = React.Platform.OS;
var b = a.React.Platform.OS;
}`
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/React\.Platform\.OS/, '"ios"')));
});
it('replaces React.Platform.OS in the code if React is a top level import', () => {
const code = `
var React = require('React');
function a() {
if (React.Platform.OS === 'android') a = function() {};
var b = a.React.Platform.OS;
}`
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/React.Platform\.OS/, '"ios"')));
});
it('replaces require("React").Platform.OS in the code', () => {
const code = `function a() {
var a = require('React').Platform.OS;
var b = a.require('React').Platform.OS;
}`
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('React'\)\.Platform\.OS/, '"android"')));
});
it('replaces process.env.NODE_ENV in the code', () => {
const code = `function a() {
if (process.env.NODE_ENV === 'production') {
return require('Prod');
}
return require('Dev');
}`
const {ast} = inline('arbitrary.js', {code}, {dev: false});
expect(toString(ast)).toEqual(
normalize(code.replace(/process\.env\.NODE_ENV/, '"production"')));
});
it('replaces process.env.NODE_ENV in the code', () => {
const code = `function a() {
if (process.env.NODE_ENV === 'production') {
return require('Prod');
}
return require('Dev');
}`
const {ast} = inline('arbitrary.js', {code}, {dev: true});
expect(toString(ast)).toEqual(
normalize(code.replace(/process\.env\.NODE_ENV/, '"development"')));
});
it('accepts an AST as input', function() {
const code = `function ifDev(a,b){return __DEV__?a:b;}`;
const {ast} = inline('arbitrary.hs', {ast: toAst(code)}, {dev: false});
expect(toString(ast)).toEqual(code.replace(/__DEV__/, 'false'))
});
});

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.autoMockOff();
const uglify = {
minify: jest.genMockFunction().mockImplementation(code => {
return {
code: code.replace(/(^|\W)\s+/g, '$1'),
map: {},
};
}),
};
jest.setMock('uglify-js', uglify);
const minify = require('../minify');
const {any} = jasmine;
describe('Minification:', () => {
const fileName = '/arbitrary/file.js';
const DEPENDENCY_MARKER = '\u0002\ueffe\ue277\uead5';
let map;
beforeEach(() => {
uglify.minify.mockClear();
map = {version: 3, sources: [fileName], mappings: ''};
});
it('passes the transformed code to `uglify.minify`, wrapped in an immediately invoked function expression', () => {
const code = 'arbitrary(code)';
minify('', code, {}, [], []);
expect(uglify.minify).toBeCalledWith(
`(function(){${code}}());`, any(Object));
});
it('uses the passed module locals as parameters of the IIFE', () => {
const moduleLocals = ['arbitrary', 'parameters'];
minify('', '', {}, [], moduleLocals);
expect(uglify.minify).toBeCalledWith(
`(function(${moduleLocals}){}());`, any(Object));
});
it('passes the transformed source map to `uglify.minify`', () => {
minify('', '', map, [], []);
const [, options] = uglify.minify.mock.calls[0];
expect(options.inSourceMap).toEqual(map);
});
it('passes the file name as `outSourceMap` to `uglify.minify` (uglify uses it for the `file` field on the source map)', () => {
minify(fileName, '', {}, [], []);
const [, options] = uglify.minify.mock.calls[0];
expect(options.outSourceMap).toEqual(fileName);
});
it('inserts a marker for every dependency offset before minifing', () => {
const code = `
var React = require('React');
var Immutable = require('Immutable');`;
const dependencyOffsets = [27, 67];
const expectedCode =
code.replace(/require\('/g, '$&' + DEPENDENCY_MARKER);
minify('', code, {}, dependencyOffsets, []);
expect(uglify.minify).toBeCalledWith(
`(function(){${expectedCode}}());`, any(Object));
});
it('returns the code provided by uglify', () => {
const code = 'some(source) + code';
uglify.minify.mockReturnValue({code: `!function(a,b,c){${code}}()`});
const result = minify('', '', {}, [], []);
expect(result.code).toBe(code);
});
it('extracts dependency offsets from the code provided by uglify', () => {
const code = `
var a=r("${DEPENDENCY_MARKER}a-dependency");
var b=r("\\x02\\ueffe\\ue277\\uead5b-dependency");
var e=r(a()?'\\u0002\\ueffe\\ue277\\uead5c-dependency'
:'\x02\ueffe\ue277\uead5d-dependency');`;
uglify.minify.mockReturnValue({code: `!function(){${code}}());`});
const result = minify('', '', {}, [], []);
expect(result.dependencyOffsets).toEqual([15, 46, 81, 114]);
});
it('returns the source map object provided by uglify', () => {
uglify.minify.mockReturnValue({map, code: ''});
const result = minify('', '', {}, [], []);
expect(result.map).toBe(map);
});
it('adds a `moduleLocals` object to the result that reflects the names of the minified module locals', () => {
const moduleLocals = ['arbitrary', 'parameters', 'here'];
uglify.minify.mockReturnValue({code: '(function(a,ll,d){}());'});
const result = minify('', '', {}, [], moduleLocals);
expect(result.moduleLocals).toEqual({
arbitrary: 'a',
parameters: 'll',
here: 'd',
});
});
});

View File

@ -0,0 +1,244 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.autoMockOff();
jest.mock('../constant-folding');
jest.mock('../extract-dependencies');
jest.mock('../inline');
jest.mock('../minify');
const {transformCode} = require('..');
const {any, objectContaining} = jasmine;
describe('code transformation worker:', () => {
let extractDependencies, transform;
beforeEach(() => {
extractDependencies =
require('../extract-dependencies').mockReturnValue({});
transform = jest.genMockFunction();
});
it('calls the transform with file name, source code, and transform options', function() {
const filename = 'arbitrary/file.js';
const sourceCode = 'arbitrary(code)';
const transformOptions = {arbitrary: 'options'};
transformCode(transform, filename, sourceCode, {transform: transformOptions});
expect(transform).toBeCalledWith(
{filename, sourceCode, options: transformOptions}, any(Function));
});
it('prefixes JSON files with an assignment to module.exports to make the code valid', function() {
const filename = 'arbitrary/file.json';
const sourceCode = '{"arbitrary":"property"}';
transformCode(transform, filename, sourceCode, {});
expect(transform).toBeCalledWith(
{filename, sourceCode: `module.exports=${sourceCode}`}, any(Function));
});
it('calls back with the result of the transform', done => {
const result = {
code: 'some.other(code)',
map: {}
};
transform.mockImplementation((_, callback) =>
callback(null, result));
transformCode(transform, 'filename', 'code', {}, (_, data) => {
expect(data).toEqual(objectContaining(result));
done();
});
});
it('removes the leading assignment to `module.exports` before passing on the result if the file is a JSON file, even if minified', done => {
const result = {
code: 'p.exports={a:1,b:2}',
};
transform.mockImplementation((_, callback) =>
callback(null, result));
transformCode(transform, 'aribtrary/file.json', 'b', {}, (_, data) => {
expect(data.code).toBe('{a:1,b:2}');
done();
});
});
it('calls back with any error yielded by the transform', done => {
const error = Error('arbitrary error');
transform.mockImplementation((_, callback) => callback(error));
transformCode(transform, 'filename', 'code', {}, e => {
expect(e).toBe(error);
done();
});
});
it('puts an empty `moduleLocals` object on the result', done => {
transform.mockImplementation(
(_, callback) => callback(null, {code: 'arbitrary'}));
transformCode(transform, 'filename', 'code', {}, (_, data) => {
expect(data.moduleLocals).toEqual({});
done();
});
});
it('if a `moduleLocals` array is passed, the `moduleLocals` object is a key mirror of its items', done => {
transform.mockImplementation(
(_, callback) => callback(null, {code: 'arbitrary'}));
const moduleLocals =
['arbitrary', 'list', 'containing', 'variable', 'names'];
transformCode(transform, 'filename', 'code', {moduleLocals}, (_, data) => {
expect(data.moduleLocals).toEqual({
arbitrary: 'arbitrary',
list: 'list',
containing: 'containing',
variable: 'variable',
names: 'names',
});
done();
});
});
describe('dependency extraction:', () => {
let code;
beforeEach(() => {
transform.mockImplementation(
(_, callback) => callback(null, {code}));
});
it('passes the transformed code the `extractDependencies`', done => {
code = 'arbitrary(code)';
transformCode(transform, 'filename', 'code', {}, (_, data) => {
expect(extractDependencies).toBeCalledWith(code);
done();
});
});
it('uses `dependencies` and `dependencyOffsets` provided by `extractDependencies` for the result', done => {
const dependencyData = {
dependencies: ['arbitrary', 'list', 'of', 'dependencies'],
dependencyOffsets: [12, 119, 185, 328, 471],
};
extractDependencies.mockReturnValue(dependencyData);
transformCode(transform, 'filename', 'code', {}, (_, data) => {
expect(data).toEqual(objectContaining(dependencyData));
done();
});
});
it('does not extract requires if files are marked as "extern"', done => {
transformCode(transform, 'filename', 'code', {extern: true}, (_, {dependencies, dependencyOffsets}) => {
expect(extractDependencies).not.toBeCalled();
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
done();
});
});
it('does not extract requires of JSON files', done => {
transformCode(transform, 'arbitrary.json', '{"arbitrary":"json"}', {}, (_, {dependencies, dependencyOffsets}) => {
expect(extractDependencies).not.toBeCalled();
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
done();
});
});
});
describe('Minifications:', () => {
let constantFolding, extractDependencies, inline, minify, options;
let transformResult, dependencyData, moduleLocals;
const filename = 'arbitrary/file.js';
const foldedCode = 'arbitrary(folded(code));'
const foldedMap = {version: 3, sources: ['fold.js']}
beforeEach(() => {
constantFolding = require('../constant-folding')
.mockReturnValue({code: foldedCode, map: foldedMap});
extractDependencies = require('../extract-dependencies');
inline = require('../inline');
minify = require('../minify').mockReturnValue({});
moduleLocals = ['module', 'require', 'exports'];
options = {moduleLocals, minify: true};
dependencyData = {
dependencies: ['a', 'b', 'c'],
dependencyOffsets: [100, 120, 140]
};
extractDependencies.mockImplementation(
code => code === foldedCode ? dependencyData : {});
transform.mockImplementation(
(_, callback) => callback(null, transformResult));
});
it('passes the transform result to `inline` for constant inlining', done => {
transformResult = {map: {version: 3}, code: 'arbitrary(code)'};
transformCode(transform, filename, 'code', options, () => {
expect(inline).toBeCalledWith(filename, transformResult, options);
done();
});
});
it('passes the result obtained from `inline` on to `constant-folding`', done => {
const inlineResult = {map: {version: 3, sources: []}, ast: {}};
inline.mockReturnValue(inlineResult);
transformCode(transform, filename, 'code', options, () => {
expect(constantFolding).toBeCalledWith(filename, inlineResult);
done();
});
});
it('Uses the code obtained from `constant-folding` to extract dependencies', done => {
transformCode(transform, filename, 'code', options, () => {
expect(extractDependencies).toBeCalledWith(foldedCode);
done();
});
});
it('passes the code obtained from `constant-folding` to `minify`', done => {
transformCode(transform, filename, 'code', options, () => {
expect(minify).toBeCalledWith(
filename,
foldedCode,
foldedMap,
dependencyData.dependencyOffsets,
moduleLocals
);
done();
});
});
it('uses the dependencies obtained from the optimized result', done => {
transformCode(transform, filename, 'code', options, (_, result) => {
expect(result.dependencies).toEqual(dependencyData.dependencies);
done();
});
});
it('uses data produced by `minify` for the result', done => {
const minifyResult = {
code: 'minified(code)',
dependencyOffsets: [10, 30, 60],
map: {version: 3, sources: ['minified.js']},
moduleLocals: {module: 'x', require: 'y', exports: 'z'},
};
minify.mockReturnValue(minifyResult);
transformCode(transform, 'filename', 'code', options, (_, result) => {
expect(result).toEqual(objectContaining(minifyResult))
done();
});
});
});
});

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
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;
const test = node.test;
if (t.isLiteral(test)) {
if (test.value || node.alternate) {
path.replaceWith(test.value ? node.consequent : node.alternate);
} else if (!test.value) {
path.remove();
}
}
},
};
const plugin = {
visitor: {
BinaryExpression: {
exit(path) {
const node = path.node;
if (t.isLiteral(node.left) && t.isLiteral(node.right)) {
const result = path.evaluate();
if (result.confident) {
path.replaceWith(t.valueToNode(result.value));
}
}
},
},
ConditionalExpression: Conditional,
IfStatement: Conditional,
LogicalExpression: {
exit(path) {
const node = path.node;
const left = node.left;
if (t.isLiteral(left)) {
const value = t.isNullLiteral(left) ? null : left.value;
if (node.operator === '||') {
path.replaceWith(value ? left : node.right);
} else {
path.replaceWith(value ? node.right : left);
}
}
}
},
UnaryExpression: {
exit(path) {
const node = path.node;
if (node.operator === '!' && t.isLiteral(node.argument)) {
path.replaceWith(t.valueToNode(!node.argument.value));
}
}
},
},
};
function constantFolding(filename, transformResult) {
return babel.transformFromAst(transformResult.ast, transformResult.code, {
filename,
plugins: [plugin],
inputSourceMap: transformResult.map,
babelrc: false,
compact: true,
})
}
module.exports = constantFolding;

View File

@ -0,0 +1,126 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const SINGLE_QUOTE = "'".charCodeAt(0);
const DOUBLE_QUOTE = '"'.charCodeAt(0);
const BACKSLASH = '\\'.charCodeAt(0);
const SLASH = '/'.charCodeAt(0);
const NEWLINE = '\n'.charCodeAt(0);
const ASTERISK = '*'.charCodeAt(0);
// dollar is the only regex special character valid in identifiers
const escapeRegExp = identifier => identifier.replace(/[$]/g, '\\$');
function binarySearch(indexes, index) {
var low = 0;
var high = indexes.length - 1;
var i = 0;
if (indexes[low] === index) {
return low;
}
while (high - low > 1) {
var current = low + ((high - low) >>> 1); // right shift divides by 2 and floors
if (index === indexes[current]) {
return current;
}
if (index > indexes[current]) {
low = current;
} else {
high = current;
}
}
return low;
}
function indexOfCharCode(string, needle, i) {
for (var charCode; (charCode = string.charCodeAt(i)); i++) {
if (charCode === needle) {
return i;
}
}
return -1;
}
const reRequire = /(?:^|[^.\s])\s*\brequire\s*\(\s*(['"])(.*?)\1/g;
/**
* Extracts dependencies (module IDs imported with the `require` function) from
* a string containing code.
* The function is regular expression based for speed reasons.
*
* The code is traversed twice:
* 1. An array of ranges is built, where indexes 0-1, 2-3, 4-5, etc. are code,
* and indexes 1-2, 3-4, 5-6, etc. are string literals and comments.
* 2. require calls are extracted with a regular expression.
*
* The result of the dependency extraction is an de-duplicated array of
* dependencies, and an array of offsets to the string literals with module IDs.
* The index points to the opening quote.
*/
function extractDependencies(code) {
const ranges = [0];
// are we currently in a quoted string? -> SINGLE_QUOTE or DOUBLE_QUOTE, else undefined
var currentQuote;
// scan the code for string literals and comments.
for (var i = 0, charCode; (charCode = code.charCodeAt(i)); i++) {
if (charCode === BACKSLASH) {
i += 1;
continue;
}
if (charCode === SLASH && currentQuote === undefined) {
var next = code.charCodeAt(i + 1);
var end = undefined;
if (next === SLASH) {
end = indexOfCharCode(code, NEWLINE, i + 2);
} else if (next === ASTERISK) {
end = code.indexOf('*/', i + 2) + 1; // assume valid JS input here
}
if (end === -1) {
// if the comment does not end, it goes to the end of the file
end += code.length;
}
if (end !== undefined) {
ranges.push(i, end);
i = end;
continue;
}
}
var isQuoteStart = currentQuote === undefined &&
(charCode === SINGLE_QUOTE || charCode === DOUBLE_QUOTE);
if (isQuoteStart || currentQuote === charCode) {
ranges.push(i);
currentQuote = currentQuote === charCode ? undefined : charCode;
}
}
ranges.push(i);
// extract dependencies
const dependencies = new Set();
const dependencyOffsets = [];
for (var match; (match = reRequire.exec(code)); ) {
// check whether the match is in a code range, and not inside of a string
// literal or a comment
if (binarySearch(ranges, match.index) % 2 === 0) {
dependencies.add(match[2]);
dependencyOffsets.push(
match[0].length - match[2].length - 2 + match.index);
}
}
return {
dependencyOffsets,
dependencies: Array.from(dependencies.values()),
};
}
module.exports = extractDependencies;

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const constantFolding = require('./constant-folding');
const extractDependencies = require('./extract-dependencies');
const inline = require('./inline');
const minify = require('./minify');
function keyMirrorFromArray(array) {
var keyMirror = {};
array.forEach(key => keyMirror[key] = key);
return keyMirror;
}
function makeTransformParams(filename, sourceCode, options) {
if (filename.endsWith('.json')) {
sourceCode = 'module.exports=' + sourceCode;
}
return {filename, sourceCode, options};
}
function transformCode(transform, filename, sourceCode, options, callback) {
const params = makeTransformParams(filename, sourceCode, options.transform);
const moduleLocals = options.moduleLocals || [];
const isJson = filename.endsWith('.json');
transform(params, (error, transformed) => {
if (error) {
callback(error);
return;
}
var code, map;
if (options.minify) {
const optimized =
constantFolding(filename, inline(filename, transformed, options));
code = optimized.code;
map = optimized.map;
} else {
code = transformed.code;
map = transformed.map;
}
if (isJson) {
code = code.replace(/^\w+\.exports=/, '');
}
const moduleLocals = options.moduleLocals || [];
const dependencyData = isJson || options.extern
? {dependencies: [], dependencyOffsets: []}
: extractDependencies(code);
var result;
if (options.minify) {
result = minify(
filename, code, map, dependencyData.dependencyOffsets, moduleLocals);
result.dependencies = dependencyData.dependencies;
} else {
result = dependencyData;
result.code = code;
result.map = map;
result.moduleLocals = keyMirrorFromArray(moduleLocals);
}
callback(null, result);
});
}
module.exports = function(transform, filename, sourceCode, options, callback) {
transformCode(require(transform), filename, sourceCode, options || {}, callback);
};
module.exports.transformCode = transformCode; // for easier testing

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const babel = require('babel-core');
const t = babel.types;
const react = {name: 'React'};
const platform = {name: 'Platform'};
const os = {name: 'OS'};
const requirePattern = {name: 'require'};
const env = {name: 'env'};
const nodeEnv = {name: 'NODE_ENV'};
const processId = {name: 'process'};
const dev = {name: '__DEV__'};
const isGlobal = (binding) => !binding;
const isToplevelBinding = (binding) => isGlobal(binding) || !binding.scope.parent;
const isRequireCall = (node, dependencyId, scope) =>
t.isCallExpression(node) &&
t.isIdentifier(node.callee, requirePattern) &&
t.isStringLiteral(node.arguments[0], t.stringLiteral(dependencyId));
const isImport = (node, scope, pattern) =>
t.isIdentifier(node, pattern) &&
isToplevelBinding(scope.getBinding(pattern.name)) ||
isRequireCall(node, pattern.name, scope);
const isPlatformOS = (node, scope) =>
t.isIdentifier(node.property, os) &&
isImport(node.object, scope, platform);
const isReactPlatformOS = (node, scope) =>
t.isIdentifier(node.property, os) &&
t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, platform) &&
isImport(node.object.object, scope, react);
const isProcessEnvNodeEnv = (node, scope) =>
t.isIdentifier(node.property, nodeEnv) &&
t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, env) &&
t.isIdentifier(node.object.object, processId) &&
isGlobal(scope.getBinding(processId.name));
const isDev = (node, parent, scope) =>
t.isIdentifier(node, dev) &&
isGlobal(scope.getBinding(dev.name)) &&
!(t.isMemberExpression(parent));
const inlinePlugin = {
visitor: {
Identifier(path, state) {
if (isDev(path.node, path.parent, path.scope)) {
path.replaceWith(t.booleanLiteral(state.opts.dev));
}
},
MemberExpression(path, state) {
const node = path.node;
const scope = path.scope;
if (isPlatformOS(node, scope) || isReactPlatformOS(node, scope)) {
path.replaceWith(t.stringLiteral(state.opts.platform));
}
if(isProcessEnvNodeEnv(node, scope)) {
path.replaceWith(
t.stringLiteral(state.opts.dev ? 'development' : 'production'));
}
},
},
};
const plugin = () => inlinePlugin;
function inline(filename, transformResult, options) {
const code = transformResult.code;
const babelOptions = {
filename,
plugins: [[plugin, options]],
inputSourceMap: transformResult.map,
code: false,
babelrc: false,
compact: true,
};
return transformResult.ast
? babel.transformFromAst(transformResult.ast, code, babelOptions)
: babel.transform(code, babelOptions);
}
module.exports = inline;

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const uglify = require('uglify-js');
const MAGIC_MARKER = '\u0002\ueffe\ue277\uead5';
const MAGIC_MARKER_SPLITTER =
/(?:\x02|\\u0002|\\x02)(?:\ueffe|\\ueffe)(?:\ue277|\\ue277)(?:\uead5|\\uead5)/;
// IIFE = "immediately invoked function expression"
// we wrap modules in functions to allow the minifier to mangle local variables
function wrapCodeInIIFE(code, moduleLocals) {
return `(function(${moduleLocals.join(',')}){${code}}());`;
}
function extractCodeFromIIFE(code) {
return code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
}
function extractModuleLocalsFromIIFE(code) {
return code.substring(code.indexOf('(', 1) + 1, code.indexOf(')')).split(',');
}
function splitFirstElementAt(array, offset) {
const first = array.shift();
array.unshift(first.slice(0, offset + 1), first.slice(offset + 1));
return array;
}
function insertMarkers(code, dependencyOffsets) {
return dependencyOffsets
.reduceRight(splitFirstElementAt, [code])
.join(MAGIC_MARKER);
}
function extractMarkers(codeWithMarkers) {
const dependencyOffsets = [];
const codeBits = codeWithMarkers.split(MAGIC_MARKER_SPLITTER);
var offset = 0;
for (var i = 0, max = codeBits.length - 1; i < max; i++) {
offset += codeBits[i].length;
dependencyOffsets.push(offset - 1);
}
return {code: codeBits.join(''), dependencyOffsets};
}
function minify(filename, code, map, dependencyOffsets, moduleLocals) {
// before minifying, code is wrapped in an immediately invoked function
// expression, so that top level variables can be shortened safely
code = wrapCodeInIIFE(
// since we don't know where the strings specifying dependencies will be
// located in the minified code, we mark them with a special marker string
// and extract them afterwards.
// That way, post-processing code can use these positions
insertMarkers(code, dependencyOffsets),
moduleLocals
);
const minifyResult = uglify.minify(code, {
fromString: true,
inSourceMap: map,
outSourceMap: filename,
output: {
ascii_only: true,
screw_ie8: true,
},
});
const minifiedModuleLocals = extractModuleLocalsFromIIFE(minifyResult.code);
const codeWithMarkers = extractCodeFromIIFE(minifyResult.code);
const result = extractMarkers(codeWithMarkers);
result.map = minifyResult.map;
result.moduleLocals = {};
moduleLocals.forEach(
(key, i) => result.moduleLocals[key] = minifiedModuleLocals[i]);
return result;
}
module.exports = minify;