@swoff/cli 0.0.2 ā 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/swoff +0 -0
- package/dist/index.js +263 -12
- package/dist/index.js.map +1 -1
- package/dist/lib/generators/sw-generator.js +392 -58
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/swoff-files-generator.js +1137 -810
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/package.json +2 -1
- package/src/lib/templates/sw-template.js +23 -6
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Swoff Files Generator
|
|
3
3
|
*
|
|
4
|
-
* Generates
|
|
5
|
-
*
|
|
4
|
+
* Generates framework-agnostic pattern files based on swoff.config.json features.
|
|
5
|
+
* All files go into swoff/ directory (or public/ for manifest).
|
|
6
6
|
*
|
|
7
7
|
* CLI Usage:
|
|
8
8
|
* node swoff-files-generator.js --project-root <path> --package-dir <path> --language <ts|js> --config-path <path>
|
|
@@ -24,6 +24,7 @@ const defaultConfig = {
|
|
|
24
24
|
version: "from-package",
|
|
25
25
|
minSupportedVersion: "0.0.0",
|
|
26
26
|
serviceWorker: {
|
|
27
|
+
autoRegister: true,
|
|
27
28
|
autoUpdate: false,
|
|
28
29
|
defaultStrategy: "cache-first",
|
|
29
30
|
strategies: {},
|
|
@@ -38,11 +39,19 @@ const defaultConfig = {
|
|
|
38
39
|
crossTabSync: true,
|
|
39
40
|
tagInvalidation: true,
|
|
40
41
|
clientRegistration: true,
|
|
42
|
+
indexeddb: false,
|
|
43
|
+
},
|
|
44
|
+
pwa: {
|
|
45
|
+
preventDefaultInstall: false,
|
|
41
46
|
},
|
|
42
47
|
build: {
|
|
43
48
|
outputDir: "dist",
|
|
44
49
|
swFilename: "sw",
|
|
45
50
|
},
|
|
51
|
+
database: {
|
|
52
|
+
name: "app-db",
|
|
53
|
+
stores: [],
|
|
54
|
+
},
|
|
46
55
|
};
|
|
47
56
|
let config = defaultConfig;
|
|
48
57
|
if (existsSync(configPath)) {
|
|
@@ -57,1004 +66,1322 @@ else {
|
|
|
57
66
|
console.log("No config found, using defaults");
|
|
58
67
|
}
|
|
59
68
|
const ext = language === "ts" ? "ts" : "js";
|
|
69
|
+
const swoffDir = join(projectRoot, "swoff");
|
|
60
70
|
const generatedFiles = [];
|
|
71
|
+
function ensureSwoffDir() {
|
|
72
|
+
if (!existsSync(swoffDir))
|
|
73
|
+
mkdirSync(swoffDir, { recursive: true });
|
|
74
|
+
}
|
|
61
75
|
function generateSwInjector() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
ensureSwoffDir();
|
|
77
|
+
const autoRegister = config.serviceWorker.autoRegister;
|
|
78
|
+
const autoUpdate = config.serviceWorker.autoUpdate;
|
|
79
|
+
const code = `/**
|
|
80
|
+
* Swoff Service Worker Injector
|
|
81
|
+
* Framework-agnostic client registration with version checking.
|
|
82
|
+
*
|
|
83
|
+
* Usage:
|
|
84
|
+
* import { initServiceWorker, shouldRegisterSW } from './swoff/sw-injector.js';
|
|
85
|
+
*
|
|
86
|
+
* // Call in your app entry point (e.g., main.tsx, app.js):
|
|
87
|
+
* if (shouldRegisterSW()) {
|
|
88
|
+
* initServiceWorker();
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* // Or defer until after onboarding:
|
|
92
|
+
* function myShouldRegister() {
|
|
93
|
+
* return localStorage.getItem('onboarding-complete') === 'true';
|
|
94
|
+
* }
|
|
95
|
+
* if (myShouldRegister()) {
|
|
96
|
+
* initServiceWorker();
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* Window events:
|
|
100
|
+
* sw-version-detected - Version info available on window
|
|
101
|
+
* sw-update-available - New version ready for user consent (detail: { version })
|
|
102
|
+
* sw-progress - Download progress (detail: { percent, downloaded, total })
|
|
103
|
+
* sw-ready - SW active and controlling page
|
|
104
|
+
* sw-error - SW registration failed
|
|
105
|
+
*
|
|
106
|
+
* Window properties:
|
|
107
|
+
* window.latestSWVersion - Latest version from version.json
|
|
108
|
+
* window.currentSWVersion - Active SW version
|
|
109
|
+
* window.swAvailableVersion - Pending update version
|
|
110
|
+
* window.swUpdateRequired - Forced update needed (version < minSupportedVersion)
|
|
111
|
+
* window.swMinSupportedVersion - Minimum supported version from version.json
|
|
112
|
+
* window.swReady - SW is active
|
|
113
|
+
* window.swError - Registration failed
|
|
114
|
+
*/
|
|
67
115
|
|
|
68
|
-
|
|
69
|
-
|
|
116
|
+
const AUTO_REGISTER = ${autoRegister};
|
|
117
|
+
const AUTO_UPDATE = ${autoUpdate};
|
|
118
|
+
|
|
119
|
+
async function checkForUpdate() {
|
|
120
|
+
const response = await fetch("/version.json");
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error("Failed to fetch version.json");
|
|
123
|
+
}
|
|
124
|
+
return response.json();
|
|
70
125
|
}
|
|
71
126
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
async function doRegisterServiceWorker(version) {
|
|
128
|
+
const swUrl = \`/sw-v\${version}.js\`;
|
|
129
|
+
const registration = await navigator.serviceWorker.register(swUrl);
|
|
130
|
+
localStorage.setItem("swRegisteredVersion", version);
|
|
131
|
+
window.currentSWVersion = version;
|
|
132
|
+
window.swRegisteredVersion = version;
|
|
133
|
+
window.dispatchEvent(new CustomEvent("sw-version-detected"));
|
|
134
|
+
window.dispatchEvent(new CustomEvent("sw-ready"));
|
|
135
|
+
return registration;
|
|
136
|
+
}
|
|
80
137
|
|
|
81
|
-
|
|
82
|
-
|
|
138
|
+
export function shouldRegisterSW() {
|
|
139
|
+
if (!AUTO_REGISTER) return false;
|
|
83
140
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
141
|
+
// Add custom conditions here. Return false to prevent registration.
|
|
142
|
+
// Examples:
|
|
143
|
+
// - Check if user completed onboarding
|
|
144
|
+
// - Check if user accepted terms
|
|
145
|
+
// - Check if user is on a slow connection
|
|
146
|
+
//
|
|
147
|
+
// if (!localStorage.getItem("onboarding-complete")) return false;
|
|
148
|
+
// if (navigator.connection?.effectiveType === "slow-2g") return false;
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function initServiceWorker() {
|
|
154
|
+
if (!("serviceWorker" in navigator)) {
|
|
155
|
+
console.warn("Service Workers not supported");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
99
158
|
|
|
100
|
-
|
|
101
|
-
|
|
159
|
+
try {
|
|
160
|
+
const manifest = await checkForUpdate();
|
|
161
|
+
const currentVersion = localStorage.getItem("swRegisteredVersion");
|
|
162
|
+
window.latestSWVersion = manifest.version;
|
|
163
|
+
window.swMinSupportedVersion = manifest.minSupportedVersion || "0.0.0";
|
|
164
|
+
|
|
165
|
+
if (currentVersion === manifest.version) {
|
|
166
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
167
|
+
if (registration && registration.active) {
|
|
168
|
+
window.currentSWVersion = currentVersion;
|
|
169
|
+
window.dispatchEvent(new CustomEvent("sw-version-detected"));
|
|
170
|
+
window.dispatchEvent(new CustomEvent("sw-ready"));
|
|
171
|
+
}
|
|
172
|
+
} else if (currentVersion && currentVersion !== manifest.version) {
|
|
173
|
+
window.swAvailableVersion = manifest.version;
|
|
174
|
+
window.swUpdateRequired =
|
|
175
|
+
currentVersion < (manifest.minSupportedVersion || "0.0.0");
|
|
176
|
+
|
|
177
|
+
if (AUTO_UPDATE) {
|
|
178
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
179
|
+
if (registration && registration.waiting) {
|
|
180
|
+
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
|
102
181
|
} else {
|
|
103
|
-
|
|
104
|
-
if ((e.target as ServiceWorker).state === 'activated') {
|
|
105
|
-
options.onSuccess?.(reg);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
182
|
+
await doRegisterServiceWorker(manifest.version);
|
|
108
183
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
184
|
+
} else {
|
|
185
|
+
window.dispatchEvent(
|
|
186
|
+
new CustomEvent("sw-update-available", {
|
|
187
|
+
detail: { version: manifest.version },
|
|
188
|
+
})
|
|
189
|
+
);
|
|
114
190
|
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
export function registerServiceWorker(swPath?: string): Promise<ServiceWorkerRegistration> {
|
|
124
|
-
if (!shouldRegister()) {
|
|
125
|
-
return Promise.reject(new Error('Service workers not supported'));
|
|
191
|
+
} else {
|
|
192
|
+
await doRegisterServiceWorker(manifest.version);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error("Service Worker initialization failed:", error);
|
|
196
|
+
window.swError = true;
|
|
197
|
+
window.dispatchEvent(new CustomEvent("sw-error"));
|
|
126
198
|
}
|
|
127
|
-
return navigator.serviceWorker.register(swPath || '/sw.js');
|
|
128
199
|
}
|
|
129
200
|
|
|
130
|
-
export function
|
|
131
|
-
return navigator.serviceWorker.
|
|
132
|
-
|
|
201
|
+
export function handleUpdateApproved(newVersion) {
|
|
202
|
+
return navigator.serviceWorker.getRegistration().then((registration) => {
|
|
203
|
+
if (registration && registration.waiting) {
|
|
204
|
+
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
|
205
|
+
registration.addEventListener("controllerchange", () => {
|
|
206
|
+
window.location.reload();
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
return doRegisterServiceWorker(newVersion).then(() => {
|
|
210
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
211
|
+
window.location.reload();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
133
215
|
});
|
|
134
216
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
217
|
+
|
|
218
|
+
export function skipWaiting() {
|
|
219
|
+
return navigator.serviceWorker.ready.then((registration) => {
|
|
220
|
+
if (registration.waiting) {
|
|
221
|
+
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
|
138
222
|
}
|
|
139
|
-
|
|
140
|
-
const code = `export function shouldRegister() {
|
|
141
|
-
return typeof window !== 'undefined' && 'serviceWorker' in navigator;
|
|
223
|
+
});
|
|
142
224
|
}
|
|
143
225
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
226
|
+
if (typeof window !== "undefined" && "serviceWorker" in navigator) {
|
|
227
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
228
|
+
if (event.data.type === "SW_PROGRESS") {
|
|
229
|
+
const { percent, downloaded, total } = event.data;
|
|
230
|
+
window.dispatchEvent(
|
|
231
|
+
new CustomEvent("sw-progress", {
|
|
232
|
+
detail: { percent, downloaded, total },
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (event.data.type === "BACKGROUND_SYNC_COMPLETE") {
|
|
237
|
+
const { succeeded, failed, tags } = event.data.detail;
|
|
238
|
+
window.dispatchEvent(
|
|
239
|
+
new CustomEvent("mutation-sync-complete", {
|
|
240
|
+
detail: { succeeded, failed },
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
if (tags && tags.length > 0) {
|
|
244
|
+
window.dispatchEvent(
|
|
245
|
+
new CustomEvent("cache-invalidated", { detail: { tags } })
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
149
251
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return reg.unregister();
|
|
252
|
+
`;
|
|
253
|
+
writeFileSync(join(swoffDir, `sw-injector.${ext}`), code);
|
|
254
|
+
generatedFiles.push(`swoff/sw-injector.${ext}`);
|
|
154
255
|
}
|
|
256
|
+
function generateFetchWrapper() {
|
|
257
|
+
ensureSwoffDir();
|
|
258
|
+
const code = `/**
|
|
259
|
+
* Swoff Fetch Wrapper
|
|
260
|
+
* Framework-agnostic fetch with cache strategy, tags, and query deduplication.
|
|
261
|
+
*
|
|
262
|
+
* Usage:
|
|
263
|
+
* import { fetchWithCache } from './swoff/fetch-wrapper.js';
|
|
264
|
+
*
|
|
265
|
+
* // GET - cached with tag
|
|
266
|
+
* const todos = await fetchWithCache("/api/todos", { tags: ["todos"] }).then(r => r.json());
|
|
267
|
+
*
|
|
268
|
+
* // POST - mutation (passes through to server)
|
|
269
|
+
* await fetchWithCache("/api/todos", {
|
|
270
|
+
* method: "POST",
|
|
271
|
+
* body: JSON.stringify({ title: "New task" }),
|
|
272
|
+
* });
|
|
273
|
+
*
|
|
274
|
+
* // Stale-while-revalidate
|
|
275
|
+
* const data = await fetchWithCache("/api/data", {
|
|
276
|
+
* tags: ["data"],
|
|
277
|
+
* staleWhileRevalidate: true,
|
|
278
|
+
* }).then(r => r.json());
|
|
279
|
+
*/
|
|
155
280
|
|
|
156
|
-
|
|
157
|
-
const [registration, setRegistration] = useState(null);
|
|
158
|
-
const [isReady, setIsReady] = useState(false);
|
|
281
|
+
const inFlightRequests = new Map();
|
|
159
282
|
|
|
160
|
-
|
|
161
|
-
|
|
283
|
+
export async function fetchWithCache(input, options = {}) {
|
|
284
|
+
const headers = new Headers(options.headers);
|
|
285
|
+
const method = options.method || "GET";
|
|
162
286
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.catch((err) => {
|
|
170
|
-
options.onError?.(err);
|
|
171
|
-
});
|
|
172
|
-
}, []);
|
|
287
|
+
if (!headers.has("X-SW-Cache-Strategy")) {
|
|
288
|
+
headers.set(
|
|
289
|
+
"X-SW-Cache-Strategy",
|
|
290
|
+
method === "GET" || method === "HEAD" ? "read" : "mutation"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
173
293
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
writeFileSync(join(outputDir, `sw-injector.${ext}`), code);
|
|
178
|
-
generatedFiles.push(`src/sw-injector.${ext}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
function generateSwGeneratorBuildScript() {
|
|
182
|
-
const outputDir = join(projectRoot, "swoff");
|
|
183
|
-
if (!existsSync(outputDir))
|
|
184
|
-
mkdirSync(outputDir, { recursive: true });
|
|
185
|
-
if (language === "ts") {
|
|
186
|
-
const code = `#!/usr/bin/env node
|
|
294
|
+
if (options.staleWhileRevalidate) {
|
|
295
|
+
headers.set("X-SW-Stale", "true");
|
|
296
|
+
}
|
|
187
297
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
298
|
+
if (options.tags && options.tags.length > 0) {
|
|
299
|
+
headers.set("X-SW-Cache-Tags", options.tags.join(","));
|
|
300
|
+
}
|
|
191
301
|
|
|
192
|
-
|
|
193
|
-
const
|
|
302
|
+
if (method === "GET" || method === "HEAD") {
|
|
303
|
+
const url = typeof input === "string" ? input : input.url;
|
|
304
|
+
if (inFlightRequests.has(url)) {
|
|
305
|
+
return inFlightRequests.get(url).then((r) => r.clone());
|
|
306
|
+
}
|
|
307
|
+
const promise = fetch(input, { ...options, headers }).finally(() => {
|
|
308
|
+
inFlightRequests.delete(url);
|
|
309
|
+
});
|
|
310
|
+
inFlightRequests.set(url, promise);
|
|
311
|
+
return promise;
|
|
312
|
+
}
|
|
194
313
|
|
|
195
|
-
|
|
196
|
-
console.error('Error: swoff.config.json not found');
|
|
197
|
-
console.log('Run "npx @swoff/cli init" to create one');
|
|
198
|
-
process.exit(1);
|
|
314
|
+
return fetch(input, { ...options, headers });
|
|
199
315
|
}
|
|
200
316
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (code === 0) {
|
|
211
|
-
console.log('ā
Service worker build complete');
|
|
212
|
-
} else {
|
|
213
|
-
console.error('ā Build failed');
|
|
214
|
-
process.exit(code || 1);
|
|
317
|
+
export async function fetchWithCacheOrQueue(input, options = {}) {
|
|
318
|
+
if (!navigator.onLine) {
|
|
319
|
+
if (options.method === "GET" || options.method === "HEAD") {
|
|
320
|
+
const cached = await caches.match(input);
|
|
321
|
+
if (cached) return cached;
|
|
322
|
+
throw new Error("Offline: no cached data");
|
|
323
|
+
} else {
|
|
324
|
+
throw new Error("Offline: mutation queued");
|
|
325
|
+
}
|
|
215
326
|
}
|
|
216
|
-
|
|
327
|
+
return fetchWithCache(input, options);
|
|
328
|
+
}
|
|
217
329
|
`;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
330
|
+
writeFileSync(join(swoffDir, "fetch-wrapper.js"), code);
|
|
331
|
+
generatedFiles.push("swoff/fetch-wrapper.js");
|
|
332
|
+
}
|
|
333
|
+
function generateCache() {
|
|
334
|
+
ensureSwoffDir();
|
|
335
|
+
const code = `/**
|
|
336
|
+
* Swoff Cache Invalidation & Cross-Tab Sync
|
|
337
|
+
* Framework-agnostic cache tag invalidation and cross-tab synchronization.
|
|
338
|
+
*
|
|
339
|
+
* Usage:
|
|
340
|
+
* import { invalidateByTag, initCrossTabSync } from './swoff/cache.js';
|
|
341
|
+
*
|
|
342
|
+
* // Call once during app init
|
|
343
|
+
* initCrossTabSync();
|
|
344
|
+
*
|
|
345
|
+
* // After a mutation, invalidate related cache
|
|
346
|
+
* await invalidateByTag("todos");
|
|
347
|
+
*/
|
|
223
348
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const path = require('path');
|
|
349
|
+
export async function invalidateByTag(tag) {
|
|
350
|
+
if (!navigator.serviceWorker?.controller) return;
|
|
227
351
|
|
|
228
|
-
|
|
352
|
+
navigator.serviceWorker.controller.postMessage({
|
|
353
|
+
type: "INVALIDATE_TAG",
|
|
354
|
+
tag,
|
|
355
|
+
});
|
|
229
356
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
process.exit(1);
|
|
357
|
+
window.dispatchEvent(
|
|
358
|
+
new CustomEvent("cache-invalidated", { detail: { tags: [tag] } })
|
|
359
|
+
);
|
|
234
360
|
}
|
|
235
361
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
'npx',
|
|
240
|
-
['@swoff/cli', 'generate', '--sw-only'],
|
|
241
|
-
{ stdio: 'inherit', shell: true }
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
proc.on('close', (code) => {
|
|
245
|
-
if (code === 0) {
|
|
246
|
-
console.log('ā
Service worker build complete');
|
|
247
|
-
} else {
|
|
248
|
-
console.error('ā Build failed');
|
|
249
|
-
process.exit(code || 1);
|
|
362
|
+
export async function invalidateByTags(tags) {
|
|
363
|
+
for (const tag of tags) {
|
|
364
|
+
await invalidateByTag(tag);
|
|
250
365
|
}
|
|
251
|
-
});
|
|
252
|
-
`;
|
|
253
|
-
writeFileSync(join(outputDir, `sw-generator.${ext}`), code);
|
|
254
|
-
generatedFiles.push(`swoff/sw-generator.${ext}`);
|
|
255
|
-
}
|
|
256
366
|
}
|
|
257
|
-
function generateSwTemplate() {
|
|
258
|
-
const outputDir = join(projectRoot, "swoff");
|
|
259
|
-
if (!existsSync(outputDir))
|
|
260
|
-
mkdirSync(outputDir, { recursive: true });
|
|
261
|
-
const template = `let CACHE_NAME = "";
|
|
262
|
-
let ASSETS_TO_CACHE = [];
|
|
263
367
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// [[FETCH_HANDLER]]
|
|
368
|
+
export function initCrossTabSync() {
|
|
369
|
+
if (!navigator.serviceWorker) return;
|
|
267
370
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const cache = await caches.open(CACHE_NAME);
|
|
276
|
-
await cache.put(request, response.clone());
|
|
277
|
-
return response;
|
|
278
|
-
},
|
|
279
|
-
async delete(key) {
|
|
280
|
-
const cache = await caches.open(CACHE_NAME);
|
|
281
|
-
return cache.delete(key);
|
|
282
|
-
},
|
|
283
|
-
async keys() {
|
|
284
|
-
const cache = await caches.open(CACHE_NAME);
|
|
285
|
-
return cache.keys();
|
|
371
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
372
|
+
if (event.data.type === "TAG_INVALIDATED" && event.data.tag) {
|
|
373
|
+
window.dispatchEvent(
|
|
374
|
+
new CustomEvent("cache-invalidated", {
|
|
375
|
+
detail: { tags: [event.data.tag] },
|
|
376
|
+
})
|
|
377
|
+
);
|
|
286
378
|
}
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
if (typeof self !== 'undefined') {
|
|
291
|
-
self.SWOFF = SWOFF;
|
|
379
|
+
});
|
|
292
380
|
}
|
|
293
381
|
`;
|
|
294
|
-
writeFileSync(join(
|
|
295
|
-
generatedFiles.push("swoff/
|
|
296
|
-
}
|
|
297
|
-
function generateTypeDefinitions() {
|
|
298
|
-
if (language !== "ts")
|
|
299
|
-
return;
|
|
300
|
-
const outputDir = join(projectRoot, "src");
|
|
301
|
-
if (!existsSync(outputDir))
|
|
302
|
-
mkdirSync(outputDir, { recursive: true });
|
|
303
|
-
const code = `declare module '*.service-worker' {
|
|
304
|
-
const serviceWorker: ServiceWorkerGlobalScope;
|
|
305
|
-
export default serviceWorker;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
interface SWOFFCache {
|
|
309
|
-
get(key: Request | string): Promise<Response | undefined>;
|
|
310
|
-
put(request: Request | string, response: Response): Promise<Response>;
|
|
311
|
-
delete(key: Request | string): Promise<boolean>;
|
|
312
|
-
keys(): Promise<Request[]>;
|
|
382
|
+
writeFileSync(join(swoffDir, "cache.js"), code);
|
|
383
|
+
generatedFiles.push("swoff/cache.js");
|
|
313
384
|
}
|
|
385
|
+
function generateMutationQueue() {
|
|
386
|
+
ensureSwoffDir();
|
|
387
|
+
const code = `/**
|
|
388
|
+
* Swoff Mutation Queue
|
|
389
|
+
* Queue offline writes and sync when connection returns.
|
|
390
|
+
*
|
|
391
|
+
* Usage:
|
|
392
|
+
* import { queueMutation, processMutationQueue, getPendingCount } from './swoff/mutation-queue.js';
|
|
393
|
+
*
|
|
394
|
+
* // Queue a mutation
|
|
395
|
+
* await queueMutation({
|
|
396
|
+
* method: "POST",
|
|
397
|
+
* url: "/api/todos",
|
|
398
|
+
* body: { title: "Grocery" },
|
|
399
|
+
* tags: ["todos"],
|
|
400
|
+
* storeName: "todos",
|
|
401
|
+
* tempId: "temp_abc123",
|
|
402
|
+
* });
|
|
403
|
+
*
|
|
404
|
+
* // Auto-processes on online event
|
|
405
|
+
*/
|
|
314
406
|
|
|
315
|
-
|
|
316
|
-
|
|
407
|
+
const DB_NAME = "swoff-queue";
|
|
408
|
+
const STORE_NAME = "mutations";
|
|
409
|
+
const MAX_RETRIES = 5;
|
|
410
|
+
|
|
411
|
+
function openQueueDB() {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
414
|
+
request.onupgradeneeded = (e) => {
|
|
415
|
+
const db = e.target.result;
|
|
416
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
417
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
418
|
+
store.createIndex("by-timestamp", "timestamp");
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
422
|
+
request.onerror = (e) => reject(e.target.error);
|
|
423
|
+
});
|
|
317
424
|
}
|
|
318
425
|
|
|
319
|
-
|
|
426
|
+
let isSyncing = false;
|
|
427
|
+
|
|
428
|
+
export async function queueMutation(mutation) {
|
|
429
|
+
const db = await openQueueDB();
|
|
430
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
431
|
+
const store = tx.objectStore(STORE_NAME);
|
|
432
|
+
|
|
433
|
+
store.add({
|
|
434
|
+
id: crypto.randomUUID(),
|
|
435
|
+
method: mutation.method,
|
|
436
|
+
url: mutation.url,
|
|
437
|
+
body: mutation.body,
|
|
438
|
+
headers: mutation.headers || {},
|
|
439
|
+
previousData: mutation.previousData || null,
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
retryCount: 0,
|
|
442
|
+
tags: mutation.tags || [],
|
|
443
|
+
storeName: mutation.storeName || null,
|
|
444
|
+
tempId: mutation.tempId || null,
|
|
445
|
+
});
|
|
320
446
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
generatedFiles.push("src/swoff.d.ts");
|
|
326
|
-
}
|
|
327
|
-
function generateOfflineHooks() {
|
|
328
|
-
const outputDir = join(projectRoot, "src", "hooks");
|
|
329
|
-
if (!existsSync(outputDir))
|
|
330
|
-
mkdirSync(outputDir, { recursive: true });
|
|
331
|
-
const useOffline = language === "ts"
|
|
332
|
-
? `import { useState, useEffect } from 'react';
|
|
447
|
+
await new Promise((resolve, reject) => {
|
|
448
|
+
tx.oncomplete = () => resolve();
|
|
449
|
+
tx.onerror = () => reject(tx.error);
|
|
450
|
+
});
|
|
333
451
|
|
|
334
|
-
|
|
335
|
-
isOnline: boolean;
|
|
336
|
-
isOffline: boolean;
|
|
452
|
+
window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
|
|
337
453
|
}
|
|
338
454
|
|
|
339
|
-
export function
|
|
340
|
-
|
|
455
|
+
export async function processMutationQueue() {
|
|
456
|
+
if (!navigator.onLine || isSyncing) return;
|
|
457
|
+
isSyncing = true;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const db = await openQueueDB();
|
|
461
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
462
|
+
const store = tx.objectStore(STORE_NAME);
|
|
463
|
+
const index = store.index("by-timestamp");
|
|
464
|
+
const queue = await new Promise((resolve, reject) => {
|
|
465
|
+
const request = index.getAll();
|
|
466
|
+
request.onsuccess = () => resolve(request.result);
|
|
467
|
+
request.onerror = () => reject(request.error);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
let succeeded = 0;
|
|
471
|
+
let failed = 0;
|
|
472
|
+
|
|
473
|
+
for (const item of queue) {
|
|
474
|
+
if (item.retryCount >= MAX_RETRIES) {
|
|
475
|
+
await rollbackMutation(item);
|
|
476
|
+
await removeFromQueue(item.id);
|
|
477
|
+
failed++;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
341
480
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
481
|
+
try {
|
|
482
|
+
const response = await fetch(item.url, {
|
|
483
|
+
method: item.method,
|
|
484
|
+
headers: {
|
|
485
|
+
"Content-Type": "application/json",
|
|
486
|
+
...item.headers,
|
|
487
|
+
},
|
|
488
|
+
body: JSON.stringify(item.body),
|
|
489
|
+
});
|
|
345
490
|
|
|
346
|
-
|
|
347
|
-
window.addEventListener('offline', handleOffline);
|
|
491
|
+
if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
|
|
348
492
|
|
|
349
|
-
|
|
350
|
-
window.removeEventListener('online', handleOnline);
|
|
351
|
-
window.removeEventListener('offline', handleOffline);
|
|
352
|
-
};
|
|
353
|
-
}, []);
|
|
493
|
+
const serverData = await response.json();
|
|
354
494
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
: `import { useState, useEffect } from 'react';
|
|
495
|
+
if (item.tempId && item.storeName) {
|
|
496
|
+
await reconcileRecord(item.storeName, item.tempId, serverData);
|
|
497
|
+
}
|
|
359
498
|
|
|
360
|
-
|
|
361
|
-
|
|
499
|
+
if (item.tags && item.tags.length > 0) {
|
|
500
|
+
const { invalidateByTags } = await import("./cache.js");
|
|
501
|
+
await invalidateByTags(item.tags);
|
|
502
|
+
}
|
|
362
503
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
504
|
+
await removeFromQueue(item.id);
|
|
505
|
+
succeeded++;
|
|
506
|
+
} catch {
|
|
507
|
+
item.retryCount++;
|
|
508
|
+
await updateInQueue(item);
|
|
509
|
+
failed++;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
366
512
|
|
|
367
|
-
window.
|
|
368
|
-
|
|
513
|
+
window.dispatchEvent(
|
|
514
|
+
new CustomEvent("mutation-sync-complete", {
|
|
515
|
+
detail: { succeeded, failed },
|
|
516
|
+
})
|
|
517
|
+
);
|
|
518
|
+
} finally {
|
|
519
|
+
isSyncing = false;
|
|
520
|
+
window.dispatchEvent(new CustomEvent("mutation-queue-changed"));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
369
523
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
524
|
+
async function removeFromQueue(id) {
|
|
525
|
+
const db = await openQueueDB();
|
|
526
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
527
|
+
tx.objectStore(STORE_NAME).delete(id);
|
|
528
|
+
await new Promise((resolve, reject) => {
|
|
529
|
+
tx.oncomplete = () => resolve();
|
|
530
|
+
tx.onerror = () => reject(tx.error);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
375
533
|
|
|
376
|
-
|
|
534
|
+
async function updateInQueue(item) {
|
|
535
|
+
const db = await openQueueDB();
|
|
536
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
537
|
+
tx.objectStore(STORE_NAME).put(item);
|
|
538
|
+
await new Promise((resolve, reject) => {
|
|
539
|
+
tx.oncomplete = () => resolve();
|
|
540
|
+
tx.onerror = () => reject(tx.error);
|
|
541
|
+
});
|
|
377
542
|
}
|
|
378
|
-
`;
|
|
379
|
-
writeFileSync(join(outputDir, `useOffline.${ext}`), useOffline);
|
|
380
|
-
generatedFiles.push(`src/hooks/useOffline.${ext}`);
|
|
381
|
-
const useApiData = language === "ts"
|
|
382
|
-
? `import { useState, useEffect, useCallback } from 'react';
|
|
383
543
|
|
|
384
|
-
|
|
385
|
-
|
|
544
|
+
async function rollbackMutation(item) {
|
|
545
|
+
if (!item.storeName) return;
|
|
546
|
+
|
|
547
|
+
if (item.method === "POST" && item.tempId) {
|
|
548
|
+
const { deleteRecord } = await import("./store.js");
|
|
549
|
+
await deleteRecord(item.storeName, item.tempId);
|
|
550
|
+
} else if (
|
|
551
|
+
(item.method === "PUT" || item.method === "PATCH") &&
|
|
552
|
+
item.previousData
|
|
553
|
+
) {
|
|
554
|
+
const { putRecord } = await import("./store.js");
|
|
555
|
+
await putRecord(item.storeName, { ...item.previousData, $synced: true });
|
|
556
|
+
} else if (item.method === "DELETE" && item.tempId && item.previousData) {
|
|
557
|
+
const { putRecord } = await import("./store.js");
|
|
558
|
+
await putRecord(item.storeName, { ...item.previousData, $synced: true });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
window.dispatchEvent(
|
|
562
|
+
new CustomEvent("mutation-rollback", {
|
|
563
|
+
detail: {
|
|
564
|
+
method: item.method,
|
|
565
|
+
url: item.url,
|
|
566
|
+
tempId: item.tempId,
|
|
567
|
+
previousData: item.previousData,
|
|
568
|
+
},
|
|
569
|
+
})
|
|
570
|
+
);
|
|
386
571
|
}
|
|
387
572
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
573
|
+
async function reconcileRecord(storeName, tempId, serverData) {
|
|
574
|
+
const { getRecord, putRecord, deleteRecord } = await import("./store.js");
|
|
575
|
+
const existing = await getRecord(storeName, tempId);
|
|
576
|
+
if (!existing) return;
|
|
577
|
+
|
|
578
|
+
const reconciled = {
|
|
579
|
+
...existing,
|
|
580
|
+
...serverData,
|
|
581
|
+
id: serverData.id,
|
|
582
|
+
$synced: true,
|
|
583
|
+
$syncedAt: Date.now(),
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
await putRecord(storeName, reconciled);
|
|
587
|
+
|
|
588
|
+
if (String(tempId) !== String(serverData.id)) {
|
|
589
|
+
await deleteRecord(storeName, tempId);
|
|
590
|
+
}
|
|
393
591
|
}
|
|
394
592
|
|
|
395
|
-
export function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
593
|
+
export async function getPendingCount() {
|
|
594
|
+
const db = await openQueueDB();
|
|
595
|
+
return new Promise((resolve, reject) => {
|
|
596
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
597
|
+
const request = tx.objectStore(STORE_NAME).count();
|
|
598
|
+
request.onsuccess = () => resolve(request.result);
|
|
599
|
+
request.onerror = () => reject(request.error);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
402
602
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
603
|
+
window.addEventListener("online", processMutationQueue);
|
|
604
|
+
`;
|
|
605
|
+
writeFileSync(join(swoffDir, "mutation-queue.js"), code);
|
|
606
|
+
generatedFiles.push("swoff/mutation-queue.js");
|
|
607
|
+
}
|
|
608
|
+
function generateBackgroundSync() {
|
|
609
|
+
ensureSwoffDir();
|
|
610
|
+
const code = `/**
|
|
611
|
+
* Swoff Background Sync
|
|
612
|
+
* Register sync events for processing mutation queue after tab close.
|
|
613
|
+
* Falls back to online event listener in unsupported browsers.
|
|
614
|
+
*
|
|
615
|
+
* Usage:
|
|
616
|
+
* import { syncWhenPossible } from './swoff/background-sync.js';
|
|
617
|
+
*
|
|
618
|
+
* await syncWhenPossible({
|
|
619
|
+
* method: "POST",
|
|
620
|
+
* url: "/api/todos",
|
|
621
|
+
* body: { title: "Grocery" },
|
|
622
|
+
* tags: ["todos"],
|
|
623
|
+
* storeName: "todos",
|
|
624
|
+
* tempId: "temp_abc123",
|
|
625
|
+
* });
|
|
626
|
+
*/
|
|
407
627
|
|
|
408
|
-
|
|
628
|
+
import { queueMutation, processMutationQueue, getPendingCount } from "./mutation-queue.js";
|
|
409
629
|
|
|
410
|
-
|
|
411
|
-
throw new Error(\`HTTP error! status: \${response.status}\`);
|
|
412
|
-
}
|
|
630
|
+
const SYNC_TAG = "sync-mutations";
|
|
413
631
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
setError(new Error(errorMessage));
|
|
632
|
+
async function registerSync() {
|
|
633
|
+
if (!("serviceWorker" in navigator) || !("SyncManager" in window)) {
|
|
634
|
+
window.addEventListener("online", processMutationQueue, { once: true });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
420
637
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
638
|
+
try {
|
|
639
|
+
const registration = await navigator.serviceWorker.ready;
|
|
640
|
+
await registration.sync.register(SYNC_TAG);
|
|
641
|
+
} catch {
|
|
642
|
+
window.addEventListener("online", processMutationQueue, { once: true });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
424
645
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}, [endpoint, JSON.stringify(options), data]);
|
|
646
|
+
export async function syncWhenPossible(mutation) {
|
|
647
|
+
await queueMutation(mutation);
|
|
648
|
+
await registerSync();
|
|
649
|
+
}
|
|
430
650
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
651
|
+
export async function retrySync() {
|
|
652
|
+
if (!("serviceWorker" in navigator) || !("SyncManager" in window)) return;
|
|
653
|
+
const count = await getPendingCount();
|
|
654
|
+
if (count > 0) {
|
|
655
|
+
await registerSync();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
436
658
|
|
|
437
|
-
|
|
659
|
+
window.addEventListener("mutation-sync-complete", retrySync);
|
|
660
|
+
`;
|
|
661
|
+
writeFileSync(join(swoffDir, "background-sync.js"), code);
|
|
662
|
+
generatedFiles.push("swoff/background-sync.js");
|
|
438
663
|
}
|
|
439
|
-
|
|
440
|
-
|
|
664
|
+
function generateSwGeneratorBuildScript() {
|
|
665
|
+
ensureSwoffDir();
|
|
666
|
+
const code = `#!/usr/bin/env node
|
|
667
|
+
/**
|
|
668
|
+
* Swoff SW Generator Build Script
|
|
669
|
+
* Reads swoff/sw-template.js and generates versioned SW output.
|
|
670
|
+
*
|
|
671
|
+
* Add to package.json:
|
|
672
|
+
* "build": "your-build && node swoff/sw-generator.js"
|
|
673
|
+
*/
|
|
441
674
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const [error, setError] = useState(null);
|
|
675
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
676
|
+
import { join, dirname } from 'path';
|
|
677
|
+
import { fileURLToPath } from 'url';
|
|
446
678
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
setLoading(true);
|
|
450
|
-
setError(null);
|
|
679
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
680
|
+
const projectRoot = join(__dirname, '..');
|
|
451
681
|
|
|
452
|
-
|
|
682
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
683
|
+
const templatePath = join(__dirname, 'sw-template.js');
|
|
684
|
+
const configPath = join(projectRoot, 'swoff.config.json');
|
|
453
685
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
686
|
+
if (!existsSync(configPath)) {
|
|
687
|
+
console.error('Error: swoff.config.json not found');
|
|
688
|
+
console.log('Run "npx @swoff/cli init" to create one');
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
457
691
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const errorMessage = err instanceof Error ? err.message : 'An error occurred';
|
|
463
|
-
setError(new Error(errorMessage));
|
|
692
|
+
if (!existsSync(templatePath)) {
|
|
693
|
+
console.error('Error: swoff/sw-template.js not found');
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
464
696
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
697
|
+
const pkg = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, 'utf8')) : { version: '1.0.0' };
|
|
698
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
699
|
+
const template = readFileSync(templatePath, 'utf8');
|
|
468
700
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
}, [endpoint, JSON.stringify(options), data]);
|
|
701
|
+
const version = config.version === 'from-package' ? pkg.version || '1.0.0' : config.version;
|
|
702
|
+
const outputDir = config.build?.outputDir || 'dist';
|
|
703
|
+
const swFilename = config.build?.swFilename || 'sw';
|
|
474
704
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
fetchData();
|
|
478
|
-
}
|
|
479
|
-
}, [endpoint]);
|
|
705
|
+
let sw = template;
|
|
706
|
+
sw = sw.replace('// [[CACHE_NAME]]', \`CACHE_NAME = 'sw-v\${version}'\`);
|
|
480
707
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
function generateMutationQueueHooks() {
|
|
488
|
-
const outputDir = join(projectRoot, "src", "hooks");
|
|
489
|
-
if (!existsSync(outputDir))
|
|
490
|
-
mkdirSync(outputDir, { recursive: true });
|
|
491
|
-
const useMutationQueue = language === "ts"
|
|
492
|
-
? `import { useState, useEffect, useCallback } from 'react';
|
|
708
|
+
const baseAssets = ['/', '/index.html'];
|
|
709
|
+
const pwaAssets = config.features?.pwa ? ['/manifest.json'] : [];
|
|
710
|
+
const assetsToCache = [...baseAssets, ...pwaAssets];
|
|
711
|
+
sw = sw.replace('// [[ASSETS_LIST]]', \`ASSETS_TO_CACHE = \${JSON.stringify(assetsToCache.map(url => ({ url, options: {} })), null, 2)}\`);
|
|
712
|
+
sw = sw.replace('// [[AUTO_SKIP_WAITING]]', \`const AUTO_SKIP_WAITING = \${config.serviceWorker?.autoUpdate || false};\`);
|
|
493
713
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
status: 'pending' | 'synced' | 'failed';
|
|
498
|
-
retries: number;
|
|
499
|
-
data: unknown;
|
|
500
|
-
endpoint: string;
|
|
501
|
-
method?: string;
|
|
714
|
+
const outDir = join(projectRoot, outputDir);
|
|
715
|
+
if (!existsSync(outDir)) {
|
|
716
|
+
import('fs').then(fs => fs.mkdirSync(outDir, { recursive: true }));
|
|
502
717
|
}
|
|
503
718
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
719
|
+
writeFileSync(join(projectRoot, outputDir, \`\${swFilename}-v\${version}.js\`), sw);
|
|
720
|
+
writeFileSync(join(projectRoot, outputDir, 'version.json'), JSON.stringify({
|
|
721
|
+
version,
|
|
722
|
+
minSupportedVersion: config.minSupportedVersion || '0.0.0',
|
|
723
|
+
generatedAt: new Date().toISOString(),
|
|
724
|
+
}, null, 2));
|
|
510
725
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
syncMutations: () => Promise<void>;
|
|
516
|
-
clearQueue: () => void;
|
|
517
|
-
retryMutation: (id: string) => Promise<void>;
|
|
726
|
+
console.log(\`Service worker built: \${outputDir}/\${swFilename}-v\${version}.js\`);
|
|
727
|
+
`;
|
|
728
|
+
writeFileSync(join(swoffDir, "sw-generator.js"), code);
|
|
729
|
+
generatedFiles.push("swoff/sw-generator.js");
|
|
518
730
|
}
|
|
731
|
+
function generateSwTemplate() {
|
|
732
|
+
ensureSwoffDir();
|
|
733
|
+
const code = `/**
|
|
734
|
+
* Swoff Service Worker Template
|
|
735
|
+
*
|
|
736
|
+
* This file is processed by swoff/sw-generator.js to create
|
|
737
|
+
* a versioned service worker. Placeholders are replaced during build.
|
|
738
|
+
*
|
|
739
|
+
* Placeholders:
|
|
740
|
+
* // [[CACHE_NAME]] - Replaced with versioned cache name
|
|
741
|
+
* // [[ASSETS_LIST]] - Replaced with assets to cache
|
|
742
|
+
* // [[AUTO_SKIP_WAITING]] - Replaced with autoUpdate config
|
|
743
|
+
*
|
|
744
|
+
* You can customize this template before running the build script.
|
|
745
|
+
*/
|
|
519
746
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
onSync,
|
|
523
|
-
maxRetries = 3,
|
|
524
|
-
storageKey = 'swoff-mutation-queue',
|
|
525
|
-
autoSync = true
|
|
526
|
-
} = options;
|
|
747
|
+
let CACHE_NAME = "";
|
|
748
|
+
let ASSETS_TO_CACHE = [];
|
|
527
749
|
|
|
528
|
-
|
|
529
|
-
|
|
750
|
+
// [[CACHE_NAME]]
|
|
751
|
+
// [[ASSETS_LIST]]
|
|
752
|
+
// [[AUTO_SKIP_WAITING]]
|
|
530
753
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
754
|
+
const CACHE_NAME_RUNTIME = "swoff-runtime";
|
|
755
|
+
|
|
756
|
+
// Install - download assets with progress tracking
|
|
757
|
+
self.addEventListener("install", (event) => {
|
|
758
|
+
event.waitUntil(
|
|
759
|
+
(async () => {
|
|
760
|
+
const cache = await caches.open(CACHE_NAME);
|
|
761
|
+
let downloaded = 0;
|
|
762
|
+
for (const asset of ASSETS_TO_CACHE) {
|
|
763
|
+
try {
|
|
764
|
+
const request = new Request(asset.url, asset.options);
|
|
765
|
+
await cache.add(request);
|
|
766
|
+
downloaded++;
|
|
767
|
+
const percent = Math.round((downloaded / ASSETS_TO_CACHE.length) * 100);
|
|
768
|
+
const clients = await self.clients.matchAll({ includeUncontrolled: true });
|
|
769
|
+
clients.forEach((client) => {
|
|
770
|
+
client.postMessage({
|
|
771
|
+
type: "SW_PROGRESS",
|
|
772
|
+
percent,
|
|
773
|
+
downloaded,
|
|
774
|
+
total: ASSETS_TO_CACHE.length,
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
} catch (err) {
|
|
778
|
+
console.error(\`Failed to cache \${asset.url}:\`, err);
|
|
538
779
|
}
|
|
539
780
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
781
|
+
if (AUTO_SKIP_WAITING) self.skipWaiting();
|
|
782
|
+
})(),
|
|
783
|
+
);
|
|
784
|
+
});
|
|
544
785
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
): string => {
|
|
556
|
-
const newMutation: Mutation = {
|
|
557
|
-
id: \`\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`,
|
|
558
|
-
timestamp: Date.now(),
|
|
559
|
-
status: 'pending',
|
|
560
|
-
retries: 0,
|
|
561
|
-
...mutation
|
|
562
|
-
};
|
|
786
|
+
// Activate - clean old caches
|
|
787
|
+
self.addEventListener("activate", (event) => {
|
|
788
|
+
event.waitUntil(
|
|
789
|
+
caches.keys().then((keys) =>
|
|
790
|
+
Promise.all(
|
|
791
|
+
keys.filter((key) => key !== CACHE_NAME && key !== CACHE_NAME_RUNTIME).map((key) => caches.delete(key))
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
});
|
|
563
796
|
|
|
564
|
-
|
|
797
|
+
// Message - skip waiting and cache invalidation
|
|
798
|
+
self.addEventListener("message", (event) => {
|
|
799
|
+
if (event.data.type === "SKIP_WAITING") {
|
|
800
|
+
self.skipWaiting();
|
|
801
|
+
}
|
|
802
|
+
});
|
|
565
803
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
804
|
+
// Fetch - cache strategies
|
|
805
|
+
self.addEventListener("fetch", (event) => {
|
|
806
|
+
if (event.request.method !== "GET" && event.request.method !== "HEAD") return;
|
|
569
807
|
|
|
570
|
-
|
|
571
|
-
|
|
808
|
+
event.respondWith(
|
|
809
|
+
(async () => {
|
|
810
|
+
const cache = await caches.open(CACHE_NAME);
|
|
811
|
+
const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
|
|
812
|
+
const url = new URL(event.request.url);
|
|
572
813
|
|
|
573
|
-
|
|
574
|
-
|
|
814
|
+
const byPath = await cache.match(url.pathname);
|
|
815
|
+
if (byPath) return byPath;
|
|
575
816
|
|
|
576
|
-
|
|
577
|
-
|
|
817
|
+
const byRequest = await runtimeCache.match(event.request);
|
|
818
|
+
if (byRequest) return byRequest;
|
|
578
819
|
|
|
579
|
-
|
|
820
|
+
if (event.request.mode === "navigate") {
|
|
821
|
+
const spa = await cache.match("/index.html");
|
|
822
|
+
if (spa) return spa;
|
|
823
|
+
}
|
|
580
824
|
|
|
581
|
-
for (const mutation of pending) {
|
|
582
825
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
await fetch(mutation.endpoint, {
|
|
587
|
-
method: mutation.method || 'POST',
|
|
588
|
-
headers: { 'Content-Type': 'application/json' },
|
|
589
|
-
body: JSON.stringify(mutation.data),
|
|
590
|
-
});
|
|
826
|
+
const response = await fetch(event.request);
|
|
827
|
+
if (response.ok) {
|
|
828
|
+
await runtimeCache.put(event.request, response.clone());
|
|
591
829
|
}
|
|
592
|
-
|
|
593
|
-
prev.map((m) => (m.id === mutation.id ? { ...m, status: 'synced' } : m))
|
|
594
|
-
);
|
|
830
|
+
return response;
|
|
595
831
|
} catch {
|
|
596
|
-
|
|
597
|
-
prev.map((m) =>
|
|
598
|
-
m.id === mutation.id
|
|
599
|
-
? { ...m, retries: m.retries + 1, status: m.retries >= maxRetries ? 'failed' : 'pending' }
|
|
600
|
-
: m
|
|
601
|
-
)
|
|
602
|
-
);
|
|
832
|
+
return new Response("Offline: content not available", { status: 503 });
|
|
603
833
|
}
|
|
834
|
+
})(),
|
|
835
|
+
);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const SWOFF = {
|
|
839
|
+
cache: {
|
|
840
|
+
async get(key) {
|
|
841
|
+
const cache = await caches.open(CACHE_NAME);
|
|
842
|
+
return cache.match(key);
|
|
843
|
+
},
|
|
844
|
+
async put(request, response) {
|
|
845
|
+
const cache = await caches.open(CACHE_NAME);
|
|
846
|
+
await cache.put(request, response);
|
|
847
|
+
},
|
|
848
|
+
async delete(request) {
|
|
849
|
+
const cache = await caches.open(CACHE_NAME);
|
|
850
|
+
return cache.delete(request);
|
|
604
851
|
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
605
854
|
|
|
606
|
-
|
|
607
|
-
|
|
855
|
+
if (typeof self !== 'undefined') {
|
|
856
|
+
self.SWOFF = SWOFF;
|
|
857
|
+
}
|
|
858
|
+
`;
|
|
859
|
+
writeFileSync(join(swoffDir, "sw-template.js"), code);
|
|
860
|
+
generatedFiles.push("swoff/sw-template.js");
|
|
861
|
+
}
|
|
862
|
+
function generateStore() {
|
|
863
|
+
ensureSwoffDir();
|
|
864
|
+
const dbName = config.database?.name || "app-db";
|
|
865
|
+
const code = `/**
|
|
866
|
+
* Swoff IndexedDB Store
|
|
867
|
+
* Generic CRUD operations for app's IndexedDB database.
|
|
868
|
+
*
|
|
869
|
+
* Usage:
|
|
870
|
+
* import { getRecord, putRecord, deleteRecord, openAppDB } from './swoff/store.js';
|
|
871
|
+
*
|
|
872
|
+
* const record = await getRecord('todos', 'todo-123');
|
|
873
|
+
* await putRecord('todos', { id: 'todo-123', title: 'New task', $synced: false });
|
|
874
|
+
* await deleteRecord('todos', 'todo-123');
|
|
875
|
+
*/
|
|
608
876
|
|
|
609
|
-
|
|
610
|
-
if (!autoSync) return;
|
|
877
|
+
const DB_NAME = "${dbName}";
|
|
611
878
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
879
|
+
export function openAppDB() {
|
|
880
|
+
return new Promise((resolve, reject) => {
|
|
881
|
+
const request = indexedDB.open(DB_NAME);
|
|
882
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
883
|
+
request.onerror = (e) => reject(e.target.error);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
616
886
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
887
|
+
export async function getRecord(storeName, id) {
|
|
888
|
+
const db = await openAppDB();
|
|
889
|
+
return new Promise((resolve, reject) => {
|
|
890
|
+
const tx = db.transaction(storeName, "readonly");
|
|
891
|
+
const store = tx.objectStore(storeName);
|
|
892
|
+
const request = store.get(id);
|
|
893
|
+
request.onsuccess = () => resolve(request.result);
|
|
894
|
+
request.onerror = () => reject(request.error);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
621
897
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
|
|
898
|
+
export async function putRecord(storeName, record) {
|
|
899
|
+
const db = await openAppDB();
|
|
900
|
+
return new Promise((resolve, reject) => {
|
|
901
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
902
|
+
const store = tx.objectStore(storeName);
|
|
903
|
+
const request = store.put(record);
|
|
904
|
+
tx.oncomplete = () => resolve(request.result);
|
|
905
|
+
tx.onerror = () => reject(tx.error);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
628
908
|
|
|
629
|
-
|
|
909
|
+
export async function deleteRecord(storeName, id) {
|
|
910
|
+
const db = await openAppDB();
|
|
911
|
+
return new Promise((resolve, reject) => {
|
|
912
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
913
|
+
const store = tx.objectStore(storeName);
|
|
914
|
+
const request = store.delete(id);
|
|
915
|
+
tx.oncomplete = () => resolve();
|
|
916
|
+
tx.onerror = () => reject(tx.error);
|
|
917
|
+
});
|
|
630
918
|
}
|
|
631
|
-
`
|
|
632
|
-
: `import { useState, useEffect, useCallback } from 'react';
|
|
633
919
|
|
|
634
|
-
export function
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
920
|
+
export async function getAllRecords(storeName) {
|
|
921
|
+
const db = await openAppDB();
|
|
922
|
+
return new Promise((resolve, reject) => {
|
|
923
|
+
const tx = db.transaction(storeName, "readonly");
|
|
924
|
+
const store = tx.objectStore(storeName);
|
|
925
|
+
const request = store.getAll();
|
|
926
|
+
request.onsuccess = () => resolve(request.result);
|
|
927
|
+
request.onerror = () => reject(request.error);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
`;
|
|
931
|
+
writeFileSync(join(swoffDir, "store.js"), code);
|
|
932
|
+
generatedFiles.push("swoff/store.js");
|
|
933
|
+
}
|
|
934
|
+
function generateReconcile() {
|
|
935
|
+
ensureSwoffDir();
|
|
936
|
+
const code = `/**
|
|
937
|
+
* Swoff ID Reconciliation
|
|
938
|
+
* Update local records with server data after mutation sync.
|
|
939
|
+
*
|
|
940
|
+
* Usage:
|
|
941
|
+
* import { reconcileRecord } from './swoff/reconcile.js';
|
|
942
|
+
*
|
|
943
|
+
* await reconcileRecord('todos', 'temp_abc123', serverData);
|
|
944
|
+
*/
|
|
641
945
|
|
|
642
|
-
|
|
643
|
-
const [isSyncing, setIsSyncing] = useState(false);
|
|
946
|
+
import { getRecord, putRecord, deleteRecord } from './store.js';
|
|
644
947
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (stored) {
|
|
649
|
-
const parsed = JSON.parse(stored);
|
|
650
|
-
if (Array.isArray(parsed)) {
|
|
651
|
-
setPendingMutations(parsed);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
} catch {
|
|
655
|
-
// ignore
|
|
656
|
-
}
|
|
657
|
-
}, [storageKey]);
|
|
948
|
+
export async function reconcileRecord(storeName, tempId, serverData) {
|
|
949
|
+
const existing = await getRecord(storeName, tempId);
|
|
950
|
+
if (!existing) return;
|
|
658
951
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const queueMutation = useCallback((mutation) => {
|
|
668
|
-
const newMutation = {
|
|
669
|
-
id: \`\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`,
|
|
670
|
-
timestamp: Date.now(),
|
|
671
|
-
status: 'pending',
|
|
672
|
-
retries: 0,
|
|
673
|
-
...mutation
|
|
674
|
-
};
|
|
952
|
+
const reconciled = {
|
|
953
|
+
...existing,
|
|
954
|
+
...serverData,
|
|
955
|
+
id: serverData.id,
|
|
956
|
+
$synced: true,
|
|
957
|
+
$syncedAt: Date.now(),
|
|
958
|
+
};
|
|
675
959
|
|
|
676
|
-
|
|
960
|
+
await putRecord(storeName, reconciled);
|
|
677
961
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
962
|
+
if (String(tempId) !== String(serverData.id)) {
|
|
963
|
+
await deleteRecord(storeName, tempId);
|
|
964
|
+
}
|
|
681
965
|
|
|
682
|
-
|
|
683
|
-
|
|
966
|
+
await reconcileReferences(storeName, tempId, serverData.id);
|
|
967
|
+
}
|
|
684
968
|
|
|
685
|
-
|
|
686
|
-
|
|
969
|
+
export async function reconcileReferences(storeName, oldId, newId) {
|
|
970
|
+
// Override this for your app's schema.
|
|
971
|
+
// Example: update foreign-key references in related stores.
|
|
972
|
+
//
|
|
973
|
+
// const txns = await getAllRecords('transactions');
|
|
974
|
+
// for (const txn of txns) {
|
|
975
|
+
// if (txn.todoId === oldId) {
|
|
976
|
+
// txn.todoId = newId;
|
|
977
|
+
// await putRecord('transactions', txn);
|
|
978
|
+
// }
|
|
979
|
+
// }
|
|
980
|
+
}
|
|
981
|
+
`;
|
|
982
|
+
writeFileSync(join(swoffDir, "reconcile.js"), code);
|
|
983
|
+
generatedFiles.push("swoff/reconcile.js");
|
|
984
|
+
}
|
|
985
|
+
function generateIndexedDB() {
|
|
986
|
+
ensureSwoffDir();
|
|
987
|
+
const dbName = config.database?.name || "app-db";
|
|
988
|
+
const stores = config.database?.stores || [];
|
|
989
|
+
const code = `/**
|
|
990
|
+
* Swoff IndexedDB Setup
|
|
991
|
+
* Database initialization with schema migrations.
|
|
992
|
+
*
|
|
993
|
+
* Usage:
|
|
994
|
+
* import { openDB } from './swoff/indexeddb.js';
|
|
995
|
+
*
|
|
996
|
+
* const db = await openDB();
|
|
997
|
+
*/
|
|
687
998
|
|
|
688
|
-
|
|
689
|
-
|
|
999
|
+
const DB_NAME = "${dbName}";
|
|
1000
|
+
const DB_VERSION = 1;
|
|
690
1001
|
|
|
691
|
-
|
|
1002
|
+
export function openDB() {
|
|
1003
|
+
return new Promise((resolve, reject) => {
|
|
1004
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
692
1005
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
await onSync(mutation);
|
|
697
|
-
} else {
|
|
698
|
-
await fetch(mutation.endpoint, {
|
|
699
|
-
method: mutation.method || 'POST',
|
|
700
|
-
headers: { 'Content-Type': 'application/json' },
|
|
701
|
-
body: JSON.stringify(mutation.data),
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
setPendingMutations((prev) =>
|
|
705
|
-
prev.map((m) => (m.id === mutation.id ? { ...m, status: 'synced' } : m))
|
|
706
|
-
);
|
|
707
|
-
} catch {
|
|
708
|
-
setPendingMutations((prev) =>
|
|
709
|
-
prev.map((m) =>
|
|
710
|
-
m.id === mutation.id
|
|
711
|
-
? { ...m, retries: m.retries + 1, status: m.retries >= maxRetries ? 'failed' : 'pending' }
|
|
712
|
-
: m
|
|
713
|
-
)
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
1006
|
+
request.onupgradeneeded = (e) => {
|
|
1007
|
+
const db = e.target.result;
|
|
1008
|
+
const oldVersion = e.oldVersion;
|
|
717
1009
|
|
|
718
|
-
|
|
719
|
-
|
|
1010
|
+
${stores.length > 0 ? stores.map((store, i) => ` if (oldVersion < ${i + 1}) {
|
|
1011
|
+
db.createObjectStore("${store}", { keyPath: "id" });
|
|
1012
|
+
}`).join("\n\n") : ` // Create your object stores here:
|
|
1013
|
+
// if (oldVersion < 1) {
|
|
1014
|
+
// const todos = db.createObjectStore("todos", { keyPath: "id" });
|
|
1015
|
+
// todos.createIndex("by-date", "date");
|
|
1016
|
+
// }`}
|
|
1017
|
+
};
|
|
720
1018
|
|
|
721
|
-
|
|
722
|
-
|
|
1019
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
1020
|
+
request.onerror = (e) => reject(e.target.error);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
723
1023
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return () => window.removeEventListener('online', handleOnline);
|
|
727
|
-
}, [syncMutations, autoSync]);
|
|
1024
|
+
export async function requestPersistentStorage() {
|
|
1025
|
+
if (!navigator.storage?.persist) return false;
|
|
728
1026
|
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
localStorage.removeItem(storageKey);
|
|
732
|
-
}, [storageKey]);
|
|
1027
|
+
const isPersisted = await navigator.storage.persisted();
|
|
1028
|
+
if (isPersisted) return true;
|
|
733
1029
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
return await navigator.storage.persist();
|
|
1032
|
+
} catch {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export async function monitorStorage() {
|
|
1038
|
+
const estimate = await navigator.storage.estimate();
|
|
1039
|
+
const ratio = estimate.usage / estimate.quota;
|
|
740
1040
|
|
|
741
|
-
return {
|
|
1041
|
+
return {
|
|
1042
|
+
usage: estimate.usage,
|
|
1043
|
+
quota: estimate.quota,
|
|
1044
|
+
ratio,
|
|
1045
|
+
status: ratio >= 0.95 ? "critical" : ratio >= 0.8 ? "warning" : "ok",
|
|
1046
|
+
};
|
|
742
1047
|
}
|
|
743
1048
|
`;
|
|
744
|
-
writeFileSync(join(
|
|
745
|
-
generatedFiles.push(
|
|
1049
|
+
writeFileSync(join(swoffDir, "indexeddb.js"), code);
|
|
1050
|
+
generatedFiles.push("swoff/indexeddb.js");
|
|
746
1051
|
}
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
755
|
-
message?: string;
|
|
756
|
-
className?: string;
|
|
1052
|
+
function generateTypeDefinitions() {
|
|
1053
|
+
if (language !== "ts")
|
|
1054
|
+
return;
|
|
1055
|
+
ensureSwoffDir();
|
|
1056
|
+
const code = `interface BeforeInstallPromptEvent extends Event {
|
|
1057
|
+
prompt(): Promise<void>;
|
|
1058
|
+
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
|
757
1059
|
}
|
|
758
1060
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
const { isOffline } = useOffline();
|
|
772
|
-
|
|
773
|
-
if (!isOffline) return null;
|
|
774
|
-
|
|
775
|
-
return (
|
|
776
|
-
<div
|
|
777
|
-
className={\`fixed \${positionClasses[position]} bg-red-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 flex items-center gap-2 \${className}\`}
|
|
778
|
-
role="alert"
|
|
779
|
-
aria-live="polite"
|
|
780
|
-
>
|
|
781
|
-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
782
|
-
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd"/>
|
|
783
|
-
</svg>
|
|
784
|
-
<span className="text-sm font-medium">{message}</span>
|
|
785
|
-
</div>
|
|
786
|
-
);
|
|
1061
|
+
declare global {
|
|
1062
|
+
interface Window {
|
|
1063
|
+
deferredInstallPrompt: BeforeInstallPromptEvent | null;
|
|
1064
|
+
latestSWVersion?: string;
|
|
1065
|
+
currentSWVersion?: string;
|
|
1066
|
+
swRegisteredVersion?: string;
|
|
1067
|
+
swAvailableVersion?: string;
|
|
1068
|
+
swUpdateRequired?: boolean;
|
|
1069
|
+
swMinSupportedVersion?: string;
|
|
1070
|
+
swReady?: boolean;
|
|
1071
|
+
swError?: boolean;
|
|
1072
|
+
}
|
|
787
1073
|
}
|
|
788
|
-
`;
|
|
789
|
-
writeFileSync(join(outputDir, "OfflineIndicator.tsx"), offlineIndicator);
|
|
790
|
-
generatedFiles.push("src/components/OfflineIndicator.tsx");
|
|
791
|
-
const pwaInstallButton = `import { useState, useEffect } from 'react';
|
|
792
|
-
|
|
793
|
-
export interface PWAInstallButtonProps {
|
|
794
|
-
installLabel?: string;
|
|
795
|
-
installedLabel?: string;
|
|
796
|
-
className?: string;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
export function PWAInstallButton({
|
|
800
|
-
installLabel = 'Install App',
|
|
801
|
-
installedLabel = 'Installed',
|
|
802
|
-
className = ''
|
|
803
|
-
}: PWAInstallButtonProps) {
|
|
804
|
-
const [isInstallable, setIsInstallable] = useState(false);
|
|
805
|
-
const [isInstalled, setIsInstalled] = useState(false);
|
|
806
|
-
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
807
|
-
|
|
808
|
-
useEffect(() => {
|
|
809
|
-
const handleBeforeInstallPrompt = (e: Event) => {
|
|
810
|
-
e.preventDefault();
|
|
811
|
-
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
|
812
|
-
setIsInstallable(true);
|
|
813
|
-
};
|
|
814
1074
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
821
|
-
window.addEventListener('appinstalled', handleAppInstalled);
|
|
822
|
-
|
|
823
|
-
return () => {
|
|
824
|
-
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
825
|
-
window.removeEventListener('appinstalled', handleAppInstalled);
|
|
826
|
-
};
|
|
827
|
-
}, []);
|
|
828
|
-
|
|
829
|
-
const handleInstall = async () => {
|
|
830
|
-
if (!deferredPrompt) return;
|
|
831
|
-
|
|
832
|
-
await deferredPrompt.prompt();
|
|
833
|
-
const { outcome } = await deferredPrompt.userChoice;
|
|
1075
|
+
export interface SWOFFCache {
|
|
1076
|
+
get(key: Request | string): Promise<Response | undefined>;
|
|
1077
|
+
put(request: Request | string, response: Response): Promise<void>;
|
|
1078
|
+
delete(request: Request | string): Promise<boolean>;
|
|
1079
|
+
}
|
|
834
1080
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
setDeferredPrompt(null);
|
|
1081
|
+
export interface SWOFF {
|
|
1082
|
+
cache: SWOFFCache;
|
|
1083
|
+
network: {
|
|
1084
|
+
fetch(request: Request | string, options?: RequestInit): Promise<Response>;
|
|
840
1085
|
};
|
|
1086
|
+
}
|
|
841
1087
|
|
|
842
|
-
|
|
1088
|
+
export interface FetchWithCacheOptions extends RequestInit {
|
|
1089
|
+
strategy?: "read" | "mutation";
|
|
1090
|
+
tags?: string[];
|
|
1091
|
+
staleWhileRevalidate?: boolean;
|
|
1092
|
+
}
|
|
843
1093
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1094
|
+
export interface MutationQueueItem {
|
|
1095
|
+
id: string;
|
|
1096
|
+
method: string;
|
|
1097
|
+
url: string;
|
|
1098
|
+
body: unknown;
|
|
1099
|
+
headers: Record<string, string>;
|
|
1100
|
+
previousData: unknown | null;
|
|
1101
|
+
timestamp: number;
|
|
1102
|
+
retryCount: number;
|
|
1103
|
+
tags: string[];
|
|
1104
|
+
storeName: string | null;
|
|
1105
|
+
tempId: string | null;
|
|
853
1106
|
}
|
|
854
1107
|
|
|
855
|
-
interface
|
|
856
|
-
|
|
857
|
-
|
|
1108
|
+
export interface MutationQueueResult {
|
|
1109
|
+
succeeded: number;
|
|
1110
|
+
failed: number;
|
|
858
1111
|
}
|
|
1112
|
+
|
|
1113
|
+
export {};
|
|
859
1114
|
`;
|
|
860
|
-
writeFileSync(join(
|
|
861
|
-
generatedFiles.push("
|
|
1115
|
+
writeFileSync(join(swoffDir, "swoff.d.ts"), code);
|
|
1116
|
+
generatedFiles.push("swoff/swoff.d.ts");
|
|
862
1117
|
}
|
|
863
|
-
function
|
|
864
|
-
const outputDir = join(projectRoot, "
|
|
1118
|
+
function generateManifest() {
|
|
1119
|
+
const outputDir = join(projectRoot, "public");
|
|
865
1120
|
if (!existsSync(outputDir))
|
|
866
1121
|
mkdirSync(outputDir, { recursive: true });
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1122
|
+
const manifest = {
|
|
1123
|
+
name: "Swoff App",
|
|
1124
|
+
short_name: "Swoff",
|
|
1125
|
+
description: "Offline-first web application",
|
|
1126
|
+
start_url: "/",
|
|
1127
|
+
display: "standalone",
|
|
1128
|
+
background_color: "#ffffff",
|
|
1129
|
+
theme_color: "#000000",
|
|
1130
|
+
icons: [
|
|
1131
|
+
{
|
|
1132
|
+
src: "/icon-192.png",
|
|
1133
|
+
sizes: "192x192",
|
|
1134
|
+
type: "image/png",
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
src: "/icon-512.png",
|
|
1138
|
+
sizes: "512x512",
|
|
1139
|
+
type: "image/png",
|
|
1140
|
+
},
|
|
1141
|
+
],
|
|
882
1142
|
};
|
|
883
|
-
|
|
1143
|
+
writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1144
|
+
generatedFiles.push("public/manifest.json");
|
|
1145
|
+
}
|
|
1146
|
+
function generatePwaInstall() {
|
|
1147
|
+
ensureSwoffDir();
|
|
1148
|
+
const preventDefault = config.pwa?.preventDefaultInstall ?? false;
|
|
1149
|
+
const code = `/**
|
|
1150
|
+
* Swoff PWA Install Prompt Handler
|
|
1151
|
+
* Captures beforeinstallprompt event and provides manual install trigger.
|
|
1152
|
+
*
|
|
1153
|
+
* Usage:
|
|
1154
|
+
* import { isInstallable, promptInstall } from './swoff/pwa-install.js';
|
|
1155
|
+
*
|
|
1156
|
+
* // Listen for installable event
|
|
1157
|
+
* window.addEventListener('pwa-installable', (e) => {
|
|
1158
|
+
* console.log('PWA can be installed:', e.detail.isInstallable);
|
|
1159
|
+
* // Show your custom install button
|
|
1160
|
+
* });
|
|
1161
|
+
*
|
|
1162
|
+
* // When user clicks install button
|
|
1163
|
+
* async function onInstallClick() {
|
|
1164
|
+
* const result = await promptInstall();
|
|
1165
|
+
* console.log('User choice:', result);
|
|
1166
|
+
* }
|
|
1167
|
+
*
|
|
1168
|
+
* Window events:
|
|
1169
|
+
* pwa-installable - PWA can be installed (detail: { isInstallable: true })
|
|
1170
|
+
* pwa-installed - User accepted install (detail: { outcome: 'accepted' })
|
|
1171
|
+
* pwa-dismissed - User dismissed install (detail: { outcome: 'dismissed' })
|
|
1172
|
+
*
|
|
1173
|
+
* Window properties:
|
|
1174
|
+
* window.deferredInstallPrompt - The captured BeforeInstallPromptEvent
|
|
1175
|
+
* window.pwaInstallable - Whether PWA can be installed
|
|
1176
|
+
*/
|
|
884
1177
|
|
|
885
|
-
|
|
886
|
-
key: string,
|
|
887
|
-
callback: (value: unknown, type: string) => void
|
|
888
|
-
): () => void {
|
|
889
|
-
if (!this.listeners.has(key)) {
|
|
890
|
-
this.listeners.set(key, new Set());
|
|
891
|
-
}
|
|
892
|
-
this.listeners.get(key)!.add(callback);
|
|
1178
|
+
const PREVENT_DEFAULT_INSTALL = ${preventDefault};
|
|
893
1179
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1180
|
+
window.addEventListener("beforeinstallprompt", (e) => {
|
|
1181
|
+
// Always capture the event so we never lose it
|
|
1182
|
+
window.deferredInstallPrompt = e;
|
|
1183
|
+
window.pwaInstallable = true;
|
|
898
1184
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
} catch {
|
|
903
|
-
// ignore
|
|
904
|
-
}
|
|
905
|
-
this.channel.postMessage({ type: 'set', key, value });
|
|
1185
|
+
if (PREVENT_DEFAULT_INSTALL) {
|
|
1186
|
+
// Suppress browser's native prompt
|
|
1187
|
+
e.preventDefault();
|
|
906
1188
|
}
|
|
1189
|
+
// When false, browser shows native prompt naturally
|
|
1190
|
+
// but we still capture the event for manual triggering
|
|
907
1191
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1192
|
+
window.dispatchEvent(
|
|
1193
|
+
new CustomEvent("pwa-installable", {
|
|
1194
|
+
detail: { isInstallable: true },
|
|
1195
|
+
})
|
|
1196
|
+
);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
window.addEventListener("appinstalled", () => {
|
|
1200
|
+
window.deferredInstallPrompt = null;
|
|
1201
|
+
window.pwaInstallable = false;
|
|
916
1202
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1203
|
+
window.dispatchEvent(
|
|
1204
|
+
new CustomEvent("pwa-installed", {
|
|
1205
|
+
detail: { outcome: "accepted" },
|
|
1206
|
+
})
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
export function isInstallable() {
|
|
1211
|
+
return !!window.deferredInstallPrompt;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export async function promptInstall() {
|
|
1215
|
+
if (!window.deferredInstallPrompt) {
|
|
1216
|
+
throw new Error("Install prompt not available");
|
|
920
1217
|
}
|
|
921
1218
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1219
|
+
const promptEvent = window.deferredInstallPrompt;
|
|
1220
|
+
await promptEvent.prompt();
|
|
1221
|
+
|
|
1222
|
+
const choice = await promptEvent.userChoice;
|
|
1223
|
+
|
|
1224
|
+
if (choice.outcome === "accepted") {
|
|
1225
|
+
window.dispatchEvent(
|
|
1226
|
+
new CustomEvent("pwa-installed", {
|
|
1227
|
+
detail: { outcome: "accepted" },
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
} else {
|
|
1231
|
+
window.dispatchEvent(
|
|
1232
|
+
new CustomEvent("pwa-dismissed", {
|
|
1233
|
+
detail: { outcome: "dismissed" },
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
925
1236
|
}
|
|
926
|
-
}
|
|
927
1237
|
|
|
928
|
-
|
|
929
|
-
return
|
|
1238
|
+
window.deferredInstallPrompt = null;
|
|
1239
|
+
return choice;
|
|
1240
|
+
}
|
|
1241
|
+
`;
|
|
1242
|
+
writeFileSync(join(swoffDir, "pwa-install.js"), code);
|
|
1243
|
+
generatedFiles.push("swoff/pwa-install.js");
|
|
930
1244
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1245
|
+
function generateInvalidationTags() {
|
|
1246
|
+
ensureSwoffDir();
|
|
1247
|
+
const code = `/**
|
|
1248
|
+
* Swoff Invalidation Tags Helper
|
|
1249
|
+
* URL-based tag generation from REST endpoints for automatic cache invalidation.
|
|
1250
|
+
*
|
|
1251
|
+
* Usage:
|
|
1252
|
+
* import { generateTags, invalidateUrl } from './swoff/invalidation-tags.js';
|
|
1253
|
+
*
|
|
1254
|
+
* // Generate tags from URL
|
|
1255
|
+
* generateTags("/api/todos"); // ["todos"]
|
|
1256
|
+
* generateTags("/api/todos/42"); // ["todos", "todo:42"]
|
|
1257
|
+
* generateTags("/api/todos/42/comments"); // ["todos", "todo:42", "comments"]
|
|
1258
|
+
*
|
|
1259
|
+
* // Use with fetch wrapper
|
|
1260
|
+
* const data = await fetchWithCache("/api/todos", {
|
|
1261
|
+
* tags: generateTags("/api/todos"),
|
|
1262
|
+
* });
|
|
1263
|
+
*
|
|
1264
|
+
* // Invalidate after mutation
|
|
1265
|
+
* await invalidateUrl("/api/todos/42");
|
|
1266
|
+
*/
|
|
936
1267
|
|
|
937
|
-
|
|
938
|
-
const { key, value, type } = event.data;
|
|
939
|
-
const callbacks = this.listeners.get(key);
|
|
940
|
-
if (callbacks) {
|
|
941
|
-
callbacks.forEach((callback) => callback(value, type));
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
}
|
|
1268
|
+
import { invalidateByTag, invalidateByTags } from "./cache.js";
|
|
945
1269
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
this.listeners.get(key).add(callback);
|
|
1270
|
+
export function generateTags(url) {
|
|
1271
|
+
const parsed = typeof url === "string" ? new URL(url, window.location.origin) : url;
|
|
1272
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
951
1273
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1274
|
+
if (segments.length === 0) return ["root"];
|
|
1275
|
+
|
|
1276
|
+
// Skip common API prefixes
|
|
1277
|
+
const skipPrefixes = ["api", "v1", "v2", "v3", "rest", "graphql", "gql"];
|
|
1278
|
+
let startIdx = 0;
|
|
1279
|
+
while (startIdx < segments.length && skipPrefixes.includes(segments[startIdx])) {
|
|
1280
|
+
startIdx++;
|
|
955
1281
|
}
|
|
956
1282
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1283
|
+
const resourceSegments = segments.slice(startIdx);
|
|
1284
|
+
if (resourceSegments.length === 0) return ["root"];
|
|
1285
|
+
|
|
1286
|
+
const tags = [];
|
|
1287
|
+
|
|
1288
|
+
// Collection tag: /api/todos -> "todos"
|
|
1289
|
+
tags.push(resourceSegments[0]);
|
|
1290
|
+
|
|
1291
|
+
// Resource tag: /api/todos/42 -> "todo:42"
|
|
1292
|
+
if (resourceSegments.length >= 2 && !isNaN(Number(resourceSegments[1]))) {
|
|
1293
|
+
const collection = resourceSegments[0];
|
|
1294
|
+
const id = resourceSegments[1];
|
|
1295
|
+
const singular = collection.replace(/s$/, "");
|
|
1296
|
+
tags.push(\`\${singular}:\${id}\`);
|
|
964
1297
|
}
|
|
965
1298
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
} catch {
|
|
971
|
-
return null;
|
|
1299
|
+
// Sub-resource tags: /api/todos/42/comments -> "comments"
|
|
1300
|
+
for (let i = 2; i < resourceSegments.length; i++) {
|
|
1301
|
+
if (isNaN(Number(resourceSegments[i]))) {
|
|
1302
|
+
tags.push(resourceSegments[i]);
|
|
972
1303
|
}
|
|
973
1304
|
}
|
|
974
1305
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
this.channel.postMessage({ type: 'delete', key, value: null });
|
|
978
|
-
}
|
|
1306
|
+
return tags;
|
|
1307
|
+
}
|
|
979
1308
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1309
|
+
export function generateTagsFromMethod(method, url) {
|
|
1310
|
+
const tags = generateTags(url);
|
|
1311
|
+
|
|
1312
|
+
if (method === "GET" || method === "HEAD") {
|
|
1313
|
+
return tags;
|
|
983
1314
|
}
|
|
1315
|
+
|
|
1316
|
+
// For mutations, add method prefix
|
|
1317
|
+
return tags.map((tag) => \`\${method.toLowerCase()}-\${tag}\`);
|
|
984
1318
|
}
|
|
985
1319
|
|
|
986
|
-
export function
|
|
987
|
-
|
|
1320
|
+
export async function invalidateUrl(url) {
|
|
1321
|
+
const tags = generateTags(url);
|
|
1322
|
+
await invalidateByTags(tags);
|
|
988
1323
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1324
|
+
|
|
1325
|
+
export async function invalidateByMethod(method, url) {
|
|
1326
|
+
const tags = generateTagsFromMethod(method, url);
|
|
1327
|
+
await invalidateByTags(tags);
|
|
992
1328
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
mkdirSync(outputDir, { recursive: true });
|
|
997
|
-
const manifest = {
|
|
998
|
-
name: 'Swoff App',
|
|
999
|
-
short_name: 'Swoff',
|
|
1000
|
-
description: 'Offline-first web application',
|
|
1001
|
-
start_url: '/',
|
|
1002
|
-
display: 'standalone',
|
|
1003
|
-
background_color: '#ffffff',
|
|
1004
|
-
theme_color: '#000000',
|
|
1005
|
-
icons: [
|
|
1006
|
-
{
|
|
1007
|
-
src: '/icon-192.png',
|
|
1008
|
-
sizes: '192x192',
|
|
1009
|
-
type: 'image/png',
|
|
1010
|
-
},
|
|
1011
|
-
{
|
|
1012
|
-
src: '/icon-512.png',
|
|
1013
|
-
sizes: '512x512',
|
|
1014
|
-
type: 'image/png',
|
|
1015
|
-
},
|
|
1016
|
-
],
|
|
1017
|
-
};
|
|
1018
|
-
writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1019
|
-
generatedFiles.push("public/manifest.json");
|
|
1329
|
+
`;
|
|
1330
|
+
writeFileSync(join(swoffDir, "invalidation-tags.js"), code);
|
|
1331
|
+
generatedFiles.push("swoff/invalidation-tags.js");
|
|
1020
1332
|
}
|
|
1021
|
-
console.log(
|
|
1022
|
-
console.log('========================\n');
|
|
1333
|
+
console.log("Generating Swoff files...");
|
|
1023
1334
|
console.log(`Language: ${language}`);
|
|
1024
1335
|
console.log(`Project: ${projectRoot}`);
|
|
1025
|
-
console.log(
|
|
1336
|
+
console.log("");
|
|
1026
1337
|
if (!config.enabled) {
|
|
1027
|
-
console.log(
|
|
1338
|
+
console.log("Config generation disabled");
|
|
1028
1339
|
process.exit(0);
|
|
1029
1340
|
}
|
|
1030
|
-
console.log(
|
|
1031
|
-
generateSwInjector();
|
|
1032
|
-
console.log(' ⢠sw-injector');
|
|
1033
|
-
generateSwGeneratorBuildScript();
|
|
1034
|
-
console.log(' ⢠sw-generator build script');
|
|
1341
|
+
console.log("Generating pattern files...");
|
|
1035
1342
|
generateSwTemplate();
|
|
1036
|
-
console.log(
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1343
|
+
console.log(" sw-template");
|
|
1344
|
+
if (config.features.clientRegistration) {
|
|
1345
|
+
generateSwInjector();
|
|
1346
|
+
console.log(" sw-injector");
|
|
1347
|
+
}
|
|
1348
|
+
generateFetchWrapper();
|
|
1349
|
+
console.log(" fetch-wrapper");
|
|
1350
|
+
if (config.features.tagInvalidation || config.features.crossTabSync) {
|
|
1351
|
+
generateCache();
|
|
1352
|
+
console.log(" cache");
|
|
1042
1353
|
}
|
|
1043
1354
|
if (config.features.mutationQueue) {
|
|
1044
|
-
|
|
1045
|
-
console.log(
|
|
1355
|
+
generateStore();
|
|
1356
|
+
console.log(" store");
|
|
1357
|
+
generateReconcile();
|
|
1358
|
+
console.log(" reconcile");
|
|
1359
|
+
generateMutationQueue();
|
|
1360
|
+
console.log(" mutation-queue");
|
|
1361
|
+
}
|
|
1362
|
+
if (config.features.backgroundSync) {
|
|
1363
|
+
generateBackgroundSync();
|
|
1364
|
+
console.log(" background-sync");
|
|
1046
1365
|
}
|
|
1366
|
+
if (config.features.indexeddb) {
|
|
1367
|
+
generateIndexedDB();
|
|
1368
|
+
console.log(" indexeddb");
|
|
1369
|
+
}
|
|
1370
|
+
generateSwGeneratorBuildScript();
|
|
1371
|
+
console.log(" sw-generator");
|
|
1372
|
+
generateTypeDefinitions();
|
|
1373
|
+
console.log(" swoff.d.ts");
|
|
1047
1374
|
if (config.features.pwa) {
|
|
1048
|
-
|
|
1049
|
-
console.log(
|
|
1375
|
+
generatePwaInstall();
|
|
1376
|
+
console.log(" pwa-install");
|
|
1050
1377
|
generateManifest();
|
|
1051
|
-
console.log(
|
|
1378
|
+
console.log(" manifest.json");
|
|
1052
1379
|
}
|
|
1053
|
-
if (config.features.
|
|
1054
|
-
|
|
1055
|
-
console.log(
|
|
1380
|
+
if (config.features.tagInvalidation) {
|
|
1381
|
+
generateInvalidationTags();
|
|
1382
|
+
console.log(" invalidation-tags");
|
|
1056
1383
|
}
|
|
1057
|
-
console.log(
|
|
1058
|
-
generatedFiles.forEach((file) => console.log(`
|
|
1059
|
-
console.log(
|
|
1384
|
+
console.log("\nGenerated files:");
|
|
1385
|
+
generatedFiles.forEach((file) => console.log(` ${file}`));
|
|
1386
|
+
console.log(`\nTotal: ${generatedFiles.length} files`);
|
|
1060
1387
|
//# sourceMappingURL=swoff-files-generator.js.map
|