From 412bebca72072f1a9e8731750351544148864547 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 25 Apr 2018 07:26:33 -0700 Subject: [PATCH] Add support for version specifications This commit adds a `#[wasm_bindgen(version = "...")]` attribute support. This information is eventually written into a `__wasm_pack_unstable` section. Currently this is a strawman for the proposal in ashleygwilliams/wasm-pack#101 --- DESIGN.md | 31 +++++++++++++++++ crates/backend/src/ast.rs | 42 ++++++++++++++++++++++ crates/cli-support/Cargo.toml | 2 ++ crates/cli-support/src/js/mod.rs | 60 ++++++++++++++++++++++++++++++++ crates/cli-support/src/lib.rs | 3 ++ crates/shared/src/lib.rs | 1 + tests/all/imports.rs | 44 +++++++++++++++++++++++ 7 files changed, 183 insertions(+) diff --git a/DESIGN.md b/DESIGN.md index 9fdfbb10657..1108481a2f8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -886,6 +886,37 @@ controlling precisely how imports are imported and what they map to in JS. This section is intended to hopefully be an exhaustive reference of the possibilities! +* `module` and `version` - we've seen `module` so far indicating where we can + import items from but `version` is also allowed: + + ```rust + #[wasm_bindgen(module = "moment", version = "^2.0.0")] + extern { + type Moment; + fn moment() -> Moment; + #[wasm_bindgen(method)] + fn format(this: &Moment) -> String; + } + ``` + + The `module` key is used to configure the module that each item is imported + from. The `version` key does not affect the generated wasm itself but rather + it's an informative directive for tools like [wasm-pack]. Tools like wasm-pack + will generate a `package.json` for you and the `version` listed here, when + `module` is also an NPM package, will correspond to what to write down in + `package.json`. + + In other words the usage of `module` as the name of an NPM package and + `version` as the version requirement allows you to, inline in Rust, depend on + the NPM ecosystem and import functionality from those packages. When bundled + with a tool like [wasm-pack] everything will automatically get wired up with + bundlers and you should be good to go! + + Note that the `version` is *required* if `module` doesn't start with `./`. If + `module` starts with `./` then it is an error to provide a version. + +[wasm-pack]: https://github.com/ashleygwilliams/wasm-pack + * `catch` - as we saw before the `catch` attribute allows catching a JS exception. This can be attached to any imported function and the function must return a `Result` where the `Err` payload is a `JsValue`, like so: diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 592c4afd90d..cf9ab19e2de 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -20,6 +20,7 @@ pub struct Export { pub struct Import { pub module: Option, + pub version: Option, pub js_namespace: Option, pub kind: ImportKind, } @@ -294,6 +295,7 @@ impl Program { BindgenAttrs::find(attrs) }; let module = item_opts.module().or(opts.module()).map(|s| s.to_string()); + let version = item_opts.version().or(opts.version()).map(|s| s.to_string()); let js_namespace = item_opts.js_namespace().or(opts.js_namespace()); let mut kind = match item { syn::ForeignItem::Fn(f) => self.push_foreign_fn(f, item_opts), @@ -304,6 +306,7 @@ impl Program { self.imports.push(Import { module, + version, js_namespace, kind, }); @@ -584,8 +587,29 @@ impl Variant { impl Import { fn shared(&self) -> shared::Import { + match (&self.module, &self.version) { + (&Some(ref m), None) if m.starts_with("./") => {} + (&Some(ref m), &Some(_)) if m.starts_with("./") => { + panic!("when a module path starts with `./` that indicates \ + that a local file is being imported so the `version` \ + key cannot also be specified"); + } + (&Some(_), &Some(_)) => {} + (&Some(_), &None) => { + panic!("when the `module` directive doesn't start with `./` \ + then it's interpreted as an NPM package which requires \ + a `version` to be specified as well, try using \ + #[wasm_bindgen(module = \"...\", version = \"...\")]") + } + (&None, &Some(_)) => { + panic!("the #[wasm_bindgen(version = \"...\")] attribute can only \ + be used when `module = \"...\"` is also specified"); + } + (&None, &None) => {} + } shared::Import { module: self.module.clone(), + version: self.version.clone(), js_namespace: self.js_namespace.map(|s| s.as_ref().to_string()), kind: self.kind.shared(), } @@ -746,6 +770,16 @@ impl BindgenAttrs { .next() } + fn version(&self) -> Option<&str> { + self.attrs + .iter() + .filter_map(|a| match *a { + BindgenAttr::Version(ref s) => Some(&s[..]), + _ => None, + }) + .next() + } + pub fn catch(&self) -> bool { self.attrs.iter().any(|a| match *a { BindgenAttr::Catch => true, @@ -844,6 +878,7 @@ enum BindgenAttr { Method, JsNamespace(syn::Ident), Module(String), + Version(String), Getter(Option), Setter(Option), Structural, @@ -897,6 +932,13 @@ impl syn::synom::Synom for BindgenAttr { (s.value()) )=> { BindgenAttr::Module } | + do_parse!( + call!(term, "version") >> + punct!(=) >> + s: syn!(syn::LitStr) >> + (s.value()) + )=> { BindgenAttr::Version } + | do_parse!( call!(term, "js_name") >> punct!(=) >> diff --git a/crates/cli-support/Cargo.toml b/crates/cli-support/Cargo.toml index 39f4cf12693..1f247595cfc 100644 --- a/crates/cli-support/Cargo.toml +++ b/crates/cli-support/Cargo.toml @@ -14,6 +14,8 @@ Shared support for the wasm-bindgen-cli package, an internal dependency base64 = "0.9" failure = "0.1" parity-wasm = "0.27" +serde = "1.0" +serde_derive = "1.0" serde_json = "1.0" wasm-bindgen-shared = { path = "../shared", version = '=0.2.5' } wasm-gc-api = "0.1" diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 1d179bc20c0..ab8eb527ad4 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -5,6 +5,7 @@ use std::mem; use failure::{Error, ResultExt}; use parity_wasm::elements::*; use parity_wasm; +use serde_json; use shared; use wasm_gc; @@ -29,6 +30,7 @@ pub struct Context<'a> { pub exported_classes: HashMap, pub function_table_needed: bool, pub run_descriptor: &'a Fn(&str) -> Vec, + pub module_versions: Vec<(String, String)>, } #[derive(Default)] @@ -341,6 +343,7 @@ impl<'a> Context<'a> { self.export_table(); self.gc()?; + self.add_wasm_pack_section(); while js.contains("\n\n\n") { js = js.replace("\n\n\n", "\n\n"); @@ -1333,6 +1336,28 @@ impl<'a> Context<'a> { self.globals.push_str(s); self.globals.push_str("\n"); } + + fn add_wasm_pack_section(&mut self) { + if self.module_versions.len() == 0 { + return + } + + #[derive(Serialize)] + struct WasmPackSchema<'a> { + version: &'a str, + modules: &'a [(String, String)], + } + + let contents = serde_json::to_string(&WasmPackSchema { + version: "0.0.1", + modules: &self.module_versions, + }).unwrap(); + + let mut section = CustomSection::default(); + *section.name_mut() = "__wasm_pack_unstable".to_string(); + *section.payload_mut() = contents.into_bytes(); + self.module.sections_mut().push(Section::Custom(section)); + } } impl<'a, 'b> SubContext<'a, 'b> { @@ -1423,6 +1448,7 @@ impl<'a, 'b> SubContext<'a, 'b> { } fn generate_import(&mut self, import: &shared::Import) -> Result<(), Error> { + self.validate_import_module(import)?; match import.kind { shared::ImportKind::Function(ref f) => { self.generate_import_function(import, f) @@ -1443,6 +1469,40 @@ impl<'a, 'b> SubContext<'a, 'b> { Ok(()) } + fn validate_import_module(&mut self, import: &shared::Import) + -> Result<(), Error> + { + let version = match import.version { + Some(ref s) => s, + None => return Ok(()), + }; + let module = match import.module { + Some(ref s) => s, + None => return Ok(()), + }; + if module.starts_with("./") { + return Ok(()) + } + let pkg = if module.starts_with("@") { + // Translate `@foo/bar/baz` to `@foo/bar` and `@foo/bar` to itself + let first_slash = match module.find('/') { + Some(i) => i, + None => { + bail!("packages starting with `@` must be of the form \ + `@foo/bar`, but found: `{}`", module) + } + }; + match module[first_slash + 1..].find('/') { + Some(i) => &module[..i], + None => module, + } + } else { + module.split('/').next().unwrap() + }; + self.cx.module_versions.push((pkg.to_string(), version.clone())); + Ok(()) + } + fn generate_import_static( &mut self, info: &shared::Import, diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 29f7dafce73..55c194776eb 100644 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -1,5 +1,7 @@ extern crate parity_wasm; extern crate wasm_bindgen_shared as shared; +#[macro_use] +extern crate serde_derive; extern crate serde_json; extern crate wasm_gc; extern crate wasmi; @@ -131,6 +133,7 @@ impl Bindgen { config: &self, module: &mut module, function_table_needed: false, + module_versions: Default::default(), run_descriptor: &|name| { let mut v = MyExternals(Vec::new()); let ret = instance diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index adbe6ee0c6b..81f10c46083 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -22,6 +22,7 @@ pub struct Program { #[derive(Deserialize, Serialize)] pub struct Import { pub module: Option, + pub version: Option, pub js_namespace: Option, pub kind: ImportKind, } diff --git a/tests/all/imports.rs b/tests/all/imports.rs index a1b812f6665..e5b9b4148a1 100644 --- a/tests/all/imports.rs +++ b/tests/all/imports.rs @@ -351,3 +351,47 @@ fn rename() { "#) .test(); } + +#[test] +fn versions() { + project() + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "webpack", version = "^0.2.0")] + extern { + fn foo(); + } + + #[wasm_bindgen] + pub fn run() { + foo(); + } + "#) + .file("test.js", r#" + const fs = require("fs"); + const assert = require("assert"); + + exports.test = function() { + const bytes = fs.readFileSync('out_bg.wasm'); + const m = new WebAssembly.Module(bytes); + const name = '__wasm_pack_unstable'; + const sections = WebAssembly.Module.customSections(m, name); + assert.strictEqual(sections.length, 1); + const b = new Uint8Array(sections[0]); + const buf = new Buffer(b); + const map = JSON.parse(buf.toString()); + assert.deepStrictEqual(map, { + version: '0.0.1', + modules: [ + ['webpack', '^0.2.0'] + ] + }); + }; + "#) + .test(); +}