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 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">Command line app to deep sort JSON files, retains package.json special key order</p>
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).map(([long, definition]) => [
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, jsonsort recursively sorts JSON files below the current
51
- directory. Discovered symbolic links are not followed. An explicitly selected
52
- symbolic-link directory is resolved as the selected root; symbolic-link files
53
- are rejected before reading.
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
- $ jsonsort "templates/**/*.json" --arrays --lineEnding lf
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
- if (
232
- input.length === 0 &&
233
- expanded.length > 0 &&
234
- !flags.help &&
235
- !flags.version
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: input.length ? input : ["**/*.json"],
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
- if (!flags.silent && !flags.ci && process.stdout.isTTY) {
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 glob(
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
- arrays: flags.arrays,
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.2.3",
4
- "description": "Command line app to deep sort JSON files, retains package.json special key order",
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.0",
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.4"
70
+ "p-map": "^7.0.7"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=18.20.8"