vanilla-jet 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/master.md ADDED
@@ -0,0 +1,450 @@
1
+ # VanillaJet — Documento maestro
2
+
3
+ > Documento canónico del proyecto: qué es, cómo funciona de punta a punta, cómo se construye,
4
+ > cómo corre en runtime, cómo se prueba y cómo mejorar su performance.
5
+ > Si solo vas a leer un archivo de este repo, lee este.
6
+
7
+ - **Paquete npm:** `vanilla-jet`
8
+ - **Versión actual:** `1.4.3`
9
+ - **Tipo:** framework + CLI para construir y servir SPAs (single page apps) sobre Node.js puro (sin Express).
10
+ - **Docs relacionadas:** [`README.md`](./README.md), [`CHANGELOG.md`](./CHANGELOG.md), [`ROADMAP_INTEGRAL.md`](./ROADMAP_INTEGRAL.md), [`docs/router.md`](./docs/router.md), [`docs/benchmark-static.md`](./docs/benchmark-static.md), [`docs/deployment/`](./docs/deployment/).
11
+
12
+ ---
13
+
14
+ ## 1. Qué es VanillaJet
15
+
16
+ VanillaJet es un framework "todo en uno" para apps de página única (SPA) que cubre dos roles:
17
+
18
+ 1. **Build pipeline (Gulp):** toma los fuentes de un proyecto consumidor (`assets/`) y produce artefactos optimizados en `public/` (JS minificado y concatenado, CSS compilado desde LESS, HTML compilado desde Nunjucks, y versiones `.gz`).
19
+ 2. **Servidor de runtime (Node http/http2):** sirve esos artefactos y resuelve rutas dinámicas a través de "endpoints" (clases) y un router estilo Backbone.
20
+
21
+ Punto clave para entenderlo: **este repositorio es el _paquete_ del framework, NO una app.** No contiene `assets/`, `public/`, `config.js` ni `vanillaJet.package.json`. Esos archivos viven en el **proyecto consumidor** que hace `npm install vanilla-jet`. El framework opera sobre el `process.cwd()` del consumidor.
22
+
23
+ ---
24
+
25
+ ## 2. Arquitectura de alto nivel
26
+
27
+ ```
28
+ ┌──────────────────────────────────────────────────────────────────────┐
29
+ │ PROYECTO CONSUMIDOR (cwd) │
30
+ │ │
31
+ │ assets/ public/ (generado por el build) │
32
+ │ ├─ pages/home.html ├─ pages/home.html(.gz) │
33
+ │ ├─ templates/**/*.html ├─ scripts/vanilla.min.js(.gz) │
34
+ │ ├─ scripts/**/*.js ├─ styles/app.min.css(.gz) │
35
+ │ └─ styles/less/admin.less ├─ images/ fonts/ anims/ │
36
+ │ config.js │
37
+ │ vanillaJet.package.json │
38
+ │ │ ▲ │
39
+ │ │ require('vanilla-jet') │ sirve artefactos │
40
+ │ ▼ │ │
41
+ │ ┌───────────────────── node_modules/vanilla-jet ─────────────────┐ │
42
+ │ │ index.js → { Server } │ │
43
+ │ │ framework/ server · router · request · response · dipper · ... │ │
44
+ │ │ gulpfile.js + scripts/compile_html.js (build) │ │
45
+ │ │ bin.js (CLI: setup | dev | build) │ │
46
+ │ └──────────────────────────────────────────────────────────────────┘ │
47
+ └──────────────────────────────────────────────────────────────────────┘
48
+ ```
49
+
50
+ Dos planos de ejecución que conviene NO confundir:
51
+
52
+ - **Build-time:** Gulp + `scripts/compile_html.js`. Corre Nunjucks, LESS, uglify, gzip. Genera `public/`.
53
+ - **Run-time:** `framework/server.js` levanta un servidor Node. Para páginas HTML **no** vuelve a renderizar Nunjucks: hace _stream_ del archivo ya compilado en `public/pages/`. Para assets estáticos hace _stream_ desde `public/`. Para rutas dinámicas invoca el método del endpoint.
54
+
55
+ ---
56
+
57
+ ## 3. Estructura del repositorio (el paquete)
58
+
59
+ | Ruta | Rol |
60
+ |---|---|
61
+ | `index.js` | Punto de entrada del paquete: `module.exports = { Server }`. |
62
+ | `bin.js` | CLI (`vanilla-jet setup\|dev\|build`). Despacha a Gulp vía `npx`. |
63
+ | `framework/server.js` | Clase `Server`: arma `global.render` (Nunjucks), `global.dipper`, crea el servidor http/http2, instancia router y endpoints. |
64
+ | `framework/router.js` | Clase `Router`: matching de rutas (regex estilo Backbone), serving de estáticos (caché de metadata, negociación br/gz, `304`, streaming). |
65
+ | `framework/request.js` | Clase `Request`: parseo de URL, método, params GET/POST, body, `accept-encoding`. |
66
+ | `framework/response.js` | Clase `Response`: headers, status, `respond()`, y `render()` (stream de páginas precompiladas con fallback `.br/.gz/original`). |
67
+ | `framework/dipper.js` | "Dipper" = gestor de recursos: registra/encola scripts, styles, fonts, animaciones, meta tags, Sentry, environment, y URLs versionadas. |
68
+ | `framework/functions.js` | `Functions.hydrate(dipper)`: lee `vanillaJet.package.json` y registra scripts/styles/fonts core + meta tags + Open Graph. |
69
+ | `gulpfile.js` | Pipeline de build (tareas Gulp). |
70
+ | `scripts/compile_html.js` | Compila `assets/pages/home.html` + templates a `public/pages/home.html` (+ `.gz`). |
71
+ | `scripts/benchmark-static.js` | Benchmark reproducible de serving estático (warm/cold). |
72
+ | `.scripts/generate_packages_json.js` | Genera `vanillaJet.package.json` base si no existe (comando `setup`). |
73
+ | `test/` | Harness de pruebas (smoke tests con `node --test`). Ver §10. |
74
+ | `docs/` | Router, benchmark y plantillas de despliegue (nginx + docker). |
75
+
76
+ ---
77
+
78
+ ## 4. Estructura esperada del proyecto consumidor
79
+
80
+ VanillaJet asume esta convención en el consumidor:
81
+
82
+ ```
83
+ assets/
84
+ pages/home.html # página raíz del SPA (incluye templates con include::)
85
+ templates/**/*.html # parciales Nunjucks (los *template.html se inyectan en bloque)
86
+ scripts/**/*.js # JS de la app (controllers, views, api, core, plugins)
87
+ styles/less/admin.less # entry point LESS
88
+ config.js # settings (profile / shared / security)
89
+ vanillaJet.package.json # dependencias front (scripts/styles/fonts/anims)
90
+ public/ # SALIDA del build (no se edita a mano)
91
+ ```
92
+
93
+ ### `config.js` (lo consume `Server`)
94
+ ```js
95
+ module.exports = {
96
+ settings: {
97
+ profile: { // obj.options
98
+ port: 8080,
99
+ https_server: false,
100
+ enable_precompressed_negotiation: false, // .br -> .gz -> original
101
+ request_timeout_ms: 30000,
102
+ headers_timeout_ms: 35000,
103
+ keep_alive_timeout_ms: 5000,
104
+ api_url: 'https://...' // expuesto al cliente vía includeEnvironment()
105
+ },
106
+ shared: { // datos compartidos build + cliente
107
+ site_name: 'Mi App',
108
+ description: '...',
109
+ environment: 'development',
110
+ version: '1.0.0',
111
+ sentry: { /* dsn_js, bundleVersion, bundleSha, sampleRate, ... */ }
112
+ },
113
+ security: {
114
+ pass_salt: '...', token_salt: '...', version: '1.0',
115
+ self_managed_certs: false, key: '...', cert: '...' // para http2 TLS propio
116
+ }
117
+ }
118
+ };
119
+ ```
120
+
121
+ ### `vanillaJet.package.json` (lo consume `Functions.hydrate` y `Dipper`)
122
+ ```jsonc
123
+ {
124
+ "coreDependencies": { "jquery": "//cdn...", "underscore": "//cdn...", ... },
125
+ "dependencies": { "miLib:dependeDe": "ruta/o/url.js" }, // ":dep" declara orden de carga
126
+ "styles": { "theme": "tema.css" },
127
+ "fonts": { "Roboto": [300,400,700] },
128
+ "anims": { "loader": "anims/loader.json" }
129
+ }
130
+ ```
131
+
132
+ ### Arranque típico del consumidor (`index.js` del consumidor)
133
+ ```js
134
+ const { Server } = require('vanilla-jet');
135
+ const Config = require('./config');
136
+
137
+ class AppEndpoint {
138
+ constructor(router) {
139
+ this.name = 'AppEndpoint';
140
+ router.setDefaultRoute('home'); // mapea la raíz '/'
141
+ router.addRoute('get', '/home', 'AppEndpoint.home');
142
+ }
143
+ home(request, response) {
144
+ response.render(request, 'home.html'); // stream de public/pages/home.html
145
+ return true; // <- IMPORTANTE: marca la request como atendida
146
+ }
147
+ }
148
+
149
+ new Server(Config, [AppEndpoint]).start();
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 5. Build pipeline (build-time) — paso a paso
155
+
156
+ Definido en `gulpfile.js`. Tarea `build` (`gulp.series`):
157
+
158
+ 1. **`cleanBuildJS`** → borra `public/scripts/vanilla.min.js`.
159
+ 2. **`uglifyJs`** → minifica todo `assets/scripts/**/*.js` (con `gulp-newer`, solo lo cambiado) a `public/scripts/**/*.min.js`.
160
+ 3. **`concatJs`** → concatena controllers + views + api + raíz en `public/scripts/vanilla.min.js` (excluye `core/` y `plugins/`).
161
+ 4. **`cleanMinified`** → borra los `.min.js` intermedios (api/controllers/views/app.min.js) ya concatenados.
162
+ 5. **`buildLess`** → compila `assets/styles/less/admin.less` → `public/styles/app.min.css` (LESS + `clean-css`) + livereload.
163
+ 6. **`compileTemplates`** → `node scripts/compile_html.js` (ver §6).
164
+ 7. **`gulp.parallel(compressJs, compressCss)`** → genera `.gz` (nivel 9) de `vanilla.min.js` y `app.min.css`.
165
+
166
+ Tarea **`dev`** = `build` + `watchFiles` (watchers de LESS, HTML y JS con livereload).
167
+
168
+ Comandos:
169
+ - `npm run dev` → `gulp dev --env development`
170
+ - `npm run build:qa | build:staging | build:prod`
171
+ - CLI: `npx vanilla-jet dev | build | setup`
172
+
173
+ > ⚠️ El build necesita la estructura del **consumidor** (`assets/`, `config.js`, `vanillaJet.package.json`). En este repo (el paquete) no existe, así que `gulp build` aquí no produce nada útil — se prueba dentro de un proyecto consumidor o con el harness (§10).
174
+
175
+ ---
176
+
177
+ ## 6. Compilación de HTML (`scripts/compile_html.js`)
178
+
179
+ No usa el bundler de Nunjucks a runtime; es un compilador propio orientado a un SPA de **una sola página**:
180
+
181
+ 1. Lee `assets/pages/home.html`.
182
+ 2. Lo renderiza con Nunjucks (contexto `{ app: dipper }`, así el template puede llamar `app.includeScripts()`, `app.metaTags()`, etc.).
183
+ 3. Recorre línea por línea buscando directivas `include::<nombre>`:
184
+ - `include::templates` → inyecta **todos** los parciales cuyo archivo contiene `template.html`.
185
+ - `include::otro.html` → inyecta ese parcial específico.
186
+ 4. Minifica el resultado con `html-minifier-terser` (colapsa whitespace, quita comentarios, minifica JS inline, etc.).
187
+ 5. Escribe `public/pages/home.html` y su `home.html.gz` (gzip nivel 9).
188
+
189
+ `Dipper` se hidrata aquí vía `Functions.hydrate(dipper)` para que los helpers de recursos estén disponibles dentro del template.
190
+
191
+ ---
192
+
193
+ ## 7. Runtime — flujo de una request
194
+
195
+ `framework/server.js` crea el servidor y delega cada request a `router.onRequest(req, res)`:
196
+
197
+ ```
198
+ req → new Response(res, options)
199
+ → new Request(req, { onDataReceived }) // junta body, parsea GET/POST, lee accept-encoding
200
+ → (al terminar el body) onDataReceived():
201
+ 1. si path == '' → path = defaultRoute
202
+ 2. recorre routes[get|post]; si regex matchea → handler "Clazz.metodo"
203
+ → validateCallback busca el endpoint y su método
204
+ → handled = callback(request, response, server) // el endpoint responde
205
+ 3. si NO se atendió y NO hubo match:
206
+ → ¿la extensión es un mime conocido? (png, css, js, svg, woff/ttf, pdf, json, ...)
207
+ sí → serving estático (ver §8)
208
+ no → 404
209
+ ```
210
+
211
+ Notas finas:
212
+ - El handler **debe retornar truthy** (`return true`) para marcar la request como atendida; si no, el router intentará tratarla como estático y probablemente caerá en `404`.
213
+ - La raíz `/` llega como `path === ''` (se quita el slash final), por eso se usa `router.setDefaultRoute('home')` para mapearla.
214
+ - `isProtectedFile()` bloquea (`404`) rutas dentro de `framework/`, `external/`, `node_modules/` y archivos de primer nivel.
215
+
216
+ ---
217
+
218
+ ## 8. Serving estático (corazón de la perf en Node)
219
+
220
+ En `framework/router.js`, para una ruta con extensión conocida:
221
+
222
+ 1. **Candidatos** (`getStaticCandidates`): para `vanilla.min.js`/`app.min.css` con cliente compatible, arma la lista `[.br?, .gz?, original]` según `enable_precompressed_negotiation` y `Accept-Encoding` (con soporte de `q=`). El resto de assets: solo el original.
223
+ 2. **Resolución con caché** (`resolveFirstAvailableStaticFile`): clave `route|accept-encoding`. Cachea qué archivo concreto sirve cada combinación (`staticResolutionCache`) y la metadata `size/mtime/etag` (`staticMetadataCache`).
224
+ 3. **Revalidación condicional**: si la request trae `If-None-Match`/`If-Modified-Since`, se fuerza refresh de metadata; si valida, responde `304` sin cuerpo.
225
+ 4. **Headers** (`buildStaticHeaders`): `Content-Type`, `Content-Length`, `ETag` (`W/"size-mtime"`), `Last-Modified`, `Vary: Accept-Encoding` (si hubo negociación), y `Cache-Control: no-cache, must-revalidate`.
226
+ 5. **Streaming**: `fs.createReadStream` (chunk 128 KB), con limpieza si el cliente cierra la conexión (`res.on('close')`).
227
+
228
+ > 📌 **Importante (ver §12):** ese `Cache-Control: no-cache, must-revalidate` se aplica a **todos** los assets, lo que obliga a revalidar cada archivo en cada carga. Es el principal candidato a "se siente lento" en visitas repetidas, y es un cambio respecto a 1.3.2.
229
+
230
+ `response.render()` (páginas HTML) usa la misma idea de fallback `.br → .gz → original` pero **no** fija `Cache-Control` (lo deja al heurístico del navegador, correcto para HTML).
231
+
232
+ ---
233
+
234
+ ## 9. El "Dipper" (gestor de recursos)
235
+
236
+ `framework/dipper.js` es el helper que los templates usan para construir el `<head>`/`<body>`:
237
+
238
+ - **Registro/encolado:** `registerScript/Style`, `enqueueScript/Style`, `dequeueScript/Style` (resuelven dependencias vía `requires`).
239
+ - **Inclusión:** `includeScripts()`, `includeStyles()`, `includeAnimations()`, `includeManifest()`, `metaTags()`.
240
+ - **URLs versionadas:** `script()`/`style()` pasan por `versionedUrl()`, que añade `?v=<size>-<mtime>` leyendo el archivo en disco (cache-busting determinista). `img()`/`pdf()` **no** versionan.
241
+ - **Integraciones:** `includeSentry()` (CDN + init según `shared.sentry`), `includeEnvironment()` (expone `ENVIRONMENT`, `API_URL`, `VERSION` al cliente).
242
+ - **Fonts:** `get_google_fonts()` arma la URL de Google Fonts a partir de `vanillaJet.package.json#fonts`.
243
+
244
+ `Functions.hydrate(dipper)` es lo que conecta `vanillaJet.package.json` con el Dipper: registra fonts, styles, dependencies (en orden por `clave:dependencia`), el core `vanillaJet.min.js`, el bundle `vanilla.min.js`, y los meta tags básicos + Open Graph.
245
+
246
+ ---
247
+
248
+ ## 10. Harness de pruebas (`test/`)
249
+
250
+ Antes no existían pruebas (`npm test` era un `console.log`). Se agregó un harness con el runner nativo `node --test` (sin dependencias nuevas):
251
+
252
+ | Archivo | Cubre |
253
+ |---|---|
254
+ | `test/router.test.js` | `routeToRegExp` (params/optionals/splats), `isProtectedFile`, `supportsEncoding` (con `q=`). |
255
+ | `test/dipper.test.js` | `urlTo`, `versionedUrl` (`?v=` y passthrough de URLs externas), register/enqueue/dequeue, salida de `includeScript`/`includeStyle`. |
256
+ | `test/server.test.js` | Levanta un `Server` real en puerto efímero contra un workspace temporal: ruta dinámica → `200`; estático → `200` + headers + `304` con `If-None-Match`; ruta protegida y archivo inexistente → `404`. |
257
+ | `test/helpers.js` | Utilidades: crear workspace temporal, levantar/cerrar server, request HTTP. |
258
+
259
+ Correr:
260
+ ```bash
261
+ npm test # node --test test/
262
+ node --test test/server.test.js # un archivo
263
+ npm run benchmark:static # benchmark de serving estático (warm/cold)
264
+ ```
265
+
266
+ El harness sirve como **red de seguridad para los cambios de performance**: cambia el `Cache-Control`, la negociación o el bundling, y `npm test` confirma que el contrato de routing/estáticos/404/304 sigue intacto. La regla del roadmap "todo cambio con medición antes/después" se apoya en `npm test` + `npm run benchmark:static`.
267
+
268
+ ---
269
+
270
+ ## 11. Despliegue
271
+
272
+ Plantillas en `docs/deployment/` (nginx + docker-compose + Dockerfile). Patrón recomendado:
273
+
274
+ - **nginx al frente** sirviendo `public/` (estáticos) con HTTP/2, brotli/gzip y cache headers fuertes; proxy_pass a Node solo para rutas dinámicas.
275
+ - **Node** corriendo el `Server` (puerto interno), detrás de nginx.
276
+ - Variables clave: `port`, `enable_precompressed_negotiation`, certificados (si `self_managed_certs`), `SENTRY_RELEASE`.
277
+
278
+ CI: `.github/workflows/deploy.yml` publica a npm en push a `main` (setup-node → `npm ci` → `npm run build --if-present` → `npm test` → `npm publish` → tag).
279
+
280
+ ---
281
+
282
+ ## 12. Diagnóstico de performance y recomendaciones
283
+
284
+ > **Hallazgo medido:** el serving estático de Node **no** es el cuello de botella. El benchmark
285
+ > (`npm run benchmark:static`, archivo de 512 KB en localhost) da **p95 warm ~0.5 ms / cold ~0.8 ms**,
286
+ > ~2900 req/s. El servidor responde rapidísimo. La lentitud percibida ("las apps tardan en renderizar")
287
+ > viene del **cliente/red** y del **build**, no del CPU de Node.
288
+
289
+ Recomendaciones priorizadas (de mayor impacto / menor riesgo, hacia abajo):
290
+
291
+ ### P0 — Caché de assets (regresión vs 1.3.2, alto impacto en visitas repetidas)
292
+ Hoy **todos** los estáticos salen con `Cache-Control: no-cache, must-revalidate` (`router.js:282`). Como los assets ya van con fingerprint `?v=size-mtime`, esto es contraproducente: cada carga revalida cada archivo (round-trip por asset, aunque devuelva `304`). En 1.3.2 no había ese header.
293
+ - **Acción:** servir los assets versionados (`vanilla.min.js`, `app.min.css`, y todo lo que lleve `?v=`) con `Cache-Control: public, max-age=31536000, immutable`. Dejar `no-cache` **solo** para HTML.
294
+ - **Pre-requisito:** extender `versionedUrl()` también a imágenes (`img()`) antes de marcarlas `immutable`; lo no versionado puede quedarse con `ETag` + `max-age` moderado.
295
+ - **Impacto esperado:** elimina N round-trips de revalidación por carga; visible sobre todo en redes reales (no localhost).
296
+
297
+ ### P0 — Scripts no bloqueantes
298
+ jQuery, Underscore, Modernizr, respond.js, el core `vanillaJet.min.js` y el bundle `vanilla.min.js` se cargan como `<script>` bloqueantes. `registerScript` ya soporta `defer`/`async`; hoy no se usan.
299
+ - **Acción:** marcar `defer` en core + bundle; evaluar quitar **Modernizr 2.8.3** y **respond.js** (solo sirven para IE ≤8, muertos).
300
+ - **Impacto:** adelanta el first render; menos requests bloqueantes.
301
+
302
+ ### P0 — Google Fonts bloqueante
303
+ `get_google_fonts()` genera un `<link rel=stylesheet>` bloqueante.
304
+ - **Acción:** cargar como `async` (el Dipper ya soporta `registerStyle(..., async)` con `preload`+`onload`) y/o `font-display: swap`; añadir `preconnect` a `fonts.googleapis.com`/`fonts.gstatic.com`.
305
+
306
+ ### P1 — Brotli realmente activo
307
+ `enable_precompressed_negotiation` busca `.br`, pero el build solo genera `.gz`. Activar el flag hoy **no** sirve porque no existen los `.br`.
308
+ - **Acción:** agregar tarea Gulp que genere `.br` (brotli) de `vanilla.min.js`/`app.min.css`/`home.html`. Brotli ~15-20% más chico que gzip.
309
+
310
+ ### P1 — Edge estático con nginx
311
+ Aprovechar las plantillas de `docs/deployment/`: que **nginx** sirva `public/` con HTTP/2 + brotli + cache inmutable y Node solo atienda dinámico. Quita CPU de Node y mejora TTFB de assets.
312
+
313
+ ### P2 — Velocidad de BUILD (si "renderiza tardan" se refiere a compilar)
314
+ Esto es la HU 2.2 del roadmap (pendiente):
315
+ - `gulp-watch` está deprecado (polling pesado) → usar `gulp.watch` nativo.
316
+ - `uglify` es lento; **esbuild** minifica/empaqueta 10-100× más rápido (gran win de DX en `dev`).
317
+ - Cualquier cambio recompila el template completo; medir por tipo de cambio (JS/LESS/HTML).
318
+
319
+ ### P2 — Otros
320
+ - `drop_console: false` en uglify → `true` para prod (menos ruido/peso).
321
+ - `request.js` usa `url.parse()` (deprecado, warning en Node 24) → migrar a `new URL()`.
322
+ - Mega-bundle único (`vanilla.min.js` = todos los controllers/views/api): para apps grandes, considerar code-splitting/lazy por ruta.
323
+ - Nunjucks runtime con `noCache:true`: solo afecta `dipper.template()`; si se usa, cachear en prod.
324
+
325
+ ### Cómo medir antes/después
326
+ 1. **Servidor:** `npm run benchmark:static` (igual `BENCH_*` antes y después).
327
+ 2. **Cliente:** Lighthouse + pestaña Network del navegador en un consumidor real (mirar TTFB, waterfall de revalidaciones `304`, blocking time de scripts/fonts).
328
+ 3. **Build:** cronometrar `gulp build` y cada watcher por tipo de cambio.
329
+
330
+ ---
331
+
332
+ ## 13. Historial 1.3.2 → 1.4.3 (¿qué cambió y qué "jala"?)
333
+
334
+ `1.3.2` (commit `658c1e3`) fue la última versión "que jalaba de maravilla". De ahí en adelante:
335
+
336
+ | Versión | Cambios | ¿Aplica/funciona? |
337
+ |---|---|---|
338
+ | 1.3.3 | Caché de metadata estática + `304` (`If-None-Match`/`If-Modified-Since`) + headers `ETag`/`Last-Modified`/`Cache-Control: no-cache`. | ✅ Funciona. ⚠️ Introduce el `no-cache, must-revalidate` global (ver P0). |
339
+ | 1.3.4 | Negociación precompressed opt-in (`.br→.gz→original`) + `Vary`. | ✅ Funciona (pero `.br` no se genera en build → §P1). |
340
+ | 1.3.5 | Fallback precompressed en `response.render()` (HTML). | ✅ Funciona. |
341
+ | 1.3.6 | Hardening: `node_mudules→node_modules`, fixes en Dipper (`includeAnimations`, `dequeue*`), fix del `npm test` recursivo. | ✅ Funciona. |
342
+ | 1.4.1 | Fast-path estáticos: caché de resolución `route+accept-encoding`; versionado `?v=size-mtime`; watch JS/CSS dispara compile de templates; benchmark. | ✅ Funciona (benchmark verde, +35% warm vs cold). |
343
+ | 1.4.2/1.4.3 | Migración total a Gulp (fuera Grunt), `compile_html.js` movido a `scripts/`, timeouts defensivos del server, limpieza de streams en disconnect. | ✅ Funciona. |
344
+
345
+ **Veredicto:** los commits de 1.3.2 hacia adelante **sí aplican y corren bien** (server arranca, sirve estáticos, negocia compresión, hace 304). No hay regresión funcional bloqueante. Las "asperezas" son de **performance percibida** (caché agresiva en assets), no de que algo se rompa, más dos detalles de higiene:
346
+
347
+ - ⚠️ **`deploy.yml` (cambio sin commitear en working tree):** revierte la sintaxis a `::set-output name=...` (deshabilitada por GitHub Actions) y **borra** la línea `git push origin v<version>`. Esto **rompería el CI de publish**. Recomendación: descartar ese cambio local (`git checkout -- .github/workflows/deploy.yml`) y quedarse con la versión commiteada (`>> "$GITHUB_OUTPUT"` + push del tag).
348
+ - `.DS_Store` aparece modificado/trackeado; conviene `git rm --cached .DS_Store` y agregarlo a `.gitignore`.
349
+
350
+ ---
351
+
352
+ ## 14. Convenciones y gotchas
353
+
354
+ - **`process.cwd()` mágico:** varios helpers (`getCwd`, `processCwd`, `staticBasePath`) limpian sufijos como `/node_modules`, `/vanilla-jet`, `/.scripts`, `/scripts`, `core/framework`. Es para que el framework resuelva rutas relativas al **proyecto consumidor** sin importar desde dónde se invoque.
355
+ - **Globals:** `global.render` (Nunjucks) y `global.dipper` se setean en el constructor de `Server`. `dipper.template()` runtime depende de `global.render`.
356
+ - **`autoescape:false` en Nunjucks:** los templates confían en su propio contenido; cuidado con inyección si se renderiza input de usuario.
357
+ - **El endpoint debe `return true`** tras responder, o el router intentará servir estático y caerá en 404.
358
+ - **Raíz `/`:** mapéala con `router.setDefaultRoute('algo')` + ruta `get '/algo'`.
359
+ - **Sin tests históricos:** ahora hay harness (`test/`); úsalo como gate antes de tocar router/estáticos.
360
+
361
+ ---
362
+
363
+ ## 15. Mapa rápido "quiero tocar X → mira aquí"
364
+
365
+ | Quiero… | Archivo |
366
+ |---|---|
367
+ | Cambiar headers/caché de estáticos | `framework/router.js` (`buildStaticHeaders`, `getStaticCandidates`) |
368
+ | Cambiar fallback/headers de páginas HTML | `framework/response.js` (`render`) |
369
+ | Cómo se cargan scripts/styles/fonts/meta | `framework/dipper.js` + `framework/functions.js` |
370
+ | Versionado `?v=` de assets | `framework/dipper.js` (`versionedUrl`) |
371
+ | Matching de rutas | `framework/router.js` (`routeToRegExp`, `addRoute`, `onRequest`) |
372
+ | Pipeline de build / minify / gzip | `gulpfile.js` |
373
+ | Compilación de la página | `scripts/compile_html.js` |
374
+ | Timeouts / http2 / TLS del server | `framework/server.js` |
375
+ | Medir performance | `scripts/benchmark-static.js`, `docs/benchmark-static.md` |
376
+ | Pruebas / smoke | `test/` (`npm test`) |
377
+ | Service worker (cache offline) | `framework/sw.template.js`, `scripts/generate_sw.js`, `dipper.includeServiceWorker()` |
378
+
379
+ ---
380
+
381
+ ## 16. Service Worker (caché cache-first, opt-in) — desde v1.5.0
382
+
383
+ Para que las apps "vuelen" en visitas repetidas y redes lentas, VanillaJet puede generar y servir un
384
+ **service worker cache-first** que guarda los bundles locales y los sirve sin tocar la red. Es la
385
+ contraparte cliente del versionado `?v=` y evita las revalidaciones `304` por asset.
386
+
387
+ ### Activación (`config.js`)
388
+ ```js
389
+ profile: {
390
+ enable_service_worker: true,
391
+ service_worker: { // todo opcional
392
+ precache: ['/public/scripts/plugins/velocity.min.js'], // extras explícitos
393
+ on_demand_prefixes: ['/public/animations/', '/public/images/'],
394
+ cache_prefix: 'm1-app' // default: slug de shared.site_name
395
+ }
396
+ }
397
+ ```
398
+
399
+ ### Cómo funciona (3 piezas)
400
+ 1. **Build** — la tarea Gulp `generateServiceWorker` (en la serie `build` y en los watchers) corre
401
+ `scripts/generate_sw.js`, que parte de `framework/sw.template.js` y genera `public/sw.js`:
402
+ - **Precache** = core (`app.min.css`, `vanilla.min.js`, `core/vanillaJet.min.js`) + recursos
403
+ **locales** que el Dipper tiene encolados + los de `service_worker.precache` (solo archivos que existen).
404
+ - **Cache name** = `<prefix>-sw-<hash>` donde el hash es md5 de `ruta:size-mtime` de lo precacheado →
405
+ cualquier cambio de asset rota el cache y `activate()` purga los viejos.
406
+ - Los `match` usan `{ ignoreSearch: true }`, así el cache sigue sirviendo aunque cambie el `?v=`.
407
+ - Si el flag está apagado, **borra** cualquier `public/sw.js` previo (deja de controlar clientes).
408
+ 2. **Serve** — el router atiende `GET /sw.js` desde scope raíz con `Service-Worker-Allowed: /` y
409
+ `Cache-Control: no-cache` (solo cuando el flag está activo; si no, `/sw.js` da `404`).
410
+ 3. **Registro** — `app.includeServiceWorker()` (helper del Dipper, úsalo en `home.html` como
411
+ `includeSentry()`) inyecta el registro inline **web-only**. En WebViews nativas, el consumidor puede
412
+ poner `window.__VJ_DISABLE_SW__ = true` antes de que corra para no registrar y desinstalar uno previo.
413
+
414
+ ### Notas
415
+ - **Opt-in y backward compatible:** apagado por defecto; no afecta apps existentes.
416
+ - Para apps en WebView (ej. Flutter), registrar el SW no aporta y un SW atascado es difícil de recuperar
417
+ → por eso el guard `__VJ_DISABLE_SW__`.
418
+ - En `1.3.1` (sin versionado `?v=`) el SW casa por URL exacta; en `1.4.1+` (con `?v=`) **se requiere**
419
+ `ignoreSearch`, que la plantilla del framework ya trae.
420
+
421
+ ---
422
+
423
+ ## 17. Compatibilidad de `config.js` (fix v1.5.0)
424
+
425
+ Existen dos formas de `config.js` y el server soporta ambas (`settings[options.profile] || settings['profile']`):
426
+
427
+ - **Legacy (1.3.x):** `module.exports = { profile, settings: { development:{...}, qa:{...}, production:{...}, shared, security } }`.
428
+ El server indexa por el perfil activo (`settings[profile]`).
429
+ - **Anidada (docs nuevas):** `module.exports = { settings: { profile:{...}, shared, security } }`.
430
+
431
+ > ⚠️ Entre 1.3.1 y 1.4.3 esto se rompió (`settings['profile']` literal): un consumidor legacy recibía
432
+ > `{}` (sin puerto/`api_url`/environment). **Restaurado en v1.5.0.** Si vas a actualizar un proyecto desde
433
+ > 1.3.x, esta es la razón por la que el upgrade fallaba. Además, el server ahora respeta `process.env.PORT`
434
+ > (Cloud Run/Heroku) antes del puerto de config.
435
+
436
+ ### Regresiones 1.3.1 → 1.4.x corregidas en v1.5.0 (checklist de upgrade)
437
+
438
+ El refactor 1.4.x rompió varias cosas para consumidores 1.3.x (como Broker-App). Todas restauradas en v1.5.0:
439
+
440
+ 1. **`server.js` perfil:** `settings[options.profile] || settings['profile']` (antes `settings['profile']` literal → `opts={}`).
441
+ 2. **`bin.js`:** restaurados `build:qa` / `build:staging` / `build:prod` (el CLI 1.4.x solo tenía `build` → el build no hacía nada).
442
+ 3. **`compile_html.js` entorno:** resuelve `settings[env]` (env pasado por gulp `--env` → argv) e inyecta `api_url`/`environment` correctos; ya no renderiza el contenido del page como nombre de template (antes: `API_URL="undefined"` y `template not found`).
443
+ 4. **`gulpfile.js`:** reenvía `--env` a `compile_html.js` y `generate_sw.js`.
444
+ 5. **`port: 0`** preservado (nullish), y `process.env.PORT` con prioridad.
445
+ 6. **`zlib@1.0.5`** eliminado de `dependencies` (rompía `npm ci` con `node-waf`).
446
+
447
+ > Para Broker-App el SW vive ahora en el framework: `assets/sw.js` y su serving se eliminaron; `config.js`
448
+ > declara `enable_service_worker: true` + `service_worker.precache` (lista de plugins) + `cache_prefix: 'broker-app'`.
449
+ > El registro web-only + teardown nativo se queda en `assets/scripts/app.js` (registra `/sw.js`, que ahora sirve el framework).
450
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jet",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "VannilaJet framework",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "build:qa": "gulp build --env qa",
13
13
  "build:staging": "gulp build --env staging",
