webfuscator 1.2.0 → 1.3.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 (111) hide show
  1. package/dist/analysis/constant.js +3 -4
  2. package/dist/analysis/constant.js.map +1 -1
  3. package/dist/analysis/inlineability.d.ts.map +1 -1
  4. package/dist/analysis/inlineability.js +37 -25
  5. package/dist/analysis/inlineability.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/obfuscator.d.ts +2 -1
  9. package/dist/obfuscator.d.ts.map +1 -1
  10. package/dist/obfuscator.js +47 -12
  11. package/dist/obfuscator.js.map +1 -1
  12. package/dist/options.d.ts +24 -4
  13. package/dist/options.d.ts.map +1 -1
  14. package/dist/preparation/expand-destructuring.d.ts +3 -3
  15. package/dist/preparation/expand-destructuring.js +6 -6
  16. package/dist/preparation/expand-destructuring.js.map +1 -1
  17. package/dist/transforms/arrow-to-function.js +3 -5
  18. package/dist/transforms/arrow-to-function.js.map +1 -1
  19. package/dist/transforms/conceal-strings.d.ts +57 -0
  20. package/dist/transforms/conceal-strings.d.ts.map +1 -0
  21. package/dist/transforms/conceal-strings.js +361 -0
  22. package/dist/transforms/conceal-strings.js.map +1 -0
  23. package/dist/transforms/const-let-to-var.d.ts +3 -3
  24. package/dist/transforms/const-let-to-var.d.ts.map +1 -1
  25. package/dist/transforms/const-let-to-var.js +36 -9
  26. package/dist/transforms/const-let-to-var.js.map +1 -1
  27. package/dist/transforms/const-to-let.d.ts +2 -3
  28. package/dist/transforms/const-to-let.d.ts.map +1 -1
  29. package/dist/transforms/const-to-let.js +3 -5
  30. package/dist/transforms/const-to-let.js.map +1 -1
  31. package/dist/transforms/fold-builtin-methods.d.ts.map +1 -1
  32. package/dist/transforms/fold-builtin-methods.js +48 -10
  33. package/dist/transforms/fold-builtin-methods.js.map +1 -1
  34. package/dist/transforms/fold-constants.js +3 -0
  35. package/dist/transforms/fold-constants.js.map +1 -1
  36. package/dist/transforms/hoist-variables.d.ts +30 -0
  37. package/dist/transforms/hoist-variables.d.ts.map +1 -0
  38. package/dist/transforms/hoist-variables.js +224 -0
  39. package/dist/transforms/hoist-variables.js.map +1 -0
  40. package/dist/transforms/inline-functions.js +3 -4
  41. package/dist/transforms/inline-functions.js.map +1 -1
  42. package/dist/transforms/mangle-properties.js +7 -4
  43. package/dist/transforms/mangle-properties.js.map +1 -1
  44. package/dist/transforms/merge-variable-declarations.d.ts +17 -0
  45. package/dist/transforms/merge-variable-declarations.d.ts.map +1 -0
  46. package/dist/transforms/merge-variable-declarations.js +68 -0
  47. package/dist/transforms/merge-variable-declarations.js.map +1 -0
  48. package/dist/transforms/numbers-to-strings.d.ts.map +1 -1
  49. package/dist/transforms/numbers-to-strings.js +1 -8
  50. package/dist/transforms/numbers-to-strings.js.map +1 -1
  51. package/dist/transforms/pack-declarations-into-parameters.js +1 -5
  52. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
  53. package/dist/transforms/pack.d.ts +5 -7
  54. package/dist/transforms/pack.d.ts.map +1 -1
  55. package/dist/transforms/pack.js +194 -32
  56. package/dist/transforms/pack.js.map +1 -1
  57. package/dist/transforms/remove-unused-code.d.ts.map +1 -1
  58. package/dist/transforms/remove-unused-code.js +7 -39
  59. package/dist/transforms/remove-unused-code.js.map +1 -1
  60. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  61. package/dist/transforms/rename-identifiers.js +29 -17
  62. package/dist/transforms/rename-identifiers.js.map +1 -1
  63. package/dist/transforms/reorder-function-declarations.d.ts +25 -0
  64. package/dist/transforms/reorder-function-declarations.d.ts.map +1 -0
  65. package/dist/transforms/reorder-function-declarations.js +64 -0
  66. package/dist/transforms/reorder-function-declarations.js.map +1 -0
  67. package/dist/transforms/specials-to-strings.js +4 -3
  68. package/dist/transforms/specials-to-strings.js.map +1 -1
  69. package/dist/transforms/update-to-assignment.js +1 -16
  70. package/dist/transforms/update-to-assignment.js.map +1 -1
  71. package/dist/utils/ast.d.ts +1 -2
  72. package/dist/utils/ast.d.ts.map +1 -1
  73. package/dist/utils/ast.js +12 -42
  74. package/dist/utils/ast.js.map +1 -1
  75. package/dist/utils/literal.d.ts.map +1 -1
  76. package/dist/utils/literal.js +74 -2
  77. package/dist/utils/literal.js.map +1 -1
  78. package/dist/utils/statement-lists.d.ts +9 -0
  79. package/dist/utils/statement-lists.d.ts.map +1 -0
  80. package/dist/utils/statement-lists.js +27 -0
  81. package/dist/utils/statement-lists.js.map +1 -0
  82. package/package.json +12 -13
  83. package/src/analysis/constant.ts +3 -6
  84. package/src/analysis/inlineability.ts +40 -32
  85. package/src/index.ts +1 -0
  86. package/src/obfuscator.ts +51 -14
  87. package/src/options.ts +28 -3
  88. package/src/preparation/expand-destructuring.ts +6 -6
  89. package/src/transforms/arrow-to-function.ts +3 -5
  90. package/src/transforms/conceal-strings.ts +530 -0
  91. package/src/transforms/const-let-to-var.ts +37 -9
  92. package/src/transforms/const-to-let.ts +3 -5
  93. package/src/transforms/fold-builtin-methods.ts +67 -12
  94. package/src/transforms/fold-constants.ts +3 -0
  95. package/src/transforms/hoist-variables.ts +272 -0
  96. package/src/transforms/inline-functions.ts +3 -4
  97. package/src/transforms/mangle-properties.ts +7 -7
  98. package/src/transforms/merge-variable-declarations.ts +91 -0
  99. package/src/transforms/numbers-to-strings.ts +2 -12
  100. package/src/transforms/pack-declarations-into-parameters.ts +1 -5
  101. package/src/transforms/pack.ts +202 -35
  102. package/src/transforms/remove-unused-code.ts +9 -64
  103. package/src/transforms/rename-identifiers.ts +42 -17
  104. package/src/transforms/reorder-function-declarations.ts +80 -0
  105. package/src/transforms/specials-to-strings.ts +4 -3
  106. package/src/transforms/update-to-assignment.ts +1 -17
  107. package/src/utils/ast.ts +15 -51
  108. package/src/utils/literal.ts +91 -2
  109. package/src/utils/statement-lists.ts +45 -0
  110. package/DISCLOSURE +0 -9
  111. package/README.md +0 -62
