diff --git a/README.md b/README.md index 59e826bf..89cd8373 100644 --- a/README.md +++ b/README.md @@ -184,16 +184,29 @@ The default set of features includes: * `'random'`: Support for cryptographic random, depends on `wasi:random`. **When disabled, random numbers will still be generated but will not be random and instead fully deterministic.** * `'clocks'`: Support for clocks and duration polls, depends on `wasi:clocks` and `wasi:io`. **When disabled, using any timer functions like setTimeout or setInterval will panic.** * `'http'`: Support for outbound HTTP via the `fetch` global in JS. +* `'fetch-event'`: Support for `fetch` based incoming request handling (i.e. `addEventListener('fetch', ...)`) -Setting `disableFeatures: ['random', 'stdio', 'clocks', 'http']` will disable all features creating a minimal "pure component", that does not depend on any WASI APIs at all and just the target world. +Setting `disableFeatures: ['random', 'stdio', 'clocks', 'http', 'fetch-event']` will disable all features creating a minimal "pure component", that does not depend on any WASI APIs at all and just the target world. Note that pure components **will not report errors and will instead trap**, so that this should only be enabled after very careful testing. Note that features explicitly imported by the target world cannot be disabled - if you target a component to a world that imports `wasi:clocks`, then `disableFeatures: ['clocks']` will not be supported. +Note that depending on your component implementation, some features may be automatically disabled. For example, if using +`wasi:http/incoming-handler` manually, the `fetch-event` cannot be used. + ## Using StarlingMonkey's `fetch-event` -The StarlingMonkey engine provides the ability to use `fetchEvent` to handle calls to `wasi:http/incoming-handler@0.2.0#handle`. When targeting worlds that export `wasi:http/incoming-handler@0.2.0` the fetch event will automatically be attached. Alternatively, to override the fetch event with a custom handler, export an explicit `incomingHandler` or `'wasi:http/incoming-handler@0.2.0'` object. Using the `fetchEvent` requires enabling the `http` feature. +The StarlingMonkey engine provides the ability to use `fetchEvent` to handle calls to `wasi:http/incoming-handler@0.2.0#handle`. + +When targeting worlds that export `wasi:http/incoming-handler@0.2.0` the fetch event will automatically be attached. Alternatively, +to override the fetch event with a custom handler, export an explicit `incomingHandler` or `'wasi:http/incoming-handler@0.2.0'` +object. Using the `fetchEvent` requires enabling the `http` feature. + +> [!WARNING] +> If using `fetch-event`, ensure that you *do not* manually import (i.e. exporting `incomingHandler` from your ES module). +> +> Modules that export `incomingHandler` and have the `http` feature enabled are assumed to be using `wasi:http` manually. ## API @@ -206,7 +219,7 @@ export function componentize(opts: { debugBuild?: bool, engine?: string, preview2Adapter?: string, - disableFeatures?: ('stdio' | 'random' | 'clocks' | 'http')[], + disableFeatures?: ('stdio' | 'random' | 'clocks' | 'http', 'fetch-event')[], }): { component: Uint8Array, imports: string[] diff --git a/crates/spidermonkey-embedding-splicer/src/bin/splicer.rs b/crates/spidermonkey-embedding-splicer/src/bin/splicer.rs index 0c3226ab..81b219f9 100644 --- a/crates/spidermonkey-embedding-splicer/src/bin/splicer.rs +++ b/crates/spidermonkey-embedding-splicer/src/bin/splicer.rs @@ -50,6 +50,10 @@ enum Commands { #[arg(short, long)] out_dir: PathBuf, + /// Features to enable (multiple allowed) + #[arg(short, long)] + features: Vec, + /// Path to WIT file or directory #[arg(long)] wit_path: Option, @@ -99,6 +103,7 @@ fn main() -> Result<()> { Commands::SpliceBindings { input, out_dir, + features, wit_path, world_name, debug, @@ -113,8 +118,15 @@ fn main() -> Result<()> { let wit_path_str = wit_path.as_ref().map(|p| p.to_string_lossy().to_string()); - let result = splice::splice_bindings(engine, world_name, wit_path_str, None, debug) - .map_err(|e| anyhow::anyhow!(e))?; + let features = features + .iter() + .map(|v| Features::from_str(v)) + .collect::>>()?; + + let result = + splice::splice_bindings(engine, features, None, wit_path_str, world_name, debug) + .map_err(|e| anyhow::anyhow!(e))?; + fs::write(out_dir.join("component.wasm"), result.wasm).with_context(|| { format!( "Failed to write output file: {}", diff --git a/crates/spidermonkey-embedding-splicer/src/bindgen.rs b/crates/spidermonkey-embedding-splicer/src/bindgen.rs index 1ddc99e3..76208edc 100644 --- a/crates/spidermonkey-embedding-splicer/src/bindgen.rs +++ b/crates/spidermonkey-embedding-splicer/src/bindgen.rs @@ -19,6 +19,8 @@ use wit_component::StringEncoding; use wit_parser::abi::WasmType; use wit_parser::abi::{AbiVariant, WasmSignature}; +use crate::wit::exports::local::spidermonkey_embedding_splicer::splicer::Features; + use crate::{uwrite, uwriteln}; #[derive(Debug)] @@ -104,6 +106,9 @@ struct JsBindgen<'a> { resource_directions: HashMap, imported_resources: BTreeSet, + + /// Features that were enabled at the time of generation + features: &'a Vec, } #[derive(Debug)] @@ -131,7 +136,11 @@ pub struct Componentization { pub resource_imports: Vec<(String, String, u32)>, } -pub fn componentize_bindgen(resolve: &Resolve, wid: WorldId) -> Result { +pub fn componentize_bindgen( + resolve: &Resolve, + wid: WorldId, + features: &Vec, +) -> Result { let mut bindgen = JsBindgen { src: Source::default(), esm_bindgen: EsmBindgen::default(), @@ -146,6 +155,7 @@ pub fn componentize_bindgen(resolve: &Resolve, wid: WorldId) -> Result { intrinsic.name().to_string() } - fn exports_bindgen( - &mut self, - // guest_exports: &Option>, - // features: Vec, - ) -> Result<()> { + fn exports_bindgen(&mut self) -> Result<()> { 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(iface_name) = iface.name.as_ref() { - // let camel_case_name = iface_name.to_lower_camel_case(); - // if !guest_exports.contains(&camel_case_name) { - // // For wasi:http/incoming-handler, we treat it - // // as a special case as the engine already - // // provides the export using fetchEvent and that - // // can be used when an explicit export is not - // // defined by the guest content. - // if iface_name == "incoming-handler" - // || name.starts_with("wasi:http/incoming-handler@0.2.") - // { - // if !features.contains(&Features::Http) { - // bail!( - // "JS export definition for '{}' not found. Cannot use fetchEvent because the http feature is not enabled.", - // camel_case_name - // ) - // } - // continue; - // } - // bail!("Expected a JS export definition for '{}'", camel_case_name); - // } - // // 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) => { - // let camel_case_name = export_name.to_lower_camel_case(); - // if !guest_exports.contains(&camel_case_name) { - // bail!("Expected a JS export definition for '{}'", camel_case_name); - // } - // } - // } + + // Skip bindings generation for wasi:http/incoming-handler if the fetch-event + // feature was enabled. We expect that the built-in engine implementation will be used + if name.starts_with("wasi:http/incoming-handler@0.2.") + && self.features.contains(&Features::FetchEvent) + { + continue; + } match export { WorldItem::Function(func) => { diff --git a/crates/spidermonkey-embedding-splicer/src/splice.rs b/crates/spidermonkey-embedding-splicer/src/splice.rs index 60f35e66..f79d0285 100644 --- a/crates/spidermonkey-embedding-splicer/src/splice.rs +++ b/crates/spidermonkey-embedding-splicer/src/splice.rs @@ -18,7 +18,7 @@ use wit_parser::Resolve; use crate::bindgen::BindingItem; use crate::wit::exports::local::spidermonkey_embedding_splicer::splicer::{ - CoreFn, CoreTy, SpliceResult, + CoreFn, CoreTy, Features, SpliceResult, }; use crate::{bindgen, map_core_fn, parse_wit, splice}; @@ -32,9 +32,10 @@ use crate::{bindgen, map_core_fn, parse_wit, splice}; // } pub fn splice_bindings( engine: Vec, - world_name: Option, - wit_path: Option, + features: Vec, wit_source: Option, + wit_path: Option, + world_name: Option, debug: bool, ) -> Result { let (mut resolve, id) = match (wit_source, wit_path) { @@ -60,6 +61,7 @@ pub fn splice_bindings( wit_component::dummy_module(&resolve, world, wit_parser::ManglingAndAbi::Standard32); // merge the engine world with the target world, retaining the engine producers + let (engine_world, producers) = if let Ok(( _, Bindgen { @@ -113,7 +115,7 @@ pub fn splice_bindings( }; let componentized = - bindgen::componentize_bindgen(&resolve, world).map_err(|err| err.to_string())?; + bindgen::componentize_bindgen(&resolve, world, &features).map_err(|err| err.to_string())?; resolve .merge_worlds(engine_world, world) @@ -255,7 +257,8 @@ pub fn splice_bindings( )); } - let mut wasm = splice::splice(engine, imports, exports, debug).map_err(|e| format!("{e:?}"))?; + let mut wasm = + splice::splice(engine, imports, exports, features, debug).map_err(|e| format!("{e:?}"))?; // add the world section to the spliced wasm wasm.push(section.id()); @@ -337,6 +340,7 @@ pub fn splice( engine: Vec, imports: Vec<(String, String, CoreFn, Option)>, exports: Vec<(String, CoreFn)>, + features: Vec, debug: bool, ) -> Result> { let mut module = Module::parse(&engine, false).unwrap(); @@ -344,12 +348,17 @@ pub fn splice( // since StarlingMonkey implements CLI Run and incoming handler, // we override them only if the guest content exports those functions remove_if_exported_by_js(&mut module, &exports, "wasi:cli/run@0.2.", "#run"); - remove_if_exported_by_js( - &mut module, - &exports, - "wasi:http/incoming-handler@0.2.", - "#handle", - ); + + // if 'fetch-event' feature is disabled (default being default-enabled), + // remove the built-in incoming-handler which is built around it's use. + if !features.contains(&Features::FetchEvent) { + remove_if_exported_by_js( + &mut module, + &exports, + "wasi:http/incoming-handler@0.2.", + "#handle", + ); + } // we reencode the WASI world component data, so strip it out from the // custom section diff --git a/crates/spidermonkey-embedding-splicer/src/stub_wasi.rs b/crates/spidermonkey-embedding-splicer/src/stub_wasi.rs index 104c7f29..f980d242 100644 --- a/crates/spidermonkey-embedding-splicer/src/stub_wasi.rs +++ b/crates/spidermonkey-embedding-splicer/src/stub_wasi.rs @@ -139,7 +139,7 @@ pub fn stub_wasi( stub_stdio(&mut module)?; } - if !features.contains(&Features::Http) { + if !features.contains(&Features::Http) && !features.contains(&Features::FetchEvent) { stub_http(&mut module)?; } diff --git a/crates/spidermonkey-embedding-splicer/src/wit.rs b/crates/spidermonkey-embedding-splicer/src/wit.rs index 01dd2f70..47fc19f6 100644 --- a/crates/spidermonkey-embedding-splicer/src/wit.rs +++ b/crates/spidermonkey-embedding-splicer/src/wit.rs @@ -16,6 +16,7 @@ impl std::str::FromStr for Features { "clocks" => Ok(Features::Clocks), "random" => Ok(Features::Random), "http" => Ok(Features::Http), + "fetch-event" => Ok(Features::FetchEvent), _ => bail!("unrecognized feature string [{s}]"), } } diff --git a/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit b/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit index cdb56841..fdc25f79 100644 --- a/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit +++ b/crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit @@ -13,6 +13,7 @@ interface splicer { clocks, random, http, + fetch-event, } record core-fn { diff --git a/crates/splicer-component/src/lib.rs b/crates/splicer-component/src/lib.rs index 1cecc3bf..63c6123b 100644 --- a/crates/splicer-component/src/lib.rs +++ b/crates/splicer-component/src/lib.rs @@ -1,7 +1,7 @@ use spidermonkey_embedding_splicer::stub_wasi::stub_wasi; use spidermonkey_embedding_splicer::wit::{self, export}; -use spidermonkey_embedding_splicer::splice; use spidermonkey_embedding_splicer::wit::exports::local::spidermonkey_embedding_splicer::splicer::{Features, Guest, SpliceResult}; +use spidermonkey_embedding_splicer::splice; struct SpidermonkeyEmbeddingSplicerComponent; @@ -18,13 +18,13 @@ impl Guest for SpidermonkeyEmbeddingSplicerComponent { fn splice_bindings( engine: Vec, - _features: Vec, - world_name: Option, - wit_path: Option, + features: Vec, wit_source: Option, + wit_path: Option, + world_name: Option, debug: bool, ) -> Result { - splice::splice_bindings(engine, wit_source, wit_path, world_name, debug) + splice::splice_bindings(engine, features, wit_source, wit_path, world_name, debug) } } diff --git a/package-lock.json b/package-lock.json index c2fc8e9e..55eafcd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bytecodealliance/componentize-js", - "version": "0.18.3-rc.4", + "version": "0.18.3-rc.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bytecodealliance/componentize-js", - "version": "0.18.3-rc.4", + "version": "0.18.3-rc.5", "workspaces": [ "." ], @@ -14,7 +14,8 @@ "@bytecodealliance/jco": "^1.9.1", "@bytecodealliance/weval": "^0.3.3", "@bytecodealliance/wizer": "^7.0.5", - "es-module-lexer": "^1.6.0" + "es-module-lexer": "^1.6.0", + "oxc-parser": "^0.76.0" }, "bin": { "componentize-js": "src/cli.js" @@ -204,20 +205,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", - "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", + "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.1", + "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", "license": "MIT", "optional": true, "dependencies": { @@ -225,9 +226,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", - "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", + "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", "license": "MIT", "optional": true, "dependencies": { @@ -615,15 +616,264 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz", - "integrity": "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.3.1", - "@emnapi/runtime": "^1.3.1", - "@tybys/wasm-util": "^0.9.0" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.76.0.tgz", + "integrity": "sha512-1XJW/16CDmF5bHE7LAyPPmEEVnxSadDgdJz+xiLqBrmC4lfAeuAfRw3HlOygcPGr+AJsbD4Z5sFJMkwjbSZlQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.76.0.tgz", + "integrity": "sha512-yoQwSom8xsB+JdGsPUU0xxmxLKiF2kdlrK7I56WtGKZilixuBf/TmOwNYJYLRWkBoW5l2/pDZOhBm2luwmLiLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.76.0.tgz", + "integrity": "sha512-uRIopPLvr3pf2Xj7f5LKyCuqzIU6zOS+zEIR8UDYhcgJyZHnvBkfrYnfcztyIcrGdQehrFUi3uplmI09E7RdiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.76.0.tgz", + "integrity": "sha512-a0EOFvnOd2FqmDSvH6uWLROSlU6KV/JDKbsYDA/zRLyKcG6HCsmFnPsp8iV7/xr9WMbNgyJi6R5IMpePQlUq7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.76.0.tgz", + "integrity": "sha512-ikRYDHL3fOdZwfJKmcdqjlLgkeNZ3Ez0qM8wAev5zlHZ+lY/Ig7qG5SCqPlvuTu+nNQ6zrFFaKvvt69EBKXU/g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.76.0.tgz", + "integrity": "sha512-dtRv5J5MRCLR7x39K8ufIIW4svIc7gYFUaI0YFXmmeOBhK/K2t/CkguPnDroKtsmXIPHDRtmJ1JJYzNcgJl6Wg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.76.0.tgz", + "integrity": "sha512-IE4iiiggFH2snagQxHrY5bv6dDpRMMat+vdlMN/ibonA65eOmRLp8VLTXnDiNrcla/itJ1L9qGABHNKU+SnE8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.76.0.tgz", + "integrity": "sha512-wi9zQPMDHrBuRuT7Iurfidc9qlZh7cKa5vfYzOWNBCaqJdgxmNOFzvYen02wVUxSWGKhpiPHxrPX0jdRyJ8Npg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.76.0.tgz", + "integrity": "sha512-0tqqu1pqPee2lLGY8vtYlX1L415fFn89e0a3yp4q5N9f03j1rRs0R31qesTm3bt/UK8HYjECZ+56FCVPs2MEMQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.76.0.tgz", + "integrity": "sha512-y36Hh1a5TA+oIGtlc8lT7N9vdHXBlhBetQJW0p457KbiVQ7jF7AZkaPWhESkjHWAsTVKD2OjCa9ZqfaqhSI0FQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.76.0.tgz", + "integrity": "sha512-7/acaG9htovp3gp/J0kHgbItQTuHctl+rbqPPqZ9DRBYTz8iV8kv3QN8t8Or8i/hOmOjfZp9McDoSU1duoR4/A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.76.0.tgz", + "integrity": "sha512-AxFt0reY6Q2rfudABmMTFGR8tFFr58NlH2rRBQgcj+F+iEwgJ+jMwAPhXd2y1I2zaI8GspuahedUYQinqxWqjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.76.0.tgz", + "integrity": "sha512-wHdkHdhf6AWBoO8vs5cpoR6zEFY1rB+fXWtq6j/xb9j/lu1evlujRVMkh8IM/M/pOUIrNkna3nzST/mRImiveQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.76.0.tgz", + "integrity": "sha512-G7ZlEWcb2hNwCK3qalzqJoyB6HaTigQ/GEa7CU8sAJ/WwMdG/NnPqiC9IqpEAEy1ARSo4XMALfKbKNuqbSs5mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.76.0.tgz", + "integrity": "sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.76.0.tgz", + "integrity": "sha512-CH3THIrSViKal8yV/Wh3FK0pFhp40nzW1MUDCik9fNuid2D/7JJXKJnfFOAvMxInGXDlvmgT6ACAzrl47TqzkQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@pkgjs/parseargs": { @@ -638,9 +888,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2027,6 +2277,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oxc-parser": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.76.0.tgz", + "integrity": "sha512-l98B2e9evuhES7zN99rb1QGhbzx25829TJFaKi2j0ib3/K/G5z1FdGYz6HZkrU3U8jdH7v2FC8mX1j2l9JrOUg==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.76.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm64": "0.76.0", + "@oxc-parser/binding-darwin-arm64": "0.76.0", + "@oxc-parser/binding-darwin-x64": "0.76.0", + "@oxc-parser/binding-freebsd-x64": "0.76.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.76.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.76.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.76.0", + "@oxc-parser/binding-linux-arm64-musl": "0.76.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.76.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.76.0", + "@oxc-parser/binding-linux-x64-gnu": "0.76.0", + "@oxc-parser/binding-linux-x64-musl": "0.76.0", + "@oxc-parser/binding-wasm32-wasi": "0.76.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.76.0", + "@oxc-parser/binding-win32-x64-msvc": "0.76.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", diff --git a/package.json b/package.json index cd2560dc..3bd5cbe2 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "@bytecodealliance/jco": "^1.9.1", "@bytecodealliance/weval": "^0.3.3", "@bytecodealliance/wizer": "^7.0.5", - "es-module-lexer": "^1.6.0" + "es-module-lexer": "^1.6.0", + "oxc-parser": "^0.76.0" }, "types": "types.d.ts", "scripts": { diff --git a/src/componentize.js b/src/componentize.js index 164173f0..0b341802 100644 --- a/src/componentize.js +++ b/src/componentize.js @@ -10,6 +10,7 @@ import { readFile, writeFile, mkdir, rm, stat } from 'node:fs/promises'; import { rmSync, existsSync } from 'node:fs'; import { createHash } from 'node:crypto'; +import oxc from 'oxc-parser'; import wizer from '@bytecodealliance/wizer'; import getWeval from '@bytecodealliance/weval'; import { @@ -59,6 +60,9 @@ const DEFAULT_DEBUG_SETTINGS = { wizerLogging: false, }; +/** Features that are used by default if not explicitly disabled */ +const DEFAULT_FEATURES = ['stdio', 'random', 'clocks', 'http', 'fetch-event']; + export async function componentize( opts, _deprecatedWitWorldOrOpts = undefined, @@ -116,24 +120,36 @@ export async function componentize( // Determine the path to the StarlingMonkey binary const engine = getEnginePath(opts); - // We never disable a feature that is already in the target world usage - const features = []; - if (!disableFeatures.includes('stdio')) { - features.push('stdio'); - } - if (!disableFeatures.includes('random')) { - features.push('random'); + // Determine the default features that should be included + const features = new Set(); + for (let f of DEFAULT_FEATURES) { + if (!disableFeatures.includes(f)) { + features.add(f); + } } - if (!disableFeatures.includes('clocks')) { - features.push('clocks'); + + if (!jsSource && sourcePath) { + jsSource = await readFile(sourcePath, 'utf8'); } - if (!disableFeatures.includes('http')) { - features.push('http'); + const detectedExports = await detectKnownSourceExportNames( + sourceName, + jsSource, + ); + + // If there is an export of incomingHandler, there is likely to be a + // manual implementation of wasi:http/incoming-handler, so we should + // disable fetch-event + if (features.has('http') && detectedExports.has('incomingHandler')) { + console.error( + 'Detected `incomingHandler` export, disabling fetch-event...', + ); + features.delete('fetch-event'); } + // Splice the bindigns for the given WIT world into the engine WASM let { wasm, jsBindings, exports, imports } = splicer.spliceBindings( await readFile(engine), - features, + [...features], witWorld, maybeWindowsPath(witPath), worldName, @@ -204,7 +220,7 @@ export async function componentize( DEBUG: enableWizerLogging ? '1' : '', SOURCE_NAME: sourceName, EXPORT_CNT: exports.length.toString(), - FEATURE_CLOCKS: features.includes('clocks') ? '1' : '', + FEATURE_CLOCKS: features.has('clocks') ? '1' : '', }; for (const [idx, [export_name, expt]] of exports.entries()) { @@ -360,7 +376,7 @@ export async function componentize( // After wizening, stub out the wasi imports depending on what features are enabled const finalBin = splicer.stubWasi( bin, - features, + [...features], witWorld, maybeWindowsPath(witPath), worldName, @@ -483,13 +499,15 @@ function getEnginePath(opts) { /** Prepare a work directory for use with componentization */ async function prepWorkDir() { - const baseDir = maybeWindowsPath(join( - tmpdir(), - createHash('sha256') - .update(Math.random().toString()) - .digest('hex') - .slice(0, 12), - )); + const baseDir = maybeWindowsPath( + join( + tmpdir(), + createHash('sha256') + .update(Math.random().toString()) + .digest('hex') + .slice(0, 12), + ), + ); await mkdir(baseDir); const sourcesDir = maybeWindowsPath(join(baseDir, 'sources')); await mkdir(sourcesDir); @@ -583,3 +601,36 @@ async function handleCheckInitOutput( throw new Error(msg); } } + +/** + * Detect known exports that correspond to certain interfaces + * + * @param {string} filename - filename + * @param {string} code - JS source code + * @returns {Promise} A Promise that resolves to a list of string that represent unversioned interfaces + */ +async function detectKnownSourceExportNames(filename, code) { + if (!filename) { + throw new Error('missing filename'); + } + if (!code) { + throw new Error('missing JS code'); + } + + const names = new Set(); + + const results = await oxc.parseAsync(filename, code); + if (results.errors.length > 0) { + throw new Error( + `failed to parse JS source, encountered [${results.errors.length}] errors`, + ); + } + + for (const staticExport of results.module.staticExports) { + for (const entry of staticExport.entries) { + names.add(entry.exportName.name); + } + } + + return names; +} diff --git a/test/cases/fetch-event-server/source.js b/test/cases/fetch-event-server/source.js new file mode 100644 index 00000000..2b01332c --- /dev/null +++ b/test/cases/fetch-event-server/source.js @@ -0,0 +1,7 @@ +addEventListener("fetch", (event) => + event.respondWith( + (async () => { + return new Response("Hello World"); + })(), + ), +); diff --git a/test/cases/fetch-event-server/test.js b/test/cases/fetch-event-server/test.js new file mode 100644 index 00000000..57e37254 --- /dev/null +++ b/test/cases/fetch-event-server/test.js @@ -0,0 +1,22 @@ +import { strictEqual } from 'node:assert'; + +import { HTTPServer } from '@bytecodealliance/preview2-shim/http'; + +import { getRandomPort } from '../../util.js'; + +export const enableFeatures = ['http', 'fetch-event']; +export const worldName = 'test3'; + +export async function test(instance) { + const server = new HTTPServer(instance.incomingHandler); + let port = await getRandomPort(); + server.listen(port); + + try { + const resp = await fetch(`http://localhost:${port}`); + const text = await resp.text(); + strictEqual(text, 'Hello World'); + } finally { + server.stop(); + } +}