Skip to content

Commit

Permalink
feat: allow accessing exported Rust functions in JavaScript
Browse files Browse the repository at this point in the history
  • Loading branch information
ctron committed Nov 29, 2023
1 parent 0d7784f commit 0e9981e
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 127 deletions.
27 changes: 19 additions & 8 deletions site/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ cargo install --locked wasm-bindgen-cli
```

## App Setup

Get setup with your favorite `wasm-bindgen` based framework. [Yew](https://github.com/yewstack/yew) & [Seed](https://github.com/seed-rs/seed) are the most popular options today, but there are others. Trunk will work with any `wasm-bindgen` based framework. The easiest way to ensure that your application launches properly is to [setup your app as an executable](https://doc.rust-lang.org/cargo/guide/project-layout.html) with a standard `main` function:

```rust
Expand All @@ -85,7 +86,7 @@ Trunk uses a source HTML file to drive all asset building and bundling. Trunk al
</html>
```

`trunk build` will produce the following HTML at `dist/index.html`, along with the compiled scss, WASM & the JS loader for the WASM:
`trunk-ng build` will produce the following HTML at `dist/index.html`, along with the compiled scss, WASM & the JS loader for the WASM:

```html
<html>
Expand All @@ -96,7 +97,8 @@ Trunk uses a source HTML file to drive all asset building and bundling. Trunk al
</head>
<body>
<script type="module">
import init from '/index-7eeee8fa37b7636a.js';
import init, * as bindings from '/index-7eeee8fa37b7636a.js';
window.wasmBindings = bindings;
init('/index-7eeee8fa37b7636a_bg.wasm');
</script>
</body>
Expand All @@ -105,20 +107,29 @@ Trunk uses a source HTML file to drive all asset building and bundling. Trunk al

The contents of your `dist` dir are now ready to be served on the web.

## JavaScript interoperability

Trunk will create the necessary JavaScript code to bootstrap and run the WebAssembly based application. It will also
include all JavaScript snippets generated by `wasm-bindgen` for interfacing with JavaScript functionality.

By default, functions exported from Rust, using `wasm-bingen`, can be accessed in the JavaScript code through the global
variable `window.wasmBindings`. This behavior can be disabled, and the name can be customized.

# Next Steps

That's not all! Trunk has even more useful features. Head on over to the following sections to learn more about how to use Trunk effectively.

