@wells-nelson/build-tools 1.0.1 → 1.0.3
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 +22 -2
- package/dist/service-worker.d.ts +16 -5
- package/dist/service-worker.js +12 -6
- package/dist/vite.d.ts +22 -0
- package/dist/vite.js +38 -0
- package/package.json +1 -1
- package/templates/deploy-pages.yml +3 -0
package/README.md
CHANGED
|
@@ -36,18 +36,38 @@ export default defineConfig(({ mode, command }) => ({
|
|
|
36
36
|
}));
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
### Content Security Policy
|
|
40
|
+
|
|
41
|
+
`contentSecurityPolicy({ supabaseUrl })` returns SvelteKit `csp` options: hashes for SvelteKit's inline
|
|
42
|
+
scripts and no other scripts, network access only to the app and its Supabase project (REST and
|
|
43
|
+
Realtime), images from the app, `data:`/`blob:` and Google profile pictures. Pass `connect` / `img` for
|
|
44
|
+
more sources.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
sveltekit({ csp: contentSecurityPolicy({ supabaseUrl: env.VITE_SUPABASE_URL }), ... })
|
|
48
|
+
```
|
|
49
|
+
|
|
39
50
|
## Service worker (`@wells-nelson/build-tools/service-worker`)
|
|
40
51
|
|
|
41
52
|
```ts
|
|
42
53
|
/// <reference types="@sveltejs/kit" />
|
|
43
|
-
import { version } from '$app/env';
|
|
54
|
+
import { dev, version } from '$app/env';
|
|
44
55
|
import { assets, immutable } from '$app/manifest';
|
|
45
56
|
import { self } from '$app/service-worker';
|
|
46
57
|
import { registerAppShellCache } from '@wells-nelson/build-tools/service-worker';
|
|
47
58
|
|
|
48
|
-
registerAppShellCache({ sw: self, version, files: [...immutable, ...assets], cachePrefix: 'my-app' });
|
|
59
|
+
registerAppShellCache({ sw: self, version, dev, files: [...immutable, ...assets], cachePrefix: 'my-app' });
|
|
49
60
|
```
|
|
50
61
|
|
|
62
|
+
Navigations go to the network first, so starting the app online always loads the latest deploy;
|
|
63
|
+
offline they fall back to the cached shell. A new worker skips waiting and takes over open
|
|
64
|
+
pages. `networkOnly` paths are never cached (default: SvelteKit's `_app/version.json`, so
|
|
65
|
+
`updated` from `$app/state` notices new deploys). `dev` caches nothing.
|
|
66
|
+
|
|
67
|
+
On activate a worker deletes only its own older caches (`<cachePrefix>-cache-*`). Give each app
|
|
68
|
+
on an origin its own `cachePrefix`: GitHub Pages project sites of one user all share
|
|
69
|
+
`https://<user>.github.io`.
|
|
70
|
+
|
|
51
71
|
## Templates
|
|
52
72
|
|
|
53
73
|
`templates/deploy-pages.yml` (replace `__REPO__`) and `templates/manifest.webmanifest`.
|
package/dist/service-worker.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offline-first app shell for a SvelteKit service worker. Everything the app needs is precached
|
|
3
|
-
* on install; navigations
|
|
4
|
-
*
|
|
3
|
+
* on install; navigations go to the network first (so starting the app online always
|
|
4
|
+
* loads the latest deploy) and fall back to the cached shell (the app root) offline. A new
|
|
5
|
+
* deploy's worker installs, skips waiting and takes over open pages straight away. Cross-origin
|
|
6
|
+
* requests (Supabase, Google avatars) are left to the network.
|
|
5
7
|
*
|
|
6
8
|
* ```ts
|
|
7
9
|
* // src/service-worker/index.ts
|
|
@@ -27,21 +29,30 @@ export interface ServiceWorkerScopeLike {
|
|
|
27
29
|
}
|
|
28
30
|
export interface AppShellCacheOptions {
|
|
29
31
|
sw: ServiceWorkerScopeLike;
|
|
30
|
-
/** Build version; each deploy gets a fresh cache and old ones are deleted. */
|
|
32
|
+
/** Build version; each deploy gets a fresh cache and this app's old ones are deleted. */
|
|
31
33
|
version: string;
|
|
32
34
|
/** Files to precache, relative to the scope (SvelteKit's `immutable` and `assets`). */
|
|
33
35
|
files: Array<string | {
|
|
34
36
|
path: string;
|
|
35
37
|
}>;
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Cache name prefix (default 'app'). Give every app on the same origin its own prefix: a worker
|
|
40
|
+
* only deletes caches named `<cachePrefix>-cache-*`, so apps sharing an origin keep each other's.
|
|
41
|
+
*/
|
|
37
42
|
cachePrefix?: string;
|
|
38
43
|
/**
|
|
39
44
|
* Development: cache nothing and delete what an earlier worker cached, so every code change
|
|
40
45
|
* loads (a cache-first worker would otherwise keep serving stale dev modules).
|
|
41
46
|
*/
|
|
42
47
|
dev?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Paths (relative to the scope) that always go to the network and are never cached. Defaults
|
|
50
|
+
* to SvelteKit's `_app/version.json`, so its update check (`updated` in `$app/state`) sees
|
|
51
|
+
* new deploys instead of the first answer it ever cached.
|
|
52
|
+
*/
|
|
53
|
+
networkOnly?: string[];
|
|
43
54
|
}
|
|
44
|
-
export declare function registerAppShellCache({ sw, version, files, cachePrefix, dev }: AppShellCacheOptions): {
|
|
55
|
+
export declare function registerAppShellCache({ sw, version, files, cachePrefix, dev, networkOnly }: AppShellCacheOptions): {
|
|
45
56
|
cacheName: null;
|
|
46
57
|
precache: never[];
|
|
47
58
|
} | {
|
package/dist/service-worker.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offline-first app shell for a SvelteKit service worker. Everything the app needs is precached
|
|
3
|
-
* on install; navigations
|
|
4
|
-
*
|
|
3
|
+
* on install; navigations go to the network first (so starting the app online always
|
|
4
|
+
* loads the latest deploy) and fall back to the cached shell (the app root) offline. A new
|
|
5
|
+
* deploy's worker installs, skips waiting and takes over open pages straight away. Cross-origin
|
|
6
|
+
* requests (Supabase, Google avatars) are left to the network.
|
|
5
7
|
*
|
|
6
8
|
* ```ts
|
|
7
9
|
* // src/service-worker/index.ts
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
* registerAppShellCache({ sw: self, version, dev, files: [...immutable, ...assets], cachePrefix: 'my-app' });
|
|
15
17
|
* ```
|
|
16
18
|
*/
|
|
17
|
-
export function registerAppShellCache({ sw, version, files, cachePrefix = 'app', dev = false }) {
|
|
19
|
+
export function registerAppShellCache({ sw, version, files, cachePrefix = 'app', dev = false, networkOnly = ['_app/version.json'] }) {
|
|
18
20
|
const CACHE = `${cachePrefix}-cache-${version}`;
|
|
19
21
|
if (dev) {
|
|
20
22
|
sw.addEventListener('install', (event) => event.waitUntil(sw.skipWaiting()));
|
|
@@ -31,6 +33,7 @@ export function registerAppShellCache({ sw, version, files, cachePrefix = 'app',
|
|
|
31
33
|
const scope = new URL(sw.registration.scope);
|
|
32
34
|
// The app root serves the SPA shell (index.html on GitHub Pages, the fallback page elsewhere).
|
|
33
35
|
const SHELL = scope.href;
|
|
36
|
+
const NETWORK_ONLY = new Set(networkOnly.map((p) => new URL(p, scope).pathname));
|
|
34
37
|
const PRECACHE = files.map((f) => new URL(typeof f === 'string' ? f : f.path, scope).href).concat(SHELL);
|
|
35
38
|
sw.addEventListener('install', (event) => {
|
|
36
39
|
event.waitUntil((async () => {
|
|
@@ -41,9 +44,12 @@ export function registerAppShellCache({ sw, version, files, cachePrefix = 'app',
|
|
|
41
44
|
});
|
|
42
45
|
sw.addEventListener('activate', (event) => {
|
|
43
46
|
event.waitUntil((async () => {
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// Only this app's older caches: other apps on the same origin (e.g. two GitHub Pages
|
|
48
|
+
// project sites under one user) keep theirs.
|
|
49
|
+
for (const key of await caches.keys()) {
|
|
50
|
+
if (key !== CACHE && key.startsWith(`${cachePrefix}-cache-`))
|
|
46
51
|
await caches.delete(key);
|
|
52
|
+
}
|
|
47
53
|
await sw.clients.claim();
|
|
48
54
|
})());
|
|
49
55
|
});
|
|
@@ -52,7 +58,7 @@ export function registerAppShellCache({ sw, version, files, cachePrefix = 'app',
|
|
|
52
58
|
if (request.method !== 'GET')
|
|
53
59
|
return;
|
|
54
60
|
const url = new URL(request.url);
|
|
55
|
-
if (url.origin !== scope.origin)
|
|
61
|
+
if (url.origin !== scope.origin || NETWORK_ONLY.has(url.pathname))
|
|
56
62
|
return;
|
|
57
63
|
if (request.mode === 'navigate') {
|
|
58
64
|
event.respondWith((async () => {
|
package/dist/vite.d.ts
CHANGED
|
@@ -44,3 +44,25 @@ export declare function devJsonModule({ id, file, enabled }: {
|
|
|
44
44
|
file: string;
|
|
45
45
|
enabled: boolean;
|
|
46
46
|
}): Plugin;
|
|
47
|
+
/**
|
|
48
|
+
* SvelteKit `csp` options for an offline-first app on static hosting. SvelteKit adds a
|
|
49
|
+
* `<meta http-equiv="content-security-policy">` with hashes of its own inline scripts, so only
|
|
50
|
+
* the app's bundled scripts run: injected markup can't load or run other scripts. Network access
|
|
51
|
+
* is limited to the app's origin and its Supabase project (REST and Realtime); images also allow
|
|
52
|
+
* data/blob URLs and Google profile pictures. Styles allow inline (Svelte transitions use them).
|
|
53
|
+
*
|
|
54
|
+
* ```js
|
|
55
|
+
* sveltekit({ csp: contentSecurityPolicy({ supabaseUrl: env.VITE_SUPABASE_URL }), ... })
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function contentSecurityPolicy({ supabaseUrl, connect, img }?: {
|
|
59
|
+
/** The project URL (`VITE_SUPABASE_URL`); without it any `*.supabase.co` project is allowed. */
|
|
60
|
+
supabaseUrl?: string;
|
|
61
|
+
/** Extra `connect-src` sources. */
|
|
62
|
+
connect?: string[];
|
|
63
|
+
/** Extra `img-src` sources. */
|
|
64
|
+
img?: string[];
|
|
65
|
+
}): {
|
|
66
|
+
mode: "hash";
|
|
67
|
+
directives: Record<string, any[]>;
|
|
68
|
+
};
|
package/dist/vite.js
CHANGED
|
@@ -80,3 +80,41 @@ export function devJsonModule({ id, file, enabled }) {
|
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* SvelteKit `csp` options for an offline-first app on static hosting. SvelteKit adds a
|
|
85
|
+
* `<meta http-equiv="content-security-policy">` with hashes of its own inline scripts, so only
|
|
86
|
+
* the app's bundled scripts run: injected markup can't load or run other scripts. Network access
|
|
87
|
+
* is limited to the app's origin and its Supabase project (REST and Realtime); images also allow
|
|
88
|
+
* data/blob URLs and Google profile pictures. Styles allow inline (Svelte transitions use them).
|
|
89
|
+
*
|
|
90
|
+
* ```js
|
|
91
|
+
* sveltekit({ csp: contentSecurityPolicy({ supabaseUrl: env.VITE_SUPABASE_URL }), ... })
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function contentSecurityPolicy({ supabaseUrl, connect = [], img = [] } = {}) {
|
|
95
|
+
let supabase = ['https://*.supabase.co', 'wss://*.supabase.co'];
|
|
96
|
+
if (supabaseUrl) {
|
|
97
|
+
try {
|
|
98
|
+
const { host, protocol } = new URL(supabaseUrl);
|
|
99
|
+
supabase = [`${protocol}//${host}`, `${protocol === 'http:' ? 'ws' : 'wss'}://${host}`];
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* keep the wildcard */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Typed loosely (`any[]`) so it fits SvelteKit's `CspDirectives` without depending on SvelteKit.
|
|
106
|
+
const directives = {
|
|
107
|
+
'default-src': ['self'],
|
|
108
|
+
'script-src': ['self'],
|
|
109
|
+
'style-src': ['self', 'unsafe-inline'],
|
|
110
|
+
'img-src': ['self', 'data:', 'blob:', 'https://*.googleusercontent.com', ...img],
|
|
111
|
+
'font-src': ['self'],
|
|
112
|
+
'connect-src': ['self', ...supabase, ...connect],
|
|
113
|
+
'worker-src': ['self'],
|
|
114
|
+
'manifest-src': ['self'],
|
|
115
|
+
'object-src': ['none'],
|
|
116
|
+
'base-uri': ['self'],
|
|
117
|
+
'form-action': ['self']
|
|
118
|
+
};
|
|
119
|
+
return { mode: 'hash', directives };
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wells-nelson/build-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Build and deployment tooling for Wells Nelson SvelteKit PWAs: postbuild, PWA icon generator, Vite config pieces, service worker, and templates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,9 @@ jobs:
|
|
|
26
26
|
node-version: 24
|
|
27
27
|
cache: npm
|
|
28
28
|
- run: npm ci
|
|
29
|
+
# Nothing ships unless it type-checks and its tests pass.
|
|
30
|
+
- run: npm run check --if-present
|
|
31
|
+
- run: npm test --if-present
|
|
29
32
|
- run: npm run build
|
|
30
33
|
env:
|
|
31
34
|
# Repository variables (Settings > Secrets and variables > Actions > Variables).
|