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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ env:
DENO_VERSION: "2.9.5"
WASM_TOOLS_VERSION: "1.247.0"
JUST_VERSION: "1.54.0"
WASMTIME_VERSION: "47.0.1"

jobs:
gates:
Expand All @@ -67,7 +68,7 @@ jobs:

- uses: taiki-e/install-action@v2
with:
tool: just@${{ env.JUST_VERSION }},wasm-tools@${{ env.WASM_TOOLS_VERSION }}
tool: just@${{ env.JUST_VERSION }},wasm-tools@${{ env.WASM_TOOLS_VERSION }},wasmtime@${{ env.WASMTIME_VERSION }}

- uses: Swatinem/rust-cache@v2

Expand Down Expand Up @@ -97,6 +98,14 @@ jobs:
- name: Build counter example component
run: just example counter

# Prerender lane. `ssg-example` checks the same golden file natively and
# under `wasmtime run`; `serve-test` adds only the wasi:http wrapper.
- name: Prerender the counter example
run: just ssg-example counter

- name: Serve the prerendered counter example
run: just serve-test counter

# cargo test (the interner unit tests; the writer names the generated
# bindings and so is wasm32-only, covered host-side instead) +
# deno task test.
Expand Down
33 changes: 33 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dioxus = { version = "=0.7.10", default-features = false, features = [
] }

[workspace]
members = [".", "fixtures/surface-probe", "examples/counter", "examples/bench-rows", "examples/todomvc", "examples/components", "examples/primitives"]
members = [".", "ssr", "fixtures/surface-probe", "examples/counter", "examples/bench-rows", "examples/todomvc", "examples/components", "examples/primitives"]

# `dioxus-sdk-time` waits by calling the browser's `setTimeout` through
# wasm-bindgen, which on wasm32-wasip2 compiles to off-target stubs that abort
Expand Down
23 changes: 21 additions & 2 deletions examples/counter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,29 @@ edition = "2021"
publish = false

[lib]
crate-type = ["cdylib"]
# `rlib` alongside the component's `cdylib` so the SSG binary below can depend
# on this crate for `App`.
crate-type = ["cdylib", "rlib"]

# Prints the prerendered HTML to stdout as a `wasi:cli/command` component, so
# `wasmtime run` needs no flags to execute it.
[[bin]]
name = "counter-ssg"
path = "src/bin/ssg.rs"
required-features = ["ssg"]

[features]
default = ["client"]
# The renderers are optional dependencies, not just optional code paths: each
# emits a `component-type` custom section, and two of those in one binary is
# not a component. Exactly one of these may be enabled at a time.
client = ["dep:polyengine-dioxus"]
ssg = ["dep:polyengine-dioxus-ssr"]
ssr = ["dep:polyengine-dioxus-ssr", "polyengine-dioxus-ssr/serve"]

