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
28 changes: 26 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ jobs:
- name: Install latest stable Rust toolchain
run: rustup toolchain install stable --profile minimal --component clippy,rustfmt,llvm-tools-preview

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 20
cache: npm

- name: Install JavaScript development dependencies
run: npm ci

- name: Install WebAssembly binding generator
uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4
with:
tool: wasm-bindgen-cli@0.2.127
fallback: none

- name: Check formatting
run: cargo +stable fmt --check

Expand All @@ -53,10 +68,19 @@ jobs:
- name: Validate standalone SVG snapshots
run: python3 scripts/validate-svg.py

- name: Build pure engine for WebAssembly
- name: Build browser WebAssembly package
run: |
rustup target add wasm32-unknown-unknown wasm32-wasip1 --toolchain stable
cargo +stable build -p stack-engine --target wasm32-unknown-unknown --locked
npm run build:wasm

- name: Test native and browser operation parity
run: npm test

- name: Check browser package types
run: npm run typecheck

- name: Verify browser package contents
run: npm run pack:check

- name: Install WebAssembly test runtime
uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/target/
/node_modules/
/packages/engine/dist/
103 changes: 102 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/stack-engine", "crates/stack-formatter"]
members = ["crates/stack-engine", "crates/stack-engine-wasm", "crates/stack-formatter"]
resolver = "3"

[workspace.package]
Expand All @@ -9,6 +9,8 @@ license = "Apache-2.0"
repository = "https://github.com/stack-sh/engine"

[workspace.dependencies]
serde = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151"
stack-compiler = { git = "https://github.com/stack-sh/compiler.git", rev = "17a0abe9c35e641761ff08fdf59b29a42828d9fd" }
stack-formatter = { path = "crates/stack-formatter" }
stack-theme = { git = "https://github.com/stack-sh/theme.git", rev = "ed6c500762fc9ccffc8777172ac672a716dcd916" }
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

`stack-sh/engine` is the pure Rust execution engine for Stack architecture diagrams.

The workspace now provides canonical Stack source formatting, the pure `stack-engine` operation facade, deterministic theme-aware scene layout and orthogonal edge routing, and safe standalone SVG rendering. A WebAssembly adapter remains planned work.
The workspace provides canonical Stack source formatting, the pure `stack-engine` operation facade, deterministic theme-aware scene layout and orthogonal edge routing, safe standalone SVG rendering, and a typed browser WebAssembly adapter.

## Workspace

- `stack-engine`: operation/output boundary, theme and icon fallback resolution, deterministic scene layout, edge routing, validation beyond the compiler stage, and standalone SVG rendering;
- `stack-formatter`: comment-preserving canonical formatting for Stack source files (implemented);
- a WebAssembly adapter exposing the same pure operations to browser consumers.
- `stack-engine-wasm` and npm `@stack-sh/engine`: a thin browser adapter exposing the same pure operations and portable result model.

The native CLI will link the Rust engine directly. Web clients will use the WASM adapter. Shared fixtures will verify that both targets produce equivalent diagnostics, formatted source, and SVG output.

Expand All @@ -27,7 +27,14 @@ cargo test --workspace
STACK_SPECIFICATION_DIR=../specification cargo test -p stack-formatter --features conformance --test conformance
STACK_SPECIFICATION_DIR=../specification cargo test -p stack-engine --features conformance
python3 scripts/validate-svg.py
cargo build -p stack-engine --target wasm32-unknown-unknown
rustup target add wasm32-unknown-unknown wasm32-wasip1
cargo build -p stack-engine-wasm --target wasm32-unknown-unknown
wasm-bindgen --version
npm ci
npm run build:wasm
npm test
npm run typecheck
npm run pack:check
CARGO_TARGET_WASM32_WASIP1_RUNNER=wasmtime cargo test -p stack-engine --lib --target wasm32-wasip1 cross_target_numeric_fixture
cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
Expand All @@ -40,13 +47,16 @@ cargo doc --workspace --no-deps

