rescript
Version:
ReScript toolchain
256 lines (233 loc) • 7.01 kB
JavaScript
// @ts-check
import * as child_process from "node:child_process";
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as asyncFs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { promisify } from "node:util";
import * as arg from "#cli/args";
const asyncExecFile = promisify(child_process.execFile);
const format_usage = `Usage: rescript format <options> [files]
\`rescript format\` formats the current directory
`;
/**
* @type {arg.stringref}
*/
const stdin = { val: "" };
/**
* @type {arg.boolref}
*/
const format = { val: false };
/**
* @type {arg.boolref}
*/
const check = { val: false };
/**
* @type{arg.specs}
*/
const specs = [
[
"-stdin",
{ kind: "String", data: { kind: "String_set", data: stdin } },
`[.res|.resi] Read the code from stdin and print
the formatted code to stdout in ReScript syntax`,
],
[
"-all",
{ kind: "Unit", data: { kind: "Unit_set", data: format } },
"Format the whole project ",
],
[
"-check",
{ kind: "Unit", data: { kind: "Unit_set", data: check } },
"Check formatting for file or the whole project. Use `-all` to check the whole project",
],
];
const formattedStdExtensions = [".res", ".resi"];
const formattedFileExtensions = [".res", ".resi"];
/**
*
* @param {string[]} extensions
*/
function hasExtension(extensions) {
/**
* @param {string} x
*/
const pred = x => extensions.some(ext => x.endsWith(ext));
return pred;
}
async function readStdin() {
const stream = process.stdin;
const chunks = [];
for await (const chunk of stream) chunks.push(chunk);
return Buffer.concat(chunks).toString("utf8");
}
const _numThreads = os.cpus().length;
/**
* Splits an array into smaller chunks of a specified size.
*
* @template T
* @param {T[]} array - The array to split into chunks.
* @param {number} chunkSize - The size of each chunk.
* @returns {T[][]} - An array of chunks, where each chunk is an array of type T.
*/
function chunkArray(array, chunkSize) {
/** @type {T[][]} */
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
/**
* @param {string[]} files
* @param {string} bsc_exe
* @param {(x: string) => boolean} isSupportedFile
* @param {boolean} checkFormatting
*/
async function formatFiles(files, bsc_exe, isSupportedFile, checkFormatting) {
const supportedFiles = files.filter(isSupportedFile);
const batchSize = 4 * os.cpus().length;
const batches = chunkArray(supportedFiles, batchSize);
let incorrectlyFormattedFiles = 0;
try {
for (const batch of batches) {
await Promise.all(
batch.map(async file => {
const flags = checkFormatting
? ["-format", file]
: ["-o", file, "-format", file];
const { stdout } = await asyncExecFile(bsc_exe, flags);
if (check.val) {
const original = await asyncFs.readFile(file, "utf-8");
if (original !== stdout) {
console.error("[format check]", file);
incorrectlyFormattedFiles++;
}
}
}),
);
}
} catch (err) {
console.error(err);
process.exit(2);
}
if (incorrectlyFormattedFiles > 0) {
if (incorrectlyFormattedFiles === 1) {
console.error("The file listed above needs formatting");
} else {
console.error(
`The ${incorrectlyFormattedFiles} files listed above need formatting`,
);
}
process.exit(3);
}
}
/**
* @param {string[]} argv
* @param {string} rescript_legacy_exe
* @param {string} bsc_exe
*/
export async function main(argv, rescript_legacy_exe, bsc_exe) {
const isSupportedFile = hasExtension(formattedFileExtensions);
const isSupportedStd = hasExtension(formattedStdExtensions);
try {
/**
* @type {string[]}
*/
let files = [];
arg.parse_exn(format_usage, argv, specs, xs => {
files = xs;
});
const format_project = format.val;
const use_stdin = stdin.val;
// Only -check arg
// Require: -all or path to a file
if (check.val && !format_project && files.length === 0) {
console.error(
"format check require path to a file or use `-all` to check the whole project",
);
process.exit(2);
}
if (format_project) {
if (use_stdin || files.length !== 0) {
console.error("format -all can not be in use with other flags");
process.exit(2);
}
// -all
// TODO: check the rest arguments
const output = child_process.spawnSync(
rescript_legacy_exe,
["info", "-list-files"],
{
encoding: "utf-8",
},
);
if (output.status !== 0) {
console.error(output.stdout);
console.error(output.stderr);
process.exit(2);
}
files = output.stdout.split("\n").map(x => x.trim());
await formatFiles(files, bsc_exe, isSupportedFile, check.val);
} else if (use_stdin) {
if (check.val) {
console.error("format -stdin cannot be used with -check flag");
process.exit(2);
}
if (isSupportedStd(use_stdin)) {
const randomHex = crypto.randomBytes(8).toString("hex");
const basename = path.basename(use_stdin);
const filename = path.join(
os.tmpdir(),
`rescript_${randomHex}${basename}`,
);
(async () => {
const content = await readStdin();
const fd = fs.openSync(filename, "wx", 0o600); // Avoid overwriting existing file
fs.writeFileSync(fd, content, "utf8");
fs.closeSync(fd);
process.addListener("exit", () => fs.unlinkSync(filename));
child_process.execFile(
bsc_exe,
["-format", filename],
(error, stdout, stderr) => {
if (error === null) {
process.stdout.write(stdout);
} else {
console.error(stderr);
process.exit(2);
}
},
);
})();
} else {
console.error(`Unsupported extension ${use_stdin}`);
console.error(`Supported extensions: ${formattedStdExtensions} `);
process.exit(2);
}
} else {
if (files.length === 0) {
// none of argumets set
// format the current directory
files = fs.readdirSync(process.cwd()).filter(isSupportedFile);
}
for (const file of files) {
if (!isSupportedStd(file)) {
console.error(`Don't know what do with ${file}`);
console.error(`Supported extensions: ${formattedFileExtensions}`);
process.exit(2);
}
}
await formatFiles(files, bsc_exe, isSupportedFile, check.val);
}
} catch (e) {
if (e instanceof arg.ArgError) {
console.error(e.message);
process.exit(2);
} else {
throw e;
}
}
}