vanilla-jet 1.4.0 → 1.4.1

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 CHANGED
@@ -4,14 +4,25 @@ All notable project changes are documented in this file.
4
4
 
5
5
  The format follows a structure inspired by Keep a Changelog and semantic versioning.
6
6
 
7
- ## [1.4.0] - 2026-02-19
7
+ ## [1.4.1] - 2026-02-19
8
8
 
9
- ### Highlights
9
+ ### Highlights (v1.4.1)
10
10
 
11
- - Added `dev:vite` and `build:vite` scripts to introduce a Vite-first JS/LESS pipeline without removing legacy Gulp commands.
12
- - Added `vite.config.js` and `.scripts/run_vite.js` to support consumer-root execution, compatibility output in `public/`, and a dev helper route (`/__vanillajet__/`).
13
- - Added CLI support in `bin.js` for `npx vanilla-jet dev:vite` and `npx vanilla-jet build:vite`.
14
- - Updated `README.md` to document legacy vs modern workflows for HU 2.1.
11
+ - Completed HU 2.1 (`Fast path de estaticos en Node`).
12
+ - Optimized static serving in `framework/router.js`:
13
+ - Added warm-path static resolution cache (`route + accept-encoding`) to avoid repeated candidate resolution work.
14
+ - Added bounded metadata revalidation window for conditional requests to reduce repeated `fs.stat` pressure.
15
+ - Consolidated static header assembly and reused mime header maps.
16
+ - Kept stream-based delivery for large assets and tuned `createReadStream` chunk size.
17
+ - Added reproducible local benchmark:
18
+ - New script: `npm run benchmark:static`.
19
+ - New guide: `docs/benchmark-static.md`.
20
+
21
+ ### Compatibility notes (v1.4.1)
22
+
23
+ - No public API changes.
24
+ - Preserves static conditional caching (`304`) and precompressed negotiation fallback.
25
+ - No intended behavior changes for dynamic routes.
15
26
 
16
27
  ## [1.3.6] - 2026-02-19
17
28
 
@@ -92,4 +103,4 @@ The format follows a structure inspired by Keep a Changelog and semantic version
92
103
  [1.3.4]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.4
93
104
  [1.3.5]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.5
94
105
  [1.3.6]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.6
95
- [1.4.0]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.0
106
+ [1.4.1]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.1
package/README.md CHANGED
@@ -6,7 +6,7 @@ Node.js framework for building SPA applications with a JS/CSS/HTML build pipelin
6
6
 
7
7
  ## Current version
8
8
 
9
- - Version: `1.4.0`
9
+ - Version: `1.4.1`
10
10
  - Changelog: see [`CHANGELOG.md`](./CHANGELOG.md)
11
11
  - Improvement plan (performance and backward compatibility): see `ROADMAP_INTEGRAL.md`
12
12
 
@@ -67,33 +67,17 @@ new Server(Config, [AppEndpoint]).start();
67
67
  From this repository:
68
68
 
69
69
  - `npm run setup`: generates a base `vanillaJet.package.json` if it does not exist.
70
- - `npm run dev`: legacy Gulp build + watcher for development.
71
- - `npm run dev:vite`: modern Vite dev server for JS/LESS DX (keeps Nunjucks flow unchanged).
72
- - `npm run build:vite`: modern Vite build for JS/LESS output in `public/`.
70
+ - `npm run dev`: build + watcher for development.
73
71
  - `npm run build:qa`: build for QA.
74
72
  - `npm run build:staging`: build for staging.
75
73
  - `npm run build:prod`: build for production.
74
+ - `npm run benchmark:static`: runs reproducible static serving benchmark (cold/warm).
76
75
 
77
76
  As CLI (`bin.js`):
78
77
 
79
78
  - `npx vanilla-jet setup`
80
79
  - `npx vanilla-jet dev`
81
80
  - `npx vanilla-jet build`
82
- - `npx vanilla-jet dev:vite`
83
- - `npx vanilla-jet build:vite`
84
-
85
- ## Legacy vs Vite mode
86
-
87
- - Legacy (`npm run dev` / `npx vanilla-jet dev`):
88
- - Keeps full historical Gulp pipeline (JS minify+concat, LESS, templates, gzip artifacts).
89
- - Recommended when you need 100% historical behavior.
90
- - Vite (`npm run dev:vite` / `npx vanilla-jet dev:vite`):
91
- - Focuses on JS/LESS developer experience with faster feedback.
92
- - Does not replace Nunjucks compilation in this stage (HU 2.1), so template flow remains legacy.
93
- - Dev helper page: open `http://localhost:5173/__vanillajet__/` to load the Vite entry.
94
- - Vite build (`npm run build:vite` / `npx vanilla-jet build:vite`):
95
- - Generates `public/scripts/vanilla.min.js` and `public/styles/app.min.css`.
96
- - Keeps existing legacy build commands available and unchanged.
97
81
 
98
82
  ## Expected consumer project structure
99
83
 
@@ -136,9 +120,23 @@ Behavior details:
136
120
  - Safe fallback: if `.br` or `.gz` does not exist, server serves the original file.
137
121
  - HTML rendering (`response.render`) also uses safe runtime fallback for precompressed templates (`.br`/`.gz`/original).
138
122
 
123
+ ## Static performance notes (HU 2.1)
124
+
125
+ Static serving includes a warm-path optimization focused on Node runtime latency:
126
+
127
+ - Reuses static resolution for repeated requests (`route + accept-encoding`).
128
+ - Reduces repeated metadata refresh with bounded revalidation windows.
129
+ - Keeps streaming strategy for large assets (`fs.createReadStream`) with tuned chunk size.
130
+ - Preserves conditional cache behavior (`ETag`/`Last-Modified` + `304`) and precompressed fallback contract.
131
+
132
+ Benchmark guide:
133
+
134
+ - [`docs/benchmark-static.md`](./docs/benchmark-static.md)
135
+
139
136
  ## Additional documentation
