esupgrade 2025.0.0 → 2025.0.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.
package/src/index.js CHANGED
@@ -1,704 +1,35 @@
1
1
  import jscodeshift from "jscodeshift"
2
+ import * as widelyAvailable from "./widelyAvailable.js"
3
+ import * as newlyAvailable from "./newlyAvailable.js"
2
4
 
3
5
  /**
4
- * Baseline levels for ECMAScript features
5
- *
6
- * - widely-available: Features available across all modern browsers
7
- * - newly-available: Newly standardized features (e.g., Promise.try)
6
+ * Result of a transformation.
7
+ * @typedef {Object} TransformResult
8
+ * @property {string} code - The transformed code
9
+ * @property {boolean} modified - Whether the code was modified
10
+ * @property {Array} changes - List of changes made
8
11
  */
9
- const BASELINE_LEVELS = {
10
- "widely-available": [
11
- "varToConst",
12
- "concatToTemplateLiteral",
13
- "objectAssignToSpread",
14
- "arrayFromForEachToForOf",
15
- "forEachToForOf",
16
- "forOfKeysToForIn",
17
- ],
18
- "newly-available": [
19
- "varToConst",
20
- "concatToTemplateLiteral",
21
- "objectAssignToSpread",
22
- "arrayFromForEachToForOf",
23
- "forEachToForOf",
24
- "forOfKeysToForIn",
25
- "promiseTry",
26
- ],
27
- }
28
-
29
- /**
30
- * Transform var to const
31
- */
32
- function varToConst(j, root) {
33
- let modified = false
34
- const changes = []
35
-
36
- root.find(j.VariableDeclaration, { kind: "var" }).forEach((path) => {
37
- path.node.kind = "const"
38
- modified = true
39
- if (path.node.loc) {
40
- changes.push({
41
- type: "varToConst",
42
- line: path.node.loc.start.line,
43
- })
44
- }
45
- })
46
-
47
- return { modified, changes }
48
- }
49
-
50
- /**
51
- * Transform string concatenation to template literals
52
- */
53
- function concatToTemplateLiteral(j, root) {
54
- let modified = false
55
- const changes = []
56
-
57
- root
58
- .find(j.BinaryExpression, { operator: "+" })
59
- .filter((path) => {
60
- // Only transform if at least one operand is a string literal
61
- const hasStringLiteral = (node) => {
62
- if (
63
- j.StringLiteral.check(node) ||
64
- (j.Literal.check(node) && typeof node.value === "string")
65
- ) {
66
- return true
67
- }
68
- if (j.BinaryExpression.check(node) && node.operator === "+") {
69
- return hasStringLiteral(node.left) || hasStringLiteral(node.right)
70
- }
71
- return false
72
- }
73
- return hasStringLiteral(path.node)
74
- })
75
- .forEach((path) => {
76
- const parts = []
77
- const expressions = []
78
-
79
- const flatten = (node) => {
80
- if (j.BinaryExpression.check(node) && node.operator === "+") {
81
- flatten(node.left)
82
- flatten(node.right)
83
- } else if (
84
- j.StringLiteral.check(node) ||
85
- (j.Literal.check(node) && typeof node.value === "string")
86
- ) {
87
- // Add string literal value
88
- if (parts.length === 0 || expressions.length >= parts.length) {
89
- parts.push(node.value)
90
- } else {
91
- parts[parts.length - 1] += node.value
92
- }
93
- } else {
94
- // Add expression
95
- if (parts.length === 0) {
96
- parts.push("")
97
- }
98
- expressions.push(node)
99
- }
100
- }
101
-
102
- flatten(path.node)
103
-
104
- // Ensure we have the right number of quasis (one more than expressions)
105
- while (parts.length <= expressions.length) {
106
- parts.push("")
107
- }
108
-
109
- // Create template literal
110
- const quasis = parts.map((part, i) =>
111
- j.templateElement({ raw: part, cooked: part }, i === parts.length - 1),
112
- )
113
-
114
- const templateLiteral = j.templateLiteral(quasis, expressions)
115
- j(path).replaceWith(templateLiteral)
116
-
117
- modified = true
118
- if (path.node.loc) {
119
- changes.push({
120
- type: "concatToTemplateLiteral",
121
- line: path.node.loc.start.line,
122
- })
123
- }
124
- })
125
-
126
- return { modified, changes }
127
- }
128
-
129
- /**
130
- * Transform Object.assign({}, ...) to object spread
131
- */
132
- function objectAssignToSpread(j, root) {
133
- let modified = false
134
- const changes = []
135
-
136
- root
137
- .find(j.CallExpression, {
138
- callee: {
139
- type: "MemberExpression",
140
- object: { name: "Object" },
141
- property: { name: "assign" },
142
- },
143
- })
144
- .filter((path) => {
145
- // First argument must be empty object literal
146
- const firstArg = path.node.arguments[0]
147
- return j.ObjectExpression.check(firstArg) && firstArg.properties.length === 0
148
- })
149
- .forEach((path) => {
150
- const spreadProperties = path.node.arguments
151
- .slice(1)
152
- .map((arg) => j.spreadElement(arg))
153
-
154
- const objectExpression = j.objectExpression(spreadProperties)
155
- j(path).replaceWith(objectExpression)
156
-
157
- modified = true
158
- if (path.node.loc) {
159
- changes.push({
160
- type: "objectAssignToSpread",
161
- line: path.node.loc.start.line,
162
- })
163
- }
164
- })
165
-
166
- return { modified, changes }
167
- }
168
-
169
- /**
170
- * Transform Array.from().forEach() to for...of
171
- */
172
- function arrayFromForEachToForOf(j, root) {
173
- let modified = false
174
- const changes = []
175
-
176
- root
177
- .find(j.CallExpression)
178
- .filter((path) => {
179
- const node = path.node
180
- // Check if this is a forEach call
181
- if (
182
- !j.MemberExpression.check(node.callee) ||
183
- !j.Identifier.check(node.callee.property) ||
184
- node.callee.property.name !== "forEach"
185
- ) {
186
- return false
187
- }
188
-
189
- // Check if the object is Array.from()
190
- const object = node.callee.object
191
- if (
192
- !j.CallExpression.check(object) ||
193
- !j.MemberExpression.check(object.callee) ||
194
- !j.Identifier.check(object.callee.object) ||
195
- object.callee.object.name !== "Array" ||
196
- !j.Identifier.check(object.callee.property) ||
197
- object.callee.property.name !== "from"
198
- ) {
199
- return false
200
- }
201
-
202
- return true
203
- })
204
- .forEach((path) => {
205
- const node = path.node
206
- const iterable = node.callee.object.arguments[0]
207
- const callback = node.arguments[0]
208
-
209
- // Only transform if callback is a function
210
- if (
211
- callback &&
212
- (j.ArrowFunctionExpression.check(callback) ||
213
- j.FunctionExpression.check(callback))
214
- ) {
215
- // Only transform if:
216
- // 1. Callback has exactly 1 parameter (element only), OR
217
- // 2. Callback has 2+ params AND first param is a destructuring pattern (e.g., [key, value])
218
- // This handles cases like Array.from(Object.entries(obj)).forEach(([k, v]) => ...)
219
- const params = callback.params
220
- const canTransform =
221
- params.length === 1 || (params.length >= 2 && j.ArrayPattern.check(params[0]))
222
-
223
- if (canTransform) {
224
- const itemParam = callback.params[0]
225
- const body = callback.body
226
-
227
- // Create for...of loop
228
- const forOfLoop = j.forOfStatement(
229
- j.variableDeclaration("const", [j.variableDeclarator(itemParam)]),
230
- iterable,
231
- j.BlockStatement.check(body)
232
- ? body
233
- : j.blockStatement([j.expressionStatement(body)]),
234
- )
235
-
236
- // Replace the expression statement containing the forEach call
237
- const statement = path.parent
238
- if (j.ExpressionStatement.check(statement.node)) {
239
- j(statement).replaceWith(forOfLoop)
240
-
241
- modified = true
242
- if (node.loc) {
243
- changes.push({
244
- type: "arrayFromForEachToForOf",
245
- line: node.loc.start.line,
246
- })
247
- }
248
- }
249
- }
250
- }
251
- })
252
-
253
- return { modified, changes }
254
- }
255
-
256
- /**
257
- * Helper function to check if an expression is definitively an array or iterable
258
- *
259
- * This function is conservative - it only returns true for expressions that we can
260
- * statically determine are iterable. This prevents transforming forEach calls on
261
- * objects that implement forEach but are not iterable (like jscodeshift's Collection).
262
- */
263
- function isDefinitelyArrayOrIterable(j, node) {
264
- // Array literal - definitely iterable
265
- if (j.ArrayExpression.check(node)) {
266
- return true
267
- }
268
-
269
- // Call expressions that return arrays/iterables
270
- if (j.CallExpression.check(node)) {
271
- const callee = node.callee
272
-
273
- if (j.MemberExpression.check(callee)) {
274
- // Array.from(), Array.of() - definitely return arrays
275
- if (j.Identifier.check(callee.object) && callee.object.name === "Array") {
276
- return true
277
- }
278
-
279
- // Array methods that return arrays
280
- const arrayMethods = [
281
- "filter",
282
- "map",
283
- "slice",
284
- "concat",
285
- "flat",
286
- "flatMap",
287
- "splice",
288
- "reverse",
289
- "sort",
290
- ]
291
- if (
292
- j.Identifier.check(callee.property) &&
293
- arrayMethods.includes(callee.property.name)
294
- ) {
295
- return true
296
- }
297
-
298
- // String methods that return arrays
299
- if (
300
- j.Identifier.check(callee.property) &&
301
- (callee.property.name === "split" || callee.property.name === "match")
302
- ) {
303
- return true
304
- }
305
-
306
- // Object.keys(), Object.values(), Object.entries() - these return arrays
307
- if (
308
- j.Identifier.check(callee.object) &&
309
- callee.object.name === "Object" &&
310
- j.Identifier.check(callee.property) &&
311
- (callee.property.name === "keys" ||
312
- callee.property.name === "values" ||
313
- callee.property.name === "entries")
314
- ) {
315
- return true
316
- }
317
-
318
- // document.querySelectorAll() returns NodeList (iterable)
319
- // document.getElementsBy* returns HTMLCollection (iterable)
320
- if (
321
- j.Identifier.check(callee.property) &&
322
- (callee.property.name === "querySelectorAll" ||
323
- callee.property.name === "getElementsByTagName" ||
324
- callee.property.name === "getElementsByClassName" ||
325
- callee.property.name === "getElementsByName")
326
- ) {
327
- return true
328
- }
329
- }
330
- }
331
-
332
- // Member expressions for known iterable properties
333
- if (j.MemberExpression.check(node)) {
334
- const property = node.property
335
- if (
336
- j.Identifier.check(property) &&
337
- (property.name === "children" || property.name === "childNodes")
338
- ) {
339
- return true
340
- }
341
- }
342
-
343
- // New expressions for known iterables
344
- if (j.NewExpression.check(node)) {
345
- if (j.Identifier.check(node.callee)) {
346
- const constructorName = node.callee.name
347
- // Set, Map, Array, WeakSet - all iterable
348
- // Note: Map has different forEach signature (value, key), so it's handled separately
349
- if (
350
- constructorName === "Set" ||
351
- constructorName === "Array" ||
352
- constructorName === "WeakSet"
353
- ) {
354
- return true
355
- }
356
- }
357
- }
358
-
359
- // Do NOT use heuristics like variable names - we can't be sure from the name alone
360
- // For example, a variable named "items" could be a jscodeshift Collection,
361
- // which has forEach but is not iterable
362
- return false
363
- }
364
-
365
- /**
366
- * Transform all .forEach() calls to for...of loops
367
- * Only transforms when the object is likely an array or iterable
368
- */
369
- function forEachToForOf(j, root) {
370
- let modified = false
371
- const changes = []
372
-
373
- root
374
- .find(j.CallExpression)
375
- .filter((path) => {
376
- const node = path.node
377
- // Check if this is a forEach call
378
- if (
379
- !j.MemberExpression.check(node.callee) ||
380
- !j.Identifier.check(node.callee.property) ||
381
- node.callee.property.name !== "forEach"
382
- ) {
383
- return false
384
- }
385
-
386
- // Skip Array.from().forEach() as it's handled by arrayFromForEachToForOf
387
- const object = node.callee.object
388
- if (
389
- j.CallExpression.check(object) &&
390
- j.MemberExpression.check(object.callee) &&
391
- j.Identifier.check(object.callee.object) &&
392
- object.callee.object.name === "Array" &&
393
- j.Identifier.check(object.callee.property) &&
394
- object.callee.property.name === "from"
395
- ) {
396
- return false
397
- }
398
-
399
- // Only transform if the object is definitely an array or iterable
400
- if (!isDefinitelyArrayOrIterable(j, object)) {
401
- return false
402
- }
403
-
404
- return true
405
- })
406
- .forEach((path) => {
407
- const node = path.node
408
- const iterable = node.callee.object
409
- const callback = node.arguments[0]
410
-
411
- // Only transform if callback is a function
412
- if (
413
- callback &&
414
- (j.ArrowFunctionExpression.check(callback) ||
415
- j.FunctionExpression.check(callback))
416
- ) {
417
- // Only transform if:
418
- // 1. Callback has exactly 1 parameter (element only), OR
419
- // 2. Callback has 2+ params AND first param is a destructuring pattern
420
- const params = callback.params
421
- const canTransform =
422
- params.length === 1 || (params.length >= 2 && j.ArrayPattern.check(params[0]))
423
-
424
- if (canTransform) {
425
- const itemParam = callback.params[0]
426
- const body = callback.body
427
-
428
- // Create for...of loop
429
- const forOfLoop = j.forOfStatement(
430
- j.variableDeclaration("const", [j.variableDeclarator(itemParam)]),
431
- iterable,
432
- j.BlockStatement.check(body)
433
- ? body
434
- : j.blockStatement([j.expressionStatement(body)]),
435
- )
436
-
437
- // Replace the expression statement containing the forEach call
438
- const statement = path.parent
439
- if (j.ExpressionStatement.check(statement.node)) {
440
- j(statement).replaceWith(forOfLoop)
441
-
442
- modified = true
443
- if (node.loc) {
444
- changes.push({
445
- type: "forEachToForOf",
446
- line: node.loc.start.line,
447
- })
448
- }
449
- }
450
- }
451
- }
452
- })
453
-
454
- return { modified, changes }
455
- }
456
-
457
- /**
458
- * Transform for...of Object.keys() loops to for...in
459
- */
460
- function forOfKeysToForIn(j, root) {
461
- let modified = false
462
- const changes = []
463
-
464
- root
465
- .find(j.ForOfStatement)
466
- .filter((path) => {
467
- const node = path.node
468
- const right = node.right
469
-
470
- // Check if iterating over Object.keys() call
471
- if (
472
- j.CallExpression.check(right) &&
473
- j.MemberExpression.check(right.callee) &&
474
- j.Identifier.check(right.callee.object) &&
475
- right.callee.object.name === "Object" &&
476
- j.Identifier.check(right.callee.property) &&
477
- right.callee.property.name === "keys" &&
478
- right.arguments.length === 1
479
- ) {
480
- return true
481
- }
482
-
483
- return false
484
- })
485
- .forEach((path) => {
486
- const node = path.node
487
- const left = node.left
488
- const objectArg = node.right.arguments[0]
489
- const body = node.body
490
-
491
- // Create for...in loop
492
- const forInLoop = j.forInStatement(left, objectArg, body)
493
-
494
- j(path).replaceWith(forInLoop)
495
-
496
- modified = true
497
- if (node.loc) {
498
- changes.push({
499
- type: "forOfKeysToForIn",
500
- line: node.loc.start.line,
501
- })
502
- }
503
- })
504
-
505
- return { modified, changes }
506
- }
507
-
508
- /**
509
- * Transform new Promise((resolve, reject) => { resolve(fn()) }) to Promise.try(fn)
510
- */
511
- function promiseTry(j, root) {
512
- let modified = false
513
- const changes = []
514
-
515
- root
516
- .find(j.NewExpression)
517
- .filter((path) => {
518
- const node = path.node
519
- // Check if this is new Promise(...)
520
- if (!j.Identifier.check(node.callee) || node.callee.name !== "Promise") {
521
- return false
522
- }
523
-
524
- // Skip if this Promise is being awaited
525
- // Check if parent is an AwaitExpression
526
- if (path.parent && j.AwaitExpression.check(path.parent.node)) {
527
- return false
528
- }
529
-
530
- // Check if there's one argument that's a function
531
- if (node.arguments.length !== 1) {
532
- return false
533
- }
534
-
535
- const executor = node.arguments[0]
536
- if (
537
- !j.ArrowFunctionExpression.check(executor) &&
538
- !j.FunctionExpression.check(executor)
539
- ) {
540
- return false
541
- }
542
-
543
- // Check if function has 1-2 params (resolve, reject)
544
- if (executor.params.length < 1 || executor.params.length > 2) {
545
- return false
546
- }
547
-
548
- // Check if body is a block with single resolve() call or expression body
549
- const body = executor.body
550
-
551
- // For arrow functions with expression body: (resolve) => expr
552
- if (!j.BlockStatement.check(body)) {
553
- // Check if expression is resolve(something) or func(resolve)
554
- if (j.CallExpression.check(body)) {
555
- const callExpr = body
556
- // Pattern: (resolve) => resolve(expr)
557
- if (
558
- j.Identifier.check(callExpr.callee) &&
559
- j.Identifier.check(executor.params[0]) &&
560
- callExpr.callee.name === executor.params[0].name &&
561
- callExpr.arguments.length > 0
562
- ) {
563
- return true
564
- }
565
- // Pattern: (resolve) => func(resolve) - resolve must be the ONLY argument
566
- if (
567
- callExpr.arguments.length === 1 &&
568
- j.Identifier.check(callExpr.arguments[0]) &&
569
- j.Identifier.check(executor.params[0]) &&
570
- callExpr.arguments[0].name === executor.params[0].name
571
- ) {
572
- return true
573
- }
574
- }
575
- return false
576
- }
577
-
578
- // For functions with block body containing single resolve(expr) call
579
- if (body.body.length === 1 && j.ExpressionStatement.check(body.body[0])) {
580
- const expr = body.body[0].expression
581
- if (
582
- j.CallExpression.check(expr) &&
583
- j.Identifier.check(expr.callee) &&
584
- expr.callee.name === executor.params[0].name
585
- ) {
586
- return true
587
- }
588
- }
589
-
590
- return false
591
- })
592
- .forEach((path) => {
593
- const node = path.node
594
- const executor = node.arguments[0]
595
- const body = executor.body
596
- const resolveParam = executor.params[0]
597
-
598
- let expression
599
- let tryArg
600
-
601
- // Extract the expression
602
- if (!j.BlockStatement.check(body)) {
603
- // Arrow function with expression body: (resolve) => expr
604
- expression = body
605
-
606
- // Check if expression is a call where resolve is passed as the only argument
607
- // e.g., (resolve) => setTimeout(resolve) should become Promise.try(setTimeout)
608
- if (
609
- j.CallExpression.check(expression) &&
610
- expression.arguments.length === 1 &&
611
- j.Identifier.check(expression.arguments[0]) &&
612
- j.Identifier.check(resolveParam) &&
613
- expression.arguments[0].name === resolveParam.name
614
- ) {
615
- // Use the callee directly (e.g., setTimeout)
616
- tryArg = expression.callee
617
- }
618
- // Check if expression is resolve(something)
619
- else if (
620
- j.CallExpression.check(expression) &&
621
- j.Identifier.check(expression.callee) &&
622
- j.Identifier.check(resolveParam) &&
623
- expression.callee.name === resolveParam.name &&
624
- expression.arguments.length > 0
625
- ) {
626
- // Extract the argument from resolve(arg) and wrap in arrow function
627
- expression = expression.arguments[0]
628
- tryArg = j.arrowFunctionExpression([], expression)
629
- } else {
630
- // Wrap expression in arrow function
631
- tryArg = j.arrowFunctionExpression([], expression)
632
- }
633
- } else if (body.body.length === 1 && j.ExpressionStatement.check(body.body[0])) {
634
- // Block with resolve(expr) call
635
- const callExpr = body.body[0].expression
636
- if (j.CallExpression.check(callExpr) && callExpr.arguments.length > 0) {
637
- expression = callExpr.arguments[0]
638
- // Wrap expression in arrow function for Promise.try
639
- tryArg = j.arrowFunctionExpression([], expression)
640
- }
641
- }
642
-
643
- if (tryArg) {
644
- // Create Promise.try(fn)
645
- const promiseTryCall = j.callExpression(
646
- j.memberExpression(j.identifier("Promise"), j.identifier("try")),
647
- [tryArg],
648
- )
649
-
650
- j(path).replaceWith(promiseTryCall)
651
-
652
- modified = true
653
- if (node.loc) {
654
- changes.push({
655
- type: "promiseTry",
656
- line: node.loc.start.line,
657
- })
658
- }
659
- }
660
- })
661
-
662
- return { modified, changes }
663
- }
664
12
 
