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/.pre-commit-config.yaml +7 -6
- package/README.md +57 -48
- package/bin/esupgrade.js +7 -21
- package/images/logo-dark.svg +3 -4
- package/images/logo-light.svg +1 -2
- package/package.json +1 -1
- package/src/index.js +18 -689
- package/src/newlyAvailable.js +154 -0
- package/src/widelyAvailable.js +265 -0
- package/tests/cli.test.js +358 -0
- package/tests/index.test.js +274 -0
- package/tests/newlyAvailable.test.js +221 -0
- package/tests/widelyAvailable.test.js +428 -0
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/esupgrade.iml +0 -8
- package/.idea/inspectionProfiles/Project_Default.xml +0 -28
- package/.idea/inspectionProfiles/profiles_settings.xml +0 -6
- package/.idea/misc.xml +0 -7
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -7
- package/tests/transform.test.js +0 -322
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
669
|
-
* @
|
|
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,
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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 }
|