Files
react-native/packager/react-packager/src/Bundler/Bundle.js
Tadeu Zagallo 0b46a0c13b 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
2015-11-17 03:39:28 -08:00

378 lines
10 KiB
JavaScript

/**
* 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 _ = require('underscore');
const base64VLQ = require('./base64-vlq');
const UglifyJS = require('uglify-js');
const ModuleTransport = require('../lib/ModuleTransport');
const Activity = require('../Activity');
const SOURCEMAPPING_URL = '\n\/\/@ sourceMappingURL=';
class Bundle {
constructor(sourceMapUrl) {
this._finalized = false;
this._modules = [];
this._assets = [];
this._sourceMap = false;
this._sourceMapUrl = sourceMapUrl;
this._shouldCombineSourceMaps = false;
}
setMainModuleId(moduleId) {
this._mainModuleId = moduleId;
}
addModule(module) {
if (!(module instanceof ModuleTransport)) {
throw new Error('Expeceted a ModuleTransport object');
}
// If we get a map from the transformer we'll switch to a mode
// were we're combining the source maps as opposed to
if (!this._shouldCombineSourceMaps && module.map != null) {
this._shouldCombineSourceMaps = true;
}
this._modules.push(module);
}
getModules() {
return this._modules;
}
addAsset(asset) {
this._assets.push(asset);
}
finalize(options) {
options = options || {};
if (options.runMainModule) {
options.runBeforeMainModule.forEach(this._addRequireCall, this);
this._addRequireCall(this._mainModuleId);
}
Object.freeze(this._modules);
Object.seal(this._modules);
Object.freeze(this._assets);
Object.seal(this._assets);
this._finalized = true;
}
_addRequireCall(moduleId) {
const code = ';require("' + moduleId + '");';
const name = 'require-' + moduleId;
this.addModule(new ModuleTransport({
name,
code,
virtual: true,
sourceCode: code,
sourcePath: name + '.js',
}));
}
_assertFinalized() {
if (!this._finalized) {
throw new Error('Bundle needs to be finalized before getting any source');
}
}
_getSource(dev) {
if (this._source) {
return this._source;
}
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) {
const sourceMap = this.getSourceMap({excludeSource: true, dev});
/*eslint-env node*/
const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64');
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
}
return this._inlineSourceMap;
}
getSource(options) {
this._assertFinalized();
options = options || {};
if (options.minify) {
return this.getMinifiedSourceAndMap(options.dev).code;
}
let source = this._getSource(options.dev);
if (options.inlineSourceMap) {
source += SOURCEMAPPING_URL + this._getInlineSourceMap(options.dev);
} else if (this._sourceMapUrl) {
source += SOURCEMAPPING_URL + this._sourceMapUrl;
}
return source;
}
getMinifiedSourceAndMap(dev) {
this._assertFinalized();
if (this._minifiedSourceAndMap) {
return this._minifiedSourceAndMap;
}
const source = this._getSource(dev);
try {
const minifyActivity = Activity.startEvent('minify');
this._minifiedSourceAndMap = UglifyJS.minify(source, {
fromString: true,
outSourceMap: 'bundle.js',
inSourceMap: this.getSourceMap(),
output: {ascii_only: true},
});
Activity.endEvent(minifyActivity);
return this._minifiedSourceAndMap;
} catch(e) {
// Sometimes, when somebody is using a new syntax feature that we
// don't yet have transform for, the untransformed line is sent to
// uglify, and it chokes on it. This code tries to print the line
// and the module for easier debugging
let errorMessage = 'Error while minifying JS\n';
if (e.line) {
errorMessage += 'Transformed code line: "' +
source.split('\n')[e.line - 1] + '"\n';
}
if (e.pos) {
let fromIndex = source.lastIndexOf('__d(\'', e.pos);
if (fromIndex > -1) {
fromIndex += '__d(\''.length;
const toIndex = source.indexOf('\'', fromIndex);
errorMessage += 'Module name (best guess): ' +
source.substring(fromIndex, toIndex) + '\n';
}
}
errorMessage += e.toString();
throw new Error(errorMessage);
}
}
/**
* I found a neat trick in the sourcemap spec that makes it easy
* to concat sourcemaps. The `sections` field allows us to combine
* the sourcemap easily by adding an offset. Tested on chrome.
* Seems like it's not yet in Firefox but that should be fine for
* now.
*/
_getCombinedSourceMaps(options) {
const result = {
version: 3,
file: 'bundle.js',
sections: [],
};
let line = 0;
this._modules.forEach(function(module) {
let map = module.map;
if (module.virtual) {
map = generateSourceMapForVirtualModule(module);
}
if (options.excludeSource) {
map = _.extend({}, map, {sourcesContent: []});
}
result.sections.push({
offset: { line: line, column: 0 },
map: map,
});
line += module.code.split('\n').length;
});
return result;
}
getSourceMap(options) {
this._assertFinalized();
options = options || {};
if (options.minify) {
return this.getMinifiedSourceAndMap(options.dev).map;
}
if (this._shouldCombineSourceMaps) {
return this._getCombinedSourceMaps(options);
}
const mappings = this._getMappings();
const map = {
file: 'bundle.js',
sources: _.pluck(this._modules, 'sourcePath'),
version: 3,
names: [],
mappings: mappings,
sourcesContent: options.excludeSource
? [] : _.pluck(this._modules, 'sourceCode')
};
return map;
}
getAssets() {
return this._assets;
}
_getMappings() {
const modules = this._modules;
// The first line mapping in our package is basically the base64vlq code for
// zeros (A).
const firstLine = 'AAAA';
// Most other lines in our mappings are all zeros (for module, column etc)
// except for the lineno mappinp: curLineno - prevLineno = 1; Which is C.
const line = 'AACA';
const moduleLines = Object.create(null);
let mappings = '';
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const code = module.code;
let lastCharNewLine = false;
moduleLines[module.sourcePath] = 0;
for (let t = 0; t < code.length; t++) {
if (t === 0 && i === 0) {
mappings += firstLine;
} else if (t === 0) {
mappings += 'AC';
// This is the only place were we actually don't know the mapping ahead
// of time. When it's a new module (and not the first) the lineno
// mapping is 0 (current) - number of lines in prev module.
mappings += base64VLQ.encode(
0 - moduleLines[modules[i - 1].sourcePath]
);
mappings += 'A';
} else if (lastCharNewLine) {
moduleLines[module.sourcePath]++;
mappings += line;
}
lastCharNewLine = code[t] === '\n';
if (lastCharNewLine) {
mappings += ';';
}
}
if (i !== modules.length - 1) {
mappings += ';';
}
}
return mappings;
}
getJSModulePaths() {
return this._modules.filter(function(module) {
// Filter out non-js files. Like images etc.
return !module.virtual;
}).map(function(module) {
return module.sourcePath;
});
}
getDebugInfo() {
return [
'<div><h3>Main Module:</h3> ' + this._mainModuleId + '</div>',
'<style>',
'pre.collapsed {',
' height: 10px;',
' width: 100px;',
' display: block;',
' text-overflow: ellipsis;',
' overflow: hidden;',
' cursor: pointer;',
'}',
'</style>',
'<h3> Module paths and transformed code: </h3>',
this._modules.map(function(m) {
return '<div> <h4> Path: </h4>' + m.sourcePath + '<br/> <h4> Source: </h4>' +
'<code><pre class="collapsed" onclick="this.classList.remove(\'collapsed\')">' +
_.escape(m.code) + '</pre></code></div>';
}).join('\n'),
].join('\n');
}
toJSON() {
if (!this._finalized) {
throw new Error('Cannot serialize bundle unless finalized');
}
return {
modules: this._modules,
assets: this._assets,
sourceMapUrl: this._sourceMapUrl,
mainModuleId: this._mainModuleId,
};
}
static fromJSON(json) {
const bundle = new Bundle(json.sourceMapUrl);
bundle._mainModuleId = json.mainModuleId;
bundle._assets = json.assets;
bundle._modules = json.modules;
bundle._sourceMapUrl = json.sourceMapUrl;
Object.freeze(bundle._modules);
Object.seal(bundle._modules);
Object.freeze(bundle._assets);
Object.seal(bundle._assets);
bundle._finalized = true;
return bundle;
}
}
function generateSourceMapForVirtualModule(module) {
// All lines map 1-to-1
let mappings = 'AAAA;';
for (let i = 1; i < module.code.split('\n').length; i++) {
mappings += 'AACA;';
}
return {
version: 3,
sources: [ module.sourcePath ],
names: [],
mappings: mappings,
file: module.sourcePath,
sourcesContent: [ module.sourceCode ],
};
}
module.exports = Bundle;