From 6edb40a807c618e43e7fd92ecb072f600c260e0a Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Feb 2019 12:20:33 -0800 Subject: [PATCH 1/3] 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>, root: PathBuf, crate_name: String, + has_package_json: Cell, } 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, + 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::>(); + 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>, inline_js: Vec<&'a str>, unique_crate_identifier: &'a str, + package_json: Option<&'a str>, } struct Import<'a> { From 362777fc756a15d23af460e696751ac09b72f1b9 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 28 Feb 2019 11:37:08 -0800 Subject: [PATCH 2/3] Start implementing a test suite for the CLI We have very few tests today so this starts to add the basics of a test suite which compiles Cargo projects on-the-fly which will hopefully help us bolster the amount of assertions we can make about the output. --- azure-pipelines.yml | 21 +++- crates/cli-support/src/js/mod.rs | 6 +- crates/cli-support/src/lib.rs | 2 +- crates/cli/Cargo.toml | 4 + crates/cli/src/bin/wasm-bindgen.rs | 2 +- crates/cli/tests/wasm-bindgen/main.rs | 138 +++++++++++++++++++++++ crates/cli/tests/wasm-bindgen/npm.rs | 155 ++++++++++++++++++++++++++ 7 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 crates/cli/tests/wasm-bindgen/main.rs create mode 100644 crates/cli/tests/wasm-bindgen/npm.rs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 275f543163d..30f0d3d76c9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -62,12 +62,17 @@ jobs: - template: ci/azure-install-sccache.yml - script: cargo test --target wasm32-unknown-unknown --features nightly --test wasm - - job: test_cli_support - displayName: "Run wasm-bindgen-cli-support crate tests" + - job: test_cli + displayName: "Run wasm-bindgen-cli crate tests" steps: - template: ci/azure-install-rust.yml - template: ci/azure-install-sccache.yml + - script: rustup target add wasm32-unknown-unknown + displayName: "install wasm target" - script: cargo test -p wasm-bindgen-cli-support + displayName: "wasm-bindgen-cli-support tests" + - script: cargo test -p wasm-bindgen-cli + displayName: "wasm-bindgen-cli tests" - job: test_web_sys displayName: "Run web-sys crate tests" @@ -139,16 +144,22 @@ jobs: steps: - template: ci/azure-install-rust.yml - template: ci/azure-install-sccache.yml - - script: npm install + - script: mv _package.json package.json && npm install && rm package.json + displayName: "run npm install" - script: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f - - script: cargo build -p wasm-bindgen-cli - - script: ln -snf `pwd`/target/debug/wasm-bindgen $HOME/.cargo/bin/wasm-bindgen + displayName: "install wasm-pack" + - script: | + set -ex + cargo build -p wasm-bindgen-cli + ln -snf `pwd`/target/debug/wasm-bindgen $HOME/.cargo/bin/wasm-bindgen + displayName: "install wasm-bindgen for `wasm-pack` to use" - script: | for dir in `ls examples | grep -v README | grep -v asm.js | grep -v raytrace | grep -v without-a-bundler`; do (cd examples/$dir && ln -fs ../../node_modules . && npm run build -- --output-path $BUILD_ARTIFACTSTAGINGDIRECTORY/exbuild/$dir) || exit 1; done + displayName: "build examples" - task: PublishPipelineArtifact@0 inputs: artifactName: examples1 diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 3d88aec4281..d9b5f220a90 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -67,6 +67,9 @@ pub struct Context<'a> { /// All package.json dependencies we've learned about so far pub package_json_read: HashSet<&'a str>, + + /// A map of the name of npm dependencies we've loaded so far to the path + /// they're defined in as well as their version specification. pub npm_dependencies: HashMap, pub anyref: wasm_bindgen_anyref_xform::Context, @@ -2967,8 +2970,7 @@ impl<'a, 'b> SubContext<'a, 'b> { } 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"); + this is only compatible with the `bundler` and `nodejs` targets"); } let contents = fs::read_to_string(path).context(format!("failed to read `{}`", path))?; let json: serde_json::Value = serde_json::from_str(&contents)?; diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 5b7ec56d984..a221c1e28ba 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -716,7 +716,7 @@ impl OutputMode { fn bundler(&self) -> bool { match self { - OutputMode::Bundler => true, + OutputMode::Bundler { .. } => true, _ => false, } } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index aa646f4018d..478e661a24b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -28,5 +28,9 @@ walrus = "0.5" wasm-bindgen-cli-support = { path = "../cli-support", version = "=0.2.40" } wasm-bindgen-shared = { path = "../shared", version = "=0.2.40" } +[dev-dependencies] +assert_cmd = "0.11" +predicates = "1.0.0" + [features] vendored-openssl = ['openssl/vendored'] diff --git a/crates/cli/src/bin/wasm-bindgen.rs b/crates/cli/src/bin/wasm-bindgen.rs index 6aa676a87f9..21df812a284 100644 --- a/crates/cli/src/bin/wasm-bindgen.rs +++ b/crates/cli/src/bin/wasm-bindgen.rs @@ -79,7 +79,7 @@ fn main() { }; eprintln!("error: {}", err); for cause in err.iter_causes() { - eprintln!("\tcaused by: {}", cause); + eprintln!(" caused by: {}", cause); } process::exit(1); } diff --git a/crates/cli/tests/wasm-bindgen/main.rs b/crates/cli/tests/wasm-bindgen/main.rs new file mode 100644 index 00000000000..b353634ff61 --- /dev/null +++ b/crates/cli/tests/wasm-bindgen/main.rs @@ -0,0 +1,138 @@ +//! A small test suite for the `wasm-bindgen` CLI command itself +//! +//! This test suite is intended to exercise functionality of the CLI in terms of +//! errors and such. It is not intended for comprehensive behavior testing, as +//! that should all be placed in the top-level `tests` directory for the +//! `wasm-bindgen` crate itself. +//! +//! Assertions about errors in `wasm-bindgen` or assertions about the output of +//! `wasm-bindgen` should all be placed in this test suite, however. Currently +//! it is largely based off actually running `cargo build` at test time which is +//! quite expensive, so it's recommended that this test suite doesn't become too +//! large! + +use assert_cmd::prelude::*; +use predicates::str; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn target_dir() -> PathBuf { + let mut dir = PathBuf::from(env::current_exe().unwrap()); + dir.pop(); // current exe + if dir.ends_with("deps") { + dir.pop(); + } + dir.pop(); // debug and/or release + return dir; +} + +fn repo_root() -> PathBuf { + let mut repo_root = env::current_dir().unwrap(); + repo_root.pop(); // remove 'cli' + repo_root.pop(); // remove 'crates' + repo_root +} + +struct Project { + root: PathBuf, + name: &'static str, +} + +impl Project { + fn new(name: &'static str) -> Project { + let root = target_dir().join("cli-tests").join(name); + drop(fs::remove_dir_all(&root)); + fs::create_dir_all(&root).unwrap(); + Project { root, name } + } + + fn file(&mut self, name: &str, contents: &str) -> &mut Project { + let dst = self.root.join(name); + fs::create_dir_all(dst.parent().unwrap()).unwrap(); + fs::write(&dst, contents).unwrap(); + self + } + + fn wasm_bindgen(&mut self, args: &str) -> (Command, PathBuf) { + let wasm = self.build(); + let output = self.root.join("pkg"); + fs::create_dir_all(&output).unwrap(); + let mut cmd = Command::cargo_bin("wasm-bindgen").unwrap(); + cmd.arg("--out-dir").arg(&output); + cmd.arg(&wasm); + for arg in args.split_whitespace() { + cmd.arg(arg); + } + (cmd, output) + } + + fn build(&mut self) -> PathBuf { + if !self.root.join("Cargo.toml").is_file() { + self.file( + "Cargo.toml", + &format!( + " + [package] + name = \"{}\" + authors = [] + version = \"1.0.0\" + edition = '2018' + + [dependencies] + wasm-bindgen = {{ path = '{}' }} + + [lib] + crate-type = ['cdylib'] + + [workspace] + ", + self.name, + repo_root().display(), + ), + ); + } + + let target_dir = target_dir(); + Command::new("cargo") + .current_dir(&self.root) + .arg("build") + .arg("--target") + .arg("wasm32-unknown-unknown") + .env("CARGO_TARGET_DIR", &target_dir) + .assert() + .success(); + + target_dir + .join("wasm32-unknown-unknown") + .join("debug") + .join(self.name) + .with_extension("wasm") + } +} + +#[test] +fn version_useful() { + Command::cargo_bin("wasm-bindgen") + .unwrap() + .arg("-V") + .assert() + .stdout(str::ends_with("\n")) + .stdout(str::starts_with("wasm-bindgen ")) + .success(); +} + +#[test] +fn works_on_empty_project() { + let (mut cmd, _out_dir) = Project::new("works_on_empty_project") + .file( + "src/lib.rs", + r#" + "#, + ) + .wasm_bindgen(""); + cmd.assert().success(); +} + +mod npm; diff --git a/crates/cli/tests/wasm-bindgen/npm.rs b/crates/cli/tests/wasm-bindgen/npm.rs new file mode 100644 index 00000000000..902cfb9f30a --- /dev/null +++ b/crates/cli/tests/wasm-bindgen/npm.rs @@ -0,0 +1,155 @@ +use crate::*; + +#[test] +fn no_modules_rejects_npm() { + let (mut cmd, _out_dir) = Project::new("no_modules_rejects_npm") + .file( + "src/lib.rs", + r#" + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "foo")] + extern { + fn foo(); + } + + #[wasm_bindgen(start)] + pub fn main() { + foo(); + } + "#, + ) + .file("package.json", "") + .wasm_bindgen("--no-modules"); + cmd.assert() + .stderr("\ +error: failed to generate bindings for JS import `foo` + caused by: import from `foo` module not allowed with `--target no-modules`; use `nodejs`, `web`, or `bundler` target instead +") + .failure(); +} + +#[test] +fn more_package_json_fields_rejected() { + let (mut cmd, _out_dir) = Project::new("more_package_json_fields_rejected") + .file( + "src/lib.rs", + r#" + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "foo")] + extern { + fn foo(); + } + + #[wasm_bindgen(start)] + pub fn main() { + foo(); + } + "#, + ) + .file( + "package.json", + r#" + { + "name": "foo", + "dependencies": {} + } + "#, + ) + .wasm_bindgen(""); + cmd.assert() + .stderr(str::is_match("\ +error: NPM manifest found at `.*` can currently only have one key, .* +").unwrap()) + .failure(); +} + +#[test] +fn npm_conflict_rejected() { + let (mut cmd, _out_dir) = Project::new("npm_conflict_rejected") + .file( + "Cargo.toml", + &format!(r#" + [package] + name = "npm_conflict_rejected" + authors = [] + version = "1.0.0" + edition = '2018' + + [dependencies] + wasm-bindgen = {{ path = '{}' }} + bar = {{ path = 'bar' }} + + [lib] + crate-type = ['cdylib'] + + [workspace] + "#, + repo_root().display() + ) + ) + .file( + "src/lib.rs", + r#" + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "bar")] + extern { + fn foo(); + } + + #[wasm_bindgen(start)] + pub fn main() { + foo(); + bar::foo(); + } + "#, + ) + .file( + "package.json", + r#" + { + "dependencies": {"bar": "0.0.0"} + } + "#, + ) + .file( + "bar/Cargo.toml", + &format!(r#" + [package] + name = "bar" + authors = [] + version = "1.0.0" + edition = '2018' + + [dependencies] + wasm-bindgen = {{ path = '{}' }} + "#, + repo_root().display() + ) + ) + .file( + "bar/src/lib.rs", + r#" + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(module = "bar")] + extern { + pub fn foo(); + } + "#, + ) + .file( + "bar/package.json", + r#" + { + "dependencies": {"bar": "1.0.0"} + } + "#, + ) + .wasm_bindgen(""); + cmd.assert() + .stderr(str::is_match("dependency on NPM package `bar` specified in two").unwrap()) + .failure(); +} From faf49c7d56f2e86519e4d172b831c28e2b5e16a0 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 25 Mar 2019 13:49:05 -0700 Subject: [PATCH 3/3] Reorganize the import_js example --- Cargo.toml | 2 +- examples/import_js/{ => crate}/Cargo.toml | 0 examples/import_js/{ => crate}/defined-in-js.js | 0 examples/import_js/{ => crate}/src/lib.rs | 0 examples/import_js/index.js | 2 +- examples/import_js/webpack.config.js | 2 +- guide/src/examples/import-js.md | 4 ++-- 7 files changed, 5 insertions(+), 5 deletions(-) rename examples/import_js/{ => crate}/Cargo.toml (100%) rename examples/import_js/{ => crate}/defined-in-js.js (100%) rename examples/import_js/{ => crate}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 7810b7ea42e..305b9671ada 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ members = [ "examples/fetch", "examples/guide-supported-types-examples", "examples/hello_world", - "examples/import_js", + "examples/import_js/crate", "examples/julia_set", "examples/paint", "examples/performance", diff --git a/examples/import_js/Cargo.toml b/examples/import_js/crate/Cargo.toml similarity index 100% rename from examples/import_js/Cargo.toml rename to examples/import_js/crate/Cargo.toml diff --git a/examples/import_js/defined-in-js.js b/examples/import_js/crate/defined-in-js.js similarity index 100% rename from examples/import_js/defined-in-js.js rename to examples/import_js/crate/defined-in-js.js diff --git a/examples/import_js/src/lib.rs b/examples/import_js/crate/src/lib.rs similarity index 100% rename from examples/import_js/src/lib.rs rename to examples/import_js/crate/src/lib.rs diff --git a/examples/import_js/index.js b/examples/import_js/index.js index 0c41f50d3d6..e3955b09370 100644 --- a/examples/import_js/index.js +++ b/examples/import_js/index.js @@ -1,4 +1,4 @@ // For more comments about what's going on here, check out the `hello_world` // example -import('./pkg/import_js') +import('./crate/pkg/import_js') .catch(console.error); diff --git a/examples/import_js/webpack.config.js b/examples/import_js/webpack.config.js index 25afd18002a..60fdbe372ec 100644 --- a/examples/import_js/webpack.config.js +++ b/examples/import_js/webpack.config.js @@ -14,7 +14,7 @@ module.exports = { template: 'index.html' }), new WasmPackPlugin({ - crateDirectory: path.resolve(__dirname, ".") + crateDirectory: path.resolve(__dirname, "crate") }), // Have this example work in Edge which doesn't ship `TextEncoder` or // `TextDecoder` at this time. diff --git a/guide/src/examples/import-js.md b/guide/src/examples/import-js.md index 879657dcc4b..e572aa631e5 100644 --- a/guide/src/examples/import-js.md +++ b/guide/src/examples/import-js.md @@ -13,13 +13,13 @@ For example if you're working with this JS file: ```js // defined-in-js.js -{{#include ../../../examples/import_js/defined-in-js.js}} +{{#include ../../../examples/import_js/crate/defined-in-js.js}} ``` you can use it in Rust with: ```rust -{{#include ../../../examples/import_js/src/lib.rs}} +{{#include ../../../examples/import_js/crate/src/lib.rs}} ``` You can also [explore the full list of ways to configure imports][attr]