webfuscator 1.1.0 → 1.2.1

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 (155) hide show
  1. package/README.md +17 -12
  2. package/dist/analysis/constant.js +3 -3
  3. package/dist/analysis/constant.js.map +1 -1
  4. package/dist/analysis/inlineability.d.ts.map +1 -1
  5. package/dist/analysis/inlineability.js +39 -27
  6. package/dist/analysis/inlineability.js.map +1 -1
  7. package/dist/analysis/purity.js +1 -1
  8. package/dist/analysis/purity.js.map +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/obfuscator.d.ts.map +1 -1
  13. package/dist/obfuscator.js.map +1 -1
  14. package/dist/options.d.ts +7 -0
  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 +2 -2
  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.map +1 -1
  73. package/dist/transforms/pack.js +194 -26
  74. package/dist/transforms/pack.js.map +1 -1
  75. package/dist/transforms/remove-anonymous-function-names.js +3 -3
  76. package/dist/transforms/remove-anonymous-function-names.js.map +1 -1
  77. package/dist/transforms/remove-unreachable-code.js +1 -1
  78. package/dist/transforms/remove-unreachable-code.js.map +1 -1
  79. package/dist/transforms/remove-unused-code.js +4 -4
  80. package/dist/transforms/remove-unused-code.js.map +1 -1
  81. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  82. package/dist/transforms/rename-identifiers.js +4 -3
  83. package/dist/transforms/rename-identifiers.js.map +1 -1
  84. package/dist/transforms/specials-to-strings.d.ts.map +1 -1
  85. package/dist/transforms/specials-to-strings.js +2 -2
  86. package/dist/transforms/specials-to-strings.js.map +1 -1
  87. package/dist/transforms/switch-to-if.js +7 -7
  88. package/dist/transforms/switch-to-if.js.map +1 -1
  89. package/dist/transforms/ternary-to-if.js +7 -4
  90. package/dist/transforms/ternary-to-if.js.map +1 -1
  91. package/dist/transforms/update-to-assignment.js +4 -2
  92. package/dist/transforms/update-to-assignment.js.map +1 -1
  93. package/dist/transforms/yodify-conditions.js +3 -3
  94. package/dist/transforms/yodify-conditions.js.map +1 -1
  95. package/dist/utils/ast.d.ts.map +1 -1
  96. package/dist/utils/ast.js +2 -1
  97. package/dist/utils/ast.js.map +1 -1
  98. package/dist/utils/evaluation-order.js.map +1 -1
  99. package/dist/utils/logical-lowering.js +2 -2
  100. package/dist/utils/logical-lowering.js.map +1 -1
  101. package/dist/utils/loop-lowering.js +1 -1
  102. package/dist/utils/loop-lowering.js.map +1 -1
  103. package/dist/utils/string-generator.js +1 -1
  104. package/dist/utils/string-generator.js.map +1 -1
  105. package/package.json +14 -12
  106. package/src/analysis/constant.ts +4 -4
  107. package/src/analysis/inlineability.ts +43 -35
  108. package/src/analysis/purity.ts +3 -3
  109. package/src/index.ts +4 -2
  110. package/src/obfuscator.ts +42 -42
  111. package/src/options.ts +8 -1
  112. package/src/preparation/expand-destructuring.ts +5 -4
  113. package/src/preparation/expand-template-literals.ts +4 -3
  114. package/src/preparation/split-sequence-expressions.ts +2 -2
  115. package/src/preparation/split-variable-declarations.ts +1 -1
  116. package/src/preparation/stringify-object-keys.ts +2 -2
  117. package/src/preparation/wrap-single-statements.ts +2 -2
  118. package/src/transforms/arguments-to-parameters.ts +5 -5
  119. package/src/transforms/arrow-to-function.ts +3 -3
  120. package/src/transforms/collapse-single-use-temps.ts +14 -14
  121. package/src/transforms/const-let-to-var.ts +6 -6
  122. package/src/transforms/const-to-let.ts +4 -4
  123. package/src/transforms/do-while-to-while.ts +6 -6
  124. package/src/transforms/drop-console.ts +8 -7
  125. package/src/transforms/drop-debugger.ts +2 -2
  126. package/src/transforms/expand-binary-assignment.ts +8 -6
  127. package/src/transforms/expand-logical-assignment.ts +8 -6
  128. package/src/transforms/extract-object-properties.ts +12 -12
  129. package/src/transforms/fold-builtin-methods.ts +20 -19
  130. package/src/transforms/fold-constants.ts +8 -8
  131. package/src/transforms/for-to-while.ts +8 -8
  132. package/src/transforms/function-declaration-to-expression.ts +5 -5
  133. package/src/transforms/inline-functions.ts +14 -14
  134. package/src/transforms/logical-to-ternary.ts +7 -7
  135. package/src/transforms/mangle-properties.ts +43 -97
  136. package/src/transforms/nullish-coalescing-to-ternary.ts +6 -6
  137. package/src/transforms/numbers-to-strings.ts +4 -4
  138. package/src/transforms/object-method-to-property.ts +4 -4
  139. package/src/transforms/optional-chaining-to-ternary.ts +5 -5
  140. package/src/transforms/pack-declarations-into-parameters.ts +7 -7
  141. package/src/transforms/pack.ts +212 -33
  142. package/src/transforms/remove-anonymous-function-names.ts +5 -5
  143. package/src/transforms/remove-unreachable-code.ts +3 -3
  144. package/src/transforms/remove-unused-code.ts +8 -8
  145. package/src/transforms/rename-identifiers.ts +9 -8
  146. package/src/transforms/specials-to-strings.ts +7 -7
  147. package/src/transforms/switch-to-if.ts +12 -12
  148. package/src/transforms/ternary-to-if.ts +12 -9
  149. package/src/transforms/update-to-assignment.ts +7 -5
  150. package/src/transforms/yodify-conditions.ts +6 -6
  151. package/src/utils/ast.ts +2 -1
  152. package/src/utils/evaluation-order.ts +1 -1
  153. package/src/utils/logical-lowering.ts +3 -3
  154. package/src/utils/loop-lowering.ts +1 -1
  155. package/src/utils/string-generator.ts +1 -1
