448 lines
13 KiB
JavaScript
448 lines
13 KiB
JavaScript
/**
|
|
* libjass
|
|
*
|
|
* https://github.com/Arnavion/libjass
|
|
*
|
|
* Copyright 2013 Arnav Singh
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
var fs = require("fs");
|
|
var path = require("path");
|
|
|
|
var UglifyJS = require("uglify-js");
|
|
|
|
var FileTransform = require("async-build").FileTransform;
|
|
|
|
var Run = (function () {
|
|
function Run(outputLibraryName, unusedVarsToIgnore) {
|
|
this._outputLibraryName = outputLibraryName;
|
|
this._unusedVarsToIgnore = unusedVarsToIgnore;
|
|
|
|
this._root = UglifyJS.parse(fs.readFileSync(path.resolve(__filename, "..", "umd-wrapper.js"), "utf8"));
|
|
|
|
this._root.figure_out_scope({ screw_ie8: true });
|
|
|
|
this._toInsert = null;
|
|
|
|
this._rootSourceMap = null;
|
|
}
|
|
|
|
Run.prototype.addFile = function (file) {
|
|
switch (path.extname(file.path)) {
|
|
case ".js":
|
|
try {
|
|
this._toInsert = UglifyJS.parse(file.contents.toString(), {
|
|
filename: path.basename(file.path),
|
|
toplevel: null,
|
|
}).body;
|
|
}
|
|
catch (ex) {
|
|
if (ex instanceof UglifyJS.JS_Parse_Error) {
|
|
throw new Error("UglifyJS parse error: " + ex.toString() + "\n");
|
|
}
|
|
|
|
throw ex;
|
|
}
|
|
break;
|
|
|
|
case ".map":
|
|
var rawSourceMap = JSON.parse(file.contents.toString());
|
|
|
|
this._rootSourceMap = UglifyJS.SourceMap({
|
|
file: this._outputLibraryName + ".js",
|
|
root: "",
|
|
orig: rawSourceMap,
|
|
});
|
|
|
|
var generator = this._rootSourceMap.get();
|
|
|
|
rawSourceMap.sources.forEach(function (sourceRelativePath, index) {
|
|
generator.setSourceContent(sourceRelativePath, rawSourceMap.sourcesContent[index]);
|
|
});
|
|
|
|
break;
|
|
}
|
|
};
|
|
|
|
Run.prototype.build = function (outputStream) {
|
|
var _this = this;
|
|
|
|
|
|
// Splice in the TS output into the UMD wrapper.
|
|
var insertionParent = this._root.body[0].body.args[1].body;
|
|
|
|
this._toInsert.reverse();
|
|
for (var i = this._toInsert.length - 1; i >= 0; i--) {
|
|
var node = this._toInsert[i];
|
|
if (node instanceof UglifyJS.AST_Var) {
|
|
for (var j = 0; j < node.definitions.length; j++) {
|
|
var definition = node.definitions[j];
|
|
if (definition.name.name === "__extends" || definition.name.name === "__decorate") {
|
|
definition.value = definition.value.right;
|
|
}
|
|
}
|
|
|
|
insertionParent.splice(-1, 0, node);
|
|
this._toInsert.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
insertionParent.splice.apply(insertionParent, [-1, 0].concat(this._toInsert));
|
|
|
|
|
|
// Fixups
|
|
for (var i = 0; i < this._toInsert.length; i++) {
|
|
var node = this._toInsert[i];
|
|
if (node instanceof UglifyJS.AST_Statement && node.body instanceof UglifyJS.AST_Call && node.body.expression.name === "define") {
|
|
var defineCall = node.body;
|
|
|
|
defineCall.expression.name = "def";
|
|
|
|
if (defineCall.args[1].elements[0].value !== "require") {
|
|
throw new Error("Expected first dep to be require");
|
|
}
|
|
defineCall.args[1].elements.shift();
|
|
defineCall.args[2].argnames.shift();
|
|
|
|
if (defineCall.args[1].elements[0].value !== "exports") {
|
|
throw new Error("Expected second dep to be exports");
|
|
}
|
|
defineCall.args[1].elements.shift();
|
|
defineCall.args[2].argnames.push(defineCall.args[2].argnames.shift());
|
|
}
|
|
}
|
|
|
|
|
|
// Remove all license headers except the one from the UMD wrapper
|
|
var firstLicenseHeader = null;
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (node.start) {
|
|
(node.start.comments_before || []).some(function (comment, i) {
|
|
if (comment.value.indexOf("Copyright") !== -1) {
|
|
if (firstLicenseHeader === null) {
|
|
firstLicenseHeader = comment;
|
|
}
|
|
else if (comment !== firstLicenseHeader) {
|
|
node.start.comments_before.splice(i, 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
}));
|
|
|
|
|
|
// Fixup anonymous functions to print a space after "function"
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (node instanceof UglifyJS.AST_Lambda && !node.name) {
|
|
node.name = Object.create(UglifyJS.AST_Node.prototype);
|
|
node.name.print = function () { };
|
|
}
|
|
}));
|
|
|
|
|
|
// Fix alignment of multi-line block comments
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (node.start && node.start.comments_before) {
|
|
node.start.comments_before.forEach(function (comment) {
|
|
if (comment.value.indexOf("Copyright") !== -1) {
|
|
return;
|
|
}
|
|
|
|
var lines = comment.value.split("\n");
|
|
if (lines.length < 2) {
|
|
return;
|
|
}
|
|
|
|
var indent = " "; // 5 spaces
|
|
for (var i = 0; i < comment.col; i++) {
|
|
indent += " ";
|
|
}
|
|
lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, indent);
|
|
comment.value = [lines[0]].concat(lines.slice(1).map(function (line) { return line.replace(/^\s+/, indent); })).join("\n");
|
|
});
|
|
}
|
|
}));
|
|
|
|
|
|
this._root.figure_out_scope({ screw_ie8: true });
|
|
|
|
|
|
// Remove some things from the AST
|
|
var nodesToRemove = [];
|
|
|
|
// Set if there are any unused variables apart from the ones in unusedVarsToIgnore
|
|
var haveUnusedVars = false;
|
|
|
|
// Repeat because removing some declarations may make others unreferenced
|
|
for (;;) {
|
|
this._root.figure_out_scope({ screw_ie8: true });
|
|
|
|
// Unreferenced variable and function declarations, and unreferenced terminal function arguments
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (node instanceof UglifyJS.AST_SymbolDeclaration && node.unreferenced()) {
|
|
if (node instanceof UglifyJS.AST_SymbolFunarg) {
|
|
if (this.parent().argnames.indexOf(node) === this.parent().argnames.length - 1) {
|
|
nodesToRemove.push({ node: node, parent: this.parent().argnames });
|
|
}
|
|
}
|
|
else if (node instanceof UglifyJS.AST_SymbolVar) {
|
|
if (_this._unusedVarsToIgnore.indexOf(node.name) !== -1) {
|
|
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).definitions });
|
|
if (this.parent(1).definitions.length === 1) {
|
|
nodesToRemove.push({ node: this.parent(1), parent: this.parent(2).body });
|
|
}
|
|
}
|
|
else {
|
|
haveUnusedVars = true;
|
|
}
|
|
}
|
|
else if (node instanceof UglifyJS.AST_SymbolDefun) {
|
|
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).body });
|
|
}
|
|
}
|
|
}));
|
|
|
|
if (nodesToRemove.length === 0) {
|
|
break;
|
|
}
|
|
|
|
nodesToRemove.forEach(function (tuple) {
|
|
tuple.parent.splice(tuple.parent.indexOf(tuple.node), 1);
|
|
});
|
|
|
|
nodesToRemove = [];
|
|
}
|
|
|
|
|
|
// Move var statements at the end of blocks (generated by TS for rest parameters) to the start of the block.
|
|
// This is needed to prevent unreachable-code warnings from UJS
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (
|
|
node instanceof UglifyJS.AST_Block &&
|
|
node.body[node.body.length - 1] instanceof UglifyJS.AST_Var
|
|
) {
|
|
node.body.unshift(node.body.pop());
|
|
}
|
|
}));
|
|
|
|
|
|
// Split multiple vars per declaration into one var per declaration
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (
|
|
node instanceof UglifyJS.AST_Var &&
|
|
node.definitions.length > 1 &&
|
|
this.parent() instanceof UglifyJS.AST_Block
|
|
) {
|
|
var parent = this.parent().body;
|
|
parent.splice.apply(parent, [parent.indexOf(node), 1].concat(node.definitions.map(function (definition) {
|
|
return new UglifyJS.AST_Var({ start: node.start, end: node.end, definitions: [definition] });
|
|
})));
|
|
}
|
|
}));
|
|
|
|
|
|
// Rename all function arguments that begin with _ to not have the _.
|
|
// This converts the TypeScript syntax of declaring private members in the constructor declaration `function Color(private _red: number, ...)` to `function Color(red, ...)`
|
|
// so that it matches the JSDoc (and looks nicer).
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (
|
|
node instanceof UglifyJS.AST_SymbolFunarg &&
|
|
node.thedef.name[0] === "_" &&
|
|
node.thedef.name[1] !== "_" &&
|
|
node.thedef.name !== "_super" // Don't rename _super (used in TypeScript's inheritance shim) to super. super is a reserved word.
|
|
) {
|
|
node.thedef.name = node.thedef.name.slice(1);
|
|
}
|
|
}));
|
|
|
|
|
|
// Output
|
|
var output = {
|
|
source_map: this._rootSourceMap,
|
|
ascii_only: true,
|
|
beautify: true,
|
|
comments: function (node, comment) {
|
|
return comment.value.indexOf("tslint") === -1;
|
|
},
|
|
};
|
|
|
|
var stream = UglifyJS.OutputStream(output);
|
|
this._root.print(stream);
|
|
|
|
outputStream.push({
|
|
path: this._outputLibraryName + ".js",
|
|
contents: Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL=" + this._outputLibraryName + ".js.map")])
|
|
});
|
|
|
|
outputStream.push({
|
|
path: this._outputLibraryName + ".js.map",
|
|
contents: new Buffer(this._rootSourceMap.get().toString())
|
|
});
|
|
|
|
// Print unused variables
|
|
if (haveUnusedVars) {
|
|
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
|
if (node instanceof UglifyJS.AST_SymbolVar && node.unreferenced()) {
|
|
if (_this._unusedVarsToIgnore.indexOf(node.name) === -1) {
|
|
console.warn("Unused variable %s at %s:%s:%s", node.name, node.start.file, node.start.line, node.start.col);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
};
|
|
|
|
return Run;
|
|
})();
|
|
|
|
module.exports = {
|
|
build: function (outputLibraryName, unusedVarsToIgnore) {
|
|
var run = new Run(outputLibraryName, unusedVarsToIgnore);
|
|
|
|
return new FileTransform(function (file) {
|
|
run.addFile(file);
|
|
}, function () {
|
|
run.build(this);
|
|
});
|
|
},
|
|
|
|
watch: function (outputLibraryName, unusedVarsToIgnore) {
|
|
var files = Object.create(null);
|
|
|
|
return new FileTransform(function (file) {
|
|
if (file.path !== "END") {
|
|
files[file.path] = file;
|
|
}
|
|
else {
|
|
var run = new Run(outputLibraryName, unusedVarsToIgnore);
|
|
Object.keys(files).forEach(function (filename) {
|
|
run.addFile(files[filename]);
|
|
});
|
|
run.build(this);
|
|
}
|
|
});
|
|
},
|
|
|
|
minify: function () {
|
|
var codeFile = null;
|
|
var sourceMapFile = null;
|
|
|
|
return new FileTransform(function (file) {
|
|
switch (path.extname(file.path)) {
|
|
case ".js":
|
|
codeFile = file;
|
|
break;
|
|
case ".map":
|
|
sourceMapFile = file;
|
|
break;
|
|
}
|
|
|
|
if (codeFile !== null && sourceMapFile !== null) {
|
|
UglifyJS.base54.reset();
|
|
|
|
|
|
// Parse
|
|
var root = null;
|
|
root = UglifyJS.parse(codeFile.contents.toString(), {
|
|
filename: path.basename(codeFile.path),
|
|
toplevel: root
|
|
});
|
|
|
|
root.figure_out_scope({ screw_ie8: true });
|
|
|
|
|
|
// Warnings
|
|
root.scope_warnings({
|
|
func_arguments: false
|
|
});
|
|
|
|
|
|
// Compress
|
|
var compressor = UglifyJS.Compressor({
|
|
warnings: true,
|
|
screw_ie8: true
|
|
});
|
|
root = root.transform(compressor);
|
|
|
|
|
|
// Mangle
|
|
root.figure_out_scope({ screw_ie8: true });
|
|
root.compute_char_frequency();
|
|
root.mangle_names({ screw_ie8: true });
|
|
root = UglifyJS.mangle_properties(root, {
|
|
regex: /^_/
|
|
});
|
|
|
|
|
|
// Output
|
|
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
|
|
|
|
var output = {
|
|
source_map: UglifyJS.SourceMap({
|
|
file: path.basename(sourceMapFile.path),
|
|
orig: sourceMapFile.contents.toString()
|
|
}),
|
|
ascii_only: true,
|
|
comments: function (node, comment) {
|
|
if (!firstLicenseHeaderFound && comment.value.indexOf("Copyright") !== -1) {
|
|
firstLicenseHeaderFound = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
screw_ie8: true
|
|
};
|
|
|
|
var stream = UglifyJS.OutputStream(output);
|
|
root.print(stream);
|
|
|
|
codeFile.path = codeFile.path.replace(/\.js$/, ".min.js");
|
|
sourceMapFile.path = sourceMapFile.path.replace(/\.js\.map$/, ".min.js.map");
|
|
|
|
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.path)]);
|
|
this.push(codeFile);
|
|
|
|
var inputSourceMapObject = JSON.parse(sourceMapFile.contents.toString());
|
|
var outputSourceMapObject = output.source_map.get();
|
|
outputSourceMapObject._sources.toArray().forEach(function (filename, i) {
|
|
outputSourceMapObject.setSourceContent(filename, inputSourceMapObject.sourcesContent[i]);
|
|
});
|
|
|
|
sourceMapFile.contents = new Buffer(output.source_map.toString());
|
|
this.push(sourceMapFile);
|
|
|
|
codeFile = null;
|
|
sourceMapFile = null;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
var originalSymbolUnreferenced = UglifyJS.AST_Symbol.prototype.unreferenced;
|
|
|
|
// Workaround for https://github.com/mishoo/UglifyJS2/issues/789 - Nodes explicitly marked with ujs:unreferenced will not be warned for.
|
|
UglifyJS.AST_Symbol.prototype.unreferenced = function () {
|
|
if (this.start.comments_before.length > 0 && this.start.comments_before[0].value.trim() === "ujs:unreferenced") {
|
|
return false;
|
|
}
|
|
|
|
return originalSymbolUnreferenced.call(this);
|
|
};
|