vanilla-jet 1.4.0 → 1.4.2
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 +27 -7
- package/README.md +17 -19
- package/ROADMAP_INTEGRAL.md +65 -57
- package/bin.js +0 -16
- package/docs/benchmark-static.md +45 -0
- package/framework/dipper.js +32 -2
- package/framework/router.js +62 -20
- package/gulpfile.js +4 -2
- package/package.json +13 -15
- package/scripts/benchmark-static.js +201 -0
- package/.scripts/run_vite.js +0 -36
- package/vite.config.js +0 -184
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,33 @@ 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.
|
|
7
|
+
## [1.4.2] - 2026-02-19
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Version bump to 1.4.2.
|
|
12
|
+
|
|
13
|
+
## [1.4.1] - 2026-02-19
|
|
14
|
+
|
|
15
|
+
### Highlights (v1.4.1)
|
|
10
16
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- Added
|
|
14
|
-
-
|
|
17
|
+
- Completed HU 2.1 (`Fast path de estaticos en Node`).
|
|
18
|
+
- Optimized static serving in `framework/router.js`:
|
|
19
|
+
- Added warm-path static resolution cache (`route + accept-encoding`) to avoid repeated candidate resolution work.
|
|
20
|
+
- Keeps strict conditional metadata revalidation so content changes are visible on reload without stale `304`.
|
|
21
|
+
- Consolidated static header assembly and reused mime header maps.
|
|
22
|
+
- Kept stream-based delivery for large assets and tuned `createReadStream` chunk size.
|
|
23
|
+
- Added asset URL versioning in `framework/dipper.js` (`?v=size-mtime`) for local scripts/styles.
|
|
24
|
+
- Updated `gulp dev` watch flow so JS/CSS recompiles also trigger template compilation and refresh asset URLs in HTML.
|
|
25
|
+
- Added reproducible local benchmark:
|
|
26
|
+
- New script: `npm run benchmark:static`.
|
|
27
|
+
- New guide: `docs/benchmark-static.md`.
|
|
28
|
+
|
|
29
|
+
### Compatibility notes (v1.4.1)
|
|
30
|
+
|
|
31
|
+
- No public API changes.
|
|
32
|
+
- Preserves static conditional caching (`304`) and precompressed negotiation fallback.
|
|
33
|
+
- No intended behavior changes for dynamic routes.
|
|
15
34
|
|
|
16
35
|
## [1.3.6] - 2026-02-19
|
|
17
36
|
|
|
@@ -92,4 +111,5 @@ The format follows a structure inspired by Keep a Changelog and semantic version
|
|
|
92
111
|
[1.3.4]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.4
|
|
93
112
|
[1.3.5]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.5
|
|
94
113
|
[1.3.6]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.6
|
|
95
|
-
[1.4.
|
|
114
|
+
[1.4.2]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.2
|
|
115
|
+
[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.
|
|
9
|
+
- Version: `1.4.2`
|
|
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`:
|
|
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
|
+
- Keeps conditional revalidation (`ETag`/`Last-Modified`) strict so reload reflects changes immediately.
|
|
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/`
|
package/ROADMAP_INTEGRAL.md
CHANGED
|
@@ -5,9 +5,9 @@ Cada historia incluye su ciclo completo: fases, tareas, entregables, metricas, c
|
|
|
5
5
|
|
|
6
6
|
## Objetivo
|
|
7
7
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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 -
|
|
118
|
+
## EPIC 2 - Performance Node + DX de compilacion (foco actual)
|
|
119
119
|
|
|
120
|
-
### HU 2.1 -
|
|
120
|
+
### HU 2.1 - Fast path de estaticos en Node (completada `v1.4.2`)
|
|
121
121
|
|
|
122
122
|
#### Fases
|
|
123
|
-
- F1:
|
|
124
|
-
- F2:
|
|
125
|
-
- F3:
|
|
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]
|
|
130
|
-
- [x]
|
|
131
|
-
- [x]
|
|
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
|
-
-
|
|
133
|
+
- [x] Patch de performance en `framework/router.js`.
|
|
134
|
+
- [x] Script de benchmark local documentado.
|
|
135
135
|
|
|
136
136
|
#### Metricas
|
|
137
|
-
-
|
|
138
|
-
-
|
|
137
|
+
- p95 de estaticos >= 20% mejor en escenario warm.
|
|
138
|
+
- Menor uso de CPU en serving concurrente.
|
|
139
139
|
|
|
140
140
|
#### Criterios
|
|
141
|
-
-
|
|
142
|
-
-
|
|
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 -
|
|
147
|
+
### HU 2.2 - Recompile en tiempo real mas rapido (pendiente)
|
|
148
148
|
|
|
149
149
|
#### Fases
|
|
150
|
-
- F1:
|
|
151
|
-
- F2:
|
|
152
|
-
- F3: validacion
|
|
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
|
-
-
|
|
156
|
-
-
|
|
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
|
-
-
|
|
160
|
+
- Pipeline `dev` optimizado y medido.
|
|
160
161
|
|
|
161
162
|
#### Metricas
|
|
162
|
-
-
|
|
163
|
+
- Recompile JS >= 35% mas rapido.
|
|
164
|
+
- Recompile LESS >= 30% mas rapido.
|
|
163
165
|
|
|
164
166
|
#### Criterios
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
+
- Output final equivalente al flujo actual.
|
|
168
|
+
- Sin cambios obligatorios en estructura de proyectos consumidores.
|
|
167
169
|
|
|
168
170
|
#### Documentacion
|
|
169
|
-
-
|
|
171
|
+
- `README.md` + `CHANGELOG.md`.
|
|
170
172
|
|
|
171
173
|
---
|
|
172
174
|
|
|
173
|
-
## EPIC 3 -
|
|
175
|
+
## EPIC 3 - Developer Experience en `dev`
|
|
174
176
|
|
|
175
|
-
### HU 3.1 -
|
|
177
|
+
### HU 3.1 - Runner `dev` con navegador auto-open + live reload estable (pendiente)
|
|
176
178
|
|
|
177
179
|
#### Fases
|
|
178
|
-
- F1:
|
|
179
|
-
- F2:
|
|
180
|
-
- F3:
|
|
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
|
-
-
|
|
185
|
-
-
|
|
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
|
-
-
|
|
189
|
-
|
|
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
|
-
-
|
|
197
|
+
- Reload confiable tras cada recompile exitoso.
|
|
198
|
+
- Sin romper el modo actual para usuarios legacy.
|
|
193
199
|
|
|
194
200
|
#### Documentacion
|
|
195
|
-
-
|
|
201
|
+
- `README.md` + `CHANGELOG.md` + guia de migracion de scripts.
|
|
196
202
|
|
|
197
203
|
---
|
|
198
204
|
|
|
199
|
-
## EPIC 4 -
|
|
205
|
+
## EPIC 4 - Hardening operativo del runtime Node
|
|
200
206
|
|
|
201
|
-
### HU 4.1 -
|
|
207
|
+
### HU 4.1 - Observabilidad y protecciones de runtime (pendiente)
|
|
202
208
|
|
|
203
209
|
#### Fases
|
|
204
|
-
- F1:
|
|
205
|
-
- F2:
|
|
206
|
-
- F3: guia
|
|
210
|
+
- F1: logging y metricas base.
|
|
211
|
+
- F2: alarmas y limites.
|
|
212
|
+
- F3: guia operativa.
|
|
207
213
|
|
|
208
214
|
#### Tareas
|
|
209
|
-
-
|
|
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
|
-
-
|
|
220
|
+
- Guia de operacion y checklist de hardening.
|
|
213
221
|
|
|
214
222
|
#### Documentacion
|
|
215
|
-
- `docs
|
|
223
|
+
- `docs/` + `README.md`.
|
|
216
224
|
|
|
217
|
-
### HU 4.2 -
|
|
225
|
+
### HU 4.2 - Paquete de referencia para despliegue Node (pendiente)
|
|
218
226
|
|
|
219
227
|
#### Fases
|
|
220
|
-
- F1:
|
|
221
|
-
- F2:
|
|
222
|
-
- F3:
|
|
228
|
+
- F1: templates de entorno.
|
|
229
|
+
- F2: validacion en staging.
|
|
230
|
+
- F3: guia final.
|
|
223
231
|
|
|
224
232
|
#### Tareas
|
|
225
|
-
- Definir
|
|
226
|
-
-
|
|
233
|
+
- Definir variables, puertos y recomendaciones de cache.
|
|
234
|
+
- Incluir ejemplo de despliegue reproducible para consumidores.
|
|
227
235
|
|
|
228
236
|
#### Entregables
|
|
229
|
-
-
|
|
237
|
+
- Template de despliegue y guia de puesta en marcha.
|
|
230
238
|
|
|
231
239
|
#### Documentacion
|
|
232
|
-
- `docs
|
|
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.
|
package/framework/dipper.js
CHANGED
|
@@ -93,12 +93,12 @@ Dipper.prototype.img = function(filename) {
|
|
|
93
93
|
|
|
94
94
|
Dipper.prototype.script = function(filename) {
|
|
95
95
|
let dir = this.getDir('scripts', false);
|
|
96
|
-
return this.
|
|
96
|
+
return this.versionedUrl(dir + filename);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
Dipper.prototype.style = function(filename) {
|
|
100
100
|
let dir = this.getDir('styles', false);
|
|
101
|
-
return this.
|
|
101
|
+
return this.versionedUrl(dir + filename);;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
Dipper.prototype.pdf = function(filename) {
|
|
@@ -128,6 +128,36 @@ Dipper.prototype.urlTo = function (route) {
|
|
|
128
128
|
return '/' + route.replace(/^\/+/, '');
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
Dipper.prototype.versionedUrl = function(route) {
|
|
132
|
+
const fs = require('fs');
|
|
133
|
+
const path = require('path');
|
|
134
|
+
const normalizedUrl = this.urlTo(route);
|
|
135
|
+
|
|
136
|
+
// External URLs should remain untouched.
|
|
137
|
+
if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(normalizedUrl)) {
|
|
138
|
+
return normalizedUrl;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const filePath = path.join(this.processCwd(), normalizedUrl.replace(/^\//, ''));
|
|
143
|
+
const stats = fs.statSync(filePath);
|
|
144
|
+
if (!stats.isFile()) {
|
|
145
|
+
return normalizedUrl;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const version = `${stats.size}-${Math.floor(stats.mtimeMs)}`;
|
|
149
|
+
return this.appendQueryParam(normalizedUrl, 'v', version);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// If file does not exist yet, keep legacy behavior.
|
|
152
|
+
return normalizedUrl;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
Dipper.prototype.appendQueryParam = function(url, key, value) {
|
|
157
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
158
|
+
return `${url}${separator}${key}=${encodeURIComponent(value)}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
131
161
|
Dipper.prototype.registerStyle = function(
|
|
132
162
|
name, url, requires,
|
|
133
163
|
cdn = false, async = false,
|
package/framework/router.js
CHANGED
|
@@ -16,8 +16,11 @@ 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.staticStreamChunkSize = 128 * 1024;
|
|
21
24
|
this.mimes = {
|
|
22
25
|
'png': 'image/png',
|
|
23
26
|
'webp': 'image/webp',
|
|
@@ -32,6 +35,10 @@ class Router {
|
|
|
32
35
|
'pdf': 'application/pdf',
|
|
33
36
|
'json': 'application/json'
|
|
34
37
|
};
|
|
38
|
+
this.mimeHeaders = Object.keys(this.mimes).reduce((headers, ext) => {
|
|
39
|
+
headers[ext] = { 'Content-Type': this.mimes[ext] };
|
|
40
|
+
return headers;
|
|
41
|
+
}, {});
|
|
35
42
|
this.compressionMimes = [ 'css', 'js' ];
|
|
36
43
|
this.compressionFiles = [ 'vanilla.min.js', 'app.min.css' ];
|
|
37
44
|
this.enablePrecompressedNegotiation = Boolean(server?.options?.enable_precompressed_negotiation);
|
|
@@ -102,14 +109,13 @@ class Router {
|
|
|
102
109
|
|
|
103
110
|
if (obj.mimes[ext] != undefined && obj.mimes[ext] != 'undefined') {
|
|
104
111
|
extHandled = true;
|
|
105
|
-
extHeader =
|
|
112
|
+
extHeader = obj.mimeHeaders[ext];
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
if (extHandled) {
|
|
109
116
|
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
filename = path.join(rep, route),
|
|
117
|
+
let route = request.path,
|
|
118
|
+
filename = path.join(obj.staticBasePath, route),
|
|
113
119
|
filePrivate = obj.isProtectedFile(route);
|
|
114
120
|
|
|
115
121
|
if (filePrivate) {
|
|
@@ -118,25 +124,13 @@ class Router {
|
|
|
118
124
|
|
|
119
125
|
let staticCandidates = obj.getStaticCandidates(request, ext, filename);
|
|
120
126
|
let hasConditionalHeaders = Boolean(req.headers['if-none-match'] || req.headers['if-modified-since']);
|
|
121
|
-
obj.resolveFirstAvailableStaticFile(staticCandidates, hasConditionalHeaders, (err, staticFile) => {
|
|
127
|
+
obj.resolveFirstAvailableStaticFile(route, request.acceptEncoding, staticCandidates, hasConditionalHeaders, (err, staticFile) => {
|
|
122
128
|
if (err || !staticFile) {
|
|
123
129
|
return obj.onNotFound(response);
|
|
124
130
|
}
|
|
125
131
|
|
|
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
132
|
let metadata = staticFile.metadata;
|
|
135
|
-
staticHeaders
|
|
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';
|
|
133
|
+
let staticHeaders = obj.buildStaticHeaders(extHeader, staticCandidates, staticFile.contentEncoding, metadata);
|
|
140
134
|
|
|
141
135
|
if (obj.isNotModified(req, metadata)) {
|
|
142
136
|
let notModifiedHeaders = Object.assign({}, staticHeaders);
|
|
@@ -145,8 +139,12 @@ class Router {
|
|
|
145
139
|
return res.end();
|
|
146
140
|
}
|
|
147
141
|
|
|
148
|
-
const fileStream = fs.createReadStream(staticFile.filename
|
|
142
|
+
const fileStream = fs.createReadStream(staticFile.filename, {
|
|
143
|
+
highWaterMark: obj.staticStreamChunkSize
|
|
144
|
+
});
|
|
149
145
|
fileStream.on('error', (streamErr) => {
|
|
146
|
+
obj.staticMetadataCache.delete(staticFile.filename);
|
|
147
|
+
obj.staticResolutionCache.clear();
|
|
150
148
|
console.error("Error reading file:", streamErr);
|
|
151
149
|
res.writeHead(500);
|
|
152
150
|
res.end('Server Error');
|
|
@@ -214,8 +212,24 @@ class Router {
|
|
|
214
212
|
return compressedCandidates.concat(candidates);
|
|
215
213
|
}
|
|
216
214
|
|
|
217
|
-
resolveFirstAvailableStaticFile(candidates, forceRefresh, callback) {
|
|
215
|
+
resolveFirstAvailableStaticFile(route, acceptEncoding, candidates, forceRefresh, callback) {
|
|
218
216
|
let obj = this;
|
|
217
|
+
let resolutionKey = obj.getStaticResolutionKey(route, acceptEncoding);
|
|
218
|
+
let cachedResolution = obj.staticResolutionCache.get(resolutionKey);
|
|
219
|
+
if (cachedResolution) {
|
|
220
|
+
return obj.getStaticFileMetadata(cachedResolution.filename, forceRefresh, (cachedErr, cachedMetadata) => {
|
|
221
|
+
if (!cachedErr && cachedMetadata) {
|
|
222
|
+
return callback(null, {
|
|
223
|
+
filename: cachedResolution.filename,
|
|
224
|
+
contentEncoding: cachedResolution.contentEncoding,
|
|
225
|
+
metadata: cachedMetadata
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
obj.staticResolutionCache.delete(resolutionKey);
|
|
229
|
+
obj.resolveFirstAvailableStaticFile(route, acceptEncoding, candidates, forceRefresh, callback);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
219
233
|
let index = 0;
|
|
220
234
|
function resolveCandidate() {
|
|
221
235
|
let currentCandidate = candidates[index];
|
|
@@ -225,6 +239,10 @@ class Router {
|
|
|
225
239
|
|
|
226
240
|
obj.getStaticFileMetadata(currentCandidate.filename, forceRefresh, (err, metadata) => {
|
|
227
241
|
if (!err && metadata) {
|
|
242
|
+
obj.staticResolutionCache.set(resolutionKey, {
|
|
243
|
+
filename: currentCandidate.filename,
|
|
244
|
+
contentEncoding: currentCandidate.contentEncoding
|
|
245
|
+
});
|
|
228
246
|
return callback(null, {
|
|
229
247
|
filename: currentCandidate.filename,
|
|
230
248
|
contentEncoding: currentCandidate.contentEncoding,
|
|
@@ -239,6 +257,28 @@ class Router {
|
|
|
239
257
|
resolveCandidate();
|
|
240
258
|
}
|
|
241
259
|
|
|
260
|
+
getStaticResolutionKey(route, acceptEncoding) {
|
|
261
|
+
let normalizedEncodings = Array.isArray(acceptEncoding) ? acceptEncoding.join(',') : '';
|
|
262
|
+
return `${route}|${normalizedEncodings}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
buildStaticHeaders(extHeader, candidates, contentEncoding, metadata) {
|
|
266
|
+
let staticHeaders = Object.assign({}, extHeader);
|
|
267
|
+
if (contentEncoding) {
|
|
268
|
+
staticHeaders['Content-Encoding'] = contentEncoding;
|
|
269
|
+
}
|
|
270
|
+
if (candidates.some((candidate) => candidate.contentEncoding)) {
|
|
271
|
+
staticHeaders['Vary'] = 'Accept-Encoding';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
staticHeaders['Content-Length'] = metadata.size;
|
|
275
|
+
staticHeaders['ETag'] = metadata.etag;
|
|
276
|
+
staticHeaders['Last-Modified'] = metadata.lastModified;
|
|
277
|
+
// Force revalidation to keep clients fresh without hard reload.
|
|
278
|
+
staticHeaders['Cache-Control'] = 'no-cache, must-revalidate';
|
|
279
|
+
return staticHeaders;
|
|
280
|
+
}
|
|
281
|
+
|
|
242
282
|
supportsEncoding(acceptEncoding, encoding) {
|
|
243
283
|
if (!Array.isArray(acceptEncoding)) {
|
|
244
284
|
return false;
|
|
@@ -279,6 +319,7 @@ class Router {
|
|
|
279
319
|
try {
|
|
280
320
|
let watcher = fs.watch(filename, (eventType) => {
|
|
281
321
|
obj.staticMetadataCache.delete(filename);
|
|
322
|
+
obj.staticResolutionCache.clear();
|
|
282
323
|
if (eventType === 'rename') {
|
|
283
324
|
let renamedWatcher = obj.staticFileWatchers.get(filename);
|
|
284
325
|
if (renamedWatcher) {
|
|
@@ -290,6 +331,7 @@ class Router {
|
|
|
290
331
|
|
|
291
332
|
watcher.on('error', () => {
|
|
292
333
|
obj.staticMetadataCache.delete(filename);
|
|
334
|
+
obj.staticResolutionCache.clear();
|
|
293
335
|
let activeWatcher = obj.staticFileWatchers.get(filename);
|
|
294
336
|
if (activeWatcher) {
|
|
295
337
|
activeWatcher.close();
|
package/gulpfile.js
CHANGED
|
@@ -123,7 +123,8 @@ function watchFiles(cb) {
|
|
|
123
123
|
// Watch LESS files
|
|
124
124
|
watch([`${base}/assets/styles/less/**/*.less`], gulp.series(
|
|
125
125
|
buildLess,
|
|
126
|
-
compressCss
|
|
126
|
+
compressCss,
|
|
127
|
+
compileTemplates
|
|
127
128
|
));
|
|
128
129
|
|
|
129
130
|
// Watch HTML files
|
|
@@ -138,7 +139,8 @@ function watchFiles(cb) {
|
|
|
138
139
|
uglifyJs,
|
|
139
140
|
concatJs,
|
|
140
141
|
cleanMinified,
|
|
141
|
-
compressJs
|
|
142
|
+
compressJs,
|
|
143
|
+
compileTemplates
|
|
142
144
|
));
|
|
143
145
|
|
|
144
146
|
cb();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-jet",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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
|
+
});
|
package/.scripts/run_vite.js
DELETED
|
@@ -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
|
-
});
|