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