[dependencies]
polyengine-dioxus = { path = "../.." }
polyengine-dioxus = { path = "../..", optional = true }
polyengine-dioxus-ssr = { path = "../../ssr", optional = true }
dioxus = { version = "=0.7.10", default-features = false, features = [
"macro",
"html",
Expand Down
1 change: 1 addition & 0 deletions examples/counter/golden.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="app"><section class="counter"><button id="dec">-</button><span id="count">0</span><button id="inc">+</button><p id="parity" class="even">count is 0</p></section><section class="echo"><input id="draft" value=""/><p id="echo"></p></section><section class="list"><button id="add">add</button><button id="remove">remove</button><ul id="items"><li>alpha</li><li>beta</li></ul></section><form id="form"><input name="who" id="who"/><button id="submit" type="submit">submit</button><p id="submitted">submitted 0 time(s)</p></form></div>
13 changes: 13 additions & 0 deletions examples/counter/src/bin/ssg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Prerenders the counter app to stdout.
//!
//! Built for `wasm32-wasip2` this is a `wasi:cli/command` component that
//! `wasmtime run` executes with no flags — the cheapest end-to-end proof that
//! the renderer works inside a component. Built natively it prints the same
//! bytes, so one golden file covers both.

use std::io::{BufWriter, stdout};

fn main() {
polyengine_dioxus_ssr::render_to(counter_example::App, BufWriter::new(stdout().lock()))
.expect("prerender to stdout");
}
6 changes: 5 additions & 1 deletion examples/counter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use dioxus::prelude::*;

#[allow(non_snake_case)]
fn App() -> Element {
pub fn App() -> Element {
let mut count = use_signal(|| 0i32);
let mut draft = use_signal(String::new);
let mut items = use_signal(|| vec!["alpha".to_string(), "beta".to_string()]);
Expand Down Expand Up @@ -86,4 +86,8 @@ fn App() -> Element {
}
}

#[cfg(feature = "client")]
polyengine_dioxus::launch!(App);

#[cfg(feature = "ssr")]
polyengine_dioxus_ssr::launch_ssr!(App);
84 changes: 84 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ deps:
check:
cargo check --workspace --target wasm32-wasip2
cargo clippy --workspace --target wasm32-wasip2 -- -D warnings
# The workspace pass above sees every crate at DEFAULT features, so it
# never compiles the prerenderer's `serve` module or the `launch_ssr!`
# expansion. Checking the example covers both.
cargo clippy -p counter-example --no-default-features --features ssr \
--target wasm32-wasip2 -- -D warnings
deno task check

test:
cargo test
# The workspace root is a package, so plain `cargo test` above is
# `-p polyengine-dioxus` and nothing else.
cargo test -p polyengine-dioxus-ssr
deno task test

# Build the surface-probe fixture component into fixtures/build/. The
Expand Down Expand Up @@ -82,6 +90,82 @@ example name:
examples/build/{{name}}.component.wasm \
-o examples/build/{{name}}.plan.json

# Prerender the example to HTML, as a `wasi:cli/command` component.
#
# Checked twice against one golden file: natively, then as a component under
# `wasmtime run`. Same source, same bytes — so a divergence between the two
# is itself the signal, and neither check needs HTTP, ports or p3.
ssg-example name:
#!/usr/bin/env bash
set -euo pipefail
mkdir -p examples/build
cargo run -q -p {{name}}-example --no-default-features --features ssg \
--bin {{name}}-ssg | diff - examples/{{name}}/golden.html
cargo build -p {{name}}-example --no-default-features --features ssg \
--bin {{name}}-ssg --target wasm32-wasip2 --release
cp target/wasm32-wasip2/release/{{name}}-ssg.wasm \
examples/build/{{name}}.ssg.component.wasm
wasm-tools validate --features component-model examples/build/{{name}}.ssg.component.wasm
wasmtime run examples/build/{{name}}.ssg.component.wasm | diff - examples/{{name}}/golden.html

# Build the example as a `wasi:http/service` component (prerender per request).
ssr-example name:
#!/usr/bin/env bash
set -euo pipefail
mkdir -p examples/build
cargo build -p {{name}}-example --no-default-features --features ssr --lib \
--target wasm32-wasip2 --release
cp "target/wasm32-wasip2/release/$(echo {{name}} | tr - _)_example.wasm" \
examples/build/{{name}}.ssr.component.wasm
wasm-tools validate --features component-model,cm-async \
examples/build/{{name}}.ssr.component.wasm

# Serve the prerendered example.
#
# `-S cli` is not optional: without it wasmtime links only the proxy world,
# and a rustc wasm32-wasip2 component imports wasi:cli/environment,
# wasi:filesystem/preopens and exit through wasi-libc. That one flag adds
# both the p2 and the wasi:cli@0.3.x tracks.
serve name:
#!/usr/bin/env bash
set -euo pipefail
if [ ! -f examples/build/{{name}}.ssr.component.wasm ]; then
just ssr-example {{name}}
fi
wasmtime serve -S cli examples/build/{{name}}.ssr.component.wasm

# Smoke-test the served component: it answers, with the golden HTML.
#
# HTML correctness is already settled by `just test` and `just ssg-example`;
# what this covers is only the ~20-line HTTP wrapper. Binds port 0 and reads
# the real port back from wasmtime's own output, so parallel checkouts cannot
# collide, and kills by PID rather than by port.
serve-test name:
#!/usr/bin/env bash
set -euo pipefail
if [ ! -f examples/build/{{name}}.ssr.component.wasm ]; then
just ssr-example {{name}}
fi
log=$(mktemp)
wasmtime serve -S cli --addr 127.0.0.1:0 \
examples/build/{{name}}.ssr.component.wasm > "$log" 2>&1 &
pid=$!
trap 'kill $pid 2>/dev/null || true; rm -f "$log"' EXIT
port=""
for _ in $(seq 100); do
# No match yet is the normal case while wasmtime is still starting, and
# `set -euo pipefail` would otherwise abort the recipe on it.
port=$(grep -o 'http://127\.0\.0\.1:[0-9]*' "$log" | head -1 | cut -d: -f3 || true)
if [ -n "$port" ]; then break; fi
sleep 0.1
done
if [ -z "$port" ]; then
echo "serve never reported a port:" >&2
cat "$log" >&2
exit 1
fi
curl -sS --fail "http://127.0.0.1:$port/" | diff - examples/{{name}}/golden.html

# Real-browser (Chromium via Playwright) E2E lane for the counter example.
# First run: `cd e2e && npm install && npx playwright install chromium --with-deps`.
# GitHub-Pages-ready static site for the TodoMVC example, assembled flat
Expand Down
35 changes: 35 additions & 0 deletions ssr/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "polyengine-dioxus-ssr"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
description = "Server-side prerendering for Dioxus components, as a wasm32-wasip2 component"
publish = false

[dependencies]
dioxus-core = "=0.7.10"
# Depends only on askama_escape / dioxus-core / dioxus-core-types / rustc-hash:
# no tokio, no net, no wasm-bindgen. Compiles to wasm32-wasip2 unpatched.
dioxus-ssr = "=0.7.9"
# Only for the `serve` artifact. Optional so the SSG artifact links no HTTP
# bindings at all — and, more to the point, so this crate and
# `polyengine-dioxus` (world `polymorph:dioxus/app`) can never contribute two
# `component-type` custom sections to one binary.
#
# `async-spawn` is not optional here: `wasi:http/types.response.new` hands the
# body's READ end to the host, which cannot read it until `handle` returns the
# response. Writing the body before that would park forever, so the render has
# to run in a task spawned to continue past `task.return`.
wasip3 = { version = "0.8", optional = true, features = ["async-spawn"] }

[features]
serve = ["dep:wasip3"]

[dev-dependencies]
# Native-only: `rsx!` for the golden-HTML tests.
dioxus = { version = "=0.7.10", default-features = false, features = [
"macro",
"html",
"signals",
"hooks",
] }
Loading