Capítulo 28B. De prototipo a release: lo que ningún tutorial te cuenta

CAP 28B · Bevy 0.19
"Hacer un juego es el 30% del trabajo. Lanzarlo es el 70%."

— Antoine de Saint-Exupéry, probablemente. Si no fue él, debería haberlo sido.

28B.1 Por qué este capítulo existe

Terminaste el cap. 28. Tienes un mini metroidvania que se mueve, tiene enemigos, recoges items, llegas a un objetivo, guardas la partida y oyes efectos. Pero no está listo para publicar. Le falta todo lo que pasa entre "anda en mi PC" y "está en Steam" o "está en itch.io".

Este capítulo es la continuación natural: cómo llevar tu proyecto Bevy de un prototipo local a un release público. Cubre:

Nota: el cap. 14B y el cap. 14 ya cubren organización del código. Acá nos enfocamos en lo que viene DESPUÉS: el release.

28B.2 Binary size: el primer shock

Tu juego con cargo run pesa ~200 MB. En el momento que lo veas, te va a dar algo. Bienvenido al desarrollo de juegos en Rust. El binario sin optimizar de Bevy con todas las features pesa eso. Hay cuatro técnicas que lo bajan a ~30 MB:

# cap-28B — sección 28B.2: Cargo.toml para release pequeño.
[profile.release]
opt-level = 3           # Para 2D, "3" suele dar mejor balance que "z".
                          # Usa "z" solo si el tamaño es absolutamente crítico
                          # (webassembly, embedidos), a costa de algo de velocidad.
lto = "fat"             # Link-Time Optimization: el linker ve todo el código.
                          # "fat" = más lento de compilar, más pequeño/rápido binario.
codegen-units = 1        # Una unidad de compilación (lento, máx. optimización).
strip = true             # Quita símbolos de debug del binario.
panic = "abort"          # En vez de unwind, aborta. Más pequeño.
incremental = false      # No incremental en release (más consistente).

Eso solo baja el binario a ~80-100 MB. Para llegar a 30 MB, sumamos selección de features:

# cap-28B — sección 28B.2: features mínimas para un 2D puro.
# Importante: NO existe una feature llamada "2d" en Bevy (es un error común
# copiado de tutoriales viejos). El soporte 2D ya viene incluido en `bevy_sprite`.
[dependencies]
bevy = { version = "0.19", default-features = false, features = [
    "bevy_asset",
    "bevy_audio",
    "bevy_sprite",
    "bevy_state",
    "bevy_text",
    "bevy_winit",
    "default_font",
    "vorbis",    # decodificador .ogg para bevy_audio
] }

Apagas las features que no usas: sin bevy_pbr (3D PBR), sin bevy_gltf (si no cargas glTF), sin tonemapping_luts, etc. Ojo: bevy_sprite ya arrastra el pipeline de render 2D, no necesitas nada llamado "2d".

Truco pro: el linker mold o lld reduce tiempos de compilación y a veces el binario:

# cap-28B — sección 28B.2: usar mold como linker.
RUSTFLAGS="-C link-arg=-fuse-ld=mold" cargo build --release

Truco aún más pro: cargo-bloat te dice qué parte del binario es Bevy, qué parte es tu código, y qué parte son features que activaste sin usar. Ejecútalo y mira los 10 crates más pesados.

cargo install cargo-bloat
cargo bloat --release --crates

Típicamente, bevy_render y bevy_winit son los gordos. Si no usas winit (por ejemplo, si tu juego corre en un dispositivo custom), puedes apagarlo.

28B.3 El binario de 8 MB: wasm-opt y friends

Si vas a publicar en web, el binario es .wasm. Un binario wasm32-unknown-unknown sin optimizar pesa ~50 MB. Con wasm-opt (parte de binaryen):

# cap-28B — sección 28B.3: optimizar wasm.
# -Oz = optimiza para tamaño. -O3 = para velocidad. -Os = balance.
wasm-opt -Oz -o game-opt.wasm game.wasm

Baja a ~15-20 MB. Con wasm-strip (parte de wabt), puedes sacar más:

wasm-strip game-opt.wasm -o game-strip.wasm

