esupgrade 2025.3.1 → 2025.3.3

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/README.md CHANGED
@@ -40,6 +40,12 @@ pre-commit run esupgrade --all-files
40
40
  npx esupgrade --help
41
41
  ```
42
42
 
43
+ <picture>
44
+ <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-wordmark-dark.svg">
45
+ <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-wordmark.svg">
46
+ <img alt="Baseline: widely available" src="https://web-platform-dx.github.io/web-features/assets/img/baseline-wordmark.svg" height="32" align="right">
47
+ </picture>
48
+
43
49
  ## Browser Support & Baseline
44
50
 
45
51
  All transformations are based on [Web Platform Baseline][baseline] features. Baseline tracks which web platform features are safe to use across browsers.
@@ -229,7 +235,28 @@ Supports:
229
235
  +});
230
236
  ```
231
237
 
238
+ ## Versioning
239
+
240
+ esupgrade uses the [calver] `YYYY.MINOR.PATCH` versioning scheme.
241
+
242
+ The year indicates the baseline version. New transformations are added in minor releases, while patches are reserved for bug fixes.
243
+
244
+ ## Related Projects
245
+
246
+ Thanks to these projects for inspiring esupgrade:
247
+
248
+ - @asottile's [pyupgrade] for Python
249
+ - @adamchainz' [django-upgrade] for Django
250
+
251
+ ### Distinction
252
+
253
+ lebab is a similar project that focuses on ECMAScript 6+ transformations without considering browser support.
254
+ esupgrade is distinct in that it applies transformations that are safe based on Baseline browser support.
255
+ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is limited to JavaScript.
256
+
232
257
  [baseline]: https://web.dev/baseline/
