UNPKG

@bytecodealliance/jco

Version:

JavaScript tooling for working with WebAssembly Components

255 lines (232 loc) 7.54 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; }