-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·323 lines (296 loc) · 10.7 KB
/
Copy pathcli.js
File metadata and controls
executable file
·323 lines (296 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
#!/usr/bin/env node
// @ts-check
/// <reference path="./types.d.ts" />
/// <reference types="node" />
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import packageJson from "./package.json" with { type: "json" };
import { formatDistanceToNow } from "date-fns";
import { findThemeFiles } from "./lib/find-files.js";
import { processFile } from "./lib/process-file.js";
import { cacheKeyExists, cacheList, cacheClear } from "./lib/cache.js";
import { cacheSchemas } from "./lib/cache-schemas.js";
import { coerceIndent } from "./lib/coerce-indent.js";
import { schemaUrls } from "./defaults/schemas.js";
import { overrides as defaultOverrides } from "./defaults/overrides.js";
import { expansions as defaultExpansions } from "./defaults/expansions.js";
import { resolve } from "path";
import { readFile, writeFile } from "fs/promises";
import { realpathSync } from "fs";
import { fileURLToPath } from "url";
import fg from "fast-glob";
import chalk from "chalk";
import ora from "ora";
import prettyMilliseconds from "pretty-ms";
/**
* Writes the formatted JSON content to the specified file path.
*
* @param {string} fullPath - The absolute path to the file.
* @param {string} formatted - The formatted JSON string.
* @param {boolean} dryRun - If true, logs the output instead of writing it.
* @returns {Promise<void>}
*/
export async function writeOutput(fullPath, formatted, dryRun) {
if (dryRun) {
if (process.stdout.isTTY) {
console.log(`Dry run: would write to ${fullPath}`);
}
console.log(formatted);
} else {
await writeFile(fullPath, formatted);
}
}
/**
* Returns the appropriate chalk color function based on duration.
* @param {number} ms - The duration in milliseconds.
* @returns {import('chalk').Chalk}
*/
function getColorForMs(ms) {
if (ms < 300) return chalk.green;
if (ms < 1000) return chalk.yellow;
return chalk.red;
}
/**
* Formats a millisecond value into a human-readable string with color coding.
*
* - Green for fast (< 300ms)
* - Yellow for moderate (< 1000ms)
* - Red for slow (≥ 1000ms)
*
* Non-numeric parts (units like "ms", "s", etc.) are dimmed.
*
* @param {number} milliseconds - The duration in milliseconds.
* @returns {string} The formatted and colored string.
*/
function prettyLogMs(milliseconds) {
const pretty = prettyMilliseconds(milliseconds);
const dimmed = pretty.replace(/([^\d.]+)/g, chalk.dim("$1"));
const colorFn = getColorForMs(milliseconds);
return colorFn(dimmed);
}
/**
* @param {import('./types.d.ts').CliArgs} argv
*/
export async function main(argv) {
const startTime = process.hrtime.bigint();
const {
file: argFile,
indent,
overrides: argOverrides = [],
expansions: argExpansions = [],
noDefaultOverrides,
dryRun,
} = argv;
let filesToProcess = [];
if (argFile) {
filesToProcess = await fg(argFile);
if (filesToProcess.length === 0) {
console.error(`No files found matching pattern: ${argFile}`);
return;
}
} else {
filesToProcess = await findThemeFiles(process.cwd());
if (!filesToProcess.includes("theme.json")) {
console.error("No theme.json file found.");
return;
}
console.log(
`${chalk.bold("sort-wp-json")} found ${chalk.yellow(filesToProcess.length)} files:`,
);
}
const missingCacheKeys = new Set();
const overrides = noDefaultOverrides
? argOverrides
: [...defaultOverrides, ...argOverrides];
const expansions = [...defaultExpansions, ...argExpansions];
for (const file of filesToProcess) {
const filepath = resolve(file);
try {
const startTime = process.hrtime.bigint();
const rawFile = (await readFile(filepath, "utf8")).toString();
const originalJson = JSON.parse(rawFile);
const schemaUrl = originalJson["$schema"];
if (!missingCacheKeys.has(schemaUrl) && !cacheKeyExists(schemaUrl)) {
const spinner = ora({
text: chalk.gray(`Caching ${chalk.bold(schemaUrl)}...`),
color: "gray",
spinner: "growVertical",
}).start();
missingCacheKeys.add(schemaUrl);
await cacheSchemas(schemaUrl);
const endTime = Number(process.hrtime.bigint() - startTime) / 1_000_000; // Convert nanoseconds to milliseconds
spinner.stopAndPersist({
symbol: chalk.gray("●"),
text: chalk.gray(`Cached ${chalk.bold(schemaUrl)}`),
suffixText: prettyLogMs(endTime),
});
}
} catch (e) {} // Ignore errors, will be handled in processFile
const relPath = filepath.replace(process.cwd(), "").replace(/^\/*/, ""); // ironically, this is usually just pre-resolved `file`
const spinner = ora({
text: relPath,
spinner: "dots3",
}).start();
const result = await processFile(filepath, {
indent,
overrides,
expansions,
});
if (result.status === "success") {
writeOutput(result.fullPath, result.content, dryRun);
// const ms = prettyMilliseconds(result.duration);
spinner.succeed(relPath + " " + prettyLogMs(result.duration));
} else if (result.status === "skipped") {
spinner.warn(relPath + " " + chalk.yellow(`Skipped: ${result.reason}`));
} else {
spinner.fail(relPath + " " + chalk.red(`Error: ${result.reason}`));
}
}
const endTime = Number(process.hrtime.bigint() - startTime) / 1_000_000; // Convert nanoseconds to milliseconds
const itemLabel = filesToProcess.length === 1 ? "file" : "files";
console.log(
`${chalk.bold("sort-wp-json")} processed ${chalk.yellow(filesToProcess.length)} ${itemLabel} in ${prettyLogMs(endTime)}.`,
);
}
/* v8 ignore start */
if (fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
yargs(hideBin(process.argv))
.command(
"$0 [file]",
"Sort WordPress JSON files",
(yargs) => {
yargs
.parserConfiguration({
"combine-arrays": true,
})
.pkgConf("sort-wp-json")
.positional("file", {
type: "string",
normalize: true,
required: false,
describe: "File path or glob pattern",
})
.option("indent", {
type: "string",
alias: "t",
describe:
"Set indent to some number of spaces. 0 returns a condensed file. " +
"Indentation will be clamped between 0-10. " +
"Out of deference to WordPress conventions, output files will be " +
"indented with tabs by default or if no number is provided. " +
"Also accepts 'tab', 'tabs' and 'inherit' (default)",
default: "inherit",
coerce: coerceIndent,
})
.option("overrides", {
type: "array",
describe:
"A list of override keys like 'settings.color.custom' to force to the top. Force nodes to the bottom by prefixing their paths with an exclamation point like '!settings.color.duotone'",
default: [],
})
.option("no-overrides", {
type: "boolean",
describe:
"Disable overrides. Use with --overrides to specify only custom overrides",
default: false,
})
.option("expansions", {
type: "array",
describe:
"A list of expansion keys like 'settings.typography.fontSizes'. Collapse nodes by prefixing with an exclamation point like '!settings.color.palette'",
default: [],
})
.option("dry-run", {
alias: "n",
type: "boolean",
describe: "Perform a dry run without writing changes",
default: false,
});
},
async (argv) => {
return await main(/** @type {import('./types.d.ts').CliArgs} */ (argv));
},
)
.command(
"cache <subcommand>",
"Manage the schema cache: clear, refresh, or list cached files.",
(yargs) => {
yargs
.command(
"clear",
"Clear the cached schema files",
{},
async (argv) => {
cacheClear();
console.log("Schema cache cleared.");
},
)
.command(
"refresh",
"Refresh and rebuild the schema cache",
{},
async (argv) => {
console.log("Refreshing schema cache...");
const startTotalTime = process.hrtime.bigint();
for (const schema of schemaUrls) {
const startTime = process.hrtime.bigint();
const spinner = ora({
text: chalk.gray(`Caching ${chalk.bold(schema)}...`),
color: "gray",
spinner: "growVertical",
}).start();
await cacheSchemas(schema);
const endTime =
Number(process.hrtime.bigint() - startTime) / 1_000_000; // Convert nanoseconds to milliseconds
spinner.stopAndPersist({
symbol: chalk.gray("●"),
text: chalk.gray(`Cached ${chalk.bold(schema)}`),
suffixText: chalk.blue(prettyMilliseconds(endTime)),
});
}
const endTotalTime =
Number(process.hrtime.bigint() - startTotalTime) / 1_000_000; // Convert nanoseconds to milliseconds
console.log(
`Schema cache refreshed in ${chalk.blue(prettyMilliseconds(endTotalTime))}.`,
);
},
)
.command("list", "List the cached schema files", {}, (argv) => {
const info = cacheList();
const items = info.items
.sort((a, b) => {
if (!!a.expires != !!b.expires) {
return a.expires ? -1 : 1;
}
return a.expires
? a.expires - b.expires
: a.key.localeCompare(b.key);
})
.map(
(item) =>
item.key +
" " +
(item.expires
? chalk.blue(formatDistanceToNow(new Date(item.expires)))
: ""),
);
console.log("Schema Key", chalk.blue("expiration"));
console.log(items.join("\n"));
const itemLabel = info.count === 1 ? "file" : "files";
console.log(
`Schema Cache contains ${chalk.cyan(info.count)} ${itemLabel}.`,
);
console.log(`Cache directory: ${chalk.green(info.dir)}`);
})
.demandCommand(
1,
"You must specify a subcommand: clear, refresh, or list",
);
},
)
.demandCommand(0)
.help()
.showHelpOnFail(false)
.version(packageJson.version).argv;
}
/* v8 ignore stop */