diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2344aff9..eaeea623 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 c636fc92..3d335138 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..3b008168 100644 --- a/cli/src/cli/generate/fig.rs +++ b/cli/src/cli/generate/fig.rs @@ -356,7 +356,10 @@ 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..dce5fbca 100644 --- a/cli/src/cli/generate/manpage.rs +++ b/cli/src/cli/generate/manpage.rs @@ -34,7 +34,10 @@ 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 999f83aa..934e2b15 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..9879afb5 100644 --- a/lib/src/docs/markdown/renderer.rs +++ b/lib/src/docs/markdown/renderer.rs @@ -2,9 +2,10 @@ 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 xx::regex; +use std::sync::LazyLock; #[derive(Debug, Clone)] pub struct MarkdownRenderer { @@ -88,22 +89,29 @@ impl MarkdownRenderer { return line.to_string(); } // replace '<' with '<' but not inside code blocks - xx::regex!(r"(`[^`]*`)|(<)") - .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() + { + 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() }) .join("\n"); 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..ec1879dd 100644 --- a/lib/src/spec/mod.rs +++ b/lib/src/spec/mod.rs @@ -13,12 +13,13 @@ 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 xx::file; +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,12 @@ 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 +318,12 @@ 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() {