From 074153a22aa24758e44977dea63a98fab664e47b Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 31 Mar 2026 17:00:11 +0100 Subject: [PATCH] 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 | 22 +++++++++++++--------- 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(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4cdb35ae..58081d27 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,3 +50,5 @@ jobs: exit 1 fi - 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 6534ce9c..bef4d757 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 168e96ff..57012f36 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}; @@ -290,7 +290,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| { @@ -379,10 +380,6 @@ fn zsh_escape(s: &str) -> String { .replace(']', "\\]") } -/// Wrap a completion value in single quotes if any character would otherwise -/// be interpreted by the shell. The result is meant to be inserted by -/// `compadd -Q` verbatim, so the user sees consistent single-quote quoting -/// instead of zsh's default mix of backslash and single-quote styles. fn zsh_shell_quote(s: &str) -> String { fn safe(c: char) -> bool { matches!(c, @@ -393,12 +390,14 @@ fn zsh_shell_quote(s: &str) -> String { if !s.is_empty() && s.chars().all(safe) { return s.to_string(); } - // Wrap in single quotes; close-open dance escapes any internal apostrophes. let escaped = s.replace('\'', "'\\''"); format!("'{escaped}'") } -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) @@ -413,3 +412,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 ffea288c..cec66574 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 336881a5..d8d06ec6 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() {