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 let name: &str = arg.split('=').next().unwrap_or(arg.as_str());
57 if EUV_ARGS.contains(&name) {
58 if arg.contains('=') || name == NO_GITIGNORE_ARG {
59 continue;
63 }
64 skip_next = true;
65 continue;
66 }
67 filtered.push(arg.clone());
68 }
69 filtered
70}
71
72pub fn reconcile_args(args: &mut ModeArgs) {
87 let wasm_pack_args: Vec<String> = args.get_wasm_pack_args().clone();
88 let mut crate_path: Option<PathBuf> = None;
89 let mut port: Option<u16> = None;
90 let mut www_dir: Option<String> = None;
91 let mut index_html: Option<Option<PathBuf>> = None;
92 let mut no_gitignore: Option<bool> = None;
93 let mut dev: Option<bool> = None;
94 let mut release: Option<bool> = None;
95 let mut profiling: Option<bool> = None;
96 let mut iter: Iter<String> = wasm_pack_args.iter();
97 while let Some(arg) = iter.next() {
98 match arg.as_str() {
99 CRATE_PATH_ARG | CRATE_PATH_ARG_SHORT => {
100 if let Some(value) = iter.next() {
101 crate_path = Some(PathBuf::from(value));
102 }
103 }
104 PORT_ARG | PORT_ARG_SHORT => {
105 if let Some(value) = iter.next()
106 && let Ok(parsed_port) = value.parse::<u16>()
107 {
108 port = Some(parsed_port);
109 }
110 }
111 WWW_DIR_ARG => {
112 if let Some(value) = iter.next() {
113 www_dir = Some(value.clone());
114 }
115 }
116 INDEX_HTML_ARG => {
117 if let Some(value) = iter.next() {
118 index_html = Some(Some(PathBuf::from(value)));
119 }
120 }
121 NO_GITIGNORE_ARG => {
122 no_gitignore = Some(true);
123 }
124 DEV_FLAG => {
125 dev = Some(true);
126 }
127 RELEASE_FLAG => {
128 release = Some(true);
129 }
130 PROFILING_FLAG => {
131 profiling = Some(true);
132 }
133 other => {
134 if let Some(value) = other.strip_prefix(&format!("{CRATE_PATH_ARG}=")) {
135 crate_path = Some(PathBuf::from(value));
136 } else if let Some(value) = other.strip_prefix(&format!("{PORT_ARG}=")) {
137 if let Ok(parsed_port) = value.parse::<u16>() {
138 port = Some(parsed_port);
139 }
140 } else if let Some(value) = other.strip_prefix(&format!("{WWW_DIR_ARG}=")) {
141 www_dir = Some(value.to_string());
142 } else if let Some(value) = other.strip_prefix(&format!("{INDEX_HTML_ARG}=")) {
143 index_html = Some(Some(PathBuf::from(value)));
144 }
145 }
146 }
147 }
148 if let Some(value) = crate_path {
149 args.set_crate_path(value);
150 }
151 if let Some(value) = port {
152 args.set_port(value);
153 }
154 if let Some(value) = www_dir {
155 args.set_www_dir(value);
156 }
157 if let Some(value) = index_html {
158 args.set_index_html(value);
159 }
160 if let Some(value) = no_gitignore {
161 args.set_no_gitignore(value);
162 }
163 if let Some(value) = dev {
164 args.set_dev(value);
165 }
166 if let Some(value) = release {
167 args.set_release(value);
168 }
169 if let Some(value) = profiling {
170 args.set_profiling(value);
171 }
172}
173
174pub fn resolve_build_mode(args: &ModeArgs) -> BuildMode {
189 if args.get_profiling() {
190 BuildMode::Profiling
191 } else if args.get_release() {
192 BuildMode::Release
193 } else if args.get_dev() {
194 BuildMode::Dev
195 } else if args
196 .get_wasm_pack_args()
197 .iter()
198 .any(|arg: &String| arg == PROFILING_FLAG)
199 {
200 BuildMode::Profiling
201 } else if args
202 .get_wasm_pack_args()
203 .iter()
204 .any(|arg: &String| arg == RELEASE_FLAG)
205 {
206 BuildMode::Release
207 } else {
208 BuildMode::Dev
209 }
210}
211
212pub fn build_mode_to_flag(build_mode: BuildMode) -> &'static str {
222 match build_mode {
223 BuildMode::Dev => DEV_FLAG,
224 BuildMode::Release => RELEASE_FLAG,
225 BuildMode::Profiling => PROFILING_FLAG,
226 }
227}
228
229async fn build_gitignore(root: &PathBuf) -> Gitignore {
239 let gitignore_path: PathBuf = root.join(GITIGNORE_FILE_NAME);
240 let mut builder: GitignoreBuilder = GitignoreBuilder::new(root);
241 let gitignore_exists: bool = metadata(&gitignore_path).await.is_ok();
242 if gitignore_exists && let Some(error) = builder.add(&gitignore_path) {
243 log::warn!("Failed to load .gitignore: {error}");
244 }
245 match builder.build() {
246 Ok(gitignore) => {
247 if gitignore_exists {
248 log::info!("Loaded .gitignore to filter file change events");
249 }
250 gitignore
251 }
252 Err(error) => {
253 log::warn!("Failed to build gitignore matcher: {error}");
254 GitignoreBuilder::new(root)
255 .build()
256 .unwrap_or_else(|_error: ignore::Error| Gitignore::empty())
257 }
258 }
259}
260
261fn extract_out_name(wasm_pack_args: &[String]) -> Option<String> {
273 let mut iter: Iter<'_, String> = wasm_pack_args.iter();
274 while let Some(arg) = iter.next() {
275 if arg == OUT_NAME_ARG {
276 return iter.next().cloned();
277 }
278 if let Some(value) = arg.strip_prefix(&format!("{OUT_NAME_ARG}=")) {
279 return Some(value.to_string());
280 }
281 }
282 None
283}
284
285fn extract_out_dir(wasm_pack_args: &[String]) -> Option<String> {
297 let mut iter: Iter<'_, String> = wasm_pack_args.iter();
298 while let Some(arg) = iter.next() {
299 if arg == OUT_DIR_ARG {
300 return iter.next().cloned();
301 }
302 if let Some(value) = arg.strip_prefix(&format!("{OUT_DIR_ARG}=")) {
303 return Some(value.to_string());
304 }
305 }
306 None
307}
308
309pub fn resolve_out_name(args: &ModeArgs) -> String {
324 let name: String = if let Some(out_name) = extract_out_name(args.get_wasm_pack_args()) {
325 out_name.replace(STR_HYPHEN, STR_UNDERSCORE)
326 } else {
327 let cargo_toml_path: PathBuf = args.get_crate_path().join(CARGO_TOML_FILE_NAME);
328 read_crate_name_from_toml(&cargo_toml_path)
329 .unwrap_or_else(|| {
330 args.get_crate_path()
331 .file_name()
332 .unwrap_or_default()
333 .to_string_lossy()
334 .to_string()
335 })
336 .replace(STR_HYPHEN, STR_UNDERSCORE)
337 };
338 format!("{name}{JS_EXTENSION}")
339}
340
341fn read_crate_name_from_toml(path: &Path) -> Option<String> {
353 let content: String = sync_read_to_string(path).ok()?;
354 let mut in_package: bool = false;
355 for line in content.lines() {
356 let trimmed: &str = line.trim();
357 if trimmed.starts_with('[') {
358 in_package = trimmed == "[package]";
359 continue;
360 }
361 if in_package
362 && trimmed.starts_with("name")
363 && let Some(value) = trimmed.strip_prefix("name")
364 {
365 let value: &str = value.trim().strip_prefix('=')?.trim();
366 let value: &str = value.strip_prefix('"')?.strip_suffix('"')?;
367 return Some(value.to_string());
368 }
369 }
370 None
371}
372
373fn compute_relative_path(base: &Path, target: &Path) -> PathBuf {
388 let base_components: Vec<Component> = base.components().collect();
389 let target_components: Vec<Component> = target.components().collect();
390 let common_len: usize = base_components
391 .iter()
392 .zip(target_components.iter())
393 .take_while(
394 |(base_component, target_component): &(&Component, &Component)| {
395 base_component == target_component
396 },
397 )
398 .count();
399 let mut result: PathBuf = PathBuf::new();
400 for _ in &base_components[common_len..] {
401 result.push("..");
402 }
403 for component in &target_components[common_len..] {
404 if let Component::Normal(os_str) = component {
405 result.push(os_str);
406 }
407 }
408 result
409}
410
411pub async fn resolve_serving_root(args: &ModeArgs) -> PathBuf {
425 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
426 let out_dir_absolute: PathBuf = resolve_out_dir(args);
427 if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
428 resolve_www_dir(&www_absolute).await
429 } else {
430 out_dir_absolute
431 .parent()
432 .map(|p: &Path| p.to_path_buf())
433 .unwrap_or_else(|| www_absolute)
434 }
435}
436
437pub fn resolve_serving_route_prefix(args: &ModeArgs) -> String {
450 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
451 let out_dir_absolute: PathBuf = resolve_out_dir(args);
452 let serving_root: PathBuf = if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
453 www_absolute
454 } else {
455 out_dir_absolute
456 .parent()
457 .map(|p: &Path| p.to_path_buf())
458 .unwrap_or_else(|| www_absolute)
459 };
460 serving_root
461 .strip_prefix(args.get_crate_path())
462 .map(|rel: &Path| {
463 rel.to_string_lossy()
464 .replace(CHAR_SLASH_BACK, STR_SLASH_FORWARD)
465 })
466 .unwrap_or_else(|_| {
467 args.get_www_dir()
468 .replace(CHAR_SLASH_BACK, STR_SLASH_FORWARD)
469 })
470}
471
472pub fn resolve_import_path(args: &ModeArgs) -> String {
486 let out_name: String = resolve_out_name(args);
487 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
488 let out_dir_absolute: PathBuf = resolve_out_dir(args);
489 let serving_root: PathBuf = if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
490 www_absolute
491 } else {
492 out_dir_absolute
493 .parent()
494 .map(|p: &Path| p.to_path_buf())
495 .unwrap_or_else(|| www_absolute)
496 };
497 let relative: PathBuf = compute_relative_path(&serving_root, &out_dir_absolute);
498 let mut components: Vec<String> = relative
499 .components()
500 .filter_map(|component: Component| match component {
501 Component::Normal(os_str) => os_str.to_str().map(|text: &str| text.to_string()),
502 Component::ParentDir => Some(PARENT_DIR.to_string()),
503 _ => None,
504 })
505 .collect();
506 components.push(out_name);
507 format!("{RELATIVE_PATH_PREFIX}{}", components.join(PATH_SEPARATOR))
508}
509
510pub fn resolve_out_dir(args: &ModeArgs) -> PathBuf {
525 let out_dir_path: PathBuf = PathBuf::from(
526 extract_out_dir(args.get_wasm_pack_args())
527 .unwrap_or_else(|| format!("{}/{PKG_DIR_NAME}", args.get_www_dir())),
528 );
529 if out_dir_path.is_absolute() {
530 out_dir_path
531 } else {
532 args.get_crate_path().join(&out_dir_path)
533 }
534}
535
536pub async fn run_build_only_pipeline(args: &ModeArgs) -> Result<(), EuvError> {
550 let src_path: PathBuf = args.get_crate_path().join(SRC_DIR_NAME);
551 if let Err(error) = format_dir(&src_path, FmtMode::Write).await {
552 log::warn!("euv fmt error: {error}");
553 }
554 let out_dir: PathBuf = resolve_out_dir(args);
555 clean_out_dir(&out_dir).await;
556 build_wasm(args).await?;
557 log::info!("WASM build completed successfully");
558 let html_config: HtmlConfig = HtmlConfig::new(
559 resolve_serving_root(args).await,
560 resolve_import_path(args),
561 resolve_build_mode(args) == BuildMode::Release,
562 args.try_get_index_html().clone(),
563 false,
564 );
565 generate_html(&html_config).await?;
566 Ok(())
567}
568
569pub async fn clean_out_dir(out_dir: &Path) {
579 let mut entries: ReadDir = match read_dir(out_dir).await {
580 Ok(dir) => dir,
581 Err(_) => return,
582 };
583 while let Ok(Some(entry)) = entries.next_entry().await {
584 let path: PathBuf = entry.path();
585 if path.is_dir() {
586 if let Err(error) = remove_dir_all(&path).await {
587 log::warn!("Failed to remove directory '{}': {error}", path.display());
588 }
589 } else if let Err(error) = remove_file(&path).await {
590 log::warn!("Failed to remove file '{}': {error}", path.display());
591 }
592 }
593}
594
595pub async fn run_build_pipeline(
607 args: &ModeArgs,
608 reload_tx: Option<&broadcast::Sender<ReloadEvent>>,
609) -> Result<String, EuvError> {
610 let src_path: PathBuf = args.get_crate_path().join(SRC_DIR_NAME);
611 if let Err(error) = format_dir(&src_path, FmtMode::Write).await {
612 log::warn!("euv fmt error: {error}");
613 }
614 match build_wasm(args).await {
615 Ok(()) => {
616 log::info!("WASM build completed successfully");
617 if let Some(sender) = reload_tx {
618 let _: Result<usize, tokio::sync::broadcast::error::SendError<ReloadEvent>> =
619 sender.send(ReloadEvent::Reload);
620 }
621 }
622 Err(error) => {
623 log::error!("WASM build failed: {error}");
624 if let Some(sender) = reload_tx {
625 let _: Result<usize, tokio::sync::broadcast::error::SendError<ReloadEvent>> =
626 sender.send(ReloadEvent::Error(error.to_string()));
627 }
628 }
629 }
630 let html_config: HtmlConfig = HtmlConfig::new(
631 resolve_serving_root(args).await,
632 resolve_import_path(args),
633 resolve_build_mode(args) == BuildMode::Release,
634 args.try_get_index_html().clone(),
635 true,
636 );
637 let html: String = generate_html(&html_config).await?;
638 Ok(html)
639}
640
641pub(crate) async fn watch_and_build(state: Arc<AppState>) -> Result<(), EuvError> {
651 let crate_path: PathBuf = state.get_args().get_crate_path().clone();
652 let src_path: PathBuf = crate_path.join(SRC_DIR_NAME);
653 let gitignore: Gitignore = build_gitignore(&crate_path).await;
654 let (tx, mut rx): (Sender<Event>, Receiver<Event>) = channel(32);
655 let mut watcher: RecommendedWatcher = RecommendedWatcher::new(
656 move |result: Result<Event, notify::Error>| {
657 if let Ok(event) = result {
658 let _: Result<(), tokio::sync::mpsc::error::SendError<Event>> =
659 tx.blocking_send(event);
660 }
661 },
662 Config::default(),
663 )?;
664 watcher.watch(&src_path, RecursiveMode::Recursive)?;
665 log::info!("Watching {} for changes...", src_path.display());
666 let mut debounce: Interval = interval(Duration::from_millis(500));
667 debounce.tick().await;
668 while let Some(event) = rx.recv().await {
669 let filtered_paths: Vec<String> = event
670 .paths
671 .iter()
672 .filter(|path: &&PathBuf| !gitignore.matched(*path, path.is_dir()).is_ignore())
673 .map(|path: &PathBuf| path.display().to_string())
674 .collect();
675 if filtered_paths.is_empty() {
676 continue;
677 }
678 log::warn!("File change detected: {}", filtered_paths.join(", "));
679 debounce.reset();
680 sleep(Duration::from_millis(300)).await;
681 let mut building: RwLockWriteGuard<bool> = state.get_is_building().write().await;
682 if *building {
683 continue;
684 }
685 *building = true;
686 drop(building);
687 let state_for_build: Arc<AppState> = Arc::clone(&state);
688 spawn(async move {
689 let args: ModeArgs = state_for_build.get_args().clone();
690 let reload_tx: broadcast::Sender<ReloadEvent> = state_for_build.get_reload_tx().clone();
691 match run_build_pipeline(&args, Some(&reload_tx)).await {
692 Ok(html) => {
693 let mut content: RwLockWriteGuard<String> =
694 state_for_build.get_html_content().write().await;
695 *content = html;
696 }
697 Err(error) => {
698 log::error!("Build pipeline error: {error}");
699 }
700 }
701 let mut building: RwLockWriteGuard<bool> =
702 state_for_build.get_is_building().write().await;
703 *building = false;
704 });
705 }
706 Ok(())
707}
708
709pub async fn build_wasm(args: &ModeArgs) -> Result<(), EuvError> {
724 let build_mode: BuildMode = resolve_build_mode(args);
725 let build_mode_flag: &str = build_mode_to_flag(build_mode);
726 let filtered_args: Vec<String> = filter_euv_args(args.get_wasm_pack_args());
727 let has_existing_build_mode: bool = has_build_mode_flag(&filtered_args);
728 let default_out_dir: String = format!("{}/{PKG_DIR_NAME}", args.get_www_dir());
729 let mut command: Command = Command::new(WASM_PACK_COMMAND);
730 command.arg(WASM_PACK_BUILD_SUBCOMMAND);
731 if !has_existing_build_mode {
732 command.arg(build_mode_flag);
733 }
734 command
735 .args(&filtered_args)
736 .env(RUST_MIN_STACK_ENV, RUST_MIN_STACK_VALUE);
737 let has_out_dir: bool = extract_out_dir(&filtered_args).is_some();
738 if !has_out_dir {
739 command.arg(OUT_DIR_ARG).arg(&default_out_dir);
740 }
741 let has_target: bool = filtered_args
742 .iter()
743 .any(|arg: &String| arg == TARGET_ARG || arg.starts_with(&format!("{TARGET_ARG}=")));
744 if !has_target {
745 command.arg(TARGET_ARG).arg(TARGET_WEB);
746 }
747 command.current_dir(args.get_crate_path());
748 command.stdout(Stdio::piped()).stderr(Stdio::piped());
749 let display_args: Vec<String> = (if has_existing_build_mode {
750 filtered_args.to_vec()
751 } else {
752 once(build_mode_flag.to_string())
753 .chain(filtered_args.iter().cloned())
754 .collect::<Vec<String>>()
755 })
756 .into_iter()
757 .chain(if has_out_dir {
758 Vec::new()
759 } else {
760 vec![OUT_DIR_ARG.to_string(), default_out_dir.clone()]
761 })
762 .chain(if has_target {
763 Vec::new()
764 } else {
765 vec![TARGET_ARG.to_string(), TARGET_WEB.to_string()]
766 })
767 .collect();
768 let out_dir_absolute: PathBuf = resolve_out_dir(args);
769 create_dir_all(&out_dir_absolute)
770 .await
771 .map_err(|error: io::Error| EuvError::IoPath {
772 message: String::from("Failed to create output directory"),
773 path: out_dir_absolute.clone(),
774 error,
775 })?;
776 log::info!(
777 "Running: {WASM_PACK_COMMAND} {WASM_PACK_BUILD_SUBCOMMAND} {} ...",
778 display_args.join(" ")
779 );
780 let output: Output = command
781 .output()
782 .await
783 .map_err(|error: io::Error| EuvError::Io {
784 message: String::from("Failed to execute wasm-pack"),
785 error,
786 })?;
787 let stdout: String = String::from_utf8_lossy(&output.stdout).to_string();
788 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
789 if args.get_no_gitignore() {
790 let gitignore_path: PathBuf = out_dir_absolute.join(GITIGNORE_FILE_NAME);
791 if gitignore_path.exists()
792 && let Err(error) = remove_file(&gitignore_path).await
793 {
794 log::warn!("Failed to remove '{}': {error}", gitignore_path.display());
795 }
796 }
797 for line in stdout.lines().filter(|line: &&str| !line.is_empty()) {
798 log::info!("{line}");
799 }
800 if output.status.success() {
801 for line in stderr.lines().filter(|line: &&str| !line.is_empty()) {
802 log::info!("{line}");
803 }
804 } else {
805 for line in stderr.lines().filter(|line: &&str| !line.is_empty()) {
806 log::error!("{line}");
807 }
808 return Err(EuvError::Message(String::from("wasm-pack build failed")));
809 }
810 Ok(())
811}
812
813pub fn print_banner(action: Action) {
819 let version: &str = env!("CARGO_PKG_VERSION");
820 if version.is_empty() {
821 log::warn!("Failed to parse version from root Cargo.toml");
822 } else {
823 log::info!("euv v{version}");
824 }
825 let action_name: &str = match action {
826 Action::Run => ACTION_RUN,
827 Action::Build => ACTION_BUILD,
828 };
829 log::info!("Mode: {action_name}");
830 log::info!(
831 "Use .gitignore to filter file change events; pass --no-gitignore to remove .gitignore from output"
832 );
833}
834
835pub(crate) fn print_server_urls(config: &ServerUrlConfig) {
847 let port: u16 = config.get_port();
848 let route_prefix: &str = config.get_route_prefix();
849 let index_html_file_name: &str = config.get_index_html_file_name();
850 let mut addresses: Vec<IpAddr> = Vec::new();
851 match if_addrs::get_if_addrs() {
852 Ok(interfaces) => {
853 for interface in interfaces {
854 let ip: IpAddr = interface.addr.ip();
855 if !addresses.contains(&ip) {
856 addresses.push(ip);
857 }
858 }
859 }
860 Err(error) => {
861 log::warn!("Failed to enumerate network interfaces: {error}");
862 }
863 }
864 if addresses.is_empty() {
865 addresses.push(IpAddr::V4(Ipv4Addr::LOCALHOST));
866 }
867 for ip in addresses {
868 let host: String = match ip {
869 IpAddr::V6(_) => format!("[{ip}]"),
870 IpAddr::V4(_) => format!("{ip}"),
871 };
872 let url: String =
873 format!("{HTTP_SCHEME}://{host}:{port}/{route_prefix}/{index_html_file_name}");
874 log::info!("Server: {url}");
875 match QrCode::new(url.as_str()) {
876 Ok(code) => {
877 let string: String = code.render::<Dense1x2>().quiet_zone(false).build();
878 for line in string.lines() {
879 log::info!("{line}");
880 }
881 }
882 Err(error) => {
883 log::warn!("Failed to generate QR code: {error}");
884 }
885 }
886 }
887}
888
889pub(crate) async fn build_inline_bridge(
909 pkg_dir: &Path,
910 js_name: &str,
911 wasm_url: &str,
912) -> Result<String, EuvError> {
913 let js_path: PathBuf = pkg_dir.join(js_name);
914 let bridge_source: String =
915 read_to_string(&js_path)
916 .await
917 .map_err(|error: io::Error| EuvError::IoPath {
918 message: String::from("Failed to read wasm-bindgen JS bridge"),
919 path: js_path.clone(),
920 error,
921 })?;
922 let snippet_bodies: String = collect_snippet_bodies(&bridge_source, pkg_dir).await?;
923 let stripped: String = strip_module_imports(&bridge_source);
924 let js_only: String = strip_trailing_module_exports(&stripped);
925 let wasm_url_json: String = serde_json_wasm_url(wasm_url);
926 Ok(format!(
927 "(function() {{\n\
928 {snippet_bodies}\n\
929 {js_only}\n\
930 var __euv_wasm_url = {wasm_url_json};\n\
931 if (typeof __wbg_init === 'function') {{\n\
932 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\
933 if (typeof main === 'function') main();\n\
934 }}).catch(function(e) {{ console.error('[euv] inline bridge init failed:', e); }});\n\
935 }} else if (typeof initSync === 'function') {{\n\
936 initSync();\n\
937 if (typeof main === 'function') main();\n\
938 }} else {{\n\
939 console.error('[euv] inline bridge: no __wbg_init or initSync exported');\n\
940 }}\n\
941 }})();\n"
942 ))
943}
944
945pub(crate) fn build_module_fallback_bridge(import_path: &str) -> String {
957 format!(
958 " import init, {{ main }} from '{import_path}';\n \
959 await init();\n \
960 main();\n"
961 )
962}
963
964pub(crate) fn inline_bridge_disabled() -> bool {
970 matches!(var(EUV_NO_INLINE_BRIDGE_ENV), Ok(value) if !value.is_empty())
971}
972
973async fn collect_snippet_bodies(bridge_source: &str, pkg_dir: &Path) -> Result<String, EuvError> {
988 let mut bodies: String = String::new();
989 let mut namespace_imports: Vec<NamespaceImport> = Vec::new();
990 for line in bridge_source.lines() {
991 let trimmed: &str = line.trim();
992 if !trimmed.starts_with("import ") {
993 continue;
994 }
995 let rest: &str = match trimmed.strip_prefix("import ") {
996 Some(value) => value,
997 None => continue,
998 };
999 let spec: &str = match extract_import_spec(rest) {
1000 Some(value) => value,
1001 None => continue,
1002 };
1003 if is_namespace_import(rest)
1004 && let Some(alias) = extract_namespace_alias(rest)
1005 {
1006 namespace_imports.push(NamespaceImport {
1007 alias: alias.to_string(),
1008 spec: spec.to_string(),
1009 });
1010 }
1011 let body: Option<String> = read_snippet_module(pkg_dir, spec).await?;
1012 if let Some(body) = body {
1013 if !bodies.is_empty() {
1014 bodies.push('\n');
1015 }
1016 bodies.push_str(&body);
1017 }
1018 }
1019 for ns in &namespace_imports {
1020 let snippet_path: PathBuf = resolve_snippet_path(pkg_dir, &ns.spec)?;
1021 let exports: Vec<String> = if snippet_path.exists() {
1022 let raw: String = read_to_string(&snippet_path)
1023 .await
1024 .map_err(|error: io::Error| EuvError::IoPath {
1025 message: String::from(
1026 "Failed to read wasm-pack snippet module for namespace export scan",
1027 ),
1028 path: snippet_path.clone(),
1029 error,
1030 })?;
1031 extract_exported_function_names(&raw)
1032 } else {
1033 Vec::new()
1034 };
1035 if !bodies.is_empty() {
1036 bodies.push('\n');
1037 }
1038 bodies.push_str(&format!("var {} = {{", ns.alias));
1039 for (i, name) in exports.iter().enumerate() {
1040 if i > 0 {
1041 bodies.push(',');
1042 }
1043 bodies.push_str(name);
1044 bodies.push(':');
1045 bodies.push_str(name);
1046 }
1047 bodies.push_str("};\n");
1048 }
1049 Ok(bodies)
1050}
1051
1052pub fn extract_exported_function_names(source: &str) -> Vec<String> {
1065 let mut names: Vec<String> = Vec::new();
1066 for line in source.lines() {
1067 let trimmed: &str = line.trim();
1068 if let Some(rest) = trimmed.strip_prefix("export function ") {
1069 let name_end: usize =
1070 match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
1071 Some(value) => value,
1072 None => rest.len(),
1073 };
1074 let name: &str = &rest[..name_end];
1075 if !name.is_empty() {
1076 names.push(name.to_string());
1077 }
1078 }
1079 }
1080 names
1081}
1082
1083pub fn is_namespace_import(rest: &str) -> bool {
1093 rest.trim_start().starts_with('*')
1094}
1095
1096pub fn extract_namespace_alias(rest: &str) -> Option<&str> {
1107 let rest: &str = rest.trim_end_matches(';').trim();
1108 if !rest.starts_with('*') {
1109 return None;
1110 }
1111 let rest: &str = rest.strip_prefix('*')?.trim();
1112 let rest: &str = rest.strip_prefix("as")?.trim();
1113 let end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_' && c != '$') {
1114 Some(value) => value,
1115 None => rest.len(),
1116 };
1117 let alias: &str = &rest[..end];
1118 if alias.is_empty() { None } else { Some(alias) }
1119}
1120
1121pub fn extract_import_spec(rest: &str) -> Option<&str> {
1136 let rest: &str = rest.trim_end_matches(';').trim();
1137 let from_idx: usize = rest.find(" from ")?;
1138 let spec_part: &str = rest[from_idx + " from ".len()..].trim();
1139 let spec: &str = spec_part
1140 .trim_start_matches('\'')
1141 .trim_start_matches('"')
1142 .trim_end_matches('\'')
1143 .trim_end_matches('"');
1144 if spec.starts_with("./") || spec.starts_with("../") {
1145 Some(spec)
1146 } else {
1147 None
1148 }
1149}
1150
1151async fn read_snippet_module(pkg_dir: &Path, spec: &str) -> Result<Option<String>, EuvError> {
1165 let snippet_path: PathBuf = resolve_snippet_path(pkg_dir, spec)?;
1166 if !snippet_path.exists() {
1167 return Ok(None);
1168 }
1169 let bytes: Vec<u8> =
1170 read(&snippet_path)
1171 .await
1172 .map_err(|error: io::Error| EuvError::IoPath {
1173 message: String::from("Failed to read wasm-pack snippet module"),
1174 path: snippet_path.clone(),
1175 error,
1176 })?;
1177 let raw: String = String::from_utf8(bytes).map_err(|error: FromUtf8Error| EuvError::Utf8 {
1178 message: String::from("Snippet module is not valid UTF-8"),
1179 error,
1180 })?;
1181 Ok(Some(strip_snippet_export(&raw)))
1182}
1183
1184fn resolve_snippet_path(pkg_dir: &Path, spec: &str) -> Result<PathBuf, EuvError> {
1196 let base: &str = spec.trim_start_matches('.').trim_start_matches('/');
1197 let mut path: PathBuf = pkg_dir.to_path_buf();
1198 for segment in base.split('/') {
1199 if segment.is_empty() || segment == "." {
1200 continue;
1201 }
1202 if segment == ".." {
1203 return Err(EuvError::Message(String::from(
1204 "Snippet import spec escapes pkg directory",
1205 )));
1206 }
1207 path.push(segment);
1208 }
1209 Ok(path)
1210}
1211
1212fn strip_snippet_export(raw: &str) -> String {
1223 let mut out: String = String::with_capacity(raw.len());
1224 for line in raw.lines() {
1225 let trimmed: &str = line.trim();
1226 if trimmed.starts_with("export ") {
1227 if let Some(rest) = trimmed.strip_prefix("export ") {
1228 out.push_str(rest);
1229 out.push('\n');
1230 }
1231 } else {
1232 out.push_str(line);
1233 out.push('\n');
1234 }
1235 }
1236 out
1237}
1238
1239fn strip_module_imports(source: &str) -> String {
1250 let mut out: String = String::with_capacity(source.len());
1251 for line in source.lines() {
1252 let trimmed: &str = line.trim();
1253 if trimmed.starts_with("import ") {
1254 continue;
1255 }
1256 if trimmed.contains("import.meta.url") {
1257 continue;
1258 }
1259 out.push_str(line);
1260 out.push('\n');
1261 }
1262 out
1263}
1264
1265fn strip_trailing_module_exports(source: &str) -> String {
1277 let mut out: String = String::with_capacity(source.len());
1278 for line in source.lines() {
1279 let trimmed: &str = line.trim();
1280 if trimmed.starts_with("export ") && trimmed.contains('{') && trimmed.contains('}') {
1281 let inner: &str = &trimmed["export ".len()..];
1282 let stripped: &str = match inner.strip_prefix('{') {
1283 Some(value) => value,
1284 None => {
1285 out.push_str(line);
1286 out.push('\n');
1287 continue;
1288 }
1289 };
1290 let close: usize = match stripped.rfind('}') {
1291 Some(value) => value,
1292 None => {
1293 out.push_str(line);
1294 out.push('\n');
1295 continue;
1296 }
1297 };
1298 let names_block: &str = &stripped[..close];
1299 let inlined: String = inline_export_aliases(names_block.trim());
1300 if !inlined.is_empty() {
1301 out.push_str(&inlined);
1302 out.push('\n');
1303 }
1304 continue;
1305 }
1306 if trimmed.starts_with("export function ") {
1307 let rest: &str = match trimmed.strip_prefix("export function ") {
1308 Some(value) => value,
1309 None => {
1310 out.push_str(line);
1311 out.push('\n');
1312 continue;
1313 }
1314 };
1315 let name_end: usize = match rest.find(|c: char| !c.is_alphanumeric() && c != '_') {
1316 Some(value) => value,
1317 None => rest.len(),
1318 };
1319 let name: &str = &rest[..name_end];
1320 if !name.is_empty() {
1321 out.push_str("function ");
1322 out.push_str(name);
1323 out.push_str(&rest[name_end..]);
1324 out.push('\n');
1325 continue;
1326 }
1327 }
1328 out.push_str(line);
1329 out.push('\n');
1330 }
1331 out
1332}
1333
1334fn inline_export_aliases(block: &str) -> String {
1348 let mut out: String = String::new();
1349 for entry in block.split(',') {
1350 let entry: &str = entry.trim();
1351 if entry.is_empty() {
1352 continue;
1353 }
1354 if let Some((original, exported)) = entry.split_once(" as ") {
1355 let original: &str = original.trim();
1356 let exported: &str = exported.trim();
1357 if original == exported || exported == "default" || original == "wasm" {
1358 continue;
1359 }
1360 out.push_str(exported);
1361 out.push_str(" = ");
1362 out.push_str(original);
1363 out.push_str(";\n");
1364 }
1365 }
1366 out
1367}
1368
1369fn serde_json_wasm_url(url: &str) -> String {
1384 let mut out: String = String::with_capacity(url.len() + 2);
1385 out.push('"');
1386 for ch in url.chars() {
1387 match ch {
1388 '"' => out.push_str("\\\""),
1389 '\\' => out.push_str("\\\\"),
1390 '\n' => out.push_str("\\n"),
1391 '\r' => out.push_str("\\r"),
1392 '\t' => out.push_str("\\t"),
1393 '\u{08}' => out.push_str("\\b"),
1394 '\u{0C}' => out.push_str("\\f"),
1395 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1396 c => out.push(c),
1397 }
1398 }
1399 out.push('"');
1400 out
1401}