vanilla-jet 1.4.3 → 1.5.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,52 @@ 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.5.0] - 2026-06-27
8
+
9
+ ### Added
10
+
11
+ - **Service worker support (opt-in)** — `settings.profile.enable_service_worker`.
12
+ - `framework/sw.template.js`: generic cache-first worker (precache + on-demand prefixes).
13
+ - `scripts/generate_sw.js` + Gulp task `generateServiceWorker`: generates `public/sw.js` at build time.
14
+ - Precache list = core bundles (`app.min.css`, `vanilla.min.js`, `core/vanillaJet.min.js`) + LOCAL resources enqueued by the Dipper + `service_worker.precache` extras (existing files only).
15
+ - Cache name is content-pinned (md5 of `path:size-mtime`), so any asset change rotates the cache; `activate()` purges stale caches.
16
+ - Matches use `{ ignoreSearch: true }` to stay compatible with fingerprinted (`?v=`) asset URLs.
17
+ - `framework/router.js`: serves `/sw.js` from root scope with `Service-Worker-Allowed: /` and `Cache-Control: no-cache` when enabled.
18
+ - `framework/dipper.js`: `includeServiceWorker()` inline registration helper (web-only; honors `window.__VJ_DISABLE_SW__` to opt out and tear down inside native WebViews).
19
+ - Config knobs: `service_worker.precache`, `service_worker.on_demand_prefixes`, `service_worker.cache_prefix`.
20
+
21
+ ### Fixed
22
+
23
+ - **Build-time environment injection regression (backward compatibility):** `scripts/compile_html.js`
24
+ again resolves the build environment (passed by Gulp via `--env`, forwarded as argv) and reads
25
+ `settings[env]`, restoring correct `api_url`/`environment` injection (`includeEnvironment()`). The 1.4.x
26
+ rewrite read a literal `settings['profile']` and also rendered the page content as a template name,
27
+ producing `API_URL="undefined"` and `template not found` for 1.3.x-shaped configs. `gulpfile.js` now
28
+ forwards `--env` to both `compile_html.js` and `generate_sw.js`.
29
+ - **Broken dependency removed:** dropped `zlib@1.0.5` from `dependencies`. The framework uses Node's
30
+ built-in `zlib` (core modules take precedence), and the npm package has a native `node-waf` build step
31
+ that fails on modern Node — it broke `npm install`/`npm ci` for consumers. Pure dead weight.
32
+ - **CLI build commands regression (backward compatibility):** `bin.js` again handles
33
+ `build:qa`, `build:staging` and `build:prod` (they map to `gulp build --env <env>`). 1.3.x consumers
34
+ call `npx vanilla-jet build:<env>`; the 1.4.x CLI had dropped these, so the build silently did nothing.
35
+ - **Profile resolution regression (backward compatibility):** `framework/server.js` now resolves
36
+ `settings[options.profile] || settings['profile']`. Legacy consumers (1.3.x) that key settings by the
37
+ active profile name (e.g. `qa`, `production`) again receive their profile options instead of `{}`.
38
+ - **PaaS port binding:** server now honors `process.env.PORT` (Cloud Run / Heroku) before `settings.profile.port`.
39
+ - **Ephemeral port (`port: 0`) preserved:** port selection uses a nullish check instead of `|| 8080`, so a
40
+ deliberate `0` binds an ephemeral port instead of falling back to `8080`.
41
+
42
+ ### Testing
43
+
44
+ - Added a real test harness (`test/`, `node --test`, no new deps) wired to `npm test`:
45
+ router, dipper, config-shape resolution, static serving (`200`/`304`/`404`), and service worker
46
+ generation + serving.
47
+
48
+ ### Compatibility notes
49
+
50
+ - Service worker is **off by default**; existing apps are unaffected until they opt in.
51
+ - Profile-resolution fix is backward compatible with both the legacy and the nested config shapes.
52
+
7
53
  ## [1.4.3] - 2026-02-19
8
54
 
9
55
  ### Removed
package/bin.js CHANGED
@@ -28,4 +28,29 @@ switch (args[0]) {
28
28
  console.error('Error executing gulp:', error.message);
29
29
  }
30
30
  break;
31
+
32
+ // Environment-specific builds (restored for 1.3.x consumer compatibility).
33
+ case 'build:qa':
34
+ try {
35
+ execSync('npx gulp build --env qa', { stdio: 'inherit', cwd: __dirname });
36
+ } catch (error) {
37
+ console.error('Error executing gulp:', error.message);
38
+ }
39
+ break;
40
+
41
+ case 'build:staging':
42
+ try {
43
+ execSync('npx gulp build --env staging', { stdio: 'inherit', cwd: __dirname });
44
+ } catch (error) {
45
+ console.error('Error executing gulp:', error.message);
46
+ }
47
+ break;
48
+
49
+ case 'build:prod':
50
+ try {
51
+ execSync('npx gulp build --env production', { stdio: 'inherit', cwd: __dirname });
52
+ } catch (error) {
53
+ console.error('Error executing gulp:', error.message);
54
+ }
55
+ break;
31
56
  }
@@ -385,6 +385,37 @@ Dipper.prototype.includeManifest = function() {
385
385
  return tagString;
386
386
  }
387
387
 
388
+ /**
389
+ * Inline registration for the VanillaJet service worker.
390
+ * Returns '' unless settings.profile.enable_service_worker is true.
391
+ * Web-only by design: a consumer running inside a native WebView can set
392
+ * `window.__VJ_DISABLE_SW__ = true` before this runs to opt out and tear down
393
+ * any previously installed worker (WebViews get no benefit and stuck SWs are
394
+ * hard to recover there).
395
+ */
396
+ Dipper.prototype.includeServiceWorker = function() {
397
+
398
+ const obj = this;
399
+ if (!obj.options || !obj.options.enable_service_worker) {
400
+ return '';
401
+ }
402
+
403
+ return `
404
+ <script>
405
+ (function () {
406
+ if (!('serviceWorker' in navigator)) { return; }
407
+ if (window.__VJ_DISABLE_SW__) {
408
+ navigator.serviceWorker.getRegistration()
409
+ .then(function (registration) { if (registration) { registration.unregister(); } })
410
+ .catch(function (error) { console.error(error); });
411
+ return;
412
+ }
413
+ navigator.serviceWorker.register('/sw.js')
414
+ .catch(function (error) { console.error(error); });
415
+ })();
416
+ </script>`;
417
+ }
418
+
388
419
  /**
389
420
  * Setup Sentry for error reporting on production and qa environments.
390
421
  */
@@ -41,6 +41,7 @@ class Router {
41
41
  this.compressionMimes = [ 'css', 'js' ];
42
42
  this.compressionFiles = [ 'vanilla.min.js', 'app.min.css' ];
43
43
  this.enablePrecompressedNegotiation = Boolean(server?.options?.enable_precompressed_negotiation);
44
+ this.enableServiceWorker = Boolean(server?.options?.enable_service_worker);
44
45
  }
45
46
 
46
47
  routeToRegExp(route) {
@@ -99,10 +100,15 @@ class Router {
99
100
  }
100
101
  });
101
102
 
103
+ // -- Service worker: served from root scope so it can control the whole origin
104
+ if (!handled && !isMatch && obj.enableServiceWorker && request.path === '/sw.js') {
105
+ return obj.serveServiceWorker(res);
106
+ }
107
+
102
108
  // -- Check static files
103
109
  if (!handled && !isMatch) {
104
110
 
105
- let ext = path.extname(request.path).replace('.', ''),
111
+ let ext = path.extname(request.path).replace('.', ''),
106
112
  extHandled = false,
107
113
  extHeader = {};
108
114
 
@@ -369,6 +375,35 @@ class Router {
369
375
  return this.defaultRoute;
370
376
  }
371
377
 
378
+ serveServiceWorker(res) {
379
+ let obj = this;
380
+ let filename = path.join(obj.staticBasePath, 'public', 'sw.js');
381
+ fs.stat(filename, (err, stats) => {
382
+ if (err || !stats.isFile()) {
383
+ res.writeHead(404);
384
+ return res.end();
385
+ }
386
+ res.writeHead(200, {
387
+ 'Content-Type': 'text/javascript; charset=utf-8',
388
+ // Allow the SW (served at /sw.js) to control the entire origin.
389
+ 'Service-Worker-Allowed': '/',
390
+ // Keep the SW script itself fresh so updates roll out promptly.
391
+ 'Cache-Control': 'no-cache'
392
+ });
393
+ let stream = fs.createReadStream(filename);
394
+ stream.on('error', () => {
395
+ res.writeHead(500);
396
+ res.end('Server Error');
397
+ });
398
+ res.on('close', () => {
399
+ if (!res.writableEnded) {
400
+ stream.destroy();
401
+ }
402
+ });
403
+ stream.pipe(res);
404
+ });
405
+ }
406
+
372
407
  onNotFound(response) {
373
408
  response.setStatus(404);
374
409
  response.respond();
@@ -23,16 +23,20 @@ class Server {
23
23
 
24
24
  init(options, endpoints) {
25
25
 
26
- let obj = this,
27
- settings = options.settings,
28
- opts = settings['profile'] || {},
29
- shared = settings['shared'] || {},
26
+ let obj = this,
27
+ settings = options.settings,
28
+ // Resolve the active profile. Legacy consumers key settings by the active
29
+ // profile name (settings[options.profile], e.g. 'qa'); newer ones expose a
30
+ // single nested 'profile' object. Support both for backward compatibility.
31
+ opts = settings[options.profile] || settings['profile'] || {},
32
+ shared = settings['shared'] || {},
30
33
  security = settings['security'] || {};
31
34
 
32
35
  _.defaults(opts, {
33
36
  https_server: false,
34
37
  wsServer: false,
35
38
  enable_precompressed_negotiation: false,
39
+ enable_service_worker: false,
36
40
  request_timeout_ms: 30000,
37
41
  headers_timeout_ms: 35000,
38
42
  keep_alive_timeout_ms: 5000
@@ -70,8 +74,11 @@ class Server {
70
74
  }
71
75
 
72
76
  start() {
77
+ let boundPort = (this.httpx && this.httpx.address && this.httpx.address())
78
+ ? this.httpx.address().port
79
+ : (process.env.PORT || this.options.port || 8080);
73
80
  this.log('__________________ VanillaJet Server started ___________________');
74
- this.log(` > Running on 0.0.0.0:${this.options.port}/ < `);
81
+ this.log(` > Running on 0.0.0.0:${boundPort}/ < `);
75
82
  this.log('------------------------------------------------------------------');
76
83
  }
77
84
 
@@ -97,7 +104,13 @@ class Server {
97
104
  obj.httpx.requestTimeout = Number(obj.options.request_timeout_ms) || 30000;
98
105
  obj.httpx.headersTimeout = Number(obj.options.headers_timeout_ms) || 35000;
99
106
  obj.httpx.keepAliveTimeout = Number(obj.options.keep_alive_timeout_ms) || 5000;
100
- obj.httpx.listen(obj.options.port || 8080);
107
+ // Honor process.env.PORT first so PaaS runtimes (Cloud Run, Heroku, …) that
108
+ // inject the listening port at runtime work without config changes. Use a
109
+ // nullish check so a deliberate port 0 (ephemeral) is preserved.
110
+ let port = (process.env.PORT !== undefined && process.env.PORT !== '')
111
+ ? process.env.PORT
112
+ : (obj.options.port !== undefined && obj.options.port !== null ? obj.options.port : 8080);
113
+ obj.httpx.listen(port);
101
114
  }
102
115
 
103
116
  log(value) {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * VanillaJet service worker template.
3
+ *
4
+ * This file is NOT used as-is. The build (scripts/generate_sw.js) replaces the
5
+ * __PLACEHOLDERS__ below with values derived from the compiled assets and the
6
+ * consumer config, and writes the result to public/sw.js.
7
+ *
8
+ * Strategy: cache-first for a pinned set of local bundles/styles/plugins and an
9
+ * on-demand cache for prefixes (animations, images). The cache name is pinned to
10
+ * a content hash of the precached assets, so a rebuild that changes any asset
11
+ * produces a new cache and activate() purges the stale ones. Because VanillaJet
12
+ * fingerprints asset URLs (`?v=size-mtime`), matches use { ignoreSearch: true }
13
+ * so a cache entry keeps serving across version query changes within the cache.
14
+ */
15
+
16
+ const CACHE_NAME = '__CACHE_NAME__';
17
+ const CACHE_PREFIX = '__CACHE_PREFIX__';
18
+ const PRECACHE_ASSETS = __PRECACHE_ASSETS__;
19
+ const ON_DEMAND_PREFIXES = __ON_DEMAND_PREFIXES__;
20
+
21
+ const MATCH_OPTIONS = { ignoreSearch: true };
22
+
23
+ function isCacheable(pathname) {
24
+ return (
25
+ PRECACHE_ASSETS.includes(pathname) ||
26
+ ON_DEMAND_PREFIXES.some((prefix) => pathname.startsWith(prefix))
27
+ );
28
+ }
29
+
30
+ globalThis.addEventListener('install', (event) => {
31
+ event.waitUntil(
32
+ caches.open(CACHE_NAME).then((cache) =>
33
+ Promise.allSettled(PRECACHE_ASSETS.map((asset) => cache.add(asset))).then((results) => {
34
+ results.forEach((result, i) => {
35
+ if (result.status === 'rejected') {
36
+ console.error('SW precache failed: ' + PRECACHE_ASSETS[i], result.reason);
37
+ }
38
+ });
39
+ })
40
+ )
41
+ );
42
+ event.waitUntil(globalThis.skipWaiting());
43
+ });
44
+
45
+ globalThis.addEventListener('activate', (event) => {
46
+ event.waitUntil(
47
+ caches.keys().then((keys) =>
48
+ Promise.all(
49
+ keys
50
+ .filter((k) => k.startsWith(CACHE_PREFIX) && k !== CACHE_NAME)
51
+ .map((k) => caches.delete(k))
52
+ )
53
+ )
54
+ );
55
+ event.waitUntil(globalThis.clients.claim());
56
+ });
57
+
58
+ globalThis.addEventListener('fetch', (event) => {
59
+ const { request } = event;
60
+ if (request.method !== 'GET') return;
61
+
62
+ const url = new URL(request.url);
63
+ if (url.origin !== globalThis.location.origin || !isCacheable(url.pathname)) return;
64
+
65
+ // Cache-first: assets are immutable within a version (CACHE_NAME is content-pinned
66
+ // and activate() purges old caches on bump), so a cache hit never needs
67
+ // revalidation — skip the network entirely to save bandwidth on slow links.
68
+ event.respondWith(
69
+ caches.open(CACHE_NAME).then((cache) =>
70
+ cache.match(request, MATCH_OPTIONS).then((cached) => {
71
+ if (cached) return cached;
72
+
73
+ return fetch(request).then((response) => {
74
+ // waitUntil keeps the SW alive until the write finishes, so the asset
75
+ // is actually cached even if the SW is terminated right after.
76
+ if (response.ok) event.waitUntil(cache.put(request, response.clone()));
77
+ return response;
78
+ });
79
+ })
80
+ )
81
+ );
82
+ });
package/gulpfile.js CHANGED
@@ -24,6 +24,11 @@ function getCwd() {
24
24
  const base = getCwd();
25
25
  const cssOrigin = `${getCwd()}/assets/styles/less/admin.less`;
26
26
 
27
+ // Build environment (passed via `gulp build --env <env>`). Forwarded to the
28
+ // HTML/SW generators so they resolve the matching profile (api_url, etc).
29
+ const argv = minimist(process.argv.slice(2));
30
+ const buildEnv = argv.env || 'development';
31
+
27
32
  // Clean tasks
28
33
  function cleanBuildJS() {
29
34
  return del([`${getCwd()}/public/scripts/vanilla.min.js`], { force: true });
@@ -113,7 +118,13 @@ function compressCss() {
113
118
  // Template compilation
114
119
  function compileTemplates() {
115
120
  return gulp.src('.')
116
- .pipe(shell([`node scripts/compile_html.js`]));
121
+ .pipe(shell([`node scripts/compile_html.js ${buildEnv}`]));
122
+ }
123
+
124
+ // Service worker generation (opt-in via settings.profile.enable_service_worker)
125
+ function generateServiceWorker() {
126
+ return gulp.src('.')
127
+ .pipe(shell([`node scripts/generate_sw.js ${buildEnv}`]));
117
128
  }
118
129
 
119
130
  // Watch task
@@ -124,7 +135,8 @@ function watchFiles(cb) {
124
135
  watch([`${base}/assets/styles/less/**/*.less`], gulp.series(
125
136
  buildLess,
126
137
  compressCss,
127
- compileTemplates
138
+ compileTemplates,
139
+ generateServiceWorker
128
140
  ));
129
141
 
130
142
  // Watch HTML files
@@ -140,7 +152,8 @@ function watchFiles(cb) {
140
152
  concatJs,
141
153
  cleanMinified,
142
154
  compressJs,
143
- compileTemplates
155
+ compileTemplates,
156
+ generateServiceWorker
144
157
  ));
145
158
 
146
159
  cb();
@@ -154,7 +167,8 @@ const build = gulp.series(
154
167
  cleanMinified,
155
168
  buildLess,
156
169
  compileTemplates,
157
- gulp.parallel(compressJs, compressCss)
170
+ gulp.parallel(compressJs, compressCss),
171
+ generateServiceWorker
158
172
  );
159
173
 
160
174
  const dev = gulp.series(
@@ -171,6 +185,7 @@ exports.concatJs = concatJs;
171
185
  exports.compressJs = compressJs;
172
186
  exports.compressCss = compressCss;
173
187
  exports.compileTemplates = compileTemplates;
188
+ exports.generateServiceWorker = generateServiceWorker;
174
189
  exports.build = build;
175
190
  exports.dev = dev;
176
191
  exports.default = dev;