spark-html-offline 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/package.json +40 -0
- package/src/index.d.ts +46 -0
- package/src/index.js +130 -0
- package/src/vite.d.ts +8 -0
- package/src/vite.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ⚡ spark-html-offline
|
|
2
|
+
|
|
3
|
+
Offline-capable URL imports for [spark-html](https://www.npmjs.com/package/spark-html)
|
|
4
|
+
— a tiny service worker that caches `<div import="https://…">` components on
|
|
5
|
+
first fetch and serves them when the CDN is unreachable or the user is
|
|
6
|
+
offline. Zero dependencies.
|
|
7
|
+
|
|
8
|
+
It kills the #1 critique of CDN imports: **"CDN down = component gone."**
|
|
9
|
+
With the worker installed, a component imported by URL is served from cache
|
|
10
|
+
instantly on every visit after the first, and refreshed in the background —
|
|
11
|
+
users are never more than one visit behind, and never broken.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// src/main.js — zero config
|
|
15
|
+
import { offline } from 'spark-html-offline';
|
|
16
|
+
offline();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// vite.config.js — emits /spark-sw.js in build, serves it in dev
|
|
21
|
+
import spark from 'spark-html/vite';
|
|
22
|
+
import offlineSw from 'spark-html-offline/vite';
|
|
23
|
+
|
|
24
|
+
export default { plugins: [spark(), offlineSw()] };
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<!-- this now works on a plane -->
|
|
29
|
+
<div import="https://esm.sh/some-pkg/card.html"></div>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Works with any CDN — esm.sh, unpkg, jsdelivr, raw.githubusercontent, your own.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install spark-html-offline
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
The worker intercepts **cross-origin GET requests only** (the CDN-import
|
|
43
|
+
case) with a cache-first, background-revalidate strategy:
|
|
44
|
+
|
|
45
|
+
1. **First visit** — fetched from the network, stored in the cache.
|
|
46
|
+
2. **Every visit after** — served from cache instantly; a background fetch
|
|
47
|
+
refreshes the entry for next time.
|
|
48
|
+
3. **Network gone** — the cached copy is served; a URL never seen before
|
|
49
|
+
answers `504`.
|
|
50
|
+
|
|
51
|
+
Same-origin requests are untouched by default, so dev servers, HMR, and your
|
|
52
|
+
own assets behave exactly as before.
|
|
53
|
+
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// vite.config.js
|
|
58
|
+
offlineSw({
|
|
59
|
+
file: 'spark-sw.js', // emitted worker file name
|
|
60
|
+
include: ['/components/'], // ALSO cache these same-origin paths
|
|
61
|
+
exclude: ['/api/'], // never touch these (substring match)
|
|
62
|
+
cacheName: 'spark-offline-v1',
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
// main.js
|
|
68
|
+
offline({
|
|
69
|
+
sw: 'spark-sw.js', // worker URL, relative to the page base
|
|
70
|
+
scope: '/', // registration scope
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`offline()` no-ops safely wherever service workers don't exist — prerender
|
|
75
|
+
builds, old browsers, non-secure origins — your app runs exactly as before,
|
|
76
|
+
just without the safety net.
|
|
77
|
+
|
|
78
|
+
## No build step?
|
|
79
|
+
|
|
80
|
+
You don't need Vite. Generate the worker once and host it next to
|
|
81
|
+
`index.html`:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// node make-sw.mjs > spark-sw.js
|
|
85
|
+
import { swSource } from 'spark-html-offline';
|
|
86
|
+
console.log(swSource({ include: ['/components/'] }));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then call `offline()` from any `<script type="module">`.
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
| Export | Meaning |
|
|
94
|
+
|--------|---------|
|
|
95
|
+
| `offline(options?)` | Register the worker. Returns the registration, or `null` where unsupported. |
|
|
96
|
+
| `swSource(options?)` | The full worker source as a string. |
|
|
97
|
+
| `shouldHandle(url, origin, config?)` | The matching rule the worker uses (exported for tests/tooling). |
|
|
98
|
+
| `CACHE_NAME` | Default cache bucket name (`'spark-offline-v1'`). |
|
|
99
|
+
| `spark-html-offline/vite` | Vite plugin — emits the worker in build, serves it in dev. |
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spark-html-offline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Offline-capable URL imports for spark-html — a tiny service worker that caches CDN-imported components on first fetch and serves them when the network is gone. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"homepage": "https://wilkinnovo.github.io/spark",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./vite": {
|
|
15
|
+
"types": "./src/vite.d.ts",
|
|
16
|
+
"default": "./src/vite.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test/offline.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/wilkinnovo/spark.git",
|
|
28
|
+
"directory": "packages/spark-html-offline"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"spark-html",
|
|
32
|
+
"service-worker",
|
|
33
|
+
"offline",
|
|
34
|
+
"pwa",
|
|
35
|
+
"cdn",
|
|
36
|
+
"cache-first",
|
|
37
|
+
"stale-while-revalidate"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Options shared by the worker generator and the vite plugin. */
|
|
2
|
+
export interface OfflineSwOptions {
|
|
3
|
+
/** Same-origin URL substrings to cache too (e.g. ['/components/']). */
|
|
4
|
+
include?: string[];
|
|
5
|
+
/** URL substrings the worker must never touch. */
|
|
6
|
+
exclude?: string[];
|
|
7
|
+
/** Override the cache bucket name (default 'spark-offline-v1'). */
|
|
8
|
+
cacheName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OfflineOptions {
|
|
12
|
+
/** Worker URL, relative to the page base (default 'spark-sw.js'). */
|
|
13
|
+
sw?: string;
|
|
14
|
+
/** Registration scope (default: the worker's directory). */
|
|
15
|
+
scope?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Default cache bucket name. */
|
|
19
|
+
export const CACHE_NAME: string;
|
|
20
|
+
|
|
21
|
+
/** True when the worker should intercept this URL. */
|
|
22
|
+
export function shouldHandle(
|
|
23
|
+
url: string,
|
|
24
|
+
origin: string,
|
|
25
|
+
config?: { include?: string[]; exclude?: string[] },
|
|
26
|
+
): boolean;
|
|
27
|
+
|
|
28
|
+
/** The full service-worker source as a string. */
|
|
29
|
+
export function swSource(options?: OfflineSwOptions): string;
|
|
30
|
+
|
|
31
|
+
/** Register the worker (no-op where service workers don't exist). */
|
|
32
|
+
export function offline(options?: OfflineOptions): Promise<ServiceWorkerRegistration | null>;
|
|
33
|
+
|
|
34
|
+
declare const _default: {
|
|
35
|
+
offline: typeof offline;
|
|
36
|
+
swSource: typeof swSource;
|
|
37
|
+
shouldHandle: typeof shouldHandle;
|
|
38
|
+
CACHE_NAME: string;
|
|
39
|
+
};
|
|
40
|
+
export default _default;
|
|
41
|
+
|
|
42
|
+
/** Vite plugin: emits the worker in build, serves it in dev. */
|
|
43
|
+
export interface OfflineVitePluginOptions extends OfflineSwOptions {
|
|
44
|
+
/** Emitted file name (default 'spark-sw.js'). */
|
|
45
|
+
file?: string;
|
|
46
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-offline — URL-imported components that survive a dead CDN.
|
|
3
|
+
*
|
|
4
|
+
* The #1 critique of `<div import="https://esm.sh/…/card.html">` is
|
|
5
|
+
* "CDN down = component gone". This package is a tiny service worker
|
|
6
|
+
* that caches those cross-origin fetches on first load and serves them
|
|
7
|
+
* cache-first afterwards (refreshing in the background), so a component
|
|
8
|
+
* imported by URL keeps working when the CDN is slow, flaky, or the
|
|
9
|
+
* user is offline.
|
|
10
|
+
*
|
|
11
|
+
* // main.js — zero config
|
|
12
|
+
* import { offline } from 'spark-html-offline';
|
|
13
|
+
* offline();
|
|
14
|
+
*
|
|
15
|
+
* // vite.config.js — emits + serves the worker file for you
|
|
16
|
+
* import offlineSw from 'spark-html-offline/vite';
|
|
17
|
+
* plugins: [spark(), offlineSw()]
|
|
18
|
+
*
|
|
19
|
+
* Works with any CDN (esm.sh, unpkg, jsdelivr, your own). By default it
|
|
20
|
+
* ONLY handles cross-origin GETs — local dev files are never intercepted,
|
|
21
|
+
* so dev servers and HMR behave exactly as before. Same-origin paths can
|
|
22
|
+
* opt in via `include` (e.g. `include: ['/components/']` to make your own
|
|
23
|
+
* fragments offline-capable too).
|
|
24
|
+
*
|
|
25
|
+
* No build step? Write the worker once with `swSource()` (or copy it from
|
|
26
|
+
* the README), host it next to index.html, and call `offline()`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Cache name — bump the suffix to invalidate everything on upgrade. */
|
|
30
|
+
export const CACHE_NAME = 'spark-offline-v1';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether the worker should handle a URL.
|
|
34
|
+
* Cross-origin http(s) → yes (the CDN-import case), unless excluded.
|
|
35
|
+
* Same-origin → only when it matches an `include` substring.
|
|
36
|
+
* This exact function is embedded into the generated worker.
|
|
37
|
+
*/
|
|
38
|
+
export function shouldHandle(url, origin, config) {
|
|
39
|
+
var u;
|
|
40
|
+
try { u = new URL(url); } catch (e) { return false; }
|
|
41
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
|
|
42
|
+
var cfg = config || {};
|
|
43
|
+
var i;
|
|
44
|
+
for (i = 0; i < (cfg.exclude || []).length; i++) {
|
|
45
|
+
if (url.indexOf(cfg.exclude[i]) !== -1) return false;
|
|
46
|
+
}
|
|
47
|
+
if (u.origin !== origin) return true;
|
|
48
|
+
for (i = 0; i < (cfg.include || []).length; i++) {
|
|
49
|
+
if (url.indexOf(cfg.include[i]) !== -1) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The service-worker source, as a string — write it to a file served from
|
|
56
|
+
* your origin (the vite plugin does this for you as /spark-sw.js).
|
|
57
|
+
*
|
|
58
|
+
* Strategy: cache-first with background revalidation (stale-while-
|
|
59
|
+
* revalidate). First visit fetches + caches; every visit after serves
|
|
60
|
+
* from cache instantly and refreshes the entry in the background, so
|
|
61
|
+
* users are never more than one visit behind — and never broken.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} [options]
|
|
64
|
+
* @param {string[]} [options.include] Same-origin URL substrings to cache too.
|
|
65
|
+
* @param {string[]} [options.exclude] URL substrings to never touch.
|
|
66
|
+
* @param {string} [options.cacheName] Override the cache bucket name.
|
|
67
|
+
*/
|
|
68
|
+
export function swSource(options = {}) {
|
|
69
|
+
const config = JSON.stringify({
|
|
70
|
+
include: options.include || [],
|
|
71
|
+
exclude: options.exclude || [],
|
|
72
|
+
});
|
|
73
|
+
const cache = JSON.stringify(options.cacheName || CACHE_NAME);
|
|
74
|
+
return `/* generated by spark-html-offline — cache-first for URL imports */
|
|
75
|
+
'use strict';
|
|
76
|
+
var CACHE = ${cache};
|
|
77
|
+
var CONFIG = ${config};
|
|
78
|
+
var shouldHandle = ${shouldHandle.toString()};
|
|
79
|
+
|
|
80
|
+
self.addEventListener('install', function () { self.skipWaiting(); });
|
|
81
|
+
self.addEventListener('activate', function (event) {
|
|
82
|
+
event.waitUntil(self.clients.claim());
|
|
83
|
+
});
|
|
84
|
+
self.addEventListener('fetch', function (event) {
|
|
85
|
+
var req = event.request;
|
|
86
|
+
if (req.method !== 'GET') return;
|
|
87
|
+
if (!shouldHandle(req.url, self.location.origin, CONFIG)) return;
|
|
88
|
+
event.respondWith(caches.open(CACHE).then(function (cache) {
|
|
89
|
+
return cache.match(req).then(function (cached) {
|
|
90
|
+
var refresh = fetch(req).then(function (res) {
|
|
91
|
+
if (res && res.ok) cache.put(req, res.clone());
|
|
92
|
+
return res;
|
|
93
|
+
}).catch(function () { return null; });
|
|
94
|
+
if (cached) {
|
|
95
|
+
// Serve instantly; keep the worker alive while the refresh lands.
|
|
96
|
+
event.waitUntil(refresh);
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
return refresh.then(function (fresh) {
|
|
100
|
+
return fresh || new Response('', { status: 504, statusText: 'offline and never cached' });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register the worker. Call once from main.js. No-ops safely where
|
|
110
|
+
* service workers don't exist (prerender, old browsers, non-secure
|
|
111
|
+
* origins) — your app runs exactly as before, just without the net.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} [options]
|
|
114
|
+
* @param {string} [options.sw='spark-sw.js'] Worker URL (relative to the page base).
|
|
115
|
+
* @param {string} [options.scope] Registration scope (default: the worker's directory).
|
|
116
|
+
* @returns {Promise<ServiceWorkerRegistration|null>}
|
|
117
|
+
*/
|
|
118
|
+
export async function offline(options = {}) {
|
|
119
|
+
if (typeof navigator === 'undefined' || !navigator.serviceWorker) return null;
|
|
120
|
+
if (typeof globalThis.__SPARK_PRERENDER__ !== 'undefined' && globalThis.__SPARK_PRERENDER__) return null;
|
|
121
|
+
const sw = options.sw || 'spark-sw.js';
|
|
122
|
+
try {
|
|
123
|
+
return await navigator.serviceWorker.register(sw, options.scope ? { scope: options.scope } : undefined);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn(`[spark-offline] service worker registration failed: ${e.message}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default { offline, swSource, shouldHandle, CACHE_NAME };
|
package/src/vite.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OfflineVitePluginOptions } from './index.js';
|
|
2
|
+
|
|
3
|
+
/** Vite plugin: emits the service worker in build, serves it in dev. */
|
|
4
|
+
export default function sparkOffline(options?: OfflineVitePluginOptions): {
|
|
5
|
+
name: string;
|
|
6
|
+
configureServer(server: unknown): void;
|
|
7
|
+
generateBundle(): void;
|
|
8
|
+
};
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-offline/vite — emit + serve the worker file.
|
|
3
|
+
*
|
|
4
|
+
* Build: writes the generated service worker into the output dir (default
|
|
5
|
+
* name /spark-sw.js) so `offline()` finds it in production.
|
|
6
|
+
* Dev: serves the same source from the dev server, so the worker can be
|
|
7
|
+
* exercised locally too (it only touches cross-origin URLs by default, so
|
|
8
|
+
* HMR and local files are unaffected).
|
|
9
|
+
*
|
|
10
|
+
* import offlineSw from 'spark-html-offline/vite';
|
|
11
|
+
* plugins: [spark(), offlineSw({ include: ['/components/'] })]
|
|
12
|
+
*/
|
|
13
|
+
import { swSource } from './index.js';
|
|
14
|
+
|
|
15
|
+
export default function sparkOffline(options = {}) {
|
|
16
|
+
const file = (options.file || 'spark-sw.js').replace(/^\//, '');
|
|
17
|
+
const source = swSource(options);
|
|
18
|
+
return {
|
|
19
|
+
name: 'spark-html-offline',
|
|
20
|
+
configureServer(server) {
|
|
21
|
+
server.middlewares.use((req, res, next) => {
|
|
22
|
+
if ((req.url || '').split('?')[0] === `/${file}`) {
|
|
23
|
+
res.setHeader('Content-Type', 'text/javascript');
|
|
24
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
25
|
+
res.end(source);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
next();
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
generateBundle() {
|
|
32
|
+
this.emitFile({ type: 'asset', fileName: file, source });
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|