@@ -10,6 +10,103 @@ import { isCalleeOrTagOf, isInStrictContext, referencesOrWritesVariable } from '
10
10
  import { mulberry32 } from '../utils/random'
11
11
  import { StringGenerator } from '../utils/string-generator'
12
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
+ ]
109
+
13
110
  /**
14
111
  * Serializes the whole program to a string and rebuilds it at runtime through
15
112
  * the `Function` constructor. A detached function body only reaches true globals
@@ -17,13 +114,11 @@ import { StringGenerator } from '../utils/string-generator'
17
114
  * bundler-injected binding, or a live import, none reachable from the body and
18
115
  * none distinguishable from a global at build time, so free names are routed
19
116
  * 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.
117
+ * and resolve each name there. Imports remain above the call. Names inside
118
+ * `with` stay bare so the object still controls lookup. A Function body cannot
119
+ * contain exports, so pack throws when it finds one. Code that needs
120
+ * `import.meta`, top-level await, dynamic `import()`, or top-level `arguments`
121
+ * will not run after the move.
27
122
  *
28
123
  * @example
29
124
  * // ◀️ before
@@ -94,14 +189,19 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
94
189
  const readProperties = new Map<string, string>()
95
190
  const typeofProperties = new Map<string, string>()
96
191
  const settable = new Set<string>()
192
+ const freshProperty = (): string => {
193
+ let property: string
194
+ do {
195
+ // `__proto__` as an object-literal key would set the prototype rather
196
+ // than name an accessor.
197
+ property = generator.next()
198
+ } while (property === '__proto__')
199
+ return property
200
+ }
97
201
  const propertyFor = (registry: Map<string, string>, name: string): string => {
98
202
  let property = registry.get(name)
99
203
  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__')
204
+ property = freshProperty()
105
205
  registry.set(name, property)
106
206
  }
107
207
  return property
@@ -109,12 +209,28 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
109
209
  const memberFor = (property: string): t.MemberExpression =>
110
210
  t.memberExpression(t.identifier(objectName), t.stringLiteral(property), true)
111
211
 
212
+ // Import locals read as free once detached but are unreachable from a Function
213
+ // body, so they route even when the caller lists them in skipGlobals.
214
+ const importedNames = new Set<string>()
215
+ for (const declaration of importDeclarations) {
216
+ for (const specifier of declaration.specifiers) {
217
+ importedNames.add(specifier.local.name)
218
+ }
219
+ }
220
+ const skipOption = ctx.pack?.skipGlobals ?? false
221
+ let skipNames: readonly string[] = []
222
+ if (skipOption === true) {
223
+ skipNames = STANDARD_GLOBALS
224
+ } else if (skipOption !== false) {
225
+ skipNames = skipOption
226
+ }
227
+ const skip = new Set<string>(skipNames)
228
+
112
229
  const isRoutable = (path: NodePath<t.Identifier>): boolean => {
113
230
  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') {
231
+ // Keep the wrapper parameter and `arguments` bare. The latter must bind to
232
+ // the wrapper function's own arguments object.
233
+ if (name === objectName || name === 'arguments') {
118
234
  return false
119
235
  }
120
236
  // Inside `with`, a free name may resolve to a property of the with object at
@@ -125,10 +241,28 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
125
241
  if (!referencesOrWritesVariable(path)) {
126
242
  return false
127
243
  }
128
- return !path.scope.getBinding(name)
244
+ if (path.scope.getBinding(name)) {
245
+ return false
246
+ }
247
+ return importedNames.has(name) || !skip.has(name)
129
248
  }
130
249
 
250
+ // A strict script's top-level `this` is the global object, but a strict
251
+ // Function called plainly sees `undefined`. Rather than reach for `.call`,
252
+ // route top-level `this` through a captured property like any free name.
253
+ const routeTopLevelThis = bodyStrict && !wasModule
254
+ let thisProperty: string | null = null
255
+
131
256
  const routingVisitor: Visitor = {
257
+ ThisExpression(path) {
258
+ if (!routeTopLevelThis || !isTopLevelThis(path)) {
259
+ return
260
+ }
261
+ if (thisProperty === null) {
262
+ thisProperty = freshProperty()
263
+ }
264
+ path.replaceWith(memberFor(thisProperty))
265
+ },
132
266
  UnaryExpression(path) {
133
267
  if (path.node.operator !== 'typeof') {
134
268
  return
@@ -161,18 +295,13 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
161
295
  }
162
296
  programPath.traverse(routingVisitor)
163
297
 
164
- // The script completion value is its trailing expression; returning it keeps
165
- // `eval` of the packed output producing the same value.
166
298
  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
299
  const innerDirectives = bodyStrict ? [t.directive(t.directiveLiteral('use strict'))] : []
173
300
  const innerCode = generate(t.program(body, innerDirectives, 'script'), {
174
301
  minified: true,
175
302
  comments: false,
303
+ // `comments: false` still allows @license and @preserve.
304
+ shouldPrintComment: () => false,
176
305
  }).code
177
306
 
178
307
  const objectExpression = buildAccessorObject({
@@ -181,6 +310,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
181
310
  settable,
182
311
  setterParam,
183
312
  strict: bodyStrict,
313
+ thisProperty,
184
314
  })
185
315
 
186
316
  const constructorReference = shadowedConstructor
@@ -193,16 +323,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
193
323
  t.stringLiteral(objectName),
194
324
  t.stringLiteral(innerCode),
195
325
  ])
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])
326
+ const invocation = t.callExpression(wrapper, [objectExpression])
206
327
 
207
328
  programNode.body = [...importDeclarations, t.expressionStatement(invocation)]
208
329
  programNode.directives = []
@@ -238,6 +359,45 @@ function bindsFunctionConstructor(declaration: t.ImportDeclaration): boolean {
238
359
  return declaration.specifiers.some((specifier) => specifier.local.name === 'Function')
239
360
  }
240
361
 
362
+ // A `this` refers to the top-level `this` when no enclosing non-arrow function,
363
+ // class field, or static block captures it before the program does. A computed
364
+ // member key is the exception: it evaluates in the scope surrounding its method
365
+ // or field, not that member's `this`, so a `this` reached through a boundary's
366
+ // `key` skips it and keeps looking outward. `thisBinding` keeps every path
367
+ // predicate off the walked `current`, whose narrowed union would otherwise trip
368
+ // NodePath's invariant generic when reassigned to `child`.
369
+ function isTopLevelThis(path: NodePath<t.ThisExpression>): boolean {
370
+ let child: NodePath = path
371
+ let current: NodePath | null = path.parentPath
372
+ while (current) {
373
+ const binding = thisBinding(current)
374
+ if (binding === 'program') {
375
+ return true
376
+ }
377
+ if (binding === 'captured' && child.key !== 'key') {
378
+ return false
379
+ }
380
+ child = current
381
+ current = current.parentPath
382
+ }
383
+ return false
384
+ }
385
+
386
+ function thisBinding(boundary: NodePath): 'captured' | 'program' | null {
387
+ if (boundary.isProgram()) {
388
+ return 'program'
389
+ }
390
+ if (
391
+ (boundary.isFunction() && !boundary.isArrowFunctionExpression()) ||
392
+ boundary.isClassProperty() ||
393
+ boundary.isClassPrivateProperty() ||
394
+ boundary.isStaticBlock()
395
+ ) {
396
+ return 'captured'
397
+ }
398
+ return null
399
+ }
400
+
241
401
  function isBareDeleteArgument(path: NodePath<t.Identifier>): boolean {
242
402
  const parent = path.parentPath
243
403
  return (
@@ -291,11 +451,12 @@ interface AccessorObjectSpec {
291
451
  setterParam: string
292
452
  settable: Set<string>
293
453
  strict: boolean
454
+ thisProperty: string | null
294
455
  typeofProperties: Map<string, string>
295
456
  }
296
457
 
297
458
  function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
298
- const properties: t.ObjectMethod[] = []
459
+ const properties: (t.ObjectMethod | t.ObjectProperty)[] = []
299
460
  for (const [name, property] of spec.readProperties) {
300
461
  properties.push(
301
462
  t.objectMethod(
@@ -336,5 +497,11 @@ function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
336
497
  ),
337
498
  )
338
499
  }
500
+ // This is a data property. Its `this` value evaluates in the output script's
501
+ // top-level scope, where `this` is the global object. A getter would bind
502
+ // `this` to the accessor object instead.
503
+ if (spec.thisProperty !== null) {
504
+ properties.push(t.objectProperty(t.stringLiteral(spec.thisProperty), t.thisExpression()))
505
+ }
339
506
  return t.objectExpression(properties)
340
507
  }
@@ -1,10 +1,9 @@
1
1
  import traverse from '@babel/traverse'
2
- import type { NodePath, Scope, Visitor } from '@babel/traverse'
2
+ 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
6
  import { isSideEffectFree } from '../analysis/purity'
7
- import { isDirectEvalCall } from '../utils/ast'
8
7
  import type { ChangeState } from '../utils/change-tracking'
9
8
  import { hasAnnexBFunctionAlias } from '../utils/paths'
10
9
 
@@ -28,10 +27,7 @@ export function removeUnusedCode(ast: File): boolean {
28
27
  // then maintain counts during removal.
29
28
  crawlProgramScope(ast)
30
29
  let anyChanged = false
31
- const state: SweepState = {
32
- changed: true,
33
- evalReachableScopes: collectEvalReachableScopes(ast),
34
- }
30
+ const state: ChangeState = { changed: true }
35
31
  while (state.changed) {
36
32
  state.changed = false
37
33
  traverse(ast, sweepVisitor, undefined, state)
@@ -51,49 +47,19 @@ function crawlProgramScope(ast: File): void {
51
47
  })
52
48
  }
53
49
 
54
- // Direct eval can read otherwise unreferenced bindings in its scope ancestry.
55
- // Sweep state keeps this set local to one pass.
56
- interface SweepState extends ChangeState {
57
- evalReachableScopes: Set<t.Node>
58
- }
59
-
60
- function collectEvalReachableScopes(ast: File): Set<t.Node> {
61
- const reachable = new Set<t.Node>()
62
- traverse(ast, {
63
- CallExpression(path) {
64
- if (!isDirectEvalCall(path.node)) {
65
- return
66
- }
67
- let scope: Scope | undefined = path.scope
68
- while (scope) {
69
- reachable.add(scope.block)
70
- scope = scope.parent
71
- }
72
- },
73
- })
74
- return reachable
75
- }
76
-
77
- function bindingReachableByEval(
78
- binding: { scope: Scope },
79
- evalReachableScopes: Set<t.Node>,
80
- ): boolean {
81
- return evalReachableScopes.has(binding.scope.block)
82
- }
83
-
84
- const sweepVisitor: Visitor<SweepState> = {
50
+ const sweepVisitor: Visitor<ChangeState> = {
85
51
  VariableDeclarator(path, state) {
86
- if (tryRemoveDeclarator(path, state.evalReachableScopes)) {
52
+ if (tryRemoveDeclarator(path)) {
87
53
  state.changed = true
88
54
  }
89
55
  },
90
56
  FunctionDeclaration(path, state) {
91
- if (tryRemoveFunctionDecl(path, state.evalReachableScopes)) {
57
+ if (tryRemoveFunctionDecl(path)) {
92
58
  state.changed = true
93
59
  }
94
60
  },
95
61
  ClassDeclaration(path, state) {
96
- if (tryRemoveClassDecl(path, state.evalReachableScopes)) {
62
+ if (tryRemoveClassDecl(path)) {
97
63
  state.changed = true
98
64
  }
99
65
  },
@@ -126,10 +92,7 @@ function dereferenceSubtree(rootPath: NodePath): void {
126
92
  rootPath.traverse(dereferenceVisitor)
127
93
  }
128
94
 
129
- function tryRemoveDeclarator(
130
- path: NodePath<t.VariableDeclarator>,
131
- evalReachableScopes: Set<t.Node>,
132
- ): boolean {
95
+ function tryRemoveDeclarator(path: NodePath<t.VariableDeclarator>): boolean {
133
96
  const declaration = path.parentPath
134
97
  if (!declaration || !declaration.isVariableDeclaration()) {
135
98
  return false
@@ -170,10 +133,6 @@ function tryRemoveDeclarator(
170
133
  if (binding.constantViolations.length > 0) {
171
134
  return false
172
135
  }
173
- if (bindingReachableByEval(binding, evalReachableScopes)) {
174
- return false
175
- }
176
-
177
136
  if (path.node.init) {
178
137
  dereferenceSubtree(path.get('init') as NodePath)
179
138
  }
@@ -185,10 +144,7 @@ function tryRemoveDeclarator(
185
144
  return true
186
145
  }
187
146
 
188
- function tryRemoveFunctionDecl(
189
- path: NodePath<t.FunctionDeclaration>,
190
- evalReachableScopes: Set<t.Node>,
191
- ): boolean {
147
+ function tryRemoveFunctionDecl(path: NodePath<t.FunctionDeclaration>): boolean {
192
148
  if (isExported(path)) {
193
149
  return false
194
150
  }
@@ -211,20 +167,13 @@ function tryRemoveFunctionDecl(
211
167
  if (binding.constantViolations.length > 0) {
212
168
  return false
213
169
  }
214
- if (bindingReachableByEval(binding, evalReachableScopes)) {
215
- return false
216
- }
217
-
218
170
  dereferenceSubtree(path.get('body') as NodePath)
219
171
  path.scope.removeBinding(id.name)
220
172
  path.remove()
221
173
  return true
222
174
  }
223
175
 
224
- function tryRemoveClassDecl(
225
- path: NodePath<t.ClassDeclaration>,
226
- evalReachableScopes: Set<t.Node>,
227
- ): boolean {
176
+ function tryRemoveClassDecl(path: NodePath<t.ClassDeclaration>): boolean {
228
177
  if (isExported(path)) {
229
178
  return false
230
179
  }
@@ -265,10 +214,6 @@ function tryRemoveClassDecl(
265
214
  if (binding.constantViolations.length > 0) {
266
215
  return false
267
216
  }
268
- if (bindingReachableByEval(binding, evalReachableScopes)) {
269
- return false
270
- }
271
-
272
217
  dereferenceSubtree(path.get('body') as NodePath)
273
218
  if (path.node.superClass) {
274
219
  dereferenceSubtree(path.get('superClass') as NodePath)
@@ -4,7 +4,7 @@ import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
6
  import type { TransformContext } from '../options'
7
- import { isDirectEvalCall, isInsideWith } from '../utils/ast'
7
+ import { isInsideWith } from '../utils/ast'
8
8
  import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from '../utils/paths'
9
9
  import { mulberry32 } from '../utils/random'
10
10
  import { StringGenerator } from '../utils/string-generator'
@@ -31,6 +31,7 @@ import { StringGenerator } from '../utils/string-generator'
31
31
  */
