Parte 14 de la serie Fitz. La Parte 13 mostró el dev loop para
.fitzv→ WASM; esta parte es qué es realmente un componente.fitzv, y cómo un lenguaje que emite binarios nativos también emite un frontend — a WebAssembly, sin adoptar un framework de JavaScript.
El problema: un lenguaje de servidor necesita frontend, sin importar un ecosistema
Fitz compila a binario nativo y tiene HTTP/async/Postgres/JWT en la sintaxis. Pero un backend tarde o temprano necesita una UI, y la respuesta habitual es: pegar el ecosistema JavaScript — un framework (React/Vue/Svelte), un bundler, un node_modules, un segundo toolchain, y una segunda definición de cada tipo que cruza el cable.
La respuesta de Fitz es un formato de componentes que el mismo compilador entiende, que compila a WebAssembly — sin framework JS, sin npm.
Un componente
Un .fitzv es un solo archivo: state, events, un <template>, estilos scoped opcionales — la forma Vue/Svelte, pero es Fitz de punta a punta:
component Counter {
state {
count: Int = 0
}
event inc() { count = count + 1 }
event dec() { count = count - 1 }
<template>
<div class="counter">
<button @click="dec">-</button>
<span class="n">{count}</span>
<button @click="inc">+</button>
</div>
</template>
<style scoped>
.counter { display: flex; gap: 12px; }
.n { font-variant-numeric: tabular-nums; }
</style>
}
{count} interpola el state. @click cablea un evento. <style scoped> queda scopeado a este componente (vía un hash en los nombres de clase). Buildealo:
fitz build --bin web --target wasm-client # → target/wasm/web/{web.js, web_bg.wasm}
Ese es el toolchain entero. Sin package.json, sin config de bundler.
Qué emite el compilador
No hay runtime de framework en el bundle. El emisor baja el componente directo a Rust sobre wasm-bindgen + web-sys: un struct con los campos de state, un mount() que construye el DOM con create_element/append_child, closures de evento que mutan el state, y un render() que re-pinta el subtree del componente ante un cambio de state. Es un módulo WASM autocontenido — el contador de arriba pesa 26 KB raw / 11.4 KB gzipped.
El template es un DSL de verdad: interpolación ({user.name.upper()} — expresiones Fitz arbitrarias sobre el state), directivas ({#if} / {#for}), eventos (@click / @input), y composición:
<template>
<ul>
{#for todo in todos}
<TodoRow label="{todo.title}" @remove="drop" />
{/for}
</ul>
</template>
<Child prop="v" /> pasa props hacia abajo; @remove="drop" burbujea un evento hacia arriba con su payload; <slot> deja que un padre inyecte contenido. La composición funciona entre archivos, así que una librería de componentes es solo imports.
El tipo es el mismo tipo
Esta es la parte que un framework pegado por encima no te puede dar. El type que define tu server es exactamente el que usa tu componente — importado del mismo módulo .fitz:
// models.fitz
type Todo {
id: Int
title: Str
done: Bool
}
// App.fitzv
from models import Todo
// ... un List<Todo> en el state, {todo.title} en el template
El server compila Todo a un struct nativo; el componente lo compila a un struct WASM; hay una sola definición. Sin un types.ts que se desincroniza del backend, sin un paso de codegen para mantenerlos en sync — por construcción no pueden divergir.
Por qué es distinto
Los modelos de componentes no son nuevos — la cuestión es qué los compila:
-
React / Vue / Svelte son excelentes, pero son JavaScript: un runtime de framework mandado al browser, un bundler, un árbol
npm, y una historia de tipos aparte para el borde de la API..fitzvcompila a WASM sin nada de eso. - Elm es un hermoso frontend de lenguaje propio, pero compila a JavaScript y vive solo en el browser — no es el mismo lenguaje que tu backend compilado.
-
Frameworks WASM de Rust (Dioxus, Leptos) compilan a WASM también, pero estás adoptando un framework, su DSL de macros, y su toolchain sobre Rust. En Fitz el componente es el lenguaje, y el mismo binario
fitzlo buildea.
Un lenguaje, un compilador, una definición de tipo — emitiendo un binario nativo para el server y un bundle WASM para el browser.
Los bordes honestos (MVP)
- El re-render es naive: un cambio de state re-pinta el subtree del componente, no un grafo de señales fine-grained. Alcanza para el caso común; la reactividad fine-grained es un slice futuro.
- El template soporta un envelope creciente de constructos (interpolación,
{#if}/{#for}, eventos, composición, slots, estilos scoped); los patrones genuinamente exóticos quedan afuera por ahora y el compilador te dice dónde. - El cliente es WASM-first; no hay target JS-vanilla — una decisión deliberada registrada cuando el contador entró bajo el gate de bundle de 40 KB.
Ese es el frontend como parte de primera clase del lenguaje: componentes que escribís en Fitz, compilados a WebAssembly, compartiendo tipos con un backend compilado a código nativo. Lo que sigue: cómo una función de ese backend se vuelve llamable desde el componente como si fuera local — @rpc.
Fitz es un lenguaje compilado con tipado gradual, HTTP/async/DB como ciudadanos de primera clase, que compila a binario nativo vía Rust. Su frontend son componentes single-file .fitzv compilados a WebAssembly — sin framework, sin npm. Código abierto.
Top comments (0)