- Start Date: 2018-02-14
- RFC PR: (leave this empty)
- Tracking Issue: (leave this empty)
Summary
Enable Rust crates to transparently depend on packages in the npm ecosystem. These dependencies will, like normal Rust dependencies through Cargo, work seamlessly when consumed by other crates.
Motivation
The primary goal of wasm-bindgen
and wasm-pack
is to enable seamless
integration of Rust with JS. A massive portion of the JS ecosystem, npm, however
currently has little support in wasm-bindgen
and wasm-pack
, making it
difficult to access this rich resource that JS offers!
The goal of this RFC is to enable these dependencies to exist. Rust crates should be able to require functionality from NPM, just like how NPM can require Rust crates compiled to wasm. Any workflow which currently uses NPM packages (such as packaging WebAssembly with a bundler) should continue to work but also allow pulling in "custom" NPM packages as well as requested by Rust dependencies.
Stakeholders
This RFC primarily affects uses of wasm-pack
and wasm-bindgen
who are also
currently using bundlers like Webpack. This also affects, however, developers of
core foundational crates in the Rust ecosystem who want to be concious of the
ability to pull in NPM dependencies and such.
Detailed Explanation
Adding an NPM dependency to a Rust project will look very similar to adding an
NPM dependency to a normal JS project. First the dependency, and its version
requirement, need to be declare. This RFC proposes doing this in a
package.json
file adjacent to the crate's Cargo.toml
file:
{
"dependencies": {
"foo": "^1.0.1"
}
}
The package.json
file will initially be a subset of NPM's package.json
,
only supporting one dependencies
top-level key which internally has key/value
pairs with strings. Beyond this validation though no validation will be
performed of either key or value pairs within dependencies
. In the future
it's intended that more keys of package.json
in NPM will be supported, but
this RFC is intended to be an MVP for now to enable dependencies on NPM at all.
After this package.json
file is created, the package next needs to be
imported in the Rust crate. Like with other Rust dependencies on JS, this will
be done with the #[wasm_bindgen]
attribute:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "foo")] extern "C" { fn function_in_foo_package(); } #}
Note: in JS the above import would be similar to:
import { function_in_foo_package } from "foo";
The exiting module
key in the #[wasm_bindgen]
attribute can be used to
indicate which ES module the import is coming from. This affects the module
key in the final output wasm binary, and corresponds to the name of the package
in package.json
. This is intended to match how bundler conventions already
interpret NPM packages as ES modules.
After these two tools are in place, all that's needed is a wasm-pack build
and
you should be good to go! The final package.json
will have the foo
dependency listed in our package.json
above and be ready for consumption via a
bundler.
Technical Implementation
Under the hood there's a few moving parts which enables all of this to happen.
Let's first take a look at the pieces in wasm-bindgen
.
The primary goal of this RFC is to enable tranparent and transitive
dependencies on NPM. The #[wasm_bindgen]
macro is the only aspect of a crate's
build which has access to all transitive dependencies, so this is what we'll be
using to slurp up package.json
. When #[wasm_bindgen]
with a module
key is
specified it will look for package.json
inside the cwd of the procedural macro
(note that the cwd is set by Cargo to be the directory with the crate's
Cargo.toml
that is being compiled, or the crate in which #[wasm_bindgen]
is
written). This package.json
, if found, will have an absolute path to it
encoded into the custom section that wasm-bindgen
already emits.
Later, when the wasm-bindgen
CLI tool executes, it will parse an interpret all
items in the wasm-bindgen custom section. All package.json
files listed will
be loaded, parsed, and validated (aka only dependencies
allowed for now). If
any package.json
is loaded then a package.json
file will be emitted next to
the output JS file inside of --out-dir
.
After wasm-bindgen
executes, then wasm-pack
will read the package.json
output, if any, and augment it with metadata and other items which are already
emitted.
If more than one crate in a dependency graph depends on an NPM package then in
this MVP proposal an error will be generated. In the future we can implement
some degree of merging version requirements, but for now to remain simple
wasm-bindgen
will emit an error.
Interaction with --no-modules
Depending on NPM packages fundamentally requires, well, NPM, in one way or
another. The wasm-bindgen
and wasm-pack
CLI tools have modes of output
(notably wasm-bindgen
's --no-modules
and wasm-pack
's --target no-modules
flags) which are intended to not require NPM and other JS tooling. In these
situations if a package.json
in any Rust crate is detected an error will be
emitted indicating so.
Note that this means that core crates which are intended to work with
--no-modules
will not be able add NPM dependencies. Instead they'll have to
either import Rust dependencies from crates.io or use a feature like local JS
snippets to import custom JS code.
Drawbacks
One of the primary drawbacks of this RFC is that it's fundamentally incompatible
with a major use case of wasm-bindgen
and wasm-pack
, the --no-modules
and
--target no-modules
flags. As a short-term band-aid this RFC proposes making
it a hard error which would hinder the adoption of this feature in crates that
want to be usable in this mode.
In the long-term, however, it may be possible to get this working. For example
many NPM packages are available on unpkg.com
or in other locations. It may be
possible, if all packages in these locations adhere to well-known conventions,
to generate code that's compatible with these locations of hosting NPM packages.
In these situations it may then be possible to "just drop a script tag" in a few
locations to get --no-modules
working with NPM packages. It's unclear how
viable this is, though.
Rationale and Alternatives
When developing this RFC, some guiding values for its design have been articulated:
-
Development on Rust-generated WebAssembly projects should allow developers to use the development environment they are most comfortable with. Developers writing Rust should get to use Rust, and developers using JavaScript should get to use a JS based runtime environment (Node.js, Chakra, etc).
-
JavaScript tooling and workflows should be usable with Rust-generated WebAssembly projects. For example, bundlers like WebPack and Parcel, or dependency management tools such as
npm audit
and GreenKeeper. -
When possible, decisions should be made that allow the solution to be available to developers of not just Rust, but also C, and C++.
-
Decisions should be focused on creating workflows that allow developers an easy learning curve and productive development experience.
These principles lead to the above proposal of using package.json
to declare
NPM dependencies which is then grouped together by wasm-bindgen
to be
published by wasm-pack
. By using package.json
we get inherent compatibility
with existing workflows like GreenKeeper and npm install
. Additionally
package.json
is very well documented and supported throughout the JS ecosystem
making it very familiar.
Some other alternatives to this RFC which have been ruled out are:
-
Using
Cargo.toml
instead ofpackage.json
to declare NPM dependencies. For example we could use:[package.metadata.npm.dependencies] foo = "0.1"
This has the drawback though of being incompatible with all existing workflows around
package.json
. Additionally it also highlights a discrepancy between NPM and Cargo and how"0.1"
as a version requirement is interpreted (e.g.^0.1
or~0.1
). -
Adding a separate manifest file instead of using
package.json
is also possibility and might be easier forwasm-bindgen
to read and later parse/include. This has a possible benefit of being scoped to exactly our use case and not being misleading by disallowing otherwise-valid fields ofpackage.json
. The downside of this approach is the same asCargo.toml
, however, in that it's an unfamiliar format to most and is incompatible with existing tooling without bringing too much benefit. -
Annotating version dependencies inline could be used rather than
package.json
as well, such as:# #![allow(unused_variables)] #fn main() { #[wasm_bindgen(module = "foo", version = "0.1")] extern "C" { // ... } #}
As with all other alternatives this is incompatible with existing tooling, but it's also not aligned with Rust's own mechanism for declaring dependencies which separates the location for version information and the code iteslf.
Unresolved Questions
- Is the MVP restriction of only using
dependencies
too limiting? Should more fields be supported inpackage.json
?