esupgrade 2025.21.0 → 2025.22.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.
- package/.claude-plugin/marketplace.json +32 -0
- package/.claude-plugin/plugin.json +22 -0
- package/README.md +26 -8
- package/SKILL.md +9 -3
- package/bin/esupgrade.js +88 -1
- package/package.json +1 -1
- package/tests/cli.test.js +112 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "esupgrade",
|
|
3
|
+
"description": "Auto-upgrade JavaScript and TypeScript syntax to new ECMAScript features based on browser support.",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "codingjoe",
|
|
6
|
+
"email": "security@codingjoe.dev"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "esupgrade",
|
|
11
|
+
"source": "./",
|
|
12
|
+
"description": "Auto-upgrade JavaScript and TypeScript syntax to new ECMAScript features based on browser support.",
|
|
13
|
+
"displayName": "esupgrade",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Johannes Maron",
|
|
16
|
+
"email": "security@codingjoe.dev"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/codingjoe/esupgrade",
|
|
19
|
+
"repository": "https://github.com/codingjoe/esupgrade",
|
|
20
|
+
"license": "BSD-2-Clause",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"javascript",
|
|
23
|
+
"ecmascript",
|
|
24
|
+
"modernize",
|
|
25
|
+
"upgrade",
|
|
26
|
+
"codemod",
|
|
27
|
+
"ast",
|
|
28
|
+
"transform"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "esupgrade",
|
|
3
|
+
"displayName": "ESUpgrade",
|
|
4
|
+
"description": "Auto-upgrade JavaScript and TypeScript syntax to new ECMAScript features based on browser support.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Johannes Maron",
|
|
8
|
+
"email": "security@codingjoe.dev"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/codingjoe/esupgrade",
|
|
11
|
+
"repository": "https://github.com/codingjoe/esupgrade",
|
|
12
|
+
"license": "BSD-2-Clause",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"javascript",
|
|
15
|
+
"ecmascript",
|
|
16
|
+
"modernize",
|
|
17
|
+
"upgrade",
|
|
18
|
+
"codemod",
|
|
19
|
+
"ast",
|
|
20
|
+
"transform"
|
|
21
|
+
]
|
|
22
|
+
}
|
package/README.md
CHANGED
|
@@ -17,12 +17,26 @@ Keeping your JavaScript and TypeScript code up to date with full browser compati
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
esupgrade is safe and meant to be used automatically on your codebase.
|
|
20
|
-
We recommend integrating it into your development workflow using [pre-commit]
|
|
20
|
+
We recommend integrating it into your development workflow using [pre-commit], [husky], or as an agent skill.
|
|
21
|
+
|
|
22
|
+
### CLI
|
|
21
23
|
|
|
22
24
|
To try it out on a repository without writing changes, run:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
npx esupgrade $(git ls-files | grep -E -i -w '.*\.(t|j)sx?')
|
|
27
|
+
npx -y esupgrade $(git ls-files | grep -E -i -w '.*\.(t|j)sx?')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To transform code from standard input, pass `-` as the input path:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
echo 'var x = 1;' | npx esupgrade -
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For help with available options:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx esupgrade --help
|
|
26
40
|
```
|
|
27
41
|
|
|
28
42
|
### pre-commit
|
|
@@ -49,15 +63,18 @@ pre-commit run esupgrade --all-files
|
|
|
49
63
|
Assuming Husky is already initialized and `.husky/pre-commit` already contains `set -e`, append:
|
|
50
64
|
|
|
51
65
|
```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
|
|
66
|
+
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 -y esupgrade -- \"\$@\"' sh" >> .husky/pre-commit
|
|
53
67
|
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
68
|
```
|
|
55
69
|
|
|
56
|
-
###
|
|
70
|
+
### Agent Skill
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
esupgrade is available as a skill in [Claude Code]. To use it:
|
|
73
|
+
|
|
74
|
+
1. Run `/plugin marketplace add codingjoe/esupgrade`
|
|
75
|
+
1. Run `/plugin install esupgrade@esupgrade`
|
|
76
|
+
|
|
77
|
+
The skill will analyze your selected code and suggest transformations based on the Baseline browser support policy.
|
|
61
78
|
|
|
62
79
|
<picture>
|
|
63
80
|
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-wordmark-dark.svg">
|
|
@@ -402,7 +419,7 @@ Transforms constructor functions (both function declarations and variable declar
|
|
|
402
419
|
|
|
403
420
|
- Function name starts with an uppercase letter
|
|
404
421
|
- At least one prototype method is defined
|
|
405
|
-
- Prototype methods are matched
|
|
422
|
+
- Prototype methods are matched to constructors declared in the current or an ancestor lexical scope (never sibling or child scopes)
|
|
406
423
|
- Prototype methods using `this` in arrow functions are skipped
|
|
407
424
|
- Prototype object literals with getters, setters, or computed properties are skipped
|
|
408
425
|
|
|
@@ -756,6 +773,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
|
|
|
756
773
|
|
|
757
774
|
[baseline]: https://web.dev/baseline/
|
|
758
775
|
[calver]: https://calver.org/
|
|
776
|
+
[claude code]: https://code.claude.com
|
|
759
777
|
[django-upgrade]: https://github.com/adamchainz/django-upgrade
|
|
760
778
|
[husky]: https://typicode.github.io/husky/
|
|
761
779
|
[mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
package/SKILL.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: esupgrade
|
|
3
3
|
description: Auto-update JavaScript and TypeScript syntax to new ECMAScript features based on browser support.
|
|
4
|
+
license: BSD-2-Clause
|
|
5
|
+
metadata:
|
|
6
|
+
author: codingjoe <security@codingjoe.dev>
|
|
7
|
+
allowed-tools: Bash(npx -y esupgrade *) READ
|
|
8
|
+
compatibility: Requires Node.js 24 or later and npx.
|
|
4
9
|
---
|
|
5
10
|
|
|
6
|
-
Use the CLI with `npx` on files or
|
|
11
|
+
Use the CLI with `npx` on files, directories, or standard input:
|
|
7
12
|
|
|
8
13
|
```console
|
|
9
|
-
npx esupgrade [--baseline <newly-available|widely-available>] [--check] [--write] <files-or-directories
|
|
14
|
+
npx -y esupgrade [--baseline <newly-available|widely-available>] [--check] [--write] <files-or-directories|->
|
|
10
15
|
```
|
|
11
16
|
|
|
12
17
|
- Use `--check` to preview changes and fail when updates are needed.
|
|
13
|
-
- Use
|
|
18
|
+
- Use `-` to read from standard input and write the transformed code to standard output.
|
|
19
|
+
- Use `--write` to apply updates in place for file inputs.
|
|
14
20
|
- Use `--baseline` to choose `newly-available` or `widely-available` (default).
|
package/bin/esupgrade.js
CHANGED
|
@@ -10,6 +10,7 @@ import path from "path"
|
|
|
10
10
|
import { fileURLToPath } from "url"
|
|
11
11
|
import { Worker } from "worker_threads"
|
|
12
12
|
import pkg from "../package.json" with { type: "json" }
|
|
13
|
+
import { transform } from "../src/index.js"
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url)
|
|
15
16
|
const __dirname = path.dirname(__filename)
|
|
@@ -179,6 +180,62 @@ class WorkerPool {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Processes stdin and handles output.
|
|
185
|
+
*/
|
|
186
|
+
class StdinProcessor {
|
|
187
|
+
/**
|
|
188
|
+
* Process stdin using the configured transformation options.
|
|
189
|
+
* @param {Object} options - Processing options.
|
|
190
|
+
* @param {string} options.baseline - Baseline level for transformations.
|
|
191
|
+
* @param {boolean} options.check - Whether to only check for changes.
|
|
192
|
+
* @param {boolean} options.write - Whether to write changes to file.
|
|
193
|
+
* @param {boolean} options.verbose - The verbosity level for logging.
|
|
194
|
+
* @returns {Promise<void>} Complete when stdin processing finishes.
|
|
195
|
+
*/
|
|
196
|
+
async processStdin(options) {
|
|
197
|
+
if (options.write) {
|
|
198
|
+
console.error("Error: '--write' cannot be used with stdin")
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const code = await this.#readStdin()
|
|
204
|
+
const result = transform(code, options.baseline)
|
|
205
|
+
|
|
206
|
+
if (options.check) {
|
|
207
|
+
if (result.modified) {
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
process.stdout.write(result.code)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (options.verbose) console.error(error)
|
|
216
|
+
console.error(`\x1b[31m✗\x1b[0m Error: stdin: ${error.message}`)
|
|
217
|
+
process.exit(128)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Read source code from stdin.
|
|
223
|
+
* @returns {Promise<string>} Source code from stdin.
|
|
224
|
+
*/
|
|
225
|
+
async #readStdin() {
|
|
226
|
+
process.stdin.setEncoding("utf8")
|
|
227
|
+
let code = ""
|
|
228
|
+
|
|
229
|
+
function handleChunk(chunk) {
|
|
230
|
+
code += chunk
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
process.stdin.on("data", handleChunk)
|
|
234
|
+
await once(process.stdin, "end")
|
|
235
|
+
return code
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
182
239
|
/**
|
|
183
240
|
* Orchestrates the CLI application.
|
|
184
241
|
*/
|
|
@@ -187,6 +244,7 @@ class CLIRunner {
|
|
|
187
244
|
const workerRunner = new WorkerRunner(workerPath)
|
|
188
245
|
const fileProcessor = new FileProcessor(workerRunner)
|
|
189
246
|
this.workerPool = new WorkerPool(fileProcessor)
|
|
247
|
+
this.stdinProcessor = new StdinProcessor()
|
|
190
248
|
}
|
|
191
249
|
|
|
192
250
|
/**
|
|
@@ -195,6 +253,17 @@ class CLIRunner {
|
|
|
195
253
|
* @param {Object} options - Processing options.
|
|
196
254
|
*/
|
|
197
255
|
async run(patterns, options) {
|
|
256
|
+
switch (this.#getInputMode(patterns)) {
|
|
257
|
+
case "stdin":
|
|
258
|
+
await this.stdinProcessor.processStdin(options)
|
|
259
|
+
return
|
|
260
|
+
case "mixed":
|
|
261
|
+
console.error("Error: '-' cannot be combined with file paths")
|
|
262
|
+
return process.exit(1)
|
|
263
|
+
default:
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
|
|
198
267
|
console.time("Processing")
|
|
199
268
|
// Hand CLI-provided names directly to the worker pool; file validation occurs in processFile.
|
|
200
269
|
const results = await this.workerPool.processFiles(patterns, options)
|
|
@@ -203,6 +272,24 @@ class CLIRunner {
|
|
|
203
272
|
this.#reportSummary(results, options)
|
|
204
273
|
}
|
|
205
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Classify the input mode selected by CLI arguments.
|
|
277
|
+
* @param {string[]} patterns - File paths to process.
|
|
278
|
+
* @returns {"files" | "stdin" | "mixed"} Selected input mode.
|
|
279
|
+
*/
|
|
280
|
+
#getInputMode(patterns) {
|
|
281
|
+
const stdinCount = patterns.filter((pattern) => pattern === "-").length
|
|
282
|
+
|
|
283
|
+
switch (stdinCount) {
|
|
284
|
+
case 0:
|
|
285
|
+
return "files"
|
|
286
|
+
case 1:
|
|
287
|
+
return patterns.length === 1 ? "stdin" : "mixed"
|
|
288
|
+
default:
|
|
289
|
+
return "mixed"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
206
293
|
#reportSummary(results, options) {
|
|
207
294
|
let modifiedCount = 0
|
|
208
295
|
const errorCount = results.filter((result) => {
|
|
@@ -257,7 +344,7 @@ program
|
|
|
257
344
|
.name("esupgrade")
|
|
258
345
|
.description("Auto-upgrade your JavaScript syntax")
|
|
259
346
|
.version(pkg.version)
|
|
260
|
-
.argument("<files...>", "Files or directories to process")
|
|
347
|
+
.argument("<files...>", "Files or directories to process, or '-' to read from stdin")
|
|
261
348
|
.addOption(
|
|
262
349
|
new Option("--baseline <level>", "Set baseline level for transformations")
|
|
263
350
|
.choices(["widely-available", "newly-available"])
|
package/package.json
CHANGED
package/tests/cli.test.js
CHANGED
|
@@ -106,6 +106,118 @@ describe("CLI", () => {
|
|
|
106
106
|
assert.equal(result.status, 0, "exits successfully")
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
+
test("transform stdin and write code to stdout", () => {
|
|
110
|
+
fs.writeFileSync(path.join(tempDir, "-"), "const fromFile = true;")
|
|
111
|
+
|
|
112
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-"], {
|
|
113
|
+
encoding: "utf8",
|
|
114
|
+
input: "var x = 1;",
|
|
115
|
+
cwd: tempDir,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
assert.match(result.stdout, /const x = 1;/, "writes transformed code to stdout")
|
|
119
|
+
assert.doesNotMatch(
|
|
120
|
+
result.stdout,
|
|
121
|
+
/would be upgraded|All files are up to date|Processing:/,
|
|
122
|
+
"omits file-processing summary output",
|
|
123
|
+
)
|
|
124
|
+
assert.equal(result.status, 0, "exits successfully")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("transform stdin with newly-available baseline", () => {
|
|
128
|
+
const result = spawnSync(
|
|
129
|
+
process.execPath,
|
|
130
|
+
[CLI_PATH, "-", "--baseline", "newly-available"],
|
|
131
|
+
{
|
|
132
|
+
encoding: "utf8",
|
|
133
|
+
input: "const p = new Promise((resolve) => resolve(getData()));",
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert.match(
|
|
138
|
+
result.stdout,
|
|
139
|
+
/Promise\.try/,
|
|
140
|
+
"writes transformations from the selected baseline",
|
|
141
|
+
)
|
|
142
|
+
assert.equal(result.status, 0, "exits successfully")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("exit with 1 when --check finds stdin changes", () => {
|
|
146
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-", "--check"], {
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
input: "var x = 1;",
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
assert.equal(result.stdout, "", "omits transformed output in check mode")
|
|
152
|
+
assert.equal(result.status, 1, "exits with 1 when stdin needs upgrading")
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test("exit with 0 when --check finds no stdin changes", () => {
|
|
156
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-", "--check"], {
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
input: "const x = 1;",
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
assert.equal(result.stdout, "", "omits transformed output in check mode")
|
|
162
|
+
assert.equal(result.status, 0, "exits with 0 when stdin is up to date")
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test("error when combining stdin with --write", () => {
|
|
166
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-", "--write"], {
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
input: "var x = 1;",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
assert.match(
|
|
172
|
+
result.stderr,
|
|
173
|
+
/cannot be used with stdin/,
|
|
174
|
+
"displays stdin write error",
|
|
175
|
+
)
|
|
176
|
+
assert.equal(result.status, 1, "exits with 1")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("error when stdin marker appears multiple times", () => {
|
|
180
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-", "-"], {
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
input: "var x = 1;",
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
assert.match(
|
|
186
|
+
result.stderr,
|
|
187
|
+
/cannot be combined with file paths/,
|
|
188
|
+
"displays input error",
|
|
189
|
+
)
|
|
190
|
+
assert.equal(result.status, 1, "exits with 1")
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test("error when combining stdin with file arguments", () => {
|
|
194
|
+
const testFile = path.join(tempDir, "test.js")
|
|
195
|
+
fs.writeFileSync(testFile, "var x = 1;")
|
|
196
|
+
|
|
197
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-", testFile], {
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
input: "var y = 2;",
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
assert.match(
|
|
203
|
+
result.stderr,
|
|
204
|
+
/cannot be combined with file paths/,
|
|
205
|
+
"displays input error",
|
|
206
|
+
)
|
|
207
|
+
assert.equal(result.status, 1, "exits with 1")
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test("show stdin parse error with --verbose", () => {
|
|
211
|
+
const result = spawnSync(process.execPath, [CLI_PATH, "-", "--verbose"], {
|
|
212
|
+
encoding: "utf8",
|
|
213
|
+
input: "var x = {{{;",
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
assert.match(result.stderr, /Error:/, "displays error message")
|
|
217
|
+
assert.match(result.stderr, /\n\s*at /, "includes stack trace at verbosity level 1")
|
|
218
|
+
assert.equal(result.status, 128, "exits with 128 on stdin errors")
|
|
219
|
+
})
|
|
220
|
+
|
|
109
221
|
test("check files without writing with --check", () => {
|
|
110
222
|
const testFile = path.join(tempDir, "test.js")
|
|
111
223
|
const originalCode = `var x = 1;`
|