-
Notifications
You must be signed in to change notification settings - Fork 10k
Commit
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
There are no files selected for viewing
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 }; |
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.
|
||
} |
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.
|
||
} | ||
} |
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.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
{ 'test': 'test' } | ||
{ "test": "test" } |
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.
|
||
} |