json-sort-cli 4.2.1 → 4.2.3
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 +7 -0
- package/cli.js +376 -309
- package/json-formatter.js +421 -0
- package/package.json +1 -3
- package/process-files.js +169 -86
- package/json-file.js +0 -42
package/process-files.js
CHANGED
|
@@ -1,87 +1,144 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, realpath, rename, unlink } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { isPlainObject, resolveEolSetting } from "codsen-utils";
|
|
5
|
-
import sortPackageJson, { sortOrder } from "sort-package-json";
|
|
6
|
-
import { writeJson } from "./json-file.js";
|
|
5
|
+
import { decodeJson, formatParsedJson, parseJson } from "./json-formatter.js";
|
|
7
6
|
|
|
8
7
|
function asError(error) {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
let description;
|
|
13
|
+
try {
|
|
14
|
+
description = String(error);
|
|
15
|
+
} catch {
|
|
16
|
+
description = "Unknown non-Error value";
|
|
16
17
|
}
|
|
17
|
-
return
|
|
18
|
+
return new Error(description, { cause: error });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
const customSortOrder = sortOrder.filter(
|
|
25
|
-
(field) => !["lect", "tap"].includes(field),
|
|
26
|
-
);
|
|
27
|
-
const resolutionsIndex = customSortOrder.indexOf("resolutions");
|
|
28
|
-
customSortOrder.splice(resolutionsIndex, 0, "tap", "lect");
|
|
29
|
-
return sortPackageJson(object, { sortOrder: customSortOrder });
|
|
21
|
+
async function openWithoutFollowing(filePath) {
|
|
22
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
23
|
+
return open(filePath, constants.O_RDONLY | noFollow);
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
async function readFileSnapshot(filePath) {
|
|
27
|
+
const realPath = await realpath(filePath);
|
|
28
|
+
const pathStat = await lstat(filePath, { bigint: true });
|
|
29
|
+
if (pathStat.isSymbolicLink()) {
|
|
30
|
+
throw new Error(`Refusing to process symbolic link: ${filePath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const handle = await openWithoutFollowing(filePath);
|
|
34
|
+
try {
|
|
35
|
+
const stat = await handle.stat({ bigint: true });
|
|
36
|
+
if (!stat.isFile()) {
|
|
37
|
+
throw new Error(`Refusing to process a non-file: ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
if (!sameIdentity(pathStat, stat)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`The file changed while it was being opened: ${filePath}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if ((await realpath(filePath)) !== realPath) {
|
|
45
|
+
throw new Error(`The file route changed while opening: ${filePath}`);
|
|
46
|
+
}
|
|
47
|
+
return { contents: await handle.readFile(), realPath, stat };
|
|
48
|
+
} finally {
|
|
49
|
+
await handle.close();
|
|
37
50
|
}
|
|
38
|
-
return stringified.replaceAll(/(?:\r?\n)|\r/g, eolChar);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
function sameIdentity(left, right) {
|
|
54
|
+
return (
|
|
55
|
+
left.dev === right.dev &&
|
|
56
|
+
left.ino === right.ino &&
|
|
57
|
+
left.size === right.size &&
|
|
58
|
+
left.mtimeNs === right.mtimeNs &&
|
|
59
|
+
left.ctimeNs === right.ctimeNs
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function commitFile(filePath, output, snapshot) {
|
|
47
64
|
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
result.every((item) => typeof item === "string")
|
|
65
|
+
!snapshot?.stat ||
|
|
66
|
+
!Buffer.isBuffer(snapshot.contents) ||
|
|
67
|
+
typeof snapshot.realPath !== "string"
|
|
52
68
|
) {
|
|
53
|
-
|
|
54
|
-
} else if (!pack && path.basename(filePath) === "package.json") {
|
|
55
|
-
result = formatPackageJson(result);
|
|
69
|
+
throw new Error("Cannot safely commit without the original file snapshot");
|
|
56
70
|
}
|
|
57
71
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
const commitPath = snapshot.realPath;
|
|
73
|
+
const directory = path.dirname(commitPath);
|
|
74
|
+
const temporaryPath = path.join(
|
|
75
|
+
directory,
|
|
76
|
+
`.${path.basename(commitPath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
77
|
+
);
|
|
78
|
+
let temporaryHandle;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if ((await realpath(filePath)) !== snapshot.realPath) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"The file route changed after it was read; refusing to overwrite it",
|
|
84
|
+
);
|
|
62
85
|
}
|
|
86
|
+
temporaryHandle = await open(
|
|
87
|
+
temporaryPath,
|
|
88
|
+
"wx",
|
|
89
|
+
Number(snapshot.stat.mode & 0o7777n),
|
|
90
|
+
);
|
|
91
|
+
await temporaryHandle.writeFile(output, "utf8");
|
|
92
|
+
await temporaryHandle.chmod(Number(snapshot.stat.mode & 0o7777n));
|
|
93
|
+
|
|
94
|
+
const temporaryStat = await temporaryHandle.stat({ bigint: true });
|
|
63
95
|
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
current.length > 1 &&
|
|
67
|
-
current.every((item) => typeof item === "string")
|
|
96
|
+
temporaryStat.uid !== snapshot.stat.uid ||
|
|
97
|
+
temporaryStat.gid !== snapshot.stat.gid
|
|
68
98
|
) {
|
|
69
|
-
|
|
99
|
+
await temporaryHandle.chown(
|
|
100
|
+
Number(snapshot.stat.uid),
|
|
101
|
+
Number(snapshot.stat.gid),
|
|
102
|
+
);
|
|
70
103
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const stringified = normalizeLineEndings(
|
|
75
|
-
JSON.stringify(value, null, spaces),
|
|
76
|
-
eol,
|
|
77
|
-
);
|
|
104
|
+
await temporaryHandle.sync();
|
|
105
|
+
await temporaryHandle.close();
|
|
106
|
+
temporaryHandle = undefined;
|
|
78
107
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
const current = await readFileSnapshot(filePath);
|
|
109
|
+
if (
|
|
110
|
+
!sameIdentity(snapshot.stat, current.stat) ||
|
|
111
|
+
!snapshot.contents.equals(current.contents)
|
|
112
|
+
) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"The file changed after it was read; refusing to overwrite it",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ((await realpath(filePath)) !== snapshot.realPath) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"The file route changed before commit; refusing to overwrite it",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
await rename(temporaryPath, commitPath);
|
|
124
|
+
|
|
125
|
+
// Directory syncing is not supported by every platform. The file is
|
|
126
|
+
// already durable and atomically visible when this best-effort step runs.
|
|
127
|
+
try {
|
|
128
|
+
const directoryHandle = await open(directory, "r");
|
|
129
|
+
try {
|
|
130
|
+
await directoryHandle.sync();
|
|
131
|
+
} finally {
|
|
132
|
+
await directoryHandle.close();
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (temporaryHandle) {
|
|
137
|
+
await temporaryHandle.close().catch(() => {});
|
|
138
|
+
}
|
|
139
|
+
await unlink(temporaryPath).catch(() => {});
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
85
142
|
}
|
|
86
143
|
|
|
87
144
|
export class FileProcessingError extends Error {
|
|
@@ -113,33 +170,43 @@ async function processFile(
|
|
|
113
170
|
options,
|
|
114
171
|
{ parse, read, transform, write },
|
|
115
172
|
) {
|
|
116
|
-
let
|
|
173
|
+
let snapshot;
|
|
117
174
|
try {
|
|
118
|
-
|
|
175
|
+
const received = await read(filePath);
|
|
176
|
+
snapshot =
|
|
177
|
+
received && typeof received === "object" && "contents" in received
|
|
178
|
+
? received
|
|
179
|
+
: { contents: received };
|
|
119
180
|
} catch (error) {
|
|
120
181
|
throw new FileProcessingError(filePath, "read", error);
|
|
121
182
|
}
|
|
122
183
|
|
|
123
|
-
let
|
|
184
|
+
let decoded;
|
|
124
185
|
try {
|
|
125
|
-
|
|
186
|
+
decoded = decodeJson(snapshot.contents);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new FileProcessingError(filePath, "decode", error);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let parsed;
|
|
192
|
+
try {
|
|
193
|
+
parsed = (parse ?? parseJson)(decoded);
|
|
126
194
|
} catch (error) {
|
|
127
195
|
throw new FileProcessingError(filePath, "parse", error);
|
|
128
196
|
}
|
|
129
197
|
|
|
130
198
|
let prepared;
|
|
131
199
|
try {
|
|
132
|
-
prepared = transform
|
|
200
|
+
prepared = transform
|
|
201
|
+
? transform(parsed, { contents: decoded, filePath, ...options })
|
|
202
|
+
: formatParsedJson(parsed, decoded, { filePath, ...options });
|
|
133
203
|
} catch (error) {
|
|
134
204
|
throw new FileProcessingError(filePath, "transform", error);
|
|
135
205
|
}
|
|
136
206
|
|
|
137
|
-
if (!options.ci) {
|
|
207
|
+
if (!options.ci && prepared.changed) {
|
|
138
208
|
try {
|
|
139
|
-
await write(filePath, prepared.
|
|
140
|
-
EOL: prepared.eol,
|
|
141
|
-
spaces: prepared.spaces,
|
|
142
|
-
});
|
|
209
|
+
await write(filePath, prepared.output, snapshot);
|
|
143
210
|
} catch (error) {
|
|
144
211
|
throw new FileProcessingError(filePath, "write", error);
|
|
145
212
|
}
|
|
@@ -177,11 +244,11 @@ export async function processFiles(
|
|
|
177
244
|
lineEnding,
|
|
178
245
|
onOutcome = () => {},
|
|
179
246
|
pack = false,
|
|
180
|
-
parse
|
|
181
|
-
read =
|
|
247
|
+
parse,
|
|
248
|
+
read = readFileSnapshot,
|
|
182
249
|
tabs = false,
|
|
183
|
-
transform
|
|
184
|
-
write =
|
|
250
|
+
transform,
|
|
251
|
+
write = commitFile,
|
|
185
252
|
} = {},
|
|
186
253
|
) {
|
|
187
254
|
const failures = [];
|
|
@@ -195,21 +262,33 @@ export async function processFiles(
|
|
|
195
262
|
pack,
|
|
196
263
|
tabs,
|
|
197
264
|
};
|
|
265
|
+
let callbackError;
|
|
266
|
+
|
|
267
|
+
function report(outcome) {
|
|
268
|
+
try {
|
|
269
|
+
onOutcome(outcome);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
callbackError ??= asError(error);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
198
274
|
|
|
199
275
|
async function captureOutcome(filePath) {
|
|
276
|
+
let outcome;
|
|
200
277
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
278
|
+
outcome = {
|
|
279
|
+
...(await processFile(filePath, options, {
|
|
280
|
+
parse,
|
|
281
|
+
read,
|
|
282
|
+
transform,
|
|
283
|
+
write,
|
|
284
|
+
})),
|
|
285
|
+
status: "success",
|
|
286
|
+
};
|
|
208
287
|
} catch (error) {
|
|
209
288
|
if (!(error instanceof FileProcessingError)) {
|
|
210
289
|
throw error;
|
|
211
290
|
}
|
|
212
|
-
|
|
291
|
+
outcome = {
|
|
213
292
|
error: error.error,
|
|
214
293
|
failure: error,
|
|
215
294
|
path: error.path,
|
|
@@ -217,6 +296,8 @@ export async function processFiles(
|
|
|
217
296
|
status: "failure",
|
|
218
297
|
};
|
|
219
298
|
}
|
|
299
|
+
report(outcome);
|
|
300
|
+
return outcome;
|
|
220
301
|
}
|
|
221
302
|
|
|
222
303
|
const outcomes = ci
|
|
@@ -237,11 +318,13 @@ export async function processFiles(
|
|
|
237
318
|
unsorted.push(outcome.path);
|
|
238
319
|
}
|
|
239
320
|
}
|
|
240
|
-
onOutcome(outcome);
|
|
241
321
|
}
|
|
242
322
|
|
|
243
323
|
if (failures.length) {
|
|
244
324
|
throw new ProcessingError(failures, successful, unsorted);
|
|
245
325
|
}
|
|
326
|
+
if (callbackError) {
|
|
327
|
+
throw callbackError;
|
|
328
|
+
}
|
|
246
329
|
return { failures, successful, unsorted };
|
|
247
330
|
}
|
package/json-file.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
export async function readJson(file, options = {}) {
|
|
4
|
-
if (typeof options === "string") {
|
|
5
|
-
options = { encoding: options };
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const shouldThrow = "throws" in options ? options.throws : true;
|
|
9
|
-
let content = await readFile(file, options);
|
|
10
|
-
|
|
11
|
-
if (Buffer.isBuffer(content)) {
|
|
12
|
-
content = content.toString("utf8");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(content.replace(/^\uFEFF/, ""), options.reviver);
|
|
17
|
-
} catch (error) {
|
|
18
|
-
if (!shouldThrow) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
error.message = `${file}: ${error.message}`;
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function writeJson(file, value, options = {}) {
|
|
28
|
-
const { EOL = "\n", finalEOL = true, replacer = null, spaces } = options;
|
|
29
|
-
const stringified = JSON.stringify(value, replacer, spaces);
|
|
30
|
-
|
|
31
|
-
if (stringified === undefined) {
|
|
32
|
-
throw new TypeError(
|
|
33
|
-
`json-sort-cli/writeJson(): [THROW_ID_01] Converting ${typeof value} value to JSON is not supported`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await writeFile(
|
|
38
|
-
file,
|
|
39
|
-
stringified.replaceAll("\n", EOL) + (finalEOL ? EOL : ""),
|
|
40
|
-
options,
|
|
41
|
-
);
|
|
42
|
-
}
|