1use std::path::Path;
2
3use tokio::fs::{read, read_to_string};
4
5use super::*;
6
7pub(crate) async fn build_inline_bridge(
26 pkg_dir: &Path,
27 js_name: &str,
28 wasm_url: &str,
29) -> Result<String, EuvError> {
30 let js_path: std::path::PathBuf = pkg_dir.join(js_name);
31 let bridge_source: String =
32 read_to_string(&js_path)
33 .await
34 .map_err(|error: io::Error| EuvError::IoPath {
35 message: String::from("Failed to read wasm-bindgen JS bridge"),
36 path: js_path.clone(),
37 error,
38 })?;
39 let snippet_bodies: String = collect_snippet_bodies(&bridge_source, pkg_dir).await?;
40 let stripped: String = strip_module_imports(&bridge_source);
41 let js_only: String = strip_trailing_module_exports(&stripped);
42 let wasm_url_json: String = serde_json_wasm_url(wasm_url);
43 Ok(format!(
44 "(function() {{\n\
45 {snippet_bodies}\n\
46 {js_only}\n\
47 var __euv_wasm_url = {wasm_url_json};\n\
48 if (typeof __wbg_init === 'function') {{\n\
49 var __euv_base = (document.querySelector('base[href]') && document.baseURI) || location.href;\n __wbg_init(new URL(__euv_wasm_url, __euv_base).toString()).then(function() {{\n\
50 if (typeof main === 'function') main();\n\
51 }}).catch(function(e) {{ console.error('[euv] inline bridge init failed:', e); }});\n\
52 }} else if (typeof initSync === 'function') {{\n\
53 initSync();\n\
54 if (typeof main === 'function') main();\n\
55 }} else {{\n\
56 console.error('[euv] inline bridge: no __wbg_init or initSync exported');\n\
57 }}\n\
58 }})();\n"
59 ))
60}
61
62pub(crate) fn build_module_fallback_bridge(import_path: &str) -> String {
68 format!(
69 " import init, {{ main }} from '{import_path}';\n \
70 await init();\n \
71 main();\n"
72 )
73}
74
75pub(crate) fn inline_bridge_disabled() -> bool {
81 matches!(std::env::var(EUV_NO_INLINE_BRIDGE_ENV), Ok(value) if !value.is_empty())
82}
83
84struct NamespaceImport {
86 alias: String,
87 spec: String,
88}
89
90async fn collect_snippet_bodies(bridge_source: &str, pkg_dir: &Path) -> Result<String, EuvError> {
91 let mut bodies: String = String::new();
92 let mut namespace_imports: Vec<NamespaceImport> = Vec::new();
93 for line in bridge_source.lines() {
94 let trimmed: &str = line.trim();
95 if !trimmed.starts_with("import ") {
96 continue;
97 }
98 let rest: &str = match trimmed.strip_prefix("import ") {
99 Some(value) => value,
100 None => continue,
101 };
102 let spec: &str = match extract_import_spec(rest) {
103 Some(value) => value,
104 None => continue,
105 };
106 if is_namespace_import(rest)
107 && let Some(alias) = extract_namespace_alias(rest)
108 {
109 namespace_imports.push(NamespaceImport {
110 alias: alias.to_string(),
111 spec: spec.to_string(),
112 });
113 }
114 let body: Option<String> = read_snippet_module(pkg_dir, spec).await?;
115 if let Some(body) = body {
116 if !bodies.is_empty() {
117 bodies.push('\n');
118 }
119 bodies.push_str(&body);
120 }
121 }
122 for ns in &namespace_imports {
123 let snippet_path: std::path::PathBuf = resolve_snippet_path(pkg_dir, &ns.spec)?;
124 let exports: Vec<String> = if snippet_path.exists() {
125 let raw: String = read_to_string(&snippet_path)
126 .await
127 .map_err(|error: io::Error| EuvError::IoPath {
128 message: String::from(
129 "Failed to read wasm-pack snippet module for namespace export scan",
130 ),
131 path: snippet_path.clone(),
132 error,
133 })?;
134 extract_exported_function_names(&raw)
135 } else {
136 Vec::new()
137 };
138 if !bodies.is_empty() {
139 bodies.push('\n');
140 }
141 bodies.push_str(&format!("var {} = {{", ns.alias));
142 for (i, name) in exports.iter().enumerate() {
143 if i > 0 {
144 bodies.push(',');
145 }
146 bodies.push_str(name);
147 bodies.push(':');
148 bodies.push_str(name);
149 }
150 bodies.push_str("};\n");
151 }
152 Ok(bodies)
153}
154
155pub fn extract_exported_function_names(source: &str) -> Vec<String> {
161 let mut names: Vec<String> = Vec::new();
162 for line in source.lines() {
163 let trimmed: &str = line.trim();
164 if let Some(rest) = trimmed.strip_prefix("export function ") {
165 let name_end: usize =
166 match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
167 Some(value) => value,
168 None => rest.len(),
169 };
170 let name: &str = &rest[..name_end];
171 if !name.is_empty() {
172 names.push(name.to_string());
173 }
174 }
175 }
176 names
177}
178
179pub fn is_namespace_import(rest: &str) -> bool {
180 rest.trim_start().starts_with('*')
181}
182
183pub fn extract_namespace_alias(rest: &str) -> Option<&str> {
184 let rest: &str = rest.trim_end_matches(';').trim();
185 if !rest.starts_with('*') {
186 return None;
187 }
188 let rest: &str = rest.strip_prefix('*')?.trim();
189 let rest: &str = rest.strip_prefix("as")?.trim();
190 let end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
191 Some(value) => value,
192 None => rest.len(),
193 };
194 let alias: &str = &rest[..end];
195 if alias.is_empty() { None } else { Some(alias) }
196}
197
198pub fn extract_import_spec(rest: &str) -> Option<&str> {
199 let rest: &str = rest.trim_end_matches(';').trim();
200 let from_idx: usize = rest.find(" from ")?;
201 let spec_part: &str = rest[from_idx + " from ".len()..].trim();
202 let spec: &str = spec_part
203 .trim_start_matches('\'')
204 .trim_start_matches('"')
205 .trim_end_matches('\'')
206 .trim_end_matches('"');
207 if spec.starts_with("./") || spec.starts_with("../") {
208 Some(spec)
209 } else {
210 None
211 }
212}
213
214async fn read_snippet_module(pkg_dir: &Path, spec: &str) -> Result<Option<String>, EuvError> {
215 let snippet_path: std::path::PathBuf = resolve_snippet_path(pkg_dir, spec)?;
216 if !snippet_path.exists() {
217 return Ok(None);
218 }
219 let bytes: Vec<u8> =
220 read(&snippet_path)
221 .await
222 .map_err(|error: io::Error| EuvError::IoPath {
223 message: String::from("Failed to read wasm-pack snippet module"),
224 path: snippet_path.clone(),
225 error,
226 })?;
227 let raw: String = String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
228 message: String::from("Snippet module is not valid UTF-8"),
229 error,
230 })?;
231 Ok(Some(strip_snippet_export(&raw)))
232}
233
234fn resolve_snippet_path(pkg_dir: &Path, spec: &str) -> Result<std::path::PathBuf, EuvError> {
235 let base: &str = spec.trim_start_matches('.').trim_start_matches('/');
236 let mut path: std::path::PathBuf = pkg_dir.to_path_buf();
237 for segment in base.split('/') {
238 if segment.is_empty() || segment == "." {
239 continue;
240 }
241 if segment == ".." {
242 return Err(EuvError::Message(String::from(
243 "Snippet import spec escapes pkg directory",
244 )));
245 }
246 path.push(segment);
247 }
248 Ok(path)
249}
250
251fn strip_snippet_export(raw: &str) -> String {
252 let mut out: String = String::with_capacity(raw.len());
253 for line in raw.lines() {
254 let trimmed: &str = line.trim();
255 if trimmed.starts_with("export ") {
256 if let Some(rest) = trimmed.strip_prefix("export ") {
257 out.push_str(rest);
258 out.push('\n');
259 }
260 } else {
261 out.push_str(line);
262 out.push('\n');
263 }
264 }
265 out
266}
267
268fn strip_module_imports(source: &str) -> String {
269 let mut out: String = String::with_capacity(source.len());
270 for line in source.lines() {
271 let trimmed: &str = line.trim();
272 if trimmed.starts_with("import ") {
273 continue;
274 }
275 if trimmed.contains("import.meta.url") {
276 continue;
277 }
278 out.push_str(line);
279 out.push('\n');
280 }
281 out
282}
283
284fn strip_trailing_module_exports(source: &str) -> String {
285 let mut out: String = String::with_capacity(source.len());
286 for line in source.lines() {
287 let trimmed: &str = line.trim();
288 if trimmed.starts_with("export ") && trimmed.contains('{') && trimmed.contains('}') {
289 let inner: &str = &trimmed["export ".len()..];
290 let stripped: &str = match inner.strip_prefix('{') {
291 Some(value) => value,
292 None => {
293 out.push_str(line);
294 out.push('\n');
295 continue;
296 }
297 };
298 let close: usize = match stripped.rfind('}') {
299 Some(value) => value,
300 None => {
301 out.push_str(line);
302 out.push('\n');
303 continue;
304 }
305 };
306 let names_block: &str = &stripped[..close];
307 let inlined: String = inline_export_aliases(names_block.trim());
308 if !inlined.is_empty() {
309 out.push_str(&inlined);
310 out.push('\n');
311 }
312 continue;
313 }
314 if trimmed.starts_with("export function ") {
315 let rest: &str = match trimmed.strip_prefix("export function ") {
316 Some(value) => value,
317 None => {
318 out.push_str(line);
319 out.push('\n');
320 continue;
321 }
322 };
323 let name_end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_') {
324 Some(value) => value,
325 None => rest.len(),
326 };
327 let name: &str = &rest[..name_end];
328 if !name.is_empty() {
329 out.push_str("function ");
330 out.push_str(name);
331 out.push_str(&rest[name_end..]);
332 out.push('\n');
333 continue;
334 }
335 }
336 out.push_str(line);
337 out.push('\n');
338 }
339 out
340}
341
342fn inline_export_aliases(block: &str) -> String {
355 let mut out: String = String::new();
356 for entry in block.split(',') {
357 let entry: &str = entry.trim();
358 if entry.is_empty() {
359 continue;
360 }
361 if let Some((original, exported)) = entry.split_once(" as ") {
362 let original: &str = original.trim();
363 let exported: &str = exported.trim();
364 if original == exported || exported == "default" || original == "wasm" {
365 continue;
366 }
367 out.push_str(exported);
368 out.push_str(" = ");
369 out.push_str(original);
370 out.push_str(";\n");
371 }
372 }
373 out
374}
375
376fn serde_json_wasm_url(url: &str) -> String {
377 let mut out: String = String::with_capacity(url.len() + 2);
378 out.push('"');
379 for ch in url.chars() {
380 match ch {
381 '"' => out.push_str("\\\""),
382 '\\' => out.push_str("\\\\"),
383 '\n' => out.push_str("\\n"),
384 '\r' => out.push_str("\\r"),
385 '\t' => out.push_str("\\t"),
386 '\u{08}' => out.push_str("\\b"),
387 '\u{0C}' => out.push_str("\\f"),
388 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
389 c => out.push(c),
390 }
391 }
392 out.push('"');
393 out
394}