It never rejects: a failure lands in registrationError, an Exception whose code is one of ERRORS. Later calls return the first call’s promise, and nothing runs until the first call, so it’s safe to import during server-side rendering.
Offer updates and installation
The snapshot tells you when a new version waits and whether the app can be installed. It keeps its identity until something changes, so React can read it directly:
Without React, subscribeToServiceWorker(listener) calls the listener on every change and returns a function that stops it.
updateApp() reloads the page once, never in a loop:
When a new version waits, it asks it to activate and reloads once it controls the page.
Otherwise it checks for one, waits up to UPDATE_INSTALL_TIMEOUT_SECONDS (30) for it to install, then activates it the same way.
When nothing new is found, the installation fails, or the time runs out, it reloads the page as it is.
preloadErrorDetected turns true when a chunk of a Vite build fails to load while the browser is online and the page has the build’s entry module: the chunk most likely went away with a newer deployment. jg-sw then prevents the vite:preloadError event’s default, so Vite doesn’t throw, and the app offers updateApp(), which loads the new version. A failure while offline, or on a page without an entry module, is left to Vite, so the app’s own error handling shows it. Call registerServiceWorker() as the app starts, so it hears the first failure.
installApp() shows the browser’s install prompt and resolves to 'accepted' or 'dismissed'. Only Chromium-based browsers offer it, so check canInstallApp() or installation.canInstall first.
On an iPhone or an iPad, no browser offers an install prompt, and web push works only once the app is installed to the Home Screen. When installing unlocks something your app needs, such as notifications, show your own Add to Home Screen instructions, and only where they apply: isIosBrowserTab() is true in an iOS browser tab, including an iPad that asks for desktop sites, and false in the installed app and everywhere else.
ts
import { isIosBrowserTab } from 'jg-sw';homeScreenHint.hidden = !isIosBrowserTab(); // Share, then Add to Home Screen
Describe the worker
Create jg-sw.config.json next to your app’s package.json:
The directory the app’s build is written to. The worker is written there as sw.js.
template
base caches the app. firebase-fcm also shows Firebase Cloud Messaging notifications.
cacheNamePrefix
The namespace of the worker’s caches, in lowercase words separated by hyphens, such as my-app. Each build caches under <cacheNamePrefix>--<10 characters>.
includeToPrecache
The files and directories, relative to outDir, cached when the worker installs. Directories are read recursively, and / stands for the app’s root page.
excludeFilesFromPrecache
File names left out of precaching, wherever they are.
excludeMIMETypesFromCache
MIME types whose responses are never cached.
navigationFallback
Optional. A precached page, such as /index.html, that answers a navigation the worker can’t serve offline, so a single-page app’s routes still open. The build fails with NAVIGATION_FALLBACK_NOT_PRECACHED when it isn’t precached.
This config precaches the hashed files under dist/assets, but not / or index.html. Nothing is precached unless it’s listed: add / and /index.html to precache the app’s shell.
Generate it with Vite
ts
// vite.config.tsimport { defineConfig } from 'vite';import { serviceWorkerPlugin } from 'jg-sw/build';export default defineConfig({ plugins: [serviceWorkerPlugin({ config: 'jg-sw.config.json' })],});
vite build writes dist/sw.js after the app’s files. vite serves a worker without caching at /sw.js, so you can test registration and updates while every request still reaches the dev server.
Generate it with the CLI
Run jg-sw after your build:
json
{ "scripts": { "build": "vite build && jg-sw" }}
text
Usage: jg-sw [options]Options: --config <path> The config file. Defaults to jg-sw.config.json. --environment <name> development, staging, or production. --no-caching Generates a worker without precaching, cache cleanup, or a fetch handler. --help Prints this message.
It prints the path it wrote and exits 0, or prints the error and exits 1.
buildServiceWorker returns the worker’s source without writing it.
What the worker does
install fills only the new version’s cache, and doesn’t skip waiting, so the previous worker keeps serving its clients.
The page activates a waiting worker by posting { type: 'JG_SW_SKIP_WAITING' } to it.
activate deletes the older caches in its own namespace, keeps every other cache, then claims its clients.
It answers same-origin GET requests from the current version’s cache, then from the network, and caches successful responses unless they’re opaque, partial, marked Vary: *, marked Cache-Control: no-store or private, or of an excluded MIME type. A failed cache write never replaces a valid response.
When a navigation is in neither its cache nor the network’s reach, such as offline, it answers with the navigationFallback page when one is set, and otherwise with a 408 network error. A 4xx or 5xx from the server is passed through as it is.
Generate the worker with an environment, such as jg-sw --environment production. It loads .env, .env.staging, or .env.production when the file exists, and variables already set keep their values.
The worker loads Firebase’s compat SDK from gstatic. When no app window is visible, it shows a notification for each data-only message, using its title, body, icon, badge, and url fields, and Firebase shows the messages that carry a notification. Clicking a notification focuses or opens its URL, limited to the app’s origin. To decide yourself, append code to the generated worker that assigns self.jgSwFcmBackgroundMessageHandler = async (payload) => { … }. The worker waits for it, and shows nothing else.
Generated from TSDoc
API reference
Every export of [email protected], 27 in all, with its signature, its errors, and an example.
Checks whether installApp can show the installation prompt now: the browser offered one, and the page doesn't already run as the installed app. Only Chromium-based browsers offer it.
Signature
canInstallApp(): boolean
Returns
boolean
Whether the app can be installed.
Example
import { canInstallApp } from 'jg-sw';installButton.hidden = !canInstallApp();
Reads the service worker's state: its registration, its newest worker, whether an update waits, and whether the app can be installed. The same object comes back until something changes, so it can be passed to React's useSyncExternalStore, during server rendering too.
Shows the browser's installation prompt and resolves to the user's answer, also kept in the snapshot as installation.promptOutcome. The browser offers each prompt once, so canInstallApp is false afterwards until it offers another.
Signature
installApp(): Promise<IInstallationPromptOutcome>
Returns
Promise<IInstallationPromptOutcome>
A promise that resolves to 'accepted' or 'dismissed'.
Throws
NO_APP_INSTALLERThe app can't be installed because the browser didn't offer the prompt.
Checks whether the page runs in a browser tab on an iPhone, an iPad, or an iPod touch, rather than as an app installed to the Home Screen. There, web push and some storage only work once the app is installed, and no browser offers an install prompt, so this decides when to show your own Add to Home Screen instructions. An iPad that asks for desktop sites reports a Mac, so a Mac with a touchscreen counts as one.
Signature
isIosBrowserTab(): boolean
Returns
boolean
Whether it's an iOS browser tab; false outside a browser.
Example
import { isIosBrowserTab } from 'jg-sw';homeScreenHint.hidden = !isIosBrowserTab();
Registers the service worker and starts following its updates, whether the app can be installed, and whether a chunk of a replaced build failed to load, which sets the snapshot's preloadErrorDetected. Call it as the app starts, so it hears the first failure. Later calls return the first call's promise. It never rejects: a failure lands in the snapshot's registrationError, an Exception with SW_NO_BROWSER_SUPPORT, EMPTY_SW_REGISTRATION, or SW_REGISTRATION_FAILED as its code, and the browser's error as its cause.
The worker's path and registration options, and whether to log.
Defaults to {}.
Returns
Promise<IServiceWorkerSnapshot>
A promise that resolves to the snapshot once the registration settles.
Example
import { hasErrorCode } from 'jg-exception';import { ERRORS, registerServiceWorker } from 'jg-sw';const { registration, registrationError } = await registerServiceWorker();if (hasErrorCode(registrationError, ERRORS.SW_NO_BROWSER_SUPPORT)) { // the browser has no service workers}
Updates the app to its newest version and reloads the page once, never in a loop. When a new version waits, it activates it and reloads once the new worker controls the page. Otherwise, it checks for one, waits up to UPDATE_INSTALL_TIMEOUT_SECONDS for it to install, then activates it the same way. When nothing new is found, the installation fails, or the time runs out, it reloads the page as it is.
Signature
updateApp(): Promise<void>
Returns
Promise<void>
A promise that resolves once the new worker was asked to activate, or the page started reloading.
the service worker's state. The object keeps its identity until something changes, so useSyncExternalStore(subscribeToServiceWorker, getServiceWorkerSnapshot) re-renders only on changes
Generates the service worker's source from a config, without writing it. Every generated worker gets a new cache name, <cacheNamePrefix>--<10 random characters>, so a new deployment replaces the previous one's cache when it activates.
The service worker config, as read by readServiceWorkerConfig or written inline.
options?IBuildServiceWorkerOptions
The environment the firebase-fcm template reads its Firebase config from, whether the worker caches (false for development), and the directory paths are resolved from.
Defaults to {}.
Returns
Promise<string>
A promise that resolves to the worker's source.
Throws
FAILED_TO_READ_BASE_CONFIGThe service worker config is invalid.
INVALID_TEMPLATE_NAMEThe template must be 'base' or 'firebase-fcm'.
INVALID_FIREBASE_SDK_VERSIONThe Firebase SDK version must be a version such as 12.4.0.
INVALID_ENVIRONMENTThe environment isn't supported, or the firebase-fcm template needs one.
NOT_A_PATH_ELEMENTAn asset to precache is outside outDir, or isn't a file or a directory.
NAVIGATION_FALLBACK_NOT_PRECACHEDThe navigation fallback isn't one of the precached paths.
FAILED_TO_BUILD_CONFIGUnable to load the environment file.
INVALID_FIREBASE_CONFIGThe environment variable that holds the Firebase config is missing or
Errorempty.
FAILED_TO_READ_FIREBASE_CONFIGUnable to read the Firebase config from the environment
Creates a Vite plugin that generates the service worker. After a production build, it writes sw.js into the config's outDir, once the client bundle is written. During development, the dev server answers /sw.js with a worker generated with caching: false, so every request reaches the dev server and updates show up without stale caches. Paths are resolved from Vite's root unless cwd is set. When generating fails, the build fails with writeServiceWorker's error, and the dev server answers the worker's request with it.
The service worker config, as read by readServiceWorkerConfig or written inline.
options?IBuildServiceWorkerOptions
The environment the firebase-fcm template reads its Firebase config from, whether the worker caches (false for development), and the directory paths are resolved from.
Defaults to {}.
Returns
Promise<string>
A promise that resolves to the written file's path.
Throws
FAILED_TO_READ_BASE_CONFIGThe service worker config is invalid.
INVALID_TEMPLATE_NAMEThe template must be 'base' or 'firebase-fcm'.
INVALID_FIREBASE_SDK_VERSIONThe Firebase SDK version must be a version such as 12.4.0.
INVALID_ENVIRONMENTThe environment isn't supported, or the firebase-fcm template needs one.
NOT_A_PATH_ELEMENTAn asset to precache is outside outDir, or isn't a file or a directory.
NAVIGATION_FALLBACK_NOT_PRECACHEDThe navigation fallback isn't one of the precached paths.
FAILED_TO_BUILD_CONFIGUnable to load the environment file.
INVALID_FIREBASE_CONFIGThe environment variable that holds the Firebase config is missing or
Errorempty.
FAILED_TO_READ_FIREBASE_CONFIGUnable to read the Firebase config from the environment
The schema of a service worker config: the jg-sw.config.json file the CLI reads, or the object passed to buildServiceWorker. template selects the worker: base caches the app, and firebase-fcm also shows Firebase Cloud Messaging notifications.
the environment whose file the firebase-fcm template reads its Firebase config from: .env, .env.staging, or .env.production. The firebase-fcm template requires it
cachingproperty
boolean
false leaves out precaching, cache cleanup, and the fetch handler, for development. Defaults to true
cwdproperty
string
the directory outDir and the environment files are resolved from. Defaults to process.cwd()