vanilla-jet 1.5.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ 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.6.0] - 2026-06-29
8
+
9
+ ### Added
10
+
11
+ - **Template externalization (opt-in):** `settings.profile.externalize_templates`. `compile_html.js`
12
+ moves `<script type="text/template">` blocks out of the page into a cacheable `public/scripts/templates.js`
13
+ (loaded `defer` before the app bundle, so views still find their templates in the DOM at boot). Shrinks
14
+ the initial HTML dramatically; the templates file is brotli + immutable + service-worker cached. The SW
15
+ precache now includes `templates.js`.
16
+
17
+ ### Fixed
18
+
19
+ - **Watch keeps precompressed assets fresh:** `compressBr` now runs in the LESS/JS watch series, so `.gz`/`.br`
20
+ no longer go stale on incremental dev rebuilds.
21
+
22
+ ### Notes
23
+
24
+ - Recommended: enable caching features (`enable_service_worker`, `externalize_templates`, precompression,
25
+ immutable) in `qa`/`production`; keep `enable_service_worker` **off in development` so a cache-first SW does
26
+ not serve stale assets during rapid rebuilds.
27
+
28
+ ## [1.5.5] - 2026-06-28
29
+
30
+ ### Changed
31
+
32
+ - **Fingerprinted assets are now cached immutably.** Static requests carrying a `?v=` (the
33
+ `?v=size-mtime` fingerprint from `dipper.versionedUrl`) are served `Cache-Control: public,
34
+ max-age=31536000, immutable`; non-versioned assets keep `no-cache, must-revalidate`. This eliminates
35
+ per-asset revalidation round-trips for clients without the service worker (notably native WebViews).
36
+
7
37
  ## [1.5.4] - 2026-06-28
8
38
 
9
39
  ### Fixed
package/README.md CHANGED
@@ -1,19 +1,23 @@
1
1
  # VanillaJet
2
2
 
3
- Node.js framework for building SPA applications with a JS/CSS/HTML build pipeline, HTTP/HTTPS server, internal router, and template rendering utilities.
3
+ Node.js framework for building SPA applications: a Gulp build pipeline (JS/CSS/HTML), a lightweight
4
+ HTTP/HTTPS server, an internal router, and template/resource utilities — with first-class, opt-in
5
+ performance features (Brotli, immutable caching, a generated service worker, deferred scripts, and
6
+ template externalization).
4
7
 
5
8
  ![VanillaJet logo](https://github.com/nalancer08/App-Builders/blob/master/Logos/logo_monocromatico_horizontal_.png)
6
9
 
7
10
  ## Current version
8
11
 
9
- - Version: `1.4.3`
10
- - Changelog: see [`CHANGELOG.md`](./CHANGELOG.md)
11
- - Improvement plan (performance and backward compatibility): see `ROADMAP_INTEGRAL.md`
12
+ - Version: `1.6.0`
13
+ - Changelog: [`CHANGELOG.md`](./CHANGELOG.md)
14
+ - **Full project docs (architecture, runtime, build, deployment, perf): [`master.md`](./master.md)**
15
+ - Roadmap: [`ROADMAP_INTEGRAL.md`](./ROADMAP_INTEGRAL.md)
12
16
 
13
17
  ## Requirements
14
18
 
15
- - Node.js `>=16` recommended
16
- - npm `>=8`
19
+ - Node.js `>= 18` (tested on 24)
20
+ - npm `>= 8`
17
21
 
18
22
  ## Installation
19
23
 
@@ -21,124 +25,110 @@ Node.js framework for building SPA applications with a JS/CSS/HTML build pipelin
21
25
  npm install vanilla-jet
22
26
  ```
23
27
 
24
- If you are working in this local repository:
25
-
26
- ```bash
27
- npm install
28
- ```
29
-
30
28
  ## Quick start
31
29
 
32
- ### 1) Export the server from your project
33
-
34
30
  ```js
31
+ // index.js
35
32
  const { Server } = require('vanilla-jet');
36
- ```
37
-
38
- ### 2) Define endpoints (classes)
39
-
40
- Each endpoint should expose a `name` and register routes with the router in the constructor.
33
+ const Config = require('./config');
41
34
 
42
- ```js
43
35
  class AppEndpoint {
44
36
  constructor(router) {
45
37
  this.name = 'AppEndpoint';
46
- router.addRoute('get', '/', 'AppEndpoint.index');
38
+ router.setDefaultRoute('home'); // maps "/"
39
+ router.addRoute('get', '/home', 'AppEndpoint.home');
47
40
  }
48
-
49
- index(request, response) {
50
- response.setBody('Hello VanillaJet');
51
- response.respond();
41
+ home(request, response) {
42
+ response.render(request, 'home.html'); // streams public/pages/home.html
43
+ return true; // <- mark the request as handled
52
44
  }
53
45
  }
54
- ```
55
-
56
- ### 3) Start the server
57
-
58
- ```js
59
- const { Server } = require('vanilla-jet');
60
- const Config = require('./config');
61
46
 
62
47
  new Server(Config, [AppEndpoint]).start();
63
48
  ```
64
49
 
65
- ## Available commands
50
+ ## Configuration (`config.js`)
66
51
 
67
- From this repository:
52
+ Two shapes are supported (both resolved automatically):
68
53
 
69
- - `npm run setup`: generates a base `vanillaJet.package.json` if it does not exist.
70
- - `npm run dev`: build + watcher for development.
71
- - `npm run build:qa`: build for QA.
72
- - `npm run build:staging`: build for staging.
73
- - `npm run build:prod`: build for production.
74
- - `npm run benchmark:static`: runs reproducible static serving benchmark (cold/warm).
54
+ ```js
55
+ // Legacy (keyed by active profile name, selected with `--p qa`)
56
+ module.exports = {
57
+ profile: process.argv... , // 'development' | 'qa' | 'production'
58
+ settings: {
59
+ development: { port: 1234, api_url: '...' },
60
+ qa: { port: 443, api_url: '...' },
61
+ production: { port: 443, api_url: '...' },
62
+ shared: { site_name: 'My App', environment: 'qa', sentry: {...} },
63
+ security: { pass_salt: '...', token_salt: '...' }
64
+ }
65
+ };
75
66
 
76
- As CLI (`bin.js`):
67
+ // Nested (single profile)
68
+ module.exports = { settings: { profile: { port: 8080, api_url: '...' }, shared: {...}, security: {...} } };
69
+ ```
77
70
 
78
- - `npx vanilla-jet setup`
79
- - `npx vanilla-jet dev`
80
- - `npx vanilla-jet build`
71
+ ### Profile options (all optional)
81
72
 
82
- ## Expected consumer project structure
73
+ | Option | Default | What it does |
74
+ |---|---|---|
75
+ | `port` | `8080` | Listen port. `process.env.PORT` wins (Cloud Run/Heroku). |
76
+ | `enable_precompressed_negotiation` | `false` | Serve `.br` → `.gz` → original via `Accept-Encoding`. |
77
+ | `enable_service_worker` | `false` | Generate + serve a cache-first service worker. **Recommended: on in prod/qa, off in dev.** |
78
+ | `service_worker` | — | `{ cache_prefix, on_demand_prefixes, precache, precache_exclude }`. Precache is auto-derived from `vanillaJet.package.json`. |
79
+ | `defer_scripts` | `false` | Add `defer` to non-async scripts so they don't block parsing. |
80
+ | `externalize_templates` | `false` | Move `<script type="text/template">` blocks out of the page into a cacheable `public/scripts/templates.js`. |
81
+ | `request_timeout_ms` / `headers_timeout_ms` / `keep_alive_timeout_ms` | `30000` / `35000` / `5000` | Defensive server timeouts. |
82
+ | `https_server` / `self_managed_certs` | `false` | HTTP/2 with self-managed `key`/`cert`. |
83
83
 
84
- VanillaJet expects a structure similar to:
84
+ ## Commands
85
85
 
86
- - `assets/pages/home.html`
87
- - `assets/templates/**/*.html`
88
- - `assets/scripts/**/*.js`
89
- - `assets/styles/less/admin.less`
90
- - `public/` (compiled output)
91
- - `config.js`
92
- - `vanillaJet.package.json`
86
+ CLI (`bin.js`): `npx vanilla-jet setup | dev | build | build:qa | build:staging | build:prod`
93
87
 
94
- ## Build pipeline (summary)
88
+ From this repo: `npm run dev` · `npm run build:prod` · `npm test` · `npm run benchmark:static`
95
89
 
96
- Gulp-based pipeline (no Grunt):
90
+ ## Expected consumer structure
97
91
 
98
- - Minifies JS and concatenates into `public/scripts/vanilla.min.js`
99
- - Compiles LESS and generates `public/styles/app.min.css`
100
- - Compiles templates and generates `public/pages/home.html`
101
- - Generates `.gz` versions of JS/CSS/HTML for compressed delivery
92
+ ```
93
+ assets/pages/home.html · assets/templates/**/*.html · assets/scripts/**/*.js · assets/styles/less/admin.less
94
+ config.js · vanillaJet.package.json · public/ (build output)
95
+ ```
102
96
 
103
- ## Compression negotiation (optional)
97
+ ## Build pipeline (Gulp)
104
98
 
105
- You can enable precompressed static negotiation from `settings.profile`:
99
+ - Minifies + concatenates JS `public/scripts/vanilla.min.js`
100
+ - Compiles LESS → `public/styles/app.min.css`
101
+ - Compiles templates → `public/pages/home.html` (+ optional `templates.js`)
102
+ - Precompresses every `.js`/`.css`/`.html` to `.gz` + `.br`
103
+ - Generates the service worker (when enabled)
106
104
 
107
- ```js
108
- module.exports = {
109
- settings: {
110
- profile: {
111
- // Enables priority: .br -> .gz -> original file
112
- enable_precompressed_negotiation: true
113
- }
114
- }
115
- };
116
- ```
117
-
118
- Behavior details:
105
+ ## Performance features (opt-in)
119
106
 
120
- - Default (`false`): keeps existing gzip behavior for supported static assets.
121
- - Enabled (`true`): if client accepts Brotli, server tries `.br` first.
122
- - Safe fallback: if `.br` or `.gz` does not exist, server serves the original file.
123
- - HTML rendering (`response.render`) also uses safe runtime fallback for precompressed templates (`.br`/`.gz`/original).
107
+ - **Brotli + gzip** precompression with `Accept-Encoding` negotiation and safe fallback.
108
+ - **Immutable caching**: fingerprinted assets (`?v=size-mtime`) are served `Cache-Control: public,
109
+ max-age=31536000, immutable`; HTML and unversioned assets stay `no-cache`. Big win for clients
110
+ without the service worker (e.g. native WebViews).
111
+ - **Service worker** (cache-first), precache auto-derived from `vanillaJet.package.json`, content-pinned
112
+ cache name, `ignoreSearch` for fingerprinted URLs, and an inline registration helper
113
+ (`dipper.includeServiceWorker()`, web-only with a `window.__VJ_DISABLE_SW__` opt-out for WebViews).
114
+ - **`defer` scripts** and **template externalization** to shrink the render-blocking critical path.
124
115
 
125
- ## Static performance notes (HU 2.1)
116
+ See [`master.md`](./master.md) §12–§17 and [`docs/benchmark-static.md`](./docs/benchmark-static.md).
126
117
 
127
- Static serving includes a warm-path optimization focused on Node runtime latency:
118
+ ## Testing
128
119
 
129
- - Reuses static resolution for repeated requests (`route + accept-encoding`).
130
- - Keeps conditional revalidation (`ETag`/`Last-Modified`) strict so reload reflects changes immediately.
131
- - Keeps streaming strategy for large assets (`fs.createReadStream`) with tuned chunk size.
132
- - Preserves conditional cache behavior (`ETag`/`Last-Modified` + `304`) and precompressed fallback contract.
120
+ ```bash
121
+ npm test # node --test (router, dipper, config, static serving, service worker)
122
+ npm run benchmark:static # reproducible static-serving benchmark (cold/warm)
123
+ ```
133
124
 
134
- Benchmark guide:
125
+ ## Deployment
135
126
 
136
- - [`docs/benchmark-static.md`](./docs/benchmark-static.md)
127
+ Templates (nginx + Docker) in [`docs/deployment/`](./docs/deployment/). Honors `process.env.PORT`, so
128
+ PaaS runtimes (Cloud Run, Heroku) work without config changes. Enable caching features in prod/qa
129
+ profiles; keep them off in `development` for fresh iteration.
137
130
 
138
- ## Additional documentation
131
+ ## More docs
139
132
 
140
- - Router: `docs/router.md`
141
- - Benchmark: [`docs/benchmark-static.md`](./docs/benchmark-static.md)
142
- - Version history: [`CHANGELOG.md`](./CHANGELOG.md)
143
- - Roadmap and improvements: `ROADMAP_INTEGRAL.md`
144
- - Deployment templates (nginx + docker): `docs/deployment/`
133
+ - **[`master.md`](./master.md)** — full architecture, runtime flow, perf playbook, upgrade notes.
134
+ - [`docs/router.md`](./docs/router.md) · [`CHANGELOG.md`](./CHANGELOG.md) · [`ROADMAP_INTEGRAL.md`](./ROADMAP_INTEGRAL.md)
@@ -139,7 +139,10 @@ class Router {
139
139
  }
140
140
 
141
141
  let metadata = staticFile.metadata;
142
- let staticHeaders = obj.buildStaticHeaders(extHeader, staticCandidates, staticFile.contentEncoding, metadata);
142
+ // Fingerprinted assets (requested with ?v=size-mtime) are safe to cache forever:
143
+ // any content change produces a new URL. Everything else keeps revalidation.
144
+ let isImmutable = Boolean(request.get('v'));
145
+ let staticHeaders = obj.buildStaticHeaders(extHeader, staticCandidates, staticFile.contentEncoding, metadata, isImmutable);
143
146
 
144
147
  if (obj.isNotModified(req, metadata)) {
145
148
  let notModifiedHeaders = Object.assign({}, staticHeaders);
@@ -280,7 +283,7 @@ class Router {
280
283
  return `${route}|${normalizedEncodings}`;
281
284
  }
282
285
 
283
- buildStaticHeaders(extHeader, candidates, contentEncoding, metadata) {
286
+ buildStaticHeaders(extHeader, candidates, contentEncoding, metadata, isImmutable) {
284
287
  let staticHeaders = Object.assign({}, extHeader);
285
288
  if (contentEncoding) {
286
289
  staticHeaders['Content-Encoding'] = contentEncoding;
@@ -292,8 +295,15 @@ class Router {
292
295
  staticHeaders['Content-Length'] = metadata.size;
293
296
  staticHeaders['ETag'] = metadata.etag;
294
297
  staticHeaders['Last-Modified'] = metadata.lastModified;
295
- // Force revalidation to keep clients fresh without hard reload.
296
- staticHeaders['Cache-Control'] = 'no-cache, must-revalidate';
298
+ if (isImmutable) {
299
+ // Fingerprinted URL (?v=): cache for a year and skip revalidation entirely.
300
+ // This is the big win for clients without the service worker (e.g. native WebViews),
301
+ // which otherwise revalidate every asset on every load.
302
+ staticHeaders['Cache-Control'] = 'public, max-age=31536000, immutable';
303
+ } else {
304
+ // Non-versioned assets: force revalidation to keep clients fresh without a hard reload.
305
+ staticHeaders['Cache-Control'] = 'no-cache, must-revalidate';
306
+ }
297
307
  return staticHeaders;
298
308
  }
299
309
 
package/gulpfile.js CHANGED
@@ -142,7 +142,8 @@ function watchFiles(cb) {
142
142
  buildLess,
143
143
  compressCss,
144
144
  compileTemplates,
145
- generateServiceWorker
145
+ generateServiceWorker,
146
+ compressBr
146
147
  ));
147
148
 
148
149
  // Watch HTML files
@@ -159,7 +160,8 @@ function watchFiles(cb) {
159
160
  cleanMinified,
160
161
  compressJs,
161
162
  compileTemplates,
162
- generateServiceWorker
163
+ generateServiceWorker,
164
+ compressBr
163
165
  ));
164
166
 
165
167
  cb();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jet",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "VannilaJet framework",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -184,7 +184,7 @@ function replaceInclude(lines, originalLine, templateCompiled) {
184
184
  // -- Step 4
185
185
  async function createHTMLFile(content, filePath) {
186
186
  const { minify } = require('html-minifier-terser');
187
- const minified = await minify(content, {
187
+ let minified = await minify(content, {
188
188
  collapseWhitespace: true,
189
189
  collapseInlineTagWhitespace: true,
190
190
  removeComments: true,
@@ -195,6 +195,14 @@ async function createHTMLFile(content, filePath) {
195
195
  minifyJS: true
196
196
  });
197
197
 
198
+ // -- Externalize <script type="text/template"> blocks into public/scripts/templates.js
199
+ // (opt-in via settings.profile.externalize_templates). The page shrinks to ~the shell;
200
+ // templates load from a separate cacheable file (brotli + SW + immutable) BEFORE the app
201
+ // bundle (defer + document order), so views still find their templates in the DOM at boot.
202
+ if (opts.externalize_templates) {
203
+ minified = externalizeTemplates(minified);
204
+ }
205
+
198
206
  const publicPath = path.join(processCwd(), '/public/pages');
199
207
  fs.mkdirSync(publicPath, { recursive: true });
200
208
  const absolutePath = path.join(publicPath, filePath);
@@ -210,6 +218,45 @@ async function createHTMLFile(content, filePath) {
210
218
  console.log(chalk.green(`Created gzipped version at: ${absolutePath}.gz`));
211
219
  }
212
220
 
221
+ // -- Move all <script type="text/template"> blocks out of the page into
222
+ // public/scripts/templates.js (a tiny injector that adds them to the DOM). Returns the
223
+ // page HTML with those blocks replaced by a single deferred <script> tag (placed where the
224
+ // first block was — before the app bundle, so it runs first by document order).
225
+ function externalizeTemplates(html) {
226
+ const templateRegex = /<script\b[^>]*\btype\s*=\s*["']text\/template["'][^>]*>[\s\S]*?<\/script>/gi;
227
+ const blocks = html.match(templateRegex) || [];
228
+ if (!blocks.length) {
229
+ return html;
230
+ }
231
+
232
+ const scriptsDir = path.join(processCwd(), 'public', 'scripts');
233
+ fs.mkdirSync(scriptsDir, { recursive: true });
234
+
235
+ const payload = blocks.join('\n');
236
+ // External file → a literal </script> inside the payload is harmless (no inline HTML parser).
237
+ const injector =
238
+ '(function(){var c=document.createElement("div");c.style.display="none";' +
239
+ 'c.setAttribute("data-vj-templates","");c.innerHTML=' + JSON.stringify(payload) + ';' +
240
+ 'document.body.insertBefore(c,document.body.firstChild);})();';
241
+
242
+ const templatesPath = path.join(scriptsDir, 'templates.js');
243
+ fs.writeFileSync(templatesPath, injector, 'utf8');
244
+ fs.writeFileSync(templatesPath + '.gz', zlib.gzipSync(injector, { level: 9 }));
245
+
246
+ const stats = fs.statSync(templatesPath);
247
+ const version = `${stats.size}-${Math.floor(stats.mtimeMs)}`;
248
+ const tag = `<script defer src="/public/scripts/templates.js?v=${version}"></script>`;
249
+
250
+ let inserted = false;
251
+ const result = html.replace(templateRegex, () => {
252
+ if (!inserted) { inserted = true; return tag; }
253
+ return '';
254
+ });
255
+
256
+ console.log(chalk.green(`VanillaJet - externalized ${blocks.length} templates to public/scripts/templates.js`));
257
+ return result;
258
+ }
259
+
213
260
  // -- Helpers
214
261
  function cleanALine(line) {
215
262
  line = line.replaceAll(' ', '');
@@ -15,7 +15,8 @@ const TEMPLATE_PATH = path.join(__dirname, '..', 'framework', 'sw.template.js');
15
15
  const CORE_PRECACHE = [
16
16
  '/public/styles/app.min.css',
17
17
  '/public/scripts/vanilla.min.js',
18
- '/public/scripts/core/vanillaJet.min.js'
18
+ '/public/scripts/core/vanillaJet.min.js',
19
+ '/public/scripts/templates.js'
19
20
  ];
20
21
 
21
22
  const DEFAULT_ON_DEMAND_PREFIXES = ['/public/animations/', '/public/images/'];
@@ -78,6 +78,17 @@ test('static asset is served with caching validators', async () => {
78
78
  assert.ok(res.headers.get('last-modified'), 'Last-Modified should be present');
79
79
  });
80
80
 
81
+ test('fingerprinted (?v=) asset is cached immutable; plain asset revalidates', async () => {
82
+ const versioned = await fetch(`${baseUrl}/public/scripts/test.min.js?v=123-456`);
83
+ await versioned.arrayBuffer();
84
+ assert.equal(versioned.status, 200);
85
+ assert.match(versioned.headers.get('cache-control') || '', /immutable/);
86
+
87
+ const plain = await fetch(`${baseUrl}/public/scripts/test.min.js`);
88
+ await plain.arrayBuffer();
89
+ assert.match(plain.headers.get('cache-control') || '', /no-cache/);
90
+ });
91
+
81
92
  test('conditional request returns 304 when ETag matches', async () => {
82
93
  const first = await fetch(`${baseUrl}/public/scripts/test.min.js`);
83
94
  const etag = first.headers.get('etag');