Skip to content

Commit

Permalink
Replace the webpack+acorn transform with a Babel plugin
Browse files Browse the repository at this point in the history
This commit converts the pdfjsdev-loader transform into a Babel plugin,
to skip a AST->string->AST round-trip.

Before this commit, the webpack build process was:
1. Babel parses the code
2. Babel transforms the AST
3. Babel generates the code
4. Acorn parses the code
5. pdfjsdev-loader transforms the AST
6. @javascript-obfuscator/escodegen generates the code
7. Webpack parses the file
8. Webpack concatenates the files

After this commit, it is reduced to:
1. Babel parses the code
2. Babel transforms the AST
3. babel-plugin-pdfjs-preprocessor transforms the AST
4. Babel generates the code
5. Webpack parses the file
6. Webpack concatenates the files

This change improves the build time by ~25% (tested on MacBook Air M2):
- `gulp lib`: 3.4s to 2.6s
- `gulp dist`: 36s to 29s
- `gulp generic`: 5.5s to 4.0s
- `gulp mozcentral`: 4.7s to 3.2s

The new Babel plugin doesn't support the `saveComments` option of
pdfjsdev-loader, and it just always discards comments. Even though
pdfjsdev-loader supported multiple values for that option, it was
effectively ignored due to `acorn` dropping comments by default.
  • Loading branch information
nicolo-ribaudo committed Jan 23, 2024
1 parent bba8318 commit 610ebcb
Show file tree
Hide file tree
Showing 16 changed files with 338 additions and 541 deletions.
238 changes: 238 additions & 0 deletions external/builder/babel-plugin-pdfjs-preprocessor.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { types as t, transformSync } from "@babel/core";
import fs from "fs";
import { join as joinPaths } from "path";
import vm from "vm";

const ROOT_PREFIX = "$ROOT/";
const PDFJS_PREPROCESSOR_NAME = "PDFJSDev";

function isPDFJSPreprocessor(obj) {
return obj.type === "Identifier" && obj.name === PDFJS_PREPROCESSOR_NAME;
}

function evalWithDefines(code, defines) {
if (!code || !code.trim()) {
throw new Error("No JavaScript expression given");
}
return vm.runInNewContext(code, defines, { displayErrors: false });
}

function handlePreprocessorAction(ctx, actionName, args, path) {
try {
const arg = args[0];
switch (actionName) {
case "test":
if (!t.isStringLiteral(arg)) {
throw new Error("No code for testing is given");
}
return !!evalWithDefines(arg.value, ctx.defines);
case "eval":
if (!t.isStringLiteral(arg)) {
throw new Error("No code for eval is given");
}
const result = evalWithDefines(arg.value, ctx.defines);
if (
typeof result === "boolean" ||
typeof result === "string" ||
typeof result === "number" ||
typeof result === "object"
) {
return result;
}
break;
case "json":
if (!t.isStringLiteral(arg)) {
throw new Error("Path to JSON is not provided");
}
let jsonPath = arg.value;
if (jsonPath.indexOf(ROOT_PREFIX) === 0) {
jsonPath = joinPaths(
ctx.rootPath,
jsonPath.substring(ROOT_PREFIX.length)
);
}
return JSON.parse(fs.readFileSync(jsonPath, "utf8"));
}
throw new Error("Unsupported action");
} catch (e) {
throw path.buildCodeFrameError(
"Could not process " +
PDFJS_PREPROCESSOR_NAME +
"." +
actionName +
": " +
e.message
);
}
}

function babelPluginPDFJSPreprocessor(babel, ctx) {
return {
name: "babel-plugin-pdfjs-preprocessor",
manipulateOptions({ parserOpts }) {
parserOpts.attachComment = false;
},
visitor: {
"ExportNamedDeclaration|ImportDeclaration": ({ node }) => {
if (node.source && ctx.map && ctx.map[node.source.value]) {
const newValue = ctx.map[node.source.value];
node.source.value = newValue;
}
},
"IfStatement|ConditionalExpression": {
exit(path) {
const { node } = path;
if (t.isBooleanLiteral(node.test)) {
// if (true) stmt1; => stmt1
// if (false) stmt1; else stmt2; => stmt2
path.replaceWith(
node.test.value === true
? node.consequent
: node.alternate || t.emptyStatement()
);
}
},
},
UnaryExpression(path) {
const { node } = path;
if (node.operator === "typeof" && isPDFJSPreprocessor(node.argument)) {
// typeof PDFJSDev => 'object'
path.replaceWith(t.stringLiteral("object"));
return;
}
if (node.operator === "!" && t.isBooleanLiteral(node.argument)) {
// !true => false, !false => true
path.replaceWith(t.booleanLiteral(!node.argument.value));
}
},
LogicalExpression: {
exit(path) {
const { node } = path;
if (!t.isBooleanLiteral(node.left)) {
return;
}

switch (node.operator) {
case "&&":
// true && expr => expr
// false && expr => false
path.replaceWith(
node.left.value === true ? node.right : node.left
);
break;
case "||":
// true || expr => true
// false || expr => expr
path.replaceWith(
node.left.value === true ? node.left : node.right
);
break;
}
},
},
BinaryExpression: {
exit(path) {
const { node } = path;

if (["==", "===", "!=", "!=="].includes(node.operator)) {
// folding == and != check that can be statically evaluated
const { confident, value } = path.evaluate();
if (confident) {
path.replaceWith(t.booleanLiteral(value));
}
}
},
},
CallExpression(path) {
const { node } = path;
if (
t.isMemberExpression(node.callee) &&
isPDFJSPreprocessor(node.callee.object) &&
t.isIdentifier(node.callee.property) &&
!node.callee.computed
) {
// PDFJSDev.xxxx(arg1, arg2, ...) => transform
const action = node.callee.property.name;
const result = handlePreprocessorAction(
ctx,
action,
node.arguments,
path
);
path.replaceWith(t.inherits(t.valueToNode(result), path.node));
}

// require('string')
if (
t.isIdentifier(node.callee, { name: "require" }) &&
node.arguments.length === 1 &&
t.isStringLiteral(node.arguments[0]) &&
ctx.map &&
ctx.map[node.arguments[0].value]
) {
const requireName = node.arguments[0];
requireName.value = requireName.raw = ctx.map[requireName.value];
}
},
BlockStatement: {
// Visit node in post-order so that recursive flattening
// of blocks works correctly.
exit(path) {
const { node } = path;

let 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 flattened
// into the parent one.
const subChildren = node.body[subExpressionIndex].body;
node.body.splice(subExpressionIndex, 1, ...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++;
}
},
},
Function: {
exit(path) {
if (!t.isBlockStatement(path.node.body)) {
// Arrow function with expression body
return;
}

const { body } = path.node.body;
if (
body.length > 0 &&
t.isReturnStatement(body.at(-1), { argument: null })
) {
// Function body ends with return without arg -- removing it.
body.pop();
}
},
},
},
};
}

