use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::cli::{CliError, Result};
use crate::config::{Config, WasmNpmTarget};
struct WasmLoaderEntrypoint {
filename: &'static str,
source: String,
}
impl WasmLoaderEntrypoint {
fn browser(filename: &'static str, module_name: &str) -> Self {
Self {
filename,
source: format!(
r#"
import init from "./{module_name}.js";
export * from "./{module_name}.js";
export {{ default as init }} from "./{module_name}.js";
export const initialized = (async () => {{
const response = await fetch(new URL("./{module_name}_bg.wasm", import.meta.url));
await init(response);
}})();
"#
),
}
}
fn node(filename: &'static str, module_name: &str) -> Self {
Self {
filename,
source: format!(
r#"
export * from "./{module_name}_node.js";
export {{ default, initialized }} from "./{module_name}_node.js";
"#
),
}
}
fn write(self, output_directory: &Path) -> Result<()> {
let output_path = output_directory.join(self.filename);
std::fs::write(&output_path, self.source).map_err(|source| CliError::WriteFailed {
path: output_path,
source,
})
}
}
impl WasmNpmTarget {
fn loader_entrypoint(&self, module_name: &str) -> WasmLoaderEntrypoint {
match self {
Self::Bundler => WasmLoaderEntrypoint::browser("bundler.js", module_name),
Self::Web => WasmLoaderEntrypoint::browser("web.js", module_name),
Self::Nodejs => WasmLoaderEntrypoint::node("node.js", module_name),
}
}
}
pub(crate) fn generate_wasm_loader_entrypoints(
module_name: &str,
enabled_targets: &[WasmNpmTarget],
output_dir: &Path,
) -> Result<()> {
enabled_targets
.iter()
.map(|target| target.loader_entrypoint(module_name))
.try_for_each(|entrypoint| entrypoint.write(output_dir))
}
pub(crate) fn generate_wasm_package_json(
config: &Config,
module_name: &str,
enabled_targets: &[WasmNpmTarget],
output_dir: &Path,
) -> Result<PathBuf> {
let package_name = config
.wasm_npm_package_name()
.ok_or_else(|| CliError::CommandFailed {
command: "targets.wasm.npm.package_name is required for pack wasm".to_string(),
status: None,
})?;
let package_version = config
.wasm_npm_version()
.unwrap_or_else(|| "0.1.0".to_string());
let has_bundler = enabled_targets.contains(&WasmNpmTarget::Bundler);
let has_web = enabled_targets.contains(&WasmNpmTarget::Web);
let has_node = enabled_targets.contains(&WasmNpmTarget::Nodejs);
let default_entry = if has_bundler {
"./bundler.js"
} else if has_web {
"./web.js"
} else {
"./node.js"
};
let runtime_package = config.wasm_runtime_package();
let runtime_version = config.wasm_runtime_version();
let mut dependencies = BTreeMap::new();
dependencies.insert(runtime_package, runtime_version);
let package_json = WasmPackageJson {
name: package_name.to_string(),
version: package_version,
package_type: "module".to_string(),
exports: WasmPackageExports {
root: WasmPackageEntry {
types: format!("./{}.d.ts", module_name),
browser: has_web.then(|| "./web.js".to_string()),
node: has_node.then(|| "./node.js".to_string()),
default: default_entry.to_string(),
},
},
types: format!("./{}.d.ts", module_name),
files: vec![
format!("{}.js", module_name),
format!("{}.d.ts", module_name),
format!("{}_bg.wasm", module_name),
"bundler.js".to_string(),
"web.js".to_string(),
"node.js".to_string(),
],
dependencies,
license: config.wasm_npm_license(),
repository: config.wasm_npm_repository(),
};
let rendered =
serde_json::to_string_pretty(&package_json).map_err(|source| CliError::CommandFailed {
command: format!("failed to serialize package.json: {}", source),
status: None,
})?;
let package_json_path = output_dir.join("package.json");
std::fs::write(&package_json_path, rendered).map_err(|source| CliError::WriteFailed {
path: package_json_path.clone(),
source,
})?;
Ok(package_json_path)
}
pub(crate) fn generate_wasm_readme(
config: &Config,
module_name: &str,
enabled_targets: &[WasmNpmTarget],
output_dir: &Path,
) -> Result<PathBuf> {
let package_name = config.wasm_npm_package_name().unwrap_or(module_name);
let targets_text = enabled_targets
.iter()
.map(|target| match target {
WasmNpmTarget::Bundler => "bundler",
WasmNpmTarget::Web => "web",
WasmNpmTarget::Nodejs => "nodejs",
})
.collect::<Vec<_>>()
.join(", ");
let content = format!(
"# {package_name}\n\nGenerated by boltffi.\n\nEnabled wasm npm targets: {targets_text}\n\n```ts\nimport {{ initialized }} from \"{package_name}\";\nawait initialized;\n```\n"
);
let readme_path = output_dir.join("README.md");
std::fs::write(&readme_path, content).map_err(|source| CliError::WriteFailed {
path: readme_path.clone(),
source,
})?;
Ok(readme_path)
}
#[derive(Serialize)]
pub(crate) struct WasmPackageJson {
name: String,
version: String,
#[serde(rename = "type")]
package_type: String,
exports: WasmPackageExports,
types: String,
files: Vec<String>,
dependencies: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
license: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
repository: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct WasmPackageExports {
#[serde(rename = ".")]
root: WasmPackageEntry,
}
#[derive(Serialize)]
pub(crate) struct WasmPackageEntry {
types: String,
#[serde(skip_serializing_if = "Option::is_none")]
browser: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
node: Option<String>,
default: String,
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use super::generate_wasm_loader_entrypoints;
use crate::config::WasmNpmTarget;
fn unique_temp_dir(prefix: &str) -> PathBuf {
let unique_suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("{prefix}-{unique_suffix}"))
}
#[test]
fn writes_browser_loader_entrypoints_without_escaped_source_noise() {
let output_directory = unique_temp_dir("boltffi-wasm-browser-loader-test");
fs::create_dir_all(&output_directory).expect("create output directory");
generate_wasm_loader_entrypoints(
"demo",
&[WasmNpmTarget::Bundler, WasmNpmTarget::Web],
&output_directory,
)
.expect("browser loader generation should succeed");
let bundler_loader =
fs::read_to_string(output_directory.join("bundler.js")).expect("read bundler loader");
let web_loader =
fs::read_to_string(output_directory.join("web.js")).expect("read web loader");
assert!(bundler_loader.contains(r#"import init from "./demo.js";"#));
assert!(bundler_loader.contains(r#"await init(response);"#));
assert!(web_loader.contains(r#"new URL("./demo_bg.wasm", import.meta.url)"#));
fs::remove_dir_all(output_directory).expect("cleanup generated output");
}
#[test]
fn writes_node_loader_entrypoint_from_typed_target_mapping() {
let output_directory = unique_temp_dir("boltffi-wasm-node-loader-test");
fs::create_dir_all(&output_directory).expect("create output directory");
generate_wasm_loader_entrypoints("demo", &[WasmNpmTarget::Nodejs], &output_directory)
.expect("node loader generation should succeed");
let node_loader =
fs::read_to_string(output_directory.join("node.js")).expect("read node loader");
assert!(node_loader.contains(r#"export * from "./demo_node.js";"#));
assert!(node_loader.contains(r#"export { default, initialized } from "./demo_node.js";"#));
fs::remove_dir_all(output_directory).expect("cleanup generated output");
}
}