Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ca28128
Clippy: fix indentation in doc comments
yozhgoor Apr 4, 2026
e103a8c
Deprecate `default_dist_dir` in favor of `default_dir_dir_debug` and …
yozhgoor Apr 4, 2026
b921f1d
Deprecate `run_in_workspace` in favor of `use_workspace_root` and `us…
yozhgoor Apr 4, 2026
fcb54d5
Add missing logger initialization in example
yozhgoor Apr 4, 2026
43a5173
Change `command` and `arg`/`args` implementation
yozhgoor Apr 5, 2026
77cdd86
Remove deprecated functions
yozhgoor Apr 5, 2026
435d476
self-review
yozhgoor Apr 5, 2026
0be34c7
Update `README.md`
yozhgoor Apr 5, 2026
af672b2
Fix CI
yozhgoor Apr 5, 2026
b870cb3
Clean up documentation
yozhgoor Apr 5, 2026
b0a49ab
Fix intra doc links
yozhgoor Apr 5, 2026
56339f1
Remove `run_in_workspace` from `Dist`
yozhgoor Apr 5, 2026
943c9fb
Change `dist_dir_path` into `dist_dir`
yozhgoor Apr 5, 2026
7f6aea1
Add a `xtask` method to `DevServer`
yozhgoor Apr 5, 2026
e64f724
Change `dist_dir_path` into `dist_dir` in `dist.rs` too
yozhgoor Apr 5, 2026
982f040
Rename `Dist::run` into `Dist::build`
yozhgoor Apr 5, 2026
1b6dbb9
Update `README.md`
yozhgoor Apr 5, 2026
ef26784
Change `static_dir_path` into `assets_dir`
yozhgoor Apr 7, 2026
f667a80
Rename `static` directory into `assets` in `examples/demo`
yozhgoor Apr 7, 2026
04ea1f9
Improve module documentation for DevServer
yozhgoor Apr 7, 2026
5832a26
Move `default_dist_dir_debug` and `default_dist_dir_release` as assoc…
yozhgoor Apr 7, 2026
0cce0a1
Add the `env` and `envs` methods on `DevServer`
yozhgoor Apr 7, 2026
b290c21
Add `pre` and `post` methods on `DevServer`
yozhgoor Apr 7, 2026
60063ed
Clean up
yozhgoor Apr 7, 2026
bcf09c8
Self review
yozhgoor Apr 8, 2026
75e6cab
Fix intra doc links for `DevServer`
yozhgoor Apr 10, 2026
00cc06a
Use package root instead of workspace root for the assets directory
yozhgoor Apr 10, 2026
f19c18a
`arg`/`args` and `env`/`envs` now return an error if no command avail…
yozhgoor Apr 10, 2026
35b37ad
Clippy
yozhgoor Apr 10, 2026
059254a
`DevServer`: fix doc about `watch` and error propagation
yozhgoor Apr 10, 2026
e06ce35
Add a `post` method on `DevServer`
yozhgoor Apr 10, 2026
2d8f042
Fix doc comments and update `README.md`
yozhgoor Apr 10, 2026
01da9d1
fix(sass): move create_dir_all after file check and guard dest.parent()
Copilot Apr 11, 2026
3fc2ed0
Fix doc comment placement on `sass_options`
cecton Apr 11, 2026
40a0ac6
Return `Utf8PathBuf` from `default_debug_dir` and `default_release_dir`
cecton Apr 11, 2026
a9470da
Rename `DevServer::not_found` to `not_found_path`
cecton Apr 11, 2026
ee1abae
Replace cfg macros with inline `#[cfg(...)]` attributes
cecton Apr 11, 2026
eabfb85
Change `DevServer::command` to accept `process::Command`
cecton Apr 11, 2026
2174586
Restore `arg`/`args`/`env`/`envs`, add `cargo`, change panics
cecton Apr 11, 2026
1ba3931
Fix auto-discovery default directory from `public` to `assets`
cecton Apr 11, 2026
cda5fc1
Add "Why xtask-wasm?" section to README and crate docs
cecton Apr 11, 2026
661ce08
Add `pres`/`posts` to DevServer for adding multiple pre/post commands…
cecton Apr 11, 2026
66c0f5b
Introduce `Processor` trait for pre/post commands
cecton Apr 11, 2026
b08c640
Rename `pre_commands`/`post_commands` to `pre_processors`/`post_proce…
cecton Apr 11, 2026
628952a
Document the `Processor` trait in README and crate-level docs
cecton Apr 11, 2026
f1627c7
Rename `Processor`/`pre_processors`/`post_processors` to `Hook`/`pre_…
cecton Apr 11, 2026
f891195
Add `Transformer` trait to `Dist` for extensible asset processing
cecton Apr 11, 2026
04d95d3
Update docs: Processor -> Hook, document Transformer and SassTransformer
cecton Apr 11, 2026
1384510
Move SassTransformer into its own src/sass.rs module
cecton Apr 11, 2026
3db53b9
Make SassTransformer opt-in rather than seeded in Dist::default
cecton Apr 11, 2026
a284a22
docs(Hook): replace DevServer::default() with idiomatic clap-parsed e…
cecton Apr 11, 2026
2f22b8c
docs(Transformer): replace Dist::default() with idiomatic clap-parsed…
cecton Apr 11, 2026
0e0d902
docs(SassTransformer): replace Dist::default() with idiomatic clap-pa…
cecton Apr 11, 2026
5c46359
fix(SassTransformer): propagate SASS compilation error instead of pan…
cecton Apr 11, 2026
1b9c0e6
docs(DevServer): improve watch field doc comment
cecton Apr 11, 2026
3ead5ac
feat(Dist): add optimize_wasm() builder to integrate WasmOpt into the…
cecton Apr 11, 2026
b7e192d
fix(Dist): skip wasm-opt for debug builds
cecton Apr 11, 2026
7596501
fix(Dist): propagate transformer errors in copy_assets
cecton Apr 11, 2026
1b11aaf
fix(Dist): always bail when assets directory does not exist
cecton Apr 11, 2026
45a446d
fix(Dist): remove unused SassTransformer import in dist.rs
cecton Apr 11, 2026
c493b80
fix(Dist): skip assets copy gracefully when directory does not exist
cecton Apr 11, 2026
f924336
Fix small issue in run_example
cecton Apr 11, 2026
68c34f5
docs: add CHANGELOG.md and link it from README and crate docs
cecton Apr 11, 2026
738b589
chore: include CHANGELOG.md in published crate
cecton Apr 11, 2026
ca12cb3
feat(WasmOpt): derive Debug
cecton Apr 11, 2026
face315
fix(docs): mark wasm-opt doc-test as ignored (requires wasm-opt feature)
cecton Apr 11, 2026
c083be1
clippy
cecton Apr 11, 2026
a8b191d
feat: derive Debug on Request and SassTransformer
cecton Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `DevServer::xtask(name)` and `DevServer::cargo(subcommand)` convenience
builders for setting the main watch command. These replace the old implicit
behavior where `arg()` would silently create an xtask command if none was set.
- `DevServer::arg()`, `DevServer::args()`, `DevServer::env()`,
`DevServer::envs()` restored as builder methods on `DevServer` for passing
extra arguments/environment to the `xtask()` and `cargo()` shorthands (which
build the command internally). These now panic with a clear message if called
before a command is set — a programmer error, not a runtime condition.
- `DevServer::dist_dir(path)` builder to explicitly set the directory served by
the dev server (previously had to be passed to `start()`).
- `Hook` trait for `DevServer` pre/post commands. Lets you construct a
`process::Command` with access to the final server configuration (e.g.
resolved `port`, `dist_dir`). A blanket impl is provided for
`process::Command` itself.
- `Transformer` trait for `Dist` asset processing. Transformers are tried in
order per file; the first to return `Ok(true)` claims the file, unclaimed
files are plain-copied. Errors are propagated immediately via `?`.
- `Dist::transformer(impl Transformer)` builder to register asset transformers.
- `SassTransformer` struct (behind the `sass` feature) implementing `Transformer`
to compile SASS/SCSS files to CSS.
- `Dist::optimize_wasm(WasmOpt)` builder (behind the `wasm-opt` feature) to
integrate wasm-opt directly into the `build()` pipeline. Automatically skipped
for debug builds.
- `Dist::default_debug_dir()` and `Dist::default_release_dir()` associated
functions returning `camino::Utf8PathBuf`.
- "Why xtask-wasm?" section added to the README and crate-level documentation.

