@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.
- package/README.md +610 -24
- package/bin/swoff +8 -14
- package/dist/__tests__/assemble-sw.test.js +17 -3
- package/dist/__tests__/assemble-sw.test.js.map +1 -1
- package/dist/__tests__/cli-integration.test.js +9 -9
- package/dist/__tests__/cli-integration.test.js.map +1 -1
- package/dist/__tests__/config-types.test.js +10 -9
- package/dist/__tests__/config-types.test.js.map +1 -1
- package/dist/__tests__/file-generators.test.js +65 -68
- package/dist/__tests__/file-generators.test.js.map +1 -1
- package/dist/__tests__/loader.test.js +12 -14
- package/dist/__tests__/loader.test.js.map +1 -1
- package/dist/__tests__/validator.test.js +41 -22
- package/dist/__tests__/validator.test.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/help.js +3 -3
- package/dist/lib/cli/help.js.map +1 -1
- package/dist/lib/commands/add.js +11 -5
- package/dist/lib/commands/add.js.map +1 -1
- package/dist/lib/commands/clean.js +5 -0
- package/dist/lib/commands/clean.js.map +1 -1
- package/dist/lib/commands/generate-guide.js +43 -13
- package/dist/lib/commands/generate-guide.js.map +1 -1
- package/dist/lib/commands/generate.js +7 -3
- package/dist/lib/commands/generate.js.map +1 -1
- package/dist/lib/commands/info.js +149 -32
- 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/commands/validate.js +3 -1
- package/dist/lib/commands/validate.js.map +1 -1
- package/dist/lib/config/validator.js +25 -16
- package/dist/lib/config/validator.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-fetch.js +29 -34
- package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-state.js +8 -5
- package/dist/lib/generators/file-generators/auth-state.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-store.js +181 -41
- package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
- package/dist/lib/generators/file-generators/auth-user.js +28 -32
- package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
- package/dist/lib/generators/file-generators/background-sync.js +9 -5
- package/dist/lib/generators/file-generators/background-sync.js.map +1 -1
- package/dist/lib/generators/file-generators/cache.js +8 -5
- package/dist/lib/generators/file-generators/cache.js.map +1 -1
- package/dist/lib/generators/file-generators/client-injector.js +101 -0
- package/dist/lib/generators/file-generators/client-injector.js.map +1 -0
- package/dist/lib/generators/file-generators/context.js +7 -6
- package/dist/lib/generators/file-generators/context.js.map +1 -1
- package/dist/lib/generators/file-generators/fetch-wrapper.js +196 -41
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
- package/dist/lib/generators/file-generators/generate-hooks.js +23 -151
- package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
- package/dist/lib/generators/file-generators/guide-generator.js +450 -0
- package/dist/lib/generators/file-generators/guide-generator.js.map +1 -0
- package/dist/lib/generators/file-generators/invalidation-tags.js +12 -10
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-queue.js +65 -87
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
- package/dist/lib/generators/file-generators/mutation-reconcile.js +57 -0
- package/dist/lib/generators/file-generators/mutation-reconcile.js.map +1 -0
- 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/pwa-install.js +41 -9
- package/dist/lib/generators/file-generators/pwa-install.js.map +1 -1
- package/dist/lib/generators/file-generators/quick-readme.js +88 -0
- package/dist/lib/generators/file-generators/quick-readme.js.map +1 -0
- package/dist/lib/generators/file-generators/reconcile.js +7 -12
- package/dist/lib/generators/file-generators/reconcile.js.map +1 -1
- package/dist/lib/generators/file-generators/store.js +30 -20
- package/dist/lib/generators/file-generators/store.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-generator-build.js +38 -17
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -1
- package/dist/lib/generators/file-generators/sw-injector.js +82 -111
- package/dist/lib/generators/file-generators/sw-injector.js.map +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 +3 -1
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
- package/dist/lib/generators/file-generators/type-definitions.js +45 -22
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +27 -19
- 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 +36 -12
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
- package/dist/lib/generators/sw-sections/config-header.js +1 -1
- package/dist/lib/generators/sw-sections/config-header.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 +176 -90
- 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 +36 -5
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +14 -12
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +26 -9
- package/dist/lib/shared/config-types.js.map +1 -1
- package/dist/lib/utils/build-script.js +35 -0
- package/dist/lib/utils/build-script.js.map +1 -0
- 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
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
153
|
+
...(contentType ? { "Content-Type": contentType } : {}),
|
|
122
154
|
${authHeadersSpread} ...item.headers,
|
|
123
155
|
},
|
|
124
|
-
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
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
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
|
|
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
|
|
9
|
-
* Manual install trigger and
|
|
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
|
|
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
|
|
24
|
+
* Window properties:
|
|
21
25
|
* window.deferredInstallPrompt - The captured BeforeInstallPromptEvent
|
|
22
26
|
*/
|
|
23
27
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|