offline-page-kit 0.1.1 β 0.2.2
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/LICENSE +0 -0
- package/README.md +264 -0
- package/dist/cli.cjs +12 -116
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +12 -116
- package/dist/cli.js.map +1 -1
- package/package.json +41 -5
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<!-- =============================================== -->
|
|
2
|
+
<!-- π₯ Animated SVG Header Banner -->
|
|
3
|
+
<!-- =============================================== -->
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://capsule-render.vercel.app/api?type=waving&color=ff7a00&height=200§ion=header&text=offline-page-kit&fontSize=42&fontColor=ffffff&animation=fadeIn&fontAlignY=38" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<b>Framework-agnostic Offline Fallback System</b><br/>
|
|
11
|
+
Lightweight β’ Deterministic β’ Production-Safe
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<!-- =============================================== -->
|
|
17
|
+
<!-- π Social & Community Links -->
|
|
18
|
+
<!-- =============================================== -->
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
π <b>Website:</b> <a href="https://devcrack.dev">devcrack.dev</a> |
|
|
22
|
+
βΆοΈ <b>YouTube:</b> <a href="https://www.youtube.com/@dev-crack">DevCrack Channel</a><br/>
|
|
23
|
+
πΉ <b>Facebook:</b> <a href="https://web.facebook.com/profile.php?id=61573018852861">Profile</a> |
|
|
24
|
+
πΉ <b>Community Group:</b> <a href="https://web.facebook.com/groups/1672263410026163">Join Community</a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<!-- =============================================== -->
|
|
30
|
+
<!-- π’ NPM + CI + Coverage Badges -->
|
|
31
|
+
<!-- =============================================== -->
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<a href="https://www.npmjs.com/package/offline-page-kit">
|
|
35
|
+
<img src="https://img.shields.io/npm/v/offline-page-kit?style=for-the-badge&color=orange" alt="npm version">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://www.npmjs.com/package/offline-page-kit">
|
|
38
|
+
<img src="https://img.shields.io/npm/dm/offline-page-kit?style=for-the-badge&color=blue" alt="npm downloads">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://bundlephobia.com/package/offline-page-kit">
|
|
41
|
+
<img src="https://img.shields.io/bundlephobia/minzip/offline-page-kit?style=for-the-badge&color=purple" alt="bundle size">
|
|
42
|
+
</a>
|
|
43
|
+
<img src="https://img.shields.io/github/actions/workflow/status/YOUR_GITHUB_USERNAME/offline-page-kit/ci.yml?style=for-the-badge" alt="CI status">
|
|
44
|
+
<img src="https://img.shields.io/codecov/c/github/YOUR_GITHUB_USERNAME/offline-page-kit?style=for-the-badge" alt="coverage">
|
|
45
|
+
<img src="https://img.shields.io/badge/TypeScript-Ready-3178c6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript Ready">
|
|
46
|
+
<img src="https://img.shields.io/badge/Zero-Dependencies-black?style=for-the-badge" alt="Zero Dependencies">
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
# π¦ offline-page-kit
|
|
52
|
+
|
|
53
|
+
> Minimal, framework-agnostic Service Worker generator that prevents blank screens during network failure.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# π‘ Professional npm Description
|
|
58
|
+
|
|
59
|
+
**offline-page-kit** is a lightweight, zero-dependency Service Worker toolkit that provides deterministic offline fallback behavior for modern web applications.
|
|
60
|
+
|
|
61
|
+
It guarantees that when network connectivity fails, users see a controlled offline experience instead of browser error pages or blank screens.
|
|
62
|
+
|
|
63
|
+
Designed for production-grade stability across:
|
|
64
|
+
|
|
65
|
+
- Next.js
|
|
66
|
+
- Vite
|
|
67
|
+
- React
|
|
68
|
+
- Vue
|
|
69
|
+
- Static sites
|
|
70
|
+
- Any public-folder architecture
|
|
71
|
+
|
|
72
|
+
No Workbox.
|
|
73
|
+
No complex runtime caching layers.
|
|
74
|
+
No unpredictable asset behavior.
|
|
75
|
+
|
|
76
|
+
Just reliable navigation fallback.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# π Quick Start
|
|
81
|
+
|
|
82
|
+
### 1οΈβ£ Install
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npm install offline-page-kit
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2οΈβ£ Generate Service Worker
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
npx offline-page-kit init --outDir public
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This generates:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
public/
|
|
98
|
+
βββ sw.js
|
|
99
|
+
βββ offline.html
|
|
100
|
+
βββ offline.svg
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3οΈβ£ Register in Client
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { registerOfflineKit } from "offline-page-kit";
|
|
107
|
+
|
|
108
|
+
registerOfflineKit({ debug: true });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Done β
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
# π§ Architecture
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Browser
|
|
119
|
+
β
|
|
120
|
+
Service Worker
|
|
121
|
+
β
|
|
122
|
+
Fetch Navigation Request
|
|
123
|
+
ββ Network OK β return live page
|
|
124
|
+
ββ Network FAIL β return cached offline.html
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Only navigation requests are intercepted.
|
|
128
|
+
|
|
129
|
+
No API mutation.
|
|
130
|
+
No asset rewriting.
|
|
131
|
+
No hidden caching layers.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
# π Lifecycle
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
Install β Activate β Control β Fetch
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Install
|
|
142
|
+
- Caches offline page + fallback image
|
|
143
|
+
- Uses Promise.allSettled() to avoid failure
|
|
144
|
+
|
|
145
|
+
### Activate
|
|
146
|
+
- Immediately claims clients
|
|
147
|
+
|
|
148
|
+
### Fetch
|
|
149
|
+
- Applies network-first
|
|
150
|
+
- Falls back to offline page
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
# π§© Framework Examples
|
|
155
|
+
|
|
156
|
+
## Next.js (App Router)
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
"use client";
|
|
160
|
+
|
|
161
|
+
import { useEffect } from "react";
|
|
162
|
+
import { registerOfflineKit } from "offline-page-kit";
|
|
163
|
+
|
|
164
|
+
export default function SWRegister() {
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
registerOfflineKit({ debug: true });
|
|
167
|
+
}, []);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Vite / React
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { registerOfflineKit } from "offline-page-kit";
|
|
178
|
+
|
|
179
|
+
registerOfflineKit();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
# β CLI Options
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
npx offline-page-kit init \
|
|
188
|
+
--outDir public \
|
|
189
|
+
--offlinePage /offline.html \
|
|
190
|
+
--offlineImage /offline.svg \
|
|
191
|
+
--cacheName my-app-cache
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Option | Default | Description |
|
|
195
|
+
|--------|---------|------------|
|
|
196
|
+
| outDir | public | Static folder |
|
|
197
|
+
| offlinePage | /offline.html | Navigation fallback |
|
|
198
|
+
| offlineImage | /offline.svg | Fallback asset |
|
|
199
|
+
| cacheName | offline-page-kit | Cache namespace |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
# π Security
|
|
204
|
+
|
|
205
|
+
- Requires HTTPS (or localhost)
|
|
206
|
+
- Does not cache authenticated APIs
|
|
207
|
+
- Does not alter CORS behavior
|
|
208
|
+
- Per-origin scoped
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
# β‘ Performance
|
|
213
|
+
|
|
214
|
+
- Zero runtime dependencies
|
|
215
|
+
- Minimal memory usage
|
|
216
|
+
- No background sync
|
|
217
|
+
- No bundle overhead
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
# π§ͺ Testing
|
|
222
|
+
|
|
223
|
+
1. Load site
|
|
224
|
+
2. Reload once
|
|
225
|
+
3. DevTools β Application β Service Workers
|
|
226
|
+
4. Enable Offline
|
|
227
|
+
5. Refresh
|
|
228
|
+
|
|
229
|
+
Offline page should render.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
# π’ Production Notes
|
|
234
|
+
|
|
235
|
+
- Always deploy over HTTPS
|
|
236
|
+
- Version cache when updating logic
|
|
237
|
+
- Avoid multiple SWs in same scope
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
# π§ Roadmap
|
|
242
|
+
|
|
243
|
+
- Asset caching layer
|
|
244
|
+
- API strategy module
|
|
245
|
+
- Auto public folder scan
|
|
246
|
+
- Cache version hashing
|
|
247
|
+
- Metrics integration
|
|
248
|
+
- Advanced fallback strategies
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
# π License
|
|
253
|
+
|
|
254
|
+
MIT
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
<p align="center">
|
|
259
|
+
Built for modern web applications.
|
|
260
|
+
</p>
|
|
261
|
+
|
|
262
|
+
<p align="center">
|
|
263
|
+
<img src="https://capsule-render.vercel.app/api?type=waving&color=ff7a00&height=120§ion=footer"/>
|
|
264
|
+
</p>
|
package/dist/cli.cjs
CHANGED
|
@@ -105,127 +105,23 @@ function offlineSvgTemplate() {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// src/core/swTemplate.ts
|
|
108
|
-
var
|
|
109
|
-
"js",
|
|
110
|
-
"css",
|
|
111
|
-
"map",
|
|
112
|
-
"ico",
|
|
113
|
-
"png",
|
|
114
|
-
"jpg",
|
|
115
|
-
"jpeg",
|
|
116
|
-
"webp",
|
|
117
|
-
"svg",
|
|
118
|
-
"gif",
|
|
119
|
-
"woff2",
|
|
120
|
-
"woff",
|
|
121
|
-
"ttf",
|
|
122
|
-
"eot",
|
|
123
|
-
"json",
|
|
124
|
-
"txt",
|
|
125
|
-
"xml",
|
|
126
|
-
"webmanifest"
|
|
127
|
-
];
|
|
128
|
-
function jsString(v) {
|
|
129
|
-
return JSON.stringify(v);
|
|
130
|
-
}
|
|
108
|
+
var js = (v) => JSON.stringify(v);
|
|
131
109
|
function buildServiceWorkerJS(options2) {
|
|
132
|
-
const {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
htmlStrategy,
|
|
138
|
-
assetStrategy,
|
|
139
|
-
imageStrategy,
|
|
140
|
-
assetExtensions,
|
|
141
|
-
apiPrefixes
|
|
142
|
-
} = options2;
|
|
143
|
-
return `/* offline-page-kit service worker */
|
|
144
|
-
const CACHE_NAME = ${jsString(cacheName)};
|
|
145
|
-
const OFFLINE_PAGE = ${jsString(offlinePage)};
|
|
146
|
-
const OFFLINE_IMAGE = ${jsString(offlineImage)};
|
|
147
|
-
const PRECACHE = ${jsString([offlinePage, offlineImage, ...precache].filter(Boolean))};
|
|
148
|
-
|
|
149
|
-
const HTML_STRATEGY = ${jsString(htmlStrategy)};
|
|
150
|
-
const ASSET_STRATEGY = ${jsString(assetStrategy)};
|
|
151
|
-
const IMAGE_STRATEGY = ${jsString(imageStrategy)};
|
|
152
|
-
|
|
153
|
-
const ASSET_EXTS = new Set(${jsString(assetExtensions.length ? assetExtensions : DEFAULT_EXTS)});
|
|
154
|
-
const API_PREFIXES = ${jsString(apiPrefixes)};
|
|
155
|
-
|
|
156
|
-
// helpers
|
|
157
|
-
const isApi = (url) => API_PREFIXES.some(p => url.href.startsWith(p) || url.pathname.startsWith(p));
|
|
158
|
-
const extOf = (pathname) => {
|
|
159
|
-
const i = pathname.lastIndexOf(".");
|
|
160
|
-
return i === -1 ? "" : pathname.slice(i+1).toLowerCase();
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
async function cacheGet(req) {
|
|
164
|
-
const cache = await caches.open(CACHE_NAME);
|
|
165
|
-
return cache.match(req);
|
|
166
|
-
}
|
|
167
|
-
async function cachePut(req, res) {
|
|
168
|
-
const cache = await caches.open(CACHE_NAME);
|
|
169
|
-
await cache.put(req, res);
|
|
170
|
-
}
|
|
171
|
-
async function cacheDeleteOld() {
|
|
172
|
-
const keys = await caches.keys();
|
|
173
|
-
await Promise.all(keys.map(k => (k === CACHE_NAME ? null : caches.delete(k))));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// strategies
|
|
177
|
-
async function networkFirst(req, fallbackUrl) {
|
|
178
|
-
try {
|
|
179
|
-
const res = await fetch(req);
|
|
180
|
-
if (res && res.ok) cachePut(req, res.clone());
|
|
181
|
-
return res;
|
|
182
|
-
} catch {
|
|
183
|
-
const cached = await cacheGet(req);
|
|
184
|
-
if (cached) return cached;
|
|
185
|
-
if (fallbackUrl) return cacheGet(fallbackUrl);
|
|
186
|
-
return new Response("Offline", { status: 503 });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function cacheFirst(req, fallbackUrl) {
|
|
191
|
-
const cached = await cacheGet(req);
|
|
192
|
-
if (cached) return cached;
|
|
193
|
-
try {
|
|
194
|
-
const res = await fetch(req);
|
|
195
|
-
if (res && res.ok) cachePut(req, res.clone());
|
|
196
|
-
return res;
|
|
197
|
-
} catch {
|
|
198
|
-
if (fallbackUrl) return cacheGet(fallbackUrl);
|
|
199
|
-
return new Response("Offline", { status: 503 });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function staleWhileRevalidate(req) {
|
|
204
|
-
const cached = await cacheGet(req);
|
|
205
|
-
const fetchPromise = fetch(req).then(res => {
|
|
206
|
-
if (res && res.ok) cachePut(req, res.clone());
|
|
207
|
-
return res;
|
|
208
|
-
}).catch(() => null);
|
|
209
|
-
|
|
210
|
-
return cached || (await fetchPromise) || new Response("Offline", { status: 503 });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function pickStrategy(name) {
|
|
214
|
-
if (name === "cacheFirst") return cacheFirst;
|
|
215
|
-
if (name === "staleWhileRevalidate") return staleWhileRevalidate;
|
|
216
|
-
return networkFirst;
|
|
217
|
-
}
|
|
110
|
+
const { cacheName, offlinePage, offlineImage } = options2;
|
|
111
|
+
return `/* offline-page-kit service worker (minimal) */
|
|
112
|
+
const CACHE_NAME = ${js(cacheName)};
|
|
113
|
+
const OFFLINE_PAGE = ${js(offlinePage)};
|
|
114
|
+
const OFFLINE_IMAGE = ${js(offlineImage)};
|
|
218
115
|
|
|
219
116
|
self.addEventListener("install", (event) => {
|
|
220
117
|
event.waitUntil((async () => {
|
|
221
118
|
const cache = await caches.open(CACHE_NAME);
|
|
222
119
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
);
|
|
120
|
+
// \u2705 Do not let one 404 kill the install
|
|
121
|
+
await Promise.allSettled([
|
|
122
|
+
cache.add(OFFLINE_PAGE),
|
|
123
|
+
cache.add(OFFLINE_IMAGE),
|
|
124
|
+
]);
|
|
229
125
|
|
|
230
126
|
await self.skipWaiting();
|
|
231
127
|
})());
|
|
@@ -240,7 +136,7 @@ self.addEventListener("activate", (event) => {
|
|
|
240
136
|
self.addEventListener("fetch", (event) => {
|
|
241
137
|
const req = event.request;
|
|
242
138
|
|
|
243
|
-
// \u2705 Offline fallback for page navigations
|
|
139
|
+
// \u2705 Offline fallback only for page navigations
|
|
244
140
|
if (req.mode === "navigate") {
|
|
245
141
|
event.respondWith((async () => {
|
|
246
142
|
try {
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n const key = a.slice(2);\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n return m;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <style>\r\n body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}\r\n .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}\r\n h1{margin:0 0 10px;font-size:28px}\r\n p{margin:0 0 18px;opacity:.85;line-height:1.5}\r\n .row{display:flex;gap:10px;flex-wrap:wrap}\r\n button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}\r\n button{background:#fff;color:#111}\r\n a{background:rgba(255,255,255,.1);color:#fff}\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"box\">\r\n <h1>β‘ Youβre Offline</h1>\r\n <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>\r\n <div class=\"row\">\r\n <button onclick=\"location.reload()\">Retry</button>\r\n <a href=\"/\">Go Home</a>\r\n </div>\r\n </div>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Youβre Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions, Strategy } from \"../types\";\r\n\r\nconst DEFAULT_EXTS = [\r\n \"js\", \"css\", \"map\", \"ico\", \"png\", \"jpg\", \"jpeg\", \"webp\", \"svg\", \"gif\",\r\n \"woff2\", \"woff\", \"ttf\", \"eot\", \"json\", \"txt\", \"xml\", \"webmanifest\"\r\n];\r\n\r\nfunction jsString(v: unknown) {\r\n return JSON.stringify(v);\r\n}\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const {\r\n cacheName,\r\n offlinePage,\r\n offlineImage,\r\n precache,\r\n htmlStrategy,\r\n assetStrategy,\r\n imageStrategy,\r\n assetExtensions,\r\n apiPrefixes,\r\n } = options;\r\n\r\n return `/* offline-page-kit service worker */\r\nconst CACHE_NAME = ${jsString(cacheName)};\r\nconst OFFLINE_PAGE = ${jsString(offlinePage)};\r\nconst OFFLINE_IMAGE = ${jsString(offlineImage)};\r\nconst PRECACHE = ${jsString([offlinePage, offlineImage, ...precache].filter(Boolean))};\r\n\r\nconst HTML_STRATEGY = ${jsString(htmlStrategy)};\r\nconst ASSET_STRATEGY = ${jsString(assetStrategy)};\r\nconst IMAGE_STRATEGY = ${jsString(imageStrategy)};\r\n\r\nconst ASSET_EXTS = new Set(${jsString(assetExtensions.length ? assetExtensions : DEFAULT_EXTS)});\r\nconst API_PREFIXES = ${jsString(apiPrefixes)};\r\n\r\n// helpers\r\nconst isApi = (url) => API_PREFIXES.some(p => url.href.startsWith(p) || url.pathname.startsWith(p));\r\nconst extOf = (pathname) => {\r\n const i = pathname.lastIndexOf(\".\");\r\n return i === -1 ? \"\" : pathname.slice(i+1).toLowerCase();\r\n};\r\n\r\nasync function cacheGet(req) {\r\n const cache = await caches.open(CACHE_NAME);\r\n return cache.match(req);\r\n}\r\nasync function cachePut(req, res) {\r\n const cache = await caches.open(CACHE_NAME);\r\n await cache.put(req, res);\r\n}\r\nasync function cacheDeleteOld() {\r\n const keys = await caches.keys();\r\n await Promise.all(keys.map(k => (k === CACHE_NAME ? null : caches.delete(k))));\r\n}\r\n\r\n// strategies\r\nasync function networkFirst(req, fallbackUrl) {\r\n try {\r\n const res = await fetch(req);\r\n if (res && res.ok) cachePut(req, res.clone());\r\n return res;\r\n } catch {\r\n const cached = await cacheGet(req);\r\n if (cached) return cached;\r\n if (fallbackUrl) return cacheGet(fallbackUrl);\r\n return new Response(\"Offline\", { status: 503 });\r\n }\r\n}\r\n\r\nasync function cacheFirst(req, fallbackUrl) {\r\n const cached = await cacheGet(req);\r\n if (cached) return cached;\r\n try {\r\n const res = await fetch(req);\r\n if (res && res.ok) cachePut(req, res.clone());\r\n return res;\r\n } catch {\r\n if (fallbackUrl) return cacheGet(fallbackUrl);\r\n return new Response(\"Offline\", { status: 503 });\r\n }\r\n}\r\n\r\nasync function staleWhileRevalidate(req) {\r\n const cached = await cacheGet(req);\r\n const fetchPromise = fetch(req).then(res => {\r\n if (res && res.ok) cachePut(req, res.clone());\r\n return res;\r\n }).catch(() => null);\r\n\r\n return cached || (await fetchPromise) || new Response(\"Offline\", { status: 503 });\r\n}\r\n\r\nfunction pickStrategy(name) {\r\n if (name === \"cacheFirst\") return cacheFirst;\r\n if (name === \"staleWhileRevalidate\") return staleWhileRevalidate;\r\n return networkFirst;\r\n}\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n const urls = [OFFLINE_PAGE, OFFLINE_IMAGE];\r\n\r\n // β
Do not let one 404 kill the whole SW install\r\n await Promise.allSettled(\r\n urls.map((u) => cache.add(u))\r\n );\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // β
Offline fallback for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":"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","names":["import_node_path","fs","path","options","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n const key = a.slice(2);\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n return m;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <style>\r\n body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}\r\n .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}\r\n h1{margin:0 0 10px;font-size:28px}\r\n p{margin:0 0 18px;opacity:.85;line-height:1.5}\r\n .row{display:flex;gap:10px;flex-wrap:wrap}\r\n button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}\r\n button{background:#fff;color:#111}\r\n a{background:rgba(255,255,255,.1);color:#fff}\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"box\">\r\n <h1>β‘ Youβre Offline</h1>\r\n <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>\r\n <div class=\"row\">\r\n <button onclick=\"location.reload()\">Retry</button>\r\n <a href=\"/\">Go Home</a>\r\n </div>\r\n </div>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Youβre Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions } from \"../types\";\r\n\r\nconst js = (v: unknown) => JSON.stringify(v);\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const { cacheName, offlinePage, offlineImage } = options;\r\n\r\n return `/* offline-page-kit service worker (minimal) */\r\nconst CACHE_NAME = ${js(cacheName)};\r\nconst OFFLINE_PAGE = ${js(offlinePage)};\r\nconst OFFLINE_IMAGE = ${js(offlineImage)};\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n // β
Do not let one 404 kill the install\r\n await Promise.allSettled([\r\n cache.add(OFFLINE_PAGE),\r\n cache.add(OFFLINE_IMAGE),\r\n ]);\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // β
Offline fallback only for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":"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","names":["import_node_path","fs","path","options","path"]}
|
package/dist/cli.js
CHANGED
|
@@ -82,127 +82,23 @@ function offlineSvgTemplate() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// src/core/swTemplate.ts
|
|
85
|
-
var
|
|
86
|
-
"js",
|
|
87
|
-
"css",
|
|
88
|
-
"map",
|
|
89
|
-
"ico",
|
|
90
|
-
"png",
|
|
91
|
-
"jpg",
|
|
92
|
-
"jpeg",
|
|
93
|
-
"webp",
|
|
94
|
-
"svg",
|
|
95
|
-
"gif",
|
|
96
|
-
"woff2",
|
|
97
|
-
"woff",
|
|
98
|
-
"ttf",
|
|
99
|
-
"eot",
|
|
100
|
-
"json",
|
|
101
|
-
"txt",
|
|
102
|
-
"xml",
|
|
103
|
-
"webmanifest"
|
|
104
|
-
];
|
|
105
|
-
function jsString(v) {
|
|
106
|
-
return JSON.stringify(v);
|
|
107
|
-
}
|
|
85
|
+
var js = (v) => JSON.stringify(v);
|
|
108
86
|
function buildServiceWorkerJS(options2) {
|
|
109
|
-
const {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
htmlStrategy,
|
|
115
|
-
assetStrategy,
|
|
116
|
-
imageStrategy,
|
|
117
|
-
assetExtensions,
|
|
118
|
-
apiPrefixes
|
|
119
|
-
} = options2;
|
|
120
|
-
return `/* offline-page-kit service worker */
|
|
121
|
-
const CACHE_NAME = ${jsString(cacheName)};
|
|
122
|
-
const OFFLINE_PAGE = ${jsString(offlinePage)};
|
|
123
|
-
const OFFLINE_IMAGE = ${jsString(offlineImage)};
|
|
124
|
-
const PRECACHE = ${jsString([offlinePage, offlineImage, ...precache].filter(Boolean))};
|
|
125
|
-
|
|
126
|
-
const HTML_STRATEGY = ${jsString(htmlStrategy)};
|
|
127
|
-
const ASSET_STRATEGY = ${jsString(assetStrategy)};
|
|
128
|
-
const IMAGE_STRATEGY = ${jsString(imageStrategy)};
|
|
129
|
-
|
|
130
|
-
const ASSET_EXTS = new Set(${jsString(assetExtensions.length ? assetExtensions : DEFAULT_EXTS)});
|
|
131
|
-
const API_PREFIXES = ${jsString(apiPrefixes)};
|
|
132
|
-
|
|
133
|
-
// helpers
|
|
134
|
-
const isApi = (url) => API_PREFIXES.some(p => url.href.startsWith(p) || url.pathname.startsWith(p));
|
|
135
|
-
const extOf = (pathname) => {
|
|
136
|
-
const i = pathname.lastIndexOf(".");
|
|
137
|
-
return i === -1 ? "" : pathname.slice(i+1).toLowerCase();
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
async function cacheGet(req) {
|
|
141
|
-
const cache = await caches.open(CACHE_NAME);
|
|
142
|
-
return cache.match(req);
|
|
143
|
-
}
|
|
144
|
-
async function cachePut(req, res) {
|
|
145
|
-
const cache = await caches.open(CACHE_NAME);
|
|
146
|
-
await cache.put(req, res);
|
|
147
|
-
}
|
|
148
|
-
async function cacheDeleteOld() {
|
|
149
|
-
const keys = await caches.keys();
|
|
150
|
-
await Promise.all(keys.map(k => (k === CACHE_NAME ? null : caches.delete(k))));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// strategies
|
|
154
|
-
async function networkFirst(req, fallbackUrl) {
|
|
155
|
-
try {
|
|
156
|
-
const res = await fetch(req);
|
|
157
|
-
if (res && res.ok) cachePut(req, res.clone());
|
|
158
|
-
return res;
|
|
159
|
-
} catch {
|
|
160
|
-
const cached = await cacheGet(req);
|
|
161
|
-
if (cached) return cached;
|
|
162
|
-
if (fallbackUrl) return cacheGet(fallbackUrl);
|
|
163
|
-
return new Response("Offline", { status: 503 });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function cacheFirst(req, fallbackUrl) {
|
|
168
|
-
const cached = await cacheGet(req);
|
|
169
|
-
if (cached) return cached;
|
|
170
|
-
try {
|
|
171
|
-
const res = await fetch(req);
|
|
172
|
-
if (res && res.ok) cachePut(req, res.clone());
|
|
173
|
-
return res;
|
|
174
|
-
} catch {
|
|
175
|
-
if (fallbackUrl) return cacheGet(fallbackUrl);
|
|
176
|
-
return new Response("Offline", { status: 503 });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function staleWhileRevalidate(req) {
|
|
181
|
-
const cached = await cacheGet(req);
|
|
182
|
-
const fetchPromise = fetch(req).then(res => {
|
|
183
|
-
if (res && res.ok) cachePut(req, res.clone());
|
|
184
|
-
return res;
|
|
185
|
-
}).catch(() => null);
|
|
186
|
-
|
|
187
|
-
return cached || (await fetchPromise) || new Response("Offline", { status: 503 });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function pickStrategy(name) {
|
|
191
|
-
if (name === "cacheFirst") return cacheFirst;
|
|
192
|
-
if (name === "staleWhileRevalidate") return staleWhileRevalidate;
|
|
193
|
-
return networkFirst;
|
|
194
|
-
}
|
|
87
|
+
const { cacheName, offlinePage, offlineImage } = options2;
|
|
88
|
+
return `/* offline-page-kit service worker (minimal) */
|
|
89
|
+
const CACHE_NAME = ${js(cacheName)};
|
|
90
|
+
const OFFLINE_PAGE = ${js(offlinePage)};
|
|
91
|
+
const OFFLINE_IMAGE = ${js(offlineImage)};
|
|
195
92
|
|
|
196
93
|
self.addEventListener("install", (event) => {
|
|
197
94
|
event.waitUntil((async () => {
|
|
198
95
|
const cache = await caches.open(CACHE_NAME);
|
|
199
96
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
97
|
+
// \u2705 Do not let one 404 kill the install
|
|
98
|
+
await Promise.allSettled([
|
|
99
|
+
cache.add(OFFLINE_PAGE),
|
|
100
|
+
cache.add(OFFLINE_IMAGE),
|
|
101
|
+
]);
|
|
206
102
|
|
|
207
103
|
await self.skipWaiting();
|
|
208
104
|
})());
|
|
@@ -217,7 +113,7 @@ self.addEventListener("activate", (event) => {
|
|
|
217
113
|
self.addEventListener("fetch", (event) => {
|
|
218
114
|
const req = event.request;
|
|
219
115
|
|
|
220
|
-
// \u2705 Offline fallback for page navigations
|
|
116
|
+
// \u2705 Offline fallback only for page navigations
|
|
221
117
|
if (req.mode === "navigate") {
|
|
222
118
|
event.respondWith((async () => {
|
|
223
119
|
try {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n const key = a.slice(2);\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n return m;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <style>\r\n body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}\r\n .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}\r\n h1{margin:0 0 10px;font-size:28px}\r\n p{margin:0 0 18px;opacity:.85;line-height:1.5}\r\n .row{display:flex;gap:10px;flex-wrap:wrap}\r\n button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}\r\n button{background:#fff;color:#111}\r\n a{background:rgba(255,255,255,.1);color:#fff}\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"box\">\r\n <h1>β‘ Youβre Offline</h1>\r\n <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>\r\n <div class=\"row\">\r\n <button onclick=\"location.reload()\">Retry</button>\r\n <a href=\"/\">Go Home</a>\r\n </div>\r\n </div>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Youβre Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions, Strategy } from \"../types\";\r\n\r\nconst DEFAULT_EXTS = [\r\n \"js\", \"css\", \"map\", \"ico\", \"png\", \"jpg\", \"jpeg\", \"webp\", \"svg\", \"gif\",\r\n \"woff2\", \"woff\", \"ttf\", \"eot\", \"json\", \"txt\", \"xml\", \"webmanifest\"\r\n];\r\n\r\nfunction jsString(v: unknown) {\r\n return JSON.stringify(v);\r\n}\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const {\r\n cacheName,\r\n offlinePage,\r\n offlineImage,\r\n precache,\r\n htmlStrategy,\r\n assetStrategy,\r\n imageStrategy,\r\n assetExtensions,\r\n apiPrefixes,\r\n } = options;\r\n\r\n return `/* offline-page-kit service worker */\r\nconst CACHE_NAME = ${jsString(cacheName)};\r\nconst OFFLINE_PAGE = ${jsString(offlinePage)};\r\nconst OFFLINE_IMAGE = ${jsString(offlineImage)};\r\nconst PRECACHE = ${jsString([offlinePage, offlineImage, ...precache].filter(Boolean))};\r\n\r\nconst HTML_STRATEGY = ${jsString(htmlStrategy)};\r\nconst ASSET_STRATEGY = ${jsString(assetStrategy)};\r\nconst IMAGE_STRATEGY = ${jsString(imageStrategy)};\r\n\r\nconst ASSET_EXTS = new Set(${jsString(assetExtensions.length ? assetExtensions : DEFAULT_EXTS)});\r\nconst API_PREFIXES = ${jsString(apiPrefixes)};\r\n\r\n// helpers\r\nconst isApi = (url) => API_PREFIXES.some(p => url.href.startsWith(p) || url.pathname.startsWith(p));\r\nconst extOf = (pathname) => {\r\n const i = pathname.lastIndexOf(\".\");\r\n return i === -1 ? \"\" : pathname.slice(i+1).toLowerCase();\r\n};\r\n\r\nasync function cacheGet(req) {\r\n const cache = await caches.open(CACHE_NAME);\r\n return cache.match(req);\r\n}\r\nasync function cachePut(req, res) {\r\n const cache = await caches.open(CACHE_NAME);\r\n await cache.put(req, res);\r\n}\r\nasync function cacheDeleteOld() {\r\n const keys = await caches.keys();\r\n await Promise.all(keys.map(k => (k === CACHE_NAME ? null : caches.delete(k))));\r\n}\r\n\r\n// strategies\r\nasync function networkFirst(req, fallbackUrl) {\r\n try {\r\n const res = await fetch(req);\r\n if (res && res.ok) cachePut(req, res.clone());\r\n return res;\r\n } catch {\r\n const cached = await cacheGet(req);\r\n if (cached) return cached;\r\n if (fallbackUrl) return cacheGet(fallbackUrl);\r\n return new Response(\"Offline\", { status: 503 });\r\n }\r\n}\r\n\r\nasync function cacheFirst(req, fallbackUrl) {\r\n const cached = await cacheGet(req);\r\n if (cached) return cached;\r\n try {\r\n const res = await fetch(req);\r\n if (res && res.ok) cachePut(req, res.clone());\r\n return res;\r\n } catch {\r\n if (fallbackUrl) return cacheGet(fallbackUrl);\r\n return new Response(\"Offline\", { status: 503 });\r\n }\r\n}\r\n\r\nasync function staleWhileRevalidate(req) {\r\n const cached = await cacheGet(req);\r\n const fetchPromise = fetch(req).then(res => {\r\n if (res && res.ok) cachePut(req, res.clone());\r\n return res;\r\n }).catch(() => null);\r\n\r\n return cached || (await fetchPromise) || new Response(\"Offline\", { status: 503 });\r\n}\r\n\r\nfunction pickStrategy(name) {\r\n if (name === \"cacheFirst\") return cacheFirst;\r\n if (name === \"staleWhileRevalidate\") return staleWhileRevalidate;\r\n return networkFirst;\r\n}\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n const urls = [OFFLINE_PAGE, OFFLINE_IMAGE];\r\n\r\n // β
Do not let one 404 kill the whole SW install\r\n await Promise.allSettled(\r\n urls.map((u) => cache.add(u))\r\n );\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // β
Offline fallback for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":"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","names":["path","options","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/core/utils.ts","../src/core/offlineHtml.ts","../src/core/offlineSvg.ts","../src/core/swTemplate.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport path from \"node:path\";\r\nimport { parseArgs, splitList, writeFileSafe, exists } from \"./core/utils\";\r\nimport { offlineHtmlTemplate } from \"./core/offlineHtml\";\r\nimport { offlineSvgTemplate } from \"./core/offlineSvg\";\r\nimport { buildServiceWorkerJS } from \"./core/swTemplate\";\r\nimport type { OfflineKitBuildOptions } from \"./types\";\r\n\r\nfunction normalizePublicPath(p: string) {\r\n if (!p.startsWith(\"/\")) return \"/\" + p;\r\n return p;\r\n}\r\n\r\nfunction withDefaults(o: OfflineKitBuildOptions): Required<OfflineKitBuildOptions> {\r\n return {\r\n outDir: o.outDir ?? \"public\",\r\n swFileName: o.swFileName ?? \"sw.js\",\r\n offlinePage: normalizePublicPath(o.offlinePage ?? \"/offline.html\"),\r\n offlineImage: normalizePublicPath(o.offlineImage ?? \"/offline.svg\"),\r\n cacheName: o.cacheName ?? \"offline-page-kit\",\r\n precache: o.precache ?? [],\r\n htmlStrategy: o.htmlStrategy ?? \"networkFirst\",\r\n assetStrategy: o.assetStrategy ?? \"staleWhileRevalidate\",\r\n imageStrategy: o.imageStrategy ?? \"cacheFirst\",\r\n assetExtensions: o.assetExtensions ?? [],\r\n apiPrefixes: o.apiPrefixes ?? [],\r\n };\r\n}\r\n\r\nconst args = parseArgs(process.argv.slice(2));\r\nconst cmd = process.argv.slice(2).find(a => !a.startsWith(\"--\")) || \"init\";\r\n\r\nconst outDir = path.resolve(process.cwd(), args.get(\"outDir\") || \"public\");\r\n\r\nconst options = withDefaults({\r\n outDir,\r\n swFileName: args.get(\"swFileName\") || \"sw.js\",\r\n offlinePage: args.get(\"offlinePage\") || \"/offline.html\",\r\n offlineImage: args.get(\"offlineImage\") || \"/offline.svg\",\r\n cacheName: args.get(\"cacheName\") || \"offline-page-kit\",\r\n precache: splitList(args.get(\"precache\")),\r\n htmlStrategy: (args.get(\"htmlStrategy\") as any) || \"networkFirst\",\r\n assetStrategy: (args.get(\"assetStrategy\") as any) || \"staleWhileRevalidate\",\r\n imageStrategy: (args.get(\"imageStrategy\") as any) || \"cacheFirst\",\r\n assetExtensions: splitList(args.get(\"assetExtensions\")),\r\n apiPrefixes: splitList(args.get(\"apiPrefixes\")),\r\n} as OfflineKitBuildOptions);\r\n\r\nconst swOut = path.join(outDir, options.swFileName);\r\nconst offlineHtmlOut = path.join(outDir, options.offlinePage.replace(/^\\//, \"\"));\r\nconst offlineSvgOut = path.join(outDir, options.offlineImage.replace(/^\\//, \"\"));\r\n\r\nif (cmd === \"init\" || cmd === \"build\") {\r\n // generate offline page if missing OR force (when build)\r\n if (cmd === \"build\" || !exists(offlineHtmlOut)) {\r\n writeFileSafe(offlineHtmlOut, offlineHtmlTemplate());\r\n }\r\n if (cmd === \"build\" || !exists(offlineSvgOut)) {\r\n writeFileSafe(offlineSvgOut, offlineSvgTemplate());\r\n }\r\n\r\n const sw = buildServiceWorkerJS(options);\r\n writeFileSafe(swOut, sw);\r\n\r\n console.log(`[offline-page-kit] Generated:\r\n- ${swOut}\r\n- ${offlineHtmlOut}\r\n- ${offlineSvgOut}\r\n`);\r\n} else {\r\n console.log(`[offline-page-kit] Unknown command: ${cmd}\r\nUse:\r\n offline-page-kit init --outDir public\r\n offline-page-kit build --outDir public\r\n`);\r\n}","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport function ensureDir(p: string) {\r\n fs.mkdirSync(p, { recursive: true });\r\n}\r\n\r\nexport function writeFileSafe(filePath: string, content: string) {\r\n ensureDir(path.dirname(filePath));\r\n fs.writeFileSync(filePath, content, \"utf8\");\r\n}\r\n\r\nexport function exists(filePath: string) {\r\n try { fs.accessSync(filePath); return true; } catch { return false; }\r\n}\r\n\r\nexport function parseArgs(argv: string[]) {\r\n const m = new Map<string, string>();\r\n for (let i = 0; i < argv.length; i++) {\r\n const a = argv[i];\r\n if (!a.startsWith(\"--\")) continue;\r\n const key = a.slice(2);\r\n const value = argv[i + 1] && !argv[i + 1].startsWith(\"--\") ? argv[++i] : \"true\";\r\n m.set(key, value);\r\n }\r\n return m;\r\n}\r\n\r\nexport function splitList(v: string | undefined) {\r\n return (v || \"\")\r\n .split(\",\")\r\n .map(s => s.trim())\r\n .filter(Boolean);\r\n}","export function offlineHtmlTemplate(title = \"You're Offline\") {\r\n return `<!doctype html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\"/>\r\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\r\n <title>${title}</title>\r\n <style>\r\n body{margin:0;height:100vh;display:grid;place-items:center;font-family:system-ui,-apple-system,Segoe UI,Roboto;background:#0b0f19;color:#fff}\r\n .box{max-width:560px;padding:28px 26px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04)}\r\n h1{margin:0 0 10px;font-size:28px}\r\n p{margin:0 0 18px;opacity:.85;line-height:1.5}\r\n .row{display:flex;gap:10px;flex-wrap:wrap}\r\n button,a{appearance:none;border:0;border-radius:12px;padding:10px 14px;cursor:pointer;text-decoration:none}\r\n button{background:#fff;color:#111}\r\n a{background:rgba(255,255,255,.1);color:#fff}\r\n </style>\r\n</head>\r\n<body>\r\n <div class=\"box\">\r\n <h1>β‘ Youβre Offline</h1>\r\n <p>No internet connection detected. You can retry, or go back to the homepage (if cached).</p>\r\n <div class=\"row\">\r\n <button onclick=\"location.reload()\">Retry</button>\r\n <a href=\"/\">Go Home</a>\r\n </div>\r\n </div>\r\n</body>\r\n</html>`;\r\n}","export function offlineSvgTemplate() {\r\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">\r\n <rect width=\"1200\" height=\"630\" fill=\"#0b0f19\"/>\r\n <text x=\"80\" y=\"220\" fill=\"#ffffff\" font-size=\"64\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Youβre Offline</text>\r\n <text x=\"80\" y=\"300\" fill=\"#cbd5e1\" font-size=\"28\" font-family=\"system-ui, -apple-system, Segoe UI, Roboto\">Please check your connection and try again.</text>\r\n <circle cx=\"1040\" cy=\"220\" r=\"90\" fill=\"rgba(255,255,255,0.08)\"/>\r\n <path d=\"M980 220c40-40 120-40 160 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <path d=\"M1010 250c25-25 75-25 100 0\" stroke=\"#fff\" stroke-width=\"10\" fill=\"none\" opacity=\"0.6\"/>\r\n <circle cx=\"1060\" cy=\"290\" r=\"10\" fill=\"#fff\" opacity=\"0.7\"/>\r\n</svg>`;\r\n}","import type { OfflineKitBuildOptions } from \"../types\";\r\n\r\nconst js = (v: unknown) => JSON.stringify(v);\r\n\r\nexport function buildServiceWorkerJS(options: Required<OfflineKitBuildOptions>) {\r\n const { cacheName, offlinePage, offlineImage } = options;\r\n\r\n return `/* offline-page-kit service worker (minimal) */\r\nconst CACHE_NAME = ${js(cacheName)};\r\nconst OFFLINE_PAGE = ${js(offlinePage)};\r\nconst OFFLINE_IMAGE = ${js(offlineImage)};\r\n\r\nself.addEventListener(\"install\", (event) => {\r\n event.waitUntil((async () => {\r\n const cache = await caches.open(CACHE_NAME);\r\n\r\n // β
Do not let one 404 kill the install\r\n await Promise.allSettled([\r\n cache.add(OFFLINE_PAGE),\r\n cache.add(OFFLINE_IMAGE),\r\n ]);\r\n\r\n await self.skipWaiting();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"activate\", (event) => {\r\n event.waitUntil((async () => {\r\n await self.clients.claim();\r\n })());\r\n});\r\n\r\nself.addEventListener(\"fetch\", (event) => {\r\n const req = event.request;\r\n\r\n // β
Offline fallback only for page navigations\r\n if (req.mode === \"navigate\") {\r\n event.respondWith((async () => {\r\n try {\r\n return await fetch(req);\r\n } catch {\r\n const cache = await caches.open(CACHE_NAME);\r\n return (await cache.match(OFFLINE_PAGE)) || new Response(\"Offline\", { status: 503 });\r\n }\r\n })());\r\n }\r\n});\r\n`;\r\n}"],"mappings":"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","names":["path","options","path"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offline-page-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Framework-agnostic offline page + service worker generator (TypeScript)",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "MD Kuhel Ahmed",
|
|
8
|
+
"email": "kuhelahmed84@gmail.com",
|
|
9
|
+
"url": "https://devcrack.dev"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://devcrack.dev",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/YOUR_GITHUB_USERNAME/offline-page-kit.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/YOUR_GITHUB_USERNAME/offline-page-kit/issues"
|
|
18
|
+
},
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "individual",
|
|
21
|
+
"url": "https://www.youtube.com/@dev-crack"
|
|
22
|
+
},
|
|
6
23
|
"type": "module",
|
|
7
24
|
"main": "./dist/index.cjs",
|
|
8
25
|
"module": "./dist/index.js",
|
|
@@ -14,15 +31,34 @@
|
|
|
14
31
|
"require": "./dist/index.cjs"
|
|
15
32
|
}
|
|
16
33
|
},
|
|
17
|
-
"files": [
|
|
18
|
-
"dist"
|
|
19
|
-
],
|
|
20
34
|
"bin": {
|
|
21
35
|
"offline-page-kit": "./dist/cli.cjs"
|
|
22
36
|
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"keywords": [
|
|
44
|
+
"offline",
|
|
45
|
+
"offline-page",
|
|
46
|
+
"service-worker",
|
|
47
|
+
"pwa",
|
|
48
|
+
"service worker",
|
|
49
|
+
"generator",
|
|
50
|
+
"typescript",
|
|
51
|
+
"nextjs",
|
|
52
|
+
"vite",
|
|
53
|
+
"framework-agnostic"
|
|
54
|
+
],
|
|
23
55
|
"scripts": {
|
|
24
|
-
"
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"dev": "tsup --watch",
|
|
58
|
+
"prepublishOnly": "npm run build"
|
|
25
59
|
},
|
|
26
60
|
"devDependencies": {
|
|
61
|
+
"tsup": "^8.0.0",
|
|
62
|
+
"typescript": "^5.0.0"
|
|
27
63
|
}
|
|
28
64
|
}
|