### Changed

- **BREAKING** `DevServer::start()` no longer takes a `dist_dir` path argument.
The directory is inferred from `Dist::default_debug_dir()` or set via the new
`DevServer::dist_dir()` builder.
- **BREAKING** `DevServer::command()` now accepts a `process::Command` instead
of a program name string, consistent with `pre()` and `post()`.
- **BREAKING** `DevServer::not_found()` renamed to `DevServer::not_found_path()`.
- **BREAKING** `Dist::run()` renamed to `Dist::build()`.
- **BREAKING** `Dist::dist_dir_path()` renamed to `Dist::dist_dir()`.
- **BREAKING** `Dist::static_dir_path()` renamed to `Dist::assets_dir()`. The
assets directory is now auto-discovered at `<package_root>/assets` when not
explicitly set. The copy step is silently skipped (with a `log::debug!`) if
the directory does not exist, allowing users with custom asset pipelines to
call `build()` without a pre-existing assets directory on disk.
- **BREAKING** `run_in_workspace` field and `use_workspace_root()` /
`use_current_dir()` methods removed from `Dist`. The build command always runs
from the workspace root, which was the correct value in virtually all
use-cases. The `false` branch was unreliable anyway since Cargo crate
resolution requires being inside the workspace.
- **BREAKING** `SassTransformer` is now opt-in. Previously `Dist::default()`
would auto-seed a `SassTransformer` when the `sass` feature was enabled,
meaning any user adding their own `SassTransformer` would silently end up with
two in the pipeline. Use `.transformer(SassTransformer::default())` to opt in.
- **BREAKING** `Request::dist_dir_path` field renamed to `Request::dist_dir`.
- `Dist::default_debug_dir()` and `Dist::default_release_dir()` now return an
owned `camino::Utf8PathBuf` instead of `&'static camino::Utf8Path`, removing
the `lazy_static` machinery and the `.as_std_path().to_path_buf()` boilerplate
at call sites.
- `walkdir` promoted from a `sass`-feature-gated dependency to a regular
dependency (used unconditionally by `copy_assets`).
- `cfg_not_wasm32!` / `cfg_wasm32!` / `cfg_sass!` / `cfg_wasm_opt!` /
`cfg_run_example!` macros removed. These wrapped `mod` declarations inside
macro bodies, making those modules invisible to `cargo fmt` and silently
skipping format checks on the bulk of the codebase. Replaced with plain
`#[cfg(...)]` attributes.
- MSRV bumped to 1.88.
- `run_example` macro: `static_dir` argument renamed to `assets_dir`.
- `run_example` macro: default `index.html` is no longer generated when
`app_name` is set. The default template hardcodes `app.js`/`app_bg.wasm`;
with a custom app name those filenames are wrong. Users setting `app_name`
should provide their own `index.html` via the `index` argument or in their
assets directory.

