1use super::*;
2
3pub fn minify_html_template(html: &str) -> String {
33 let bytes: &[u8] = html.as_bytes();
34 let len: usize = bytes.len();
35 let mut out: Vec<u8> = Vec::with_capacity(len);
36 let mut i: usize = 0;
37 let mut in_tag: bool = false;
38 let mut in_preserve: bool = false;
39 let mut pending_strip_tag_gap: bool = false;
40 let mut last_was_space: bool = false;
41 while i < len {
42 let b: u8 = bytes[i];
43 if in_preserve {
44 out.push(b);
45 if b == HTML_LT && i + 1 < len && bytes[i + 1] == HTML_SLASH {
46 let is_close_style: bool = i + HTML_STYLE_CLOSE_PREFIX_BYTES.len() <= len
47 && bytes[i..i + HTML_STYLE_CLOSE_PREFIX_BYTES.len()]
48 .eq_ignore_ascii_case(HTML_STYLE_CLOSE_PREFIX_BYTES);
49 let is_close_script: bool = i + HTML_SCRIPT_CLOSE_PREFIX_BYTES.len() <= len
50 && bytes[i..i + HTML_SCRIPT_CLOSE_PREFIX_BYTES.len()]
51 .eq_ignore_ascii_case(HTML_SCRIPT_CLOSE_PREFIX_BYTES);
52 if is_close_style || is_close_script {
53 let close_end: usize = i
54 + (if is_close_style {
55 HTML_STYLE_CLOSE_PREFIX_BYTES.len()
56 } else {
57 HTML_SCRIPT_CLOSE_PREFIX_BYTES.len()
58 });
59 if close_end >= len
60 || bytes[close_end].is_ascii_whitespace()
61 || bytes[close_end] == HTML_GT
62 {
63 let end_len: usize = if is_close_style {
64 HTML_STYLE_CLOSE_PREFIX_BYTES.len()
65 } else {
66 HTML_SCRIPT_CLOSE_PREFIX_BYTES.len()
67 };
68 for &b in &bytes[i + 1..i + end_len] {
69 out.push(b);
70 }
71 i = close_end;
72 in_preserve = false;
73 pending_strip_tag_gap = true;
74 last_was_space = false;
75 continue;
76 }
77 }
78 }
79 i += 1;
80 continue;
81 }
82 if pending_strip_tag_gap {
83 if b.is_ascii_whitespace() {
84 i += 1;
85 continue;
86 }
87 pending_strip_tag_gap = false;
88 }
89 if b == HTML_LT {
90 if i + HTML_COMMENT_OPEN_BYTES.len() <= len
91 && &bytes[i..i + HTML_COMMENT_OPEN_BYTES.len()] == HTML_COMMENT_OPEN_BYTES
92 {
93 let mut end: usize = i + HTML_COMMENT_OPEN_BYTES.len();
94 while end + HTML_COMMENT_CLOSE_BYTES.len() <= len
95 && &bytes[end..end + HTML_COMMENT_CLOSE_BYTES.len()] != HTML_COMMENT_CLOSE_BYTES
96 {
97 end += 1;
98 }
99 i = if end + HTML_COMMENT_CLOSE_BYTES.len() <= len {
100 end + HTML_COMMENT_CLOSE_BYTES.len()
101 } else {
102 len
103 };
104 pending_strip_tag_gap = true;
105 last_was_space = false;
106 continue;
107 }
108 if i + HTML_SCRIPT_OPEN_PREFIX_BYTES.len() <= len
109 && bytes[i..i + HTML_SCRIPT_OPEN_PREFIX_BYTES.len()]
110 .eq_ignore_ascii_case(HTML_SCRIPT_OPEN_PREFIX_BYTES)
111 {
112 let probe: usize = i + HTML_SCRIPT_OPEN_PREFIX_BYTES.len();
113 if probe >= len || bytes[probe].is_ascii_whitespace() || bytes[probe] == HTML_GT {
114 in_preserve = true;
115 }
116 } else if i + HTML_STYLE_OPEN_PREFIX_BYTES.len() <= len
117 && bytes[i..i + HTML_STYLE_OPEN_PREFIX_BYTES.len()]
118 .eq_ignore_ascii_case(HTML_STYLE_OPEN_PREFIX_BYTES)
119 {
120 let probe: usize = i + HTML_STYLE_OPEN_PREFIX_BYTES.len();
121 if probe >= len || bytes[probe].is_ascii_whitespace() || bytes[probe] == HTML_GT {
122 in_preserve = true;
123 }
124 }
125 in_tag = true;
126 last_was_space = false;
127 out.push(b);
128 i += 1;
129 continue;
130 }
131 if b == HTML_GT {
132 in_tag = false;
133 out.push(b);
134 i += 1;
135 pending_strip_tag_gap = true;
136 last_was_space = false;
137 continue;
138 }
139 if in_tag {
140 if b.is_ascii_whitespace() {
141 if !last_was_space {
142 out.push(HTML_SPACE);
143 last_was_space = true;
144 }
145 } else {
146 out.push(b);
147 last_was_space = false;
148 }
149 i += 1;
150 continue;
151 }
152 if b.is_ascii_whitespace() {
153 if !last_was_space {
154 out.push(HTML_SPACE);
155 last_was_space = true;
156 }
157 i += 1;
158 continue;
159 }
160 out.push(b);
161 last_was_space = false;
162 i += 1;
163 }
164 String::from_utf8(out).unwrap_or_else(|_| String::from(html))
165}
166
167pub fn has_build_mode_flag(wasm_pack_args: &[String]) -> bool {
180 wasm_pack_args
181 .iter()
182 .any(|arg: &String| arg == DEV_FLAG || arg == RELEASE_FLAG || arg == PROFILING_FLAG)
183}
184
185pub fn filter_euv_args(wasm_pack_args: &[String]) -> Vec<String> {
200 let raw_args: &[String] = if let Some(position) = wasm_pack_args
201 .iter()
202 .rposition(|arg: &String| arg == DOUBLE_DASH)
203 {
204 &wasm_pack_args[position + 1..]
205 } else {
206 wasm_pack_args
207 };
208 let mut filtered: Vec<String> = Vec::new();
209 let mut skip_next: bool = false;
210 for arg in raw_args {
211 if skip_next {
212 skip_next = false;
213 continue;
214 }
215 if EUV_ARGS.contains(&arg.as_str()) {
216 if arg.contains('=') {
217 continue;
218 }
219 skip_next = true;
220 continue;
221 }
222 filtered.push(arg.clone());
223 }
224 filtered
225}
226
227pub fn reconcile_args(args: &mut ModeArgs) {
242 let wasm_pack_args: Vec<String> = args.get_wasm_pack_args().clone();
243 let mut crate_path: Option<PathBuf> = None;
244 let mut port: Option<u16> = None;
245 let mut www_dir: Option<String> = None;
246 let mut index_html: Option<Option<PathBuf>> = None;
247 let mut no_gitignore: Option<bool> = None;
248 let mut dev: Option<bool> = None;
249 let mut release: Option<bool> = None;
250 let mut profiling: Option<bool> = None;
251 let mut iter: Iter<String> = wasm_pack_args.iter();
252 while let Some(arg) = iter.next() {
253 match arg.as_str() {
254 CRATE_PATH_ARG | CRATE_PATH_ARG_SHORT => {
255 if let Some(value) = iter.next() {
256 crate_path = Some(PathBuf::from(value));
257 }
258 }
259 PORT_ARG | PORT_ARG_SHORT => {
260 if let Some(value) = iter.next()
261 && let Ok(parsed_port) = value.parse::<u16>()
262 {
263 port = Some(parsed_port);
264 }
265 }
266 WWW_DIR_ARG => {
267 if let Some(value) = iter.next() {
268 www_dir = Some(value.clone());
269 }
270 }
271 INDEX_HTML_ARG => {
272 if let Some(value) = iter.next() {
273 index_html = Some(Some(PathBuf::from(value)));
274 }
275 }
276 NO_GITIGNORE_ARG => {
277 no_gitignore = Some(true);
278 }
279 DEV_FLAG => {
280 dev = Some(true);
281 }
282 RELEASE_FLAG => {
283 release = Some(true);
284 }
285 PROFILING_FLAG => {
286 profiling = Some(true);
287 }
288 other => {
289 if let Some(value) = other.strip_prefix(&format!("{CRATE_PATH_ARG}=")) {
290 crate_path = Some(PathBuf::from(value));
291 } else if let Some(value) = other.strip_prefix(&format!("{PORT_ARG}=")) {
292 if let Ok(parsed_port) = value.parse::<u16>() {
293 port = Some(parsed_port);
294 }
295 } else if let Some(value) = other.strip_prefix(&format!("{WWW_DIR_ARG}=")) {
296 www_dir = Some(value.to_string());
297 } else if let Some(value) = other.strip_prefix(&format!("{INDEX_HTML_ARG}=")) {
298 index_html = Some(Some(PathBuf::from(value)));
299 }
300 }
301 }
302 }
303 if let Some(value) = crate_path {
304 args.set_crate_path(value);
305 }
306 if let Some(value) = port {
307 args.set_port(value);
308 }
309 if let Some(value) = www_dir {
310 args.set_www_dir(value);
311 }
312 if let Some(value) = index_html {
313 args.set_index_html(value);
314 }
315 if let Some(value) = no_gitignore {
316 args.set_no_gitignore(value);
317 }
318 if let Some(value) = dev {
319 args.set_dev(value);
320 }
321 if let Some(value) = release {
322 args.set_release(value);
323 }
324 if let Some(value) = profiling {
325 args.set_profiling(value);
326 }
327}
328
329pub fn resolve_build_mode(args: &ModeArgs) -> BuildMode {
344 if args.get_profiling() {
345 BuildMode::Profiling
346 } else if args.get_release() {
347 BuildMode::Release
348 } else if args.get_dev() {
349 BuildMode::Dev
350 } else if args
351 .get_wasm_pack_args()
352 .iter()
353 .any(|arg: &String| arg == PROFILING_FLAG)
354 {
355 BuildMode::Profiling
356 } else if args
357 .get_wasm_pack_args()
358 .iter()
359 .any(|arg: &String| arg == RELEASE_FLAG)
360 {
361 BuildMode::Release
362 } else {
363 BuildMode::Dev
364 }
365}
366
367pub fn build_mode_to_flag(build_mode: BuildMode) -> &'static str {
377 match build_mode {
378 BuildMode::Dev => DEV_FLAG,
379 BuildMode::Release => RELEASE_FLAG,
380 BuildMode::Profiling => PROFILING_FLAG,
381 }
382}
383
384async fn build_gitignore(root: &PathBuf) -> Gitignore {
394 let gitignore_path: PathBuf = root.join(GITIGNORE_FILE_NAME);
395 let mut builder: GitignoreBuilder = GitignoreBuilder::new(root);
396 let gitignore_exists: bool = metadata(&gitignore_path).await.is_ok();
397 if gitignore_exists && let Some(error) = builder.add(&gitignore_path) {
398 log::warn!("Failed to load .gitignore: {error}");
399 }
400 match builder.build() {
401 Ok(gitignore) => {
402 if gitignore_exists {
403 log::info!("Loaded .gitignore to filter file change events");
404 }
405 gitignore
406 }
407 Err(error) => {
408 log::warn!("Failed to build gitignore matcher: {error}");
409 GitignoreBuilder::new(root)
410 .build()
411 .unwrap_or_else(|_error: ignore::Error| Gitignore::empty())
412 }
413 }
414}
415
416fn extract_out_name(wasm_pack_args: &[String]) -> Option<String> {
428 let mut iter: Iter<'_, String> = wasm_pack_args.iter();
429 while let Some(arg) = iter.next() {
430 if arg == OUT_NAME_ARG {
431 return iter.next().cloned();
432 }
433 if let Some(value) = arg.strip_prefix(&format!("{OUT_NAME_ARG}=")) {
434 return Some(value.to_string());
435 }
436 }
437 None
438}
439
440fn extract_out_dir(wasm_pack_args: &[String]) -> Option<String> {
452 let mut iter: Iter<'_, String> = wasm_pack_args.iter();
453 while let Some(arg) = iter.next() {
454 if arg == OUT_DIR_ARG {
455 return iter.next().cloned();
456 }
457 if let Some(value) = arg.strip_prefix(&format!("{OUT_DIR_ARG}=")) {
458 return Some(value.to_string());
459 }
460 }
461 None
462}
463
464pub fn resolve_out_name(args: &ModeArgs) -> String {
479 let name: String = if let Some(out_name) = extract_out_name(args.get_wasm_pack_args()) {
480 out_name
481 } else {
482 let cargo_toml_path: PathBuf = args.get_crate_path().join(CARGO_TOML_FILE_NAME);
483 read_crate_name_from_toml(&cargo_toml_path).unwrap_or_else(|| {
484 args.get_crate_path()
485 .file_name()
486 .unwrap_or_default()
487 .to_string_lossy()
488 .to_string()
489 })
490 };
491 format!("{name}{JS_EXTENSION}")
492}
493
494fn read_crate_name_from_toml(path: &Path) -> Option<String> {
506 let content: String = std::fs::read_to_string(path).ok()?;
507 let mut in_package: bool = false;
508 for line in content.lines() {
509 let trimmed: &str = line.trim();
510 if trimmed.starts_with('[') {
511 in_package = trimmed == "[package]";
512 continue;
513 }
514 if in_package
515 && trimmed.starts_with("name")
516 && let Some(value) = trimmed.strip_prefix("name")
517 {
518 let value: &str = value.trim().strip_prefix('=')?.trim();
519 let value: &str = value.strip_prefix('"')?.strip_suffix('"')?;
520 return Some(value.to_string());
521 }
522 }
523 None
524}
525
526fn compute_relative_path(base: &Path, target: &Path) -> PathBuf {
541 let base_components: Vec<Component> = base.components().collect();
542 let target_components: Vec<Component> = target.components().collect();
543 let common_len: usize = base_components
544 .iter()
545 .zip(target_components.iter())
546 .take_while(|(base_component, target_component)| base_component == target_component)
547 .count();
548 let mut result: PathBuf = PathBuf::new();
549 for _ in &base_components[common_len..] {
550 result.push("..");
551 }
552 for component in &target_components[common_len..] {
553 if let Component::Normal(os_str) = component {
554 result.push(os_str);
555 }
556 }
557 result
558}
559
560pub async fn resolve_serving_root(args: &ModeArgs) -> PathBuf {
574 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
575 let out_dir_absolute: PathBuf = resolve_out_dir(args);
576 if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
577 resolve_www_dir(&www_absolute).await
578 } else {
579 out_dir_absolute
580 .parent()
581 .map(|p: &Path| p.to_path_buf())
582 .unwrap_or_else(|| www_absolute)
583 }
584}
585
586pub fn resolve_serving_route_prefix(args: &ModeArgs) -> String {
599 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
600 let out_dir_absolute: PathBuf = resolve_out_dir(args);
601 let serving_root: PathBuf = if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
602 www_absolute
603 } else {
604 out_dir_absolute
605 .parent()
606 .map(|p: &Path| p.to_path_buf())
607 .unwrap_or_else(|| www_absolute)
608 };
609 serving_root
610 .strip_prefix(args.get_crate_path())
611 .map(|rel: &Path| {
612 rel.to_string_lossy()
613 .replace(CHAR_SLASH_BACK, STR_SLASH_FORWARD)
614 })
615 .unwrap_or_else(|_| {
616 args.get_www_dir()
617 .replace(CHAR_SLASH_BACK, STR_SLASH_FORWARD)
618 })
619}
620
621pub fn resolve_import_path(args: &ModeArgs) -> String {
635 let out_name: String = resolve_out_name(args);
636 let www_absolute: PathBuf = args.get_crate_path().join(args.get_www_dir());
637 let out_dir_absolute: PathBuf = resolve_out_dir(args);
638 let serving_root: PathBuf = if out_dir_absolute.strip_prefix(&www_absolute).is_ok() {
639 www_absolute
640 } else {
641 out_dir_absolute
642 .parent()
643 .map(|p: &Path| p.to_path_buf())
644 .unwrap_or_else(|| www_absolute)
645 };
646 let relative: PathBuf = compute_relative_path(&serving_root, &out_dir_absolute);
647 let mut components: Vec<String> = relative
648 .components()
649 .filter_map(|component: Component| match component {
650 Component::Normal(os_str) => os_str.to_str().map(|text: &str| text.to_string()),
651 Component::ParentDir => Some(PARENT_DIR.to_string()),
652 _ => None,
653 })
654 .collect();
655 components.push(out_name);
656 format!("{RELATIVE_PATH_PREFIX}{}", components.join(PATH_SEPARATOR))
657}
658
659pub fn resolve_out_dir(args: &ModeArgs) -> PathBuf {
674 let out_dir_path: PathBuf = PathBuf::from(
675 extract_out_dir(args.get_wasm_pack_args())
676 .unwrap_or_else(|| format!("{}/{PKG_DIR_NAME}", args.get_www_dir())),
677 );
678 if out_dir_path.is_absolute() {
679 out_dir_path
680 } else {
681 args.get_crate_path().join(&out_dir_path)
682 }
683}
684
685pub async fn run_build_only_pipeline(args: &ModeArgs) -> Result<(), EuvError> {
699 let src_path: PathBuf = args.get_crate_path().join(SRC_DIR_NAME);
700 if let Err(error) = format_dir(&src_path, FmtMode::Write).await {
701 log::warn!("euv fmt error: {error}");
702 }
703 let out_dir: PathBuf = resolve_out_dir(args);
704 clean_out_dir(&out_dir).await;
705 build_wasm(args).await?;
706 log::info!("WASM build completed successfully");
707 let out_dir_path: PathBuf = resolve_out_dir(args);
708 if let Err(error) = minify_inline_js_snippets(&out_dir_path).await {
709 log::warn!("inline-js minify step error: {error}");
710 }
711 let html_config: HtmlConfig = HtmlConfig::new(
712 resolve_serving_root(args).await,
713 resolve_import_path(args),
714 resolve_build_mode(args) == BuildMode::Release,
715 args.try_get_index_html().clone(),
716 );
717 generate_html(&html_config).await?;
718 Ok(())
719}
720
721pub async fn minify_inline_js_snippets(out_dir: &Path) -> Result<(), EuvError> {
749 let snippets_root: PathBuf = out_dir.join(SNIPPETS_DIR_NAME);
750 if !snippets_root.is_dir() {
751 return Ok(());
752 }
753 let mut crate_dirs: ReadDir = match read_dir(&snippets_root).await {
754 Ok(dir) => dir,
755 Err(_) => return Ok(()),
756 };
757 let session: Session = Session::new();
758 let mut total_in: u64 = 0;
759 let mut total_out: u64 = 0;
760 let mut touched: u32 = 0;
761 while let Ok(Some(entry)) = crate_dirs.next_entry().await {
762 let crate_dir: PathBuf = entry.path();
763 if !crate_dir.is_dir() {
764 continue;
765 }
766 let mut files: ReadDir = match read_dir(&crate_dir).await {
767 Ok(dir) => dir,
768 Err(error) => {
769 log::warn!(
770 "Failed to read snippet dir '{}': {error}",
771 crate_dir.display()
772 );
773 continue;
774 }
775 };
776 while let Ok(Some(file_entry)) = files.next_entry().await {
777 let path: PathBuf = file_entry.path();
778 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
779 continue;
780 };
781 if !name.starts_with(SNIPPET_FILE_PREFIX) || !name.ends_with(JS_EXTENSION) {
782 continue;
783 }
784 let input: Vec<u8> = match tokio::fs::read(&path).await {
785 Ok(bytes) => bytes,
786 Err(error) => {
787 log::warn!("Failed to read snippet '{}': {error}", path.display());
788 continue;
789 }
790 };
791 total_in = total_in.saturating_add(input.len() as u64);
792 let mut output: Vec<u8> = Vec::with_capacity(input.len());
793 if let Err(error) = minify(&session, TopLevelMode::Module, &input, &mut output) {
794 log::warn!(
795 "Failed to minify snippet '{}' (left as-is): {}",
796 path.display(),
797 error
798 );
799 continue;
800 }
801 if let Err(error) = tokio::fs::write(&path, &output).await {
802 log::warn!(
803 "Failed to write minified snippet '{}': {error}",
804 path.display()
805 );
806 continue;
807 }
808 total_out = total_out.saturating_add(output.len() as u64);
809 touched = touched.saturating_add(1);
810 }
811 }
812 if touched > 0 && total_in > 0 {
813 log::info!(
814 "Minified {touched} inline-js snippet(s): {total_in} -> {total_out} bytes ({:.1}%)",
815 (total_out as f64 / total_in as f64) * 100.0
816 );
817 }
818 Ok(())
819}
820
821pub async fn clean_out_dir(out_dir: &Path) {
831 let mut entries: ReadDir = match read_dir(out_dir).await {
832 Ok(dir) => dir,
833 Err(_) => return,
834 };
835 while let Ok(Some(entry)) = entries.next_entry().await {
836 let path: PathBuf = entry.path();
837 if path.is_dir() {
838 if let Err(error) = remove_dir_all(&path).await {
839 log::warn!("Failed to remove directory '{}': {error}", path.display());
840 }
841 } else if let Err(error) = remove_file(&path).await {
842 log::warn!("Failed to remove file '{}': {error}", path.display());
843 }
844 }
845}
846
847pub async fn run_build_pipeline(
861 args: &ModeArgs,
862 reload_tx: Option<&broadcast::Sender<ReloadEvent>>,
863) -> Result<String, EuvError> {
864 let src_path: PathBuf = args.get_crate_path().join(SRC_DIR_NAME);
865 if let Err(error) = format_dir(&src_path, FmtMode::Write).await {
866 log::warn!("euv fmt error: {error}");
867 }
868 match build_wasm(args).await {
869 Ok(()) => {
870 log::info!("WASM build completed successfully");
871 let out_dir_path: PathBuf = resolve_out_dir(args);
872 if let Err(error) = minify_inline_js_snippets(&out_dir_path).await {
873 log::warn!("inline-js minify step error: {error}");
874 }
875 if let Some(sender) = reload_tx {
876 let _: Result<usize, tokio::sync::broadcast::error::SendError<ReloadEvent>> =
877 sender.send(ReloadEvent::Reload);
878 }
879 }
880 Err(error) => {
881 log::error!("WASM build failed: {error}");
882 if let Some(sender) = reload_tx {
883 let _: Result<usize, tokio::sync::broadcast::error::SendError<ReloadEvent>> =
884 sender.send(ReloadEvent::Error(error.to_string()));
885 }
886 }
887 }
888 let html_config: HtmlConfig = HtmlConfig::new(
889 resolve_serving_root(args).await,
890 resolve_import_path(args),
891 resolve_build_mode(args) == BuildMode::Release,
892 args.try_get_index_html().clone(),
893 );
894 let html: String = generate_html(&html_config).await?;
895 spawn(async move {
896 if let Err(error) = run_hyperlane_fmt().await {
897 log::warn!("hyperlane-cli fmt error: {error}");
898 }
899 });
900 Ok(html)
901}
902
903pub(crate) async fn watch_and_build(state: Arc<AppState>) -> Result<(), EuvError> {
913 let crate_path: PathBuf = state.get_args().get_crate_path().clone();
914 let src_path: PathBuf = crate_path.join(SRC_DIR_NAME);
915 let gitignore: Gitignore = build_gitignore(&crate_path).await;
916 let (tx, mut rx): (Sender<Event>, Receiver<Event>) = channel(32);
917 let mut watcher: RecommendedWatcher = RecommendedWatcher::new(
918 move |result: Result<Event, notify::Error>| {
919 if let Ok(event) = result {
920 let _: Result<(), tokio::sync::mpsc::error::SendError<Event>> =
921 tx.blocking_send(event);
922 }
923 },
924 Config::default(),
925 )?;
926 watcher.watch(&src_path, RecursiveMode::Recursive)?;
927 log::info!("Watching {} for changes...", src_path.display());
928 let mut debounce: Interval = interval(Duration::from_millis(500));
929 debounce.tick().await;
930 while let Some(event) = rx.recv().await {
931 let filtered_paths: Vec<String> = event
932 .paths
933 .iter()
934 .filter(|path: &&PathBuf| !gitignore.matched(*path, path.is_dir()).is_ignore())
935 .map(|path: &PathBuf| path.display().to_string())
936 .collect();
937 if filtered_paths.is_empty() {
938 continue;
939 }
940 log::warn!("File change detected: {}", filtered_paths.join(", "));
941 debounce.reset();
942 sleep(Duration::from_millis(300)).await;
943 let mut building: RwLockWriteGuard<bool> = state.get_is_building().write().await;
944 if *building {
945 continue;
946 }
947 *building = true;
948 drop(building);
949 let state_for_build: Arc<AppState> = Arc::clone(&state);
950 spawn(async move {
951 let args: ModeArgs = state_for_build.get_args().clone();
952 let reload_tx: broadcast::Sender<ReloadEvent> = state_for_build.get_reload_tx().clone();
953 match run_build_pipeline(&args, Some(&reload_tx)).await {
954 Ok(html) => {
955 let mut content: RwLockWriteGuard<String> =
956 state_for_build.get_html_content().write().await;
957 *content = html;
958 }
959 Err(error) => {
960 log::error!("Build pipeline error: {error}");
961 }
962 }
963 let mut building: RwLockWriteGuard<bool> =
964 state_for_build.get_is_building().write().await;
965 *building = false;
966 });
967 }
968 Ok(())
969}
970
971pub async fn build_wasm(args: &ModeArgs) -> Result<(), EuvError> {
986 let build_mode: BuildMode = resolve_build_mode(args);
987 let build_mode_flag: &str = build_mode_to_flag(build_mode);
988 let filtered_args: Vec<String> = filter_euv_args(args.get_wasm_pack_args());
989 let has_existing_build_mode: bool = has_build_mode_flag(&filtered_args);
990 let default_out_dir: String = format!("{}/{PKG_DIR_NAME}", args.get_www_dir());
991 let mut command: Command = Command::new(WASM_PACK_COMMAND);
992 command.arg(WASM_PACK_BUILD_SUBCOMMAND);
993 if !has_existing_build_mode {
994 command.arg(build_mode_flag);
995 }
996 command
997 .args(&filtered_args)
998 .env(RUST_MIN_STACK_ENV, RUST_MIN_STACK_VALUE);
999 let has_out_dir: bool = extract_out_dir(&filtered_args).is_some();
1000 if !has_out_dir {
1001 command.arg(OUT_DIR_ARG).arg(&default_out_dir);
1002 }
1003 let has_target: bool = filtered_args
1004 .iter()
1005 .any(|arg: &String| arg == TARGET_ARG || arg.starts_with(&format!("{TARGET_ARG}=")));
1006 if !has_target {
1007 command.arg(TARGET_ARG).arg(TARGET_WEB);
1008 }
1009 command.current_dir(args.get_crate_path());
1010 command.stdout(Stdio::piped()).stderr(Stdio::piped());
1011 let display_args: Vec<String> = (if has_existing_build_mode {
1012 filtered_args.to_vec()
1013 } else {
1014 std::iter::once(build_mode_flag.to_string())
1015 .chain(filtered_args.iter().cloned())
1016 .collect::<Vec<String>>()
1017 })
1018 .into_iter()
1019 .chain(if has_out_dir {
1020 Vec::new()
1021 } else {
1022 vec![OUT_DIR_ARG.to_string(), default_out_dir.clone()]
1023 })
1024 .chain(if has_target {
1025 Vec::new()
1026 } else {
1027 vec![TARGET_ARG.to_string(), TARGET_WEB.to_string()]
1028 })
1029 .collect();
1030 let out_dir_absolute: PathBuf = resolve_out_dir(args);
1031 create_dir_all(&out_dir_absolute)
1032 .await
1033 .map_err(|error: IoError| EuvError::IoPath {
1034 message: String::from("Failed to create output directory"),
1035 path: out_dir_absolute.clone(),
1036 error,
1037 })?;
1038 log::info!(
1039 "Running: {WASM_PACK_COMMAND} {WASM_PACK_BUILD_SUBCOMMAND} {} ...",
1040 display_args.join(" ")
1041 );
1042 let output: Output = command
1043 .output()
1044 .await
1045 .map_err(|error: IoError| EuvError::Io {
1046 message: String::from("Failed to execute wasm-pack"),
1047 error,
1048 })?;
1049 let stdout: String = String::from_utf8_lossy(&output.stdout).to_string();
1050 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
1051 if args.get_no_gitignore() {
1052 let gitignore_path: PathBuf = out_dir_absolute.join(GITIGNORE_FILE_NAME);
1053 if gitignore_path.exists()
1054 && let Err(error) = remove_file(&gitignore_path).await
1055 {
1056 log::warn!("Failed to remove '{}': {error}", gitignore_path.display());
1057 }
1058 }
1059 for line in stdout.lines().filter(|line: &&str| !line.is_empty()) {
1060 log::info!("{line}");
1061 }
1062 if output.status.success() {
1063 for line in stderr.lines().filter(|line: &&str| !line.is_empty()) {
1064 log::info!("{line}");
1065 }
1066 } else {
1067 for line in stderr.lines().filter(|line: &&str| !line.is_empty()) {
1068 log::error!("{line}");
1069 }
1070 return Err(EuvError::Message(String::from("wasm-pack build failed")));
1071 }
1072 Ok(())
1073}
1074
1075pub fn print_banner(action: Action) {
1081 let version: &str = env!("CARGO_PKG_VERSION");
1082 if version.is_empty() {
1083 log::warn!("Failed to parse version from root Cargo.toml");
1084 } else {
1085 log::info!("euv v{version}");
1086 }
1087 let action_name: &str = match action {
1088 Action::Run => ACTION_RUN,
1089 Action::Build => ACTION_BUILD,
1090 };
1091 log::info!("Mode: {action_name}");
1092 log::info!(
1093 "Use .gitignore to filter file change events; pass --no-gitignore to remove .gitignore from output"
1094 );
1095}
1096
1097pub(crate) fn print_server_urls(config: &ServerUrlConfig) {
1109 let port: u16 = config.get_port();
1110 let route_prefix: &str = config.get_route_prefix();
1111 let index_html_file_name: &str = config.get_index_html_file_name();
1112 let mut addresses: Vec<IpAddr> = Vec::new();
1113 match if_addrs::get_if_addrs() {
1114 Ok(interfaces) => {
1115 for interface in interfaces {
1116 let ip: IpAddr = interface.addr.ip();
1117 if !addresses.contains(&ip) {
1118 addresses.push(ip);
1119 }
1120 }
1121 }
1122 Err(error) => {
1123 log::warn!("Failed to enumerate network interfaces: {error}");
1124 }
1125 }
1126 if addresses.is_empty() {
1127 addresses.push(IpAddr::V4(Ipv4Addr::LOCALHOST));
1128 }
1129 for ip in addresses {
1130 let host: String = match ip {
1131 IpAddr::V6(_) => format!("[{ip}]"),
1132 IpAddr::V4(_) => format!("{ip}"),
1133 };
1134 let url: String =
1135 format!("{HTTP_SCHEME}://{host}:{port}/{route_prefix}/{index_html_file_name}");
1136 log::info!("Server: {url}");
1137 match QrCode::new(url.as_str()) {
1138 Ok(code) => {
1139 let string: String = code.render::<Dense1x2>().quiet_zone(false).build();
1140 for line in string.lines() {
1141 log::info!("{line}");
1142 }
1143 }
1144 Err(error) => {
1145 log::warn!("Failed to generate QR code: {error}");
1146 }
1147 }
1148 }
1149}
1150
1151pub async fn run_hyperlane_fmt() -> Result<(), EuvError> {
1157 let args: hyperlane_cli::Args = hyperlane_cli::Args {
1158 command: hyperlane_cli::CommandType::Fmt,
1159 check: false,
1160 manifest_path: None,
1161 bump_type: None,
1162 max_retries: 0,
1163 project_name: None,
1164 template_type: None,
1165 model_sub_type: None,
1166 component_name: None,
1167 };
1168 hyperlane_cli::execute_fmt(&args)
1169 .await
1170 .map_err(|error: IoError| EuvError::Io {
1171 message: String::from("hyperlane-cli fmt error"),
1172 error,
1173 })
1174}