Skip to content

Commit

Permalink
Type generation with WIT folder support (#426)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford authored Apr 26, 2024
1 parent d85c587 commit 5984e9c
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 25 deletions.
25 changes: 19 additions & 6 deletions crates/js-component-bindgen-component/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::PathBuf;

use anyhow::Result;
use anyhow::{Context, Result};
use js_component_bindgen::{
generate_types,
source::wit_parser::{Resolve, UnresolvedPackage},
Expand Down Expand Up @@ -117,17 +117,30 @@ impl Guest for JsComponentBindgenComponent {
opts: TypeGenerationOptions,
) -> Result<Vec<(String, Vec<u8>)>, String> {
let mut resolve = Resolve::default();
let pkg = match opts.wit {
let id = match opts.wit {
Wit::Source(source) => {
UnresolvedPackage::parse(&PathBuf::from(format!("{name}.wit")), &source)
.map_err(|e| e.to_string())?
let pkg = UnresolvedPackage::parse(&PathBuf::from(format!("{name}.wit")), &source)
.map_err(|e| e.to_string())?;
resolve.push(pkg).map_err(|e| e.to_string())?
}
Wit::Path(path) => {
UnresolvedPackage::parse_file(&PathBuf::from(path)).map_err(|e| e.to_string())?
let path = PathBuf::from(path);
if path.is_dir() {
resolve.push_dir(&path).map_err(|e| e.to_string())?.0
} else {
let contents = std::fs::read(&path)
.with_context(|| format!("failed to read file {path:?}"))
.map_err(|e| e.to_string())?;
let text = match std::str::from_utf8(&contents) {
Ok(s) => s,
Err(_) => return Err("input file is not valid utf-8".into()),
};
let pkg = UnresolvedPackage::parse(&path, text).map_err(|e| e.to_string())?;
resolve.push(pkg).map_err(|e| e.to_string())?
}
}
Wit::Binary(_) => todo!(),
};
let id = resolve.push(pkg).map_err(|e| e.to_string())?;

let world_string = opts.world.map(|world| world.to_string());
let world = resolve
Expand Down
2 changes: 1 addition & 1 deletion src/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { optimizeComponent as opt } from './cmd/opt.js';
export { transpileComponent as transpile } from './cmd/transpile.js';
export { transpileComponent as transpile, typesComponent as types } from './cmd/transpile.js';
import { $init, tools } from "../obj/wasm-tools.js";
const { print: printFn, parse: parseFn, componentWit: componentWitFn, componentNew: componentNewFn, componentEmbed: componentEmbedFn, metadataAdd: metadataAddFn, metadataShow: metadataShowFn } = tools;

Expand Down
70 changes: 55 additions & 15 deletions src/cmd/transpile.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $init, generate } from '../../obj/js-component-bindgen-component.js';
import { $init, generate, generateTypes } from '../../obj/js-component-bindgen-component.js';
import { writeFile } from 'node:fs/promises';
import { mkdir } from 'node:fs/promises';
import { dirname, extname, basename, resolve } from 'node:path';
Expand All @@ -14,6 +14,58 @@ import { platform } from 'node:process';

const isWindows = platform === 'win32';

export async function types (witPath, opts) {
const files = await typesComponent(witPath, opts);
await writeFiles(files, opts.quiet ? false : 'Generated Type Files');
}

/**
* @param {string} witPath
* @param {{
* name?: string,
* worldName?: string,
* instantiation?: 'async' | 'sync',
* tlaCompat?: bool,
* outDir?: string,
* }} opts
* @returns {Promise<{ [filename: string]: Uint8Array }>}
*/
export async function typesComponent (witPath, opts) {
await $init;
const name = opts.name || (opts.worldName
? opts.worldName.split(':').pop().split('/').pop()
: basename(witPath.slice(0, -extname(witPath).length || Infinity)));
let instantiation;
if (opts.instantiation) {
instantiation = { tag: opts.instantiation };
}
let outDir = (opts.outDir ?? '').replace(/\\/g, '/');
if (!outDir.endsWith('/') && outDir !== '')
outDir += '/';
return Object.fromEntries(generateTypes(name, {
wit: { tag: 'path', val: (isWindows ? '//?/' : '') + resolve(witPath) },
instantiation,
tlaCompat: opts.tlaCompat ?? false,
world: opts.worldName
}).map(([name, file]) => [`${outDir}${name}`, file]));
}

async function writeFiles(files, summaryTitle) {
await Promise.all(Object.entries(files).map(async ([name, file]) => {
await mkdir(dirname(name), { recursive: true });
await writeFile(name, file);
}));
if (!summaryTitle)
return;
console.log(c`
{bold ${summaryTitle}:}
${table(Object.entries(files).map(([name, source]) => [
c` - {italic ${name}} `,
c`{black.italic ${sizeStr(source.length)}}`
]))}`);
}

export async function transpile (componentPath, opts, program) {
const varIdx = program?.parent.rawArgs.indexOf('--');
if (varIdx !== undefined && varIdx !== -1)
Expand All @@ -37,20 +89,7 @@ export async function transpile (componentPath, opts, program) {
if (opts.map)
opts.map = Object.fromEntries(opts.map.map(mapping => mapping.split('=')));
const { files } = await transpileComponent(component, opts);

await Promise.all(Object.entries(files).map(async ([name, file]) => {
await mkdir(dirname(name), { recursive: true });
await writeFile(name, file);
}));

if (!opts.quiet)
console.log(c`
{bold Transpiled JS Component Files:}
${table(Object.entries(files).map(([name, source]) => [
c` - {italic ${name}} `,
c`{black.italic ${sizeStr(source.length)}}`
]))}`);
await writeFiles(files, opts.quiet ? false : 'Transpiled JS Component Files');
}

let WASM_2_JS;
Expand Down Expand Up @@ -91,6 +130,7 @@ async function wasm2Js (source) {
* minify?: bool,
* optimize?: bool,
* namespacedExports?: bool,
* outDir?: string,
* multiMemory?: bool,
* optArgs?: string[],
* }} opts
Expand Down
18 changes: 15 additions & 3 deletions 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 } from './cmd/transpile.js';
import { transpile, types } 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 @@ -48,12 +48,24 @@ program.command('transpile')
.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 logging')
.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')
.option('--', 'for --optimize, custom wasm-opt arguments (defaults to best size optimization)')
.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'))
.option('-q, --quiet', 'disable output summary')
.action(asyncAction(types));

program.command('run')
.description('Run a WASI Command component')
.usage('<command.wasm> <args...>')
Expand Down Expand Up @@ -158,7 +170,7 @@ program.command('embed')
.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('--world <world-name>', 'positional world path to embed')
.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));