@@ -4,11 +4,108 @@ import type { NodePath, Visitor } from '@babel/traverse'
4
4
  import * as t from '@babel/types'
5
5
  import type { File } from '@babel/types'
6
6
 
7
- import type { TransformContext } from 'src/options'
8
- import { isInsideWith } from 'src/utils/ast'
9
- import { isCalleeOrTagOf, isInStrictContext, referencesOrWritesVariable } from 'src/utils/paths'
10
- import { mulberry32 } from 'src/utils/random'
11
- import { StringGenerator } from 'src/utils/string-generator'
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
+ // The ECMAScript intrinsics and the common host globals. A detached Function
14
+ // body reaches every one of them and gets the same value, so pack can leave them
15
+ // bare. The CommonJS wrapper names (require, module, exports, __dirname,
16
+ // __filename) are left out on purpose. They are function parameters, not
17
+ // properties of the global object, so a Function body cannot see them and pack
18
+ // routes them like any other free name. `skipGlobals: true` opts into this list.
19
+ const STANDARD_GLOBALS: readonly string[] = [
20
+ 'globalThis',
21
+ 'self',
22
+ 'Infinity',
23
+ 'NaN',
24
+ 'undefined',
25
+ 'parseInt',
26
+ 'parseFloat',
27
+ 'isNaN',
28
+ 'isFinite',
29
+ 'decodeURI',
30
+ 'decodeURIComponent',
31
+ 'encodeURI',
32
+ 'encodeURIComponent',
33
+ 'escape',
34
+ 'unescape',
35
+ 'btoa',
36
+ 'atob',
37
+ 'structuredClone',
38
+ 'fetch',
39
+ 'queueMicrotask',
40
+ 'setTimeout',
41
+ 'clearTimeout',
42
+ 'setInterval',
43
+ 'clearInterval',
44
+ 'setImmediate',
45
+ 'clearImmediate',
46
+ 'Object',
47
+ 'Function',
48
+ 'Boolean',
49
+ 'Symbol',
50
+ 'BigInt',
51
+ 'Number',
52
+ 'Math',
53
+ 'Date',
54
+ 'String',
55
+ 'RegExp',
56
+ 'JSON',
57
+ 'Promise',
58
+ 'Proxy',
59
+ 'Reflect',
60
+ 'Error',
61
+ 'EvalError',
62
+ 'RangeError',
63
+ 'ReferenceError',
64
+ 'SyntaxError',
65
+ 'TypeError',
66
+ 'URIError',
67
+ 'AggregateError',
68
+ 'Array',
69
+ 'Map',
70
+ 'Set',
71
+ 'WeakMap',
72
+ 'WeakSet',
73
+ 'WeakRef',
74
+ 'FinalizationRegistry',
75
+ 'ArrayBuffer',
76
+ 'SharedArrayBuffer',
77
+ 'DataView',
78
+ 'Atomics',
79
+ 'Int8Array',
80
+ 'Uint8Array',
81
+ 'Uint8ClampedArray',
82
+ 'Int16Array',
83
+ 'Uint16Array',
84
+ 'Int32Array',
85
+ 'Uint32Array',
86
+ 'Float32Array',
87
+ 'Float64Array',
88
+ 'BigInt64Array',
89
+ 'BigUint64Array',
90
+ 'console',
91
+ 'crypto',
92
+ 'performance',
93
+ 'Intl',
94
+ 'URL',
95
+ 'URLSearchParams',
96
+ 'TextEncoder',
97
+ 'TextDecoder',
98
+ 'WebAssembly',
99
+ 'window',
100
+ 'document',
101
+ 'location',
102
+ 'postMessage',
103
+ 'alert',
104
+ 'confirm',
105
+ 'global',
106
+ 'process',
107
+ 'Buffer',
108
+ ]
12
109
 
