From 6edb40a807c618e43e7fd92ecb072f600c260e0a Mon Sep 17 00:00:00 2001 From: Alex Crichton <alex@alexcrichton.com> Date: Wed, 27 Feb 2019 12:20:33 -0800 Subject: [PATCH] Implement transitive support for NPM dependencies This commit implements [RFC 8], which enables transitive and transparent dependencies on NPM. The `module` attribute, when seen and not part of a local JS snippet, triggers detection of a `package.json` next to `Cargo.toml`. If found it will cause the `wasm-bindgen` CLI tool to load and parse the `package.json` within each crate and then create a merged `package.json` at the end. [RFC 8]: https://github.com/rustwasm/rfcs/pull/8 --- package.json => _package.json | 0 crates/backend/src/encode.rs | 20 ++++++++- crates/cli-support/Cargo.toml | 1 + crates/cli-support/src/js/mod.rs | 70 ++++++++++++++++++++++++++++++++ crates/cli-support/src/lib.rs | 21 +++++++++- crates/shared/src/lib.rs | 1 + 6 files changed, 111 insertions(+), 2 deletions(-) rename package.json => _package.json (100%) diff --git a/package.json b/_package.json similarity index 100% rename from package.json rename to _package.json diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index 2e327d20253..a36adda6a06 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -1,5 +1,5 @@ use proc_macro2::{Ident, Span}; -use std::cell::RefCell; +use std::cell::{RefCell, Cell}; use std::collections::HashMap; use std::env; use std::fs; @@ -28,6 +28,7 @@ struct Interner { files: RefCell<HashMap<String, LocalFile>>, root: PathBuf, crate_name: String, + has_package_json: Cell<bool>, } struct LocalFile { @@ -43,6 +44,7 @@ impl Interner { files: RefCell::new(HashMap::new()), root: env::var_os("CARGO_MANIFEST_DIR").unwrap().into(), crate_name: env::var("CARGO_PKG_NAME").unwrap(), + has_package_json: Cell::new(false), } } @@ -67,6 +69,7 @@ impl Interner { if let Some(file) = files.get(id) { return Ok(self.intern_str(&file.new_identifier)) } + self.check_for_package_json(); let path = if id.starts_with("/") { self.root.join(&id[1..]) } else if id.starts_with("./") || id.starts_with("../") { @@ -92,6 +95,16 @@ impl Interner { fn unique_crate_identifier(&self) -> String { format!("{}-{}", self.crate_name, ShortHash(0)) } + + fn check_for_package_json(&self) { + if self.has_package_json.get() { + return + } + let path = self.root.join("package.json"); + if path.exists() { + self.has_package_json.set(true); + } + } } fn shared_program<'a>( @@ -144,6 +157,11 @@ fn shared_program<'a>( .map(|js| intern.intern_str(js)) .collect(), unique_crate_identifier: intern.intern_str(&intern.unique_crate_identifier()), + package_json: if intern.has_package_json.get() { + Some(intern.intern_str(intern.root.join("package.json").to_str().unwrap())) + } else { + None + }, }) } diff --git a/crates/cli-support/Cargo.toml b/crates/cli-support/Cargo.toml index 8f19d8ec5b3..66082c09a04 100644 --- a/crates/cli-support/Cargo.toml +++ b/crates/cli-support/Cargo.toml @@ -16,6 +16,7 @@ base64 = "0.9" failure = "0.1.2" log = "0.4" rustc-demangle = "0.1.13" +serde_json = "1.0" tempfile = "3.0" walrus = "0.5.0" wasm-bindgen-anyref-xform = { path = '../anyref-xform', version = '=0.2.40' } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 80a96bb304d..3d88aec4281 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -4,6 +4,7 @@ use crate::{Bindgen, EncodeInto, OutputMode}; use failure::{bail, Error, ResultExt}; use std::collections::{HashMap, HashSet, BTreeMap}; use std::env; +use std::fs; use walrus::{MemoryId, Module}; use wasm_bindgen_wasm_interpreter::Interpreter; @@ -64,6 +65,10 @@ pub struct Context<'a> { /// the same `Program`. pub snippet_offsets: HashMap<&'a str, usize>, + /// All package.json dependencies we've learned about so far + pub package_json_read: HashSet<&'a str>, + pub npm_dependencies: HashMap<String, (&'a str, String)>, + pub anyref: wasm_bindgen_anyref_xform::Context, } @@ -2480,6 +2485,10 @@ impl<'a, 'b> SubContext<'a, 'b> { self.cx.typescript.push_str("\n\n"); } + if let Some(path) = self.program.package_json { + self.add_package_json(path)?; + } + Ok(()) } @@ -2951,6 +2960,67 @@ impl<'a, 'b> SubContext<'a, 'b> { let import = self.determine_import(import, item)?; Ok(self.cx.import_identifier(import)) } + + fn add_package_json(&mut self, path: &'b str) -> Result<(), Error> { + if !self.cx.package_json_read.insert(path) { + return Ok(()); + } + if !self.cx.config.mode.nodejs() && !self.cx.config.mode.bundler() { + bail!("NPM dependencies have been specified in `{}` but \ + this is only compatible with the default output of \ + `wasm-bindgen` or the `--nodejs` flag"); + } + let contents = fs::read_to_string(path).context(format!("failed to read `{}`", path))?; + let json: serde_json::Value = serde_json::from_str(&contents)?; + let object = match json.as_object() { + Some(s) => s, + None => bail!( + "expected `package.json` to have an JSON object in `{}`", + path + ), + }; + let mut iter = object.iter(); + let (key, value) = match iter.next() { + Some(pair) => pair, + None => return Ok(()), + }; + if key != "dependencies" || iter.next().is_some() { + bail!( + "NPM manifest found at `{}` can currently only have one key, \ + `dependencies`, and no other fields", + path + ); + } + let value = match value.as_object() { + Some(s) => s, + None => bail!("expected `dependencies` to be a JSON object in `{}`", path), + }; + + for (name, value) in value.iter() { + let value = match value.as_str() { + Some(s) => s, + None => bail!( + "keys in `dependencies` are expected to be strings in `{}`", + path + ), + }; + if let Some((prev, _prev_version)) = self.cx.npm_dependencies.get(name) { + bail!( + "dependency on NPM package `{}` specified in two `package.json` files, \ + which at the time is not allowed:\n * {}\n * {}", + name, + path, + prev + ) + } + + self.cx + .npm_dependencies + .insert(name.to_string(), (path, value.to_string())); + } + + Ok(()) + } } #[derive(Hash, Eq, PartialEq)] diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 526a72eb212..5b7ec56d984 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -1,7 +1,7 @@ #![doc(html_root_url = "https://docs.rs/wasm-bindgen-cli-support/0.2")] use failure::{bail, Error, ResultExt}; -use std::collections::BTreeSet; +use std::collections::{BTreeSet, BTreeMap}; use std::env; use std::fs; use std::mem; @@ -329,6 +329,8 @@ impl Bindgen { start: None, anyref: Default::default(), snippet_offsets: Default::default(), + npm_dependencies: Default::default(), + package_json_read: Default::default(), }; cx.anyref.enabled = self.anyref; cx.anyref.prepare(cx.module)?; @@ -366,6 +368,16 @@ impl Bindgen { .with_context(|_| format!("failed to write `{}`", path.display()))?; } + if cx.npm_dependencies.len() > 0 { + let map = cx + .npm_dependencies + .iter() + .map(|(k, v)| (k, &v.1)) + .collect::<BTreeMap<_, _>>(); + let json = serde_json::to_string_pretty(&map)?; + fs::write(out_dir.join("package.json"), json)?; + } + cx.finalize(stem)? }; @@ -701,4 +713,11 @@ impl OutputMode { _ => false, } } + + fn bundler(&self) -> bool { + match self { + OutputMode::Bundler => true, + _ => false, + } + } } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 5bd0072cd23..6bbfefb6a56 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -17,6 +17,7 @@ macro_rules! shared_api { local_modules: Vec<LocalModule<'a>>, inline_js: Vec<&'a str>, unique_crate_identifier: &'a str, + package_json: Option<&'a str>, } struct Import<'a> {