Skip to content

feat: enable wasm32 compilation by making xx crate conditional#570

Open
andreaTP wants to merge 2 commits intojdx:mainfrom
andreaTP:java-wasm-enablement
Open

feat: enable wasm32 compilation by making xx crate conditional#570
andreaTP wants to merge 2 commits intojdx:mainfrom
andreaTP:java-wasm-enablement

Conversation

@andreaTP
Copy link
Copy Markdown

Supersedes #561 this PR contains only the Rust-level changes needed for wasm32 support to avoid having to patch on top downstream.

The xx crate doesn't support wasm32. This makes xx a conditional dependency (cfg(not(target_arch = "wasm32"))) and replaces its usages (xx::regex!, xx::file, xx::process) with std equivalents. sh execution is disabled on wasm (returns an error).

CI now includes a minimal cargo build --target wasm32-wasip1 check to prevent regressions and this is all I need to bundle and ship a usage4j library usable and embeddable in Java.

Please @jdx have a look and let me know your thoughts 🙏

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 8.00000% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.56%. Comparing base (62936a2) to head (be98ac4).

Files with missing lines Patch % Lines
lib/src/docs/markdown/renderer.rs 16.66% 6 Missing and 9 partials ⚠️
lib/src/spec/mod.rs 0.00% 4 Missing and 7 partials ⚠️
cli/src/cli/complete_word.rs 0.00% 3 Missing and 3 partials ⚠️
cli/src/cli/generate/markdown.rs 16.66% 2 Missing and 3 partials ⚠️
cli/src/cli/generate/fig.rs 0.00% 4 Missing ⚠️
cli/src/cli/generate/manpage.rs 0.00% 1 Missing and 3 partials ⚠️
lib/src/sh.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #570      +/-   ##
==========================================
- Coverage   79.04%   73.56%   -5.49%     
==========================================
  Files          48       48              
  Lines        7235     7361     +126     
  Branches     7235     7361     +126     
==========================================
- Hits         5719     5415     -304     
- Misses       1141     1255     +114     
- Partials      375      691     +316     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enables wasm32 compatibility by making the xx dependency conditional and replacing its utility functions with standard library and regex crate equivalents. The review feedback highlights a performance regression where regexes previously compiled at compile-time (via the xx::regex! macro) are now being recompiled on every function call. It is recommended to use std::sync::LazyLock to define these regexes as static variables to restore efficient execution.

let stdout = sh(&run)?;
// trace!("stdout: {stdout}");
let re = regex!(r"[^\\]:");
let re = Regex::new(r"[^\\]:").unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Compiling this regex on every call to complete_arg is inefficient. The original xx::regex! macro compiled it at compile-time. To retain similar performance, you should compile this regex only once. You can use std::sync::LazyLock (which is already imported) to define it as a static variable at the module level.

For example:

static COMPLETION_DESC_SPLIT_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^\\]:").unwrap());

Then, you can use it here like this:

let re = &*COMPLETION_DESC_SPLIT_RE;
// or directly
match COMPLETION_DESC_SPLIT_RE.find(l) //...

}
// replace '<' with '&lt;' but not inside code blocks
xx::regex!(r"(`[^`]*`)|(<)")
Regex::new(r"(`[^`]*`)|(<)").unwrap()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Compiling this regex for every line being processed is inefficient. The original xx::regex! macro avoided this by compiling at compile-time. To maintain performance, you should compile this regex only once. You can use std::sync::LazyLock to define it as a static variable at the module level.

For example:

use std::sync::LazyLock;
use regex::Regex;

