jg-sw 1.1.0 → 1.2.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/API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # <a id="api-reference"></a>jg-sw API reference
2
2
 
3
- Every export of `jg-sw@1.1.0`, 26 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.
3
+ Every export of `jg-sw@1.2.0`, 26 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.
4
4
 
5
5
  ## <a id="api-entry-main"></a>`jg-sw`
6
6
 
@@ -358,14 +358,14 @@ Generates the service worker's source from a config, without writing it. Every g
358
358
 
359
359
  ```ts
360
360
  buildServiceWorker(
361
- config: { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string },
361
+ 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 },
362
362
  options?: IBuildServiceWorkerOptions,
363
363
  ): Promise<string>
364
364
  ```
365
365
 
366
366
  **Parameters**
367
367
 
368
- - `config` `{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }`: The service worker config, as read by readServiceWorkerConfig or written inline.
368
+ - `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.
369
369
  - `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 `{}`.
370
370
 
371
371
  **Returns** `Promise<string>`: A promise that resolves to the worker's source.
@@ -377,6 +377,7 @@ buildServiceWorker(
377
377
  - `INVALID_FIREBASE_SDK_VERSION`: The Firebase SDK version must be a version such as 12.4.0.
378
378
  - `INVALID_ENVIRONMENT`: The environment isn't supported, or the firebase-fcm template needs one.
379
379
  - `NOT_A_PATH_ELEMENT`: An asset to precache is outside outDir, or isn't a file or a directory.
380
+ - `NAVIGATION_FALLBACK_NOT_PRECACHED`: The navigation fallback isn't one of the precached paths.
380
381
  - `FAILED_TO_BUILD_CONFIG`: Unable to load the environment file.
381
382
  - `INVALID_FIREBASE_CONFIG`: The environment variable that holds the Firebase config is missing or
382
383
  - empty.
@@ -410,14 +411,14 @@ Reads and validates a service worker config file, such as `jg-sw.config.json`.
410
411
  ```ts
411
412
  readServiceWorkerConfig(
412
413
  path: string,
413
- ): Promise<{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }>
414
+ ): 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 }>
414
415
  ```
415
416
 
416
417
  **Parameters**
417
418
 
418
419
  - `path` `string`: The config file's path.
419
420
 
420
- **Returns** `Promise<{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }>`: A promise that resolves to the config.
421
+ **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.
421
422
 
422
423
  **Throws**
423
424
 
@@ -442,13 +443,13 @@ Creates a Vite plugin that generates the service worker. After a production buil
442
443
 
443
444
  ```ts
444
445
  serviceWorkerPlugin(
445
- options: { config: string | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string } } & IBuildServiceWorkerOptions,
446
+ 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,
446
447
  ): Plugin
447
448
  ```
448
449
 
449
450
  **Parameters**
450
451
 
451
- - `options` `{ config: string | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: 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.
452
+ - `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.
452
453
 
453
454
  **Returns** `Plugin`: The Vite plugin.
454
455
 
@@ -472,14 +473,14 @@ Generates the service worker and writes it to `sw.js` in the config's outDir, af
472
473
 
473
474
  ```ts
474
475
  writeServiceWorker(
475
- config: { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string },
476
+ 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 },
476
477
  options?: IBuildServiceWorkerOptions,
477
478
  ): Promise<string>
478
479
  ```
479
480
 
480
481
  **Parameters**
481
482
 
482
- - `config` `{ outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "base" } | { outDir: string; cacheNamePrefix: string; includeToPrecache: string[]; excludeFilesFromPrecache: string[]; excludeMIMETypesFromCache: string[]; template: "firebase-fcm"; firebaseConfigProcessEnvKey: string; firebaseSdkVersion: string }`: The service worker config, as read by readServiceWorkerConfig or written inline.
483
+ - `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.
483
484
  - `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 `{}`.
484
485
 
485
486
  **Returns** `Promise<string>`: A promise that resolves to the written file's path.
@@ -491,6 +492,7 @@ writeServiceWorker(
491
492
  - `INVALID_FIREBASE_SDK_VERSION`: The Firebase SDK version must be a version such as 12.4.0.
492
493
  - `INVALID_ENVIRONMENT`: The environment isn't supported, or the firebase-fcm template needs one.
493
494
  - `NOT_A_PATH_ELEMENT`: An asset to precache is outside outDir, or isn't a file or a directory.
495
+ - `NAVIGATION_FALLBACK_NOT_PRECACHED`: The navigation fallback isn't one of the precached paths.
494
496
  - `FAILED_TO_BUILD_CONFIG`: Unable to load the environment file.
495
497
  - `INVALID_FIREBASE_CONFIG`: The environment variable that holds the Firebase config is missing or
496
498
  - empty.
@@ -515,7 +517,7 @@ await writeServiceWorker(config, { environment: 'production' }); // '/app/dist/s
515
517
  the codes for every error thrown by the build side
516
518
 
517
519
  ```ts
518
- 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" }
520
+ 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" }
519
521
  ```
520
522
 
521
523
  #### <a id="api-build-ServiceWorkerConfigSchema"></a>`ServiceWorkerConfigSchema`
@@ -525,7 +527,7 @@ const ERRORS: { INVALID_TEMPLATE_NAME: "INVALID_TEMPLATE_NAME"; NOT_A_PATH_ELEME
525
527
  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.
526
528
 
527
529
  ```ts
528
- const ServiceWorkerConfigSchema: ZodDiscriminatedUnion<[ZodObject<{ outDir: ZodString; cacheNamePrefix: ZodString; includeToPrecache: ZodArray<ZodString>; excludeFilesFromPrecache: ZodArray<ZodString>; excludeMIMETypesFromCache: ZodArray<ZodString>; template: ZodLiteral<"base"> }, $strict>, ZodObject<{ outDir: ZodString; cacheNamePrefix: ZodString; includeToPrecache: ZodArray<ZodString>; excludeFilesFromPrecache: ZodArray<ZodString>; excludeMIMETypesFromCache: ZodArray<ZodString>; template: ZodLiteral<"firebase-fcm">; firebaseConfigProcessEnvKey: ZodString; firebaseSdkVersion: ZodString }, $strict>], "template">
530
+ 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">
529
531
  ```
530
532
 
531
533
  **Example**
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Added
6
+
7
+ - `navigationFallback` in the config: a precached page, such as `/index.html`, that answers a navigation the worker can neither find in its cache nor fetch, so a single-page app's routes, such as `/settings`, open offline. Without it, the worker behaves as before.
8
+ - `ERRORS.NAVIGATION_FALLBACK_NOT_PRECACHED`, which the build throws when the fallback isn't one of the precached paths.
9
+
10
+ ## 1.1.1
11
+
12
+ ### Added
13
+
14
+ - `RECIPES.md`, patterns for apps that use the package: an offline Vite app, and telling people an update is ready. Each block is typechecked against the release.
15
+
3
16
  ## 1.1.0
4
17
 
5
18
  A fresh start: breaking changes in a minor version, once, because nothing depended on 1.0.x.
package/README.md CHANGED
@@ -68,14 +68,15 @@ Create `jg-sw.config.json` next to your app's `package.json`:
68
68
  }
69
69
  ```
70
70
 
71
- | Field | What it does |
72
- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
- | `outDir` | The directory the app's build is written to. The worker is written there as `sw.js`. |
74
- | `template` | `base` caches the app. `firebase-fcm` also shows Firebase Cloud Messaging notifications. |
75
- | `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>`. |
76
- | `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. |
77
- | `excludeFilesFromPrecache` | File names left out of precaching, wherever they are. |
78
- | `excludeMIMETypesFromCache` | MIME types whose responses are never cached. |
71
+ | Field | What it does |
72
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `outDir` | The directory the app's build is written to. The worker is written there as `sw.js`. |
74
+ | `template` | `base` caches the app. `firebase-fcm` also shows Firebase Cloud Messaging notifications. |
75
+ | `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>`. |
76
+ | `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. |
77
+ | `excludeFilesFromPrecache` | File names left out of precaching, wherever they are. |
78
+ | `excludeMIMETypesFromCache` | MIME types whose responses are never cached. |
79
+ | `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. |
79
80
 
80
81
  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.
81
82
 
@@ -134,6 +135,7 @@ await writeServiceWorker(config, { environment: 'production' }); // '/app/dist/s
134
135
  - The page activates a waiting worker by posting `{ type: 'JG_SW_SKIP_WAITING' }` to it.
135
136
  - `activate` deletes the older caches in its own namespace, keeps every other cache, then claims its clients.
136
137
  - 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.
138
+ - 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.
137
139
 
138
140
  ### Firebase Cloud Messaging
139
141
 
@@ -161,6 +163,8 @@ The worker loads Firebase's compat SDK from `gstatic`. When no app window is vis
161
163
 
162
164
  The full API reference ships in the package as `API.md`, generated from its TSDoc for the installed version, and is at https://jg-packages.jesusgraterol.dev/packages/jg-sw/.
163
165
 
166
+ Recipes for common jobs ship as `RECIPES.md`, with every block typechecked against the release, and are at https://jg-packages.jesusgraterol.dev/packages/jg-sw/recipes/.
167
+
164
168
  ## License
165
169
 
166
170
  [MIT](https://github.com/jesusgraterol/jg-packages/blob/main/packages/jg-sw/LICENSE)
package/RECIPES.md ADDED
@@ -0,0 +1,108 @@
1
+ # jg-sw recipes
2
+
3
+ Patterns for Vite apps that keep working offline, and tell people when a new version is ready.
4
+
5
+ ## An offline Vite app
6
+
7
+ Use this when the app should open without a network after the first visit. The plugin writes `sw.js` into the build after Vite's own files, and the worker caches the files you list when it installs. Putting the config in `vite.config.ts` gets it typechecked with the rest of the file:
8
+
9
+ ```ts
10
+ // vite.config.ts
11
+ import { defineConfig } from 'vite';
12
+ import { serviceWorkerPlugin } from 'jg-sw/build';
13
+
14
+ export default defineConfig({
15
+ plugins: [
16
+ serviceWorkerPlugin({
17
+ config: {
18
+ // Vite's build.outDir, where the worker is written too
19
+ outDir: 'dist',
20
+ template: 'base',
21
+
22
+ // the namespace of this app's caches, so a new version deletes only its own old ones
23
+ cacheNamePrefix: 'my-app',
24
+
25
+ // the page and Vite's hashed bundles, which is all the app needs to start offline
26
+ includeToPrecache: ['/', '/index.html', '/assets'],
27
+ excludeFilesFromPrecache: [],
28
+
29
+ // the worker answers from its cache first, so keep API data and other pages out of it
30
+ excludeMIMETypesFromCache: ['application/json', 'text/html'],
31
+
32
+ // offline, a route the app handles, such as /settings, opens through the page
33
+ navigationFallback: '/index.html',
34
+ },
35
+ }),
36
+ ],
37
+ });
38
+ ```
39
+
40
+ The worker answers same-origin `GET` requests from its cache before the network, and caches what it fetches, so whatever it caches stays the same until the next build replaces the cache. That suits hashed bundles, and would serve stale data from an API on the same origin: `excludeMIMETypesFromCache` keeps JSON out, and responses marked `Cache-Control: no-store` or `private` never enter the cache either. Add the files from `public/` that the page loads, such as its icons, to `includeToPrecache` when they should show offline. `config` also takes a path, such as `'jg-sw.config.json'`, when the CLI reads the same file.
41
+
42
+ Register the worker from the app's entry module. Precaching downloads every listed file, so it starts once the page has loaded:
43
+
44
+ ```ts
45
+ // src/main.ts
46
+ import { hasErrorCode } from 'jg-exception';
47
+ import { ERRORS, registerServiceWorker } from 'jg-sw';
48
+
49
+ window.addEventListener(
50
+ 'load',
51
+ async () => {
52
+ // it never rejects: a failure lands in registrationError
53
+ const { registrationError } = await registerServiceWorker();
54
+
55
+ // without service workers the app still runs, online only, so that case needs no report
56
+ if (
57
+ registrationError !== undefined &&
58
+ !hasErrorCode(registrationError, ERRORS.SW_NO_BROWSER_SUPPORT)
59
+ ) {
60
+ console.error(registrationError);
61
+ }
62
+ },
63
+ { once: true },
64
+ );
65
+ ```
66
+
67
+ During `vite`, the dev server answers `/sw.js` with a worker that caches nothing, so registration works and every request still reaches the dev server. To try the offline app, run `vite build` and `vite preview`, load the page once, then turn the network off in the browser's devtools. The worker matches each request exactly, so a client-side route, such as `/settings`, is never in its cache. Offline, `navigationFallback` answers the navigation with the precached page, at the route's URL, so the app's router shows it; without it, reloading the route gets the worker's network error. The fallback must be one of the precached paths, or the build fails with `NAVIGATION_FALLBACK_NOT_PRECACHED`.
68
+
69
+ ## Telling people an update is ready
70
+
71
+ Each build makes a new worker, which installs next to the running one and waits, so open pages keep the version they loaded. Use this to tell people a new version is ready and let them choose when to switch, because switching reloads the page. The state comes from `registerServiceWorker`, so call it first, as in the previous recipe:
72
+
73
+ ```ts
74
+ import { getServiceWorkerSnapshot, subscribeToServiceWorker, updateApp } from 'jg-sw';
75
+
76
+ // shows the button only while a new version waits
77
+ const showUpdateState = (): void => {
78
+ updateButton.hidden = !getServiceWorkerSnapshot().hasWaitingWorker;
79
+ };
80
+ subscribeToServiceWorker(showUpdateState);
81
+
82
+ // a version may already wait when this runs, such as one installed during an earlier visit
83
+ showUpdateState();
84
+
85
+ updateButton.addEventListener('click', () => {
86
+ // it reloads the page, so it runs only when the person asks
87
+ updateButton.disabled = true;
88
+ void updateApp();
89
+ });
90
+ ```
91
+
92
+ `updateApp()` asks the waiting worker to take over, and reloads the page a single time when the new worker controls it, never in a loop. Don't call it on your own while a form holds unsaved input. A "Check for updates" item, such as in a settings menu, can call it with no version waiting: it looks for one, waits up to `UPDATE_INSTALL_TIMEOUT_SECONDS` for it to install, and reloads the page either way.
93
+
94
+ Browsers look for a new worker when a page in its scope loads. A single-page app can stay open for days without loading another page, so look again when the person returns to the tab:
95
+
96
+ ```ts
97
+ import { getServiceWorkerSnapshot } from 'jg-sw';
98
+
99
+ document.addEventListener('visibilitychange', () => {
100
+ const { registration } = getServiceWorkerSnapshot();
101
+ if (document.visibilityState === 'visible' && registration !== undefined) {
102
+ // offline, the check fails, and the next return to the tab tries again
103
+ registration.update().catch(() => undefined);
104
+ }
105
+ });
106
+ ```
107
+
108
+ When the check finds a new version, the snapshot changes once it has installed, and the button from the previous block appears.
@@ -22,6 +22,7 @@ export declare const ServiceWorkerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodOb
22
22
  includeToPrecache: z.ZodArray<z.ZodString>;
23
23
  excludeFilesFromPrecache: z.ZodArray<z.ZodString>;
24
24
  excludeMIMETypesFromCache: z.ZodArray<z.ZodString>;
25
+ navigationFallback: z.ZodOptional<z.ZodString>;
25
26
  }, z.core.$strict>, z.ZodObject<{
26
27
  template: z.ZodLiteral<"firebase-fcm">;
27
28
  firebaseConfigProcessEnvKey: z.ZodString;
@@ -31,6 +32,7 @@ export declare const ServiceWorkerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodOb
31
32
  includeToPrecache: z.ZodArray<z.ZodString>;
32
33
  excludeFilesFromPrecache: z.ZodArray<z.ZodString>;
33
34
  excludeMIMETypesFromCache: z.ZodArray<z.ZodString>;
35
+ navigationFallback: z.ZodOptional<z.ZodString>;
34
36
  }, z.core.$strict>], "template">;
35
37
  export type IServiceWorkerConfig = z.infer<typeof ServiceWorkerConfigSchema>;
36
38
  export declare const FirebaseOptionsSchema: z.ZodObject<{
@@ -14,6 +14,13 @@ const BaseConfigShape = {
14
14
  excludeFilesFromPrecache: z.array(z.string()),
15
15
  // the MIME types whose responses the worker never caches, such as 'application/json'
16
16
  excludeMIMETypesFromCache: z.array(z.string()),
17
+ // the precached page that answers a navigation the worker can neither find in its cache nor
18
+ // fetch, such as '/index.html' for a single-page app offline. Without it, such a navigation gets
19
+ // the worker's network error
20
+ navigationFallback: z
21
+ .string()
22
+ .startsWith('/', 'The navigation fallback must be a path such as /index.html.')
23
+ .optional(),
17
24
  };
18
25
  // the config of a worker that caches the app
19
26
  const BaseConfigSchema = z.strictObject({
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/build/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,yBAAyB,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAE7F,mCAAmC;AACnC,MAAM,eAAe,GAAG;IACtB,+EAA+E;IAC/E,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEzB,qFAAqF;IACrF,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,KAAK,CACJ,yBAAyB,EACzB,wFAAwF,CACzF;IAEH,oFAAoF;IACpF,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAEtC,2DAA2D;IAC3D,wBAAwB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAE7C,qFAAqF;IACrF,yBAAyB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;CAC/C,CAAC;AAEF,6CAA6C;AAC7C,MAAM,gBAAgB,GAAG,CAAC,CAAC,YAAY,CAAC;IACtC,GAAG,eAAe;IAClB,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;CAC5B,CAAC,CAAC;AAEH,8FAA8F;AAC9F,MAAM,uBAAuB,GAAG,CAAC,CAAC,YAAY,CAAC;IAC7C,GAAG,eAAe;IAClB,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;IAEnC,sFAAsF;IACtF,2BAA2B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9C,yEAAyE;IACzE,kBAAkB,EAAE,CAAC;SAClB,MAAM,EAAE;SACR,KAAK,CACJ,4BAA4B,EAC5B,4DAA4D,CAC7D;CACJ,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,kBAAkB,CAAC,UAAU,EAAE;IACxE,gBAAgB;IAChB,uBAAuB;CACxB,CAAC,CAAC;AAKH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,YAAY,CAAC;IAClD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACzB,CAAC,CAAC;AAIH,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,qBAAqB;CAC/B,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\n\nimport { CACHE_NAME_PREFIX_PATTERN, FIREBASE_SDK_VERSION_PATTERN } from '../shared/index.js';\n\n// the fields every template shares\nconst BaseConfigShape = {\n // the directory the app's build is written to, where the worker is written too\n outDir: z.string().min(1),\n\n // the application-owned namespace the worker's caches are named in, such as 'my-app'\n cacheNamePrefix: z\n .string()\n .regex(\n CACHE_NAME_PREFIX_PATTERN,\n 'The cache name prefix must contain lowercase alphanumeric tokens separated by hyphens.',\n ),\n\n // the files and directories, relative to outDir, the worker caches when it installs\n includeToPrecache: z.array(z.string()),\n\n // the file names left out of precaching, wherever they are\n excludeFilesFromPrecache: z.array(z.string()),\n\n // the MIME types whose responses the worker never caches, such as 'application/json'\n excludeMIMETypesFromCache: z.array(z.string()),\n};\n\n// the config of a worker that caches the app\nconst BaseConfigSchema = z.strictObject({\n ...BaseConfigShape,\n template: z.literal('base'),\n});\n\n// the config of a worker that caches the app and shows Firebase Cloud Messaging notifications\nconst FirebaseFcmConfigSchema = z.strictObject({\n ...BaseConfigShape,\n template: z.literal('firebase-fcm'),\n\n // the environment variable holding the Firebase config, as JSON: { \"options\": { … } }\n firebaseConfigProcessEnvKey: z.string().min(1),\n\n // the Firebase JavaScript SDK version the worker loads, such as '12.4.0'\n firebaseSdkVersion: z\n .string()\n .regex(\n FIREBASE_SDK_VERSION_PATTERN,\n 'The Firebase SDK version must be a version such as 12.4.0.',\n ),\n});\n\n/**\n * The schema of a service worker config: the `jg-sw.config.json` file the CLI reads, or the\n * object passed to `buildServiceWorker`. `template` selects the worker: `base` caches the app,\n * and `firebase-fcm` also shows Firebase Cloud Messaging notifications.\n * @example\n * import { ServiceWorkerConfigSchema } from 'jg-sw/build';\n *\n * const config = ServiceWorkerConfigSchema.parse({\n * outDir: 'dist',\n * cacheNamePrefix: 'my-app',\n * template: 'base',\n * includeToPrecache: ['/', '/index.html', '/assets'],\n * excludeFilesFromPrecache: [],\n * excludeMIMETypesFromCache: ['application/json'],\n * });\n */\nexport const ServiceWorkerConfigSchema = z.discriminatedUnion('template', [\n BaseConfigSchema,\n FirebaseFcmConfigSchema,\n]);\n\n// a service worker config, validated by ServiceWorkerConfigSchema\nexport type IServiceWorkerConfig = z.infer<typeof ServiceWorkerConfigSchema>;\n\n// the options Firebase's initializeApp receives in the worker\nexport const FirebaseOptionsSchema = z.strictObject({\n apiKey: z.string().min(1),\n authDomain: z.string().min(1),\n projectId: z.string().min(1),\n storageBucket: z.string().min(1),\n messagingSenderId: z.string().min(1),\n appId: z.string().min(1),\n});\n\nexport type IFirebaseOptions = z.infer<typeof FirebaseOptionsSchema>;\n\n// the JSON value the environment variable named by firebaseConfigProcessEnvKey holds\nexport const FirebaseConfigSchema = z.object({\n options: FirebaseOptionsSchema,\n});\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/build/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,yBAAyB,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAE7F,mCAAmC;AACnC,MAAM,eAAe,GAAG;IACtB,+EAA+E;IAC/E,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEzB,qFAAqF;IACrF,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,KAAK,CACJ,yBAAyB,EACzB,wFAAwF,CACzF;IAEH,oFAAoF;IACpF,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAEtC,2DAA2D;IAC3D,wBAAwB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAE7C,qFAAqF;IACrF,yBAAyB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAE9C,4FAA4F;IAC5F,iGAAiG;IACjG,6BAA6B;IAC7B,kBAAkB,EAAE,CAAC;SAClB,MAAM,EAAE;SACR,UAAU,CAAC,GAAG,EAAE,6DAA6D,CAAC;SAC9E,QAAQ,EAAE;CACd,CAAC;AAEF,6CAA6C;AAC7C,MAAM,gBAAgB,GAAG,CAAC,CAAC,YAAY,CAAC;IACtC,GAAG,eAAe;IAClB,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;CAC5B,CAAC,CAAC;AAEH,8FAA8F;AAC9F,MAAM,uBAAuB,GAAG,CAAC,CAAC,YAAY,CAAC;IAC7C,GAAG,eAAe;IAClB,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;IAEnC,sFAAsF;IACtF,2BAA2B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9C,yEAAyE;IACzE,kBAAkB,EAAE,CAAC;SAClB,MAAM,EAAE;SACR,KAAK,CACJ,4BAA4B,EAC5B,4DAA4D,CAC7D;CACJ,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,kBAAkB,CAAC,UAAU,EAAE;IACxE,gBAAgB;IAChB,uBAAuB;CACxB,CAAC,CAAC;AAKH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,YAAY,CAAC;IAClD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACzB,CAAC,CAAC;AAIH,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,qBAAqB;CAC/B,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\n\nimport { CACHE_NAME_PREFIX_PATTERN, FIREBASE_SDK_VERSION_PATTERN } from '../shared/index.js';\n\n// the fields every template shares\nconst BaseConfigShape = {\n // the directory the app's build is written to, where the worker is written too\n outDir: z.string().min(1),\n\n // the application-owned namespace the worker's caches are named in, such as 'my-app'\n cacheNamePrefix: z\n .string()\n .regex(\n CACHE_NAME_PREFIX_PATTERN,\n 'The cache name prefix must contain lowercase alphanumeric tokens separated by hyphens.',\n ),\n\n // the files and directories, relative to outDir, the worker caches when it installs\n includeToPrecache: z.array(z.string()),\n\n // the file names left out of precaching, wherever they are\n excludeFilesFromPrecache: z.array(z.string()),\n\n // the MIME types whose responses the worker never caches, such as 'application/json'\n excludeMIMETypesFromCache: z.array(z.string()),\n\n // the precached page that answers a navigation the worker can neither find in its cache nor\n // fetch, such as '/index.html' for a single-page app offline. Without it, such a navigation gets\n // the worker's network error\n navigationFallback: z\n .string()\n .startsWith('/', 'The navigation fallback must be a path such as /index.html.')\n .optional(),\n};\n\n// the config of a worker that caches the app\nconst BaseConfigSchema = z.strictObject({\n ...BaseConfigShape,\n template: z.literal('base'),\n});\n\n// the config of a worker that caches the app and shows Firebase Cloud Messaging notifications\nconst FirebaseFcmConfigSchema = z.strictObject({\n ...BaseConfigShape,\n template: z.literal('firebase-fcm'),\n\n // the environment variable holding the Firebase config, as JSON: { \"options\": { … } }\n firebaseConfigProcessEnvKey: z.string().min(1),\n\n // the Firebase JavaScript SDK version the worker loads, such as '12.4.0'\n firebaseSdkVersion: z\n .string()\n .regex(\n FIREBASE_SDK_VERSION_PATTERN,\n 'The Firebase SDK version must be a version such as 12.4.0.',\n ),\n});\n\n/**\n * The schema of a service worker config: the `jg-sw.config.json` file the CLI reads, or the\n * object passed to `buildServiceWorker`. `template` selects the worker: `base` caches the app,\n * and `firebase-fcm` also shows Firebase Cloud Messaging notifications.\n * @example\n * import { ServiceWorkerConfigSchema } from 'jg-sw/build';\n *\n * const config = ServiceWorkerConfigSchema.parse({\n * outDir: 'dist',\n * cacheNamePrefix: 'my-app',\n * template: 'base',\n * includeToPrecache: ['/', '/index.html', '/assets'],\n * excludeFilesFromPrecache: [],\n * excludeMIMETypesFromCache: ['application/json'],\n * });\n */\nexport const ServiceWorkerConfigSchema = z.discriminatedUnion('template', [\n BaseConfigSchema,\n FirebaseFcmConfigSchema,\n]);\n\n// a service worker config, validated by ServiceWorkerConfigSchema\nexport type IServiceWorkerConfig = z.infer<typeof ServiceWorkerConfigSchema>;\n\n// the options Firebase's initializeApp receives in the worker\nexport const FirebaseOptionsSchema = z.strictObject({\n apiKey: z.string().min(1),\n authDomain: z.string().min(1),\n projectId: z.string().min(1),\n storageBucket: z.string().min(1),\n messagingSenderId: z.string().min(1),\n appId: z.string().min(1),\n});\n\nexport type IFirebaseOptions = z.infer<typeof FirebaseOptionsSchema>;\n\n// the JSON value the environment variable named by firebaseConfigProcessEnvKey holds\nexport const FirebaseConfigSchema = z.object({\n options: FirebaseOptionsSchema,\n});\n"]}
@@ -28,6 +28,7 @@ import { type IBuildServiceWorkerOptions } from '../shared/index.js';
28
28
  * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.
29
29
  * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.
30
30
  * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.
31
+ * - NAVIGATION_FALLBACK_NOT_PRECACHED: The navigation fallback isn't one of the precached paths.
31
32
  * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.
32
33
  * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or
33
34
  * empty.
@@ -53,6 +54,7 @@ export declare const buildServiceWorker: (config: IServiceWorkerConfig, options?
53
54
  * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.
54
55
  * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.
55
56
  * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.
57
+ * - NAVIGATION_FALLBACK_NOT_PRECACHED: The navigation fallback isn't one of the precached paths.
56
58
  * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.
57
59
  * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or
58
60
  * empty.
@@ -1,10 +1,11 @@
1
1
  import { join, resolve } from 'node:path';
2
+ import { Exception } from 'jg-exception';
2
3
  import { writeTextFile } from 'jg-fs';
3
4
  import { generateRandomString } from 'jg-utilities';
4
5
  import { SERVICE_WORKER_FILE_NAME } from '../../shared/index.js';
5
6
  import { parseEnvironment, parseServiceWorkerConfig, readFirebaseOptions, } from '../config/index.js';
6
7
  import { buildPrecacheAssetPaths } from '../precache/index.js';
7
- import { CACHE_NAME_CHARACTERS, CACHE_NAME_LENGTH, CACHE_NAME_SEPARATOR, } from '../shared/index.js';
8
+ import { CACHE_NAME_CHARACTERS, CACHE_NAME_LENGTH, CACHE_NAME_SEPARATOR, ERRORS, } from '../shared/index.js';
8
9
  import { renderServiceWorker } from '../worker/index.js';
9
10
  /**
10
11
  * Generates the worker and works out the directory it belongs in.
@@ -18,15 +19,22 @@ const generateServiceWorker = async (config, options) => {
18
19
  const cwd = options.cwd ?? process.cwd();
19
20
  const caching = options.caching ?? true;
20
21
  const outDir = resolve(cwd, validConfig.outDir);
22
+ const precacheAssets = caching
23
+ ? await buildPrecacheAssetPaths(outDir, validConfig.includeToPrecache, validConfig.excludeFilesFromPrecache)
24
+ : [];
25
+ const navigationFallback = validConfig.navigationFallback ?? null;
26
+ // offline, only a precached page can answer
27
+ if (caching && navigationFallback !== null && !precacheAssets.includes(navigationFallback)) {
28
+ throw new Exception(`The navigation fallback '${navigationFallback}' isn't precached. Add it to includeToPrecache.`, { code: ERRORS.NAVIGATION_FALLBACK_NOT_PRECACHED, data: { navigationFallback } });
29
+ }
21
30
  const source = await renderServiceWorker({
22
31
  template: validConfig.template,
23
32
  cacheNamePrefix: validConfig.cacheNamePrefix,
24
33
  cacheName: `${validConfig.cacheNamePrefix}${CACHE_NAME_SEPARATOR}${generateRandomString(CACHE_NAME_LENGTH, CACHE_NAME_CHARACTERS)}`,
25
- precacheAssets: caching
26
- ? await buildPrecacheAssetPaths(outDir, validConfig.includeToPrecache, validConfig.excludeFilesFromPrecache)
27
- : [],
34
+ precacheAssets,
28
35
  excludeMIMETypes: validConfig.excludeMIMETypesFromCache,
29
36
  caching,
37
+ navigationFallback,
30
38
  ...(validConfig.template === 'firebase-fcm'
31
39
  ? {
32
40
  firebase: {
@@ -66,6 +74,7 @@ const generateServiceWorker = async (config, options) => {
66
74
  * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.
67
75
  * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.
68
76
  * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.
77
+ * - NAVIGATION_FALLBACK_NOT_PRECACHED: The navigation fallback isn't one of the precached paths.
69
78
  * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.
70
79
  * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or
71
80
  * empty.
@@ -91,6 +100,7 @@ export const buildServiceWorker = async (config, options = {}) => (await generat
91
100
  * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.
92
101
  * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.
93
102
  * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.
103
+ * - NAVIGATION_FALLBACK_NOT_PRECACHED: The navigation fallback isn't one of the precached paths.
94
104
  * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.
95
105
  * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or
96
106
  * empty.
@@ -1 +1 @@
1
- {"version":3,"file":"service-worker.js","sourceRoot":"","sources":["../../../src/build/service-worker/service-worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,mBAAmB,GAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,GAErB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,qBAAqB,GAAG,KAAK,EACjC,MAA4B,EAC5B,OAAmC,EACU,EAAE;IAC/C,MAAM,WAAW,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC;QACvC,QAAQ,EAAE,WAAW,CAAC,QAAQ;QAC9B,eAAe,EAAE,WAAW,CAAC,eAAe;QAC5C,SAAS,EAAE,GAAG,WAAW,CAAC,eAAe,GAAG,oBAAoB,GAAG,oBAAoB,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,EAAE;QACnI,cAAc,EAAE,OAAO;YACrB,CAAC,CAAC,MAAM,uBAAuB,CAC3B,MAAM,EACN,WAAW,CAAC,iBAAiB,EAC7B,WAAW,CAAC,wBAAwB,CACrC;YACH,CAAC,CAAC,EAAE;QACN,gBAAgB,EAAE,WAAW,CAAC,yBAAyB;QACvD,OAAO;QACP,GAAG,CAAC,WAAW,CAAC,QAAQ,KAAK,cAAc;YACzC,CAAC,CAAC;gBACE,QAAQ,EAAE;oBACR,UAAU,EAAE,WAAW,CAAC,kBAAkB;oBAC1C,OAAO,EAAE,MAAM,mBAAmB,CAChC,WAAW,CAAC,2BAA2B,EACvC,WAAW,EACX,GAAG,CACJ;iBACF;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,MAA4B,EAC5B,UAAsC,EAAE,EACvB,EAAE,CAAC,CAAC,MAAM,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,MAA4B,EAC5B,UAAsC,EAAE,EACvB,EAAE;IACnB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IACpD,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC,CAAC","sourcesContent":["import { join, resolve } from 'node:path';\nimport { writeTextFile } from 'jg-fs';\nimport { generateRandomString } from 'jg-utilities';\n\nimport { SERVICE_WORKER_FILE_NAME } from '../../shared/index.js';\nimport {\n parseEnvironment,\n parseServiceWorkerConfig,\n readFirebaseOptions,\n type IServiceWorkerConfig,\n} from '../config/index.js';\nimport { buildPrecacheAssetPaths } from '../precache/index.js';\nimport {\n CACHE_NAME_CHARACTERS,\n CACHE_NAME_LENGTH,\n CACHE_NAME_SEPARATOR,\n type IBuildServiceWorkerOptions,\n} from '../shared/index.js';\nimport { renderServiceWorker } from '../worker/index.js';\n\n/**\n * Generates the worker and works out the directory it belongs in.\n * @param config The service worker config.\n * @param options The environment, caching, and working directory.\n * @returns A promise that resolves to the worker's source and its output directory.\n */\nconst generateServiceWorker = async (\n config: IServiceWorkerConfig,\n options: IBuildServiceWorkerOptions,\n): Promise<{ source: string; outDir: string }> => {\n const validConfig = parseServiceWorkerConfig(config);\n const environment = parseEnvironment(options.environment);\n const cwd = options.cwd ?? process.cwd();\n const caching = options.caching ?? true;\n const outDir = resolve(cwd, validConfig.outDir);\n const source = await renderServiceWorker({\n template: validConfig.template,\n cacheNamePrefix: validConfig.cacheNamePrefix,\n cacheName: `${validConfig.cacheNamePrefix}${CACHE_NAME_SEPARATOR}${generateRandomString(CACHE_NAME_LENGTH, CACHE_NAME_CHARACTERS)}`,\n precacheAssets: caching\n ? await buildPrecacheAssetPaths(\n outDir,\n validConfig.includeToPrecache,\n validConfig.excludeFilesFromPrecache,\n )\n : [],\n excludeMIMETypes: validConfig.excludeMIMETypesFromCache,\n caching,\n ...(validConfig.template === 'firebase-fcm'\n ? {\n firebase: {\n sdkVersion: validConfig.firebaseSdkVersion,\n options: await readFirebaseOptions(\n validConfig.firebaseConfigProcessEnvKey,\n environment,\n cwd,\n ),\n },\n }\n : {}),\n });\n return { source, outDir };\n};\n\n/**\n * Generates the service worker's source from a config, without writing it. Every generated worker\n * gets a new cache name, `<cacheNamePrefix>--<10 random characters>`, so a new deployment\n * replaces the previous one's cache when it activates.\n * @param config The service worker config, as read by readServiceWorkerConfig or written inline.\n * @param options The environment the firebase-fcm template reads its Firebase config from,\n * whether the worker caches (false for development), and the directory paths are resolved from.\n * @returns A promise that resolves to the worker's source.\n * @example\n * import { buildServiceWorker } from 'jg-sw/build';\n *\n * const source = await buildServiceWorker(\n * {\n * outDir: 'dist',\n * cacheNamePrefix: 'my-app',\n * template: 'base',\n * includeToPrecache: ['/', '/assets'],\n * excludeFilesFromPrecache: [],\n * excludeMIMETypesFromCache: ['application/json'],\n * },\n * { caching: true },\n * );\n * @throws\n * - FAILED_TO_READ_BASE_CONFIG: The service worker config is invalid.\n * - INVALID_TEMPLATE_NAME: The template must be 'base' or 'firebase-fcm'.\n * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.\n * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.\n * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.\n * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.\n * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or\n * empty.\n * - FAILED_TO_READ_FIREBASE_CONFIG: Unable to read the Firebase config from the environment\n * variable.\n */\nexport const buildServiceWorker = async (\n config: IServiceWorkerConfig,\n options: IBuildServiceWorkerOptions = {},\n): Promise<string> => (await generateServiceWorker(config, options)).source;\n\n/**\n * Generates the service worker and writes it to `sw.js` in the config's outDir, after the app's\n * build has written its files there.\n * @param config The service worker config, as read by readServiceWorkerConfig or written inline.\n * @param options The environment the firebase-fcm template reads its Firebase config from,\n * whether the worker caches (false for development), and the directory paths are resolved from.\n * @returns A promise that resolves to the written file's path.\n * @example\n * import { readServiceWorkerConfig, writeServiceWorker } from 'jg-sw/build';\n *\n * const config = await readServiceWorkerConfig('jg-sw.config.json');\n * await writeServiceWorker(config, { environment: 'production' }); // '/app/dist/sw.js'\n * @throws\n * - FAILED_TO_READ_BASE_CONFIG: The service worker config is invalid.\n * - INVALID_TEMPLATE_NAME: The template must be 'base' or 'firebase-fcm'.\n * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.\n * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.\n * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.\n * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.\n * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or\n * empty.\n * - FAILED_TO_READ_FIREBASE_CONFIG: Unable to read the Firebase config from the environment\n * variable.\n */\nexport const writeServiceWorker = async (\n config: IServiceWorkerConfig,\n options: IBuildServiceWorkerOptions = {},\n): Promise<string> => {\n const { source, outDir } = await generateServiceWorker(config, options);\n const path = join(outDir, SERVICE_WORKER_FILE_NAME);\n await writeTextFile(path, source);\n return path;\n};\n"]}
1
+ {"version":3,"file":"service-worker.js","sourceRoot":"","sources":["../../../src/build/service-worker/service-worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,mBAAmB,GAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,MAAM,GAEP,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,qBAAqB,GAAG,KAAK,EACjC,MAA4B,EAC5B,OAAmC,EACU,EAAE;IAC/C,MAAM,WAAW,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,OAAO;QAC5B,CAAC,CAAC,MAAM,uBAAuB,CAC3B,MAAM,EACN,WAAW,CAAC,iBAAiB,EAC7B,WAAW,CAAC,wBAAwB,CACrC;QACH,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,kBAAkB,GAAG,WAAW,CAAC,kBAAkB,IAAI,IAAI,CAAC;IAClE,4CAA4C;IAC5C,IAAI,OAAO,IAAI,kBAAkB,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC3F,MAAM,IAAI,SAAS,CACjB,4BAA4B,kBAAkB,iDAAiD,EAC/F,EAAE,IAAI,EAAE,MAAM,CAAC,iCAAiC,EAAE,IAAI,EAAE,EAAE,kBAAkB,EAAE,EAAE,CACjF,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC;QACvC,QAAQ,EAAE,WAAW,CAAC,QAAQ;QAC9B,eAAe,EAAE,WAAW,CAAC,eAAe;QAC5C,SAAS,EAAE,GAAG,WAAW,CAAC,eAAe,GAAG,oBAAoB,GAAG,oBAAoB,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,EAAE;QACnI,cAAc;QACd,gBAAgB,EAAE,WAAW,CAAC,yBAAyB;QACvD,OAAO;QACP,kBAAkB;QAClB,GAAG,CAAC,WAAW,CAAC,QAAQ,KAAK,cAAc;YACzC,CAAC,CAAC;gBACE,QAAQ,EAAE;oBACR,UAAU,EAAE,WAAW,CAAC,kBAAkB;oBAC1C,OAAO,EAAE,MAAM,mBAAmB,CAChC,WAAW,CAAC,2BAA2B,EACvC,WAAW,EACX,GAAG,CACJ;iBACF;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,MAA4B,EAC5B,UAAsC,EAAE,EACvB,EAAE,CAAC,CAAC,MAAM,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,MAA4B,EAC5B,UAAsC,EAAE,EACvB,EAAE;IACnB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IACpD,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC,CAAC","sourcesContent":["import { join, resolve } from 'node:path';\nimport { Exception } from 'jg-exception';\nimport { writeTextFile } from 'jg-fs';\nimport { generateRandomString } from 'jg-utilities';\n\nimport { SERVICE_WORKER_FILE_NAME } from '../../shared/index.js';\nimport {\n parseEnvironment,\n parseServiceWorkerConfig,\n readFirebaseOptions,\n type IServiceWorkerConfig,\n} from '../config/index.js';\nimport { buildPrecacheAssetPaths } from '../precache/index.js';\nimport {\n CACHE_NAME_CHARACTERS,\n CACHE_NAME_LENGTH,\n CACHE_NAME_SEPARATOR,\n ERRORS,\n type IBuildServiceWorkerOptions,\n} from '../shared/index.js';\nimport { renderServiceWorker } from '../worker/index.js';\n\n/**\n * Generates the worker and works out the directory it belongs in.\n * @param config The service worker config.\n * @param options The environment, caching, and working directory.\n * @returns A promise that resolves to the worker's source and its output directory.\n */\nconst generateServiceWorker = async (\n config: IServiceWorkerConfig,\n options: IBuildServiceWorkerOptions,\n): Promise<{ source: string; outDir: string }> => {\n const validConfig = parseServiceWorkerConfig(config);\n const environment = parseEnvironment(options.environment);\n const cwd = options.cwd ?? process.cwd();\n const caching = options.caching ?? true;\n const outDir = resolve(cwd, validConfig.outDir);\n const precacheAssets = caching\n ? await buildPrecacheAssetPaths(\n outDir,\n validConfig.includeToPrecache,\n validConfig.excludeFilesFromPrecache,\n )\n : [];\n const navigationFallback = validConfig.navigationFallback ?? null;\n // offline, only a precached page can answer\n if (caching && navigationFallback !== null && !precacheAssets.includes(navigationFallback)) {\n throw new Exception(\n `The navigation fallback '${navigationFallback}' isn't precached. Add it to includeToPrecache.`,\n { code: ERRORS.NAVIGATION_FALLBACK_NOT_PRECACHED, data: { navigationFallback } },\n );\n }\n const source = await renderServiceWorker({\n template: validConfig.template,\n cacheNamePrefix: validConfig.cacheNamePrefix,\n cacheName: `${validConfig.cacheNamePrefix}${CACHE_NAME_SEPARATOR}${generateRandomString(CACHE_NAME_LENGTH, CACHE_NAME_CHARACTERS)}`,\n precacheAssets,\n excludeMIMETypes: validConfig.excludeMIMETypesFromCache,\n caching,\n navigationFallback,\n ...(validConfig.template === 'firebase-fcm'\n ? {\n firebase: {\n sdkVersion: validConfig.firebaseSdkVersion,\n options: await readFirebaseOptions(\n validConfig.firebaseConfigProcessEnvKey,\n environment,\n cwd,\n ),\n },\n }\n : {}),\n });\n return { source, outDir };\n};\n\n/**\n * Generates the service worker's source from a config, without writing it. Every generated worker\n * gets a new cache name, `<cacheNamePrefix>--<10 random characters>`, so a new deployment\n * replaces the previous one's cache when it activates.\n * @param config The service worker config, as read by readServiceWorkerConfig or written inline.\n * @param options The environment the firebase-fcm template reads its Firebase config from,\n * whether the worker caches (false for development), and the directory paths are resolved from.\n * @returns A promise that resolves to the worker's source.\n * @example\n * import { buildServiceWorker } from 'jg-sw/build';\n *\n * const source = await buildServiceWorker(\n * {\n * outDir: 'dist',\n * cacheNamePrefix: 'my-app',\n * template: 'base',\n * includeToPrecache: ['/', '/assets'],\n * excludeFilesFromPrecache: [],\n * excludeMIMETypesFromCache: ['application/json'],\n * },\n * { caching: true },\n * );\n * @throws\n * - FAILED_TO_READ_BASE_CONFIG: The service worker config is invalid.\n * - INVALID_TEMPLATE_NAME: The template must be 'base' or 'firebase-fcm'.\n * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.\n * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.\n * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.\n * - NAVIGATION_FALLBACK_NOT_PRECACHED: The navigation fallback isn't one of the precached paths.\n * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.\n * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or\n * empty.\n * - FAILED_TO_READ_FIREBASE_CONFIG: Unable to read the Firebase config from the environment\n * variable.\n */\nexport const buildServiceWorker = async (\n config: IServiceWorkerConfig,\n options: IBuildServiceWorkerOptions = {},\n): Promise<string> => (await generateServiceWorker(config, options)).source;\n\n/**\n * Generates the service worker and writes it to `sw.js` in the config's outDir, after the app's\n * build has written its files there.\n * @param config The service worker config, as read by readServiceWorkerConfig or written inline.\n * @param options The environment the firebase-fcm template reads its Firebase config from,\n * whether the worker caches (false for development), and the directory paths are resolved from.\n * @returns A promise that resolves to the written file's path.\n * @example\n * import { readServiceWorkerConfig, writeServiceWorker } from 'jg-sw/build';\n *\n * const config = await readServiceWorkerConfig('jg-sw.config.json');\n * await writeServiceWorker(config, { environment: 'production' }); // '/app/dist/sw.js'\n * @throws\n * - FAILED_TO_READ_BASE_CONFIG: The service worker config is invalid.\n * - INVALID_TEMPLATE_NAME: The template must be 'base' or 'firebase-fcm'.\n * - INVALID_FIREBASE_SDK_VERSION: The Firebase SDK version must be a version such as 12.4.0.\n * - INVALID_ENVIRONMENT: The environment isn't supported, or the firebase-fcm template needs one.\n * - NOT_A_PATH_ELEMENT: An asset to precache is outside outDir, or isn't a file or a directory.\n * - NAVIGATION_FALLBACK_NOT_PRECACHED: The navigation fallback isn't one of the precached paths.\n * - FAILED_TO_BUILD_CONFIG: Unable to load the environment file.\n * - INVALID_FIREBASE_CONFIG: The environment variable that holds the Firebase config is missing or\n * empty.\n * - FAILED_TO_READ_FIREBASE_CONFIG: Unable to read the Firebase config from the environment\n * variable.\n */\nexport const writeServiceWorker = async (\n config: IServiceWorkerConfig,\n options: IBuildServiceWorkerOptions = {},\n): Promise<string> => {\n const { source, outDir } = await generateServiceWorker(config, options);\n const path = join(outDir, SERVICE_WORKER_FILE_NAME);\n await writeTextFile(path, source);\n return path;\n};\n"]}
@@ -7,4 +7,5 @@ export declare const ERRORS: {
7
7
  readonly FAILED_TO_BUILD_CONFIG: "FAILED_TO_BUILD_CONFIG";
8
8
  readonly FAILED_TO_READ_FIREBASE_CONFIG: "FAILED_TO_READ_FIREBASE_CONFIG";
9
9
  readonly INVALID_FIREBASE_SDK_VERSION: "INVALID_FIREBASE_SDK_VERSION";
10
+ readonly NAVIGATION_FALLBACK_NOT_PRECACHED: "NAVIGATION_FALLBACK_NOT_PRECACHED";
10
11
  };
@@ -8,5 +8,6 @@ export const ERRORS = {
8
8
  FAILED_TO_BUILD_CONFIG: 'FAILED_TO_BUILD_CONFIG',
9
9
  FAILED_TO_READ_FIREBASE_CONFIG: 'FAILED_TO_READ_FIREBASE_CONFIG',
10
10
  INVALID_FIREBASE_SDK_VERSION: 'INVALID_FIREBASE_SDK_VERSION',
11
+ NAVIGATION_FALLBACK_NOT_PRECACHED: 'NAVIGATION_FALLBACK_NOT_PRECACHED',
11
12
  };
12
13
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/build/shared/errors.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,qBAAqB,EAAE,uBAAuB;IAC9C,kBAAkB,EAAE,oBAAoB;IACxC,0BAA0B,EAAE,4BAA4B;IACxD,uBAAuB,EAAE,yBAAyB;IAClD,mBAAmB,EAAE,qBAAqB;IAC1C,sBAAsB,EAAE,wBAAwB;IAChD,8BAA8B,EAAE,gCAAgC;IAChE,4BAA4B,EAAE,8BAA8B;CACpD,CAAC","sourcesContent":["// the codes for every error thrown by the build side\nexport const ERRORS = {\n INVALID_TEMPLATE_NAME: 'INVALID_TEMPLATE_NAME',\n NOT_A_PATH_ELEMENT: 'NOT_A_PATH_ELEMENT',\n FAILED_TO_READ_BASE_CONFIG: 'FAILED_TO_READ_BASE_CONFIG',\n INVALID_FIREBASE_CONFIG: 'INVALID_FIREBASE_CONFIG',\n INVALID_ENVIRONMENT: 'INVALID_ENVIRONMENT',\n FAILED_TO_BUILD_CONFIG: 'FAILED_TO_BUILD_CONFIG',\n FAILED_TO_READ_FIREBASE_CONFIG: 'FAILED_TO_READ_FIREBASE_CONFIG',\n INVALID_FIREBASE_SDK_VERSION: 'INVALID_FIREBASE_SDK_VERSION',\n} as const;\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/build/shared/errors.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,qBAAqB,EAAE,uBAAuB;IAC9C,kBAAkB,EAAE,oBAAoB;IACxC,0BAA0B,EAAE,4BAA4B;IACxD,uBAAuB,EAAE,yBAAyB;IAClD,mBAAmB,EAAE,qBAAqB;IAC1C,sBAAsB,EAAE,wBAAwB;IAChD,8BAA8B,EAAE,gCAAgC;IAChE,4BAA4B,EAAE,8BAA8B;IAC5D,iCAAiC,EAAE,mCAAmC;CAC9D,CAAC","sourcesContent":["// the codes for every error thrown by the build side\nexport const ERRORS = {\n INVALID_TEMPLATE_NAME: 'INVALID_TEMPLATE_NAME',\n NOT_A_PATH_ELEMENT: 'NOT_A_PATH_ELEMENT',\n FAILED_TO_READ_BASE_CONFIG: 'FAILED_TO_READ_BASE_CONFIG',\n INVALID_FIREBASE_CONFIG: 'INVALID_FIREBASE_CONFIG',\n INVALID_ENVIRONMENT: 'INVALID_ENVIRONMENT',\n FAILED_TO_BUILD_CONFIG: 'FAILED_TO_BUILD_CONFIG',\n FAILED_TO_READ_FIREBASE_CONFIG: 'FAILED_TO_READ_FIREBASE_CONFIG',\n INVALID_FIREBASE_SDK_VERSION: 'INVALID_FIREBASE_SDK_VERSION',\n NAVIGATION_FALLBACK_NOT_PRECACHED: 'NAVIGATION_FALLBACK_NOT_PRECACHED',\n} as const;\n"]}
@@ -21,6 +21,9 @@ const PRECACHE_ASSETS = [];
21
21
  // the list of MIME Types that won't be cached when the app sends HTTP GET requests
22
22
  const EXCLUDE_MIME_TYPES = [];
23
23
 
24
+ // the precached page that answers a navigation the network can't serve, or null for none
25
+ const NAVIGATION_FALLBACK = null;
26
+
24
27
  // the Cache-Control directives that keep a response out of the cache
25
28
  const UNCACHEABLE_CACHE_CONTROL_DIRECTIVES = ['no-store', 'private'];
26
29
 
@@ -157,6 +160,13 @@ const cacheFirst = async (request, event) => {
157
160
  try {
158
161
  responseFromNetwork = await fetch(request);
159
162
  } catch {
163
+ // a page the app routes on the client, such as /settings, opens through the fallback page
164
+ if (NAVIGATION_FALLBACK !== null && request.mode === 'navigate') {
165
+ const fallback = await cache.match(NAVIGATION_FALLBACK);
166
+ if (fallback) {
167
+ return fallback;
168
+ }
169
+ }
160
170
  return new Response('Network error happened', {
161
171
  status: 408,
162
172
  headers: { 'Content-Type': 'text/plain' },
@@ -6,6 +6,7 @@ export type IRenderServiceWorkerInput = {
6
6
  precacheAssets: string[];
7
7
  excludeMIMETypes: string[];
8
8
  caching: boolean;
9
+ navigationFallback: string | null;
9
10
  firebase?: {
10
11
  sdkVersion: string;
11
12
  options: IFirebaseOptions;
@@ -38,6 +38,7 @@ export const renderServiceWorker = async (input) => {
38
38
  EXCLUDE_MIME_TYPES: input.excludeMIMETypes,
39
39
  SKIP_WAITING_MESSAGE_TYPE,
40
40
  CACHING: input.caching,
41
+ NAVIGATION_FALLBACK: input.navigationFallback,
41
42
  });
42
43
  if (input.template === 'base' || input.firebase === undefined) {
43
44
  return `${GENERATED_HEADER}\n${base}`;
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/build/worker/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAErC,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,+DAA+D;AAC/D,MAAM,aAAa,GAAG;IACpB,IAAI,EAAE,IAAI,GAAG,CAAC,yBAAyB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;IACzD,cAAc,EAAE,IAAI,GAAG,CAAC,iCAAiC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;CACnE,CAAC;AAEX,2CAA2C;AAC3C,MAAM,gBAAgB,GACpB,4FAA4F,CAAC;AAe/F;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAE,MAA+B,EAAU,EAAE,CACjF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAC3B,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CACxB,MAAM,CAAC,OAAO,CACZ,IAAI,MAAM,CAAC,UAAU,IAAI,SAAS,EAAE,GAAG,CAAC;AACxC,mFAAmF;AACnF,GAAG,EAAE,CAAC,SAAS,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAC3D,EACH,QAAQ,CACT,CAAC;AAEJ;;;;GAIG;AACH,MAAM,YAAY,GAAG,CAAC,IAAgC,EAAmB,EAAE,CACzE,YAAY,CAAC,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,KAAgC,EAAmB,EAAE;IAC7F,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,EAAE;QACpD,iBAAiB,EAAE,KAAK,CAAC,eAAe;QACxC,UAAU,EAAE,KAAK,CAAC,SAAS;QAC3B,eAAe,EAAE,KAAK,CAAC,cAAc;QACrC,kBAAkB,EAAE,KAAK,CAAC,gBAAgB;QAC1C,yBAAyB;QACzB,OAAO,EAAE,KAAK,CAAC,OAAO;KACvB,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC9D,OAAO,GAAG,gBAAgB,KAAK,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,YAAY,CAAC,cAAc,CAAC,EAAE;QACnE,oBAAoB,EAAE,KAAK,CAAC,QAAQ,CAAC,UAAU;QAC/C,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAC,OAAO;KACzC,CAAC,CAAC;IACH,OAAO,GAAG,gBAAgB,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,EAAE,CAAC;AACpE,CAAC,CAAC","sourcesContent":["import { fileURLToPath } from 'node:url';\nimport { readTextFile } from 'jg-fs';\n\nimport { SKIP_WAITING_MESSAGE_TYPE } from '../../shared/index.js';\nimport type { IFirebaseOptions, IServiceWorkerConfig } from '../config/index.js';\n\n// the templates, copied next to the compiled code by the build\nconst TEMPLATE_URLS = {\n base: new URL('../templates/base.sw.js', import.meta.url),\n 'firebase-fcm': new URL('../templates/firebase-fcm.sw.js', import.meta.url),\n} as const;\n\n// the first line of every generated worker\nconst GENERATED_HEADER =\n '// Generated by jg-sw. Changes to this file are overwritten the next time it is generated.';\n\n// what the worker is generated from\nexport type IRenderServiceWorkerInput = {\n template: IServiceWorkerConfig['template'];\n cacheNamePrefix: string;\n cacheName: string;\n precacheAssets: string[];\n excludeMIMETypes: string[];\n caching: boolean;\n\n // the firebase-fcm template's SDK version and app options\n firebase?: { sdkVersion: string; options: IFirebaseOptions };\n};\n\n/**\n * Sets the values of a template's constants, each declared on its own line as\n * `const NAME = <placeholder>;`. Values are serialized as JSON, so strings and paths are escaped.\n * @param template The template's source.\n * @param values The value of each constant, by name.\n * @returns The source with the constants set.\n */\nconst setConstants = (template: string, values: Record<string, unknown>): string =>\n Object.entries(values).reduce(\n (source, [name, value]) =>\n source.replace(\n new RegExp(`^const ${name} = .*;$`, 'm'),\n // a function, so `$` sequences in the value are never read as replacement patterns\n () => `const ${name} = ${JSON.stringify(value, null, 2)};`,\n ),\n template,\n );\n\n/**\n * Reads one of the worker templates.\n * @param name The template's name.\n * @returns A promise that resolves to the template's source.\n */\nconst readTemplate = (name: keyof typeof TEMPLATE_URLS): Promise<string> =>\n readTextFile(fileURLToPath(TEMPLATE_URLS[name]));\n\n/**\n * Generates the worker's source from its templates: the base template, followed by the\n * firebase-fcm template when the config selects it.\n * @param input What the worker is generated from.\n * @returns A promise that resolves to the worker's source.\n */\nexport const renderServiceWorker = async (input: IRenderServiceWorkerInput): Promise<string> => {\n const base = setConstants(await readTemplate('base'), {\n CACHE_NAME_PREFIX: input.cacheNamePrefix,\n CACHE_NAME: input.cacheName,\n PRECACHE_ASSETS: input.precacheAssets,\n EXCLUDE_MIME_TYPES: input.excludeMIMETypes,\n SKIP_WAITING_MESSAGE_TYPE,\n CACHING: input.caching,\n });\n if (input.template === 'base' || input.firebase === undefined) {\n return `${GENERATED_HEADER}\\n${base}`;\n }\n const firebaseFcm = setConstants(await readTemplate('firebase-fcm'), {\n FIREBASE_SDK_VERSION: input.firebase.sdkVersion,\n FIREBASE_OPTIONS: input.firebase.options,\n });\n return `${GENERATED_HEADER}\\n${base.trimEnd()}\\n\\n${firebaseFcm}`;\n};\n"]}
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/build/worker/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAErC,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAGlE,+DAA+D;AAC/D,MAAM,aAAa,GAAG;IACpB,IAAI,EAAE,IAAI,GAAG,CAAC,yBAAyB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;IACzD,cAAc,EAAE,IAAI,GAAG,CAAC,iCAAiC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;CACnE,CAAC;AAEX,2CAA2C;AAC3C,MAAM,gBAAgB,GACpB,4FAA4F,CAAC;AAkB/F;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAE,MAA+B,EAAU,EAAE,CACjF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAC3B,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CACxB,MAAM,CAAC,OAAO,CACZ,IAAI,MAAM,CAAC,UAAU,IAAI,SAAS,EAAE,GAAG,CAAC;AACxC,mFAAmF;AACnF,GAAG,EAAE,CAAC,SAAS,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAC3D,EACH,QAAQ,CACT,CAAC;AAEJ;;;;GAIG;AACH,MAAM,YAAY,GAAG,CAAC,IAAgC,EAAmB,EAAE,CACzE,YAAY,CAAC,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,KAAgC,EAAmB,EAAE;IAC7F,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,EAAE;QACpD,iBAAiB,EAAE,KAAK,CAAC,eAAe;QACxC,UAAU,EAAE,KAAK,CAAC,SAAS;QAC3B,eAAe,EAAE,KAAK,CAAC,cAAc;QACrC,kBAAkB,EAAE,KAAK,CAAC,gBAAgB;QAC1C,yBAAyB;QACzB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,mBAAmB,EAAE,KAAK,CAAC,kBAAkB;KAC9C,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC9D,OAAO,GAAG,gBAAgB,KAAK,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,YAAY,CAAC,cAAc,CAAC,EAAE;QACnE,oBAAoB,EAAE,KAAK,CAAC,QAAQ,CAAC,UAAU;QAC/C,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAC,OAAO;KACzC,CAAC,CAAC;IACH,OAAO,GAAG,gBAAgB,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,EAAE,CAAC;AACpE,CAAC,CAAC","sourcesContent":["import { fileURLToPath } from 'node:url';\nimport { readTextFile } from 'jg-fs';\n\nimport { SKIP_WAITING_MESSAGE_TYPE } from '../../shared/index.js';\nimport type { IFirebaseOptions, IServiceWorkerConfig } from '../config/index.js';\n\n// the templates, copied next to the compiled code by the build\nconst TEMPLATE_URLS = {\n base: new URL('../templates/base.sw.js', import.meta.url),\n 'firebase-fcm': new URL('../templates/firebase-fcm.sw.js', import.meta.url),\n} as const;\n\n// the first line of every generated worker\nconst GENERATED_HEADER =\n '// Generated by jg-sw. Changes to this file are overwritten the next time it is generated.';\n\n// what the worker is generated from\nexport type IRenderServiceWorkerInput = {\n template: IServiceWorkerConfig['template'];\n cacheNamePrefix: string;\n cacheName: string;\n precacheAssets: string[];\n excludeMIMETypes: string[];\n caching: boolean;\n\n // the precached page that answers a navigation the network can't serve, or null for none\n navigationFallback: string | null;\n\n // the firebase-fcm template's SDK version and app options\n firebase?: { sdkVersion: string; options: IFirebaseOptions };\n};\n\n/**\n * Sets the values of a template's constants, each declared on its own line as\n * `const NAME = <placeholder>;`. Values are serialized as JSON, so strings and paths are escaped.\n * @param template The template's source.\n * @param values The value of each constant, by name.\n * @returns The source with the constants set.\n */\nconst setConstants = (template: string, values: Record<string, unknown>): string =>\n Object.entries(values).reduce(\n (source, [name, value]) =>\n source.replace(\n new RegExp(`^const ${name} = .*;$`, 'm'),\n // a function, so `$` sequences in the value are never read as replacement patterns\n () => `const ${name} = ${JSON.stringify(value, null, 2)};`,\n ),\n template,\n );\n\n/**\n * Reads one of the worker templates.\n * @param name The template's name.\n * @returns A promise that resolves to the template's source.\n */\nconst readTemplate = (name: keyof typeof TEMPLATE_URLS): Promise<string> =>\n readTextFile(fileURLToPath(TEMPLATE_URLS[name]));\n\n/**\n * Generates the worker's source from its templates: the base template, followed by the\n * firebase-fcm template when the config selects it.\n * @param input What the worker is generated from.\n * @returns A promise that resolves to the worker's source.\n */\nexport const renderServiceWorker = async (input: IRenderServiceWorkerInput): Promise<string> => {\n const base = setConstants(await readTemplate('base'), {\n CACHE_NAME_PREFIX: input.cacheNamePrefix,\n CACHE_NAME: input.cacheName,\n PRECACHE_ASSETS: input.precacheAssets,\n EXCLUDE_MIME_TYPES: input.excludeMIMETypes,\n SKIP_WAITING_MESSAGE_TYPE,\n CACHING: input.caching,\n NAVIGATION_FALLBACK: input.navigationFallback,\n });\n if (input.template === 'base' || input.firebase === undefined) {\n return `${GENERATED_HEADER}\\n${base}`;\n }\n const firebaseFcm = setConstants(await readTemplate('firebase-fcm'), {\n FIREBASE_SDK_VERSION: input.firebase.sdkVersion,\n FIREBASE_OPTIONS: input.firebase.options,\n });\n return `${GENERATED_HEADER}\\n${base.trimEnd()}\\n\\n${firebaseFcm}`;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jg-sw",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Service workers for web apps: a generated caching worker with a Vite plugin and CLI, plus registration, updates, and installation.",
5
5
  "author": "Jesus Graterol",
6
6
  "license": "MIT",
@@ -23,7 +23,8 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "CHANGELOG.md",
26
- "API.md"
26
+ "API.md",
27
+ "RECIPES.md"
27
28
  ],
28
29
  "sideEffects": false,
29
30
  "engines": {
@@ -50,9 +51,9 @@
50
51
  "firebase-cloud-messaging"
51
52
  ],
52
53
  "dependencies": {
53
- "jg-exception": "^1.1.0",
54
- "jg-fs": "^1.1.0",
55
- "jg-utilities": "^1.1.0",
54
+ "jg-exception": "^1.2.0",
55
+ "jg-fs": "^1.2.0",
56
+ "jg-utilities": "^1.1.1",
56
57
  "zod": "^4.6.7"
57
58
  },
58
59
  "peerDependencies": {