@swoff/cli 0.0.2 → 0.2.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.
Files changed (100) hide show
  1. package/README.md +49 -0
  2. package/bin/swoff +0 -0
  3. package/dist/__tests__/assemble-sw.test.js +145 -0
  4. package/dist/__tests__/assemble-sw.test.js.map +1 -0
  5. package/dist/__tests__/cli-integration.test.js +128 -0
  6. package/dist/__tests__/cli-integration.test.js.map +1 -0
  7. package/dist/__tests__/config-types.test.js +82 -0
  8. package/dist/__tests__/config-types.test.js.map +1 -0
  9. package/dist/__tests__/detect-language.test.js +51 -0
  10. package/dist/__tests__/detect-language.test.js.map +1 -0
  11. package/dist/__tests__/file-generators.test.js +271 -0
  12. package/dist/__tests__/file-generators.test.js.map +1 -0
  13. package/dist/__tests__/invalidation-tags.test.js +99 -0
  14. package/dist/__tests__/invalidation-tags.test.js.map +1 -0
  15. package/dist/__tests__/loader.test.js +77 -0
  16. package/dist/__tests__/loader.test.js.map +1 -0
  17. package/dist/__tests__/validator.test.js +232 -0
  18. package/dist/__tests__/validator.test.js.map +1 -0
  19. package/dist/index.js +36 -348
  20. package/dist/index.js.map +1 -1
  21. package/dist/lib/cli/colors.js +14 -0
  22. package/dist/lib/cli/colors.js.map +1 -0
  23. package/dist/lib/cli/help.js +63 -0
  24. package/dist/lib/cli/help.js.map +1 -0
  25. package/dist/lib/cli/logger.js +13 -0
  26. package/dist/lib/cli/logger.js.map +1 -0
  27. package/dist/lib/commands/add.js +58 -0
  28. package/dist/lib/commands/add.js.map +1 -0
  29. package/dist/lib/commands/clean.js +39 -0
  30. package/dist/lib/commands/clean.js.map +1 -0
  31. package/dist/lib/commands/generate.js +84 -0
  32. package/dist/lib/commands/generate.js.map +1 -0
  33. package/dist/lib/commands/info.js +44 -0
  34. package/dist/lib/commands/info.js.map +1 -0
  35. package/dist/lib/commands/init.js +46 -0
  36. package/dist/lib/commands/init.js.map +1 -0
  37. package/dist/lib/commands/validate.js +30 -0
  38. package/dist/lib/commands/validate.js.map +1 -0
  39. package/dist/lib/config/loader.js +80 -0
  40. package/dist/lib/config/loader.js.map +1 -0
  41. package/dist/lib/config/validator.js +102 -0
  42. package/dist/lib/config/validator.js.map +1 -0
  43. package/dist/lib/generators/file-generators/background-sync.js +59 -0
  44. package/dist/lib/generators/file-generators/background-sync.js.map +1 -0
  45. package/dist/lib/generators/file-generators/cache.js +55 -0
  46. package/dist/lib/generators/file-generators/cache.js.map +1 -0
  47. package/dist/lib/generators/file-generators/context.js +16 -0
  48. package/dist/lib/generators/file-generators/context.js.map +1 -0
  49. package/dist/lib/generators/file-generators/fetch-wrapper.js +80 -0
  50. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -0
  51. package/dist/lib/generators/file-generators/indexeddb.js +73 -0
  52. package/dist/lib/generators/file-generators/indexeddb.js.map +1 -0
  53. package/dist/lib/generators/file-generators/invalidation-tags.js +91 -0
  54. package/dist/lib/generators/file-generators/invalidation-tags.js.map +1 -0
  55. package/dist/lib/generators/file-generators/manifest.js +26 -0
  56. package/dist/lib/generators/file-generators/manifest.js.map +1 -0
  57. package/dist/lib/generators/file-generators/mutation-queue.js +226 -0
  58. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -0
  59. package/dist/lib/generators/file-generators/pwa-install.js +102 -0
  60. package/dist/lib/generators/file-generators/pwa-install.js.map +1 -0
  61. package/dist/lib/generators/file-generators/reconcile.js +54 -0
  62. package/dist/lib/generators/file-generators/reconcile.js.map +1 -0
  63. package/dist/lib/generators/file-generators/store.js +75 -0
  64. package/dist/lib/generators/file-generators/store.js.map +1 -0
  65. package/dist/lib/generators/file-generators/sw-generator-build.js +70 -0
  66. package/dist/lib/generators/file-generators/sw-generator-build.js.map +1 -0
  67. package/dist/lib/generators/file-generators/sw-injector.js +195 -0
  68. package/dist/lib/generators/file-generators/sw-injector.js.map +1 -0
  69. package/dist/lib/generators/file-generators/sw-template.js +143 -0
  70. package/dist/lib/generators/file-generators/sw-template.js.map +1 -0
  71. package/dist/lib/generators/file-generators/type-definitions.js +69 -0
  72. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -0
  73. package/dist/lib/generators/sw-generator.js +37 -239
  74. package/dist/lib/generators/sw-generator.js.map +1 -1
  75. package/dist/lib/generators/sw-sections/activate-handler.js +16 -0
  76. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -0
  77. package/dist/lib/generators/sw-sections/assemble-sw.js +38 -0
  78. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -0
  79. package/dist/lib/generators/sw-sections/background-sync-handler.js +106 -0
  80. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -0
  81. package/dist/lib/generators/sw-sections/config-header.js +15 -0
  82. package/dist/lib/generators/sw-sections/config-header.js.map +1 -0
  83. package/dist/lib/generators/sw-sections/default-template.js +51 -0
  84. package/dist/lib/generators/sw-sections/default-template.js.map +1 -0
  85. package/dist/lib/generators/sw-sections/fetch-handler.js +210 -0
  86. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -0
  87. package/dist/lib/generators/sw-sections/install-handler.js +35 -0
  88. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -0
  89. package/dist/lib/generators/sw-sections/message-handler.js +20 -0
  90. package/dist/lib/generators/sw-sections/message-handler.js.map +1 -0
  91. package/dist/lib/generators/sw-sections/tag-management.js +69 -0
  92. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -0
  93. package/dist/lib/generators/swoff-files-generator.js +80 -1043
  94. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  95. package/dist/lib/shared/config-types.js +87 -0
  96. package/dist/lib/shared/config-types.js.map +1 -0
  97. package/dist/lib/utils/detect-language.js +33 -0
  98. package/dist/lib/utils/detect-language.js.map +1 -0
  99. package/package.json +6 -2
  100. package/src/lib/templates/sw-template.js +23 -6
