diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 05b373c47..b1797734f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 - name: Install Rust run: rustup update stable --no-self-update && rustup default stable - name: Install wasm32-unknown-unknown target diff --git a/.gitignore b/.gitignore index 558d4561e..60cbcb2d8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ package-lock.json /test/output /jco.sh /docs/book +/src/**/*.d.ts +/src/**/*.d.ts.map diff --git a/README.md b/README.md index eddb46203..986ef3188 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ Options include: * `--instantiation [mode]`: Instead of a direct ES module, export an `instantiate` function which can take the imports as an argument instead of implicit imports. The `instantiate` function can be async (with `--instantiation` or `--instantiation async`), or sync (with `--instantiation sync`). * `--valid-lifting-optimization`: Internal validations are removed assuming that core Wasm binaries are valid components, providing a minor output size saving. * `--tracing`: Emit tracing calls for all function entry and exits. +* `--no-namespaced-exports`: Removes exports of the type `test as "test:flavorful/test"` which are not compatible with typescript #### Bindgen Crate diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index c131ca774..6a90a5eff 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -66,6 +66,7 @@ impl Guest for JsComponentBindgenComponent { .unwrap_or(options.compat.unwrap_or(false)), valid_lifting_optimization: options.valid_lifting_optimization.unwrap_or(false), tracing: options.tracing.unwrap_or(false), + no_namespaced_exports: options.no_namespaced_exports.unwrap_or(false), }; let js_component_bindgen::Transpiled { @@ -131,6 +132,7 @@ impl Guest for JsComponentBindgenComponent { valid_lifting_optimization: false, base64_cutoff: 0, tracing: false, + no_namespaced_exports: 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 b8222751a..521cafc80 100644 --- a/crates/js-component-bindgen-component/wit/js-component-bindgen.wit +++ b/crates/js-component-bindgen-component/wit/js-component-bindgen.wit @@ -44,6 +44,10 @@ world js-component-bindgen { /// Whether or not to emit `tracing` calls on function entry/exit. tracing: option, + + /// Whether to generate namespaced exports like `foo as "local:package/foo"`. + /// These exports can break typescript builds. + no-namespaced-exports: option, } variant wit { diff --git a/crates/js-component-bindgen/src/esm_bindgen.rs b/crates/js-component-bindgen/src/esm_bindgen.rs index 605cae6ae..4b9900c15 100644 --- a/crates/js-component-bindgen/src/esm_bindgen.rs +++ b/crates/js-component-bindgen/src/esm_bindgen.rs @@ -2,7 +2,7 @@ use heck::ToLowerCamelCase; use crate::names::{maybe_quote_id, maybe_quote_member, LocalNames}; use crate::source::Source; -use crate::{uwrite, uwriteln}; +use crate::{uwrite, uwriteln, TranspileOpts}; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; @@ -131,6 +131,7 @@ impl EsmBindgen { output: &mut Source, instantiation: bool, local_names: &mut LocalNames, + opts: &TranspileOpts, ) { if self.exports.is_empty() { if instantiation { @@ -165,8 +166,6 @@ impl EsmBindgen { for (alias, export_name) in &self.export_aliases { if first { first = false - } else { - uwrite!(output, ", "); } let local_name = match &self.exports[export_name] { Binding::Local(local_name) => local_name, @@ -175,17 +174,18 @@ impl EsmBindgen { let alias_maybe_quoted = maybe_quote_id(alias); if local_name == alias_maybe_quoted { output.push_str(local_name); + uwrite!(output, ", "); } else if instantiation { uwrite!(output, "{alias_maybe_quoted}: {local_name}"); - } else { + uwrite!(output, ", "); + } else if !self.contains_js_quote(&alias_maybe_quoted) || !opts.no_namespaced_exports { uwrite!(output, "{local_name} as {alias_maybe_quoted}"); + uwrite!(output, ", "); } } for (export_name, export) in &self.exports { if first { first = false - } else { - uwrite!(output, ", "); } let local_name = match export { Binding::Local(local_name) => local_name, @@ -194,15 +194,24 @@ impl EsmBindgen { let export_name_maybe_quoted = maybe_quote_id(export_name); if local_name == export_name_maybe_quoted { output.push_str(local_name); + uwrite!(output, ", "); } else if instantiation { uwrite!(output, "{export_name_maybe_quoted}: {local_name}"); - } else { + uwrite!(output, ", "); + } else if !self.contains_js_quote(&export_name_maybe_quoted) + || !opts.no_namespaced_exports + { uwrite!(output, "{local_name} as {export_name_maybe_quoted}"); + uwrite!(output, ", "); } } uwrite!(output, " }}"); } + fn contains_js_quote(&self, js_string: &String) -> bool { + js_string.contains("\"") || js_string.contains("'") || js_string.contains("`") + } + fn binding_has_used(&self, binding: &Binding) -> bool { match binding { Binding::Interface(iface) => iface diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 519866ad9..0b269f2bc 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -66,6 +66,9 @@ pub struct TranspileOpts { pub valid_lifting_optimization: bool, /// Whether or not to emit `tracing` calls on function entry/exit. pub tracing: bool, + /// Whether to generate namespaced exports like `foo as "local:package/foo"`. + /// These exports can break typescript builds. + pub no_namespaced_exports: bool, } #[derive(Default, Clone, Debug)] @@ -152,7 +155,7 @@ pub fn transpile_bindgen( instantiator.gen.src.js(&instantiator.src.js); instantiator.gen.src.js_init(&instantiator.src.js_init); - instantiator.gen.finish_component(name, files); + instantiator.gen.finish_component(name, files, &opts); let exports = instantiator .gen @@ -173,7 +176,7 @@ pub fn transpile_bindgen( } impl<'a> JsBindgen<'a> { - fn finish_component(&mut self, name: &str, files: &mut Files) { + fn finish_component(&mut self, name: &str, files: &mut Files, opts: &TranspileOpts) { let mut output = source::Source::default(); let mut compilation_promises = source::Source::default(); @@ -262,6 +265,7 @@ impl<'a> JsBindgen<'a> { &mut self.src.js, self.opts.instantiation.is_some(), &mut self.local_names, + opts, ); uwrite!( output, @@ -312,6 +316,7 @@ impl<'a> JsBindgen<'a> { &mut output, self.opts.instantiation.is_some(), &mut self.local_names, + opts, ); } diff --git a/package.json b/package.json index 26521ef9e..23d75485c 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ }, "homepage": "https://github.com/bytecodealliance/jco#readme", "scripts": { - "build": "cargo xtask build workspace", + "build": "cargo xtask build workspace && npm run build:typescript", + "build:typescript": "tsc -p tsconfig.json", "build:types:preview2-shim": "cargo xtask generate wasi-types", "lint": "eslint -c eslintrc.cjs lib/**/*.js packages/*/lib/**/*.js", "test": "mocha -u tdd test/test.js --timeout 120000" diff --git a/src/cmd/transpile.js b/src/cmd/transpile.js index d3caa812f..3cc6dda48 100644 --- a/src/cmd/transpile.js +++ b/src/cmd/transpile.js @@ -74,6 +74,7 @@ async function wasm2Js (source) { * js?: bool, * minify?: bool, * optimize?: bool, + * noNamespacedExports?: bool, * optArgs?: string[], * }} opts * @returns {Promise<{ files: { [filename: string]: Uint8Array }, imports: string[], exports: [string, 'function' | 'instance'][] }>} @@ -120,7 +121,8 @@ export async function transpileComponent (component, opts = {}) { noNodejsCompat: !(opts.nodejsCompat ?? true), noTypescript: opts.noTypescript || false, tlaCompat: opts.tlaCompat ?? false, - base64Cutoff: opts.js ? 0 : opts.base64Cutoff ?? 5000 + base64Cutoff: opts.js ? 0 : opts.base64Cutoff ?? 5000, + noNamespacedExports: !opts.namespacedExports, }); let outDir = (opts.outDir ?? '').replace(/\\/g, '/'); diff --git a/src/jco.js b/src/jco.js index 4f5433808..0df9f976a 100755 --- a/src/jco.js +++ b/src/jco.js @@ -47,6 +47,7 @@ program.command('transpile') .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('--no-namespaced-exports', 'disable namespaced exports for typescript compatibility') .option('--', 'for --optimize, custom wasm-opt arguments (defaults to best size optimization)') .action(asyncAction(transpile)); diff --git a/test/cli.js b/test/cli.js index 14142c35a..c9155b940 100644 --- a/test/cli.js +++ b/test/cli.js @@ -2,7 +2,7 @@ import { deepStrictEqual, ok, strictEqual } from 'node:assert'; import { mkdir, readFile, rm, symlink, writeFile, mkdtemp } from 'node:fs/promises'; import { fileURLToPath, pathToFileURL } from 'url'; import { exec, jcoPath } from './helpers.js'; -import { tmpdir } from 'node:os'; +import { tmpdir, EOL } from 'node:os'; import { resolve, normalize, sep } from 'node:path'; export async function cliTest (fixtures) { @@ -89,6 +89,30 @@ export async function cliTest (fixtures) { ok(source.includes('export const $init')); }); + test('Transpile without namespaced exports', async () => { + const name = 'flavorful'; + const { stderr } = await exec(jcoPath, 'transpile', `test/fixtures/components/${name}.component.wasm`, '--no-namespaced-exports', '--no-wasi-shim', '--name', name, '-o', outDir); + strictEqual(stderr, ''); + const source = await readFile(`${outDir}/${name}.js`); + const finalLine = source.toString().split("\n").at(-1) + //Check final line is the export statement + ok(finalLine.toString().includes("export {")); + //Check that it does not contain the namespaced export + ok(!finalLine.toString().includes("test:flavorful/test")); + }); + + test('Transpile with namespaced exports', async () => { + const name = 'flavorful'; + const { stderr } = await exec(jcoPath, 'transpile', `test/fixtures/components/${name}.component.wasm`, '--no-wasi-shim', '--name', name, '-o', outDir); + strictEqual(stderr, ''); + const source = await readFile(`${outDir}/${name}.js`); + const finalLine = source.toString().split("\n").at(-1) + //Check final line is the export statement + ok(finalLine.toString().includes("export {")); + //Check that it does contain the namespaced export + ok(finalLine.toString().includes("test as 'test:flavorful/test'")); + }); + test('Optimize', async () => { const component = await readFile(`test/fixtures/components/flavorful.component.wasm`); const { stderr, stdout } = await exec(jcoPath, 'opt', `test/fixtures/components/flavorful.component.wasm`, '-o', outFile); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..567998b0d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["src/**/*"], + "exclude": ["node_modules", "src/cmd/**", "src/*.d.ts"], + "compilerOptions": { + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "skipLibCheck": true, + } +} diff --git a/xtask/src/build/jco.rs b/xtask/src/build/jco.rs index 27fc362f1..048691993 100644 --- a/xtask/src/build/jco.rs +++ b/xtask/src/build/jco.rs @@ -1,7 +1,5 @@ -use anyhow::Context; +use anyhow::{Context, Result}; use std::{collections::HashMap, fs, io::Write, path::PathBuf}; - -use anyhow::Result; use wit_component::ComponentEncoder; pub(crate) fn run() -> Result<()> { @@ -67,6 +65,7 @@ fn transpile(component_path: &str, name: String) -> Result<()> { tla_compat: true, valid_lifting_optimization: false, tracing: false, + no_namespaced_exports: true, }; 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 c02ed899f..b64d2614a 100644 --- a/xtask/src/generate/wasi_types.rs +++ b/xtask/src/generate/wasi_types.rs @@ -35,6 +35,7 @@ pub(crate) fn run() -> Result<()> { valid_lifting_optimization: false, base64_cutoff: 0, tracing: false, + no_namespaced_exports: true, }; let files = generate_types(name, resolve, world, opts)?;