Para web serio, la cadena es: cargo build --release --target wasm32-unknown-unknownwasm-bindgenwasm-optwasm-strip → gzip en el servidor (los CDNs como itch.io, GitHub Pages, Cloudflare Pages lo hacen automáticamente).

Caveat: un .wasm de 20 MB tarda en descargarse. Para un juego web, lo óptimo es ~5-10 MB. Si no llegas, considera lazy loading: cargas assets y partes del juego según se necesitan.

28B.4 Build matrices: el juego en Windows, Mac y Linux

El escenario clásico: tu juego anda en tu Linux de desarrollo, pero ¿anda en Windows? ¿En Mac? La respuesta corta: con Bevy y Rust, sí, pero hay que configurar bien.

# cap-28B — sección 28B.4: GitHub Actions para build matriz.
# .github/workflows/release.yml
name: release
on:
  push:
    tags: ['v*']
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            artifact: my-game-linux.tar.gz
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: my-game-windows.zip
          - os: macos-latest
            target: x86_64-apple-darwin
            artifact: my-game-macos-x86_64.tar.gz
          - os: macos-latest
            target: aarch64-apple-darwin
            artifact: my-game-macos-arm64.tar.gz
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --release --target ${{ matrix.target }}
      - run: |
          if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
            tar czf ${{ matrix.artifact }} -C target/${{ matrix.target }}/release my-game
          elif [ "${{ matrix.os }}" = "windows-latest" ]; then
            7z a ${{ matrix.artifact }} target/${{ matrix.target }}/release/my-game.exe
          else
            tar czf ${{ matrix.artifact }} -C target/${{ matrix.target }}/release my-game
          fi
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.artifact }}
          path: ${{ matrix.artifact }}

Eso te da builds para los tres OS principales (con Mac cubriendo Intel y Apple Silicon). Para web, agregas un job adicional con ubuntu-latest y target wasm32-unknown-unknown.

Caveat Linux: las distros Linux vienen en sabores (glibc, musl, etc.). Para un binario verdaderamente portable, compila con --target x86_64-unknown-linux-musl. Eso produce un binario estático que corre en cualquier Linux moderno. Para Bevy, además necesitas instalar las dependencias del sistema (Ubuntu: libasound2-dev libudev-dev pkg-config).

Caveat Mac: Apple Silicon (M1, M2, M3) vs Intel. Publica ambos: aarch64-apple-darwin y x86_64-apple-darwin. O un "universal binary" con lipo (más complejo).

28B.5 Asset packaging: cómo distribuyes los assets

Tu juego tiene sprites, sonidos, música, fuentes, datos. ¿Cómo van en el binario?

Opción A: assets como carpeta al lado del binario. El método simple. Estructura:

my-game/
├── my-game.exe     # binario.
└── assets/         # carpeta con todo.
    ├── sprites/
    │   ├── player.png
    │   └── enemy.png
    ├── sounds/
    │   ├── jump.ogg
    │   └── gem.ogg
    └── fonts/
        └── pixel.ttf

Pros: simple, el usuario puede moddear. Contras: el usuario puede borrar la carpeta y romper el juego. Para un release, en general quieres evitar esto.

Opción B: assets embebidos en el binario. El crate include_dir te permite meter toda la carpeta en el binario en tiempo de compilación:

// cap-28B — sección 28B.5: assets embebidos con include_dir.
use include_dir::include_dir;

static ASSETS: include_dir::Dir = include_dir!("$CARGO_MANIFEST_DIR/assets");

// En Bevy, para cargar un asset embebido NO uses turbofish en `load`.
// La forma estándar es dejar que el tipo se infiera del handle al que se asigna:
fn load_player_sprite(asset_server: Res<AssetServer>) -> Handle<Image> {
    asset_server.load("embedded://crate/assets/sprites/player.png")
    //                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                 Prefijo especial que `bevy_asset` interpreta como "embedded".
    //                 El tipo Image se infiere del tipo de retorno del handle.
}

Importante: evita el turbofish asset_server.load::<Image>(...). La forma estándar es dejar que el tipo se infiera del handle al que asignas el resultado (let h: Handle<Image> = asset_server.load(...)) o usar el helper del AssetServer. El turbofish no es la API recomendada y en algunas versiones compila mal con paths embedded://.