13
110
  /**
14
111
  * Serializes the whole program to a string and rebuilds it at runtime through
@@ -94,14 +191,19 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
94
191
  const readProperties = new Map<string, string>()
95
192
  const typeofProperties = new Map<string, string>()
96
193
  const settable = new Set<string>()
194
+ const freshProperty = (): string => {
195
+ let property: string
196
+ do {
197
+ // `__proto__` as an object-literal key would set the prototype rather
198
+ // than name an accessor.
199
+ property = generator.next()
200
+ } while (property === '__proto__')
201
+ return property
202
+ }
97
203
  const propertyFor = (registry: Map<string, string>, name: string): string => {
98
204
  let property = registry.get(name)
99
205
  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__')
206
+ property = freshProperty()
105
207
  registry.set(name, property)
106
208
  }
107
209
  return property
@@ -109,8 +211,25 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
109
211
  const memberFor = (property: string): t.MemberExpression =>
110
212
  t.memberExpression(t.identifier(objectName), t.stringLiteral(property), true)
111
213
 
214
+ // Import locals read as free once detached but are unreachable from a Function
215
+ // body, so they route even when the caller lists them in skipGlobals.
216
+ const importedNames = new Set<string>()
217
+ for (const declaration of importDeclarations) {
218
+ for (const specifier of declaration.specifiers) {
219
+ importedNames.add(specifier.local.name)
220
+ }
221
+ }
222
+ const skipOption = ctx.pack?.skipGlobals ?? false
223
+ let skipNames: readonly string[] = []
224
+ if (skipOption === true) {
225
+ skipNames = STANDARD_GLOBALS
226
+ } else if (skipOption !== false) {
227
+ skipNames = skipOption
228
+ }
229
+ const skip = new Set<string>(skipNames)
230
+
112
231
  const isRoutable = (path: NodePath<t.Identifier>): boolean => {
113
- const { name } = path.node
232
+ const name = path.node.name
114
233
  // The wrapper parameter stays as written. `arguments` and `eval` are never
115
234
  // routed: `arguments` binds to its own function's object, and leaving `eval`
116
235
  // bare keeps a direct eval reading the body scope rather than the global one.
@@ -125,10 +244,28 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
125
244
  if (!referencesOrWritesVariable(path)) {
126
245
  return false
127
246
  }
128
- return !path.scope.getBinding(name)
247
+ if (path.scope.getBinding(name)) {
248
+ return false
249
+ }
250
+ return importedNames.has(name) || !skip.has(name)
129
251
  }
130
252
 
253
+ // A strict script's top-level `this` is the global object, but a strict
254
+ // Function called plainly sees `undefined`. Rather than reach for `.call`,
255
+ // route top-level `this` through a captured property like any free name.
256
+ const routeTopLevelThis = bodyStrict && !wasModule
257
+ let thisProperty: string | null = null
258
+
131
259
  const routingVisitor: Visitor = {
260
+ ThisExpression(path) {
261
+ if (!routeTopLevelThis || !isTopLevelThis(path)) {
262
+ return
263
+ }
264
+ if (thisProperty === null) {
265
+ thisProperty = freshProperty()
266
+ }
267
+ path.replaceWith(memberFor(thisProperty))
268
+ },
132
269
  UnaryExpression(path) {
133
270
  if (path.node.operator !== 'typeof') {
134
271
  return
@@ -147,7 +284,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
147
284
  if (isBareDeleteArgument(path) || !isRoutable(path)) {
148
285
  return
149
286
  }
150
- const { name } = path.node
287
+ const name = path.node.name
151
288
  if (isWriteReference(path)) {
152
289
  settable.add(name)
153
290
  }
@@ -163,7 +300,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
163
300
 
164
301
  // The script completion value is its trailing expression; returning it keeps
165
302
  // `eval` of the packed output producing the same value.
166
- const { body } = programNode
303
+ const body = programNode.body
167
304
  const lastStatement = body.at(-1)
168
305
  if (lastStatement && t.isExpressionStatement(lastStatement)) {
169
306
  body[body.length - 1] = t.returnStatement(lastStatement.expression)
@@ -181,6 +318,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
181
318
  settable,
182
319
  setterParam,
183
320
  strict: bodyStrict,
321
+ thisProperty,
184
322
  })
185
323
 
186
324
  const constructorReference = shadowedConstructor
@@ -193,16 +331,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
193
331
  t.stringLiteral(objectName),
194
332
  t.stringLiteral(innerCode),
195
333
  ])
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])
334
+ const invocation = t.callExpression(wrapper, [objectExpression])
206
335
 
207
336
  programNode.body = [...importDeclarations, t.expressionStatement(invocation)]
208
337
  programNode.directives = []
@@ -238,6 +367,45 @@ function bindsFunctionConstructor(declaration: t.ImportDeclaration): boolean {
238
367
  return declaration.specifiers.some((specifier) => specifier.local.name === 'Function')
239
368
  }
240
369
 
370
+ // A `this` refers to the top-level `this` when no enclosing non-arrow function,
371
+ // class field, or static block captures it before the program does. A computed
372
+ // member key is the exception: it evaluates in the scope surrounding its method
373
+ // or field, not that member's `this`, so a `this` reached through a boundary's
374
+ // `key` skips it and keeps looking outward. `thisBinding` keeps every path
375
+ // predicate off the walked `current`, whose narrowed union would otherwise trip
376
+ // NodePath's invariant generic when reassigned to `child`.
377
+ function isTopLevelThis(path: NodePath<t.ThisExpression>): boolean {
378
+ let child: NodePath = path
379
+ let current: NodePath | null = path.parentPath
380
+ while (current) {
381
+ const binding = thisBinding(current)
382
+ if (binding === 'program') {
383
+ return true
384
+ }
385
+ if (binding === 'captured' && child.key !== 'key') {
386
+ return false
387
+ }
388
+ child = current
389
+ current = current.parentPath
390
+ }
391
+ return false
392
+ }
393
+
394
+ function thisBinding(boundary: NodePath): 'captured' | 'program' | null {
395
+ if (boundary.isProgram()) {
396
+ return 'program'
397
+ }
398
+ if (
399
+ (boundary.isFunction() && !boundary.isArrowFunctionExpression()) ||
400
+ boundary.isClassProperty() ||
401
+ boundary.isClassPrivateProperty() ||
402
+ boundary.isStaticBlock()
403
+ ) {
404
+ return 'captured'
405
+ }
406
+ return null
407
+ }
408
+
241
409
  function isBareDeleteArgument(path: NodePath<t.Identifier>): boolean {
242
410
  const parent = path.parentPath
243
411
  return (
@@ -252,7 +420,7 @@ function isWriteReference(path: NodePath<t.Identifier>): boolean {
252
420
  if (!parent) {
253
421
  return false
254
422
  }
255
- const { node } = path
423
+ const node = path.node
256
424
  if (parent.isAssignmentExpression() && parent.node.left === node) {
257
425
  return true
258
426
  }
@@ -291,13 +459,13 @@ interface AccessorObjectSpec {
291
459
  setterParam: string
292
460
  settable: Set<string>
293
461
  strict: boolean
462
+ thisProperty: string | null
294
463
  typeofProperties: Map<string, string>
295
464
  }
296
465
 
297
466
  function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
298
- const { readProperties, setterParam, settable, strict, typeofProperties } = spec
299
- const properties: t.ObjectMethod[] = []
300
- for (const [name, property] of readProperties) {
467
+ const properties: (t.ObjectMethod | t.ObjectProperty)[] = []
468
+ for (const [name, property] of spec.readProperties) {
301
469
  properties.push(
302
470
  t.objectMethod(
303
471
  'get',
@@ -306,23 +474,28 @@ function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
306
474
  t.blockStatement([t.returnStatement(t.identifier(name))]),
307
475
  ),
308
476
  )
309
- if (settable.has(name)) {
477
+ if (spec.settable.has(name)) {
310
478
  const setterBody = t.blockStatement([
311
479
  t.expressionStatement(
312
- t.assignmentExpression('=', t.identifier(name), t.identifier(setterParam)),
480
+ t.assignmentExpression('=', t.identifier(name), t.identifier(spec.setterParam)),
313
481
  ),
314
482
  ])
315
483
  // A strict script becomes a sloppy output script, so the setter stays
316
484
  // strict to keep an undeclared-global write throwing.
317
- if (strict) {
485
+ if (spec.strict) {
318
486
  setterBody.directives.push(t.directive(t.directiveLiteral('use strict')))
319
487
  }
320
488
  properties.push(
321
- t.objectMethod('set', t.stringLiteral(property), [t.identifier(setterParam)], setterBody),
489
+ t.objectMethod(
490
+ 'set',
491
+ t.stringLiteral(property),
492
+ [t.identifier(spec.setterParam)],
493
+ setterBody,
494
+ ),
322
495
  )
323
496
  }
324
497
  }
325
- for (const [name, property] of typeofProperties) {
498
+ for (const [name, property] of spec.typeofProperties) {
326
499
  properties.push(
327
500
  t.objectMethod(
328
501
  'get',
@@ -332,5 +505,11 @@ function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
332
505
  ),
333
506
  )
334
507
  }
508
+ // This is a data property. Its `this` value evaluates in the output script's
509
+ // top-level scope, where `this` is the global object. A getter would bind
510
+ // `this` to the accessor object instead.
511
+ if (spec.thisProperty !== null) {
512
+ properties.push(t.objectProperty(t.stringLiteral(spec.thisProperty), t.thisExpression()))
513
+ }
335
514
  return t.objectExpression(properties)
336
515
  }
@@ -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
  }