@swoff/cli 0.3.6 → 0.3.7
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/README.md +375 -91
- package/bin/swoff +8 -14
- package/dist/__tests__/assemble-sw.test.js +2 -2
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +0 -1
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +4 -3
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +12 -23
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +0 -1
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +0 -1
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/add.js +4 -4
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +33 -6
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/info.js +18 -4
- package/dist/lib/commands/info.js.map +1 -1
- package/dist/lib/commands/init.js +6 -2
- package/dist/lib/commands/init.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
- package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-store.js +114 -5
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-user.js +2 -2
- package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +1 -3
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +4 -16
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +40 -15
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +23 -209
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/guide-generator.js +153 -36
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +31 -3
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/push.js +143 -0
- package/dist/lib/generators/file-generators/push.js.map +1 -0
- package/dist/lib/generators/file-generators/quick-readme.js +13 -3
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +1 -1
- package/dist/lib/generators/file-generators/sw-push.js +67 -0
- package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-template.js +2 -0
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +13 -13
- package/dist/lib/generators/sw-generator.js +8 -9
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js +5 -1
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/default-template.js +1 -30
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
- package/dist/lib/generators/sw-sections/fetch-handler.js +157 -86
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/install-handler.js +12 -10
- package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
- package/dist/lib/generators/sw-sections/tag-management.js +11 -0
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +9 -9
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +5 -3
- package/dist/lib/shared/config-types.js.map +1 -1
- package/package.json +3 -2
- package/templates/hooks/useAuth.jsx +30 -0
- package/templates/hooks/useAuth.tsx +30 -0
- package/templates/hooks/useBackgroundSync.jsx +31 -0
- package/templates/hooks/useBackgroundSync.tsx +31 -0
- package/templates/hooks/useCacheInvalidation.jsx +19 -0
- package/templates/hooks/useCacheInvalidation.tsx +19 -0
- package/templates/hooks/useCachedFetch.jsx +55 -0
- package/templates/hooks/useCachedFetch.tsx +69 -0
- package/templates/hooks/useMutationQueue.jsx +34 -0
- package/templates/hooks/useMutationQueue.tsx +34 -0
- package/templates/hooks/useNetworkStatus.jsx +19 -0
- package/templates/hooks/useNetworkStatus.tsx +19 -0
- package/templates/hooks/usePushSubscription.jsx +67 -0
- package/templates/hooks/usePushSubscription.tsx +67 -0
- package/templates/hooks/useSWUpdate.jsx +76 -0
- package/templates/hooks/useSWUpdate.tsx +76 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Swoff generates a **service worker** and **client-side utilities** that give you
|
|
|
9
9
|
- **Auth** — token management with automatic 401 handling
|
|
10
10
|
- **Cache invalidation** — tag-based cache busting after mutations
|
|
11
11
|
- **PWA** — install prompt and manifest
|
|
12
|
+
- **Push notifications** — subscription management with IndexedDB persistence and VAPID support
|
|
12
13
|
- **Cross-tab sync** — broadcast changes across open tabs
|
|
13
14
|
- **Background Sync API** — process mutations even after tab close
|
|
14
15
|
|
|
@@ -94,7 +95,7 @@ swoff add auth
|
|
|
94
95
|
swoff add tag-invalidation
|
|
95
96
|
swoff add cross-tab
|
|
96
97
|
swoff add background-sync
|
|
97
|
-
swoff add
|
|
98
|
+
swoff add push-notification
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
### `info`
|
|
@@ -123,23 +124,57 @@ This deletes `swoff/`, `swoff.config.json`, `version.json`, and removes the SW g
|
|
|
123
124
|
|
|
124
125
|
```json
|
|
125
126
|
{
|
|
127
|
+
"$schema": "https://swoff.netlify.app/schema/v1.json",
|
|
128
|
+
"enabled": true,
|
|
126
129
|
"framework": "react",
|
|
127
|
-
"
|
|
128
|
-
|
|
130
|
+
"build": {
|
|
131
|
+
"outputDir": "dist",
|
|
132
|
+
"swFilename": "sw"
|
|
133
|
+
},
|
|
129
134
|
"features": {
|
|
130
|
-
"mutationQueue": true,
|
|
131
|
-
"backgroundSync": false,
|
|
132
|
-
"crossTabSync": false,
|
|
133
|
-
"tagInvalidation": true,
|
|
134
|
-
"clientRegistration": true,
|
|
135
135
|
"pwa": {
|
|
136
|
-
"enabled":
|
|
136
|
+
"enabled": true,
|
|
137
|
+
"preventDefaultInstall": false
|
|
138
|
+
},
|
|
139
|
+
"serviceWorker": {
|
|
140
|
+
"version": {
|
|
141
|
+
"enabled": true,
|
|
142
|
+
"source": "from-package",
|
|
143
|
+
"minSupportedVersion": "1.0.0"
|
|
144
|
+
},
|
|
145
|
+
"autoUpdate": true,
|
|
146
|
+
"autoActivate": false,
|
|
147
|
+
"defaultStrategy": "cache-first",
|
|
148
|
+
"strategies": {
|
|
149
|
+
"/api/*": "network-first",
|
|
150
|
+
"/static/*": {
|
|
151
|
+
"strategy": "cache-first",
|
|
152
|
+
"maxCacheEntries": 50,
|
|
153
|
+
"maxCacheAge": 3600000
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"cacheStrategy": "all",
|
|
157
|
+
"navigationPreload": true,
|
|
158
|
+
"maxCacheEntries": 100,
|
|
159
|
+
"maxCacheAge": 86400000,
|
|
160
|
+
"runtimeCacheName": "swoff-runtime",
|
|
161
|
+
"clearRuntimeOnUpdate": false,
|
|
162
|
+
"navigationMode": "spa",
|
|
163
|
+
"spaEntry": "/index.html"
|
|
137
164
|
},
|
|
165
|
+
"mutationQueue": false,
|
|
166
|
+
"backgroundSync": false,
|
|
138
167
|
"auth": {
|
|
139
|
-
"enabled":
|
|
168
|
+
"enabled": false,
|
|
140
169
|
"type": "bearer",
|
|
141
170
|
"refreshPath": "/api/refresh",
|
|
142
171
|
"userEndpoint": "/api/me"
|
|
172
|
+
},
|
|
173
|
+
"crossTabSync": true,
|
|
174
|
+
"tagInvalidation": true,
|
|
175
|
+
"pushNotifications": {
|
|
176
|
+
"enabled": false,
|
|
177
|
+
"vapidPublicKey": ""
|
|
143
178
|
}
|
|
144
179
|
}
|
|
145
180
|
}
|
|
@@ -147,53 +182,106 @@ This deletes `swoff/`, `swoff.config.json`, `version.json`, and removes the SW g
|
|
|
147
182
|
|
|
148
183
|
| Field | Type | Default | Description |
|
|
149
184
|
|-------|------|---------|-------------|
|
|
185
|
+
| `$schema` | `string` | — | JSON Schema URL |
|
|
186
|
+
| `enabled` | `boolean` | `true` | Enable Swoff |
|
|
150
187
|
| `framework` | `"react"` \| `"vue"` \| `"svelte"` \| `"vanilla"` | auto | Your UI framework |
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
188
|
+
| `build.outputDir` | `string` | `"dist"` | Build output directory |
|
|
189
|
+
| `build.swFilename` | `string` | `"sw"` | Service worker filename prefix |
|
|
190
|
+
| `features.pwa.enabled` | `boolean` | `true` | PWA install + manifest |
|
|
191
|
+
| `features.pwa.preventDefaultInstall` | `boolean` | `false` | Prevent default install prompt |
|
|
192
|
+
| `features.serviceWorker.version.enabled` | `boolean` | `true` | Enable versioned SW |
|
|
193
|
+
| `features.serviceWorker.version.source` | `"from-package"` \| `"manual"` | `"from-package"` | Version source |
|
|
194
|
+
| `features.serviceWorker.version.value` | `string` | — | Manual version (required if source is `"manual"`) |
|
|
195
|
+
| `features.serviceWorker.version.minSupportedVersion` | `string` | `"0.0.0"` | Min supported SW version |
|
|
196
|
+
| `features.serviceWorker.autoUpdate` | `boolean` | `true` | Auto-update SW |
|
|
197
|
+
| `features.serviceWorker.autoActivate` | `boolean` | `false` | Auto-activate SW |
|
|
198
|
+
| `features.serviceWorker.defaultStrategy` | `string` | `"cache-first"` | Default cache strategy |
|
|
199
|
+
| `features.serviceWorker.strategies` | `object` | `{}` | Per-route caching strategies. Each value is a strategy name string, or `{ strategy, maxCacheEntries?, maxCacheAge? }` |
|
|
200
|
+
| `features.serviceWorker.cacheStrategy` | `"all"` \| `"explicit-only"` | `"all"` | When to apply caching strategies. `"all"`: every GET/HEAD; `"explicit-only"`: only if `X-SW-Cache-Strategy` header is present |
|
|
201
|
+
| `features.serviceWorker.navigationPreload` | `boolean` | `true` | Enable Navigation Preload API — reduces SW startup latency for navigation requests |
|
|
202
|
+
| `features.serviceWorker.maxCacheEntries` | `number` | — | Max runtime cache entries (0 = unlimited) |
|
|
203
|
+
| `features.serviceWorker.maxCacheAge` | `number` | — | Max runtime cache age in ms (0 = unlimited) |
|
|
204
|
+
| `features.serviceWorker.runtimeCacheName` | `string` | `"swoff-runtime"` | Runtime cache name |
|
|
205
|
+
| `features.serviceWorker.clearRuntimeOnUpdate` | `boolean` | `false` | Clear runtime cache on update |
|
|
206
|
+
| `features.serviceWorker.navigationMode` | `"spa"` \| `"default"` | `"spa"` | Navigation caching mode |
|
|
207
|
+
| `features.serviceWorker.spaEntry` | `string` | `"/index.html"` | SPA entry for nav fallback |
|
|
153
208
|
| `features.mutationQueue` | `boolean` | `false` | Offline write queue |
|
|
154
209
|
| `features.backgroundSync` | `boolean` | `false` | Background Sync API |
|
|
155
|
-
| `features.crossTabSync` | `boolean` | `false` | Cross-tab broadcast |
|
|
156
|
-
| `features.tagInvalidation` | `boolean` | `false` | Tag-based cache invalidation |
|
|
157
|
-
| `features.clientRegistration` | `boolean` | `false` | SW registration lifecycle |
|
|
158
|
-
| `features.pwa.enabled` | `boolean` | `false` | PWA install + manifest |
|
|
159
210
|
| `features.auth.enabled` | `boolean` | `false` | Auth module |
|
|
160
|
-
| `features.auth.type` | `"bearer"` \| `"cookie"` \| `"custom"` |
|
|
211
|
+
| `features.auth.type` | `"bearer"` \| `"cookie"` \| `"custom"` | `"bearer"` | Auth strategy |
|
|
161
212
|
| `features.auth.refreshPath` | `string` | `"/api/refresh"` | Token refresh endpoint |
|
|
162
213
|
| `features.auth.userEndpoint` | `string` | `"/api/me"` | Current user endpoint |
|
|
214
|
+
| `features.crossTabSync` | `boolean` | `false` | Cross-tab broadcast |
|
|
215
|
+
| `features.tagInvalidation` | `boolean` | `false` | Tag-based cache invalidation |
|
|
216
|
+
| `features.pushNotifications.enabled` | `boolean` | `false` | Push notification subscription management |
|
|
217
|
+
| `features.pushNotifications.vapidPublicKey` | `string` | `""` | VAPID public key (can also be passed at runtime) |
|
|
163
218
|
|
|
164
219
|
---
|
|
165
220
|
|
|
166
|
-
## Generated
|
|
221
|
+
## Generated Files
|
|
167
222
|
|
|
168
223
|
```
|
|
169
224
|
swoff/
|
|
170
|
-
├── client-injector.ts #
|
|
225
|
+
├── client-injector.ts # Orchestrator — wires all features together
|
|
226
|
+
│ Imports: sw/injector, pwa/install, mutation-queue
|
|
227
|
+
│ Exports: initServiceWorker()
|
|
228
|
+
│ Always generated
|
|
229
|
+
│
|
|
171
230
|
├── fetch-wrapper.ts # Unified fetch with caching, auth, offline queue
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
├──
|
|
231
|
+
│ Imports: invalidation-tags, cache, auth/store, mutation-queue (conditional)
|
|
232
|
+
│ Exports: fetchWithCache(input, options?)
|
|
233
|
+
│ Always generated
|
|
234
|
+
│
|
|
235
|
+
├── cache.ts # Low-level cache invalidation
|
|
236
|
+
│ Exports: invalidateByTag(), invalidateByTags()
|
|
237
|
+
│ Generated when: tagInvalidation is true
|
|
177
238
|
│
|
|
178
|
-
├──
|
|
179
|
-
│
|
|
180
|
-
│
|
|
239
|
+
├── invalidation-tags.ts # Tag generation helpers
|
|
240
|
+
│ Exports: generateTags(), invalidateUrl(), invalidateByMethod()
|
|
241
|
+
│ Generated when: tagInvalidation is true
|
|
242
|
+
│
|
|
243
|
+
├── mutation-queue.ts # Offline write queue
|
|
244
|
+
│ Exports: queueMutation(), processMutationQueue(), flushMutations(), getPendingCount()
|
|
245
|
+
│ Generated when: mutationQueue is true
|
|
246
|
+
│
|
|
247
|
+
├── push.ts # Push notification subscription management
|
|
248
|
+
│ Exports: subscribeToPush(), unsubscribeFromPush(), isSubscribed(), getPushSubscription(), requestNotificationPermission()
|
|
249
|
+
│ Generated when: pushNotifications.enabled is true
|
|
250
|
+
│
|
|
251
|
+
├── background-sync.ts # Background Sync API
|
|
252
|
+
│ Exports: syncWhenPossible(), retrySync()
|
|
253
|
+
│ Generated when: backgroundSync is true
|
|
254
|
+
│
|
|
255
|
+
├── swoff.d.ts # TypeScript declarations for all generated modules
|
|
256
|
+
│
|
|
257
|
+
├── auth/ # Generated when auth.enabled is true
|
|
258
|
+
│ ├── store.ts # Token/user persistence (memory + IndexedDB) + auth header helpers
|
|
259
|
+
│ │ Exports: setAuth(), getAuth(), clearAuth(), isAuthValid(), createAuthFromResponse(), ensureValidAuth(), withAuthHeaders()
|
|
181
260
|
│ ├── user.ts # User data caching
|
|
261
|
+
│ │ Exports: fetchCurrentUser(), getCachedUser(), cacheUser(), clearCachedUser()
|
|
182
262
|
│ └── state.ts # Online/offline × auth state detection
|
|
263
|
+
│ Exports: getAuthState()
|
|
183
264
|
│
|
|
184
|
-
├── hooks/ #
|
|
185
|
-
│ ├── useAuth.tsx
|
|
186
|
-
│ ├── useCachedFetch.tsx
|
|
187
|
-
│ ├── useMutationQueue.tsx
|
|
188
|
-
│
|
|
265
|
+
├── hooks/ # Generated when framework is "react"
|
|
266
|
+
│ ├── useAuth.tsx # Reactive auth + connectivity state
|
|
267
|
+
│ ├── useCachedFetch.tsx # Auto-refetch on cache invalidation
|
|
268
|
+
│ ├── useMutationQueue.tsx # Queue status and sync results
|
|
269
|
+
│ ├── useSWUpdate.tsx # SW update management (+ useSWProgress in same file)
|
|
270
|
+
│ ├── useNetworkStatus.tsx # Reactive online/offline state
|
|
271
|
+
│ ├── useBackgroundSync.tsx # Background sync state and trigger
|
|
272
|
+
│ └── useCacheInvalidation.tsx # Cache invalidation helpers
|
|
189
273
|
│
|
|
190
274
|
├── pwa/
|
|
191
|
-
│ └── install.ts
|
|
275
|
+
│ └── install.ts # PWA install prompt handling
|
|
276
|
+
│ Exports: setupPwaInstall(), isInstallable(), promptInstall()
|
|
277
|
+
│ Generated when: pwa.enabled is true
|
|
192
278
|
│
|
|
193
279
|
├── sw/
|
|
194
|
-
│ ├── template.js # Service worker source
|
|
280
|
+
│ ├── template.js # Service worker source (runs in SW scope)
|
|
281
|
+
│ │ # Implements all 5 caching strategies in the SW
|
|
195
282
|
│ ├── injector.ts # SW registration logic
|
|
196
|
-
│
|
|
283
|
+
│ │ # Exports: initServiceWorker(), handleUpdateApproved(), skipWaiting()
|
|
284
|
+
│ └── generator.js # Build-time script — embeds asset hashes into SW
|
|
197
285
|
│
|
|
198
286
|
├── manifest.json
|
|
199
287
|
├── GUIDE.md # Full integration walkthrough
|
|
@@ -202,72 +290,217 @@ swoff/
|
|
|
202
290
|
|
|
203
291
|
---
|
|
204
292
|
|
|
205
|
-
##
|
|
293
|
+
## Cache Strategies
|
|
206
294
|
|
|
207
|
-
|
|
295
|
+
The service worker applies a caching strategy to GET/HEAD requests based on `features.serviceWorker.cacheStrategy`:
|
|
208
296
|
|
|
209
|
-
|
|
297
|
+
| Mode | Behavior |
|
|
298
|
+
|------|----------|
|
|
299
|
+
| `"all"` (default) | All GET/HEAD requests go through the strategy system. Plain `fetch()` calls are cached by the SW just like `fetchWithCache()` calls. |
|
|
300
|
+
| `"explicit-only"` | Only requests with a `X-SW-Cache-Strategy` header are processed by the SW strategy system. Plain `fetch()` calls pass through the SW unmodified. `fetchWithCache()` sets this header automatically — use it for all API calls to ensure caching works. |
|
|
210
301
|
|
|
211
|
-
|
|
212
|
-
import { queueMutation, getPendingCount } from "./swoff/mutation-queue.js";
|
|
302
|
+
### Strategy resolution (3 tiers, highest to lowest priority)
|
|
213
303
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
304
|
+
1. **Per-request override** — set `strategy` or `staleWhileRevalidate` on any `fetchWithCache(options)`. Sends `X-SW-Strategy` header to the SW.
|
|
305
|
+
2. **URL pattern match** — `features.serviceWorker.strategies` object maps URL prefixes (e.g. `/api/*`) to strategies.
|
|
306
|
+
3. **Default** — `features.serviceWorker.defaultStrategy` (default: `"cache-first"`).
|
|
307
|
+
|
|
308
|
+
### Available strategies
|
|
309
|
+
|
|
310
|
+
| Strategy | Behavior | Best for |
|
|
311
|
+
|----------|----------|----------|
|
|
312
|
+
| `cache-first` | Return cached if available, else fetch + cache. Default. | Static assets, images, fonts, rarely-changing data |
|
|
313
|
+
| `network-first` | Try network. On success, cache. On failure, serve cache. | API endpoints, dynamic content where freshness matters |
|
|
314
|
+
| `stale-while-revalidate` | Return cached immediately, refresh cache in background. | Fast UI, non-critical data, content that can be slightly stale |
|
|
315
|
+
| `cache-only` | Serve from cache only. Returns 404 if missing. | Offline-critical assets that must always be available |
|
|
316
|
+
| `network-only` | Always fetch, never cache. | Sensitive or real-time data, payment flows |
|
|
317
|
+
|
|
318
|
+
### Request dispatch flow
|
|
319
|
+
|
|
320
|
+
Every GET/HEAD request goes through this flow in the SW:
|
|
221
321
|
|
|
222
|
-
// Show sync badge
|
|
223
|
-
const count = await getPendingCount();
|
|
224
322
|
```
|
|
323
|
+
navigation (SPA fallback) → precache hit? → strategy dispatch → pass-through
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- **Navigation requests** (SPA mode): if no cached response is found, the SPA entry (`/index.html` by default) is served as a fallback.
|
|
327
|
+
- **Precache**: build assets cached at install time are checked first.
|
|
328
|
+
- **Strategy**: the resolved strategy (via the 3-tier priority above) determines how the request is cached and served.
|
|
329
|
+
- **Pass-through**: if no strategy matches, the request goes to the network.
|
|
225
330
|
|
|
226
|
-
|
|
331
|
+
---
|
|
227
332
|
|
|
228
|
-
|
|
333
|
+
## fetchWithCache API
|
|
229
334
|
|
|
230
|
-
|
|
335
|
+
A drop-in replacement for `fetch()` that communicates with the service worker about caching, handles offline mode, deduplicates in-flight requests, auto-generates cache tags, and auto-invalidates after mutations.
|
|
231
336
|
|
|
232
337
|
```js
|
|
233
338
|
import { fetchWithCache } from "./swoff/fetch-wrapper.js";
|
|
234
339
|
|
|
235
|
-
//
|
|
340
|
+
// Read — cached by the SW for offline access
|
|
236
341
|
const { response, fromCache } = await fetchWithCache("/api/todos");
|
|
237
342
|
const data = await response.json();
|
|
238
343
|
|
|
239
|
-
//
|
|
344
|
+
// Mutation — passes through, auto-invalidates cache tags
|
|
240
345
|
await fetchWithCache("/api/todos", {
|
|
241
346
|
method: "POST",
|
|
242
347
|
body: JSON.stringify({ title: "New task" }),
|
|
243
348
|
});
|
|
244
349
|
|
|
245
|
-
//
|
|
246
|
-
|
|
350
|
+
// POST used as a read (search, GraphQL) — override with type: "read"
|
|
351
|
+
await fetchWithCache("/api/search", {
|
|
352
|
+
method: "POST",
|
|
353
|
+
type: "read",
|
|
354
|
+
body: JSON.stringify({ query: "hello" }),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// With cache tags + stale-while-revalidate
|
|
358
|
+
const { response: staleRes, fromCache } = await fetchWithCache("/api/data", {
|
|
359
|
+
tags: ["data"],
|
|
360
|
+
staleWhileRevalidate: true,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Auth + disable offline queue
|
|
364
|
+
const { response: userRes } = await fetchWithCache("/api/me", {
|
|
365
|
+
auth: true,
|
|
366
|
+
queueOffline: false,
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Options
|
|
371
|
+
|
|
372
|
+
All `RequestInit` fields are supported (`method`, `body`, `headers`, `credentials`, `signal`, etc.), plus:
|
|
373
|
+
|
|
374
|
+
| Option | Type | Default | Description |
|
|
375
|
+
|--------|------|---------|-------------|
|
|
376
|
+
| `tags` | `string[]` | auto-generated | Cache invalidation tags for this request |
|
|
377
|
+
| `staleWhileRevalidate` | `boolean` | `false` | Return cached immediately, refresh in background |
|
|
378
|
+
| `auth` | `boolean` | `false` | Attach auth token (uses `getAuth()`) |
|
|
379
|
+
| `queueOffline` | `boolean` | `true` | When offline, queue writes to IndexedDB for later replay |
|
|
380
|
+
| `invalidate` | `'auto' \| string[] \| false` | `'auto'` | Auto-invalidate cache tags after a successful mutation |
|
|
381
|
+
| `type` | `'read' \| 'mutation'` | auto-detected | Override read/mutation detection |
|
|
382
|
+
| `strategy` | `'cache-first' \| 'network-first' \| 'stale-while-revalidate' \| 'cache-only' \| 'network-only'` | — | Override caching strategy per-request (highest priority, overrides config strategies and default) |
|
|
383
|
+
|
|
384
|
+
### Behavior
|
|
385
|
+
|
|
386
|
+
- **Read vs mutation**: GET/HEAD → read (cached). POST/PUT/DELETE/PATCH → mutation (pass through). Override with `type: 'read'` or `type: 'mutation'`.
|
|
387
|
+
- **Offline reads**: returns cached response if available, throws if not cached.
|
|
388
|
+
- **Offline writes**: queues to IndexedDB (when `mutationQueue` enabled). Replays on `online` event. Disable per-request with `queueOffline: false`.
|
|
389
|
+
- **Dedup**: in-flight GETs to the same URL return a single promise (cloned response).
|
|
390
|
+
- **Auto-tags**: when `tagInvalidation` is enabled, tags are derived from the URL for read requests.
|
|
391
|
+
- **Auto-invalidate**: after a successful mutation, matching cache tags are invalidated so the SW re-fetches fresh data.
|
|
392
|
+
- **Auth**: when `auth: true`, attaches auth headers via `withAuthHeaders()` (supports bearer, cookie, and custom). Dispatches `sw-auth-unauthorized` on 401 and clears auth.
|
|
393
|
+
|
|
394
|
+
### Return value
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
{ response: Response, fromCache: boolean }
|
|
247
398
|
```
|
|
248
399
|
|
|
249
|
-
|
|
400
|
+
- `response`: the fetch Response (from cache or network)
|
|
401
|
+
- `fromCache`: `true` when the response was served from cache
|
|
250
402
|
|
|
251
|
-
|
|
403
|
+
In TypeScript, use the generic variant for typed responses:
|
|
252
404
|
|
|
253
|
-
|
|
405
|
+
```ts
|
|
406
|
+
const { response } = await fetchWithCache<Todo[]>("/api/todos");
|
|
407
|
+
const data: Todo[] = await response.json();
|
|
408
|
+
// data is typed as Todo[]
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Auth API
|
|
414
|
+
|
|
415
|
+
The auth module is generated when `features.auth.enabled` is `true`. It manages authentication state with a **memory-only token** (never persisted to disk) and optional IndexedDB caching for offline user display.
|
|
416
|
+
|
|
417
|
+
There is no separate auth fetch wrapper — `fetchWithCache` handles all auth types
|
|
418
|
+
natively. Just pass `{ auth: true }`:
|
|
254
419
|
|
|
255
420
|
```js
|
|
256
|
-
import {
|
|
421
|
+
import { fetchWithCache } from "./swoff/fetch-wrapper.js";
|
|
257
422
|
|
|
258
|
-
// Attaches
|
|
259
|
-
const
|
|
423
|
+
// Attaches auth headers, bypasses SW cache for auth URLs, handles 401
|
|
424
|
+
const { response } = await fetchWithCache("/api/me", { auth: true });
|
|
425
|
+
const user = await response.json();
|
|
260
426
|
|
|
261
|
-
//
|
|
262
|
-
await
|
|
427
|
+
// Mutations too
|
|
428
|
+
await fetchWithCache("/api/todos", {
|
|
263
429
|
method: "POST",
|
|
430
|
+
auth: true,
|
|
264
431
|
body: JSON.stringify({ title: "New" }),
|
|
265
432
|
});
|
|
266
433
|
```
|
|
267
434
|
|
|
268
|
-
|
|
435
|
+
**What `auth: true` does:**
|
|
436
|
+
|
|
437
|
+
| Step | What happens |
|
|
438
|
+
|------|-------------|
|
|
439
|
+
| 1 | Calls `getAuth()` to retrieve the stored token/user |
|
|
440
|
+
| 2 | Calls `withAuthHeaders(headers, auth)` — injects Bearer token, cookie, or custom header based on `auth.type` in config |
|
|
441
|
+
| 3 | Marks auth endpoints (`/login`, `/logout`, `/register`, `refreshPath`, `userEndpoint`) as `"mutation"` strategy so the SW never caches them |
|
|
442
|
+
| 4 | For `auth.type: "cookie"`, sets `credentials: "include"` |
|
|
443
|
+
| 5 | On 401 response: calls `clearAuth()`, dispatches `sw-auth-unauthorized` event |
|
|
444
|
+
|
|
445
|
+
**Returns:** `{ response: Response, fromCache: boolean }` — same as any `fetchWithCache` call
|
|
446
|
+
|
|
447
|
+
### Auth functions
|
|
448
|
+
|
|
449
|
+
| Function | Arguments | Returns | Description |
|
|
450
|
+
|----------|-----------|---------|-------------|
|
|
451
|
+
| `setAuth(authData)` | `{ token?, user?, expiresAt? }` | `Promise<void>` | Store auth in memory, persist user to IndexedDB |
|
|
452
|
+
| `getAuth()` | — | `Promise<AuthData \| null>` | Retrieve auth from memory (or IndexedDB fallback after page refresh) |
|
|
453
|
+
| `clearAuth()` | — | `Promise<void>` | Clear memory + IndexedDB. Call on logout/401 |
|
|
454
|
+
| `isAuthValid(auth)` | `AuthData \| null` | `boolean` | Check existence + expiry (`expiresAt`). Returns `true` if no `expiresAt` set |
|
|
455
|
+
| `createAuthFromResponse(response)` | server login response | `AuthData` | **Edit this** to match your backend's login response shape |
|
|
456
|
+
| `ensureValidAuth()` | — | `Promise<AuthData \| null>` | Check expiry, refresh token via `refreshPath` if needed |
|
|
457
|
+
| `fetchCurrentUser()` | — | `Promise<Record<string, unknown> \| null>` | Fetch from `userEndpoint` and cache in IndexedDB |
|
|
458
|
+
| `getCachedUser()` | — | `Promise<Record<string, unknown> \| null>` | Load user from IndexedDB (available offline) |
|
|
459
|
+
| `cacheUser(user)` | user object | `Promise<void>` | Manually persist user data |
|
|
460
|
+
| `clearCachedUser()` | — | `Promise<void>` | Remove user from cache |
|
|
461
|
+
| `getAuthState()` | — | `Promise<{ authenticated, user, online }>` | Detect which of 4 states the app is in |
|
|
462
|
+
|
|
463
|
+
### Auth types
|
|
464
|
+
|
|
465
|
+
| `auth.type` | How auth headers are set | Notes |
|
|
466
|
+
|-------------|------------------------|-------|
|
|
467
|
+
| `"bearer"` | `Authorization: Bearer <token>` | Token in memory only. Re-login required after page refresh. Use `refreshPath` for token refresh. |
|
|
468
|
+
| `"cookie"` | No explicit header. `credentials: "include"` is set for all requests. | HttpOnly cookie handled by the server. |
|
|
469
|
+
| `"custom"` | **Edit the `withAuthHeaders` function** in `auth/store.ts` | Full control over header injection. |
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Mutation Queue
|
|
474
|
+
|
|
475
|
+
When the user is offline, write operations (POST/PUT/DELETE) are stored in IndexedDB. They replay automatically when the connection returns.
|
|
476
|
+
|
|
477
|
+
```js
|
|
478
|
+
import { queueMutation, getPendingCount } from "./swoff/mutation-queue.js";
|
|
479
|
+
|
|
480
|
+
// Offline write — stored and replayed when online
|
|
481
|
+
await queueMutation({
|
|
482
|
+
method: "POST",
|
|
483
|
+
url: "/api/todos",
|
|
484
|
+
body: { title: "Buy milk" },
|
|
485
|
+
tags: ["todos"],
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Show sync badge
|
|
489
|
+
const count = await getPendingCount();
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| Function | Description |
|
|
493
|
+
|----------|-------------|
|
|
494
|
+
| `queueMutation(mutation)` | Store a write for later sync |
|
|
495
|
+
| `processMutationQueue()` | Replay all queued writes. Runs automatically on `online` event |
|
|
496
|
+
| `flushMutations()` | Same as `processMutationQueue`. Call after re-login (queued mutations may have stale auth) |
|
|
497
|
+
| `getPendingCount()` | Number of mutations waiting to sync |
|
|
498
|
+
|
|
499
|
+
Generated when `features.mutationQueue` is `true`.
|
|
269
500
|
|
|
270
|
-
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Cache Invalidation
|
|
271
504
|
|
|
272
505
|
Tag-based invalidation keeps the service worker cache fresh after mutations.
|
|
273
506
|
|
|
@@ -285,7 +518,18 @@ await invalidateUrl("/api/todos/42");
|
|
|
285
518
|
|
|
286
519
|
When the SW receives an invalidation, it removes matching cache entries and attempts to background-refetch them. If the refetch fails (network error), the old cached entry is served with a stale-while-revalidate fallback.
|
|
287
520
|
|
|
288
|
-
|
|
521
|
+
| Function | Description |
|
|
522
|
+
|----------|-------------|
|
|
523
|
+
| `generateTags(url)` | Extract tags from URL path. e.g. `/api/todos/42` → `["todos", "todo:42"]` |
|
|
524
|
+
| `invalidateUrl(url)` | Extract tags from URL and invalidate all matching cache entries |
|
|
525
|
+
| `invalidateByTag(tag)` | Invalidate a single tag. Dispatches `cache-invalidated` event |
|
|
526
|
+
| `invalidateByTags(tags)` | Invalidate multiple tags |
|
|
527
|
+
|
|
528
|
+
Generated when `features.tagInvalidation` is `true`.
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## PWA
|
|
289
533
|
|
|
290
534
|
Install prompt handling and manifest generation.
|
|
291
535
|
|
|
@@ -298,30 +542,77 @@ if (isInstallable()) {
|
|
|
298
542
|
}
|
|
299
543
|
```
|
|
300
544
|
|
|
301
|
-
|
|
545
|
+
| Function | Description |
|
|
546
|
+
|----------|-------------|
|
|
547
|
+
| `setupPwaInstall()` | Listen for `beforeinstallprompt`/`appinstalled` events. Called automatically by `client-injector.ts` |
|
|
548
|
+
| `isInstallable()` | Check if install prompt is available |
|
|
549
|
+
| `promptInstall()` | Show the native install prompt. Returns `{ outcome }` |
|
|
550
|
+
|
|
551
|
+
Generated when `features.pwa.enabled` is `true`.
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Push Notifications
|
|
302
556
|
|
|
303
|
-
|
|
557
|
+
Push notification subscription management with IndexedDB persistence.
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
import { subscribeToPush, unsubscribeFromPush, isSubscribed } from "./swoff/push.js";
|
|
561
|
+
|
|
562
|
+
// Subscribe (triggers permission prompt)
|
|
563
|
+
const sub = await subscribeToPush("YOUR_VAPID_PUBLIC_KEY");
|
|
564
|
+
if (sub) {
|
|
565
|
+
await fetch("/api/push/subscribe", {
|
|
566
|
+
method: "POST",
|
|
567
|
+
body: JSON.stringify(sub.toJSON()),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Unsubscribe
|
|
572
|
+
await unsubscribeFromPush();
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
| Function | Description |
|
|
576
|
+
|----------|-------------|
|
|
577
|
+
| `subscribeToPush(vapidPublicKey)` | Request permission and subscribe. Returns `PushSubscription` or `null` if denied |
|
|
578
|
+
| `unsubscribeFromPush()` | Unsubscribe and clear stored subscription from IndexedDB |
|
|
579
|
+
| `isSubscribed()` | Check if currently subscribed |
|
|
580
|
+
| `getPushSubscription()` | Get current `PushSubscription` object, or `null` |
|
|
581
|
+
| `requestNotificationPermission()` | Request notification permission only. Returns `boolean` |
|
|
582
|
+
|
|
583
|
+
Generated when `features.pushNotifications.enabled` is `true`.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## React Hooks
|
|
588
|
+
|
|
589
|
+
When `features.framework` is `"react"`, generated hooks provide reactive state:
|
|
304
590
|
|
|
305
591
|
| Hook | Returns | Description |
|
|
306
592
|
|------|---------|-------------|
|
|
307
|
-
| `useAuth()` | `{ authenticated, user, online }` | Auth + connectivity state |
|
|
308
|
-
| `useCachedFetch(url, options?)` | `{ data, error, loading, refetch }` |
|
|
309
|
-
| `useMutationQueue()` | `{ pending, lastSync }` | Queue status and sync
|
|
310
|
-
| `
|
|
311
|
-
| `useSWProgress()` | `{ status, progress }` | Download progress during SW update |
|
|
593
|
+
| `useAuth()` | `{ authenticated, user, online }` | Auth + connectivity state. Listens to online/offline/auth changes |
|
|
594
|
+
| `useCachedFetch(url, options?)` | `{ data, error, loading, refetch }` | Fetches data (auto-parsed JSON), auto-refetches on tag invalidation events |
|
|
595
|
+
| `useMutationQueue()` | `{ pending, lastSync }` | Queue status (`pending` count) and last sync result (`lastSync.succeeded`, `lastSync.failed`) |
|
|
596
|
+
| `useSWUpdate()` | `{ updateStatus, currentVersion, availableVersion, forceUpdate, error, acceptUpdate, dismissUpdate }` | SW update management. `updateStatus` is one of `"idle"`, `"available"`, `"downloading"`, `"ready"` |
|
|
597
|
+
| `useSWProgress()` | `{ status, progress }` | Download progress during SW update. `progress` is `{ percent, downloaded, total }` |
|
|
598
|
+
| `usePushSubscription(vapidPublicKey)` | `{ subscribed, subscription, permission, loading, subscribe, unsubscribe }` | Push subscription state. `subscribe()`/`unsubscribe()` toggle notifications at runtime |
|
|
599
|
+
| `useNetworkStatus()` | `boolean` | Reactive online/offline state |
|
|
600
|
+
| `useBackgroundSync()` | `{ supported, registered, lastSync, triggerSync }` | Background sync support detection and manual sync trigger |
|
|
601
|
+
| `useCacheInvalidation()` | `{ invalidateByTag, invalidateByTags, invalidateUrl }` | Stable callback wrappers around cache invalidation functions |
|
|
312
602
|
|
|
313
603
|
```tsx
|
|
314
604
|
import { useCachedFetch } from "./swoff/hooks/useCachedFetch.tsx";
|
|
315
605
|
|
|
316
|
-
function
|
|
606
|
+
function Todos() {
|
|
317
607
|
const { data, loading } = useCachedFetch("/api/todos");
|
|
318
|
-
|
|
319
608
|
if (loading) return <Spinner />;
|
|
320
609
|
return <TodoList data={data} />;
|
|
321
610
|
}
|
|
322
611
|
```
|
|
323
612
|
|
|
324
|
-
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Build Script
|
|
325
616
|
|
|
326
617
|
After `swoff generate`, your `package.json` build script is updated to:
|
|
327
618
|
|
|
@@ -329,22 +620,15 @@ After `swoff generate`, your `package.json` build script is updated to:
|
|
|
329
620
|
"build": "vite build && node swoff/sw/generator.js"
|
|
330
621
|
```
|
|
331
622
|
|
|
332
|
-
The generator
|
|
333
|
-
|
|
334
|
-
---
|
|
335
|
-
|
|
336
|
-
## Service Worker Architecture
|
|
337
|
-
|
|
338
|
-
The generated service worker (`sw/template.js`) implements:
|
|
623
|
+
The generator (`sw/generator.js`) runs after every build:
|
|
339
624
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
- **PWA lifecycle**: skip-waiting, activate, and install event handling
|
|
625
|
+
1. Reads `swoff.config.json` for output dir, version, and strategy config
|
|
626
|
+
2. Reads `sw/template.js` — the service worker source
|
|
627
|
+
3. Collects all built assets from the output directory
|
|
628
|
+
4. Replaces placeholders (`[[CACHE_NAME]]`, `[[ASSETS_LIST]]`, `[[AUTO_SKIP_WAITING]]`) with actual values
|
|
629
|
+
5. Writes the final versioned SW file (e.g. `dist/sw-v1.2.3.js`) and `version.json`
|
|
346
630
|
|
|
347
|
-
|
|
631
|
+
Running `swoff clean` removes the generator suffix from your build script.
|
|
348
632
|
|
|
349
633
|
---
|
|
350
634
|
|
package/bin/swoff
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const entryPoint = join(distDir, 'index.js');
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const entryPoint = join(__dirname, '..', 'dist', 'index.js');
|
|
11
9
|
|
|
12
10
|
if (!existsSync(entryPoint)) {
|
|
13
11
|
console.error('Error: CLI not built. Run: npm run build');
|
|
14
12
|
process.exit(1);
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
proc.on('exit', (code) => {
|
|
23
|
-
process.exit(code || 0);
|
|
15
|
+
import(entryPoint).catch((err) => {
|
|
16
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
17
|
+
process.exit(1);
|
|
24
18
|
});
|
|
@@ -9,7 +9,7 @@ describe("assembleSW", () => {
|
|
|
9
9
|
const sw = assembleSW(config, "1.0.0");
|
|
10
10
|
expect(sw).toContain("CACHE_NAME = 'sw-v1.0.0'");
|
|
11
11
|
expect(sw).toContain("self.addEventListener");
|
|
12
|
-
expect(sw).toContain("
|
|
12
|
+
expect(sw).toContain("CACHE_NAME_RUNTIME");
|
|
13
13
|
});
|
|
14
14
|
it("includes config header with resolved version", () => {
|
|
15
15
|
const sw = assembleSW(config, "2.5.0");
|
|
@@ -41,7 +41,7 @@ describe("assembleSW", () => {
|
|
|
41
41
|
const sw = assembleSW(config, "1.0.0");
|
|
42
42
|
expect(sw).toContain("fromPrecache");
|
|
43
43
|
expect(sw).toContain("async function fromPrecache");
|
|
44
|
-
expect(sw).toContain("cache.match(
|
|
44
|
+
expect(sw).toContain("return cache.match(new URL");
|
|
45
45
|
expect(sw).toContain("precached = await fromPrecache(request)");
|
|
46
46
|
});
|
|
47
47
|
it("includes message handler", () => {
|