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 +262 -146
- package/package.json +1 -1
- package/src/index.js +12 -9
- package/src/worker.js +26 -0
- package/tests/cli.test.js +76 -4
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
35
|
+
const [message] = await once(worker, "message")
|
|
36
|
+
return message
|
|
37
|
+
}
|
|
38
|
+
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Processes individual files and handles output
|
|
42
|
+
*/
|
|
43
|
+
class FileProcessor {
|
|
44
|
+
constructor(workerRunner) {
|
|
45
|
+
this.workerRunner = workerRunner
|
|
46
|
+
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
64
|
-
|
|
98
|
+
console.error(`✗ Error: ${filePath}: ${error.message}`)
|
|
99
|
+
return { modified: false, changes: [], error: true }
|
|
65
100
|
}
|
|
66
101
|
}
|
|
67
102
|
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Process files and report results
|
|
227
|
+
*/
|
|
228
|
+
async run(patterns, options) {
|
|
229
|
+
const files = [...this.fileFinder.find(patterns)]
|
|
160
230
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
if
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
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}
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
/
|
|
30
|
+
/error: missing required argument 'files'/,
|
|
31
31
|
"displays error for no files",
|
|
32
32
|
)
|
|
33
|
-
assert.equal(result.status,
|
|
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("
|
|
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,
|
|
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
|
})
|