Skip to content

Commit

Permalink
feat: add initial support for guest-types command (experimental) (b…
Browse files Browse the repository at this point in the history
  • Loading branch information
lachieh authored Dec 13, 2024
1 parent c8c04b4 commit fe90d2f
Show file tree
Hide file tree
Showing 11 changed files with 74 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Commands:
componentize [options] <js-source> Create a component from a JavaScript module
transpile [options] <component-path> Transpile a WebAssembly Component to JS + core Wasm for JavaScript execution
types [options] <wit-path> Generate types for the given WIT
guest-types [options] <wit-path> (experimental) Generate guest types for the given WIT
run [options] <command> [args...] Run a WASI Command component
serve [options] <server> [args...] Serve a WASI HTTP component
opt [options] <component-file> optimizes a Wasm component, including running wasm-opt Binaryen optimizations
Expand Down
2 changes: 2 additions & 0 deletions crates/js-component-bindgen-component/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>,

/// Whether to generate module declarations like `declare module "local:package/foo" {...`.
guest: option<bool>,

/// Whether to output core Wasm utilizing multi-memory or to polyfill
/// this handling.
Expand Down Expand Up @@ -91,6 +94,8 @@ world js-component-bindgen {
map: option<maps>,
/// Features that should be enabled as part of feature gating
features: option<enabled-feature-set>,
/// Whether to generate module declarations like `declare module "local:package/foo" {...`.
guest: option<bool>,
}

enum export-type {
Expand Down
2 changes: 2 additions & 0 deletions crates/js-component-bindgen/src/transpile_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
12 changes: 11 additions & 1 deletion crates/js-component-bindgen/src/ts_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/cmd/transpile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {{
Expand All @@ -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 }>}
*/
Expand Down Expand Up @@ -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]));
}

Expand Down
14 changes: 13 additions & 1 deletion src/jco.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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('<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...>')
Expand Down
14 changes: 13 additions & 1 deletion test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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`
Expand Down
18 changes: 18 additions & 0 deletions test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { execArgv, env } from "node:process";
import { deepStrictEqual, ok, strictEqual } from "node:assert";
import {
mkdir,
readdir,
readFile,
rm,
symlink,
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions xtask/src/build/jco.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
1 change: 1 addition & 0 deletions xtask/src/generate/wasi_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down

0 comments on commit fe90d2f

Please sign in to comment.