@bytecodealliance/jco
Version:
JavaScript tooling for working with WebAssembly Components
255 lines (232 loc) • 7.54 kB
JavaScript
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;
}