mirror of https://github.com/status-im/metro.git
Add a naive WPO implementation
Summary: public RFC: The minifier haven't been stripping dead-code, and it also can't kill unused modules, so as a temporary solution this inlines `__DEV__`, kill dead branches and kill dead modules. For now I'm just white-listing the dev variable, but we could definitely do better than that, but as a temporary fix this should be helpful. I also intend to kill some dead variables, so we can kill unused requires, although inline-requires can also fix it. Reviewed By: vjeux Differential Revision: D2605454 fb-gh-sync-id: 50acb9dcbded07a43080b93ac826a5ceda695936
This commit is contained in:
parent
a8ded758d0
commit
844282c37b
|
@ -21,6 +21,7 @@ class Bundle {
|
||||||
this._finalized = false;
|
this._finalized = false;
|
||||||
this._modules = [];
|
this._modules = [];
|
||||||
this._assets = [];
|
this._assets = [];
|
||||||
|
this._sourceMap = false;
|
||||||
this._sourceMapUrl = sourceMapUrl;
|
this._sourceMapUrl = sourceMapUrl;
|
||||||
this._shouldCombineSourceMaps = false;
|
this._shouldCombineSourceMaps = false;
|
||||||
}
|
}
|
||||||
|
@ -83,16 +84,36 @@ class Bundle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSource() {
|
_getSource(dev) {
|
||||||
if (this._source == null) {
|
if (this._source) {
|
||||||
this._source = _.pluck(this._modules, 'code').join('\n');
|
|
||||||
}
|
|
||||||
return this._source;
|
return this._source;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getInlineSourceMap() {
|
this._source = _.pluck(this._modules, 'code').join('\n');
|
||||||
|
|
||||||
|
if (dev) {
|
||||||
|
return this._source;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wpoActivity = Activity.startEvent('Whole Program Optimisations');
|
||||||
|
const result = require('babel-core').transform(this._source, {
|
||||||
|
retainLines: true,
|
||||||
|
compact: true,
|
||||||
|
plugins: require('../transforms/whole-program-optimisations'),
|
||||||
|
inputSourceMap: this.getSourceMap(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this._source = result.code;
|
||||||
|
this._sourceMap = result.map;
|
||||||
|
|
||||||
|
Activity.endEvent(wpoActivity);
|
||||||
|
|
||||||
|
return this._source;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInlineSourceMap(dev) {
|
||||||
if (this._inlineSourceMap == null) {
|
if (this._inlineSourceMap == null) {
|
||||||
const sourceMap = this.getSourceMap({excludeSource: true});
|
const sourceMap = this.getSourceMap({excludeSource: true, dev});
|
||||||
/*eslint-env node*/
|
/*eslint-env node*/
|
||||||
const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
|
const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
|
||||||
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
|
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
|
||||||
|
@ -106,13 +127,13 @@ class Bundle {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
if (options.minify) {
|
if (options.minify) {
|
||||||
return this.getMinifiedSourceAndMap().code;
|
return this.getMinifiedSourceAndMap(options.dev).code;
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = this._getSource();
|
let source = this._getSource(options.dev);
|
||||||
|
|
||||||
if (options.inlineSourceMap) {
|
if (options.inlineSourceMap) {
|
||||||
source += SOURCEMAPPING_URL + this._getInlineSourceMap();
|
source += SOURCEMAPPING_URL + this._getInlineSourceMap(options.dev);
|
||||||
} else if (this._sourceMapUrl) {
|
} else if (this._sourceMapUrl) {
|
||||||
source += SOURCEMAPPING_URL + this._sourceMapUrl;
|
source += SOURCEMAPPING_URL + this._sourceMapUrl;
|
||||||
}
|
}
|
||||||
|
@ -120,14 +141,14 @@ class Bundle {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinifiedSourceAndMap() {
|
getMinifiedSourceAndMap(dev) {
|
||||||
this._assertFinalized();
|
this._assertFinalized();
|
||||||
|
|
||||||
if (this._minifiedSourceAndMap) {
|
if (this._minifiedSourceAndMap) {
|
||||||
return this._minifiedSourceAndMap;
|
return this._minifiedSourceAndMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = this._getSource();
|
const source = this._getSource(dev);
|
||||||
try {
|
try {
|
||||||
const minifyActivity = Activity.startEvent('minify');
|
const minifyActivity = Activity.startEvent('minify');
|
||||||
this._minifiedSourceAndMap = UglifyJS.minify(source, {
|
this._minifiedSourceAndMap = UglifyJS.minify(source, {
|
||||||
|
@ -203,7 +224,7 @@ class Bundle {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
if (options.minify) {
|
if (options.minify) {
|
||||||
return this.getMinifiedSourceAndMap().map;
|
return this.getMinifiedSourceAndMap(options.dev).map;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._shouldCombineSourceMaps) {
|
if (this._shouldCombineSourceMaps) {
|
||||||
|
@ -314,7 +335,6 @@ class Bundle {
|
||||||
modules: this._modules,
|
modules: this._modules,
|
||||||
assets: this._assets,
|
assets: this._assets,
|
||||||
sourceMapUrl: this._sourceMapUrl,
|
sourceMapUrl: this._sourceMapUrl,
|
||||||
shouldCombineSourceMaps: this._shouldCombineSourceMaps,
|
|
||||||
mainModuleId: this._mainModuleId,
|
mainModuleId: this._mainModuleId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe('Bundle', function() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
bundle.finalize({});
|
bundle.finalize({});
|
||||||
expect(bundle.getSource()).toBe([
|
expect(bundle.getSource({dev: true})).toBe([
|
||||||
'transformed foo;',
|
'transformed foo;',
|
||||||
'transformed bar;',
|
'transformed bar;',
|
||||||
'\/\/@ sourceMappingURL=test_url'
|
'\/\/@ sourceMappingURL=test_url'
|
||||||
|
@ -61,7 +61,7 @@ describe('Bundle', function() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
p.finalize({});
|
p.finalize({});
|
||||||
expect(p.getSource()).toBe([
|
expect(p.getSource({dev: true})).toBe([
|
||||||
'transformed foo;',
|
'transformed foo;',
|
||||||
'transformed bar;',
|
'transformed bar;',
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
|
@ -85,7 +85,7 @@ describe('Bundle', function() {
|
||||||
runBeforeMainModule: ['bar'],
|
runBeforeMainModule: ['bar'],
|
||||||
runMainModule: true,
|
runMainModule: true,
|
||||||
});
|
});
|
||||||
expect(bundle.getSource()).toBe([
|
expect(bundle.getSource({dev: true})).toBe([
|
||||||
'transformed foo;',
|
'transformed foo;',
|
||||||
'transformed bar;',
|
'transformed bar;',
|
||||||
';require("bar");',
|
';require("bar");',
|
||||||
|
@ -110,7 +110,7 @@ describe('Bundle', function() {
|
||||||
sourcePath: 'foo path'
|
sourcePath: 'foo path'
|
||||||
}));
|
}));
|
||||||
bundle.finalize();
|
bundle.finalize();
|
||||||
expect(bundle.getMinifiedSourceAndMap()).toBe(minified);
|
expect(bundle.getMinifiedSourceAndMap({dev: true})).toBe(minified);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ describe('Bundle', function() {
|
||||||
runBeforeMainModule: [],
|
runBeforeMainModule: [],
|
||||||
runMainModule: true,
|
runMainModule: true,
|
||||||
});
|
});
|
||||||
var s = p.getSourceMap();
|
var s = p.getSourceMap({dev: true});
|
||||||
expect(s).toEqual(genSourceMap(p.getModules()));
|
expect(s).toEqual(genSourceMap(p.getModules()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ describe('Bundle', function() {
|
||||||
runMainModule: true,
|
runMainModule: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
var s = p.getSourceMap();
|
var s = p.getSourceMap({dev: true});
|
||||||
expect(s).toEqual({
|
expect(s).toEqual({
|
||||||
file: 'bundle.js',
|
file: 'bundle.js',
|
||||||
version: 3,
|
version: 3,
|
||||||
|
|
|
@ -240,6 +240,7 @@ class Server {
|
||||||
p.getSource({
|
p.getSource({
|
||||||
inlineSourceMap: options.inlineSourceMap,
|
inlineSourceMap: options.inlineSourceMap,
|
||||||
minify: options.minify,
|
minify: options.minify,
|
||||||
|
dev: options.dev,
|
||||||
});
|
});
|
||||||
return p;
|
return p;
|
||||||
});
|
});
|
||||||
|
@ -366,6 +367,7 @@ class Server {
|
||||||
var bundleSource = p.getSource({
|
var bundleSource = p.getSource({
|
||||||
inlineSourceMap: options.inlineSourceMap,
|
inlineSourceMap: options.inlineSourceMap,
|
||||||
minify: options.minify,
|
minify: options.minify,
|
||||||
|
dev: options.dev,
|
||||||
});
|
});
|
||||||
res.setHeader('Content-Type', 'application/javascript');
|
res.setHeader('Content-Type', 'application/javascript');
|
||||||
res.end(bundleSource);
|
res.end(bundleSource);
|
||||||
|
@ -373,6 +375,7 @@ class Server {
|
||||||
} else if (requestType === 'map') {
|
} else if (requestType === 'map') {
|
||||||
var sourceMap = p.getSourceMap({
|
var sourceMap = p.getSourceMap({
|
||||||
minify: options.minify,
|
minify: options.minify,
|
||||||
|
dev: options.dev,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof sourceMap !== 'string') {
|
if (typeof sourceMap !== 'string') {
|
||||||
|
|
106
react-packager/src/transforms/whole-program-optimisations/__tests__/dead-module-elimination-test.js
vendored
Normal file
106
react-packager/src/transforms/whole-program-optimisations/__tests__/dead-module-elimination-test.js
vendored
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
var deadModuleElimintation = require('../dead-module-elimination');
|
||||||
|
var babel = require('babel-core');
|
||||||
|
|
||||||
|
const compile = (code) =>
|
||||||
|
babel.transform(code, {
|
||||||
|
plugins: [deadModuleElimintation],
|
||||||
|
}).code;
|
||||||
|
|
||||||
|
const compare = (source, output) => {
|
||||||
|
const out = trim(compile(source))
|
||||||
|
// workaround babel/source map bug
|
||||||
|
.replace(/^false;/, '');
|
||||||
|
|
||||||
|
expect(out).toEqual(trim(output));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const trim = (str) =>
|
||||||
|
str.replace(/\s/g, '');
|
||||||
|
|
||||||
|
describe('dead-module-elimination', () => {
|
||||||
|
it('should inline __DEV__', () => {
|
||||||
|
compare(
|
||||||
|
`__DEV__ = false;
|
||||||
|
var foo = __DEV__;`,
|
||||||
|
`var foo = false;`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept unary operators with literals', () => {
|
||||||
|
compare(
|
||||||
|
`__DEV__ = !1;
|
||||||
|
var foo = __DEV__;`,
|
||||||
|
`var foo = false;`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should kill dead branches', () => {
|
||||||
|
compare(
|
||||||
|
`__DEV__ = false;
|
||||||
|
if (__DEV__) {
|
||||||
|
doSomething();
|
||||||
|
}`,
|
||||||
|
``
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should kill unreferenced modules', () => {
|
||||||
|
compare(
|
||||||
|
`__d('foo', function() {})`,
|
||||||
|
``
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should kill unreferenced modules at multiple levels', () => {
|
||||||
|
compare(
|
||||||
|
`__d('bar', function() {});
|
||||||
|
__d('foo', function() { require('bar'); });`,
|
||||||
|
``
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should kill modules referenced only from dead branches', () => {
|
||||||
|
compare(
|
||||||
|
`__DEV__ = false;
|
||||||
|
__d('bar', function() {});
|
||||||
|
if (__DEV__) { require('bar'); }`,
|
||||||
|
``
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace logical expressions with the result', () => {
|
||||||
|
compare(
|
||||||
|
`__DEV__ = false;
|
||||||
|
__d('bar', function() {});
|
||||||
|
__DEV__ && require('bar');`,
|
||||||
|
`false;`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep if result branch', () => {
|
||||||
|
compare(
|
||||||
|
`__DEV__ = false;
|
||||||
|
__d('bar', function() {});
|
||||||
|
if (__DEV__) {
|
||||||
|
killWithFire();
|
||||||
|
} else {
|
||||||
|
require('bar');
|
||||||
|
}`,
|
||||||
|
`__d('bar', function() {});
|
||||||
|
require('bar');`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
130
react-packager/src/transforms/whole-program-optimisations/dead-module-elimination.js
vendored
Normal file
130
react-packager/src/transforms/whole-program-optimisations/dead-module-elimination.js
vendored
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
const t = require('babel-types');
|
||||||
|
|
||||||
|
var globals = Object.create(null);
|
||||||
|
var requires = Object.create(null);
|
||||||
|
var _requires;
|
||||||
|
|
||||||
|
const hasDeadModules = modules =>
|
||||||
|
Object.keys(modules).some(key => modules[key] === 0);
|
||||||
|
|
||||||
|
function CallExpression(path) {
|
||||||
|
const { node } = path;
|
||||||
|
const fnName = node.callee.name;
|
||||||
|
|
||||||
|
if (fnName === 'require' || fnName === '__d') {
|
||||||
|
var moduleName = node.arguments[0].value;
|
||||||
|
if (fnName === '__d' && _requires && !_requires[moduleName]) {
|
||||||
|
path.remove();
|
||||||
|
} else if (fnName === '__d'){
|
||||||
|
requires[moduleName] = requires[moduleName] || 0;
|
||||||
|
} else {
|
||||||
|
requires[moduleName] = (requires[moduleName] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function () {
|
||||||
|
var firstPass = {
|
||||||
|
AssignmentExpression(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (node.left.type === 'Identifier' && node.left.name === '__DEV__') {
|
||||||
|
var value;
|
||||||
|
if (node.right.type === 'BooleanLiteral') {
|
||||||
|
value = node.right.value;
|
||||||
|
} else if (
|
||||||
|
node.right.type === 'UnaryExpression' &&
|
||||||
|
node.right.operator === '!' &&
|
||||||
|
node.right.argument.type === 'NumericLiteral'
|
||||||
|
) {
|
||||||
|
value = !node.right.argument.value;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globals[node.left.name] = value;
|
||||||
|
|
||||||
|
// workaround babel/source map bug - the minifier should strip it
|
||||||
|
path.replaceWith(t.booleanLiteral(value));
|
||||||
|
|
||||||
|
//path.remove();
|
||||||
|
//scope.removeBinding(node.left.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IfStatement(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (node.test.type === 'Identifier' && node.test.name in globals) {
|
||||||
|
if (globals[node.test.name]) {
|
||||||
|
path.replaceWithMultiple(node.consequent.body);
|
||||||
|
} else if (node.alternate) {
|
||||||
|
path.replaceWithMultiple(node.alternate.body);
|
||||||
|
} else {
|
||||||
|
path.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Identifier(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
var parent = path.parent;
|
||||||
|
if (parent.type === 'AssignmentExpression' && parent.left === node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name in globals) {
|
||||||
|
path.replaceWith(t.booleanLiteral(globals[node.name]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
CallExpression,
|
||||||
|
|
||||||
|
LogicalExpression(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (node.left.type === 'Identifier' && node.left.name in globals) {
|
||||||
|
const value = globals[node.left.name];
|
||||||
|
|
||||||
|
if (node.operator === '&&') {
|
||||||
|
if (value) {
|
||||||
|
path.replaceWith(node.right);
|
||||||
|
} else {
|
||||||
|
path.replaceWith(t.booleanLiteral(value));
|
||||||
|
}
|
||||||
|
} else if (node.operator === '||') {
|
||||||
|
if (value) {
|
||||||
|
path.replaceWith(t.booleanLiteral(value));
|
||||||
|
} else {
|
||||||
|
path.replaceWith(node.right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var secondPass = {
|
||||||
|
CallExpression,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
Program(path) {
|
||||||
|
path.traverse(firstPass);
|
||||||
|
while (hasDeadModules(requires)) {
|
||||||
|
_requires = requires;
|
||||||
|
requires = {};
|
||||||
|
path.traverse(secondPass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
// Return the list of plugins use for Whole Program Optimisations
|
||||||
|
module.exports = [
|
||||||
|
require('./dead-module-elimination'),
|
||||||
|
];
|
Loading…
Reference in New Issue