// Helpers shared by the build, release, and maintenance scripts.import { execFileSync } from "node:child_process";import { createHash } from "node:crypto";import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";import { createServer } from "node:net";import { dirname, join, resolve } from "node:path";import { fileURLToPath, pathToFileURL } from "node:url"; export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); /** True when `moduleUrl` (pass `import.meta.url`) is the script Node was asked to run. */export function isMain(moduleUrl) { return Boolean(process.argv[1]) && moduleUrl === pathToFileURL(resolve(process.argv[1])).href;} /** A TCP port on 127.0.0.1 that was free a moment ago. */export function freePort() { return new Promise((resolve, reject) => { const server = createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const { port } = server.address(); server.close(() => resolve(port)); }); });} export const readJson = (path) => JSON.parse(readFileSync(path, "utf8")); /** Pretty-printed with a trailing newline, the shape every tracked JSON file uses. */export function writeJson(path, value) { writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);} export const sha256 = (data) => createHash("sha256").update(data).digest("hex"); /** Run a command with inherited stdio. */export function run(command, args, options = {}) { return execFileSync(command, args, { stdio: "inherit", ...options });} /** Run a command and return its stdout as text; stderr is captured into the error. */export function capture(command, args, options = {}) { return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], // Binary Git patches easily exceed Node's 1 MiB default. maxBuffer: 64 * 1024 * 1024, ...options, });} /** * Every regular file below `root`; [] when it is missing. Symbolic links are * skipped, not followed (unlike `readdirSync`'s `recursive` option): in a pnpm * or npm tree they point back at files that are already being visited. */export function walkFiles(root, files = []) { if (!existsSync(root)) return files; for (const entry of readdirSync(root, { withFileTypes: true })) { const path = join(root, entry.name); if (entry.isDirectory()) walkFiles(path, files); else if (entry.isFile()) files.push(path); } return files;} export const treeBytes = (root) => walkFiles(root).reduce((total, path) => total + statSync(path).size, 0); /** Delete a file or directory tree and return the bytes it held (0 when it is missing). */export function removePath(path) { if (!existsSync(path)) return 0; const bytes = statSync(path).isDirectory() ? treeBytes(path) : statSync(path).size; rmSync(path, { recursive: true, force: true }); return bytes;}
// Helpers shared by the build, release, and maintenance scripts.
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
/** True when `moduleUrl` (pass `import.meta.url`) is the script Node was asked to run. */
export function isMain(moduleUrl) {
return Boolean(process.argv[1]) && moduleUrl === pathToFileURL(resolve(process.argv[1])).href;
}
/** A TCP port on 127.0.0.1 that was free a moment ago. */
export function freePort() {
return new Promise((resolve, reject) => {
const server = createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
}
export const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
/** Pretty-printed with a trailing newline, the shape every tracked JSON file uses. */
export function writeJson(path, value) {
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}
export const sha256 = (data) => createHash("sha256").update(data).digest("hex");
/** Run a command with inherited stdio. */
export function run(command, args, options = {}) {
return execFileSync(command, args, { stdio: "inherit", ...options });
}
/** Run a command and return its stdout as text; stderr is captured into the error. */
export function capture(command, args, options = {}) {
return execFileSync(command, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
// Binary Git patches easily exceed Node's 1 MiB default.
maxBuffer: 64 * 1024 * 1024,
...options,
});
}
/**
* Every regular file below `root`; [] when it is missing. Symbolic links are
* skipped, not followed (unlike `readdirSync`'s `recursive` option): in a pnpm
* or npm tree they point back at files that are already being visited.
*/
export function walkFiles(root, files = []) {
if (!existsSync(root)) return files;
for (const entry of readdirSync(root, { withFileTypes: true })) {
const path = join(root, entry.name);
if (entry.isDirectory()) walkFiles(path, files);
else if (entry.isFile()) files.push(path);
}
return files;
}
export const treeBytes = (root) => walkFiles(root).reduce((total, path) => total + statSync(path).size, 0);
/** Delete a file or directory tree and return the bytes it held (0 when it is missing). */
export function removePath(path) {
if (!existsSync(path)) return 0;
const bytes = statSync(path).isDirectory() ? treeBytes(path) : statSync(path).size;
rmSync(path, { recursive: true, force: true });
return bytes;
}