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 +18 -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/router.js +76 -21
- 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,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.
|
|
7
|
+
## [1.4.1] - 2026-02-19
|
|
8
8
|
|
|
9
|
-
### Highlights
|
|
9
|
+
### Highlights (v1.4.1)
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- Added
|
|
14
|
-
-
|
|
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.
|
|
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.
|
|
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`:
|
|
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/`
|
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.1`)
|
|
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/router.js
CHANGED
|
@@ -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 =
|
|
113
|
+
extHeader = obj.mimeHeaders[ext];
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
if (extHandled) {
|
|
109
117
|
|
|
110
|
-
let
|
|
111
|
-
|
|
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
|
|
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.
|
|
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
|
+
});
|
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
|
-
});
|