140
137
 
141
138
  - Router: `docs/router.md`
139
+ - Benchmark: [`docs/benchmark-static.md`](./docs/benchmark-static.md)
142
140
  - Version history: [`CHANGELOG.md`](./CHANGELOG.md)
143
141
  - Roadmap and improvements: `ROADMAP_INTEGRAL.md`
144
142
  - Deployment templates (nginx + docker): `docs/deployment/`
@@ -5,9 +5,9 @@ Cada historia incluye su ciclo completo: fases, tareas, entregables, metricas, c
5
5
 
6
6
  ## Objetivo
7
7
 
8
- - Modernizar el framework con `Vite` como base de DX.
9
- - Reducir dependencia de Node para servir frontend.
10
- - Adoptar `nginx` y Docker al final, con evidencia y sin romper legacy.
8
+ - Maximizar performance de serving estatico sobre Node sin romper compatibilidad.
9
+ - Acelerar el pipeline de compilacion en tiempo real (watch/recompile) para DX diaria.
10
+ - Mejorar flujo `dev` con apertura automatica de navegador y live reload estable.
11
11
 
12
12
  ## Reglas de ejecucion
13
13
 
@@ -115,121 +115,129 @@ Cada historia incluye su ciclo completo: fases, tareas, entregables, metricas, c
115
115
 
116
116
  ---
117
117
 
118
- ## EPIC 2 - Vite first (foco actual)
118
+ ## EPIC 2 - Performance Node + DX de compilacion (foco actual)
119
119
 
120
- ### HU 2.1 - `dev:vite` y `build:vite` sin romper legacy (completada `v1.4.0`)
120
+ ### HU 2.1 - Fast path de estaticos en Node (completada `v1.4.1`)
121
121
 
122
122
  #### Fases
123
- - F1: baseline.
124
- - F2: integrar scripts Vite.
125
- - F3: coexistencia con legacy.
126
- - F4: validacion en consumidor real.
123
+ - F1: profiling de request estatico.
124
+ - F2: optimizacion de lectura/respuesta.
125
+ - F3: validacion de no-regresion.
127
126
 
128
127
  #### Tareas
129
- - [x] Config Vite (JS/LESS).
130
- - [x] Mantener Nunjucks en esta etapa.
131
- - [x] Documentar `dev` legacy vs `dev:vite`.
128
+ - [x] Optimizar resolucion de archivo y headers para evitar trabajo repetido.
129
+ - [x] Revisar estrategia de `fs`/stream para minimizar latencia en assets grandes.
130
+ - [x] Agregar benchmark local reproducible (cold/warm cache).
132
131
 
133
132
  #### Entregables
134
- - Config y scripts de Vite.
133
+ - [x] Patch de performance en `framework/router.js`.
134
+ - [x] Script de benchmark local documentado.
135
135
 
136
136
  #### Metricas
137
- - Arranque dev >= 40% mas rapido.
138
- - Rebuild incremental >= 50% mas rapido.
137
+ - p95 de estaticos >= 20% mejor en escenario warm.
138
+ - Menor uso de CPU en serving concurrente.
139
139
 
140
140
  #### Criterios
141
- - HMR estable.
142
- - Legacy intacto.
141
+ - [x] Sin romper cache condicional (`304`) ni negociacion precompressed.
142
+ - [x] Sin impacto en rutas dinamicas.
143
143
 
144
144
  #### Documentacion
145
- - `README.md` + `CHANGELOG.md`.
145
+ - [x] `README.md` + `CHANGELOG.md` + guia de benchmark.
146
146
 
147
- ### HU 2.2 - Node deja de servir frontend en modo moderno (pendiente)
147
+ ### HU 2.2 - Recompile en tiempo real mas rapido (pendiente)
148
148
 
149
149
  #### Fases
150
- - F1: contrato `legacy` vs `modern`.
151
- - F2: flag de transicion.
152
- - F3: validacion de compatibilidad.
150
+ - F1: baseline de tiempos por tarea de build.
151
+ - F2: optimizacion incremental.
152
+ - F3: validacion en proyecto consumidor.
153
153
 
154
154
  #### Tareas
155
- - Node solo API/dinamico en modo moderno.
156
- - Frontend servido por Vite (dev) y luego Nginx (prod).
155
+ - Reducir trabajo redundante en `gulpfile.js` (globs, minify y concatenacion).
156
+ - Mejorar estrategia de watch para recompilar solo lo tocado.
157
+ - Medir tiempo de recompile por tipo de cambio (JS, LESS, HTML).
157
158
 
158
159
  #### Entregables
159
- - Contrato por entorno.
160
+ - Pipeline `dev` optimizado y medido.
160
161
 
161
162
  #### Metricas
162
- - Menos carga de static serving en Node.
163
+ - Recompile JS >= 35% mas rapido.
164
+ - Recompile LESS >= 30% mas rapido.
163
165
 
164
166
  #### Criterios
165
- - Modo moderno sin dependencia de `response.render()`.
166
- - Modo legacy intacto.
167
+ - Output final equivalente al flujo actual.
168
+ - Sin cambios obligatorios en estructura de proyectos consumidores.
167
169
 
168
170
  #### Documentacion
169
- - Guia de migracion de modo.
171
+ - `README.md` + `CHANGELOG.md`.
170
172
 
171
173
  ---
172
174
 
173
- ## EPIC 3 - Benchmark y decision gate
175
+ ## EPIC 3 - Developer Experience en `dev`
174
176
 
175
- ### HU 3.1 - Go/No-Go de Nginx basado en datos (pendiente)
177
+ ### HU 3.1 - Runner `dev` con navegador auto-open + live reload estable (pendiente)
176
178
 
