Skip to main content

zlayer_builder/
wasm_builder.rs

1//! WebAssembly component builder with multi-language support
2//!
3//! This module provides functionality to build WebAssembly components from
4//! various source languages, with support for both WASI Preview 1 and Preview 2
5//! (component model) targets.
6//!
7//! # Supported Languages
8//!
9//! - **Rust**: `cargo build --target wasm32-wasip1` or `cargo component build`
10//! - **Go**: `TinyGo` compiler with WASI target
11//! - **Python**: componentize-py for component model
12//! - **TypeScript/JavaScript**: jco/componentize-js
13//! - **`AssemblyScript`**: asc compiler
14//! - **C**: Clang with WASI SDK
15//! - **Zig**: Native WASI support
16//!
17//! # Usage
18//!
19//! ```no_run
20//! use std::path::Path;
21//! use zlayer_builder::wasm_builder::{build_wasm, WasmBuildConfig, WasiTarget};
22//!
23//! # async fn example() -> Result<(), zlayer_builder::wasm_builder::WasmBuildError> {
24//! let config = WasmBuildConfig {
25//!     language: None,  // Auto-detect
26//!     target: WasiTarget::Preview2,
27//!     optimize: true,
28//!     wit_path: None,
29//!     output_path: None,
30//! };
31//!
32//! let result = build_wasm(Path::new("./my-plugin"), config).await?;
33//! println!("Built {} WASM at {}", result.language, result.wasm_path.display());
34//! # Ok(())
35//! # }
36//! ```
37
38use std::fmt;
39use std::path::{Path, PathBuf};
40
41use thiserror::Error;
42use tokio::process::Command;
43use tracing::{debug, info, instrument, trace, warn};
44
45/// Error types for WASM build operations
46#[derive(Debug, Error)]
47pub enum WasmBuildError {
48    /// Failed to detect the source language
49    #[error("Could not detect source language in '{path}'")]
50    LanguageNotDetected {
51        /// The path that was inspected
52        path: PathBuf,
53    },
54
55    /// Build tool not found
56    #[error("Build tool '{tool}' not found: {message}")]
57    ToolNotFound {
58        /// Name of the missing tool
59        tool: String,
60        /// Additional context
61        message: String,
62    },
63
64    /// Build command failed
65    #[error("Build failed with exit code {exit_code}: {stderr}")]
66    BuildFailed {
67        /// Exit code from the build command
68        exit_code: i32,
69        /// Standard error output
70        stderr: String,
71        /// Standard output (may contain useful info)
72        stdout: String,
73    },
74
75    /// WASM output not found after build
76    #[error("WASM output not found at expected path: {path}")]
77    OutputNotFound {
78        /// The expected output path
79        path: PathBuf,
80    },
81
82    /// Configuration error
83    #[error("Configuration error: {message}")]
84    ConfigError {
85        /// Description of the configuration problem
86        message: String,
87    },
88
89    /// IO error during build operations
90    #[error("IO error: {0}")]
91    Io(#[from] std::io::Error),
92
93    /// Failed to read project configuration
94    #[error("Failed to read project configuration: {message}")]
95    ProjectConfigError {
96        /// Description of what went wrong
97        message: String,
98    },
99}
100
101/// Result type for WASM build operations
102pub type Result<T, E = WasmBuildError> = std::result::Result<T, E>;
103
104/// Supported source languages for WASM compilation
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum WasmLanguage {
107    /// Standard Rust with wasm32-wasip1 or wasm32-wasip2 target
108    Rust,
109    /// Rust with cargo-component for WASI Preview 2 components
110    RustComponent,
111    /// Go using `TinyGo` compiler
112    Go,
113    /// Python using componentize-py
114    Python,
115    /// TypeScript using jco/componentize-js
116    TypeScript,
117    /// `AssemblyScript` using asc compiler
118    AssemblyScript,
119    /// C using WASI SDK (clang)
120    C,
121    /// Zig with native WASI support
122    Zig,
123}
124
125impl WasmLanguage {
126    /// Get all supported languages
127    #[must_use]
128    pub fn all() -> &'static [WasmLanguage] {
129        &[
130            WasmLanguage::Rust,
131            WasmLanguage::RustComponent,
132            WasmLanguage::Go,
133            WasmLanguage::Python,
134            WasmLanguage::TypeScript,
135            WasmLanguage::AssemblyScript,
136            WasmLanguage::C,
137            WasmLanguage::Zig,
138        ]
139    }
140
141    /// Get the display name for this language
142    #[must_use]
143    pub fn name(&self) -> &'static str {
144        match self {
145            WasmLanguage::Rust => "Rust",
146            WasmLanguage::RustComponent => "Rust (cargo-component)",
147            WasmLanguage::Go => "Go (TinyGo)",
148            WasmLanguage::Python => "Python",
149            WasmLanguage::TypeScript => "TypeScript",
150            WasmLanguage::AssemblyScript => "AssemblyScript",
151            WasmLanguage::C => "C",
152            WasmLanguage::Zig => "Zig",
153        }
154    }
155
156    /// Check if this language produces component model output by default
157    #[must_use]
158    pub fn is_component_native(&self) -> bool {
159        matches!(
160            self,
161            WasmLanguage::RustComponent | WasmLanguage::Python | WasmLanguage::TypeScript
162        )
163    }
164}
165
166impl fmt::Display for WasmLanguage {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "{}", self.name())
169    }
170}
171
172/// WASI target version
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
174pub enum WasiTarget {
175    /// WASI Preview 1 (wasm32-wasip1)
176    Preview1,
177    /// WASI Preview 2 (component model)
178    #[default]
179    Preview2,
180}
181
182impl WasiTarget {
183    /// Get the Rust target triple for this WASI version
184    #[must_use]
185    pub fn rust_target(&self) -> &'static str {
186        match self {
187            WasiTarget::Preview1 => "wasm32-wasip1",
188            WasiTarget::Preview2 => "wasm32-wasip2",
189        }
190    }
191
192    /// Get the display name
193    #[must_use]
194    pub fn name(&self) -> &'static str {
195        match self {
196            WasiTarget::Preview1 => "WASI Preview 1",
197            WasiTarget::Preview2 => "WASI Preview 2",
198        }
199    }
200}
201
202impl fmt::Display for WasiTarget {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "{}", self.name())
205    }
206}
207
208/// Configuration for building a WASM component
209#[derive(Debug, Clone, Default)]
210pub struct WasmBuildConfig {
211    /// Source language (None = auto-detect)
212    pub language: Option<WasmLanguage>,
213
214    /// Target WASI version
215    pub target: WasiTarget,
216
217    /// Whether to optimize the output (release mode)
218    pub optimize: bool,
219
220    /// Path to WIT files for component model
221    pub wit_path: Option<PathBuf>,
222
223    /// Override output path
224    pub output_path: Option<PathBuf>,
225}
226
227impl WasmBuildConfig {
228    /// Create a new default configuration
229    #[must_use]
230    pub fn new() -> Self {
231        Self::default()
232    }
233
234    /// Set the source language
235    #[must_use]
236    pub fn language(mut self, lang: WasmLanguage) -> Self {
237        self.language = Some(lang);
238        self
239    }
240
241    /// Set the WASI target
242    #[must_use]
243    pub fn target(mut self, target: WasiTarget) -> Self {
244        self.target = target;
245        self
246    }
247
248    /// Enable optimization
249    #[must_use]
250    pub fn optimize(mut self, optimize: bool) -> Self {
251        self.optimize = optimize;
252        self
253    }
254
255    /// Set the WIT path
256    #[must_use]
257    pub fn wit_path(mut self, path: impl Into<PathBuf>) -> Self {
258        self.wit_path = Some(path.into());
259        self
260    }
261
262    /// Set the output path
263    #[must_use]
264    pub fn output_path(mut self, path: impl Into<PathBuf>) -> Self {
265        self.output_path = Some(path.into());
266        self
267    }
268}
269
270/// Result of a successful WASM build
271#[derive(Debug, Clone)]
272pub struct WasmBuildResult {
273    /// Path to the built WASM file
274    pub wasm_path: PathBuf,
275
276    /// The language that was used
277    pub language: WasmLanguage,
278
279    /// The WASI target that was used
280    pub target: WasiTarget,
281
282    /// Size of the output file in bytes
283    pub size: u64,
284}
285
286/// Detect the source language from files in the given directory
287///
288/// This function examines project files to determine which language
289/// should be used for WASM compilation.
290///
291/// # Detection Order
292///
293/// 1. Cargo.toml with cargo-component -> `RustComponent`
294/// 2. Cargo.toml -> Rust
295/// 3. go.mod -> Go (`TinyGo`)
296/// 4. pyproject.toml or requirements.txt -> Python
297/// 5. package.json with assemblyscript -> `AssemblyScript`
298/// 6. package.json -> TypeScript
299/// 7. build.zig -> Zig
300/// 8. Makefile with *.c files -> C
301/// # Errors
302///
303/// Returns an error if no supported WASM language project is detected in the context directory.
304#[instrument(level = "debug", skip_all, fields(path = %context.as_ref().display()))]
305pub fn detect_language(context: impl AsRef<Path>) -> Result<WasmLanguage> {
306    let path = context.as_ref();
307    debug!("Detecting WASM source language");
308
309    // Check for Rust projects
310    let cargo_toml = path.join("Cargo.toml");
311    if cargo_toml.exists() {
312        trace!("Found Cargo.toml");
313
314        // Check if this is a cargo-component project
315        if is_cargo_component_project(&cargo_toml)? {
316            debug!("Detected Rust (cargo-component) project");
317            return Ok(WasmLanguage::RustComponent);
318        }
319
320        debug!("Detected Rust project");
321        return Ok(WasmLanguage::Rust);
322    }
323
324    // Check for Go projects
325    if path.join("go.mod").exists() {
326        debug!("Detected Go (TinyGo) project");
327        return Ok(WasmLanguage::Go);
328    }
329
330    // Check for Python projects
331    if path.join("pyproject.toml").exists()
332        || path.join("requirements.txt").exists()
333        || path.join("setup.py").exists()
334    {
335        debug!("Detected Python project");
336        return Ok(WasmLanguage::Python);
337    }
338
339    // Check for Node.js/TypeScript projects
340    let package_json = path.join("package.json");
341    if package_json.exists() {
342        trace!("Found package.json");
343
344        // Check for AssemblyScript
345        if is_assemblyscript_project(&package_json)? {
346            debug!("Detected AssemblyScript project");
347            return Ok(WasmLanguage::AssemblyScript);
348        }
349
350        debug!("Detected TypeScript project");
351        return Ok(WasmLanguage::TypeScript);
352    }
353
354    // Check for Zig projects
355    if path.join("build.zig").exists() {
356        debug!("Detected Zig project");
357        return Ok(WasmLanguage::Zig);
358    }
359
360    // Check for C projects (Makefile + *.c files)
361    if (path.join("Makefile").exists() || path.join("CMakeLists.txt").exists())
362        && has_c_source_files(path)
363    {
364        debug!("Detected C project");
365        return Ok(WasmLanguage::C);
366    }
367
368    // Also check for C without makefile (just source files)
369    if has_c_source_files(path) {
370        debug!("Detected C project (source files only)");
371        return Ok(WasmLanguage::C);
372    }
373
374    Err(WasmBuildError::LanguageNotDetected {
375        path: path.to_path_buf(),
376    })
377}
378
379/// Check if a Cargo.toml indicates a cargo-component project
380fn is_cargo_component_project(cargo_toml: &Path) -> Result<bool> {
381    let content =
382        std::fs::read_to_string(cargo_toml).map_err(|e| WasmBuildError::ProjectConfigError {
383            message: format!("Failed to read Cargo.toml: {e}"),
384        })?;
385
386    // Check for cargo-component specific configuration
387    // This includes [package.metadata.component] or [lib] with crate-type = ["cdylib"]
388    // and presence of wit files
389    if content.contains("[package.metadata.component]") {
390        return Ok(true);
391    }
392
393    // Check for component-related dependencies
394    if content.contains("wit-bindgen") || content.contains("cargo-component-bindings") {
395        return Ok(true);
396    }
397
398    // Check for cargo-component.toml in the same directory
399    let component_toml = cargo_toml.parent().map(|p| p.join("cargo-component.toml"));
400    if let Some(ref component_toml) = component_toml {
401        if component_toml.exists() {
402            return Ok(true);
403        }
404    }
405
406    Ok(false)
407}
408
409/// Check if a package.json indicates an `AssemblyScript` project
410fn is_assemblyscript_project(package_json: &Path) -> Result<bool> {
411    let content =
412        std::fs::read_to_string(package_json).map_err(|e| WasmBuildError::ProjectConfigError {
413            message: format!("Failed to read package.json: {e}"),
414        })?;
415
416    let json: serde_json::Value =
417        serde_json::from_str(&content).map_err(|e| WasmBuildError::ProjectConfigError {
418            message: format!("Invalid package.json: {e}"),
419        })?;
420
421    // Check dependencies and devDependencies for assemblyscript
422    let has_assemblyscript = |deps: Option<&serde_json::Value>| -> bool {
423        deps.and_then(|d| d.as_object())
424            .is_some_and(|d| d.contains_key("assemblyscript"))
425    };
426
427    if has_assemblyscript(json.get("dependencies"))
428        || has_assemblyscript(json.get("devDependencies"))
429    {
430        return Ok(true);
431    }
432
433    // Check for asc in scripts
434    if let Some(scripts) = json.get("scripts").and_then(|s| s.as_object()) {
435        for script in scripts.values() {
436            if let Some(cmd) = script.as_str() {
437                if cmd.contains("asc ") || cmd.starts_with("asc") {
438                    return Ok(true);
439                }
440            }
441        }
442    }
443
444    Ok(false)
445}
446
447/// Check if the directory contains C source files
448fn has_c_source_files(path: &Path) -> bool {
449    if let Ok(entries) = std::fs::read_dir(path) {
450        for entry in entries.flatten() {
451            let file_path = entry.path();
452            if let Some(ext) = file_path.extension() {
453                if ext == "c" || ext == "h" {
454                    return true;
455                }
456            }
457        }
458    }
459
460    // Also check src/ subdirectory
461    let src_dir = path.join("src");
462    if src_dir.is_dir() {
463        if let Ok(entries) = std::fs::read_dir(&src_dir) {
464            for entry in entries.flatten() {
465                let file_path = entry.path();
466                if let Some(ext) = file_path.extension() {
467                    if ext == "c" || ext == "h" {
468                        return true;
469                    }
470                }
471            }
472        }
473    }
474
475    false
476}
477
478/// Get the build command for a specific language and target
479#[must_use]
480#[allow(clippy::too_many_lines)]
481pub fn get_build_command(language: WasmLanguage, target: WasiTarget, release: bool) -> Vec<String> {
482    match language {
483        WasmLanguage::Rust => {
484            let mut cmd = vec![
485                "cargo".to_string(),
486                "build".to_string(),
487                "--target".to_string(),
488                target.rust_target().to_string(),
489            ];
490            if release {
491                cmd.push("--release".to_string());
492            }
493            cmd
494        }
495
496        WasmLanguage::RustComponent => {
497            let mut cmd = vec![
498                "cargo".to_string(),
499                "component".to_string(),
500                "build".to_string(),
501            ];
502            if release {
503                cmd.push("--release".to_string());
504            }
505            cmd
506        }
507
508        WasmLanguage::Go => {
509            // TinyGo command for WASI
510            let wasi_target = match target {
511                WasiTarget::Preview1 => "wasip1",
512                WasiTarget::Preview2 => "wasip2",
513            };
514            let mut cmd = vec![
515                "tinygo".to_string(),
516                "build".to_string(),
517                "-target".to_string(),
518                wasi_target.to_string(),
519                "-o".to_string(),
520                "main.wasm".to_string(),
521            ];
522            if release {
523                cmd.push("-opt".to_string());
524                cmd.push("2".to_string());
525            }
526            cmd.push(".".to_string());
527            cmd
528        }
529
530        WasmLanguage::Python => {
531            // componentize-py for WASI Preview 2
532            // Note: componentize-py doesn't have explicit optimization flags,
533            // the output is already optimized regardless of the release flag
534            let _ = release; // Silence unused warning
535            vec![
536                "componentize-py".to_string(),
537                "-d".to_string(),
538                "wit".to_string(),
539                "-w".to_string(),
540                "world".to_string(),
541                "componentize".to_string(),
542                "app".to_string(),
543                "-o".to_string(),
544                "app.wasm".to_string(),
545            ]
546        }
547
548        WasmLanguage::TypeScript => {
549            // jco componentize for TypeScript
550            vec![
551                "npx".to_string(),
552                "jco".to_string(),
553                "componentize".to_string(),
554                "src/index.js".to_string(),
555                "--wit".to_string(),
556                "wit".to_string(),
557                "-o".to_string(),
558                "dist/component.wasm".to_string(),
559            ]
560        }
561
562        WasmLanguage::AssemblyScript => {
563            let mut cmd = vec![
564                "npx".to_string(),
565                "asc".to_string(),
566                "assembly/index.ts".to_string(),
567                "--target".to_string(),
568                "release".to_string(),
569                "-o".to_string(),
570                "build/release.wasm".to_string(),
571            ];
572            if release {
573                cmd.push("--optimize".to_string());
574            }
575            cmd
576        }
577
578        WasmLanguage::C => {
579            // Using WASI SDK's clang
580            let mut cmd = vec![
581                "clang".to_string(),
582                "--target=wasm32-wasi".to_string(),
583                "-o".to_string(),
584                "main.wasm".to_string(),
585            ];
586            if release {
587                cmd.push("-O2".to_string());
588            }
589            cmd.push("src/main.c".to_string());
590            cmd
591        }
592
593        WasmLanguage::Zig => {
594            // Zig with WASI target
595            let mut cmd = vec![
596                "zig".to_string(),
597                "build".to_string(),
598                "-Dtarget=wasm32-wasi".to_string(),
599            ];
600            if release {
601                cmd.push("-Doptimize=ReleaseFast".to_string());
602            }
603            cmd
604        }
605    }
606}
607
608/// Build a WASM component from source
609///
610/// This is the main entry point for building WASM components.
611/// It will detect the source language if not specified, run the
612/// appropriate build command, and return information about the
613/// built artifact.
614///
615/// # Errors
616///
617/// Returns an error if language detection fails, the build command fails, or no output artifact is found.
618#[instrument(level = "info", skip_all, fields(
619    context = %context.as_ref().display(),
620    language = ?config.language,
621    target = ?config.target
622))]
623pub async fn build_wasm(
624    context: impl AsRef<Path>,
625    config: WasmBuildConfig,
626) -> Result<WasmBuildResult> {
627    let context = context.as_ref();
628    info!("Building WASM component");
629
630    // Detect language if not specified
631    let language = if let Some(lang) = config.language {
632        debug!("Using specified language: {}", lang);
633        lang
634    } else {
635        let detected = detect_language(context)?;
636        info!("Auto-detected language: {}", detected);
637        detected
638    };
639
640    // Verify build tool is available
641    verify_build_tool(language).await?;
642
643    // Get build command
644    let cmd = get_build_command(language, config.target, config.optimize);
645    debug!("Build command: {:?}", cmd);
646
647    // Execute build
648    let output = execute_build_command(context, &cmd).await?;
649
650    // Check for success
651    if !output.status.success() {
652        let exit_code = output.status.code().unwrap_or(-1);
653        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
654        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
655
656        warn!("Build failed with exit code {}", exit_code);
657        trace!("stdout: {}", stdout);
658        trace!("stderr: {}", stderr);
659
660        return Err(WasmBuildError::BuildFailed {
661            exit_code,
662            stderr,
663            stdout,
664        });
665    }
666
667    // Find output file
668    let wasm_path = find_wasm_output(context, language, config.target, config.optimize)?;
669
670    // Apply output path override if specified
671    let final_path = if let Some(ref output_path) = config.output_path {
672        std::fs::copy(&wasm_path, output_path)?;
673        output_path.clone()
674    } else {
675        wasm_path
676    };
677
678    // Get file size
679    let metadata = std::fs::metadata(&final_path)?;
680    let size = metadata.len();
681
682    info!("Successfully built {} WASM ({} bytes)", language, size);
683
684    Ok(WasmBuildResult {
685        wasm_path: final_path,
686        language,
687        target: config.target,
688        size,
689    })
690}
691
692/// Verify that the required build tool is available
693async fn verify_build_tool(language: WasmLanguage) -> Result<()> {
694    let (tool, check_cmd) = match language {
695        WasmLanguage::Rust | WasmLanguage::RustComponent => ("cargo", vec!["cargo", "--version"]),
696        WasmLanguage::Go => ("tinygo", vec!["tinygo", "version"]),
697        WasmLanguage::Python => ("componentize-py", vec!["componentize-py", "--version"]),
698        WasmLanguage::TypeScript | WasmLanguage::AssemblyScript => {
699            ("npx", vec!["npx", "--version"])
700        }
701        WasmLanguage::C => ("clang", vec!["clang", "--version"]),
702        WasmLanguage::Zig => ("zig", vec!["zig", "version"]),
703    };
704
705    debug!("Checking for tool: {}", tool);
706
707    let result = Command::new(check_cmd[0])
708        .args(&check_cmd[1..])
709        .output()
710        .await;
711
712    match result {
713        Ok(output) if output.status.success() => {
714            trace!("{} is available", tool);
715            Ok(())
716        }
717        Ok(output) => {
718            let stderr = String::from_utf8_lossy(&output.stderr);
719            Err(WasmBuildError::ToolNotFound {
720                tool: tool.to_string(),
721                message: format!("Command failed: {stderr}"),
722            })
723        }
724        Err(e) => Err(WasmBuildError::ToolNotFound {
725            tool: tool.to_string(),
726            message: format!("Not found in PATH: {e}"),
727        }),
728    }
729}
730
731/// Execute the build command
732async fn execute_build_command(context: &Path, cmd: &[String]) -> Result<std::process::Output> {
733    let mut command = Command::new(&cmd[0]);
734    command
735        .args(&cmd[1..])
736        .current_dir(context)
737        .stdout(std::process::Stdio::piped())
738        .stderr(std::process::Stdio::piped());
739
740    debug!("Executing: {} in {}", cmd.join(" "), context.display());
741
742    command.output().await.map_err(WasmBuildError::Io)
743}
744
745/// Find the WASM output file after a successful build
746fn find_wasm_output(
747    context: &Path,
748    language: WasmLanguage,
749    target: WasiTarget,
750    release: bool,
751) -> Result<PathBuf> {
752    // Language-specific output locations
753    let candidates: Vec<PathBuf> = match language {
754        WasmLanguage::Rust => {
755            let profile = if release { "release" } else { "debug" };
756            let target_name = target.rust_target();
757
758            // Try to find the package name from Cargo.toml
759            let package_name =
760                get_rust_package_name(context).unwrap_or_else(|_| "output".to_string());
761
762            vec![
763                context
764                    .join("target")
765                    .join(target_name)
766                    .join(profile)
767                    .join(format!("{package_name}.wasm")),
768                context
769                    .join("target")
770                    .join(target_name)
771                    .join(profile)
772                    .join(format!("{}.wasm", package_name.replace('-', "_"))),
773            ]
774        }
775
776        WasmLanguage::RustComponent => {
777            let profile = if release { "release" } else { "debug" };
778            let package_name =
779                get_rust_package_name(context).unwrap_or_else(|_| "output".to_string());
780
781            vec![
782                // cargo-component outputs to wasm32-wasip1 or wasm32-wasip2
783                context
784                    .join("target")
785                    .join("wasm32-wasip1")
786                    .join(profile)
787                    .join(format!("{package_name}.wasm")),
788                context
789                    .join("target")
790                    .join("wasm32-wasip2")
791                    .join(profile)
792                    .join(format!("{package_name}.wasm")),
793                context
794                    .join("target")
795                    .join("wasm32-wasi")
796                    .join(profile)
797                    .join(format!("{package_name}.wasm")),
798            ]
799        }
800
801        WasmLanguage::Go | WasmLanguage::C => {
802            vec![context.join("main.wasm")]
803        }
804
805        WasmLanguage::Python => {
806            vec![context.join("app.wasm")]
807        }
808
809        WasmLanguage::TypeScript => {
810            vec![
811                context.join("dist").join("component.wasm"),
812                context.join("component.wasm"),
813            ]
814        }
815
816        WasmLanguage::AssemblyScript => {
817            vec![
818                context.join("build").join("release.wasm"),
819                context.join("build").join("debug.wasm"),
820            ]
821        }
822
823        WasmLanguage::Zig => {
824            vec![
825                context.join("zig-out").join("bin").join("main.wasm"),
826                context.join("zig-out").join("lib").join("main.wasm"),
827            ]
828        }
829    };
830
831    // Find the first existing file
832    for candidate in &candidates {
833        if candidate.exists() {
834            debug!("Found WASM output at: {}", candidate.display());
835            return Ok(candidate.clone());
836        }
837    }
838
839    // If no specific file found, search for any .wasm file
840    if let Some(wasm_path) = find_any_wasm_file(context) {
841        debug!("Found WASM file via search: {}", wasm_path.display());
842        return Ok(wasm_path);
843    }
844
845    Err(WasmBuildError::OutputNotFound {
846        path: candidates
847            .first()
848            .cloned()
849            .unwrap_or_else(|| context.join("output.wasm")),
850    })
851}
852
853/// Get the package name from Cargo.toml
854#[allow(clippy::similar_names)]
855fn get_rust_package_name(context: &Path) -> Result<String> {
856    let cargo_toml = context.join("Cargo.toml");
857    let content =
858        std::fs::read_to_string(&cargo_toml).map_err(|e| WasmBuildError::ProjectConfigError {
859            message: format!("Failed to read Cargo.toml: {e}"),
860        })?;
861
862    // Simple TOML parsing for package name
863    for line in content.lines() {
864        let line = line.trim();
865        if line.starts_with("name") {
866            if let Some(name) = line
867                .split('=')
868                .nth(1)
869                .map(|s| s.trim().trim_matches('"').trim_matches('\''))
870            {
871                return Ok(name.to_string());
872            }
873        }
874    }
875
876    Err(WasmBuildError::ProjectConfigError {
877        message: "Could not find package name in Cargo.toml".to_string(),
878    })
879}
880
881/// Search for any .wasm file in the project
882fn find_any_wasm_file(context: &Path) -> Option<PathBuf> {
883    // Common output directories to search
884    let search_dirs = [
885        context.to_path_buf(),
886        context.join("target"),
887        context.join("build"),
888        context.join("dist"),
889        context.join("out"),
890        context.join("zig-out"),
891    ];
892
893    for dir in &search_dirs {
894        if let Some(path) = search_wasm_recursive(dir, 3) {
895            return Some(path);
896        }
897    }
898
899    None
900}
901
902/// Recursively search for .wasm files up to a certain depth
903fn search_wasm_recursive(dir: &Path, max_depth: usize) -> Option<PathBuf> {
904    if max_depth == 0 || !dir.is_dir() {
905        return None;
906    }
907
908    if let Ok(entries) = std::fs::read_dir(dir) {
909        for entry in entries.flatten() {
910            let path = entry.path();
911
912            if path.is_file() {
913                if let Some(ext) = path.extension() {
914                    if ext == "wasm" {
915                        return Some(path);
916                    }
917                }
918            } else if path.is_dir() {
919                if let Some(found) = search_wasm_recursive(&path, max_depth - 1) {
920                    return Some(found);
921                }
922            }
923        }
924    }
925
926    None
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use std::fs;
933    use tempfile::TempDir;
934
935    fn create_temp_dir() -> TempDir {
936        TempDir::new().expect("Failed to create temp directory")
937    }
938
939    // =========================================================================
940    // WasmLanguage tests
941    // =========================================================================
942
943    mod wasm_language_tests {
944        use super::*;
945
946        #[test]
947        fn test_display_all_variants() {
948            assert_eq!(WasmLanguage::Rust.to_string(), "Rust");
949            assert_eq!(
950                WasmLanguage::RustComponent.to_string(),
951                "Rust (cargo-component)"
952            );
953            assert_eq!(WasmLanguage::Go.to_string(), "Go (TinyGo)");
954            assert_eq!(WasmLanguage::Python.to_string(), "Python");
955            assert_eq!(WasmLanguage::TypeScript.to_string(), "TypeScript");
956            assert_eq!(WasmLanguage::AssemblyScript.to_string(), "AssemblyScript");
957            assert_eq!(WasmLanguage::C.to_string(), "C");
958            assert_eq!(WasmLanguage::Zig.to_string(), "Zig");
959        }
960
961        #[test]
962        fn test_debug_formatting() {
963            // Verify Debug trait is implemented and produces expected output
964            let debug_str = format!("{:?}", WasmLanguage::Rust);
965            assert_eq!(debug_str, "Rust");
966
967            let debug_str = format!("{:?}", WasmLanguage::RustComponent);
968            assert_eq!(debug_str, "RustComponent");
969
970            let debug_str = format!("{:?}", WasmLanguage::Go);
971            assert_eq!(debug_str, "Go");
972
973            let debug_str = format!("{:?}", WasmLanguage::Python);
974            assert_eq!(debug_str, "Python");
975
976            let debug_str = format!("{:?}", WasmLanguage::TypeScript);
977            assert_eq!(debug_str, "TypeScript");
978
979            let debug_str = format!("{:?}", WasmLanguage::AssemblyScript);
980            assert_eq!(debug_str, "AssemblyScript");
981
982            let debug_str = format!("{:?}", WasmLanguage::C);
983            assert_eq!(debug_str, "C");
984
985            let debug_str = format!("{:?}", WasmLanguage::Zig);
986            assert_eq!(debug_str, "Zig");
987        }
988
989        #[test]
990        fn test_clone() {
991            let lang = WasmLanguage::Rust;
992            let cloned = lang;
993            assert_eq!(lang, cloned);
994
995            let lang = WasmLanguage::Python;
996            let cloned = lang;
997            assert_eq!(lang, cloned);
998        }
999
1000        #[test]
1001        fn test_copy() {
1002            let lang = WasmLanguage::Go;
1003            let copied = lang; // Copy semantics
1004            assert_eq!(lang, copied);
1005            // Verify original is still usable (proving Copy, not Move)
1006            assert_eq!(lang, WasmLanguage::Go);
1007        }
1008
1009        #[test]
1010        fn test_partial_eq() {
1011            assert_eq!(WasmLanguage::Rust, WasmLanguage::Rust);
1012            assert_ne!(WasmLanguage::Rust, WasmLanguage::Go);
1013            assert_ne!(WasmLanguage::Rust, WasmLanguage::RustComponent);
1014            assert_eq!(WasmLanguage::TypeScript, WasmLanguage::TypeScript);
1015            assert_ne!(WasmLanguage::TypeScript, WasmLanguage::AssemblyScript);
1016        }
1017
1018        #[test]
1019        fn test_name_method() {
1020            assert_eq!(WasmLanguage::Rust.name(), "Rust");
1021            assert_eq!(WasmLanguage::RustComponent.name(), "Rust (cargo-component)");
1022            assert_eq!(WasmLanguage::Go.name(), "Go (TinyGo)");
1023            assert_eq!(WasmLanguage::Python.name(), "Python");
1024            assert_eq!(WasmLanguage::TypeScript.name(), "TypeScript");
1025            assert_eq!(WasmLanguage::AssemblyScript.name(), "AssemblyScript");
1026            assert_eq!(WasmLanguage::C.name(), "C");
1027            assert_eq!(WasmLanguage::Zig.name(), "Zig");
1028        }
1029
1030        #[test]
1031        fn test_all_returns_all_variants() {
1032            let all = WasmLanguage::all();
1033            assert_eq!(all.len(), 8);
1034            assert!(all.contains(&WasmLanguage::Rust));
1035            assert!(all.contains(&WasmLanguage::RustComponent));
1036            assert!(all.contains(&WasmLanguage::Go));
1037            assert!(all.contains(&WasmLanguage::Python));
1038            assert!(all.contains(&WasmLanguage::TypeScript));
1039            assert!(all.contains(&WasmLanguage::AssemblyScript));
1040            assert!(all.contains(&WasmLanguage::C));
1041            assert!(all.contains(&WasmLanguage::Zig));
1042        }
1043
1044        #[test]
1045        fn test_is_component_native() {
1046            // Component-native languages (produce component model output by default)
1047            assert!(WasmLanguage::RustComponent.is_component_native());
1048            assert!(WasmLanguage::Python.is_component_native());
1049            assert!(WasmLanguage::TypeScript.is_component_native());
1050
1051            // Non-component-native languages
1052            assert!(!WasmLanguage::Rust.is_component_native());
1053            assert!(!WasmLanguage::Go.is_component_native());
1054            assert!(!WasmLanguage::AssemblyScript.is_component_native());
1055            assert!(!WasmLanguage::C.is_component_native());
1056            assert!(!WasmLanguage::Zig.is_component_native());
1057        }
1058
1059        #[test]
1060        fn test_hash() {
1061            use std::collections::HashSet;
1062
1063            let mut set = HashSet::new();
1064            set.insert(WasmLanguage::Rust);
1065            set.insert(WasmLanguage::Go);
1066            set.insert(WasmLanguage::Rust); // Duplicate
1067
1068            assert_eq!(set.len(), 2);
1069            assert!(set.contains(&WasmLanguage::Rust));
1070            assert!(set.contains(&WasmLanguage::Go));
1071        }
1072    }
1073
1074    // =========================================================================
1075    // WasiTarget tests
1076    // =========================================================================
1077
1078    mod wasi_target_tests {
1079        use super::*;
1080
1081        #[test]
1082        fn test_default_returns_preview2() {
1083            let target = WasiTarget::default();
1084            assert_eq!(target, WasiTarget::Preview2);
1085        }
1086
1087        #[test]
1088        fn test_display_preview1() {
1089            assert_eq!(WasiTarget::Preview1.to_string(), "WASI Preview 1");
1090        }
1091
1092        #[test]
1093        fn test_display_preview2() {
1094            assert_eq!(WasiTarget::Preview2.to_string(), "WASI Preview 2");
1095        }
1096
1097        #[test]
1098        fn test_debug_formatting() {
1099            let debug_str = format!("{:?}", WasiTarget::Preview1);
1100            assert_eq!(debug_str, "Preview1");
1101
1102            let debug_str = format!("{:?}", WasiTarget::Preview2);
1103            assert_eq!(debug_str, "Preview2");
1104        }
1105
1106        #[test]
1107        fn test_clone() {
1108            let target = WasiTarget::Preview1;
1109            let cloned = target;
1110            assert_eq!(target, cloned);
1111
1112            let target = WasiTarget::Preview2;
1113            let cloned = target;
1114            assert_eq!(target, cloned);
1115        }
1116
1117        #[test]
1118        fn test_copy() {
1119            let target = WasiTarget::Preview1;
1120            let copied = target; // Copy semantics
1121            assert_eq!(target, copied);
1122            // Verify original is still usable (proving Copy, not Move)
1123            assert_eq!(target, WasiTarget::Preview1);
1124        }
1125
1126        #[test]
1127        fn test_partial_eq() {
1128            assert_eq!(WasiTarget::Preview1, WasiTarget::Preview1);
1129            assert_eq!(WasiTarget::Preview2, WasiTarget::Preview2);
1130            assert_ne!(WasiTarget::Preview1, WasiTarget::Preview2);
1131        }
1132
1133        #[test]
1134        fn test_rust_target_preview1() {
1135            assert_eq!(WasiTarget::Preview1.rust_target(), "wasm32-wasip1");
1136        }
1137
1138        #[test]
1139        fn test_rust_target_preview2() {
1140            assert_eq!(WasiTarget::Preview2.rust_target(), "wasm32-wasip2");
1141        }
1142
1143        #[test]
1144        fn test_name_method() {
1145            assert_eq!(WasiTarget::Preview1.name(), "WASI Preview 1");
1146            assert_eq!(WasiTarget::Preview2.name(), "WASI Preview 2");
1147        }
1148
1149        #[test]
1150        fn test_hash() {
1151            use std::collections::HashSet;
1152
1153            let mut set = HashSet::new();
1154            set.insert(WasiTarget::Preview1);
1155            set.insert(WasiTarget::Preview2);
1156            set.insert(WasiTarget::Preview1); // Duplicate
1157
1158            assert_eq!(set.len(), 2);
1159            assert!(set.contains(&WasiTarget::Preview1));
1160            assert!(set.contains(&WasiTarget::Preview2));
1161        }
1162    }
1163
1164    // =========================================================================
1165    // WasmBuildConfig tests
1166    // =========================================================================
1167
1168    mod wasm_build_config_tests {
1169        use super::*;
1170
1171        #[test]
1172        fn test_default_trait() {
1173            let config = WasmBuildConfig::default();
1174
1175            assert_eq!(config.language, None);
1176            assert_eq!(config.target, WasiTarget::Preview2); // Default WasiTarget
1177            assert!(!config.optimize);
1178            assert_eq!(config.wit_path, None);
1179            assert_eq!(config.output_path, None);
1180        }
1181
1182        #[test]
1183        fn test_new_equals_default() {
1184            let new_config = WasmBuildConfig::new();
1185            let default_config = WasmBuildConfig::default();
1186
1187            assert_eq!(new_config.language, default_config.language);
1188            assert_eq!(new_config.target, default_config.target);
1189            assert_eq!(new_config.optimize, default_config.optimize);
1190            assert_eq!(new_config.wit_path, default_config.wit_path);
1191            assert_eq!(new_config.output_path, default_config.output_path);
1192        }
1193
1194        #[test]
1195        fn test_with_language() {
1196            let config = WasmBuildConfig::new().language(WasmLanguage::Rust);
1197            assert_eq!(config.language, Some(WasmLanguage::Rust));
1198
1199            let config = WasmBuildConfig::new().language(WasmLanguage::Python);
1200            assert_eq!(config.language, Some(WasmLanguage::Python));
1201        }
1202
1203        #[test]
1204        fn test_with_target() {
1205            let config = WasmBuildConfig::new().target(WasiTarget::Preview1);
1206            assert_eq!(config.target, WasiTarget::Preview1);
1207
1208            let config = WasmBuildConfig::new().target(WasiTarget::Preview2);
1209            assert_eq!(config.target, WasiTarget::Preview2);
1210        }
1211
1212        #[test]
1213        fn test_with_optimize_true() {
1214            let config = WasmBuildConfig::new().optimize(true);
1215            assert!(config.optimize);
1216        }
1217
1218        #[test]
1219        fn test_with_optimize_false() {
1220            let config = WasmBuildConfig::new().optimize(false);
1221            assert!(!config.optimize);
1222        }
1223
1224        #[test]
1225        fn test_with_wit_path_string() {
1226            let config = WasmBuildConfig::new().wit_path("/path/to/wit");
1227            assert_eq!(config.wit_path, Some(PathBuf::from("/path/to/wit")));
1228        }
1229
1230        #[test]
1231        fn test_with_wit_path_pathbuf() {
1232            let path = PathBuf::from("/another/wit/path");
1233            let config = WasmBuildConfig::new().wit_path(path.clone());
1234            assert_eq!(config.wit_path, Some(path));
1235        }
1236
1237        #[test]
1238        fn test_with_output_path_string() {
1239            let config = WasmBuildConfig::new().output_path("/output/file.wasm");
1240            assert_eq!(config.output_path, Some(PathBuf::from("/output/file.wasm")));
1241        }
1242
1243        #[test]
1244        fn test_with_output_path_pathbuf() {
1245            let path = PathBuf::from("/custom/output.wasm");
1246            let config = WasmBuildConfig::new().output_path(path.clone());
1247            assert_eq!(config.output_path, Some(path));
1248        }
1249
1250        #[test]
1251        fn test_builder_pattern_chaining() {
1252            let config = WasmBuildConfig::new()
1253                .language(WasmLanguage::Go)
1254                .target(WasiTarget::Preview1)
1255                .optimize(true)
1256                .wit_path("/wit")
1257                .output_path("/out.wasm");
1258
1259            assert_eq!(config.language, Some(WasmLanguage::Go));
1260            assert_eq!(config.target, WasiTarget::Preview1);
1261            assert!(config.optimize);
1262            assert_eq!(config.wit_path, Some(PathBuf::from("/wit")));
1263            assert_eq!(config.output_path, Some(PathBuf::from("/out.wasm")));
1264        }
1265
1266        #[test]
1267        fn test_debug_formatting() {
1268            let config = WasmBuildConfig::new().language(WasmLanguage::Rust);
1269            let debug_str = format!("{:?}", config);
1270
1271            assert!(debug_str.contains("WasmBuildConfig"));
1272            assert!(debug_str.contains("Rust"));
1273        }
1274
1275        #[test]
1276        fn test_clone() {
1277            let config = WasmBuildConfig::new()
1278                .language(WasmLanguage::Python)
1279                .optimize(true);
1280
1281            let cloned = config.clone();
1282
1283            assert_eq!(cloned.language, Some(WasmLanguage::Python));
1284            assert!(cloned.optimize);
1285        }
1286    }
1287
1288    // =========================================================================
1289    // WasmBuildResult tests
1290    // =========================================================================
1291
1292    mod wasm_build_result_tests {
1293        use super::*;
1294
1295        #[test]
1296        fn test_struct_creation() {
1297            let result = WasmBuildResult {
1298                wasm_path: PathBuf::from("/path/to/output.wasm"),
1299                language: WasmLanguage::Rust,
1300                target: WasiTarget::Preview2,
1301                size: 1024,
1302            };
1303
1304            assert_eq!(result.wasm_path, PathBuf::from("/path/to/output.wasm"));
1305            assert_eq!(result.language, WasmLanguage::Rust);
1306            assert_eq!(result.target, WasiTarget::Preview2);
1307            assert_eq!(result.size, 1024);
1308        }
1309
1310        #[test]
1311        fn test_struct_creation_all_languages() {
1312            for lang in WasmLanguage::all() {
1313                let result = WasmBuildResult {
1314                    wasm_path: PathBuf::from("/test.wasm"),
1315                    language: *lang,
1316                    target: WasiTarget::Preview1,
1317                    size: 512,
1318                };
1319                assert_eq!(result.language, *lang);
1320            }
1321        }
1322
1323        #[test]
1324        fn test_debug_formatting() {
1325            let result = WasmBuildResult {
1326                wasm_path: PathBuf::from("/test.wasm"),
1327                language: WasmLanguage::Go,
1328                target: WasiTarget::Preview1,
1329                size: 2048,
1330            };
1331
1332            let debug_str = format!("{:?}", result);
1333            assert!(debug_str.contains("WasmBuildResult"));
1334            assert!(debug_str.contains("test.wasm"));
1335            assert!(debug_str.contains("Go"));
1336            assert!(debug_str.contains("2048"));
1337        }
1338
1339        #[test]
1340        fn test_clone() {
1341            let result = WasmBuildResult {
1342                wasm_path: PathBuf::from("/original.wasm"),
1343                language: WasmLanguage::Zig,
1344                target: WasiTarget::Preview2,
1345                size: 4096,
1346            };
1347
1348            let cloned = result.clone();
1349
1350            assert_eq!(cloned.wasm_path, result.wasm_path);
1351            assert_eq!(cloned.language, result.language);
1352            assert_eq!(cloned.target, result.target);
1353            assert_eq!(cloned.size, result.size);
1354        }
1355
1356        #[test]
1357        fn test_zero_size() {
1358            let result = WasmBuildResult {
1359                wasm_path: PathBuf::from("/empty.wasm"),
1360                language: WasmLanguage::C,
1361                target: WasiTarget::Preview1,
1362                size: 0,
1363            };
1364            assert_eq!(result.size, 0);
1365        }
1366
1367        #[test]
1368        fn test_large_size() {
1369            let result = WasmBuildResult {
1370                wasm_path: PathBuf::from("/large.wasm"),
1371                language: WasmLanguage::AssemblyScript,
1372                target: WasiTarget::Preview2,
1373                size: u64::MAX,
1374            };
1375            assert_eq!(result.size, u64::MAX);
1376        }
1377    }
1378
1379    // =========================================================================
1380    // WasmBuildError tests
1381    // =========================================================================
1382
1383    mod wasm_build_error_tests {
1384        use super::*;
1385
1386        #[test]
1387        fn test_display_language_not_detected() {
1388            let err = WasmBuildError::LanguageNotDetected {
1389                path: PathBuf::from("/test/path"),
1390            };
1391            let display = err.to_string();
1392            assert!(display.contains("Could not detect source language"));
1393            assert!(display.contains("/test/path"));
1394        }
1395
1396        #[test]
1397        fn test_display_tool_not_found() {
1398            let err = WasmBuildError::ToolNotFound {
1399                tool: "cargo".to_string(),
1400                message: "Not in PATH".to_string(),
1401            };
1402            let display = err.to_string();
1403            assert!(display.contains("Build tool 'cargo' not found"));
1404            assert!(display.contains("Not in PATH"));
1405        }
1406
1407        #[test]
1408        fn test_display_build_failed() {
1409            let err = WasmBuildError::BuildFailed {
1410                exit_code: 1,
1411                stderr: "compilation error".to_string(),
1412                stdout: "some output".to_string(),
1413            };
1414            let display = err.to_string();
1415            assert!(display.contains("Build failed with exit code 1"));
1416            assert!(display.contains("compilation error"));
1417        }
1418
1419        #[test]
1420        fn test_display_output_not_found() {
1421            let err = WasmBuildError::OutputNotFound {
1422                path: PathBuf::from("/expected/output.wasm"),
1423            };
1424            let display = err.to_string();
1425            assert!(display.contains("WASM output not found"));
1426            assert!(display.contains("/expected/output.wasm"));
1427        }
1428
1429        #[test]
1430        fn test_display_config_error() {
1431            let err = WasmBuildError::ConfigError {
1432                message: "Invalid configuration".to_string(),
1433            };
1434            let display = err.to_string();
1435            assert!(display.contains("Configuration error"));
1436            assert!(display.contains("Invalid configuration"));
1437        }
1438
1439        #[test]
1440        fn test_display_project_config_error() {
1441            let err = WasmBuildError::ProjectConfigError {
1442                message: "Failed to parse Cargo.toml".to_string(),
1443            };
1444            let display = err.to_string();
1445            assert!(display.contains("Failed to read project configuration"));
1446            assert!(display.contains("Failed to parse Cargo.toml"));
1447        }
1448
1449        #[test]
1450        fn test_display_io_error() {
1451            let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1452            let err = WasmBuildError::Io(io_err);
1453            let display = err.to_string();
1454            assert!(display.contains("IO error"));
1455            assert!(display.contains("file not found"));
1456        }
1457
1458        #[test]
1459        fn test_debug_formatting_all_variants() {
1460            let errors = vec![
1461                WasmBuildError::LanguageNotDetected {
1462                    path: PathBuf::from("/test"),
1463                },
1464                WasmBuildError::ToolNotFound {
1465                    tool: "test".to_string(),
1466                    message: "msg".to_string(),
1467                },
1468                WasmBuildError::BuildFailed {
1469                    exit_code: 0,
1470                    stderr: String::new(),
1471                    stdout: String::new(),
1472                },
1473                WasmBuildError::OutputNotFound {
1474                    path: PathBuf::from("/test"),
1475                },
1476                WasmBuildError::ConfigError {
1477                    message: "test".to_string(),
1478                },
1479                WasmBuildError::ProjectConfigError {
1480                    message: "test".to_string(),
1481                },
1482            ];
1483
1484            for err in errors {
1485                let debug_str = format!("{:?}", err);
1486                assert!(!debug_str.is_empty());
1487            }
1488        }
1489
1490        #[test]
1491        fn test_from_io_error() {
1492            let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
1493            let wasm_err: WasmBuildError = io_err.into();
1494
1495            match wasm_err {
1496                WasmBuildError::Io(e) => {
1497                    assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied);
1498                }
1499                _ => panic!("Expected Io variant"),
1500            }
1501        }
1502
1503        #[test]
1504        fn test_from_io_error_various_kinds() {
1505            let kinds = vec![
1506                std::io::ErrorKind::NotFound,
1507                std::io::ErrorKind::PermissionDenied,
1508                std::io::ErrorKind::AlreadyExists,
1509                std::io::ErrorKind::InvalidData,
1510            ];
1511
1512            for kind in kinds {
1513                let io_err = std::io::Error::new(kind, "test error");
1514                let wasm_err: WasmBuildError = io_err.into();
1515                assert!(matches!(wasm_err, WasmBuildError::Io(_)));
1516            }
1517        }
1518
1519        #[test]
1520        fn test_error_implements_std_error() {
1521            let err = WasmBuildError::ConfigError {
1522                message: "test".to_string(),
1523            };
1524            // Verify it implements std::error::Error by using the trait
1525            let _: &dyn std::error::Error = &err;
1526        }
1527    }
1528
1529    // =========================================================================
1530    // detect_language tests
1531    // =========================================================================
1532
1533    mod detect_language_tests {
1534        use super::*;
1535
1536        #[test]
1537        fn test_detect_cargo_toml_rust() {
1538            let dir = create_temp_dir();
1539            fs::write(
1540                dir.path().join("Cargo.toml"),
1541                r#"[package]
1542name = "test"
1543version = "0.1.0"
1544"#,
1545            )
1546            .unwrap();
1547
1548            let lang = detect_language(dir.path()).unwrap();
1549            assert_eq!(lang, WasmLanguage::Rust);
1550        }
1551
1552        #[test]
1553        fn test_detect_cargo_toml_with_cargo_component_metadata() {
1554            let dir = create_temp_dir();
1555            fs::write(
1556                dir.path().join("Cargo.toml"),
1557                r#"[package]
1558name = "test"
1559version = "0.1.0"
1560
1561[package.metadata.component]
1562package = "test:component"
1563"#,
1564            )
1565            .unwrap();
1566
1567            let lang = detect_language(dir.path()).unwrap();
1568            assert_eq!(lang, WasmLanguage::RustComponent);
1569        }
1570
1571        #[test]
1572        fn test_detect_cargo_toml_with_wit_bindgen_dep() {
1573            let dir = create_temp_dir();
1574            fs::write(
1575                dir.path().join("Cargo.toml"),
1576                r#"[package]
1577name = "test"
1578version = "0.1.0"
1579
1580[dependencies]
1581wit-bindgen = "0.20"
1582"#,
1583            )
1584            .unwrap();
1585
1586            let lang = detect_language(dir.path()).unwrap();
1587            assert_eq!(lang, WasmLanguage::RustComponent);
1588        }
1589
1590        #[test]
1591        fn test_detect_cargo_toml_with_cargo_component_bindings() {
1592            let dir = create_temp_dir();
1593            fs::write(
1594                dir.path().join("Cargo.toml"),
1595                r#"[package]
1596name = "test"
1597version = "0.1.0"
1598
1599[dependencies]
1600cargo-component-bindings = "0.1"
1601"#,
1602            )
1603            .unwrap();
1604
1605            let lang = detect_language(dir.path()).unwrap();
1606            assert_eq!(lang, WasmLanguage::RustComponent);
1607        }
1608
1609        #[test]
1610        fn test_detect_cargo_component_toml_file() {
1611            let dir = create_temp_dir();
1612            fs::write(
1613                dir.path().join("Cargo.toml"),
1614                r#"[package]
1615name = "test"
1616version = "0.1.0"
1617"#,
1618            )
1619            .unwrap();
1620            fs::write(
1621                dir.path().join("cargo-component.toml"),
1622                "# cargo-component config",
1623            )
1624            .unwrap();
1625
1626            let lang = detect_language(dir.path()).unwrap();
1627            assert_eq!(lang, WasmLanguage::RustComponent);
1628        }
1629
1630        #[test]
1631        fn test_detect_go_mod() {
1632            let dir = create_temp_dir();
1633            fs::write(
1634                dir.path().join("go.mod"),
1635                "module example.com/test\n\ngo 1.21\n",
1636            )
1637            .unwrap();
1638
1639            let lang = detect_language(dir.path()).unwrap();
1640            assert_eq!(lang, WasmLanguage::Go);
1641        }
1642
1643        #[test]
1644        fn test_detect_pyproject_toml() {
1645            let dir = create_temp_dir();
1646            fs::write(
1647                dir.path().join("pyproject.toml"),
1648                r#"[project]
1649name = "my-package"
1650version = "0.1.0"
1651"#,
1652            )
1653            .unwrap();
1654
1655            let lang = detect_language(dir.path()).unwrap();
1656            assert_eq!(lang, WasmLanguage::Python);
1657        }
1658
1659        #[test]
1660        fn test_detect_requirements_txt() {
1661            let dir = create_temp_dir();
1662            fs::write(
1663                dir.path().join("requirements.txt"),
1664                "flask==2.0.0\nrequests>=2.25.0",
1665            )
1666            .unwrap();
1667
1668            let lang = detect_language(dir.path()).unwrap();
1669            assert_eq!(lang, WasmLanguage::Python);
1670        }
1671
1672        #[test]
1673        fn test_detect_setup_py() {
1674            let dir = create_temp_dir();
1675            fs::write(
1676                dir.path().join("setup.py"),
1677                r#"from setuptools import setup
1678setup(name="mypackage")
1679"#,
1680            )
1681            .unwrap();
1682
1683            let lang = detect_language(dir.path()).unwrap();
1684            assert_eq!(lang, WasmLanguage::Python);
1685        }
1686
1687        #[test]
1688        fn test_detect_package_json_assemblyscript_in_dependencies() {
1689            let dir = create_temp_dir();
1690            fs::write(
1691                dir.path().join("package.json"),
1692                r#"{"name": "test", "dependencies": {"assemblyscript": "^0.27.0"}}"#,
1693            )
1694            .unwrap();
1695
1696            let lang = detect_language(dir.path()).unwrap();
1697            assert_eq!(lang, WasmLanguage::AssemblyScript);
1698        }
1699
1700        #[test]
1701        fn test_detect_package_json_assemblyscript_in_dev_dependencies() {
1702            let dir = create_temp_dir();
1703            fs::write(
1704                dir.path().join("package.json"),
1705                r#"{"name": "test", "devDependencies": {"assemblyscript": "^0.27.0"}}"#,
1706            )
1707            .unwrap();
1708
1709            let lang = detect_language(dir.path()).unwrap();
1710            assert_eq!(lang, WasmLanguage::AssemblyScript);
1711        }
1712
1713        #[test]
1714        fn test_detect_package_json_assemblyscript_in_scripts() {
1715            let dir = create_temp_dir();
1716            fs::write(
1717                dir.path().join("package.json"),
1718                r#"{"name": "test", "scripts": {"build": "asc assembly/index.ts"}}"#,
1719            )
1720            .unwrap();
1721
1722            let lang = detect_language(dir.path()).unwrap();
1723            assert_eq!(lang, WasmLanguage::AssemblyScript);
1724        }
1725
1726        #[test]
1727        fn test_detect_package_json_assemblyscript_asc_command() {
1728            let dir = create_temp_dir();
1729            fs::write(
1730                dir.path().join("package.json"),
1731                r#"{"name": "test", "scripts": {"compile": "asc"}}"#,
1732            )
1733            .unwrap();
1734
1735            let lang = detect_language(dir.path()).unwrap();
1736            assert_eq!(lang, WasmLanguage::AssemblyScript);
1737        }
1738
1739        #[test]
1740        fn test_detect_package_json_typescript() {
1741            let dir = create_temp_dir();
1742            fs::write(
1743                dir.path().join("package.json"),
1744                r#"{"name": "test", "version": "1.0.0", "devDependencies": {"typescript": "^5.0.0"}}"#,
1745            )
1746            .unwrap();
1747
1748            let lang = detect_language(dir.path()).unwrap();
1749            assert_eq!(lang, WasmLanguage::TypeScript);
1750        }
1751
1752        #[test]
1753        fn test_detect_package_json_plain_no_assemblyscript() {
1754            let dir = create_temp_dir();
1755            fs::write(
1756                dir.path().join("package.json"),
1757                r#"{"name": "test", "version": "1.0.0"}"#,
1758            )
1759            .unwrap();
1760
1761            let lang = detect_language(dir.path()).unwrap();
1762            assert_eq!(lang, WasmLanguage::TypeScript);
1763        }
1764
1765        #[test]
1766        fn test_detect_build_zig() {
1767            let dir = create_temp_dir();
1768            fs::write(
1769                dir.path().join("build.zig"),
1770                r#"const std = @import("std");
1771pub fn build(b: *std.build.Builder) void {}
1772"#,
1773            )
1774            .unwrap();
1775
1776            let lang = detect_language(dir.path()).unwrap();
1777            assert_eq!(lang, WasmLanguage::Zig);
1778        }
1779
1780        #[test]
1781        fn test_detect_makefile_with_c_files() {
1782            let dir = create_temp_dir();
1783            fs::write(dir.path().join("Makefile"), "all:\n\t$(CC) main.c -o main").unwrap();
1784            fs::write(dir.path().join("main.c"), "int main() { return 0; }").unwrap();
1785
1786            let lang = detect_language(dir.path()).unwrap();
1787            assert_eq!(lang, WasmLanguage::C);
1788        }
1789
1790        #[test]
1791        fn test_detect_cmakelists_with_c_files() {
1792            let dir = create_temp_dir();
1793            fs::write(
1794                dir.path().join("CMakeLists.txt"),
1795                "cmake_minimum_required(VERSION 3.10)\nproject(test)",
1796            )
1797            .unwrap();
1798            fs::write(dir.path().join("main.c"), "int main() { return 0; }").unwrap();
1799
1800            let lang = detect_language(dir.path()).unwrap();
1801            assert_eq!(lang, WasmLanguage::C);
1802        }
1803
1804        #[test]
1805        fn test_detect_c_header_file_only() {
1806            let dir = create_temp_dir();
1807            fs::write(
1808                dir.path().join("header.h"),
1809                "#ifndef HEADER_H\n#define HEADER_H\n#endif",
1810            )
1811            .unwrap();
1812
1813            let lang = detect_language(dir.path()).unwrap();
1814            assert_eq!(lang, WasmLanguage::C);
1815        }
1816
1817        #[test]
1818        fn test_detect_c_in_src_directory() {
1819            let dir = create_temp_dir();
1820            let src_dir = dir.path().join("src");
1821            fs::create_dir(&src_dir).unwrap();
1822            fs::write(src_dir.join("main.c"), "int main() { return 0; }").unwrap();
1823
1824            let lang = detect_language(dir.path()).unwrap();
1825            assert_eq!(lang, WasmLanguage::C);
1826        }
1827
1828        #[test]
1829        fn test_detect_empty_directory_error() {
1830            let dir = create_temp_dir();
1831            // Empty directory
1832
1833            let result = detect_language(dir.path());
1834            assert!(matches!(
1835                result,
1836                Err(WasmBuildError::LanguageNotDetected { .. })
1837            ));
1838        }
1839
1840        #[test]
1841        fn test_detect_unknown_files_error() {
1842            let dir = create_temp_dir();
1843            fs::write(dir.path().join("random.txt"), "some text").unwrap();
1844            fs::write(dir.path().join("data.json"), "{}").unwrap();
1845
1846            let result = detect_language(dir.path());
1847            assert!(matches!(
1848                result,
1849                Err(WasmBuildError::LanguageNotDetected { .. })
1850            ));
1851        }
1852
1853        #[test]
1854        fn test_detect_makefile_without_c_files_error() {
1855            let dir = create_temp_dir();
1856            fs::write(dir.path().join("Makefile"), "all:\n\techo hello").unwrap();
1857
1858            let result = detect_language(dir.path());
1859            assert!(matches!(
1860                result,
1861                Err(WasmBuildError::LanguageNotDetected { .. })
1862            ));
1863        }
1864
1865        #[test]
1866        fn test_detect_priority_rust_over_package_json() {
1867            // When both Cargo.toml and package.json exist, Rust takes priority
1868            let dir = create_temp_dir();
1869            fs::write(
1870                dir.path().join("Cargo.toml"),
1871                r#"[package]
1872name = "test"
1873version = "0.1.0"
1874"#,
1875            )
1876            .unwrap();
1877            fs::write(dir.path().join("package.json"), r#"{"name": "test"}"#).unwrap();
1878
1879            let lang = detect_language(dir.path()).unwrap();
1880            assert_eq!(lang, WasmLanguage::Rust);
1881        }
1882
1883        #[test]
1884        fn test_detect_priority_go_over_python() {
1885            // When both go.mod and pyproject.toml exist, Go takes priority
1886            let dir = create_temp_dir();
1887            fs::write(dir.path().join("go.mod"), "module test").unwrap();
1888            fs::write(dir.path().join("pyproject.toml"), "[project]").unwrap();
1889
1890            let lang = detect_language(dir.path()).unwrap();
1891            assert_eq!(lang, WasmLanguage::Go);
1892        }
1893    }
1894
1895    // =========================================================================
1896    // get_build_command tests
1897    // =========================================================================
1898
1899    mod get_build_command_tests {
1900        use super::*;
1901
1902        #[test]
1903        fn test_rust_preview1_release() {
1904            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, true);
1905            assert_eq!(cmd[0], "cargo");
1906            assert_eq!(cmd[1], "build");
1907            assert!(cmd.contains(&"--target".to_string()));
1908            assert!(cmd.contains(&"wasm32-wasip1".to_string()));
1909            assert!(cmd.contains(&"--release".to_string()));
1910        }
1911
1912        #[test]
1913        fn test_rust_preview2_release() {
1914            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview2, true);
1915            assert_eq!(cmd[0], "cargo");
1916            assert_eq!(cmd[1], "build");
1917            assert!(cmd.contains(&"--target".to_string()));
1918            assert!(cmd.contains(&"wasm32-wasip2".to_string()));
1919            assert!(cmd.contains(&"--release".to_string()));
1920        }
1921
1922        #[test]
1923        fn test_rust_preview1_debug() {
1924            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, false);
1925            assert_eq!(cmd[0], "cargo");
1926            assert!(cmd.contains(&"wasm32-wasip1".to_string()));
1927            assert!(!cmd.contains(&"--release".to_string()));
1928        }
1929
1930        #[test]
1931        fn test_rust_preview2_debug() {
1932            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview2, false);
1933            assert_eq!(cmd[0], "cargo");
1934            assert!(cmd.contains(&"wasm32-wasip2".to_string()));
1935            assert!(!cmd.contains(&"--release".to_string()));
1936        }
1937
1938        #[test]
1939        fn test_rust_component_release() {
1940            let cmd = get_build_command(WasmLanguage::RustComponent, WasiTarget::Preview2, true);
1941            assert_eq!(cmd[0], "cargo");
1942            assert_eq!(cmd[1], "component");
1943            assert_eq!(cmd[2], "build");
1944            assert!(cmd.contains(&"--release".to_string()));
1945        }
1946
1947        #[test]
1948        fn test_rust_component_debug() {
1949            let cmd = get_build_command(WasmLanguage::RustComponent, WasiTarget::Preview2, false);
1950            assert_eq!(cmd[0], "cargo");
1951            assert_eq!(cmd[1], "component");
1952            assert_eq!(cmd[2], "build");
1953            assert!(!cmd.contains(&"--release".to_string()));
1954        }
1955
1956        #[test]
1957        fn test_go_preview1() {
1958            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
1959            assert_eq!(cmd[0], "tinygo");
1960            assert_eq!(cmd[1], "build");
1961            assert!(cmd.contains(&"-target".to_string()));
1962            assert!(cmd.contains(&"wasip1".to_string()));
1963        }
1964
1965        #[test]
1966        fn test_go_preview2() {
1967            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, false);
1968            assert_eq!(cmd[0], "tinygo");
1969            assert!(cmd.contains(&"-target".to_string()));
1970            assert!(cmd.contains(&"wasip2".to_string()));
1971        }
1972
1973        #[test]
1974        fn test_go_release_optimization() {
1975            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, true);
1976            assert_eq!(cmd[0], "tinygo");
1977            assert!(cmd.contains(&"-opt".to_string()));
1978            assert!(cmd.contains(&"2".to_string()));
1979        }
1980
1981        #[test]
1982        fn test_go_debug_no_optimization() {
1983            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview2, false);
1984            assert_eq!(cmd[0], "tinygo");
1985            assert!(!cmd.contains(&"-opt".to_string()));
1986        }
1987
1988        #[test]
1989        fn test_tinygo_correct_target_flag() {
1990            // TinyGo uses -target (single dash) not --target
1991            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
1992            assert!(cmd.contains(&"-target".to_string()));
1993            assert!(!cmd.contains(&"--target".to_string()));
1994        }
1995
1996        #[test]
1997        fn test_python() {
1998            let cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, true);
1999            assert_eq!(cmd[0], "componentize-py");
2000            assert!(cmd.contains(&"-d".to_string()));
2001            assert!(cmd.contains(&"wit".to_string()));
2002            assert!(cmd.contains(&"-w".to_string()));
2003            assert!(cmd.contains(&"world".to_string()));
2004            assert!(cmd.contains(&"componentize".to_string()));
2005            assert!(cmd.contains(&"app".to_string()));
2006            assert!(cmd.contains(&"-o".to_string()));
2007            assert!(cmd.contains(&"app.wasm".to_string()));
2008        }
2009
2010        #[test]
2011        fn test_python_release_same_as_debug() {
2012            // componentize-py doesn't have explicit optimization flags
2013            let release_cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, true);
2014            let debug_cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, false);
2015            assert_eq!(release_cmd, debug_cmd);
2016        }
2017
2018        #[test]
2019        fn test_componentize_py_arguments() {
2020            let cmd = get_build_command(WasmLanguage::Python, WasiTarget::Preview2, false);
2021            // Verify the order and presence of key arguments
2022            let cmd_str = cmd.join(" ");
2023            assert!(
2024                cmd_str.contains("componentize-py -d wit -w world componentize app -o app.wasm")
2025            );
2026        }
2027
2028        #[test]
2029        fn test_typescript() {
2030            let cmd = get_build_command(WasmLanguage::TypeScript, WasiTarget::Preview2, true);
2031            assert_eq!(cmd[0], "npx");
2032            assert!(cmd.contains(&"jco".to_string()));
2033            assert!(cmd.contains(&"componentize".to_string()));
2034            assert!(cmd.contains(&"--wit".to_string()));
2035        }
2036
2037        #[test]
2038        fn test_assemblyscript_release() {
2039            let cmd = get_build_command(WasmLanguage::AssemblyScript, WasiTarget::Preview2, true);
2040            assert_eq!(cmd[0], "npx");
2041            assert!(cmd.contains(&"asc".to_string()));
2042            assert!(cmd.contains(&"--optimize".to_string()));
2043        }
2044
2045        #[test]
2046        fn test_assemblyscript_debug() {
2047            let cmd = get_build_command(WasmLanguage::AssemblyScript, WasiTarget::Preview2, false);
2048            assert_eq!(cmd[0], "npx");
2049            assert!(cmd.contains(&"asc".to_string()));
2050            assert!(!cmd.contains(&"--optimize".to_string()));
2051        }
2052
2053        #[test]
2054        fn test_c_release() {
2055            let cmd = get_build_command(WasmLanguage::C, WasiTarget::Preview1, true);
2056            assert_eq!(cmd[0], "clang");
2057            assert!(cmd.contains(&"--target=wasm32-wasi".to_string()));
2058            assert!(cmd.contains(&"-O2".to_string()));
2059        }
2060
2061        #[test]
2062        fn test_c_debug() {
2063            let cmd = get_build_command(WasmLanguage::C, WasiTarget::Preview1, false);
2064            assert_eq!(cmd[0], "clang");
2065            assert!(cmd.contains(&"--target=wasm32-wasi".to_string()));
2066            assert!(!cmd.contains(&"-O2".to_string()));
2067        }
2068
2069        #[test]
2070        fn test_zig_release() {
2071            let cmd = get_build_command(WasmLanguage::Zig, WasiTarget::Preview1, true);
2072            assert_eq!(cmd[0], "zig");
2073            assert_eq!(cmd[1], "build");
2074            assert!(cmd.contains(&"-Dtarget=wasm32-wasi".to_string()));
2075            assert!(cmd.contains(&"-Doptimize=ReleaseFast".to_string()));
2076        }
2077
2078        #[test]
2079        fn test_zig_debug() {
2080            let cmd = get_build_command(WasmLanguage::Zig, WasiTarget::Preview1, false);
2081            assert_eq!(cmd[0], "zig");
2082            assert_eq!(cmd[1], "build");
2083            assert!(cmd.contains(&"-Dtarget=wasm32-wasi".to_string()));
2084            assert!(!cmd.contains(&"-Doptimize=ReleaseFast".to_string()));
2085        }
2086
2087        #[test]
2088        fn test_cargo_uses_double_dash_target() {
2089            // cargo uses --target (double dash)
2090            let cmd = get_build_command(WasmLanguage::Rust, WasiTarget::Preview1, false);
2091            assert!(cmd.contains(&"--target".to_string()));
2092        }
2093
2094        #[test]
2095        fn test_go_output_file() {
2096            let cmd = get_build_command(WasmLanguage::Go, WasiTarget::Preview1, false);
2097            assert!(cmd.contains(&"-o".to_string()));
2098            assert!(cmd.contains(&"main.wasm".to_string()));
2099        }
2100
2101        #[test]
2102        fn test_all_commands_non_empty() {
2103            for lang in WasmLanguage::all() {
2104                for target in [WasiTarget::Preview1, WasiTarget::Preview2] {
2105                    for release in [true, false] {
2106                        let cmd = get_build_command(*lang, target, release);
2107                        assert!(
2108                            !cmd.is_empty(),
2109                            "Command for {:?}/{:?}/{} should not be empty",
2110                            lang,
2111                            target,
2112                            release
2113                        );
2114                        assert!(
2115                            !cmd[0].is_empty(),
2116                            "First command element should not be empty"
2117                        );
2118                    }
2119                }
2120            }
2121        }
2122    }
2123
2124    // =========================================================================
2125    // Additional helper function tests
2126    // =========================================================================
2127
2128    mod helper_function_tests {
2129        use super::*;
2130
2131        #[test]
2132        fn test_get_rust_package_name_success() {
2133            let dir = create_temp_dir();
2134            fs::write(
2135                dir.path().join("Cargo.toml"),
2136                r#"[package]
2137name = "my-cool-package"
2138version = "0.1.0"
2139"#,
2140            )
2141            .unwrap();
2142
2143            let name = get_rust_package_name(dir.path()).unwrap();
2144            assert_eq!(name, "my-cool-package");
2145        }
2146
2147        #[test]
2148        fn test_get_rust_package_name_with_single_quotes() {
2149            let dir = create_temp_dir();
2150            fs::write(
2151                dir.path().join("Cargo.toml"),
2152                "[package]\nname = 'single-quoted'\nversion = '0.1.0'\n",
2153            )
2154            .unwrap();
2155
2156            let name = get_rust_package_name(dir.path()).unwrap();
2157            assert_eq!(name, "single-quoted");
2158        }
2159
2160        #[test]
2161        fn test_get_rust_package_name_missing_file() {
2162            let dir = create_temp_dir();
2163            // No Cargo.toml
2164
2165            let result = get_rust_package_name(dir.path());
2166            assert!(matches!(
2167                result,
2168                Err(WasmBuildError::ProjectConfigError { .. })
2169            ));
2170        }
2171
2172        #[test]
2173        fn test_get_rust_package_name_no_name_field() {
2174            let dir = create_temp_dir();
2175            fs::write(
2176                dir.path().join("Cargo.toml"),
2177                "[package]\nversion = \"0.1.0\"\n",
2178            )
2179            .unwrap();
2180
2181            let result = get_rust_package_name(dir.path());
2182            assert!(matches!(
2183                result,
2184                Err(WasmBuildError::ProjectConfigError { .. })
2185            ));
2186        }
2187
2188        #[test]
2189        fn test_find_any_wasm_file_in_root() {
2190            let dir = create_temp_dir();
2191            fs::write(dir.path().join("test.wasm"), "wasm content").unwrap();
2192
2193            let found = find_any_wasm_file(dir.path());
2194            assert!(found.is_some());
2195            assert!(found.unwrap().ends_with("test.wasm"));
2196        }
2197
2198        #[test]
2199        fn test_find_any_wasm_file_in_target() {
2200            let dir = create_temp_dir();
2201            let target_dir = dir.path().join("target");
2202            fs::create_dir(&target_dir).unwrap();
2203            fs::write(target_dir.join("output.wasm"), "wasm content").unwrap();
2204
2205            let found = find_any_wasm_file(dir.path());
2206            assert!(found.is_some());
2207        }
2208
2209        #[test]
2210        fn test_find_any_wasm_file_in_build() {
2211            let dir = create_temp_dir();
2212            let build_dir = dir.path().join("build");
2213            fs::create_dir(&build_dir).unwrap();
2214            fs::write(build_dir.join("module.wasm"), "wasm content").unwrap();
2215
2216            let found = find_any_wasm_file(dir.path());
2217            assert!(found.is_some());
2218        }
2219
2220        #[test]
2221        fn test_find_any_wasm_file_in_dist() {
2222            let dir = create_temp_dir();
2223            let dist_dir = dir.path().join("dist");
2224            fs::create_dir(&dist_dir).unwrap();
2225            fs::write(dist_dir.join("bundle.wasm"), "wasm content").unwrap();
2226
2227            let found = find_any_wasm_file(dir.path());
2228            assert!(found.is_some());
2229        }
2230
2231        #[test]
2232        fn test_find_any_wasm_file_nested() {
2233            let dir = create_temp_dir();
2234            let nested = dir
2235                .path()
2236                .join("target")
2237                .join("wasm32-wasip2")
2238                .join("release");
2239            fs::create_dir_all(&nested).unwrap();
2240            fs::write(nested.join("deep.wasm"), "wasm content").unwrap();
2241
2242            let found = find_any_wasm_file(dir.path());
2243            assert!(found.is_some());
2244        }
2245
2246        #[test]
2247        fn test_find_any_wasm_file_none() {
2248            let dir = create_temp_dir();
2249            fs::write(dir.path().join("not_wasm.txt"), "text").unwrap();
2250
2251            let found = find_any_wasm_file(dir.path());
2252            assert!(found.is_none());
2253        }
2254
2255        #[test]
2256        fn test_find_any_wasm_file_respects_depth_limit() {
2257            let dir = create_temp_dir();
2258            // Create a deeply nested wasm file (beyond max_depth of 3)
2259            let deep = dir.path().join("a").join("b").join("c").join("d").join("e");
2260            fs::create_dir_all(&deep).unwrap();
2261            fs::write(deep.join("too_deep.wasm"), "wasm").unwrap();
2262
2263            // The recursive search has max_depth of 3, so this might not be found
2264            // depending on the search order
2265            let _ = find_any_wasm_file(dir.path());
2266            // We don't assert the result as it depends on implementation details
2267        }
2268
2269        #[test]
2270        fn test_has_c_source_files_true_c() {
2271            let dir = create_temp_dir();
2272            fs::write(dir.path().join("main.c"), "int main() {}").unwrap();
2273
2274            assert!(has_c_source_files(dir.path()));
2275        }
2276
2277        #[test]
2278        fn test_has_c_source_files_true_h() {
2279            let dir = create_temp_dir();
2280            fs::write(dir.path().join("header.h"), "#pragma once").unwrap();
2281
2282            assert!(has_c_source_files(dir.path()));
2283        }
2284
2285        #[test]
2286        fn test_has_c_source_files_in_src() {
2287            let dir = create_temp_dir();
2288            let src = dir.path().join("src");
2289            fs::create_dir(&src).unwrap();
2290            fs::write(src.join("lib.c"), "void foo() {}").unwrap();
2291
2292            assert!(has_c_source_files(dir.path()));
2293        }
2294
2295        #[test]
2296        fn test_has_c_source_files_false() {
2297            let dir = create_temp_dir();
2298            fs::write(dir.path().join("main.rs"), "fn main() {}").unwrap();
2299
2300            assert!(!has_c_source_files(dir.path()));
2301        }
2302
2303        #[test]
2304        fn test_is_cargo_component_project_with_metadata() {
2305            let dir = create_temp_dir();
2306            let cargo_toml = dir.path().join("Cargo.toml");
2307            fs::write(
2308                &cargo_toml,
2309                r#"[package]
2310name = "test"
2311[package.metadata.component]
2312package = "test:component"
2313"#,
2314            )
2315            .unwrap();
2316
2317            assert!(is_cargo_component_project(&cargo_toml).unwrap());
2318        }
2319
2320        #[test]
2321        fn test_is_cargo_component_project_with_wit_bindgen() {
2322            let dir = create_temp_dir();
2323            let cargo_toml = dir.path().join("Cargo.toml");
2324            fs::write(
2325                &cargo_toml,
2326                r#"[package]
2327name = "test"
2328[dependencies]
2329wit-bindgen = "0.20"
2330"#,
2331            )
2332            .unwrap();
2333
2334            assert!(is_cargo_component_project(&cargo_toml).unwrap());
2335        }
2336
2337        #[test]
2338        fn test_is_cargo_component_project_plain_rust() {
2339            let dir = create_temp_dir();
2340            let cargo_toml = dir.path().join("Cargo.toml");
2341            fs::write(
2342                &cargo_toml,
2343                r#"[package]
2344name = "test"
2345version = "0.1.0"
2346"#,
2347            )
2348            .unwrap();
2349
2350            assert!(!is_cargo_component_project(&cargo_toml).unwrap());
2351        }
2352
2353        #[test]
2354        fn test_is_assemblyscript_project_dependencies() {
2355            let dir = create_temp_dir();
2356            let package_json = dir.path().join("package.json");
2357            fs::write(
2358                &package_json,
2359                r#"{"dependencies": {"assemblyscript": "^0.27"}}"#,
2360            )
2361            .unwrap();
2362
2363            assert!(is_assemblyscript_project(&package_json).unwrap());
2364        }
2365
2366        #[test]
2367        fn test_is_assemblyscript_project_dev_dependencies() {
2368            let dir = create_temp_dir();
2369            let package_json = dir.path().join("package.json");
2370            fs::write(
2371                &package_json,
2372                r#"{"devDependencies": {"assemblyscript": "^0.27"}}"#,
2373            )
2374            .unwrap();
2375
2376            assert!(is_assemblyscript_project(&package_json).unwrap());
2377        }
2378
2379        #[test]
2380        fn test_is_assemblyscript_project_script_with_asc() {
2381            let dir = create_temp_dir();
2382            let package_json = dir.path().join("package.json");
2383            fs::write(
2384                &package_json,
2385                r#"{"scripts": {"build": "asc assembly/index.ts"}}"#,
2386            )
2387            .unwrap();
2388
2389            assert!(is_assemblyscript_project(&package_json).unwrap());
2390        }
2391
2392        #[test]
2393        fn test_is_assemblyscript_project_false() {
2394            let dir = create_temp_dir();
2395            let package_json = dir.path().join("package.json");
2396            fs::write(&package_json, r#"{"dependencies": {"react": "^18.0.0"}}"#).unwrap();
2397
2398            assert!(!is_assemblyscript_project(&package_json).unwrap());
2399        }
2400
2401        #[test]
2402        fn test_is_assemblyscript_project_invalid_json() {
2403            let dir = create_temp_dir();
2404            let package_json = dir.path().join("package.json");
2405            fs::write(&package_json, "not valid json").unwrap();
2406
2407            let result = is_assemblyscript_project(&package_json);
2408            assert!(matches!(
2409                result,
2410                Err(WasmBuildError::ProjectConfigError { .. })
2411            ));
2412        }
2413    }
2414
2415    // =========================================================================
2416    // find_wasm_output tests
2417    // =========================================================================
2418
2419    mod find_wasm_output_tests {
2420        use super::*;
2421
2422        #[test]
2423        fn test_find_rust_release_output() {
2424            let dir = create_temp_dir();
2425            fs::write(
2426                dir.path().join("Cargo.toml"),
2427                "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
2428            )
2429            .unwrap();
2430
2431            let output_dir = dir
2432                .path()
2433                .join("target")
2434                .join("wasm32-wasip2")
2435                .join("release");
2436            fs::create_dir_all(&output_dir).unwrap();
2437            fs::write(output_dir.join("myapp.wasm"), "wasm").unwrap();
2438
2439            let result =
2440                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2441            assert!(result.is_ok());
2442            assert!(result.unwrap().ends_with("myapp.wasm"));
2443        }
2444
2445        #[test]
2446        fn test_find_rust_debug_output() {
2447            let dir = create_temp_dir();
2448            fs::write(
2449                dir.path().join("Cargo.toml"),
2450                "[package]\nname = \"myapp\"\nversion = \"0.1.0\"\n",
2451            )
2452            .unwrap();
2453
2454            let output_dir = dir
2455                .path()
2456                .join("target")
2457                .join("wasm32-wasip1")
2458                .join("debug");
2459            fs::create_dir_all(&output_dir).unwrap();
2460            fs::write(output_dir.join("myapp.wasm"), "wasm").unwrap();
2461
2462            let result =
2463                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview1, false);
2464            assert!(result.is_ok());
2465        }
2466
2467        #[test]
2468        fn test_find_rust_underscore_name() {
2469            let dir = create_temp_dir();
2470            fs::write(
2471                dir.path().join("Cargo.toml"),
2472                "[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
2473            )
2474            .unwrap();
2475
2476            let output_dir = dir
2477                .path()
2478                .join("target")
2479                .join("wasm32-wasip2")
2480                .join("release");
2481            fs::create_dir_all(&output_dir).unwrap();
2482            // Rust converts hyphens to underscores in the output filename
2483            fs::write(output_dir.join("my_app.wasm"), "wasm").unwrap();
2484
2485            let result =
2486                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2487            assert!(result.is_ok());
2488        }
2489
2490        #[test]
2491        fn test_find_go_output() {
2492            let dir = create_temp_dir();
2493            fs::write(dir.path().join("main.wasm"), "wasm").unwrap();
2494
2495            let result =
2496                find_wasm_output(dir.path(), WasmLanguage::Go, WasiTarget::Preview1, false);
2497            assert!(result.is_ok());
2498            assert!(result.unwrap().ends_with("main.wasm"));
2499        }
2500
2501        #[test]
2502        fn test_find_python_output() {
2503            let dir = create_temp_dir();
2504            fs::write(dir.path().join("app.wasm"), "wasm").unwrap();
2505
2506            let result =
2507                find_wasm_output(dir.path(), WasmLanguage::Python, WasiTarget::Preview2, true);
2508            assert!(result.is_ok());
2509            assert!(result.unwrap().ends_with("app.wasm"));
2510        }
2511
2512        #[test]
2513        fn test_find_typescript_output() {
2514            let dir = create_temp_dir();
2515            let dist_dir = dir.path().join("dist");
2516            fs::create_dir(&dist_dir).unwrap();
2517            fs::write(dist_dir.join("component.wasm"), "wasm").unwrap();
2518
2519            let result = find_wasm_output(
2520                dir.path(),
2521                WasmLanguage::TypeScript,
2522                WasiTarget::Preview2,
2523                true,
2524            );
2525            assert!(result.is_ok());
2526        }
2527
2528        #[test]
2529        fn test_find_assemblyscript_release_output() {
2530            let dir = create_temp_dir();
2531            let build_dir = dir.path().join("build");
2532            fs::create_dir(&build_dir).unwrap();
2533            fs::write(build_dir.join("release.wasm"), "wasm").unwrap();
2534
2535            let result = find_wasm_output(
2536                dir.path(),
2537                WasmLanguage::AssemblyScript,
2538                WasiTarget::Preview2,
2539                true,
2540            );
2541            assert!(result.is_ok());
2542        }
2543
2544        #[test]
2545        fn test_find_c_output() {
2546            let dir = create_temp_dir();
2547            fs::write(dir.path().join("main.wasm"), "wasm").unwrap();
2548
2549            let result = find_wasm_output(dir.path(), WasmLanguage::C, WasiTarget::Preview1, true);
2550            assert!(result.is_ok());
2551        }
2552
2553        #[test]
2554        fn test_find_zig_output() {
2555            let dir = create_temp_dir();
2556            let zig_out = dir.path().join("zig-out").join("bin");
2557            fs::create_dir_all(&zig_out).unwrap();
2558            fs::write(zig_out.join("main.wasm"), "wasm").unwrap();
2559
2560            let result =
2561                find_wasm_output(dir.path(), WasmLanguage::Zig, WasiTarget::Preview1, true);
2562            assert!(result.is_ok());
2563        }
2564
2565        #[test]
2566        fn test_find_output_not_found() {
2567            let dir = create_temp_dir();
2568            // No wasm files
2569
2570            let result =
2571                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2572            assert!(matches!(result, Err(WasmBuildError::OutputNotFound { .. })));
2573        }
2574
2575        #[test]
2576        fn test_find_output_fallback_search() {
2577            let dir = create_temp_dir();
2578            fs::write(
2579                dir.path().join("Cargo.toml"),
2580                "[package]\nname = \"test\"\n",
2581            )
2582            .unwrap();
2583
2584            // Put wasm in unexpected location
2585            let other_dir = dir.path().join("target").join("other");
2586            fs::create_dir_all(&other_dir).unwrap();
2587            fs::write(other_dir.join("unexpected.wasm"), "wasm").unwrap();
2588
2589            // Should find via fallback search
2590            let result =
2591                find_wasm_output(dir.path(), WasmLanguage::Rust, WasiTarget::Preview2, true);
2592            assert!(result.is_ok());
2593        }
2594    }
2595}