Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions .claude/rules/wasm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# WASM Code Rules

## Never add `test` to wasm32 cfg guards

The cfg pattern `#[cfg(any(target_arch = "wasm32", test))]` is prohibited. It forces
native tests through the WASM-restricted Lua stdlib, which fails on Windows.

Correct pattern:
```rust
#[cfg(target_arch = "wasm32")]
// WASM-specific code (restricted Lua stdlib, synthetic io/os)

#[cfg(not(target_arch = "wasm32"))]
// Native code (full Lua stdlib via Lua::new())
```
8 changes: 7 additions & 1 deletion .claude/rules/xtask.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ paths:

| Command | Alias | Purpose |
|---------|-------|---------|
| `cargo xtask dev-setup` | `cargo dev-setup` | Install required dev tools (cargo-nextest, wasm-pack) |
| `cargo xtask dev-setup` | `cargo dev-setup` | Install required dev tools (cargo-nextest, wasm-bindgen-cli) |
| `cargo xtask lint` | — | Run custom lint checks |
| `cargo xtask verify` | — | Full project verification (build + tests for Rust and hub-client) |

## Dev tool version pinning

Dev tools whose versions must match Cargo.lock (e.g., `wasm-bindgen-cli`) are installed
via `cargo xtask dev-setup`, which reads the locked version automatically. Never hardcode
these versions in CI workflows or documentation — always use `cargo xtask dev-setup`.

## Adding a new subcommand

1. Create `crates/xtask/src/<name>.rs` with `pub fn run() -> Result<()>`
Expand Down
20 changes: 9 additions & 11 deletions claude-notes/instructions/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@
- When choosing hex colors for CSS test assertions (`ensureCssRegexMatches`), use **non-condensable** 6-digit hex values. CSS minifiers shorten `#RRGGBB` to `#RGB` when each pair is a repeated digit (e.g., `#cc5500` → `#c50`). Break at least one pair to prevent this: `#cc5501` instead of `#cc5500`.
- Do not write tests that expect known-bad inputs. Instead, add a failing test, and create a beads task to handle the problem.

## WASM-Restricted Stdlib for Lua Tests
## Native vs WASM Lua Testing

Shortcode and filter tests always run against the WASM-restricted Lua stdlib
(`StdLib::COROUTINE | TABLE | STRING | UTF8 | MATH`), even on native CI. This is
enforced via `#[cfg(any(target_arch = "wasm32", test))]` guards in `shortcode.rs`
and `filter.rs`.
Native tests (`cargo nextest run`) use `Lua::new()` with the full C stdlib on all platforms.
This is the standard Lua environment — tests can use `io.open`, `os.time`, and all standard
library functions.

This means tests cannot use Lua standard libraries that are unavailable in WASM
(`package`, `debug`, or the real C-backed `io`/`os`). Instead, synthetic `io` and
`os` tables are registered from Rust, backed by the `SystemRuntime` abstraction.
WASM-specific code paths (restricted Lua stdlib, synthetic io/os modules) are tested
separately on the real `wasm32-unknown-unknown` target in CI. See `dev-docs/wasm.md` for
the WASM architecture and build details.

If a test needs file I/O, use `io.open()` (which goes through the runtime) or
`pandoc.system.read_file()` / `pandoc.system.write_file()`. These work identically
in tests and WASM because they both go through the runtime abstraction.
**Never add `test` to the `#[cfg(target_arch = "wasm32")]` guard.** This was a prior pattern
that caused Windows test failures. WASM coverage is provided by dedicated WASM tests in CI.

## End-to-End Testing for WASM Features

Expand Down
6 changes: 6 additions & 0 deletions crates/pampa/src/lua/dofile_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,13 @@ end
}
}