177
179
  #### Fases
178
- - F1: dise;o benchmark A/B.
179
- - F2: ejecucion.
180
- - F3: analisis.
181
- - F4: decision.
180
+ - F1: contrato de ejecucion unificado.
181
+ - F2: implementacion runner.
182
+ - F3: validacion en Mac/Windows/Linux.
182
183
 
183
184
  #### Tareas
184
- - Medir p50/p95/p99, throughput, CPU, memoria.
185
- - Definir umbrales minimos de aprobacion.
185
+ - Abrir navegador automaticamente al iniciar `dev`.
186
+ - Disparar live reload al terminar recompile de assets/templates.
187
+ - Mantener modo fallback para entornos sin navegador.
186
188
 
187
189
  #### Entregables
188
- - Reporte benchmark.
189
- - Decision log Go/No-Go.
190
+ - Nuevo flujo `npm run dev` mas simple para consumidores.
191
+
192
+ #### Metricas
193
+ - Tiempo a primer render local menor.
194
+ - Menos pasos manuales de arranque en onboarding.
190
195
 
191
196
  #### Criterios
192
- - Decision trazable con evidencia.
197
+ - Reload confiable tras cada recompile exitoso.
198
+ - Sin romper el modo actual para usuarios legacy.
193
199
 
194
200
  #### Documentacion
195
- - Documento benchmark + resumen tecnico.
201
+ - `README.md` + `CHANGELOG.md` + guia de migracion de scripts.
196
202
 
197
203
  ---
198
204
 
199
- ## EPIC 4 - Adopcion final de infraestructura
205
+ ## EPIC 4 - Hardening operativo del runtime Node
200
206
 
201
- ### HU 4.1 - Nginx oficial para consumidores (pendiente)
207
+ ### HU 4.1 - Observabilidad y protecciones de runtime (pendiente)
202
208
 
203
209
  #### Fases
204
- - F1: template.
205
- - F2: staging.
206
- - F3: guia de operacion.
210
+ - F1: logging y metricas base.
211
+ - F2: alarmas y limites.
212
+ - F3: guia operativa.
207
213
 
208
214
  #### Tareas
209
- - `try_files`, SPA fallback, proxy a Node, cache y precompressed.
215
+ - Estandarizar logs de serving estatico y errores.
216
+ - Exponer metricas clave (latencia/errores/cache hits).
217
+ - Documentar manejo de picos y rollback.
210
218
 
211
219
  #### Entregables
212
- - Template y guia.
220
+ - Guia de operacion y checklist de hardening.
213
221
 
214
222
  #### Documentacion
215
- - `docs/deployment/` + `README.md`.
223
+ - `docs/` + `README.md`.
216
224
 
217
- ### HU 4.2 - Docker de referencia (pendiente)
225
+ ### HU 4.2 - Paquete de referencia para despliegue Node (pendiente)
218
226
 
219
227
  #### Fases
220
- - F1: Dockerfile.
221
- - F2: docker-compose.
222
- - F3: validacion server.
228
+ - F1: templates de entorno.
229
+ - F2: validacion en staging.
230
+ - F3: guia final.
223
231
 
224
232
  #### Tareas
225
- - Definir imagenes, puertos y variables.
226
- - Documentar rollback.
233
+ - Definir variables, puertos y recomendaciones de cache.
234
+ - Incluir ejemplo de despliegue reproducible para consumidores.
227
235
 
228
236
  #### Entregables
229
- - Dockerfile y compose de referencia.
237
+ - Template de despliegue y guia de puesta en marcha.
230
238
 
231
239
  #### Documentacion
232
- - `docs/deployment/` + `README.md`.
240
+ - `docs/` + `README.md`.
233
241
 
234
242
  ---
235
243
 
package/bin.js CHANGED
@@ -28,20 +28,4 @@ switch (args[0]) {
28
28
  console.error('Error executing gulp:', error.message);
29
29
  }
30
30
  break;
31
-
32
- case 'dev:vite':
33
- try {
34
- execSync('node ./.scripts/run_vite.js dev', { stdio: 'inherit', cwd: __dirname });
35
- } catch (error) {
36
- console.error('Error executing Vite dev:', error.message);
37
- }
38
- break;
39
-
40
- case 'build:vite':
41
- try {
42
- execSync('node ./.scripts/run_vite.js build', { stdio: 'inherit', cwd: __dirname });
43
- } catch (error) {
44
- console.error('Error executing Vite build:', error.message);
45
- }
46
- break;
47
31
  }
