esupgrade 2025.20.1 → 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
+ }
@@ -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
package/README.md CHANGED
@@ -10,15 +10,33 @@
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] or [husky].
20
+ We recommend integrating it into your development workflow using [pre-commit], [husky], or as an agent skill.
21
+
22
+ ### CLI
17
23
 
18
24
  To try it out on a repository without writing changes, run:
19
25
 
20
26
  ```bash
21
- 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
22
40
  ```
23
41
 
24
42
  ### pre-commit
@@ -45,15 +63,18 @@ pre-commit run esupgrade --all-files
45
63
  Assuming Husky is already initialized and `.husky/pre-commit` already contains `set -e`, append:
46
64
 
47
65
  ```bash
48
- 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
49
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
50
68
  ```
51
69
 
52
- ### CLI
70
+ ### Agent Skill
53
71
 
54
- ```bash
55
- npx esupgrade --help
56
- ```
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.
57
78
 
58
79
  <picture>
59
80
  <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-wordmark-dark.svg">
@@ -229,12 +250,21 @@ Supports:
229
250
  ```diff
230
251
  -const budget = 1000000;
231
252
  -const maxUsers = 1000000n;
253
+ -const permission = 0o1234567;
254
+ -const mask = 0xabcdef;
255
+ -const flags = 0b1010000111000011;
256
+ -const ratio = 100000.25;
257
+ -const large = 1e100000;
232
258
  +const budget = 1_000_000;
233
259
  +const maxUsers = 1_000_000n;
260
+ +const permission = 0o1_234_567;
261
+ +const mask = 0xab_cd_ef;
262
+ +const flags = 0b10100001_11000011;
263
+ +const ratio = 100_000.25;
264
+ +const large = 1e100_000;
234
265
  ```
235
266
 
236
- esupgrade transforms decimal integer and bigint literals with at least five digits.
237
- Fractional, exponential, hexadecimal, octal, binary, and already formatted literals are left unchanged.
267
+ Decimal and octal numerals are grouped by triplets; hex and binary by byte.
238
268
 
239
269
  #### `Array.slice(0)` → [Array spread [...]][mdn-spread]
240
270
 
@@ -389,7 +419,7 @@ Transforms constructor functions (both function declarations and variable declar
389
419
 
390
420
  - Function name starts with an uppercase letter
391
421
  - At least one prototype method is defined
392
- - 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)
393
423
  - Prototype methods using `this` in arrow functions are skipped
394
424
  - Prototype object literals with getters, setters, or computed properties are skipped
395
425
 
@@ -743,6 +773,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
743
773
 
744
774
  [baseline]: https://web.dev/baseline/
745
775
  [calver]: https://calver.org/
776
+ [claude code]: https://code.claude.com
746
777
  [django-upgrade]: https://github.com/adamchainz/django-upgrade
747
778
  [husky]: https://typicode.github.io/husky/
748
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.20.1",
3
+ "version": "2025.22.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,6 +1,33 @@
1
1
  import { default as j } from "jscodeshift"
2
2
 
3
- const LARGE_DECIMAL_INTEGER_LITERAL = /^(?<digits>[1-9]\d{4,})(?<suffix>n)?$/
3
+ const MIN_DIGITS_FOR_GROUPING = 5
4
+ const TRANSFORMABLE_DECIMAL_INTEGER_LITERAL = new RegExp(
5
+ `^(?<digits>[1-9]\\d{${MIN_DIGITS_FOR_GROUPING - 1},})(?<suffix>n)?$`,
6
+ )
7
+ const TRANSFORMABLE_OCTAL_LITERAL =
8
+ /^(?<prefix>0[oO])(?<digits>[0-7]{4,})(?<suffix>n)?$/
9
+ const TRANSFORMABLE_HEX_LITERAL =
10
+ /^(?<prefix>0[xX])(?<digits>[0-9a-fA-F]{3,})(?<suffix>n)?$/
11
+ const TRANSFORMABLE_BINARY_LITERAL =
12
+ /^(?<prefix>0[bB])(?<digits>[01]{9,})(?<suffix>n)?$/
13
+ const FLOAT_LITERAL = /^(?<integer>\d+)\.(?<decimal>\d*)$/
14
+ const EXPONENTIAL_LITERAL =
15
+ /^(?:(?<mantissaInt>\d+)(?:\.(?<mantissaDec>\d*))?|\.(?<mantissaLeadingDec>\d+))(?<expMarker>[eE])(?<sign>[+-]?)(?<exponent>\d+)$/
16
+
17
+ /**
18
+ * Apply numeric separators to digit groups counted from the right.
19
+ *
20
+ * @param {string} digits - Digit string without separators.
21
+ * @param {number} groupSize - Number of characters per group.
22
+ * @returns {string} Digits with `_` inserted between right-aligned groups.
23
+ */
24
+ function applyDigitGroupSep(digits, groupSize) {
25
+ const firstGroupLength = digits.length % groupSize || groupSize
26
+
27
+ return digits.length > groupSize
28
+ ? `${digits.slice(0, firstGroupLength)}_${applyDigitGroupSep(digits.slice(firstGroupLength), groupSize)}`
29
+ : digits
30
+ }
4
31
 
