Skip to content

Commit fe90d2f

Browse files
authored
feat: add initial support for guest-types command (experimental) (bytecodealliance#528)
1 parent c8c04b4 commit fe90d2f

File tree

11 files changed

+74
-3
lines changed

11 files changed

+74
-3
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Commands:
6060
componentize [options] <js-source> Create a component from a JavaScript module
6161
transpile [options] <component-path> Transpile a WebAssembly Component to JS + core Wasm for JavaScript execution
6262
types [options] <wit-path> Generate types for the given WIT
63+
guest-types [options] <wit-path> (experimental) Generate guest types for the given WIT
6364
run [options] <command> [args...] Run a WASI Command component
6465
serve [options] <server> [args...] Serve a WASI HTTP component
6566
opt [options] <component-file> optimizes a Wasm component, including running wasm-opt Binaryen optimizations

crates/js-component-bindgen-component/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ impl Guest for JsComponentBindgenComponent {
7575
no_namespaced_exports: options.no_namespaced_exports.unwrap_or(false),
7676
multi_memory: options.multi_memory.unwrap_or(false),
7777
import_bindings: options.import_bindings.map(Into::into),
78+
guest: options.guest.unwrap_or(false),
7879
};
7980

8081
let js_component_bindgen::Transpiled {
@@ -160,6 +161,7 @@ impl Guest for JsComponentBindgenComponent {
160161
no_namespaced_exports: false,
161162
multi_memory: false,
162163
import_bindings: None,
164+
guest: opts.guest.unwrap_or(false),
163165
};
164166

165167
let files = generate_types(name, resolve, world, opts).map_err(|e| e.to_string())?;

crates/js-component-bindgen-component/wit/js-component-bindgen.wit

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ world js-component-bindgen {
5858
/// Whether to generate namespaced exports like `foo as "local:package/foo"`.
5959
/// These exports can break typescript builds.
6060
no-namespaced-exports: option<bool>,
61+
62+
/// Whether to generate module declarations like `declare module "local:package/foo" {...`.
63+
guest: option<bool>,
6164

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

96101
enum export-type {

crates/js-component-bindgen/src/transpile_bindgen.rs

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ pub struct TranspileOpts {
6868
/// Whether to output core Wasm utilizing multi-memory or to polyfill
6969
/// this handling.
7070
pub multi_memory: bool,
71+
/// Whether to generate types for a guest module using module declarations.
72+
pub guest: bool,
7173
}
7274

7375
#[derive(Default, Clone, Debug)]

crates/js-component-bindgen/src/ts_bindgen.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ struct TsBindgen {
2929
import_object: Source,
3030
/// TypeScript definitions which will become the export object
3131
export_object: Source,
32+
33+
/// Whether or not the types should be generated for a guest module
34+
guest: bool,
3235
}
3336

3437
/// Used to generate a `*.d.ts` file for each imported and exported interface for
@@ -59,6 +62,7 @@ pub fn ts_bindgen(
5962
local_names: LocalNames::default(),
6063
import_object: Source::default(),
6164
export_object: Source::default(),
65+
guest: opts.guest,
6266
};
6367

6468
let world = &resolve.worlds[id];
@@ -520,9 +524,15 @@ impl TsBindgen {
520524
return local_name;
521525
}
522526

527+
let module_or_namespace = if self.guest {
528+
format!("declare module '{id_name}' {{")
529+
} else {
530+
format!("export namespace {camel} {{")
531+
};
532+
523533
let mut gen = self.ts_interface(resolve, false);
524534

525-
uwriteln!(gen.src, "export namespace {camel} {{");
535+
uwriteln!(gen.src, "{module_or_namespace}");
526536
for (_, func) in resolve.interfaces[id].functions.iter() {
527537
// Ensure that the function the world item for stability guarantees and exclude if they do not match
528538
if !feature_gate_allowed(resolve, package, &func.stability, &func.name)

src/cmd/transpile.js

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export async function types (witPath, opts) {
1919
await writeFiles(files, opts.quiet ? false : 'Generated Type Files');
2020
}
2121

22+
export async function guestTypes (witPath, opts) {
23+
const files = await typesComponent(witPath, { ...opts, guest: true });
24+
await writeFiles(files, opts.quiet ? false : 'Generated Guest Typescript Definition Files (.d.ts)');
25+
}
26+
2227
/**
2328
* @param {string} witPath
2429
* @param {{
@@ -28,6 +33,7 @@ export async function types (witPath, opts) {
2833
* tlaCompat?: bool,
2934
* outDir?: string,
3035
* features?: string[] | 'all',
36+
* guest?: bool,
3137
* }} opts
3238
* @returns {Promise<{ [filename: string]: Uint8Array }>}
3339
*/
@@ -57,6 +63,7 @@ export async function typesComponent (witPath, opts) {
5763
tlaCompat: opts.tlaCompat ?? false,
5864
world: opts.worldName,
5965
features,
66+
guest: opts.guest ?? false,
6067
}).map(([name, file]) => [`${outDir}${name}`, file]));
6168
}
6269

src/jco.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22
import { program, Option } from 'commander';
33
import { opt } from './cmd/opt.js';
4-
import { transpile, types } from './cmd/transpile.js';
4+
import { transpile, types, guestTypes } from './cmd/transpile.js';
55
import { run as runCmd, serve as serveCmd } from './cmd/run.js';
66
import { parse, print, componentNew, componentEmbed, metadataAdd, metadataShow, componentWit } from './cmd/wasm-tools.js';
77
import { componentize } from './cmd/componentize.js';
@@ -81,6 +81,18 @@ program.command('types')
8181
.option('--all-features', 'enable all features')
8282
.action(asyncAction(types));
8383

84+
program.command('guest-types')
85+
.description('(experimental) Generate guest types for the given WIT')
86+
.usage('<wit-path> -o <out-dir>')
87+
.argument('<wit-path>', 'path to a WIT file or directory')
88+
.option('--name <name>', 'custom output name')
89+
.option('-n, --world-name <world>', 'WIT world to generate types for')
90+
.requiredOption('-o, --out-dir <out-dir>', 'output directory')
91+
.option('-q, --quiet', 'disable output summary')
92+
.option('--feature <feature>', 'enable one specific WIT feature (repeatable)', collectOptions, [])
93+
.option('--all-features', 'enable all features')
94+
.action(asyncAction(guestTypes));
95+
8496
program.command('run')
8597
.description('Run a WASI Command component')
8698
.usage('<command.wasm> <args...>')

test/api.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,20 @@ export async function apiTest(_fixtures) {
9898
});
9999
strictEqual(Object.keys(files).length, 2);
100100
strictEqual(Object.keys(files)[0], 'flavorful.d.ts');
101+
strictEqual(Object.keys(files)[1], 'interfaces/test-flavorful-test.d.ts');
101102
ok(Buffer.from(files[Object.keys(files)[0]]).includes('export const test'));
103+
ok(Buffer.from(files[Object.keys(files)[1]]).includes('export namespace TestFlavorfulTest {'));
102104
});
105+
106+
test('Type generation (declare imports)', async () => {
107+
const files = await types('test/fixtures/wit', {
108+
worldName: 'test:flavorful/flavorful',
109+
guest: true,
110+
});
111+
strictEqual(Object.keys(files).length, 2);
112+
strictEqual(Object.keys(files)[1], 'interfaces/test-flavorful-test.d.ts');
113+
ok(Buffer.from(files[Object.keys(files)[1]]).includes('declare module \'test:flavorful/test\' {'));
114+
})
103115

