esupgrade 2025.0.2 → 2025.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.
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Transform var to const
3
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
4
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
3
5
  */
4
6
  export function varToConst(j, root) {
5
7
  let modified = false
@@ -21,6 +23,7 @@ export function varToConst(j, root) {
21
23
 
22
24
  /**
23
25
  * Transform string concatenation to template literals
26
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
24
27
  */
25
28
  export function concatToTemplateLiteral(j, root) {
26
29
  let modified = false
@@ -94,6 +97,7 @@ export function concatToTemplateLiteral(j, root) {
94
97
 
95
98
  /**
96
99
  * Transform Object.assign({}, ...) to object spread
100
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
97
101
  */
98
102
  export function objectAssignToSpread(j, root) {
99
103
  let modified = false
@@ -128,6 +132,7 @@ export function objectAssignToSpread(j, root) {
128
132
 
129
133
  /**
130
134
  * Transform Array.from().forEach() to for...of
135
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
131
136
  */
132
137
  export function arrayFromForEachToForOf(j, root) {
133
138
  let modified = false
@@ -215,6 +220,7 @@ export function arrayFromForEachToForOf(j, root) {
215
220
 
216
221
  /**
217
222
  * Transform for...of Object.keys() loops to for...in
223
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
218
224
  */
219
225
  export function forOfKeysToForIn(j, root) {
220
226
  let modified = false
@@ -263,3 +269,473 @@ export function forOfKeysToForIn(j, root) {
263
269
 
264
270
  return { modified, changes }
265
271
  }
272
+
273
+ /**
274
+ * Transform Array.from(obj) to [...obj] spread syntax
275
+ * This handles cases like Array.from(obj).map(), .filter(), .some(), etc.
276
+ * that are not covered by the forEach transformer
277
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
278
+ */
279
+ export function arrayFromToSpread(j, root) {
280
+ let modified = false
281
+ const changes = []
282
+
283
+ root
284
+ .find(j.CallExpression)
285
+ .filter((path) => {
286
+ const node = path.node
287
+
288
+ // Check if this is Array.from() call
289
+ if (
290
+ !j.MemberExpression.check(node.callee) ||
291
+ !j.Identifier.check(node.callee.object) ||
292
+ node.callee.object.name !== "Array" ||
293
+ !j.Identifier.check(node.callee.property) ||
294
+ node.callee.property.name !== "from"
295
+ ) {
296
+ return false
297
+ }
298
+
299
+ // Must have exactly one argument (the iterable)
300
+ // If there's a second argument (mapping function), we should not transform
301
+ if (node.arguments.length !== 1) {
302
+ return false
303
+ }
304
+
305
+ // Don't transform if this is Array.from().forEach()
306
+ // as that's handled by arrayFromForEachToForOf
307
+ const parent = path.parent.node
308
+ if (
309
+ j.MemberExpression.check(parent) &&
310
+ j.Identifier.check(parent.property) &&
311
+ parent.property.name === "forEach"
312
+ ) {
313
+ return false
314
+ }
315
+
316
+ return true
317
+ })
318
+ .forEach((path) => {
319
+ const node = path.node
320
+ const iterable = node.arguments[0]
321
+
322
+ // Create array with spread element
323
+ const spreadArray = j.arrayExpression([j.spreadElement(iterable)])
324
+
325
+ j(path).replaceWith(spreadArray)
326
+
327
+ modified = true
328
+ if (node.loc) {
329
+ changes.push({
330
+ type: "arrayFromToSpread",
331
+ line: node.loc.start.line,
332
+ })
333
+ }
334
+ })
335
+
336
+ return { modified, changes }
337
+ }
338
+
339
+ /**
340
+ * Transform Math.pow() to exponentiation operator (**)
341
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
342
+ */
343
+ export function mathPowToExponentiation(j, root) {
344
+ let modified = false
345
+ const changes = []
346
+
347
+ root
348
+ .find(j.CallExpression, {
349
+ callee: {
350
+ type: "MemberExpression",
351
+ object: { name: "Math" },
352
+ property: { name: "pow" },
353
+ },
354
+ })
355
+ .filter((path) => {
356
+ // Must have exactly 2 arguments (base and exponent)
357
+ return path.node.arguments.length === 2
358
+ })
359
+ .forEach((path) => {
360
+ const node = path.node
361
+ const [base, exponent] = node.arguments
362
+
363
+ // Create exponentiation expression
364
+ const expExpression = j.binaryExpression("**", base, exponent)
365
+
366
+ j(path).replaceWith(expExpression)
367
+
368
+ modified = true
369
+ if (node.loc) {
370
+ changes.push({
371
+ type: "mathPowToExponentiation",
372
+ line: node.loc.start.line,
373
+ })
374
+ }
375
+ })
376
+
377
+ return { modified, changes }
378
+ }
379
+
380
+ /**
381
+ * Transform traditional for loops to for...of where safe
382
+ * Converts: for (let i = 0; i < arr.length; i++) { const item = arr[i]; ... }
383
+ * To: for (const item of arr) { ... }
384
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
385
+ */
386
+ export function forLoopToForOf(j, root) {
387
+ let modified = false
388
+ const changes = []
389
+
390
+ root
391
+ .find(j.ForStatement)
392
+ .filter((path) => {
393
+ const node = path.node
394
+
395
+ // Check init: must be let/const i = 0
396
+ if (!j.VariableDeclaration.check(node.init)) {
397
+ return false
398
+ }
399
+ if (node.init.declarations.length !== 1) {
400
+ return false
401
+ }
402
+ const initDeclarator = node.init.declarations[0]
403
+ if (!j.Identifier.check(initDeclarator.id)) {
404
+ return false
405
+ }
406
+ const indexVar = initDeclarator.id.name
407
+ if (!j.Literal.check(initDeclarator.init) || initDeclarator.init.value !== 0) {
408
+ return false
409
+ }
410
+
411
+ // Check test: must be i < arr.length
412
+ if (!j.BinaryExpression.check(node.test)) {
413
+ return false
414
+ }
415
+ if (node.test.operator !== "<") {
416
+ return false
417
+ }
418
+ if (!j.Identifier.check(node.test.left) || node.test.left.name !== indexVar) {
419
+ return false
420
+ }
421
+ if (!j.MemberExpression.check(node.test.right)) {
422
+ return false
423
+ }
424
+ if (
425
+ !j.Identifier.check(node.test.right.property) ||
426
+ node.test.right.property.name !== "length"
427
+ ) {
428
+ return false
429
+ }
430
+ if (!j.Identifier.check(node.test.right.object)) {
431
+ return false
432
+ }
433
+ const arrayVar = node.test.right.object.name
434
+
435
+ // Check update: must be i++ or ++i
436
+ if (j.UpdateExpression.check(node.update)) {
437
+ if (
438
+ !j.Identifier.check(node.update.argument) ||
439
+ node.update.argument.name !== indexVar ||
440
+ node.update.operator !== "++"
441
+ ) {
442
+ return false
443
+ }
444
+ } else {
445
+ return false
446
+ }
447
+
448
+ // Check body: must be a block statement
449
+ if (!j.BlockStatement.check(node.body)) {
450
+ return false
451
+ }
452
+
453
+ // Look for first statement that assigns arr[i] to a variable
454
+ if (node.body.body.length === 0) {
455
+ return false
456
+ }
457
+
458
+ const firstStmt = node.body.body[0]
459
+ if (!j.VariableDeclaration.check(firstStmt)) {
460
+ return false
461
+ }
462
+ if (firstStmt.declarations.length !== 1) {
463
+ return false
464
+ }
465
+ const varDeclarator = firstStmt.declarations[0]
466
+ if (!j.Identifier.check(varDeclarator.id)) {
467
+ return false
468
+ }
469
+ if (!j.MemberExpression.check(varDeclarator.init)) {
470
+ return false
471
+ }
472
+ if (
473
+ !j.Identifier.check(varDeclarator.init.object) ||
474
+ varDeclarator.init.object.name !== arrayVar
475
+ ) {
476
+ return false
477
+ }
478
+ if (
479
+ !j.Identifier.check(varDeclarator.init.property) ||
480
+ varDeclarator.init.property.name !== indexVar ||
481
+ varDeclarator.init.computed !== true
482
+ ) {
483
+ return false
484
+ }
485
+
486
+ // Check that the index variable is not used elsewhere in the body
487
+ const bodyWithoutFirst = node.body.body.slice(1)
488
+ let indexVarUsed = false
489
+
490
+ // Recursively check if identifier is used in AST nodes
491
+ const checkNode = (astNode) => {
492
+ if (!astNode || typeof astNode !== "object") return
493
+
494
+ if (astNode.type === "Identifier" && astNode.name === indexVar) {
495
+ indexVarUsed = true
496
+ return
497
+ }
498
+
499
+ // Traverse all properties
500
+ for (const key in astNode) {
501
+ if (
502
+ key === "loc" ||
503
+ key === "start" ||
504
+ key === "end" ||
505
+ key === "tokens" ||
506
+ key === "comments"
507
+ )
508
+ continue
509
+ const value = astNode[key]
510
+ if (Array.isArray(value)) {
511
+ value.forEach(checkNode)
512
+ } else if (value && typeof value === "object") {
513
+ checkNode(value)
514
+ }
515
+ }
516
+ }
517
+
518
+ bodyWithoutFirst.forEach(checkNode)
519
+
520
+ if (indexVarUsed) {
521
+ return false
522
+ }
523
+
524
+ return true
525
+ })
526
+ .forEach((path) => {
527
+ const node = path.node
528
+ const arrayVar = node.test.right.object.name
529
+ const itemVar = node.body.body[0].declarations[0].id.name
530
+ const itemKind = node.body.body[0].kind
531
+
532
+ // Create new body without the first declaration
533
+ const newBody = j.blockStatement(node.body.body.slice(1))
534
+
535
+ // Create for...of loop
536
+ const forOfLoop = j.forOfStatement(
537
+ j.variableDeclaration(itemKind, [j.variableDeclarator(j.identifier(itemVar))]),
538
+ j.identifier(arrayVar),
539
+ newBody,
540
+ )
541
+
542
+ j(path).replaceWith(forOfLoop)
543
+
544
+ modified = true
545
+ if (node.loc) {
546
+ changes.push({
547
+ type: "forLoopToForOf",
548
+ line: node.loc.start.line,
549
+ })
550
+ }
551
+ })
552
+
553
+ return { modified, changes }
554
+ }
555
+
556
+ /**
557
+ * Transform iterables' forEach() to for...of loop
558
+ * Handles DOM APIs like querySelectorAll, getElementsBy*, etc. and other known iterables
559
+ * Only transforms when forEach callback is declared inline with a function body (block statement)
560
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
561
+ */
562
+ export function iterableForEachToForOf(j, root) {
563
+ let modified = false
564
+ const changes = []
565
+
566
+ // Define known iterable-returning methods by their object/context
567
+ const knownIterableMethods = {
568
+ document: [
569
+ "querySelectorAll",
570
+ "getElementsByTagName",
571
+ "getElementsByClassName",
572
+ "getElementsByName",
573
+ ],
574
+ }
575
+
576
+ // Define known iterable properties
577
+ const knownIterableProperties = {
578
+ window: ["frames"],
579
+ }
580
+
581
+ root
582
+ .find(j.CallExpression)
583
+ .filter((path) => {
584
+ const node = path.node
585
+ // Check if this is a forEach call
586
+ if (
587
+ !j.MemberExpression.check(node.callee) ||
588
+ !j.Identifier.check(node.callee.property) ||
589
+ node.callee.property.name !== "forEach"
590
+ ) {
591
+ return false
592
+ }
593
+
594
+ const object = node.callee.object
595
+
596
+ // Check if this is a property access pattern like window.frames
597
+ if (j.MemberExpression.check(object) && !j.CallExpression.check(object)) {
598
+ const objectName = j.Identifier.check(object.object) ? object.object.name : null
599
+ const propertyName = j.Identifier.check(object.property)
600
+ ? object.property.name
601
+ : null
602
+
603
+ if (
604
+ objectName &&
605
+ propertyName &&
606
+ knownIterableProperties[objectName] &&
607
+ knownIterableProperties[objectName].includes(propertyName)
608
+ ) {
609
+ // This is a valid iterable property like window.frames - continue to callback check
610
+ } else {
611
+ // Not a known iterable property
612
+ return false
613
+ }
614
+ }
615
+ // Check for method call patterns like document.querySelectorAll()
616
+ else if (j.CallExpression.check(object)) {
617
+ // Check if it's a member expression (e.g., document.querySelectorAll)
618
+ if (!j.MemberExpression.check(object.callee)) {
619
+ return false
620
+ }
621
+
622
+ // Get the method name
623
+ const methodName = j.Identifier.check(object.callee.property)
624
+ ? object.callee.property.name
625
+ : null
626
+
627
+ if (!methodName) {
628
+ return false
629
+ }
630
+
631
+ // Verify the object is document only
632
+ const callerObject = object.callee.object
633
+ if (j.Identifier.check(callerObject)) {
634
+ const objectName = callerObject.name
635
+ // Only allow document
636
+ if (objectName !== "document") {
637
+ return false
638
+ }
639
+ // Verify method belongs to document
640
+ if (!knownIterableMethods.document.includes(methodName)) {
641
+ return false
642
+ }
643
+ }
644
+ // Handle cases like document.getElementById('x').querySelectorAll()
645
+ // Only allow these from document-originating chains
646
+ else if (
647
+ j.MemberExpression.check(callerObject) ||
648
+ j.CallExpression.check(callerObject)
649
+ ) {
650
+ // Check if this eventually chains from document
651
+ const isFromDocument = (node) => {
652
+ if (j.Identifier.check(node)) {
653
+ return node.name === "document"
654
+ }
655
+ if (j.MemberExpression.check(node)) {
656
+ return isFromDocument(node.object)
657
+ }
658
+ if (j.CallExpression.check(node)) {
659
+ if (j.MemberExpression.check(node.callee)) {
660
+ return isFromDocument(node.callee.object)
661
+ }
662
+ }
663
+ return false
664
+ }
665
+
666
+ if (!isFromDocument(callerObject)) {
667
+ return false
668
+ }
669
+
670
+ // Verify the method is valid for document
671
+ if (!knownIterableMethods.document.includes(methodName)) {
672
+ return false
673
+ }
674
+ } else {
675
+ return false
676
+ }
677
+ } else {
678
+ return false
679
+ }
680
+
681
+ // Check that forEach has a callback argument
682
+ if (node.arguments.length === 0) {
683
+ return false
684
+ }
685
+
686
+ const callback = node.arguments[0]
687
+ // Only transform if callback is an inline function (arrow or function expression)
688
+ if (
689
+ !j.ArrowFunctionExpression.check(callback) &&
690
+ !j.FunctionExpression.check(callback)
691
+ ) {
692
+ return false
693
+ }
694
+
695
+ // Only transform if the callback has a block statement body (with braces)
696
+ // Arrow functions with expression bodies (e.g., item => item.value) should NOT be transformed
697
+ if (!j.BlockStatement.check(callback.body)) {
698
+ return false
699
+ }
700
+
701
+ // Only transform if callback uses only the first parameter (element)
702
+ // Don't transform if it uses index or array parameters
703
+ const params = callback.params
704
+ if (params.length !== 1) {
705
+ return false
706
+ }
707
+
708
+ return true
709
+ })
710
+ .forEach((path) => {
711
+ const node = path.node
712
+ const iterable = node.callee.object
713
+ const callback = node.arguments[0]
714
+
715
+ const itemParam = callback.params[0]
716
+ const body = callback.body
717
+
718
+ // Create for...of loop
719
+ const forOfLoop = j.forOfStatement(
720
+ j.variableDeclaration("const", [j.variableDeclarator(itemParam)]),
721
+ iterable,
722
+ body,
723
+ )
724
+
725
+ // Replace the expression statement containing the forEach call
726
+ const statement = path.parent
727
+ if (j.ExpressionStatement.check(statement.node)) {
728
+ j(statement).replaceWith(forOfLoop)
729
+
730
+ modified = true
731
+ if (node.loc) {
732
+ changes.push({
733
+ type: "iterableForEachToForOf",
734
+ line: node.loc.start.line,
735
+ })
736
+ }
737
+ }
738
+ })
739
+
740
+ return { modified, changes }
741
+ }
package/tests/cli.test.js CHANGED
@@ -41,7 +41,7 @@ describe("CLI", () => {
41
41
 
42
42
  const transformedCode = fs.readFileSync(testFile, "utf8")
43
43
  assert.match(transformedCode, /const x = 1/)
44
- assert.match(result.stdout, /Summary: 1 file\(s\) upgraded/)
44
+ assert.match(result.stdout, /✓ 1 file\(s\) upgraded/)
45
45
  assert.strictEqual(result.status, 0)
46
46
  })
47
47
 
@@ -57,7 +57,6 @@ describe("CLI", () => {
57
57
  const transformedCode = fs.readFileSync(testFile, "utf8")
58
58
  assert.match(transformedCode, /const x = 1/)
59
59
  assert.doesNotMatch(transformedCode, /Promise\.try/) // Promise.try not in widely-available
60
- assert.match(result.stdout, /widely-available/)
61
60
  assert.strictEqual(result.status, 0)
62
61
  })
63
62
 
@@ -77,7 +76,6 @@ describe("CLI", () => {
77
76
  const transformedCode = fs.readFileSync(testFile, "utf8")
78
77
  assert.match(transformedCode, /const x = 1/)
79
78
  assert.match(transformedCode, /Promise\.try/) // Promise.try in newly-available
80
- assert.match(result.stdout, /newly-available/)
81
79
  assert.strictEqual(result.status, 0)
82
80
  })
83
81
 
@@ -108,7 +106,7 @@ describe("CLI", () => {
108
106
 
109
107
  const fileContent = fs.readFileSync(testFile, "utf8")
110
108
  assert.strictEqual(fileContent, originalCode) // File unchanged
111
- assert.match(result.stdout, /All files are already modern/)
109
+ assert.match(result.stdout, /All files are up to date/)
112
110
  assert.strictEqual(result.status, 0)
113
111
  })
114
112
 
@@ -128,8 +126,7 @@ describe("CLI", () => {
128
126
  const transformedCode = fs.readFileSync(testFile, "utf8")
129
127
  assert.match(transformedCode, /const x = 1/)
130
128
  assert.match(result.stdout, /✗/)
131
- assert.match(result.stdout, /Changes written to file/)
132
- assert.match(result.stdout, /1 file\(s\) upgraded/)
129
+ assert.match(result.stdout, /Changes have been written/)
133
130
  assert.strictEqual(result.status, 1) // Still exit 1 with --check
134
131
  })
135
132
 
@@ -150,7 +147,6 @@ describe("CLI", () => {
150
147
  const transformed2 = fs.readFileSync(file2, "utf8")
151
148
  assert.match(transformed1, /const x = 1/)
152
149
  assert.match(transformed2, /const y = 2/)
153
- assert.match(result.stdout, /Processing 2 file/)
154
150
  assert.match(result.stdout, /2 file\(s\) upgraded/)
155
151
  assert.strictEqual(result.status, 0)
156
152
  })
@@ -172,7 +168,7 @@ describe("CLI", () => {
172
168
  encoding: "utf8",
173
169
  })
174
170
 
175
- assert.match(result.stdout, /Processing 1 file/)
171
+ // Should only process file1 in tempDir, not in node_modules or .git
176
172
  assert.strictEqual(result.status, 0)
177
173
  })
178
174
 
@@ -191,7 +187,6 @@ describe("CLI", () => {
191
187
  encoding: "utf8",
192
188
  })
193
189
 
194
- assert.match(result.stdout, /Processing 4 file/)
195
190
  assert.match(result.stdout, /4 file\(s\) upgraded/)
196
191
  assert.strictEqual(result.status, 0)
197
192
  })
@@ -207,7 +202,6 @@ describe("CLI", () => {
207
202
  encoding: "utf8",
208
203
  })
209
204
 
210
- assert.match(result.stdout, /Processing 2 file/)
211
205
  assert.strictEqual(result.status, 0)
212
206
  })
213
207
 
@@ -250,7 +244,6 @@ describe("CLI", () => {
250
244
  })
251
245
 
252
246
  assert.match(result.stdout, /var to const/)
253
- assert.match(result.stdout, /line/)
254
247
  assert.strictEqual(result.status, 1)
255
248
  })
256
249
 
@@ -268,7 +261,7 @@ describe("CLI", () => {
268
261
  const transformed2 = fs.readFileSync(file2, "utf8")
269
262
  assert.match(transformed1, /const x = 1/)
270
263
  assert.match(transformed2, /const y = 2/)
271
- assert.match(result.stdout, /Processing 2 file/)
264
+ assert.match(result.stdout, /2 file\(s\) upgraded/)
272
265
  assert.strictEqual(result.status, 0)
273
266
  })
274
267
 
@@ -283,7 +276,7 @@ describe("CLI", () => {
283
276
 
284
277
  const fileContent = fs.readFileSync(testFile, "utf8")
285
278
  assert.strictEqual(fileContent, originalCode)
286
- assert.match(result.stdout, /All files are already modern/)
279
+ assert.match(result.stdout, /All files are up to date/)
287
280
  assert.strictEqual(result.status, 0)
288
281
  })
289
282
 
@@ -296,7 +289,7 @@ describe("CLI", () => {
296
289
  encoding: "utf8",
297
290
  })
298
291
 
299
- assert.match(result.stdout, /No changes:/)
292
+ assert.match(result.stdout, /All files are up to date/)
300
293
  assert.strictEqual(result.status, 0)
301
294
  })
