diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index e8333429..05d8a62d 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -144,6 +144,7 @@ pub struct ImportType { pub doc_comment: Option, pub instanceof_shim: String, pub extends: Vec, + pub polyfills: Vec, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] @@ -478,6 +479,7 @@ impl ImportType { shared::ImportType { name: self.js_name.clone(), instanceof_shim: self.instanceof_shim.clone(), + polyfills: self.polyfills.iter().map(|s| s.to_string()).collect(), } } } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 0b4fedd0..710a0b5c 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -21,6 +21,7 @@ mod closures; pub struct Context<'a> { pub globals: String, pub imports: String, + pub imports_post: String, pub footer: String, pub typescript: String, pub exposed_globals: HashSet<&'static str>, @@ -68,6 +69,7 @@ struct ClassField { pub struct SubContext<'a, 'b: 'a> { pub program: &'a shared::Program, pub cx: &'a mut Context<'b>, + pub polyfills: HashMap>, } const INITIAL_SLAB_VALUES: &[&str] = &["undefined", "null", "true", "false"]; @@ -469,12 +471,14 @@ impl<'a> Context<'a> { /* tslint:disable */\n\ {import_wasm}\n\ {imports}\n\ + {imports_post}\n\ {globals}\n\ {footer}", import_wasm = import_wasm, globals = self.globals, imports = self.imports, + imports_post = self.imports_post, footer = self.footer, ) }; @@ -1765,6 +1769,11 @@ impl<'a, 'b> SubContext<'a, 'b> { ) })?; } + for f in self.program.imports.iter() { + if let shared::ImportKind::Type(ty) = &f.kind { + self.register_polyfills(ty); + } + } for f in self.program.imports.iter() { self.generate_import(f)?; } @@ -2140,6 +2149,19 @@ impl<'a, 'b> SubContext<'a, 'b> { self.cx.typescript.push_str("}\n"); } + fn register_polyfills( + &mut self, + info: &shared::ImportType, + ) { + if info.polyfills.len() == 0 { + return + } + self.polyfills + .entry(info.name.to_string()) + .or_insert(Vec::new()) + .extend(info.polyfills.iter().cloned()); + } + fn import_name(&mut self, import: &shared::Import, item: &str) -> Result { // First up, imports don't work at all in `--no-modules` mode as we're // not sure how to import them. @@ -2153,6 +2175,30 @@ impl<'a, 'b> SubContext<'a, 'b> { } } + // Similar to `--no-modules`, only allow polyfills basically for web + // apis, shouldn't be necessary for things like npm packages or other + // imported items. + let polyfills = self.polyfills.get(item); + if let Some(polyfills) = polyfills { + assert!(polyfills.len() > 0); + + if let Some(module) = &import.module { + bail!( + "import of `{}` from `{}` has a polyfill of `{}` listed, but + polyfills aren't supported when importing from modules", + item, + module, + &polyfills[0], + ); + } + if let Some(ns) = &import.js_namespace { + bail!("import of `{}` through js namespace `{}` isn't supported \ + right now when it lists a polyfill", + item, + ns); + } + } + // Figure out what identifier we're importing from the module. If we've // got a namespace we use that, otherwise it's the name specified above. let name_to_import = import.js_namespace.as_ref().map(|s| &**s).unwrap_or(item); @@ -2171,6 +2217,7 @@ impl<'a, 'b> SubContext<'a, 'b> { let use_node_require = self.cx.use_node_require(); let imported_identifiers = &mut self.cx.imported_identifiers; let imports = &mut self.cx.imports; + let imports_post = &mut self.cx.imports_post; let identifier = self .cx .imported_names @@ -2193,8 +2240,33 @@ impl<'a, 'b> SubContext<'a, 'b> { name_to_import, name, module )); } + name + } else if let Some(polyfills) = polyfills { + imports_post.push_str("const l"); + imports_post.push_str(&name); + imports_post.push_str(" = "); + switch(imports_post, &name, polyfills); + imports_post.push_str(";\n"); + + fn switch(dst: &mut String, name: &str, left: &[String]) { + if left.len() == 0 { + return dst.push_str(name); + } + dst.push_str("(typeof "); + dst.push_str(name); + dst.push_str(" == 'undefined' ? "); + match left.len() { + 1 => dst.push_str(&left[0]), + _ => switch(dst, &left[0], &left[1..]), + } + dst.push_str(" : "); + dst.push_str(name); + dst.push_str(")"); + } + format!("l{}", name) + } else { + name } - name }); // If there's a namespace we didn't actually import `item` but rather diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index a5441f26..59fb56f8 100644 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -189,6 +189,7 @@ impl Bindgen { let mut cx = js::Context { globals: String::new(), imports: String::new(), + imports_post: String::new(), footer: String::new(), typescript: format!("/* tslint:disable */\n"), exposed_globals: Default::default(), @@ -208,6 +209,7 @@ impl Bindgen { js::SubContext { program, cx: &mut cx, + polyfills: Default::default(), }.generate()?; } cx.finalize(stem)? diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 96cf82c7..aef0aa16 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -185,6 +185,13 @@ impl BindgenAttrs { }) } + fn polyfills(&self) -> impl Iterator { + self.attrs.iter().filter_map(|a| match a { + BindgenAttr::Polyfill(s) => Some(s), + _ => None, + }) + } + /// Whether the variadic attributes is present fn variadic(&self) -> bool { self.attrs.iter().any(|a| match *a { @@ -226,6 +233,7 @@ pub enum BindgenAttr { JsName(String, Span), JsClass(String), Extends(Ident), + Polyfill(Ident), Variadic, } @@ -286,6 +294,10 @@ impl Parse for BindgenAttr { input.parse::()?; return Ok(BindgenAttr::Extends(input.parse::()?.0)); } + if attr == "polyfill" { + input.parse::()?; + return Ok(BindgenAttr::Polyfill(input.parse::()?.0)); + } if attr == "module" { input.parse::()?; return Ok(BindgenAttr::Module(input.parse::()?.value())); @@ -556,6 +568,7 @@ impl ConvertToAst for syn::ForeignItemType { rust_name: self.ident, js_name, extends: attrs.extends().cloned().collect(), + polyfills: attrs.polyfills().cloned().collect(), })) } } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index fcfd4968..b5d58ea7 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -87,6 +87,7 @@ pub struct ImportStatic { pub struct ImportType { pub name: String, pub instanceof_shim: String, + pub polyfills: Vec, } #[derive(Deserialize, Serialize)] diff --git a/crates/webidl/src/lib.rs b/crates/webidl/src/lib.rs index 4b2cfb92..4f37aadd 100644 --- a/crates/webidl/src/lib.rs +++ b/crates/webidl/src/lib.rs @@ -503,7 +503,16 @@ impl<'src> FirstPassRecord<'src> { doc_comment: None, instanceof_shim: format!("__widl_instanceof_{}", name), extends: Vec::new(), + polyfills: Vec::new(), }; + + // whitelist a few names that have known polyfills + match name { + "AudioContext" => { + import_type.polyfills.push(Ident::new("webkitAudioContext", Span::call_site())); + } + _ => {} + } let extra = camel_case_ident(name); let extra = &[&extra[..]]; self.append_required_features_doc(&import_type, &mut doc_comment, extra); diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 1f9f339d..9abb834b 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -66,7 +66,8 @@ - [`module = "blah"`](./reference/attributes/on-js-imports/module.md) - [`static_method_of = Blah`](./reference/attributes/on-js-imports/static_method_of.md) - [`structural`](./reference/attributes/on-js-imports/structural.md) - - [variadic](./reference/attributes/on-js-imports/variadic.md) + - [`variadic`](./reference/attributes/on-js-imports/variadic.md) + - [`polyfill`](./reference/attributes/on-js-imports/polyfill.md) - [On Rust Exports](./reference/attributes/on-rust-exports/index.md) - [`constructor`](./reference/attributes/on-rust-exports/constructor.md) - [`js_name = Blah`](./reference/attributes/on-rust-exports/js_name.md) diff --git a/guide/src/reference/attributes/on-js-imports/polyfill.md b/guide/src/reference/attributes/on-js-imports/polyfill.md new file mode 100644 index 00000000..aa089df0 --- /dev/null +++ b/guide/src/reference/attributes/on-js-imports/polyfill.md @@ -0,0 +1,24 @@ +# Polyfilling APIs + +In JS new APIs often have polyfills via different names in various contexts. For +example the `AudioContext` API is known as `webkitAudioContext` in Safari at the +time of this writing. The `polyfill` attribute indicates these alternative +names. + +For example to use `AudioContext` you might do: + +```rust +#[wasm_bindgen] +extern { + #[wasm_bindgen(polyfill = webkitAudioContext)] + type AudioContext; + + // methods on `AudioContext` ... +} +``` + +Whenever `AudioContext` is used it'll use `AudioContext` if the global namespace +defines it or alternatively it'll fall back to `webkitAudioContext`. + +Note that `polyfill` cannot be used with `module = "..."` or `js_namespace = +...`, so it's basically limited to web-platform APIs today. diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs index fcf9e3e1..d79216ad 100644 --- a/tests/wasm/main.rs +++ b/tests/wasm/main.rs @@ -26,6 +26,7 @@ pub mod math; pub mod node; pub mod option; pub mod optional_primitives; +pub mod polyfill; pub mod rethrow; pub mod simple; pub mod slice; diff --git a/tests/wasm/polyfill.js b/tests/wasm/polyfill.js new file mode 100644 index 00000000..9fc3325c --- /dev/null +++ b/tests/wasm/polyfill.js @@ -0,0 +1,5 @@ +exports.import_me = function() {}; + +global.PolyfillBar = class { + foo() { return 123; } +}; diff --git a/tests/wasm/polyfill.rs b/tests/wasm/polyfill.rs new file mode 100644 index 00000000..2c1c9fc2 --- /dev/null +++ b/tests/wasm/polyfill.rs @@ -0,0 +1,40 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; + +#[wasm_bindgen(module = "tests/wasm/polyfill.js")] +extern "C" { + fn import_me(); +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(polyfill = PolyfillBar)] + type PolyfillFoo; + #[wasm_bindgen(constructor)] + fn new() -> PolyfillFoo; + #[wasm_bindgen(method)] + fn foo(this: &PolyfillFoo) -> u32; + + #[wasm_bindgen(polyfill = PolyfillBaz1, polyfill = PolyfillBar)] + type PolyfillFoo2; + #[wasm_bindgen(constructor)] + fn new() -> PolyfillFoo2; + #[wasm_bindgen(method)] + fn foo(this: &PolyfillFoo2) -> u32; + + #[wasm_bindgen(polyfill = PolyfillBaz1, polyfill = PolyfillBar, polyfill = PolyfillBaz2)] + type PolyfillFoo3; + #[wasm_bindgen(constructor)] + fn new() -> PolyfillFoo3; + #[wasm_bindgen(method)] + fn foo(this: &PolyfillFoo3) -> u32; +} + +#[wasm_bindgen_test] +pub fn polyfill_works() { + import_me(); + + assert_eq!(PolyfillFoo::new().foo(), 123); + assert_eq!(PolyfillFoo2::new().foo(), 123); + assert_eq!(PolyfillFoo3::new().foo(), 123); +}