32
32
  export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
33
33
  const generator = new StringGenerator(ctx.stringGeneratorMode, mulberry32(ctx.seed))
34
+ const separatedDirectExports = separateDirectExports(ast.program)
34
35
 
35
36
  let programScope: Scope | undefined
36
37
  traverse(ast, {
@@ -40,14 +41,14 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
40
41
  },
41
42
  })
42
43
  if (!programScope) {
43
- return false
44
+ return separatedDirectExports
44
45
  }
45
46
  programScope.crawl()
46
47
 
47
48
  const liveAncestors = new Map<Scope, Set<Binding>>()
48
49
  // Reserve unbound names so generated bindings cannot capture them.
49
50
  const freeNames = new Set<string>()
50
- // `with` and direct eval pin bindings to their source names.
51
+ // A reference inside `with` pins its binding to the source name.
51
52
  const unrenamable = new Set<Binding>()
52
53
  // Reserve pinned source names globally, including before their binding is seen.
53
54
  const pinnedNames = new Set<string>()
@@ -82,19 +83,6 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
82
83
  scope = scope.parent
83
84
  }
84
85
  },
85
- CallExpression(path) {
86
- // Direct eval can read every visible binding by its source name.
87
- if (!isDirectEvalCall(path.node)) {
88
- return
89
- }
90
- let scope: Scope | undefined = path.scope
91
- while (scope) {
92
- for (const name of Object.keys(scope.bindings)) {
93
- pin(scope.bindings[name]!, name)
94
- }
95
- scope = scope.parent
96
- }
97
- },
98
86
  FunctionDeclaration(path) {
99
87
  // ECMA-262 B.3.3 gives sloppy block functions an alias Babel does not model.
100
88
  if (!hasAnnexBFunctionAlias(path)) {
@@ -169,7 +157,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
169
157
  })
