1use super::*;
2
3pub fn has_build_mode_flag(wasm_pack_args: &[String]) -> bool {
16 wasm_pack_args
17 .iter()
18 .any(|arg: &String| arg == DEV_FLAG || arg == RELEASE_FLAG || arg == PROFILING_FLAG)
19}
20
21pub fn filter_euv_args(wasm_pack_args: &[String]) -> Vec<String> {
36 let raw_args: &[String] = if let Some(position) = wasm_pack_args
37 .iter()
38 .rposition(|arg: &String| arg == DOUBLE_DASH)
39 {
40 &wasm_pack_args[position + 1..]
41 } else {
42 wasm_pack_args
43 };
44 let mut filtered: Vec<String> = Vec::new();
45 let mut skip_next: bool = false;
46 for arg in raw_args {
47 if skip_next {
48 skip_next = false;
49 continue;
50 }
51 if EUV_ARGS.contains(&arg.as_str()) {
52 if arg.contains('=') {
53 continue;
54 }
55 skip_next = true;
56 continue;
57 }
58 filtered.push(arg.clone());
59 }
60 filtered
61}
62
63pub fn reconcile_args(args: &mut ModeArgs) {
78 let wasm_pack_args: Vec<String> = args.get_wasm_pack_args().clone();
79 let mut crate_path: Option<PathBuf> = None;
80 let mut port: Option<u16> = None;
81 let mut www_dir: Option<String> = None;
82 let mut index_html: Option<Option<PathBuf>> = None;
83 let mut no_gitignore: Option<bool> = None;
84 let mut dev: Option<bool> = None;
85 let mut release: Option<bool> = None;
86 let mut profiling: Option<bool> = None;
87 let mut iter: Iter<String> = wasm_pack_args.iter();
88 while let Some(arg) = iter.next() {
89 match arg.as_str() {
90 CRATE_PATH_ARG | CRATE_PATH_ARG_SHORT => {
91 if let Some(value) = iter.next() {
92 crate_path = Some(PathBuf::from(value));
93 }
94 }
95 PORT_ARG | PORT_ARG_SHORT => {
96 if let Some(value) = iter.next()
97 && let Ok(parsed_port) = value.parse::<u16>()
98 {
99 port = Some(parsed_port);
100 }
101 }
102 WWW_DIR_ARG => {
103 if let Some(value) = iter.next() {
104 www_dir = Some(value.clone());
105 }
106 }
107 INDEX_HTML_ARG => {
108 if let Some(value) = iter.next() {
109 index_html = Some(Some(PathBuf::from(value)));
110 }
111 }
112 NO_GITIGNORE_ARG => {
113 no_gitignore = Some(true);
114 }
115 DEV_FLAG => {
116 dev = Some(true);
117 }
118 RELEASE_FLAG => {
119 release = Some(true);
120 }
121 PROFILING_FLAG => {
122 profiling = Some(true);
123 }
124 other => {
125 if let Some(value) = other.strip_prefix(&format!("{CRATE_PATH_ARG}=")) {
126 crate_path = Some(PathBuf::from(value));
127 } else if let Some(value) = other.strip_prefix(&format!("{PORT_ARG}=")) {
128 if let Ok(parsed_port) = value.parse::<u16>() {
129 port = Some(parsed_port);
130 }
131 } else if let Some(value) = other.strip_prefix(&format!("{WWW_DIR_ARG}=")) {
132 www_dir = Some(value.to_string());
133 } else if let Some(value) = other.strip_prefix(&format!("{INDEX_HTML_ARG}=")) {
134 index_html = Some(Some(PathBuf::from(value)));
135 }
136 }
137 }
138 }
139 if let Some(value) = crate_path {
140 args.set_crate_path(value);
141 }
142 if let Some(value) = port {
143 args.set_port(value);
144 }
145 if let Some(value) = www_dir {
146 args.set_www_dir(value);
147 }
148 if let Some(value) = index_html {
149 args.set_index_html(value);
150 }
151 if let Some(value) = no_gitignore {
152 args.set_no_gitignore(value);
153 }
154 if let Some(value) = dev {
155 args.set_dev(value);
156 }
157 if let Some(value) = release {
158 args.set_release(value);
159 }
160 if let Some(value) = profiling {
161 args.set_profiling(value);
162 }
163}
164
165pub fn resolve_build_mode(args: &ModeArgs) -> BuildMode {
180 if args.get_profiling() {
181 BuildMode::Profiling
182 } else if args.get_release() {
183 BuildMode::Release
184 } else if args.get_dev() {
185 BuildMode::Dev
186 } else if args
187 .get_wasm_pack_args()
188 .iter()
189 .any(|arg: &String| arg == PROFILING_FLAG)
190 {
191 BuildMode::Profiling
192 } else if args
193 .get_wasm_pack_args()
194 .iter()
195 .any(|arg: &String| arg == RELEASE_FLAG)
196 {
197 BuildMode::Release
198 } else {
199 BuildMode::Dev
200 }
201}
202
203pub fn build_mode_to_flag(build_mode: BuildMode) -> &'static str {
213 match build_mode {
214 BuildMode::Dev => DEV_FLAG,
215 BuildMode::Release => RELEASE_FLAG,
216 BuildMode::Profiling => PROFILING_FLAG,
217 }
218}
219
220async fn build_gitignore(root: &PathBuf) -> Gitignore {
230 let gitignore_path: PathBuf = root.join(GITIGNORE_FILE_NAME);
231 let mut builder: GitignoreBuilder = GitignoreBuilder::new(root);
232 let gitignore_exists: bool = metadata(&gitignore_path).await.is_ok();
233 if gitignore_exists && let Some(error) = builder.add(&gitignore_path) {
234 log::warn!("Failed to load .gitignore: {error}");
235 }
236 match builder.build() {
237 Ok(gitignore) => {
238 if gitignore_exists {
239 log::info!("Loaded .gitignore to filter file change events");
240 }
241 gitignore
242 }
243 Err(error) => {
244 log::warn!("Failed to build gitignore matcher: {error}");
245 GitignoreBuilder::new(root)
246 .build()
247 .unwrap_or_else(|_error: ignore::Error| Gitignore::empty())
248 }
249 }
250}
251
252fn extract_out_name(wasm_pack_args: &[String]) -> Option<String> {
264 let mut iter: Iter<'_, String> = wasm_pack_args.iter();
265 while let Some(arg) = iter.next() {
266 if arg == OUT_NAME_ARG {
267 return iter.next().cloned();
268 }
269 if let Some(value) = arg.strip_prefix(&format!("{OUT_NAME_ARG}=")) {
270 return Some(value.to_string());
271 }
272 }
273 None
274}
275
276fn extract_out_dir(wasm_pack_args: &[String]) -> Option<String> {
288 let mut iter: Iter<'_, String> = wasm_pack_args.iter();
289 while let Some(arg) = iter.next() {
290 if arg == OUT_DIR_ARG {
291 return iter.next().cloned();
292 }
293 if let Some(value) = arg.strip_prefix(&format!("{OUT_DIR_ARG}=")) {
294 return Some(value.to_string());
295 }
296 }
297 None
298}
299
300pub fn resolve_out_name(args: &ModeArgs) -> String {
315 let name: String = if let Some(out_name) = extract_out_name(args.get_wasm_pack_args()) {
316 out_name.replace(STR_HYPHEN, STR_UNDERSCORE)
317 } else {
318 let cargo_toml_path: PathBuf = args.get_crate_path().join(CARGO_TOML_FILE_NAME);
319 read_crate_name_from_toml(&cargo_toml_path)
320 .unwrap_or_else(|| {
321 args.get_crate_path()
322 .file_name()
323 .unwrap_or_default()
324 .to_string_lossy()
325 .to_string()
326 })
327 .replace(STR_HYPHEN, STR_UNDERSCORE)
328 };
329 format!("{name}{JS_EXTENSION}")
330}
331
332fn read_crate_name_from_toml(path: &Path) -> Option<String> {
344 let content: String = std::fs::read_to_string(path).ok()?;
345 let mut in_package: bool = false;
346 for line in content.lines() {
347 let trimmed: &str = line.trim();
348 if trimmed.starts_with('[') {
349 in_package = trimmed == "[package]";
350 continue;
351 }
352 if in_package
353 && trimmed.starts_with("name")
354 && let Some(value) = trimmed.strip_prefix("name")
355 {
356 let value: &str = value.trim().strip_prefix('=')?.trim();
357 let value: &str = value.strip_prefix('"')?.strip_suffix('"')?;
358 return Some(value.to_string());
359 }
360 }
361 None
362}
363
364fn compute_relative_path(base: &Path, target: &Path) -> PathBuf {
379 let base_components: Vec<Component> = base.components().collect();
380 let target_components: Vec<Component> = target.components().collect();
381 let common_len: usize = base_components
382 .iter()
383 .zip(target_components.iter())
384 .take_while(
385 |(base_component, target_component): &(&Component, &Component)| {
386 base_component == target_component
387 },
388 )
389 .count();
390 let mut result: PathBuf = PathBuf::new();
391 for _ in &base_components[common_len..] {
392 result.push("..");
393 }
394 for component in &target_components[common_len..] {
395 if let Component::Normal(os_str) = component {
396 result.push(os_str);
397 }
398 }
399 result
400}
401
402pub async fn resolve_serving_root(args: &ModeArgs) -> PathBuf {
416 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
417 let out_dir_absolute: PathBuf = resolve_out_dir(args);
418 if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
419 resolve_www_dir(&www_absolute).await
420 } else {
421 out_dir_absolute
422 .parent()
423 .map(|p: &Path| p.to_path_buf())
424 .unwrap_or_else(|| www_absolute)
425 }
426}
427
428pub fn resolve_serving_route_prefix(args: &ModeArgs) -> String {
441 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
442 let out_dir_absolute: PathBuf = resolve_out_dir(args);
443 let serving_root: PathBuf = if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
444 www_absolute
445 } else {
446 out_dir_absolute
447 .parent()
448 .map(|p: &Path| p.to_path_buf())
449 .unwrap_or_else(|| www_absolute)
450 };
451 serving_root
452 .strip_prefix(args.get_crate_path())
453 .map(|rel: &Path| {
454 rel.to_string_lossy()
455 .replace(CHAR_SLASH_BACK, STR_SLASH_FORWARD)
456 })
457 .unwrap_or_else(|_| {
458 args.get_www_dir()
459 .replace(CHAR_SLASH_BACK, STR_SLASH_FORWARD)
460 })
461}
462
463pub fn resolve_import_path(args: &ModeArgs) -> String {
477 let out_name: String = resolve_out_name(args);
478 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
479 let out_dir_absolute: PathBuf = resolve_out_dir(args);
480 let serving_root: PathBuf = if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
481 www_absolute
482 } else {
483 out_dir_absolute
484 .parent()
485 .map(|p: &Path| p.to_path_buf())
486 .unwrap_or_else(|| www_absolute)
487 };
488 let relative: PathBuf = compute_relative_path(&serving_root, &out_dir_absolute);
489 let mut components: Vec<String> = relative
490 .components()
491 .filter_map(|component: Component| match component {
492 Component::Normal(os_str) => os_str.to_str().map(|text: &str| text.to_string()),
493 Component::ParentDir => Some(PARENT_DIR.to_string()),
494 _ => None,
495 })
496 .collect();
497 components.push(out_name);
498 format!("{RELATIVE_PATH_PREFIX}{}", components.join(PATH_SEPARATOR))
499}
500
501pub fn resolve_out_dir(args: &ModeArgs) -> PathBuf {
516 let out_dir_path: PathBuf = PathBuf::from(
517 extract_out_dir(args.get_wasm_pack_args())
518 .unwrap_or_else(|| format!("{}/{PKG_DIR_NAME}", args.get_www_dir())),
519 );
520 if out_dir_path.is_absolute() {
521 out_dir_path
522 } else {
523 args.get_crate_path().join(&out_dir_path)
524 }
525}
526
527pub async fn run_build_only_pipeline(args: &ModeArgs) -> Result<(), EuvError> {
541 let src_path: PathBuf = args.get_crate_path().join(SRC_DIR_NAME);
542 if let Err(error) = format_dir(&src_path, FmtMode::Write).await {
543 log::warn!("euv fmt error: {error}");
544 }
545 let out_dir: PathBuf = resolve_out_dir(args);
546 clean_out_dir(&out_dir).await;
547 build_wasm(args).await?;
548 log::info!("WASM build completed successfully");
549 let html_config: HtmlConfig = HtmlConfig::new(
550 resolve_serving_root(args).await,
551 resolve_import_path(args),
552 resolve_build_mode(args) == BuildMode::Release,
553 args.try_get_index_html().clone(),
554 false,
555 );
556 generate_html(&html_config).await?;
557 Ok(())
558}
559
560pub async fn clean_out_dir(out_dir: &Path) {
570 let mut entries: ReadDir = match read_dir(out_dir).await {
571 Ok(dir) => dir,
572 Err(_) => return,
573 };
574 while let Ok(Some(entry)) = entries.next_entry().await {
575 let path: PathBuf = entry.path();
576 if path.is_dir() {
577 if let Err(error) = remove_dir_all(&path).await {
578 log::warn!("Failed to remove directory '{}': {error}", path.display());
579 }
580 } else if let Err(error) = remove_file(&path).await {
581 log::warn!("Failed to remove file '{}': {error}", path.display());
582 }
583 }
584}
585
586pub async fn run_build_pipeline(
598 args: &ModeArgs,
599 reload_tx: Option<&broadcast::Sender<ReloadEvent>>,
600) -> Result<String, EuvError> {
601 let src_path: PathBuf = args.get_crate_path().join(SRC_DIR_NAME);
602 if let Err(error) = format_dir(&src_path, FmtMode::Write).await {
603 log::warn!("euv fmt error: {error}");
604 }
605 match build_wasm(args).await {
606 Ok(()) => {
607 log::info!("WASM build completed successfully");
608 if let Some(sender) = reload_tx {
609 let _: Result<usize, tokio::sync::broadcast::error::SendError<ReloadEvent>> =
610 sender.send(ReloadEvent::Reload);
611 }
612 }
613 Err(error) => {
614 log::error!("WASM build failed: {error}");
615 if let Some(sender) = reload_tx {
616 let _: Result<usize, tokio::sync::broadcast::error::SendError<ReloadEvent>> =
617 sender.send(ReloadEvent::Error(error.to_string()));
618 }
619 }
620 }
621 let html_config: HtmlConfig = HtmlConfig::new(
622 resolve_serving_root(args).await,
623 resolve_import_path(args),
624 resolve_build_mode(args) == BuildMode::Release,
625 args.try_get_index_html().clone(),
626 true,
627 );
628 let html: String = generate_html(&html_config).await?;
629 Ok(html)
630}
631
632pub(crate) async fn watch_and_build(state: Arc<AppState>) -> Result<(), EuvError> {
642 let crate_path: PathBuf = state.get_args().get_crate_path().clone();
643 let src_path: PathBuf = crate_path.join(SRC_DIR_NAME);
644 let gitignore: Gitignore = build_gitignore(&crate_path).await;
645 let (tx, mut rx): (Sender<Event>, Receiver<Event>) = channel(32);
646 let mut watcher: RecommendedWatcher = RecommendedWatcher::new(
647 move |result: Result<Event, notify::Error>| {
648 if let Ok(event) = result {
649 let _: Result<(), tokio::sync::mpsc::error::SendError<Event>> =
650 tx.blocking_send(event);
651 }
652 },
653 Config::default(),
654 )?;
655 watcher.watch(&src_path, RecursiveMode::Recursive)?;
656 log::info!("Watching {} for changes...", src_path.display());
657 let mut debounce: Interval = interval(Duration::from_millis(500));
658 debounce.tick().await;
659 while let Some(event) = rx.recv().await {
660 let filtered_paths: Vec<String> = event
661 .paths
662 .iter()
663 .filter(|path: &&PathBuf| !gitignore.matched(*path, path.is_dir()).is_ignore())
664 .map(|path: &PathBuf| path.display().to_string())
665 .collect();
666 if filtered_paths.is_empty() {
667 continue;
668 }
669 log::warn!("File change detected: {}", filtered_paths.join(", "));
670 debounce.reset();
671 sleep(Duration::from_millis(300)).await;
672 let mut building: RwLockWriteGuard<bool> = state.get_is_building().write().await;
673 if *building {
674 continue;
675 }
676 *building = true;
677 drop(building);
678 let state_for_build: Arc<AppState> = Arc::clone(&state);
679 spawn(async move {
680 let args: ModeArgs = state_for_build.get_args().clone();
681 let reload_tx: broadcast::Sender<ReloadEvent> = state_for_build.get_reload_tx().clone();
682 match run_build_pipeline(&args, Some(&reload_tx)).await {
683 Ok(html) => {
684 let mut content: RwLockWriteGuard<String> =
685 state_for_build.get_html_content().write().await;
686 *content = html;
687 }
688 Err(error) => {
689 log::error!("Build pipeline error: {error}");
690 }
691 }
692 let mut building: RwLockWriteGuard<bool> =
693 state_for_build.get_is_building().write().await;
694 *building = false;
695 });
696 }
697 Ok(())
698}
699
700pub async fn build_wasm(args: &ModeArgs) -> Result<(), EuvError> {
715 let build_mode: BuildMode = resolve_build_mode(args);
716 let build_mode_flag: &str = build_mode_to_flag(build_mode);
717 let filtered_args: Vec<String> = filter_euv_args(args.get_wasm_pack_args());
718 let has_existing_build_mode: bool = has_build_mode_flag(&filtered_args);
719 let default_out_dir: String = format!("{}/{PKG_DIR_NAME}", args.get_www_dir());
720 let mut command: Command = Command::new(WASM_PACK_COMMAND);
721 command.arg(WASM_PACK_BUILD_SUBCOMMAND);
722 if !has_existing_build_mode {
723 command.arg(build_mode_flag);
724 }
725 command
726 .args(&filtered_args)
727 .env(RUST_MIN_STACK_ENV, RUST_MIN_STACK_VALUE);
728 let has_out_dir: bool = extract_out_dir(&filtered_args).is_some();
729 if !has_out_dir {
730 command.arg(OUT_DIR_ARG).arg(&default_out_dir);
731 }
732 let has_target: bool = filtered_args
733 .iter()
734 .any(|arg: &String| arg == TARGET_ARG || arg.starts_with(&format!("{TARGET_ARG}=")));
735 if !has_target {
736 command.arg(TARGET_ARG).arg(TARGET_WEB);
737 }
738 command.current_dir(args.get_crate_path());
739 command.stdout(Stdio::piped()).stderr(Stdio::piped());
740 let display_args: Vec<String> = (if has_existing_build_mode {
741 filtered_args.to_vec()
742 } else {
743 std::iter::once(build_mode_flag.to_string())
744 .chain(filtered_args.iter().cloned())
745 .collect::<Vec<String>>()
746 })
747 .into_iter()
748 .chain(if has_out_dir {
749 Vec::new()
750 } else {
751 vec![OUT_DIR_ARG.to_string(), default_out_dir.clone()]
752 })
753 .chain(if has_target {
754 Vec::new()
755 } else {
756 vec![TARGET_ARG.to_string(), TARGET_WEB.to_string()]
757 })
758 .collect();
759 let out_dir_absolute: PathBuf = resolve_out_dir(args);
760 create_dir_all(&out_dir_absolute)
761 .await
762 .map_err(|error: io::Error| EuvError::IoPath {
763 message: String::from("Failed to create output directory"),
764 path: out_dir_absolute.clone(),
765 error,
766 })?;
767 log::info!(
768 "Running: {WASM_PACK_COMMAND} {WASM_PACK_BUILD_SUBCOMMAND} {} ...",
769 display_args.join(" ")
770 );
771 let output: Output = command
772 .output()
773 .await
774 .map_err(|error: io::Error| EuvError::Io {
775 message: String::from("Failed to execute wasm-pack"),
776 error,
777 })?;
778 let stdout: String = String::from_utf8_lossy(&output.stdout).to_string();
779 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
780 if args.get_no_gitignore() {
781 let gitignore_path: PathBuf = out_dir_absolute.join(GITIGNORE_FILE_NAME);
782 if gitignore_path.exists()
783 && let Err(error) = remove_file(&gitignore_path).await
784 {
785 log::warn!("Failed to remove '{}': {error}", gitignore_path.display());
786 }
787 }
788 for line in stdout.lines().filter(|line: &&str| !line.is_empty()) {
789 log::info!("{line}");
790 }
791 if output.status.success() {
792 for line in stderr.lines().filter(|line: &&str| !line.is_empty()) {
793 log::info!("{line}");
794 }
795 } else {
796 for line in stderr.lines().filter(|line: &&str| !line.is_empty()) {
797 log::error!("{line}");
798 }
799 return Err(EuvError::Message(String::from("wasm-pack build failed")));
800 }
801 Ok(())
802}
803
804pub fn print_banner(action: Action) {
810 let version: &str = env!("CARGO_PKG_VERSION");
811 if version.is_empty() {
812 log::warn!("Failed to parse version from root Cargo.toml");
813 } else {
814 log::info!("euv v{version}");
815 }
816 let action_name: &str = match action {
817 Action::Run => ACTION_RUN,
818 Action::Build => ACTION_BUILD,
819 };
820 log::info!("Mode: {action_name}");
821 log::info!(
822 "Use .gitignore to filter file change events; pass --no-gitignore to remove .gitignore from output"
823 );
824}
825
826pub(crate) fn print_server_urls(config: &ServerUrlConfig) {
838 let port: u16 = config.get_port();
839 let route_prefix: &str = config.get_route_prefix();
840 let index_html_file_name: &str = config.get_index_html_file_name();
841 let mut addresses: Vec<IpAddr> = Vec::new();
842 match if_addrs::get_if_addrs() {
843 Ok(interfaces) => {
844 for interface in interfaces {
845 let ip: IpAddr = interface.addr.ip();
846 if !addresses.contains(&ip) {
847 addresses.push(ip);
848 }
849 }
850 }
851 Err(error) => {
852 log::warn!("Failed to enumerate network interfaces: {error}");
853 }
854 }
855 if addresses.is_empty() {
856 addresses.push(IpAddr::V4(Ipv4Addr::LOCALHOST));
857 }
858 for ip in addresses {
859 let host: String = match ip {
860 IpAddr::V6(_) => format!("[{ip}]"),
861 IpAddr::V4(_) => format!("{ip}"),
862 };
863 let url: String =
864 format!("{HTTP_SCHEME}://{host}:{port}/{route_prefix}/{index_html_file_name}");
865 log::info!("Server: {url}");
866 match QrCode::new(url.as_str()) {
867 Ok(code) => {
868 let string: String = code.render::<Dense1x2>().quiet_zone(false).build();
869 for line in string.lines() {
870 log::info!("{line}");
871 }
872 }
873 Err(error) => {
874 log::warn!("Failed to generate QR code: {error}");
875 }
876 }
877 }
878}
879
880pub(crate) async fn build_inline_bridge(
900 pkg_dir: &Path,
901 js_name: &str,
902 wasm_url: &str,
903) -> Result<String, EuvError> {
904 let js_path: std::path::PathBuf = pkg_dir.join(js_name);
905 let bridge_source: String =
906 read_to_string(&js_path)
907 .await
908 .map_err(|error: io::Error| EuvError::IoPath {
909 message: String::from("Failed to read wasm-bindgen JS bridge"),
910 path: js_path.clone(),
911 error,
912 })?;
913 let snippet_bodies: String = collect_snippet_bodies(&bridge_source, pkg_dir).await?;
914 let stripped: String = strip_module_imports(&bridge_source);
915 let js_only: String = strip_trailing_module_exports(&stripped);
916 let wasm_url_json: String = serde_json_wasm_url(wasm_url);
917 Ok(format!(
918 "(function() {{\n\
919 {snippet_bodies}\n\
920 {js_only}\n\
921 var __euv_wasm_url = {wasm_url_json};\n\
922 if (typeof __wbg_init === 'function') {{\n\
923 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\
924 if (typeof main === 'function') main();\n\
925 }}).catch(function(e) {{ console.error('[euv] inline bridge init failed:', e); }});\n\
926 }} else if (typeof initSync === 'function') {{\n\
927 initSync();\n\
928 if (typeof main === 'function') main();\n\
929 }} else {{\n\
930 console.error('[euv] inline bridge: no __wbg_init or initSync exported');\n\
931 }}\n\
932 }})();\n"
933 ))
934}
935
936pub(crate) fn build_module_fallback_bridge(import_path: &str) -> String {
948 format!(
949 " import init, {{ main }} from '{import_path}';\n \
950 await init();\n \
951 main();\n"
952 )
953}
954
955pub(crate) fn inline_bridge_disabled() -> bool {
961 matches!(std::env::var(EUV_NO_INLINE_BRIDGE_ENV), Ok(value) if !value.is_empty())
962}
963
964async fn collect_snippet_bodies(bridge_source: &str, pkg_dir: &Path) -> Result<String, EuvError> {
979 let mut bodies: String = String::new();
980 let mut namespace_imports: Vec<NamespaceImport> = Vec::new();
981 for line in bridge_source.lines() {
982 let trimmed: &str = line.trim();
983 if !trimmed.starts_with("import ") {
984 continue;
985 }
986 let rest: &str = match trimmed.strip_prefix("import ") {
987 Some(value) => value,
988 None => continue,
989 };
990 let spec: &str = match extract_import_spec(rest) {
991 Some(value) => value,
992 None => continue,
993 };
994 if is_namespace_import(rest)
995 && let Some(alias) = extract_namespace_alias(rest)
996 {
997 namespace_imports.push(NamespaceImport {
998 alias: alias.to_string(),
999 spec: spec.to_string(),
1000 });
1001 }
1002 let body: Option<String> = read_snippet_module(pkg_dir, spec).await?;
1003 if let Some(body) = body {
1004 if !bodies.is_empty() {
1005 bodies.push('\n');
1006 }
1007 bodies.push_str(&body);
1008 }
1009 }
1010 for ns in &namespace_imports {
1011 let snippet_path: std::path::PathBuf = resolve_snippet_path(pkg_dir, &ns.spec)?;
1012 let exports: Vec<String> = if snippet_path.exists() {
1013 let raw: String = read_to_string(&snippet_path)
1014 .await
1015 .map_err(|error: io::Error| EuvError::IoPath {
1016 message: String::from(
1017 "Failed to read wasm-pack snippet module for namespace export scan",
1018 ),
1019 path: snippet_path.clone(),
1020 error,
1021 })?;
1022 extract_exported_function_names(&raw)
1023 } else {
1024 Vec::new()
1025 };
1026 if !bodies.is_empty() {
1027 bodies.push('\n');
1028 }
1029 bodies.push_str(&format!("var {} = {{", ns.alias));
1030 for (i, name) in exports.iter().enumerate() {
1031 if i > 0 {
1032 bodies.push(',');
1033 }
1034 bodies.push_str(name);
1035 bodies.push(':');
1036 bodies.push_str(name);
1037 }
1038 bodies.push_str("};\n");
1039 }
1040 Ok(bodies)
1041}
1042
1043pub fn extract_exported_function_names(source: &str) -> Vec<String> {
1056 let mut names: Vec<String> = Vec::new();
1057 for line in source.lines() {
1058 let trimmed: &str = line.trim();
1059 if let Some(rest) = trimmed.strip_prefix("export function ") {
1060 let name_end: usize =
1061 match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
1062 Some(value) => value,
1063 None => rest.len(),
1064 };
1065 let name: &str = &rest[..name_end];
1066 if !name.is_empty() {
1067 names.push(name.to_string());
1068 }
1069 }
1070 }
1071 names
1072}
1073
1074pub fn is_namespace_import(rest: &str) -> bool {
1084 rest.trim_start().starts_with('*')
1085}
1086
1087pub fn extract_namespace_alias(rest: &str) -> Option<&str> {
1098 let rest: &str = rest.trim_end_matches(';').trim();
1099 if !rest.starts_with('*') {
1100 return None;
1101 }
1102 let rest: &str = rest.strip_prefix('*')?.trim();
1103 let rest: &str = rest.strip_prefix("as")?.trim();
1104 let end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
1105 Some(value) => value,
1106 None => rest.len(),
1107 };
1108 let alias: &str = &rest[..end];
1109 if alias.is_empty() { None } else { Some(alias) }
1110}
1111
1112pub fn extract_import_spec(rest: &str) -> Option<&str> {
1127 let rest: &str = rest.trim_end_matches(';').trim();
1128 let from_idx: usize = rest.find(" from ")?;
1129 let spec_part: &str = rest[from_idx + " from ".len()..].trim();
1130 let spec: &str = spec_part
1131 .trim_start_matches('\'')
1132 .trim_start_matches('"')
1133 .trim_end_matches('\'')
1134 .trim_end_matches('"');
1135 if spec.starts_with("./") || spec.starts_with("../") {
1136 Some(spec)
1137 } else {
1138 None
1139 }
1140}
1141
1142async fn read_snippet_module(pkg_dir: &Path, spec: &str) -> Result<Option<String>, EuvError> {
1156 let snippet_path: std::path::PathBuf = resolve_snippet_path(pkg_dir, spec)?;
1157 if !snippet_path.exists() {
1158 return Ok(None);
1159 }
1160 let bytes: Vec<u8> =
1161 read(&snippet_path)
1162 .await
1163 .map_err(|error: io::Error| EuvError::IoPath {
1164 message: String::from("Failed to read wasm-pack snippet module"),
1165 path: snippet_path.clone(),
1166 error,
1167 })?;
1168 let raw: String = String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
1169 message: String::from("Snippet module is not valid UTF-8"),
1170 error,
1171 })?;
1172 Ok(Some(strip_snippet_export(&raw)))
1173}
1174
1175fn resolve_snippet_path(pkg_dir: &Path, spec: &str) -> Result<std::path::PathBuf, EuvError> {
1187 let base: &str = spec.trim_start_matches('.').trim_start_matches('/');
1188 let mut path: std::path::PathBuf = pkg_dir.to_path_buf();
1189 for segment in base.split('/') {
1190 if segment.is_empty() || segment == "." {
1191 continue;
1192 }
1193 if segment == ".." {
1194 return Err(EuvError::Message(String::from(
1195 "Snippet import spec escapes pkg directory",
1196 )));
1197 }
1198 path.push(segment);
1199 }
1200 Ok(path)
1201}
1202
1203fn strip_snippet_export(raw: &str) -> String {
1214 let mut out: String = String::with_capacity(raw.len());
1215 for line in raw.lines() {
1216 let trimmed: &str = line.trim();
1217 if trimmed.starts_with("export ") {
1218 if let Some(rest) = trimmed.strip_prefix("export ") {
1219 out.push_str(rest);
1220 out.push('\n');
1221 }
1222 } else {
1223 out.push_str(line);
1224 out.push('\n');
1225 }
1226 }
1227 out
1228}
1229
1230fn strip_module_imports(source: &str) -> String {
1241 let mut out: String = String::with_capacity(source.len());
1242 for line in source.lines() {
1243 let trimmed: &str = line.trim();
1244 if trimmed.starts_with("import ") {
1245 continue;
1246 }
1247 if trimmed.contains("import.meta.url") {
1248 continue;
1249 }
1250 out.push_str(line);
1251 out.push('\n');
1252 }
1253 out
1254}
1255
1256fn strip_trailing_module_exports(source: &str) -> String {
1268 let mut out: String = String::with_capacity(source.len());
1269 for line in source.lines() {
1270 let trimmed: &str = line.trim();
1271 if trimmed.starts_with("export ") && trimmed.contains('{') && trimmed.contains('}') {
1272 let inner: &str = &trimmed["export ".len()..];
1273 let stripped: &str = match inner.strip_prefix('{') {
1274 Some(value) => value,
1275 None => {
1276 out.push_str(line);
1277 out.push('\n');
1278 continue;
1279 }
1280 };
1281 let close: usize = match stripped.rfind('}') {
1282 Some(value) => value,
1283 None => {
1284 out.push_str(line);
1285 out.push('\n');
1286 continue;
1287 }
1288 };
1289 let names_block: &str = &stripped[..close];
1290 let inlined: String = inline_export_aliases(names_block.trim());
1291 if !inlined.is_empty() {
1292 out.push_str(&inlined);
1293 out.push('\n');
1294 }
1295 continue;
1296 }
1297 if trimmed.starts_with("export function ") {
1298 let rest: &str = match trimmed.strip_prefix("export function ") {
1299 Some(value) => value,
1300 None => {
1301 out.push_str(line);
1302 out.push('\n');
1303 continue;
1304 }
1305 };
1306 let name_end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_') {
1307 Some(value) => value,
1308 None => rest.len(),
1309 };
1310 let name: &str = &rest[..name_end];
1311 if !name.is_empty() {
1312 out.push_str("function ");
1313 out.push_str(name);
1314 out.push_str(&rest[name_end..]);
1315 out.push('\n');
1316 continue;
1317 }
1318 }
1319 out.push_str(line);
1320 out.push('\n');
1321 }
1322 out
1323}
1324
1325fn inline_export_aliases(block: &str) -> String {
1339 let mut out: String = String::new();
1340 for entry in block.split(',') {
1341 let entry: &str = entry.trim();
1342 if entry.is_empty() {
1343 continue;
1344 }
1345 if let Some((original, exported)) = entry.split_once(" as ") {
1346 let original: &str = original.trim();
1347 let exported: &str = exported.trim();
1348 if original == exported || exported == "default" || original == "wasm" {
1349 continue;
1350 }
1351 out.push_str(exported);
1352 out.push_str(" = ");
1353 out.push_str(original);
1354 out.push_str(";\n");
1355 }
1356 }
1357 out
1358}
1359
1360fn serde_json_wasm_url(url: &str) -> String {
1375 let mut out: String = String::with_capacity(url.len() + 2);
1376 out.push('"');
1377 for ch in url.chars() {
1378 match ch {
1379 '"' => out.push_str("\\\""),
1380 '\\' => out.push_str("\\\\"),
1381 '\n' => out.push_str("\\n"),
1382 '\r' => out.push_str("\\r"),
1383 '\t' => out.push_str("\\t"),
1384 '\u{08}' => out.push_str("\\b"),
1385 '\u{0C}' => out.push_str("\\f"),
1386 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1387 c => out.push(c),
1388 }
1389 }
1390 out.push('"');
1391 out
1392}