vanilla-jet 1.4.1 → 1.4.3

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.
@@ -41,7 +41,6 @@ async function createFileIfnotExists(content) {
41
41
  // -- Helpers
42
42
  function processCwd() {
43
43
  return process.cwd()
44
- .replace('/.grunt', '')
45
44
  .replace('/.scripts', '');
46
45
  }
47
46
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ 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.3] - 2026-02-19
8
+
9
+ ### Removed
10
+
11
+ - Grunt build artifacts: removed `.grunt/` folder, `build_styles_task.js`, and `compile_html.js`.
12
+ - Build pipeline now uses only Gulp; template compilation moved to `scripts/compile_html.js`.
13
+
14
+ ### Changed
15
+
16
+ - `gulpfile.js`: `compileTemplates` now invokes `node scripts/compile_html.js` instead of `.grunt/compile_html.js`.
17
+ - `framework/dipper.js`, `.scripts/generate_packages_json.js`: removed `.grunt` references from `processCwd` helpers.
18
+
19
+ ### Fixed
20
+
21
+ - **Reliability under rapid reloads (F5)**: server no longer stops responding after repeated refreshes.
22
+ - Removed `fs.watch` per static file (`staticFileWatchers`); could exhaust resources with prolonged use.
23
+ - Added fallback to `404` for routes without a handled static extension (avoids hanging requests).
24
+ - Destroy file streams when client disconnects (`res.on('close')`) to avoid orphaned streams.
25
+ - Applied same stream cleanup in `response.render()` for HTML template delivery.
26
+ - Added defensive server timeouts: `requestTimeout`, `headersTimeout`, `keepAliveTimeout` (configurable via `settings.profile`).
27
+
28
+ ### Compatibility notes
29
+
30
+ - No public API changes.
31
+ - Build output and route behavior unchanged; only internal reliability improvements.
32
+
33
+ ## [1.4.2] - 2026-02-19
34
+
35
+ ### Changed
36
+
37
+ - Version bump to 1.4.2.
38
+
7
39
  ## [1.4.1] - 2026-02-19
8
40
 
9
41
  ### Highlights (v1.4.1)
@@ -11,9 +43,11 @@ The format follows a structure inspired by Keep a Changelog and semantic version
11
43
  - Completed HU 2.1 (`Fast path de estaticos en Node`).
12
44
  - Optimized static serving in `framework/router.js`:
13
45
  - 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.
46
+ - Keeps strict conditional metadata revalidation so content changes are visible on reload without stale `304`.
15
47
  - Consolidated static header assembly and reused mime header maps.
16
48
  - Kept stream-based delivery for large assets and tuned `createReadStream` chunk size.
49
+ - Added asset URL versioning in `framework/dipper.js` (`?v=size-mtime`) for local scripts/styles.
50
+ - Updated `gulp dev` watch flow so JS/CSS recompiles also trigger template compilation and refresh asset URLs in HTML.
17
51
  - Added reproducible local benchmark:
18
52
  - New script: `npm run benchmark:static`.
19
53
  - New guide: `docs/benchmark-static.md`.
@@ -103,4 +137,6 @@ The format follows a structure inspired by Keep a Changelog and semantic version
103
137
  [1.3.4]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.4
104
138
  [1.3.5]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.5
105
139
  [1.3.6]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.6
140
+ [1.4.2]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.2
141
+ [1.4.3]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.3
106
142
  [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.1`
9
+ - Version: `1.4.3`
10
10
  - Changelog: see [`CHANGELOG.md`](./CHANGELOG.md)
11
11
  - Improvement plan (performance and backward compatibility): see `ROADMAP_INTEGRAL.md`
12
12
 
@@ -93,6 +93,8 @@ VanillaJet expects a structure similar to:
93
93
 
94
94
  ## Build pipeline (summary)
95
95
 
96
+ Gulp-based pipeline (no Grunt):
97
+
96
98
  - Minifies JS and concatenates into `public/scripts/vanilla.min.js`
97
99
  - Compiles LESS and generates `public/styles/app.min.css`
98
100
  - Compiles templates and generates `public/pages/home.html`
@@ -125,7 +127,7 @@ Behavior details:
125
127
  Static serving includes a warm-path optimization focused on Node runtime latency:
126
128
 
127
129
  - Reuses static resolution for repeated requests (`route + accept-encoding`).
128
- - Reduces repeated metadata refresh with bounded revalidation windows.
130
+ - Keeps conditional revalidation (`ETag`/`Last-Modified`) strict so reload reflects changes immediately.
129
131
  - Keeps streaming strategy for large assets (`fs.createReadStream`) with tuned chunk size.
130
132
  - Preserves conditional cache behavior (`ETag`/`Last-Modified` + `304`) and precompressed fallback contract.
131
133
 
@@ -117,7 +117,7 @@ Cada historia incluye su ciclo completo: fases, tareas, entregables, metricas, c
117
117
 
118
118
  ## EPIC 2 - Performance Node + DX de compilacion (foco actual)
119
119
 
120
- ### HU 2.1 - Fast path de estaticos en Node (completada `v1.4.1`)
120
+ ### HU 2.1 - Fast path de estaticos en Node (completada `v1.4.2`)
121
121
 
122
122
  #### Fases
123
123
  - F1: profiling de request estatico.
@@ -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.urlTo(dir + filename);
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.urlTo(dir + filename);;
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,
@@ -515,7 +545,6 @@ Dipper.prototype.includeEnvironment = function() {
515
545
  Dipper.prototype.processCwd = function() {
516
546
 
517
547
  let cwd = process.cwd()
518
- .replace('/.grunt', '')
519
548
  .replace('/.scripts', '')
520
549
  .replace('/node_modules', '')
521
550
  .replace('/vanilla-jet', '');
@@ -111,6 +111,11 @@ class Response {
111
111
  fileStream.on('error', () => {
112
112
  obj.error404();
113
113
  });
114
+ obj.res.on('close', () => {
115
+ if (!obj.res.writableEnded) {
116
+ fileStream.destroy();
117
+ }
118
+ });
114
119
  fileStream.pipe(obj.res);
115
120
  });
116
121
  }
@@ -19,8 +19,6 @@ class Router {
19
19
  this.staticBasePath = this.cwd.replace('core/framework', '');
20
20
  this.staticMetadataCache = new Map();
21
21
  this.staticResolutionCache = new Map();
22
- this.staticFileWatchers = new Map();
23
- this.staticMetadataMaxAgeMs = 1000;
24
22
  this.staticStreamChunkSize = 128 * 1024;
25
23
  this.mimes = {
26
24
  'png': 'image/png',
@@ -150,12 +148,18 @@ class Router {
150
148
  res.writeHead(500);
151
149
  res.end('Server Error');
152
150
  });
151
+ res.on('close', () => {
152
+ if (!res.writableEnded) {
153
+ fileStream.destroy();
154
+ }
155
+ });
153
156
 
154
157
  res.writeHead(200, staticHeaders);
155
158
  fileStream.pipe(res);
156
- res.on('close', () => {});
157
159
  });
158
- }
160
+ } else {
161
+ return obj.onNotFound(response);
162
+ }
159
163
  }
160
164
  }
161
165
  }), handled = false;
@@ -170,10 +174,6 @@ class Router {
170
174
  return callback(null, cachedMetadata);
171
175
  }
172
176
 
173
- if (cachedMetadata && forceRefresh && !obj.shouldRefreshConditionalMetadata(cachedMetadata)) {
174
- return callback(null, cachedMetadata);
175
- }
176
-
177
177
  fs.stat(filename, (err, stats) => {
178
178
  if (err || !stats.isFile()) {
179
179
  return callback(err || new Error('File not found'));
@@ -182,12 +182,10 @@ class Router {
182
182
  let metadata = {
183
183
  size: stats.size,
184
184
  lastModified: stats.mtime.toUTCString(),
185
- etag: `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`,
186
- cachedAt: Date.now()
185
+ etag: `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`
187
186
  };
188
187
 
189
188
  obj.staticMetadataCache.set(filename, metadata);
190
- obj.watchStaticFile(filename);
191
189
  callback(null, metadata);
192
190
  });
193
191
  }
@@ -268,13 +266,6 @@ class Router {
268
266
  return `${route}|${normalizedEncodings}`;
269
267
  }
270
268
 
271
- shouldRefreshConditionalMetadata(metadata) {
272
- if (!metadata || !metadata.cachedAt) {
273
- return true;
274
- }
275
- return Date.now() - metadata.cachedAt > this.staticMetadataMaxAgeMs;
276
- }
277
-
278
269
  buildStaticHeaders(extHeader, candidates, contentEncoding, metadata) {
279
270
  let staticHeaders = Object.assign({}, extHeader);
280
271
  if (contentEncoding) {
@@ -323,41 +314,6 @@ class Router {
323
314
  });
324
315
  }
325
316
 
326
- watchStaticFile(filename) {
327
- let obj = this;
328
- if (obj.staticFileWatchers.has(filename)) {
329
- return;
330
- }
331
-
332
- try {
333
- let watcher = fs.watch(filename, (eventType) => {
334
- obj.staticMetadataCache.delete(filename);
335
- obj.staticResolutionCache.clear();
336
- if (eventType === 'rename') {
337
- let renamedWatcher = obj.staticFileWatchers.get(filename);
338
- if (renamedWatcher) {
339
- renamedWatcher.close();
340
- }
341
- obj.staticFileWatchers.delete(filename);
342
- }
343
- });
344
-
345
- watcher.on('error', () => {
346
- obj.staticMetadataCache.delete(filename);
347
- obj.staticResolutionCache.clear();
348
- let activeWatcher = obj.staticFileWatchers.get(filename);
349
- if (activeWatcher) {
350
- activeWatcher.close();
351
- }
352
- obj.staticFileWatchers.delete(filename);
353
- });
354
-
355
- obj.staticFileWatchers.set(filename, watcher);
356
- } catch (err) {
357
- // If watch cannot be created, keep runtime behavior and continue.
358
- }
359
- }
360
-
361
317
  isNotModified(req, metadata) {
362
318
  let ifNoneMatch = req.headers['if-none-match'];
363
319
  if (ifNoneMatch) {
@@ -32,7 +32,10 @@ class Server {
32
32
  _.defaults(opts, {
33
33
  https_server: false,
34
34
  wsServer: false,
35
- enable_precompressed_negotiation: false
35
+ enable_precompressed_negotiation: false,
36
+ request_timeout_ms: 30000,
37
+ headers_timeout_ms: 35000,
38
+ keep_alive_timeout_ms: 5000
36
39
  });
37
40
  obj.options = opts;
38
41
 
@@ -91,6 +94,9 @@ class Server {
91
94
  }
92
95
 
93
96
  // -- Set the port
97
+ obj.httpx.requestTimeout = Number(obj.options.request_timeout_ms) || 30000;
98
+ obj.httpx.headersTimeout = Number(obj.options.headers_timeout_ms) || 35000;
99
+ obj.httpx.keepAliveTimeout = Number(obj.options.keep_alive_timeout_ms) || 5000;
94
100
  obj.httpx.listen(obj.options.port || 8080);
95
101
  }
96
102
 
package/gulpfile.js CHANGED
@@ -113,7 +113,7 @@ function compressCss() {
113
113
  // Template compilation
114
114
  function compileTemplates() {
115
115
  return gulp.src('.')
116
- .pipe(shell([`node .grunt/compile_html.js`]));
116
+ .pipe(shell([`node scripts/compile_html.js`]));
117
117
  }
118
118
 
119
119
  // Watch task
@@ -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.1",
3
+ "version": "1.4.3",
4
4
  "description": "VannilaJet framework",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -2,7 +2,6 @@
2
2
  const path = require("path"),
3
3
  fs = require("fs"),
4
4
  nunjucks = require('nunjucks'),
5
- identifier = 'templates',
6
5
  chalk = require('chalk'),
7
6
  zlib = require('zlib');
8
7
 
@@ -43,9 +42,11 @@ function main() {
43
42
  // -- Get home.html
44
43
  let homePageName = 'home.html';
45
44
  getHtmlFromPage(homePageName).then((htmlContent) => {
46
- if (htmlContent) {
45
+ if (htmlContent) {
46
+ // -- Compile the htmlContent
47
+ const compiledHtmlContent = compileTemplate(htmlContent);
47
48
  // -- Divide content line by line
48
- const htmlContentLines = htmlContent.split('\n');
49
+ const htmlContentLines = compiledHtmlContent.split('\n');
49
50
  let lines = Array.from(htmlContentLines);
50
51
  // -- Iterate over each line
51
52
  for (let line of htmlContentLines) {
@@ -58,7 +59,7 @@ function main() {
58
59
  // -- Get template name
59
60
  var templateName = line.replace('include::', '');
60
61
  // -- Check if its name "templates" add all templates if not add specific one
61
- if (templateName === identifier) {
62
+ if (templateName === 'templates') {
62
63
 
63
64
  let allTemplatesCompiled = '';
64
65
  for (let templateName in templates) {
@@ -211,7 +212,7 @@ function cleanALine(line) {
211
212
 
212
213
  function processCwd() {
213
214
  return process.cwd()
214
- .replace('/.grunt', '')
215
+ .replace('/scripts', '')
215
216
  .replace('/gulp', '')
216
217
  .replace('/node_modules/vanilla-jet', '');
217
- }
218
+ }
@@ -1,30 +0,0 @@
1
- module.exports = function(grunt) {
2
- grunt.registerTask('buildLess', 'Compile admin.less and add .section.less', function() {
3
-
4
- // -- Functions
5
- function getCleanedCWD() {
6
- const cwd = process.cwd();
7
- return cwd
8
- .replace('/node_modules', '')
9
- .replace('/vanilla-jet', '')
10
- .replace('/.grunt', '');
11
- }
12
-
13
- // -- Content
14
- let adminContent = grunt.file.read(`${getCleanedCWD()}/assets/styles/less/admin.less`);
15
- let sectionFiles = grunt.file.expand([
16
- `${getCleanedCWD()}/assets/styles/less/sections/**/*.section.less`,
17
- `${getCleanedCWD()}/assets/styles/less/sections/*.section.less`
18
- ]);
19
-
20
- let combinedContent = adminContent + '\n';
21
- sectionFiles.forEach(function(filePath) {
22
- let sectionContent = grunt.file.read(filePath);
23
- combinedContent += `\n/* ${filePath} */\n` + sectionContent;
24
- });
25
-
26
- // -- New file
27
- grunt.file.write(`${getCleanedCWD()}/assets/styles/less/admin_build.less`, combinedContent);
28
- grunt.task.run(['less']);
29
- });
30
- };