json-sort-cli 4.3.0 → 4.3.1

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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 4.3.1 (2026-09-06)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **json-sort-cli:** replace external update notifier ([ee5b24c](https://github.com/codsen/codsen/commit/ee5b24cc3fd7e640c9a5a4a2c5a805ef4647fb9b))
11
+
6
12
  ## 4.3.0 (2026-09-03)
7
13
 
8
14
  ### Features
package/README.md CHANGED
@@ -36,7 +36,7 @@ sortjson
36
36
 
37
37
  ## Documentation
38
38
 
39
- Please [visit codsen.com](https://codsen.com/os/json-sort-cli/) for a full description of the API. If you’re looking for the **Changelog**, it’s [here](https://github.com/codsen/codsen/blob/main/packages/json-sort-cli/CHANGELOG.md).
39
+ Please [visit codsen.com](https://codsen.com/os/json-sort-cli/) for a full description of the API. For **Changelog**, see [raw md on GitHub](https://github.com/codsen/codsen/blob/main/packages/json-sort-cli/CHANGELOG.md) or [the website](https://codsen.com/os/json-sort-cli/changelog).
40
40
 
41
41
  ## Contributing
42
42
 
package/cli-main.js ADDED
@@ -0,0 +1,548 @@
1
+ import { fstatSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { glob } from "codsen-glob";
5
+ import { notifyOfCliUpdate } from "./cli-update-notifier.js";
6
+ import { formatParsedJson } from "./json-formatter.js";
7
+ import { ProcessingError, processFiles } from "./process-files.js";
8
+
9
+ const require1 = createRequire(import.meta.url);
10
+ const pkg = require1("./package.json");
11
+ const prefix = "✨ json-sort-cli: ";
12
+ const nonJsonFormats = new Set([".yml", ".toml", ".yaml"]);
13
+ const ignoredBasenames = new Set([
14
+ ".DS_Store",
15
+ "npm-debug.log",
16
+ ".svn",
17
+ "CVS",
18
+ "config.gypi",
19
+ ".lock-wscript",
20
+ "package-lock.json",
21
+ "npm-shrinkwrap.json",
22
+ "yarn.lock",
23
+ ]);
24
+ const flagDefinitions = {
25
+ arrays: { short: "a", type: "boolean" },
26
+ ci: { short: "c", type: "boolean" },
27
+ dry: { short: "d", type: "boolean" },
28
+ help: { short: "h", type: "boolean" },
29
+ indentationCount: { short: "i", type: "value" },
30
+ lineEnding: { short: "l", type: "value" },
31
+ nodemodules: { short: "n", type: "boolean" },
32
+ pack: { short: "p", type: "boolean" },
33
+ silent: { short: "s", type: "boolean" },
34
+ stdout: { type: "boolean" },
35
+ tabs: { short: "t", type: "boolean" },
36
+ version: { short: "v", type: "boolean" },
37
+ };
38
+ const shortToLong = new Map(
39
+ Object.entries(flagDefinitions)
40
+ .filter(([, definition]) => definition.short)
41
+ .map(([long, definition]) => [definition.short, long]),
42
+ );
43
+
44
+ const help = `
45
+ Usage
46
+ $ jsonsort YOURFILE.json
47
+ $ jsonsort YOURFILE.json --stdout
48
+ $ jsonsort - < YOURFILE.json
49
+ $ cat YOURFILE.json | jsonsort
50
+ $ sortjson templatesfolder1 templatesfolder2 package.json
51
+ $ jsonsort
52
+
53
+ With no file arguments, piped input is sorted to stdout. The "-" operand reads
54
+ stdin, and --stdout prints one matched file. These forms don't write files or mix
55
+ status messages into stdout. Otherwise, with no arguments, jsonsort recursively
56
+ sorts JSON files below the current directory. Discovered symbolic links are not
57
+ followed. An explicitly selected symbolic-link directory is resolved as the
58
+ selected root; symbolic-link files are rejected before reading.
59
+
60
+ Options
61
+ -n, --nodemodules Include JSON files inside node_modules; lockfiles remain excluded
62
+ -t, --tabs Use tabs for indentation
63
+ -i, --indentationCount Use 0-10 spaces or tabs (default: 2 spaces or 1 tab)
64
+ -s, --silent Suppress all terminal output; use the exit code for the result
65
+ -h, --help Show this help
66
+ -v, --version Show the current version
67
+ -a, --arrays Sort arrays that contain only strings
68
+ -d, --dry List candidate files without reading or writing them
69
+ -p, --pack Exclude package.json files
70
+ -c, --ci Check without writing; exit 9 when canonical output differs
71
+ -l, --lineEnding Use "cr", "crlf", or "lf" instead of the detected line ending
72
+ --stdout Print one sorted JSON document without writing files
73
+
74
+ Use -- before a path that begins with a dash. Invalid options exit 1 before
75
+ file discovery. Processing failures exit 1 and don't stop independent files.
76
+
77
+ Example
78
+ $ cat data.json | jsonsort --arrays | other-command
79
+ `;
80
+
81
+ function argumentError(message) {
82
+ throw new TypeError(
83
+ `json-sort-cli/parseArguments(): [THROW_ID_01] ${message}`,
84
+ );
85
+ }
86
+
87
+ function requestsSilent(rawArguments) {
88
+ let optionsEnded = false;
89
+ for (const argument of rawArguments) {
90
+ if (optionsEnded) {
91
+ continue;
92
+ }
93
+ if (argument === "--") {
94
+ optionsEnded = true;
95
+ } else if (argument === "--silent") {
96
+ return true;
97
+ } else if (argument.startsWith("-") && !argument.startsWith("--")) {
98
+ for (const short of argument.slice(1)) {
99
+ const name = shortToLong.get(short);
100
+ if (!name) {
101
+ break;
102
+ }
103
+ if (name === "silent") {
104
+ return true;
105
+ }
106
+ if (flagDefinitions[name].type === "value") {
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ return false;
113
+ }
114
+
115
+ function parseArguments(rawArguments, { stdinIsPiped = false } = {}) {
116
+ const expanded = rawArguments.flatMap((argument) => {
117
+ const match = argument.match(/^-(i|l)\s+(.+)$/u);
118
+ return match ? [`-${match[1]}`, match[2]] : [argument];
119
+ });
120
+ const flags = Object.fromEntries(
121
+ Object.keys(flagDefinitions).map((name) => [name, false]),
122
+ );
123
+ flags.indentationCount = undefined;
124
+ flags.lineEnding = undefined;
125
+ const input = [];
126
+ const seen = new Set();
127
+ let optionsEnded = false;
128
+
129
+ function setFlag(name, value = true) {
130
+ if (seen.has(name)) {
131
+ argumentError(`Option --${name} was provided more than once`);
132
+ }
133
+ seen.add(name);
134
+ flags[name] = value;
135
+ }
136
+
137
+ for (let index = 0; index < expanded.length; index += 1) {
138
+ const argument = expanded[index];
139
+ if (optionsEnded) {
140
+ input.push(argument);
141
+ continue;
142
+ }
143
+ if (argument === "--") {
144
+ optionsEnded = true;
145
+ continue;
146
+ }
147
+ if (!argument.startsWith("-") || argument === "-") {
148
+ input.push(argument);
149
+ continue;
150
+ }
151
+
152
+ if (argument.startsWith("--")) {
153
+ const equalsIndex = argument.indexOf("=");
154
+ const name = argument.slice(
155
+ 2,
156
+ equalsIndex === -1 ? undefined : equalsIndex,
157
+ );
158
+ const definition = flagDefinitions[name];
159
+ if (!definition) {
160
+ argumentError(`Unknown option --${name}`);
161
+ }
162
+ if (definition.type === "boolean") {
163
+ if (equalsIndex !== -1) {
164
+ argumentError(`Option --${name} doesn't accept a value`);
165
+ }
166
+ setFlag(name);
167
+ continue;
168
+ }
169
+
170
+ let value =
171
+ equalsIndex === -1 ? undefined : argument.slice(equalsIndex + 1);
172
+ if (value === undefined) {
173
+ value = expanded[index + 1];
174
+ const flagShapedValue =
175
+ value?.startsWith("-") &&
176
+ !(name === "indentationCount" && /^-\d/u.test(value));
177
+ if (value === undefined || flagShapedValue) {
178
+ argumentError(`Option --${name} requires a value`);
179
+ }
180
+ index += 1;
181
+ }
182
+ if (value === "") {
183
+ argumentError(`Option --${name} requires a value`);
184
+ }
185
+ setFlag(name, value);
186
+ continue;
187
+ }
188
+
189
+ let shortFlags = argument.slice(1);
190
+ while (shortFlags.length) {
191
+ const short = shortFlags[0];
192
+ const name = shortToLong.get(short);
193
+ if (!name) {
194
+ argumentError(`Unknown option -${short}`);
195
+ }
196
+ const definition = flagDefinitions[name];
197
+ shortFlags = shortFlags.slice(1);
198
+ if (definition.type === "boolean") {
199
+ setFlag(name);
200
+ continue;
201
+ }
202
+
203
+ let value = shortFlags.replace(/^=/u, "");
204
+ shortFlags = "";
205
+ if (!value) {
206
+ value = expanded[index + 1];
207
+ const flagShapedValue =
208
+ value?.startsWith("-") &&
209
+ !(name === "indentationCount" && /^-\d/u.test(value));
210
+ if (value === undefined || flagShapedValue) {
211
+ argumentError(`Option -${short} requires a value`);
212
+ }
213
+ index += 1;
214
+ }
215
+ setFlag(name, value);
216
+ }
217
+ }
218
+
219
+ const defaultIndentation = flags.tabs ? 1 : 2;
220
+ const indentationCount =
221
+ flags.indentationCount === undefined
222
+ ? defaultIndentation
223
+ : Number(flags.indentationCount);
224
+ if (
225
+ !Number.isInteger(indentationCount) ||
226
+ indentationCount < 0 ||
227
+ indentationCount > 10
228
+ ) {
229
+ argumentError("indentationCount must be an integer from 0 to 10");
230
+ }
231
+ if (
232
+ flags.lineEnding !== undefined &&
233
+ !["cr", "crlf", "lf"].includes(flags.lineEnding)
234
+ ) {
235
+ argumentError('lineEnding must be "cr", "crlf", or "lf"');
236
+ }
237
+
238
+ const hasInput = input.length > 0;
239
+ const informational = flags.help || flags.version;
240
+ const printsJson =
241
+ flags.stdout || input.includes("-") || (!hasInput && stdinIsPiped);
242
+ if (!hasInput && expanded.length > 0 && !informational && !stdinIsPiped) {
243
+ argumentError(
244
+ "Provide at least one path, or run jsonsort with no arguments",
245
+ );
246
+ }
247
+ if (!informational && printsJson) {
248
+ for (const incompatible of ["ci", "dry", "silent"]) {
249
+ if (flags[incompatible]) {
250
+ argumentError(
251
+ `Option --${incompatible} cannot be used when printing sorted JSON to stdout`,
252
+ );
253
+ }
254
+ }
255
+ }
256
+
257
+ return {
258
+ flags,
259
+ hasInput,
260
+ indentationCount,
261
+ input: hasInput ? input : ["**/*.json"],
262
+ };
263
+ }
264
+
265
+ function isCandidate(filePath) {
266
+ const basename = path.basename(filePath);
267
+ if (ignoredBasenames.has(basename)) {
268
+ return false;
269
+ }
270
+
271
+ const extension = path.extname(filePath).toLowerCase();
272
+ return (
273
+ extension === ".json" ||
274
+ (basename.startsWith(".") && !nonJsonFormats.has(extension))
275
+ );
276
+ }
277
+
278
+ function standardInputIsPiped(statInput) {
279
+ if (process.stdin.isTTY === true) {
280
+ return false;
281
+ }
282
+ try {
283
+ return !statInput(process.stdin.fd).isCharacterDevice();
284
+ } catch {
285
+ // A missing or inaccessible stdin is not a pipeline source.
286
+ return false;
287
+ }
288
+ }
289
+
290
+ async function readStdin() {
291
+ const chunks = [];
292
+ for await (const chunk of process.stdin) {
293
+ chunks.push(chunk);
294
+ }
295
+ return Buffer.concat(chunks);
296
+ }
297
+
298
+ async function discoverPaths(input, flags, findPaths) {
299
+ const paths = await findPaths(
300
+ [
301
+ ...input,
302
+ "!**/package-lock.json",
303
+ "!**/npm-shrinkwrap.json",
304
+ "!**/yarn.lock",
305
+ ...(flags.nodemodules ? [] : ["!**/node_modules/**"]),
306
+ ...(flags.pack ? ["!**/package.json"] : []),
307
+ ],
308
+ {
309
+ dot: true,
310
+ expandDirectories: { files: [".*", "*.json", "*.JSON"] },
311
+ followSymbolicLinks: false,
312
+ },
313
+ );
314
+ return paths.filter(isCandidate);
315
+ }
316
+
317
+ async function resolveStdoutSource(input, flags, findPaths) {
318
+ if (input.includes("-")) {
319
+ if (input.length !== 1) {
320
+ argumentError(
321
+ 'The standard-input operand "-" cannot be combined with file paths',
322
+ );
323
+ }
324
+ return "-";
325
+ }
326
+
327
+ const paths = await discoverPaths(input, flags, findPaths);
328
+ if (paths.length !== 1) {
329
+ throw new Error(
330
+ `Printing sorted JSON to stdout requires exactly one input; found ${paths.length}`,
331
+ );
332
+ }
333
+ return paths[0];
334
+ }
335
+
336
+ async function formatSource(source, options, processSources) {
337
+ let output;
338
+ await processSources([source], {
339
+ ...options,
340
+ ci: true,
341
+ ...(source === "-" ? { read: readStdin } : {}),
342
+ transform(parsed, formatOptions) {
343
+ const prepared = formatParsedJson(
344
+ parsed,
345
+ formatOptions.contents,
346
+ formatOptions,
347
+ );
348
+ output = prepared.output;
349
+ return prepared;
350
+ },
351
+ });
352
+ return output;
353
+ }
354
+
355
+ function errorMessage(error) {
356
+ if (error instanceof ProcessingError) {
357
+ return error.failures.map((failure) => failure.message).join("\n");
358
+ }
359
+ return error instanceof Error ? error.message : String(error);
360
+ }
361
+
362
+ function handleStdoutError(error) {
363
+ if (error?.code === "EPIPE") {
364
+ return;
365
+ }
366
+ console.error(`${prefix}${errorMessage(error)}`);
367
+ process.exitCode = 1;
368
+ }
369
+
370
+ function createPrinter() {
371
+ const useColour = Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
372
+ const colours = { green: 32, grey: 90, red: 31, white: 37, yellow: 33 };
373
+ const colour = (value, name) =>
374
+ useColour ? `\u001b[${colours[name]}m${value}\u001b[39m` : value;
375
+ return { colour };
376
+ }
377
+
378
+ export async function main({
379
+ findPaths = glob,
380
+ notify = notifyOfCliUpdate,
381
+ processSources = processFiles,
382
+ statInput = fstatSync,
383
+ } = {}) {
384
+ const rawArguments = process.argv.slice(2);
385
+ const silentRequested = requestsSilent(rawArguments);
386
+ const stdinIsPiped = standardInputIsPiped(statInput);
387
+ let parsed;
388
+ try {
389
+ parsed = parseArguments(rawArguments, { stdinIsPiped });
390
+ } catch (error) {
391
+ if (!silentRequested) {
392
+ console.error(error.message);
393
+ }
394
+ process.exitCode = 1;
395
+ return;
396
+ }
397
+
398
+ const { flags, hasInput, indentationCount, input } = parsed;
399
+ if (flags.version) {
400
+ console.log(pkg.version);
401
+ return;
402
+ }
403
+ if (flags.help) {
404
+ console.log(help);
405
+ return;
406
+ }
407
+
408
+ const printsJson =
409
+ flags.stdout || input.includes("-") || (!hasInput && stdinIsPiped);
410
+
411
+ if (!flags.silent && !flags.ci && !printsJson) {
412
+ try {
413
+ notify({ pkg });
414
+ } catch {}
415
+ }
416
+
417
+ const options = {
418
+ arrays: flags.arrays,
419
+ indentationCount,
420
+ lineEnding: flags.lineEnding || undefined,
421
+ pack: flags.pack,
422
+ tabs: flags.tabs,
423
+ };
424
+
425
+ if (printsJson) {
426
+ try {
427
+ const source = await resolveStdoutSource(
428
+ hasInput ? input : ["-"],
429
+ flags,
430
+ findPaths,
431
+ );
432
+ process.stdout.on("error", handleStdoutError);
433
+ process.stdout.write(await formatSource(source, options, processSources));
434
+ } catch (error) {
435
+ if (!flags.silent) {
436
+ console.error(`${prefix}${errorMessage(error)}`);
437
+ }
438
+ process.exitCode = 1;
439
+ }
440
+ return;
441
+ }
442
+
443
+ const { colour } = createPrinter();
444
+ let paths;
445
+ try {
446
+ paths = await discoverPaths(input, flags, findPaths);
447
+ } catch (error) {
448
+ if (!flags.silent) {
449
+ console.error(`${prefix}${error}`);
450
+ }
451
+ process.exitCode = 1;
452
+ return;
453
+ }
454
+
455
+ if (!paths.length) {
456
+ if (!flags.silent) {
457
+ console.log(`${prefix}The inputs don't lead to any JSON files. Exiting.`);
458
+ }
459
+ return;
460
+ }
461
+
462
+ if (flags.dry) {
463
+ if (!flags.silent) {
464
+ console.log(
465
+ `${prefix}We'd try to sort the following files:\n${paths.join("\n")}`,
466
+ );
467
+ }
468
+ return;
469
+ }
470
+
471
+ try {
472
+ const { successful, unsorted } = await processSources(paths, {
473
+ ...options,
474
+ ci: flags.ci,
475
+ onOutcome(outcome) {
476
+ if (flags.silent) {
477
+ return;
478
+ }
479
+ if (outcome.status === "failure") {
480
+ console.error(
481
+ `${prefix}${outcome.path} - BAD (${outcome.stage}) - ${outcome.error}`,
482
+ );
483
+ } else if (!flags.ci) {
484
+ console.log(`${prefix}${outcome.path} - OK`);
485
+ }
486
+ },
487
+ });
488
+
489
+ if (flags.silent) {
490
+ if (flags.ci && unsorted.length) {
491
+ process.exitCode = 9;
492
+ }
493
+ return;
494
+ }
495
+ if (flags.ci) {
496
+ if (unsorted.length) {
497
+ console.log(
498
+ `${prefix}${colour("Unsorted files:", "red")}\n${unsorted.join("\n")}`,
499
+ );
500
+ process.exitCode = 9;
501
+ } else {
502
+ console.log(
503
+ `${prefix}${colour("All files were already sorted:", "white")}\n${successful.join("\n")}`,
504
+ );
505
+ }
506
+ return;
507
+ }
508
+ console.log(
509
+ `\n${prefix}${colour(
510
+ `All ${successful.length} file${successful.length === 1 ? "" : "s"} sorted`,
511
+ "green",
512
+ )}`,
513
+ );
514
+ } catch (error) {
515
+ if (!(error instanceof ProcessingError)) {
516
+ if (!flags.silent) {
517
+ console.error(`${prefix}${error}`);
518
+ }
519
+ process.exitCode = 1;
520
+ return;
521
+ }
522
+
523
+ if (!flags.silent) {
524
+ if (flags.ci) {
525
+ const unsorted = new Set(error.unsorted);
526
+ const alreadySorted = error.successful.filter(
527
+ (filePath) => !unsorted.has(filePath),
528
+ );
529
+ if (alreadySorted.length) {
530
+ console.log(
531
+ `${prefix}${alreadySorted.length} file${alreadySorted.length === 1 ? "" : "s"} already sorted:\n${alreadySorted.join("\n")}`,
532
+ );
533
+ }
534
+ if (error.unsorted.length) {
535
+ console.log(`${prefix}Unsorted files:\n${error.unsorted.join("\n")}`);
536
+ }
537
+ } else if (error.successful.length) {
538
+ console.log(
539
+ `\n${prefix}${error.successful.length} file${error.successful.length === 1 ? "" : "s"} sorted`,
540
+ );
541
+ }
542
+ console.error(
543
+ `${prefix}${error.failures.length} file${error.failures.length === 1 ? "" : "s"} could not be ${flags.ci ? "checked" : "sorted"} - ${error.failures.map(({ path: failedPath }) => failedPath).join(" - ")}`,
544
+ );
545
+ }
546
+ process.exitCode = 1;
547
+ }
548
+ }