Skip to content

10. phaze-cli

─── TypeScript toolchain (Node/TS — Babel → Oxc → Rolldown) ───────────
1. phaze-tsplugin 2. phaze-compile 3. phaze-vite
4. phaze-astro / 5. phaze-cloudflare (host adapter — pick one)
6. phaze-language-tools 7. phaze-vscode 8. phaze-glow 9. phaze-check
─── Rust toolchain (crates.io — Rust → Wasm, NOT the Babel/Vite pipeline) ──
10. phaze-cli ← codegen binary ← you are here
└ runtime crates: phaze · phaze-core · phaze-macros · phaze-crypto · phaze-fb · phaze-native

phaze-cli is the one tooling package of the Rust toolchain — everything else in that ecosystem (phaze, phaze-core, phaze-macros, phaze-crypto, phaze-fb, phaze-native) is runtime, documented under Integrations › Rust. It’s an installable binary, not a runtime dependency:

Terminal window
cargo install phaze-cli # → `phaze` on PATH

It reads your handler source (and, ahead, your schemas) and emits typed artifacts for the TypeScript side of the app. That’s what makes phaze the only Rust HTTP framework with end-to-end Rust → TS types: the Rust handler source is the single source of truth for the client.

phaze gen client is how Rust endpoints join Phaze Transport — the unified, typed data+API layer. The architecture: all endpoints are Rust (src/api/*.rs) bar a tiny TS set (introspection + the app-router actions that leverage the cache); Rust is the canonical API. Both runtimes feed one consumption face — phaze-compile populates it for TS actions, phaze gen client folds the Rust endpoints in — so a caller hits any endpoint the same way regardless of which runtime hosts it.

A third path keeps the Rust inside the fence: lang: rust marks the fence body itself as Rust, and a gen-rust build step hoists it into a rust-api handler at the fence’s rust: URL (the TS side proxies to it, exactly as an empty-body rust: knob would). It’s the “write the handler right here” option — distinct from proxying to an endpoint that already exists. And because the body is real Rust, it gets first-class editor support: live rust-analyzer hover and cargo check red squiggles, mapped back onto the .phaze through the sourcemap gen-rust emits — see phaze-vscode › Rust in lang: rust fence bodies.

Crucially, parity between the two runtimes is by a runtime-neutral wire contract, not shared code. Each Transport field defines a contract (sign:X-Phaze-Sig: ed25519:<base64> over the body; flatbuffer:application/x-flatbuf + the raw buffer), and both emitters conform to it — so a Rust-produced and a TS-produced response are interchangeable to one client-side consumer. That’s why gen client carries the field contracts, not just the fetch: a #[get("/…", sign = "ed25519")] endpoint’s generated client emits the same native subtle.verify the TS action arrow does (ed25519 is Web Crypto native → the browser verifies for free; see Security). Testing the round-trip is testing parity.

CommandWalksEmits
phaze gen clientsrc/api/**/*.rs (syn AST) — #[get/post/...] + handler signatures, respecting #[group("/prefix")]a typed TS client: camelCase fns, path interpolation, body/query handling, a PhazeError-aware fetch wrapper
phaze gen openapithe same metadataOpenAPI 3.1 JSON — paths keyed by {id}, request/response schemas derived from Json<T> / Form<T> / Path<T> / Query<T> / return types; user types via $ref

Both share one AST walker over src/api/**/*.rs — the Endpoint metadata model — so the client and the OpenAPI description never drift from each other or from the handlers.

Terminal window
phaze gen client --out src/lib/api.gen.ts
phaze gen openapi --out openapi.json

phaze-cli grows along the gen surface. Status of the next targets (✓ shipped · ▶ planned):

TargetStatusWhat it is
Transport-aware client from Rust fields✓ wire contract ships · ▶ CLI codegen plannedPhaze Transport is a runtime-neutral wire contract: a sign = "ed25519" Rust #[get] already emits the detached X-Phaze-Sig the browser verifies — phaze-compile injects the inline-native subtle.verify from the declared field, with the same inlined PUBLIC_PHAZE_SIGN_KEY whether a TS action or a Rust handler signed it (one model, two runtimes). So a signed Rust endpoint is a first-class Transport citizen today. What’s planned for the CLI is narrower: gen client reads the handler’s outbound sign / encrypt fields so the generated typed client carries the inverse straight from the Rust source — the handler stays the single source of truth, instead of the field being re-declared on a TS action.
.fbs → runtime-free reads✓ reads ship · ▶ CLI .fbs parser plannedThe runtime-free FlatBuffer read already ships: a flatbuffer: action’s field reads inline at the call site to native DataView/TextDecoder ops (strip macro — zero shipped reader, zero npm flatbuffers, phaze runtime unchanged; Flat Buffers & SSR). Today the vtable layout is read from a flatc-generated reader (an opt-in flatbuffersReaderPlugin also swaps such a reader’s npm-flatbuffers import for a lean DataView ByteBuffer). Planned for the CLI: a pure-Rust .fbs parser (no flatc; mirrors parse.rsEndpointemit.rs) so the layout comes straight from .fbs — feeding the same inline reads + the typed flat() client. Rust structs stay flatc-generated for now.
Further gen targets▶ plannede.g. typed query / route helpers — slot into the same walker.
  • Integrations › Rust — the phaze Rust HTTP runtime that phaze-cli generates clients for (handler shape, bindings, FlatBuffers, performance).
  • Tooling overview — where phaze-cli sits in the two-toolchain ecosystem.
  • phaze-compile — the Node-side transpiler it hands off to at the codegen seam.