@@ -0,0 +1,45 @@
1
+ # Static serving benchmark guide
2
+
3
+ This benchmark measures static file serving in two reproducible scenarios:
4
+
5
+ - Warm cache latency/throughput with stable keep-alive traffic.
6
+ - Cold cache latency by forcing metadata/resolution misses on each request.
7
+
8
+ ## Run
9
+
10
+ ```bash
11
+ npm run benchmark:static
12
+ ```
13
+
14
+ ## Optional tuning variables
15
+
16
+ You can override defaults with environment variables:
17
+
18
+ - `BENCH_PORT` (default: `3199`)
19
+ - `BENCH_FILE_SIZE_KB` (default: `512`)
20
+ - `BENCH_TOTAL_REQUESTS` (default: `2000`)
21
+ - `BENCH_COLD_ITERATIONS` (default: `300`)
22
+
23
+ Example:
24
+
25
+ ```bash
26
+ BENCH_TOTAL_REQUESTS=4000 BENCH_COLD_ITERATIONS=600 npm run benchmark:static
27
+ ```
28
+
29
+ ## Output
30
+
31
+ The script reports for warm and cold scenarios:
32
+
33
+ - request count
34
+ - average latency
35
+ - `p50`, `p95`, `p99`
36
+ - max latency
37
+ - requests per second
38
+
39
+ It also prints warm `p95` improvement vs cold.
40
+
41
+ ## Notes for consistent results
42
+
43
+ - Close heavy apps before running benchmarks.
44
+ - Run at least 3 times and compare medians.
45
+ - Keep the same `BENCH_*` values before/after a performance patch.
@@ -16,8 +16,12 @@ class Router {
16
16
  this.defaultRoute = '';
17
17
  this.server = server;
18
18
  this.cwd = process.cwd();
19
+ this.staticBasePath = this.cwd.replace('core/framework', '');
19
20
  this.staticMetadataCache = new Map();
21
+ this.staticResolutionCache = new Map();
20
22
  this.staticFileWatchers = new Map();
23
+ this.staticMetadataMaxAgeMs = 1000;
24
+ this.staticStreamChunkSize = 128 * 1024;
21
25
  this.mimes = {
22
26
  'png': 'image/png',
23
27
  'webp': 'image/webp',
@@ -32,6 +36,10 @@ class Router {
32
36
  'pdf': 'application/pdf',
33
37
  'json': 'application/json'
34
38
  };
39
+ this.mimeHeaders = Object.keys(this.mimes).reduce((headers, ext) => {
40
+ headers[ext] = { 'Content-Type': this.mimes[ext] };
41
+ return headers;
42
+ }, {});
35
43
  this.compressionMimes = [ 'css', 'js' ];
36
44
  this.compressionFiles = [ 'vanilla.min.js', 'app.min.css' ];
37
45
  this.enablePrecompressedNegotiation = Boolean(server?.options?.enable_precompressed_negotiation);
@@ -102,14 +110,13 @@ class Router {
102
110
 
103
111
  if (obj.mimes[ext] != undefined && obj.mimes[ext] != 'undefined') {
104
112
  extHandled = true;
105
- extHeader = { 'Content-Type': obj.mimes[ext] };
113
+ extHeader = obj.mimeHeaders[ext];
106
114
  }
107
115
 
108
116
  if (extHandled) {
109
117
 
110
- let rep = obj.cwd.replace('core/framework', ''),
111
- route = request.path,
112
- filename = path.join(rep, route),
118
+ let route = request.path,
119
+ filename = path.join(obj.staticBasePath, route),
113
120
  filePrivate = obj.isProtectedFile(route);
114
121
 
115
122
  if (filePrivate) {
@@ -118,25 +125,13 @@ class Router {
118
125
 
119
126
  let staticCandidates = obj.getStaticCandidates(request, ext, filename);
120
127
  let hasConditionalHeaders = Boolean(req.headers['if-none-match'] || req.headers['if-modified-since']);
121
- obj.resolveFirstAvailableStaticFile(staticCandidates, hasConditionalHeaders, (err, staticFile) => {
128
+ obj.resolveFirstAvailableStaticFile(route, request.acceptEncoding, staticCandidates, hasConditionalHeaders, (err, staticFile) => {
122
129
  if (err || !staticFile) {
123
130
  return obj.onNotFound(response);
124
131
  }
125
132
 
126
- let staticHeaders = Object.assign({}, extHeader);
127
- if (staticFile.contentEncoding) {
128
- staticHeaders['Content-Encoding'] = staticFile.contentEncoding;
129
- }
130
- if (staticCandidates.some((candidate) => candidate.contentEncoding)) {
131
- staticHeaders['Vary'] = 'Accept-Encoding';
132
- }
133
-
134
133
  let metadata = staticFile.metadata;
135
- staticHeaders['Content-Length'] = metadata.size;
136
- staticHeaders['ETag'] = metadata.etag;
137
- staticHeaders['Last-Modified'] = metadata.lastModified;
138
- // Force revalidation to keep clients fresh without hard reload.
139
- staticHeaders['Cache-Control'] = 'no-cache, must-revalidate';
134
+ let staticHeaders = obj.buildStaticHeaders(extHeader, staticCandidates, staticFile.contentEncoding, metadata);
140
135
 
141
136
  if (obj.isNotModified(req, metadata)) {
142
137
  let notModifiedHeaders = Object.assign({}, staticHeaders);
@@ -145,8 +140,12 @@ class Router {
145
140
  return res.end();
146
141
  }
147
142
 
148
- const fileStream = fs.createReadStream(staticFile.filename);
143
+ const fileStream = fs.createReadStream(staticFile.filename, {
144
+ highWaterMark: obj.staticStreamChunkSize
145
+ });
149
146
  fileStream.on('error', (streamErr) => {
147
+ obj.staticMetadataCache.delete(staticFile.filename);
148
+ obj.staticResolutionCache.clear();
150
149
  console.error("Error reading file:", streamErr);
151
150
  res.writeHead(500);
152
151
  res.end('Server Error');
@@ -171,6 +170,10 @@ class Router {
171
170
  return callback(null, cachedMetadata);
172
171
  }
173
172
 
173
+ if (cachedMetadata && forceRefresh && !obj.shouldRefreshConditionalMetadata(cachedMetadata)) {
174
+ return callback(null, cachedMetadata);
175
+ }
176
+
174
177
  fs.stat(filename, (err, stats) => {
175
178
  if (err || !stats.isFile()) {
176
179
  return callback(err || new Error('File not found'));
@@ -179,7 +182,8 @@ class Router {
179
182
  let metadata = {
180
183
  size: stats.size,
181
184
  lastModified: stats.mtime.toUTCString(),
182
- etag: `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`
185
+ etag: `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`,
186
+ cachedAt: Date.now()
183
187
  };
184
188
 
185
189
  obj.staticMetadataCache.set(filename, metadata);
@@ -214,8 +218,24 @@ class Router {
214
218
  return compressedCandidates.concat(candidates);
215
219
  }
216
220
 
217
- resolveFirstAvailableStaticFile(candidates, forceRefresh, callback) {
221
+ resolveFirstAvailableStaticFile(route, acceptEncoding, candidates, forceRefresh, callback) {
218
222
  let obj = this;
223
+ let resolutionKey = obj.getStaticResolutionKey(route, acceptEncoding);
224
+ let cachedResolution = obj.staticResolutionCache.get(resolutionKey);
225
+ if (cachedResolution) {
226
+ return obj.getStaticFileMetadata(cachedResolution.filename, forceRefresh, (cachedErr, cachedMetadata) => {
227
+ if (!cachedErr && cachedMetadata) {
228
+ return callback(null, {
229
+ filename: cachedResolution.filename,
230
+ contentEncoding: cachedResolution.contentEncoding,
231
+ metadata: cachedMetadata
232
+ });
233
+ }
234
+ obj.staticResolutionCache.delete(resolutionKey);
235
+ obj.resolveFirstAvailableStaticFile(route, acceptEncoding, candidates, forceRefresh, callback);
236
+ });
237
+ }
238
+
219
239
  let index = 0;
220
240
  function resolveCandidate() {
221
241
  let currentCandidate = candidates[index];
@@ -225,6 +245,10 @@ class Router {
225
245
 
226
246
  obj.getStaticFileMetadata(currentCandidate.filename, forceRefresh, (err, metadata) => {
227
247
  if (!err && metadata) {
248
+ obj.staticResolutionCache.set(resolutionKey, {
249
+ filename: currentCandidate.filename,
250
+ contentEncoding: currentCandidate.contentEncoding
251
+ });
228
252
  return callback(null, {
229
253
  filename: currentCandidate.filename,
230
254
  contentEncoding: currentCandidate.contentEncoding,
@@ -239,6 +263,35 @@ class Router {
239
263
  resolveCandidate();
240
264
  }
241
265
 
266
+ getStaticResolutionKey(route, acceptEncoding) {
267
+ let normalizedEncodings = Array.isArray(acceptEncoding) ? acceptEncoding.join(',') : '';
268
+ return `${route}|${normalizedEncodings}`;
269
+ }
270
+
271
+ shouldRefreshConditionalMetadata(metadata) {
272
+ if (!metadata || !metadata.cachedAt) {
273
+ return true;
274
+ }
275
+ return Date.now() - metadata.cachedAt > this.staticMetadataMaxAgeMs;
276
+ }
277
+
278
+ buildStaticHeaders(extHeader, candidates, contentEncoding, metadata) {
279
+ let staticHeaders = Object.assign({}, extHeader);
280
+ if (contentEncoding) {
281
+ staticHeaders['Content-Encoding'] = contentEncoding;
282
+ }
283
+ if (candidates.some((candidate) => candidate.contentEncoding)) {
284
+ staticHeaders['Vary'] = 'Accept-Encoding';
285
+ }
286
+
287
+ staticHeaders['Content-Length'] = metadata.size;
288
+ staticHeaders['ETag'] = metadata.etag;
289
+ staticHeaders['Last-Modified'] = metadata.lastModified;
290
+ // Force revalidation to keep clients fresh without hard reload.
291
+ staticHeaders['Cache-Control'] = 'no-cache, must-revalidate';
292
+ return staticHeaders;
293
+ }
294
+
242
295
  supportsEncoding(acceptEncoding, encoding) {
243
296
  if (!Array.isArray(acceptEncoding)) {
244
297
  return false;
@@ -279,6 +332,7 @@ class Router {
279
332
  try {
280
333
  let watcher = fs.watch(filename, (eventType) => {
281
334
  obj.staticMetadataCache.delete(filename);
335
+ obj.staticResolutionCache.clear();
282
336
  if (eventType === 'rename') {
283
337
  let renamedWatcher = obj.staticFileWatchers.get(filename);
284
338
  if (renamedWatcher) {
@@ -290,6 +344,7 @@ class Router {
290
344
 
291
345
  watcher.on('error', () => {
292
346
  obj.staticMetadataCache.delete(filename);
347
+ obj.staticResolutionCache.clear();
293
348
  let activeWatcher = obj.staticFileWatchers.get(filename);
294
349
  if (activeWatcher) {
295
350
  activeWatcher.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jet",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "VannilaJet framework",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,12 +9,11 @@
9
9
  "scripts": {
10
10
  "setup": "node ./.scripts/generate_packages_json.js",
11
11
  "dev": "gulp dev --env development",
12
- "dev:vite": "node ./.scripts/run_vite.js dev",
13
- "build:vite": "node ./.scripts/run_vite.js build",
14
12
  "build:qa": "gulp build --env qa",
15
13
  "build:staging": "gulp build --env staging",
16
14
  "build:prod": "gulp build --env production",
17
- "test": "node -e \"console.log('No automated tests configured yet')\""
15
+ "test": "node -e \"console.log('No automated tests configured yet')\"",
16
+ "benchmark:static": "node ./scripts/benchmark-static.js"
18
17
  },
19
18
  "repository": {
20
19
  "type": "git",
@@ -33,6 +32,15 @@
33
32
  "dependencies": {
34
33
  "blueimp-md5": "2.19.0",
35
34
  "chalk": "4.1.2",
35
+ "html-minifier-terser": "7.2.0",
36
+ "js-beautify": "1.15.4",
37
+ "jsrsasign": "11.1.0",
38
+ "jwt-simple": "0.5.6",
39
+ "minimist": "1.2.8",
40
+ "nodemon": "3.1.10",
41
+ "nunjucks": "3.2.4",
42
+ "underscore": ">= 1.12.x",
43
+ "zlib": "1.0.5",
36
44
  "del": "^6.0.0",
37
45
  "gulp": "^4.0.2",
38
46
  "gulp-clean-css": "^4.3.0",
@@ -45,16 +53,6 @@
45
53
  "gulp-rename": "^2.0.0",
46
54
  "gulp-shell": "^0.8.0",
47
55
  "gulp-uglify": "^3.0.2",
48
- "gulp-watch": "^5.0.1",
49
- "html-minifier-terser": "7.2.0",
50
- "js-beautify": "1.15.4",
51
- "jsrsasign": "11.1.0",
52
- "jwt-simple": "0.5.6",
53
- "minimist": "1.2.8",
54
- "nodemon": "3.1.10",
55
- "nunjucks": "3.2.4",
56
- "underscore": ">= 1.12.x",
57
- "vite": "^7.3.1",
58
- "zlib": "1.0.5"
56
+ "gulp-watch": "^5.0.1"
59
57
  }
60
58
  }
@@ -0,0 +1,201 @@
1
+ const fs = require('fs');
2
+ const http = require('http');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { performance } = require('perf_hooks');
6
+
7
+ const Server = require('../framework/server.js');
8
+
9
+ const BENCH_PORT = Number(process.env.BENCH_PORT || 3199);
10
+ const BENCH_FILE_SIZE_KB = Number(process.env.BENCH_FILE_SIZE_KB || 512);
11
+ const BENCH_TOTAL_REQUESTS = Number(process.env.BENCH_TOTAL_REQUESTS || 2000);
12
+ const BENCH_COLD_ITERATIONS = Number(process.env.BENCH_COLD_ITERATIONS || 300);
13
+ const BENCH_PATH = '/public/scripts/vanilla.min.js';
14
+
15
+ function ensureDir(dirPath) {
16
+ fs.mkdirSync(dirPath, { recursive: true });
17
+ }
18
+
19
+ function createFixtureWorkspace(rootPath) {
20
+ ensureDir(path.join(rootPath, 'assets'));
21
+ ensureDir(path.join(rootPath, 'public', 'scripts'));
22
+
23
+ const bytes = BENCH_FILE_SIZE_KB * 1024;
24
+ const filePath = path.join(rootPath, BENCH_PATH);
25
+ const content = Buffer.alloc(bytes, 'a');
26
+ fs.writeFileSync(filePath, content);
27
+ }
28
+
29
+ function createServerOptions(port) {
30
+ return {
31
+ settings: {
32
+ profile: {
33
+ port: port,
34
+ https_server: false,
35
+ enable_precompressed_negotiation: true
36
+ },
37
+ shared: {},
38
+ security: {}
39
+ }
40
+ };
41
+ }
42
+
43
+ function percentile(values, percentileValue) {
44
+ if (!values.length) {
45
+ return 0;
46
+ }
47
+ const sorted = values.slice().sort((a, b) => a - b);
48
+ const index = Math.min(sorted.length - 1, Math.ceil((percentileValue / 100) * sorted.length) - 1);
49
+ return sorted[index];
50
+ }
51
+
52
+ function summarize(values, elapsedMs) {
53
+ const total = values.length;
54
+ const sum = values.reduce((acc, current) => acc + current, 0);
55
+ return {
56
+ count: total,
57
+ avgMs: total ? sum / total : 0,
58
+ p50Ms: percentile(values, 50),
59
+ p95Ms: percentile(values, 95),
60
+ p99Ms: percentile(values, 99),
61
+ maxMs: values.length ? Math.max(...values) : 0,
62
+ rps: elapsedMs > 0 ? (total * 1000) / elapsedMs : 0
63
+ };
64
+ }
65
+
66
+ function printSummary(title, summary) {
67
+ console.log(`\n${title}`);
68
+ console.log(` requests: ${summary.count}`);
69
+ console.log(` avg: ${summary.avgMs.toFixed(2)} ms`);
70
+ console.log(` p50: ${summary.p50Ms.toFixed(2)} ms`);
71
+ console.log(` p95: ${summary.p95Ms.toFixed(2)} ms`);
72
+ console.log(` p99: ${summary.p99Ms.toFixed(2)} ms`);
73
+ console.log(` max: ${summary.maxMs.toFixed(2)} ms`);
74
+ console.log(` rps: ${summary.rps.toFixed(2)}`);
75
+ }
76
+
77
+ function requestOnce(agent, port, headers) {
78
+ return new Promise((resolve, reject) => {
79
+ const start = performance.now();
80
+ const req = http.request({
81
+ hostname: '127.0.0.1',
82
+ port: port,
83
+ path: BENCH_PATH,
84
+ method: 'GET',
85
+ headers: headers || {},
86
+ agent: agent
87
+ }, (res) => {
88
+ res.on('data', () => {});
89
+ res.on('end', () => {
90
+ resolve({
91
+ statusCode: res.statusCode,
92
+ durationMs: performance.now() - start
93
+ });
94
+ });
95
+ });
96
+ req.on('error', reject);
97
+ req.end();
98
+ });
99
+ }
100
+
101
+ function waitForServerListening(instance) {
102
+ if (instance && instance.httpx && instance.httpx.listening) {
103
+ return Promise.resolve();
104
+ }
105
+ return new Promise((resolve) => {
106
+ instance.httpx.once('listening', resolve);
107
+ });
108
+ }
109
+
110
+ async function runWarmBenchmark(port) {
111
+ const agent = new http.Agent({
112
+ keepAlive: true,
113
+ maxSockets: 1
114
+ });
115
+ const latencies = [];
116
+ const startedAt = performance.now();
117
+ for (let index = 0; index < BENCH_TOTAL_REQUESTS; index += 1) {
118
+ const result = await requestOnce(agent, port, {
119
+ 'accept-encoding': 'br, gzip'
120
+ });
121
+ if (result.statusCode !== 200) {
122
+ throw new Error(`Unexpected status ${result.statusCode} in warm benchmark`);
123
+ }
124
+ latencies.push(result.durationMs);
125
+ }
126
+ agent.destroy();
127
+ return summarize(latencies, performance.now() - startedAt);
128
+ }
129
+
130
+ async function runColdBenchmark(server, port) {
131
+ const latencies = [];
132
+ const agent = new http.Agent({
133
+ keepAlive: true,
134
+ maxSockets: 1
135
+ });
136
+ const startedAt = performance.now();
137
+ for (let index = 0; index < BENCH_COLD_ITERATIONS; index += 1) {
138
+ server.router.staticMetadataCache.clear();
139
+ server.router.staticResolutionCache.clear();
140
+
141
+ const sample = await requestOnce(agent, port, {
142
+ 'accept-encoding': 'br, gzip'
143
+ });
144
+ if (sample.statusCode !== 200) {
145
+ throw new Error(`Unexpected status ${sample.statusCode} in cold benchmark`);
146
+ }
147
+ latencies.push(sample.durationMs);
148
+ }
149
+ agent.destroy();
150
+ return summarize(latencies, performance.now() - startedAt);
151
+ }
152
+
153
+ async function main() {
154
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vanillajet-bench-'));
155
+ createFixtureWorkspace(tmpDir);
156
+ const previousCwd = process.cwd();
157
+ process.chdir(tmpDir);
158
+
159
+ const server = new Server(createServerOptions(BENCH_PORT), []);
160
+ await waitForServerListening(server);
161
+ try {
162
+ const warmup = await requestOnce(undefined, BENCH_PORT, {});
163
+ if (warmup.statusCode !== 200) {
164
+ throw new Error(`Warmup request failed with status ${warmup.statusCode}`);
165
+ }
166
+
167
+ const coldResult = await runColdBenchmark(server, BENCH_PORT);
168
+ printSummary('Cold cache benchmark', coldResult);
169
+
170
+ const warmResult = await runWarmBenchmark(BENCH_PORT);
171
+ printSummary('Warm cache benchmark', warmResult);
172
+
173
+ await new Promise((resolve) => {
174
+ server.httpx.close(() => {
175
+ resolve();
176
+ });
177
+ });
178
+ process.chdir(previousCwd);
179
+
180
+ const p95Improvement = coldResult.p95Ms > 0
181
+ ? ((coldResult.p95Ms - warmResult.p95Ms) / coldResult.p95Ms) * 100
182
+ : 0;
183
+ console.log(`\nWarm p95 improvement vs cold: ${p95Improvement.toFixed(2)}%`);
184
+ } catch (error) {
185
+ await new Promise((resolve) => {
186
+ server.httpx.close(() => {
187
+ resolve();
188
+ });
189
+ }).catch(() => {});
190
+ process.chdir(previousCwd);
191
+ throw error;
192
+ } finally {
193
+ fs.rmSync(tmpDir, { recursive: true, force: true });
194
+ }
195
+ }
196
+
197
+ main().catch((error) => {
198
+ console.error('\nStatic benchmark failed');
199
+ console.error(error);
200
+ process.exit(1);
201
+ });
@@ -1,36 +0,0 @@
1
- const path = require('path');
2
- const { spawnSync } = require('child_process');
3
-
4
- function resolveConsumerRoot() {
5
- return process.cwd()
6
- .replace('/node_modules/vanilla-jet', '')
7
- .replace('/.scripts', '')
8
- .replace('/.grunt', '');
9
- }
10
-
11
- function main() {
12
- const action = process.argv[2] || 'dev';
13
- const viteCommand = action === 'build' ? 'build' : 'serve';
14
- const packageRoot = path.resolve(__dirname, '..');
15
- const consumerRoot = resolveConsumerRoot();
16
- const vitePackageJson = require.resolve('vite/package.json', { paths: [packageRoot] });
17
- const viteBin = path.join(path.dirname(vitePackageJson), 'bin/vite.js');
18
- const configFile = path.join(packageRoot, 'vite.config.js');
19
-
20
- const args = [viteBin, viteCommand, '--config', configFile];
21
- if (viteCommand === 'serve') {
22
- args.push('--host');
23
- }
24
-
25
- const result = spawnSync(process.execPath, args, {
26
- stdio: 'inherit',
27
- cwd: consumerRoot,
28
- env: Object.assign({}, process.env, {
29
- VANILLAJET_PACKAGE_ROOT: packageRoot
30
- })
31
- });
32
-
33
- process.exit(typeof result.status === 'number' ? result.status : 1);
34
- }
35
-
36
- main();
package/vite.config.js DELETED
@@ -1,184 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { defineConfig } = require('vite');
4
-
5
- function resolveConsumerRoot() {
6
- return process.cwd()
7
- .replace('/node_modules/vanilla-jet', '')
8
- .replace('/.scripts', '')
9
- .replace('/.grunt', '');
10
- }
11
-
12
- function collectScriptFiles(dirPath) {
13
- if (!fs.existsSync(dirPath)) {
14
- return [];
15
- }
16
-
17
- const files = [];
18
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
19
- entries.forEach((entry) => {
20
- const entryPath = path.join(dirPath, entry.name);
21
- if (entry.isDirectory()) {
22
- files.push(...collectScriptFiles(entryPath));
23
- return;
24
- }
25
-
26
- if (entry.isFile() && entry.name.endsWith('.js')) {
27
- files.push(entryPath);
28
- }
29
- });
30
- return files;
31
- }
32
-
33
- function normalize(filePath) {
34
- return filePath.split(path.sep).join('/');
35
- }
36
-
37
- function scriptPriority(filePath) {
38
- const normalized = normalize(filePath);
39
- if (normalized.includes('/assets/scripts/controllers/')) return 0;
40
- if (normalized.includes('/assets/scripts/views/')) return 1;
41
- if (normalized.includes('/assets/scripts/api/')) return 2;
42
- return 3;
43
- }
44
-
45
- function includeInBundle(filePath) {
46
- const normalized = normalize(filePath);
47
- if (normalized.includes('/assets/scripts/core/')) return false;
48
- if (normalized.includes('/assets/scripts/plugins/')) return false;
49
- return true;
50
- }
51
-
52
- function writeVirtualEntry(virtualEntryPath, orderedScripts, lessEntryPath, hasLess) {
53
- const imports = orderedScripts.map((scriptPath, index) => {
54
- const importPath = normalize(scriptPath) + '?raw';
55
- return `import __vanillajet_script_${index} from ${JSON.stringify(importPath)};`;
56
- });
57
-
58
- if (hasLess) {
59
- imports.push(`import ${JSON.stringify(normalize(lessEntryPath))};`);
60
- }
61
-
62
- const mappings = orderedScripts.map((scriptPath, index) => {
63
- const rel = normalize(path.relative(path.dirname(virtualEntryPath), scriptPath));
64
- return `{ source: __vanillajet_script_${index}, file: ${JSON.stringify(rel)} }`;
65
- });
66
-
67
- const lines = [
68
- '/* Auto-generated by VanillaJet Vite config. */',
69
- ...imports,
70
- '',
71
- 'function executeGlobalScript(sourceCode, sourceFile) {',
72
- ' const tag = document.createElement("script");',
73
- ' tag.type = "text/javascript";',
74
- ' tag.setAttribute("data-vanillajet-source", sourceFile);',
75
- ' tag.text = sourceCode;',
76
- ' document.head.appendChild(tag);',
77
- ' document.head.removeChild(tag);',
78
- '}',
79
- '',
80
- `const scriptMap = [${mappings.join(', ')}];`,
81
- 'scriptMap.forEach(({ source, file }) => executeGlobalScript(source, file));',
82
- '',
83
- 'if (import.meta.hot) {',
84
- ' import.meta.hot.accept(() => {',
85
- ' window.location.reload();',
86
- ' });',
87
- '}'
88
- ];
89
-
90
- fs.mkdirSync(path.dirname(virtualEntryPath), { recursive: true });
91
- fs.writeFileSync(virtualEntryPath, lines.join('\n'), 'utf8');
92
- }
93
-
94
- module.exports = defineConfig(({ command, mode }) => {
95
- const rootDir = resolveConsumerRoot();
96
- const scriptsDir = path.join(rootDir, 'assets/scripts');
97
- const lessEntryPath = path.join(rootDir, 'assets/styles/less/admin.less');
98
- const virtualEntryPath = path.join(rootDir, '.vanillajet/vite-entry.js');
99
-
100
- const scripts = collectScriptFiles(scriptsDir)
101
- .filter(includeInBundle)
102
- .sort((left, right) => {
103
- const priorityDiff = scriptPriority(left) - scriptPriority(right);
104
- if (priorityDiff !== 0) {
105
- return priorityDiff;
106
- }
107
- return normalize(left).localeCompare(normalize(right));
108
- });
109
- const hasLess = fs.existsSync(lessEntryPath);
110
- const hasSources = scripts.length > 0 || hasLess;
111
-
112
- writeVirtualEntry(virtualEntryPath, scripts, lessEntryPath, hasLess);
113
-
114
- const helperPath = '/__vanillajet__/';
115
- const helperHtml = [
116
- '<!doctype html>',
117
- '<html lang="en">',
118
- '<head>',
119
- ' <meta charset="utf-8" />',
120
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
121
- ' <title>VanillaJet Vite Dev</title>',
122
- '</head>',
123
- '<body>',
124
- ' <h3>VanillaJet Vite dev helper</h3>',
125
- ' <p>This page only loads JS/LESS from assets for DX.</p>',
126
- ' <p>Nunjucks templates and legacy Node routes remain unchanged.</p>',
127
- ` <script type="module" src="${normalize(path.relative(rootDir, virtualEntryPath)).startsWith('.') ? '/' + normalize(path.relative(rootDir, virtualEntryPath)).replace(/^\.\//, '') : '/' + normalize(path.relative(rootDir, virtualEntryPath))}"></script>`,
128
- '</body>',
129
- '</html>'
130
- ].join('\n');
131
-
132
- return {
133
- root: rootDir,
134
- publicDir: false,
135
- server: {
136
- host: true,
137
- port: 5173
138
- },
139
- build: {
140
- outDir: path.join(rootDir, 'public'),
141
- emptyOutDir: false,
142
- sourcemap: mode !== 'production',
143
- rollupOptions: {
144
- input: virtualEntryPath,
145
- output: {
146
- entryFileNames: 'scripts/vanilla.min.js',
147
- chunkFileNames: 'scripts/chunks/[name]-[hash].js',
148
- assetFileNames: (assetInfo) => {
149
- if (assetInfo.name && assetInfo.name.endsWith('.css')) {
150
- return 'styles/app.min.css';
151
- }
152
- return 'assets/[name]-[hash][extname]';
153
- }
154
- }
155
- }
156
- },
157
- plugins: [
158
- {
159
- name: 'vanillajet-dev-helper',
160
- configureServer(server) {
161
- if (!hasSources) {
162
- server.config.logger.warn(
163
- '[vanillajet] No JS/LESS sources found under assets/. Vite will run with an empty entry.'
164
- );
165
- }
166
-
167
- server.middlewares.use((req, res, next) => {
168
- if (req.url === helperPath) {
169
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
170
- res.end(helperHtml);
171
- return;
172
- }
173
- next();
174
- });
175
- },
176
- buildStart() {
177
- if (command === 'build' && !hasSources) {
178
- this.warn('[vanillajet] No JS/LESS sources found under assets/. Build will output an empty JS bundle.');
179
- }
180
- }
181
- }
182
- ]
183
- };
184
- });