@swoff/cli 0.3.5 → 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.
Files changed (121) hide show
  1. package/README.md +610 -24
  2. package/bin/swoff +8 -14
  3. package/dist/__tests__/assemble-sw.test.js +17 -3
  4. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  5. package/dist/__tests__/cli-integration.test.js +9 -9
  6. package/dist/__tests__/cli-integration.test.js.map +1 -1
  7. package/dist/__tests__/config-types.test.js +10 -9
  8. package/dist/__tests__/config-types.test.js.map +1 -1
  9. package/dist/__tests__/file-generators.test.js +65 -68
  10. package/dist/__tests__/file-generators.test.js.map +1 -1
  11. package/dist/__tests__/loader.test.js +12 -14
  12. package/dist/__tests__/loader.test.js.map +1 -1
  13. package/dist/__tests__/validator.test.js +41 -22
  14. package/dist/__tests__/validator.test.js.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/cli/help.js +3 -3
  18. package/dist/lib/cli/help.js.map +1 -1
  19. package/dist/lib/commands/add.js +11 -5
  20. package/dist/lib/commands/add.js.map +1 -1
  21. package/dist/lib/commands/clean.js +5 -0
  22. package/dist/lib/commands/clean.js.map +1 -1
  23. package/dist/lib/commands/generate-guide.js +43 -13
  24. package/dist/lib/commands/generate-guide.js.map +1 -1
  25. package/dist/lib/commands/generate.js +7 -3
  26. package/dist/lib/commands/generate.js.map +1 -1
  27. package/dist/lib/commands/info.js +149 -32
  28. package/dist/lib/commands/info.js.map +1 -1
  29. package/dist/lib/commands/init.js +6 -2
  30. package/dist/lib/commands/init.js.map +1 -1
  31. package/dist/lib/commands/validate.js +3 -1
  32. package/dist/lib/commands/validate.js.map +1 -1
  33. package/dist/lib/config/validator.js +25 -16
  34. package/dist/lib/config/validator.js.map +1 -1
  35. package/dist/lib/generators/file-generators/auth-fetch.js +29 -34
  36. package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
  37. package/dist/lib/generators/file-generators/auth-state.js +8 -5
  38. package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
  39. package/dist/lib/generators/file-generators/auth-store.js +181 -41
  40. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  41. package/dist/lib/generators/file-generators/auth-user.js +28 -32
  42. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  43. package/dist/lib/generators/file-generators/background-sync.js +9 -5
  44. package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
  45. package/dist/lib/generators/file-generators/cache.js +8 -5
  46. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  47. package/dist/lib/generators/file-generators/client-injector.js +101 -0
  48. package/dist/lib/generators/file-generators/client-injector.js.map +1 -0
  49. package/dist/lib/generators/file-generators/context.js +7 -6
  50. package/dist/lib/generators/file-generators/context.js.map +1 -1
  51. package/dist/lib/generators/file-generators/fetch-wrapper.js +196 -41
  52. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  53. package/dist/lib/generators/file-generators/generate-hooks.js +23 -151
  54. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  55. package/dist/lib/generators/file-generators/guide-generator.js +450 -0
  56. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -0
  57. package/dist/lib/generators/file-generators/invalidation-tags.js +12 -10
  58. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
  59. package/dist/lib/generators/file-generators/mutation-queue.js +65 -87
  60. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  61. package/dist/lib/generators/file-generators/mutation-reconcile.js +57 -0
  62. package/dist/lib/generators/file-generators/mutation-reconcile.js.map +1 -0
  63. package/dist/lib/generators/file-generators/push.js +143 -0
  64. package/dist/lib/generators/file-generators/push.js.map +1 -0
  65. package/dist/lib/generators/file-generators/pwa-install.js +41 -9
  66. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
  67. package/dist/lib/generators/file-generators/quick-readme.js +88 -0
  68. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -0
  69. package/dist/lib/generators/file-generators/reconcile.js +7 -12
  70. package/dist/lib/generators/file-generators/reconcile.js.map +1 -1
  71. package/dist/lib/generators/file-generators/store.js +30 -20
  72. package/dist/lib/generators/file-generators/store.js.map +1 -1
  73. package/dist/lib/generators/file-generators/sw-generator-build.js +38 -17
  74. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
  75. package/dist/lib/generators/file-generators/sw-injector.js +82 -111
  76. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -1
  77. package/dist/lib/generators/file-generators/sw-push.js +67 -0
  78. package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
  79. package/dist/lib/generators/file-generators/sw-template.js +3 -1
  80. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  81. package/dist/lib/generators/file-generators/type-definitions.js +45 -22
  82. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  83. package/dist/lib/generators/sw-generator.js +27 -19
  84. package/dist/lib/generators/sw-generator.js.map +1 -1
  85. package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
  86. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  87. package/dist/lib/generators/sw-sections/assemble-sw.js +36 -12
  88. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  89. package/dist/lib/generators/sw-sections/config-header.js +1 -1
  90. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  91. package/dist/lib/generators/sw-sections/default-template.js +1 -30
  92. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  93. package/dist/lib/generators/sw-sections/fetch-handler.js +176 -90
  94. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  95. package/dist/lib/generators/sw-sections/install-handler.js +12 -10
  96. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  97. package/dist/lib/generators/sw-sections/tag-management.js +36 -5
  98. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  99. package/dist/lib/generators/swoff-files-generator.js +14 -12
  100. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  101. package/dist/lib/shared/config-types.js +26 -9
  102. package/dist/lib/shared/config-types.js.map +1 -1
  103. package/dist/lib/utils/build-script.js +35 -0
  104. package/dist/lib/utils/build-script.js.map +1 -0
  105. package/package.json +3 -2
  106. package/templates/hooks/useAuth.jsx +30 -0
  107. package/templates/hooks/useAuth.tsx +30 -0
  108. package/templates/hooks/useBackgroundSync.jsx +31 -0
  109. package/templates/hooks/useBackgroundSync.tsx +31 -0
  110. package/templates/hooks/useCacheInvalidation.jsx +19 -0
  111. package/templates/hooks/useCacheInvalidation.tsx +19 -0
  112. package/templates/hooks/useCachedFetch.jsx +55 -0
  113. package/templates/hooks/useCachedFetch.tsx +69 -0
  114. package/templates/hooks/useMutationQueue.jsx +34 -0
  115. package/templates/hooks/useMutationQueue.tsx +34 -0
  116. package/templates/hooks/useNetworkStatus.jsx +19 -0
  117. package/templates/hooks/useNetworkStatus.tsx +19 -0
  118. package/templates/hooks/usePushSubscription.jsx +67 -0
  119. package/templates/hooks/usePushSubscription.tsx +67 -0
  120. package/templates/hooks/useSWUpdate.jsx +76 -0
  121. package/templates/hooks/useSWUpdate.tsx +76 -0
