vanilla-jet 1.6.1 → 1.6.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.
- package/CHANGELOG.md +27 -0
- package/framework/sw.template.js +11 -2
- package/package.json +1 -1
- package/scripts/compile_html.js +20 -0
- package/scripts/generate_sw.js +19 -0
- package/test/service-worker.test.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ 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.3] - 2026-07-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **SW precache no longer reads stale bundles from the browser's HTTP cache:** install now fetches
|
|
12
|
+
the fingerprinted urls (`?v=size-mtime`, guaranteed HTTP-cache miss) with `cache: 'no-cache'` as a
|
|
13
|
+
second guard. Previously it fetched the bare asset paths through the HTTP cache; with
|
|
14
|
+
`static_cache_max_age` set, a new SW could pin an outdated bundle into a brand-new cache and —
|
|
15
|
+
because serving is cache-first with `ignoreSearch` — every deploy became invisible to returning
|
|
16
|
+
users for up to `static_cache_max_age` seconds unless they hard-reloaded (which bypasses the SW
|
|
17
|
+
per spec but never rewrites its Cache Storage). Diagnosed in production on a consumer app.
|
|
18
|
+
|
|
19
|
+
## [1.6.2] - 2026-06-30
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Template externalization now self-cleans:** disabling `externalize_templates` (or never enabling it)
|
|
24
|
+
removes any previously generated `public/scripts/templates.js` (+ `.gz`/`.br`), so toggling the flag
|
|
25
|
+
never leaves a stale 749 KB file behind (which the service worker would otherwise precache).
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
|
|
29
|
+
- **`externalize_templates` caveat (WebView):** it shrinks the initial HTML for web browsers, but it injects
|
|
30
|
+
all templates via one synchronous `innerHTML`. On slower JS engines (native WebViews — WKWebView /
|
|
31
|
+
Android WebView) that block can stall the boot. **Prefer it OFF for apps loaded primarily inside a
|
|
32
|
+
WebView**; keep it ON only for pure-web apps that benefit from a smaller first byte.
|
|
33
|
+
|
|
7
34
|
## [1.6.1] - 2026-06-29
|
|
8
35
|
|
|
9
36
|
### Security
|
package/framework/sw.template.js
CHANGED
|
@@ -11,11 +11,18 @@
|
|
|
11
11
|
* produces a new cache and activate() purges the stale ones. Because VanillaJet
|
|
12
12
|
* fingerprints asset URLs (`?v=size-mtime`), matches use { ignoreSearch: true }
|
|
13
13
|
* so a cache entry keeps serving across version query changes within the cache.
|
|
14
|
+
*
|
|
15
|
+
* Precaching fetches the FINGERPRINTED urls (PRECACHE_URLS) with cache: 'no-cache'.
|
|
16
|
+
* Both guards exist so install never reads a stale copy from the browser's HTTP
|
|
17
|
+
* cache: bare asset paths may sit there for `static_cache_max_age` seconds, and a
|
|
18
|
+
* precache that goes through it would pin an outdated bundle into a brand-new
|
|
19
|
+
* cache — making every deploy invisible until a hard reload or header expiry.
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
const CACHE_NAME = '__CACHE_NAME__';
|
|
17
23
|
const CACHE_PREFIX = '__CACHE_PREFIX__';
|
|
18
24
|
const PRECACHE_ASSETS = __PRECACHE_ASSETS__;
|
|
25
|
+
const PRECACHE_URLS = __PRECACHE_URLS__;
|
|
19
26
|
const ON_DEMAND_PREFIXES = __ON_DEMAND_PREFIXES__;
|
|
20
27
|
|
|
21
28
|
const MATCH_OPTIONS = { ignoreSearch: true };
|
|
@@ -30,10 +37,12 @@ function isCacheable(pathname) {
|
|
|
30
37
|
globalThis.addEventListener('install', (event) => {
|
|
31
38
|
event.waitUntil(
|
|
32
39
|
caches.open(CACHE_NAME).then((cache) =>
|
|
33
|
-
Promise.allSettled(
|
|
40
|
+
Promise.allSettled(
|
|
41
|
+
PRECACHE_URLS.map((asset) => cache.add(new Request(asset, { cache: 'no-cache' })))
|
|
42
|
+
).then((results) => {
|
|
34
43
|
results.forEach((result, i) => {
|
|
35
44
|
if (result.status === 'rejected') {
|
|
36
|
-
console.error('SW precache failed: ' +
|
|
45
|
+
console.error('SW precache failed: ' + PRECACHE_URLS[i], result.reason);
|
|
37
46
|
}
|
|
38
47
|
});
|
|
39
48
|
})
|
package/package.json
CHANGED
package/scripts/compile_html.js
CHANGED
|
@@ -201,6 +201,10 @@ async function createHTMLFile(content, filePath) {
|
|
|
201
201
|
// bundle (defer + document order), so views still find their templates in the DOM at boot.
|
|
202
202
|
if (opts.externalize_templates) {
|
|
203
203
|
minified = externalizeTemplates(minified);
|
|
204
|
+
} else {
|
|
205
|
+
// Feature off: remove any previously generated templates.js so a stale 749KB file
|
|
206
|
+
// isn't left behind (and isn't precached by the service worker).
|
|
207
|
+
removeStaleTemplatesFile();
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
const publicPath = path.join(processCwd(), '/public/pages');
|
|
@@ -218,6 +222,22 @@ async function createHTMLFile(content, filePath) {
|
|
|
218
222
|
console.log(chalk.green(`Created gzipped version at: ${absolutePath}.gz`));
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
// -- Remove a previously generated public/scripts/templates.js (+ compressed siblings)
|
|
226
|
+
// when externalization is disabled, so toggling the flag never leaves a stale file behind.
|
|
227
|
+
function removeStaleTemplatesFile() {
|
|
228
|
+
const base = path.join(processCwd(), 'public', 'scripts', 'templates.js');
|
|
229
|
+
['', '.gz', '.br'].forEach((ext) => {
|
|
230
|
+
try {
|
|
231
|
+
if (fs.existsSync(base + ext)) {
|
|
232
|
+
fs.unlinkSync(base + ext);
|
|
233
|
+
console.log(chalk.green(`VanillaJet - removed stale public/scripts/templates.js${ext}`));
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
// Non-fatal: never fail the build over cleanup.
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
221
241
|
// -- Move all <script type="text/template"> blocks out of the page into
|
|
222
242
|
// public/scripts/templates.js (a tiny injector that adds them to the DOM). Returns the
|
|
223
243
|
// page HTML with those blocks replaced by a single deferred <script> tag (placed where the
|
package/scripts/generate_sw.js
CHANGED
|
@@ -123,6 +123,23 @@ function buildPrecacheList(root, opts, shared) {
|
|
|
123
123
|
return precache;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// Fingerprint each precache path (`?v=size-mtime`, same scheme the Dipper uses for
|
|
127
|
+
// asset tags). Install fetches THESE urls: a content change produces a new URL, which
|
|
128
|
+
// guarantees an HTTP-cache miss. Precaching the bare path instead can read a stale
|
|
129
|
+
// copy the HTTP cache is allowed to keep for `static_cache_max_age` seconds, pinning
|
|
130
|
+
// an outdated bundle into the new SW cache (deploys invisible until a hard reload).
|
|
131
|
+
function versionAssets(root, precache) {
|
|
132
|
+
return precache.map((assetPath) => {
|
|
133
|
+
const absolute = path.join(root, assetPath.replace(/^\//, ''));
|
|
134
|
+
try {
|
|
135
|
+
const stats = fs.statSync(absolute);
|
|
136
|
+
return `${assetPath}?v=${stats.size}-${Math.floor(stats.mtimeMs)}`;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return assetPath;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
function computeCacheHash(root, precache) {
|
|
127
144
|
const hash = crypto.createHash('md5');
|
|
128
145
|
precache.forEach((assetPath) => {
|
|
@@ -161,6 +178,7 @@ function main() {
|
|
|
161
178
|
: DEFAULT_ON_DEMAND_PREFIXES;
|
|
162
179
|
|
|
163
180
|
const precache = buildPrecacheList(root, opts, shared);
|
|
181
|
+
const precacheUrls = versionAssets(root, precache);
|
|
164
182
|
const cacheName = `${baseSlug}-sw-${computeCacheHash(root, precache)}`;
|
|
165
183
|
|
|
166
184
|
let template = fs.readFileSync(TEMPLATE_PATH, 'utf8');
|
|
@@ -168,6 +186,7 @@ function main() {
|
|
|
168
186
|
.replace(/__CACHE_NAME__/g, cacheName)
|
|
169
187
|
.replace(/__CACHE_PREFIX__/g, cachePrefix)
|
|
170
188
|
.replace('__PRECACHE_ASSETS__', JSON.stringify(precache, null, '\t'))
|
|
189
|
+
.replace('__PRECACHE_URLS__', JSON.stringify(precacheUrls, null, '\t'))
|
|
171
190
|
.replace('__ON_DEMAND_PREFIXES__', JSON.stringify(onDemandPrefixes, null, '\t'));
|
|
172
191
|
|
|
173
192
|
const publicDir = path.join(root, 'public');
|
|
@@ -38,7 +38,12 @@ describe('service worker generation', () => {
|
|
|
38
38
|
assert.match(sw, /testapp-sw-[a-f0-9]{12}/, 'cache name should be content-pinned');
|
|
39
39
|
assert.match(sw, /\/public\/scripts\/vanilla\.min\.js/);
|
|
40
40
|
assert.match(sw, /\/public\/styles\/app\.min\.css/);
|
|
41
|
+
// Precache must fetch fingerprinted urls, bypassing any HTTP-cached bare path
|
|
42
|
+
// (a stale HTTP-cache hit would pin an outdated bundle into the new SW cache).
|
|
43
|
+
assert.match(sw, /\/public\/scripts\/vanilla\.min\.js\?v=\d+-\d+/, 'precache urls should be fingerprinted');
|
|
44
|
+
assert.match(sw, /cache: 'no-cache'/, 'precache requests should revalidate against the server');
|
|
41
45
|
assert.ok(!sw.includes('__PRECACHE_ASSETS__'), 'no placeholders should remain');
|
|
46
|
+
assert.ok(!sw.includes('__PRECACHE_URLS__'), 'no placeholders should remain');
|
|
42
47
|
assert.ok(!sw.includes('__CACHE_NAME__'), 'no placeholders should remain');
|
|
43
48
|
} finally {
|
|
44
49
|
removeWorkspace(ws);
|