From fe90d2ff0af2fc43d42002373e3edb9f892d7f53 Mon Sep 17 00:00:00 2001 From: Lachlan Heywood Date: Fri, 13 Dec 2024 15:12:38 -0500 Subject: [PATCH] feat: add initial support for `guest-types` command (experimental) (#528) --- README.md | 1 + .../js-component-bindgen-component/src/lib.rs | 2 ++ .../wit/js-component-bindgen.wit | 5 +++++ .../src/transpile_bindgen.rs | 2 ++ crates/js-component-bindgen/src/ts_bindgen.rs | 12 +++++++++++- src/cmd/transpile.js | 7 +++++++ src/jco.js | 14 +++++++++++++- test/api.js | 14 +++++++++++++- test/cli.js | 18 ++++++++++++++++++ xtask/src/build/jco.rs | 1 + xtask/src/generate/wasi_types.rs | 1 + 11 files changed, 74 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0aca54bdd..1d08097c5 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Commands: componentize [options] Create a component from a JavaScript module transpile [options] Transpile a WebAssembly Component to JS + core Wasm for JavaScript execution types [options] Generate types for the given WIT + guest-types [options] (experimental) Generate guest types for the given WIT run [options] [args...] Run a WASI Command component serve [options] [args...] Serve a WASI HTTP component opt [options] optimizes a Wasm component, including running wasm-opt Binaryen optimizations diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index e16bd44dc..9535ad2e8 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -75,6 +75,7 @@ impl Guest for JsComponentBindgenComponent { no_namespaced_exports: options.no_namespaced_exports.unwrap_or(false), multi_memory: options.multi_memory.unwrap_or(false), import_bindings: options.import_bindings.map(Into::into), + guest: options.guest.unwrap_or(false), }; let js_component_bindgen::Transpiled { @@ -160,6 +161,7 @@ impl Guest for JsComponentBindgenComponent { no_namespaced_exports: false, multi_memory: false, import_bindings: None, + guest: opts.guest.unwrap_or(false), }; let files = generate_types(name, resolve, world, opts).map_err(|e| e.to_string())?; diff --git a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit index 631e5deef..13e36eb32 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -58,6 +58,9 @@ world js-component-bindgen { /// Whether to generate namespaced exports like `foo as "local:package/foo"`. /// These exports can break typescript builds. no-namespaced-exports: option, + + /// Whether to generate module declarations like `declare module "local:package/foo" {...`. + guest: option, /// Whether to output core Wasm utilizing multi-memory or to polyfill /// this handling. @@ -91,6 +94,8 @@ world js-component-bindgen { map: option, /// Features that should be enabled as part of feature gating features: option, + /// Whether to generate module declarations like `declare module "local:package/foo" {...`. + guest: option, } enum export-type { diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 7f13c6997..00cb6af61 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -68,6 +68,8 @@ pub struct TranspileOpts { /// Whether to output core Wasm utilizing multi-memory or to polyfill /// this handling. pub multi_memory: bool, + /// Whether to generate types for a guest module using module declarations. + pub guest: bool, } #[derive(Default, Clone, Debug)] diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index bd250fc92..8cda37a5f 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -29,6 +29,9 @@ struct TsBindgen { import_object: Source, /// TypeScript definitions which will become the export object export_object: Source, + + /// Whether or not the types should be generated for a guest module + guest: bool, } /// Used to generate a `*.d.ts` file for each imported and exported interface for @@ -59,6 +62,7 @@ pub fn ts_bindgen( local_names: LocalNames::default(), import_object: Source::default(), export_object: Source::default(), + guest: opts.guest, }; let world = &resolve.worlds[id]; @@ -520,9 +524,15 @@ impl TsBindgen { return local_name; } + let module_or_namespace = if self.guest { + format!("declare module '{id_name}' {{") + } else { + format!("export namespace {camel} {{") + }; + let mut gen = self.ts_interface(resolve, false); - uwriteln!(gen.src, "export namespace {camel} {{"); + uwriteln!(gen.src, "{module_or_namespace}"); for (_, func) in resolve.interfaces[id].functions.iter() { // Ensure that the function the world item for stability guarantees and exclude if they do not match if !feature_gate_allowed(resolve, package, &func.stability, &func.name) diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index 6a58dda17..1282be092 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -19,6 +19,11 @@ export async function types (witPath, opts) { await writeFiles(files, opts.quiet ? false : 'Generated Type Files'); } +export async function guestTypes (witPath, opts) { + const files = await typesComponent(witPath, { ...opts, guest: true }); + await writeFiles(files, opts.quiet ? false : 'Generated Guest Typescript Definition Files (.d.ts)'); +} + /** * @param {string} witPath * @param {{ @@ -28,6 +33,7 @@ export async function types (witPath, opts) { * tlaCompat?: bool, * outDir?: string, * features?: string[] | 'all', + * guest?: bool, * }} opts * @returns {Promise<{ [filename: string]: Uint8Array }>} */ @@ -57,6 +63,7 @@ export async function typesComponent (witPath, opts) { tlaCompat: opts.tlaCompat ?? false, world: opts.worldName, features, + guest: opts.guest ?? false, }).map(([name, file]) => [`${outDir}${name}`, file])); } diff --git a/src/jco.js b/src/jco.js index 99ef7cde6..ad7942a74 100755 --- a/src/jco.js +++ b/src/jco.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import { program, Option } from 'commander'; import { opt } from './cmd/opt.js'; -import { transpile, types } from './cmd/transpile.js'; +import { transpile, types, guestTypes } from './cmd/transpile.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'; @@ -81,6 +81,18 @@ program.command('types') .option('--all-features', 'enable all features') .action(asyncAction(types)); +program.command('guest-types') + .description('(experimental) Generate guest types for the given WIT') + .usage(' -o ') + .argument('', 'path to a WIT file or directory') + .option('--name ', 'custom output name') + .option('-n, --world-name ', 'WIT world to generate types for') + .requiredOption('-o, --out-dir ', 'output directory') + .option('-q, --quiet', 'disable output summary') + .option('--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(' ') diff --git a/test/api.js b/test/api.js index 57e9d4f0c..6428577af 100644 --- a/test/api.js +++ b/test/api.js @@ -98,8 +98,20 @@ export async function apiTest(_fixtures) { }); strictEqual(Object.keys(files).length, 2); strictEqual(Object.keys(files)[0], 'flavorful.d.ts'); + strictEqual(Object.keys(files)[1], 'interfaces/test-flavorful-test.d.ts'); ok(Buffer.from(files[Object.keys(files)[0]]).includes('export const test')); + ok(Buffer.from(files[Object.keys(files)[1]]).includes('export namespace TestFlavorfulTest {')); }); + + test('Type generation (declare imports)', async () => { + const files = await types('test/fixtures/wit', { + worldName: 'test:flavorful/flavorful', + guest: true, + }); + strictEqual(Object.keys(files).length, 2); + strictEqual(Object.keys(files)[1], 'interfaces/test-flavorful-test.d.ts'); + ok(Buffer.from(files[Object.keys(files)[1]]).includes('declare module \'test:flavorful/test\' {')); + }) test("Optimize", async () => { const component = await readFile( @@ -108,7 +120,7 @@ export async function apiTest(_fixtures) { const { component: optimizedComponent } = await opt(component); ok(optimizedComponent.byteLength < component.byteLength); }); - + test("Print & Parse", async () => { const component = await readFile( `test/fixtures/components/flavorful.component.wasm` diff --git a/test/cli.js b/test/cli.js index de4632f92..4e3e2af3b 100644 --- a/test/cli.js +++ b/test/cli.js @@ -3,6 +3,7 @@ import { execArgv, env } from "node:process"; import { deepStrictEqual, ok, strictEqual } from "node:assert"; import { mkdir, + readdir, readFile, rm, symlink, @@ -183,6 +184,8 @@ export async function cliTest(_fixtures) { strictEqual(stderr, ""); const source = await readFile(`${outDir}/flavorful.d.ts`, "utf8"); ok(source.includes("export const test")); + const iface = await readFile(`${outDir}/interfaces/test-flavorful-test.d.ts`, "utf8"); + ok(iface.includes("export namespace TestFlavorfulTest {")); }); test("Type generation (specific features)", async () => { @@ -243,6 +246,21 @@ export async function cliTest(_fixtures) { ok(source.includes("export function c(): void;")); }); + test("Type generation (declare imports)", async () => { + const { stderr } = await exec( + jcoPath, + "guest-types", + "test/fixtures/wit", + "--world-name", + "test:flavorful/flavorful", + "-o", + outDir + ); + strictEqual(stderr, ""); + const source = await readFile(`${outDir}/interfaces/test-flavorful-test.d.ts`, "utf8"); + ok(source.includes("declare module 'test:flavorful/test' {")); + }); + test("TypeScript naming checks", async () => { const { stderr } = await exec( jcoPath, diff --git a/xtask/src/build/jco.rs b/xtask/src/build/jco.rs index 8f2577eae..cdd53437e 100644 --- a/xtask/src/build/jco.rs +++ b/xtask/src/build/jco.rs @@ -84,6 +84,7 @@ fn transpile(component_path: &str, name: String, optimize: bool) -> Result<()> { no_namespaced_exports: true, multi_memory: true, import_bindings: Some(BindingsMode::Js), + guest: false, }; let transpiled = js_component_bindgen::transpile(&adapted_component, opts)?; diff --git a/xtask/src/generate/wasi_types.rs b/xtask/src/generate/wasi_types.rs index f4942d06c..12b34fa28 100644 --- a/xtask/src/generate/wasi_types.rs +++ b/xtask/src/generate/wasi_types.rs @@ -38,6 +38,7 @@ pub(crate) fn run() -> Result<()> { no_namespaced_exports: true, multi_memory: false, import_bindings: Some(BindingsMode::Js), + guest: false, }; let files = generate_types(name, resolve, world, opts)?;