Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependent module support #3034

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
138 changes: 123 additions & 15 deletions crates/backend/src/encode.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -120,6 +139,105 @@ impl Interner {
}
}

trait CollectImports<'a, 'b>
where
Self: Sized + Iterator<Item = &'b LocalFile>,
{
fn collect_imports(self, intern: &'a Interner) -> ImportCollector<'a, 'b, Self>;
}

impl<'a, 'b, I: Iterator<Item = &'b LocalFile>> 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<Item = &'b LocalFile>> {
base: I,
intern: &'a Interner,
stack: Vec<LocalFile>,
done: HashSet<PathBuf>,
}

impl<'a, 'b, I: Iterator<Item = &'b LocalFile>> Iterator for ImportCollector<'a, 'b, I> {
type Item = Result<LocalModule<'a>, Diagnostic>;

fn next(&mut self) -> Option<Result<LocalModule<'a>, Diagnostic>> {
let pop = self.stack.pop();
let file = pop.as_ref().or_else(|| self.base.next())?;

let cm: Lrc<SourceMap> = 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 {
Comment on lines +204 to +206
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little weird that "$wbg_main" only gets expanded when used as an import specifier. That means that you can't do import("$wbg_main"), importScripts("$wbg_main"), require("$wbg_main"), etc. I think it'd be better if all instances of "$wbg_main" were expanded, not just the ones in import specifiers.

Also, I think $wbg_shim might be a better name.

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,
Expand Down Expand Up @@ -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::<Result<Vec<_>, _>>()?,
.collect_imports(intern)
.collect::<Result<Vec<LocalModule<'a>>, _>>()?,
inline_js: prog
.inline_js
.iter()
Expand Down
82 changes: 82 additions & 0 deletions crates/cli-support/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContentPartBuf>,
}

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<String> 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<F>(&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
}
}
7 changes: 3 additions & 4 deletions crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::decode::ContentPlaceholderBuf;
use crate::descriptor::VectorKind;
use crate::intrinsic::Intrinsic;
use crate::wit::{
Expand Down Expand Up @@ -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}`)"
Expand Down
37 changes: 23 additions & 14 deletions crates/cli-support/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -66,7 +67,7 @@ struct JsGenerated {
ts: String,
start: Option<String>,
snippets: HashMap<String, Vec<String>>,
local_modules: HashMap<String, String>,
local_modules: HashMap<String, ModuleContentBuf>,
npm_dependencies: HashMap<String, (PathBuf, String)>,
typescript: bool,
}
Expand Down Expand Up @@ -676,7 +677,7 @@ impl Output {
&self.gen().snippets
}

pub fn local_modules(&self) -> &HashMap<String, String> {
pub fn local_modules(&self) -> &HashMap<String, ModuleContentBuf> {
&self.gen().local_modules
}

Expand Down Expand Up @@ -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
Expand All @@ -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<P, C>(path: P, contents: C) -> Result<(), anyhow::Error>
where
P: AsRef<Path>,
Expand All @@ -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);

Expand Down
Loading