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.
@@ -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] or [husky].
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
- ### CLI
70
+ ### Agent Skill
57
71
 
58
- ```bash
59
- npx esupgrade --help
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 only to constructors declared in the current or an ancestor lexical scope (never sibling or child scopes)
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 directories:
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 `--write` to apply updates in place.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.21.0",
3
+ "version": "2025.22.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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;`