Move a loader into Rust
You'll take one loader out of TypeScript and answer it from Rust. The page, the route and the tests don't change. At the end you'll load the page and see your Rust string in the markup.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Start from the app you built in 320, which has a /product/{id} route whose loader is TypeScript.
Find the name you're taking over#
The plan file names everything. Build the app and read the sources column:
product.$id is the name. A route directory product/[id] gives the source product.$id. You bind that string in Rust.
Add a Cargo project beside the app#
Up to now the stock fsr binary has been serving this. To write Rust you need your own binary. Put this Cargo.toml next to app/ and config/:
[]
= "shop"
= "0.1.0"
= "2024"
[]
= "0"
= "0"
= "0"
= { = "1", = ["macros", "rt-multi-thread"] }A client on the mock transport needs no feature. Against a real service over HTTP turn on the host's http feature: snapfire_fsr_host = { version = "0", features = ["http"] }. [auth] needs auth, a .graphql client graphql and a .proto client grpc.
Write the override#
Here is src/main.rs in full. It compiles as written.
use Arc;
use ;
use Host;
async
Data is a ValueMap, which you build by collecting (String, Value) pairs. A nested object is a Value::Map. The keys have to match what the page destructures, because the page is unchanged and still expects product.
ctx.params is the same params the TypeScript loader read. ctx also carries query, the session cell and the service handle, so your Rust calls the services through the same registry and against the same contract.
Host::from_cwd takes the directory you run the binary in as the project root, not the app directory. It reads config/, the plan and the contracts from there. So the binary serves whichever project it runs inside: this one under cargo run, the deployed tree in 410.
Run it#
One row changed. Open /product/9 and the page renders "Answered by Rust." inside the same markup the TSX component produced. The component gets the same props and doesn't know they came from Rust.
Rules for claiming a name#
The host refuses to guess when the plan file and Rust both name something. Each conflict is a boot error that names the source.
.source("pricing", f) on a name the plan doesn't lower just binds it. That's how you add a source that has no TypeScript at all.
.source("product.$id", f) on a name the plan lowered is refused:
Use .source_override instead. Every loader Rust replaces is then marked as an override in main.rs.
.source_override("products.$id", f)when no such source exists fails at boot. That's almost always a rename that left the override dangling. A dangling override would leave the TypeScript running while you believed Rust had taken it.
Override an action#
An action override takes the context and the decoded input and returns a Value. Chain it onto the builder in main, after .source_override(...) and before .build():
.action_override
input arrives as a Value, so you match on Value::Map to read a field off it. There is no .get() on Value itself.
Add a route in Rust#
.route binds a pattern the plan file doesn't have, so it adds a route and doesn't override anything. A Plan says which module renders and what fills the shell's slot. Add use snapfire_fsr::Plan; at the top of main.rs and chain the route onto the builder:
.routeRender a module yourself#
An evaluator answers a set of module ids with a Rust renderer. It has one method, which returns a stream of chunks. Put this in main.rs above main:
use stream;
use ;
use ;
;
Then chain it onto the builder with the module ids it answers:
.evaluatorThat needs snapfire_fsr_runtime and futures-util in your Cargo.toml. snapfire_fsr_tera uses the same mechanism, which is how a Tera template and a React page compose into one document.
Read the report#
Calling Rust without taking a name#
Sometimes the loader is fine and one computation belongs in Rust. Mark an impl block instead. The build reads every .rs file under src/, so this can sit in main.rs above main. It needs snapfire_fsr_macros in your Cargo.toml:
use native;
;
Register it on the builder under the name the TypeScript uses:
.nativeThe build writes declarations into generated/native.d.ts, so a loader reads native off its context and calls the method with its arguments as an object:
;
The work happens in Rust. A native call is synchronous, so it needs no await. This needs no override and doesn't take a source name.
Services from Rust#
Rust calls services through the same registry as the TypeScript loader did, checked against the same contract document.
Next up: 510. Add FSR to an existing Rust service.