function preprocessPDFJSCode(ctx, content) {
return transformSync(content, {
configFile: false,
plugins: [[babelPluginPDFJSPreprocessor, ctx]],
}).code;
}

export { babelPluginPDFJSPreprocessor, preprocessPDFJSCode };
16 changes: 8 additions & 8 deletions external/builder/fixtures_esprima/blocks-expected.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
function test() {
"test";
"1";
"2";
"3";
if ("test") {
"5";
}
"4";
"test";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: 'test'.
"1";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '1'.
"2";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '2'.
"3";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '3'.
if ("test") {
"5";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
"4";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
22 changes: 11 additions & 11 deletions external/builder/fixtures_esprima/comments-expected.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
function f1() {
"1";
"2";
"1";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '1'.
"2";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '2'.
}
function f2() {
"1";
"2";
"1";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '1'.
"2";

Check warning

Code scanning / CodeQL

Unknown directive Warning library

Unknown directive: '2'.
}
function f3() {
if ("1") {
"1";
}
"2";
if ("3") {
"4";
}
if ("1") {
"1";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
"2";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
if ("3") {
"4";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
}
2 changes: 1 addition & 1 deletion external/builder/fixtures_esprima/constants-expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ var i = true;
var j = false;
var k = false;
var l = true;
var m = '1' === true;
var m = false;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable m.
var n = false;
var o = true;
22 changes: 15 additions & 7 deletions external/builder/fixtures_esprima/deadcode-expected.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
function f1() {
}
function f1() {}

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused function f1.
function f2() {
return 1;
return 1;
}
function f3() {
var i = 0;
throw "test";
var i = 0;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable i.
throw "test";
}
function f4() {
var i = 0;
var i = 0;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable i.
}
var obj = {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable obj.
method1() {},
method2() {}
};
class C {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused class C.
method1() {}
method2() {}
}

var arrow1 = () => {};

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable arrow1.
var arrow2 = () => {};

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable arrow2.
12 changes: 12 additions & 0 deletions external/builder/fixtures_esprima/deadcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ function f4() {
var j = 0;
}

var obj = {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable obj.
method1() { return; var i = 0; },

Check warning

Code scanning / CodeQL

Unreachable statement Warning library

This statement is unreachable.
method2() { return; },
};

class C {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused class C.
method1() { return; var i = 0; }

Check warning

Code scanning / CodeQL

Unreachable statement Warning library

This statement is unreachable.
method2() { return; }
}

var arrow1 = () => { return; var i = 0; };

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable arrow1.

Check warning

Code scanning / CodeQL

Unreachable statement Warning library

This statement is unreachable.
var arrow2 = () => { return; };

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable arrow2.
16 changes: 11 additions & 5 deletions external/builder/fixtures_esprima/evals-expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ var b = true;
var c = true;
var d = false;
var e = true;
var f = 'text';
var f = "text";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable f.
var g = {
"obj": { "i": 1 },
"j": 2
obj: {
i: 1
},
j: 2
};
var h = {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable h.
test: "test"
};
var h = { 'test': 'test' };
var i = '0';
var j = { "i": 1 };
var j = {

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note library

Unused variable j.
i: 1
};
2 changes: 1 addition & 1 deletion external/builder/fixtures_esprima/evals.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ 'test': 'test' }
{ "test": "test" }
10 changes: 5 additions & 5 deletions external/builder/fixtures_esprima/ifs-expected.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
if ('test') {
"1";
"1";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
{
"1";
"1";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
{
"1";
"1";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
;
{
"2";
"2";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
;
if ('1') {
"1";
"1";

Check warning

Code scanning / CodeQL

Expression has no effect Warning library

This expression has no effect.
}
Loading

0 comments on commit 610ebcb

Please sign in to comment.