Skip to content

Commit 5984e9c

Browse files
authored
Type generation with WIT folder support (#426)
1 parent d85c587 commit 5984e9c

File tree

6 files changed

+115
-25
lines changed

6 files changed

+115
-25
lines changed

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

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::PathBuf;
22

3-
use anyhow::Result;
3+
use anyhow::{Context, Result};
44
use js_component_bindgen::{
55
generate_types,
66
source::wit_parser::{Resolve, UnresolvedPackage},
@@ -117,17 +117,30 @@ impl Guest for JsComponentBindgenComponent {
117117
opts: TypeGenerationOptions,
118118
) -> Result<Vec<(String, Vec<u8>)>, String> {
119119
let mut resolve = Resolve::default();
120-
let pkg = match opts.wit {
120+
let id = match opts.wit {
121121
Wit::Source(source) => {
122-
UnresolvedPackage::parse(&PathBuf::from(format!("{name}.wit")), &source)
123-
.map_err(|e| e.to_string())?
122+
let pkg = UnresolvedPackage::parse(&PathBuf::from(format!("{name}.wit")), &source)
123+
.map_err(|e| e.to_string())?;
124+
resolve.push(pkg).map_err(|e| e.to_string())?
124125
}
125126
Wit::Path(path) => {
126-
UnresolvedPackage::parse_file(&PathBuf::from(path)).map_err(|e| e.to_string())?
127+
let path = PathBuf::from(path);
128+
if path.is_dir() {
129+
resolve.push_dir(&path).map_err(|e| e.to_string())?.0
130+
} else {
131+
let contents = std::fs::read(&path)
132+
.with_context(|| format!("failed to read file {path:?}"))
133+
.map_err(|e| e.to_string())?;
134+
let text = match std::str::from_utf8(&contents) {
135+
Ok(s) => s,
136+
Err(_) => return Err("input file is not valid utf-8".into()),
137+
};
138+
let pkg = UnresolvedPackage::parse(&path, text).map_err(|e| e.to_string())?;
139+
resolve.push(pkg).map_err(|e| e.to_string())?
140+
}
127141
}
128142
Wit::Binary(_) => todo!(),
129143
};
130-
let id = resolve.push(pkg).map_err(|e| e.to_string())?;
131144

132145
let world_string = opts.world.map(|world| world.to_string());
133146
let world = resolve

src/api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { optimizeComponent as opt } from './cmd/opt.js';
2-
export { transpileComponent as transpile } from './cmd/transpile.js';
2+
export { transpileComponent as transpile, typesComponent as types } from './cmd/transpile.js';
33
import { $init, tools } from "../obj/wasm-tools.js";
44
const { print: printFn, parse: parseFn, componentWit: componentWitFn, componentNew: componentNewFn, componentEmbed: componentEmbedFn, metadataAdd: metadataAddFn, metadataShow: metadataShowFn } = tools;
55

src/cmd/transpile.js

+55-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $init, generate } from '../../obj/js-component-bindgen-component.js';
1+
import { $init, generate, generateTypes } from '../../obj/js-component-bindgen-component.js';
22
import { writeFile } from 'node:fs/promises';
33
import { mkdir } from 'node:fs/promises';
44
import { dirname, extname, basename, resolve } from 'node:path';
@@ -14,6 +14,58 @@ import { platform } from 'node:process';
1414

1515
const isWindows = platform === 'win32';
1616

17+
export async function types (witPath, opts) {
18+
const files = await typesComponent(witPath, opts);
19+
await writeFiles(files, opts.quiet ? false : 'Generated Type Files');
20+
}
21+
22+
/**
23+
* @param {string} witPath
24+
* @param {{
25+
* name?: string,
26+
* worldName?: string,
27+
* instantiation?: 'async' | 'sync',
28+
* tlaCompat?: bool,
29+
* outDir?: string,
30+
* }} opts
31+
* @returns {Promise<{ [filename: string]: Uint8Array }>}
32+
*/
33+
export async function typesComponent (witPath, opts) {
34+
await $init;
35+
const name = opts.name || (opts.worldName
36+
? opts.worldName.split(':').pop().split('/').pop()
37+
: basename(witPath.slice(0, -extname(witPath).length || Infinity)));
38+
let instantiation;
39+
if (opts.instantiation) {
40+
instantiation = { tag: opts.instantiation };
41+
}
42+
let outDir = (opts.outDir ?? '').replace(/\\/g, '/');
43+
if (!outDir.endsWith('/') && outDir !== '')
44+
outDir += '/';
45+
return Object.fromEntries(generateTypes(name, {
46+
wit: { tag: 'path', val: (isWindows ? '//?/' : '') + resolve(witPath) },
47+
instantiation,
48+
tlaCompat: opts.tlaCompat ?? false,
49+
world: opts.worldName
50+
}).map(([name, file]) => [`${outDir}${name}`, file]));
51+
}
52+
53+
async function writeFiles(files, summaryTitle) {
54+
await Promise.all(Object.entries(files).map(async ([name, file]) => {
55+
await mkdir(dirname(name), { recursive: true });
56+
await writeFile(name, file);
57+
}));
58+
if (!summaryTitle)
59+
return;
60+
console.log(c`
61+
{bold ${summaryTitle}:}
62+
63+
${table(Object.entries(files).map(([name, source]) => [
64+
c` - {italic ${name}} `,
65+
c`{black.italic ${sizeStr(source.length)}}`
66+
]))}`);
67+
}
68+
1769
export async function transpile (componentPath, opts, program) {
1870
const varIdx = program?.parent.rawArgs.indexOf('--');
1971
if (varIdx !== undefined && varIdx !== -1)
@@ -37,20 +89,7 @@ export async function transpile (componentPath, opts, program) {
3789
if (opts.map)
3890
opts.map = Object.fromEntries(opts.map.map(mapping => mapping.split('=')));
3991
const { files } = await transpileComponent(component, opts);
40-
41-
await Promise.all(Object.entries(files).map(async ([name, file]) => {
42-
await mkdir(dirname(name), { recursive: true });
43-
await writeFile(name, file);
44-
}));
45-
46-
if (!opts.quiet)
47-
console.log(c`
48-
{bold Transpiled JS Component Files:}
49-
50-
${table(Object.entries(files).map(([name, source]) => [
51-
c` - {italic ${name}} `,
52-
c`{black.italic ${sizeStr(source.length)}}`
53-
]))}`);
92+
await writeFiles(files, opts.quiet ? false : 'Transpiled JS Component Files');
5493
}
5594

5695
let WASM_2_JS;
@@ -91,6 +130,7 @@ async function wasm2Js (source) {
91130
* minify?: bool,
92131
* optimize?: bool,
93132
* namespacedExports?: bool,
133+
* outDir?: string,
94134
* multiMemory?: bool,
95135
* optArgs?: string[],
96136
* }} opts

src/jco.js

+15-3
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 } from './cmd/transpile.js';
4+
import { transpile, types } 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';
@@ -48,12 +48,24 @@ program.command('transpile')
4848
.option('--stub', 'generate a stub implementation from a WIT file directly')
4949
.option('--js', 'output JS instead of core WebAssembly')
5050
.addOption(new Option('-I, --instantiation [mode]', 'output for custom module instantiation').choices(['async', 'sync']).preset('async'))
51-
.option('-q, --quiet', 'disable logging')
51+
.option('-q, --quiet', 'disable output summary')
5252
.option('--no-namespaced-exports', 'disable namespaced exports for typescript compatibility')
5353
.option('--multi-memory', 'optimized output for Wasm multi-memory')
5454
.option('--', 'for --optimize, custom wasm-opt arguments (defaults to best size optimization)')
5555
.action(asyncAction(transpile));
5656

57+
program.command('types')
58+
.description('Generate types for the given WIT')
59+
.usage('<wit-path> -o <out-dir>')
60+
.argument('<wit-path>', 'path to a WIT file or directory')
61+
.option('--name <name>', 'custom output name')
62+
.option('-n, --world-name <world>', 'WIT world to generate types for')
63+
.requiredOption('-o, --out-dir <out-dir>', 'output directory')
64+
.option('--tla-compat', 'generates types for the TLA compat output with an async $init promise export')
65+
.addOption(new Option('-I, --instantiation [mode]', 'type output for custom module instantiation').choices(['async', 'sync']).preset('async'))
66+
.option('-q, --quiet', 'disable output summary')
67+
.action(asyncAction(types));
68+
5769
program.command('run')
5870
.description('Run a WASI Command component')
5971
.usage('<command.wasm> <args...>')
@@ -158,7 +170,7 @@ program.command('embed')
158170
.requiredOption('--wit <wit-world>', 'WIT world path')
159171
.option('--dummy', 'generate a dummy component')
160172
.option('--string-encoding <utf8|utf16|compact-utf16>', 'set the component string encoding')
161-
.option('--world <world-name>', 'positional world path to embed')
173+
.option('-n, --world-name <world-name>', 'world name to embed')
162174
.option('-m, --metadata <metadata...>', 'field=name[@version] producer metadata to add with the embedding')
163175
.action(asyncAction(componentEmbed));
164176

test/api.js

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { deepStrictEqual, ok, strictEqual } from "node:assert";
22
import { readFile } from "node:fs/promises";
33
import {
44
transpile,
5+
types,
56
opt,
67
print,
78
parse,
@@ -92,6 +93,15 @@ export async function apiTest(fixtures) {
9293
ok(source.includes("'#testimport'"));
9394
});
9495

96+
test('Type generation', async () => {
97+
const files = await types('test/fixtures/wit', {
98+
worldName: 'test:flavorful/flavorful',
99+
});
100+
strictEqual(Object.keys(files).length, 2);
101+
strictEqual(Object.keys(files)[0], 'flavorful.d.ts');
102+
ok(Buffer.from(files[Object.keys(files)[0]]).includes('export const test'));
103+
});
104+
95105
test("Optimize", async () => {
96106
const component = await readFile(
97107
`test/fixtures/components/flavorful.component.wasm`

test/cli.js

+15
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,21 @@ export async function cliTest(fixtures) {
180180
);
181181
});
182182

183+
test("Type generation", async () => {
184+
const { stderr } = await exec(
185+
jcoPath,
186+
"types",
187+
"test/fixtures/wit",
188+
"--world-name",
189+
"test:flavorful/flavorful",
190+
"-o",
191+
outDir
192+
);
193+
strictEqual(stderr, "");
194+
const source = await readFile(`${outDir}/flavorful.d.ts`, "utf8");
195+
ok(source.includes("export const test"));
196+
});
197+
183198
test("TypeScript naming checks", async () => {
184199
const { stderr } = await exec(
185200
jcoPath,

0 commit comments

Comments
 (0)