package/README.md CHANGED
@@ -2,55 +2,641 @@
2
2
 
3
3
  CLI for [Swoff](https://swoff.netlify.app) — offline-first web apps made easy.
4
4
 
5
+ Swoff generates a **service worker** and **client-side utilities** that give your web app:
6
+
7
+ - **Offline support** — cached API responses work without a connection
8
+ - **Mutation queue** — writes performed offline replay when back online
9
+ - **Auth** — token management with automatic 401 handling
10
+ - **Cache invalidation** — tag-based cache busting after mutations
11
+ - **PWA** — install prompt and manifest
12
+ - **Push notifications** — subscription management with IndexedDB persistence and VAPID support
13
+ - **Cross-tab sync** — broadcast changes across open tabs
14
+ - **Background Sync API** — process mutations even after tab close
15
+
16
+ It lives in your project as a `swoff/` directory — everything is generated, auditable, and editable.
17
+
18
+ Source: [github.com/iamsuudi/swoff](https://github.com/iamsuudi/swoff)
19
+
20
+ ---
21
+
5
22
  ## Quick Start
6
23
 
7
24
  ```bash
25
+ # Initialize configuration (creates swoff.config.json)
8
26
  npx @swoff/cli init
27
+
28
+ # Generate service worker and supporting files
9
29
  npx @swoff/cli generate
10
- ```
11
30
 
12
- Import the generated SW injector in your app entry point:
31
+ # Import the generated entry point in your app
32
+ ```
13
33
 
14
34
  ```js
15
- import { initServiceWorker } from './swoff/sw-injector.js';
35
+ import { initServiceWorker } from "./swoff/client-injector.js";
16
36
 
17
37
  initServiceWorker();
18
38
  ```
19
39
 
40
+ Then build your app:
41
+
42
+ ```bash
43
+ npm run build
44
+ ```
45
+
46
+ The build script auto-appends `node swoff/sw/generator.js` to finalize the service worker with your asset hashes.
47
+
48
+ ---
49
+
20
50
  ## Commands
21
51
 
22
52
  | Command | Description |
23
53
  |---------|-------------|
24
- | `init` | Create `swoff.config.json` and `swoff/` directory |
25
- | `generate` | Generate service worker and supporting files |
26
- | `validate` | Validate `swoff.config.json` |
54
+ | `init` | Create `swoff.config.json` with auto-detected framework and language |
55
+ | `generate` | Generate service worker and all supporting files |
27
56
  | `add <feature>` | Enable a feature and regenerate |
28
- | `info` | Show configuration summary and generated files |
29
- | `clean` | Remove all Swoff files (swoff/, config, version.json) |
30
- | `help` | Show help information |
57
+ | `validate` | Validate `swoff.config.json` |
58
+ | `info [feature]` | Show summary or per-feature details |
59
+ | `clean` | Remove all Swoff files (`swoff/`, config, `version.json`) |
60
+ | `help [command]` | Show help for a specific command |
61
+
62
+ ### `init`
63
+
64
+ Creates `swoff.config.json` in your project root. Auto-detects:
65
+
66
+ - **Framework**: react, vue, svelte, or vanilla
67
+ - **Language**: TypeScript or JavaScript
68
+ - **Build tool**: Uses existing `package.json` build script
69
+
70
+ ```bash
71
+ swoff init
72
+ swoff init --framework react # override detection
73
+ ```
74
+
75
+ ### `generate`
76
+
77
+ Generates the service worker (`dist/sw-<version>.js`) and all supporting files into `swoff/`. Also auto-appends the SW generator to your `package.json` build script.
78
+
79
+ ```bash
80
+ swoff generate
81
+ swoff generate --sw-only # regenerate only the service worker
82
+ swoff generate --files-only # regenerate only supporting files
83
+ ```
84
+
85
+ After generation, read `swoff/GUIDE.md` for the full integration guide for your project.
86
+
87
+ ### `add <feature>`
88
+
89
+ Enables a feature in `swoff.config.json` and regenerates.
90
+
91
+ ```bash
92
+ swoff add pwa
93
+ swoff add mutation-queue
94
+ swoff add auth
95
+ swoff add tag-invalidation
96
+ swoff add cross-tab
97
+ swoff add background-sync
98
+ swoff add push-notification
99
+ ```
100
+
101
+ ### `info`
102
+
103
+ Shows enabled features and file count, or detailed docs for a specific feature.
104
+
105
+ ```bash
106
+ swoff info # summary
107
+ swoff info auth # auth details and functions
108
+ swoff info mutation-queue # mutation queue details
109
+ ```
110
+
111
+ ### `clean`
112
+
113
+ Removes every generated file:
114
+
115
+ ```bash
116
+ swoff clean
117
+ ```
118
+
119
+ This deletes `swoff/`, `swoff.config.json`, `version.json`, and removes the SW generator from your build script.
120
+
121
+ ---
122
+
123
+ ## Configuration (`swoff.config.json`)
124
+
125
+ ```json
126
+ {
127
+ "$schema": "https://swoff.netlify.app/schema/v1.json",
128
+ "enabled": true,
129
+ "framework": "react",
130
+ "build": {
131
+ "outputDir": "dist",
132
+ "swFilename": "sw"
133
+ },
134
+ "features": {
135
+ "pwa": {
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"
164
+ },
165
+ "mutationQueue": false,
166
+ "backgroundSync": false,
167
+ "auth": {
168
+ "enabled": false,
169
+ "type": "bearer",
170
+ "refreshPath": "/api/refresh",
171
+ "userEndpoint": "/api/me"
172
+ },
173
+ "crossTabSync": true,
174
+ "tagInvalidation": true,
175
+ "pushNotifications": {
176
+ "enabled": false,
177
+ "vapidPublicKey": ""
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ | Field | Type | Default | Description |
184
+ |-------|------|---------|-------------|
185
+ | `$schema` | `string` | — | JSON Schema URL |
186
+ | `enabled` | `boolean` | `true` | Enable Swoff |
187
+ | `framework` | `"react"` \| `"vue"` \| `"svelte"` \| `"vanilla"` | auto | Your UI framework |
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 |
208
+ | `features.mutationQueue` | `boolean` | `false` | Offline write queue |
209
+ | `features.backgroundSync` | `boolean` | `false` | Background Sync API |
210
+ | `features.auth.enabled` | `boolean` | `false` | Auth module |
211
+ | `features.auth.type` | `"bearer"` \| `"cookie"` \| `"custom"` | `"bearer"` | Auth strategy |
212
+ | `features.auth.refreshPath` | `string` | `"/api/refresh"` | Token refresh endpoint |
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) |
218
+
219
+ ---
220
+
221
+ ## Generated Files
222
+
223
+ ```
224
+ swoff/
225
+ ├── client-injector.ts # Orchestrator — wires all features together
226
+ │ Imports: sw/injector, pwa/install, mutation-queue
227
+ │ Exports: initServiceWorker()
228
+ │ Always generated
229
+
230
+ ├── fetch-wrapper.ts # Unified fetch with caching, auth, offline queue
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
238
+
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()
260
+ │ ├── user.ts # User data caching
261
+ │ │ Exports: fetchCurrentUser(), getCachedUser(), cacheUser(), clearCachedUser()
262
+ │ └── state.ts # Online/offline × auth state detection
263
+ │ Exports: getAuthState()
264
+
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
273
+
274
+ ├── pwa/
275
+ │ └── install.ts # PWA install prompt handling
276
+ │ Exports: setupPwaInstall(), isInstallable(), promptInstall()
277
+ │ Generated when: pwa.enabled is true
278
+
279
+ ├── sw/
280
+ │ ├── template.js # Service worker source (runs in SW scope)
281
+ │ │ # Implements all 5 caching strategies in the SW
282
+ │ ├── injector.ts # SW registration logic
283
+ │ │ # Exports: initServiceWorker(), handleUpdateApproved(), skipWaiting()
284
+ │ └── generator.js # Build-time script — embeds asset hashes into SW
285
+
286
+ ├── manifest.json
287
+ ├── GUIDE.md # Full integration walkthrough
288
+ └── README.md # Quick reference
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Cache Strategies
294
+
295
+ The service worker applies a caching strategy to GET/HEAD requests based on `features.serviceWorker.cacheStrategy`:
296
+
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. |
301
+
302
+ ### Strategy resolution (3 tiers, highest to lowest priority)
303
+
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:
321
+
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.
330
+
331
+ ---
332
+
333
+ ## fetchWithCache API
334
+
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.
336
+
337
+ ```js
338
+ import { fetchWithCache } from "./swoff/fetch-wrapper.js";
339
+
340
+ // Read — cached by the SW for offline access
341
+ const { response, fromCache } = await fetchWithCache("/api/todos");
342
+ const data = await response.json();
343
+
344
+ // Mutation — passes through, auto-invalidates cache tags
345
+ await fetchWithCache("/api/todos", {
346
+ method: "POST",
347
+ body: JSON.stringify({ title: "New task" }),
348
+ });
349
+
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:
31
373
 
32
- ### Generate Options
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) |
33
383
 
34
- | Flag | Description |
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 }
398
+ ```
399
+
400
+ - `response`: the fetch Response (from cache or network)
401
+ - `fromCache`: `true` when the response was served from cache
402
+
403
+ In TypeScript, use the generic variant for typed responses:
404
+
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 }`:
419
+
420
+ ```js
421
+ import { fetchWithCache } from "./swoff/fetch-wrapper.js";
422
+
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();
426
+
427
+ // Mutations too
428
+ await fetchWithCache("/api/todos", {
429
+ method: "POST",
430
+ auth: true,
431
+ body: JSON.stringify({ title: "New" }),
432
+ });
433
+ ```
434
+
435
+ **What `auth: true` does:**
436
+
437
+ | Step | What happens |
35
438
  |------|-------------|
36
- | `--sw-only` | Regenerate only the service worker |
37
- | `--files-only` | Regenerate only supporting files (sw-template, sw-injector, etc.) |
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 |
38
444
 
39
- ### Available Features
445
+ **Returns:** `{ response: Response, fromCache: boolean }` — same as any `fetchWithCache` call
40
446
 
41
- | Feature | Description |
42
- |---------|-------------|
43
- | `mutation-queue` | Queue offline writes and sync when back online |
44
- | `pwa` | PWA installability (install prompt, manifest) |
45
- | `cross-tab` | Cross-tab cache invalidation sync |
46
- | `auth` | Auth integration (cookie/bearer/custom) |
47
- | `tag-invalidation` | Tag-based cache invalidation |
48
- | `background-sync` | Background Sync API (Chrome/Edge) |
49
- | `client-registration` | Client registration and lifecycle tracking |
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`.
500
+
501
+ ---
502
+
503
+ ## Cache Invalidation
504
+
505
+ Tag-based invalidation keeps the service worker cache fresh after mutations.
506
+
507
+ ```js
508
+ import { generateTags, invalidateUrl } from "./swoff/invalidation-tags.js";
509
+
510
+ // Tag reads automatically
511
+ const data = await fetchWithCache("/api/todos", {
512
+ tags: generateTags("/api/todos"),
513
+ });
514
+
515
+ // Invalidate after writing
516
+ await invalidateUrl("/api/todos/42");
517
+ ```
518
+
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.
520
+
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
533
+
534
+ Install prompt handling and manifest generation.
535
+
536
+ ```js
537
+ import { isInstallable, promptInstall } from "./swoff/pwa/install.js";
538
+
539
+ // Show install button conditionally
540
+ if (isInstallable()) {
541
+ const { outcome } = await promptInstall();
542
+ }
543
+ ```
544
+
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
556
+
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:
590
+
591
+ | Hook | Returns | Description |
592
+ |------|---------|-------------|
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 |
602
+
603
+ ```tsx
604
+ import { useCachedFetch } from "./swoff/hooks/useCachedFetch.tsx";
605
+
606
+ function Todos() {
607
+ const { data, loading } = useCachedFetch("/api/todos");
608
+ if (loading) return <Spinner />;
609
+ return <TodoList data={data} />;
610
+ }
611
+ ```
612
+
613
+ ---
614
+
615
+ ## Build Script
616
+
617
+ After `swoff generate`, your `package.json` build script is updated to:
618
+
619
+ ```json
620
+ "build": "vite build && node swoff/sw/generator.js"
621
+ ```
622
+
623
+ The generator (`sw/generator.js`) runs after every build:
624
+
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`
630
+
631
+ Running `swoff clean` removes the generator suffix from your build script.
632
+
633
+ ---
50
634
 
51
635
  ## Requirements
52
636
 
53
- Node.js >= 18
637
+ - Node.js >= 18
638
+ - A build tool with a `package.json` build script (Vite, Webpack, etc.)
639
+ - HTTPS or localhost (required for service workers)
54
640
 
55
641
  ## License
56
642