bestjsonformatter 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +149 -25
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +429 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/json-check.d.ts +32 -0
- package/dist/lib/json-check.js +132 -0
- package/dist/lib/json-schema.d.ts +19 -0
- package/dist/lib/json-schema.js +195 -0
- package/dist/lib/json-tools.d.ts +41 -0
- package/dist/lib/json-tools.js +543 -0
- package/dist/lib/lossless-json.d.ts +83 -0
- package/dist/lib/lossless-json.js +508 -0
- package/package.json +48 -12
- package/dist/cli.mjs +0 -248
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suraj Krishnan Rajan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,39 +1,163 @@
|
|
|
1
|
-
# Best JSON Formatter
|
|
1
|
+
# Best JSON Formatter CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/bestjsonformatter)
|
|
4
|
+
[](https://github.com/skrcode/bestjsonformatter-cli/actions/workflows/ci.yml)
|
|
5
|
+
[](https://skills.sh/skrcode/bestjsonformatter-cli)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Fast, local JSON formatting and correctness checks without lossy
|
|
8
|
+
`JSON.parse()`/`JSON.stringify()` round trips.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npx -y bestjsonformatter@latest check response.json
|
|
12
|
+
npx -y bestjsonformatter@latest format response.json --write
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Best JSON Formatter preserves duplicate object properties, integers beyond JavaScript's safe
|
|
16
|
+
range, decimal and exponent spelling, string escape spelling, and property order. It performs no
|
|
17
|
+
network requests, collects no analytics, and requires no MCP server or account.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Use it without installing:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx -y bestjsonformatter@latest check package.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install the short `bjf` command:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --global bestjsonformatter
|
|
31
|
+
bjf check package.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Node.js 20 or newer is required.
|
|
35
|
+
|
|
36
|
+
## Check JSON
|
|
37
|
+
|
|
38
|
+
`check` validates syntax and finds duplicate keys and numbers that ordinary JavaScript parsing may
|
|
39
|
+
round, overflow, underflow, or rewrite:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bjf check response.json
|
|
43
|
+
bjf check response.json --json
|
|
44
|
+
bjf check response.json --schema response.schema.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exit codes are stable for CI and coding agents:
|
|
48
|
+
|
|
49
|
+
| Code | Meaning |
|
|
50
|
+
| ---: | --- |
|
|
51
|
+
| `0` | Valid JSON with no selected correctness findings |
|
|
52
|
+
| `1` | Invalid JSON, correctness findings, schema errors, or formatting differences |
|
|
53
|
+
| `2` | Invalid command, inaccessible file, or internal failure |
|
|
54
|
+
|
|
55
|
+
## Format JSON
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bjf format response.json
|
|
59
|
+
bjf format response.json --check
|
|
60
|
+
bjf format response.json --write
|
|
61
|
+
bjf format response.json --indent minified
|
|
62
|
+
bjf format response.json --indent tab
|
|
63
|
+
bjf format response.json --sort
|
|
64
|
+
```
|
|
10
65
|
|
|
11
|
-
|
|
12
|
-
|
|
66
|
+
Ordinary formatting changes whitespace only. `--sort` explicitly reorders object properties while
|
|
67
|
+
preserving array order and exact scalar tokens. `--write` uses a same-directory temporary file and
|
|
68
|
+
atomic rename.
|
|
69
|
+
|
|
70
|
+
Multiple files are supported when checking or writing:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
bjf format package.json fixtures/*.json --check
|
|
74
|
+
bjf format package.json fixtures/*.json --write
|
|
75
|
+
bjf check fixtures/*.json --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Repair, query, compare, convert, and use schemas
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bjf repair broken.json
|
|
82
|
+
bjf repair broken.json --write
|
|
83
|
+
bjf duplicates response.json
|
|
84
|
+
bjf precision response.json
|
|
85
|
+
bjf compare before.json after.json
|
|
86
|
+
bjf query '$.items[*].id' response.json
|
|
87
|
+
bjf to-yaml response.json
|
|
88
|
+
bjf to-csv records.json
|
|
89
|
+
bjf to-xml response.json
|
|
90
|
+
bjf schema-infer response.json
|
|
91
|
+
bjf schema-validate response.json schema.json
|
|
92
|
+
bjf schema-sample schema.json
|
|
13
93
|
```
|
|
14
94
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
95
|
+
Repair output is a proposal unless `--write` is explicitly supplied. Use `-o output.ext` to write
|
|
96
|
+
conversion or repair output to a separate file.
|
|
97
|
+
|
|
98
|
+
## Coding agents
|
|
99
|
+
|
|
100
|
+
Agents can call the CLI directly through their normal shell, so JSON never has to be copied into an
|
|
101
|
+
MCP tool argument or model context.
|
|
18
102
|
|
|
19
|
-
|
|
103
|
+
Install the agent skill:
|
|
20
104
|
|
|
21
105
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
106
|
+
npx skills add skrcode/bestjsonformatter-cli
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The skill teaches Codex, Claude Code, Cursor, GitHub Copilot, and other compatible agents to run
|
|
110
|
+
compact checks first and request explicit authorization before `--write`.
|
|
111
|
+
|
|
112
|
+
## Library
|
|
113
|
+
|
|
114
|
+
The same engine is available as an ESM library:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { checkJson, formatLosslessJson } from "bestjsonformatter";
|
|
118
|
+
|
|
119
|
+
const checked = checkJson(source);
|
|
120
|
+
const formatted = formatLosslessJson(source, 2).formatted;
|
|
28
121
|
```
|
|
29
122
|
|
|
30
|
-
|
|
123
|
+
Heavy repair, diff, query, conversion, and schema dependencies are loaded only when their
|
|
124
|
+
operations are requested. Formatting, validation, and correctness checks stay on the small local
|
|
125
|
+
hot path.
|
|
126
|
+
|
|
127
|
+
## Performance and correctness
|
|
128
|
+
|
|
129
|
+
In the repository's reproducible five-run benchmark, Best JSON Formatter is the fastest formatter
|
|
130
|
+
that passes every exact-token check.
|
|
131
|
+
|
|
132
|
+
| Fresh-process median | 155 B | 105 KB | 1.07 MB | 5.35 MB | Fully lossless |
|
|
133
|
+
| --- | ---: | ---: | ---: | ---: | :---: |
|
|
134
|
+
| Best JSON Formatter 0.2.0 | **54 ms** | **76 ms** | **143 ms** | **473 ms** | Yes |
|
|
135
|
+
| Biome 2.5.6 | 58 ms | 88 ms | 280 ms | 1,150 ms | No |
|
|
136
|
+
| Prettier 3.9.6 | 120 ms | 234 ms | 955 ms | 3,926 ms | No |
|
|
137
|
+
| jsonc-parser 3.3.1 | 63 ms | 825 ms | >15,000 ms | >15,000 ms | Yes |
|
|
138
|
+
| jq 1.8.2 | 5 ms | 14 ms | 105 ms | 500 ms | No |
|
|
31
139
|
|
|
32
|
-
|
|
140
|
+
The comparison uses identical stdin payloads and complete fresh CLI processes. jq is faster on
|
|
141
|
+
small and one-megabyte inputs but discards duplicate keys and rewrites number and escape spelling.
|
|
142
|
+
Results are bounded to the recorded machine, versions, inputs, and command boundaries—not a
|
|
143
|
+
universal speed claim. See [the complete method, raw samples, Python and native Node
|
|
144
|
+
results](docs/BENCHMARKS.md).
|
|
145
|
+
|
|
146
|
+
## Privacy
|
|
147
|
+
|
|
148
|
+
Documents are read and processed inside the local Node.js process. The package contains no
|
|
149
|
+
telemetry client, post-install script, network request, account system, or hosted document API.
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm install
|
|
155
|
+
npm run check
|
|
156
|
+
npm run benchmark
|
|
157
|
+
```
|
|
33
158
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
notation, and original escape spelling. Repair output is always a proposal for
|
|
37
|
-
review rather than an automatic source-file mutation.
|
|
159
|
+
Every formatting change must preserve the exact-token fixtures. Performance changes must record
|
|
160
|
+
the complete journey, competitor versions, raw samples, median, p95, correctness, and timeouts.
|
|
38
161
|
|
|
39
|
-
|
|
162
|
+
MIT licensed. The browser tool is available at
|
|
163
|
+
[bestjsonformatter.org](https://bestjsonformatter.org).
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
const help = `Best JSON Formatter — fast, local, lossless JSON tools
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
bjf format [file ...] [--indent minified|2|3|4|tab] [--sort] [--write|--check]
|
|
10
|
+
bjf check [file ...] [--schema schema.json] [--json]
|
|
11
|
+
bjf validate [file ...] [--json]
|
|
12
|
+
bjf repair [file|-] [--write|-o output.json]
|
|
13
|
+
bjf duplicates [file|-]
|
|
14
|
+
bjf precision [file|-]
|
|
15
|
+
bjf compare <original.json> <changed.json>
|
|
16
|
+
bjf query <jsonpath> [file|-]
|
|
17
|
+
bjf to-yaml|to-csv|to-xml [file|-] [-o output]
|
|
18
|
+
bjf schema-infer [file|-]
|
|
19
|
+
bjf schema-validate <document.json> <schema.json>
|
|
20
|
+
bjf schema-sample [schema.json|-]
|
|
21
|
+
|
|
22
|
+
Use "-" or pipe stdin for document input. Formatting changes whitespace only:
|
|
23
|
+
duplicate keys, large integers, number notation, string escapes, and key order
|
|
24
|
+
remain exact unless --sort or repair is explicitly requested.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-w, --write Write the result back atomically
|
|
28
|
+
-c, --check Exit 1 when formatting differs
|
|
29
|
+
-o, --output Write to a separate file
|
|
30
|
+
--json Emit machine-readable check or validation results
|
|
31
|
+
-q, --quiet Suppress successful human-readable status
|
|
32
|
+
-h, --help Show help
|
|
33
|
+
-v, --version Show the installed version`;
|
|
34
|
+
function parseIndent(value) {
|
|
35
|
+
if (value === "minified" || value === "0")
|
|
36
|
+
return 0;
|
|
37
|
+
if (value === "tab")
|
|
38
|
+
return "tab";
|
|
39
|
+
if (value === "2" || value === "3" || value === "4")
|
|
40
|
+
return Number(value);
|
|
41
|
+
throw new TypeError("--indent must be minified, 2, 3, 4, or tab.");
|
|
42
|
+
}
|
|
43
|
+
function parseArguments(args) {
|
|
44
|
+
const options = {
|
|
45
|
+
indent: 2,
|
|
46
|
+
sort: false,
|
|
47
|
+
write: false,
|
|
48
|
+
check: false,
|
|
49
|
+
json: false,
|
|
50
|
+
quiet: false,
|
|
51
|
+
};
|
|
52
|
+
const operands = [];
|
|
53
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
54
|
+
const argument = args[index];
|
|
55
|
+
if (argument === "--") {
|
|
56
|
+
operands.push(...args.slice(index + 1));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (!argument.startsWith("-") || argument === "-") {
|
|
60
|
+
operands.push(argument);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const [name, inlineValue] = argument.split("=", 2);
|
|
64
|
+
const value = () => {
|
|
65
|
+
if (inlineValue !== undefined)
|
|
66
|
+
return inlineValue;
|
|
67
|
+
const next = args[index + 1];
|
|
68
|
+
if (next === undefined)
|
|
69
|
+
throw new TypeError(`${name} requires a value.`);
|
|
70
|
+
index += 1;
|
|
71
|
+
return next;
|
|
72
|
+
};
|
|
73
|
+
switch (name) {
|
|
74
|
+
case "--indent":
|
|
75
|
+
options.indent = parseIndent(value());
|
|
76
|
+
break;
|
|
77
|
+
case "--output":
|
|
78
|
+
case "-o":
|
|
79
|
+
options.output = value();
|
|
80
|
+
break;
|
|
81
|
+
case "--schema":
|
|
82
|
+
options.schema = value();
|
|
83
|
+
break;
|
|
84
|
+
case "--sort":
|
|
85
|
+
options.sort = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--write":
|
|
88
|
+
case "-w":
|
|
89
|
+
options.write = true;
|
|
90
|
+
break;
|
|
91
|
+
case "--check":
|
|
92
|
+
case "-c":
|
|
93
|
+
options.check = true;
|
|
94
|
+
break;
|
|
95
|
+
case "--json":
|
|
96
|
+
options.json = true;
|
|
97
|
+
break;
|
|
98
|
+
case "--quiet":
|
|
99
|
+
case "-q":
|
|
100
|
+
options.quiet = true;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
throw new TypeError(`Unknown option: ${name}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { options, operands };
|
|
107
|
+
}
|
|
108
|
+
async function installedVersion() {
|
|
109
|
+
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
110
|
+
return packageJson.version;
|
|
111
|
+
}
|
|
112
|
+
async function readStdin() {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
for await (const chunk of process.stdin) {
|
|
115
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
116
|
+
}
|
|
117
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
118
|
+
}
|
|
119
|
+
async function readDocument(path) {
|
|
120
|
+
if (path !== "-")
|
|
121
|
+
return readFile(path, "utf8");
|
|
122
|
+
if (process.stdin.isTTY)
|
|
123
|
+
throw new TypeError("Provide a file path or pipe JSON through stdin.");
|
|
124
|
+
return readStdin();
|
|
125
|
+
}
|
|
126
|
+
async function writeStdout(value) {
|
|
127
|
+
if (!process.stdout.write(value))
|
|
128
|
+
await once(process.stdout, "drain");
|
|
129
|
+
}
|
|
130
|
+
function withFinalNewline(value) {
|
|
131
|
+
return `${value.replace(/\n*$/, "")}\n`;
|
|
132
|
+
}
|
|
133
|
+
async function writeAtomically(path, value) {
|
|
134
|
+
const temporaryPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
135
|
+
let mode;
|
|
136
|
+
try {
|
|
137
|
+
mode = (await stat(path)).mode;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await writeFile(temporaryPath, value, mode === undefined ? undefined : { mode });
|
|
145
|
+
await rename(temporaryPath, path);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function displayPath(path) {
|
|
153
|
+
return path === "-" ? "<stdin>" : path;
|
|
154
|
+
}
|
|
155
|
+
function setFailure() {
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
}
|
|
158
|
+
function syntaxFailure(path, error) {
|
|
159
|
+
if (error &&
|
|
160
|
+
typeof error === "object" &&
|
|
161
|
+
"line" in error &&
|
|
162
|
+
"column" in error &&
|
|
163
|
+
"message" in error) {
|
|
164
|
+
const line = String(error.line);
|
|
165
|
+
const column = String(error.column);
|
|
166
|
+
process.stderr.write(`${displayPath(path)}:${line}:${column} ${String(error.message)}\n`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
170
|
+
process.stderr.write(`${displayPath(path)}: ${message}\n`);
|
|
171
|
+
}
|
|
172
|
+
setFailure();
|
|
173
|
+
}
|
|
174
|
+
async function formatCommand(files, options) {
|
|
175
|
+
const paths = files.length ? files : ["-"];
|
|
176
|
+
if (options.write && options.check)
|
|
177
|
+
throw new TypeError("--write and --check cannot be combined.");
|
|
178
|
+
if (options.output && (options.write || options.check || paths.length !== 1)) {
|
|
179
|
+
throw new TypeError("--output requires one input and cannot be combined with --write or --check.");
|
|
180
|
+
}
|
|
181
|
+
if (paths.length > 1 && !options.write && !options.check) {
|
|
182
|
+
throw new TypeError("Multiple format inputs require --write or --check.");
|
|
183
|
+
}
|
|
184
|
+
if ((options.write || options.check) && paths.includes("-")) {
|
|
185
|
+
throw new TypeError("--write and --check require file paths, not stdin.");
|
|
186
|
+
}
|
|
187
|
+
const { JsonSyntaxError, formatLosslessJson } = await import("./lib/lossless-json.js");
|
|
188
|
+
for (const path of paths) {
|
|
189
|
+
const input = await readDocument(path);
|
|
190
|
+
try {
|
|
191
|
+
let output;
|
|
192
|
+
if (options.sort) {
|
|
193
|
+
const { runTool } = await import("./lib/json-tools.js");
|
|
194
|
+
const result = await runTool({
|
|
195
|
+
operation: "sort",
|
|
196
|
+
input,
|
|
197
|
+
indent: options.indent,
|
|
198
|
+
includeEdits: false,
|
|
199
|
+
});
|
|
200
|
+
if (!result.ok) {
|
|
201
|
+
syntaxFailure(path, result);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
output = result.value;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
output = formatLosslessJson(input, options.indent).formatted;
|
|
208
|
+
}
|
|
209
|
+
const rendered = withFinalNewline(output);
|
|
210
|
+
if (options.check) {
|
|
211
|
+
if (input !== rendered) {
|
|
212
|
+
process.stderr.write(`${path}: formatting differs\n`);
|
|
213
|
+
setFailure();
|
|
214
|
+
}
|
|
215
|
+
else if (!options.quiet) {
|
|
216
|
+
process.stdout.write(`${path}: formatted\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (options.write) {
|
|
220
|
+
if (input !== rendered)
|
|
221
|
+
await writeAtomically(path, rendered);
|
|
222
|
+
if (!options.quiet)
|
|
223
|
+
process.stdout.write(`${path}: ${input === rendered ? "unchanged" : "formatted"}\n`);
|
|
224
|
+
}
|
|
225
|
+
else if (options.output) {
|
|
226
|
+
await writeAtomically(options.output, rendered);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
await writeStdout(rendered);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error instanceof JsonSyntaxError)
|
|
234
|
+
syntaxFailure(path, error);
|
|
235
|
+
else
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function validateCommand(files, options) {
|
|
241
|
+
const paths = files.length ? files : ["-"];
|
|
242
|
+
const { JsonSyntaxError, parseLosslessJson } = await import("./lib/lossless-json.js");
|
|
243
|
+
const results = [];
|
|
244
|
+
for (const path of paths) {
|
|
245
|
+
const input = await readDocument(path);
|
|
246
|
+
try {
|
|
247
|
+
const parsed = parseLosslessJson(input);
|
|
248
|
+
results.push({ path: displayPath(path), valid: true, metadata: parsed.metadata });
|
|
249
|
+
if (!options.json && !options.quiet)
|
|
250
|
+
process.stdout.write(`${displayPath(path)}: valid JSON\n`);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
setFailure();
|
|
254
|
+
if (error instanceof JsonSyntaxError) {
|
|
255
|
+
results.push({
|
|
256
|
+
path: displayPath(path),
|
|
257
|
+
valid: false,
|
|
258
|
+
message: error.message,
|
|
259
|
+
line: error.line,
|
|
260
|
+
column: error.column,
|
|
261
|
+
offset: error.offset,
|
|
262
|
+
});
|
|
263
|
+
if (!options.json)
|
|
264
|
+
syntaxFailure(path, error);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (options.json) {
|
|
272
|
+
await writeStdout(`${JSON.stringify(paths.length === 1 ? results[0] : results, null, 2)}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function checkCommand(files, options) {
|
|
276
|
+
const paths = files.length ? files : ["-"];
|
|
277
|
+
const { checkJson } = await import("./lib/json-check.js");
|
|
278
|
+
const schemaInput = options.schema ? await readFile(options.schema, "utf8") : undefined;
|
|
279
|
+
const results = [];
|
|
280
|
+
for (const path of paths) {
|
|
281
|
+
const input = await readDocument(path);
|
|
282
|
+
const result = checkJson(input);
|
|
283
|
+
const schemaErrors = [];
|
|
284
|
+
if (result.valid && schemaInput !== undefined) {
|
|
285
|
+
const { validateJsonSchema } = await import("./lib/json-schema.js");
|
|
286
|
+
const schemaResult = await validateJsonSchema(input, schemaInput);
|
|
287
|
+
schemaErrors.push(...schemaResult.errors);
|
|
288
|
+
}
|
|
289
|
+
const clean = result.valid && result.clean && schemaErrors.length === 0;
|
|
290
|
+
if (!clean)
|
|
291
|
+
setFailure();
|
|
292
|
+
results.push({
|
|
293
|
+
path: displayPath(path),
|
|
294
|
+
...result,
|
|
295
|
+
...(schemaInput === undefined
|
|
296
|
+
? {}
|
|
297
|
+
: { schemaValid: schemaErrors.length === 0, schemaErrors }),
|
|
298
|
+
});
|
|
299
|
+
if (options.json)
|
|
300
|
+
continue;
|
|
301
|
+
if (!result.valid) {
|
|
302
|
+
process.stderr.write(`${displayPath(path)}:${result.line}:${result.column} ${result.message}\n`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
for (const issue of result.issues) {
|
|
306
|
+
const location = issue.locations[0];
|
|
307
|
+
process.stderr.write(`${displayPath(path)}:${location.line}:${location.column} ${issue.severity} ${issue.code} ${issue.path}: ${issue.message}\n`);
|
|
308
|
+
}
|
|
309
|
+
for (const error of schemaErrors) {
|
|
310
|
+
process.stderr.write(`${displayPath(path)}: error schema: ${error}\n`);
|
|
311
|
+
}
|
|
312
|
+
if (clean && !options.quiet) {
|
|
313
|
+
process.stdout.write(`${displayPath(path)}: valid, lossless-safe JSON\n`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (options.json) {
|
|
317
|
+
await writeStdout(`${JSON.stringify(paths.length === 1 ? results[0] : results, null, 2)}\n`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function executeToolCommand(command, operands, options) {
|
|
321
|
+
const { runTool } = await import("./lib/json-tools.js");
|
|
322
|
+
let operation;
|
|
323
|
+
let inputPath = operands[0] ?? "-";
|
|
324
|
+
let secondaryInput;
|
|
325
|
+
let query;
|
|
326
|
+
switch (command) {
|
|
327
|
+
case "repair":
|
|
328
|
+
operation = "repair";
|
|
329
|
+
break;
|
|
330
|
+
case "duplicates":
|
|
331
|
+
operation = "duplicates";
|
|
332
|
+
break;
|
|
333
|
+
case "precision":
|
|
334
|
+
operation = "precision";
|
|
335
|
+
break;
|
|
336
|
+
case "compare":
|
|
337
|
+
if (operands.length !== 2)
|
|
338
|
+
throw new TypeError("compare requires two JSON files.");
|
|
339
|
+
operation = "compare";
|
|
340
|
+
inputPath = operands[0];
|
|
341
|
+
secondaryInput = await readDocument(operands[1]);
|
|
342
|
+
break;
|
|
343
|
+
case "query":
|
|
344
|
+
if (!operands[0])
|
|
345
|
+
throw new TypeError("query requires a JSONPath expression.");
|
|
346
|
+
operation = "jsonpath";
|
|
347
|
+
query = operands[0];
|
|
348
|
+
inputPath = operands[1] ?? "-";
|
|
349
|
+
break;
|
|
350
|
+
case "to-yaml":
|
|
351
|
+
case "to-csv":
|
|
352
|
+
case "to-xml":
|
|
353
|
+
operation = command.slice(3);
|
|
354
|
+
break;
|
|
355
|
+
case "schema-infer":
|
|
356
|
+
operation = "schema";
|
|
357
|
+
break;
|
|
358
|
+
case "schema-validate":
|
|
359
|
+
if (operands.length !== 2) {
|
|
360
|
+
throw new TypeError("schema-validate requires a JSON file and a schema file.");
|
|
361
|
+
}
|
|
362
|
+
operation = "schema_validate";
|
|
363
|
+
inputPath = operands[0];
|
|
364
|
+
secondaryInput = await readDocument(operands[1]);
|
|
365
|
+
break;
|
|
366
|
+
case "schema-sample":
|
|
367
|
+
operation = "schema_sample";
|
|
368
|
+
break;
|
|
369
|
+
default:
|
|
370
|
+
throw new TypeError(`Unknown command: ${command}`);
|
|
371
|
+
}
|
|
372
|
+
if (options.write && command !== "repair") {
|
|
373
|
+
throw new TypeError("--write is supported by format and repair.");
|
|
374
|
+
}
|
|
375
|
+
if (options.output && options.write)
|
|
376
|
+
throw new TypeError("--output and --write cannot be combined.");
|
|
377
|
+
if (options.write && inputPath === "-")
|
|
378
|
+
throw new TypeError("--write requires a file path.");
|
|
379
|
+
const input = await readDocument(inputPath);
|
|
380
|
+
const result = await runTool({
|
|
381
|
+
operation,
|
|
382
|
+
input,
|
|
383
|
+
secondaryInput,
|
|
384
|
+
query,
|
|
385
|
+
indent: options.indent,
|
|
386
|
+
includeEdits: false,
|
|
387
|
+
});
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
syntaxFailure(inputPath, result);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const rendered = withFinalNewline(result.value);
|
|
393
|
+
if (options.write) {
|
|
394
|
+
await writeAtomically(inputPath, rendered);
|
|
395
|
+
if (!options.quiet)
|
|
396
|
+
process.stdout.write(`${inputPath}: repaired\n`);
|
|
397
|
+
}
|
|
398
|
+
else if (options.output) {
|
|
399
|
+
await writeAtomically(options.output, rendered);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
await writeStdout(rendered);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function main(args) {
|
|
406
|
+
const first = args[0];
|
|
407
|
+
if (!first || first === "help" || first === "--help" || first === "-h") {
|
|
408
|
+
process.stdout.write(`${help}\n`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (first === "--version" || first === "-v" || first === "version") {
|
|
412
|
+
process.stdout.write(`${await installedVersion()}\n`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const { options, operands } = parseArguments(args.slice(1));
|
|
416
|
+
if (first === "format")
|
|
417
|
+
await formatCommand(operands, options);
|
|
418
|
+
else if (first === "validate")
|
|
419
|
+
await validateCommand(operands, options);
|
|
420
|
+
else if (first === "check")
|
|
421
|
+
await checkCommand(operands, options);
|
|
422
|
+
else
|
|
423
|
+
await executeToolCommand(first, operands, options);
|
|
424
|
+
}
|
|
425
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
426
|
+
const message = error instanceof Error ? error.message : "The command failed.";
|
|
427
|
+
process.stderr.write(`bestjsonformatter: ${message}\n`);
|
|
428
|
+
process.exitCode = 2;
|
|
429
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { checkJson, type JsonCheckIssue, type JsonCheckResult, type JsonIssueLocation, } from "./lib/json-check.js";
|
|
2
|
+
export { parseJson, runTool, type TextEdit, type ToolFailure, type ToolOperation, type ToolRequest, type ToolResponse, type ToolResult, } from "./lib/json-tools.js";
|
|
3
|
+
export { canonicalJsonNumber, formatLosslessJson, type JsonNode, type JsonParseMetadata, JsonSyntaxError, jsonNodeToNative, type LosslessFormatResult, LosslessNumber, type LosslessParseResult, type LosslessTreeResult, parseLosslessJson, serializeJsonNode, stringifyNativeLosslessly, } from "./lib/lossless-json.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { checkJson, } from "./lib/json-check.js";
|
|
2
|
+
export { parseJson, runTool, } from "./lib/json-tools.js";
|
|
3
|
+
export { canonicalJsonNumber, formatLosslessJson, JsonSyntaxError, jsonNodeToNative, LosslessNumber, parseLosslessJson, serializeJsonNode, stringifyNativeLosslessly, } from "./lib/lossless-json.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type JsonParseMetadata } from "./lossless-json.js";
|
|
2
|
+
export type JsonIssueLocation = {
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
offset: number;
|
|
6
|
+
};
|
|
7
|
+
export type JsonCheckIssue = {
|
|
8
|
+
code: "duplicate-key" | "unsafe-number" | "number-overflow" | "number-underflow" | "number-spelling";
|
|
9
|
+
severity: "error" | "warning";
|
|
10
|
+
path: string;
|
|
11
|
+
message: string;
|
|
12
|
+
token?: string;
|
|
13
|
+
locations: JsonIssueLocation[];
|
|
14
|
+
};
|
|
15
|
+
type JsonCheckSuccess = {
|
|
16
|
+
valid: true;
|
|
17
|
+
clean: boolean;
|
|
18
|
+
issues: JsonCheckIssue[];
|
|
19
|
+
metadata: JsonParseMetadata;
|
|
20
|
+
};
|
|
21
|
+
type JsonCheckFailure = {
|
|
22
|
+
valid: false;
|
|
23
|
+
clean: false;
|
|
24
|
+
issues: [];
|
|
25
|
+
message: string;
|
|
26
|
+
line: number;
|
|
27
|
+
column: number;
|
|
28
|
+
offset: number;
|
|
29
|
+
};
|
|
30
|
+
export type JsonCheckResult = JsonCheckSuccess | JsonCheckFailure;
|
|
31
|
+
export declare function checkJson(input: string): JsonCheckResult;
|
|
32
|
+
export {};
|