diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 7703e2570f2..064e0137713 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -2,7 +2,7 @@ //! with all the added metadata necessary to generate WASM bindings //! for it. -use crate::Diagnostic; +use crate::{util::ShortHash, Diagnostic}; use proc_macro2::{Ident, Span}; use std::hash::{Hash, Hasher}; use wasm_bindgen_shared as shared; @@ -16,6 +16,8 @@ pub struct Program { pub exports: Vec, /// js -> rust interfaces pub imports: Vec, + /// linked-to modules + pub linked_modules: Vec, /// rust enums pub enums: Vec, /// rust structs @@ -36,8 +38,23 @@ impl Program { && self.typescript_custom_sections.is_empty() && self.inline_js.is_empty() } + + /// Name of the link function for a specific linked module + pub fn link_function_name(&self, idx: usize) -> String { + let hash = match &self.linked_modules[idx] { + ImportModule::Inline(idx, _) => ShortHash((1, &self.inline_js[*idx])).to_string(), + other => ShortHash((0, other)).to_string(), + }; + format!("__wbindgen_link_{}", hash) + } } +/// An abstract syntax tree representing a link to a module in Rust. +/// In contrast to Program, LinkToModule must expand to an expression. +/// linked_modules of the inner Program must contain exactly one element +/// whose link is produced by the expression. +pub struct LinkToModule(pub Program); + /// A rust to js interface. Allows interaction with rust objects/functions /// from javascript. #[cfg_attr(feature = "extra-traits", derive(Debug))] diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index b0ff20e7d27..e2d57f43373 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -129,6 +129,29 @@ impl TryToTokens for ast::Program { } } +impl TryToTokens for ast::LinkToModule { + fn try_to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostic> { + let mut program = TokenStream::new(); + self.0.try_to_tokens(&mut program)?; + let link_function_name = self.0.link_function_name(0); + let name = Ident::new(&link_function_name, Span::call_site()); + let abi_ret = quote! { ::Abi }; + let extern_fn = extern_fn(&name, &[], &[], &[], abi_ret); + (quote! { + { + #program + #extern_fn + + unsafe { + ::from_abi(#name()) + } + } + }) + .to_tokens(tokens); + Ok(()) + } +} + impl ToTokens for ast::Struct { fn to_tokens(&self, tokens: &mut TokenStream) { let name = &self.rust_name; @@ -1118,8 +1141,8 @@ impl TryToTokens for ast::ImportFunction { let import_name = &self.shim; let attrs = &self.function.rust_attrs; let arguments = &arguments; - let abi_arguments = &abi_arguments; - let abi_argument_names = &abi_argument_names; + let abi_arguments = &abi_arguments[..]; + let abi_argument_names = &abi_argument_names[..]; let doc_comment = &self.doc_comment; let me = if is_method { @@ -1144,23 +1167,13 @@ impl TryToTokens for ast::ImportFunction { // like rustc itself doesn't do great in that regard so let's just do // the best we can in the meantime. let extern_fn = respan( - quote! { - #[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))] - #(#attrs)* - #[link(wasm_import_module = "__wbindgen_placeholder__")] - extern "C" { - fn #import_name(#(#abi_arguments),*) -> #abi_ret; - } - - #[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))] - unsafe fn #import_name(#(#abi_arguments),*) -> #abi_ret { - #( - drop(#abi_argument_names); - )* - panic!("cannot call wasm-bindgen imported functions on \ - non-wasm targets"); - } - }, + extern_fn( + import_name, + attrs, + abi_arguments, + abi_argument_names, + abi_ret, + ), &self.rust_name, ); @@ -1399,6 +1412,32 @@ impl<'a, T: ToTokens> ToTokens for Descriptor<'a, T> { } } +fn extern_fn( + import_name: &Ident, + attrs: &[syn::Attribute], + abi_arguments: &[TokenStream], + abi_argument_names: &[Ident], + abi_ret: TokenStream, +) -> TokenStream { + quote! { + #[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))] + #(#attrs)* + #[link(wasm_import_module = "__wbindgen_placeholder__")] + extern "C" { + fn #import_name(#(#abi_arguments),*) -> #abi_ret; + } + + #[cfg(not(all(target_arch = "wasm32", not(target_os = "emscripten"))))] + unsafe fn #import_name(#(#abi_arguments),*) -> #abi_ret { + #( + drop(#abi_argument_names); + )* + panic!("cannot call wasm-bindgen imported functions on \ + non-wasm targets"); + } + } +} + /// Converts `span` into a stream of tokens, and attempts to ensure that `input` /// has all the appropriate span information so errors in it point to `span`. fn respan(input: TokenStream, span: &dyn ToTokens) -> TokenStream { diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index 41d3c8b773a..254d0e88537 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -146,6 +146,12 @@ fn shared_program<'a>( .iter() .map(|x| -> &'a str { &x }) .collect(), + linked_modules: prog + .linked_modules + .iter() + .enumerate() + .map(|(i, a)| shared_linked_module(&prog.link_function_name(i), a, intern)) + .collect::, _>>()?, local_modules: intern .files .borrow() @@ -249,6 +255,17 @@ fn shared_import<'a>(i: &'a ast::Import, intern: &'a Interner) -> Result( + name: &str, + i: &'a ast::ImportModule, + intern: &'a Interner, +) -> Result, Diagnostic> { + Ok(LinkedModule { + module: shared_module(i, intern)?, + link_function_name: intern.intern_str(name), + }) +} + fn shared_module<'a>( m: &'a ast::ImportModule, intern: &'a Interner, diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index a3112d6f3b9..5f0cb08abe3 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -384,7 +384,7 @@ impl<'a> Context<'a> { if (typeof document === 'undefined') { script_src = location.href; } else { - script_src = document.currentScript.src; + script_src = new URL(document.currentScript.src, location.href).toString(); }\n", ); js.push_str("let wasm;\n"); @@ -3143,6 +3143,41 @@ impl<'a> Context<'a> { assert!(!variadic); self.invoke_intrinsic(intrinsic, args, prelude) } + + AuxImport::LinkTo(path, content) => { + assert!(kind == AdapterJsImportKind::Normal); + assert!(!variadic); + assert_eq!(args.len(), 0); + if self.config.split_linked_modules { + let base = match self.config.mode { + OutputMode::Web + | OutputMode::Bundler { .. } + | OutputMode::Deno + | OutputMode::Node { + experimental_modules: true, + } => "import.meta.url", + OutputMode::Node { + experimental_modules: false, + } => "require('url').pathToFileURL(__filename)", + OutputMode::NoModules { .. } => "script_src", + }; + Ok(format!("new URL('{}', {}).toString()", path, base)) + } else { + if let Some(content) = content { + let mut escaped = String::with_capacity(content.len()); + content.chars().for_each(|c| match c { + '`' | '\\' | '$' => escaped.extend(['\\', c]), + _ => escaped.extend([c]), + }); + Ok(format!( + "\"data:application/javascript,\" + encodeURIComponent(`{escaped}`)" + )) + } else { + Err(anyhow!("wasm-bindgen needs to be invoked with `--split-linked-modules`, because \"{}\" cannot be embedded.\n\ + See https://rustwasm.github.io/wasm-bindgen/reference/cli.html#--split-linked-modules for details.", path)) + } + } + } } } diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 9da7f9ad5a1..1f8b446c2dd 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -46,6 +46,7 @@ pub struct Bindgen { multi_value: bool, wasm_interface_types: bool, encode_into: EncodeInto, + split_linked_modules: bool, } pub struct Output { @@ -120,6 +121,7 @@ impl Bindgen { wasm_interface_types, encode_into: EncodeInto::Test, omit_default_module_path: true, + split_linked_modules: true, } } @@ -304,6 +306,11 @@ impl Bindgen { self } + pub fn split_linked_modules(&mut self, split_linked_modules: bool) -> &mut Bindgen { + self.split_linked_modules = split_linked_modules; + self + } + pub fn generate>(&mut self, path: P) -> Result<(), Error> { self.generate_output()?.emit(path.as_ref()) } diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index cd8deace584..7986afb1c3a 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -1,3 +1,4 @@ +use crate::decode::LocalModule; use crate::descriptor::{Descriptor, Function}; use crate::descriptors::WasmBindgenDescriptorsSection; use crate::intrinsic::Intrinsic; @@ -340,6 +341,45 @@ impl<'a> Context<'a> { Ok(()) } + fn link_module( + &mut self, + id: ImportId, + module: &decode::ImportModule, + offset: usize, + local_modules: &[LocalModule], + inline_js: &[&str], + ) -> Result<(), Error> { + let descriptor = Function { + shim_idx: 0, + arguments: Vec::new(), + ret: Descriptor::String, + inner_ret: None, + }; + let id = self.import_adapter(id, descriptor, AdapterJsImportKind::Normal)?; + let (path, content) = match module { + decode::ImportModule::Named(n) => ( + format!("snippets/{}", n), + local_modules + .iter() + .find(|m| m.identifier == *n) + .map(|m| m.contents), + ), + decode::ImportModule::RawNamed(n) => (n.to_string(), None), + decode::ImportModule::Inline(idx) => ( + format!( + "snippets/{}/inline{}.js", + self.unique_crate_identifier, + *idx as usize + offset + ), + Some(inline_js[*idx as usize]), + ), + }; + self.aux + .import_map + .insert(id, AuxImport::LinkTo(path, content.map(str::to_string))); + Ok(()) + } + fn program(&mut self, program: decode::Program<'a>) -> Result<(), Error> { self.unique_crate_identifier = program.unique_crate_identifier; let decode::Program { @@ -352,9 +392,10 @@ impl<'a> Context<'a> { inline_js, unique_crate_identifier, package_json, + linked_modules, } = program; - for module in local_modules { + for module in &local_modules { // All local modules we find should be unique, but the same module // may have showed up in a few different blocks. If that's the case // all the same identifiers should have the same contents. @@ -373,6 +414,25 @@ impl<'a> Context<'a> { self.export(export)?; } + let offset = self + .aux + .snippets + .get(unique_crate_identifier) + .map(|s| s.len()) + .unwrap_or(0); + for module in linked_modules { + match self.function_imports.remove(module.link_function_name) { + Some((id, _)) => self.link_module( + id, + &module.module, + offset, + &local_modules[..], + &inline_js[..], + )?, + None => (), + } + } + // Register vendor prefixes for all types before we walk over all the // imports to ensure that if a vendor prefix is listed somewhere it'll // apply to all the imports. diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index 16f0cd2b39f..4c7eb523555 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -328,6 +328,12 @@ pub enum AuxImport { /// This is an intrinsic function expected to be implemented with a JS glue /// shim. Each intrinsic has its own expected signature and implementation. Intrinsic(Intrinsic), + + /// This is a function which returns a URL pointing to a specific file, + /// usually a JS snippet. The supplied path is relative to the JS glue shim. + /// The Option may contain the contents of the linked file, so it can be + /// embedded. + LinkTo(String, Option), } /// Values that can be imported verbatim to hook up to an import. diff --git a/crates/cli-support/src/wit/section.rs b/crates/cli-support/src/wit/section.rs index 112256193cb..bd1fb23a2e7 100644 --- a/crates/cli-support/src/wit/section.rs +++ b/crates/cli-support/src/wit/section.rs @@ -355,6 +355,9 @@ fn check_standard_import(import: &AuxImport) -> Result<(), Error> { AuxImport::Intrinsic(intrinsic) => { format!("wasm-bindgen specific intrinsic `{}`", intrinsic.name()) } + AuxImport::LinkTo(path, _) => { + format!("wasm-bindgen specific link function for `{}`", path) + } AuxImport::Closure { .. } => format!("creating a `Closure` wrapper"), }; bail!("import of {} requires JS glue", item); diff --git a/crates/cli/src/bin/wasm-bindgen.rs b/crates/cli/src/bin/wasm-bindgen.rs index ff16a908dda..f6e16a4fb8e 100644 --- a/crates/cli/src/bin/wasm-bindgen.rs +++ b/crates/cli/src/bin/wasm-bindgen.rs @@ -36,6 +36,8 @@ Options: --remove-name-section Remove the debugging `name` section of the file --remove-producers-section Remove the telemetry `producers` section --omit-default-module-path Don't add WebAssembly fallback imports in generated JavaScript + --split-linked-modules Split linked modules out into their own files. Recommended if possible. + If a bundler is used, it needs to be set up accordingly. --encode-into MODE Whether or not to use TextEncoder#encodeInto, valid values are [test, always, never] --nodejs Deprecated, use `--target nodejs` @@ -44,6 +46,8 @@ Options: --weak-refs Enable usage of the JS weak references proposal --reference-types Enable usage of WebAssembly reference types -V --version Print the version number of wasm-bindgen + +Additional documentation: https://rustwasm.github.io/wasm-bindgen/reference/cli.html "; #[derive(Debug, Deserialize)] @@ -70,6 +74,7 @@ struct Args { flag_encode_into: Option, flag_target: Option, flag_omit_default_module_path: bool, + flag_split_linked_modules: bool, arg_input: Option, } @@ -123,7 +128,8 @@ fn rmain(args: &Args) -> Result<(), Error> { .remove_producers_section(args.flag_remove_producers_section) .typescript(typescript) .omit_imports(args.flag_omit_imports) - .omit_default_module_path(args.flag_omit_default_module_path); + .omit_default_module_path(args.flag_omit_default_module_path) + .split_linked_modules(args.flag_split_linked_modules); if let Some(true) = args.flag_weak_refs { b.weak_refs(true); } diff --git a/crates/cli/tests/wasm-bindgen/main.rs b/crates/cli/tests/wasm-bindgen/main.rs index 1fb94f7c97b..e854de23e51 100644 --- a/crates/cli/tests/wasm-bindgen/main.rs +++ b/crates/cli/tests/wasm-bindgen/main.rs @@ -263,7 +263,7 @@ fn default_module_path_target_no_modules() { if (typeof document === 'undefined') { script_src = location.href; } else { - script_src = document.currentScript.src; + script_src = new URL(document.currentScript.src, location.href).toString(); }", )); assert!(contents.contains( diff --git a/crates/futures/src/task/wait_async_polyfill.rs b/crates/futures/src/task/wait_async_polyfill.rs index 81b50d95259..68332d91a57 100644 --- a/crates/futures/src/task/wait_async_polyfill.rs +++ b/crates/futures/src/task/wait_async_polyfill.rs @@ -36,21 +36,12 @@ * when possible. The worker communicates with its parent using postMessage. */ -use js_sys::{encode_uri_component, Array, Promise}; +use js_sys::{Array, Promise}; use std::cell::RefCell; use std::sync::atomic::AtomicI32; use wasm_bindgen::prelude::*; use web_sys::{MessageEvent, Worker}; -const HELPER_CODE: &'static str = " -onmessage = function (ev) { - let [ia, index, value] = ev.data; - ia = new Int32Array(ia.buffer); - let result = Atomics.wait(ia, index, value); - postMessage(result); -}; -"; - thread_local! { static HELPERS: RefCell> = RefCell::new(vec![]); } @@ -61,11 +52,8 @@ fn alloc_helper() -> Worker { return helper; } - let mut initialization_string = "data:application/javascript,".to_owned(); - let encoded: String = encode_uri_component(HELPER_CODE).into(); - initialization_string.push_str(&encoded); - - Worker::new(&initialization_string).unwrap_or_else(|js| wasm_bindgen::throw_val(js)) + let worker_url = wasm_bindgen::link_to!(module = "/src/task/worker.js"); + Worker::new(&worker_url).unwrap_or_else(|js| wasm_bindgen::throw_val(js)) }) } diff --git a/crates/futures/src/task/worker.js b/crates/futures/src/task/worker.js new file mode 100644 index 00000000000..d25dab6606d --- /dev/null +++ b/crates/futures/src/task/worker.js @@ -0,0 +1,6 @@ +onmessage = function (ev) { + let [ia, index, value] = ev.data; + ia = new Int32Array(ia.buffer); + let result = Atomics.wait(ia, index, value); + postMessage(result); +}; diff --git a/crates/macro-support/src/lib.rs b/crates/macro-support/src/lib.rs index b7a35ae1bed..bf9f71a9d20 100644 --- a/crates/macro-support/src/lib.rs +++ b/crates/macro-support/src/lib.rs @@ -40,6 +40,18 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> Result Result { + parser::reset_attrs_used(); + let opts = syn::parse2(input)?; + + let mut tokens = proc_macro2::TokenStream::new(); + let link = parser::link_to(opts)?; + link.try_to_tokens(&mut tokens)?; + + Ok(tokens) +} + /// Takes the parsed input from a `#[wasm_bindgen]` macro and returns the generated bindings pub fn expand_class_marker( attr: TokenStream, diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 4b8afc49bbc..def3642cc51 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -95,6 +95,23 @@ macro_rules! methods { ($(($name:ident, $variant:ident($($contents:tt)*)),)*) => { $(methods!(@method $name, $variant($($contents)*));)* + fn enforce_used(self) -> Result<(), Diagnostic> { + // Account for the fact this method was called + ATTRS.with(|state| state.checks.set(state.checks.get() + 1)); + + let mut errors = Vec::new(); + for (used, attr) in self.attrs.iter() { + if used.get() { + continue + } + let span = match attr { + $(BindgenAttr::$variant(span, ..) => span,)* + }; + errors.push(Diagnostic::span_error(*span, "unused wasm_bindgen attribute")); + } + Diagnostic::from_vec(errors) + } + fn check_used(self) { // Account for the fact this method was called ATTRS.with(|state| { @@ -1361,39 +1378,15 @@ impl MacroParse for syn::ItemConst { impl MacroParse for syn::ItemForeignMod { fn macro_parse(self, program: &mut ast::Program, opts: BindgenAttrs) -> Result<(), Diagnostic> { let mut errors = Vec::new(); - match self.abi.name { - Some(ref l) if l.value() == "C" => {} - None => {} - Some(ref other) => { - errors.push(err_span!( - other, - "only foreign mods with the `C` ABI are allowed" - )); - } + if let Some(other) = self.abi.name.filter(|l| l.value() != "C") { + errors.push(err_span!( + other, + "only foreign mods with the `C` ABI are allowed" + )); } - let module = if let Some((name, span)) = opts.module() { - if opts.inline_js().is_some() { - let msg = "cannot specify both `module` and `inline_js`"; - errors.push(Diagnostic::span_error(span, msg)); - } - if opts.raw_module().is_some() { - let msg = "cannot specify both `module` and `raw_module`"; - errors.push(Diagnostic::span_error(span, msg)); - } - Some(ast::ImportModule::Named(name.to_string(), span)) - } else if let Some((name, span)) = opts.raw_module() { - if opts.inline_js().is_some() { - let msg = "cannot specify both `raw_module` and `inline_js`"; - errors.push(Diagnostic::span_error(span, msg)); - } - Some(ast::ImportModule::RawNamed(name.to_string(), span)) - } else if let Some((js, span)) = opts.inline_js() { - let i = program.inline_js.len(); - program.inline_js.push(js.to_string()); - Some(ast::ImportModule::Inline(i, span)) - } else { - None - }; + let module = module_from_opts(program, &opts) + .map_err(|e| errors.push(e)) + .unwrap_or_default(); for item in self.items.into_iter() { if let Err(e) = item.macro_parse(program, module.clone()) { errors.push(e); @@ -1438,6 +1431,38 @@ impl MacroParse> for syn::ForeignItem { } } +pub fn module_from_opts( + program: &mut ast::Program, + opts: &BindgenAttrs, +) -> Result, Diagnostic> { + let mut errors = Vec::new(); + let module = if let Some((name, span)) = opts.module() { + if opts.inline_js().is_some() { + let msg = "cannot specify both `module` and `inline_js`"; + errors.push(Diagnostic::span_error(span, msg)); + } + if opts.raw_module().is_some() { + let msg = "cannot specify both `module` and `raw_module`"; + errors.push(Diagnostic::span_error(span, msg)); + } + Some(ast::ImportModule::Named(name.to_string(), span)) + } else if let Some((name, span)) = opts.raw_module() { + if opts.inline_js().is_some() { + let msg = "cannot specify both `raw_module` and `inline_js`"; + errors.push(Diagnostic::span_error(span, msg)); + } + Some(ast::ImportModule::RawNamed(name.to_string(), span)) + } else if let Some((js, span)) = opts.inline_js() { + let i = program.inline_js.len(); + program.inline_js.push(js.to_string()); + Some(ast::ImportModule::Inline(i, span)) + } else { + None + }; + Diagnostic::from_vec(errors)?; + Ok(module) +} + /// Get the first type parameter of a generic type, errors on incorrect input. fn extract_first_ty_param(ty: Option<&syn::Type>) -> Result, Diagnostic> { let t = match ty { @@ -1654,3 +1679,21 @@ fn operation_kind(opts: &BindgenAttrs) -> ast::OperationKind { } operation_kind } + +pub fn link_to(opts: BindgenAttrs) -> Result { + let mut program = ast::Program::default(); + let module = module_from_opts(&mut program, &opts)?.ok_or_else(|| { + Diagnostic::span_error(Span::call_site(), "`link_to!` requires a module.") + })?; + if let ast::ImportModule::Named(p, s) | ast::ImportModule::RawNamed(p, s) = &module { + if !p.starts_with("./") && !p.starts_with("../") && !p.starts_with("/") { + return Err(Diagnostic::span_error( + *s, + "`link_to!` does not support module paths.", + )); + } + } + opts.enforce_used()?; + program.linked_modules.push(module); + Ok(ast::LinkToModule(program)) +} diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index f7a0bbb2176..9807e6870c4 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -27,3 +27,4 @@ quote = "1.0" trybuild = "1.0" wasm-bindgen = { path = "../..", version = "0.2.83" } wasm-bindgen-futures = { path = "../futures", version = "0.4.33" } +web-sys = { path = "../web-sys", version = "0.3.60" } diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index c677aaf24bf..fbae818750e 100644 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -18,6 +18,35 @@ pub fn wasm_bindgen(attr: TokenStream, input: TokenStream) -> TokenStream { } } +/// This macro takes a JS module as input and returns a URL that can be used to +/// access it at runtime. +/// +/// The module can be specified in a few ways: +/// - You can use `inline_js = "..."` to create an inline JS file. +/// - You can use `module = "/foo/bar"` to reference a file relative to the +/// root of the crate the macro is invoked in. +/// +/// The returned URL can be used for things like creating workers/worklets: +/// ```no_run +/// use web_sys::Worker; +/// let worker = Worker::new(&wasm_bindgen::link_to!(module = "/src/worker.js")); +/// ``` +#[proc_macro] +pub fn link_to(input: TokenStream) -> TokenStream { + match wasm_bindgen_macro_support::expand_link_to(input.into()) { + Ok(tokens) => { + if cfg!(feature = "xxx_debug_only_print_generated_code") { + println!("{}", tokens); + } + tokens.into() + } + // This `String::clone` is here so that IDEs know this is supposed to be a + // `String` and can keep type-checking the rest of the program even if the macro + // fails. + Err(diagnostic) => (quote! { String::clone(#diagnostic) }).into(), + } +} + #[proc_macro_attribute] pub fn __wasm_bindgen_class_marker(attr: TokenStream, input: TokenStream) -> TokenStream { match wasm_bindgen_macro_support::expand_class_marker(attr.into(), input.into()) { diff --git a/crates/macro/src/worker.js b/crates/macro/src/worker.js new file mode 100644 index 00000000000..360d4d42923 --- /dev/null +++ b/crates/macro/src/worker.js @@ -0,0 +1 @@ +// This file is needed for the doctest of `crate::link_to`. diff --git a/crates/macro/ui-tests/link-to.rs b/crates/macro/ui-tests/link-to.rs new file mode 100644 index 00000000000..ce51c128e2e --- /dev/null +++ b/crates/macro/ui-tests/link-to.rs @@ -0,0 +1,32 @@ +fn good1() -> String { + wasm_bindgen::link_to!(inline_js = "console.log('Hello world!');") +} + +fn good2() -> String { + wasm_bindgen::link_to!(raw_module = "./foo.js") +} + +fn bad1() -> String { + wasm_bindgen::link_to!(module = "package/foo.js") +} + +fn bad2() -> String { + wasm_bindgen::link_to!(raw_module = "package/foo.js") +} + +fn bad3() -> String { + wasm_bindgen::link_to!(module = "/src/not-found.js") +} + +fn bad4() -> String { + wasm_bindgen::link_to!() +} + +fn bad5() -> String { + wasm_bindgen::link_to!( + inline_js = "console.log('Hello world!');", + js_namespace = foo + ) +} + +fn main() {} diff --git a/crates/macro/ui-tests/link-to.stderr b/crates/macro/ui-tests/link-to.stderr new file mode 100644 index 00000000000..61aaec7830f --- /dev/null +++ b/crates/macro/ui-tests/link-to.stderr @@ -0,0 +1,31 @@ +error: `link_to!` does not support module paths. + --> ui-tests/link-to.rs:10:37 + | +10 | wasm_bindgen::link_to!(module = "package/foo.js") + | ^^^^^^^^^^^^^^^^ + +error: `link_to!` does not support module paths. + --> ui-tests/link-to.rs:14:41 + | +14 | wasm_bindgen::link_to!(raw_module = "package/foo.js") + | ^^^^^^^^^^^^^^^^ + +error: failed to read file `$WORKSPACE/target/tests/trybuild/wasm-bindgen-macro/src/not-found.js`: No such file or directory (os error 2) + --> ui-tests/link-to.rs:18:37 + | +18 | wasm_bindgen::link_to!(module = "/src/not-found.js") + | ^^^^^^^^^^^^^^^^^^^ + +error: `link_to!` requires a module. + --> ui-tests/link-to.rs:22:5 + | +22 | wasm_bindgen::link_to!() + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `wasm_bindgen::link_to` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unused wasm_bindgen attribute + --> ui-tests/link-to.rs:28:9 + | +28 | js_namespace = foo + | ^^^^^^^^^^^^ diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index bb20598ddce..408613be9f1 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -22,6 +22,7 @@ macro_rules! shared_api { inline_js: Vec<&'a str>, unique_crate_identifier: &'a str, package_json: Option<&'a str>, + linked_modules: Vec>, } struct Import<'a> { @@ -30,6 +31,11 @@ macro_rules! shared_api { kind: ImportKind<'a>, } + struct LinkedModule<'a> { + module: ImportModule<'a>, + link_function_name: &'a str, + } + enum ImportModule<'a> { Named(&'a str), RawNamed(&'a str), diff --git a/crates/shared/src/schema_hash_approval.rs b/crates/shared/src/schema_hash_approval.rs index 25084e90566..0d4d0bb38f1 100644 --- a/crates/shared/src/schema_hash_approval.rs +++ b/crates/shared/src/schema_hash_approval.rs @@ -8,7 +8,7 @@ // If the schema in this library has changed then: // 1. Bump the version in `crates/shared/Cargo.toml` // 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version -const APPROVED_SCHEMA_FILE_HASH: &'static str = "1473650450341157828"; +const APPROVED_SCHEMA_FILE_HASH: &'static str = "584864585234329974"; #[test] fn schema_version() { diff --git a/guide/src/reference/cli.md b/guide/src/reference/cli.md index 9c26649606f..d08e066361c 100644 --- a/guide/src/reference/cli.md +++ b/guide/src/reference/cli.md @@ -103,4 +103,22 @@ about reference types](./reference-types.md). ### `--omit-default-module-path` -Don't add WebAssembly fallback imports in generated JavaScript. \ No newline at end of file +Don't add WebAssembly fallback imports in generated JavaScript. + +### `--split-linked-modules` + +Controls whether wasm-bindgen will split linked modules out into their own +files. Enabling this is recommended, because it allows lazy-loading the linked +modules and setting a stricter Content Security Policy. + +wasm-bindgen uses the `new URL('…', import.meta.url)` syntax to resolve the +links to such split out files. This breaks with most bundlers, since the bundler +doesn't know to include the linked module in its output. That's why this option +is disabled by default. Webpack 5 is an exception, which has special treatment +for that syntax. + +For other bundlers, you'll need to take extra steps to get it to work, likely by +using a plugin. Alternatively, you can leave the syntax as is and instead +manually configure the bundler to copy all files in `snippets/` to the output +directory, preserving their paths relative to whichever bundled file ends up +containing the JS shim. diff --git a/src/lib.rs b/src/lib.rs index 50920321b37..f8576ed4c3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,8 @@ pub mod prelude { pub use crate::JsError; } +pub use wasm_bindgen_macro::link_to; + pub mod convert; pub mod describe; diff --git a/tests/wasm/link_to.js b/tests/wasm/link_to.js new file mode 100644 index 00000000000..3028a6ae8a6 --- /dev/null +++ b/tests/wasm/link_to.js @@ -0,0 +1,4 @@ +const fs = require('fs'); +const url = require('url'); + +exports.read_file = (str) => fs.readFileSync(url.fileURLToPath(str), "utf8"); diff --git a/tests/wasm/link_to.rs b/tests/wasm/link_to.rs new file mode 100644 index 00000000000..e631bdb03d9 --- /dev/null +++ b/tests/wasm/link_to.rs @@ -0,0 +1,30 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; + +#[wasm_bindgen(module = "/tests/wasm/link_to.js")] +extern "C" { + #[wasm_bindgen(catch)] + fn read_file(url: &str) -> Result; +} + +#[wasm_bindgen_test] +fn test_module() { + let link = wasm_bindgen::link_to!(module = "/tests/wasm/linked_module.js"); + assert_eq!(read_file(&link).unwrap(), "// linked module\n"); +} + +#[wasm_bindgen_test] +fn test_raw_module() { + let link = wasm_bindgen::link_to!(raw_module = "./not-found.js"); + assert!(read_file(&link).is_err()); +} + +#[wasm_bindgen_test] +fn test_inline_js() { + // Test two invocations to ensure that snippet indices from different + // Program structs are offset correctly. + let link1 = wasm_bindgen::link_to!(inline_js = "// inline js 1\n"); + let link2 = wasm_bindgen::link_to!(inline_js = "// inline js 2\n"); + assert_eq!(read_file(&link1).unwrap(), "// inline js 1\n"); + assert_eq!(read_file(&link2).unwrap(), "// inline js 2\n"); +} diff --git a/tests/wasm/linked_module.js b/tests/wasm/linked_module.js new file mode 100644 index 00000000000..b979535b28e --- /dev/null +++ b/tests/wasm/linked_module.js @@ -0,0 +1 @@ +// linked module diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs index fc838ada611..987962c88aa 100644 --- a/tests/wasm/main.rs +++ b/tests/wasm/main.rs @@ -32,6 +32,7 @@ pub mod intrinsics; pub mod js_keywords; pub mod js_objects; pub mod jscast; +pub mod link_to; pub mod math; pub mod no_shims; pub mod node;