@swoff/cli 0.1.0 → 0.2.1
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 +49 -0
- package/dist/__tests__/assemble-sw.test.js +145 -0
- package/dist/__tests__/assemble-sw.test.js.map +1 -0
- package/dist/__tests__/cli-integration.test.js +128 -0
- package/dist/__tests__/cli-integration.test.js.map +1 -0
- package/dist/__tests__/config-types.test.js +82 -0
- package/dist/__tests__/config-types.test.js.map +1 -0
- package/dist/__tests__/detect-language.test.js +51 -0
- package/dist/__tests__/detect-language.test.js.map +1 -0
- package/dist/__tests__/file-generators.test.js +272 -0
- package/dist/__tests__/file-generators.test.js.map +1 -0
- package/dist/__tests__/invalidation-tags.test.js +99 -0
- package/dist/__tests__/invalidation-tags.test.js.map +1 -0
- package/dist/__tests__/loader.test.js +77 -0
- package/dist/__tests__/loader.test.js.map +1 -0
- package/dist/__tests__/validator.test.js +231 -0
- package/dist/__tests__/validator.test.js.map +1 -0
- package/dist/index.js +22 -585
- package/dist/index.js.map +1 -1
- package/dist/lib/cli/colors.js +14 -0
- package/dist/lib/cli/colors.js.map +1 -0
- package/dist/lib/cli/help.js +63 -0
- package/dist/lib/cli/help.js.map +1 -0
- package/dist/lib/cli/logger.js +13 -0
- package/dist/lib/cli/logger.js.map +1 -0
- package/dist/lib/commands/add.js +58 -0
- package/dist/lib/commands/add.js.map +1 -0
- package/dist/lib/commands/clean.js +39 -0
- package/dist/lib/commands/clean.js.map +1 -0
- package/dist/lib/commands/generate.js +84 -0
- package/dist/lib/commands/generate.js.map +1 -0
- package/dist/lib/commands/info.js +44 -0
- package/dist/lib/commands/info.js.map +1 -0
- package/dist/lib/commands/init.js +46 -0
- package/dist/lib/commands/init.js.map +1 -0
- package/dist/lib/commands/validate.js +30 -0
- package/dist/lib/commands/validate.js.map +1 -0
- package/dist/lib/config/loader.js +80 -0
- package/dist/lib/config/loader.js.map +1 -0
- package/dist/lib/config/validator.js +102 -0
- package/dist/lib/config/validator.js.map +1 -0
- package/dist/lib/generators/file-generators/background-sync.js +59 -0
- package/dist/lib/generators/file-generators/background-sync.js.map +1 -0
- package/dist/lib/generators/file-generators/cache.js +55 -0
- package/dist/lib/generators/file-generators/cache.js.map +1 -0
- package/dist/lib/generators/file-generators/context.js +16 -0
- package/dist/lib/generators/file-generators/context.js.map +1 -0
- package/dist/lib/generators/file-generators/fetch-wrapper.js +80 -0
- package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -0
- package/dist/lib/generators/file-generators/indexeddb.js +73 -0
- package/dist/lib/generators/file-generators/indexeddb.js.map +1 -0
- package/dist/lib/generators/file-generators/invalidation-tags.js +91 -0
- package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -0
- package/dist/lib/generators/file-generators/manifest.js +26 -0
- package/dist/lib/generators/file-generators/manifest.js.map +1 -0
- package/dist/lib/generators/file-generators/mutation-queue.js +226 -0
- package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -0
- package/dist/lib/generators/file-generators/pwa-install.js +102 -0
- package/dist/lib/generators/file-generators/pwa-install.js.map +1 -0
- package/dist/lib/generators/file-generators/reconcile.js +54 -0
- package/dist/lib/generators/file-generators/reconcile.js.map +1 -0
- package/dist/lib/generators/file-generators/store.js +75 -0
- package/dist/lib/generators/file-generators/store.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js +70 -0
- package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-injector.js +201 -0
- package/dist/lib/generators/file-generators/sw-injector.js.map +1 -0
- package/dist/lib/generators/file-generators/sw-template.js +143 -0
- package/dist/lib/generators/file-generators/sw-template.js.map +1 -0
- package/dist/lib/generators/file-generators/type-definitions.js +69 -0
- package/dist/lib/generators/file-generators/type-definitions.js.map +1 -0
- package/dist/lib/generators/sw-generator.js +33 -569
- package/dist/lib/generators/sw-generator.js.map +1 -1
- package/dist/lib/generators/sw-sections/activate-handler.js +16 -0
- package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/assemble-sw.js +38 -0
- package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -0
- package/dist/lib/generators/sw-sections/background-sync-handler.js +106 -0
- package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/config-header.js +15 -0
- package/dist/lib/generators/sw-sections/config-header.js.map +1 -0
- package/dist/lib/generators/sw-sections/default-template.js +51 -0
- package/dist/lib/generators/sw-sections/default-template.js.map +1 -0
- package/dist/lib/generators/sw-sections/fetch-handler.js +203 -0
- package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/install-handler.js +35 -0
- package/dist/lib/generators/sw-sections/install-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/message-handler.js +20 -0
- package/dist/lib/generators/sw-sections/message-handler.js.map +1 -0
- package/dist/lib/generators/sw-sections/tag-management.js +69 -0
- package/dist/lib/generators/sw-sections/tag-management.js.map +1 -0
- package/dist/lib/generators/swoff-files-generator.js +78 -1368
- package/dist/lib/generators/swoff-files-generator.js.map +1 -1
- package/dist/lib/shared/config-types.js +87 -0
- package/dist/lib/shared/config-types.js.map +1 -0
- package/dist/lib/utils/detect-language.js +33 -0
- package/dist/lib/utils/detect-language.js.map +1 -0
- package/package.json +5 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the SW activate event handler (old cache cleanup).
|
|
3
|
+
*/
|
|
4
|
+
export function generateActivateHandler() {
|
|
5
|
+
return `
|
|
6
|
+
self.addEventListener("activate", (event) => {
|
|
7
|
+
event.waitUntil(
|
|
8
|
+
caches.keys().then((keys) =>
|
|
9
|
+
Promise.all(
|
|
10
|
+
keys.filter((key) => key !== CACHE_NAME && key !== CACHE_NAME_RUNTIME).map((key) => caches.delete(key))
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
);
|
|
14
|
+
});`;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=activate-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activate-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/activate-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,uBAAuB;IACrC,OAAO;;;;;;;;;IASL,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembles the complete service worker string from config and version.
|
|
3
|
+
* Calls individual section generators and performs placeholder replacement.
|
|
4
|
+
*/
|
|
5
|
+
import { getDefaultTemplate } from "./default-template.js";
|
|
6
|
+
import { generateConfigHeader } from "./config-header.js";
|
|
7
|
+
import { generateInstallHandler } from "./install-handler.js";
|
|
8
|
+
import { generateActivateHandler } from "./activate-handler.js";
|
|
9
|
+
import { generateMessageHandler } from "./message-handler.js";
|
|
10
|
+
import { generateFetchHandler } from "./fetch-handler.js";
|
|
11
|
+
import { generateTagManagement } from "./tag-management.js";
|
|
12
|
+
import { generateBackgroundSyncHandler } from "./background-sync-handler.js";
|
|
13
|
+
export function assembleSW(config, version) {
|
|
14
|
+
const { serviceWorker, features } = config;
|
|
15
|
+
const baseAssets = ["/", "/index.html"];
|
|
16
|
+
const pwaAssets = features.pwa ? ["/manifest.json"] : [];
|
|
17
|
+
const assetsToCache = [...baseAssets, ...pwaAssets];
|
|
18
|
+
let sw = getDefaultTemplate();
|
|
19
|
+
sw = sw.replace("// [[CACHE_NAME]]", `CACHE_NAME = 'sw-v${version}'`);
|
|
20
|
+
sw = sw.replace("// [[ASSETS_LIST]]", `ASSETS_TO_CACHE = ${JSON.stringify(assetsToCache.map((url) => ({ url, options: {} })), null, 2)}`);
|
|
21
|
+
sw = sw.replace("// [[AUTO_SKIP_WAITING]]", `const AUTO_SKIP_WAITING = ${config.serviceWorker.autoActivate};`);
|
|
22
|
+
sw = sw.replace("// [[FETCH_HANDLER]]", generateFetchHandler(serviceWorker, features.tagInvalidation));
|
|
23
|
+
sw = sw.replace("// [[ACTIVATE_HANDLER]]", generateActivateHandler());
|
|
24
|
+
sw = sw.replace("// [[INSTALL_HANDLER]]", generateInstallHandler());
|
|
25
|
+
sw = sw.replace("// [[MESSAGE_HANDLER]]", generateMessageHandler(features.tagInvalidation));
|
|
26
|
+
if (features.tagInvalidation) {
|
|
27
|
+
sw = sw.replace("// [[TAG_MANAGEMENT]]", generateTagManagement());
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
sw = sw.replace("// [[TAG_MANAGEMENT]]", "");
|
|
31
|
+
}
|
|
32
|
+
sw = `${generateConfigHeader(config, version)}\n\n${sw}`;
|
|
33
|
+
if (features.backgroundSync) {
|
|
34
|
+
sw += `\n\n${generateBackgroundSyncHandler()}`;
|
|
35
|
+
}
|
|
36
|
+
return sw;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=assemble-sw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assemble-sw.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/assemble-sw.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAE7E,MAAM,UAAU,UAAU,CAAC,MAAmB,EAAE,OAAe;IAC7D,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAE3C,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzD,MAAM,aAAa,GAAG,CAAC,GAAG,UAAU,EAAE,GAAG,SAAS,CAAC,CAAC;IAEpD,IAAI,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAE9B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,qBAAqB,OAAO,GAAG,CAAC,CAAC;IACtE,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,oBAAoB,EAAE,qBAAqB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1I,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,0BAA0B,EAAE,6BAA6B,MAAM,CAAC,aAAa,CAAC,YAAY,GAAG,CAAC,CAAC;IAE/G,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;IACvG,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,yBAAyB,EAAE,uBAAuB,EAAE,CAAC,CAAC;IACtE,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,wBAAwB,EAAE,sBAAsB,EAAE,CAAC,CAAC;IACpE,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,wBAAwB,EAAE,sBAAsB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;IAE5F,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;QAC7B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,uBAAuB,EAAE,qBAAqB,EAAE,CAAC,CAAC;IACpE,CAAC;SAAM,CAAC;QACN,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,EAAE,GAAG,GAAG,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;IAEzD,IAAI,QAAQ,CAAC,cAAc,EAAE,CAAC;QAC5B,EAAE,IAAI,OAAO,6BAA6B,EAAE,EAAE,CAAC;IACjD,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates Background Sync event handler for the SW.
|
|
3
|
+
* Processes mutation queue when browser sync fires.
|
|
4
|
+
*/
|
|
5
|
+
export function generateBackgroundSyncHandler() {
|
|
6
|
+
return `
|
|
7
|
+
self.addEventListener("sync", (event) => {
|
|
8
|
+
if (event.tag === "sync-mutations") {
|
|
9
|
+
event.waitUntil(processMutationQueueInSW());
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function processMutationQueueInSW() {
|
|
14
|
+
const SW_DB_NAME = "swoff-queue";
|
|
15
|
+
const SW_STORE_NAME = "mutations";
|
|
16
|
+
const SW_MAX_RETRIES = 5;
|
|
17
|
+
|
|
18
|
+
let succeeded = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
const tagsToInvalidate = new Set();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const db = await new Promise((resolve, reject) => {
|
|
24
|
+
const request = indexedDB.open(SW_DB_NAME, 1);
|
|
25
|
+
request.onupgradeneeded = (e) => {
|
|
26
|
+
const db = e.target.result;
|
|
27
|
+
if (!db.objectStoreNames.contains(SW_STORE_NAME)) {
|
|
28
|
+
const store = db.createObjectStore(SW_STORE_NAME, { keyPath: "id" });
|
|
29
|
+
store.createIndex("by-timestamp", "timestamp");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
33
|
+
request.onerror = (e) => reject(e.target.error);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const tx = db.transaction(SW_STORE_NAME, "readonly");
|
|
37
|
+
const store = tx.objectStore(SW_STORE_NAME);
|
|
38
|
+
const index = store.index("by-timestamp");
|
|
39
|
+
const queue = await new Promise((resolve, reject) => {
|
|
40
|
+
const request = index.getAll();
|
|
41
|
+
request.onsuccess = () => resolve(request.result);
|
|
42
|
+
request.onerror = () => reject(request.error);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const item of queue) {
|
|
46
|
+
if (item.retryCount >= SW_MAX_RETRIES) {
|
|
47
|
+
await removeFromSWQueue(db, item.id);
|
|
48
|
+
failed++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(item.url, {
|
|
53
|
+
method: item.method,
|
|
54
|
+
headers: { "Content-Type": "application/json", ...item.headers },
|
|
55
|
+
body: JSON.stringify(item.body),
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
|
|
58
|
+
|
|
59
|
+
if (item.tags) {
|
|
60
|
+
item.tags.forEach((tag) => tagsToInvalidate.add(tag));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await removeFromSWQueue(db, item.id);
|
|
64
|
+
succeeded++;
|
|
65
|
+
} catch {
|
|
66
|
+
item.retryCount++;
|
|
67
|
+
await updateInSWQueue(db, item);
|
|
68
|
+
failed++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("Background sync failed:", err);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const tag of tagsToInvalidate) {
|
|
76
|
+
await invalidateByTag(tag);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const clients = await self.clients.matchAll();
|
|
80
|
+
for (const client of clients) {
|
|
81
|
+
client.postMessage({
|
|
82
|
+
type: "BACKGROUND_SYNC_COMPLETE",
|
|
83
|
+
detail: { succeeded, failed, tags: [...tagsToInvalidate] },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function removeFromSWQueue(db, id) {
|
|
89
|
+
const tx = db.transaction(SW_STORE_NAME, "readwrite");
|
|
90
|
+
tx.objectStore(SW_STORE_NAME).delete(id);
|
|
91
|
+
await new Promise((resolve, reject) => {
|
|
92
|
+
tx.oncomplete = () => resolve();
|
|
93
|
+
tx.onerror = () => reject(tx.error);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function updateInSWQueue(db, item) {
|
|
98
|
+
const tx = db.transaction(SW_STORE_NAME, "readwrite");
|
|
99
|
+
tx.objectStore(SW_STORE_NAME).put(item);
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
tx.oncomplete = () => resolve();
|
|
102
|
+
tx.onerror = () => reject(tx.error);
|
|
103
|
+
});
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=background-sync-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background-sync-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/background-sync-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,6BAA6B;IAC3C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkGP,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the SW file header comment with config metadata.
|
|
3
|
+
*/
|
|
4
|
+
export function generateConfigHeader(config, resolvedVersion) {
|
|
5
|
+
return `/**
|
|
6
|
+
* Swoff Service Worker - Auto-Generated
|
|
7
|
+
* Generated from swoff.config.json
|
|
8
|
+
* DO NOT EDIT MANUALLY
|
|
9
|
+
* Version: ${resolvedVersion}
|
|
10
|
+
* Features: versionedSw=${config.features.versionedSw}, offlineReads=${config.features.offlineReads}, mutationQueue=${config.features.mutationQueue}, backgroundSync=${config.features.backgroundSync}, tagInvalidation=${config.features.tagInvalidation}
|
|
11
|
+
* Default Strategy: ${config.serviceWorker.defaultStrategy}
|
|
12
|
+
* See: https://swoff.netlify.app/docs
|
|
13
|
+
*/`;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=config-header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-header.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/config-header.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,oBAAoB,CAAC,MAAmB,EAAE,eAAuB;IAC/E,OAAO;;;;cAIK,eAAe;2BACF,MAAM,CAAC,QAAQ,CAAC,WAAW,kBAAkB,MAAM,CAAC,QAAQ,CAAC,YAAY,mBAAmB,MAAM,CAAC,QAAQ,CAAC,aAAa,oBAAoB,MAAM,CAAC,QAAQ,CAAC,cAAc,qBAAqB,MAAM,CAAC,QAAQ,CAAC,eAAe;uBACnO,MAAM,CAAC,aAAa,CAAC,eAAe;;IAEvD,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default SW template with placeholder markers.
|
|
3
|
+
* Placeholders are replaced during generation with feature-specific code.
|
|
4
|
+
*/
|
|
5
|
+
export function getDefaultTemplate() {
|
|
6
|
+
return `let CACHE_NAME = "";
|
|
7
|
+
let ASSETS_TO_CACHE = [];
|
|
8
|
+
|
|
9
|
+
// [[CACHE_NAME]]
|
|
10
|
+
// [[ASSETS_LIST]]
|
|
11
|
+
// [[AUTO_SKIP_WAITING]]
|
|
12
|
+
|
|
13
|
+
const CACHE_NAME_RUNTIME = "swoff-runtime";
|
|
14
|
+
|
|
15
|
+
// [[INSTALL_HANDLER]]
|
|
16
|
+
// [[ACTIVATE_HANDLER]]
|
|
17
|
+
// [[MESSAGE_HANDLER]]
|
|
18
|
+
// [[FETCH_HANDLER]]
|
|
19
|
+
// [[TAG_MANAGEMENT]]
|
|
20
|
+
|
|
21
|
+
const SWOFF = {
|
|
22
|
+
cache: {
|
|
23
|
+
async get(key) {
|
|
24
|
+
const cache = await caches.open(CACHE_NAME);
|
|
25
|
+
return cache.match(key);
|
|
26
|
+
},
|
|
27
|
+
async put(request, response) {
|
|
28
|
+
const cache = await caches.open(CACHE_NAME);
|
|
29
|
+
await cache.put(request, response);
|
|
30
|
+
},
|
|
31
|
+
async delete(request) {
|
|
32
|
+
const cache = await caches.open(CACHE_NAME);
|
|
33
|
+
return cache.delete(request);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
network: {
|
|
37
|
+
async fetch(request, options = {}) {
|
|
38
|
+
try {
|
|
39
|
+
return await fetch(request, options);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(\`Network request failed: \${error.message}\`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (typeof self !== 'undefined') {
|
|
48
|
+
self.SWOFF = SWOFF;
|
|
49
|
+
}`;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=default-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-template.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/default-template.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,kBAAkB;IAChC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CP,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the SW fetch event handler with all caching strategies.
|
|
3
|
+
*/
|
|
4
|
+
export function generateFetchHandler(swConfig, tagInvalidation) {
|
|
5
|
+
const { defaultStrategy, strategies, maxCacheEntries, maxCacheAge } = swConfig;
|
|
6
|
+
const hasTrim = (maxCacheEntries ?? 0) > 0 || (maxCacheAge ?? 0) > 0;
|
|
7
|
+
const tagInvalidationCode = tagInvalidation ? `
|
|
8
|
+
const tagsHeader = event.request.headers.get("X-SW-Cache-Tags");
|
|
9
|
+
if (tagsHeader) {
|
|
10
|
+
const url = new URL(event.request.url).href;
|
|
11
|
+
const tags = tagsHeader.split(",").map((t) => t.trim());
|
|
12
|
+
await cacheTagUrl(url, tags);
|
|
13
|
+
}` : "";
|
|
14
|
+
const staleTagCode = tagInvalidation ? `
|
|
15
|
+
const tagsHeader = request.headers.get("X-SW-Cache-Tags");
|
|
16
|
+
if (tagsHeader) {
|
|
17
|
+
const url = new URL(request.url).href;
|
|
18
|
+
const tags = tagsHeader.split(",").map((t) => t.trim());
|
|
19
|
+
await cacheTagUrl(url, tags);
|
|
20
|
+
}` : "";
|
|
21
|
+
const trimCode = hasTrim ? ` await trimRuntimeCache(CACHE_NAME_RUNTIME);\n` : "";
|
|
22
|
+
const trimFunction = hasTrim ? `
|
|
23
|
+
const MAX_CACHE_ENTRIES = ${maxCacheEntries ?? 0};
|
|
24
|
+
const MAX_CACHE_AGE = ${maxCacheAge ?? 0};
|
|
25
|
+
|
|
26
|
+
async function trimRuntimeCache(cacheName) {
|
|
27
|
+
const cache = await caches.open(cacheName);
|
|
28
|
+
|
|
29
|
+
if (MAX_CACHE_ENTRIES > 0) {
|
|
30
|
+
const keys = await cache.keys();
|
|
31
|
+
if (keys.length >= MAX_CACHE_ENTRIES) {
|
|
32
|
+
const toDelete = keys.slice(0, keys.length - MAX_CACHE_ENTRIES + 1);
|
|
33
|
+
await Promise.all(toDelete.map((key) => cache.delete(key)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (MAX_CACHE_AGE > 0) {
|
|
38
|
+
const keys = await cache.keys();
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
for (const request of keys) {
|
|
41
|
+
const response = await cache.match(request);
|
|
42
|
+
const dateHeader = response?.headers.get("date");
|
|
43
|
+
if (dateHeader) {
|
|
44
|
+
const age = now - new Date(dateHeader).getTime();
|
|
45
|
+
if (age > MAX_CACHE_AGE) {
|
|
46
|
+
await cache.delete(request);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
` : "";
|
|
53
|
+
return `${trimFunction}
|
|
54
|
+
function isReadRequest(request) {
|
|
55
|
+
const strategy = request.headers.get("X-SW-Cache-Strategy");
|
|
56
|
+
if (strategy === "read") return true;
|
|
57
|
+
if (strategy === "mutation") return false;
|
|
58
|
+
return request.method === "GET" || request.method === "HEAD";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function determineCacheStrategy(request, customStrategies, defaultStrategy) {
|
|
62
|
+
const url = request.url;
|
|
63
|
+
for (const [pattern, strategy] of Object.entries(customStrategies)) {
|
|
64
|
+
if (url.includes(pattern.replace("*", ""))) return strategy;
|
|
65
|
+
}
|
|
66
|
+
return defaultStrategy;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
self.addEventListener("fetch", (event) => {
|
|
70
|
+
if (!isReadRequest(event.request)) return;
|
|
71
|
+
|
|
72
|
+
const strategy = determineCacheStrategy(event.request, ${JSON.stringify(strategies)}, "${defaultStrategy}");
|
|
73
|
+
|
|
74
|
+
if (strategy === "stale-while-revalidate" || event.request.headers.get("X-SW-Stale") === "true") {
|
|
75
|
+
event.respondWith(staleWhileRevalidate(event, event.request));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (strategy === "network-first") {
|
|
80
|
+
event.respondWith(networkFirst(event, event.request));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (strategy === "cache-only") {
|
|
85
|
+
event.respondWith(cacheOnly(event, event.request));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (strategy === "network-only") {
|
|
90
|
+
|
|
91
|
+
if (strategy === "network-only") {
|
|
92
|
+
event.respondWith(networkOnly(event, event.request));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// cache-first (default)
|
|
97
|
+
event.respondWith(cacheFirst(event, event.request));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
async function cacheFirst(event, request) {
|
|
101
|
+
const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
|
|
102
|
+
|
|
103
|
+
const byRequest = await runtimeCache.match(request);
|
|
104
|
+
if (byRequest) return byRequest;
|
|
105
|
+
|
|
106
|
+
if (request.mode === "navigate") {
|
|
107
|
+
const spa = await cache.match("/index.html");
|
|
108
|
+
if (spa) return spa;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(request);
|
|
113
|
+
if (response.ok) {
|
|
114
|
+
const cloned = response.clone();
|
|
115
|
+
event.waitUntil(
|
|
116
|
+
(async () => {
|
|
117
|
+
await runtimeCache.put(request, cloned);${tagInvalidationCode}
|
|
118
|
+
${trimCode} })(),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return response;
|
|
122
|
+
} catch {
|
|
123
|
+
return new Response("Offline: content not available", { status: 503 });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function networkFirst(event, request) {
|
|
128
|
+
const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(request);
|
|
132
|
+
if (response.ok) {
|
|
133
|
+
const cloned = response.clone();
|
|
134
|
+
event.waitUntil(
|
|
135
|
+
(async () => {
|
|
136
|
+
await runtimeCache.put(request, cloned);${tagInvalidationCode}
|
|
137
|
+
${trimCode} })(),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return response;
|
|
141
|
+
} catch {
|
|
142
|
+
const cached = await runtimeCache.match(request);
|
|
143
|
+
if (cached) return cached;
|
|
144
|
+
|
|
145
|
+
if (request.mode === "navigate") {
|
|
146
|
+
const cache = await caches.open(CACHE_NAME);
|
|
147
|
+
const spa = await cache.match("/index.html");
|
|
148
|
+
if (spa) return spa;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return new Response("Offline: content not available", { status: 503 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function staleWhileRevalidate(event, request) {
|
|
156
|
+
const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
|
|
157
|
+
const cached = await runtimeCache.match(request);
|
|
158
|
+
|
|
159
|
+
if (cached) {
|
|
160
|
+
event.waitUntil(refreshCache(runtimeCache, request));
|
|
161
|
+
return cached;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(request);
|
|
166
|
+
if (response.ok) {
|
|
167
|
+
await runtimeCache.put(request, response.clone());${staleTagCode}
|
|
168
|
+
${hasTrim ? " await trimRuntimeCache(CACHE_NAME_RUNTIME);\n" : ""} }
|
|
169
|
+
return response;
|
|
170
|
+
} catch {
|
|
171
|
+
return new Response("Offline: content not available", { status: 503 });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function refreshCache(cache, request) {
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(request);
|
|
178
|
+
if (response.ok) {
|
|
179
|
+
await cache.put(request, response.clone());
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Background refresh failed - stale cache remains usable
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function cacheOnly(event, request) {
|
|
187
|
+
const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
|
|
188
|
+
|
|
189
|
+
const byRequest = await runtimeCache.match(request);
|
|
190
|
+
if (byRequest) return byRequest;
|
|
191
|
+
|
|
192
|
+
return new Response("Not in cache", { status: 404 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function networkOnly(event, request) {
|
|
196
|
+
try {
|
|
197
|
+
return await fetch(request);
|
|
198
|
+
} catch {
|
|
199
|
+
return new Response("Network error", { status: 503 });
|
|
200
|
+
}
|
|
201
|
+
}`;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=fetch-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/fetch-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,oBAAoB,CAClC,QAAyH,EACzH,eAAwB;IAExB,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC;IAE/E,MAAM,OAAO,GAAG,CAAC,eAAe,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAErE,MAAM,mBAAmB,GAAG,eAAe,CAAC,CAAC,CAAC;;;;;;YAMpC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEhB,MAAM,YAAY,GAAG,eAAe,CAAC,CAAC,CAAC;;;;;;QAMjC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEZ,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,uDAAuD,CAAC,CAAC,CAAC,EAAE,CAAC;IAExF,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC;4BACL,eAAe,IAAI,CAAC;wBACxB,WAAW,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BvC,CAAC,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,GAAG,YAAY;;;;;;;;;;;;;;;;;;;2DAmBmC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oDA6CtD,mBAAmB;EACrE,QAAQ;;;;;;;;;;;;;;;;;;oDAkB0C,mBAAmB;EACrE,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0DA8BgD,YAAY;EACpE,OAAO,CAAC,CAAC,CAAC,qDAAqD,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCpE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the SW install event handler (asset pre-caching with progress).
|
|
3
|
+
*/
|
|
4
|
+
export function generateInstallHandler() {
|
|
5
|
+
return `
|
|
6
|
+
self.addEventListener("install", (event) => {
|
|
7
|
+
event.waitUntil(
|
|
8
|
+
(async () => {
|
|
9
|
+
const cache = await caches.open(CACHE_NAME);
|
|
10
|
+
let downloaded = 0;
|
|
11
|
+
for (const asset of ASSETS_TO_CACHE) {
|
|
12
|
+
try {
|
|
13
|
+
const request = new Request(asset.url, asset.options);
|
|
14
|
+
await cache.add(request);
|
|
15
|
+
downloaded++;
|
|
16
|
+
const percent = Math.round((downloaded / ASSETS_TO_CACHE.length) * 100);
|
|
17
|
+
const clients = await self.clients.matchAll({ includeUncontrolled: true });
|
|
18
|
+
clients.forEach((client) => {
|
|
19
|
+
client.postMessage({
|
|
20
|
+
type: "SW_PROGRESS",
|
|
21
|
+
percent,
|
|
22
|
+
downloaded,
|
|
23
|
+
total: ASSETS_TO_CACHE.length,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(\`Failed to cache \${asset.url}:\`, err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (AUTO_SKIP_WAITING) self.skipWaiting();
|
|
31
|
+
})(),
|
|
32
|
+
);
|
|
33
|
+
});`;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=install-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/install-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4BL,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the SW message event handler (SKIP_WAITING, INVALIDATE_TAG).
|
|
3
|
+
*/
|
|
4
|
+
export function generateMessageHandler(tagInvalidation) {
|
|
5
|
+
let code = `
|
|
6
|
+
self.addEventListener("message", (event) => {
|
|
7
|
+
if (event.data.type === "SKIP_WAITING") {
|
|
8
|
+
self.skipWaiting();
|
|
9
|
+
}`;
|
|
10
|
+
if (tagInvalidation) {
|
|
11
|
+
code += `
|
|
12
|
+
if (event.data.type === "INVALIDATE_TAG" && event.data.tag) {
|
|
13
|
+
event.waitUntil(invalidateByTag(event.data.tag));
|
|
14
|
+
}`;
|
|
15
|
+
}
|
|
16
|
+
code += `
|
|
17
|
+
});`;
|
|
18
|
+
return code;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=message-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-handler.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/message-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,sBAAsB,CAAC,eAAwB;IAC7D,IAAI,IAAI,GAAG;;;;IAIT,CAAC;IAEH,IAAI,eAAe,EAAE,CAAC;QACpB,IAAI,IAAI;;;IAGR,CAAC;IACH,CAAC;IAED,IAAI,IAAI;IACN,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates IndexedDB-based tag invalidation logic for the SW.
|
|
3
|
+
*/
|
|
4
|
+
export function generateTagManagement() {
|
|
5
|
+
return `
|
|
6
|
+
const TAG_DB_NAME = "swoff-cache-tags";
|
|
7
|
+
const TAG_STORE_NAME = "tags";
|
|
8
|
+
|
|
9
|
+
function openTagDB() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const request = indexedDB.open(TAG_DB_NAME, 1);
|
|
12
|
+
request.onupgradeneeded = (e) => {
|
|
13
|
+
const db = e.target.result;
|
|
14
|
+
if (!db.objectStoreNames.contains(TAG_STORE_NAME)) {
|
|
15
|
+
const store = db.createObjectStore(TAG_STORE_NAME, { keyPath: "url" });
|
|
16
|
+
store.createIndex("by-tag", "tags", { multiEntry: true });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
request.onsuccess = (e) => resolve(e.target.result);
|
|
20
|
+
request.onerror = (e) => reject(e.target.error);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function cacheTagUrl(url, tags) {
|
|
25
|
+
const db = await openTagDB();
|
|
26
|
+
const tx = db.transaction(TAG_STORE_NAME, "readwrite");
|
|
27
|
+
const store = tx.objectStore(TAG_STORE_NAME);
|
|
28
|
+
store.put({ url, tags });
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
tx.oncomplete = () => resolve();
|
|
31
|
+
tx.onerror = () => reject(tx.error);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function invalidateByTag(tag) {
|
|
36
|
+
const db = await openTagDB();
|
|
37
|
+
const tx = db.transaction(TAG_STORE_NAME, "readonly");
|
|
38
|
+
const store = tx.objectStore(TAG_STORE_NAME);
|
|
39
|
+
const index = store.index("by-tag");
|
|
40
|
+
const entries = await new Promise((resolve, reject) => {
|
|
41
|
+
const request = index.getAll(tag);
|
|
42
|
+
request.onsuccess = () => resolve(request.result);
|
|
43
|
+
request.onerror = () => reject(request.error);
|
|
44
|
+
});
|
|
45
|
+
await db.close();
|
|
46
|
+
|
|
47
|
+
const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
await runtimeCache.delete(entry.url);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const writeDb = await openTagDB();
|
|
53
|
+
const writeTx = writeDb.transaction(TAG_STORE_NAME, "readwrite");
|
|
54
|
+
const writeStore = writeTx.objectStore(TAG_STORE_NAME);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
writeStore.delete(entry.url);
|
|
57
|
+
}
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
writeTx.oncomplete = () => resolve();
|
|
60
|
+
writeTx.onerror = () => reject(writeTx.error);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const clients = await self.clients.matchAll();
|
|
64
|
+
clients.forEach((client) => {
|
|
65
|
+
client.postMessage({ type: "TAG_INVALIDATED", tag });
|
|
66
|
+
});
|
|
67
|
+
}`;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=tag-management.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tag-management.js","sourceRoot":"","sources":["../../../../src/lib/generators/sw-sections/tag-management.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,UAAU,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DP,CAAC;AACH,CAAC"}
|