The renderer emits fixed-dimension standalone SVG with embedded catalog icons, local marker references, escaped authored text, accessible title and description metadata, and no script, event handler, external URL, host font measurement, or runtime I/O. Canonical SVG snapshots are byte-stable and parsed by `scripts/validate-svg.py`; set `UPDATE_STACK_SNAPSHOTS=1` only when intentionally regenerating them. CI also executes one exact numeric geometry fixture in both the native suite and a WASI build.

The npm package exports synchronous `format`, `check`, and `render` functions after asynchronous module initialization. Each operation accepts `string | Uint8Array` and returns a specific typed result with camel-case metadata and portable diagnostics. Invalid UTF-8 remains a normal `STK1001` result. Unsupported JavaScript input types and internal operational failures throw at the adapter boundary. Shared fixtures compare complete native and WebAssembly results, including formatted source, diagnostics, SVG, and metadata. Artifact validation audits WebAssembly imports and package contents; browser consumers retain responsibility for loading the module and performing any DOM, filesystem, network, or clock work.

## Architecture

- [`docs/decisions/0001-build-the-formatter-from-compiler-models.md`](./docs/decisions/0001-build-the-formatter-from-compiler-models.md)
- [`docs/decisions/0002-use-a-pure-versioned-engine-facade.md`](./docs/decisions/0002-use-a-pure-versioned-engine-facade.md)
- [`docs/decisions/0003-use-integer-ranked-scene-layout.md`](./docs/decisions/0003-use-integer-ranked-scene-layout.md)
- [`docs/decisions/0004-route-orthogonal-edges-on-a-visibility-grid.md`](./docs/decisions/0004-route-orthogonal-edges-on-a-visibility-grid.md)
- [`docs/decisions/0005-serialize-safe-standalone-svg.md`](./docs/decisions/0005-serialize-safe-standalone-svg.md)
- [`docs/decisions/0006-expose-one-typed-browser-wasm-adapter.md`](./docs/decisions/0006-expose-one-typed-browser-wasm-adapter.md)
- [`docs/dependency-audit.md`](./docs/dependency-audit.md)

## Licensing
Expand Down
12 changes: 11 additions & 1 deletion THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
| `itoa` | `1.0.18` | MIT OR Apache-2.0 | <https://github.com/dtolnay/itoa> | Transitive runtime dependency of `serde_json`. |
| `memchr` | `2.8.3` | Unlicense OR MIT | <https://github.com/BurntSushi/memchr> | Transitive runtime dependency of `serde_json`. |
| `zmij` | `1.0.23` | MIT | <https://github.com/dtolnay/zmij> | Transitive runtime dependency of `serde_json`. |
| `wasm-bindgen` / `wasm-bindgen-shared` | `0.2.127` | MIT OR Apache-2.0 | <https://github.com/wasm-bindgen/wasm-bindgen> | JavaScript ABI and generated glue shipped by `@stack-sh/engine`. |
| `js-sys` | `0.3.104` | MIT OR Apache-2.0 | <https://github.com/wasm-bindgen/wasm-bindgen/tree/main/crates/js-sys> | Typed-array input and plain JavaScript result objects; built without default features. |
| `cfg-if` | `1.0.4` | MIT OR Apache-2.0 | <https://github.com/alexcrichton/cfg-if> | Transitive runtime dependency of `wasm-bindgen` and `js-sys`. |
| `once_cell` | `1.21.4` | MIT OR Apache-2.0 | <https://github.com/matklad/once_cell> | Transitive runtime dependency of `wasm-bindgen`. |

## Build-only dependencies

