Skip to content

Commit 439fe1c

Browse files
authored
Merge pull request #8 from alexcrichton/npm-deps
RFC: Enable depending on NPM packages
2 parents 1194a8e + 57cc2dc commit 439fe1c

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed

text/008-npm-dependencies.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
- Start Date: 2018-02-14
2+
- RFC PR: (leave this empty)
3+
- Tracking Issue: (leave this empty)
4+
5+
# Summary
6+
[summary]: #summary
7+
8+
Enable Rust crates to transparently depend on packages in the npm ecosystem.
9+
These dependencies will, like normal Rust dependencies through Cargo, work
10+
seamlessly when consumed by other crates.
11+
12+
# Motivation
13+
[motivation]: #motivation
14+
15+
The primary goal of `wasm-bindgen` and `wasm-pack` is to enable seamless
16+
integration of Rust with JS. A massive portion of the JS ecosystem, npm, however
17+
currently has little support in `wasm-bindgen` and `wasm-pack`, making it
18+
difficult to access this rich resource that JS offers!
19+
20+
The goal of this RFC is to enable these dependencies to exist. Rust crates
21+
should be able to require functionality from NPM, just like how NPM can require
22+
Rust crates compiled to wasm. Any workflow which currently uses NPM packages
23+
(such as packaging WebAssembly with a bundler) should continue to work but also
24+
allow pulling in "custom" NPM packages as well as requested by Rust
25+
dependencies.
26+
27+
# Stakeholders
28+
[stakeholders]: #stakeholders
29+
30+
This RFC primarily affects uses of `wasm-pack` and `wasm-bindgen` who are also
31+
currently using bundlers like Webpack. This also affects, however, developers of
32+
core foundational crates in the Rust ecosystem who want to be concious of the
33+
ability to pull in NPM dependencies and such.
34+
35+
# Detailed Explanation
36+
[detailed-explanation]: #detailed-explanation
37+
38+
Adding an NPM dependency to a Rust project will look very similar to adding an
39+
NPM dependency to a normal JS project. First the dependency, and its version
40+
requirement, need to be declare. This RFC proposes doing this in a
41+
`package.json` file adjacent to the crate's `Cargo.toml` file:
42+
43+
```json
44+
{
45+
"dependencies": {
46+
"foo": "^1.0.1"
47+
}
48+
}
49+
```
50+
51+
The `package.json` file will initially be a subset of NPM's `package.json`,
52+
only supporting one `dependencies` top-level key which internally has key/value
53+
pairs with strings. Beyond this validation though no validation will be
54+
performed of either key or value pairs within `dependencies`. In the future
55+
it's intended that more keys of `package.json` in NPM will be supported, but
56+
this RFC is intended to be an MVP for now to enable dependencies on NPM at all.
57+
58+
After this `package.json` file is created, the package next needs to be
59+
imported in the Rust crate. Like with other Rust dependencies on JS, this will
60+
be done with the `#[wasm_bindgen]` attribute:
61+
62+
```rust
63+
#[wasm_bindgen(module = "foo")]
64+
extern "C" {
65+
fn function_in_foo_package();
66+
}
67+
```
68+
69+
> **Note**: in JS the above import would be similar to:
70+
>
71+
> ```js
72+
> import { function_in_foo_package } from "foo";
73+
> ```
74+
75+
The exiting `module` key in the `#[wasm_bindgen]` attribute can be used to
76+
indicate which ES module the import is coming from. This affects the `module`
77+
key in the final output wasm binary, and corresponds to the name of the package
78+
in `package.json`. This is intended to match how bundler conventions already
79+
interpret NPM packages as ES modules.
80+
81+
After these two tools are in place, all that's needed is a `wasm-pack build` and
82+
you should be good to go! The final `package.json` will have the `foo`
83+
dependency listed in our `package.json` above and be ready for consumption via a
84+
bundler.
85+
86+
### Technical Implementation
87+
88+
Under the hood there's a few moving parts which enables all of this to happen.
89+
Let's first take a look at the pieces in `wasm-bindgen`.
90+
91+
The primary goal of this RFC is to enable *tranparent* and *transitive*
92+
dependencies on NPM. The `#[wasm_bindgen]` macro is the only aspect of a crate's
93+
build which has access to all transitive dependencies, so this is what we'll be
94+
using to slurp up `package.json`. When `#[wasm_bindgen]` with a `module` key is
95+
specified it will look for `package.json` inside the cwd of the procedural macro
96+
(note that the cwd is set by Cargo to be the directory with the crate's
97+
`Cargo.toml` that is being compiled, or the crate in which `#[wasm_bindgen]` is
98+
written). This `package.json`, if found, will have an absolute path to it
99+
encoded into the custom section that `wasm-bindgen` already emits.
100+
101+
Later, when the `wasm-bindgen` CLI tool executes, it will parse an interpret all
102+
items in the wasm-bindgen custom section. All `package.json` files listed will
103+
be loaded, parsed, and validated (aka only `dependencies` allowed for now). If
104+
any `package.json` is loaded then a `package.json` file will be emitted next to
105+
the output JS file inside of `--out-dir`.
106+
107+
After `wasm-bindgen` executes, then `wasm-pack` will read the `package.json`
108+
output, if any, and augment it with metadata and other items which are already
109+
emitted.
110+
111+
If more than one crate in a dependency graph depends on an NPM package then in
112+
this MVP proposal an error will be generated. In the future we can implement
113+
some degree of merging version requirements, but for now to remain simple
114+
`wasm-bindgen` will emit an error.
115+
116+
### Interaction with `--no-modules`
117+
118+
Depending on NPM packages fundamentally requires, well, NPM, in one way or
119+
another. The `wasm-bindgen` and `wasm-pack` CLI tools have modes of output
120+
(notably `wasm-bindgen`'s `--no-modules` and `wasm-pack`'s `--target no-modules`
121+
flags) which are intended to not require NPM and other JS tooling. In these
122+
situations if a `package.json` in any Rust crate is detected an error will be
123+
emitted indicating so.
124+
125+
Note that this means that core crates which are intended to work with
126+
`--no-modules` will not be able add NPM dependencies. Instead they'll have to
127+
either import Rust dependencies from crates.io or use a feature like [local JS
128+
snippets][js] to import custom JS code.
129+
130+
[js]: https://github.com/rustwasm/rfcs/pull/6
131+
132+
# Drawbacks
133+
[drawbacks]: #drawbacks
134+
135+
One of the primary drawbacks of this RFC is that it's fundamentally incompatible
136+
with a major use case of `wasm-bindgen` and `wasm-pack`, the `--no-modules` and
137+
`--target no-modules` flags. As a short-term band-aid this RFC proposes making
138+
it a hard error which would hinder the adoption of this feature in crates that
139+
want to be usable in this mode.
140+
141+
In the long-term, however, it may be possible to get this working. For example
142+
many NPM packages are available on `unpkg.com` or in other locations. It may be
143+
possible, if all packages in these locations adhere to well-known conventions,
144+
to generate code that's compatible with these locations of hosting NPM packages.
145+
In these situations it may then be possible to "just drop a script tag" in a few
146+
locations to get `--no-modules` working with NPM packages. It's unclear how
147+
viable this is, though.
148+
149+
# Rationale and Alternatives
150+
[alternatives]: #rationale-and-alternatives
151+
152+
When developing this RFC, some guiding values for its design have been
153+
articulated:
154+
155+
- Development on Rust-generated WebAssembly projects should allow developers to
156+
use the development environment they are most comfortable with. Developers
157+
writing Rust should get to use Rust, and developers using JavaScript should
158+
get to use a JS based runtime environment (Node.js, Chakra, etc).
159+
160+
- JavaScript tooling and workflows should be usable with Rust-generated
161+
WebAssembly projects. For example, bundlers like WebPack and Parcel, or
162+
dependency management tools such as `npm audit` and GreenKeeper.
163+
164+
- When possible, decisions should be made that allow the solution to be
165+
available to developers of not just Rust, but also C, and C++.
166+
167+
- Decisions should be focused on creating workflows that allow developers an
168+
easy learning curve and productive development experience.
169+
170+
These principles lead to the above proposal of using `package.json` to declare
171+
NPM dependencies which is then grouped together by `wasm-bindgen` to be
172+
published by `wasm-pack`. By using `package.json` we get inherent compatibility
173+
with existing workflows like GreenKeeper and `npm install`. Additionally
174+
`package.json` is very well documented and supported throughout the JS ecosystem
175+
making it very familiar.
176+
177+
Some other alternatives to this RFC which have been ruled out are:
178+
179+
* **Using `Cargo.toml` instead of `package.json`** to declare NPM dependencies.
180+
For example we could use:
181+
182+
```toml
183+
[package.metadata.npm.dependencies]
184+
foo = "0.1"
185+
```
186+
187+
This has the drawback though of being incompatible with all existing workflows
188+
around `package.json`. Additionally it also highlights a discrepancy between
189+
NPM and Cargo and how `"0.1"` as a version requirement is interpreted (e.g.
190+
`^0.1` or `~0.1`).
191+
192+
* **Adding a separate manifest file** instead of using `package.json` is also
193+
possibility and might be easier for `wasm-bindgen` to read and later
194+
parse/include. This has a possible benefit of being scoped to exactly our use
195+
case and not being misleading by disallowing otherwise-valid fields of
196+
`package.json`. The downside of this approach is the same as `Cargo.toml`,
197+
however, in that it's an unfamiliar format to most and is incompatible with
198+
existing tooling without bringing too much benefit.
199+
200+
* **Annotating version dependencies inline** could be used rather than
201+
`package.json` as well, such as:
202+
203+
```rust
204+
#[wasm_bindgen(module = "foo", version = "0.1")]
205+
extern "C" {
206+
// ...
207+
}
208+
```
209+
210+
As with all other alternatives this is incompatible with existing tooling, but
211+
it's also not aligned with Rust's own mechanism for declaring dependencies
212+
which separates the location for version information and the code iteslf.
213+
214+
# Unresolved Questions
215+
[unresolved]: #unresolved-questions
216+
217+
* Is the MVP restriction of only using `dependencies` too limiting? Should more
218+
fields be supported in `package.json`?

0 commit comments

Comments
 (0)