@@ -0,0 +1,210 @@
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
+ event.respondWith(networkOnly(event, event.request));
91
+ return;
92
+ }
93
+
94
+ // cache-first (default)
95
+ event.respondWith(cacheFirst(event, event.request));
96
+ });
97
+
98
+ async function cacheFirst(event, request) {
99
+ const cache = await caches.open(CACHE_NAME);
100
+ const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
101
+ const url = new URL(request.url);
102
+
103
+ const byPath = await cache.match(url.pathname);
104
+ if (byPath) return byPath;
105
+
106
+ const byRequest = await runtimeCache.match(request);
107
+ if (byRequest) return byRequest;
108
+
109
+ if (request.mode === "navigate") {
110
+ const spa = await cache.match("/index.html");
111
+ if (spa) return spa;
112
+ }
113
+
114
+ try {
115
+ const response = await fetch(request);
116
+ if (response.ok) {
117
+ const cloned = response.clone();
118
+ event.waitUntil(
119
+ (async () => {
120
+ await runtimeCache.put(request, cloned);${tagInvalidationCode}
121
+ ${trimCode} })(),
122
+ );
123
+ }
124
+ return response;
125
+ } catch {
126
+ return new Response("Offline: content not available", { status: 503 });
127
+ }
128
+ }
129
+
130
+ async function networkFirst(event, request) {
131
+ const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
132
+
133
+ try {
134
+ const response = await fetch(request);
135
+ if (response.ok) {
136
+ const cloned = response.clone();
137
+ event.waitUntil(
138
+ (async () => {
139
+ await runtimeCache.put(request, cloned);${tagInvalidationCode}
140
+ ${trimCode} })(),
141
+ );
142
+ }
143
+ return response;
144
+ } catch {
145
+ const cached = await runtimeCache.match(request);
146
+ if (cached) return cached;
147
+
148
+ if (request.mode === "navigate") {
149
+ const cache = await caches.open(CACHE_NAME);
150
+ const spa = await cache.match("/index.html");
151
+ if (spa) return spa;
152
+ }
153
+
154
+ return new Response("Offline: content not available", { status: 503 });
155
+ }
156
+ }
157
+
158
+ async function staleWhileRevalidate(event, request) {
159
+ const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
160
+ const cached = await runtimeCache.match(request);
161
+
162
+ if (cached) {
163
+ event.waitUntil(refreshCache(runtimeCache, request));
164
+ return cached;
165
+ }
166
+
167
+ try {
168
+ const response = await fetch(request);
169
+ if (response.ok) {
170
+ await runtimeCache.put(request, response.clone());${staleTagCode}
171
+ ${hasTrim ? " await trimRuntimeCache(CACHE_NAME_RUNTIME);\n" : ""} }
172
+ return response;
173
+ } catch {
174
+ return new Response("Offline: content not available", { status: 503 });
175
+ }
176
+ }
177
+
178
+ async function refreshCache(cache, request) {
179
+ try {
180
+ const response = await fetch(request);
181
+ if (response.ok) {
182
+ await cache.put(request, response.clone());
183
+ }
184
+ } catch {
185
+ // Background refresh failed - stale cache remains usable
186
+ }
187
+ }
188
+
189
+ async function cacheOnly(event, request) {
190
+ const cache = await caches.open(CACHE_NAME);
191
+ const runtimeCache = await caches.open(CACHE_NAME_RUNTIME);
192
+
193
+ const byPath = await cache.match(new URL(request.url).pathname);
194
+ if (byPath) return byPath;
195
+
196
+ const byRequest = await runtimeCache.match(request);
197
+ if (byRequest) return byRequest;
198
+
199
+ return new Response("Not in cache", { status: 404 });
200
+ }
201
+
202
+ async function networkOnly(event, request) {
203
+ try {
204
+ return await fetch(request);
205
+ } catch {
206
+ return new Response("Network error", { status: 503 });
207
+ }
208
+ }`;
209
+ }
210
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oDAgDtD,mBAAmB;EACrE,QAAQ;;;;;;;;;;;;;;;;;;oDAkB0C,mBAAmB;EACrE,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0DA8BgD,YAAY;EACpE,OAAO,CAAC,CAAC,CAAC,qDAAqD,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqCpE,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"}