258
+ [calver]: https://calver.org/
259
+ [django-upgrade]: https://github.com/adamchainz/django-upgrade
233
260
  [mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
234
261
  [mdn-const]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
235
262
  [mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
@@ -240,3 +267,4 @@ Supports:
240
267
  [mdn-spread]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
241
268
  [mdn-template-literals]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
242
269
  [pre-commit]: https://pre-commit.com/
270
+ [pyupgrade]: https://github.com/asottile/pyupgrade
package/bin/esupgrade.js CHANGED
@@ -2,201 +2,317 @@
2
2
 
3
3
  import fs from "fs"
4
4
  import path from "path"
5
+ import os from "os"
6
+ import { Worker } from "worker_threads"
7
+ import { once } from "events"
5
8
  import { Command, Option } from "commander"
6
- import { transform } from "../src/index.js"
9
+ import { fileURLToPath } from "url"
10
+ import process from "node:process"
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = path.dirname(__filename)
7
14
 
8
15
  /**
9
16
  * CLI tool for esupgrade
10
17
  */
11
18
 
12
- const program = new Command()
13
-
14
- program
15
- .name("esupgrade")
16
- .description("Auto-upgrade your JavaScript syntax")
17
- .argument("[files...]", "Files or directories to process")
18
- .addOption(
19
- new Option("--baseline <level>", "Set baseline level for transformations")
20
- .choices(["widely-available", "newly-available"])
21
- .default("widely-available"),
22
- )
23
- .option("--check", "Report which files need upgrading and exit with code 1 if any do")
24
- .option(
25
- "--write",
26
- "Write changes to files (default: true unless only --check is specified)",
27
- )
28
- .action((files, options) => {
29
- if (files.length === 0) {
30
- console.error("Error: No files specified")
31
- program.help()
32
- }
19
+ /**
20
+ * Handles worker thread execution for file processing
21
+ */
22
+ class WorkerRunner {
23
+ constructor(workerPath) {
24
+ this.workerPath = workerPath
25
+ }
33
26
 
34
- // Handle check/write options - they are not mutually exclusive
35
- // Default: write is true unless ONLY --check is specified (no --write)
36
- const shouldWrite = options.write !== undefined ? options.write : !options.check
37
- const shouldCheck = options.check || false
27
+ /**
28
+ * Run a worker thread to process a file
29
+ */
30
+ async run(filePath, baseline) {
31
+ const worker = new Worker(this.workerPath, {
32
+ workerData: { filePath, baseline },
33
+ })
38
34
 
39
- const processingOptions = {
40
- baseline: options.baseline,
41
- write: shouldWrite,
42
- check: shouldCheck,
43
- }
35
+ const [message] = await once(worker, "message")
36
+ return message
37
+ }
38
+ }
44
39
 
45
- processFiles(files, processingOptions)
46
- })
40
+ /**
41
+ * Processes individual files and handles output
42
+ */
43
+ class FileProcessor {
44
+ constructor(workerRunner) {
45
+ this.workerRunner = workerRunner
46
+ }
47
47
 
48
- function findFiles(patterns) {
49
- const files = []
48
+ /**
49
+ * @typedef {Object} ProcessResult
50
+ * @property {boolean} modified - Whether the file was modified
51
+ * @property {Array} changes - List of changes made
52
+ * @property {boolean} error - Whether an error occurred during processing
53
+ */
50
54
 
51
- for (const pattern of patterns) {
55
+ /**
56
+ * Process a file using a worker thread
57
+ *
58
+ * @param {string} filePath - Path to the file to process
59
+ * @param {Object} options - Processing options
60
+ * @param {string} options.baseline - Baseline level for transformations
61
+ * @param {boolean} options.check - Whether to only check for changes
62
+ * @param {boolean} options.write - Whether to write changes to file
63
+ * @returns {Promise<ProcessResult>} Result of processing
64
+ */
65
+ async processFile(filePath, options) {
52
66
  try {
53
- const stats = fs.statSync(pattern)
54
-
55
- if (stats.isFile()) {
56
- files.push(pattern)
57
- } else if (stats.isDirectory()) {
58
- // Recursively find .js, .jsx, .ts, .tsx files
59
- const dirFiles = walkDirectory(pattern)
60
- files.push(...dirFiles)
67
+ const workerResult = await this.workerRunner.run(filePath, options.baseline)
68
+
69
+ if (!workerResult.success) {
70
+ console.error(`✗ Error: ${filePath}: ${workerResult.error}`)
71
+ return { modified: false, changes: [], error: true }
72
+ }
73
+
74
+ const result = workerResult.result
75
+
76
+ if (result.modified) {
77
+ if (options.check) {
78
+ this.#reportChanges(filePath, result.changes)
79
+ }
80
+
81
+ if (options.write) {
82
+ fs.writeFileSync(filePath, result.code, "utf8")
83
+ if (!options.check) {
84
+ console.log(`✓ ${filePath}`)
85
+ } else {
86
+ console.log(` ✓ written`)
87
+ }
88
+ }
89
+
90
+ return { modified: true, changes: result.changes, error: false }
91
+ } else {
92
+ if (!options.check) {
93
+ console.log(` ${filePath}`)
94
+ }
95
+ return { modified: false, changes: [], error: false }
61
96
  }
62
97
  } catch (error) {
63
- console.error(`Error: Cannot access '${pattern}': ${error.message}`)
64
- process.exit(1)
98
+ console.error(`✗ Error: ${filePath}: ${error.message}`)
99
+ return { modified: false, changes: [], error: true }
65
100
  }
66
101
  }
67
102
 
68
- return files
103
+ #reportChanges(filePath, changes) {
104
+ const changesByType = {}
105
+ if (changes && changes.length > 0) {
106
+ for (const change of changes) {
107
+ if (!changesByType[change.type]) {
108
+ changesByType[change.type] = 0
109
+ }
110
+ changesByType[change.type]++
111
+ }
112
+ }
113
+
114
+ const transformations = Object.keys(changesByType)
115
+ .map((type) =>
116
+ type
117
+ .replace(/([A-Z])/g, " $1")
118
+ .trim()
119
+ .toLowerCase(),
120
+ )
121
+ .join(", ")
122
+
123
+ console.log(`✗ ${filePath}`)
124
+ if (transformations) {
125
+ console.log(` ${transformations}`)
126
+ }
127
+ }
69
128
  }
70
129
 
71
- function walkDirectory(dir) {
72
- const files = []
73
- const entries = fs.readdirSync(dir, { withFileTypes: true })
130
+ /**
131
+ * Manages a pool of workers for parallel file processing
132
+ */
133
+ class WorkerPool {
134
+ constructor(fileProcessor, maxWorkers = os.cpus().length) {
135
+ this.fileProcessor = fileProcessor
136
+ this.maxWorkers = maxWorkers
137
+ }
74
138
 
75
- for (const entry of entries) {
76
- const fullPath = path.join(dir, entry.name)
139
+ /**
140
+ * Process files with a worker pool for better CPU utilization
141
+ */
142
+ async processFiles(files, options) {
143
+ const results = new Array(files.length)
144
+ let fileIndex = 0
77
145
 
78
- if (entry.isDirectory()) {
79
- if (entry.name === "node_modules" || entry.name === ".git") {
80
- continue
81
- }
82
- files.push(...walkDirectory(fullPath))
83
- } else if (entry.isFile()) {
84
- const ext = path.extname(entry.name)
85
- if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) {
86
- files.push(fullPath)
146
+ // Worker pool pattern - each worker processes files until queue is empty
147
+ const processNext = async () => {
148
+ while (fileIndex < files.length) {
149
+ const currentIndex = fileIndex++
150
+ const file = files[currentIndex]
151
+ results[currentIndex] = await this.fileProcessor.processFile(file, options)
87
152
  }
88
153
  }
89
- }
90
154
 
91
- return files
155
+ // Start worker pool and wait for all to complete
156
+ const workerCount = Math.min(this.maxWorkers, files.length)
157
+ const workers = Array.from({ length: workerCount }, () => processNext())
158
+ await Promise.all(workers)
159
+
160
+ return results
161
+ }
92
162
  }
93
163
 
94
- function processFile(filePath, options) {
95
- try {
96
- const code = fs.readFileSync(filePath, "utf8")
97
- const result = transform(code, options.baseline)
98
-
99
- if (result.modified) {
100
- if (options.check) {
101
- // Group changes by type for summary
102
- const changesByType = {}
103
- if (result.changes && result.changes.length > 0) {
104
- for (const change of result.changes) {
105
- if (!changesByType[change.type]) {
106
- changesByType[change.type] = 0
107
- }
108
- changesByType[change.type]++
109
- }
110
- }
164
+ /**
165
+ * Finds JavaScript files from patterns
166
+ */
167
+ class FileFinder {
168
+ static IGNORED_DIRS = ["node_modules", ".git"]
169
+ static JS_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]
111
170
 
112
- const transformations = Object.keys(changesByType)
113
- .map((type) => {
114
- const displayName = type
115
- .replace(/([A-Z])/g, " $1")
116
- .trim()
117
- .toLowerCase()
118
- return displayName
119
- })
120
- .join(", ")
121
-
122
- console.log(`✗ ${filePath}`)
123
- if (transformations) {
124
- console.log(` ${transformations}`)
125
- }
126
- }
171
+ constructor() {}
127
172
 
128
- if (options.write) {
129
- fs.writeFileSync(filePath, result.code, "utf8")
130
- if (!options.check) {
131
- console.log(`✓ ${filePath}`)
132
- } else {
133
- console.log(` ✓ written`)
173
+ /**
174
+ * Find files matching patterns
175
+ */
176
+ *find(patterns) {
177
+ for (const pattern of patterns) {
178
+ try {
179
+ const stats = fs.statSync(pattern)
180
+
181
+ if (stats.isFile()) {
182
+ yield pattern
183
+ } else if (stats.isDirectory()) {
184
+ yield* this._walkDirectory(pattern)
134
185
  }
186
+ } catch (error) {
187
+ console.error(`Error: Cannot access '${pattern}': ${error.message}`)
188
+ process.exit(1)
135
189
  }
190
+ }
191
+ }
136
192
 
137
- return { modified: true, changes: result.changes }
138
- } else {
139
- if (!options.check) {
140
- console.log(` ${filePath}`)
193
+ *_walkDirectory(dir) {
194
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
195
+
196
+ for (const entry of entries) {
197
+ const fullPath = path.join(dir, entry.name)
198
+
199
+ if (entry.isDirectory()) {
200
+ if (FileFinder.IGNORED_DIRS.includes(entry.name)) {
201
+ continue
202
+ }
203
+ yield* this._walkDirectory(fullPath)
204
+ } else if (entry.isFile()) {
205
+ const ext = path.extname(entry.name)
206
+ if (FileFinder.JS_EXTENSIONS.includes(ext)) {
207
+ yield fullPath
208
+ }
141
209
  }
142
- return { modified: false, changes: [] }
143
210
  }
144
- } catch (error) {
145
- console.error(`✗ Error: ${filePath}: ${error.message}`)
146
- return { modified: false, changes: [] }
147
211
  }
148
212
  }
149
213
 
150
- function processFiles(patterns, options) {
151
- const files = findFiles(patterns)
152
-
153
- if (files.length === 0) {
154
- console.log("No JavaScript files found")
155
- process.exit(0)
214
+ /**
215
+ * Orchestrates the CLI application
216
+ */
217
+ class CLIRunner {
218
+ constructor(workerPath) {
219
+ const workerRunner = new WorkerRunner(workerPath)
220
+ const fileProcessor = new FileProcessor(workerRunner)
221
+ this.workerPool = new WorkerPool(fileProcessor)
222
+ this.fileFinder = new FileFinder()
156
223
  }
157
224
 
158
- let modifiedCount = 0
159
- const allChanges = []
225
+ /**
226
+ * Process files and report results
227
+ */
228
+ async run(patterns, options) {
229
+ const files = [...this.fileFinder.find(patterns)]
160
230
 
161
- for (const file of files) {
162
- const result = processFile(file, options)
163
- if (result.modified) {
164
- modifiedCount++
165
- allChanges.push(...result.changes)
231
+ if (files.length === 0) {
232
+ console.log("No JavaScript files found")
233
+ process.exit(0)
166
234
  }
235
+
236
+ const results = await this.workerPool.processFiles(files, options)
237
+ this.#reportSummary(results, options)
167
238
  }
168
239
 
169
- // Summary
170
- if (options.check) {
240
+ #reportSummary(results, options) {
241
+ let modifiedCount = 0
242
+ const allChanges = results.flatMap((result) =>
243
+ result.modified ? (modifiedCount++, result.changes) : [],
244
+ )
245
+
246
+ const errorCount = results.filter((result) => result.error).length
247
+
171
248
  console.log("")
172
- if (modifiedCount > 0) {
173
- // Count unique transformation types
174
- const transformTypes = new Set(allChanges.map((c) => c.type))
175
- const typeCount = transformTypes.size
176
- const totalChanges = allChanges.length
177
-
178
- console.log(
179
- `${modifiedCount} file(s) need upgrading (${totalChanges} change${totalChanges !== 1 ? "s" : ""}, ${typeCount} type${typeCount !== 1 ? "s" : ""})`,
180
- )
181
- if (options.write) {
182
- console.log("Changes have been written")
249
+
250
+ if (options.check) {
251
+ if (modifiedCount > 0) {
252
+ const transformTypes = new Set(allChanges.map((c) => c.type))
253
+ const typeCount = transformTypes.size
254
+ const totalChanges = allChanges.length
255
+
256
+ console.log(
257
+ `${modifiedCount} file${modifiedCount !== 1 ? "s" : ""} need${modifiedCount === 1 ? "s" : ""} upgrading (${totalChanges} change${totalChanges !== 1 ? "s" : ""}, ${typeCount} type${typeCount !== 1 ? "s" : ""})`,
258
+ )
259
+ if (options.write) {
260
+ console.log("Changes have been written")
261
+ }
262
+ } else {
263
+ console.log("All files are up to date")
183
264
  }
184
265
  } else {
185
- console.log("All files are up to date")
266
+ if (modifiedCount > 0) {
267
+ console.log(`✓ ${modifiedCount} file${modifiedCount !== 1 ? "s" : ""} upgraded`)
268
+ } else {
269
+ console.log("All files are up to date")
270
+ }
186
271
  }
187
- } else {
188
- console.log("")
189
- if (modifiedCount > 0) {
190
- console.log(`✓ ${modifiedCount} file(s) upgraded`)
191
- } else {
192
- console.log("All files are up to date")
272
+
273
+ // Errors take precedence over --check flag.
274
+ // Exit with error code if any file processing errors occurred.
275
+ if (errorCount > 0) {
276
+ process.exit(1)
193
277
  }
194
- }
195
278
 
196
- // Exit with code 1 if --check specified and there were changes
197
- if (options.check && modifiedCount > 0) {
198
- process.exit(1)
279
+ if (options.check && modifiedCount > 0) {
280
+ process.exit(1)
281
+ }
199
282
  }
200
283
  }
201
284
 
285
+ // Initialize CLI
286
+ const program = new Command()
287
+ const cliRunner = new CLIRunner(path.join(__dirname, "../src/worker.js"))
288
+
289
+ program
290
+ .name("esupgrade")
291
+ .description("Auto-upgrade your JavaScript syntax")
292
+ .argument("<files...>", "Files or directories to process")
293
+ .addOption(
294
+ new Option("--baseline <level>", "Set baseline level for transformations")
295
+ .choices(["widely-available", "newly-available"])
296
+ .default("widely-available"),
297
+ )
298
+ .option("--check", "Report which files need upgrading and exit with code 1 if any do")
299
+ .option(
300
+ "--write",
301
+ "Write changes to files (default: true unless only --check is specified)",
302
+ )
303
+ .action(async (files, options) => {
304
+ // Handle check/write options - they are not mutually exclusive
305
+ // Default: write is true unless ONLY --check is specified (no --write)
306
+ const shouldWrite = options.write !== undefined ? options.write : !options.check
307
+ const shouldCheck = options.check || false
308
+
309
+ const processingOptions = {
310
+ baseline: options.baseline,
311
+ write: shouldWrite,
312
+ check: shouldCheck,
313
+ }
314
+
315
+ await cliRunner.run(files, processingOptions)
316
+ })
317
+
202
318
  program.parse()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.3.1",
3
+ "version": "2025.3.3",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -11,10 +11,10 @@ import * as newlyAvailable from "./newlyAvailable.js"
11
11
  */
12
12
 
13
13
  /**
14
- * Transform JavaScript code using the specified transformers
15
- * @param {string} code - The source code to transform
16
- * @param {string} baseline - Baseline level ('widely-available' or 'newly-available')
17
- * @returns {TransformResult} - Object with { code, modified, changes }
14
+ * Transform JavaScript code using the specified transformers.
15
+ * @param {string} code - The source code to transform.
16
+ * @param {string} baseline - Baseline level ('widely-available' or 'newly-available').
17
+ * @returns {TransformResult} Object with transformed code, modification status, and changes.
18
18
  */
19
19
  export function transform(code, baseline = "widely-available") {
20
20
  const j = jscodeshift.withParser("tsx")
@@ -22,11 +22,14 @@ export function transform(code, baseline = "widely-available") {
22
22
 
23
23
  let modified = false
24
24
  const allChanges = []
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)
25
+
26
+ const transformers =
27
+ baseline === "newly-available"
28
+ ? { ...widelyAvailable, ...newlyAvailable }
29
+ : widelyAvailable
30
+
31
+ for (const transformer of Object.values(transformers)) {
32
+ const result = transformer(j, root)
30
33
  if (result.modified) {
31
34
  modified = true
32
35
  allChanges.push(...result.changes)
package/src/worker.js ADDED
@@ -0,0 +1,26 @@
1
+ import { parentPort, workerData } from "worker_threads"
2
+ import fs from "fs"
3
+ import { transform } from "./index.js"
4
+
5
+ /**
6
+ * Worker thread for processing files in parallel.
7
+ */
8
+
9
+ const { filePath, baseline } = workerData
10
+
11
+ try {
12
+ const code = fs.readFileSync(filePath, "utf8")
13
+ const result = transform(code, baseline)
14
+
15
+ parentPort.postMessage({
16
+ success: true,
17
+ filePath,
18
+ result,
19
+ })
20
+ } catch (error) {
21
+ parentPort.postMessage({
22
+ success: false,
23
+ filePath,
24
+ error: error.message,
25
+ })
26
+ }
package/tests/cli.test.js CHANGED
@@ -27,10 +27,10 @@ describe("CLI", () => {
27
27
 
28
28
  assert.match(
29
29
  result.stderr,
30
- /Error: No files specified/,
30
+ /error: missing required argument 'files'/,
31
31
  "displays error for no files",
32
32
  )
33
- assert.equal(result.status, 0, "exits with 0 when showing help")
33
+ assert.equal(result.status, 1, "exits with 1 when showing help")
34
34
  })
35
35
 
36
36
  test("transform a single file with --write", () => {
@@ -46,7 +46,7 @@ describe("CLI", () => {
46
46
  /const x = 1/,
47
47
  "transforms var to const",
48
48
  )
49
- assert.match(result.stdout, /✓ 1 file\(s\) upgraded/, "reports 1 file upgraded")
49
+ assert.match(result.stdout, /✓ 1 file upgraded/, "reports 1 file upgraded")
50
50
  assert.equal(result.status, 0, "exits successfully")
51
51
  })
52
52
 
@@ -113,7 +113,7 @@ describe("CLI", () => {
113
113
  assert.match(result.stdout, /✗/, "indicates changes needed")
114
114
  assert.match(
115
115
  result.stdout,
116
- /1 file\(s\) need upgrading/,
116
+ /1 file needs upgrading/,
117
117
  "reports 1 file needs upgrading",
118
118
  )
119
119
  assert.equal(result.status, 1, "exits with 1 when changes needed")
@@ -178,7 +178,7 @@ describe("CLI", () => {
178
178
 
179
179
  assert.match(fs.readFileSync(file1, "utf8"), /const x = 1/, "transforms file1")
180
180
  assert.match(fs.readFileSync(file2, "utf8"), /const y = 2/, "transforms file2")
181
- assert.match(result.stdout, /2 file\(s\) upgraded/, "reports 2 files upgraded")
181
+ assert.match(result.stdout, /2 files upgraded/, "reports 2 files upgraded")
182
182
  assert.equal(result.status, 0, "exits successfully")
183
183
  })
184
184
 
@@ -215,7 +215,7 @@ describe("CLI", () => {
215
215
  encoding: "utf8",
216
216
  })
217
217
 
218
- assert.match(result.stdout, /4 file\(s\) upgraded/, "reports 4 files upgraded")
218
+ assert.match(result.stdout, /4 files upgraded/, "reports 4 files upgraded")
219
219
  assert.equal(result.status, 0, "exits successfully")
220
220
  })
221
221
 
@@ -290,7 +290,7 @@ describe("CLI", () => {
290
290
 
291
291
  assert.match(fs.readFileSync(file1, "utf8"), /const x = 1/, "transforms file1")
292
292
  assert.match(fs.readFileSync(file2, "utf8"), /const y = 2/, "transforms file2")
293
- assert.match(result.stdout, /2 file\(s\) upgraded/, "reports 2 files upgraded")
293
+ assert.match(result.stdout, /2 files upgraded/, "reports 2 files upgraded")
294
294
  assert.equal(result.status, 0, "exits successfully")
295
295
  })
296
296
 
@@ -361,7 +361,7 @@ describe("CLI", () => {
361
361
  assert.equal(result.status, 1, "exits with 1")
362
362
  })
363
363
 
364
- test("handle syntax errors gracefully", () => {
364
+ test("exit with 1 on syntax errors", () => {
365
365
  const testFile = path.join(tempDir, "test.js")
366
366
  fs.writeFileSync(testFile, `var x = {{{;`)
367
367
 
@@ -370,7 +370,50 @@ describe("CLI", () => {
370
370
  })
371
371
 
372
372
  assert.match(result.stderr, /✗ Error:/, "displays error for syntax issues")
373
- assert.equal(result.status, 0, "continues despite errors")
373
+ assert.equal(result.status, 1, "exits with 1 on errors")
374
+ })
375
+
376
+ test("exit with 1 on parsing errors with --check", () => {
377
+ const testFile = path.join(tempDir, "test.js")
378
+ fs.writeFileSync(testFile, `const a;\na = 'asdf'`)
379
+
380
+ const result = spawnSync(process.execPath, [CLI_PATH, testFile, "--check"], {
381
+ encoding: "utf8",
382
+ })
383
+
384
+ assert.match(result.stderr, /✗ Error:/, "displays error for parsing issues")
385
+ assert.equal(result.status, 1, "exits with 1 on errors with --check")
386
+ })
387
+
388
+ test("exit with 1 on parsing errors without --check", () => {
389
+ const testFile = path.join(tempDir, "test.js")
390
+ fs.writeFileSync(testFile, `const a;\na = 'asdf'`)
391
+
392
+ const result = spawnSync(process.execPath, [CLI_PATH, testFile], {
393
+ encoding: "utf8",
394
+ })
395
+
396
+ assert.match(result.stderr, /✗ Error:/, "displays error for parsing issues")
397
+ assert.equal(result.status, 1, "exits with 1 on errors without --check")
398
+ })
399
+
400
+ test("exit with 1 on errors even with valid files", () => {
401
+ const validFile = path.join(tempDir, "valid.js")
402
+ const invalidFile = path.join(tempDir, "invalid.js")
403
+ fs.writeFileSync(validFile, `var x = 1;`)
404
+ fs.writeFileSync(invalidFile, `const a;\na = 'asdf'`)
405
+
406
+ const result = spawnSync(
407
+ process.execPath,
408
+ [CLI_PATH, validFile, invalidFile, "--check"],
409
+ {
410
+ encoding: "utf8",
411
+ },
412
+ )
413
+
414
+ assert.match(result.stderr, /✗ Error:/, "displays error for invalid file")
415
+ assert.match(result.stdout, /valid\.js/, "processes valid file")
416
+ assert.equal(result.status, 1, "exits with 1 when any file has errors")
374
417
  })
375
418
 
376
419
  test("handle mixed directory and file arguments", () => {
@@ -388,7 +431,36 @@ describe("CLI", () => {
388
431
 
389
432
  assert.match(fs.readFileSync(file1, "utf8"), /const x = 1/, "transforms file1")
390
433
  assert.match(fs.readFileSync(file2, "utf8"), /const y = 2/, "transforms file2")
391
- assert.match(result.stdout, /2 file\(s\) upgraded/, "reports 2 files upgraded")
434
+ assert.match(result.stdout, /2 files upgraded/, "reports 2 files upgraded")
435
+ assert.equal(result.status, 0, "exits successfully")
436
+ })
437
+
438
+ test("show individual file markers for mixed results", () => {
439
+ const file1 = path.join(tempDir, "test1.js")
440
+ const file2 = path.join(tempDir, "test2.js")
441
+ fs.writeFileSync(file1, `var x = 1;`)
442
+ fs.writeFileSync(file2, `const y = 2;`) // Already upgraded
443
+
444
+ const result = spawnSync(process.execPath, [CLI_PATH, file1, file2, "--write"], {
445
+ encoding: "utf8",
446
+ })
447
+
448
+ assert.match(result.stdout, /✓/, "shows check mark for modified file")
449
+ assert.match(result.stdout, /test2\.js/, "shows unmodified file path")
392
450
  assert.equal(result.status, 0, "exits successfully")
393
451
  })
452
+
453
+ test("exit with 1 on file write errors", () => {
454
+ const testFile = path.join(tempDir, "test.js")
455
+ fs.writeFileSync(testFile, `var x = 1;`)
456
+ // Make file read-only to trigger write error
457
+ fs.chmodSync(testFile, 0o444)
458
+
459
+ const result = spawnSync(process.execPath, [CLI_PATH, testFile, "--write"], {
460
+ encoding: "utf8",
461
+ })
462
+
463
+ assert.match(result.stderr, /✗ Error:/, "displays error for write issues")
464
+ assert.equal(result.status, 1, "exits with 1 on write errors")
465
+ })
394
466
  })
@@ -5,20 +5,12 @@ import { transform } from "../src/index.js"
5
5
  describe("newly-available", () => {
6
6
  describe("Promise.try", () => {
7
7
  test("transforms resolve call with argument", () => {
8
- assert(
9
- transform(
10
- `const p = new Promise((resolve) => resolve(getData()));`,
11
- "newly-available",
12
- ).modified,
13
- "transform Promise constructor to Promise.try",
14
- )
15
- assert.match(
16
- transform(
17
- `const p = new Promise((resolve) => resolve(getData()));`,
18
- "newly-available",
19
- ).code,
20
- /Promise\.try/,
8
+ const result = transform(
9
+ `const p = new Promise((resolve) => resolve(getData()));`,
10
+ "newly-available",
21
11
  )
12
+ assert(result.modified, "transform Promise constructor to Promise.try")
13
+ assert.match(result.code, /Promise\.try/)
22
14
  })
23
15
 
24
16
  test("not available in widely-available baseline", () => {
@@ -33,347 +25,199 @@ describe("newly-available", () => {
33
25
  })
34
26
 
35
27
  test("transforms function passed to resolve", () => {
36
- assert(
37
- transform(
38
- `const p = new Promise((resolve) => setTimeout(resolve));`,
39
- "newly-available",
40
- ).modified,
28
+ const result = transform(
29
+ `const p = new Promise((resolve) => setTimeout(resolve));`,
30
+ "newly-available",
41
31
  )
32
+ assert(result.modified)
42
33
  assert.match(
43
- transform(
44
- `const p = new Promise((resolve) => setTimeout(resolve));`,
45
- "newly-available",
46
- ).code,
34
+ result.code,
47
35
  /Promise\.try\(setTimeout\)/,
48
36
  "transform to Promise.try(setTimeout) not Promise.try(() => setTimeout(resolve))",
49
37
  )
50
- assert.doesNotMatch(
51
- transform(
52
- `const p = new Promise((resolve) => setTimeout(resolve));`,
53
- "newly-available",
54
- ).code,
55
- /resolve/,
56
- )
38
+ assert.doesNotMatch(result.code, /resolve/)
57
39
  })
58
40
 
59
41
  test("skip when awaited", () => {
60
- assert(
61
- !transform(
62
- `async function foo() {
63
- await new Promise((resolve) => setTimeout(resolve, 1000));
64
- }`,
65
- "newly-available",
66
- ).modified,
67
- "do not transform awaited Promises",
68
- )
69
- assert.match(
70
- transform(
71
- `async function foo() {
72
- await new Promise((resolve) => setTimeout(resolve, 1000));
73
- }`,
74
- "newly-available",
75
- ).code,
76
- /await new Promise/,
77
- )
78
- assert.doesNotMatch(
79
- transform(
80
- `async function foo() {
42
+ const result = transform(
43
+ `async function foo() {
81
44
  await new Promise((resolve) => setTimeout(resolve, 1000));
82
45
  }`,
83
- "newly-available",
84
- ).code,
85
- /Promise\.try/,
46
+ "newly-available",
86
47
  )
48
+ assert(!result.modified, "do not transform awaited Promises")
49
+ assert.match(result.code, /await new Promise/)
50
+ assert.doesNotMatch(result.code, /Promise\.try/)
87
51
  })
88
52
 
89
53
  test("skip non-Promise constructors", () => {
90
- assert(
91
- !transform(
92
- `const p = new MyPromise((resolve) => resolve(getData()));`,
93
- "newly-available",
94
- ).modified,
95
- )
96
- assert.match(
97
- transform(
98
- `const p = new MyPromise((resolve) => resolve(getData()));`,
99
- "newly-available",
100
- ).code,
101
- /new MyPromise/,
54
+ const result = transform(
55
+ `const p = new MyPromise((resolve) => resolve(getData()));`,
56
+ "newly-available",
102
57
  )
58
+ assert(!result.modified)
59
+ assert.match(result.code, /new MyPromise/)
103
60
  })
104
61
 
105
62
  test("skip with 0 arguments", () => {
106
- assert(!transform(`const p = new Promise();`, "newly-available").modified)
107
- assert.match(
108
- transform(`const p = new Promise();`, "newly-available").code,
109
- /new Promise\(\)/,
110
- )
63
+ const result = transform(`const p = new Promise();`, "newly-available")
64
+ assert(!result.modified)
65
+ assert.match(result.code, /new Promise\(\)/)
111
66
  })
112
67
 
113
68
  test("skip with multiple arguments", () => {
114
- assert(
115
- !transform(
116
- `const p = new Promise((resolve) => resolve(1), extraArg);`,
117
- "newly-available",
118
- ).modified,
119
- )
120
- assert.match(
121
- transform(
122
- `const p = new Promise((resolve) => resolve(1), extraArg);`,
123
- "newly-available",
124
- ).code,
125
- /new Promise/,
69
+ const result = transform(
70
+ `const p = new Promise((resolve) => resolve(1), extraArg);`,
71
+ "newly-available",
126
72
  )
73
+ assert(!result.modified)
74
+ assert.match(result.code, /new Promise/)
127
75
  })
128
76
 
129
77
  test("skip with non-function argument", () => {
130
- assert(!transform(`const p = new Promise(executor);`, "newly-available").modified)
131
- assert.match(
132
- transform(`const p = new Promise(executor);`, "newly-available").code,
133
- /new Promise\(executor\)/,
134
- )
78
+ const result = transform(`const p = new Promise(executor);`, "newly-available")
79
+ assert(!result.modified)
80
+ assert.match(result.code, /new Promise\(executor\)/)
135
81
  })
136
82
 
137
83
  test("skip with 0 params", () => {
138
- assert(
139
- !transform(
140
- `const p = new Promise(() => console.log('test'));`,
141
- "newly-available",
142
- ).modified,
143
- )
144
- assert.match(
145
- transform(
146
- `const p = new Promise(() => console.log('test'));`,
147
- "newly-available",
148
- ).code,
149
- /new Promise/,
84
+ const result = transform(
85
+ `const p = new Promise(() => console.log('test'));`,
86
+ "newly-available",
150
87
  )
88
+ assert(!result.modified)
89
+ assert.match(result.code, /new Promise/)
151
90
  })
152
91
 
153
92
  test("skip with more than 2 params", () => {
154
- assert(
155
- !transform(
156
- `const p = new Promise((resolve, reject, extra) => resolve(1));`,
157
- "newly-available",
158
- ).modified,
159
- )
160
- assert.match(
161
- transform(
162
- `const p = new Promise((resolve, reject, extra) => resolve(1));`,
163
- "newly-available",
164
- ).code,
165
- /new Promise/,
93
+ const result = transform(
94
+ `const p = new Promise((resolve, reject, extra) => resolve(1));`,
95
+ "newly-available",
166
96
  )
97
+ assert(!result.modified)
98
+ assert.match(result.code, /new Promise/)
167
99
  })
168
100
 
169
101
  test("transforms block statement with resolve call", () => {
170
- assert(
171
- transform(
172
- `const p = new Promise((resolve) => { resolve(getData()); });`,
173
- "newly-available",
174
- ).modified,
175
- )
176
- assert.match(
177
- transform(
178
- `const p = new Promise((resolve) => { resolve(getData()); });`,
179
- "newly-available",
180
- ).code,
181
- /Promise\.try/,
102
+ const result = transform(
103
+ `const p = new Promise((resolve) => { resolve(getData()); });`,
104
+ "newly-available",
182
105
  )
106
+ assert(result.modified)
107
+ assert.match(result.code, /Promise\.try/)
183
108
  })
184
109
 
185
110
  test("skip with arrow function expression body as function call", () => {
111
+ const result = transform(
112
+ `const p = new Promise((resolve) => computeValue());`,
113
+ "newly-available",
114
+ )
186
115
  assert(
187
- !transform(
188
- `const p = new Promise((resolve) => computeValue());`,
189
- "newly-available",
190
- ).modified,
116
+ !result.modified,
191
117
  "do not transform because computeValue() is not calling resolve",
192
118
  )
193
- assert.match(
194
- transform(
195
- `const p = new Promise((resolve) => computeValue());`,
196
- "newly-available",
197
- ).code,
198
- /new Promise/,
199
- )
119
+ assert.match(result.code, /new Promise/)
200
120
  })
201
121
 
202
122
  test("skip with arrow function returning a value directly", () => {
123
+ const result = transform(
124
+ `const p = new Promise((resolve) => someFunction(arg1, arg2));`,
125
+ "newly-available",
126
+ )
203
127
  assert(
204
- !transform(
205
- `const p = new Promise((resolve) => someFunction(arg1, arg2));`,
206
- "newly-available",
207
- ).modified,
128
+ !result.modified,
208
129
  "do not transform function call that doesn't involve resolve",
209
130
  )
210
- assert.match(
211
- transform(
212
- `const p = new Promise((resolve) => someFunction(arg1, arg2));`,
213
- "newly-available",
214
- ).code,
215
- /new Promise/,
216
- )
131
+ assert.match(result.code, /new Promise/)
217
132
  })
218
133
 
219
134
  test("skip with non-call expression body", () => {
220
- assert(
221
- !transform(`const p = new Promise((resolve) => someValue);`, "newly-available")
222
- .modified,
223
- )
224
- assert.match(
225
- transform(`const p = new Promise((resolve) => someValue);`, "newly-available")
226
- .code,
227
- /new Promise/,
135
+ const result = transform(
136
+ `const p = new Promise((resolve) => someValue);`,
137
+ "newly-available",
228
138
  )
139
+ assert(!result.modified)
140
+ assert.match(result.code, /new Promise/)
229
141
  })
230
142
 
231
143
  test("skip with wrong number of arguments to resolve", () => {
232
- assert(
233
- !transform(
234
- `const p = new Promise((resolve) => func(resolve, extra));`,
235
- "newly-available",
236
- ).modified,
237
- )
238
- assert.match(
239
- transform(
240
- `const p = new Promise((resolve) => func(resolve, extra));`,
241
- "newly-available",
242
- ).code,
243
- /new Promise/,
144
+ const result = transform(
145
+ `const p = new Promise((resolve) => func(resolve, extra));`,
146
+ "newly-available",
244
147
  )
148
+ assert(!result.modified)
149
+ assert.match(result.code, /new Promise/)
245
150
  })
246
151
 
247
152
  test("skip with non-identifier resolve", () => {
248
- assert(
249
- !transform(`const p = new Promise((resolve) => func(123));`, "newly-available")
250
- .modified,
251
- )
252
- assert.match(
253
- transform(`const p = new Promise((resolve) => func(123));`, "newly-available")
254
- .code,
255
- /new Promise/,
153
+ const result = transform(
154
+ `const p = new Promise((resolve) => func(123));`,
155
+ "newly-available",
256
156
  )
157
+ assert(!result.modified)
158
+ assert.match(result.code, /new Promise/)
257
159
  })
258
160
 
259
161
  test("skip with resolve call with 0 arguments", () => {
260
- assert(
261
- !transform(`const p = new Promise((resolve) => resolve());`, "newly-available")
262
- .modified,
263
- )
264
- assert.match(
265
- transform(`const p = new Promise((resolve) => resolve());`, "newly-available")
266
- .code,
267
- /new Promise/,
162
+ const result = transform(
163
+ `const p = new Promise((resolve) => resolve());`,
164
+ "newly-available",
268
165
  )
166
+ assert(!result.modified)
167
+ assert.match(result.code, /new Promise/)
269
168
  })
270
169
 
271
170
  test("skip with block with multiple statements", () => {
272
- assert(
273
- !transform(
274
- `const p = new Promise((resolve) => {
171
+ const result = transform(
172
+ `const p = new Promise((resolve) => {
275
173
  const data = getData();
276
174
  resolve(data);
277
175
  });`,
278
- "newly-available",
279
- ).modified,
280
- )
281
- assert.match(
282
- transform(
283
- `const p = new Promise((resolve) => {
284
- const data = getData();
285
- resolve(data);
286
- });`,
287
- "newly-available",
288
- ).code,
289
- /new Promise/,
176
+ "newly-available",
290
177
  )
178
+ assert(!result.modified)
179
+ assert.match(result.code, /new Promise/)
291
180
  })
292
181
 
293
182
  test("skip with block with non-expression statement", () => {
294
- assert(
295
- !transform(
296
- `const p = new Promise((resolve) => {
183
+ const result = transform(
184
+ `const p = new Promise((resolve) => {
297
185
  if (true) resolve(1);
298
186
  });`,
299
- "newly-available",
300
- ).modified,
301
- )
302
- assert.match(
303
- transform(
304
- `const p = new Promise((resolve) => {
305
- if (true) resolve(1);
306
- });`,
307
- "newly-available",
308
- ).code,
309
- /new Promise/,
187
+ "newly-available",
310
188
  )
189
+ assert(!result.modified)
190
+ assert.match(result.code, /new Promise/)
311
191
  })
312
192
 
313
193
  test("transforms function expression", () => {
314
- assert(
315
- transform(
316
- `const p = new Promise(function(resolve) { resolve(getData()); });`,
317
- "newly-available",
318
- ).modified,
319
- )
320
- assert.match(
321
- transform(
322
- `const p = new Promise(function(resolve) { resolve(getData()); });`,
323
- "newly-available",
324
- ).code,
325
- /Promise\.try/,
194
+ const result = transform(
195
+ `const p = new Promise(function(resolve) { resolve(getData()); });`,
196
+ "newly-available",
326
197
  )
198
+ assert(result.modified)
199
+ assert.match(result.code, /Promise\.try/)
327
200
  })
328
201
 
329
202
  test("transforms with both resolve and reject params", () => {
330
- assert(
331
- transform(
332
- `const p = new Promise((resolve, reject) => resolve(getData()));`,
333
- "newly-available",
334
- ).modified,
335
- )
336
- assert.match(
337
- transform(
338
- `const p = new Promise((resolve, reject) => resolve(getData()));`,
339
- "newly-available",
340
- ).code,
341
- /Promise\.try/,
203
+ const result = transform(
204
+ `const p = new Promise((resolve, reject) => resolve(getData()));`,
205
+ "newly-available",
342
206
  )
207
+ assert(result.modified)
208
+ assert.match(result.code, /Promise\.try/)
343
209
  })
344
210
 
345
211
  test("tracks line numbers correctly", () => {
346
- assert(
347
- transform(
348
- `// Line 1
349
- const p = new Promise((resolve) => resolve(getData()));`,
350
- "newly-available",
351
- ).modified,
352
- )
353
- assert.equal(
354
- transform(
355
- `// Line 1
356
- const p = new Promise((resolve) => resolve(getData()));`,
357
- "newly-available",
358
- ).changes.length,
359
- 1,
360
- )
361
- assert.equal(
362
- transform(
363
- `// Line 1
364
- const p = new Promise((resolve) => resolve(getData()));`,
365
- "newly-available",
366
- ).changes[0].type,
367
- "promiseTry",
368
- )
369
- assert.equal(
370
- transform(
371
- `// Line 1
212
+ const result = transform(
213
+ `// Line 1
372
214
  const p = new Promise((resolve) => resolve(getData()));`,
373
- "newly-available",
374
- ).changes[0].line,
375
- 2,
215
+ "newly-available",
376
216
  )
217
+ assert(result.modified)
218
+ assert.equal(result.changes.length, 1)
219
+ assert.equal(result.changes[0].type, "promiseTry")
220
+ assert.equal(result.changes[0].line, 2)
377
221
  })
378
222
  })
379
223
  })
@@ -1726,6 +1726,22 @@ const result = [1, 2].concat(other);`)
1726
1726
  })
1727
1727
 
1728
1728
  describe("general", () => {
1729
+ const input = `var x = 1;`
1730
+
1731
+ test("baseline widely-available", () => {
1732
+ const result = transform(input)
1733
+
1734
+ assert(result.modified, "transform with baseline widely-available")
1735
+ assert.match(result.code, /const x = 1/)
1736
+ })
1737
+
1738
+ test("baseline newly-available", () => {
1739
+ const result = transform(input, "newly-available")
1740
+
1741
+ assert(result.modified, "transform with baseline newly-available")
1742
+ assert.match(result.code, /const x = 1/)
1743
+ })
1744
+
1729
1745
  test("no changes", () => {
1730
1746
  const result = transform(`
1731
1747
  const x = 1;
@@ -1745,18 +1761,4 @@ describe("general", () => {
1745
1761
  assert.match(result.code, /const userName/)
1746
1762
  assert.match(result.code, /`Hello \$\{userName\}`/)
1747
1763
  })
1748
-
1749
- test("baseline widely-available", () => {
1750
- const result = transform(`var x = 1;`)
1751
-
1752
- assert(result.modified, "transform with baseline widely-available")
1753
- assert.match(result.code, /const x = 1/)
1754
- })
1755
-
1756
- test("baseline newly-available", () => {
1757
- const result = transform(`var x = 1;`, "newly-available")
1758
-
1759
- assert(result.modified, "transform with baseline newly-available")
1760
- assert.match(result.code, /const x = 1/)
1761
- })
1762
1764
  })