104116
test("Optimize", async () => {
105117
const component = await readFile(
@@ -108,7 +120,7 @@ export async function apiTest(_fixtures) {
108120
const { component: optimizedComponent } = await opt(component);
109121
ok(optimizedComponent.byteLength < component.byteLength);
110122
});
111-
123+
112124
test("Print & Parse", async () => {
113125
const component = await readFile(
114126
`test/fixtures/components/flavorful.component.wasm`

test/cli.js

+18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { execArgv, env } from "node:process";
33
import { deepStrictEqual, ok, strictEqual } from "node:assert";
44
import {
55
mkdir,
6+
readdir,
67
readFile,
78
rm,
89
symlink,
@@ -183,6 +184,8 @@ export async function cliTest(_fixtures) {
183184
strictEqual(stderr, "");
184185
const source = await readFile(`${outDir}/flavorful.d.ts`, "utf8");
185186
ok(source.includes("export const test"));
187+
const iface = await readFile(`${outDir}/interfaces/test-flavorful-test.d.ts`, "utf8");
188+
ok(iface.includes("export namespace TestFlavorfulTest {"));
186189
});
187190

188191
test("Type generation (specific features)", async () => {
@@ -243,6 +246,21 @@ export async function cliTest(_fixtures) {
243246
ok(source.includes("export function c(): void;"));
244247
});
245248

249+
test("Type generation (declare imports)", async () => {
250+
const { stderr } = await exec(
251+
jcoPath,
252+
"guest-types",
253+
"test/fixtures/wit",
254+
"--world-name",
255+
"test:flavorful/flavorful",
256+
"-o",
257+
outDir
258+
);
259+
strictEqual(stderr, "");
260+
const source = await readFile(`${outDir}/interfaces/test-flavorful-test.d.ts`, "utf8");
261+
ok(source.includes("declare module 'test:flavorful/test' {"));
262+
});
263+
246264
test("TypeScript naming checks", async () => {
247265
const { stderr } = await exec(
248266
jcoPath,

xtask/src/build/jco.rs

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ fn transpile(component_path: &str, name: String, optimize: bool) -> Result<()> {
8484
no_namespaced_exports: true,
8585
multi_memory: true,
8686
import_bindings: Some(BindingsMode::Js),
87+
guest: false,
8788
};
8889

8990
let transpiled = js_component_bindgen::transpile(&adapted_component, opts)?;

xtask/src/generate/wasi_types.rs

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub(crate) fn run() -> Result<()> {
3838
no_namespaced_exports: true,
3939
multi_memory: false,
4040
import_bindings: Some(BindingsMode::Js),
41+
guest: false,
4142
};
4243

4344
let files = generate_types(name, resolve, world, opts)?;

0 commit comments

Comments
 (0)