esupgrade 2025.3.2 → 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/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${modifiedCount !== 1 ? "s" : ""} need${modifiedCount === 1 ? "s" : ""} 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${modifiedCount !== 1 ? "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.2",
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", () => {
@@ -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", () => {
@@ -391,4 +434,33 @@ describe("CLI", () => {
391
434
  assert.match(result.stdout, /2 files upgraded/, "reports 2 files upgraded")
392
435
  assert.equal(result.status, 0, "exits successfully")
393
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")
450
+ assert.equal(result.status, 0, "exits successfully")
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
  })