Generar los bindings: cómo funciona
Una derivación sobre los tipos, un macro que recoge los comandos, y un exportador que escribe el fichero de TypeScript. Qué sustituye a qué, cómo se monta, y la nota de versión que hay que conocer antes de instalarlo.
Las tres piezas
Los tipos
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]#[serde(rename_all = "camelCase")]pub struct ProyectoResumen { pub id: ProyectoId, pub nombre: String, pub numero_ficheros: usize,}Una derivación más sobre la plantilla del nivel 17.5. Y algo que importa: respeta los atributos de serde. El renombrado, el etiquetado de los enums, la omisión de nulos — todo lo que decidiste en el nivel 17 se refleja en el tipo generado, porque describe la forma serializada y no la de Rust.
Si a un tipo le falta esa derivación, no compila al recogerlo: el error dice que el tipo no implementa la descripción. Es un error de compilación, que es exactamente donde debe estar.
El arranque
Aquí es donde cambia lib.rs:
use tauri_specta::{collect_commands, Builder};pub fn run() { let bindings = Builder::<tauri::Wry>::new() .commands(collect_commands![ proyectos::comandos::cargar_proyecto, proyectos::comandos::guardar_proyecto, ]); // Solo en desarrollo: escribe el fichero al arrancar. #[cfg(debug_assertions)] bindings .export(specta_typescript::Typescript::default(), "../src/lib/bindings.ts") .expect("no se pudo exportar el binding"); tauri::Builder::default() .invoke_handler(bindings.invoke_handler()) // sustituye a generate_handler .run(tauri::generate_context!()) .expect("error al arrancar la aplicación");}Dos sustituciones, y son el nivel entero:
| antes | ahora |
|---|---|
tauri::generate_handler![…] |
collect_commands![…] dentro del builder |
.invoke_handler(generate_handler![…]) |
.invoke_handler(bindings.invoke_handler()) |
Los comandos se declaran una sola vez. No hay una lista para registrar y otra para describir: es la misma, y de ahí sale la propiedad que el nivel 19.5 desarrolla.
Lo que sale
// src/lib/bindings.ts — GENERADO, no se editaexport type ProyectoResumen = { id: number; nombre: string; numeroFicheros: number };export type ErrorPayload = { code: string; message: string };export const commands = { async cargarProyecto(id: number): Promise<Result<ProyectoResumen, ErrorPayload>> { … }, async guardarProyecto(id: number, contenido: string): Promise<Result<null, ErrorPayload>> { … },};Y el frontend deja de escribir cadenas:
import { commands } from './lib/bindings';const r = await commands.cargarProyecto(42);// ^ el editor autocompleta el nombre, los argumentos y el tipo del resultadoFíjate en que el retorno viene envuelto: un comando que devuelve Result genera un resultado que hay que abrir, en vez de una promesa que se rechaza. Eso convierte el manejo de errores del nivel 13.3 en algo que el compilador comprueba, y es una de las mejores consecuencias del cambio.
Qué hacer con el fichero generado
Se versiona. Aunque sea generado, y por dos razones: el frontend no puede compilar sin él —así que un clon nuevo no funcionaría— y verlo en el diff es una de las ventajas del sistema, porque un cambio en la frontera se ve como lo que es.
No se edita. Con una cabecera que lo diga, y conviene que el linter o la revisión lo hagan cumplir. Editarlo funciona hasta la siguiente regeneración.
Se regenera de forma determinista, y dónde vive ese paso es el nivel 19.3.