vanilla-jet 1.4.3 → 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/CHANGELOG.md +46 -0
- package/bin.js +25 -0
- package/framework/dipper.js +31 -0
- package/framework/router.js +36 -1
- package/framework/server.js +19 -6
- package/framework/sw.template.js +82 -0
- package/gulpfile.js +19 -4
- package/master.md +450 -0
- package/package.json +2 -3
- package/scripts/compile_html.js +14 -6
- package/scripts/generate_sw.js +177 -0
- package/test/config.test.js +47 -0
- package/test/dipper.test.js +76 -0
- package/test/helpers.js +66 -0
- package/test/router.test.js +58 -0
- package/test/server.test.js +103 -0
- package/test/service-worker.test.js +118 -0
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.
|
|
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
|
|
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",
|
package/scripts/compile_html.js
CHANGED
|
@@ -9,9 +9,17 @@ let Functions = require('../framework/functions.js');
|
|
|
9
9
|
let Dipper = require('../framework/dipper.js');
|
|
10
10
|
let Config = require(processCwd() + '/config.js');
|
|
11
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
|
+
|
|
12
19
|
// -- Init Dipper
|
|
13
20
|
let settings = Config.settings;
|
|
14
|
-
|
|
21
|
+
if (settings['shared']) { settings['shared']['environment'] = env; }
|
|
22
|
+
let opts = settings[env] || settings['profile'] || {},
|
|
15
23
|
shared = settings['shared'] || {};
|
|
16
24
|
const dipper = new Dipper(opts, shared);
|
|
17
25
|
|
|
@@ -42,11 +50,11 @@ function main() {
|
|
|
42
50
|
// -- Get home.html
|
|
43
51
|
let homePageName = 'home.html';
|
|
44
52
|
getHtmlFromPage(homePageName).then((htmlContent) => {
|
|
45
|
-
if (htmlContent) {
|
|
46
|
-
// --
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
const htmlContentLines =
|
|
53
|
+
if (htmlContent) {
|
|
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".
|
|
57
|
+
const htmlContentLines = htmlContent.split('\n');
|
|
50
58
|
let lines = Array.from(htmlContentLines);
|
|
51
59
|
// -- Iterate over each line
|
|
52
60
|
for (let line of htmlContentLines) {
|