From 3e7327c013606cf8aa99c8eed66dc9b33abcf8a5 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 31 Mar 2026 17:00:11 +0100 Subject: [PATCH 1/2] feat: enable wasm32 compilation by making xx crate conditional Make the xx crate dependency conditional on non-wasm32 targets and replace xx::regex!, xx::file, and xx::process usages with std equivalents (LazyLock, std::fs, std::process) so the codebase compiles for wasm32-wasip1. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test.yml | 2 ++ cli/Cargo.toml | 1 + cli/src/cli/complete_word.rs | 17 +++++++++++++---- cli/src/cli/generate/fig.rs | 6 +++++- cli/src/cli/generate/manpage.rs | 6 +++++- cli/src/cli/generate/markdown.rs | 8 ++++++-- cli/src/lib.rs | 1 + lib/Cargo.toml | 1 + lib/src/docs/markdown/renderer.rs | 12 ++++++++---- lib/src/error.rs | 1 + lib/src/sh.rs | 12 ++++++++++++ lib/src/spec/mod.rs | 17 ++++++++++------- 12 files changed, 65 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f9ebaf3..2124e227 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,3 +26,5 @@ jobs: - run: mise r build - run: mise r test - run: mise r lint + - name: check wasm build + run: rustup target add wasm32-wasip1 && cargo build --target wasm32-wasip1 -p usage-cli diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b92e015b..5ad17ea0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -40,6 +40,7 @@ serde_with = "3" tera = "1" thiserror = "2" usage-lib = { workspace = true, features = ["clap", "docs", "unstable_choices_env"] } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] xx = "2" [target.'cfg(unix)'.dependencies] diff --git a/cli/src/cli/complete_word.rs b/cli/src/cli/complete_word.rs index 115a7676..9d2958f4 100644 --- a/cli/src/cli/complete_word.rs +++ b/cli/src/cli/complete_word.rs @@ -2,15 +2,15 @@ use std::collections::BTreeMap; use std::env; use std::fmt::Debug; use std::path::{Path, PathBuf}; +#[cfg(not(target_arch = "wasm32"))] use std::process::Command; use std::sync::Arc; use clap::Args; use itertools::Itertools; use miette::IntoDiagnostic; +use regex::Regex; use std::sync::LazyLock; -use xx::process::check_status; -use xx::{regex, XXError, XXResult}; use usage::{Spec, SpecArg, SpecCommand, SpecComplete, SpecFlag}; @@ -283,7 +283,8 @@ impl CompleteWord { trace!("run: {run}"); let stdout = sh(&run)?; // trace!("stdout: {stdout}"); - let re = regex!(r"[^\\]:"); + static COLON_RE: LazyLock = LazyLock::new(|| Regex::new(r"[^\\]:").unwrap()); + let re = &*COLON_RE; return Ok(stdout .lines() .map(|l| { @@ -372,7 +373,10 @@ fn zsh_escape(s: &str) -> String { .replace(']', "\\]") } -fn sh(script: &str) -> XXResult { +#[cfg(not(target_arch = "wasm32"))] +fn sh(script: &str) -> xx::XXResult { + use xx::process::check_status; + use xx::XXError; let output = Command::new("sh") .arg("-c") .arg(script) @@ -387,3 +391,8 @@ fn sh(script: &str) -> XXResult { let stdout = String::from_utf8(output.stdout).expect("stdout is not utf-8"); Ok(stdout) } + +#[cfg(target_arch = "wasm32")] +fn sh(_script: &str) -> miette::Result { + Err(miette::miette!("shell execution is not supported on wasm")) +} diff --git a/cli/src/cli/generate/fig.rs b/cli/src/cli/generate/fig.rs index 7d0896a9..0af966d9 100644 --- a/cli/src/cli/generate/fig.rs +++ b/cli/src/cli/generate/fig.rs @@ -356,7 +356,11 @@ impl Fig { pub fn run(&self) -> miette::Result<()> { let write = |path: &PathBuf, md: &str| -> miette::Result<()> { println!("writing to {}", path.display()); - xx::file::write(path, format!("{}\n", md.trim()))?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| miette::miette!("{e}"))?; + } + std::fs::write(path, format!("{}\n", md.trim())) + .map_err(|e| miette::miette!("{e}"))?; Ok(()) }; let spec = generate::file_or_spec(&self.file, &self.spec)?; diff --git a/cli/src/cli/generate/manpage.rs b/cli/src/cli/generate/manpage.rs index 797e36be..c770f999 100644 --- a/cli/src/cli/generate/manpage.rs +++ b/cli/src/cli/generate/manpage.rs @@ -34,7 +34,11 @@ impl Manpage { if let Some(out_file) = &self.out_file { println!("writing to {}", out_file.display()); - xx::file::write(out_file, &manpage)?; + if let Some(parent) = out_file.parent() { + std::fs::create_dir_all(parent).map_err(|e| miette::miette!("{e}"))?; + } + std::fs::write(out_file, &manpage) + .map_err(|e| miette::miette!("{e}"))?; } else { print!("{}", manpage); } diff --git a/cli/src/cli/generate/markdown.rs b/cli/src/cli/generate/markdown.rs index 6938386f..d2e1cdd4 100644 --- a/cli/src/cli/generate/markdown.rs +++ b/cli/src/cli/generate/markdown.rs @@ -43,13 +43,17 @@ impl Markdown { pub fn run(&self) -> miette::Result<()> { let write = |path: &PathBuf, md: &str| -> miette::Result<()> { println!("writing to {}", path.display()); - xx::file::write( + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| miette::miette!("{e}"))?; + } + std::fs::write( path, format!( "\n{}\n", md.trim() ), - )?; + ) + .map_err(|e| miette::miette!("{e}"))?; Ok(()) }; let spec = parse_file_or_stdin(&self.file)?; diff --git a/cli/src/lib.rs b/cli/src/lib.rs index eeef3bff..b4502c7f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate log; extern crate miette; +#[cfg(not(target_arch = "wasm32"))] extern crate xx; use miette::Result; diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c6ed057e..c889b896 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -37,6 +37,7 @@ strum = { version = "0.28", features = ["derive"] } tera = { version = "1", optional = true } thiserror = "2" versions = "7" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] xx = "2" [features] diff --git a/lib/src/docs/markdown/renderer.rs b/lib/src/docs/markdown/renderer.rs index 9772b3b9..db6161ac 100644 --- a/lib/src/docs/markdown/renderer.rs +++ b/lib/src/docs/markdown/renderer.rs @@ -4,7 +4,8 @@ use crate::error::UsageErr; use itertools::Itertools; use serde::Serialize; use std::collections::HashMap; -use xx::regex; +use regex::Regex; +use std::sync::LazyLock; #[derive(Debug, Clone)] pub struct MarkdownRenderer { @@ -88,7 +89,10 @@ impl MarkdownRenderer { return line.to_string(); } // replace '<' with '<' but not inside code blocks - xx::regex!(r"(`[^`]*`)|(<)") + { + static RE: LazyLock = LazyLock::new(|| Regex::new(r"(`[^`]*`)|(<)").unwrap()); + &RE + } .replace_all(line, |caps: ®ex::Captures| { if caps.get(1).is_some() { caps.get(1).unwrap().as_str().to_string() @@ -102,8 +106,8 @@ impl MarkdownRenderer { Ok(value.into()) }, ); - let path_re = - regex!(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/"); + static PATH_RE: LazyLock = LazyLock::new(|| Regex::new(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/").unwrap()); + let path_re = &*PATH_RE; tera.register_function("source_code_link", |args: &HashMap| { let spec = args.get("spec").unwrap().as_object().unwrap(); let cmd = args.get("cmd").unwrap().as_object().unwrap(); diff --git a/lib/src/error.rs b/lib/src/error.rs index d117d9bd..9101c680 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -50,6 +50,7 @@ pub enum UsageErr { #[diagnostic(transparent)] KdlError(#[from] kdl::KdlError), + #[cfg(not(target_arch = "wasm32"))] #[error(transparent)] #[diagnostic(transparent)] XXError(#[from] xx::error::XXError), diff --git a/lib/src/sh.rs b/lib/src/sh.rs index 3640ab04..42b1cdea 100644 --- a/lib/src/sh.rs +++ b/lib/src/sh.rs @@ -1,7 +1,11 @@ +#[cfg(not(target_arch = "wasm32"))] use std::process::Command; +#[cfg(not(target_arch = "wasm32"))] use xx::process::check_status; +#[cfg(not(target_arch = "wasm32"))] use xx::{XXError, XXResult}; +#[cfg(not(target_arch = "wasm32"))] pub(crate) fn sh(script: &str) -> XXResult { #[cfg(unix)] let (shell, flag) = ("sh", "-c"); @@ -23,3 +27,11 @@ pub(crate) fn sh(script: &str) -> XXResult { let stdout = String::from_utf8(output.stdout).expect("stdout is not utf-8"); Ok(stdout) } + +#[cfg(target_arch = "wasm32")] +pub(crate) fn sh(_script: &str) -> std::result::Result { + Err(crate::error::UsageErr::IO(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "shell execution is not supported on wasm", + ))) +} diff --git a/lib/src/spec/mod.rs b/lib/src/spec/mod.rs index f5563605..5c0dab5f 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -18,7 +18,8 @@ use std::fmt::{Display, Formatter}; use std::iter::once; use std::path::Path; use std::str::FromStr; -use xx::file; +use regex::Regex; +use std::sync::LazyLock; use crate::error::UsageErr; use crate::spec::cmd::{SpecCommand, SpecExample}; @@ -102,7 +103,7 @@ impl Spec { /// If `bin` is not specified in the spec, it defaults to the filename. #[must_use = "parsing result should be used"] pub fn parse_script(file: &Path) -> Result { - let raw = extract_usage_from_comments(&file::read_to_string(file)?); + let raw = extract_usage_from_comments(&std::fs::read_to_string(file)?); let ctx = ParsingContext::new(file, &raw); let mut spec = Self::parse(&ctx, &raw)?; if spec.bin.is_empty() { @@ -303,11 +304,11 @@ fn check_usage_version(version: &str) { } fn split_script(file: &Path) -> Result { - let full = file::read_to_string(file)?; + let full = std::fs::read_to_string(file)?; // If file has a shebang and USAGE comments, extract the spec from comments if full.starts_with("#!") { - let usage_regex = xx::regex!(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])"); - if full.lines().any(|l| usage_regex.is_match(l)) { + static USAGE_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])").unwrap()); + if full.lines().any(|l| USAGE_RE.is_match(l)) { return Ok(extract_usage_from_comments(&full)); } } @@ -316,8 +317,10 @@ fn split_script(file: &Path) -> Result { } fn extract_usage_from_comments(full: &str) -> String { - let usage_regex = xx::regex!(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])(.*)$"); - let blank_comment_regex = xx::regex!(r"^(?:#|//|::)\s*$"); + static USAGE_CAPTURE_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])(.*)$").unwrap()); + static BLANK_COMMENT_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(?:#|//|::)\s*$").unwrap()); + let usage_regex = &*USAGE_CAPTURE_RE; + let blank_comment_regex = &*BLANK_COMMENT_RE; let mut usage = vec![]; let mut found = false; for line in full.lines() { From be98ac4474995bc05f0c44087573291e52fb287d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:32:50 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- cli/src/cli/generate/fig.rs | 3 +-- cli/src/cli/generate/manpage.rs | 3 +-- lib/src/docs/markdown/renderer.rs | 26 +++++++++++++++----------- lib/src/spec/mod.rs | 11 +++++++---- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cli/src/cli/generate/fig.rs b/cli/src/cli/generate/fig.rs index 0af966d9..3b008168 100644 --- a/cli/src/cli/generate/fig.rs +++ b/cli/src/cli/generate/fig.rs @@ -359,8 +359,7 @@ impl Fig { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).map_err(|e| miette::miette!("{e}"))?; } - std::fs::write(path, format!("{}\n", md.trim())) - .map_err(|e| miette::miette!("{e}"))?; + std::fs::write(path, format!("{}\n", md.trim())).map_err(|e| miette::miette!("{e}"))?; Ok(()) }; let spec = generate::file_or_spec(&self.file, &self.spec)?; diff --git a/cli/src/cli/generate/manpage.rs b/cli/src/cli/generate/manpage.rs index c770f999..dce5fbca 100644 --- a/cli/src/cli/generate/manpage.rs +++ b/cli/src/cli/generate/manpage.rs @@ -37,8 +37,7 @@ impl Manpage { if let Some(parent) = out_file.parent() { std::fs::create_dir_all(parent).map_err(|e| miette::miette!("{e}"))?; } - std::fs::write(out_file, &manpage) - .map_err(|e| miette::miette!("{e}"))?; + std::fs::write(out_file, &manpage).map_err(|e| miette::miette!("{e}"))?; } else { print!("{}", manpage); } diff --git a/lib/src/docs/markdown/renderer.rs b/lib/src/docs/markdown/renderer.rs index db6161ac..9879afb5 100644 --- a/lib/src/docs/markdown/renderer.rs +++ b/lib/src/docs/markdown/renderer.rs @@ -2,9 +2,9 @@ use crate::docs::markdown::tera::TERA; use crate::docs::models::Spec; use crate::error::UsageErr; use itertools::Itertools; +use regex::Regex; use serde::Serialize; use std::collections::HashMap; -use regex::Regex; use std::sync::LazyLock; #[derive(Debug, Clone)] @@ -90,23 +90,27 @@ impl MarkdownRenderer { } // replace '<' with '<' but not inside code blocks { - static RE: LazyLock = LazyLock::new(|| Regex::new(r"(`[^`]*`)|(<)").unwrap()); + static RE: LazyLock = + LazyLock::new(|| Regex::new(r"(`[^`]*`)|(<)").unwrap()); &RE } - .replace_all(line, |caps: ®ex::Captures| { - if caps.get(1).is_some() { - caps.get(1).unwrap().as_str().to_string() - } else { - "<".to_string() - } - }) - .to_string() + .replace_all(line, |caps: ®ex::Captures| { + if caps.get(1).is_some() { + caps.get(1).unwrap().as_str().to_string() + } else { + "<".to_string() + } + }) + .to_string() }) .join("\n"); Ok(value.into()) }, ); - static PATH_RE: LazyLock = LazyLock::new(|| Regex::new(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/").unwrap()); + static PATH_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/") + .unwrap() + }); let path_re = &*PATH_RE; tera.register_function("source_code_link", |args: &HashMap| { let spec = args.get("spec").unwrap().as_object().unwrap(); diff --git a/lib/src/spec/mod.rs b/lib/src/spec/mod.rs index 5c0dab5f..ec1879dd 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -13,12 +13,12 @@ pub mod mount; use indexmap::IndexMap; use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; use log::{info, warn}; +use regex::Regex; use serde::Serialize; use std::fmt::{Display, Formatter}; use std::iter::once; use std::path::Path; use std::str::FromStr; -use regex::Regex; use std::sync::LazyLock; use crate::error::UsageErr; @@ -307,7 +307,8 @@ fn split_script(file: &Path) -> Result { let full = std::fs::read_to_string(file)?; // If file has a shebang and USAGE comments, extract the spec from comments if full.starts_with("#!") { - static USAGE_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])").unwrap()); + static USAGE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])").unwrap()); if full.lines().any(|l| USAGE_RE.is_match(l)) { return Ok(extract_usage_from_comments(&full)); } @@ -317,8 +318,10 @@ fn split_script(file: &Path) -> Result { } fn extract_usage_from_comments(full: &str) -> String { - static USAGE_CAPTURE_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])(.*)$").unwrap()); - static BLANK_COMMENT_RE: LazyLock = LazyLock::new(|| Regex::new(r"^(?:#|//|::)\s*$").unwrap()); + static USAGE_CAPTURE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])(.*)$").unwrap()); + static BLANK_COMMENT_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^(?:#|//|::)\s*$").unwrap()); let usage_regex = &*USAGE_CAPTURE_RE; let blank_comment_regex = &*BLANK_COMMENT_RE; let mut usage = vec![];