UNPKG

@bytecodealliance/jco

Version:

JavaScript tooling for working with WebAssembly Components

351 lines (327 loc) 15.5 kB
#!/usr/bin/env node 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); } })(); }; }