46646a9dd1
ES6 modules: remove UMD header validation
377 lines
13 KiB
JavaScript
377 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
var acorn = require('acorn');
|
|
var escodegen = require('escodegen');
|
|
var vm = require('vm');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
|
|
var PDFJS_PREPROCESSOR_NAME = 'PDFJSDev';
|
|
var ROOT_PREFIX = '$ROOT/';
|
|
|
|
function isLiteral(obj, value) {
|
|
return obj.type === 'Literal' && obj.value === value;
|
|
}
|
|
|
|
function isPDFJSPreprocessor(obj) {
|
|
return obj.type === 'Identifier' &&
|
|
obj.name === PDFJS_PREPROCESSOR_NAME;
|
|
}
|
|
|
|
function evalWithDefines(code, defines, loc) {
|
|
if (!code || !code.trim()) {
|
|
throw new Error('No JavaScript expression given');
|
|
}
|
|
return vm.runInNewContext(code, defines, {displayErrors: false});
|
|
}
|
|
|
|
function handlePreprocessorAction(ctx, actionName, args, loc) {
|
|
try {
|
|
var arg;
|
|
switch (actionName) {
|
|
case 'test':
|
|
arg = args[0];
|
|
if (!arg || arg.type !== 'Literal' ||
|
|
typeof arg.value !== 'string') {
|
|
throw new Error('No code for testing is given');
|
|
}
|
|
var isTrue = !!evalWithDefines(arg.value, ctx.defines);
|
|
return {type: 'Literal', value: isTrue, loc: loc};
|
|
case 'eval':
|
|
arg = args[0];
|
|
if (!arg || arg.type !== 'Literal' ||
|
|
typeof arg.value !== 'string') {
|
|
throw new Error('No code for eval is given');
|
|
}
|
|
var result = evalWithDefines(arg.value, ctx.defines);
|
|
if (typeof result === 'boolean' || typeof result === 'string' ||
|
|
typeof result === 'number') {
|
|
return {type: 'Literal', value: result, loc: loc};
|
|
}
|
|
if (typeof result === 'object') {
|
|
var parsedObj = acorn.parse('(' + JSON.stringify(result) + ')');
|
|
parsedObj.body[0].expression.loc = loc;
|
|
return parsedObj.body[0].expression;
|
|
}
|
|
break;
|
|
case 'json':
|
|
arg = args[0];
|
|
if (!arg || arg.type !== 'Literal' ||
|
|
typeof arg.value !== 'string') {
|
|
throw new Error('Path to JSON is not provided');
|
|
}
|
|
var jsonPath = arg.value;
|
|
if (jsonPath.indexOf(ROOT_PREFIX) === 0) {
|
|
jsonPath = path.join(ctx.rootPath,
|
|
jsonPath.substring(ROOT_PREFIX.length));
|
|
}
|
|
var jsonContent = fs.readFileSync(jsonPath).toString();
|
|
var parsedJSON = acorn.parse('(' + jsonContent + ')');
|
|
parsedJSON.body[0].expression.loc = loc;
|
|
return parsedJSON.body[0].expression;
|
|
}
|
|
throw new Error('Unsupported action');
|
|
} catch (e) {
|
|
throw new Error('Could not process ' + PDFJS_PREPROCESSOR_NAME + '.' +
|
|
actionName + ' at ' + JSON.stringify(loc) + '\n' +
|
|
e.name + ': ' + e.message);
|
|
}
|
|
}
|
|
|
|
function postprocessNode(ctx, node) {
|
|
switch (node.type) {
|
|
case 'IfStatement':
|
|
if (isLiteral(node.test, true)) {
|
|
// if (true) stmt1; => stmt1
|
|
return node.consequent;
|
|
} else if (isLiteral(node.test, false)) {
|
|
// if (false) stmt1; else stmt2; => stmt2
|
|
return node.alternate || {type: 'EmptyStatement', loc: node.loc};
|
|
}
|
|
break;
|
|
case 'ConditionalExpression':
|
|
if (isLiteral(node.test, true)) {
|
|
// true ? stmt1 : stmt2 => stmt1
|
|
return node.consequent;
|
|
} else if (isLiteral(node.test, false)) {
|
|
// false ? stmt1 : stmt2 => stmt2
|
|
return node.alternate;
|
|
}
|
|
break;
|
|
case 'UnaryExpression':
|
|
if (node.operator === 'typeof' &&
|
|
isPDFJSPreprocessor(node.argument)) {
|
|
// typeof PDFJSDev => 'object'
|
|
return {type: 'Literal', value: 'object', loc: node.loc};
|
|
}
|
|
if (node.operator === '!' &&
|
|
node.argument.type === 'Literal' &&
|
|
typeof node.argument.value === 'boolean') {
|
|
// !true => false, !false => true
|
|
return {type: 'Literal', value: !node.argument.value, loc: node.loc};
|
|
}
|
|
break;
|
|
case 'LogicalExpression':
|
|
switch (node.operator) {
|
|
case '&&':
|
|
if (isLiteral(node.left, true)) {
|
|
return node.right;
|
|
}
|
|
if (isLiteral(node.left, false)) {
|
|
return node.left;
|
|
}
|
|
break;
|
|
case '||':
|
|
if (isLiteral(node.left, true)) {
|
|
return node.left;
|
|
}
|
|
if (isLiteral(node.left, false)) {
|
|
return node.right;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 'BinaryExpression':
|
|
switch (node.operator) {
|
|
case '==':
|
|
case '===':
|
|
case '!=':
|
|
case '!==':
|
|
if (node.left.type === 'Literal' &&
|
|
node.right.type === 'Literal' &&
|
|
typeof node.left.value === typeof node.right.value) {
|
|
// folding two literals == and != check
|
|
switch (typeof node.left.value) {
|
|
case 'string':
|
|
case 'boolean':
|
|
case 'number':
|
|
var equal = node.left.value === node.right.value;
|
|
return {
|
|
type: 'Literal',
|
|
value: (node.operator[0] === '=') === equal,
|
|
loc: node.loc
|
|
};
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 'CallExpression':
|
|
if (node.callee.type === 'MemberExpression' &&
|
|
isPDFJSPreprocessor(node.callee.object) &&
|
|
node.callee.property.type === 'Identifier') {
|
|
// PDFJSDev.xxxx(arg1, arg2, ...) => tranform
|
|
var action = node.callee.property.name;
|
|
return handlePreprocessorAction(ctx, action,
|
|
node.arguments, node.loc);
|
|
}
|
|
break;
|
|
case 'BlockStatement':
|
|
var subExpressionIndex = 0;
|
|
while (subExpressionIndex < node.body.length) {
|
|
switch (node.body[subExpressionIndex].type) {
|
|
case 'EmptyStatement':
|
|
// Removing empty statements from the blocks.
|
|
node.body.splice(subExpressionIndex, 1);
|
|
continue;
|
|
case 'BlockStatement':
|
|
// Block statements inside a block are moved to the parent one.
|
|
var subChildren = node.body[subExpressionIndex].body;
|
|
Array.prototype.splice.apply(node.body,
|
|
[subExpressionIndex, 1].concat(subChildren));
|
|
subExpressionIndex += Math.max(subChildren.length - 1, 0);
|
|
continue;
|
|
case 'ReturnStatement':
|
|
case 'ThrowStatement':
|
|
// Removing dead code after return or throw.
|
|
node.body.splice(subExpressionIndex + 1,
|
|
node.body.length - subExpressionIndex - 1);
|
|
break;
|
|
}
|
|
subExpressionIndex++;
|
|
}
|
|
break;
|
|
case 'FunctionDeclaration':
|
|
case 'FunctionExpression':
|
|
var block = node.body;
|
|
if (block.body.length > 0 &&
|
|
block.body[block.body.length - 1].type === 'ReturnStatement' &&
|
|
!block.body[block.body.length - 1].argument) {
|
|
// Function body ends with return without arg -- removing it.
|
|
block.body.pop();
|
|
}
|
|
break;
|
|
case 'Program':
|
|
// Checking for a function closure that looks like UMD header.
|
|
node.body.some(function (item, index) {
|
|
// Is it `(function (root, factory) { ? }(this, function (?) {?}));` ?
|
|
if (item.type !== 'ExpressionStatement' ||
|
|
item.expression.type !== 'CallExpression' ||
|
|
item.expression.callee.type !== 'FunctionExpression' ||
|
|
item.expression.callee.params.length !== 2 ||
|
|
item.expression.arguments.length !== 2 ||
|
|
item.expression.arguments[0].type !== 'ThisExpression' ||
|
|
item.expression.arguments[1].type !== 'FunctionExpression') {
|
|
return false;
|
|
}
|
|
var init = item.expression.callee;
|
|
// Is init body looks like
|
|
// `if (?) { ? } else if (typeof exports !== 'undefined') { ? } ...`?
|
|
if (init.body.type !== 'BlockStatement' ||
|
|
init.body.body.length !== 1 ||
|
|
init.body.body[0].type !== 'IfStatement') {
|
|
return false;
|
|
}
|
|
var initIf = init.body.body[0];
|
|
if (initIf.alternate.type !== 'IfStatement' ||
|
|
initIf.alternate.test.type !== 'BinaryExpression' ||
|
|
initIf.alternate.test.operator !== '!==' ||
|
|
initIf.alternate.test.left.type !== 'UnaryExpression' ||
|
|
initIf.alternate.test.left.operator !== 'typeof' ||
|
|
initIf.alternate.test.left.argument.type !== 'Identifier' ||
|
|
initIf.alternate.test.left.argument.name !== 'exports' ||
|
|
initIf.alternate.test.right.type !== 'Literal' ||
|
|
initIf.alternate.test.right.value !== 'undefined' ||
|
|
initIf.alternate.consequent.type !== 'BlockStatement') {
|
|
return false;
|
|
}
|
|
var commonJsInit = initIf.alternate.consequent;
|
|
// Is commonJsInit `factory(exports, ...)` ?
|
|
if (commonJsInit.body.length !== 1 ||
|
|
commonJsInit.body[0].type !== 'ExpressionStatement' ||
|
|
commonJsInit.body[0].expression.type !== 'CallExpression' ||
|
|
commonJsInit.body[0].expression.callee.type !== 'Identifier') {
|
|
return false;
|
|
}
|
|
var commonJsInitArgs = commonJsInit.body[0].expression.arguments;
|
|
if (commonJsInitArgs.length === 0 ||
|
|
commonJsInitArgs[0].type !== 'Identifier' ||
|
|
commonJsInitArgs[0].name !== 'exports') {
|
|
return false;
|
|
}
|
|
var factory = item.expression.arguments[1];
|
|
// Is factory `function (exports, ....) { ? }` ?
|
|
if (factory.params.length === 0 ||
|
|
factory.params[0].type !== 'Identifier' ||
|
|
factory.params[0].name !== 'exports' ||
|
|
factory.body.type !== 'BlockStatement') {
|
|
return true;
|
|
}
|
|
var factoryParams = factory.params;
|
|
var factoryBody = factory.body;
|
|
|
|
// Remove closure and function and replacing parameters with vars.
|
|
node.body.splice(index, 1);
|
|
for (var i = 1, ii = factoryParams.length; i < ii; i++) {
|
|
var varNode = {
|
|
type: 'VariableDeclaration',
|
|
'declarations': [{
|
|
type: 'VariableDeclarator',
|
|
id: factoryParams[i],
|
|
init: commonJsInitArgs[i] || null,
|
|
loc: factoryParams[i].loc
|
|
}],
|
|
kind: 'var'
|
|
};
|
|
node.body.splice(index++, 0, varNode);
|
|
}
|
|
factoryBody.body.forEach(function (item) {
|
|
node.body.splice(index++, 0, item);
|
|
});
|
|
return true;
|
|
});
|
|
break;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function fixComments(ctx, node) {
|
|
if (!ctx.saveComments) {
|
|
return;
|
|
}
|
|
// Fixes double comments in the escodegen output.
|
|
delete node.trailingComments;
|
|
// Removes ESLint and other service comments.
|
|
if (node.leadingComments) {
|
|
var CopyrightRegExp = /\bcopyright\b/i;
|
|
var BlockCommentRegExp = /^\s*(globals|eslint|falls through)\b/;
|
|
var LineCommentRegExp = /^\s*eslint\b/;
|
|
|
|
var i = 0;
|
|
while (i < node.leadingComments.length) {
|
|
var type = node.leadingComments[i].type;
|
|
var value = node.leadingComments[i].value;
|
|
|
|
if (ctx.saveComments === 'copyright') {
|
|
// Remove all comments, except Copyright notices and License headers.
|
|
if (!(type === 'Block' && CopyrightRegExp.test(value))) {
|
|
node.leadingComments.splice(i, 1);
|
|
continue;
|
|
}
|
|
} else if ((type === 'Block' && BlockCommentRegExp.test(value)) ||
|
|
(type === 'Line' && LineCommentRegExp.test(value))) {
|
|
node.leadingComments.splice(i, 1);
|
|
continue;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
function traverseTree(ctx, node) {
|
|
// generic node processing
|
|
for (var i in node) {
|
|
var child = node[i];
|
|
if (typeof child === 'object' && child !== null && child.type) {
|
|
var result = traverseTree(ctx, child);
|
|
if (result !== child) {
|
|
node[i] = result;
|
|
}
|
|
} else if (Array.isArray(child)) {
|
|
child.forEach(function (childItem, index) {
|
|
if (typeof childItem === 'object' && childItem !== null &&
|
|
childItem.type) {
|
|
var result = traverseTree(ctx, childItem);
|
|
if (result !== childItem) {
|
|
child[index] = result;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
node = postprocessNode(ctx, node) || node;
|
|
|
|
fixComments(ctx, node);
|
|
return node;
|
|
}
|
|
|
|
function preprocessPDFJSCode(ctx, code) {
|
|
var saveComments = !!ctx.saveComments;
|
|
var format = ctx.format || {
|
|
indent: {
|
|
style: ' ',
|
|
adjustMultilineComment: saveComments,
|
|
}
|
|
};
|
|
var comments;
|
|
var parseComment = {
|
|
locations: true,
|
|
onComments: saveComments || (comments = []),
|
|
sourceType: 'module',
|
|
};
|
|
var codegenOptions = {
|
|
format: format,
|
|
comment: saveComments,
|
|
parse: acorn.parse,
|
|
};
|
|
var syntax = acorn.parse(code, parseComment);
|
|
if (saveComments) {
|
|
escodegen.attachComments(syntax, comments);
|
|
}
|
|
traverseTree(ctx, syntax);
|
|
return escodegen.generate(syntax, codegenOptions);
|
|
}
|
|
|
|
exports.preprocessPDFJSCode = preprocessPDFJSCode;
|