### Removed

- **BREAKING** Free functions `default_dist_dir(release: bool)`,
`default_dist_dir_debug()`, and `default_dist_dir_release()` removed in favor
of `Dist::default_debug_dir()` and `Dist::default_release_dir()`.
- **BREAKING** `Dist::sass_options()` removed. Configure SASS compilation
options via `SassTransformer { options: ... }` passed to `Dist::transformer()`.

### Fixed

- `SassTransformer`: SASS compilation errors are now propagated as `Err` instead
of panicking.
- `copy_assets`: transformer errors are now propagated immediately. The previous
iterator chain used `find()` with `map_or(false, ...)` which treated `Err` as
`false` and silently fell through to the plain-copy fallback.
- Auto-discovery was looking for a `public/` directory while the rest of the
codebase used `assets`, silently breaking auto-discovery for anyone following
the documented naming convention.

[Unreleased]: https://github.com/rustminded/xtask-wasm/compare/v0.5.3...HEAD
11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ description = "Customizable subcommands to build your Wasm projects using xtask.
homepage = "https://github.com/rustminded/xtask-wasm"
repository = "https://github.com/rustminded/xtask-wasm"
documentation = "https://docs.rs/xtask-wasm"
rust-version = "1.82"
rust-version = "1.88"
readme = "README.md"
categories = ["development-tools::build-utils"]
keywords = ["wasm", "cli"]
include = ["src/**/*.rs", "README.md", "LICENSE.Apache-2.0", "LICENSE.MIT"]
include = ["src/**/*.rs", "README.md", "CHANGELOG.md", "LICENSE.Apache-2.0", "LICENSE.MIT"]

[features]
run-example = ["xtask-wasm-run-example", "console_error_panic_hook", "wasm-bindgen", "env_logger"]
sass = ["sass-rs", "walkdir"]
sass = ["sass-rs"]
wasm-opt = ["binary-install"]

[dependencies]
Expand All @@ -33,7 +33,7 @@ fs_extra = "1.2.0"
lazy_static = "1.4.0"
log = "0.4.14"
sass-rs = { version = "0.2.2", optional = true }
walkdir = { version = "2.3.2", optional = true }
walkdir = "2.3.2"
# NOTE: we don't depend on this crate but we need to activate this feature otherwise it's super slow
walrus = { version = "0.23.0", features = ["parallel"] }
wasm-bindgen-cli-support = "0.2.100"
Expand All @@ -42,6 +42,9 @@ xtask-watch = "0.3.2"
[target.'cfg(unix)'.dependencies]
libc = "0.2.112"

