use std::{ env::{self, VarError}, fs::{self, File}, io::{Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, }; use anyhow::{Context, Result}; use clap::Parser; /// Default output dir passed to the internal build pipeline. /// /// Note: all filesystem access must be relative to the crate root discovered by /// `Build::try_from_opts` (i.e. `Build::out_dir`), NOT the process current-dir. const OUT_DIR: &str = "build"; const SHIM_FILE: &str = include_str!("./js/shim.js"); pub(crate) mod binary; mod build; mod build_lock; mod emoji; mod lockfile; mod main_legacy; mod versions; use build::{Build, BuildOptions}; use build_lock::BuildLock; use crate::{ binary::{Esbuild, GetBinary}, build::Target, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); fn fix_wasm_import(out_dir: &Path) -> Result<()> { let index_path = output_path(out_dir, "index.js"); let content = fs::read_to_string(&index_path) .with_context(|| format!("Failed to read {}", index_path.display()))?; let updated_content = content.replace("import source ", "import "); fs::write(&index_path, updated_content) .with_context(|| format!("Failed to write {}", index_path.display()))?; Ok(()) } fn update_package_json(out_dir: &Path) -> Result<()> { let package_json_path = output_path(out_dir, "package.json"); let original_content = fs::read_to_string(&package_json_path) .with_context(|| format!("Failed to read {}", package_json_path.display()))?; let mut package_json: serde_json::Value = serde_json::from_str(&original_content)?; package_json["files"] = serde_json::json!(["index_bg.wasm", "index.js", "index.d.ts"]); package_json["main"] = serde_json::Value::String("index.js".to_string()); package_json["sideEffects"] = serde_json::json!(["./index.js"]); let updated_content = serde_json::to_string_pretty(&package_json)?; fs::write(&package_json_path, updated_content) .with_context(|| format!("Failed to write {}", package_json_path.display()))?; Ok(()) } pub fn main() -> Result<()> { env_logger::init(); let args: Vec<_> = env::args().collect(); if args.len() > 1 && (args[1].as_str() == "--version" || args[1].as_str() == "-v") { println!("{VERSION}"); return Ok(()); } let no_panic_recovery = args.iter().any(|a| a == "--no-panic-recovery"); let wasm_pack_opts = parse_wasm_pack_opts(env::args().skip(1))?; let mut builder = Build::try_from_opts(wasm_pack_opts)?; // IMPORTANT: Build output is always relative to the crate root discovered by // `Build::try_from_opts`, not the process current working directory. let out_dir = builder.out_dir.clone(); // Acquire the build lock: waits for any concurrent build to finish, // then creates a fresh .tmp staging directory with a heartbeat thread. let lock = BuildLock::acquire(&out_dir)?; let staging_dir = lock.staging_dir().to_path_buf(); // Point the builder at the staging directory builder.out_dir = staging_dir.clone(); builder.init()?; let supports_reset_state = builder.supports_target_module_and_reset_state()?; let module_target = supports_reset_state && !no_panic_recovery && env::var("CUSTOM_SHIM").is_err(); if module_target { builder .extra_args .push("--experimental-reset-state-function".to_string()); builder.run()?; } else { if supports_reset_state { // Enable once we have DO bindings to offer an alternative // eprintln!("Using CUSTOM_SHIM will be deprecated in a future release."); } else { eprintln!("A newer version of wasm-bindgen is available. Update to use the latest workers-rs features."); } builder.target = Target::Bundler; builder.run()?; } let with_coredump = env::var("COREDUMP").is_ok(); if with_coredump { println!("Adding wasm coredump"); wasm_coredump(&staging_dir)?; } if module_target { let shim = SHIM_FILE .replace("$HANDLERS", &generate_handlers(&staging_dir)?) .replace( "$PANIC_CRITICAL_ERROR", if builder.panic_unwind { "" } else { "criticalError = true;" }, ); let shim_path = output_path(&staging_dir, "shim.js"); fs::write(&shim_path, shim) .with_context(|| format!("Failed to write {}", shim_path.display()))?; add_export_wrappers(&staging_dir)?; update_package_json(&staging_dir)?; let esbuild_path = Esbuild.get_binary(None)?.0; bundle(&staging_dir, &esbuild_path)?; fix_wasm_import(&staging_dir)?; remove_unused_files(&staging_dir)?; create_wrapper_alias(&staging_dir, false)?; } else { main_legacy::process(&staging_dir)?; create_wrapper_alias(&staging_dir, true)?; } // Swap staging entries into the real output directory and clean up. lock.finish()?; Ok(()) } fn generate_handlers(out_dir: &Path) -> Result { let index_path = output_path(out_dir, "index.js"); let content = fs::read_to_string(&index_path) .with_context(|| format!("Failed to read {}", index_path.display()))?; // Extract ESM function exports from the wasm-bindgen generated output. // This code is specialized to what wasm-bindgen outputs for ESM and is therefore // brittle to upstream changes. It is comprehensive to current output patterns though. // TODO: Convert this to Wasm binary exports analysis for entry point detection instead. let mut func_names = Vec::new(); for line in content.lines() { if let Some(rest) = line.strip_prefix("export function") { if let Some(bracket_pos) = rest.find("(") { let func_name = rest[..bracket_pos].trim(); // strip the exported function (we re-wrap all handlers) if !SYSTEM_FNS.contains(&func_name) { func_names.push(func_name); } } } else if let Some(rest) = line.strip_prefix("export {") { if let Some(as_pos) = rest.find(" as ") { let rest = &rest[as_pos + 4..]; if let Some(brace_pos) = rest.find("}") { let func_name = rest[..brace_pos].trim(); if !SYSTEM_FNS.contains(&func_name) { func_names.push(func_name); } } } } } let mut handlers = String::new(); for func_name in func_names { if func_name == "fetch" && env::var("RUN_TO_COMPLETION").is_ok() { handlers += "Entrypoint.prototype.fetch = async function fetch(request) { let response = exports.fetch(request, this.env, this.ctx); this.ctx.waitUntil(response); return response; } "; } else if func_name == "fetch" || func_name == "queue" || func_name == "scheduled" { // TODO: Switch these over to https://github.com/wasm-bindgen/wasm-bindgen/pull/4757 // once that lands. handlers += &format!( "Entrypoint.prototype.{func_name} = function {func_name} (arg) {{ return exports.{func_name}.call(this, arg, this.env, this.ctx); }} " ); } else { handlers += &format!("Entrypoint.prototype.{func_name} = exports.{func_name};\n"); } } Ok(handlers) } static SYSTEM_FNS: &[&str] = &["__wbg_reset_state", "setPanicHook"]; fn add_export_wrappers(out_dir: &Path) -> Result<()> { let index_path = output_path(out_dir, "index.js"); let content = fs::read_to_string(&index_path) .with_context(|| format!("Failed to read {}", index_path.display()))?; let mut class_names = Vec::new(); for line in content.lines() { if let Some(rest) = line.strip_prefix("export class ") { if let Some(brace_pos) = rest.find("{") { let class_name = rest[..brace_pos].trim(); class_names.push(class_name.to_string()); } } } let shim_path = output_path(out_dir, "shim.js"); let mut output = fs::read_to_string(&shim_path) .with_context(|| format!("Failed to read {}", shim_path.display()))?; for class_name in class_names { output.push_str(&format!( "export const {class_name} = new Proxy(exports.{class_name}, classProxyHooks);\n" )); } fs::write(&shim_path, output) .with_context(|| format!("Failed to write {}", shim_path.display()))?; Ok(()) } const INSTALL_HELP: &str = "In case you are missing the binary, you can install it using: `cargo install wasm-coredump-rewriter`"; fn wasm_coredump(out_dir: &Path) -> Result<()> { let coredump_flags = env::var("COREDUMP_FLAGS"); let coredump_flags: Vec<&str> = if let Ok(flags) = &coredump_flags { flags.split(' ').collect() } else { vec![] }; let mut child = Command::new("wasm-coredump-rewriter") .args(coredump_flags) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .map_err(|err| { anyhow::anyhow!("failed to spawn wasm-coredump-rewriter: {err}\n\n{INSTALL_HELP}.") })?; let input_filename = output_path(out_dir, "index.wasm"); let input_bytes = { let mut input = File::open(input_filename.clone()) .map_err(|err| anyhow::anyhow!("failed to open input file: {err}"))?; let mut input_bytes = Vec::new(); input .read_to_end(&mut input_bytes) .map_err(|err| anyhow::anyhow!("failed to open input file: {err}"))?; input_bytes }; { let child_stdin = child.stdin.as_mut().unwrap(); child_stdin .write_all(&input_bytes) .map_err(|err| anyhow::anyhow!("failed to write input file to rewriter: {err}"))?; // Close stdin to finish and avoid indefinite blocking } let output = child .wait_with_output() .map_err(|err| anyhow::anyhow!("failed to get rewriter's status: {err}"))?; if output.status.success() { // Open the input file again with truncate to write the output let mut f = fs::OpenOptions::new() .truncate(true) .write(true) .open(input_filename) .map_err(|err| anyhow::anyhow!("failed to open output file: {err}"))?; f.write_all(&output.stdout) .map_err(|err| anyhow::anyhow!("failed to write output file: {err}"))?; Ok(()) } else { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); Err(anyhow::anyhow!(format!( "failed to run Wasm coredump rewriter: {stdout}\n{stderr}" ))) } } fn create_wrapper_alias(out_dir: &Path, legacy: bool) -> Result<()> { let msg = if !legacy { "// Use index.js directly, this file provided for backwards compat // with former shim.mjs only. " } else { "" }; let path = if !legacy { "../index.js" } else { "./worker/shim.mjs" }; let shim_content = format!( "{msg}export * from '{path}'; export {{ default }} from '{path}'; " ); if !legacy { let worker_dir = output_path(out_dir, "worker"); fs::create_dir_all(&worker_dir) .with_context(|| format!("Failed to create directory {}", worker_dir.display()))?; let shim_path = output_path(out_dir, "worker/shim.mjs"); fs::write(&shim_path, shim_content) .with_context(|| format!("Failed to write {}", shim_path.display()))?; } else { let index_path = output_path(out_dir, "index.js"); fs::write(&index_path, shim_content) .with_context(|| format!("Failed to write {}", index_path.display()))?; } Ok(()) } #[derive(Parser)] struct BuildArgs { #[clap(flatten)] pub build_options: BuildOptions, } fn parse_wasm_pack_opts(args: I) -> Result where I: IntoIterator, { // This is done instead of explicitly constructing // BuildOptions to preserve the behavior of appending // arbitrary arguments in `args`. let mut build_args = vec![ env!("CARGO_BIN_NAME").to_owned(), "--no-typescript".to_owned(), "--target".to_owned(), "module".to_owned(), "--out-dir".to_owned(), OUT_DIR.to_owned(), "--out-name".to_owned(), "index".to_owned(), ]; build_args.extend(args); let command = BuildArgs::try_parse_from(build_args)?; Ok(command.build_options) } // Bundles the snippets and worker-related code into a single file. fn bundle(out_dir: &Path, esbuild_path: &Path) -> Result<()> { let no_minify = !matches!(env::var("NO_MINIFY"), Err(VarError::NotPresent)); let path = out_dir .canonicalize() .with_context(|| format!("Failed to resolve output directory {}", out_dir.display()))?; let esbuild_path = esbuild_path .canonicalize() .with_context(|| format!("Failed to resolve esbuild path {}", esbuild_path.display()))?; let mut command = Command::new(esbuild_path); command.args([ "--external:./index_bg.wasm", "--external:cloudflare:sockets", "--external:cloudflare:workers", "--format=esm", "--bundle", "./shim.js", "--outfile=index.js", "--allow-overwrite", ]); if !no_minify { command.arg("--minify"); } let exit_status = command.current_dir(path).spawn()?.wait()?; match exit_status.success() { true => Ok(()), false => anyhow::bail!("esbuild exited with status {exit_status}"), } } fn remove_unused_files(out_dir: &Path) -> Result<()> { let shim_path = output_path(out_dir, "shim.js"); std::fs::remove_file(&shim_path) .with_context(|| format!("Failed to remove {}", shim_path.display()))?; let snippets_path = output_path(out_dir, "snippets"); if snippets_path.exists() { std::fs::remove_dir_all(&snippets_path) .with_context(|| format!("Failed to remove {}", snippets_path.display()))?; } Ok(()) } pub fn output_path(out_dir: &Path, name: impl AsRef) -> PathBuf { out_dir.join(name.as_ref()) } #[cfg(test)] mod test { use super::parse_wasm_pack_opts; #[test] fn test_wasm_pack_args_build_arg() { let args = vec!["--release".to_owned()]; let result = parse_wasm_pack_opts(args); assert!(result.is_ok()); } }