use std::path::Path;
use tokio::fs::{read, read_to_string};
use super::*;
pub(crate) async fn build_inline_bridge(
pkg_dir: &Path,
js_name: &str,
wasm_url: &str,
) -> Result<String, EuvError> {
let js_path: std::path::PathBuf = pkg_dir.join(js_name);
let bridge_source: String =
read_to_string(&js_path)
.await
.map_err(|error: IoError| EuvError::IoPath {
message: String::from("Failed to read wasm-bindgen JS bridge"),
path: js_path.clone(),
error,
})?;
let snippet_bodies: String = collect_snippet_bodies(&bridge_source, pkg_dir).await?;
let stripped: String = strip_module_imports(&bridge_source);
let js_only: String = strip_trailing_module_exports(&stripped);
let wasm_url_json: String = serde_json_wasm_url(wasm_url);
Ok(format!(
"(function() {{\n\
{snippet_bodies}\n\
{js_only}\n\
var __euv_wasm_url = {wasm_url_json};\n\
if (typeof __wbg_init === 'function') {{\n\
__wbg_init(new URL(__euv_wasm_url, location.href).toString()).then(function() {{\n\
if (typeof main === 'function') main();\n\
}}).catch(function(e) {{ console.error('[euv] inline bridge init failed:', e); }});\n\
}} else if (typeof initSync === 'function') {{\n\
initSync();\n\
if (typeof main === 'function') main();\n\
}} else {{\n\
console.error('[euv] inline bridge: no __wbg_init or initSync exported');\n\
}}\n\
}})();\n"
))
}
pub(crate) fn build_module_fallback_bridge(import_path: &str) -> String {
format!(
" import init, {{ main }} from '{import_path}';\n \
await init();\n \
main();\n"
)
}
pub(crate) fn inline_bridge_disabled() -> bool {
matches!(std::env::var(EUV_NO_INLINE_BRIDGE_ENV), Ok(value) if !value.is_empty())
}
struct NamespaceImport {
alias: String,
spec: String,
}
async fn collect_snippet_bodies(bridge_source: &str, pkg_dir: &Path) -> Result<String, EuvError> {
let mut bodies: String = String::new();
let mut namespace_imports: Vec<NamespaceImport> = Vec::new();
for line in bridge_source.lines() {
let trimmed: &str = line.trim();
if !trimmed.starts_with("import ") {
continue;
}
let rest: &str = match trimmed.strip_prefix("import ") {
Some(value) => value,
None => continue,
};
let spec: &str = match extract_import_spec(rest) {
Some(value) => value,
None => continue,
};
if is_namespace_import(rest)
&& let Some(alias) = extract_namespace_alias(rest)
{
namespace_imports.push(NamespaceImport {
alias: alias.to_string(),
spec: spec.to_string(),
});
}
let body: Option<String> = read_snippet_module(pkg_dir, spec).await?;
if let Some(body) = body {
if !bodies.is_empty() {
bodies.push('\n');
}
bodies.push_str(&body);
}
}
for ns in &namespace_imports {
let snippet_path: std::path::PathBuf = resolve_snippet_path(pkg_dir, &ns.spec)?;
let exports: Vec<String> = if snippet_path.exists() {
let raw: String = read_to_string(&snippet_path)
.await
.map_err(|error: IoError| EuvError::IoPath {
message: String::from(
"Failed to read wasm-pack snippet module for namespace export scan",
),
path: snippet_path.clone(),
error,
})?;
extract_exported_function_names(&raw)
} else {
Vec::new()
};
if !bodies.is_empty() {
bodies.push('\n');
}
bodies.push_str(&format!("var {} = {{", ns.alias));
for (i, name) in exports.iter().enumerate() {
if i > 0 {
bodies.push(',');
}
bodies.push_str(name);
bodies.push(':');
bodies.push_str(name);
}
bodies.push_str("};\n");
}
Ok(bodies)
}
pub fn extract_exported_function_names(source: &str) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
for line in source.lines() {
let trimmed: &str = line.trim();
if let Some(rest) = trimmed.strip_prefix("export function ") {
let name_end: usize =
match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
Some(value) => value,
None => rest.len(),
};
let name: &str = &rest[..name_end];
if !name.is_empty() {
names.push(name.to_string());
}
}
}
names
}
pub fn is_namespace_import(rest: &str) -> bool {
rest.trim_start().starts_with('*')
}
pub fn extract_namespace_alias(rest: &str) -> Option<&str> {
let rest: &str = rest.trim_end_matches(';').trim();
if !rest.starts_with('*') {
return None;
}
let rest: &str = rest.strip_prefix('*')?.trim();
let rest: &str = rest.strip_prefix("as")?.trim();
let end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
Some(value) => value,
None => rest.len(),
};
let alias: &str = &rest[..end];
if alias.is_empty() { None } else { Some(alias) }
}
pub fn extract_import_spec(rest: &str) -> Option<&str> {
let rest: &str = rest.trim_end_matches(';').trim();
let from_idx: usize = rest.find(" from ")?;
let spec_part: &str = rest[from_idx + " from ".len()..].trim();
let spec: &str = spec_part
.trim_start_matches('\'')
.trim_start_matches('"')
.trim_end_matches('\'')
.trim_end_matches('"');
if spec.starts_with("./") || spec.starts_with("../") {
Some(spec)
} else {
None
}
}
async fn read_snippet_module(pkg_dir: &Path, spec: &str) -> Result<Option<String>, EuvError> {
let snippet_path: std::path::PathBuf = resolve_snippet_path(pkg_dir, spec)?;
if !snippet_path.exists() {
return Ok(None);
}
let bytes: Vec<u8> = read(&snippet_path)
.await
.map_err(|error: IoError| EuvError::IoPath {
message: String::from("Failed to read wasm-pack snippet module"),
path: snippet_path.clone(),
error,
})?;
let raw: String = String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
message: String::from("Snippet module is not valid UTF-8"),
error,
})?;
Ok(Some(strip_snippet_export(&raw)))
}
fn resolve_snippet_path(pkg_dir: &Path, spec: &str) -> Result<std::path::PathBuf, EuvError> {
let base: &str = spec.trim_start_matches('.').trim_start_matches('/');
let mut path: std::path::PathBuf = pkg_dir.to_path_buf();
for segment in base.split('/') {
if segment.is_empty() || segment == "." {
continue;
}
if segment == ".." {
return Err(EuvError::Message(String::from(
"Snippet import spec escapes pkg directory",
)));
}
path.push(segment);
}
Ok(path)
}
fn strip_snippet_export(raw: &str) -> String {
let mut out: String = String::with_capacity(raw.len());
for line in raw.lines() {
let trimmed: &str = line.trim();
if trimmed.starts_with("export ") {
if let Some(rest) = trimmed.strip_prefix("export ") {
out.push_str(rest);
out.push('\n');
}
} else {
out.push_str(line);
out.push('\n');
}
}
out
}
fn strip_module_imports(source: &str) -> String {
let mut out: String = String::with_capacity(source.len());
for line in source.lines() {
let trimmed: &str = line.trim();
if trimmed.starts_with("import ") {
continue;
}
if trimmed.contains("import.meta.url") {
continue;
}
out.push_str(line);
out.push('\n');
}
out
}
fn strip_trailing_module_exports(source: &str) -> String {
let mut out: String = String::with_capacity(source.len());
for line in source.lines() {
let trimmed: &str = line.trim();
if trimmed.starts_with("export ") && trimmed.contains('{') && trimmed.contains('}') {
let inner: &str = &trimmed["export ".len()..];
let stripped: &str = match inner.strip_prefix('{') {
Some(value) => value,
None => {
out.push_str(line);
out.push('\n');
continue;
}
};
let close: usize = match stripped.rfind('}') {
Some(value) => value,
None => {
out.push_str(line);
out.push('\n');
continue;
}
};
let names_block: &str = &stripped[..close];
let inlined: String = inline_export_aliases(names_block.trim());
if !inlined.is_empty() {
out.push_str(&inlined);
out.push('\n');
}
continue;
}
if trimmed.starts_with("export function ") {
let rest: &str = match trimmed.strip_prefix("export function ") {
Some(value) => value,
None => {
out.push_str(line);
out.push('\n');
continue;
}
};
let name_end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_') {
Some(value) => value,
None => rest.len(),
};
let name: &str = &rest[..name_end];
if !name.is_empty() {
out.push_str("function ");
out.push_str(name);
out.push_str(&rest[name_end..]);
out.push('\n');
continue;
}
}
out.push_str(line);
out.push('\n');
}
out
}
fn inline_export_aliases(block: &str) -> String {
let mut out: String = String::new();
for entry in block.split(',') {
let entry: &str = entry.trim();
if entry.is_empty() {
continue;
}
if let Some((original, exported)) = entry.split_once(" as ") {
let original: &str = original.trim();
let exported: &str = exported.trim();
if original == exported || exported == "default" || original == "wasm" {
continue;
}
out.push_str(exported);
out.push_str(" = ");
out.push_str(original);
out.push_str(";\n");
}
}
out
}
fn serde_json_wasm_url(url: &str) -> String {
let mut out: String = String::with_capacity(url.len() + 2);
out.push('"');
for ch in url.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0C}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}