UNPKG

@bytecodealliance/jco

Version:

JavaScript tooling for working with WebAssembly Components

462 lines (439 loc) 15.7 kB
#!/usr/bin/env node import c from 'chalk-template'; 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'; program .name('jco') .description( c`{bold jco - WebAssembly JS Component Tools}\n JS Component Transpilation Bindgen & Wasm Tools for JS` ) .usage('<command> [options]') .enablePositionalOptions() .version('1.13.3'); 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]); } 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(['clocks', 'http', 'random', 'stdio', 'fetch-event', 'all']) ) // .addOption(new Option('-e, --enable <feature...>', 'enable WASI features').choices(['http'])) .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') .requiredOption('-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: "wasi:cli/run@#run", "handle")' ) .option('-q, --quiet', 'disable output summary') .option( '--feature <feature>', 'enable one specific WIT feature (repeatable)', collectOptions, [] ) .option('--all-features', 'enable all features') .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') .requiredOption('-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') .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') .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(c`{red.bold Error}: ${e}\n`); } else { console.error(e); } process.exit(1); } })(); }; }