665
13
  /**
666
14
  * Transform JavaScript code using the specified transformers
667
15
  * @param {string} code - The source code to transform
668
- * @param {Object} options - Transformation options
669
- * @param {string} options.baseline - Baseline level ('widely-available' or 'newly-available')
670
- * @returns {Object} - Object with { code, modified, changes }
16
+ * @param {string} baseline - Baseline level ('widely-available' or 'newly-available')
17
+ * @returns {TransformResult} - Object with { code, modified, changes }
671
18
  */
672
- function transform(code, options = {}) {
673
- const baseline = options.baseline || "widely-available"
674
- const enabledTransformers =
675
- BASELINE_LEVELS[baseline] || BASELINE_LEVELS["widely-available"]
676
-
19
+ export function transform(code, baseline = "widely-available") {
677
20
  const j = jscodeshift.withParser("tsx")
678
21
  const root = j(code)
679
22
 
680
23
  let modified = false
681
24
  const allChanges = []
682
-
683
- // Apply transformers
684
- const transformerFunctions = {
685
- varToConst,
686
- concatToTemplateLiteral,
687
- objectAssignToSpread,
688
- arrayFromForEachToForOf,
689
- forEachToForOf,
690
- forOfKeysToForIn,
691
- promiseTry,
692
- }
693
-
694
- for (const transformerName of enabledTransformers) {
695
- const transformer = transformerFunctions[transformerName]
696
- if (transformer) {
697
- const result = transformer(j, root)
698
- if (result.modified) {
699
- modified = true
700
- allChanges.push(...result.changes)
701
- }
25
+ let transformers = widelyAvailable
26
+ if (baseline === "newly-available")
27
+ transformers = { ...widelyAvailable, ...newlyAvailable }
28
+ for (const name in transformers) {
29
+ const result = transformers[name](j, root)
30
+ if (result.modified) {
31
+ modified = true
32
+ allChanges.push(...result.changes)
702
33
  }
703
34
  }
704
35
 
@@ -708,5 +39,3 @@ function transform(code, options = {}) {
708
39
  changes: allChanges,
709
40
  }
710
41
  }
711
-
712
- export { transform, BASELINE_LEVELS }