Expand Down
10 changes: 10 additions & 0 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { deepStrictEqual, ok, strictEqual } from "node:assert";
import { readFile } from "node:fs/promises";
import {
transpile,
types,
opt,
print,
parse,
Expand Down Expand Up @@ -92,6 +93,15 @@ export async function apiTest(fixtures) {
ok(source.includes("'#testimport'"));
});

test('Type generation', async () => {
const files = await types('test/fixtures/wit', {
worldName: 'test:flavorful/flavorful',
});
strictEqual(Object.keys(files).length, 2);
strictEqual(Object.keys(files)[0], 'flavorful.d.ts');
ok(Buffer.from(files[Object.keys(files)[0]]).includes('export const test'));
});

test("Optimize", async () => {
const component = await readFile(
`test/fixtures/components/flavorful.component.wasm`
Expand Down
15 changes: 15 additions & 0 deletions test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ export async function cliTest(fixtures) {
);
});

test("Type generation", async () => {
const { stderr } = await exec(
jcoPath,
"types",
"test/fixtures/wit",
"--world-name",
"test:flavorful/flavorful",
"-o",
outDir
);
strictEqual(stderr, "");
const source = await readFile(`${outDir}/flavorful.d.ts`, "utf8");
ok(source.includes("export const test"));
});

test("TypeScript naming checks", async () => {
const { stderr } = await exec(
jcoPath,
Expand Down

0 comments on commit 5984e9c

Please sign in to comment.