esupgrade 2025.20.0 → 2025.21.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.
@@ -5,12 +5,12 @@ name: SuperJoe
5
5
  description: CodingJoe's digital clone following his coding guidelines and best practices.
6
6
  ---
7
7
 
8
-
9
8
  # SuperJoe
10
9
 
11
10
  ## Planning
12
11
 
13
12
  You MUST ALWAYS follow the `naming-things` guidelines. Use the following command to access the guidelines:
13
+
14
14
  ```console
15
15
  curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | head -n 500
16
16
  ```
@@ -44,7 +44,6 @@ Avoid functions or other code inside functions.
44
44
  Avoid if-statements in favor of switch/match-statements or polymorphism.
45
45
  Do not assign names to objects which are returned in the next line.
46
46
 
47
-
48
47
  ## Python
49
48
 
50
49
  Follow PEP 8 guidelines for code style.
@@ -28,7 +28,7 @@ jobs:
28
28
  node-version-file: package.json
29
29
  - run: npm ci
30
30
  - run: node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=lcov.info
31
- - uses: codecov/codecov-action@v6
31
+ - uses: codecov/codecov-action@v7
32
32
  with:
33
33
  token: ${{ secrets.CODECOV_TOKEN }}
34
34
  flags: javascript
@@ -18,10 +18,10 @@ repos:
18
18
  hooks:
19
19
  - id: mdformat
20
20
  additional_dependencies:
21
+ - mdformat-front-matters
21
22
  - mdformat-footnote
22
23
  - mdformat-gfm
23
24
  - mdformat-gfm-alerts
24
- exclude: '.github/agents/'
25
25
  - repo: https://github.com/google/yamlfmt
26
26
  rev: v0.21.0
27
27
  hooks:
@@ -32,7 +32,7 @@ repos:
32
32
  - id: write-good
33
33
  args: [--no-passive]
34
34
  - repo: https://github.com/pre-commit/mirrors-eslint
35
- rev: v10.4.0
35
+ rev: v10.4.1
36
36
  hooks:
37
37
  - id: eslint
38
38
  args: ["--fix"]
package/README.md CHANGED
@@ -10,10 +10,14 @@
10
10
 
11
11
  Keeping your JavaScript and TypeScript code up to date with full browser compatibility.
12
12
 
