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> {