302
295
 
@@ -322,7 +315,6 @@ describe("CLI", () => {
322
315
  })
323
316
 
324
317
  assert.match(result.stdout, /var to const/)
325
- assert.match(result.stdout, /lines:/)
326
318
  assert.strictEqual(result.status, 1)
327
319
  })
328
320
 
@@ -335,7 +327,7 @@ describe("CLI", () => {
335
327
  encoding: "utf8",
336
328
  })
337
329
 
338
- assert.match(result.stderr, /Error processing/)
330
+ assert.match(result.stderr, /✗ Error:/)
339
331
  assert.strictEqual(result.status, 0) // CLI continues despite errors
340
332
  })
341
333
 
@@ -352,7 +344,11 @@ describe("CLI", () => {
352
344
  encoding: "utf8",
353
345
  })
354
346
 
355
- assert.match(result.stdout, /Processing 2 file/)
347
+ const file1Content = fs.readFileSync(file1, "utf8")
348
+ const file2Content = fs.readFileSync(file2, "utf8")
349
+ assert.match(file1Content, /const x = 1/)
350
+ assert.match(file2Content, /const y = 2/)
351
+ assert.match(result.stdout, /2 file\(s\) upgraded/)
356
352
  assert.strictEqual(result.status, 0)
357
353
  })
358
354
  })