diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 9c674369341..812860b29d1 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -23,3 +23,6 @@ proc-macro2 = "1.0" quote = '1.0' syn = { version = '1.0', features = ['full'] } wasm-bindgen-shared = { path = "../shared", version = "=0.2.84" } +swc_ecma_parser = "0.99.1" +swc_ecma_ast = "0.74.0" +swc_common = "0.17.25" diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index 254d0e88537..7e034cefb41 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -1,10 +1,13 @@ use crate::util::ShortHash; use proc_macro2::{Ident, Span}; use std::cell::{Cell, RefCell}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::env; -use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +use swc_common::{sync::Lrc, SourceMap}; +use swc_ecma_ast::EsVersion; +use swc_ecma_parser::Syntax; use crate::ast; use crate::Diagnostic; @@ -46,6 +49,22 @@ struct LocalFile { new_identifier: String, } +impl LocalFile { + fn join(&self, rel: &str) -> Self { + Self { + path: self.path.parent().unwrap().join(rel), + definition: self.definition, + new_identifier: Path::new(&self.new_identifier) + .parent() + .unwrap() + .join(rel) + .to_str() + .unwrap() + .to_owned(), + } + } +} + impl Interner { fn new() -> Interner { let root = env::var_os("CARGO_MANIFEST_DIR") @@ -120,6 +139,105 @@ impl Interner { } } +trait CollectImports<'a, 'b> +where + Self: Sized + Iterator, +{ + fn collect_imports(self, intern: &'a Interner) -> ImportCollector<'a, 'b, Self>; +} + +impl<'a, 'b, I: Iterator> CollectImports<'a, 'b> for I { + fn collect_imports(self, intern: &'a Interner) -> ImportCollector<'a, 'b, Self> { + ImportCollector { + base: self, + intern, + stack: Vec::new(), + done: HashSet::new(), + } + } +} + +struct ImportCollector<'a, 'b, I: Iterator> { + base: I, + intern: &'a Interner, + stack: Vec, + done: HashSet, +} + +impl<'a, 'b, I: Iterator> Iterator for ImportCollector<'a, 'b, I> { + type Item = Result, Diagnostic>; + + fn next(&mut self) -> Option, Diagnostic>> { + let pop = self.stack.pop(); + let file = pop.as_ref().or_else(|| self.base.next())?; + + let cm: Lrc = Default::default(); + let fm = match cm.load_file(&file.path) { + Ok(fm) => fm, + Err(e) => { + let msg = format!("failed to read file `{}`: {}", file.path.display(), e); + return Some(Err(Diagnostic::span_error(file.definition, msg))); + } + }; + + let r = match swc_ecma_parser::parse_file_as_module( + &*fm, + Syntax::Es(Default::default()), + EsVersion::latest(), + None, + &mut Vec::new(), + ) { + Ok(r) => r, + Err(e) => { + let msg = format!("failed to parse file `{}`: {:?}", file.path.display(), e); + return Some(Err(Diagnostic::span_error(file.definition, msg))); + } + }; + + let mut pl = Vec::new(); + for imp in r + .body + .iter() + .flat_map(|i| i.as_module_decl().and_then(|i| i.as_import())) + { + let val = imp.src.value.as_ref(); + if val == "$wbg_main" { + pl.push((imp.src.span.lo.0 + 1, imp.src.span.hi.0 - 1)); + } else { + let f = file.join(val); + let fc = f.path.canonicalize().unwrap_or_else(|_| f.path.clone()); + if !self.done.contains(&fc) { + self.stack.push(f); + self.done.insert(fc); + } + } + } + + let mut v = vec![&fm.src[..]]; + let mut lolen = 0; + for &(a, b) in pl.iter() { + let s = v.pop().unwrap(); + let (lo, hi) = s.split_at(a as usize - lolen); + v.push(lo); + v.push(&hi[(b - a) as usize..]); + lolen += lo.len(); + } + Some(Ok(LocalModule { + identifier: self.intern.intern_str(&file.new_identifier), + contents: ModuleContent { + head: self.intern.intern_str(v[0]), + tail: v[1..] + .iter() + .map(|x| ContentPart { + p: ContentPlaceholder::WbgMain, + t: self.intern.intern_str(x), + }) + .collect(), + }, + })) + } +} + fn shared_program<'a>( prog: &'a ast::Program, intern: &'a Interner, @@ -156,18 +274,8 @@ fn shared_program<'a>( .files .borrow() .values() - .map(|file| { - fs::read_to_string(&file.path) - .map(|s| LocalModule { - identifier: intern.intern_str(&file.new_identifier), - contents: intern.intern_str(&s), - }) - .map_err(|e| { - let msg = format!("failed to read file `{}`: {}", file.path.display(), e); - Diagnostic::span_error(file.definition, msg) - }) - }) - .collect::, _>>()?, + .collect_imports(intern) + .collect::>, _>>()?, inline_js: prog .inline_js .iter() diff --git a/crates/cli-support/src/decode.rs b/crates/cli-support/src/decode.rs index d58b49ff06d..062f4351045 100644 --- a/crates/cli-support/src/decode.rs +++ b/crates/cli-support/src/decode.rs @@ -155,3 +155,85 @@ macro_rules! decode_api { } wasm_bindgen_shared::shared_api!(decode_api); + +#[derive(PartialEq, Clone)] +pub enum ContentPlaceholderBuf { + WbgMain, +} + +#[derive(PartialEq, Clone)] +pub struct ContentPartBuf { + p: ContentPlaceholderBuf, + t: String, +} + +#[derive(PartialEq, Clone)] +pub struct ModuleContentBuf { + head: String, + tail: Vec, +} + +impl core::fmt::Debug for ModuleContentBuf { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(fmt, "{}", self.head) + } +} + +impl core::fmt::Debug for ModuleContent<'_> { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(fmt, "{}", self.head) + } +} + +impl ModuleContent<'_> { + pub fn to_owned(&self) -> ModuleContentBuf { + ModuleContentBuf { + head: self.head.to_string(), + tail: self + .tail + .iter() + .map(|c| ContentPartBuf { + p: ContentPlaceholderBuf::WbgMain, + t: c.t.to_string(), + }) + .collect(), + } + } +} + +impl From for ModuleContentBuf { + fn from(head: String) -> Self { + Self { + head, + tail: Vec::new(), + } + } +} + +impl ModuleContentBuf { + pub fn fill(&self, wbg_main: &str) -> String { + Some(&self.head[..]) + .into_iter() + .chain(self.tail.iter().flat_map(|p| [&wbg_main, &p.t[..]])) + .fold(String::new(), |a, b| a + b) + } + + pub fn escape(&self, mut f: F) -> String + where + F: FnMut(&ContentPlaceholderBuf) -> String, + { + let mut escaped = String::with_capacity(self.head.len()); + self.head.chars().for_each(|c| match c { + '`' | '\\' | '$' => escaped.extend(['\\', c]), + _ => escaped.extend([c]), + }); + for part in &self.tail { + escaped.push_str(&f(&part.p)); + part.t.chars().for_each(|c| match c { + '`' | '\\' | '$' => escaped.extend(['\\', c]), + _ => escaped.extend([c]), + }); + } + escaped + } +} diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index eb2583a9e7c..d2b7ccb49c2 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -1,3 +1,4 @@ +use crate::decode::ContentPlaceholderBuf; use crate::descriptor::VectorKind; use crate::intrinsic::Intrinsic; use crate::wit::{ @@ -3162,10 +3163,8 @@ impl<'a> Context<'a> { 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]), + let escaped = content.escape(|p| match p { + ContentPlaceholderBuf::WbgMain => "${script_src}".to_string(), }); Ok(format!( "\"data:application/javascript,\" + encodeURIComponent(`{escaped}`)" diff --git a/crates/cli-support/src/lib.rs b/crates/cli-support/src/lib.rs index 00800ca08a3..a4923f22857 100755 --- a/crates/cli-support/src/lib.rs +++ b/crates/cli-support/src/lib.rs @@ -1,5 +1,6 @@ #![doc(html_root_url = "https://docs.rs/wasm-bindgen-cli-support/0.2")] +use crate::decode::ModuleContentBuf; use anyhow::{bail, Context, Error}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::env; @@ -66,7 +67,7 @@ struct JsGenerated { ts: String, start: Option, snippets: HashMap>, - local_modules: HashMap, + local_modules: HashMap, npm_dependencies: HashMap, typescript: bool, } @@ -676,7 +677,7 @@ impl Output { &self.gen().snippets } - pub fn local_modules(&self) -> &HashMap { + pub fn local_modules(&self) -> &HashMap { &self.gen().local_modules } @@ -731,13 +732,31 @@ 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 js_path = Path::new(&self.stem).with_extension(extension); + for (path, contents) in gen.local_modules.iter() { - let path = out_dir.join("snippets").join(path); + let path = Path::new("snippets").join(path); + let backlink = path + .with_file_name("") + .components() + .fold(PathBuf::new(), |a, _| a.join("..")) + .join(&js_path); + let path = out_dir.join(path); fs::create_dir_all(path.parent().unwrap())?; - fs::write(&path, contents) + fs::write(&path, contents.fill(backlink.to_str().unwrap())) .with_context(|| format!("failed to write `{}`", path.display()))?; } + let js_path = out_dir.join(&js_path); + if gen.npm_dependencies.len() > 0 { let map = gen .npm_dependencies @@ -748,14 +767,6 @@ impl Output { fs::write(out_dir.join("package.json"), json)?; } - // 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" - }; - fn write(path: P, contents: C) -> Result<(), anyhow::Error> where P: AsRef, @@ -765,8 +776,6 @@ impl Output { .with_context(|| format!("failed to write `{}`", path.as_ref().display())) } - let js_path = out_dir.join(&self.stem).with_extension(extension); - if gen.mode.esm_integration() { let js_name = format!("{}_bg.{}", self.stem, extension); diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index 7986afb1c3a..7fbb2df1ac5 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -1,4 +1,4 @@ -use crate::decode::LocalModule; +use crate::decode::{LocalModule, ModuleContentBuf}; use crate::descriptor::{Descriptor, Function}; use crate::descriptors::WasmBindgenDescriptorsSection; use crate::intrinsic::Intrinsic; @@ -362,7 +362,7 @@ impl<'a> Context<'a> { local_modules .iter() .find(|m| m.identifier == *n) - .map(|m| m.contents), + .map(|m| m.contents.to_owned()), ), decode::ImportModule::RawNamed(n) => (n.to_string(), None), decode::ImportModule::Inline(idx) => ( @@ -371,12 +371,12 @@ impl<'a> Context<'a> { self.unique_crate_identifier, *idx as usize + offset ), - Some(inline_js[*idx as usize]), + Some(inline_js[*idx as usize].to_string().into()), ), }; self.aux .import_map - .insert(id, AuxImport::LinkTo(path, content.map(str::to_string))); + .insert(id, AuxImport::LinkTo(path, content)); Ok(()) } @@ -402,9 +402,9 @@ impl<'a> Context<'a> { if let Some(prev) = self .aux .local_modules - .insert(module.identifier.to_string(), module.contents.to_string()) + .insert(module.identifier.to_string(), module.contents.to_owned()) { - assert_eq!(prev, module.contents); + assert_eq!(prev, module.contents.to_owned()); } } if let Some(s) = package_json { diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs index 4c7eb523555..2640c5eadcd 100644 --- a/crates/cli-support/src/wit/nonstandard.rs +++ b/crates/cli-support/src/wit/nonstandard.rs @@ -1,3 +1,4 @@ +use crate::decode::ModuleContentBuf; use crate::intrinsic::Intrinsic; use crate::wit::AdapterId; use std::borrow::Cow; @@ -17,7 +18,7 @@ pub struct WasmBindgenAux { /// A map from identifier to the contents of each local module defined via /// the `#[wasm_bindgen(module = "/foo.js")]` import options. - pub local_modules: HashMap, + pub local_modules: HashMap, /// A map from unique crate identifier to the list of inline JS snippets for /// that crate identifier. @@ -333,7 +334,7 @@ pub enum AuxImport { /// 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), + LinkTo(String, Option), } /// Values that can be imported verbatim to hook up to an import. diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 408613be9f1..1daafda3f15 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -141,9 +141,23 @@ macro_rules! shared_api { generate_typescript: bool, } + enum ContentPlaceholder { + WbgMain, + } + + struct ContentPart<'a> { + p: ContentPlaceholder, + t: &'a str, + } + + struct ModuleContent<'a> { + head: &'a str, + tail: Vec>, + } + struct LocalModule<'a> { identifier: &'a str, - contents: &'a str, + contents: ModuleContent<'a>, } } }; // end of mac case diff --git a/examples/wasm-audio-worklet/build.sh b/examples/wasm-audio-worklet/build.sh index 576fec80605..105a7ed404f 100755 --- a/examples/wasm-audio-worklet/build.sh +++ b/examples/wasm-audio-worklet/build.sh @@ -20,3 +20,5 @@ cargo run -p wasm-bindgen-cli -- \ --out-dir . \ --target web \ --split-linked-modules + +find snippets -type f -exec grep -q '//!bundle$' {} \; -exec npx rollup -i {} -o {} \; diff --git a/examples/wasm-audio-worklet/src/dependent_module.rs b/examples/wasm-audio-worklet/src/dependent_module.rs deleted file mode 100644 index fa01b0acb23..00000000000 --- a/examples/wasm-audio-worklet/src/dependent_module.rs +++ /dev/null @@ -1,48 +0,0 @@ -use js_sys::{Array, JsString}; -use wasm_bindgen::prelude::*; -use web_sys::{Blob, BlobPropertyBag, Url}; - -// This is a not-so-clean approach to get the current bindgen ES module URL -// in Rust. This will fail at run time on bindgen targets not using ES modules. -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen] - type ImportMeta; - - #[wasm_bindgen(method, getter)] - fn url(this: &ImportMeta) -> JsString; - - #[wasm_bindgen(js_namespace = import, js_name = meta)] - static IMPORT_META: ImportMeta; -} - -pub fn on_the_fly(code: &str) -> Result { - // Generate the import of the bindgen ES module, assuming `--target web`, - // preluded by the TextEncoder/TextDecoder polyfill needed inside worklets. - let header = format!( - "import '{}';\n\ - import init, * as bindgen from '{}';\n\n", - wasm_bindgen::link_to!(module = "/src/polyfill.js"), - IMPORT_META.url(), - ); - - Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options( - &Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)), - &BlobPropertyBag::new().type_("text/javascript"), - )?) -} - -// dependent_module! takes a local file name to a JS module as input and -// returns a URL to a slightly modified module in run time. This modified module -// has an additional import statement in the header that imports the current -// bindgen JS module under the `bindgen` alias, and the separate init function. -// How this URL is produced does not matter for the macro user. on_the_fly -// creates a blob URL in run time. A better, more sophisticated solution -// would add wasm_bindgen support to put such a module in pkg/ during build time -// and return a URL to this file instead (described in #3019). -#[macro_export] -macro_rules! dependent_module { - ($file_name:expr) => { - crate::dependent_module::on_the_fly(include_str!($file_name)) - }; -} diff --git a/examples/wasm-audio-worklet/src/lib.rs b/examples/wasm-audio-worklet/src/lib.rs index d5a1ded9bde..4b242aea43e 100644 --- a/examples/wasm-audio-worklet/src/lib.rs +++ b/examples/wasm-audio-worklet/src/lib.rs @@ -1,4 +1,3 @@ -mod dependent_module; mod gui; mod oscillator; mod wasm_audio; diff --git a/examples/wasm-audio-worklet/src/wasm_audio.rs b/examples/wasm-audio-worklet/src/wasm_audio.rs index 8fe7c6d77c7..8c0fad11237 100644 --- a/examples/wasm-audio-worklet/src/wasm_audio.rs +++ b/examples/wasm-audio-worklet/src/wasm_audio.rs @@ -1,4 +1,3 @@ -use crate::dependent_module; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; @@ -53,7 +52,7 @@ pub fn wasm_audio_node( } pub async fn prepare_wasm_audio(ctx: &AudioContext) -> Result<(), JsValue> { - let mod_url = dependent_module!("worklet.js")?; + let mod_url = wasm_bindgen::link_to!(module = "/src/worklet.js"); JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?).await?; Ok(()) } diff --git a/examples/wasm-audio-worklet/src/worklet.js b/examples/wasm-audio-worklet/src/worklet.js index 4223d72ca30..405ba148fcf 100644 --- a/examples/wasm-audio-worklet/src/worklet.js +++ b/examples/wasm-audio-worklet/src/worklet.js @@ -1,3 +1,8 @@ +//!bundle + +import './polyfill.js'; +import * as bindgen from '$wbg_main'; + registerProcessor("WasmProcessor", class WasmProcessor extends AudioWorkletProcessor { constructor(options) { super();