// This test requires register_wasm_dofile (which pushes/pops the script-dir
// stack around dofile calls). On native, the C Lua dofile doesn't interact
// with the stack, so resolve_path resolves against the top-level filter dir.
// The WASM dofile reimplementation provides this feature; adding it to native
// is tracked as a follow-up improvement.
#[tokio::test]
#[cfg_attr(not(target_arch = "wasm32"), ignore)]
async fn test_dofile_script_dir_stack() {
// Extension in /ext/ calls dofile("helpers/ui.lua"), and ui.lua calls
// quarto.utils.resolve_path("style.css") — should resolve to
Expand Down
4 changes: 2 additions & 2 deletions crates/pampa/src/lua/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ pub async fn apply_lua_filter(
// Create Lua state
// On WASM, we can't load all libraries (no package/io/os/debug support),
// so use a restricted set. On native, load everything for full compatibility.
#[cfg(any(target_arch = "wasm32", test))]
#[cfg(target_arch = "wasm32")]
let lua = {
use mlua::StdLib;
let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
Expand All @@ -143,7 +143,7 @@ pub async fn apply_lua_filter(
super::dofile_wasm::register_wasm_dofile(&lua, runtime.clone())?;
lua
};
#[cfg(not(any(target_arch = "wasm32", test)))]
#[cfg(not(target_arch = "wasm32"))]
let lua = Lua::new();

// Create mediabag for storing media items
Expand Down
4 changes: 2 additions & 2 deletions crates/pampa/src/lua/shortcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl LuaShortcodeEngine {
target_format: &str,
runtime: Arc<dyn SystemRuntime>,
) -> std::result::Result<Self, LuaShortcodeError> {
#[cfg(any(target_arch = "wasm32", test))]
#[cfg(target_arch = "wasm32")]
let lua = {
use mlua::StdLib;
let libs =
Expand All @@ -84,7 +84,7 @@ impl LuaShortcodeEngine {
.map_err(LuaShortcodeError::LuaError)?;
lua
};
#[cfg(not(any(target_arch = "wasm32", test)))]
#[cfg(not(target_arch = "wasm32"))]
let lua = Lua::new();

let mediabag = create_shared_mediabag();
Expand Down
69 changes: 43 additions & 26 deletions dev-docs/wasm.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
## Wasm builds
# WASM Architecture

## Overview

The `wasm-quarto-hub-client` crate builds the Quarto rendering engine (pampa + quarto-core)
as a WASM module for use in the hub-client web application. It targets
`wasm32-unknown-unknown` and uses `-Zbuild-std=std,panic_unwind` to rebuild the standard
library (required for Lua error handling via setjmp/longjmp → panic/catch_unwind).

## Build

The WASM module is built via `hub-client/scripts/build-wasm.js`, which runs:
1. `cargo build --target wasm32-unknown-unknown -Zbuild-std=std,panic_unwind`
2. `wasm-bindgen` CLI to generate JS glue code

From hub-client:
```bash
npm run build:all # Full build including WASM
```
cd crates/wasm-qmd-parser

# To work around this error, because Apple Clang doesn't work with wasm32-unknown-unknown?
# I believe this is not required on a Linux machine.
# Requires `brew install llvm`.
# https://github.com/briansmith/ring/issues/1824
# error: unable to create target: 'No available targets are compatible with triple "wasm32-unknown-unknown"'
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"

# To tell rustc to include our C shims located in `wasm-sysroot`, which we eventually compile into the project
# with `c_shim.rs`.
# https://github.com/tree-sitter/tree-sitter/discussions/1550#discussioncomment-8445285
#
# It also seems like we need to define HAVE_ENDIAN_H to tell tree-sitter we have `endian.h`
# as it doesn't seem to pick up on that automatically?
# https://github.com/tree-sitter/tree-sitter/blob/0be215e152d58351d2691484b4398ceff041f2fb/lib/src/portable/endian.h#L18
export CFLAGS_wasm32_unknown_unknown="-I$(pwd)/wasm-sysroot -Wbad-function-cast -Wcast-function-type -fno-builtin -DHAVE_ENDIAN_H"

# To just build the wasm-qmd-parser crate
# cargo build --target wasm32-unknown-unknown

# To build the wasm-pack bundle
# Note that you'll need `opt-level = "s"` in your `profile.dev` cargo profile
# otherwise you can get a "too many locals" error.
wasm-pack build --target web --dev

This project does **not** use wasm-pack (deprecated, rustwasm sunset Sep 2025).
The `wasm-bindgen-cli` version is pinned to match `Cargo.lock` and installed via
`cargo xtask dev-setup`.

## C Toolchain

Building for `wasm32-unknown-unknown` requires Clang with wasm32 support. The `cc` crate
invokes Clang to compile C dependencies (tree-sitter, Lua). Environment variables:

```bash
CC_wasm32_unknown_unknown=clang
CFLAGS_wasm32_unknown_unknown="-isystem <path>/wasm-sysroot -fno-builtin"
```

The wasm-sysroot at `crates/wasm-quarto-hub-client/wasm-sysroot/` provides minimal C
headers. The `-fno-builtin` flag is needed because debug-mode builds emit `__builtin_*`
intrinsic calls not present in the stub sysroot.

## Native vs WASM Testing

Native tests (`cargo nextest run`) use `Lua::new()` with the full C stdlib on all platforms.
WASM-specific code paths use `#[cfg(target_arch = "wasm32")]` guards — never
`#[cfg(any(target_arch = "wasm32", test))]` (see `.claude/rules/wasm.md`).

Hub-client integration tests (`npm run test:ci`) exercise the compiled WASM module through
the JavaScript API.
Loading