@bytecodealliance/jco
Version:
JavaScript tooling for working with WebAssembly Components
165 lines (149 loc) • 4.73 kB
JavaScript
import { rm, mkdir, writeFile, symlink } from "node:fs/promises";
import { basename, resolve, extname } from "node:path";
import { spawn } from "node:child_process";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { getTmpDir, styleText } from "../common.js";
import { transpile } from "./transpile.js";
const DEFAULT_SERVE_HOST = "localhost";
export async function run(componentPath, args, opts) {
// Ensure that `args` is an array
args = [...args];
return runComponent(
componentPath,
args,
opts,
`
if (!mod.run || !mod.run.run) {
console.error('Not a valid command component to execute.');
process.exit(1);
}
try {
mod.run.run();
// for stdout flushing
await new Promise(resolve => setTimeout(resolve));
process.exit(0);
}
catch (e) {
console.error(e);
process.exit(1);
}
`,
);
}
export async function serve(componentPath, args, opts) {
let tryFindPort = false;
let { port, host } = opts;
if (port === undefined) {
tryFindPort = true;
port = "8000";
}
// Ensure that `args` is an array
args = [...args];
host = host ?? DEFAULT_SERVE_HOST;
return runComponent(
componentPath,
args,
opts,
`
import { HTTPServer } from '@bytecodealliance/preview2-shim/http';
const server = new HTTPServer(mod.incomingHandler);
let port = ${port};
${
tryFindPort
? `
while (true) {
try {
server.listen(port, ${JSON.stringify(host)});
break;
} catch (e) {
if (e.code !== 'EADDRINUSE')
throw e;
}
port++;
}
`
: `server.listen(port, ${JSON.stringify(host)})`
}
console.error(\`Server listening @ ${host}:${port}...\`);
`,
);
}
async function runComponent(componentPath, args, opts, executor) {
const jcoImport = opts.jcoImport ? resolve(opts.jcoImport) : null;
const name = basename(componentPath.slice(0, -extname(componentPath).length || Infinity));
const outDir = opts.jcoDir || (await getTmpDir());
if (opts.jcoDir) {
await mkdir(outDir, { recursive: true });
}
try {
try {
await transpile(componentPath, {
name,
quiet: true,
noTypescript: true,
wasiShim: true,
outDir,
tracing: opts.jcoTrace,
map: opts.jcoMap,
importBindings: opts.jcoImportBindings,
});
} catch (e) {
throw new Error("Unable to transpile command for execution", {
cause: e,
});
}
await writeFile(resolve(outDir, "package.json"), JSON.stringify({ type: "module" }));
let preview2ShimPath;
try {
preview2ShimPath = resolve(
fileURLToPath(import.meta.resolve("@bytecodealliance/preview2-shim")),
"../../../",
);
} catch (err) {
let msg = `${styleText(["red", "bold"], "error")} Failed to resolve ${styleText("bold", "@bytecodealliance/preview2-shim")}, ensure it is installed.`;
msg += `\nERROR:\n${err.toString()}`;
throw new Error(msg);
}
const modulesDir = resolve(outDir, "node_modules", "@bytecodealliance");
await mkdir(modulesDir, { recursive: true });
try {
await symlink(preview2ShimPath, resolve(modulesDir, "preview2-shim"), "dir");
} catch (e) {
if (e.code !== "EEXIST") {
throw e;
}
}
const runPath = resolve(outDir, "_run.js");
await writeFile(
runPath,
`
${jcoImport ? `import ${JSON.stringify(pathToFileURL(jcoImport))}` : ""}
import process from 'node:process';
try {
process.argv[1] = "${name}";
} catch {}
const mod = await import('./${name}.js');
${executor}
`,
);
const nodePath = process.env.JCO_RUN_PATH || process.argv[0];
process.exitCode = await new Promise((resolve, reject) => {
const cp = spawn(
nodePath,
[...(process.env.JCO_RUN_ARGS ? process.env.JCO_RUN_ARGS.split(" ") : []), runPath, ...args],
{ stdio: "inherit" },
);
cp.on("error", reject);
cp.on("exit", resolve);
});
} finally {
try {
if (!opts.jcoDir) {
await rm(outDir, { recursive: true });
}
} catch {
// empty
}
}
}