170
158
 
171
159
  if (bindingToName.size === 0) {
172
- return renameLabels(ast, generator)
160
+ return renameLabels(ast, generator) || separatedDirectExports
173
161
  }
174
162
 
175
163
  traverse(ast, {
@@ -199,6 +187,43 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
199
187
  return true
200
188
  }
201
189
 
190
+ function separateDirectExports(program: t.Program): boolean {
191
+ const body: t.Program['body'] = []
192
+ let changed = false
193
+
194
+ for (const statement of program.body) {
195
+ const declaration = t.isExportNamedDeclaration(statement) ? statement.declaration : null
196
+ if (
197
+ !t.isVariableDeclaration(declaration) &&
198
+ !t.isFunctionDeclaration(declaration) &&
199
+ !t.isClassDeclaration(declaration)
200
+ ) {
201
+ body.push(statement)
202
+ continue
203
+ }
204
+
205
+ const names = Object.keys(t.getBindingIdentifiers(declaration))
206
+ if (names.length === 0) {
207
+ body.push(statement)
208
+ continue
209
+ }
210
+
211
+ body.push(
212
+ declaration,
213
+ t.exportNamedDeclaration(
214
+ null,
215
+ names.map((name) => t.exportSpecifier(t.identifier(name), t.identifier(name))),
216
+ ),
217
+ )
218
+ changed = true
219
+ }
220
+
221
+ if (changed) {
222
+ program.body = body
223
+ }
224
+ return changed
225
+ }
226
+
202
227
  function isNonBindingPosition(path: { node: t.Identifier; parent: t.Node }): boolean {
203
228
  const node = path.node
204
229
  const parent = path.parent
@@ -0,0 +1,80 @@
1
+ import type { NodePath } from '@babel/traverse'
2
+ import type * as t from '@babel/types'
3
+ import type { File } from '@babel/types'
4
+
5
+ import { hasAnnexBFunctionAlias } from '../utils/paths'
6
+ import { rewriteStatementLists } from '../utils/statement-lists'
7
+ import type { StatementListContainer } from '../utils/statement-lists'
8
+
9
+ /**
10
+ * Moves hoisted function declarations to the end of each program, block, switch
11
+ * case, or static block. It keeps declaration order, including duplicate names,
12
+ * and leaves sloppy Annex B block functions in place.
13
+ *
14
+ * If moving every declaration would turn a string expression into a directive,
15
+ * one declaration stays before the string. Direct exports stay in place.
16
+ * Indirect exports move with their local declaration.
17
+ *
18
+ * @example
19
+ * // ◀️ before
20
+ * function area(r) {
21
+ * return r * r;
22
+ * }
23
+ * log(area(2));
24
+ *
25
+ * // ▶️ after
26
+ * log(area(2));
27
+ * function area(r) {
28
+ * return r * r;
29
+ * }
30
+ */
31
+ export function reorderFunctionDeclarations(ast: File): boolean {
32
+ return rewriteStatementLists(ast, (statements, container) =>
33
+ reorderedStatements(statements, containerAllowsDirective(container)),
34
+ )
35
+ }
36
+
37
+ // Only programs and function bodies recognize directives, so only they need the guard.
38
+ function containerAllowsDirective(container: NodePath<StatementListContainer>): boolean {
39
+ if (container.isProgram()) {
40
+ return true
41
+ }
42
+ return container.isBlockStatement() && container.parentPath.isFunction()
43
+ }
44
+
45
+ function reorderedStatements(
46
+ children: NodePath<t.Statement>[],
47
+ canCreateDirective: boolean,
48
+ ): t.Statement[] | null {
49
+ const rest: t.Statement[] = []
50
+ const moved: t.Statement[] = []
51
+ for (const child of children) {
52
+ if (isMovable(child)) {
53
+ moved.push(child.node)
54
+ } else {
55
+ rest.push(child.node)
56
+ }
57
+ }
58
+
59
+ if (moved.length === 0) {
60
+ return null
61
+ }
62
+ if (canCreateDirective && createsDirective(rest)) {
63
+ rest.unshift(moved.shift()!)
64
+ }
65
+ const reordered = [...rest, ...moved]
66
+ return reordered.every((node, index) => node === children[index]?.node) ? null : reordered
67
+ }
68
+
69
+ function createsDirective(statements: readonly t.Statement[]): boolean {
70
+ const first = statements[0]
71
+ return (
72
+ first !== undefined &&
73
+ first.type === 'ExpressionStatement' &&
74
+ first.expression.type === 'StringLiteral'
75
+ )
76
+ }
77
+
78
+ function isMovable(child: NodePath<t.Statement>): boolean {
79
+ return child.isFunctionDeclaration() && child.node.id !== null && !hasAnnexBFunctionAlias(child)
80
+ }
@@ -4,7 +4,7 @@ import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
6
  import type { TransformContext } from '../options'
7
- import { enclosingScopeHasDirectEval, isInsideWith } from '../utils/ast'
7
+ import { isInsideWith } from '../utils/ast'
8
8
  import type { ChangeState } from '../utils/change-tracking'
9
9
  import { mulberry32 } from '../utils/random'
10
10
  import { StringGenerator } from '../utils/string-generator'
@@ -69,8 +69,9 @@ const visitor: Visitor<State> = {
69
69
  if (path.scope.getBinding(name)) {
70
70
  return
71
71
  }
72
- // Replacements are not write targets. `with` and eval can also shadow globals.
73
- if (isWriteTarget(path) || isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
72
+ // A replacement cannot occupy a write target. `with` may also shadow `NaN`,
73
+ // `Infinity`, or `undefined`.
74
+ if (isWriteTarget(path) || isInsideWith(path)) {
74
75
  return
75
76
  }
76
77
  const str = t.stringLiteral(state.generator.next())