From 09e0e6d28b769141fde6100b6785dfdb40d2fe8f Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 4 Aug 2024 22:58:52 +0200 Subject: [PATCH] Add experimental Node module output target (#4027) --- CHANGELOG.md | 3 + crates/cli-support/src/js/mod.rs | 124 +++++++++--------- crates/cli-support/src/lib.rs | 68 ++++------ .../src/bin/wasm-bindgen-test-runner/main.rs | 18 ++- .../src/bin/wasm-bindgen-test-runner/node.rs | 25 +++- crates/cli/src/bin/wasm-bindgen.rs | 3 +- crates/js-sys/tests/browser.rs | 11 ++ crates/js-sys/tests/{ => common}/headless.js | 0 .../tests/{headless.rs => common/mod.rs} | 10 +- crates/js-sys/tests/node.rs | 11 ++ crates/test/src/lib.rs | 6 + guide/src/reference/deployment.md | 19 ++- guide/src/wasm-bindgen-test/browsers.md | 3 + 13 files changed, 181 insertions(+), 120 deletions(-) create mode 100644 crates/js-sys/tests/browser.rs rename crates/js-sys/tests/{ => common}/headless.js (100%) rename crates/js-sys/tests/{headless.rs => common/mod.rs} (81%) create mode 100644 crates/js-sys/tests/node.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f1f1e8cee..014f3e6f8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ * Added bindings to `NavigatorOptions.vibrate`. [#4041](https://github.com/rustwasm/wasm-bindgen/pull/4041) +* Added an experimental Node.JS ES module target, in comparison the current `node` target uses CommonJS, with `--target experimental-nodejs-module` or when testing with `wasm_bindgen_test_configure!(run_in_node_experimental)`. + [#4027](https://github.com/rustwasm/wasm-bindgen/pull/4027) + ### Changed * Stabilize Web Share API. diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 1fae3de1454..759ddcc4678 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -142,9 +142,7 @@ impl<'a> Context<'a> { self.globals.push_str(c); } let global = match self.config.mode { - OutputMode::Node { - experimental_modules: false, - } => { + OutputMode::Node { module: false } => { if contents.starts_with("class") { format!("{}\nmodule.exports.{1} = {1};\n", contents, export_name) } else { @@ -159,9 +157,7 @@ impl<'a> Context<'a> { } } OutputMode::Bundler { .. } - | OutputMode::Node { - experimental_modules: true, - } + | OutputMode::Node { module: true } | OutputMode::Web | OutputMode::Deno => { if let Some(body) = contents.strip_prefix("function") { @@ -217,26 +213,29 @@ impl<'a> Context<'a> { let mut shim = String::new(); - shim.push_str("let imports = {};\n"); + shim.push_str("\nlet imports = {};\n"); - if self.config.mode.nodejs_experimental_modules() { + if self.config.mode.uses_es_modules() { for (i, module) in imports.iter().enumerate() { if module.as_str() != PLACEHOLDER_MODULE { shim.push_str(&format!("import * as import{} from '{}';\n", i, module)); } } - } - - for (i, module) in imports.iter().enumerate() { - if module.as_str() == PLACEHOLDER_MODULE { - shim.push_str(&format!( - "imports['{0}'] = module.exports;\n", - PLACEHOLDER_MODULE - )); - } else if self.config.mode.nodejs_experimental_modules() { - shim.push_str(&format!("imports['{}'] = import{};\n", module, i)); - } else { - shim.push_str(&format!("imports['{0}'] = require('{0}');\n", module)); + for (i, module) in imports.iter().enumerate() { + if module.as_str() != PLACEHOLDER_MODULE { + shim.push_str(&format!("imports['{}'] = import{};\n", module, i)); + } + } + } else { + for module in imports.iter() { + if module.as_str() == PLACEHOLDER_MODULE { + shim.push_str(&format!( + "imports['{0}'] = module.exports;\n", + PLACEHOLDER_MODULE + )); + } else { + shim.push_str(&format!("imports['{0}'] = require('{0}');\n", module)); + } } } @@ -246,17 +245,16 @@ impl<'a> Context<'a> { fn generate_node_wasm_loading(&self, path: &Path) -> String { let mut shim = String::new(); - if self.config.mode.nodejs_experimental_modules() { + if self.config.mode.uses_es_modules() { // On windows skip the leading `/` which comes out when we parse a // url to use `C:\...` instead of `\C:\...` shim.push_str(&format!( " - import * as path from 'path'; - import * as fs from 'fs'; - import * as url from 'url'; - import * as process from 'process'; + import * as path from 'node:path'; + import * as fs from 'node:fs'; + import * as process from 'node:process'; - let file = path.dirname(url.parse(import.meta.url).pathname); + let file = path.dirname(new URL(import.meta.url).pathname); if (process.platform === 'win32') {{ file = file.substring(1); }} @@ -264,6 +262,14 @@ impl<'a> Context<'a> { ", path.file_name().unwrap().to_str().unwrap() )); + shim.push_str( + " + const wasmModule = new WebAssembly.Module(bytes); + const wasmInstance = new WebAssembly.Instance(wasmModule, imports); + const wasm = wasmInstance.exports; + export const __wasm = wasm; + ", + ); } else { shim.push_str(&format!( " @@ -272,17 +278,16 @@ impl<'a> Context<'a> { ", path.file_name().unwrap().to_str().unwrap() )); + shim.push_str( + " + const wasmModule = new WebAssembly.Module(bytes); + const wasmInstance = new WebAssembly.Instance(wasmModule, imports); + wasm = wasmInstance.exports; + module.exports.__wasm = wasm; + ", + ); } - shim.push_str( - " - const wasmModule = new WebAssembly.Module(bytes); - const wasmInstance = new WebAssembly.Instance(wasmModule, imports); - wasm = wasmInstance.exports; - module.exports.__wasm = wasm; - ", - ); - reset_indentation(&shim) } @@ -400,9 +405,7 @@ impl<'a> Context<'a> { // With normal CommonJS node we need to defer requiring the wasm // until the end so most of our own exports are hooked up - OutputMode::Node { - experimental_modules: false, - } => { + OutputMode::Node { module: false } => { js.push_str(&self.generate_node_imports()); js.push_str("let wasm;\n"); @@ -442,13 +445,10 @@ impl<'a> Context<'a> { } } - // With Bundlers and modern ES6 support in Node we can simply import - // the wasm file as if it were an ES module and let the - // bundler/runtime take care of it. - OutputMode::Bundler { .. } - | OutputMode::Node { - experimental_modules: true, - } => { + // With Bundlers we can simply import the wasm file as if it were an ES module + // and let the bundler/runtime take care of it. + // With Node we manually read the wasm file from the filesystem and instantiate it. + OutputMode::Bundler { .. } | OutputMode::Node { module: true } => { for (id, js) in crate::sorted_iter(&self.wasm_import_definitions) { let import = self.module.imports.get_mut(*id); import.module = format!("./{}_bg.js", module_name); @@ -475,8 +475,18 @@ impl<'a> Context<'a> { ", ); + if matches!(self.config.mode, OutputMode::Node { module: true }) { + let start = start.get_or_insert_with(String::new); + start.push_str(&self.generate_node_imports()); + start.push_str(&self.generate_node_wasm_loading(Path::new(&format!( + "./{}_bg.wasm", + module_name + )))); + } if needs_manual_start { - start = Some("\nwasm.__wbindgen_start();\n".to_string()); + start + .get_or_insert_with(String::new) + .push_str("\nwasm.__wbindgen_start();\n"); } } @@ -509,7 +519,9 @@ impl<'a> Context<'a> { // Emit all the JS for importing all our functionality assert!( !self.config.mode.uses_es_modules() || js.is_empty(), - "ES modules require imports to be at the start of the file" + "ES modules require imports to be at the start of the file, but we \ + generated some JS before the imports: {}", + js ); let mut push_with_newline = |s| { @@ -556,9 +568,7 @@ impl<'a> Context<'a> { } } - OutputMode::Node { - experimental_modules: false, - } => { + OutputMode::Node { module: false } => { for (module, items) in crate::sorted_iter(&self.js_imports) { imports.push_str("const { "); for (i, (item, rename)) in items.iter().enumerate() { @@ -582,9 +592,7 @@ impl<'a> Context<'a> { } OutputMode::Bundler { .. } - | OutputMode::Node { - experimental_modules: true, - } + | OutputMode::Node { module: true } | OutputMode::Web | OutputMode::Deno => { for (module, items) in crate::sorted_iter(&self.js_imports) { @@ -3216,12 +3224,10 @@ impl<'a> Context<'a> { OutputMode::Web | OutputMode::Bundler { .. } | OutputMode::Deno - | OutputMode::Node { - experimental_modules: true, - } => "import.meta.url", - OutputMode::Node { - experimental_modules: false, - } => "require('url').pathToFileURL(__filename)", + | OutputMode::Node { module: true } => "import.meta.url", + OutputMode::Node { module: false } => { + "require('url').pathToFileURL(__filename)" + } OutputMode::NoModules { .. } => { prelude.push_str( "if (script_src === undefined) { diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 37428bd20fd..53270eb6eda 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -66,7 +66,7 @@ enum OutputMode { Bundler { browser_only: bool }, Web, NoModules { global: String }, - Node { experimental_modules: bool }, + Node { module: bool }, Deno, } @@ -154,23 +154,16 @@ impl Bindgen { pub fn nodejs(&mut self, node: bool) -> Result<&mut Bindgen, Error> { if node { - self.switch_mode( - OutputMode::Node { - experimental_modules: false, - }, - "--target nodejs", - )?; + self.switch_mode(OutputMode::Node { module: false }, "--target nodejs")?; } Ok(self) } - pub fn nodejs_experimental_modules(&mut self, node: bool) -> Result<&mut Bindgen, Error> { + pub fn nodejs_module(&mut self, node: bool) -> Result<&mut Bindgen, Error> { if node { self.switch_mode( - OutputMode::Node { - experimental_modules: true, - }, - "--nodejs-experimental-modules", + OutputMode::Node { module: true }, + "--target experimental-nodejs-module", )?; } Ok(self) @@ -548,22 +541,11 @@ impl OutputMode { self, OutputMode::Bundler { .. } | OutputMode::Web - | OutputMode::Node { - experimental_modules: true, - } + | OutputMode::Node { module: true } | OutputMode::Deno ) } - fn nodejs_experimental_modules(&self) -> bool { - match self { - OutputMode::Node { - experimental_modules, - } => *experimental_modules, - _ => false, - } - } - fn nodejs(&self) -> bool { matches!(self, OutputMode::Node { .. }) } @@ -579,10 +561,7 @@ impl OutputMode { fn esm_integration(&self) -> bool { matches!( self, - OutputMode::Bundler { .. } - | OutputMode::Node { - experimental_modules: true, - } + OutputMode::Bundler { .. } | OutputMode::Node { module: true } ) } } @@ -687,11 +666,7 @@ impl Output { // And now that we've got all our JS and TypeScript, actually write it // out to the filesystem. - let extension = if gen.mode.nodejs_experimental_modules() { - "mjs" - } else { - "js" - }; + let extension = "js"; fn write(path: P, contents: C) -> Result<(), anyhow::Error> where @@ -709,17 +684,30 @@ impl Output { let start = gen.start.as_deref().unwrap_or(""); - write( - &js_path, - format!( - "import * as wasm from \"./{wasm_name}.wasm\"; + if matches!(gen.mode, OutputMode::Node { .. }) { + write( + &js_path, + format!( + " +import {{ __wbg_set_wasm }} from \"./{js_name}\"; +{start} +__wbg_set_wasm(wasm); +export * from \"./{js_name}\";", + ), + )?; + } else { + write( + &js_path, + format!( + " +import * as wasm from \"./{wasm_name}.wasm\"; import {{ __wbg_set_wasm }} from \"./{js_name}\"; __wbg_set_wasm(wasm); export * from \"./{js_name}\"; {start}" - ), - )?; - + ), + )?; + } write(out_dir.join(&js_name), reset_indentation(&gen.js))?; } else { write(&js_path, reset_indentation(&gen.js))?; diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs index 06d386119ed..585f8aa34ca 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs @@ -27,7 +27,7 @@ mod shell; #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum TestMode { - Node, + Node { no_modules: bool }, Deno, Browser { no_modules: bool }, DedicatedWorker { no_modules: bool }, @@ -45,9 +45,9 @@ impl TestMode { fn no_modules(self) -> bool { match self { - Self::Node => true, Self::Deno => true, Self::Browser { no_modules } + | Self::Node { no_modules } | Self::DedicatedWorker { no_modules } | Self::SharedWorker { no_modules } | Self::ServiceWorker { no_modules } => no_modules, @@ -150,16 +150,19 @@ fn main() -> anyhow::Result<()> { Some(section) if section.data.contains(&0x04) => TestMode::ServiceWorker { no_modules: std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok(), }, + Some(section) if section.data.contains(&0x05) => TestMode::Node { + no_modules: std::env::var("WASM_BINDGEN_USE_NO_MODULE").is_ok(), + }, Some(_) => bail!("invalid __wasm_bingen_test_unstable value"), None if std::env::var("WASM_BINDGEN_USE_DENO").is_ok() => TestMode::Deno, - None => TestMode::Node, + None => TestMode::Node { no_modules: true }, }; let headless = env::var("NO_HEADLESS").is_err(); let debug = env::var("WASM_BINDGEN_NO_DEBUG").is_err(); // Gracefully handle requests to execute only node or only web tests. - let node = test_mode == TestMode::Node; + let node = matches!(test_mode, TestMode::Node { .. }); if env::var_os("WASM_BINDGEN_TEST_ONLY_NODE").is_some() && !node { println!( @@ -200,7 +203,8 @@ fn main() -> anyhow::Result<()> { shell.status("Executing bindgen..."); let mut b = Bindgen::new(); match test_mode { - TestMode::Node => b.nodejs(true)?, + TestMode::Node { no_modules: true } => b.nodejs(true)?, + TestMode::Node { no_modules: false } => b.nodejs_module(true)?, TestMode::Deno => b.deno(true)?, TestMode::Browser { .. } | TestMode::DedicatedWorker { .. } @@ -229,7 +233,9 @@ fn main() -> anyhow::Result<()> { let args: Vec<_> = args.collect(); match test_mode { - TestMode::Node => node::execute(module, &tmpdir, &args, &tests)?, + TestMode::Node { no_modules } => { + node::execute(module, &tmpdir, &args, &tests, !no_modules)? + } TestMode::Deno => deno::execute(module, &tmpdir, &args, &tests)?, TestMode::Browser { .. } | TestMode::DedicatedWorker { .. } diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs index 0e1718119ae..2bc9f2ae640 100644 --- a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs @@ -43,11 +43,12 @@ pub fn execute( tmpdir: &Path, args: &[OsString], tests: &[String], + module_format: bool, ) -> Result<(), Error> { let mut js_to_execute = format!( r#" - const {{ exit }} = require('process'); - const wasm = require("./{0}"); + {exit}; + {wasm}; {console_override} @@ -67,7 +68,16 @@ pub fn execute( const tests = []; "#, - module, + wasm = if !module_format { + format!(r"const wasm = require('./{0}.js')", module) + } else { + format!(r"import * as wasm from './{0}.js'", module) + }, + exit = if !module_format { + r"const { exit } = require('node:process')".to_string() + } else { + r"import { exit } from 'node:process'".to_string() + }, console_override = SHARED_SETUP, ); @@ -88,7 +98,14 @@ pub fn execute( ", ); - let js_path = tmpdir.join("run.js"); + let js_path = if module_format { + // fixme: this is a hack to make node understand modules + let package_json = tmpdir.join("package.json"); + fs::write(&package_json, r#"{"type": "module"}"#).unwrap(); + tmpdir.join("run.mjs") + } else { + tmpdir.join("run.cjs") + }; fs::write(&js_path, js_to_execute).context("failed to write JS file")?; // Augment `NODE_PATH` so things like `require("tests/my-custom.js")` work diff --git a/crates/cli/src/bin/wasm-bindgen.rs b/crates/cli/src/bin/wasm-bindgen.rs index b62449392c9..ee33c8de036 100644 --- a/crates/cli/src/bin/wasm-bindgen.rs +++ b/crates/cli/src/bin/wasm-bindgen.rs @@ -18,7 +18,7 @@ Options: --out-dir DIR Output directory --out-name VAR Set a custom output filename (Without extension. Defaults to crate name) --target TARGET What type of output to generate, valid - values are [web, bundler, nodejs, no-modules, deno], + values are [web, bundler, nodejs, no-modules, deno, experimental-nodejs-module], and the default is [bundler] --no-modules-global VAR Name of the global variable to initialize --browser Hint that JS should only be compatible with a browser @@ -109,6 +109,7 @@ fn rmain(args: &Args) -> Result<(), Error> { "no-modules" => b.no_modules(true)?, "nodejs" => b.nodejs(true)?, "deno" => b.deno(true)?, + "experimental-nodejs-module" => b.nodejs_module(true)?, s => bail!("invalid encode-into mode: `{}`", s), }; } diff --git a/crates/js-sys/tests/browser.rs b/crates/js-sys/tests/browser.rs new file mode 100644 index 00000000000..38b55e9792c --- /dev/null +++ b/crates/js-sys/tests/browser.rs @@ -0,0 +1,11 @@ +#![cfg(target_arch = "wasm32")] + +extern crate js_sys; +extern crate wasm_bindgen; +extern crate wasm_bindgen_test; + +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +pub mod common; diff --git a/crates/js-sys/tests/headless.js b/crates/js-sys/tests/common/headless.js similarity index 100% rename from crates/js-sys/tests/headless.js rename to crates/js-sys/tests/common/headless.js diff --git a/crates/js-sys/tests/headless.rs b/crates/js-sys/tests/common/mod.rs similarity index 81% rename from crates/js-sys/tests/headless.rs rename to crates/js-sys/tests/common/mod.rs index aafa6b0ae2a..b5cdd579c76 100644 --- a/crates/js-sys/tests/headless.rs +++ b/crates/js-sys/tests/common/mod.rs @@ -1,16 +1,8 @@ -#![cfg(target_arch = "wasm32")] - -extern crate js_sys; -extern crate wasm_bindgen; -extern crate wasm_bindgen_test; - use js_sys::Array; use wasm_bindgen::prelude::*; use wasm_bindgen_test::*; -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen(module = "/tests/headless.js")] +#[wasm_bindgen(module = "/tests/common/headless.js")] extern "C" { fn is_array_values_supported() -> bool; } diff --git a/crates/js-sys/tests/node.rs b/crates/js-sys/tests/node.rs new file mode 100644 index 00000000000..d4d5d1139c9 --- /dev/null +++ b/crates/js-sys/tests/node.rs @@ -0,0 +1,11 @@ +#![cfg(target_arch = "wasm32")] + +extern crate js_sys; +extern crate wasm_bindgen; +extern crate wasm_bindgen_test; + +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_node_experimental); + +pub mod common; diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 6b4174b1d77..d05d46f5785 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -76,6 +76,12 @@ macro_rules! wasm_bindgen_test_configure { pub static __WBG_TEST_RUN_IN_SERVICE_WORKER: [u8; 1] = [0x04]; $crate::wasm_bindgen_test_configure!($($others)*); ); + (run_in_node_experimental $($others:tt)*) => ( + #[link_section = "__wasm_bindgen_test_unstable"] + #[cfg(target_arch = "wasm32")] + pub static __WBG_TEST_run_in_node_experimental: [u8; 1] = [0x05]; + $crate::wasm_bindgen_test_configure!($($others)*); + ); () => () } diff --git a/guide/src/reference/deployment.md b/guide/src/reference/deployment.md index d0530941a55..aa66e8d6f65 100644 --- a/guide/src/reference/deployment.md +++ b/guide/src/reference/deployment.md @@ -12,9 +12,10 @@ The methods of deployment and integration here are primarily tied to the |-----------------|------------------------------------------------------------| | [`bundler`] | Suitable for loading in bundlers like Webpack | | [`web`] | Directly loadable in a web browser | -| [`nodejs`] | Loadable via `require` as a Node.js module | +| [`nodejs`] | Loadable via `require` as a Node.js CommonJS module | | [`deno`] | Loadable using imports from Deno modules | | [`no-modules`] | Like `web`, but older and doesn't use ES modules | +| [`experimental-nodejs-module`] | Loadable via `import` as a Node.js ESM module. | [`bundler`]: #bundlers [`web`]: #without-a-bundler @@ -87,6 +88,22 @@ as it has a JS shim generated as well). Note that this method requires a version of Node.js with WebAssembly support, which is currently Node 8 and above. +## Node.js Module + +**`--target experemintal-nodejs-module`** + +If you're deploying WebAssembly into Node.js as a JavaScript module, +then you'll want to pass the `--target experimental-nodejs-module` flag to `wasm-bindgen`. + +Like the "node" strategy, this method of deployment does not +require any further postprocessing. The generated JS shims can be `import`ed +just like any other Node module. + +Note that this method requires a version of Node.js with WebAssembly and module support, +which is currently Node 12 and above. + +**Currently experimental. Target is expected to be changed before stabilization.** + ## Deno **`--target deno`** diff --git a/guide/src/wasm-bindgen-test/browsers.md b/guide/src/wasm-bindgen-test/browsers.md index 5477e6b4d2f..7028575efc3 100644 --- a/guide/src/wasm-bindgen-test/browsers.md +++ b/guide/src/wasm-bindgen-test/browsers.md @@ -23,6 +23,8 @@ wasm_bindgen_test_configure!(run_in_dedicated_worker); wasm_bindgen_test_configure!(run_in_shared_worker); // Or run in service worker. wasm_bindgen_test_configure!(run_in_service_worker); +// Or run in Node.js but as an ES module. +wasm_bindgen_test_configure!(run_in_node_experimental); ``` Note that although a particular test crate must target either headless browsers @@ -33,6 +35,7 @@ project by using multiple test crates. For example: $MY_CRATE/ `-- tests |-- node.rs # The tests in this suite use the default Node.js. + |-- node_module.rs # The tests in this suite are configured for Node.js but as an ES module. |-- dedicated_worker.rs # The tests in this suite are configured for dedicated workers. |-- shared_worker.rs # The tests in this suite are configured for shared workers. |-- service_worker.rs # The tests in this suite are configured for service workers.