La aplicación
Al final de este módulo tendrás TaskFlow corriendo y entenderás qué produce el build, porque de eso depende toda la estrategia de caché que viene después.
El código de la aplicación viene casi completo. El tema del taller es AWS, no construir una librería de reactividad, así que aquí lees y cambias cosas puntuales. Donde vas a escribir de verdad es en Terraform y en el pipeline.
Paso 3.1 · Arrancar
Sección titulada «Paso 3.1 · Arrancar»cd taskflow-p01-appmake installmake devQué deberías ver: http://localhost:5173 con el tablero. Crea un par de tareas y
recarga: siguen ahí, porque se guardan en localStorage.
En este workshop la aplicación no tiene backend. La API con Lambda y DynamoDB es el workshop 02, y va a consumir exactamente esta misma SPA.
Paso 3.2 · Compilar y mirar el resultado
Sección titulada «Paso 3.2 · Compilar y mirar el resultado»make buildQué deberías ver:
dist/index.html 0.94 kB │ gzip: 0.51 kBdist/assets/index.BqbgjO9-.css 3.25 kB │ gzip: 1.16 kBdist/assets/index.eXtURQbc.js 12.13 kB │ gzip: 4.79 kBDoce kilobytes de JavaScript, menos de cinco comprimidos. Ese directorio dist/ es
literalmente todo lo que vas a subir a S3: no hay proceso de servidor, no hay runtime que
mantener, no hay nada que parchar.
Fíjate en el nombre: index.eXtURQbc.js. Ese hash del contenido es el contrato que
permite cachear ese archivo un año entero sin miedo. Y el index.html, sin hash, es el
único que nunca se cachea. Toda la estrategia del módulo 8 se apoya en esa diferencia.
Paso 3.3 · Cambia una línea y compruébalo
Sección titulada «Paso 3.3 · Cambia una línea y compruébalo»Objetivo: ver el hash cambiar cuando cambia el contenido.
Anota el hash actual del JavaScript. Ahora cambia el título del tablero:
seccion.append( h("h1", {}, "Tablero"), resumen, formulario(scope), filtros(scope),);seccion.append( h("h1", {}, "Mis tareas"), resumen, formulario(scope), filtros(scope),);Y vuelve a compilar:
make buildQué deberías ver: el hash del index...js es distinto. Eso es lo que hace que un
navegador que tenía el archivo viejo pida el nuevo, sin necesidad de invalidar nada.
Paso 3.4 · Cambia el otro extremo
Sección titulada «Paso 3.4 · Cambia el otro extremo»Objetivo: ver de dónde sale la versión que muestra la aplicación.
Abre /acerca en el navegador. Dice dev y local.
Ahora compila pasando los valores a mano, que es lo que hará el pipeline con el tag de git:
make shellAPP_VERSION=v9.9.9 APP_COMMIT=abc1234def npm run buildexitQué deberías ver: en /acerca, v9.9.9 y abc1234. El commit se recorta a siete
caracteres.
Eso es lo que permitirá abrir el sitio en producción y saber exactamente qué versión
está sirviendo el CDN. Vuelve a make build para dejarlo en dev.
La estructura, de un vistazo
Sección titulada «La estructura, de un vistazo»- index.html el único HTML, punto de entrada de Vite
- Makefile comandos dockerizados
- tsconfig.json tipos del navegador
- tsconfig.node.json tipos de Node, solo para vite.config.ts
- vite.config.ts
Directoriopublic/
- favicon.svg se copia tal cual, sin procesar
Directoriosrc/
- main.ts arranca la app y monta las vistas
- styles.css
Directoriolib/
- reactive.ts el puente entre los signals y el DOM
- router.ts el router sobre la History API
Directoriostate/
- tasks.ts el estado del dominio
Directorioviews/
- tablero.ts
- detalle.ts
- acerca.ts
- no-encontrada.ts
Tres carpetas con responsabilidades claras: state no sabe que existe el DOM, views no
sabe cómo se guardan las tareas, y lib no sabe nada del dominio.
Sin framework, y por qué
TaskFlow usa Vite, TypeScript y una sola dependencia de runtime.
{ "name": "taskflow", "version": "0.1.0", "private": true, "type": "module", "description": "SPA sin framework del Workshop 01: reactividad con signals sobre el DOM nativo", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json" }, "dependencies": { "@preact/signals-core": "^1.14.4" }, "devDependencies": { "@types/node": "^24.9.2", "typescript": "^7.0.2", "vite": "^8.2.2" }}@preact/signals-core pesa alrededor de 1,6 kB y expone cinco funciones. No arrastra
Preact ni React: es el paquete de reactividad suelto.
La razón es pedagógica. Un framework resuelve dos problemas: mantener el estado y reflejarlo en el DOM. Si los resuelves tú, en unas cien líneas, entiendes qué hace React por debajo y en qué casos no lo necesitas. Y de paso el bundle queda en unos pocos kilobytes, lo que hace que la lección de caché se vea con total claridad.
Esto no es una recomendación de no usar frameworks. Para una aplicación de cincuenta pantallas con formularios complejos, un framework te ahorra meses. El punto es que la decisión sea informada, y que veas que la parte de AWS es idéntica en ambos casos.
La configuración de Vite y los dos tsconfig
import { defineConfig } from 'vite';
/** * La versión y el commit se inyectan en tiempo de build. El pipeline los pasa * como variables de entorno a partir del tag de git, así la app puede mostrar * exactamente qué release está sirviendo el CDN. */const version = process.env.APP_VERSION ?? 'dev';const commit = process.env.APP_COMMIT ?? 'local';
export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(version), __APP_COMMIT__: JSON.stringify(commit.slice(0, 7)), }, build: { // Nombres con hash: son la base de la estrategia de caché en CloudFront. // Estos archivos se pueden cachear para siempre porque si cambian, cambia // su nombre. El index.html es el único que nunca se cachea. assetsDir: 'assets', sourcemap: true, rollupOptions: { output: { entryFileNames: 'assets/[name].[hash].js', chunkFileNames: 'assets/[name].[hash].js', assetFileNames: 'assets/[name].[hash][extname]', }, }, }, server: { port: 5173, },});define reemplaza __APP_VERSION__ y __APP_COMMIT__ por literales en tiempo de build.
Es lo que probaste en el paso 3.4.
entryFileNames con [hash] es lo que probaste en el paso 3.3, y la base de toda la
estrategia de caché.
{ "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler",
// Solo tipos del navegador. Si algo en src/ intenta usar `process` o // `fs`, el typecheck falla, que es exactamente lo que queremos: este // código corre en un navegador, no en Node. "types": ["vite/client"],
"strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true,
"isolatedModules": true, "verbatimModuleSyntax": true, "noEmit": true, "skipLibCheck": true }, "include": ["src"]}Hay dos tsconfig, y no es capricho. Este cubre src/ y declara types: ["vite/client"]:
si algo en src/ intentara usar process o fs, el typecheck fallaría, y eso es
deseable porque ese código corre en un navegador.
vite.config.ts sí corre en Node y necesita process, así que vive en
tsconfig.node.json con types: ["node"]. Es la convención de Vite y evita que los
tipos de Node se filtren a la aplicación.
Tres opciones que normalmente no se activan y aquí sí: noUncheckedIndexedAccess hace
que array[0] tenga tipo T | undefined, lo que elimina una categoría entera de errores
en tiempo de ejecución; exactOptionalPropertyTypes distingue «la propiedad no está» de
«la propiedad vale undefined»; y verbatimModuleSyntax obliga a escribir import type
cuando importas solo un tipo.
El HTML, y qué no es reactivo
<!doctype html><html lang="es"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>TaskFlow</title> <meta name="description" content="Tablero de tareas del Workshop 01" /> <meta name="color-scheme" content="light dark" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> </head> <body> <header class="cabecera"> <a href="/" data-link class="marca">TaskFlow</a> <nav aria-label="Navegación principal"> <a href="/" data-link>Tablero</a> <a href="/acerca" data-link>Acerca</a> </nav> </header>
<!-- El router monta cada vista aquí dentro. --> <main id="app"></main>
<footer class="pie"> <span id="version" aria-label="Versión desplegada"></span> </footer>
<script type="module" src="/src/main.ts"></script> </body></html>Fíjate en lo que no es dinámico: la cabecera, la navegación y el pie son HTML
estático. No todo en una SPA necesita ser reactivo, y el router monta las vistas solo
dentro de <main id="app">.
Los enlaces llevan data-link. Ese atributo es la marca que el router usa para
interceptar el clic y navegar sin recargar. Un enlace sin data-link provoca una
navegación normal del navegador, lo cual sigue siendo útil para enlaces externos.
Resumen de comandos
Sección titulada «Resumen de comandos»make install # dependencias desde el lockmake dev # http://localhost:5173make build # a dist/make check # typecheckmake clean # si algo se corrompióAhora cómo funciona la reactividad.