5
32
  /**
6
33
  * Return a decimal integer literal with numeric separators when applicable.
@@ -8,8 +35,8 @@ const LARGE_DECIMAL_INTEGER_LITERAL = /^(?<digits>[1-9]\d{4,})(?<suffix>n)?$/
8
35
  * @param {string} rawLiteral - Raw literal source text.
9
36
  * @returns {string | null} Formatted literal or null when no change applies.
10
37
  */
11
- function formatNumericLiteral(rawLiteral) {
12
- const match = rawLiteral.match(LARGE_DECIMAL_INTEGER_LITERAL)
38
+ function formatDecimalIntegerLiteral(rawLiteral) {
39
+ const match = rawLiteral.match(TRANSFORMABLE_DECIMAL_INTEGER_LITERAL)
13
40
 
14
41
  if (!match?.groups) {
15
42
  return null
@@ -17,7 +44,147 @@ function formatNumericLiteral(rawLiteral) {
17
44
 
18
45
  const { digits, suffix = "" } = match.groups
19
46
 
20
- return `${digits.replace(/\B(?=(\d{3})+(?!\d))/g, "_")}${suffix}`
47
+ return `${applyDigitGroupSep(digits, 3)}${suffix}`
48
+ }
49
+
50
+ /**
51
+ * Return an octal literal with triplet numeric separators when applicable.
52
+ *
53
+ * @param {string} rawLiteral - Raw literal source text.
54
+ * @returns {string | null} Formatted literal or null when no change applies.
55
+ */
56
+ function formatOctalLiteral(rawLiteral) {
57
+ const match = rawLiteral.match(TRANSFORMABLE_OCTAL_LITERAL)
58
+
59
+ if (!match?.groups) {
60
+ return null
61
+ }
62
+
63
+ const { prefix, digits, suffix = "" } = match.groups
64
+
65
+ return `${prefix}${applyDigitGroupSep(digits, 3)}${suffix}`
66
+ }
67
+
68
+ /**
69
+ * Return a hex literal with byte-level numeric separators when applicable.
70
+ *
71
+ * @param {string} rawLiteral - Raw literal source text.
72
+ * @returns {string | null} Formatted literal or null when no change applies.
73
+ */
74
+ function formatHexLiteral(rawLiteral) {
75
+ const match = rawLiteral.match(TRANSFORMABLE_HEX_LITERAL)
76
+
77
+ if (!match?.groups) {
78
+ return null
79
+ }
80
+
81
+ const { prefix, digits, suffix = "" } = match.groups
82
+
83
+ // Insert `_` before each group of 2 hex chars counted from the right.
84
+ return `${prefix}${applyDigitGroupSep(digits, 2)}${suffix}`
85
+ }
86
+
87
+ /**
88
+ * Return a binary literal with byte-level numeric separators when applicable.
89
+ *
90
+ * @param {string} rawLiteral - Raw literal source text.
91
+ * @returns {string | null} Formatted literal or null when no change applies.
92
+ */
93
+ function formatBinaryLiteral(rawLiteral) {
94
+ const match = rawLiteral.match(TRANSFORMABLE_BINARY_LITERAL)
95
+
96
+ if (!match?.groups) {
97
+ return null
98
+ }
99
+
100
+ const { prefix, digits, suffix = "" } = match.groups
101
+
102
+ // Insert `_` before each group of 8 bits counted from the right.
103
+ return `${prefix}${applyDigitGroupSep(digits, 8)}${suffix}`
104
+ }
105
+
106
+ /**
107
+ * Return a float literal with thousands separators on the integer part when applicable.
108
+ *
109
+ * @param {string} rawLiteral - Raw literal source text.
110
+ * @returns {string | null} Formatted literal or null when no change applies.
111
+ */
112
+ function formatFloatLiteral(rawLiteral) {
113
+ const match = rawLiteral.match(FLOAT_LITERAL)
114
+
115
+ if (!match?.groups) {
116
+ return null
117
+ }
118
+
119
+ const { integer, decimal } = match.groups
120
+
121
+ if (integer.length < MIN_DIGITS_FOR_GROUPING) {
122
+ return null
123
+ }
124
+
125
+ return `${applyDigitGroupSep(integer, 3)}.${decimal}`
126
+ }
127
+
128
+ /**
129
+ * Return an exponential literal with thousands separators on large parts when applicable.
130
+ *
131
+ * @param {string} rawLiteral - Raw literal source text.
132
+ * @returns {string | null} Formatted literal or null when no change applies.
133
+ */
134
+ function formatExponentialLiteral(rawLiteral) {
135
+ const match = rawLiteral.match(EXPONENTIAL_LITERAL)
136
+
137
+ if (!match?.groups) {
138
+ return null
139
+ }
140
+
141
+ const {
142
+ mantissaInt = "",
143
+ mantissaDec,
144
+ mantissaLeadingDec,
145
+ expMarker,
146
+ sign,
147
+ exponent,
148
+ } = match.groups
149
+
150
+ const formattedMantissaInt =
151
+ mantissaInt.length >= MIN_DIGITS_FOR_GROUPING
152
+ ? applyDigitGroupSep(mantissaInt, 3)
153
+ : mantissaInt
154
+ const formattedExponent =
155
+ exponent.length >= MIN_DIGITS_FOR_GROUPING
156
+ ? applyDigitGroupSep(exponent, 3)
157
+ : exponent
158
+
159
+ if (formattedMantissaInt === mantissaInt && formattedExponent === exponent) {
160
+ return null
161
+ }
162
+
163
+ const mantissa =
164
+ typeof mantissaLeadingDec === "string"
165
+ ? `.${mantissaLeadingDec}`
166
+ : typeof mantissaDec === "string"
167
+ ? `${formattedMantissaInt}.${mantissaDec}`
168
+ : formattedMantissaInt
169
+
170
+ return `${mantissa}${expMarker}${sign}${formattedExponent}`
171
+ }
172
+
173
+ /**
174
+ * Return a numeric literal with separators when applicable.
175
+ *
176
+ * @param {string} rawLiteral - Raw literal source text.
177
+ * @returns {string | null} Formatted literal or null when no change applies.
178
+ */
179
+ function formatLiteral(rawLiteral) {
180
+ return (
181
+ formatDecimalIntegerLiteral(rawLiteral) ??
182
+ formatOctalLiteral(rawLiteral) ??
183
+ formatHexLiteral(rawLiteral) ??
184
+ formatBinaryLiteral(rawLiteral) ??
185
+ formatFloatLiteral(rawLiteral) ??
186
+ formatExponentialLiteral(rawLiteral)
187
+ )
21
188
  }
22
189
 
23
190
  /**
@@ -29,7 +196,7 @@ function formatNumericLiteral(rawLiteral) {
29
196
  function updateNumericLiteral(literalNode) {
30
197
  const formattedLiteral =
31
198
  typeof literalNode.extra?.raw === "string"
32
- ? formatNumericLiteral(literalNode.extra.raw)
199
+ ? formatLiteral(literalNode.extra.raw)
33
200
  : null
34
201
 
35
202
  if (!formattedLiteral) {
@@ -45,7 +212,7 @@ function updateNumericLiteral(literalNode) {
45
212
  }
46
213
 
47
214
  /**
48
- * Transform large decimal integer literals to use numeric separators.
215
+ * Transform numeric literals to use numeric separators.
49
216
  *
50
217
  * @param {import("jscodeshift").Collection} root - The root AST collection.
51
218
  * @returns {boolean} True if code was modified.
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;`
@@ -40,10 +40,240 @@ suite("widely-available", () => {
40
40
  assert(!result.modified)
41
41
  })
42
42
 
43
- test("skip non-decimal literals", () => {
44
- const result = transform(
45
- `const hexMask = 0xabcdef; const exponent = 1e12; const ratio = 10000.25;`,
46
- )
43
+ test("skip small float and short exponent literals", () => {
44
+ const result = transform(`const ratio = 1000.25; const exponent = 1e12;`)
45
+
46
+ assert(!result.modified)
47
+ })
48
+
49
+ test("transform float literals", () => {
50
+ const result = transform(`const ratio = 100000.25;`)
51
+
52
+ assert.match(result.code, /100_000\.25/)
53
+ assert(result.modified)
54
+ })
55
+
56
+ test("transform float literals with exact five-digit integer part", () => {
57
+ const result = transform(`const ratio = 10000.5;`)
58
+
59
+ assert.match(result.code, /10_000\.5/)
60
+ assert(result.modified)
61
+ })
62
+
63
+ test("transform float literals without decimal digits", () => {
64
+ const result = transform(`const ratio = 10000.;`)
65
+
66
+ assert.match(result.code, /10_000\./)
67
+ assert(result.modified)
68
+ })
69
+
70
+ test("skip float literals with short integer part", () => {
71
+ const result = transform(`const ratio = 1000.5;`)
72
+
73
+ assert(!result.modified)
74
+ })
75
+
76
+ test("transform exponential literals with large exponent", () => {
77
+ const result = transform(`const n = 1e100000;`)
78
+
79
+ assert.match(result.code, /1e100_000/)
80
+ assert(result.modified)
81
+ })
82
+
83
+ test("transform exponential literals with large mantissa", () => {
84
+ const result = transform(`const n = 10000e12;`)
85
+
86
+ assert.match(result.code, /10_000e12/)
87
+ assert(result.modified)
88
+ })
89
+
90
+ test("transform exponential literals with large mantissa and exponent", () => {
91
+ const result = transform(`const n = 10000e100000;`)
92
+
93
+ assert.match(result.code, /10_000e100_000/)
94
+ assert(result.modified)
95
+ })
96
+
97
+ test("transform exponential literals with float mantissa and large exponent", () => {
98
+ const result = transform(`const n = 1.5e100000;`)
99
+
100
+ assert.match(result.code, /1\.5e100_000/)
101
+ assert(result.modified)
102
+ })
103
+
104
+ test("transform exponential literals with leading-dot mantissa", () => {
105
+ const result = transform(`const n = .5e100000;`)
106
+
107
+ assert.match(result.code, /\.5e100_000/)
108
+ assert(result.modified)
109
+ })
110
+
111
+ test("transform exponential literals with trailing-dot mantissa", () => {
112
+ const result = transform(`const n = 10000.e100000;`)
113
+
114
+ assert.match(result.code, /10_000\.e100_000/)
115
+ assert(result.modified)
116
+ })
117
+
118
+ test("transform exponential literals with signed exponent", () => {
119
+ const result = transform(`const n = 1e+100000;`)
120
+
121
+ assert.match(result.code, /1e\+100_000/)
122
+ assert(result.modified)
123
+ })
124
+
125
+ test("transform exponential literals with negative exponent", () => {
126
+ const result = transform(`const n = 1e-100000;`)
127
+
128
+ assert.match(result.code, /1e-100_000/)
129
+ assert(result.modified)
130
+ })
131
+
132
+ test("transform float literals with leading-zero decimal part", () => {
133
+ const result = transform(`const ratio = 10000.05;`)
134
+
135
+ assert.match(result.code, /10_000\.05/)
136
+ assert(result.modified)
137
+ })
138
+
139
+ test("transform hex literals", () => {
140
+ const result = transform(`const mask = 0xabcdef;`)
141
+
142
+ assert.match(result.code, /0xab_cd_ef/)
143
+ assert(result.modified)
144
+ })
145
+
146
+ test("transform octal literals", () => {
147
+ const result = transform(`const permission = 0o1234567;`)
148
+
149
+ assert.match(result.code, /0o1_234_567/)
150
+ assert(result.modified)
151
+ })
152
+
153
+ test("transform uppercase octal literals", () => {
154
+ const result = transform(`const permission = 0O1234567;`)
155
+
156
+ assert.match(result.code, /0O1_234_567/)
157
+ assert(result.modified)
158
+ })
159
+
160
+ test("transform octal bigint literals", () => {
161
+ const result = transform(`const permission = 0o1234567n;`)
162
+
163
+ assert.match(result.code, /0o1_234_567n/)
164
+ assert(result.modified)
165
+ })
166
+
167
+ test("skip short octal literals", () => {
168
+ const result = transform(`const permission = 0o777;`)
169
+
170
+ assert(!result.modified)
171
+ })
172
+
173
+ test("skip already formatted octal literals", () => {
174
+ const result = transform(`const permission = 0o1_234_567;`)
175
+
176
+ assert(!result.modified)
177
+ })
178
+
179
+ test("transform uppercase hex literals", () => {
180
+ const result = transform(`const mask = 0xABCDEF;`)
181
+
182
+ assert.match(result.code, /0xAB_CD_EF/)
183
+ assert(result.modified)
184
+ })
185
+
186
+ test("transform hex literals with odd digit count", () => {
187
+ const result = transform(`const mask = 0xabcde;`)
188
+
189
+ assert.match(result.code, /0xa_bc_de/)
190
+ assert(result.modified)
191
+ })
192
+
193
+ test("transform hex bigint literals", () => {
194
+ const result = transform(`const id = 0xabcdefn;`)
195
+
196
+ assert.match(result.code, /0xab_cd_efn/)
197
+ assert(result.modified)
198
+ })
199
+
200
+ test("skip short hex literals", () => {
201
+ const result = transform(`const byte = 0xff;`)
202
+
203
+ assert(!result.modified)
204
+ })
205
+
206
+ test("skip already formatted hex literals", () => {
207
+ const result = transform(`const mask = 0xab_cd_ef;`)
208
+
209
+ assert(!result.modified)
210
+ })
211
+
212
+ test("transform binary literals", () => {
213
+ const result = transform(`const flags = 0b1010000111000011;`)
214
+
215
+ assert.match(result.code, /0b10100001_11000011/)
216
+ assert(result.modified)
217
+ })
218
+
219
+ test("transform binary literals with partial leading byte", () => {
220
+ const result = transform(`const flags = 0b100000001;`)
221
+
222
+ assert.match(result.code, /0b1_00000001/)
223
+ assert(result.modified)
224
+ })
225
+
226
+ test("transform binary bigint literals", () => {
227
+ const result = transform(`const flags = 0b1010000111000011n;`)
228
+
229
+ assert.match(result.code, /0b10100001_11000011n/)
230
+ assert(result.modified)
231
+ })
232
+
233
+ test("skip short binary literals", () => {
234
+ const result = transform(`const byte = 0b11111111;`)
235
+
236
+ assert(!result.modified)
237
+ })
238
+
239
+ test("skip already formatted binary literals", () => {
240
+ const result = transform(`const flags = 0b10100001_11000011;`)
241
+
242
+ assert(!result.modified)
243
+ })
244
+
245
+ test("skip hex literals with non-byte grouping", () => {
246
+ const result = transform(`const mask = 0xabc_def;`)
247
+
248
+ assert(!result.modified)
249
+ })
250
+
251
+ test("skip binary literals with non-byte grouping", () => {
252
+ const result = transform(`const flags = 0b1010_0001_1100_0011;`)
253
+
254
+ assert(!result.modified)
255
+ })
256
+
257
+ test("skip decimal literals with non-thousand grouping", () => {
258
+ const result = transform(`const n = 1_0000_000;`)
259
+
260
+ assert(!result.modified)
261
+ })
262
+
263
+ test("skip octal literals with non-triplet grouping", () => {
264
+ const result = transform(`const permission = 0o12_34567;`)
265
+
266
+ assert(!result.modified)
267
+ })
268
+
269
+ test("skip float literals with non-standard existing separators", () => {
270
+ const result = transform(`const ratio = 100_00.25;`)
271
+
272
+ assert(!result.modified)
273
+ })
274
+
275
+ test("skip exponential literals with non-standard existing separators", () => {
276
+ const result = transform(`const n = 1_0000e12;`)
47
277
 
48
278
  assert(!result.modified)
49
279
  })