@bytecodealliance/jco
Version:
JavaScript tooling for working with WebAssembly Components
351 lines (327 loc) • 15.5 kB
JavaScript
import { program, Option } from "commander";
import { opt } from "./cmd/opt.js";
import { transpile } from "./cmd/transpile.js";
import { types, guestTypes } from "./cmd/types.js";
import { run as runCmd, serve as serveCmd } from "./cmd/run.js";
import {
parse,
print,
componentNew,
componentEmbed,
metadataAdd,
metadataShow,
componentWit,
} from "./cmd/wasm-tools.js";
import { componentize } from "./cmd/componentize.js";
import { styleText } from "./common.js";
program
.name("jco")
.description(
`${styleText("bold", "jco - WebAssembly JS Component Tools")}\n JS Component Transpilation Bindgen & Wasm Tools for JS`,
)
.usage("<command> [options]")
.enablePositionalOptions()
.version("1.16.1");
function myParseInt(value) {
return parseInt(value, 10);
}
/**
* Option parsing that allows for collecting repeated arguments
*
* @param {string} value - the new value that is added
* @param {string[]} previous - the existing list of values
*/
function collectOptions(value, previous) {
return previous.concat([value]);
}
/** Choices for features (enabling/disabling) */
const FEATURE_CHOICES = ["clocks", "http", "random", "stdio", "fetch-event", "all"];
program
.command("componentize")
.description("Create a component from a JavaScript module")
.usage("<js-source> --wit wit-world.wit -o <component-path>")
.argument("<js-source>", "JS source file to build")
.requiredOption("-w, --wit <path>", "WIT path to build with")
.option("-n, --world-name <name>", "WIT world to build")
.option("--aot", "Enable Weval AOT compilation of JS")
.option("--aot-min-stack-size-bytes <number>", "Set the min stack size to be used during AOT")
.option("--weval-bin <path>", "Specify a custom weval binary to use")
.addOption(new Option("-d, --disable <feature...>", "disable WASI features").choices(FEATURE_CHOICES))
.addOption(new Option("--enable <feature...>", "enable WASI features").choices(FEATURE_CHOICES))
.option("--debug", "configure jco for debug (e.g. disable all features except stdio, etc)")
.option("--preview2-adapter <adapter>", "provide a custom preview2 adapter path")
.option("--debug-starlingmonkey-build", "use a debug build of StarlingMonkey")
.option("--engine <path>", "use a specific StarlingMonkey build")
.requiredOption("-o, --out <out>", "output component file")
.option("--debug-bindings", "Output debug bindings and metadata during componentization (by default to stderr)")
.option("--debug-bindings-dir <dir>", "Directory to which to output generated bindings and metadata")
.option(
"--debug-binary",
"Output binary (without component metadata) created during componentization (by default to tmp dir)",
)
.option("--debug-binary-path <path>", "Path to which to write the generated debug binary")
.option("--debug-enable-wizer-logging", "Enable wizer call debugging")
.action(asyncAction(componentize));
program
.command("transpile")
.description("Transpile a WebAssembly Component to JS + core Wasm for JavaScript execution")
.usage("<component-path> -o <out-dir>")
.argument("<component-path>", "Wasm component binary filepath")
.option("--name <name>", "custom output name")
.requiredOption("-o, --out-dir <out-dir>", "output directory")
.option("-m, --minify", "minify the JS output (--optimize / opt cmd still required)")
.option("-O, --optimize", `optimize the component first (use -- and arguments to wasm-opt)`)
.option("--no-typescript", "do not output TypeScript .d.ts types")
.option(
"--valid-lifting-optimization",
"optimize component binary validations assuming all lifted values are valid",
)
.addOption(
new Option("--import-bindings [mode]", "bindings mode for imports")
.choices(["js", "optimized", "hybrid", "direct-optimized"])
.preset("js"),
)
.addOption(
new Option("--async-mode [mode]", "EXPERIMENTAL: use async imports and exports")
.choices(["sync", "jspi"])
.preset("sync"),
)
.option("--async-wasi-imports", "EXPERIMENTAL: async component imports from WASI interfaces")
.option("--async-wasi-exports", "EXPERIMENTAL: async component exports from WASI interfaces")
.option(
"--async-imports <imports...>",
'EXPERIMENTAL: async component imports (examples: "wasi:io/poll@0.2.0#poll", "wasi:io/poll#[method]pollable.block")',
)
.option(
"--async-exports <exports...>",
'EXPERIMENTAL: async component exports (examples: "wasi:cli/run@#run", "handle")',
)
.option("--tracing", "emit `tracing` calls on function entry/exit")
.option(
"-b, --base64-cutoff <bytes>",
"set the byte size under which core Wasm binaries will be inlined as base64",
myParseInt,
)
.option(
"--tla-compat",
"enables compatibility for JS environments without top-level await support via an async $init promise export",
)
.option("--no-nodejs-compat", "disables compatibility in Node.js without a fetch global")
.option("-M, --map <mappings...>", "specifier=./output custom mappings for the component imports")
.option("--no-wasi-shim", "disable automatic rewriting of WASI imports to use @bytecodealliance/preview2-shim")
.option("--stub", "generate a stub implementation from a WIT file directly")
.option("--js", "output JS instead of core WebAssembly")
.addOption(
new Option("-I, --instantiation [mode]", "output for custom module instantiation")
.choices(["async", "sync"])
.preset("async"),
)
.option("-q, --quiet", "disable output summary")
.option("--no-namespaced-exports", "disable namespaced exports for typescript compatibility")
.option("--multi-memory", "optimized output for Wasm multi-memory")
.allowExcessArguments(true)
.action(asyncAction(transpile));
program
.command("types")
.description("Generate types for the given WIT")
.usage("<wit-path> -o <out-dir>")
.argument("[<wit-path>]", "path to a WIT file or directory")
.option("--name <name>", "custom output name")
.option("-n, --world-name <world>", "WIT world to generate types for")
.option("-o, --out-dir <out-dir>", "output directory")
.option("--tla-compat", "generates types for the TLA compat output with an async $init promise export")
.addOption(
new Option("-I, --instantiation [mode]", "type output for custom module instantiation")
.choices(["async", "sync"])
.preset("async"),
)
.addOption(
new Option("--async-mode [mode]", "EXPERIMENTAL: use async imports and exports")
.choices(["sync", "jspi"])
.preset("sync"),
)
.option("--async-wasi-imports", "EXPERIMENTAL: async component imports from WASI interfaces")
.option("--async-wasi-exports", "EXPERIMENTAL: async component exports from WASI interfaces")
.option(
"--async-imports <imports...>",
'EXPERIMENTAL: async component imports (examples: "wasi:io/poll@0.2.0#poll", "wasi:io/poll#[method]pollable.block")',
)
.option(
"--async-exports <exports...>",
'EXPERIMENTAL: async component exports (examples: "ns:pkg/iface#func", "wasi:cli/run@0.2.3#run", "handle")',
)
.option("-q, --quiet", "disable output summary")
.option("--feature <feature>", "enable one specific WIT feature (repeatable)", collectOptions, [])
.option("--all-features", "enable all features")
.option("--wasm-opt-bin <path-to-wasm-opt>', 'wasm-opt binary path (default: 'binaryen/bin/wasm-opt')")
.action(asyncAction(types));
program
.command("guest-types")
.description("(experimental) Generate guest types for the given WIT")
.usage("<wit-path> -o <out-dir>")
.argument("[<wit-path>]", "path to a WIT file or directory")
.option("--name <name>", "custom output name")
.option("-n, --world-name <world>", "WIT world to generate types for")
.option("-o, --out-dir <out-dir>", "output directory")
.option("-q, --quiet", "disable output summary")
.option("--feature <feature>", "enable one specific WIT feature (repeatable)", collectOptions, [])
.option("--all-features", "enable all features")
.option(
"--async-exports <exports...>",
'EXPERIMENTAL: generate async exports (examples: "ns:pkg/iface#func", "wasi:cli/run@0.2.3#run", "handle")',
)
.addOption(
new Option("--async-mode [mode]", "EXPERIMENTAL: use async imports and exports")
.choices(["sync", "jspi"])
.preset("sync"),
)
.action(asyncAction(guestTypes));
program
.command("run")
.description("Run a WASI Command component")
.usage("<command.wasm> <args...>")
.helpOption(false)
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("<command>", "WASI command binary to run")
.option("--jco-dir <dir>", "Instead of using a temporary dir, set the output directory for the run command")
.option("--jco-trace", "Enable call tracing")
.option(
"--jco-import <module>",
"Custom module to import before the run executes to support custom environment setup",
)
.option("--jco-map <mappings...>", "specifier=./output custom mappings for the component imports")
.addOption(
new Option("--jco-import-bindings [mode]", "bindings mode for imports")
.choices(["js", "optimized", "hybrid", "direct-optimized"])
.preset("js"),
)
.argument("[args...]", "Any CLI arguments for the component")
.action(
asyncAction(async function run(cmd, args, opts, command) {
// specially only allow help option in first position
if (cmd === "--help" || cmd === "-h") {
command.help();
} else {
return runCmd(cmd, args, opts);
}
}),
);
program
.command("serve")
.description("Serve a WASI HTTP component")
.usage("<server.wasm> <args...>")
.helpOption(false)
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("<server>", "WASI server binary to run")
.option("--port <number>")
.option("--host <host>")
.option("--jco-dir <dir>", "Instead of using a temporary dir, set the output directory for the transpiled code")
.option("--jco-trace", "Enable call tracing")
.option(
"--jco-import <module>",
"Custom module to import before the server executes to support custom environment setup",
)
.addOption(
new Option("--jco-import-bindings [mode]", "bindings mode for imports")
.choices(["js", "optimized", "hybrid", "direct-optimized"])
.preset("js"),
)
.option("--jco-map <mappings...>", "specifier=./output custom mappings for the component imports")
.argument("[args...]", "Any CLI arguments for the component")
.action(
asyncAction(async function serve(cmd, args, opts, command) {
// specially only allow help option in first position
if (cmd === "--help" || cmd === "-h") {
command.help();
} else {
return serveCmd(cmd, args, opts);
}
}),
);
program
.command("opt")
.description("optimizes a Wasm component, including running wasm-opt Binaryen optimizations")
.usage("<component-file> -o <output-file> -- [wasm-opt arguments]")
.argument("<component-file>", "Wasm component binary filepath")
.requiredOption("-o, --output <output-file>", "optimized component output filepath")
.option("--asyncify", "runs Asyncify pass in wasm-opt")
.option("-q, --quiet")
.option("--wasm-opt-bin <path-to-wasm-opt>', 'wasm-opt binary path (default: 'binaryen/bin/wasm-opt')")
.allowExcessArguments(true)
.action(asyncAction(opt));
program
.command("wit")
.description("extract the WIT from a WebAssembly Component [wasm-tools component wit]")
.argument("<component-path>", "Wasm component binary filepath")
.option("-d, --document <name>", "WIT document of a package to print")
.option("-o, --output <output-file>", "WIT output file path")
.action(asyncAction(componentWit));
program
.command("print")
.description("print the WebAssembly WAT text for a binary file [wasm-tools print]")
.argument("<input>", "input file to process")
.option("-o, --output <output-file>", "output file path")
.action(asyncAction(print));
program
.command("metadata-show")
.description("extract the producer metadata for a Wasm binary [wasm-tools metadata show]")
.argument("[module]", "Wasm component or core module filepath")
.option("--json", "output component metadata as JSON")
.action(asyncAction(metadataShow));
program
.command("metadata-add")
.description("add producer metadata for a Wasm binary [wasm-tools metadata add]")
.argument("[module]", "Wasm component or core module filepath")
.requiredOption("-m, --metadata <metadata...>", "field=name[@version] producer metadata to add with the embedding")
.requiredOption("-o, --output <output-file>", "output binary path")
.action(asyncAction(metadataAdd));
program
.command("parse")
.description("parses the Wasm text format into a binary file [wasm-tools parse]")
.argument("<input>", "input file to process")
.requiredOption("-o, --output <output-file>", "output binary file path")
.action(asyncAction(parse));
program
.command("new")
.description("create a WebAssembly component adapted from a component core Wasm [wasm-tools component new]")
.argument("<core-module>", "Wasm core module filepath")
.requiredOption("-o, --output <output-file>", "Wasm component output filepath")
.option("--name <name>", "custom output name")
.option("--adapt <[NAME=]adapter...>", "component adapters to apply")
.option("--wasi-reactor", "build with the WASI Reactor adapter")
.option("--wasi-command", "build with the WASI Command adapter")
.action(asyncAction(componentNew));
program
.command("embed")
.description("embed the component typing section into a core Wasm module [wasm-tools component embed]")
.argument("[core-module]", "Wasm core module filepath")
.requiredOption("-o, --output <output-file>", "Wasm component output filepath")
.requiredOption("--wit <wit-world>", "WIT world path")
.option("--dummy", "generate a dummy component")
.option("--string-encoding <utf8|utf16|compact-utf16>", "set the component string encoding")
.option("-n, --world-name <world-name>", "world name to embed")
.option("-m, --metadata <metadata...>", "field=name[@version] producer metadata to add with the embedding")
.action(asyncAction(componentEmbed));
program.showHelpAfterError();
program.parse();
function asyncAction(cmd) {
return function () {
const args = [...arguments];
(async () => {
try {
await cmd.apply(null, args);
} catch (e) {
process.stdout.write(`(jco ${cmd.name}) `);
if (typeof e === "string") {
console.error(`${styleText(["red", "bold"], "Error")}: ${e}\n`);
} else {
console.error(e);
}
process.exit(1);
}
})();
};
}