json-sort-cli 4.2.3 → 4.3.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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/cli.js +158 -41
- package/package.json +4 -4
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.0 (2026-09-03)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **json-sort-cli:** support piped input and stdout output ([6da20a9](https://github.com/codsen/codsen/commit/6da20a97ad629ee4956a59dada40cd05f8ccf6f5))
|
|
11
|
+
|
|
6
12
|
## 4.2.3 (2026-09-01)
|
|
7
13
|
|
|
8
14
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<h1 align="center">json-sort-cli</h1>
|
|
2
2
|
|
|
3
|
-
<p align="center">
|
|
3
|
+
<p align="center">Deep-sort JSON files or standard input; package.json retains its special key order</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://codsen.com/os/json-sort-cli" rel="nofollow noreferrer noopener">
|
package/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { fstatSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { glob } from "codsen-glob";
|
|
6
7
|
import updateNotifier from "update-notifier";
|
|
8
|
+
import { formatParsedJson } from "./json-formatter.js";
|
|
7
9
|
import { ProcessingError, processFiles } from "./process-files.js";
|
|
8
10
|
|
|
9
11
|
const require1 = createRequire(import.meta.url);
|
|
@@ -31,26 +33,31 @@ const flagDefinitions = {
|
|
|
31
33
|
nodemodules: { short: "n", type: "boolean" },
|
|
32
34
|
pack: { short: "p", type: "boolean" },
|
|
33
35
|
silent: { short: "s", type: "boolean" },
|
|
36
|
+
stdout: { type: "boolean" },
|
|
34
37
|
tabs: { short: "t", type: "boolean" },
|
|
35
38
|
version: { short: "v", type: "boolean" },
|
|
36
39
|
};
|
|
37
40
|
const shortToLong = new Map(
|
|
38
|
-
Object.entries(flagDefinitions)
|
|
39
|
-
definition.short
|
|
40
|
-
long,
|
|
41
|
-
]),
|
|
41
|
+
Object.entries(flagDefinitions)
|
|
42
|
+
.filter(([, definition]) => definition.short)
|
|
43
|
+
.map(([long, definition]) => [definition.short, long]),
|
|
42
44
|
);
|
|
43
45
|
|
|
44
46
|
const help = `
|
|
45
47
|
Usage
|
|
46
48
|
$ jsonsort YOURFILE.json
|
|
49
|
+
$ jsonsort YOURFILE.json --stdout
|
|
50
|
+
$ jsonsort - < YOURFILE.json
|
|
51
|
+
$ cat YOURFILE.json | jsonsort
|
|
47
52
|
$ sortjson templatesfolder1 templatesfolder2 package.json
|
|
48
53
|
$ jsonsort
|
|
49
54
|
|
|
50
|
-
With no arguments,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
With no file arguments, piped input is sorted to stdout. The "-" operand reads
|
|
56
|
+
stdin, and --stdout prints one matched file. These forms don't write files or mix
|
|
57
|
+
status messages into stdout. Otherwise, with no arguments, jsonsort recursively
|
|
58
|
+
sorts JSON files below the current directory. Discovered symbolic links are not
|
|
59
|
+
followed. An explicitly selected symbolic-link directory is resolved as the
|
|
60
|
+
selected root; symbolic-link files are rejected before reading.
|
|
54
61
|
|
|
55
62
|
Options
|
|
56
63
|
-n, --nodemodules Include JSON files inside node_modules; lockfiles remain excluded
|
|
@@ -64,12 +71,13 @@ Options
|
|
|
64
71
|
-p, --pack Exclude package.json files
|
|
65
72
|
-c, --ci Check without writing; exit 9 when canonical output differs
|
|
66
73
|
-l, --lineEnding Use "cr", "crlf", or "lf" instead of the detected line ending
|
|
74
|
+
--stdout Print one sorted JSON document without writing files
|
|
67
75
|
|
|
68
76
|
Use -- before a path that begins with a dash. Invalid options exit 1 before
|
|
69
77
|
file discovery. Processing failures exit 1 and don't stop independent files.
|
|
70
78
|
|
|
71
79
|
Example
|
|
72
|
-
$
|
|
80
|
+
$ cat data.json | jsonsort --arrays | other-command
|
|
73
81
|
`;
|
|
74
82
|
|
|
75
83
|
function argumentError(message) {
|
|
@@ -106,7 +114,7 @@ function requestsSilent(rawArguments) {
|
|
|
106
114
|
return false;
|
|
107
115
|
}
|
|
108
116
|
|
|
109
|
-
function parseArguments(rawArguments) {
|
|
117
|
+
function parseArguments(rawArguments, { stdinIsPiped = false } = {}) {
|
|
110
118
|
const expanded = rawArguments.flatMap((argument) => {
|
|
111
119
|
const match = argument.match(/^-(i|l)\s+(.+)$/u);
|
|
112
120
|
return match ? [`-${match[1]}`, match[2]] : [argument];
|
|
@@ -228,21 +236,31 @@ function parseArguments(rawArguments) {
|
|
|
228
236
|
) {
|
|
229
237
|
argumentError('lineEnding must be "cr", "crlf", or "lf"');
|
|
230
238
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
) {
|
|
239
|
+
|
|
240
|
+
const hasInput = input.length > 0;
|
|
241
|
+
const informational = flags.help || flags.version;
|
|
242
|
+
const printsJson =
|
|
243
|
+
flags.stdout || input.includes("-") || (!hasInput && stdinIsPiped);
|
|
244
|
+
if (!hasInput && expanded.length > 0 && !informational && !stdinIsPiped) {
|
|
237
245
|
argumentError(
|
|
238
246
|
"Provide at least one path, or run jsonsort with no arguments",
|
|
239
247
|
);
|
|
240
248
|
}
|
|
249
|
+
if (!informational && printsJson) {
|
|
250
|
+
for (const incompatible of ["ci", "dry", "silent"]) {
|
|
251
|
+
if (flags[incompatible]) {
|
|
252
|
+
argumentError(
|
|
253
|
+
`Option --${incompatible} cannot be used when printing sorted JSON to stdout`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
241
258
|
|
|
242
259
|
return {
|
|
243
260
|
flags,
|
|
261
|
+
hasInput,
|
|
244
262
|
indentationCount,
|
|
245
|
-
input:
|
|
263
|
+
input: hasInput ? input : ["**/*.json"],
|
|
246
264
|
};
|
|
247
265
|
}
|
|
248
266
|
|
|
@@ -259,6 +277,98 @@ function isCandidate(filePath) {
|
|
|
259
277
|
);
|
|
260
278
|
}
|
|
261
279
|
|
|
280
|
+
function standardInputIsPiped() {
|
|
281
|
+
if (process.stdin.isTTY === true) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
return !fstatSync(process.stdin.fd).isCharacterDevice();
|
|
286
|
+
} catch {
|
|
287
|
+
// A missing or inaccessible stdin is not a pipeline source.
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function readStdin() {
|
|
293
|
+
const chunks = [];
|
|
294
|
+
for await (const chunk of process.stdin) {
|
|
295
|
+
chunks.push(chunk);
|
|
296
|
+
}
|
|
297
|
+
return Buffer.concat(chunks);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function discoverPaths(input, flags) {
|
|
301
|
+
const paths = await glob(
|
|
302
|
+
[
|
|
303
|
+
...input,
|
|
304
|
+
"!**/package-lock.json",
|
|
305
|
+
"!**/npm-shrinkwrap.json",
|
|
306
|
+
"!**/yarn.lock",
|
|
307
|
+
...(flags.nodemodules ? [] : ["!**/node_modules/**"]),
|
|
308
|
+
...(flags.pack ? ["!**/package.json"] : []),
|
|
309
|
+
],
|
|
310
|
+
{
|
|
311
|
+
dot: true,
|
|
312
|
+
expandDirectories: { files: [".*", "*.json", "*.JSON"] },
|
|
313
|
+
followSymbolicLinks: false,
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
return paths.filter(isCandidate);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function resolveStdoutSource(input, flags) {
|
|
320
|
+
if (input.includes("-")) {
|
|
321
|
+
if (input.length !== 1) {
|
|
322
|
+
argumentError(
|
|
323
|
+
'The standard-input operand "-" cannot be combined with file paths',
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return "-";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const paths = await discoverPaths(input, flags);
|
|
330
|
+
if (paths.length !== 1) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Printing sorted JSON to stdout requires exactly one input; found ${paths.length}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return paths[0];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function formatSource(source, options) {
|
|
339
|
+
let output;
|
|
340
|
+
await processFiles([source], {
|
|
341
|
+
...options,
|
|
342
|
+
ci: true,
|
|
343
|
+
...(source === "-" ? { read: readStdin } : {}),
|
|
344
|
+
transform(parsed, formatOptions) {
|
|
345
|
+
const prepared = formatParsedJson(
|
|
346
|
+
parsed,
|
|
347
|
+
formatOptions.contents,
|
|
348
|
+
formatOptions,
|
|
349
|
+
);
|
|
350
|
+
output = prepared.output;
|
|
351
|
+
return prepared;
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
return output;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function errorMessage(error) {
|
|
358
|
+
if (error instanceof ProcessingError) {
|
|
359
|
+
return error.failures.map((failure) => failure.message).join("\n");
|
|
360
|
+
}
|
|
361
|
+
return error instanceof Error ? error.message : String(error);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function handleStdoutError(error) {
|
|
365
|
+
if (error?.code === "EPIPE") {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.error(`${prefix}${errorMessage(error)}`);
|
|
369
|
+
process.exitCode = 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
262
372
|
function createPrinter() {
|
|
263
373
|
const useColour = Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
|
|
264
374
|
const colours = { green: 32, grey: 90, red: 31, white: 37, yellow: 33 };
|
|
@@ -270,9 +380,10 @@ function createPrinter() {
|
|
|
270
380
|
async function main() {
|
|
271
381
|
const rawArguments = process.argv.slice(2);
|
|
272
382
|
const silentRequested = requestsSilent(rawArguments);
|
|
383
|
+
const stdinIsPiped = standardInputIsPiped();
|
|
273
384
|
let parsed;
|
|
274
385
|
try {
|
|
275
|
-
parsed = parseArguments(rawArguments);
|
|
386
|
+
parsed = parseArguments(rawArguments, { stdinIsPiped });
|
|
276
387
|
} catch (error) {
|
|
277
388
|
if (!silentRequested) {
|
|
278
389
|
console.error(error.message);
|
|
@@ -281,7 +392,7 @@ async function main() {
|
|
|
281
392
|
return;
|
|
282
393
|
}
|
|
283
394
|
|
|
284
|
-
const { flags, indentationCount, input } = parsed;
|
|
395
|
+
const { flags, hasInput, indentationCount, input } = parsed;
|
|
285
396
|
if (flags.version) {
|
|
286
397
|
console.log(pkg.version);
|
|
287
398
|
return;
|
|
@@ -291,31 +402,41 @@ async function main() {
|
|
|
291
402
|
return;
|
|
292
403
|
}
|
|
293
404
|
|
|
294
|
-
|
|
405
|
+
const printsJson =
|
|
406
|
+
flags.stdout || input.includes("-") || (!hasInput && stdinIsPiped);
|
|
407
|
+
|
|
408
|
+
if (!flags.silent && !flags.ci && !printsJson && process.stdout.isTTY) {
|
|
295
409
|
try {
|
|
296
410
|
updateNotifier({ pkg }).notify();
|
|
297
411
|
} catch {}
|
|
298
412
|
}
|
|
299
413
|
|
|
414
|
+
const options = {
|
|
415
|
+
arrays: flags.arrays,
|
|
416
|
+
indentationCount,
|
|
417
|
+
lineEnding: flags.lineEnding || undefined,
|
|
418
|
+
pack: flags.pack,
|
|
419
|
+
tabs: flags.tabs,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
if (printsJson) {
|
|
423
|
+
try {
|
|
424
|
+
const source = await resolveStdoutSource(hasInput ? input : ["-"], flags);
|
|
425
|
+
process.stdout.on("error", handleStdoutError);
|
|
426
|
+
process.stdout.write(await formatSource(source, options));
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (!flags.silent) {
|
|
429
|
+
console.error(`${prefix}${errorMessage(error)}`);
|
|
430
|
+
}
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
300
436
|
const { colour } = createPrinter();
|
|
301
437
|
let paths;
|
|
302
438
|
try {
|
|
303
|
-
paths = await
|
|
304
|
-
[
|
|
305
|
-
...input,
|
|
306
|
-
"!**/package-lock.json",
|
|
307
|
-
"!**/npm-shrinkwrap.json",
|
|
308
|
-
"!**/yarn.lock",
|
|
309
|
-
...(flags.nodemodules ? [] : ["!**/node_modules/**"]),
|
|
310
|
-
...(flags.pack ? ["!**/package.json"] : []),
|
|
311
|
-
],
|
|
312
|
-
{
|
|
313
|
-
dot: true,
|
|
314
|
-
expandDirectories: { files: [".*", "*.json", "*.JSON"] },
|
|
315
|
-
followSymbolicLinks: false,
|
|
316
|
-
},
|
|
317
|
-
);
|
|
318
|
-
paths = paths.filter(isCandidate);
|
|
439
|
+
paths = await discoverPaths(input, flags);
|
|
319
440
|
} catch (error) {
|
|
320
441
|
if (!flags.silent) {
|
|
321
442
|
console.error(`${prefix}${error}`);
|
|
@@ -342,12 +463,8 @@ async function main() {
|
|
|
342
463
|
|
|
343
464
|
try {
|
|
344
465
|
const { successful, unsorted } = await processFiles(paths, {
|
|
345
|
-
|
|
466
|
+
...options,
|
|
346
467
|
ci: flags.ci,
|
|
347
|
-
indentationCount,
|
|
348
|
-
lineEnding: flags.lineEnding || undefined,
|
|
349
|
-
pack: flags.pack,
|
|
350
|
-
tabs: flags.tabs,
|
|
351
468
|
onOutcome(outcome) {
|
|
352
469
|
if (flags.silent) {
|
|
353
470
|
return;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-sort-cli",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"description": "Deep-sort JSON files or standard input; package.json retains its special key order",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
7
7
|
"cli",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"codsen-glob": "^1.1.
|
|
65
|
+
"codsen-glob": "^1.1.1",
|
|
66
66
|
"sort-package-json": "^2.15.1",
|
|
67
67
|
"update-notifier": "^7.3.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"p-map": "^7.0.
|
|
70
|
+
"p-map": "^7.0.7"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=18.20.8"
|