Pros: binario único, imposible de romper. Contras: binario más grande, hot-reload más difícil.

Opción C: assets como archivo .pak (custom o con crate). Empaquetas todos los assets en un solo archivo. El usuario no puede tocarlo. Más complejo; solo vale la pena para juegos grandes.

Para un indie pequeño, opción A con instrucciones claras ("no borres la carpeta assets") es suficiente. Para un juego grande, opción B con include_dir es razonable.

28B.6 Configuración: lo que cambia entre dev y release

Tu juego tiene cosas que cambian: URL del servidor, claves de API, paths de assets, niveles de logging. La solución idiomática en Rust: el crate config o un struct propio con serde.

//! cap-28B — sección 28B.6: config con serde + RON.
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct GameConfig {
    pub title: String,
    pub server_url: Option<String>,  // None en single-player.
    pub assets_path: String,
    pub log_level: String,
}

fn load_config() -> GameConfig {
    let path = if cfg!(debug_assertions) {
        "config.dev.ron"
    } else {
        "config.release.ron"
    };
    let raw = std::fs::read_to_string(path).expect("config file");
    ron::from_str(&raw).expect("valid RON")
}

Y los archivos:

// config.dev.ron
(
    title: "Mi Juego (dev)",
    server_url: None,
    assets_path: "assets/",
    log_level: "debug",
)

// config.release.ron
(
    title: "Mi Juego",
    server_url: Some("https://api.mijuego.com"),
    assets_path: "assets/",
    log_level: "warn",
)

Truco: nunca hardcodees URLs ni claves en el código. Siempre en un archivo de config. Y nunca commitees el config.release.ron con las claves reales. Usa variables de entorno o un .gitignore para los secrets.

28B.7 Versionado del save: la pesadilla de v0.4

Tu juego v0.3 guarda así:

#[derive(Serialize, Deserialize)]
struct SaveV1 { player_name: String, hp: i32 }

En v0.4 agregas un campo level: u32. ¿Qué pasa con los saves de v0.3? El Deserialize falla o produce un struct incompleto. La solución: migraciones.

//! cap-28B — sección 28B.7: migración manual con versión en el header.
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct SaveV1 { player_name: String, hp: i32 }

#[derive(Serialize, Deserialize)]
struct SaveV2 { player_name: String, hp: i32, level: u32 }

impl SaveV1 {
    fn migrate(self) -> SaveV2 {
        SaveV2 {
            player_name: self.player_name,
            hp: self.hp,
            level: 1,   // default para migraciones v1 → v2.
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "version")]
enum SaveAny {
    #[serde(rename = "1")]
    V1(SaveV1),
    #[serde(rename = "2")]
    V2(SaveV2),
}

impl SaveAny {
    fn to_v2(self) -> SaveV2 {
        match self {
            SaveAny::V1(v1) => v1.migrate(),
            SaveAny::V2(v2) => v2,
        }
    }
}

// Al cargar:
fn load_save() -> SaveV2 {
    let raw = std::fs::read_to_string("save.json").unwrap();
    let any: SaveAny = serde_json::from_str(&raw).unwrap();
    any.to_v2()
}

La regla: cada save tiene una versión en su header. Al cargar, miras la versión; si es vieja, aplicas las migraciones en cadena. Esto se vuelve crítico cuando un jugador tiene 50 horas de save y subes un patch. El crate bevy_save automatiza esto con un trait Migrate, pero la idea es la misma.

El cap. 24 cubre persistencia. Acá el énfasis: si tu juego tiene saves, las migraciones son obligatorias desde el día uno. Piénsalo antes, no después.

28B.8 Profiling en producción: cómo saber si va bien

Después de publicar, ¿cómo sabes si tu juego corre bien en hardware modesto? Tres formas:

  1. Telemetry anónima y opt-in (cap. 27B): el juego manda reportes de FPS, hardware, errores a un servidor. Cuestionable por privacidad, útil para estadísticas.
  2. Builds de QA internos: distribuyes a un grupo pequeño de testers, ellos reportan problemas.
  3. Foros y Steam reviews: la fuente más honesta. Si la gente reporta "se traba en X", vas a verlo.

Para un indie, opción 2 + 3 es suficiente. Para un juego grande, opción 1 tiene sentido.

Una métrica clave: el percentil 95 de FPS (no el promedio). El promedio puede decir 60 fps mientras que el 5% de los frames son 12 fps (lo que se siente horrible). Tracy (cap. 26) o un profiler simple te da esto.

28B.9 Release checklist: lo que corres antes de publicar

□ El binario arranca y se cierra limpiamente.
□ El binario arranca y se cierra en cada OS target.
□ El binario corre con assets completos.
□ El binario corre sin assets (al menos da un error claro, no un panic).
□ Los saves se cargan y guardan correctamente.
□ Las migraciones de save se aplican correctamente.
□ El binario de release no tiene el inspector/BRP.
□ El binario de release no tiene features de debug.
□ El tamaño del binario es razonable (< 50 MB para desktop, < 10 MB para web).
□ El juego cabe en una resolución mínima razonable (1280×720).
□ El input funciona con gamepad y teclado.
□ El sonido funciona (verifica que los .ogg cargan, no solo en tu PC).
□ El idioma del juego es el correcto.
□ El icono del juego está bien (Windows: .ico; Mac: .icns; Linux: .png en .desktop).
□ El "about" tiene la versión correcta.
□ El save default no contiene datos sensibles.
□ El save default está en una carpeta accesible al usuario (no en Program Files).

28B.10 Glosario del capítulo

Release: el binario que se distribuye al usuario final.
Binary size: tamaño del binario. Bevy sin optimizar ~200 MB; optimizado ~30 MB.
opt-level: nivel de optimización del compilador Rust. "z" = tamaño, "3" = velocidad, "s" = balance.
LTO (Link-Time Optimization): el linker optimiza entre crates. Más lento de compilar, binario más chico/rápido. Valores: true/false/"thin"/"fat".
codegen-units: número de unidades de compilación. Menos = más optimización, más lento.
strip: quitar símbolos de debug del binario final.
panic = abort: en caso de panic, abortar en vez de hacer unwinding. Más chico.
mold / lld: linkers alternativos a ld. Más rápidos.
cargo-bloat: herramienta que muestra qué crates pesan más en tu binario.
wasm-opt: optimizador de WebAssembly (parte de binaryen).
wasm-strip: eliminador de secciones innecesarias de .wasm (parte de wabt).
wasm-bindgen: puente entre Rust y JavaScript para WebAssembly.
GitHub Actions: CI de GitHub. Estándar para builds de open source.
Build matrix: estrategia de CI que corre el mismo build en múltiples OS/arquitecturas.
include_dir: crate que embebe una carpeta en el binario en tiempo de compilación.
config (crate): crate para gestionar configuración desde archivos.
Migración de save: conversión de un save de versión antigua a la nueva.
Telemetry: envío de datos de uso desde el juego a un servidor.
Percentil 95: métrica que ignora el 5% peor. Útil para "frame time" porque el promedio miente.
Release checklist: lista mental o automatizada de cosas a verificar antes de publicar.
bevy_cli: CLI emergente (TheBevyFlock/bevy_cli, alpha) que automatiza builds, migraciones y scaffolding.

28B.11 Trivia histórica

Hollow Knight (Team Cherry, 2017) — el equipo de 3 personas en Australia tardó 3 años en hacer el release. La parte de "publicar y mantener" les tomó otros 2 años (DLC, ports a Switch). Lección: el release es un trabajo continuo, no un evento.
Stardew Valley (ConcernedApe, 2016) — un solo desarrollador, publicado en 2016, sigue recibiendo updates en 2024. Cada update requiere un test de "no rompo saves viejos". La herramienta de migración de ConcernedApe es interna; el cap. 28B es la versión "hazlo bien desde el día uno".
Bevy 0.18 (2025) — mejoras notables en binary size: un Hello World cabe mucho mejor que en 0.13/0.14. El equipo de Bevy invirtió mucho en reducir el peso base.
GitHub Actions free tier (2020) — democratizó el CI. Antes, configurar Travis o AppVeyor era un dolor. Ahora, un `.github/workflows/release.yml` y listo.
mold linker (Rui Ueyama, 2020) — el linker rápido que cambió las expectativas de tiempo de compilación. Antes, linkear un proyecto grande era 30+ segundos; mold lo baja a 2-3.
WebAssembly MVP (W3C, 2017) — el primer estándar de WebAssembly. Permitió el ecosistema que hoy tiene Bevy como uno de los frameworks más serios.
bevy_save (hankjordan, 2024) — el crate de save/load más maduro para Bevy. Trae el trait `Migrate` que el cap. recomienda.
Steam Deck (Valve, 2022) — obligó a la industria indie a pensar en hardware modesto. Si tu juego corre en un Steam Deck, corre en la mayoría de PCs de los jugadores.
itch.io (Leaf Corcoran, 2013) — la plataforma que hizo accesible el publishing de juegos indie. Sin gatekeepers, sin review, sin comisión significativa.
Humble Bundle (Wolfire Games, 2010) — la primera plataforma de "paga lo que quieras". Cambió el modelo de negocio indie.

28B.12 Metedura de pata

❌ Hardcodear URLs de servidor, API keys, paths absolutos.
✅ Un archivo de config + variables de entorno + `cfg!(debug_assertions)`.
💡 Por qué: hardcodear obliga a recompilar para cambiar nada. Un atacante que decompile tu binario ve tus claves. Un día quieres apuntar a otro servidor y tienes que re-buildear.
❌ Publicar sin haber probado el instalador en una máquina limpia.
✅ Probar en una VM con Windows/Linux/Mac limpios.
💡 Por qué: tu PC tiene mil cosas instaladas que el jugador no tiene. DLLs faltantes, codecs de audio, drivers. La VM limpia es la verdad.
❌ Asumir que los saves de v0.3 cargan en v0.4 porque "solo agregué un campo".
✅ Testear la migración con saves reales de v0.3.
💡 Por qué: serde es quisquilloso. `#[serde(default)]` te salva la vida, pero no es magia. Prueba.
❌ Poner `features = ["2d"]` (o "3d") en tu dependencia de Bevy. Esas features no existen.
✅ `features = ["bevy_sprite"]` para 2D, `features = ["bevy_pbr"]` para 3D PBR.
💡 Por qué: el error de cargo es críptico: "Package bevy does not have feature `2d`". La gente lo copia de tutoriales viejos.
❌ Olvidar incluir el icono y los assets de branding.
✅ Un checklist de "qué archivos van en el release".
💡 Por qué: un binario sin icono en Windows es amateur. Un juego sin logo en el menú principal es olvidable. La primera impresión importa.

🎯 28B.13 Patrón del capítulo: "release es una feature"

Problema: el equipo de desarrollo trata el release como "el día que subimos el binario a Steam". El día llega, hay problemas, se patea para atrás, se hace a las apuradas, sale con bugs.

Solución: tratar el release como una feature más, con su milestone, su checklist, su test, su plan. No es un evento; es un proceso.

Milestone 12: el binario de release.
  - Binary size < 50 MB (desktop), < 10 MB (web).
  - Build matrix para 3 OS + wasm32.
  - Config separada dev/release.
  - Migraciones de save testeadas.
  - Checklist corrida.
  - QA en máquinas limpias.
  - Icon, branding, metadata.
  - Página de store (Steam, itch.io) lista.

Cuándo sí:

Cuándo no:

Epílogo: el ciclo completo

Empezaste este libro con un "Hola Mundo" en Bevy. Terminaste con un proyecto con su ciclo de release. Eso es el camino de un indie Bevy-dev. Lo que viene después es:

El libro se cierra. Tu viaje no. ¡A publicar!

28B.14 — bevy_github_ci_template + bevy_cli: CI/CD sin reinventar la rueda

Hay dos herramientas que ahorran semanas de fricción. La primera es la plantilla oficial de GitHub Actions que el Bevy Engine org mantiene en bevyengine/bevy_github_ci_template. La segunda, emergente pero prometedora, es bevy_cli (TheBevyFlock/bevy_cli), un comando unificado que automatiza builds, scaffolding de plugins y migraciones de versión.

# Opción 1: clonar la plantilla dentro de tu proyecto.
git submodule add https://github.com/bevyengine/bevy_github_ci_template .github

# Opción 2: usar bevy_cli para scaffolding + build.
cargo install bevy_cli --git https://github.com/TheBevyFlock/bevy_cli
bevy new my-game            # scaffolding con estructura recomendada
bevy build --release --web  # build para web (incluye wasm-bindgen, wasm-opt)
bevy migrate 0.18 0.19      # aplica migraciones automáticas de versión

Estado de bevy_cli: al escribir esto (julio 2026) está en alpha. Algunos subcomandos pueden cambiar o romper entre versiones. Para producción hoy (2026), usa la plantilla oficial de CI; deja bevy_cli para scaffolding y experimentación local.

Lo que trae la plantilla oficial de CI:

  1. ci.yml: en cada PR, corre cargo check, cargo clippy, cargo fmt --check, cargo test para Linux, Windows, macOS, y WASM. Cachea las deps con Swatinem/rust-cache para que tarde 2 min en vez de 30.
  2. release.yml: cuando pusheas un tag v*.*.*, compila para las cinco plataformas (linux-x64, linux-arm64, windows-x64, macos-x64, macos-arm64, wasm32), comprime los bins, y crea un GitHub Release con los assets adjuntos.
  3. itch.yml (opcional): si activas el secreto BUTLER_CREDENTIALS, sube automáticamente a itch.io con butler push.
# .github/workflows/release.yml (extracto).
name: Release
on:
  push:
    tags: ['v*']
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: windows-latest
            target: x86_64-pc-windows-msvc
          - os: macos-latest
            target: x86_64-apple-darwin
          - os: macos-latest
            target: aarch64-apple-darwin
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --release --target ${{ matrix.target }}
      - run: |
          mkdir -p artifacts
          cp target/${{ matrix.target }}/release/my_game artifacts/
      - uses: softprops/action-gh-release@v2
        with:
          files: artifacts/*

WASM build (en el mismo workflow):

  wasm:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
      - run: rustup target add wasm32-unknown-unknown
      - run: cargo install wasm-bindgen-cli --locked
      - run: cargo build --release --target wasm32-unknown-unknown
      - run: |
          wasm-bindgen --out-dir artifacts/web --target web \
            target/wasm32-unknown-unknown/release/my_game.wasm
      - run: |
          # Optimización opcional: wasm-opt + wasm-strip.
          wasm-opt -Oz -o artifacts/web/my_game.wasm artifacts/web/my_game_bg.wasm
          wasm-strip artifacts/web/my_game.wasm
      - uses: softprops/action-gh-release@v2
        with:
          files: artifacts/web/*

Publicación en Steam y itch.io:

# Publicar a itch.io desde tu máquina o desde CI.
butler login
butler push artifacts/windows myuser/my-game:windows
butler push artifacts/linux   myuser/my-game:linux
butler push artifacts/web     myuser/my-game:html5

Cuándo usar la plantilla + bevy_cli:

Cuándo NO usarlas:

Personalización común:

La plantilla es el punto de partida, no el final. La lees, entiendes qué hace, y la adaptas a tu juego. Pero empezar desde cero (sin la plantilla) es un error que cuesta 1 semana de fricción con YAML.

Lo que vimos

En el siguiente

Aquí se cierra el libro principal. Lo que viene después son los anexos: glosario maestro (Anexo A), compatibilidades Bevy 0.13→0.19 (Anexo B), recursos recomendados (libros, blogs, Discord, crates — Anexo C), plugins de terceros ordenados por bloque (Anexo D), y guía de WASM (Anexo E). El Anexo A es tu diccionario de cabecera; el B tu tabla de migración; el C tus siguientes lecturas; el D tu catálogo de crates cuando necesites algo concreto; el E tu puente al navegador. Si el libro principal era el camino, los anexos son el mapa que dejas en la mesita para no perderte en el próximo proyecto. Anexos a la vista, y un abrazo de quien esto escribe.