← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
starter-remix/public/sw.js
300 lines · 8.2 KB · JavaScript/**
* Package-app service worker shipped via kody.app.assets (`public/sw.js`).
* Precache the fingerprinted client from GET <assetBase>/__version.json — no hashes in source.
* Brand revision: kody-racer list/PWA mark (v4) — image/* cache guard + handoff-aware precache.
*/
'use strict'
var HTML_PATHS = ['/', '/about', '/notes']
function appBase() {
try {
return new URL(self.registration.scope).pathname.replace(/\/$/, '')
} catch (e) {
return self.location.pathname
.replace(/\/_assets\/sw\.js$/, '')
.replace(/\/sw\.js$/, '')
}
}
function assetBase(base) {
return base + '/_assets'
}
function sameOrigin(url) {
return url.origin === self.location.origin
}
function isApi(url, base) {
return url.pathname.indexOf(base + '/api/') === 0
}
function isManifest(url) {
return /manifest\.webmanifest$/.test(url.pathname)
}
function isStatic(url) {
return !isManifest(url) && /\.(png|svg|ico|webp|jpg|jpeg)$/.test(url.pathname)
}
function isClientAsset(url) {
return /\/_assets\/client\.[^/]+\.js$/.test(url.pathname)
}
function isImagePath(url) {
return /\.(png|svg|ico|webp|jpg|jpeg)$/.test(url.pathname)
}
function contentType(response) {
return String(response && response.headers && response.headers.get('content-type') || '')
.toLowerCase()
.split(';')[0]
.trim()
}
function isImageContentType(response) {
return contentType(response).indexOf('image/') === 0
}
function isHandoffRequired(response) {
if (!response) return false
if (response.status === 403) return true
try {
return String(response.headers.get('x-kody-handoff') || '').toLowerCase() === 'required'
} catch (e) {
return false
}
}
function relativePath(url, base) {
var path = url.pathname
if (path === base || path === base + '/') return '/'
if (path.indexOf(base + '/') === 0) {
var rel = path.slice(base.length)
if (rel.length > 1 && rel.charAt(rel.length - 1) === '/') rel = rel.slice(0, -1)
return rel || '/'
}
return path
}
function isHtmlPage(url, base) {
return HTML_PATHS.indexOf(relativePath(url, base)) !== -1
}
function cacheableResponse(response) {
return Boolean(response && response.ok && response.type !== 'opaque' && response.type !== 'error')
}
/** Never cache 403 handoff HTML (or non-image bodies) as static image assets. */
function cacheableStaticResponse(url, response) {
if (!cacheableResponse(response) || isHandoffRequired(response)) return false
if (isImagePath(url)) return isImageContentType(response)
return true
}
function offlineHtml() {
return new Response(
'<!doctype html><meta name="viewport" content="width=device-width, initial-scale=1"><title>Offline</title><body style="font-family:system-ui;padding:24px"><h1>Offline</h1><p>Re-open this app from Kody when you are back online.</p></body>',
{ status: 503, headers: { 'content-type': 'text/html; charset=utf-8' } },
)
}
function matchShell(cache, request) {
return cache.match(request).then(function (cached) {
if (cached) return cached
var url = new URL(request.url)
var alt = url.pathname.endsWith('/')
? url.origin + url.pathname.replace(/\/$/, '') + url.search
: url.origin + url.pathname + '/' + url.search
return cache.match(alt)
})
}
function activeCacheName(base) {
return fetch(assetBase(base) + '/__version.json', {
cache: 'no-store',
credentials: 'same-origin',
})
.then(function (res) {
if (!res.ok) throw new Error('version ' + res.status)
return res.json()
})
.then(function (version) {
return {
name: version.publishedCommit ? 'pak-' + version.publishedCommit : 'pak-fallback',
version: version,
}
})
.catch(function () {
return { name: 'pak-fallback', version: null }
})
}
function openActiveCache(base) {
return activeCacheName(base).then(function (info) {
return caches.open(info.name).then(function (cache) {
return { cache: cache, info: info }
})
})
}
/** Precache brand/icons only when the network returns a real image/* body. */
function precacheImage(cache, url) {
return fetch(url, { credentials: 'same-origin', cache: 'no-store' }).then(function (response) {
if (!cacheableStaticResponse(new URL(url, self.location.origin), response)) {
console.warn('[pak-sw] skip precache (not image/* or handoff):', url, response && response.status)
return false
}
return cache.put(url, response).then(function () {
return true
})
})
}
function precacheUrl(cache, url) {
if (isImagePath(new URL(url, self.location.origin))) {
return precacheImage(cache, url)
}
return cache.add(url).then(function () {
return true
}).catch(function () {
return false
})
}
self.addEventListener('install', function (event) {
event.waitUntil(
openActiveCache(appBase()).then(function (opened) {
var base = appBase()
var version = opened.info.version
var urls = [
base + '/',
base + '/about',
base + '/notes',
base + '/manifest.webmanifest',
base + '/icons/icon-192.png?v=4',
base + '/icons/icon-512.png?v=4',
assetBase(base) + '/sw.js',
assetBase(base) + '/styles.css',
]
if (version && version.clientModuleUrl) urls.push(version.clientModuleUrl)
return Promise.all(
urls.map(function (url) {
return precacheUrl(opened.cache, url)
}),
).then(function () {
return self.skipWaiting()
})
}),
)
})
self.addEventListener('message', function (event) {
if (event.data === 'SKIP_WAITING') self.skipWaiting()
})
self.addEventListener('activate', function (event) {
event.waitUntil(
activeCacheName(appBase())
.then(function (info) {
return caches.keys().then(function (keys) {
return Promise.all(
keys.map(function (key) {
if (key === info.name) return null
if (key.indexOf('pak-') === 0) return caches.delete(key)
if (key.indexOf('package-app-kit-demo-') === 0) return caches.delete(key)
return null
}),
)
})
})
.then(function () {
return self.clients.claim()
}),
)
})
self.addEventListener('fetch', function (event) {
var request = event.request
if (request.method !== 'GET') return
var url = new URL(request.url)
var base = appBase()
if (!sameOrigin(url) || isApi(url, base)) return
if (isManifest(url)) {
event.respondWith(
fetch(request, { credentials: 'same-origin', cache: 'no-store' })
.then(function (response) {
if (cacheableResponse(response) && !isHandoffRequired(response)) {
var copy = response.clone()
openActiveCache(base).then(function (opened) {
opened.cache.put(request, copy)
})
}
return response
})
.catch(function () {
return caches.match(request).then(function (cached) {
return cached && cached.ok ? cached : Response.error()
})
}),
)
return
}
if (request.mode === 'navigate' || isHtmlPage(url, base)) {
event.respondWith(
openActiveCache(base).then(function (opened) {
return matchShell(opened.cache, request).then(function (cached) {
var network = fetch(request, { credentials: 'same-origin' })
.then(function (response) {
if (cacheableResponse(response) && !isHandoffRequired(response)) {
opened.cache.put(request, response.clone())
var sibling = url.pathname.endsWith('/')
? url.origin + url.pathname.replace(/\/$/, '') + url.search
: url.origin + url.pathname + '/' + url.search
opened.cache.put(sibling, response.clone())
}
return response
})
.catch(function () {
return cached || offlineHtml()
})
return cached || network
})
}),
)
return
}
if (isStatic(url) || isClientAsset(url)) {
event.respondWith(
fetch(request, { credentials: 'same-origin' })
.then(function (response) {
if (cacheableStaticResponse(url, response)) {
var copy = response.clone()
openActiveCache(base).then(function (opened) {
opened.cache.put(request, copy)
})
}
return response
})
.catch(function () {
return caches.match(request).then(function (cached) {
if (!cached || !cached.ok) return Response.error()
if (isImagePath(url) && !isImageContentType(cached)) return Response.error()
return cached
})
}),
)
return
}
event.respondWith(
fetch(request, { credentials: 'same-origin' }).catch(function () {
return caches.match(request).then(function (cached) {
return cached || Response.error()
})
}),
)
})