[dev-dependencies]
env_logger = { version = "0.11.6" }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
Expand Down
108 changes: 86 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,51 @@
[deps-url]: https://deps.rs/repo/github/rustminded/xtask-wasm
[licenses-badge]: https://img.shields.io/crates/l/xtask-wasm

**[Changelog](CHANGELOG.md)**

<!-- cargo-rdme start -->

This crate aims to provide an easy and customizable way to help you build
Wasm projects by extending them with custom subcommands, based on the
[`xtask` concept](https://github.com/matklad/cargo-xtask/), instead of using
external tooling like [`wasm-pack`](https://github.com/rustwasm/wasm-pack).

**[Changelog](https://github.com/rustminded/xtask-wasm/blob/main/CHANGELOG.md)**

## Why xtask-wasm?

### No external tools to install

`wasm-pack` and `trunk` are separate binaries that must be installed outside
of Cargo — via `cargo install`, a shell script, or a system package manager.
This means every contributor and every CI machine needs an extra installation
step, and there is no built-in guarantee that everyone is running the same
version.

With xtask-wasm, `cargo xtask` is all you need. The build tooling is a
regular Cargo dependency, versioned in your `Cargo.lock` and reproduced
exactly like every other dependency in your project.

### `wasm-bindgen` version is always in sync

This is the most common source of pain with `wasm-pack` and `trunk`: the
`wasm-bindgen` CLI tool version must exactly match the `wasm-bindgen` library
version declared in your `Cargo.toml`. When they drift — after a `cargo
update`, a fresh clone, or a CI cache invalidation — you get a cryptic error
at runtime rather than a clear compile-time failure.

xtask-wasm uses [`wasm-bindgen-cli-support`](https://crates.io/crates/wasm-bindgen-cli-support)
as a library dependency. The version is pinned in your `Cargo.lock` alongside
your `wasm-bindgen` library dependency and kept in sync automatically — no
manual version matching required.

### Fully customizable

Because the build process is plain Rust code living inside your workspace,
you can extend, replace or wrap any step. `wasm-pack` and `trunk` are
opaque binaries driven by configuration files; xtask-wasm gives you the full
build logic as code, under your control.

## Setup

The best way to add xtask-wasm to your project is to create a workspace
Expand Down Expand Up @@ -113,6 +151,22 @@ They all implement [`clap::Parser`](https://docs.rs/clap/latest/clap/trait.Parse
allowing them to be added easily to an existing CLI implementation and are
flexible enough to be customized for most use-cases.

The pre and post hooks of [`DevServer`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.DevServer.html)
accept any type implementing the
[`Hook`](https://docs.rs/xtask-wasm/latest/xtask_wasm/trait.Hook.html) trait.
This lets you construct a [`process::Command`](https://doc.rust-lang.org/std/process/struct.Command.html) based on the server's final configuration
— for example, to pass the resolved `dist_dir` or `port` as arguments to an external tool.
A blanket implementation is provided for [`process::Command`](https://doc.rust-lang.org/std/process/struct.Command.html) itself, so no changes are
needed for simple use-cases.

Asset files copied by [`Dist`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.Dist.html)
can be processed by types implementing the
[`Transformer`](https://docs.rs/xtask-wasm/latest/xtask_wasm/trait.Transformer.html) trait.
Transformers are tried in order for each file; the first to return `Ok(true)` claims the file,
while unclaimed files are copied verbatim. When the `sass` feature is enabled,
[`SassTransformer`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.SassTransformer.html)
is available to compile SASS/SCSS files to CSS.

You can find further information for each type at their documentation level.

## Examples
Expand All @@ -121,7 +175,7 @@ You can find further information for each type at their documentation level.

```rust
use std::process::Command;
use xtask_wasm::{anyhow::Result, clap, default_dist_dir};
use xtask_wasm::{anyhow::Result, clap};

#[derive(clap::Parser)]
enum Opt {
Expand All @@ -132,18 +186,20 @@ enum Opt {


fn main() -> Result<()> {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
Comment thread
yozhgoor marked this conversation as resolved.

let opt: Opt = clap::Parser::parse();

match opt {
Opt::Dist(dist) => {
log::info!("Generating package...");

dist
.dist_dir_path(default_dist_dir(false))
.static_dir_path("my-project/static")
.assets_dir("my-project/assets")
.app_name("my-project")
.run_in_workspace(true)
.run("my-project")?;
.build("my-project")?;
}
Opt::Watch(watch) => {
log::info!("Watching for changes and check...");
Expand All @@ -156,36 +212,34 @@ fn main() -> Result<()> {
Opt::Start(dev_server) => {
log::info!("Starting the development server...");

dev_server.arg("dist").start(default_dist_dir(false))?;
dev_server
.xtask("dist")
.start()?;
}
}

Ok(())
}
```

Note: this basic implementation uses `env_logger` and `log`. Add them to the `Cargo.toml` of
your `xtask` (or use your preferred logger).

### [`examples/demo`](https://github.com/rustminded/xtask-wasm/tree/main/examples/demo)

Provides a basic implementation of xtask-wasm to generate the web app
package, an "hello world" app using [Yew](https://yew.rs/). This example
demonstrates a simple directory layout and a customized dist process
that use the `wasm-opt` feature.
demonstrates a simple directory layout and a dist process that uses the
`wasm-opt` feature via [`Dist::optimize_wasm`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.Dist.html#method.optimize_wasm).

The available subcommands are:

* Build the web app package.
* Build and optimize the web app package (downloads
[`wasm-opt`](https://github.com/WebAssembly/binaryen#tools) if not cached).

```console
cargo xtask dist
```
* Build the web app package, download the
[`wasm-opt`](https://github.com/WebAssembly/binaryen#tools)
binary (currently using the 123 version) and optimize the Wasm generated by the dist
process.

```console
cargo xtask dist --optimize
```

* Build the web app package and watch for changes in the workspace root.

Expand Down Expand Up @@ -214,12 +268,22 @@ This command will run the code in `examples/run_example` using the development s
## Features

* `wasm-opt`: enable the
[`WasmOpt`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.WasmOpt.html) struct that helps
downloading and using [`wasm-opt`](https://github.com/WebAssembly/binaryen#tools) very
easily.
[`WasmOpt`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.WasmOpt.html) struct and
[`Dist::optimize_wasm`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.Dist.html#method.optimize_wasm)
for downloading and running [`wasm-opt`](https://github.com/WebAssembly/binaryen#tools)
automatically as part of the dist build. This is the recommended way to integrate wasm-opt —
no custom wrapper struct or manual path computation needed:

```rust
// requires the `wasm-opt` feature
dist.optimize_wasm(WasmOpt::level(1).shrink(2))
.build("my-project")?;
```

* `run-example`: a helper to run examples from `examples/` directory using a development
server.
* `sass`: allow the use of SASS/SCSS in your project.
server.
* `sass`: enable SASS/SCSS compilation via [`SassTransformer`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.SassTransformer.html).
Add it to your [`Dist`](https://docs.rs/xtask-wasm/latest/xtask_wasm/struct.Dist.html) with `.transformer(SassTransformer::default())`.

## Troubleshooting

Expand Down
2 changes: 1 addition & 1 deletion examples/demo/webapp/examples/run_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extern "C" {
fn log(message: &str);
}

#[xtask_wasm::run_example(static_dir = "webapp/static", app_name = "web_app")]
#[xtask_wasm::run_example(app_name = "web_app")]
fn run_app() {
log("Hello World!");
}
29 changes: 6 additions & 23 deletions examples/demo/xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,11 @@ struct Opt {

#[derive(clap::Parser)]
enum Command {
Dist(Build),
Dist(xtask_wasm::Dist),
Watch(xtask_wasm::Watch),
Start(xtask_wasm::DevServer),
}

#[derive(clap::Parser)]
struct Build {
/// Optimize the generated package using `wasm-opt`.
#[clap(long)]
optimize: bool,

#[clap(flatten)]
base: xtask_wasm::Dist,
}

fn main() -> Result<()> {
let opt: Opt = clap::Parser::parse();

Expand All @@ -34,20 +24,13 @@ fn main() -> Result<()> {
.init();

match opt.cmd {
Command::Dist(arg) => {
Command::Dist(dist) => {
log::info!("Generating package...");

let dist_result = arg
.base
.static_dir_path("webapp/static")
dist.assets_dir("webapp/assets")
.app_name("web_app")
.run("webapp")?;

if arg.optimize {
xtask_wasm::WasmOpt::level(1)
.shrink(2)
.optimize(dist_result.join("web_app.wasm"))?;
}
.optimize_wasm(xtask_wasm::WasmOpt::level(1).shrink(2))
.build("webapp")?;
}
Command::Watch(arg) => {
log::info!("Watching for changes and check...");
Expand All @@ -60,7 +43,7 @@ fn main() -> Result<()> {
Command::Start(arg) => {
log::info!("Starting the development server...");

arg.arg("dist").start(xtask_wasm::default_dist_dir(false))?;
arg.xtask("dist").start()?;
}
}

Expand Down
Loading
Loading