webfuscator 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +0 -2
  2. package/dist/analysis/constant.js +3 -3
  3. package/dist/analysis/constant.js.map +1 -1
  4. package/dist/analysis/inlineability.js +2 -2
  5. package/dist/analysis/inlineability.js.map +1 -1
  6. package/dist/analysis/purity.js +1 -1
  7. package/dist/analysis/purity.js.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/obfuscator.d.ts.map +1 -1
  12. package/dist/obfuscator.js +14 -0
  13. package/dist/obfuscator.js.map +1 -1
  14. package/dist/options.d.ts +14 -2
  15. package/dist/options.d.ts.map +1 -1
  16. package/dist/preparation/expand-destructuring.js +4 -3
  17. package/dist/preparation/expand-destructuring.js.map +1 -1
  18. package/dist/preparation/expand-template-literals.js +3 -2
  19. package/dist/preparation/expand-template-literals.js.map +1 -1
  20. package/dist/preparation/split-sequence-expressions.js +1 -1
  21. package/dist/preparation/split-sequence-expressions.js.map +1 -1
  22. package/dist/preparation/split-variable-declarations.js +1 -1
  23. package/dist/preparation/split-variable-declarations.js.map +1 -1
  24. package/dist/preparation/stringify-object-keys.js +1 -1
  25. package/dist/preparation/stringify-object-keys.js.map +1 -1
  26. package/dist/preparation/wrap-single-statements.js +2 -2
  27. package/dist/preparation/wrap-single-statements.js.map +1 -1
  28. package/dist/transforms/arguments-to-parameters.js +1 -1
  29. package/dist/transforms/arguments-to-parameters.js.map +1 -1
  30. package/dist/transforms/arrow-to-function.js.map +1 -1
  31. package/dist/transforms/collapse-single-use-temps.js +7 -7
  32. package/dist/transforms/collapse-single-use-temps.js.map +1 -1
  33. package/dist/transforms/const-let-to-var.js +3 -3
  34. package/dist/transforms/const-let-to-var.js.map +1 -1
  35. package/dist/transforms/const-to-let.js.map +1 -1
  36. package/dist/transforms/do-while-to-while.js +3 -3
  37. package/dist/transforms/do-while-to-while.js.map +1 -1
  38. package/dist/transforms/drop-console.js +4 -3
  39. package/dist/transforms/drop-console.js.map +1 -1
  40. package/dist/transforms/drop-debugger.js.map +1 -1
  41. package/dist/transforms/expand-binary-assignment.js +5 -3
  42. package/dist/transforms/expand-binary-assignment.js.map +1 -1
  43. package/dist/transforms/expand-logical-assignment.js +5 -3
  44. package/dist/transforms/expand-logical-assignment.js.map +1 -1
  45. package/dist/transforms/extract-object-properties.js +7 -7
  46. package/dist/transforms/extract-object-properties.js.map +1 -1
  47. package/dist/transforms/fold-builtin-methods.js +16 -15
  48. package/dist/transforms/fold-builtin-methods.js.map +1 -1
  49. package/dist/transforms/fold-constants.js +2 -2
  50. package/dist/transforms/fold-constants.js.map +1 -1
  51. package/dist/transforms/for-to-while.js +5 -5
  52. package/dist/transforms/for-to-while.js.map +1 -1
  53. package/dist/transforms/function-declaration-to-expression.js +3 -3
  54. package/dist/transforms/function-declaration-to-expression.js.map +1 -1
  55. package/dist/transforms/inline-functions.js +7 -7
  56. package/dist/transforms/inline-functions.js.map +1 -1
  57. package/dist/transforms/logical-to-ternary.js +2 -2
  58. package/dist/transforms/logical-to-ternary.js.map +1 -1
  59. package/dist/transforms/mangle-properties.d.ts.map +1 -1
  60. package/dist/transforms/mangle-properties.js +35 -86
  61. package/dist/transforms/mangle-properties.js.map +1 -1
  62. package/dist/transforms/nullish-coalescing-to-ternary.js +2 -2
  63. package/dist/transforms/nullish-coalescing-to-ternary.js.map +1 -1
  64. package/dist/transforms/numbers-to-strings.js +2 -2
  65. package/dist/transforms/numbers-to-strings.js.map +1 -1
  66. package/dist/transforms/object-method-to-property.js +1 -1
  67. package/dist/transforms/object-method-to-property.js.map +1 -1
  68. package/dist/transforms/optional-chaining-to-ternary.js +7 -4
  69. package/dist/transforms/optional-chaining-to-ternary.js.map +1 -1
  70. package/dist/transforms/pack-declarations-into-parameters.js +2 -2
  71. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
  72. package/dist/transforms/pack.d.ts +33 -0
  73. package/dist/transforms/pack.d.ts.map +1 -0
  74. package/dist/transforms/pack.js +265 -0
  75. package/dist/transforms/pack.js.map +1 -0
  76. package/dist/transforms/remove-anonymous-function-names.js +3 -3
  77. package/dist/transforms/remove-anonymous-function-names.js.map +1 -1
  78. package/dist/transforms/remove-unreachable-code.js +1 -1
  79. package/dist/transforms/remove-unreachable-code.js.map +1 -1
  80. package/dist/transforms/remove-unused-code.js +4 -4
  81. package/dist/transforms/remove-unused-code.js.map +1 -1
  82. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  83. package/dist/transforms/rename-identifiers.js +4 -3
  84. package/dist/transforms/rename-identifiers.js.map +1 -1
  85. package/dist/transforms/specials-to-strings.d.ts.map +1 -1
  86. package/dist/transforms/specials-to-strings.js +2 -2
  87. package/dist/transforms/specials-to-strings.js.map +1 -1
  88. package/dist/transforms/switch-to-if.js +7 -7
  89. package/dist/transforms/switch-to-if.js.map +1 -1
  90. package/dist/transforms/ternary-to-if.js +7 -4
  91. package/dist/transforms/ternary-to-if.js.map +1 -1
  92. package/dist/transforms/update-to-assignment.js +4 -2
  93. package/dist/transforms/update-to-assignment.js.map +1 -1
  94. package/dist/transforms/yodify-conditions.js +3 -3
  95. package/dist/transforms/yodify-conditions.js.map +1 -1
  96. package/dist/utils/ast.d.ts.map +1 -1
  97. package/dist/utils/ast.js +7 -2
  98. package/dist/utils/ast.js.map +1 -1
  99. package/dist/utils/evaluation-order.js.map +1 -1
  100. package/dist/utils/logical-lowering.js +2 -2
  101. package/dist/utils/logical-lowering.js.map +1 -1
  102. package/dist/utils/loop-lowering.js +1 -1
  103. package/dist/utils/loop-lowering.js.map +1 -1
  104. package/dist/utils/string-generator.js +1 -1
  105. package/dist/utils/string-generator.js.map +1 -1
  106. package/package.json +11 -10
  107. package/src/analysis/constant.ts +4 -4
  108. package/src/analysis/inlineability.ts +4 -4
  109. package/src/analysis/purity.ts +3 -3
  110. package/src/index.ts +9 -2
  111. package/src/obfuscator.ts +57 -41
  112. package/src/options.ts +16 -2
  113. package/src/preparation/expand-destructuring.ts +5 -4
  114. package/src/preparation/expand-template-literals.ts +4 -3
  115. package/src/preparation/split-sequence-expressions.ts +2 -2
  116. package/src/preparation/split-variable-declarations.ts +1 -1
  117. package/src/preparation/stringify-object-keys.ts +2 -2
  118. package/src/preparation/wrap-single-statements.ts +2 -2
  119. package/src/transforms/arguments-to-parameters.ts +5 -5
  120. package/src/transforms/arrow-to-function.ts +3 -3
  121. package/src/transforms/collapse-single-use-temps.ts +14 -14
  122. package/src/transforms/const-let-to-var.ts +6 -6
  123. package/src/transforms/const-to-let.ts +4 -4
  124. package/src/transforms/do-while-to-while.ts +6 -6
  125. package/src/transforms/drop-console.ts +8 -7
  126. package/src/transforms/drop-debugger.ts +2 -2
  127. package/src/transforms/expand-binary-assignment.ts +8 -6
  128. package/src/transforms/expand-logical-assignment.ts +8 -6
  129. package/src/transforms/extract-object-properties.ts +12 -12
  130. package/src/transforms/fold-builtin-methods.ts +20 -19
  131. package/src/transforms/fold-constants.ts +8 -8
  132. package/src/transforms/for-to-while.ts +8 -8
  133. package/src/transforms/function-declaration-to-expression.ts +5 -5
  134. package/src/transforms/inline-functions.ts +14 -14
  135. package/src/transforms/logical-to-ternary.ts +7 -7
  136. package/src/transforms/mangle-properties.ts +43 -97
  137. package/src/transforms/nullish-coalescing-to-ternary.ts +6 -6
  138. package/src/transforms/numbers-to-strings.ts +4 -4
  139. package/src/transforms/object-method-to-property.ts +4 -4
  140. package/src/transforms/optional-chaining-to-ternary.ts +10 -7
  141. package/src/transforms/pack-declarations-into-parameters.ts +7 -7
  142. package/src/transforms/pack.ts +340 -0
  143. package/src/transforms/remove-anonymous-function-names.ts +5 -5
  144. package/src/transforms/remove-unreachable-code.ts +3 -3
  145. package/src/transforms/remove-unused-code.ts +8 -8
  146. package/src/transforms/rename-identifiers.ts +9 -8
  147. package/src/transforms/specials-to-strings.ts +7 -7
  148. package/src/transforms/switch-to-if.ts +12 -12
  149. package/src/transforms/ternary-to-if.ts +12 -9
  150. package/src/transforms/update-to-assignment.ts +7 -5
  151. package/src/transforms/yodify-conditions.ts +6 -6
  152. package/src/utils/ast.ts +9 -2
  153. package/src/utils/evaluation-order.ts +1 -1
  154. package/src/utils/logical-lowering.ts +3 -3
  155. package/src/utils/loop-lowering.ts +1 -1
  156. package/src/utils/string-generator.ts +1 -1
