-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathesbuild-plugin-dead-code-elimination.mjs
More file actions
156 lines (142 loc) · 4.51 KB
/
esbuild-plugin-dead-code-elimination.mjs
File metadata and controls
156 lines (142 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
* @fileoverview esbuild plugin for dead code elimination.
*
* Removes unreachable code branches like `if (false) { ... }` and `if (true) { } else { ... }`.
* Uses Babel parser + magic-string for safe AST-based transformations.
*
* @example
* import { deadCodeEliminationPlugin } from 'build-infra/lib/esbuild-plugin-dead-code-elimination'
*
* export default {
* plugins: [deadCodeEliminationPlugin()],
* }
*/
import { parse } from '@babel/parser'
import { default as traverseImport } from '@babel/traverse'
import MagicString from 'magic-string'
const traverse =
typeof traverseImport === 'function' ? traverseImport : traverseImport.default
/**
* Evaluate a test expression to determine if it's a constant boolean.
*
* @param {import('@babel/types').Node} test - Test expression node
* @returns {boolean | null} true/false if constant, null if dynamic
*/
function evaluateTest(test) {
if (test.type === 'BooleanLiteral') {
return test.value
}
if (test.type === 'UnaryExpression' && test.operator === '!') {
const argValue = evaluateTest(test.argument)
return argValue !== null ? !argValue : null
}
return null
}
/**
* Remove dead code branches from JavaScript code.
*
* @param {string} code - JavaScript code to transform
* @returns {string} Transformed code with dead branches removed
*/
function removeDeadCode(code) {
const ast = parse(code, {
sourceType: 'module',
plugins: [],
})
const s = new MagicString(code)
const nodesToRemove = []
traverse(ast, {
IfStatement(path) {
const testValue = evaluateTest(path.node.test)
if (testValue === false) {
// if (false) { ... } [else { ... }].
// Remove entire if statement, keep else block if present.
if (path.node.alternate) {
// Replace if statement with else block content.
const { alternate } = path.node
if (alternate.type === 'BlockStatement') {
// Remove braces from else block.
const start = alternate.start + 1
const end = alternate.end - 1
const elseContent = code.slice(start, end)
nodesToRemove.push({
start: path.node.start,
end: path.node.end,
replacement: elseContent,
})
} else {
// Single statement else.
nodesToRemove.push({
start: path.node.start,
end: path.node.end,
replacement: code.slice(alternate.start, alternate.end),
})
}
} else {
// No else block, remove entire if statement.
nodesToRemove.push({
start: path.node.start,
end: path.node.end,
replacement: '',
})
}
} else if (testValue === true) {
// if (true) { ... } [else { ... }].
// Keep consequent, remove else block.
const { consequent } = path.node
if (consequent.type === 'BlockStatement') {
// Remove braces from consequent block.
const start = consequent.start + 1
const end = consequent.end - 1
const consequentContent = code.slice(start, end)
nodesToRemove.push({
start: path.node.start,
end: path.node.end,
replacement: consequentContent,
})
} else {
// Single statement consequent.
nodesToRemove.push({
start: path.node.start,
end: path.node.end,
replacement: code.slice(consequent.start, consequent.end),
})
}
}
},
})
// Apply replacements in reverse order to maintain correct positions.
for (const node of nodesToRemove.reverse()) {
s.overwrite(node.start, node.end, node.replacement)
}
return s.toString()
}
/**
* Create esbuild plugin for dead code elimination.
*
* @returns {import('esbuild').Plugin} esbuild plugin
*/
export function deadCodeEliminationPlugin() {
return {
name: 'dead-code-elimination',
setup(build) {
build.onEnd(result => {
const outputs = result.outputFiles
if (!outputs || !outputs.length) {
return
}
for (const output of outputs) {
// Only process JavaScript files.
if (!output.path.endsWith('.js')) {
continue
}
let content = output.text
// Remove dead code branches.
content = removeDeadCode(content)
// Update the output content.
output.contents = Buffer.from(content, 'utf8')
}
})
},
}
}