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: IoError| 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 __wbg_init(new URL(__euv_wasm_url, location.href).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: IoError| 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> = read(&snippet_path)
220 .await
221 .map_err(|error: IoError| EuvError::IoPath {
222 message: String::from("Failed to read wasm-pack snippet module"),
223 path: snippet_path.clone(),
224 error,
225 })?;
226 let raw: String = String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
227 message: String::from("Snippet module is not valid UTF-8"),
228 error,
229 })?;
230 Ok(Some(strip_snippet_export(&raw)))
231}
232
233fn resolve_snippet_path(pkg_dir: &Path, spec: &str) -> Result<std::path::PathBuf, EuvError> {
234 let base: &str = spec.trim_start_matches('.').trim_start_matches('/');
235 let mut path: std::path::PathBuf = pkg_dir.to_path_buf();
236 for segment in base.split('/') {
237 if segment.is_empty() || segment == "." {
238 continue;
239 }
240 if segment == ".." {
241 return Err(EuvError::Message(String::from(
242 "Snippet import spec escapes pkg directory",
243 )));
244 }
245 path.push(segment);
246 }
247 Ok(path)
248}
249
250fn strip_snippet_export(raw: &str) -> String {
251 let mut out: String = String::with_capacity(raw.len());
252 for line in raw.lines() {
253 let trimmed: &str = line.trim();
254 if trimmed.starts_with("export ") {
255 if let Some(rest) = trimmed.strip_prefix("export ") {
256 out.push_str(rest);
257 out.push('\n');
258 }
259 } else {
260 out.push_str(line);
261 out.push('\n');
262 }
263 }
264 out
265}
266
267fn strip_module_imports(source: &str) -> String {
268 let mut out: String = String::with_capacity(source.len());
269 for line in source.lines() {
270 let trimmed: &str = line.trim();
271 if trimmed.starts_with("import ") {
272 continue;
273 }
274 if trimmed.contains("import.meta.url") {
275 continue;
276 }
277 out.push_str(line);
278 out.push('\n');
279 }
280 out
281}
282
283fn strip_trailing_module_exports(source: &str) -> String {
284 let mut out: String = String::with_capacity(source.len());
285 for line in source.lines() {
286 let trimmed: &str = line.trim();
287 if trimmed.starts_with("export ") && trimmed.contains('{') && trimmed.contains('}') {
288 let inner: &str = &trimmed["export ".len()..];
289 let stripped: &str = match inner.strip_prefix('{') {
290 Some(value) => value,
291 None => {
292 out.push_str(line);
293 out.push('\n');
294 continue;
295 }
296 };
297 let close: usize = match stripped.rfind('}') {
298 Some(value) => value,
299 None => {
300 out.push_str(line);
301 out.push('\n');
302 continue;
303 }
304 };
305 let names_block: &str = &stripped[..close];
306 let inlined: String = inline_export_aliases(names_block.trim());
307 if !inlined.is_empty() {
308 out.push_str(&inlined);
309 out.push('\n');
310 }
311 continue;
312 }
313 if trimmed.starts_with("export function ") {
314 let rest: &str = match trimmed.strip_prefix("export function ") {
315 Some(value) => value,
316 None => {
317 out.push_str(line);
318 out.push('\n');
319 continue;
320 }
321 };
322 let name_end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_') {
323 Some(value) => value,
324 None => rest.len(),
325 };
326 let name: &str = &rest[..name_end];
327 if !name.is_empty() {
328 out.push_str("function ");
329 out.push_str(name);
330 out.push_str(&rest[name_end..]);
331 out.push('\n');
332 continue;
333 }
334 }
335 out.push_str(line);
336 out.push('\n');
337 }
338 out
339}
340
341fn inline_export_aliases(block: &str) -> String {
354 let mut out: String = String::new();
355 for entry in block.split(',') {
356 let entry: &str = entry.trim();
357 if entry.is_empty() {
358 continue;
359 }
360 if let Some((original, exported)) = entry.split_once(" as ") {
361 let original: &str = original.trim();
362 let exported: &str = exported.trim();
363 if original == exported || exported == "default" || original == "wasm" {
364 continue;
365 }
366 out.push_str(exported);
367 out.push_str(" = ");
368 out.push_str(original);
369 out.push_str(";\n");
370 }
371 }
372 out
373}
374
375fn serde_json_wasm_url(url: &str) -> String {
376 let mut out: String = String::with_capacity(url.len() + 2);
377 out.push('"');
378 for ch in url.chars() {
379 match ch {
380 '"' => out.push_str("\\\""),
381 '\\' => out.push_str("\\\\"),
382 '\n' => out.push_str("\\n"),
383 '\r' => out.push_str("\\r"),
384 '\t' => out.push_str("\\t"),
385 '\u{08}' => out.push_str("\\b"),
386 '\u{0C}' => out.push_str("\\f"),
387 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
388 c => out.push(c),
389 }
390 }
391 out.push('"');
392 out
393}