@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
@@ -4,14 +4,22 @@
4
4
  import { writeFile } from "./context.js";
5
5
  export function generateMutationQueue(ctx) {
6
6
  const ext = ctx.ext;
7
+ const ts = ext === "ts";
8
+ const T = (type) => (ts ? `: ${type}` : "");
9
+ const R = (type) => (ts ? `: ${type} ` : " ");
10
+ const PT = (type) => (ts ? `<${type}>` : "");
11
+ const AS = (type) => (ts ? ` as ${type}` : "");
7
12
  const authEnabled = ctx.config.features.auth.enabled;
8
13
  const importLines = authEnabled
9
- ? `import { getAuth } from "./auth-store.${ext}";
14
+ ? `import { getAuth } from "./auth/store.${ext}";
10
15
  `
11
16
  : "";
17
+ const additionalImports = `import { invalidateByTags } from "./cache.${ext}";
18
+ ${ts ? `import type { MutationQueueItem } from "./swoff.d.ts";
19
+ ` : ""}`;
12
20
  const authReplayBlock = authEnabled
13
21
  ? ` const auth = await getAuth();
14
- const authHeader = auth?.token ? { Authorization: \`Bearer \${auth.token}\` } : {};
22
+ const authHeader${T("Record<string, string>")} = auth?.token ? { Authorization: \`Bearer \${auth.token}\` } : {};
15
23
  `
16
24
  : "";
17
25
  const authHeadersSpread = authEnabled
@@ -30,8 +38,6 @@ export function generateMutationQueue(ctx) {
30
38
  * url: "/api/todos",
31
39
  * body: { title: "Grocery" },
32
40
  * tags: ["todos"],
33
- * storeName: "todos",
34
- * tempId: "temp_abc123",
35
41
  * });
36
42
  *
37
43
  * // Flush queued mutations (e.g., after login)
@@ -40,47 +46,57 @@ export function generateMutationQueue(ctx) {
40
46
  * // Auto-processes on online event
41
47
  */
42
48
 
43
- ${importLines}const DB_NAME = "swoff-queue";
49
+ ${importLines}${additionalImports}const DB_NAME = "swoff-queue";
44
50
  const STORE_NAME = "mutations";
45
51
  const MAX_RETRIES = 5;
46
52
 
47
- function openQueueDB() {
48
- return new Promise((resolve, reject) => {
53
+ function openQueueDB()${R("Promise<IDBDatabase>")}{
54
+ return new Promise${PT("IDBDatabase")}((resolve, reject) => {
49
55
  const request = indexedDB.open(DB_NAME, 1);
50
56
  request.onupgradeneeded = (e) => {
51
- const db = e.target.result;
57
+ const db = (e.target${AS("IDBOpenDBRequest")}).result;
52
58
  if (!db.objectStoreNames.contains(STORE_NAME)) {
53
59
  const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
54
60
  store.createIndex("by-timestamp", "timestamp");
55
61
  }
56
62
  };
57
- request.onsuccess = (e) => resolve(e.target.result);
58
- request.onerror = (e) => reject(e.target.error);
63
+ request.onsuccess = (e) => resolve((e.target${AS("IDBOpenDBRequest")}).result);
64
+ request.onerror = (e) => reject((e.target${AS("IDBRequest")}).error);
59
65
  });
60
66
  }
61
67
 
62
68
  let isSyncing = false;
63
69
 
64
- export async function queueMutation(mutation) {
70
+ /** Store a write operation in IndexedDB for later sync. Works offline — use it for POST/PUT/PATCH/DELETE when the user might be offline. */
71
+ export async function queueMutation(mutation${T("Partial<MutationQueueItem>")})${R("Promise<void>")}{
65
72
  const db = await openQueueDB();
66
73
  const tx = db.transaction(STORE_NAME, "readwrite");
67
74
  const store = tx.objectStore(STORE_NAME);
68
75
 
76
+ let body = mutation.body;
77
+ let bodyType = "json";
78
+ if (body instanceof FormData) {
79
+ bodyType = "formdata";
80
+ body = [...body.entries()];
81
+ } else if (body instanceof Blob) {
82
+ bodyType = "blob";
83
+ } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
84
+ bodyType = "buffer";
85
+ }
86
+
69
87
  store.add({
70
88
  id: crypto.randomUUID(),
71
89
  method: mutation.method,
72
90
  url: mutation.url,
73
- body: mutation.body,
91
+ body,
92
+ bodyType,
74
93
  headers: mutation.headers || {},
75
- previousData: mutation.previousData || null,
76
94
  timestamp: Date.now(),
77
95
  retryCount: 0,
78
96
  tags: mutation.tags || [],
79
- storeName: mutation.storeName || null,
80
- tempId: mutation.tempId || null,
81
97
  });
82
98
 
83
- await new Promise((resolve, reject) => {
99
+ await new Promise<void>((resolve, reject) => {
84
100
  tx.oncomplete = () => resolve();
85
101
  tx.onerror = () => reject(tx.error);
86
102
  });
@@ -88,7 +104,8 @@ export async function queueMutation(mutation) {
88
104
  window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
89
105
  }
90
106
 
91
- export async function processMutationQueue() {
107
+ /** Process all queued mutations in order. Sends them to the server. Runs automatically on the online event. */
108
+ export async function processMutationQueue()${R("Promise<void>")}{
92
109
  if (!navigator.onLine || isSyncing) return;
93
110
  isSyncing = true;
94
111
 
@@ -97,7 +114,7 @@ ${authReplayBlock} const db = await openQueueDB();
97
114
  const tx = db.transaction(STORE_NAME, "readonly");
98
115
  const store = tx.objectStore(STORE_NAME);
99
116
  const index = store.index("by-timestamp");
100
- const queue = await new Promise((resolve, reject) => {
117
+ const queue${T("MutationQueueItem[]")} = await new Promise${PT("MutationQueueItem[]")}((resolve, reject) => {
101
118
  const request = index.getAll();
102
119
  request.onsuccess = () => resolve(request.result);
103
120
  request.onerror = () => reject(request.error);
@@ -108,32 +125,40 @@ ${authReplayBlock} const db = await openQueueDB();
108
125
 
109
126
  for (const item of queue) {
110
127
  if (item.retryCount >= MAX_RETRIES) {
111
- await rollbackMutation(item);
112
128
  await removeFromQueue(item.id);
113
129
  failed++;
114
130
  continue;
115
131
  }
116
132
 
117
133
  try {
134
+ let replayBody${T("BodyInit | null")}${ts ? " = null" : ""};
135
+ let contentType${T("string | undefined")};
136
+ const bt = item.bodyType || "json";
137
+ if (bt === "formdata") {
138
+ replayBody = new FormData();
139
+ for (const [key, value] of (item.body || [])${AS("[string, FormDataEntryValue][]")}) {
140
+ replayBody.append(key, value);
141
+ }
142
+ } else if (bt === "blob") {
143
+ replayBody = item.body${AS("BodyInit | null")};
144
+ } else if (bt === "buffer") {
145
+ replayBody = item.body instanceof ArrayBuffer ? new Uint8Array(item.body)${AS("BodyInit")} : item.body${AS("BodyInit")};
146
+ } else {
147
+ replayBody = JSON.stringify(item.body);
148
+ contentType = "application/json";
149
+ }
118
150
  const response = await fetch(item.url, {
119
151
  method: item.method,
120
152
  headers: {
121
- "Content-Type": "application/json",
153
+ ...(contentType ? { "Content-Type": contentType } : {}),
122
154
  ${authHeadersSpread} ...item.headers,
123
155
  },
124
- body: JSON.stringify(item.body),
156
+ body: replayBody,
125
157
  });
126
158
 
127
159
  if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
128
160
 
129
- const serverData = await response.json();
130
-
131
- if (item.tempId && item.storeName) {
132
- await reconcileRecord(item.storeName, item.tempId, serverData);
133
- }
134
-
135
161
  if (item.tags && item.tags.length > 0) {
136
- const { invalidateByTags } = await import("./cache.${ext}");
137
162
  await invalidateByTags(item.tags);
138
163
  }
139
164
 
@@ -157,88 +182,41 @@ ${authHeadersSpread} ...item.headers,
157
182
  }
158
183
  }
159
184
 
160
- export async function flushMutations() {
185
+ /** Immediately process all queued mutations. Call this after re-login to flush mutations that failed due to auth expiry. */
186
+ export async function flushMutations()${R("Promise<void>")}{
161
187
  await processMutationQueue();
162
188
  }
163
189
  `;
164
- // Helper functions (unchanged) are appended outside the template string
190
+ // Helper functions appended outside the template string
165
191
  const helpers = `
166
- async function removeFromQueue(id) {
192
+ async function removeFromQueue(id${T("string")})${R("Promise<void>")}{
167
193
  const db = await openQueueDB();
168
194
  const tx = db.transaction(STORE_NAME, "readwrite");
169
195
  tx.objectStore(STORE_NAME).delete(id);
170
- await new Promise((resolve, reject) => {
196
+ await new Promise${PT("void")}((resolve, reject) => {
171
197
  tx.oncomplete = () => resolve();
172
198
  tx.onerror = () => reject(tx.error);
173
199
  });
174
200
  }
175
201
 
176
- async function updateInQueue(item) {
202
+ async function updateInQueue(item${T("MutationQueueItem")})${R("Promise<void>")}{
177
203
  const db = await openQueueDB();
178
204
  const tx = db.transaction(STORE_NAME, "readwrite");
179
205
  tx.objectStore(STORE_NAME).put(item);
180
- await new Promise((resolve, reject) => {
206
+ await new Promise${PT("void")}((resolve, reject) => {
181
207
  tx.oncomplete = () => resolve();
182
208
  tx.onerror = () => reject(tx.error);
183
209
  });
184
210
  }
185
211
 
186
- async function rollbackMutation(item) {
187
- if (!item.storeName) return;
188
-
189
- if (item.method === "POST" && item.tempId) {
190
- const { deleteRecord } = await import("./store.${ext}");
191
- await deleteRecord(item.storeName, item.tempId);
192
- } else if (
193
- (item.method === "PUT" || item.method === "PATCH") &&
194
- item.previousData
195
- ) {
196
- const { putRecord } = await import("./store.${ext}");
197
- await putRecord(item.storeName, { ...item.previousData, $synced: true });
198
- } else if (item.method === "DELETE" && item.tempId && item.previousData) {
199
- const { putRecord } = await import("./store.${ext}");
200
- await putRecord(item.storeName, { ...item.previousData, $synced: true });
201
- }
202
-
203
- window.dispatchEvent(
204
- new CustomEvent("mutation-rollback", {
205
- detail: {
206
- method: item.method,
207
- url: item.url,
208
- tempId: item.tempId,
209
- previousData: item.previousData,
210
- },
211
- })
212
- );
213
- }
214
-
215
- async function reconcileRecord(storeName, tempId, serverData) {
216
- const { getRecord, putRecord, deleteRecord } = await import("./store.${ext}");
217
- const existing = await getRecord(storeName, tempId);
218
- if (!existing) return;
219
-
220
- const reconciled = {
221
- ...existing,
222
- ...serverData,
223
- id: serverData.id,
224
- $synced: true,
225
- $syncedAt: Date.now(),
226
- };
227
-
228
- await putRecord(storeName, reconciled);
229
-
230
- if (String(tempId) !== String(serverData.id)) {
231
- await deleteRecord(storeName, tempId);
232
- }
233
- }
234
-
235
- export async function getPendingCount() {
212
+ /** Get the number of queued mutations waiting to be synced. Useful for showing a sync badge. */
213
+ export async function getPendingCount()${R("Promise<number>")}{
236
214
  const db = await openQueueDB();
237
- return new Promise((resolve, reject) => {
215
+ return new Promise${PT("number")}((resolve, reject) => {
238
216
  const tx = db.transaction(STORE_NAME, "readonly");
239
217
  const request = tx.objectStore(STORE_NAME).count();
240
- request.onsuccess = () => resolve(request.result);
241
- request.onerror = () => reject(request.error);
218
+ request.onsuccess = () => resolve((request${AS("IDBRequest<number>")}).result);
219
+ request.onerror = () => reject((request${AS("IDBRequest")}).error);
242
220
  });
243
221
  }
244
222
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,qBAAqB,CAAC,GAAqB;IACzD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;IAErD,MAAM,WAAW,GAAG,WAAW;QAC7B,CAAC,CAAC,yCAAyC,GAAG;CACjD;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,eAAe,GAAG,WAAW;QACjC,CAAC,CAAC;;CAEL;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,iBAAiB,GAAG,WAAW;QACnC,CAAC,CAAC,4BAA4B;QAC9B,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,IAAI,GAAG;;;;;oHAKqG,GAAG;;;;;;;;;;;;;;;;;;EAkBrH,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqDX,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;EA0Bf,iBAAiB;;;;;;;;;;;;;;+DAc4C,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BjE,CAAC;IAEA,wEAAwE;IACxE,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;qDAyBmC,GAAG;;;;;;kDAMN,GAAG;;;kDAGH,GAAG;;;;;;;;;;;;;;;;;yEAiBoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4B3E,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,kBAAkB,GAAG,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;AAC1D,CAAC"}
1
+ {"version":3,"file":"mutation-queue.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-queue.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,qBAAqB,CAAC,GAAqB;IACzD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACvD,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;IAErD,MAAM,WAAW,GAAG,WAAW;QAC7B,CAAC,CAAC,yCAAyC,GAAG;CACjD;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,iBAAiB,GAAG,6CAA6C,GAAG;EAC1E,EAAE,CAAC,CAAC,CAAC;CACN,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAEP,MAAM,eAAe,GAAG,WAAW;QACjC,CAAC,CAAC;oBACc,CAAC,CAAC,wBAAwB,CAAC;CAC9C;QACG,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,iBAAiB,GAAG,WAAW;QACnC,CAAC,CAAC,4BAA4B;QAC9B,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,IAAI,GAAG;;;;;oHAKqG,GAAG;;;;;;;;;;;;;;;;EAgBrH,WAAW,GAAG,iBAAiB;;;;wBAIT,CAAC,CAAC,sBAAsB,CAAC;sBAC3B,EAAE,CAAC,aAAa,CAAC;;;4BAGX,EAAE,CAAC,kBAAkB,CAAC;;;;;;kDAMA,EAAE,CAAC,kBAAkB,CAAC;+CACzB,EAAE,CAAC,YAAY,CAAC;;;;;;;8CAOjB,CAAC,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8CAqCrD,CAAC,CAAC,eAAe,CAAC;;;;;EAK9D,eAAe;;;;iBAIA,CAAC,CAAC,qBAAqB,CAAC,uBAAuB,EAAE,CAAC,qBAAqB,CAAC;;;;;;;;;;;;;;;;;wBAiBjE,CAAC,CAAC,iBAAiB,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;yBACzC,CAAC,CAAC,oBAAoB,CAAC;;;;wDAIQ,EAAE,CAAC,gCAAgC,CAAC;;;;kCAI1D,EAAE,CAAC,iBAAiB,CAAC;;qFAE8B,EAAE,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,UAAU,CAAC;;;;;;;;;EAS9H,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wCAgCqB,CAAC,CAAC,eAAe,CAAC;;;CAGzD,CAAC;IAEA,wDAAwD;IACxD,MAAM,OAAO,GAAG;mCACiB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;qBAI/C,EAAE,CAAC,MAAM,CAAC;;;;;;mCAMI,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;qBAI1D,EAAE,CAAC,MAAM,CAAC;;;;;;;yCAOU,CAAC,CAAC,iBAAiB,CAAC;;sBAEvC,EAAE,CAAC,QAAQ,CAAC;;;gDAGc,EAAE,CAAC,oBAAoB,CAAC;6CAC3B,EAAE,CAAC,YAAY,CAAC;;;CAG5D,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,kBAAkB,GAAG,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Generates mutation-reconcile.js - ID reconciliation after mutation sync.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateMutationReconcile(ctx) {
6
+ const ext = ctx.ext;
7
+ const ts = ext === "ts";
8
+ const T = (type) => (ts ? `: ${type}` : "");
9
+ const R = (type) => (ts ? `: ${type} ` : " ");
10
+ const AS = (type) => (ts ? ` as ${type}` : "");
11
+ const code = `/**
12
+ * Swoff Mutation Reconciliation
13
+ * Reconciles local optimistic records with server response after a queued
14
+ * mutation is processed by the service worker.
15
+ *
16
+ * Usage:
17
+ * import { reconcileRecord } from './swoff/mutation-reconcile.${ext}';
18
+ *
19
+ * // Called automatically by mutation-queue after a successful sync
20
+ * await reconcileRecord('todos', 'temp_abc123', serverData);
21
+ *
22
+ * // Only override this if your data has foreign key references:
23
+ * // export async function reconcileReferences(...) { ... }
24
+ */
25
+
26
+ import { getRecord, putRecord, deleteRecord } from './store.${ext}';
27
+
28
+ /** Replace a temporary optimistic ID with the server-assigned ID after mutation sync. Called automatically by mutation-queue. */
29
+ export async function reconcileRecord(storeName${T("string")}, tempId${T("string")}, serverData${T("Record<string, unknown>")})${R("Promise<void>")}{
30
+ const existing = await getRecord(storeName, tempId);
31
+ if (!existing) return;
32
+
33
+ const reconciled = {
34
+ ...existing,
35
+ ...serverData,
36
+ id: serverData.id,
37
+ $synced: true,
38
+ $syncedAt: Date.now(),
39
+ };
40
+
41
+ await putRecord(storeName, reconciled);
42
+
43
+ if (String(tempId) !== String(serverData.id)) {
44
+ await deleteRecord(storeName, tempId);
45
+ }
46
+
47
+ await reconcileReferences(storeName, tempId, serverData.id${AS("string | number")});
48
+ }
49
+
50
+ /** Update foreign key references in other records that pointed to the old temp ID. Override this for your app's schema relationships. */
51
+ export async function reconcileReferences(_storeName${T("string")}, _oldId${T("string")}, _newId${T("string | number")})${R("Promise<void>")}{
52
+ // Override this for your app's schema.
53
+ }
54
+ `;
55
+ writeFile(ctx, `mutation-reconcile.${ext}`, code);
56
+ }
57
+ //# sourceMappingURL=mutation-reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutation-reconcile.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/mutation-reconcile.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,yBAAyB,CAAC,GAAqB;IAC7D,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAEvD,MAAM,IAAI,GAAG;;;;;;mEAMoD,GAAG;;;;;;;;;8DASR,GAAG;;;iDAGhB,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;;;;8DAkBrF,EAAE,CAAC,iBAAiB,CAAC;;;;sDAI7B,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC;;;CAG3I,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,sBAAsB,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Generates push.{js|ts} - push notification subscription management.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generatePush(ctx) {
6
+ const ext = ctx.ext;
7
+ const ts = ext === "ts";
8
+ const T = (type) => (ts ? `: ${type}` : "");
9
+ const R = (type) => (ts ? `: ${type} ` : " ");
10
+ const PT = (type) => (ts ? `<${type}>` : "");
11
+ const AS = (type) => (ts ? ` as ${type}` : "");
12
+ const code = `/**
13
+ * Swoff Push Notifications
14
+ * Push subscription management with IndexedDB persistence.
15
+ *
16
+ * Usage:
17
+ * import { subscribeToPush, unsubscribeFromPush, isSubscribed } from './swoff/push.${ext}';
18
+ *
19
+ * // Enable (triggers permission prompt)
20
+ * const subscription = await subscribeToPush("YOUR_VAPID_PUBLIC_KEY");
21
+ * await fetch("/api/push/subscribe", {
22
+ * method: "POST",
23
+ * body: JSON.stringify(subscription.toJSON()),
24
+ * });
25
+ *
26
+ * // Disable
27
+ * await unsubscribeFromPush();
28
+ *
29
+ * Window events:
30
+ * push-permission-changed - Permission granted/denied (detail: { permission })
31
+ * push-subscription-changed - Subscribed/unsubscribed (detail: { subscribed })
32
+ */
33
+
34
+ const SUBSCRIPTION_DB = "swoff-push";
35
+ const SUBSCRIPTION_STORE = "subscription";
36
+
37
+ let permissionState = Notification.permission;
38
+
39
+ function openPushDB()${R("Promise<IDBDatabase>")}{
40
+ return new Promise${PT("IDBDatabase")}((resolve, reject) => {
41
+ const request = indexedDB.open(SUBSCRIPTION_DB, 1);
42
+ request.onupgradeneeded = (e) => {
43
+ const db = (e.target${AS("IDBOpenDBRequest")}).result;
44
+ if (!db.objectStoreNames.contains(SUBSCRIPTION_STORE)) {
45
+ db.createObjectStore(SUBSCRIPTION_STORE, { keyPath: "id" });
46
+ }
47
+ };
48
+ request.onsuccess = (e) => resolve((e.target${AS("IDBOpenDBRequest")}).result);
49
+ request.onerror = (e) => reject((e.target${AS("IDBRequest")}).error);
50
+ });
51
+ }
52
+
53
+ /** Request notification permission from the user. Returns true if granted. */
54
+ export async function requestNotificationPermission()${R("Promise<boolean>")}{
55
+ if (permissionState === "granted") return true;
56
+ if (permissionState === "denied") return false;
57
+
58
+ const result = await Notification.requestPermission();
59
+ permissionState = result;
60
+ window.dispatchEvent(
61
+ new CustomEvent("push-permission-changed", { detail: { permission: result } }),
62
+ );
63
+ return result === "granted";
64
+ }
65
+
66
+ /** Get the current push subscription, or null if not subscribed. */
67
+ export async function getPushSubscription()${R("Promise<PushSubscription | null>")}{
68
+ try {
69
+ const registration = await navigator.serviceWorker.ready;
70
+ return await registration.pushManager.getSubscription();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /** Subscribe to push notifications. Returns the subscription or null if permission denied. */
77
+ export async function subscribeToPush(vapidPublicKey${T("string")})${R("Promise<PushSubscription | null>")}{
78
+ const granted = await requestNotificationPermission();
79
+ if (!granted) return null;
80
+
81
+ const registration = await navigator.serviceWorker.ready;
82
+ const subscription = await registration.pushManager.subscribe({
83
+ userVisibleOnly: true,
84
+ applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)${AS("BufferSource")},
85
+ });
86
+
87
+ const db = await openPushDB();
88
+ const tx = db.transaction(SUBSCRIPTION_STORE, "readwrite");
89
+ tx.objectStore(SUBSCRIPTION_STORE).put({
90
+ id: "current",
91
+ endpoint: subscription.endpoint,
92
+ keys: subscription.toJSON().keys,
93
+ subscribedAt: Date.now(),
94
+ });
95
+ await new Promise<void>((resolve, reject) => {
96
+ tx.oncomplete = () => resolve();
97
+ tx.onerror = () => reject(tx.error);
98
+ });
99
+
100
+ window.dispatchEvent(
101
+ new CustomEvent("push-subscription-changed", { detail: { subscribed: true } }),
102
+ );
103
+
104
+ return subscription;
105
+ }
106
+
107
+ /** Unsubscribe from push notifications. */
108
+ export async function unsubscribeFromPush()${R("Promise<void>")}{
109
+ const subscription = await getPushSubscription();
110
+ if (!subscription) return;
111
+
112
+ await subscription.unsubscribe();
113
+
114
+ const db = await openPushDB();
115
+ const tx = db.transaction(SUBSCRIPTION_STORE, "readwrite");
116
+ tx.objectStore(SUBSCRIPTION_STORE).delete("current");
117
+ await new Promise<void>((resolve, reject) => {
118
+ tx.oncomplete = () => resolve();
119
+ tx.onerror = () => reject(tx.error);
120
+ });
121
+
122
+ window.dispatchEvent(
123
+ new CustomEvent("push-subscription-changed", { detail: { subscribed: false } }),
124
+ );
125
+ }
126
+
127
+ /** Check if the user is subscribed to push notifications. */
128
+ export async function isSubscribed()${R("Promise<boolean>")}{
129
+ const sub = await getPushSubscription();
130
+ return sub !== null;
131
+ }
132
+
133
+ /** Convert a base64 VAPID public key to a Uint8Array for pushManager.subscribe(). */
134
+ function urlBase64ToUint8Array(base64String${T("string")})${R("Uint8Array")}{
135
+ const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
136
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
137
+ const rawData = atob(base64);
138
+ return Uint8Array.from(rawData, (c) => c.charCodeAt(0));
139
+ }
140
+ `;
141
+ writeFile(ctx, `push.${ext}`, code);
142
+ }
143
+ //# sourceMappingURL=push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/push.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,YAAY,CAAC,GAAqB;IAChD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAEvD,MAAM,IAAI,GAAG;;;;;wFAKyE,GAAG;;;;;;;;;;;;;;;;;;;;;;uBAsBpE,CAAC,CAAC,sBAAsB,CAAC;sBAC1B,EAAE,CAAC,aAAa,CAAC;;;4BAGX,EAAE,CAAC,kBAAkB,CAAC;;;;;kDAKA,EAAE,CAAC,kBAAkB,CAAC;+CACzB,EAAE,CAAC,YAAY,CAAC;;;;;uDAKR,CAAC,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;6CAa/B,CAAC,CAAC,kCAAkC,CAAC;;;;;;;;;;sDAU5B,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,kCAAkC,CAAC;;;;;;;iEAOzC,EAAE,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;6CAwBtC,CAAC,CAAC,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;sCAoBzB,CAAC,CAAC,kBAAkB,CAAC;;;;;;6CAMd,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC;;;;;;CAM1E,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC"}
@@ -4,28 +4,60 @@
4
4
  import { writeFile } from "./context.js";
5
5
  export function generatePwaInstall(ctx) {
6
6
  const ext = ctx.ext;
7
+ const ts = ext === "ts";
8
+ const R = (type) => (ts ? `: ${type} ` : " ");
9
+ const preventDefaultInstall = ctx.config.features.pwa.preventDefaultInstall;
10
+ const preventLine = preventDefaultInstall ? " e.preventDefault();\n" : "";
7
11
  const code = `/**
8
- * Swoff PWA Install Prompt
9
- * Manual install trigger and installability check.
10
- * Event listeners are registered in sw-injector at app entry.
12
+ * Swoff PWA Install Support
13
+ * Manual install trigger, installability check, and event listener setup.
11
14
  *
12
15
  * Usage:
13
- * import { isInstallable, promptInstall } from './swoff/pwa-install.${ext}';
16
+ * import { setupPwaInstall, isInstallable, promptInstall } from './swoff/pwa/install.${ext}';
17
+ *
18
+ * setupPwaInstall(); // sets up beforeinstallprompt and appinstalled listeners
14
19
  *
15
20
  * if (isInstallable()) {
16
21
  * const result = await promptInstall();
17
- * console.log('User choice:', result.outcome);
18
22
  * }
19
23
  *
20
- * Window properties (set by sw-injector):
24
+ * Window properties:
21
25
  * window.deferredInstallPrompt - The captured BeforeInstallPromptEvent
22
26
  */
23
27
 
24
- export function isInstallable() {
28
+ /** Set up beforeinstallprompt and appinstalled event listeners. Call once at app startup. */
29
+ export function setupPwaInstall()${R("void")}{
30
+ window.addEventListener("beforeinstallprompt", (e) => {
31
+ window.deferredInstallPrompt = e as BeforeInstallPromptEvent;
32
+ window.pwaInstallable = true;
33
+
34
+ ${preventLine}
35
+ window.dispatchEvent(
36
+ new CustomEvent("pwa-installable", {
37
+ detail: { isInstallable: true },
38
+ })
39
+ );
40
+ });
41
+
42
+ window.addEventListener("appinstalled", () => {
43
+ window.deferredInstallPrompt = null;
44
+ window.pwaInstallable = false;
45
+
46
+ window.dispatchEvent(
47
+ new CustomEvent("pwa-installed", {
48
+ detail: { outcome: "accepted" },
49
+ })
50
+ );
51
+ });
52
+ }
53
+
54
+ /** Check if the PWA install prompt is available (user has met install criteria). */
55
+ export function isInstallable()${R("boolean")}{
25
56
  return !!window.deferredInstallPrompt;
26
57
  }
27
58
 
28
- export async function promptInstall() {
59
+ /** Show the browser's native install prompt. Throws if prompt is not available. */
60
+ export async function promptInstall()${R("Promise<{ outcome: string }>")}{
29
61
  if (!window.deferredInstallPrompt) {
30
62
  throw new Error("Install prompt not available");
31
63
  }
@@ -53,6 +85,6 @@ export async function promptInstall() {
53
85
  return choice;
54
86
  }
55
87
  `;
56
- writeFile(ctx, `pwa-install.${ext}`, code);
88
+ writeFile(ctx, `pwa/install.${ext}`, code);
57
89
  }
58
90
  //# sourceMappingURL=pwa-install.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pwa-install.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/pwa-install.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,kBAAkB,CAAC,GAAqB;IACtD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IAEpB,MAAM,IAAI,GAAG;;;;;;yEAM0D,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0C3E,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,eAAe,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"pwa-install.js","sourceRoot":"","sources":["../../../../src/lib/generators/file-generators/pwa-install.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,UAAU,kBAAkB,CAAC,GAAqB;IACtD,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IACpB,MAAM,EAAE,GAAG,GAAG,KAAK,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,qBAAqB,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAC5E,MAAM,WAAW,GAAG,qBAAqB,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,EAAE,CAAC;IAE7E,MAAM,IAAI,GAAG;;;;;0FAK2E,GAAG;;;;;;;;;;;;;mCAa1D,CAAC,CAAC,MAAM,CAAC;;;;;EAK1C,WAAW;;;;;;;;;;;;;;;;;;;;;iCAqBoB,CAAC,CAAC,SAAS,CAAC;;;;;uCAKN,CAAC,CAAC,8BAA8B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BvE,CAAC;IAEA,SAAS,CAAC,GAAG,EAAE,eAAe,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Generates README.md — quick reference for integrating enabled features.
3
+ */
4
+ import { writeFile } from "./context.js";
5
+ export function generateReadme(ctx) {
6
+ const { config } = ctx;
7
+ const ext = ctx.ext;
8
+ const lines = [];
9
+ const w = (s) => lines.push(s);
10
+ w("# Swoff — Quick Reference");
11
+ w("");
12
+ w("For the full guide with explanations, read **[GUIDE.md](./GUIDE.md)**.");
13
+ w("");
14
+ const hasClientInjector = true;
15
+ if (hasClientInjector) {
16
+ w("## Entry point");
17
+ w("```ts");
18
+ w(`import { initServiceWorker } from "./swoff/client-injector.${ext}";`);
19
+ w("initServiceWorker();");
20
+ w("```");
21
+ w("");
22
+ }
23
+ w("## API calls (use for all fetch requests)");
24
+ w("Use `fetchWithCache` instead of `fetch()` — it sets caching headers the SW needs.");
25
+ w("Plain `fetch()` skips SW caching when `cacheStrategy` is `\"explicit-only\"`.");
26
+ w("```ts");
27
+ w(`import { fetchWithCache } from "./swoff/fetch-wrapper.${ext}";`);
28
+ w('const data = await fetchWithCache("/api/data").then(r => r.json());');
29
+ w("```");
30
+ w("");
31
+ if (config.features.auth.enabled) {
32
+ w("## Authenticated API calls");
33
+ w("```ts");
34
+ w(`import { fetchWithCache } from "./swoff/fetch-wrapper.${ext}";`);
35
+ w('const data = await fetchWithCache("/api/me", { auth: true }).then(r => r.json());');
36
+ w("```");
37
+ w("");
38
+ }
39
+ if (config.features.mutationQueue) {
40
+ w("## Offline mutations (queue writes when offline)");
41
+ w("```ts");
42
+ w(`import { queueMutation, getPendingCount } from "./swoff/mutation-queue.${ext}";`);
43
+ w("await queueMutation({ method: \"POST\", url: \"/api/todos\", body: {...} });");
44
+ w("```");
45
+ w("");
46
+ if (config.features.auth.enabled) {
47
+ w("## After re-login (flush queued mutations)");
48
+ w("```ts");
49
+ w(`import { flushMutations } from "./swoff/mutation-queue.${ext}";`);
50
+ w("await flushMutations();");
51
+ w("```");
52
+ w("");
53
+ }
54
+ }
55
+ if (config.features.pushNotifications?.enabled) {
56
+ w("## Push notifications");
57
+ w("```ts");
58
+ w(`import { subscribeToPush, unsubscribeFromPush, isSubscribed } from "./swoff/push.${ext}";`);
59
+ w('const sub = await subscribeToPush("YOUR_VAPID_PUBLIC_KEY");');
60
+ w("```");
61
+ w("");
62
+ }
63
+ if (config.features.tagInvalidation) {
64
+ w("## Cache invalidation");
65
+ w("```ts");
66
+ w(`import { generateTags, invalidateUrl } from "./swoff/invalidation-tags.${ext}";`);
67
+ w('const data = await fetchWithCache("/api/todos", { tags: generateTags("/api/todos") });');
68
+ w('await invalidateUrl("/api/todos/42"); // after mutation');
69
+ w("```");
70
+ w("");
71
+ }
72
+ if (ctx.frameworkName === "react") {
73
+ w("## React hook — auto-refetch on cache invalidation");
74
+ w("```tsx");
75
+ w(`import { useCachedFetch } from "./swoff/hooks/useCachedFetch.${ext}x";`);
76
+ w('const { data, error, loading, refetch } = useCachedFetch("/api/todos");');
77
+ w("```");
78
+ w("");
79
+ }
80
+ w("## Build script");
81
+ w("The SW generator learns your build output to precache assets.");
82
+ w("Swoff has already added this to your `package.json`:");
83
+ w("```json");
84
+ w('"build": "<your-build> && node swoff/sw/generator.js"');
85
+ w("```");
86
+ writeFile(ctx, "README.md", lines.join("\n"));
87
+ }
88
+ //# sourceMappingURL=quick-readme.js.map