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
1 change: 0 additions & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ reviews:
# Skip generated and lock files entirely.
path_filters:
- "!**/routeTree.gen.ts"
- "!examples/simulator/server/ts-bindings/**"
- "!Cargo.lock"
- "!pixi.lock"
- "!ui/pnpm-lock.yaml"
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/license-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ jobs:
persist-credentials: false
- name: Check SPDX license headers
run: |
# ts-bindings are auto-generated by ts-rs from Rust structs
missing=()
while IFS= read -r file; do
if ! grep -q "SPDX-License-Identifier" "$file"; then
missing+=("$file")
fi
done < <(git ls-files -- '*.ts' '*.tsx' '*.rs' '*.proto' '*.css' \
':!examples/simulator/server/ts-bindings/')
done < <(git ls-files -- '*.ts' '*.tsx' '*.rs' '*.proto' '*.css')
Comment thread
johanpel marked this conversation as resolved.

if [[ ${#missing[@]} -gt 0 ]]; then
echo "The following files are missing an SPDX license header:"
Expand All @@ -37,5 +35,4 @@ jobs:
exit 1
fi

echo "All $(git ls-files -- '*.ts' '*.tsx' '*.rs' '*.proto' '*.css' \
':!examples/simulator/server/ts-bindings/' | wc -l | tr -d ' ') files have SPDX license headers."
echo "All $(git ls-files -- '*.ts' '*.tsx' '*.rs' '*.proto' '*.css' | wc -l | tr -d ' ') files have SPDX license headers."
97 changes: 97 additions & 0 deletions .github/workflows/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ on:
branches: [main]
paths:
- "ui/**"
- "**/*.rs"
- "**/Cargo.toml"
- "**/Cargo.lock"
- "**/*.proto"
- "pixi.toml"
- "pixi.lock"
- ".github/workflows/ui.yml"
pull_request:
paths:
- "ui/**"
- "**/*.rs"
- "**/Cargo.toml"
- "**/Cargo.lock"
- "**/*.proto"
- "pixi.toml"
- "pixi.lock"
- ".github/workflows/ui.yml"

permissions:
Expand Down Expand Up @@ -66,6 +78,7 @@ jobs:
with:
cache: true
- run: pixi run pnpm --dir ui install --frozen-lockfile
- run: pixi run pnpm --dir ui bindings
- run: pixi run pnpm --dir ui routes:generate
- run: pixi run pnpm --dir ui test:coverage
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -114,6 +127,90 @@ jobs:
- run: pixi run pnpm --dir ui install --frozen-lockfile
- run: pixi run pnpm --dir ui audit --audit-level=high

detect-binding-changes:
name: Detect TS Binding Changes
if: github.event_name == 'pull_request'
outputs:
changed: ${{ steps.compare.outputs.changed }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
persist-credentials: false
- uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6
with:
cache: true
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- run: pixi run cargo run -p quent-simulator-ui-bindings --locked
- name: Generate base bindings
run: |
if [ -d base/examples/simulator/ui-bindings ]; then
pixi run cargo run \
--manifest-path base/Cargo.toml \
-p quent-simulator-ui-bindings \
--locked
else
pixi run cargo build \
--manifest-path base/Cargo.toml \
-p quent-simulator-server \
--locked
fi
- name: Compare generated bindings
id: compare
run: |
diff_status=0
if [ -d base/ui/generated/ts-bindings ]; then
base_bindings=base/ui/generated/ts-bindings
else
base_bindings=base/examples/simulator/server/ts-bindings
fi
diff --recursive --unified=3 "$base_bindings" \
ui/generated/ts-bindings > ts-bindings.diff || diff_status=$?
if [ "$diff_status" -gt 1 ]; then
exit "$diff_status"
elif [ "$diff_status" -eq 1 ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: steps.compare.outputs.changed == 'true'
with:
name: ts-bindings-diff
path: |
ts-bindings.diff
ui/generated/ts-bindings/
retention-days: 7

diff-bindings:
name: TS Binding Diff
if: needs.detect-binding-changes.outputs.changed == 'true'
needs: [detect-binding-changes]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ts-bindings-diff
path: .
- name: Post binding diff to step summary
run: |
echo "## TS Binding Changes" >> "$GITHUB_STEP_SUMMARY"
echo '```diff' >> "$GITHUB_STEP_SUMMARY"
cat ts-bindings.diff >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"

build:
name: Build
needs: [fmt, lint, typecheck, test, e2e]
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ docs/agents/
__pycache__/
.env

ui/generated/ts-bindings/
15 changes: 11 additions & 4 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ members = [
"examples/simulator/instrumentation",
"examples/simulator/server",
"examples/simulator/ui",
"examples/simulator/ui-bindings",
# Deterministic query-engine event emitter (opt-in via `-p`).
# Activates `quent-time/__test-clock-override`; excluded from default-members
# so default builds keep `quent-time` zero-cost.
Expand Down Expand Up @@ -112,6 +113,7 @@ default-members = [
"examples/simulator/instrumentation",
"examples/simulator/server",
"examples/simulator/ui",
"examples/simulator/ui-bindings",
]

[workspace.package]
Expand Down
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,14 @@ opinionated but replaceable implementation based on the modeling approach.

### Prerequisites

- Rust (stable, >= 1.93)
- Node.js (>= 24.11)
- pnpm (>= 10)
- protoc (protobuf compiler)

Or use [pixi](https://pixi.sh) to manage all dependencies:
Install [Pixi](https://pixi.sh), then enter the repository environment:

```bash
pixi shell
```

This installs the required toolchains and drops you into a shell with
everything on `PATH`.
Pixi provides the required Rust, Node.js, pnpm, and protoc versions. Run
development and build commands from this environment.

### UI development

Expand All @@ -143,6 +138,10 @@ pnpm install
pnpm dev
```

`pnpm dev` generates the TypeScript bindings consumed by the UI before starting
Vite. Run `pnpm bindings` after changing Rust types while the dev server remains
open. The `start`, `typecheck`, and `build` scripts also ensure bindings exist.

The dev server starts on <http://localhost:5173> by default.

#### Running the server without Docker
Expand Down Expand Up @@ -171,8 +170,8 @@ stress testing on the UI.
cargo build -p quent-simulator-server --features ui --release
```

This runs `pnpm install && pnpm build` in `ui/` as part of the Cargo build and
bundles the output into the binary.
The Cargo build generates the TypeScript bindings, builds the UI, and bundles
the output into the binary.

### Swagger UI

Expand Down
5 changes: 4 additions & 1 deletion domains/query_engine/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition.workspace = true
publish.workspace = true
[features]
swagger = ["dep:utoipa", "dep:utoipa-swagger-ui"]
ui = ["dep:mime_guess", "dep:rust-embed"]
ui = ["dep:mime_guess", "dep:quent-simulator-ui-bindings", "dep:rust-embed"]

[dependencies]
axum = { version = "0.8.7" }
Expand All @@ -32,3 +32,6 @@ utoipa-swagger-ui = { version = "9", features = ["axum"], optional = true }
uuid.workspace = true
mime_guess = { version = "2", optional = true }
rust-embed = { version = "8", optional = true }

[build-dependencies]
quent-simulator-ui-bindings = { path = "../../../examples/simulator/ui-bindings", optional = true }
92 changes: 58 additions & 34 deletions domains/query_engine/server/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,68 @@
// SPDX-License-Identifier: Apache-2.0

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build the UI with pnpm when the ui feature is enabled.
if std::env::var("CARGO_FEATURE_UI").is_ok() {
let ui_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../ui");
println!("cargo:rerun-if-changed={}", ui_dir.join("src").display());
println!(
"cargo:rerun-if-changed={}",
ui_dir.join("index.html").display()
);
println!(
"cargo:rerun-if-changed={}",
ui_dir.join("package.json").display()
);
#[cfg(feature = "ui")]
build_ui()?;

let run_pnpm = |args: &[&str]| -> Result<(), Box<dyn std::error::Error>> {
let output = std::process::Command::new("pnpm")
.args(args)
.current_dir(&ui_dir)
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
// Vite prefixes warnings with "(!)".
let has_warnings = stderr.lines().any(|l| l.trim_start().starts_with("(!)"));
if !output.status.success() || has_warnings {
for line in String::from_utf8_lossy(&output.stdout).lines() {
println!("cargo:warning=pnpm: {line}");
}
for line in stderr.lines() {
println!("cargo:warning=pnpm: {line}");
}
Ok(())
}

#[cfg(feature = "ui")]
fn build_ui() -> Result<(), Box<dyn std::error::Error>> {
let ui_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../ui");
let bindings_dir = ui_dir.join("generated/ts-bindings");
let rerun_if_changed = |path: &std::path::Path| {
println!("cargo:rerun-if-changed={}", path.display());
};
for path in [
"src",
"public",
"index.html",
"package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"tsconfig.base.json",
"tsconfig.json",
"tsconfig.node.json",
"tsr.config.json",
"vite.config.ts",
] {
rerun_if_changed(&ui_dir.join(path));
}
for package in ["client", "components", "hooks", "utils"] {
let package_dir = ui_dir.join("packages/@quent").join(package);
for path in ["src", "package.json", "tsconfig.json", "tsdown.config.ts"] {
rerun_if_changed(&package_dir.join(path));
}
}

let run_pnpm = |args: &[&str]| -> Result<(), Box<dyn std::error::Error>> {
let output = std::process::Command::new("pnpm")
.args(args)
.current_dir(&ui_dir)
.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
// Vite prefixes warnings with "(!)".
let has_warnings = stderr
.lines()
.any(|line| line.trim_start().starts_with("(!)"));
if !output.status.success() || has_warnings {
for line in String::from_utf8_lossy(&output.stdout).lines() {
println!("cargo:warning=pnpm: {line}");
}
if !output.status.success() {
return Err(format!("pnpm {} failed", args.join(" ")).into());
for line in stderr.lines() {
println!("cargo:warning=pnpm: {line}");
}
Ok(())
};
}
if !output.status.success() {
return Err(format!("pnpm {} failed", args.join(" ")).into());
}
Ok(())
};

run_pnpm(&["install", "--frozen-lockfile"])?;
run_pnpm(&["build"])?;
}
quent_simulator_ui_bindings::generate(&bindings_dir)?;
run_pnpm(&["install", "--frozen-lockfile"])?;
run_pnpm(&["ci:build"])?;

Ok(())
}
6 changes: 0 additions & 6 deletions examples/simulator/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,3 @@ quent-simulator-instrumentation = { path = "../instrumentation", features = ["co
tokio = { workspace = true, features = ["rt-multi-thread", "sync"]}
tracing.workspace = true
uuid.workspace = true

[build-dependencies]
ts-rs = { workspace = true, features = ["uuid-impl", "serde-json-impl", "format"] }
quent-ui = { path = "../../../crates/ui" }
quent-query-engine-ui = { path = "../../../domains/query_engine/ui" }
quent-simulator-ui = { path = "../ui" }
Loading