13
+ ## Sponsors
14
+
15
+ [![Sponsors](https://django.the-box.sh/sponsors/codingjoe/esupgrade.svg)](https://github.com/sponsors/codingjoe)
16
+
13
17
  ## Usage
14
18
 
15
19
  esupgrade is safe and meant to be used automatically on your codebase.
16
- We recommend integrating it into your development workflow using [pre-commit].
20
+ We recommend integrating it into your development workflow using [pre-commit] or [husky].
17
21
 
18
22
  To try it out on a repository without writing changes, run:
19
23
 
@@ -40,6 +44,15 @@ repos:
40
44
  pre-commit run esupgrade --all-files
41
45
  ```
42
46
 
47
+ ### Husky
48
+
49
+ Assuming Husky is already initialized and `.husky/pre-commit` already contains `set -e`, append:
50
+
51
+ ```bash
52
+ echo "git diff --cached --name-only --diff-filter=ACMR -z -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' | xargs -0 sh -c 'test \"\$#\" -eq 0 && exit 0; npx esupgrade -- \"\$@\"' sh" >> .husky/pre-commit
53
+ echo "git diff --cached --name-only --diff-filter=ACMR -z -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' | xargs -0 sh -c 'test \"\$#\" -eq 0 && exit 0; git add -- \"\$@\"' sh" >> .husky/pre-commit
54
+ ```
55
+
43
56
  ### CLI
44
57
 
45
58
  ```bash
@@ -220,12 +233,21 @@ Supports:
220
233
  ```diff
221
234
  -const budget = 1000000;
222
235
  -const maxUsers = 1000000n;
236
+ -const permission = 0o1234567;
237
+ -const mask = 0xabcdef;
238
+ -const flags = 0b1010000111000011;
239
+ -const ratio = 100000.25;
240
+ -const large = 1e100000;
223
241
  +const budget = 1_000_000;
224
242
  +const maxUsers = 1_000_000n;
243
+ +const permission = 0o1_234_567;
244
+ +const mask = 0xab_cd_ef;
245
+ +const flags = 0b10100001_11000011;
246
+ +const ratio = 100_000.25;
247
+ +const large = 1e100_000;
225
248
  ```
226
249
 
227
- esupgrade transforms decimal integer and bigint literals with at least five digits.
228
- Fractional, exponential, hexadecimal, octal, binary, and already formatted literals are left unchanged.
250
+ Decimal and octal numerals are grouped by triplets; hex and binary by byte.
229
251
 
230
252
  #### `Array.slice(0)` → [Array spread [...]][mdn-spread]
231
253
 
@@ -380,6 +402,7 @@ Transforms constructor functions (both function declarations and variable declar
380
402
 
381
403
  - Function name starts with an uppercase letter
382
404
  - At least one prototype method is defined
405
+ - Prototype methods are matched only to constructors declared in the current or an ancestor lexical scope (never sibling or child scopes)
383
406
  - Prototype methods using `this` in arrow functions are skipped
384
407
  - Prototype object literals with getters, setters, or computed properties are skipped
385
408
 
@@ -734,6 +757,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
734
757
  [baseline]: https://web.dev/baseline/
735
758
  [calver]: https://calver.org/
736
759
  [django-upgrade]: https://github.com/adamchainz/django-upgrade
760
+ [husky]: https://typicode.github.io/husky/
737
761
  [mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
738
762
  [mdn-async-await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
739
763
  [mdn-at]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at
package/SKILL.md ADDED
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: esupgrade
3
+ description: Auto-update JavaScript and TypeScript syntax to new ECMAScript features based on browser support.
4
+ ---
5
+
6
+ Use the CLI with `npx` on files or directories:
7
+
8
+ ```console
9
+ npx esupgrade [--baseline <newly-available|widely-available>] [--check] [--write] <files-or-directories>
10
+ ```
11
+
12
+ - Use `--check` to preview changes and fail when updates are needed.
13
+ - Use `--write` to apply updates in place.
14
+ - Use `--baseline` to choose `newly-available` or `widely-available` (default).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.20.0",
3
+ "version": "2025.21.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,33 +1,67 @@
1
1
  import { default as j } from "jscodeshift"
2
2
  import { NodeTest } from "../types.js"
3
3
 
4
- /**
5
- * Transform old-school constructor functions with prototype methods to ES6 class
6
- * syntax.
7
- *
8
- * @param {import("jscodeshift").Collection} root - The root AST collection
9
- * @returns {boolean} True if code was modified
10
- * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
11
- */
12
- export function constructorToClass(root) {
13
- function findConstructors(root) {
14
- const constructors = new Map()
15
-
16
- root.find(j.FunctionDeclaration).forEach((path) => {
17
- if (
18
- !new NodeTest(path.node.id).isConstructorName() ||
19
- !new NodeTest(path.node.body).hasSimpleConstructorBody()
20
- ) {
21
- return
22
- }
4
+ function getDeclarationScope(path) {
5
+ return j.FunctionDeclaration.check(path.node)
6
+ ? (path.scope.parent ?? path.scope)
7
+ : path.scope
8
+ }
23
9
 
24
- constructors.set(path.node.id.name, {
25
- declaration: path,
26
- prototypeMethods: [],
27
- })
28
- })
10
+ function addConstructor(constructorsByScope, constructorName, declarationPath) {
11
+ const scopeNode = getDeclarationScope(declarationPath).path.node
12
+ const constructorsInScope = constructorsByScope.get(scopeNode) ?? new Map()
13
+
14
+ if (constructorsInScope.has(constructorName)) {
15
+ constructorsInScope.set(constructorName, null)
16
+ constructorsByScope.set(scopeNode, constructorsInScope)
17
+ return
18
+ }
19
+
20
+ const constructorInfo = {
21
+ constructorName,
22
+ declaration: declarationPath,
23
+ prototypeMethods: [],
24
+ }
25
+
26
+ constructorsInScope.set(constructorName, constructorInfo)
27
+ constructorsByScope.set(scopeNode, constructorsInScope)
28
+ }
29
+
30
+ function getConstructor(constructorsByScope, path, constructorName) {
31
+ let activeScope = path.scope
29
32
 
30
- root.find(j.VariableDeclaration).forEach((path) => {
33
+ while (activeScope) {
34
+ const constructorsInScope = constructorsByScope.get(activeScope.path.node)
35
+ const hasConstructorInScope = constructorsInScope?.has(constructorName)
36
+
37
+ if (hasConstructorInScope) {
38
+ return constructorsInScope.get(constructorName)
39
+ }
40
+
41
+ activeScope = activeScope.parent
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ function findConstructors(root) {
48
+ const constructorsByScope = new Map()
49
+
50
+ root.find(j.FunctionDeclaration).forEach((path) => {
51
+ if (
52
+ !new NodeTest(path.node.id).isConstructorName() ||
53
+ !new NodeTest(path.node.body).hasSimpleConstructorBody()
54
+ ) {
55
+ return
56
+ }
57
+
58
+ addConstructor(constructorsByScope, path.node.id.name, path)
59
+ })
60
+
61
+ root
62
+ .find(j.VariableDeclaration)
63
+ .filter((path) => path.node.declarations.length === 1)
64
+ .forEach((path) => {
31
65
  path.node.declarations.forEach((declarator) => {
32
66
  if (
33
67
  !new NodeTest(declarator.id).isConstructorName() ||
@@ -37,153 +71,166 @@ export function constructorToClass(root) {
37
71
  return
38
72
  }
39
73
 
40
- constructors.set(declarator.id.name, {
41
- declaration: path,
42
- prototypeMethods: [],
43
- })
74
+ addConstructor(constructorsByScope, declarator.id.name, path)
44
75
  })
45
76
  })
46
77
 
47
- return constructors
48
- }
78
+ return constructorsByScope
79
+ }
49
80
 
50
- /**
51
- * Find and associate prototype methods with constructors.
52
- *
53
- * @param {import("jscodeshift").Collection} root - The root AST collection.
54
- * @param {Map<
55
- * string,
56
- * { declaration: import("ast-types").NodePath; prototypeMethods: any[] }
57
- * >} constructors
58
- * - Map of constructors.
59
- */
60
- function findPrototypeMethods(root, constructors) {
61
- // Pattern 1: ConstructorName.prototype.methodName = ...
62
- root
63
- .find(j.ExpressionStatement)
64
- .filter(({ node }) => {
65
- if (!j.AssignmentExpression.check(node.expression)) {
66
- return false
67
- }
81
+ /**
82
+ * Find and associate prototype methods with constructors.
83
+ *
84
+ * @param {import("jscodeshift").Collection} root - The root AST collection.
85
+ * @param {Map<
86
+ * object,
87
+ * Map<
88
+ * string,
89
+ * {
90
+ * constructorName: string
91
+ * declaration: import("ast-types").NodePath
92
+ * prototypeMethods: any[]
93
+ * }
94
+ * >
95
+ * >} constructorsByScope
96
+ * - Map of constructors grouped by lexical scope.
97
+ */
98
+ function findPrototypeMethods(root, constructorsByScope) {
99
+ // Pattern 1: ConstructorName.prototype.methodName = ...
100
+ root.find(j.ExpressionStatement).forEach((path) => {
101
+ const { node } = path
102
+
103
+ if (!j.AssignmentExpression.check(node.expression)) {
104
+ return
105
+ }
106
+
107
+ const assignment = node.expression
108
+ const left = assignment.left
109
+
110
+ if (
111
+ !j.MemberExpression.check(left) ||
112
+ !j.MemberExpression.check(left.object) ||
113
+ !j.Identifier.check(left.object.object) ||
114
+ !j.Identifier.check(left.object.property) ||
115
+ left.object.property.name !== "prototype" ||
116
+ !j.Identifier.check(left.property)
117
+ ) {
118
+ return
119
+ }
120
+
121
+ const constructorName = left.object.object.name
122
+ const constructorInfo = getConstructor(constructorsByScope, path, constructorName)
123
+
124
+ if (!constructorInfo) {
125
+ return
126
+ }
127
+
128
+ const methodName = left.property.name
129
+ const methodValue = assignment.right
130
+
131
+ if (!new NodeTest(methodValue).canBeClassMethod()) {
132
+ return
133
+ }
134
+
135
+ constructorInfo.prototypeMethods.push({
136
+ path,
137
+ methodName,
138
+ methodValue,
139
+ })
140
+ })
68
141
 
69
- const assignment = node.expression
70
- const left = assignment.left
142
+ // Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
143
+ root.find(j.ExpressionStatement).forEach((path) => {
144
+ const { node } = path
71
145
 
72
- if (
73
- !j.MemberExpression.check(left) ||
74
- !j.MemberExpression.check(left.object) ||
75
- !j.Identifier.check(left.object.object) ||
76
- !j.Identifier.check(left.object.property) ||
77
- left.object.property.name !== "prototype" ||
78
- !j.Identifier.check(left.property)
79
- ) {
80
- return false
81
- }
146
+ if (!j.AssignmentExpression.check(node.expression)) {
147
+ return
148
+ }
82
149
 
83
- const constructorName = left.object.object.name
84
- return constructors.has(constructorName)
85
- })
86
- .forEach((path) => {
87
- const assignment = path.node.expression
88
- const left = assignment.left
89
- const constructorName = left.object.object.name
90
- const methodName = left.property.name
91
- const methodValue = assignment.right
92
-
93
- if (!new NodeTest(methodValue).canBeClassMethod()) {
94
- return
95
- }
150
+ const assignment = node.expression
151
+ const left = assignment.left
96
152
 
97
- constructors.get(constructorName).prototypeMethods.push({
98
- path,
99
- methodName,
100
- methodValue,
101
- })
102
- })
153
+ if (
154
+ !j.MemberExpression.check(left) ||
155
+ !j.Identifier.check(left.object) ||
156
+ !j.Identifier.check(left.property) ||
157
+ left.property.name !== "prototype"
158
+ ) {
159
+ return
160
+ }
103
161
 
104
- // Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
105
- root
106
- .find(j.ExpressionStatement)
107
- .filter(({ node }) => {
108
- if (!j.AssignmentExpression.check(node.expression)) {
109
- return false
110
- }
162
+ const constructorName = left.object.name
163
+ const constructorInfo = getConstructor(constructorsByScope, path, constructorName)
111
164
 
112
- const assignment = node.expression
113
- const left = assignment.left
165
+ if (!constructorInfo) {
166
+ return
167
+ }
114
168
 
115
- if (
116
- !j.MemberExpression.check(left) ||
117
- !j.Identifier.check(left.object) ||
118
- !j.Identifier.check(left.property) ||
119
- left.property.name !== "prototype"
120
- ) {
121
- return false
122
- }
169
+ const methodValue = assignment.right
123
170
 
124
- const constructorName = left.object.name
125
- return constructors.has(constructorName)
126
- })
127
- .forEach((path) => {
128
- const assignment = path.node.expression
129
- const left = assignment.left
130
- const constructorName = left.object.name
131
- const methodValue = assignment.right
171
+ if (!j.ObjectExpression.check(methodValue)) {
172
+ return
173
+ }
132
174
 
133
- if (!j.ObjectExpression.check(methodValue)) {
134
- return
135
- }
175
+ methodValue.properties.forEach((prop) => {
176
+ if (!j.Property.check(prop) && !j.ObjectProperty.check(prop)) {
177
+ return
178
+ }
179
+
180
+ if (prop.computed) {
181
+ return
182
+ }
183
+
184
+ let methodName
185
+ if (j.Identifier.check(prop.key)) {
186
+ methodName = prop.key.name
187
+ } else {
188
+ return
189
+ }
190
+
191
+ if (!new NodeTest(prop.value).canBeClassMethod()) {
192
+ return
193
+ }
136
194
 
137
- methodValue.properties.forEach((prop) => {
138
- if (!j.Property.check(prop) && !j.ObjectProperty.check(prop)) {
139
- return
140
- }
141
-
142
- if (prop.computed) {
143
- return
144
- }
145
-
146
- let methodName
147
- if (j.Identifier.check(prop.key)) {
148
- methodName = prop.key.name
149
- } else {
150
- return
151
- }
152
-
153
- if (!new NodeTest(prop.value).canBeClassMethod()) {
154
- return
155
- }
156
-
157
- constructors.get(constructorName).prototypeMethods.push({
158
- path,
159
- methodName,
160
- methodValue: prop.value,
161
- isObjectLiteral: true,
162
- })
163
- })
195
+ constructorInfo.prototypeMethods.push({
196
+ path,
197
+ methodName,
198
+ methodValue: prop.value,
199
+ isObjectLiteral: true,
164
200
  })
165
- }
201
+ })
202
+ })
203
+ }
204
+
205
+ /**
206
+ * Transform constructors and their prototype methods to class syntax.
207
+ *
208
+ * @param {import("jscodeshift").Collection} root - The root AST collection.
209
+ * @param {Map<
210
+ * object,
211
+ * Map<
212
+ * string,
213
+ * {
214
+ * constructorName: string
215
+ * declaration: import("ast-types").NodePath
216
+ * prototypeMethods: any[]
217
+ * }
218
+ * >
219
+ * >} constructorsByScope
220
+ * - Map of constructors grouped by lexical scope.
221
+ *
222
+ * @returns {boolean} True if code was modified.
223
+ */
224
+ function transformConstructorsToClasses(root, constructorsByScope) {
225
+ let modified = false
166
226
 
167
- /**
168
- * Transform constructors and their prototype methods to class syntax.
169
- *
170
- * @param {import("jscodeshift").Collection} root - The root AST collection.
171
- * @param {Map<
172
- * string,
173
- * { declaration: import("ast-types").NodePath; prototypeMethods: any[] }
174
- * >} constructors
175
- * - Map of constructors.
176
- *
177
- * @returns {boolean} True if code was modified.
178
- */
179
- function transformConstructorsToClasses(root, constructors) {
180
- let modified = false
181
-
182
- constructors.forEach((info, constructorName) => {
183
- if (info.prototypeMethods.length === 0) {
227
+ constructorsByScope.forEach((constructors) => {
228
+ constructors.forEach((info) => {
229
+ if (!info || info.prototypeMethods.length === 0) {
184
230
  return
185
231
  }
186
232
 
233
+ const constructorName = info.constructorName
187
234
  const declarationPath = info.declaration
188
235
  const declarationNode = declarationPath.node
189
236
 
@@ -197,20 +244,29 @@ export function constructorToClass(root) {
197
244
  constructorNode = declarator.init
198
245
  }
199
246
 
200
- const classBody = [
201
- j.methodDefinition(
202
- "constructor",
203
- j.identifier("constructor"),
204
- j.functionExpression(
205
- null,
206
- constructorNode.params,
207
- constructorNode.body,
208
- constructorNode.generator,
209
- constructorNode.async,
210
- ),
211
- false,
212
- ),
213
- ]
247
+ const hasNodeOrBodyComments =
248
+ (constructorNode.comments && constructorNode.comments.length > 0) ||
249
+ (constructorNode.body.comments && constructorNode.body.comments.length > 0)
250
+
251
+ const classBody =
252
+ constructorNode.params.length > 0 ||
253
+ constructorNode.body.body.length > 0 ||
254
+ hasNodeOrBodyComments
255
+ ? [
256
+ j.methodDefinition(
257
+ "constructor",
258
+ j.identifier("constructor"),
259
+ j.functionExpression(
260
+ null,
261
+ constructorNode.params,
262
+ constructorNode.body,
263
+ constructorNode.generator,
264
+ constructorNode.async,
265
+ ),
266
+ false,
267
+ ),
268
+ ]
269
+ : []
214
270
 
215
271
  info.prototypeMethods.forEach(({ methodName, methodValue }) => {
216
272
  const functionExpr = j.functionExpression(
@@ -254,12 +310,22 @@ export function constructorToClass(root) {
254
310
 
255
311
  modified = true
256
312
  })
313
+ })
257
314
 
258
- return modified
259
- }
315
+ return modified
316
+ }
260
317
 
261
- const constructors = findConstructors(root)
262
- findPrototypeMethods(root, constructors)
263
- return transformConstructorsToClasses(root, constructors)
318
+ /**
319
+ * Transform old-school constructor functions with prototype methods to ES6 class
320
+ * syntax.
321
+ *
322
+ * @param {import("jscodeshift").Collection} root - The root AST collection
323
+ * @returns {boolean} True if code was modified
324
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
325
+ */
326
+ export function constructorToClass(root) {
327
+ const constructorsByScope = findConstructors(root)
328
+ findPrototypeMethods(root, constructorsByScope)
329
+ return transformConstructorsToClasses(root, constructorsByScope)
264
330
  }
265
331
  constructorToClass.baselineDate = new Date(Date.UTC(2016, 2, 8))