@herb-tools/formatter 0.7.4 → 0.8.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.
- package/README.md +116 -11
- package/dist/herb-format.js +23209 -2215
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +1457 -330
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1457 -330
- package/dist/index.esm.js.map +1 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/format-helpers.d.ts +160 -0
- package/dist/types/format-printer.d.ts +87 -50
- package/dist/types/formatter.d.ts +18 -2
- package/dist/types/options.d.ts +7 -0
- package/dist/types/scaffold-template-detector.d.ts +12 -0
- package/dist/types/types.d.ts +23 -0
- package/package.json +5 -6
- package/src/cli.ts +357 -111
- package/src/format-helpers.ts +508 -0
- package/src/format-printer.ts +1010 -390
- package/src/formatter.ts +76 -4
- package/src/options.ts +12 -0
- package/src/scaffold-template-detector.ts +33 -0
- package/src/types.ts +27 -0
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import dedent from "dedent"
|
|
2
2
|
import { readFileSync, writeFileSync, statSync } from "fs"
|
|
3
3
|
import { glob } from "glob"
|
|
4
|
-
import {
|
|
4
|
+
import { resolve, relative } from "path"
|
|
5
5
|
|
|
6
6
|
import { Herb } from "@herb-tools/node-wasm"
|
|
7
|
+
import { Config, addHerbExtensionRecommendation, getExtensionsJsonRelativePath } from "@herb-tools/config"
|
|
8
|
+
import { colorize } from "@herb-tools/highlighter"
|
|
9
|
+
|
|
7
10
|
import { Formatter } from "./formatter.js"
|
|
11
|
+
import { ASTRewriter, StringRewriter, CustomRewriterLoader, builtinRewriters, isASTRewriterClass, isStringRewriterClass } from "@herb-tools/rewriter/loader"
|
|
8
12
|
import { parseArgs } from "util"
|
|
9
|
-
import { resolveFormatOptions } from "./options.js"
|
|
10
13
|
|
|
11
14
|
import { name, version, dependencies } from "../package.json"
|
|
12
15
|
|
|
@@ -16,32 +19,39 @@ const pluralize = (count: number, singular: string, plural: string = singular +
|
|
|
16
19
|
|
|
17
20
|
export class CLI {
|
|
18
21
|
private usage = dedent`
|
|
19
|
-
Usage: herb-format [
|
|
22
|
+
Usage: herb-format [files|directories|glob-patterns...] [options]
|
|
20
23
|
|
|
21
24
|
Arguments:
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
files|directories|glob-patterns Files, directories, or glob patterns to format, or '-' for stdin
|
|
26
|
+
Multiple arguments are supported (e.g., herb-format file1.erb file2.erb dir/)
|
|
27
|
+
Omit to format all configured files in current directory
|
|
24
28
|
|
|
25
29
|
Options:
|
|
26
30
|
-c, --check check if files are formatted without modifying them
|
|
27
31
|
-h, --help show help
|
|
28
32
|
-v, --version show version
|
|
33
|
+
--init create a .herb.yml configuration file in the current directory
|
|
34
|
+
--config-file <path> explicitly specify path to .herb.yml config file
|
|
35
|
+
--force force formatting even if disabled in .herb.yml
|
|
29
36
|
--indent-width <number> number of spaces per indentation level (default: 2)
|
|
30
37
|
--max-line-length <number> maximum line length before wrapping (default: 80)
|
|
31
38
|
|
|
32
39
|
Examples:
|
|
33
|
-
herb-format
|
|
34
|
-
herb-format index.html.erb
|
|
35
|
-
herb-format templates/index.html.erb
|
|
36
|
-
herb-format templates/
|
|
37
|
-
herb-format "templates/**/*.html.erb"
|
|
38
|
-
herb-format "**/*.html.erb"
|
|
39
|
-
herb-format "**/*.xml.erb"
|
|
40
|
-
|
|
41
|
-
herb-format --check
|
|
42
|
-
herb-format --
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
herb-format # Format all configured files in current directory
|
|
41
|
+
herb-format index.html.erb # Format and write single file
|
|
42
|
+
herb-format templates/index.html.erb # Format and write single file
|
|
43
|
+
herb-format templates/ # Format all configured files within the given directory
|
|
44
|
+
herb-format "templates/**/*.html.erb" # Format all \`**/*.html.erb\` files in the templates/ directory
|
|
45
|
+
herb-format "**/*.html.erb" # Format all \`*.html.erb\` files using glob pattern
|
|
46
|
+
herb-format "**/*.xml.erb" # Format all \`*.xml.erb\` files using glob pattern
|
|
47
|
+
|
|
48
|
+
herb-format --check # Check if all configured files are formatted
|
|
49
|
+
herb-format --check templates/ # Check if all configured files in templates/ are formatted
|
|
50
|
+
|
|
51
|
+
herb-format --force # Format even if disabled in project config
|
|
52
|
+
herb-format --indent-width 4 # Format with 4-space indentation
|
|
53
|
+
herb-format --max-line-length 100 # Format with 100-character line limit
|
|
54
|
+
cat template.html.erb | herb-format # Format from stdin to stdout
|
|
45
55
|
`
|
|
46
56
|
|
|
47
57
|
private parseArguments() {
|
|
@@ -49,8 +59,11 @@ export class CLI {
|
|
|
49
59
|
args: process.argv.slice(2),
|
|
50
60
|
options: {
|
|
51
61
|
help: { type: "boolean", short: "h" },
|
|
62
|
+
force: { type: "boolean" },
|
|
52
63
|
version: { type: "boolean", short: "v" },
|
|
53
64
|
check: { type: "boolean", short: "c" },
|
|
65
|
+
init: { type: "boolean" },
|
|
66
|
+
"config-file": { type: "string" },
|
|
54
67
|
"indent-width": { type: "string" },
|
|
55
68
|
"max-line-length": { type: "string" }
|
|
56
69
|
},
|
|
@@ -92,13 +105,16 @@ export class CLI {
|
|
|
92
105
|
positionals,
|
|
93
106
|
isCheckMode: values.check,
|
|
94
107
|
isVersionMode: values.version,
|
|
108
|
+
isForceMode: values.force,
|
|
109
|
+
isInitMode: values.init,
|
|
110
|
+
configFile: values["config-file"],
|
|
95
111
|
indentWidth,
|
|
96
112
|
maxLineLength
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
async run() {
|
|
101
|
-
const { positionals, isCheckMode, isVersionMode, indentWidth, maxLineLength } = this.parseArguments()
|
|
117
|
+
const { positionals, isCheckMode, isVersionMode, isForceMode, isInitMode, configFile, indentWidth, maxLineLength } = this.parseArguments()
|
|
102
118
|
|
|
103
119
|
try {
|
|
104
120
|
await Herb.load()
|
|
@@ -112,17 +128,209 @@ export class CLI {
|
|
|
112
128
|
process.exit(0)
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
if (positionals.includes('-') && positionals.length > 1) {
|
|
132
|
+
console.error("Error: Cannot mix stdin ('-') with file arguments")
|
|
133
|
+
process.exit(1)
|
|
134
|
+
}
|
|
117
135
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
maxLineLength
|
|
121
|
-
})
|
|
136
|
+
const file = positionals[0]
|
|
137
|
+
const startPath = file || process.cwd()
|
|
122
138
|
|
|
123
|
-
|
|
139
|
+
if (isInitMode) {
|
|
140
|
+
const configPath = configFile || startPath
|
|
124
141
|
|
|
125
|
-
|
|
142
|
+
if (Config.exists(configPath)) {
|
|
143
|
+
const fullPath = configFile || Config.configPathFromProjectPath(startPath)
|
|
144
|
+
console.log(`\n✗ Configuration file already exists at ${fullPath}`)
|
|
145
|
+
console.log(` Use --config-file to specify a different location.\n`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const config = await Config.loadForCLI(configPath, version, true)
|
|
150
|
+
|
|
151
|
+
await Config.mutateConfigFile(config.path, {
|
|
152
|
+
formatter: {
|
|
153
|
+
enabled: true
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const projectPath = configFile ? resolve(configFile) : startPath
|
|
158
|
+
const projectDir = statSync(projectPath).isDirectory() ? projectPath : resolve(projectPath, '..')
|
|
159
|
+
const extensionAdded = addHerbExtensionRecommendation(projectDir)
|
|
160
|
+
|
|
161
|
+
console.log(`\n✓ Configuration initialized at ${config.path}`)
|
|
162
|
+
|
|
163
|
+
if (extensionAdded) {
|
|
164
|
+
console.log(`✓ VSCode extension recommended in ${getExtensionsJsonRelativePath()}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(` Formatter is enabled by default.`)
|
|
168
|
+
console.log(` Edit this file to customize linter and formatter settings.\n`)
|
|
169
|
+
|
|
170
|
+
process.exit(0)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const config = await Config.loadForCLI(configFile || startPath, version)
|
|
174
|
+
const formatterConfig = config.formatter || {}
|
|
175
|
+
|
|
176
|
+
if (formatterConfig.enabled === false && !isForceMode) {
|
|
177
|
+
console.log("Formatter is disabled in .herb.yml configuration.")
|
|
178
|
+
console.log("To enable formatting, set formatter.enabled: true in .herb.yml")
|
|
179
|
+
console.log("Or use --force to format anyway.")
|
|
180
|
+
|
|
181
|
+
process.exit(0)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isForceMode && formatterConfig.enabled === false) {
|
|
185
|
+
console.error("⚠️ Forcing formatter run (disabled in .herb.yml)")
|
|
186
|
+
console.error()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.error("⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md")
|
|
190
|
+
console.error()
|
|
191
|
+
|
|
192
|
+
if (indentWidth !== undefined) {
|
|
193
|
+
formatterConfig.indentWidth = indentWidth
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (maxLineLength !== undefined) {
|
|
197
|
+
formatterConfig.maxLineLength = maxLineLength
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let preRewriters: ASTRewriter[] = []
|
|
201
|
+
let postRewriters: StringRewriter[] = []
|
|
202
|
+
const rewriterNames = { pre: formatterConfig.rewriter?.pre || [], post: formatterConfig.rewriter?.post || [] }
|
|
203
|
+
|
|
204
|
+
if (formatterConfig.rewriter && (rewriterNames.pre.length > 0 || rewriterNames.post.length > 0)) {
|
|
205
|
+
const baseDir = config.projectPath || process.cwd()
|
|
206
|
+
const warnings: string[] = []
|
|
207
|
+
const allRewriterClasses: any[] = []
|
|
208
|
+
|
|
209
|
+
allRewriterClasses.push(...builtinRewriters)
|
|
210
|
+
|
|
211
|
+
const loader = new CustomRewriterLoader({ baseDir })
|
|
212
|
+
const { rewriters: customRewriters, rewriterInfo, duplicateWarnings } = await loader.loadRewritersWithInfo()
|
|
213
|
+
|
|
214
|
+
if (customRewriters.length > 0) {
|
|
215
|
+
console.error(colorize(`\nLoaded ${customRewriters.length} custom ${pluralize(customRewriters.length, 'rewriter')}:`, "green"))
|
|
216
|
+
|
|
217
|
+
for (const { name, path } of rewriterInfo) {
|
|
218
|
+
const relativePath = config.projectPath ? path.replace(config.projectPath + '/', '') : path
|
|
219
|
+
|
|
220
|
+
console.error(colorize(` • ${name}`, "cyan") + colorize(` (${relativePath})`, "dim"))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.error()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
allRewriterClasses.push(...customRewriters)
|
|
227
|
+
warnings.push(...duplicateWarnings)
|
|
228
|
+
|
|
229
|
+
const rewriterMap = new Map<string, any>()
|
|
230
|
+
for (const RewriterClass of allRewriterClasses) {
|
|
231
|
+
const instance = new RewriterClass()
|
|
232
|
+
|
|
233
|
+
if (rewriterMap.has(instance.name)) {
|
|
234
|
+
warnings.push(`Rewriter "${instance.name}" is defined multiple times. Using the last definition.`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
rewriterMap.set(instance.name, RewriterClass)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const name of rewriterNames.pre) {
|
|
241
|
+
const RewriterClass = rewriterMap.get(name)
|
|
242
|
+
|
|
243
|
+
if (!RewriterClass) {
|
|
244
|
+
warnings.push(`Pre-format rewriter "${name}" not found. Skipping.`)
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isASTRewriterClass(RewriterClass)) {
|
|
249
|
+
warnings.push(`Rewriter "${name}" is not a pre-format rewriter. Skipping.`)
|
|
250
|
+
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const instance = new RewriterClass()
|
|
255
|
+
try {
|
|
256
|
+
await instance.initialize({ baseDir })
|
|
257
|
+
preRewriters.push(instance)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
warnings.push(`Failed to initialize pre-format rewriter "${name}": ${error}`)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const name of rewriterNames.post) {
|
|
264
|
+
const RewriterClass = rewriterMap.get(name)
|
|
265
|
+
|
|
266
|
+
if (!RewriterClass) {
|
|
267
|
+
warnings.push(`Post-format rewriter "${name}" not found. Skipping.`)
|
|
268
|
+
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!isStringRewriterClass(RewriterClass)) {
|
|
273
|
+
warnings.push(`Rewriter "${name}" is not a post-format rewriter. Skipping.`)
|
|
274
|
+
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const instance = new RewriterClass()
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await instance.initialize({ baseDir })
|
|
282
|
+
|
|
283
|
+
postRewriters.push(instance)
|
|
284
|
+
} catch (error) {
|
|
285
|
+
warnings.push(`Failed to initialize post-format rewriter "${name}": ${error}`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (preRewriters.length > 0 || postRewriters.length > 0) {
|
|
290
|
+
const customRewriterPaths = new Map(rewriterInfo.map(r => [r.name, r.path]))
|
|
291
|
+
|
|
292
|
+
if (preRewriters.length > 0) {
|
|
293
|
+
console.error(colorize(`\nUsing ${preRewriters.length} pre-format ${pluralize(preRewriters.length, 'rewriter')}:`, "green"))
|
|
294
|
+
|
|
295
|
+
for (const rewriter of preRewriters) {
|
|
296
|
+
const customPath = customRewriterPaths.get(rewriter.name)
|
|
297
|
+
|
|
298
|
+
if (customPath) {
|
|
299
|
+
const relativePath = config.projectPath ? customPath.replace(config.projectPath + '/', '') : customPath
|
|
300
|
+
console.error(colorize(` • ${rewriter.name}`, "cyan") + colorize(` (${relativePath})`, "dim"))
|
|
301
|
+
} else {
|
|
302
|
+
console.error(colorize(` • ${rewriter.name}`, "cyan") + colorize(` (built-in)`, "dim"))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.error()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (postRewriters.length > 0) {
|
|
310
|
+
console.error(colorize(`\nUsing ${postRewriters.length} post-format ${pluralize(postRewriters.length, 'rewriter')}:`, "green"))
|
|
311
|
+
|
|
312
|
+
for (const rewriter of postRewriters) {
|
|
313
|
+
const customPath = customRewriterPaths.get(rewriter.name)
|
|
314
|
+
|
|
315
|
+
if (customPath) {
|
|
316
|
+
const relativePath = config.projectPath ? customPath.replace(config.projectPath + '/', '') : customPath
|
|
317
|
+
console.error(colorize(` • ${rewriter.name}`, "cyan") + colorize(` (${relativePath})`, "dim"))
|
|
318
|
+
} else {
|
|
319
|
+
console.error(colorize(` • ${rewriter.name}`, "cyan") + colorize(` (built-in)`, "dim"))
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.error()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (warnings.length > 0) {
|
|
328
|
+
warnings.forEach(warning => console.error(`⚠️ ${warning}`))
|
|
329
|
+
console.error()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const formatter = Formatter.from(Herb, config, { preRewriters, postRewriters })
|
|
126
334
|
|
|
127
335
|
if (!file && !process.stdin.isTTY) {
|
|
128
336
|
if (isCheckMode) {
|
|
@@ -148,107 +356,90 @@ export class CLI {
|
|
|
148
356
|
const output = result.endsWith('\n') ? result : result + '\n'
|
|
149
357
|
|
|
150
358
|
process.stdout.write(output)
|
|
151
|
-
} else if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
359
|
+
} else if (positionals.length > 0) {
|
|
360
|
+
const allFiles: string[] = []
|
|
361
|
+
|
|
362
|
+
let hasErrors = false
|
|
363
|
+
|
|
364
|
+
for (const pattern of positionals) {
|
|
365
|
+
try {
|
|
366
|
+
const files = await this.resolvePatternToFiles(pattern, config, isForceMode)
|
|
367
|
+
|
|
368
|
+
if (files.length === 0) {
|
|
369
|
+
const isLikelySpecificFile = !pattern.includes('*') && !pattern.includes('?') &&
|
|
370
|
+
!pattern.includes('[') && !pattern.includes('{')
|
|
163
371
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (output !== source) {
|
|
172
|
-
if (isCheckMode) {
|
|
173
|
-
console.log(`File is not formatted: ${file}`)
|
|
174
|
-
process.exit(1)
|
|
175
|
-
} else {
|
|
176
|
-
writeFileSync(file, output, "utf-8")
|
|
177
|
-
console.log(`Formatted: ${file}`)
|
|
372
|
+
if (isLikelySpecificFile) {
|
|
373
|
+
continue
|
|
374
|
+
} else {
|
|
375
|
+
console.log(`No files found matching pattern: ${pattern}`)
|
|
376
|
+
process.exit(0)
|
|
377
|
+
}
|
|
178
378
|
}
|
|
179
|
-
} else if (isCheckMode) {
|
|
180
|
-
console.log(`File is properly formatted: ${file}`)
|
|
181
|
-
}
|
|
182
379
|
|
|
183
|
-
|
|
380
|
+
allFiles.push(...files)
|
|
381
|
+
} catch (error: any) {
|
|
382
|
+
console.error(`Error: ${error.message}`)
|
|
383
|
+
hasErrors = true
|
|
384
|
+
break
|
|
385
|
+
}
|
|
184
386
|
}
|
|
185
387
|
|
|
186
|
-
|
|
187
|
-
|
|
388
|
+
if (hasErrors) {
|
|
389
|
+
process.exit(1)
|
|
390
|
+
}
|
|
188
391
|
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
statSync(file)
|
|
192
|
-
} catch {
|
|
193
|
-
if (!file.includes('*') && !file.includes('?') && !file.includes('[') && !file.includes('{')) {
|
|
194
|
-
console.error(`Error: Cannot access '${file}': ENOENT: no such file or directory`)
|
|
392
|
+
const files = [...new Set(allFiles)]
|
|
195
393
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
394
|
+
if (files.length === 0) {
|
|
395
|
+
console.log(`No files found matching patterns: ${positionals.join(', ')}`)
|
|
396
|
+
process.exit(0)
|
|
397
|
+
}
|
|
199
398
|
|
|
200
|
-
|
|
399
|
+
let formattedCount = 0
|
|
400
|
+
let unformattedFiles: string[] = []
|
|
201
401
|
|
|
202
|
-
|
|
203
|
-
|
|
402
|
+
for (const filePath of files) {
|
|
403
|
+
const displayPath = relative(process.cwd(), filePath)
|
|
204
404
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const source = readFileSync(filePath, "utf-8")
|
|
211
|
-
const result = formatter.format(source)
|
|
212
|
-
const output = result.endsWith('\n') ? result : result + '\n'
|
|
213
|
-
|
|
214
|
-
if (output !== source) {
|
|
215
|
-
if (isCheckMode) {
|
|
216
|
-
unformattedFiles.push(filePath)
|
|
217
|
-
} else {
|
|
218
|
-
writeFileSync(filePath, output, "utf-8")
|
|
219
|
-
console.log(`Formatted: ${filePath}`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
formattedCount++
|
|
223
|
-
}
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error(`Error formatting ${filePath}:`, error)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
405
|
+
try {
|
|
406
|
+
const source = readFileSync(filePath, "utf-8")
|
|
407
|
+
const result = formatter.format(source)
|
|
408
|
+
const output = result.endsWith('\n') ? result : result + '\n'
|
|
228
409
|
|
|
229
|
-
if (
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
unformattedFiles.forEach(file => console.log(` ${file}`))
|
|
233
|
-
console.log(`\nChecked ${files.length} ${pluralize(files.length, 'file')}, found ${unformattedFiles.length} unformatted ${pluralize(unformattedFiles.length, 'file')}`)
|
|
234
|
-
process.exit(1)
|
|
410
|
+
if (output !== source) {
|
|
411
|
+
if (isCheckMode) {
|
|
412
|
+
unformattedFiles.push(displayPath)
|
|
235
413
|
} else {
|
|
236
|
-
|
|
414
|
+
writeFileSync(filePath, output, "utf-8")
|
|
415
|
+
console.log(`Formatted: ${displayPath}`)
|
|
237
416
|
}
|
|
238
|
-
|
|
239
|
-
console.log(`\nChecked ${files.length} ${pluralize(files.length, 'file')}, formatted ${formattedCount} ${pluralize(formattedCount, 'file')}`)
|
|
417
|
+
formattedCount++
|
|
240
418
|
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error(`Error formatting ${displayPath}:`, error)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
241
423
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
424
|
+
if (isCheckMode) {
|
|
425
|
+
if (unformattedFiles.length > 0) {
|
|
426
|
+
console.log(`\nThe following ${pluralize(unformattedFiles.length, 'file is', 'files are')} not formatted:`)
|
|
427
|
+
unformattedFiles.forEach(file => console.log(` ${file}`))
|
|
428
|
+
console.log(`\nChecked ${files.length} ${pluralize(files.length, 'file')}, found ${unformattedFiles.length} unformatted ${pluralize(unformattedFiles.length, 'file')}`)
|
|
429
|
+
process.exit(1)
|
|
430
|
+
} else {
|
|
431
|
+
console.log(`\nChecked ${files.length} ${pluralize(files.length, 'file')}, all files are properly formatted`)
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`\nChecked ${files.length} ${pluralize(files.length, 'file')}, formatted ${formattedCount} ${pluralize(formattedCount, 'file')}`)
|
|
246
435
|
}
|
|
436
|
+
|
|
437
|
+
process.exit(0)
|
|
247
438
|
} else {
|
|
248
|
-
const files = await
|
|
439
|
+
const files = await config.findFilesForTool('formatter', process.cwd())
|
|
249
440
|
|
|
250
441
|
if (files.length === 0) {
|
|
251
|
-
console.log(`No files found matching
|
|
442
|
+
console.log(`No files found matching configured patterns`)
|
|
252
443
|
|
|
253
444
|
process.exit(0)
|
|
254
445
|
}
|
|
@@ -257,6 +448,8 @@ export class CLI {
|
|
|
257
448
|
let unformattedFiles: string[] = []
|
|
258
449
|
|
|
259
450
|
for (const filePath of files) {
|
|
451
|
+
const displayPath = relative(process.cwd(), filePath)
|
|
452
|
+
|
|
260
453
|
try {
|
|
261
454
|
const source = readFileSync(filePath, "utf-8")
|
|
262
455
|
const result = formatter.format(source)
|
|
@@ -264,15 +457,15 @@ export class CLI {
|
|
|
264
457
|
|
|
265
458
|
if (output !== source) {
|
|
266
459
|
if (isCheckMode) {
|
|
267
|
-
unformattedFiles.push(
|
|
460
|
+
unformattedFiles.push(displayPath)
|
|
268
461
|
} else {
|
|
269
462
|
writeFileSync(filePath, output, "utf-8")
|
|
270
|
-
console.log(`Formatted: ${
|
|
463
|
+
console.log(`Formatted: ${displayPath}`)
|
|
271
464
|
}
|
|
272
465
|
formattedCount++
|
|
273
466
|
}
|
|
274
467
|
} catch (error) {
|
|
275
|
-
console.error(`Error formatting ${
|
|
468
|
+
console.error(`Error formatting ${displayPath}:`, error)
|
|
276
469
|
}
|
|
277
470
|
}
|
|
278
471
|
|
|
@@ -306,4 +499,57 @@ export class CLI {
|
|
|
306
499
|
|
|
307
500
|
return Buffer.concat(chunks).toString("utf8")
|
|
308
501
|
}
|
|
502
|
+
|
|
503
|
+
private async resolvePatternToFiles(pattern: string, config: Config, isForceMode: boolean | undefined): Promise<string[]> {
|
|
504
|
+
let isDirectory = false
|
|
505
|
+
let isFile = false
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const stats = statSync(pattern)
|
|
509
|
+
isDirectory = stats.isDirectory()
|
|
510
|
+
isFile = stats.isFile()
|
|
511
|
+
} catch {
|
|
512
|
+
// Not a file/directory, treat as glob pattern
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const filesConfig = config.getFilesConfigForTool('formatter')
|
|
516
|
+
|
|
517
|
+
if (isDirectory) {
|
|
518
|
+
const files = await config.findFilesForTool('formatter', resolve(pattern))
|
|
519
|
+
return files
|
|
520
|
+
} else if (isFile) {
|
|
521
|
+
const testFiles = await glob(pattern, {
|
|
522
|
+
cwd: process.cwd(),
|
|
523
|
+
ignore: filesConfig.exclude || []
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
if (testFiles.length === 0) {
|
|
527
|
+
if (!isForceMode) {
|
|
528
|
+
console.error(`⚠️ File ${pattern} is excluded by configuration patterns.`)
|
|
529
|
+
console.error(` Use --force to format it anyway.\n`)
|
|
530
|
+
process.exit(0)
|
|
531
|
+
} else {
|
|
532
|
+
console.error(`⚠️ Forcing formatter on excluded file: ${pattern}`)
|
|
533
|
+
console.error()
|
|
534
|
+
return [pattern]
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return [pattern]
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const files = await glob(pattern, { ignore: filesConfig.exclude || [] })
|
|
542
|
+
|
|
543
|
+
if (files.length === 0) {
|
|
544
|
+
try {
|
|
545
|
+
statSync(pattern)
|
|
546
|
+
} catch {
|
|
547
|
+
if (!pattern.includes('*') && !pattern.includes('?') && !pattern.includes('[') && !pattern.includes('{')) {
|
|
548
|
+
throw new Error(`Cannot access '${pattern}': ENOENT: no such file or directory`)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return files
|
|
554
|
+
}
|
|
309
555
|
}
|