UNPKG

@bytecodealliance/jco

Version:

JavaScript tooling for working with WebAssembly Components

241 lines (218 loc) 7.47 kB
import { normalize, resolve, sep, dirname } from "node:path"; import { tmpdir } from "node:os"; import { readFile, writeFile, rm, mkdtemp, mkdir, stat } from "node:fs/promises"; import { spawn } from "node:child_process"; import { argv0 } from "node:process"; import { platform } from "node:process"; import * as nodeUtils from "node:util"; export const isWindows = platform === "win32"; export const ASYNC_WASI_IMPORTS = [ "wasi:io/poll#poll", "wasi:io/poll#[method]pollable.block", "wasi:io/streams#[method]input-stream.blocking-read", "wasi:io/streams#[method]input-stream.blocking-skip", "wasi:io/streams#[method]output-stream.blocking-flush", "wasi:io/streams#[method]output-stream.blocking-write-and-flush", "wasi:io/streams#[method]output-stream.blocking-write-zeroes-and-flush", "wasi:io/streams#[method]output-stream.blocking-splice", ]; export const ASYNC_WASI_EXPORTS = ["wasi:cli/run#run", "wasi:http/incoming-handler#handle"]; export const DEFAULT_ASYNC_MODE = "sync"; /** Path of WIT files by default when one is not specified */ export const DEFAULT_WIT_PATH = "./wit"; let _showSpinner = false; export function setShowSpinner(val) { _showSpinner = val; } export function getShowSpinner() { const showSpinner = _showSpinner; _showSpinner = false; return showSpinner; } export function sizeStr(num) { num /= 1024; if (num < 1000) { return `${fixedDigitDisplay(num, 4)} KiB`; } num /= 1024; if (num < 1000) { return `${fixedDigitDisplay(num, 4)} MiB`; } } export function fixedDigitDisplay(num, maxChars) { const significantDigits = String(num).split(".")[0].length; let str; if (significantDigits >= maxChars - 1) { str = String(Math.round(num)); } else { const decimalPlaces = maxChars - significantDigits - 1; const rounding = 10 ** decimalPlaces; str = String(Math.round(num * rounding) / rounding); } if (maxChars - str.length < 0) { return str; } return " ".repeat(maxChars - str.length) + str; } /** * Generate tabular output * * @param {string[][]} rows - data to put in rows * @param {string[]} align - alignment of columns * @returns string */ export function table(rows, align = []) { if (rows.length === 0) { return ""; } const colLens = rows.reduce( (maxLens, cur) => maxLens.map((len, i) => Math.max(len, cur[i].length)), rows[0].map((cell) => cell.length), ); let outTable = ""; for (const row of rows) { for (const [i, cell] of row.entries()) { if (align[i] === "right") { outTable += " ".repeat(colLens[i] - cell.length) + cell; } else { outTable += cell + " ".repeat(colLens[i] - cell.length); } } outTable += "\n"; } return outTable; } /** * Securely creates a temporary directory and returns its path. * * The new directory is created using `fsPromises.mkdtemp()`. * * @returns {Promise<string>} A `Promise` that resovles to a created temporary directory path */ export async function getTmpDir() { return await mkdtemp(normalize(tmpdir() + sep)); } /** * Read a given file, throwing a formatted error if one occurs * * @param {string} filePath - path to teh file to read * @param {encoding} encoding - file encoding * @returns {Promise<Buffer>} A promise that resolves to the contents of the file */ async function readFileCli(filePath, encoding) { try { return await readFile(filePath, encoding); } catch { throw `Unable to read file ${styleText("bold", filePath)}`; } } export { readFileCli as readFile }; /** * Spawn a command that processes a given wasm binary bytes with some * command. * * The command invocations that are generated by this function * take the following form: * * ``` * <cmd> <input wasm file> <...arguments> <output wasm file> * ``` * * @param {string} cmd - the command to run * @param {Buffer<ArrayBufferLike>} inputWasmBytes - bytes that of the input WebAssembly binary * @param {string[]} args - arguments to pass to the command (after the input file and before the output file) * @returns {Promise<Buffer<ArrayBufferLike>>} A `Promise` that resolves when the command has exited */ export async function spawnIOTmp(cmd, inputWasmBytes, args) { const tmpDir = await getTmpDir(); try { const inFile = resolve(tmpDir, "in.wasm"); let outFile = resolve(tmpDir, "out.wasm"); await writeFile(inFile, inputWasmBytes); const cp = spawn(argv0, [cmd, inFile, ...args, outFile], { stdio: "pipe", }); let stderr = ""; const p = new Promise((resolve, reject) => { cp.stderr.on("data", (data) => (stderr += data.toString())); cp.on("error", (e) => { reject(e); }); cp.on("exit", (code) => { if (code === 0) { resolve(); } else { reject(stderr); } }); }); await p; var output = await readFile(outFile); return output; } finally { await rm(tmpDir, { recursive: true }); } } /** * Given an object that has file names as keys and file contents as values, * write out the files to a their locations. * * This function also prints out the files that were written * * @param {Record<string, string>} files - object which contains files to be written out * @param {boolean} summaryTitle - whether to print the summary after writing out files * @returns {Promise<void>>} A `Promise` that resolves when the fiels are all written * */ export async function writeFiles(files, summaryTitle) { await Promise.all( Object.entries(files).map(async ([filePath, contents]) => { await mkdir(dirname(filePath), { recursive: true }); await writeFile(filePath, contents); }), ); if (!summaryTitle) { return; } let rows = Object.entries(files).map(([name, source]) => [ ` - ${styleText("italic", name)} `, `${styleText(["black", "italic"], sizeStr(source.length))}`, ]); console.log(` ${styleText("bold", summaryTitle + ":")} ${table(rows)}`); } /** * Resolve the deafult WIT path, given a possibly * * @param {string | undefined} [witPath] * @returns {Promise<string>} */ export async function resolveDefaultWITPath(witPath) { if (witPath) { return witPath; } // Use a default/standard current-folder WIT directory (wit) if we can find it const witDirExists = await stat(DEFAULT_WIT_PATH) .then((p) => p.isDirectory()) .catch(() => false); if (!witDirExists) { throw new Error("Failed to determine WIT directory, please specify WIT directory argument"); } witPath = resolve(DEFAULT_WIT_PATH); console.error(`no WIT directory specified, using detected WIT directory @ [${DEFAULT_WIT_PATH}]`); return witPath; } /** * Partial polyfill for 'node:util' `styleText()` * * @param {string | string[]} styles - styles to apply to the given text * @param {string} text - text that should be styled * @returns {string} The styled string */ export function styleText(styles, text) { if (nodeUtils.styleText) { return nodeUtils.styleText(styles, text); } return text; }