@@ -0,0 +1,340 @@
1
+ import generate from '@babel/generator'
2
+ import traverse from '@babel/traverse'
3
+ import type { NodePath, Visitor } from '@babel/traverse'
4
+ import * as t from '@babel/types'
5
+ import type { File } from '@babel/types'
6
+
7
+ import type { TransformContext } from '../options'
8
+ import { isInsideWith } from '../utils/ast'
9
+ import { isCalleeOrTagOf, isInStrictContext, referencesOrWritesVariable } from '../utils/paths'
10
+ import { mulberry32 } from '../utils/random'
11
+ import { StringGenerator } from '../utils/string-generator'
12
+
13
+ /**
14
+ * Serializes the whole program to a string and rebuilds it at runtime through
15
+ * the `Function` constructor. A detached function body only reaches true globals
16
+ * and its own parameters. A free name might instead be a CommonJS `require`, a
17
+ * bundler-injected binding, or a live import, none reachable from the body and
18
+ * none distinguishable from a global at build time, so free names are routed
19
+ * through a parameter object whose accessors run in the original top-level scope
20
+ * and resolve each name there. Import declarations return above the call. `eval`
21
+ * is left un-routed so a direct eval still reads the body scope. Names inside a
22
+ * `with` stay un-routed too, since they may resolve against the with object at
23
+ * runtime. Export statements cannot live inside a Function body, so pack throws
24
+ * on them. It packs everything else as written. Code that needs `import.meta`,
25
+ * top-level await, dynamic `import()`, or a top-level `arguments` will not run
26
+ * once moved into a Function body.
27
+ *
28
+ * @example
29
+ * // ◀️ before
30
+ * console.log(value);
31
+ *
32
+ * // ▶️ after
33
+ * Function("M", "return M[\"b\"].log(M[\"_\"]);")({
34
+ * get "b"() {
35
+ * return console;
36
+ * },
37
+ * get "_"() {
38
+ * return value;
39
+ * }
40
+ * });
41
+ */
42
+ export function pack(ast: File, ctx: TransformContext): boolean {
43
+ let programPath: NodePath<t.Program> | null = null
44
+ traverse(ast, {
45
+ Program(path) {
46
+ programPath = path
47
+ path.stop()
48
+ },
49
+ })
50
+ if (!programPath) {
51
+ return false
52
+ }
53
+ return packProgram(programPath, ctx)
54
+ }
55
+
56
+ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): boolean {
57
+ assertNoExports(programPath)
58
+ const importPaths = collectImportDeclarations(programPath)
59
+ const importDeclarations = importPaths.map((path) => path.node)
60
+ // An import named `Function` shadows the constructor at the output top level,
61
+ // so the wrap reaches the real one through a function instance instead.
62
+ const shadowedConstructor = importDeclarations.some((declaration) =>
63
+ bindsFunctionConstructor(declaration),
64
+ )
65
+
66
+ const programNode = programPath.node
67
+ const wasModule = programNode.sourceType === 'module'
68
+ // Escaping strict is a deliberate opt-in: drop the directive so the packed
69
+ // body runs sloppy even when the source was strict.
70
+ const escapeStrict = ctx.pack?.escapeStrict ?? false
71
+ const bodyStrict = !escapeStrict && isInStrictContext(programPath)
72
+
73
+ // Detaching imports drops their bindings, so their names read as free and are
74
+ // routed like any other global. The declarations return at the output top
75
+ // level, where the accessors close over them.
76
+ for (const path of importPaths) {
77
+ path.remove()
78
+ }
79
+ programPath.scope.crawl()
80
+
81
+ const usedNames = collectUsedNames(programNode, importDeclarations)
82
+ const generator = new StringGenerator(ctx.stringGeneratorMode, mulberry32(ctx.seed))
83
+ const freshIdentifier = (): string => {
84
+ let name: string
85
+ do {
86
+ name = generator.next()
87
+ } while (usedNames.has(name))
88
+ usedNames.add(name)
89
+ return name
90
+ }
91
+ const objectName = freshIdentifier()
92
+ const setterParam = freshIdentifier()
93
+
94
+ const readProperties = new Map<string, string>()
95
+ const typeofProperties = new Map<string, string>()
96
+ const settable = new Set<string>()
97
+ const propertyFor = (registry: Map<string, string>, name: string): string => {
98
+ let property = registry.get(name)
99
+ if (property === undefined) {
100
+ do {
101
+ // `__proto__` as an object-literal key would set the prototype rather
102
+ // than name an accessor.
103
+ property = generator.next()
104
+ } while (property === '__proto__')
105
+ registry.set(name, property)
106
+ }
107
+ return property
108
+ }
109
+ const memberFor = (property: string): t.MemberExpression =>
110
+ t.memberExpression(t.identifier(objectName), t.stringLiteral(property), true)
111
+
112
+ const isRoutable = (path: NodePath<t.Identifier>): boolean => {
113
+ const name = path.node.name
114
+ // The wrapper parameter stays as written. `arguments` and `eval` are never
115
+ // routed: `arguments` binds to its own function's object, and leaving `eval`
116
+ // bare keeps a direct eval reading the body scope rather than the global one.
117
+ if (name === objectName || name === 'arguments' || name === 'eval') {
118
+ return false
119
+ }
120
+ // Inside `with`, a free name may resolve to a property of the with object at
121
+ // runtime, so routing it to the top-level accessor would change behavior.
122
+ if (isInsideWith(path)) {
123
+ return false
124
+ }
125
+ if (!referencesOrWritesVariable(path)) {
126
+ return false
127
+ }
128
+ return !path.scope.getBinding(name)
129
+ }
130
+
131
+ const routingVisitor: Visitor = {
132
+ UnaryExpression(path) {
133
+ if (path.node.operator !== 'typeof') {
134
+ return
135
+ }
136
+ const argument = path.get('argument')
137
+ // A bare `typeof name` must not throw for an undeclared global, so it maps
138
+ // to an accessor that applies `typeof` in the original scope.
139
+ if (!argument.isIdentifier() || !isRoutable(argument)) {
140
+ return
141
+ }
142
+ path.replaceWith(memberFor(propertyFor(typeofProperties, argument.node.name)))
143
+ },
144
+ Identifier(path) {
145
+ // `delete name` must keep hitting the global binding; `delete obj[prop]`
146
+ // would drop the accessor instead. A real global stays reachable bare.
147
+ if (isBareDeleteArgument(path) || !isRoutable(path)) {
148
+ return
149
+ }
150
+ const name = path.node.name
151
+ if (isWriteReference(path)) {
152
+ settable.add(name)
153
+ }
154
+ const member = memberFor(propertyFor(readProperties, name))
155
+ // A member callee would bind `this` to the object; the comma discards the
156
+ // base and keeps the original receiver.
157
+ path.replaceWith(
158
+ isCalleeOrTagOf(path) ? t.sequenceExpression([t.numericLiteral(0), member]) : member,
159
+ )
160
+ },
161
+ }
162
+ programPath.traverse(routingVisitor)
163
+
164
+ // The script completion value is its trailing expression; returning it keeps
165
+ // `eval` of the packed output producing the same value.
166
+ const body = programNode.body
167
+ const lastStatement = body.at(-1)
168
+ if (lastStatement && t.isExpressionStatement(lastStatement)) {
169
+ body[body.length - 1] = t.returnStatement(lastStatement.expression)
170
+ }
171
+
172
+ const innerDirectives = bodyStrict ? [t.directive(t.directiveLiteral('use strict'))] : []
173
+ const innerCode = generate(t.program(body, innerDirectives, 'script'), {
174
+ minified: true,
175
+ comments: false,
176
+ }).code
177
+
178
+ const objectExpression = buildAccessorObject({
179
+ readProperties,
180
+ typeofProperties,
181
+ settable,
182
+ setterParam,
183
+ strict: bodyStrict,
184
+ })
185
+
186
+ const constructorReference = shadowedConstructor
187
+ ? t.memberExpression(
188
+ t.functionExpression(null, [], t.blockStatement([])),
189
+ t.identifier('constructor'),
190
+ )
191
+ : t.identifier('Function')
192
+ const wrapper = t.callExpression(constructorReference, [
193
+ t.stringLiteral(objectName),
194
+ t.stringLiteral(innerCode),
195
+ ])
196
+ // A strict script keeps top-level `this` bound to the global object. A plainly
197
+ // called strict function would instead see `undefined`, so pass the global
198
+ // `this` of the sloppy output script.
199
+ const invocation =
200
+ bodyStrict && !wasModule
201
+ ? t.callExpression(t.memberExpression(wrapper, t.identifier('call')), [
202
+ t.thisExpression(),
203
+ objectExpression,
204
+ ])
205
+ : t.callExpression(wrapper, [objectExpression])
206
+
207
+ programNode.body = [...importDeclarations, t.expressionStatement(invocation)]
208
+ programNode.directives = []
209
+ programNode.sourceType = importDeclarations.length > 0 ? 'module' : 'script'
210
+ return true
211
+ }
212
+
213
+ // Export statements are module-only syntax and cannot exist inside a Function
214
+ // body. Pack throws rather than emit output that fails to parse at runtime.
215
+ function assertNoExports(programPath: NodePath<t.Program>): void {
216
+ const hasExport = programPath.node.body.some(
217
+ (statement) =>
218
+ t.isExportNamedDeclaration(statement) ||
219
+ t.isExportDefaultDeclaration(statement) ||
220
+ t.isExportAllDeclaration(statement),
221
+ )
222
+ if (hasExport) {
223
+ throw new Error('pack cannot wrap a program that uses export statements')
224
+ }
225
+ }
226
+
227
+ function collectImportDeclarations(
228
+ programPath: NodePath<t.Program>,
229
+ ): NodePath<t.ImportDeclaration>[] {
230
+ return programPath
231
+ .get('body')
232
+ .filter((statement): statement is NodePath<t.ImportDeclaration> =>
233
+ statement.isImportDeclaration(),
234
+ )
235
+ }
236
+
237
+ function bindsFunctionConstructor(declaration: t.ImportDeclaration): boolean {
238
+ return declaration.specifiers.some((specifier) => specifier.local.name === 'Function')
239
+ }
240
+
241
+ function isBareDeleteArgument(path: NodePath<t.Identifier>): boolean {
242
+ const parent = path.parentPath
243
+ return (
244
+ parent != null &&
245
+ parent.isUnaryExpression({ operator: 'delete' }) &&
246
+ parent.node.argument === path.node
247
+ )
248
+ }
249
+
250
+ function isWriteReference(path: NodePath<t.Identifier>): boolean {
251
+ const parent = path.parentPath
252
+ if (!parent) {
253
+ return false
254
+ }
255
+ const node = path.node
256
+ if (parent.isAssignmentExpression() && parent.node.left === node) {
257
+ return true
258
+ }
259
+ if (parent.isUpdateExpression() && parent.node.argument === node) {
260
+ return true
261
+ }
262
+ if ((parent.isForInStatement() || parent.isForOfStatement()) && parent.node.left === node) {
263
+ return true
264
+ }
265
+ // Destructuring assignment targets bind through patterns Babel flags here.
266
+ return path.isBindingIdentifier()
267
+ }
268
+
269
+ function collectUsedNames(
270
+ program: t.Program,
271
+ importDeclarations: t.ImportDeclaration[],
272
+ ): Set<string> {
273
+ // The wrapper parameter and setter parameter must avoid every name the body
274
+ // uses, so no nested binding can shadow their accesses.
275
+ const names = new Set<string>(['Function'])
276
+ for (const declaration of importDeclarations) {
277
+ for (const specifier of declaration.specifiers) {
278
+ names.add(specifier.local.name)
279
+ }
280
+ }
281
+ t.traverseFast(program, (node) => {
282
+ if (t.isIdentifier(node)) {
283
+ names.add(node.name)
284
+ }
285
+ })
286
+ return names
287
+ }
288
+
289
+ interface AccessorObjectSpec {
290
+ readProperties: Map<string, string>
291
+ setterParam: string
292
+ settable: Set<string>
293
+ strict: boolean
294
+ typeofProperties: Map<string, string>
295
+ }
296
+
297
+ function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
298
+ const properties: t.ObjectMethod[] = []
299
+ for (const [name, property] of spec.readProperties) {
300
+ properties.push(
301
+ t.objectMethod(
302
+ 'get',
303
+ t.stringLiteral(property),
304
+ [],
305
+ t.blockStatement([t.returnStatement(t.identifier(name))]),
306
+ ),
307
+ )
308
+ if (spec.settable.has(name)) {
309
+ const setterBody = t.blockStatement([
310
+ t.expressionStatement(
311
+ t.assignmentExpression('=', t.identifier(name), t.identifier(spec.setterParam)),
312
+ ),
313
+ ])
314
+ // A strict script becomes a sloppy output script, so the setter stays
315
+ // strict to keep an undeclared-global write throwing.
316
+ if (spec.strict) {
317
+ setterBody.directives.push(t.directive(t.directiveLiteral('use strict')))
318
+ }
319
+ properties.push(
320
+ t.objectMethod(
321
+ 'set',
322
+ t.stringLiteral(property),
323
+ [t.identifier(spec.setterParam)],
324
+ setterBody,
325
+ ),
326
+ )
327
+ }
328
+ }
329
+ for (const [name, property] of spec.typeofProperties) {
330
+ properties.push(
331
+ t.objectMethod(
332
+ 'get',
333
+ t.stringLiteral(property),
334
+ [],
335
+ t.blockStatement([t.returnStatement(t.unaryExpression('typeof', t.identifier(name)))]),
336
+ ),
337
+ )
338
+ }
339
+ return t.objectExpression(properties)
340
+ }
@@ -2,8 +2,8 @@ import type { NodePath, Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
- import { traverseForChanges } from 'src/utils/change-tracking'
6
- import type { ChangeState } from 'src/utils/change-tracking'
5
+ import { traverseForChanges } from '../utils/change-tracking'
6
+ import type { ChangeState } from '../utils/change-tracking'
7
7
 
8
8
  /**
9
9
  * Removes a function expression's name when the body does not use its binding
@@ -25,7 +25,7 @@ export function removeAnonymousFunctionNames(ast: File): boolean {
25
25
 
26
26
  const visitor: Visitor<ChangeState> = {
27
27
  FunctionExpression(path, state) {
28
- const { id } = path.node
28
+ const id = path.node.id
29
29
  if (!id) {
30
30
  return
31
31
  }
@@ -48,8 +48,8 @@ const visitor: Visitor<ChangeState> = {
48
48
 
49
49
  // No inferred name means `.name` would become empty.
50
50
  function inferredName(path: NodePath<t.FunctionExpression>): string | null {
51
- const { node } = path
52
- const { parentPath } = path
51
+ const node = path.node
52
+ const parentPath = path.parentPath
53
53
  if (!parentPath) {
54
54
  return null
55
55
  }
@@ -2,8 +2,8 @@ import type { NodePath, Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
- import { traverseForChanges } from 'src/utils/change-tracking'
6
- import type { ChangeState } from 'src/utils/change-tracking'
5
+ import { traverseForChanges } from '../utils/change-tracking'
6
+ import type { ChangeState } from '../utils/change-tracking'
7
7
 
8
8
  /**
9
9
  * Removes statements after unconditional control flow in the same statement list.
@@ -72,7 +72,7 @@ function isCompletion(node: t.Node): boolean {
72
72
  // Hoisted bindings and referenced lexical TDZ remain observable from reachable
73
73
  // code. An unreferenced lexical declaration is inert.
74
74
  function isSafeToRemoveAfterCompletion(path: NodePath<t.Statement>): boolean {
75
- const { node } = path
75
+ const node = path.node
76
76
  if (t.isFunctionDeclaration(node) || (t.isVariableDeclaration(node) && node.kind === 'var')) {
77
77
  return false
78
78
  }
@@ -3,10 +3,10 @@ import type { NodePath, Scope, Visitor } from '@babel/traverse'
3
3
  import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
- import { isSideEffectFree } from 'src/analysis/purity'
7
- import { isDirectEvalCall } from 'src/utils/ast'
8
- import type { ChangeState } from 'src/utils/change-tracking'
9
- import { hasAnnexBFunctionAlias } from 'src/utils/paths'
6
+ import { isSideEffectFree } from '../analysis/purity'
7
+ import { isDirectEvalCall } from '../utils/ast'
8
+ import type { ChangeState } from '../utils/change-tracking'
9
+ import { hasAnnexBFunctionAlias } from '../utils/paths'
10
10
 
11
11
  /**
12
12
  * Removes unused declarations and expression statements that have no effects.
@@ -150,12 +150,12 @@ function tryRemoveDeclarator(
150
150
  return false
151
151
  }
152
152
 
153
- const { id } = path.node
153
+ const id = path.node.id
154
154
  if (!t.isIdentifier(id)) {
155
155
  return false
156
156
  }
157
157
 
158
- const { init } = path.node
158
+ const init = path.node.init
159
159
  if (init && !isSideEffectFree(init, path.scope, path.get('init') as NodePath)) {
160
160
  return false
161
161
  }
@@ -196,7 +196,7 @@ function tryRemoveFunctionDecl(
196
196
  if (hasAnnexBFunctionAlias(path)) {
197
197
  return false
198
198
  }
199
- const { id } = path.node
199
+ const id = path.node.id
200
200
  if (!id) {
201
201
  return false
202
202
  }
@@ -228,7 +228,7 @@ function tryRemoveClassDecl(
228
228
  if (isExported(path)) {
229
229
  return false
230
230
  }
231
- const { id } = path.node
231
+ const id = path.node.id
232
232
  if (!id) {
233
233
  return false
234
234
  }
@@ -3,11 +3,11 @@ import type { Binding, Scope } from '@babel/traverse'
3
3
  import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
- import type { TransformContext } from 'src/options'
7
- import { isDirectEvalCall, isInsideWith } from 'src/utils/ast'
8
- import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from 'src/utils/paths'
9
- import { mulberry32 } from 'src/utils/random'
10
- import { StringGenerator } from 'src/utils/string-generator'
6
+ import type { TransformContext } from '../options'
7
+ import { isDirectEvalCall, isInsideWith } from '../utils/ast'
8
+ import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from '../utils/paths'
9
+ import { mulberry32 } from '../utils/random'
10
+ import { StringGenerator } from '../utils/string-generator'
11
11
 
12
12
  /**
13
13
  * Renames bindings and labels with a fresh `StringGenerator`. Each scope avoids
@@ -125,7 +125,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
125
125
 
126
126
  traverse(ast, {
127
127
  Scopable(path) {
128
- const { scope } = path
128
+ const scope = path.scope
129
129
  // Test global reservations during allocation to avoid copying them per scope.
130
130
  const offLimits = new Set<string>()
131
131
  const live = liveAncestors.get(scope)
@@ -174,7 +174,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
174
174
 
175
175
  traverse(ast, {
176
176
  Identifier(path) {
177
- const { node } = path
177
+ const node = path.node
178
178
  const directName = idToName.get(node)
179
179
  if (directName !== undefined) {
180
180
  node.name = directName
@@ -200,7 +200,8 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
200
200
  }
201
201
 
202
202
  function isNonBindingPosition(path: { node: t.Identifier; parent: t.Node }): boolean {
203
- const { node, parent } = path
203
+ const node = path.node
204
+ const parent = path.parent
204
205
  // A PrivateName id is not a variable and must match its class declaration.
205
206
  if (t.isPrivateName(parent)) {
206
207
  return true
@@ -3,11 +3,11 @@ import type { NodePath, Visitor } from '@babel/traverse'
3
3
  import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
- import type { TransformContext } from 'src/options'
7
- import { enclosingScopeHasDirectEval, isInsideWith } from 'src/utils/ast'
8
- import type { ChangeState } from 'src/utils/change-tracking'
9
- import { mulberry32 } from 'src/utils/random'
10
- import { StringGenerator } from 'src/utils/string-generator'
6
+ import type { TransformContext } from '../options'
7
+ import { enclosingScopeHasDirectEval, isInsideWith } from '../utils/ast'
8
+ import type { ChangeState } from '../utils/change-tracking'
9
+ import { mulberry32 } from '../utils/random'
10
+ import { StringGenerator } from '../utils/string-generator'
11
11
 
12
12
  /**
13
13
  * Rewrites five constant-like values through strings for later string mangling:
@@ -59,7 +59,7 @@ const visitor: Visitor<State> = {
59
59
  state.changed = true
60
60
  },
61
61
  Identifier(path, state) {
62
- const { name } = path.node
62
+ const name = path.node.name
63
63
  if (!GLOBAL_SPECIALS.has(name)) {
64
64
  return
65
65
  }
@@ -93,7 +93,7 @@ function buildGlobalReplacement(name: string, str: t.StringLiteral): t.Expressio
93
93
  // Babel calls update and iterator targets references, but replacements there
94
94
  // would not be assignable. Delete also distinguishes references from values.
95
95
  function isWriteTarget(path: NodePath<t.Identifier>): boolean {
96
- const { parentPath } = path
96
+ const parentPath = path.parentPath
97
97
  if (!parentPath) {
98
98
  return false
99
99
  }
@@ -2,11 +2,11 @@ import type { NodePath, Scope, Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
- import { isPure } from 'src/analysis/purity'
6
- import { traverseForChanges } from 'src/utils/change-tracking'
7
- import type { ChangeState } from 'src/utils/change-tracking'
8
- import { isPrimitiveLiteral } from 'src/utils/literal'
9
- import { generateLoopLabel } from 'src/utils/loop-lowering'
5
+ import { isPure } from '../analysis/purity'
6
+ import { traverseForChanges } from '../utils/change-tracking'
7
+ import type { ChangeState } from '../utils/change-tracking'
8
+ import { isPrimitiveLiteral } from '../utils/literal'
9
+ import { generateLoopLabel } from '../utils/loop-lowering'
10
10
 
11
11
  /**
12
12
  * Lowers `switch` to an `if` chain or, when cases fall through, a labeled block
@@ -46,7 +46,7 @@ const visitor: Visitor<ChangeState> = {
46
46
  }
47
47
 
48
48
  function transformSwitch(path: NodePath<t.SwitchStatement>): boolean {
49
- const { node } = path
49
+ const node = path.node
50
50
  // Separate `if` blocks cannot preserve cross-case lexical scope or Annex B
51
51
  // function hoisting.
52
52
  if (hasBlockScopedDeclaration(node)) {
@@ -114,7 +114,7 @@ function canLowerToIfElseChain(node: t.SwitchStatement): boolean {
114
114
  }
115
115
 
116
116
  function caseEligibleForIfElseChain(caseClause: t.SwitchCase, isLast: boolean): boolean {
117
- const { consequent } = caseClause
117
+ const consequent = caseClause.consequent
118
118
  if (consequent.length === 0) {
119
119
  return isLast
120
120
  }
@@ -189,8 +189,8 @@ function buildBody(consequent: t.Statement[]): t.BlockStatement {
189
189
  }
190
190
 
191
191
  function lowerToIfElseChain(path: NodePath<t.SwitchStatement>): void {
192
- const { node } = path
193
- const { scope } = path
192
+ const node = path.node
193
+ const scope = path.scope
194
194
 
195
195
  let discriminantExpr: t.Expression
196
196
  const preStmts: t.Statement[] = []
@@ -244,8 +244,8 @@ function lowerToIfElseChain(path: NodePath<t.SwitchStatement>): void {
244
244
  }
245
245
 
246
246
  function lowerToLabeledBlock(path: NodePath<t.SwitchStatement>): void {
247
- const { node } = path
248
- const { scope } = path
247
+ const node = path.node
248
+ const scope = path.scope
249
249
 
250
250
  // `scope.generateUid` alone can collide with enclosing or nested labels.
251
251
  const labelName = generateLoopLabel(path, 'switch')
@@ -314,7 +314,7 @@ function lowerToLabeledBlock(path: NodePath<t.SwitchStatement>): void {
314
314
 
315
315
  const labeledBlock = t.labeledStatement(t.identifier(labelName), t.blockStatement(statements))
316
316
 
317
- const [labeledPath] = path.replaceWith(labeledBlock)
317
+ const labeledPath = path.replaceWith(labeledBlock)[0]
318
318
  rewriteUnlabeledBreaks(labeledPath, labelName)
319
319
  }
320
320
 
@@ -2,11 +2,11 @@ import type { NodePath, Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
- import { isInsideWith } from 'src/utils/ast'
6
- import { traverseForChanges } from 'src/utils/change-tracking'
7
- import type { ChangeState } from 'src/utils/change-tracking'
8
- import { isConditionalGate, reorderableToStatement } from 'src/utils/evaluation-order'
9
- import { isStandaloneDeclaration } from 'src/utils/paths'
5
+ import { isInsideWith } from '../utils/ast'
6
+ import { traverseForChanges } from '../utils/change-tracking'
7
+ import type { ChangeState } from '../utils/change-tracking'
8
+ import { isConditionalGate, reorderableToStatement } from '../utils/evaluation-order'
9
+ import { isStandaloneDeclaration } from '../utils/paths'
10
10
 
11
11
  /**
12
12
  * Lowers conditional expressions to `if` statements. Expression values use a
@@ -67,7 +67,9 @@ function lowerConditional(path: NodePath<t.ConditionalExpression>): boolean {
67
67
  return false
68
68
  }
69
69
 
70
- const { test, consequent, alternate } = path.node
70
+ const test = path.node.test
71
+ const consequent = path.node.consequent
72
+ const alternate = path.node.alternate
71
73
 
72
74
  if (parent.isExpressionStatement() && parent.node.expression === path.node) {
73
75
  parent.replaceWith(
@@ -163,7 +165,7 @@ function lowerConditional(path: NodePath<t.ConditionalExpression>): boolean {
163
165
  return false
164
166
  }
165
167
 
166
- const { scope } = path
168
+ const scope = path.scope
167
169
  const tmpId = scope.generateUidIdentifier('ternaryResult')
168
170
  scope.push({ id: t.cloneNode(tmpId) })
169
171
 
@@ -293,8 +295,9 @@ function lowerEnclosingLogical(path: NodePath, stmtPath: NodePath): boolean {
293
295
  if (!logical || logical.removed || !logical.container) {
294
296
  return false
295
297
  }
296
- const { node } = logical
297
- const { left, right } = node
298
+ const node = logical.node
299
+ const left = node.left
300
+ const right = node.right
298
301
  let test: t.Expression
299
302
  let valueRef: t.Expression
300
303
  if (t.isIdentifier(left) && logical.scope.hasBinding(left.name)) {
@@ -2,9 +2,9 @@ import type { Binding, NodePath, Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
- import { isDirectEvalCall, isInsideWith } from 'src/utils/ast'
6
- import { traverseForChanges } from 'src/utils/change-tracking'
7
- import type { ChangeState } from 'src/utils/change-tracking'
5
+ import { isDirectEvalCall, isInsideWith } from '../utils/ast'
6
+ import { traverseForChanges } from '../utils/change-tracking'
7
+ import type { ChangeState } from '../utils/change-tracking'
8
8
 
9
9
  /**
10
10
  * Rewrites numeric updates as compound assignments for later expansion. Prefix
@@ -35,7 +35,9 @@ export function updateToAssignment(ast: File): boolean {
35
35
 
36
36
  const visitor: Visitor<ChangeState> = {
37
37
  UpdateExpression(path, state) {
38
- const { argument, prefix, operator } = path.node
38
+ const argument = path.node.argument
39
+ const prefix = path.node.prefix
40
+ const operator = path.node.operator
39
41
  if (!t.isIdentifier(argument)) {
40
42
  return
41
43
  }
@@ -61,7 +63,7 @@ const visitor: Visitor<ChangeState> = {
61
63
  }
62
64
 
63
65
  function valueIsDiscarded(path: NodePath<t.UpdateExpression>): boolean {
64
- const { parentPath } = path
66
+ const parentPath = path.parentPath
65
67
  if (!parentPath) {
66
68
  return false
67
69
  }