Expand All @@ -21,7 +25,13 @@
| `quote` | `1.0.47` | MIT OR Apache-2.0 | <https://github.com/dtolnay/quote> | Transitive procedural-macro build dependency. |
| `syn` | `3.0.4` | MIT OR Apache-2.0 | <https://github.com/dtolnay/syn> | Transitive procedural-macro build dependency. |
| `unicode-ident` | `1.0.24` | (MIT OR Apache-2.0) AND Unicode-3.0 | <https://github.com/dtolnay/unicode-ident> | Transitive procedural-macro build dependency. |
| `wasm-bindgen-macro` / `wasm-bindgen-macro-support` | `0.2.127` | MIT OR Apache-2.0 | <https://github.com/wasm-bindgen/wasm-bindgen> | Procedural macro and support code used to build the browser adapter. |
| `bumpalo` | `3.20.3` | MIT OR Apache-2.0 | <https://github.com/fitzgen/bumpalo> | Transitive build dependency of `wasm-bindgen-macro-support`. |
| `rustversion` | `1.0.23` | MIT OR Apache-2.0 | <https://github.com/dtolnay/rustversion> | Transitive build dependency of `wasm-bindgen`. |
| `syn` | `2.0.119` | MIT OR Apache-2.0 | <https://github.com/dtolnay/syn> | Transitive procedural-macro build dependency of `wasm-bindgen`. |
| `wasm-bindgen-cli` | `0.2.127` | MIT OR Apache-2.0 | <https://github.com/wasm-bindgen/wasm-bindgen> | Version-matched build tool; not shipped in the npm package. |
| `typescript` | `7.0.2` | Apache-2.0 | <https://github.com/microsoft/TypeScript> | Type-check tool; not shipped in the npm package. |

No third-party asset is bundled in a Stack Engine distribution yet.
No third-party visual asset is bundled in a Stack Engine distribution. The npm package includes this inventory and the Apache-2.0, MIT, and Unicode-3.0 license texts required by its compiled dependency choices.

Before publishing a native library, binary-derived artifact, or WASM package, this inventory must list the shipped dependencies and assets, their pinned versions, exact licenses, required license texts, attribution, modifications, and redistribution conditions. Build-only dependencies that are not shipped should be distinguished from distributed code.
30 changes: 30 additions & 0 deletions crates/stack-engine-wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "stack-engine-wasm"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Browser WebAssembly adapter for Stack diagram operations"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
serde.workspace = true
stack-engine = { path = "../stack-engine" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "=0.3.104", default-features = false }
wasm-bindgen = "=0.2.127"

[dev-dependencies]
serde_json.workspace = true

[lints.clippy]
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
unreachable = "deny"
unwrap_used = "deny"
59 changes: 59 additions & 0 deletions crates/stack-engine-wasm/examples/native-parity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::error::Error;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use stack_engine_wasm::{CheckResult, FormatResult, RenderResult};

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FixtureCase {
name: String,
input: FixtureInput,
}

#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
enum FixtureInput {
String { value: String },
Bytes { value: Vec<u8> },
}

impl FixtureInput {
fn bytes(&self) -> &[u8] {
match self {
Self::String { value } => value.as_bytes(),
Self::Bytes { value } => value,
}
}
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FixtureOutput {
name: String,
format: FormatResult,
check: CheckResult,
render: RenderResult,
}

fn main() -> Result<(), Box<dyn Error>> {
let path = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.ok_or("usage: native-parity <fixture-path>")?;
let cases = serde_json::from_slice::<Vec<FixtureCase>>(&std::fs::read(path)?)?;
let outputs = cases
.into_iter()
.map(|case| {
let source = case.input.bytes();
Ok(FixtureOutput {
name: case.name,
format: stack_engine_wasm::format_bytes(source)?,
check: stack_engine_wasm::check_bytes(source)?,
render: stack_engine_wasm::render_bytes(source)?,
})
})
.collect::<Result<Vec<_>, stack_engine::OperationalError>>()?;
println!("{}", serde_json::to_string(&outputs)?);
Ok(())
}
Loading