- [Assets](@/assets.md): learn about all of Trunk's supported asset types.
- [Configuration](@/configuration.md): learn about Trunk's configuration system and how to use the Trunk proxy.
- [Commands](@/commands.md): learn about Trunk's CLI commands for use in your development workflows.
- Join us on Discord by following this link <a href="https://discord.gg/JEPdBujTDr"><img src="https://img.shields.io/discord/793890238267260958?logo=discord&style=flat-square" style="vertical-align:text-top;" alt="Discord Chat"/></a>
- Join us on Discord by following this link [![](https://img.shields.io/discord/793890238267260958?logo=discord&style=flat-square "Discord Chat")](https://discord.gg/JEPdBujTDr)

# Contributing

Anyone and everyone is welcome to contribute! Please review the [CONTRIBUTING.md](https://github.com/thedodd/trunk/blob/master/CONTRIBUTING.md) document for more details. The best way to get started is to find an open issue, and then start hacking on implementing it. Letting other folks know that you are working on it, and sharing progress is a great approach. Open pull requests early and often, and please use GitHub's draft pull request feature.

# License
<p>
<span><img src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue?style=flat-square" alt="license badge"/></span>
<br/>
trunk is licensed under the terms of the MIT License or the Apache License 2.0, at your choosing.
</p>

![](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue?style=flat-square "license badge")

trunk-ng (as well as trunk) is licensed under the terms of the MIT License or the Apache License 2.0, at your choosing.
2 changes: 2 additions & 0 deletions site/content/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ This will typically look like: `<link data-trunk rel="{type}" href="{path}" ..ot
- `data-loader-shim`: (optional) instruct `trunk` to create a loader shim for web workers. Defaults to false.
- `data-cross-origin`: (optional) the `crossorigin` setting when loading the code & script resources. Defaults to plain `anonymous`.
- `data-integrity`: (optional) the `integrity` digest type for code & script resources. Defaults to plain `sha384`.
- `data-wasm-no-import`: (optional) by default, Trunk will generate an import of functions exported from Rust. Enabling this flag disables this feature. Defaults to false.
- `data-wasm-import-name`: (optional) the name of the global variable where the functions imported from WASM will be available (under the `window` object). Defaults to `wasmBindings` (which makes them available via `window.wasmBindings.<functionName>`).

## sass/scss

Expand Down
141 changes: 22 additions & 119 deletions src/pipelines/rust.rs → src/pipelines/rust/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
//! Rust application pipeline.
mod output;

pub use output::RustAppOutput;

use super::{Attrs, TrunkAssetPipelineOutput, ATTR_HREF, SNIPPETS_DIR};
use crate::{
common::{self, copy_dir_recursive, path_exists},
Expand All @@ -10,7 +14,6 @@ use anyhow::{anyhow, bail, ensure, Context, Result};
use cargo_lock::Lockfile;
use cargo_metadata::camino::Utf8PathBuf;
use minify_js::TopLevelMode;
use nipper::Document;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::iter::Iterator;
Expand Down Expand Up @@ -64,6 +67,10 @@ pub struct RustApp {
cross_origin: CrossOrigin,
/// Subresource integrity setting
integrity: IntegrityType,
/// If exporting Rust functions should be imported
import_bindings: bool,
/// Name of the global variable holding the imported WASM bindings
import_bindings_name: Option<String>,
}

/// Describes how the rust application is used.
Expand Down Expand Up @@ -183,6 +190,13 @@ impl RustApp {
cfg.cargo_features.clone()
};

// bindings

let import_bindings = !attrs.get("data-wasm-no-import").is_some();
let import_bindings_name = attrs.get("data-wasm-import-name").cloned();

// done

Ok(Self {
id,
cfg,
Expand All @@ -201,6 +215,8 @@ impl RustApp {
loader_shim,
cross_origin,
integrity,
import_bindings,
import_bindings_name,
})
}

Expand Down Expand Up @@ -241,6 +257,8 @@ impl RustApp {
loader_shim: false,
cross_origin: Default::default(),
integrity: Default::default(),
import_bindings: true,
import_bindings_name: None,
}))
}

Expand Down Expand Up @@ -583,6 +601,8 @@ impl RustApp {
cross_origin: self.cross_origin,
integrity,
snippet_integrities,
import_bindings: self.import_bindings,
import_bindings_name: self.import_bindings_name.clone(),
})
}

Expand Down Expand Up @@ -612,6 +632,7 @@ impl RustApp {
fs::write(destination_path, write_bytes)
.await
.context("error writing JS loader file to stage dir")?;

Ok(())
}

Expand Down Expand Up @@ -715,124 +736,6 @@ fn find_wasm_bindgen_version<'a>(
.or_else(find_manifest)
}

/// The output of a cargo build pipeline.
pub struct RustAppOutput {
/// The runtime build config.
pub cfg: Arc<RtcBuild>,
/// The ID of this pipeline.
pub id: Option<usize>,
/// The filename of the generated JS loader file written to the dist dir.
pub js_output: String,
/// The filename of the generated WASM file written to the dist dir.
pub wasm_output: String,
/// The filename of the generated .ts file written to the dist dir.
pub ts_output: Option<String>,
/// The filename of the generated loader shim script for web workers written to the dist dir.
pub loader_shim_output: Option<String>,
/// Is this module main or a worker.
pub r#type: RustAppType,
/// The cross-origin setting for loading the resources
pub cross_origin: CrossOrigin,
/// The integrity and digest of the output, ignored in case of [`IntegrityType::None`]
pub integrity: IntegrityOutput,
/// The output digests for the discovered snippets
pub snippet_integrities: HashMap<String, OutputDigest>,
}

pub fn pattern_evaluate(template: &str, params: &HashMap<String, String>) -> String {
let mut result = template.to_string();
for (k, v) in params.iter() {
let pattern = format!("{{{}}}", k.as_str());
if let Some(file_path) = v.strip_prefix('@') {
if let Ok(contents) = std::fs::read_to_string(file_path) {
result = str::replace(result.as_str(), &pattern, contents.as_str());
}
} else {
result = str::replace(result.as_str(), &pattern, v);
}
}
result
}

impl RustAppOutput {
pub async fn finalize(self, dom: &mut Document) -> Result<()> {
if self.r#type == RustAppType::Worker {
// Skip the script tag and preload links for workers, and remove the link tag only.
// Workers are initialized and managed by the app itself at runtime.
if let Some(id) = self.id {
dom.select(&super::trunk_id_selector(id)).remove();
}
return Ok(());
}

if !self.cfg.inject_scripts {
// Configuration directed we do not inject any scripts.
return Ok(());
}

let (base, js, wasm, head, body) = (
&self.cfg.public_url,
&self.js_output,
&self.wasm_output,
"html head",
"html body",
);
let (pattern_script, pattern_preload) =
(&self.cfg.pattern_script, &self.cfg.pattern_preload);
let mut params: HashMap<String, String> = match &self.cfg.pattern_params {
Some(x) => x.clone(),
None => HashMap::new(),
};
params.insert("base".to_owned(), base.clone());
params.insert("js".to_owned(), js.clone());
params.insert("wasm".to_owned(), wasm.clone());
params.insert("crossorigin".to_owned(), self.cross_origin.to_string());

let preload = match pattern_preload {
Some(pattern) => pattern_evaluate(pattern, &params),
None => {
format!(
r#"
<link rel="preload" href="{base}{wasm}" as="fetch" type="application/wasm" crossorigin={cross_origin}{wasm_integrity}>
<link rel="modulepreload" href="{base}{js}" crossorigin={cross_origin}{js_integrity}>"#,
cross_origin = self.cross_origin,
wasm_integrity = self.integrity.wasm.make_attribute(),
js_integrity = self.integrity.js.make_attribute(),
)
}
};
dom.select(head).append_html(preload);

for (name, integrity) in self.snippet_integrities {
if let Some(integrity) = integrity.to_integrity_value() {
let preload = format!(
r#"
<link rel="modulepreload" href="{base}{name}" crossorigin={cross_origin} integrity="{integrity}">"#,
cross_origin = self.cross_origin,
);
dom.select(head).append_html(preload);
}
}

let script = match pattern_script {
Some(pattern) => pattern_evaluate(pattern, &params),
None => {
format!(
r#"
<script type="module">import init from '{base}{js}';init('{base}{wasm}');</script>"#,
)
}
};
match self.id {
Some(id) => dom
.select(&super::trunk_id_selector(id))
.replace_with_html(script),
None => dom.select(body).append_html(script),
}
Ok(())
}
}

/// Different optimization levels that can be configured with `wasm-opt`.
#[derive(PartialEq, Eq)]
enum WasmOptLevel {
Expand Down
Loading

0 comments on commit 0e9981e

Please sign in to comment.