json-sort-cli 4.2.2 → 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 +13 -0
- package/README.md +1 -1
- package/cli.js +495 -311
- package/json-formatter.js +421 -0
- package/package.json +4 -6
- package/process-files.js +169 -86
- package/json-file.js +0 -42
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { sortOrder } from "sort-package-json";
|
|
2
|
+
|
|
3
|
+
const numberToken = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
4
|
+
const hexDigit = /^[\dA-Fa-f]$/;
|
|
5
|
+
const packageOrder = sortOrder.filter((key) => !["lect", "tap"].includes(key));
|
|
6
|
+
packageOrder.splice(packageOrder.indexOf("resolutions"), 0, "tap", "lect");
|
|
7
|
+
const packageRank = new Map(packageOrder.map((key, index) => [key, index]));
|
|
8
|
+
|
|
9
|
+
function compareStrings(left, right) {
|
|
10
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function comparePackageKeys(left, right) {
|
|
14
|
+
const leftRank = packageRank.get(left);
|
|
15
|
+
const rightRank = packageRank.get(right);
|
|
16
|
+
|
|
17
|
+
if (leftRank !== undefined || rightRank !== undefined) {
|
|
18
|
+
if (leftRank === undefined) {
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
if (rightRank === undefined) {
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
return leftRank - rightRank;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const leftPrivate = left.startsWith("_");
|
|
28
|
+
const rightPrivate = right.startsWith("_");
|
|
29
|
+
if (leftPrivate !== rightPrivate) {
|
|
30
|
+
return leftPrivate ? 1 : -1;
|
|
31
|
+
}
|
|
32
|
+
return compareStrings(left, right);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function syntaxError(message, position) {
|
|
36
|
+
return new SyntaxError(
|
|
37
|
+
`json-sort-cli/parseJson(): [THROW_ID_01] ${message} at character ${position}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tokenize(json) {
|
|
42
|
+
let index = 0;
|
|
43
|
+
|
|
44
|
+
return function nextToken() {
|
|
45
|
+
while (/[\t\n\r ]/u.test(json[index] ?? "")) {
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const position = index;
|
|
50
|
+
const character = json[index];
|
|
51
|
+
if (character === undefined) {
|
|
52
|
+
return { position, type: "eof" };
|
|
53
|
+
}
|
|
54
|
+
if ("{}[]:,".includes(character)) {
|
|
55
|
+
index += 1;
|
|
56
|
+
return { position, type: character };
|
|
57
|
+
}
|
|
58
|
+
if (character === '"') {
|
|
59
|
+
index += 1;
|
|
60
|
+
while (index < json.length) {
|
|
61
|
+
const code = json.charCodeAt(index);
|
|
62
|
+
if (code === 0x22) {
|
|
63
|
+
index += 1;
|
|
64
|
+
const raw = json.slice(position, index);
|
|
65
|
+
return {
|
|
66
|
+
position,
|
|
67
|
+
raw,
|
|
68
|
+
type: "string",
|
|
69
|
+
value: JSON.parse(raw),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (code <= 0x1f) {
|
|
73
|
+
throw syntaxError("Unescaped control character in string", index);
|
|
74
|
+
}
|
|
75
|
+
if (code === 0x5c) {
|
|
76
|
+
index += 1;
|
|
77
|
+
const escaped = json[index];
|
|
78
|
+
if (escaped === "u") {
|
|
79
|
+
for (let offset = 1; offset <= 4; offset += 1) {
|
|
80
|
+
if (!hexDigit.test(json[index + offset] ?? "")) {
|
|
81
|
+
throw syntaxError("Invalid Unicode escape", index);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
index += 5;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!'"\\/bfnrt'.includes(escaped ?? "")) {
|
|
88
|
+
throw syntaxError("Invalid escape sequence", index);
|
|
89
|
+
}
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
throw syntaxError("Unterminated string", position);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
numberToken.lastIndex = index;
|
|
99
|
+
const numberMatch = numberToken.exec(json);
|
|
100
|
+
if (numberMatch) {
|
|
101
|
+
index = numberToken.lastIndex;
|
|
102
|
+
return { position, raw: numberMatch[0], type: "number" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const literal of ["true", "false", "null"]) {
|
|
106
|
+
if (json.startsWith(literal, index)) {
|
|
107
|
+
index += literal.length;
|
|
108
|
+
return { position, raw: literal, type: "literal" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw syntaxError(
|
|
113
|
+
`Unexpected token ${JSON.stringify(character)}`,
|
|
114
|
+
position,
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scalarNode(token) {
|
|
120
|
+
if (token.type === "string") {
|
|
121
|
+
return { type: "string", value: token.value };
|
|
122
|
+
}
|
|
123
|
+
if (token.type === "number" || token.type === "literal") {
|
|
124
|
+
return { raw: token.raw, type: token.type };
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parseJson(json) {
|
|
130
|
+
const nextToken = tokenize(json.replace(/^\uFEFF/u, ""));
|
|
131
|
+
const stack = [];
|
|
132
|
+
let root;
|
|
133
|
+
let hasRoot = false;
|
|
134
|
+
|
|
135
|
+
function attachValue(token) {
|
|
136
|
+
let node = scalarNode(token);
|
|
137
|
+
if (!node && token.type === "{") {
|
|
138
|
+
node = { entries: [], type: "object" };
|
|
139
|
+
} else if (!node && token.type === "[") {
|
|
140
|
+
node = { items: [], type: "array" };
|
|
141
|
+
}
|
|
142
|
+
if (!node) {
|
|
143
|
+
throw syntaxError("Expected a JSON value", token.position);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const parent = stack.at(-1);
|
|
147
|
+
if (!parent) {
|
|
148
|
+
root = node;
|
|
149
|
+
hasRoot = true;
|
|
150
|
+
} else if (parent.node.type === "array") {
|
|
151
|
+
parent.node.items.push(node);
|
|
152
|
+
parent.state = "commaOrEnd";
|
|
153
|
+
} else {
|
|
154
|
+
parent.node.entries.push({ key: parent.pendingKey, value: node });
|
|
155
|
+
parent.pendingKey = undefined;
|
|
156
|
+
parent.state = "commaOrEnd";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (node.type === "array") {
|
|
160
|
+
stack.push({ node, state: "firstValueOrEnd" });
|
|
161
|
+
} else if (node.type === "object") {
|
|
162
|
+
stack.push({ keys: new Set(), node, state: "firstKeyOrEnd" });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
while (true) {
|
|
167
|
+
const token = nextToken();
|
|
168
|
+
const frame = stack.at(-1);
|
|
169
|
+
|
|
170
|
+
if (!frame) {
|
|
171
|
+
if (!hasRoot) {
|
|
172
|
+
if (token.type === "eof") {
|
|
173
|
+
throw syntaxError("Expected a JSON value", token.position);
|
|
174
|
+
}
|
|
175
|
+
attachValue(token);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (token.type !== "eof") {
|
|
179
|
+
throw syntaxError(
|
|
180
|
+
"Unexpected content after the JSON value",
|
|
181
|
+
token.position,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return root;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (frame.node.type === "array") {
|
|
188
|
+
if (frame.state === "firstValueOrEnd") {
|
|
189
|
+
if (token.type === "]") {
|
|
190
|
+
stack.pop();
|
|
191
|
+
} else {
|
|
192
|
+
attachValue(token);
|
|
193
|
+
}
|
|
194
|
+
} else if (frame.state === "value") {
|
|
195
|
+
attachValue(token);
|
|
196
|
+
} else if (token.type === ",") {
|
|
197
|
+
frame.state = "value";
|
|
198
|
+
} else if (token.type === "]") {
|
|
199
|
+
stack.pop();
|
|
200
|
+
} else {
|
|
201
|
+
throw syntaxError(
|
|
202
|
+
"Expected a comma or closing bracket",
|
|
203
|
+
token.position,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (frame.state === "firstKeyOrEnd") {
|
|
210
|
+
if (token.type === "}") {
|
|
211
|
+
stack.pop();
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
frame.state = "key";
|
|
215
|
+
}
|
|
216
|
+
if (frame.state === "key") {
|
|
217
|
+
if (token.type !== "string") {
|
|
218
|
+
throw syntaxError("Expected an object member name", token.position);
|
|
219
|
+
}
|
|
220
|
+
if (frame.keys.has(token.value)) {
|
|
221
|
+
throw syntaxError(
|
|
222
|
+
`Duplicate object member ${JSON.stringify(token.value)}`,
|
|
223
|
+
token.position,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
frame.keys.add(token.value);
|
|
227
|
+
frame.pendingKey = token.value;
|
|
228
|
+
frame.state = "colon";
|
|
229
|
+
} else if (frame.state === "colon") {
|
|
230
|
+
if (token.type !== ":") {
|
|
231
|
+
throw syntaxError("Expected a colon", token.position);
|
|
232
|
+
}
|
|
233
|
+
frame.state = "value";
|
|
234
|
+
} else if (frame.state === "value") {
|
|
235
|
+
attachValue(token);
|
|
236
|
+
} else if (token.type === ",") {
|
|
237
|
+
frame.state = "key";
|
|
238
|
+
} else if (token.type === "}") {
|
|
239
|
+
stack.pop();
|
|
240
|
+
} else {
|
|
241
|
+
throw syntaxError("Expected a comma or closing brace", token.position);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function decodeJson(contents) {
|
|
247
|
+
if (typeof contents === "string") {
|
|
248
|
+
return contents;
|
|
249
|
+
}
|
|
250
|
+
if (!(contents instanceof Uint8Array)) {
|
|
251
|
+
throw new TypeError(
|
|
252
|
+
"json-sort-cli/decodeJson(): [THROW_ID_01] Input must be a string or Uint8Array",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
257
|
+
contents,
|
|
258
|
+
);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new SyntaxError(
|
|
261
|
+
"json-sort-cli/decodeJson(): [THROW_ID_02] Input is not valid UTF-8",
|
|
262
|
+
{ cause: error },
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function sortTree(root, { arrays, packageJson }) {
|
|
268
|
+
const pending = [{ isRoot: true, node: root }];
|
|
269
|
+
while (pending.length) {
|
|
270
|
+
const { isRoot, node } = pending.pop();
|
|
271
|
+
if (node.type === "object") {
|
|
272
|
+
node.entries.sort((left, right) =>
|
|
273
|
+
isRoot && packageJson
|
|
274
|
+
? comparePackageKeys(left.key, right.key)
|
|
275
|
+
: compareStrings(left.key, right.key),
|
|
276
|
+
);
|
|
277
|
+
for (let index = node.entries.length - 1; index >= 0; index -= 1) {
|
|
278
|
+
pending.push({ isRoot: false, node: node.entries[index].value });
|
|
279
|
+
}
|
|
280
|
+
} else if (node.type === "array") {
|
|
281
|
+
if (
|
|
282
|
+
arrays &&
|
|
283
|
+
node.items.length > 1 &&
|
|
284
|
+
node.items.every((item) => item.type === "string")
|
|
285
|
+
) {
|
|
286
|
+
node.items.sort((left, right) =>
|
|
287
|
+
compareStrings(left.value, right.value),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
for (let index = node.items.length - 1; index >= 0; index -= 1) {
|
|
291
|
+
pending.push({ isRoot: false, node: node.items[index] });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function serialize(root, indentation) {
|
|
298
|
+
const chunks = [];
|
|
299
|
+
const events = [{ depth: 0, node: root, type: "node" }];
|
|
300
|
+
|
|
301
|
+
while (events.length) {
|
|
302
|
+
const event = events.pop();
|
|
303
|
+
if (event.type === "text") {
|
|
304
|
+
chunks.push(event.value);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { depth, node } = event;
|
|
309
|
+
if (node.type === "number" || node.type === "literal") {
|
|
310
|
+
chunks.push(node.raw);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (node.type === "string") {
|
|
314
|
+
chunks.push(JSON.stringify(node.value));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const values = node.type === "array" ? node.items : node.entries;
|
|
319
|
+
const opening = node.type === "array" ? "[" : "{";
|
|
320
|
+
const closing = node.type === "array" ? "]" : "}";
|
|
321
|
+
if (!values.length) {
|
|
322
|
+
chunks.push(`${opening}${closing}`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const multiline = indentation.length > 0;
|
|
327
|
+
const localEvents = [{ type: "text", value: opening }];
|
|
328
|
+
if (multiline) {
|
|
329
|
+
localEvents.push({ type: "text", value: "\n" });
|
|
330
|
+
}
|
|
331
|
+
values.forEach((value, index) => {
|
|
332
|
+
if (multiline) {
|
|
333
|
+
localEvents.push({
|
|
334
|
+
type: "text",
|
|
335
|
+
value: indentation.repeat(depth + 1),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (node.type === "object") {
|
|
339
|
+
localEvents.push({
|
|
340
|
+
type: "text",
|
|
341
|
+
value: `${JSON.stringify(value.key)}${multiline ? ": " : ":"}`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
localEvents.push({
|
|
345
|
+
depth: depth + 1,
|
|
346
|
+
node: node.type === "array" ? value : value.value,
|
|
347
|
+
type: "node",
|
|
348
|
+
});
|
|
349
|
+
if (index < values.length - 1) {
|
|
350
|
+
localEvents.push({ type: "text", value: "," });
|
|
351
|
+
}
|
|
352
|
+
if (multiline) {
|
|
353
|
+
localEvents.push({ type: "text", value: "\n" });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
if (multiline) {
|
|
357
|
+
localEvents.push({ type: "text", value: indentation.repeat(depth) });
|
|
358
|
+
}
|
|
359
|
+
localEvents.push({ type: "text", value: closing });
|
|
360
|
+
|
|
361
|
+
for (let index = localEvents.length - 1; index >= 0; index -= 1) {
|
|
362
|
+
events.push(localEvents[index]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return chunks.join("");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function resolveEol(contents, setting) {
|
|
370
|
+
if (setting) {
|
|
371
|
+
return { cr: "\r", crlf: "\r\n", lf: "\n" }[setting];
|
|
372
|
+
}
|
|
373
|
+
return contents.match(/\r\n|\r|\n/u)?.[0] ?? "\n";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function formatParsedJson(
|
|
377
|
+
parsed,
|
|
378
|
+
contents,
|
|
379
|
+
{
|
|
380
|
+
arrays = false,
|
|
381
|
+
filePath = "",
|
|
382
|
+
indentationCount = 2,
|
|
383
|
+
lineEnding,
|
|
384
|
+
pack = false,
|
|
385
|
+
tabs = false,
|
|
386
|
+
} = {},
|
|
387
|
+
) {
|
|
388
|
+
if (
|
|
389
|
+
!Number.isInteger(indentationCount) ||
|
|
390
|
+
indentationCount < 0 ||
|
|
391
|
+
indentationCount > 10
|
|
392
|
+
) {
|
|
393
|
+
throw new RangeError(
|
|
394
|
+
"json-sort-cli/formatJson(): [THROW_ID_01] indentationCount must be an integer from 0 to 10",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (lineEnding !== undefined && !["cr", "crlf", "lf"].includes(lineEnding)) {
|
|
398
|
+
throw new TypeError(
|
|
399
|
+
'json-sort-cli/formatJson(): [THROW_ID_02] lineEnding must be "cr", "crlf" or "lf"',
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
sortTree(parsed, {
|
|
404
|
+
arrays,
|
|
405
|
+
packageJson: !pack && filePath.split(/[\\/]/u).at(-1) === "package.json",
|
|
406
|
+
});
|
|
407
|
+
const indentation = tabs
|
|
408
|
+
? "\t".repeat(indentationCount)
|
|
409
|
+
: " ".repeat(indentationCount);
|
|
410
|
+
const eol = resolveEol(contents, lineEnding);
|
|
411
|
+
const output = `${serialize(parsed, indentation).replaceAll("\n", eol)}${eol}`;
|
|
412
|
+
return {
|
|
413
|
+
changed: output !== contents,
|
|
414
|
+
output,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function formatJson(contents, options = {}) {
|
|
419
|
+
const decoded = decodeJson(contents);
|
|
420
|
+
return formatParsedJson(parseJson(decoded), decoded, options);
|
|
421
|
+
}
|
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,14 +62,12 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"
|
|
66
|
-
"codsen-glob": "^1.1.0",
|
|
67
|
-
"codsen-utils": "^1.9.0",
|
|
65
|
+
"codsen-glob": "^1.1.1",
|
|
68
66
|
"sort-package-json": "^2.15.1",
|
|
69
67
|
"update-notifier": "^7.3.1"
|
|
70
68
|
},
|
|
71
69
|
"devDependencies": {
|
|
72
|
-
"p-map": "^7.0.
|
|
70
|
+
"p-map": "^7.0.7"
|
|
73
71
|
},
|
|
74
72
|
"engines": {
|
|
75
73
|
"node": ">=18.20.8"
|