mirror of https://github.com/status-im/metro.git
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:
parent
bf0806b803
commit
5309991ba6
|
@ -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');}}`);
|
||||
});
|
||||
});
|
96
react-packager/src/JSTransformer/worker/__tests__/extract-dependencies-test.js
vendored
Normal file
96
react-packager/src/JSTransformer/worker/__tests__/extract-dependencies-test.js
vendored
Normal 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]);
|
||||
});
|
||||
});
|
|
@ -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'))
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
@ -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;
|
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
Loading…
Reference in New Issue