diff --git a/crates/spidermonkey-embedding-splicer/src/bindgen.rs b/crates/spidermonkey-embedding-splicer/src/bindgen.rs index 11912fd0..0c7c559a 100644 --- a/crates/spidermonkey-embedding-splicer/src/bindgen.rs +++ b/crates/spidermonkey-embedding-splicer/src/bindgen.rs @@ -124,7 +124,13 @@ pub struct Componentization { pub resource_imports: Vec<(String, String, u32)>, } -pub fn componentize_bindgen(resolve: &Resolve, id: WorldId, name: &str) -> Componentization { +pub fn componentize_bindgen( + resolve: &Resolve, + id: WorldId, + name: &str, + guest_imports: &Vec, + guest_exports: &Vec, +) -> Componentization { let mut bindgen = JsBindgen { src: Source::default(), esm_bindgen: EsmBindgen::default(), @@ -147,9 +153,9 @@ pub fn componentize_bindgen(resolve: &Resolve, id: WorldId, name: &str) -> Compo .local_names .exclude_globals(Intrinsic::get_global_names()); - bindgen.imports_bindgen(); + bindgen.imports_bindgen(&guest_imports); - bindgen.exports_bindgen(); + bindgen.exports_bindgen(&guest_exports); bindgen.esm_bindgen.populate_export_aliases(); // consolidate import specifiers and generate wrappers @@ -363,9 +369,38 @@ impl JsBindgen<'_> { return intrinsic.name().to_string(); } - fn exports_bindgen(&mut self) { + fn exports_bindgen(&mut self, guest_exports: &Vec) { for (key, export) in &self.resolve.worlds[self.world].exports { let name = self.resolve.name_world_key(key); + + // Do not generate exports when the guest export is not implemented. + // We check both the full interface name - "ns:pkg@v/my-interface" and the + // aliased interface name "myInterface". All other names are always + // camel-case in the check. + match key { + WorldKey::Interface(iface) => { + if !guest_exports.contains(&name) { + let iface = &self.resolve.interfaces[*iface]; + if let Some(name) = iface.name.as_ref() { + let camel_case_name = name.to_lower_camel_case(); + if !guest_exports.contains(&camel_case_name) { + continue; + } + // TODO: move populate_export_aliases to a preprocessing + // step that doesn't require esm_bindgen, so that we can + // do alias deduping here as well. + } else { + continue; + } + } + } + WorldKey::Name(export_name) => { + if !guest_exports.contains(&export_name.to_lower_camel_case()) { + continue; + } + } + } + match export { WorldItem::Function(func) => { let local_name = self.local_names.create_once(&func.name).to_string(); @@ -451,9 +486,12 @@ impl JsBindgen<'_> { } } - fn imports_bindgen(&mut self) { + fn imports_bindgen(&mut self, guest_imports: &Vec) { for (key, impt) in &self.resolve.worlds[self.world].imports { let import_name = self.resolve.name_world_key(key); + if !guest_imports.contains(&import_name) { + continue; + } match &impt { WorldItem::Function(f) => { self.import_bindgen(import_name, f, false, None); diff --git a/crates/spidermonkey-embedding-splicer/src/lib.rs b/crates/spidermonkey-embedding-splicer/src/lib.rs index 390cc722..eefe3cf7 100644 --- a/crates/spidermonkey-embedding-splicer/src/lib.rs +++ b/crates/spidermonkey-embedding-splicer/src/lib.rs @@ -1,6 +1,9 @@ use anyhow::{bail, Context, Result}; use bindgen::BindingItem; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + vec, +}; mod bindgen; mod splice; @@ -110,6 +113,8 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent { wit_source: Option, wit_path: Option, world_name: Option, + mut guest_imports: Vec, + guest_exports: Vec, debug: bool, ) -> Result { let source_name = source_name.unwrap_or("source.js".to_string()); @@ -131,7 +136,6 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent { .map_err(|e| e.to_string())?; let mut wasm_bytes = wit_component::dummy_module(&resolve, world); - let componentized = bindgen::componentize_bindgen(&resolve, world, &source_name); // merge the engine world with the target world, retaining the engine producers let producers = if let Ok(( @@ -144,6 +148,11 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent { }, )) = decode(&engine) { + // merge the imports from the engine with the imports from the guest content. + for (k, _) in &engine_resolve.worlds[engine_world].imports { + guest_imports.push(engine_resolve.name_world_key(k)); + } + // we disable the engine run and incoming handler as we recreate these exports // when needed, so remove these from the world before initiating the merge let maybe_run = engine_resolve.worlds[engine_world] @@ -187,6 +196,14 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent { None }; + let componentized = bindgen::componentize_bindgen( + &resolve, + world, + &source_name, + &guest_imports, + &guest_exports, + ); + let encoded = wit_component::metadata::encode( &resolve, world, @@ -327,8 +344,8 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent { )); } - // println!("{:?}", &imports); // println!("{:?}", &componentized.imports); + // println!("{:?}", &componentized.resource_imports); // println!("{:?}", &exports); let mut wasm = splice::splice(engine, imports, exports, debug).map_err(|e| format!("{:?}", e))?; diff --git a/crates/spidermonkey-embedding-splicer/src/splice.rs b/crates/spidermonkey-embedding-splicer/src/splice.rs index 12c397f9..59f4fb47 100644 --- a/crates/spidermonkey-embedding-splicer/src/splice.rs +++ b/crates/spidermonkey-embedding-splicer/src/splice.rs @@ -38,20 +38,30 @@ pub fn splice( let mut module = config.parse(&engine)?; // since StarlingMonkey implements CLI Run and incoming handler, - // we override these in ComponentizeJS, removing them from the - // core function exports - if let Ok(run) = module.exports.get_func("wasi:cli/run@0.2.0#run") { - let expt = module.exports.get_exported_func(run).unwrap(); - module.exports.delete(expt.id()); - module.funcs.delete(run); + // we override them only if the guest content exports those functions + if exports + .iter() + .any(|(name, _)| name == "wasi:cli/run@0.2.0#run") + { + if let Ok(run) = module.exports.get_func("wasi:cli/run@0.2.0#run") { + let expt = module.exports.get_exported_func(run).unwrap(); + module.exports.delete(expt.id()); + module.funcs.delete(run); + } } - if let Ok(serve) = module - .exports - .get_func("wasi:http/incoming-handler@0.2.0#handle") + + if exports + .iter() + .any(|(name, _)| name == "wasi:http/incoming-handler@0.2.0#handle") { - let expt = module.exports.get_exported_func(serve).unwrap(); - module.exports.delete(expt.id()); - module.funcs.delete(serve); + if let Ok(serve) = module + .exports + .get_func("wasi:http/incoming-handler@0.2.0#handle") + { + let expt = module.exports.get_exported_func(serve).unwrap(); + module.exports.delete(expt.id()); + module.funcs.delete(serve); + } } // we reencode the WASI world component data, so strip it out from the diff --git a/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit b/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit index 08aa15d2..af851340 100644 --- a/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit +++ b/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit @@ -33,5 +33,5 @@ world spidermonkey-embedding-splicer { export stub-wasi: func(engine: list, features: list, wit-world: option, wit-path: option, world-name: option) -> result, string>; - export splice-bindings: func(source-name: option, spidermonkey-engine: list, wit-world: option, wit-path: option, world-name: option, debug: bool) -> result; + export splice-bindings: func(source-name: option, spidermonkey-engine: list, wit-world: option, wit-path: option, world-name: option, guest-imports: list, guest-exports: list, debug: bool) -> result; } diff --git a/src/componentize.js b/src/componentize.js index 63b11774..d424792d 100644 --- a/src/componentize.js +++ b/src/componentize.js @@ -47,12 +47,33 @@ export async function componentize(jsSource, witWorld, opts) { enableFeatures = [], } = opts || {}; + await lexerInit; + let jsImports = []; + let jsExports = []; + try { + [jsImports, jsExports] = parse(jsSource); + } catch { + // ignore parser errors - will show up as engine parse errors shortly + } + + let guestImports = [] + jsImports.map(k => { + guestImports.push(k.n) + }) + + let guestExports = [] + jsExports.map(k => { + guestExports.push(k.n) + }) + let { wasm, jsBindings, importWrappers, exports, imports } = spliceBindings( sourceName, await readFile(engine), witWorld, maybeWindowsPath(witPath), worldName, + guestImports, + guestExports, false ); @@ -103,13 +124,6 @@ export async function componentize(jsSource, witWorld, opts) { await writeFile(input, Buffer.from(wasm)); // rewrite the JS source import specifiers to reference import wrappers - await lexerInit; - let jsImports = []; - try { - [jsImports] = parse(jsSource); - } catch { - // ignore parser errors - will show up as engine parse errors shortly - } let source = '', curIdx = 0; for (const jsImpt of jsImports) { @@ -320,5 +334,6 @@ export async function componentize(jsSource, witWorld, opts) { return { component, imports, + exports: exports.map(([name]) => name) }; } diff --git a/test/cases/smoke/imports.js b/test/cases/smoke/imports.js deleted file mode 100644 index ab6e6c05..00000000 --- a/test/cases/smoke/imports.js +++ /dev/null @@ -1,3 +0,0 @@ -export function y () { - // console.log('y'); -} diff --git a/test/cases/smoke/source.js b/test/cases/smoke/source.js index bfe6bb59..24df9d8d 100644 --- a/test/cases/smoke/source.js +++ b/test/cases/smoke/source.js @@ -1,7 +1,3 @@ -import { y } from 'imports'; - -export const exports = { - hello () { +export function unusedHello () { return 'world (' + getNum('world') + ')'; - } -}; +} diff --git a/test/cases/smoke/test.js b/test/cases/smoke/test.js index e2dfc02c..17f43533 100644 --- a/test/cases/smoke/test.js +++ b/test/cases/smoke/test.js @@ -1,5 +1,7 @@ -import { ok } from 'node:assert'; +import { ok, strictEqual } from 'node:assert'; -export function test (instance) { +export function test (instance, { imports, exports }) { + strictEqual(imports.length, 0); + strictEqual(exports.length, 0); ok(instance); } diff --git a/test/test.js b/test/test.js index b8167465..bbbd4198 100644 --- a/test/test.js +++ b/test/test.js @@ -17,7 +17,7 @@ suite('Builtins', () => { disableFeatures, } = await import(`./builtins/${filename}`); - const { component, imports } = await componentize( + const { component } = await componentize( source, ` package local:runworld; @@ -141,14 +141,16 @@ suite('Bindings', () => { const test = await import(`./cases/${name}/test.js`); + let testArg; try { - const { component, imports } = await componentize(source, { + const { component, imports, exports } = await componentize(source, { sourceName: `${name}.js`, witWorld, witPath, worldName, - disableFeatures: isWasiTarget ? [] : ['random', 'clocks'] + disableFeatures: isWasiTarget ? [] : ['random', 'clocks', 'http', 'stdio'] }); + testArg = { imports, exports }; const map = { 'wasi:cli-base/*': '@bytecodealliance/preview2-shim/cli-base#*', @@ -200,7 +202,7 @@ suite('Bindings', () => { } throw e; } - await test.test(instance); + await test.test(instance, testArg); }); } });