# jg-sw

> A generated caching worker with a Vite plugin and CLI, plus registration, updates that reload once, and app installation.

- **Version:** `1.4.0`
- **Runtimes:** Browser, Node.js
- **Install:** `npm install jg-sw`
- **On npm:** [jg-sw](https://www.npmjs.com/package/jg-sw)
- **Source:** [packages/jg-sw](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-sw)
- **Recipes:** [jg-sw recipes](https://jg-packages.jesusgraterol.dev/packages/jg-sw/recipes/index.md)

## Usage

### Register the worker

```ts
import { registerServiceWorker } from 'jg-sw';

const { registration, registrationError } = await registerServiceWorker(); // registers /sw.js
```

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:

```tsx
import { useSyncExternalStore } from 'react';
import { getServiceWorkerSnapshot, installApp, subscribeToServiceWorker, updateApp } from 'jg-sw';

export const AppBanner = () => {
  const { hasWaitingWorker, preloadErrorDetected, installation } = useSyncExternalStore(
    subscribeToServiceWorker,
    getServiceWorkerSnapshot,
    getServiceWorkerSnapshot,
  );
  return (
    <>
      {(hasWaitingWorker || preloadErrorDetected) && (
        <button onClick={() => void updateApp()}>Update</button>
      )}
      {installation.canInstall && <button onClick={() => void installApp()}>Install</button>}
    </>
  );
};
```

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`:

```json
{
  "outDir": "dist",
  "template": "base",
  "cacheNamePrefix": "my-app",
  "includeToPrecache": ["/assets"],
  "excludeFilesFromPrecache": ["some-ignorable-file.woff2"],
  "excludeMIMETypesFromCache": ["application/json", "text/plain", "text/html"]
}
```

| Field                       | What it does                                                                                                                                                                                                                             |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `outDir`                    | 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.ts
import { 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.

### Generate it from code

```ts
import { readServiceWorkerConfig, writeServiceWorker } from 'jg-sw/build';

const config = await readServiceWorkerConfig('jg-sw.config.json');
await writeServiceWorker(config, { environment: 'production' }); // '/app/dist/sw.js'
```

`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.

### Firebase Cloud Messaging

The `firebase-fcm` template adds two fields:

```json
{
  "template": "firebase-fcm",
  "firebaseConfigProcessEnvKey": "VITE_FIREBASE_CONFIG",
  "firebaseSdkVersion": "12.4.0"
}
```

The environment variable holds the Firebase web app's options as JSON:

```bash
VITE_FIREBASE_CONFIG='{"options":{"apiKey":"example-api-key","authDomain":"example.firebaseapp.com","projectId":"example-project","storageBucket":"example.firebasestorage.app","messagingSenderId":"123456789012","appId":"1:123456789012:web:example"}}'
```

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.

## <a id="api-reference"></a>jg-sw API reference

Every export of `jg-sw@1.4.0`, 27 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

### <a id="api-entry-main"></a>`jg-sw`

- Functions: [`canInstallApp`](#api-canInstallApp), [`getServiceWorkerSnapshot`](#api-getServiceWorkerSnapshot), [`installApp`](#api-installApp), [`isIosBrowserTab`](#api-isIosBrowserTab), [`registerServiceWorker`](#api-registerServiceWorker), [`subscribeToServiceWorker`](#api-subscribeToServiceWorker), [`updateApp`](#api-updateApp)
- Constants: [`DEFAULT_SERVICE_WORKER_PATH`](#api-DEFAULT_SERVICE_WORKER_PATH), [`DEFAULT_SERVICE_WORKER_SCOPE`](#api-DEFAULT_SERVICE_WORKER_SCOPE), [`ERRORS`](#api-ERRORS), [`REGISTRATION_DURATION_SECONDS`](#api-REGISTRATION_DURATION_SECONDS), [`SKIP_WAITING_MESSAGE_TYPE`](#api-SKIP_WAITING_MESSAGE_TYPE), [`UPDATE_INSTALL_TIMEOUT_SECONDS`](#api-UPDATE_INSTALL_TIMEOUT_SECONDS)
- Interfaces: [`IBeforeInstallPromptEvent`](#api-IBeforeInstallPromptEvent)
- Types: [`IAppInstallationState`](#api-IAppInstallationState), [`IInstallationPromptOutcome`](#api-IInstallationPromptOutcome), [`IRegisterServiceWorkerOptions`](#api-IRegisterServiceWorkerOptions), [`IServiceWorkerSnapshot`](#api-IServiceWorkerSnapshot), [`IUserChoice`](#api-IUserChoice)

#### <a id="api-functions"></a>Functions in `jg-sw`

##### <a id="api-canInstallApp"></a>`canInstallApp`

*Function*

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.

```ts
canInstallApp(): boolean
```

**Returns** `boolean`: Whether the app can be installed.

**Example**

```ts
import { canInstallApp } from 'jg-sw';

installButton.hidden = !canInstallApp();
```

##### <a id="api-getServiceWorkerSnapshot"></a>`getServiceWorkerSnapshot`

*Function*

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.

```ts
getServiceWorkerSnapshot(): IServiceWorkerSnapshot
```

**Returns** `IServiceWorkerSnapshot`: The snapshot.

**Example**

```ts
import { getServiceWorkerSnapshot, subscribeToServiceWorker } from 'jg-sw';

const { hasWaitingWorker, installation } = useSyncExternalStore(
  subscribeToServiceWorker,
  getServiceWorkerSnapshot,
  getServiceWorkerSnapshot,
);
```

##### <a id="api-installApp"></a>`installApp`

*Function*

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.

```ts
installApp(): Promise<IInstallationPromptOutcome>
```

**Returns** `Promise<IInstallationPromptOutcome>`: A promise that resolves to 'accepted' or 'dismissed'.

**Throws**

- `NO_APP_INSTALLER`: The app can't be installed because the browser didn't offer the prompt.
- `UNKNOWN_INSTALLATION_PROMPT_ERROR`: The installation prompt failed.

**Example**

```ts
import { installApp } from 'jg-sw';

installButton.addEventListener('click', async () => {
  const outcome = await installApp(); // 'accepted' or 'dismissed'
});
```

##### <a id="api-isIosBrowserTab"></a>`isIosBrowserTab`

*Function*

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.

```ts
isIosBrowserTab(): boolean
```

**Returns** `boolean`: Whether it's an iOS browser tab; `false` outside a browser.

**Example**

```ts
import { isIosBrowserTab } from 'jg-sw';

homeScreenHint.hidden = !isIosBrowserTab();
```

##### <a id="api-registerServiceWorker"></a>`registerServiceWorker`

*Function*

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.

```ts
registerServiceWorker(
  options?: IRegisterServiceWorkerOptions,
): Promise<IServiceWorkerSnapshot>
```

**Parameters**

- `options?` `IRegisterServiceWorkerOptions`: 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**

```ts
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
}
```

##### <a id="api-subscribeToServiceWorker"></a>`subscribeToServiceWorker`

*Function*

Calls a listener whenever the snapshot changes, such as when an update finishes installing or the app becomes installable.

```ts
subscribeToServiceWorker(listener: () => void): () => void
```

**Parameters**

- `listener` `() => void`: The function to call, without arguments; read the new state with getServiceWorkerSnapshot.

**Returns** `() => void`: A function that stops the calls.

**Example**

```ts
import { getServiceWorkerSnapshot, subscribeToServiceWorker } from 'jg-sw';

const unsubscribe = subscribeToServiceWorker(() => {
  if (getServiceWorkerSnapshot().hasWaitingWorker) {
    showUpdateBanner();
  }
});
```

##### <a id="api-updateApp"></a>`updateApp`

*Function*

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.

```ts
updateApp(): Promise<void>
```

**Returns** `Promise<void>`: A promise that resolves once the new worker was asked to activate, or the page started reloading.

**Example**

```ts
import { getServiceWorkerSnapshot, updateApp } from 'jg-sw';

if (getServiceWorkerSnapshot().hasWaitingWorker) {
  updateButton.addEventListener('click', () => updateApp());
}
```

#### <a id="api-constants"></a>Constants in `jg-sw`

##### <a id="api-DEFAULT_SERVICE_WORKER_PATH"></a>`DEFAULT_SERVICE_WORKER_PATH`

*Constant*

the path the runtime registers the worker from, and the Vite plugin serves it at in development

```ts
const DEFAULT_SERVICE_WORKER_PATH: "/sw.js"
```

##### <a id="api-DEFAULT_SERVICE_WORKER_SCOPE"></a>`DEFAULT_SERVICE_WORKER_SCOPE`

*Constant*

the scope the runtime registers the worker with

```ts
const DEFAULT_SERVICE_WORKER_SCOPE: "/"
```

##### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error thrown or reported by the runtime

```ts
const ERRORS: { SW_NO_BROWSER_SUPPORT: "SW_NO_BROWSER_SUPPORT"; EMPTY_SW_REGISTRATION: "EMPTY_SW_REGISTRATION"; SW_REGISTRATION_FAILED: "SW_REGISTRATION_FAILED"; NO_APP_INSTALLER: "NO_APP_INSTALLER"; UNKNOWN_INSTALLATION_PROMPT_ERROR: "UNKNOWN_INSTALLATION_PROMPT_ERROR" }
```

##### <a id="api-REGISTRATION_DURATION_SECONDS"></a>`REGISTRATION_DURATION_SECONDS`

*Constant*

the seconds a registration usually takes, for apps that wait before reading the snapshot

```ts
const REGISTRATION_DURATION_SECONDS: 5
```

##### <a id="api-SKIP_WAITING_MESSAGE_TYPE"></a>`SKIP_WAITING_MESSAGE_TYPE`

*Constant*

the type of the message the runtime posts to make a waiting worker activate

```ts
const SKIP_WAITING_MESSAGE_TYPE: "JG_SW_SKIP_WAITING"
```

##### <a id="api-UPDATE_INSTALL_TIMEOUT_SECONDS"></a>`UPDATE_INSTALL_TIMEOUT_SECONDS`

*Constant*

the seconds updateApp waits for a new worker to finish installing before it reloads anyway

```ts
const UPDATE_INSTALL_TIMEOUT_SECONDS: 30
```

#### <a id="api-interfaces"></a>Interfaces in `jg-sw`

##### <a id="api-IBeforeInstallPromptEvent"></a>`IBeforeInstallPromptEvent`

*Interface*

the event Chromium-based browsers fire when the app can be installed, which lets the app show the installation prompt when it chooses

```ts
interface IBeforeInstallPromptEvent extends Event {
    readonly platforms: string[];
    readonly userChoice: Promise<IUserChoice>;
    prompt(): Promise<void>;
}
```

**Members**

- `platforms: string[]` (property): the platforms the event was dispatched for, such as 'web' or 'play'
- `userChoice: Promise<IUserChoice>` (property): resolves to the user's answer once the prompt closes
- `prompt(): Promise<void>` (method): shows the installation prompt

#### <a id="api-types"></a>Types in `jg-sw`

##### <a id="api-IAppInstallationState"></a>`IAppInstallationState`

*Type*

whether the app can be installed, and what happened when it was offered

```ts
type IAppInstallationState = {
    canInstall: boolean;
    isAppInstalled: boolean | undefined;
    isRunningInstalledApp: boolean;
    promptOutcome: IInstallationPromptOutcome | undefined;
};
```

**Members**

- `canInstall: boolean` (property): whether installApp can show the installation prompt now
- `isAppInstalled: boolean | undefined` (property): true once the browser reports the app was installed, undefined until then
- `isRunningInstalledApp: boolean` (property): whether the page runs as the installed app, in a standalone window
- `promptOutcome: IInstallationPromptOutcome | undefined` (property): the user's answer to the last prompt installApp showed, undefined until one is answered

##### <a id="api-IInstallationPromptOutcome"></a>`IInstallationPromptOutcome`

*Type*

the action the user took on the installation prompt

```ts
type IInstallationPromptOutcome = 'accepted' | 'dismissed';
```

##### <a id="api-IRegisterServiceWorkerOptions"></a>`IRegisterServiceWorkerOptions`

*Type*

the options registerServiceWorker accepts

```ts
type IRegisterServiceWorkerOptions = {
    path?: string;
    options?: RegistrationOptions;
    debugMode?: boolean;
};
```

**Members**

- `path: string` (property): the worker's script. Defaults to DEFAULT_SERVICE_WORKER_PATH, '/sw.js'
- `options: RegistrationOptions` (property): the options passed to navigator.serviceWorker.register. Defaults to { scope: '/' }
- `debugMode: boolean` (property): logs the registration, update, and installation events to the console

##### <a id="api-IServiceWorkerSnapshot"></a>`IServiceWorkerSnapshot`

*Type*

the service worker's state. The object keeps its identity until something changes, so useSyncExternalStore(subscribeToServiceWorker, getServiceWorkerSnapshot) re-renders only on changes

```ts
type IServiceWorkerSnapshot = {
    registration: ServiceWorkerRegistration | undefined;
    worker: ServiceWorker | undefined;
    registrationError: Exception | undefined;
    hasWaitingWorker: boolean;
    preloadErrorDetected: boolean;
    installation: IAppInstallationState;
};
```

**Members**

- `registration: ServiceWorkerRegistration | undefined` (property): the registration, once registerServiceWorker succeeds
- `worker: ServiceWorker | undefined` (property): the newest worker: the installing one, else the waiting one, else the active one
- `registrationError: Exception | undefined` (property): why the registration failed, with one of the ERRORS codes
- `hasWaitingWorker: boolean` (property): whether a new version is installed and waits to activate, which updateApp does
- `preloadErrorDetected: boolean` (property): whether a chunk of the page's build failed to load while online, most likely because a newer deployment replaced it; updateApp loads the new version
- `installation: IAppInstallationState` (property): whether the app can be installed, and what happened when it was offered

##### <a id="api-IUserChoice"></a>`IUserChoice`

*Type*

the user's answer to the installation prompt

```ts
type IUserChoice = {
    outcome: IInstallationPromptOutcome;
    platform: string;
};
```

**Members**

- `outcome: IInstallationPromptOutcome` (property)
- `platform: string` (property)

### <a id="api-entry-build"></a>`jg-sw/build`

- Functions: [`buildServiceWorker`](#api-build-buildServiceWorker), [`readServiceWorkerConfig`](#api-build-readServiceWorkerConfig), [`serviceWorkerPlugin`](#api-build-serviceWorkerPlugin), [`writeServiceWorker`](#api-build-writeServiceWorker)
- Constants: [`ERRORS`](#api-build-ERRORS), [`ServiceWorkerConfigSchema`](#api-build-ServiceWorkerConfigSchema)
- Types: [`IBuildServiceWorkerOptions`](#api-build-IBuildServiceWorkerOptions), [`IServiceWorkerConfig`](#api-build-IServiceWorkerConfig)

#### <a id="api-build-functions"></a>Functions in `jg-sw/build`

##### <a id="api-build-buildServiceWorker"></a>`buildServiceWorker`

*Function*

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.

```ts
buildServiceWorker(
  config: { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string },
  options?: IBuildServiceWorkerOptions,
): Promise<string>
```

**Parameters**

- `config` `{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }`: 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_CONFIG`: The service worker config is invalid.
- `INVALID_TEMPLATE_NAME`: The template must be 'base' or 'firebase-fcm'.
- `INVALID_FIREBASE_SDK_VERSION`: The Firebase SDK version must be a version such as 12.4.0.
- `INVALID_ENVIRONMENT`: The environment isn't supported, or the firebase-fcm template needs one.
- `NOT_A_PATH_ELEMENT`: An asset to precache is outside outDir, or isn't a file or a directory.
- `NAVIGATION_FALLBACK_NOT_PRECACHED`: The navigation fallback isn't one of the precached paths.
- `FAILED_TO_BUILD_CONFIG`: Unable to load the environment file.
- `INVALID_FIREBASE_CONFIG`: The environment variable that holds the Firebase config is missing or
- empty.
- `FAILED_TO_READ_FIREBASE_CONFIG`: Unable to read the Firebase config from the environment
- variable.

**Example**

```ts
import { buildServiceWorker } from 'jg-sw/build';

const source = await buildServiceWorker(
  {
    outDir: 'dist',
    cacheNamePrefix: 'my-app',
    template: 'base',
    includeToPrecache: ['/', '/assets'],
    excludeFilesFromPrecache: [],
    excludeMIMETypesFromCache: ['application/json'],
  },
  { caching: true },
);
```

##### <a id="api-build-readServiceWorkerConfig"></a>`readServiceWorkerConfig`

*Function*

Reads and validates a service worker config file, such as `jg-sw.config.json`.

```ts
readServiceWorkerConfig(
  path: string,
): Promise<{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }>
```

**Parameters**

- `path` `string`: The config file's path.

**Returns** `Promise<{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }>`: A promise that resolves to the config.

**Throws**

- `FAILED_TO_READ_BASE_CONFIG`: Unable to read the service worker config, or it is invalid.
- `INVALID_TEMPLATE_NAME`: The template must be 'base' or 'firebase-fcm'.
- `INVALID_FIREBASE_SDK_VERSION`: The Firebase SDK version must be a version such as 12.4.0.

**Example**

```ts
import { readServiceWorkerConfig } from 'jg-sw/build';

const config = await readServiceWorkerConfig('jg-sw.config.json');
config.template; // 'base'
```

##### <a id="api-build-serviceWorkerPlugin"></a>`serviceWorkerPlugin`

*Function*

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.

```ts
serviceWorkerPlugin(
  options: { config: string | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string } } & IBuildServiceWorkerOptions,
): Plugin
```

**Parameters**

- `options` `{ config: string | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string } } & IBuildServiceWorkerOptions`: The config, as a file path or an object, plus the build options. The dev server uses the environment option, or `development` when it isn't set.

**Returns** `Plugin`: The Vite plugin.

**Example**

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import { serviceWorkerPlugin } from 'jg-sw/build';

export default defineConfig({
  plugins: [serviceWorkerPlugin({ config: 'jg-sw.config.json', environment: 'production' })],
});
```

##### <a id="api-build-writeServiceWorker"></a>`writeServiceWorker`

*Function*

Generates the service worker and writes it to `sw.js` in the config's outDir, after the app's build has written its files there.

```ts
writeServiceWorker(
  config: { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string },
  options?: IBuildServiceWorkerOptions,
): Promise<string>
```

**Parameters**

- `config` `{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; navigationFallback?: string; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }`: 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_CONFIG`: The service worker config is invalid.
- `INVALID_TEMPLATE_NAME`: The template must be 'base' or 'firebase-fcm'.
- `INVALID_FIREBASE_SDK_VERSION`: The Firebase SDK version must be a version such as 12.4.0.
- `INVALID_ENVIRONMENT`: The environment isn't supported, or the firebase-fcm template needs one.
- `NOT_A_PATH_ELEMENT`: An asset to precache is outside outDir, or isn't a file or a directory.
- `NAVIGATION_FALLBACK_NOT_PRECACHED`: The navigation fallback isn't one of the precached paths.
- `FAILED_TO_BUILD_CONFIG`: Unable to load the environment file.
- `INVALID_FIREBASE_CONFIG`: The environment variable that holds the Firebase config is missing or
- empty.
- `FAILED_TO_READ_FIREBASE_CONFIG`: Unable to read the Firebase config from the environment
- variable.

**Example**

```ts
import { readServiceWorkerConfig, writeServiceWorker } from 'jg-sw/build';

const config = await readServiceWorkerConfig('jg-sw.config.json');
await writeServiceWorker(config, { environment: 'production' }); // '/app/dist/sw.js'
```

#### <a id="api-build-constants"></a>Constants in `jg-sw/build`

##### <a id="api-build-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error thrown by the build side

```ts
const ERRORS: { INVALID_TEMPLATE_NAME: "INVALID_TEMPLATE_NAME"; NOT_A_PATH_ELEMENT: "NOT_A_PATH_ELEMENT"; FAILED_TO_READ_BASE_CONFIG: "FAILED_TO_READ_BASE_CONFIG"; INVALID_FIREBASE_CONFIG: "INVALID_FIREBASE_CONFIG"; INVALID_ENVIRONMENT: "INVALID_ENVIRONMENT"; FAILED_TO_BUILD_CONFIG: "FAILED_TO_BUILD_CONFIG"; FAILED_TO_READ_FIREBASE_CONFIG: "FAILED_TO_READ_FIREBASE_CONFIG"; INVALID_FIREBASE_SDK_VERSION: "INVALID_FIREBASE_SDK_VERSION"; NAVIGATION_FALLBACK_NOT_PRECACHED: "NAVIGATION_FALLBACK_NOT_PRECACHED" }
```

##### <a id="api-build-ServiceWorkerConfigSchema"></a>`ServiceWorkerConfigSchema`

*Constant*

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.

```ts
const ServiceWorkerConfigSchema: ZodDiscriminatedUnion<[ZodObject<{ outDir: ZodString; cacheNamePrefix: ZodString; includeToPrecache: ZodArray<ZodString>; excludeFilesFromPrecache: ZodArray<ZodString>; excludeMIMETypesFromCache: ZodArray<ZodString>; navigationFallback: ZodOptional<ZodString>; template: ZodLiteral<"base"> }, $strict>, ZodObject<{ outDir: ZodString; cacheNamePrefix: ZodString; includeToPrecache: ZodArray<ZodString>; excludeFilesFromPrecache: ZodArray<ZodString>; excludeMIMETypesFromCache: ZodArray<ZodString>; navigationFallback: ZodOptional<ZodString>; template: ZodLiteral<"firebase-fcm">; firebaseConfigProcessEnvKey: ZodString; firebaseSdkVersion: ZodString }, $strict>], "template">
```

**Example**

```ts
import { ServiceWorkerConfigSchema } from 'jg-sw/build';

const config = ServiceWorkerConfigSchema.parse({
  outDir: 'dist',
  cacheNamePrefix: 'my-app',
  template: 'base',
  includeToPrecache: ['/', '/index.html', '/assets'],
  excludeFilesFromPrecache: [],
  excludeMIMETypesFromCache: ['application/json'],
});
```

#### <a id="api-build-types"></a>Types in `jg-sw/build`

##### <a id="api-build-IBuildServiceWorkerOptions"></a>`IBuildServiceWorkerOptions`

*Type*

the options buildServiceWorker and writeServiceWorker accept

```ts
type IBuildServiceWorkerOptions = {
    environment?: 'development' | 'staging' | 'production';
    caching?: boolean;
    cwd?: string;
};
```

**Members**

- `environment: "development" | "staging" | "production"` (property): 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
- `caching: boolean` (property): false leaves out precaching, cache cleanup, and the fetch handler, for development. Defaults to true
- `cwd: string` (property): the directory outDir and the environment files are resolved from. Defaults to process.cwd()

##### <a id="api-build-IServiceWorkerConfig"></a>`IServiceWorkerConfig`

*Type*

a service worker config, validated by ServiceWorkerConfigSchema

```ts
type IServiceWorkerConfig = z.infer<typeof ServiceWorkerConfigSchema>;
```