14
14
  "build:prod": "gulp build --env production",
15
- "test": "node -e \"console.log('No automated tests configured yet')\"",
15
+ "test": "node --test test/*.test.js",
16
16
  "benchmark:static": "node ./scripts/benchmark-static.js"
17
17
  },
18
18
  "repository": {
@@ -40,7 +40,6 @@
40
40
  "nodemon": "3.1.10",
41
41
  "nunjucks": "3.2.4",
42
42
  "underscore": ">= 1.12.x",
43
- "zlib": "1.0.5",
44
43
  "del": "^6.0.0",
45
44
  "gulp": "^4.0.2",
46
45
  "gulp-clean-css": "^4.3.0",
@@ -2,7 +2,6 @@
2
2
  const path = require("path"),
3
3
  fs = require("fs"),
4
4
  nunjucks = require('nunjucks'),
5
- identifier = 'templates',
6
5
  chalk = require('chalk'),
7
6
  zlib = require('zlib');
8
7
 
@@ -10,9 +9,17 @@ let Functions = require('../framework/functions.js');
10
9
  let Dipper = require('../framework/dipper.js');
11
10
  let Config = require(processCwd() + '/config.js');
12
11
 
12
+ // -- Resolve build environment (passed as argv by gulp). Supports env-keyed configs
13
+ // (settings[env], e.g. 'qa'/'production') and the nested 'profile' shape. This is what
14
+ // injects the correct api_url/environment into the page via the Dipper.
15
+ let env = process.argv[2] || Config.profile || 'development';
16
+ const ENV_ALIASES = { dev: 'development', prod: 'production', 'build:qa': 'qa', 'build:staging': 'staging', 'build:prod': 'production' };
17
+ env = ENV_ALIASES[env] || env;
18
+
13
19
  // -- Init Dipper
14
20
  let settings = Config.settings;
15
- let opts = settings['profile'] || {},
21
+ if (settings['shared']) { settings['shared']['environment'] = env; }
22
+ let opts = settings[env] || settings['profile'] || {},
16
23
  shared = settings['shared'] || {};
17
24
  const dipper = new Dipper(opts, shared);
18
25
 
@@ -44,7 +51,9 @@ function main() {
44
51
  let homePageName = 'home.html';
45
52
  getHtmlFromPage(homePageName).then((htmlContent) => {
46
53
  if (htmlContent) {
47
- // -- Divide content line by line
54
+ // -- Divide the page content line by line. The page itself is NOT rendered through
55
+ // Nunjucks (it only holds `include::` directives); each included template IS rendered.
56
+ // Rendering the raw page content as a template name breaks with "template not found".
48
57
  const htmlContentLines = htmlContent.split('\n');
49
58
  let lines = Array.from(htmlContentLines);
50
59
  // -- Iterate over each line
@@ -58,7 +67,7 @@ function main() {
58
67
  // -- Get template name
59
68
  var templateName = line.replace('include::', '');
60
69
  // -- Check if its name "templates" add all templates if not add specific one
61
- if (templateName === identifier) {
70
+ if (templateName === 'templates') {
62
71
 
63
72
  let allTemplatesCompiled = '';
64
73
  for (let templateName in templates) {
@@ -211,7 +220,7 @@ function cleanALine(line) {
211
220
 
212
221
  function processCwd() {
213
222
  return process.cwd()
214
- .replace('/.grunt', '')
223
+ .replace('/scripts', '')
215
224
  .replace('/gulp', '')
216
225
  .replace('/node_modules/vanilla-jet', '');
217
- }
226
+ }