static ESCAPE_MD_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(`[^`]*`)|(<)").unwrap());

Then you can use it here like ESCAPE_MD_RE.replace_all(...).

let path_re =
regex!(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/");
tera.register_function("source_code_link", |args: &HashMap<String, tera::Value>| {
let path_re = Regex::new(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/").unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This regex is compiled every time render is called. Since it's a constant pattern, it's more efficient to compile it once using std::sync::LazyLock and reuse the compiled Regex object across calls. This avoids the performance overhead of repeated regex compilation.

For example, at the module level:

use std::sync::LazyLock;
use regex::Regex;

static SOURCE_CODE_PATH_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/").unwrap());

Then you can use SOURCE_CODE_PATH_RE directly in your register_function closure.

if full.starts_with("#!") {
let usage_regex = xx::regex!(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])");
if full.lines().any(|l| usage_regex.is_match(l)) {
let usage_re = Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])").unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This regex is compiled on every call to split_script. It's more efficient to compile it once using std::sync::LazyLock and reuse it. The original xx::regex! macro handled this by compiling at compile-time.

For example, at the module level:

use std::sync::LazyLock;
use regex::Regex;

static USAGE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(?:#|//|::)(?:USAGE| ?\\[USAGE\\])").unwrap());

Then you can use USAGE_RE here.

Comment on lines +319 to +320
let usage_regex = Regex::new(r"^(?:#|//|::)(?:USAGE| ?\[USAGE\])(.*)$").unwrap();
let blank_comment_regex = Regex::new(r"^(?:#|//|::)\s*$").unwrap();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These regexes are compiled on every call to extract_usage_from_comments. For better performance, they should be compiled only once using std::sync::LazyLock and stored in static variables, similar to how xx::regex! worked. This will avoid performance degradation, especially since this function processes file contents line by line.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR enables wasm32 compilation by making the xx crate a conditional dependency (cfg(not(target_arch = "wasm32"))) and replacing its three usage surfaces — xx::regex!, xx::file, and xx::process — with stdlib equivalents or cfg-gated stubs. A CI step is added to verify the wasm32-wasip1 build doesn't regress.

Key changes:

  • xx moved to [target.'cfg(not(target_arch = \"wasm32\"))'.dependencies] in both cli/Cargo.toml and lib/Cargo.toml
  • All xx::regex!(…) calls replaced with std::sync::LazyLock<Regex> statics, preserving the original one-time compilation behaviour
  • xx::file::write replaced with create_dir_all(parent) + std::fs::write (addressing the previously-noted parent-directory creation gap)
  • xx::file::read_to_string replaced with std::fs::read_to_string (minor: loses file-path context in IO error messages)
  • sh execution returns an explicit "unsupported on wasm" error on wasm32 in both the lib and cli crates
  • UsageErr::XXError variant gated behind cfg(not(target_arch = \"wasm32\"))
  • CI matrix extended with a cargo build --target wasm32-wasip1 -p usage-cli step (transitively covers usage-lib)

Confidence Score: 5/5

Safe to merge — all previous P1 concerns (missing create_dir_all, regex recompilation) are addressed; only a minor P2 UX note remains about IO error messages losing file-path context.

The changes are structurally sound: cfg guards are consistent across Cargo.toml, source files, and error types; the LazyLock<Regex> refactor correctly restores one-time compilation; the create_dir_all addition fixes the parent-directory regression flagged in prior review; the wasm stub functions return meaningful errors. The sole remaining finding (P2) does not affect correctness.

lib/src/spec/mod.rs — minor: std::fs::read_to_string drops file-path context from IO error messages.

Important Files Changed

Filename Overview
.github/workflows/test.yml Adds a wasm32-wasip1 build-check step; since usage-cli depends on usage-lib, this transitively validates both crates.
cli/Cargo.toml Moves xx to a [target.'cfg(not(target_arch = "wasm32"))'.dependencies] section — correct and minimal change.
lib/Cargo.toml Same conditional-dependency treatment for xx in the lib crate — correct.
cli/src/cli/complete_word.rs Regex correctly promoted to LazyLock; sh function split into cfg-gated variants with consistent wasm stub returning an unsupported-operation error.
cli/src/cli/generate/fig.rs Replaces xx::file::write with create_dir_all + std::fs::write; correctly handles empty parent paths since Rust's create_dir_all("") is a no-op.
cli/src/cli/generate/manpage.rs Same xx::file::writecreate_dir_all + std::fs::write replacement as fig.rs — correct.
cli/src/cli/generate/markdown.rs Same xx::file::writecreate_dir_all + std::fs::write replacement — correct.
lib/src/docs/markdown/renderer.rs All xx::regex! usages replaced with LazyLock<Regex> statics — one-time compilation preserved, no regressions.
lib/src/error.rs Gates XXError variant behind cfg(not(target_arch = "wasm32")) — correct; IO variant remains available for the wasm stub to use.
lib/src/sh.rs Clean cfg split: non-wasm32 keeps existing logic; wasm32 returns UsageErr::IO(Unsupported) — appropriate and symmetric with the cli stub.
lib/src/spec/mod.rs Replaces xx::file::read_to_string and xx::regex!; regex correctly promoted to LazyLock, but std::fs::read_to_string drops file-path context from IO errors (minor UX regression).
cli/src/lib.rs Adds #[cfg(not(target_arch = "wasm32"))] guard to extern crate xx — correct and minimal.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[compile target] -->|wasm32| B[xx excluded\nCargo.toml cfg guard]
    A -->|non-wasm32| C[xx included\nnormal path]
    B --> D[sh stub\nreturns Unsupported error]
    B --> E[std::fs::read_to_string\nfor file reads]
    B --> F[LazyLock<Regex>\nfor pattern matching]
    B --> G[std::fs::create_dir_all\n+ std::fs::write]
    C --> H[xx::process::check_status\nfor sh execution]
    C --> I[std::fs::read_to_string\nfor file reads]
    C --> J[LazyLock<Regex>\nfor pattern matching]
    C --> K[std::fs::create_dir_all\n+ std::fs::write]
    D --> L[UsageErr::IO Unsupported\nor miette error]
    H --> M[XXResult / miette error]
Loading

Reviews (2): Last reviewed commit: "[autofix.ci] apply automated fixes" | Re-trigger Greptile

Make the xx crate dependency conditional on non-wasm32 targets and
replace xx::regex!, xx::file, and xx::process usages with std
equivalents (LazyLock<Regex>, std::fs, std::process) so the codebase
compiles for wasm32-wasip1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@andreaTP andreaTP force-pushed the java-wasm-enablement branch from e23f7b7 to 3e7327c Compare March 31, 2026 17:31
@andreaTP
Copy link
Copy Markdown
Author

All comments should be addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant