vanilla-jet 1.4.2 → 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/.scripts/generate_packages_json.js +0 -1
- package/CHANGELOG.md +73 -0
- package/README.md +3 -1
- package/bin.js +25 -0
- package/framework/dipper.js +31 -1
- package/framework/response.js +5 -0
- package/framework/router.js +44 -40
- package/framework/server.js +26 -7
- package/framework/sw.template.js +82 -0
- package/gulpfile.js +19 -4
- package/master.md +450 -0
- package/package.json +2 -3
- package/{.grunt → scripts}/compile_html.js +15 -6
- package/scripts/generate_sw.js +177 -0
- package/test/config.test.js +47 -0
- package/test/dipper.test.js +76 -0
- package/test/helpers.js +66 -0
- package/test/router.test.js +58 -0
- package/test/server.test.js +103 -0
- package/test/service-worker.test.js +118 -0
- package/.grunt/build_styles_task.js +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,78 @@ 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
|
+
|
|
53
|
+
## [1.4.3] - 2026-02-19
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- Grunt build artifacts: removed `.grunt/` folder, `build_styles_task.js`, and `compile_html.js`.
|
|
58
|
+
- Build pipeline now uses only Gulp; template compilation moved to `scripts/compile_html.js`.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- `gulpfile.js`: `compileTemplates` now invokes `node scripts/compile_html.js` instead of `.grunt/compile_html.js`.
|
|
63
|
+
- `framework/dipper.js`, `.scripts/generate_packages_json.js`: removed `.grunt` references from `processCwd` helpers.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- **Reliability under rapid reloads (F5)**: server no longer stops responding after repeated refreshes.
|
|
68
|
+
- Removed `fs.watch` per static file (`staticFileWatchers`); could exhaust resources with prolonged use.
|
|
69
|
+
- Added fallback to `404` for routes without a handled static extension (avoids hanging requests).
|
|
70
|
+
- Destroy file streams when client disconnects (`res.on('close')`) to avoid orphaned streams.
|
|
71
|
+
- Applied same stream cleanup in `response.render()` for HTML template delivery.
|
|
72
|
+
- Added defensive server timeouts: `requestTimeout`, `headersTimeout`, `keepAliveTimeout` (configurable via `settings.profile`).
|
|
73
|
+
|
|
74
|
+
### Compatibility notes
|
|
75
|
+
|
|
76
|
+
- No public API changes.
|
|
77
|
+
- Build output and route behavior unchanged; only internal reliability improvements.
|
|
78
|
+
|
|
7
79
|
## [1.4.2] - 2026-02-19
|
|
8
80
|
|
|
9
81
|
### Changed
|
|
@@ -112,4 +184,5 @@ The format follows a structure inspired by Keep a Changelog and semantic version
|
|
|
112
184
|
[1.3.5]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.5
|
|
113
185
|
[1.3.6]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.3.6
|
|
114
186
|
[1.4.2]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.2
|
|
187
|
+
[1.4.3]: https://github.com/nalancer08/VanillaJet/releases/tag/v1.4.3
|
|
115
188
|
[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.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`
|
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
|
}
|
package/framework/dipper.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -545,7 +576,6 @@ Dipper.prototype.includeEnvironment = function() {
|
|
|
545
576
|
Dipper.prototype.processCwd = function() {
|
|
546
577
|
|
|
547
578
|
let cwd = process.cwd()
|
|
548
|
-
.replace('/.grunt', '')
|
|
549
579
|
.replace('/.scripts', '')
|
|
550
580
|
.replace('/node_modules', '')
|
|
551
581
|
.replace('/vanilla-jet', '');
|
package/framework/response.js
CHANGED
package/framework/router.js
CHANGED
|
@@ -19,7 +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
22
|
this.staticStreamChunkSize = 128 * 1024;
|
|
24
23
|
this.mimes = {
|
|
25
24
|
'png': 'image/png',
|
|
@@ -42,6 +41,7 @@ class Router {
|
|
|
42
41
|
this.compressionMimes = [ 'css', 'js' ];
|
|
43
42
|
this.compressionFiles = [ 'vanilla.min.js', 'app.min.css' ];
|
|
44
43
|
this.enablePrecompressedNegotiation = Boolean(server?.options?.enable_precompressed_negotiation);
|
|
44
|
+
this.enableServiceWorker = Boolean(server?.options?.enable_service_worker);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
routeToRegExp(route) {
|
|
@@ -100,10 +100,15 @@ class Router {
|
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
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
|
+
|
|
103
108
|
// -- Check static files
|
|
104
109
|
if (!handled && !isMatch) {
|
|
105
110
|
|
|
106
|
-
let ext = path.extname(request.path).replace('.', ''),
|
|
111
|
+
let ext = path.extname(request.path).replace('.', ''),
|
|
107
112
|
extHandled = false,
|
|
108
113
|
extHeader = {};
|
|
109
114
|
|
|
@@ -149,12 +154,18 @@ class Router {
|
|
|
149
154
|
res.writeHead(500);
|
|
150
155
|
res.end('Server Error');
|
|
151
156
|
});
|
|
157
|
+
res.on('close', () => {
|
|
158
|
+
if (!res.writableEnded) {
|
|
159
|
+
fileStream.destroy();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
152
162
|
|
|
153
163
|
res.writeHead(200, staticHeaders);
|
|
154
164
|
fileStream.pipe(res);
|
|
155
|
-
res.on('close', () => {});
|
|
156
165
|
});
|
|
157
|
-
}
|
|
166
|
+
} else {
|
|
167
|
+
return obj.onNotFound(response);
|
|
168
|
+
}
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
171
|
}), handled = false;
|
|
@@ -181,7 +192,6 @@ class Router {
|
|
|
181
192
|
};
|
|
182
193
|
|
|
183
194
|
obj.staticMetadataCache.set(filename, metadata);
|
|
184
|
-
obj.watchStaticFile(filename);
|
|
185
195
|
callback(null, metadata);
|
|
186
196
|
});
|
|
187
197
|
}
|
|
@@ -310,41 +320,6 @@ class Router {
|
|
|
310
320
|
});
|
|
311
321
|
}
|
|
312
322
|
|
|
313
|
-
watchStaticFile(filename) {
|
|
314
|
-
let obj = this;
|
|
315
|
-
if (obj.staticFileWatchers.has(filename)) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
let watcher = fs.watch(filename, (eventType) => {
|
|
321
|
-
obj.staticMetadataCache.delete(filename);
|
|
322
|
-
obj.staticResolutionCache.clear();
|
|
323
|
-
if (eventType === 'rename') {
|
|
324
|
-
let renamedWatcher = obj.staticFileWatchers.get(filename);
|
|
325
|
-
if (renamedWatcher) {
|
|
326
|
-
renamedWatcher.close();
|
|
327
|
-
}
|
|
328
|
-
obj.staticFileWatchers.delete(filename);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
watcher.on('error', () => {
|
|
333
|
-
obj.staticMetadataCache.delete(filename);
|
|
334
|
-
obj.staticResolutionCache.clear();
|
|
335
|
-
let activeWatcher = obj.staticFileWatchers.get(filename);
|
|
336
|
-
if (activeWatcher) {
|
|
337
|
-
activeWatcher.close();
|
|
338
|
-
}
|
|
339
|
-
obj.staticFileWatchers.delete(filename);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
obj.staticFileWatchers.set(filename, watcher);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
// If watch cannot be created, keep runtime behavior and continue.
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
323
|
isNotModified(req, metadata) {
|
|
349
324
|
let ifNoneMatch = req.headers['if-none-match'];
|
|
350
325
|
if (ifNoneMatch) {
|
|
@@ -400,6 +375,35 @@ class Router {
|
|
|
400
375
|
return this.defaultRoute;
|
|
401
376
|
}
|
|
402
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
|
+
|
|
403
407
|
onNotFound(response) {
|
|
404
408
|
response.setStatus(404);
|
|
405
409
|
response.respond();
|
package/framework/server.js
CHANGED
|
@@ -23,16 +23,23 @@ class Server {
|
|
|
23
23
|
|
|
24
24
|
init(options, endpoints) {
|
|
25
25
|
|
|
26
|
-
let obj = this,
|
|
27
|
-
settings = options.settings,
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
enable_precompressed_negotiation: false
|
|
38
|
+
enable_precompressed_negotiation: false,
|
|
39
|
+
enable_service_worker: false,
|
|
40
|
+
request_timeout_ms: 30000,
|
|
41
|
+
headers_timeout_ms: 35000,
|
|
42
|
+
keep_alive_timeout_ms: 5000
|
|
36
43
|
});
|
|
37
44
|
obj.options = opts;
|
|
38
45
|
|
|
@@ -67,8 +74,11 @@ class Server {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
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);
|
|
70
80
|
this.log('__________________ VanillaJet Server started ___________________');
|
|
71
|
-
this.log(` > Running on 0.0.0.0:${
|
|
81
|
+
this.log(` > Running on 0.0.0.0:${boundPort}/ < `);
|
|
72
82
|
this.log('------------------------------------------------------------------');
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -91,7 +101,16 @@ class Server {
|
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
// -- Set the port
|
|
94
|
-
obj.httpx.
|
|
104
|
+
obj.httpx.requestTimeout = Number(obj.options.request_timeout_ms) || 30000;
|
|
105
|
+
obj.httpx.headersTimeout = Number(obj.options.headers_timeout_ms) || 35000;
|
|
106
|
+
obj.httpx.keepAliveTimeout = Number(obj.options.keep_alive_timeout_ms) || 5000;
|
|
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);
|
|
95
114
|
}
|
|
96
115
|
|
|
97
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
|
|
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;
|