@vesk/plugin-pwa 0.2.15
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 +44 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +312 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @vesk/plugin-pwa
|
|
2
|
+
|
|
3
|
+
Progressive Web App plugin for Vesk — generates a `manifest.webmanifest`, a
|
|
4
|
+
service worker, and CSP-safe PWA head tags with zero image dependencies.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @vesk/plugin-pwa
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineConfig } from '@vesk/compiler'
|
|
16
|
+
import pwaPlugin from '@vesk/plugin-pwa'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
plugins: [pwaPlugin({ name: 'My App', shortName: 'MyApp', themeColor: '#4f46e5' })],
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What it does
|
|
24
|
+
|
|
25
|
+
- **Manifest** — writes `manifest.webmanifest` plus real `icon-192.png` /
|
|
26
|
+
`icon-512.png` (inline-generated, no asset pipeline needed) into your
|
|
27
|
+
`publicDir`, satisfying Chrome's installability audit.
|
|
28
|
+
- **Service worker** — writes `sw.js` (precache of the app shell) and an
|
|
29
|
+
external `pwa-init.js` that registers it. Default strategy is
|
|
30
|
+
stale-while-revalidate; `cache-first` and `network-first` are available.
|
|
31
|
+
- **Head injection** — via the `onHead` hook, injects the manifest link,
|
|
32
|
+
`theme-color`, apple meta tags, and the SW-registration script. Everything
|
|
33
|
+
is a static file URL — no inline scripts, so CSP remains strict.
|
|
34
|
+
- `{#client}` islands and `on*` handlers keep working unchanged.
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
`name`, `shortName`, `description`, `scope`, `lang`, `display`, `orientation`,
|
|
39
|
+
`themeColor`, `backgroundColor`, `icons`, `publicDir`, `noServiceWorker`,
|
|
40
|
+
`swStrategy`. See the `PwaOptions` type for details.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vesk/plugin-pwa — Progressive Web App plugin for Vesk.
|
|
3
|
+
*
|
|
4
|
+
* - Generates manifest.webmanifest + sw.js + pwa-init.js at build start
|
|
5
|
+
* (written into publicDir so both dev and prod serve them as static files).
|
|
6
|
+
* - Injects the manifest link / theme-color / apple meta tags / SW registration
|
|
7
|
+
* into the generated head for every page via the `onHead` hook — no manual
|
|
8
|
+
* layout edits and no inline scripts (CSP-safe): SW registration lives in the
|
|
9
|
+
* external `/pwa-init.js`.
|
|
10
|
+
*
|
|
11
|
+
* @module plugin-pwa
|
|
12
|
+
*/
|
|
13
|
+
import type { VeskPlugin } from '@vesk/types';
|
|
14
|
+
export interface PwaIcon {
|
|
15
|
+
src: string;
|
|
16
|
+
sizes: string;
|
|
17
|
+
type: string;
|
|
18
|
+
purpose?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PwaOptions {
|
|
21
|
+
/** App name — used for manifest name, short_name default. */
|
|
22
|
+
name?: string;
|
|
23
|
+
/** Short name for the homescreen prompt. */
|
|
24
|
+
shortName?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Base URL of the deployed site (default '/'). */
|
|
27
|
+
scope?: string;
|
|
28
|
+
/** Default locale — 'en-US'. */
|
|
29
|
+
lang?: string;
|
|
30
|
+
/** display mode: standalone | fullscreen | minimal-ui | browser. */
|
|
31
|
+
display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';
|
|
32
|
+
/** Preferred orientation. */
|
|
33
|
+
orientation?: 'any' | 'natural' | 'landscape' | 'landscape-primary' | 'landscape-secondary' | 'portrait' | 'portrait-primary' | 'portrait-secondary';
|
|
34
|
+
themeColor?: string;
|
|
35
|
+
backgroundColor?: string;
|
|
36
|
+
icons?: PwaIcon[];
|
|
37
|
+
/** Where the plugin writes its generated files (public dir of the app). */
|
|
38
|
+
publicDir?: string;
|
|
39
|
+
/** Disable auto service-worker generation (sw.js + pwa-init.js). */
|
|
40
|
+
noServiceWorker?: boolean;
|
|
41
|
+
/** Service-worker caching strategy (default 'stale-while-revalidate'). */
|
|
42
|
+
swStrategy?: 'stale-while-revalidate' | 'cache-first' | 'network-first';
|
|
43
|
+
}
|
|
44
|
+
export declare const PWA_MANIFEST_URL = "/manifest.webmanifest";
|
|
45
|
+
export declare const PWA_SW_URL = "/sw.js";
|
|
46
|
+
export declare const PWA_INIT_URL = "/pwa-init.js";
|
|
47
|
+
export declare function solidPng(size: number, color: string): Buffer;
|
|
48
|
+
export declare function pwaPlugin(opts?: PwaOptions): VeskPlugin;
|
|
49
|
+
export default pwaPlugin;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;IACjE,6BAA6B;IAC7B,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,WAAW,GAAG,mBAAmB,GAAG,qBAAqB,GAAG,UAAU,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;IACrJ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,wBAAwB,GAAG,aAAa,GAAG,eAAe,CAAC;CACzE;AAeD,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AACxD,eAAO,MAAM,UAAU,WAAW,CAAC;AACnC,eAAO,MAAM,YAAY,iBAAiB,CAAC;AA4C3C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAuB5D;AA4HD,wBAAgB,SAAS,CAAC,IAAI,GAAE,UAAe,GAAG,UAAU,CAyF3D;AAGD,eAAe,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vesk/plugin-pwa — Progressive Web App plugin for Vesk.
|
|
3
|
+
*
|
|
4
|
+
* - Generates manifest.webmanifest + sw.js + pwa-init.js at build start
|
|
5
|
+
* (written into publicDir so both dev and prod serve them as static files).
|
|
6
|
+
* - Injects the manifest link / theme-color / apple meta tags / SW registration
|
|
7
|
+
* into the generated head for every page via the `onHead` hook — no manual
|
|
8
|
+
* layout edits and no inline scripts (CSP-safe): SW registration lives in the
|
|
9
|
+
* external `/pwa-init.js`.
|
|
10
|
+
*
|
|
11
|
+
* @module plugin-pwa
|
|
12
|
+
*/
|
|
13
|
+
import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { deflateSync } from 'node:zlib';
|
|
15
|
+
import { resolve, join } from 'node:path';
|
|
16
|
+
const DEFAULTS = {
|
|
17
|
+
name: 'Vesk App',
|
|
18
|
+
shortName: 'Vesk',
|
|
19
|
+
description: 'A Vesk app',
|
|
20
|
+
scope: '/',
|
|
21
|
+
lang: 'en-US',
|
|
22
|
+
display: 'standalone',
|
|
23
|
+
orientation: 'any',
|
|
24
|
+
themeColor: '#000000',
|
|
25
|
+
backgroundColor: '#ffffff',
|
|
26
|
+
swStrategy: 'stale-while-revalidate',
|
|
27
|
+
};
|
|
28
|
+
export const PWA_MANIFEST_URL = '/manifest.webmanifest';
|
|
29
|
+
export const PWA_SW_URL = '/sw.js';
|
|
30
|
+
export const PWA_INIT_URL = '/pwa-init.js';
|
|
31
|
+
/**
|
|
32
|
+
* Solid-color RGBA PNG encoder (no image dependencies). Chrome's
|
|
33
|
+
* installability audit requires the manifest icons to actually exist and be
|
|
34
|
+
* fetchable, so the plugin writes real icon-192.png / icon-512.png files at
|
|
35
|
+
* build time.
|
|
36
|
+
*/
|
|
37
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
38
|
+
const CRC_TABLE = (() => {
|
|
39
|
+
const t = new Uint32Array(256);
|
|
40
|
+
for (let n = 0; n < 256; n++) {
|
|
41
|
+
let c = n;
|
|
42
|
+
for (let k = 0; k < 8; k++)
|
|
43
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
44
|
+
t[n] = c >>> 0;
|
|
45
|
+
}
|
|
46
|
+
return t;
|
|
47
|
+
})();
|
|
48
|
+
function crc32(buf) {
|
|
49
|
+
let c = 0xffffffff;
|
|
50
|
+
for (let i = 0; i < buf.length; i++)
|
|
51
|
+
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
52
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
53
|
+
}
|
|
54
|
+
function pngChunk(type, data) {
|
|
55
|
+
const len = Buffer.alloc(4);
|
|
56
|
+
len.writeUInt32BE(data.length, 0);
|
|
57
|
+
const typeBuf = Buffer.from(type, 'ascii');
|
|
58
|
+
const crc = Buffer.alloc(4);
|
|
59
|
+
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
|
60
|
+
return Buffer.concat([len, typeBuf, data, crc]);
|
|
61
|
+
}
|
|
62
|
+
function hexToRgba(hex) {
|
|
63
|
+
let h = hex.trim().replace(/^#/, '');
|
|
64
|
+
if (h.length === 3)
|
|
65
|
+
h = h.split('').map((c) => c + c).join('');
|
|
66
|
+
if (h.length === 6)
|
|
67
|
+
h += 'ff';
|
|
68
|
+
const n = parseInt(h.slice(0, 8), 16);
|
|
69
|
+
if (Number.isNaN(n))
|
|
70
|
+
return [0, 0, 0, 255];
|
|
71
|
+
return [(n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
72
|
+
}
|
|
73
|
+
export function solidPng(size, color) {
|
|
74
|
+
const [r, g, b, a] = hexToRgba(color);
|
|
75
|
+
const ihdr = Buffer.alloc(13);
|
|
76
|
+
ihdr.writeUInt32BE(size, 0);
|
|
77
|
+
ihdr.writeUInt32BE(size, 4);
|
|
78
|
+
ihdr[8] = 8;
|
|
79
|
+
ihdr[9] = 6;
|
|
80
|
+
const row = Buffer.alloc(1 + size * 4);
|
|
81
|
+
for (let x = 0; x < size; x++) {
|
|
82
|
+
row[1 + x * 4] = r;
|
|
83
|
+
row[2 + x * 4] = g;
|
|
84
|
+
row[3 + x * 4] = b;
|
|
85
|
+
row[4 + x * 4] = a;
|
|
86
|
+
}
|
|
87
|
+
const raw = Buffer.alloc(size * row.length);
|
|
88
|
+
for (let y = 0; y < size; y++)
|
|
89
|
+
row.copy(raw, y * row.length);
|
|
90
|
+
const idat = deflateSync(raw);
|
|
91
|
+
return Buffer.concat([
|
|
92
|
+
PNG_SIGNATURE,
|
|
93
|
+
pngChunk('IHDR', ihdr),
|
|
94
|
+
pngChunk('IDAT', idat),
|
|
95
|
+
pngChunk('IEND', Buffer.alloc(0)),
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
function generateManifest(opts) {
|
|
99
|
+
const name = opts.name || DEFAULTS.name;
|
|
100
|
+
const shortName = opts.shortName || name;
|
|
101
|
+
const icons = (opts.icons && opts.icons.length > 0)
|
|
102
|
+
? opts.icons.map(i => ({ src: i.src, sizes: i.sizes, type: i.type, ...(i.purpose ? { purpose: i.purpose } : {}) }))
|
|
103
|
+
: [{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' }, { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }];
|
|
104
|
+
const manifest = {
|
|
105
|
+
name,
|
|
106
|
+
short_name: shortName,
|
|
107
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
108
|
+
scope: opts.scope || DEFAULTS.scope,
|
|
109
|
+
start_url: opts.scope || DEFAULTS.scope,
|
|
110
|
+
id: opts.scope || DEFAULTS.scope,
|
|
111
|
+
lang: opts.lang || DEFAULTS.lang,
|
|
112
|
+
display: opts.display || DEFAULTS.display,
|
|
113
|
+
...(opts.orientation && opts.orientation !== 'any' ? { orientation: opts.orientation } : {}),
|
|
114
|
+
...(opts.themeColor ? { theme_color: opts.themeColor } : {}),
|
|
115
|
+
...(opts.backgroundColor ? { background_color: opts.backgroundColor } : {}),
|
|
116
|
+
icons,
|
|
117
|
+
};
|
|
118
|
+
return JSON.stringify(manifest, null, 2);
|
|
119
|
+
}
|
|
120
|
+
function generateServiceWorker(strategy) {
|
|
121
|
+
const strat = strategy || DEFAULTS.swStrategy;
|
|
122
|
+
return `// Vesk PWA — auto-generated service worker (strategy: ${strat})
|
|
123
|
+
const CACHE = 'vesk-pwa-v1';
|
|
124
|
+
self.addEventListener('install', (e) => { e.waitUntil(self.skipWaiting()); });
|
|
125
|
+
self.addEventListener('activate', (e) => { e.waitUntil(self.clients.claim()); });
|
|
126
|
+
self.addEventListener('fetch', (e) => {
|
|
127
|
+
const url = new URL(e.request.url);
|
|
128
|
+
if (e.request.method !== 'GET' || url.protocol === 'chrome-extension:' || url.protocol === 'chrome:' || url.protocol === 'file:') return;
|
|
129
|
+
e.respondWith((async () => {
|
|
130
|
+
try {
|
|
131
|
+
${strat === 'cache-first'
|
|
132
|
+
? ` const cached = await caches.match(e.request);
|
|
133
|
+
if (cached) return cached;
|
|
134
|
+
const resp = await fetch(e.request);
|
|
135
|
+
const cache = await caches.open(CACHE);
|
|
136
|
+
cache.put(e.request, resp.clone());
|
|
137
|
+
return resp;`
|
|
138
|
+
: strat === 'network-first'
|
|
139
|
+
? ` const resp = await fetch(e.request);
|
|
140
|
+
const cache = await caches.open(CACHE);
|
|
141
|
+
cache.put(e.request, resp.clone());
|
|
142
|
+
return resp;`
|
|
143
|
+
: ` const cached = await caches.match(e.request);
|
|
144
|
+
const resp = await fetch(e.request);
|
|
145
|
+
if (resp && resp.ok) { const cache = await caches.open(CACHE); cache.put(e.request, resp.clone()); }
|
|
146
|
+
return cached || resp;`}
|
|
147
|
+
} catch { return caches.match(e.request); }
|
|
148
|
+
})());
|
|
149
|
+
});
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
/** External (non-inline) service-worker registration script. */
|
|
153
|
+
function generatePwaInit(opts) {
|
|
154
|
+
const scope = opts.scope || DEFAULTS.scope;
|
|
155
|
+
return `// Vesk PWA — service-worker registration (external, CSP-safe)
|
|
156
|
+
(() => {
|
|
157
|
+
if ('serviceWorker' in navigator) {
|
|
158
|
+
navigator.serviceWorker.register(${JSON.stringify(PWA_SW_URL)}, { scope: ${JSON.stringify(scope)} }).catch(() => {});
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
/** The head tags the `onHead` hook and `provides.pwaHeadTags` share. */
|
|
164
|
+
function headTags(opts) {
|
|
165
|
+
const name = opts.name || DEFAULTS.name;
|
|
166
|
+
const shortName = opts.shortName || name;
|
|
167
|
+
const lines = [
|
|
168
|
+
`<link rel="manifest" href="${PWA_MANIFEST_URL}" />`,
|
|
169
|
+
`<meta name="theme-color" content="${opts.themeColor || DEFAULTS.themeColor}" />`,
|
|
170
|
+
];
|
|
171
|
+
if (opts.backgroundColor)
|
|
172
|
+
lines.push(`<meta name="background-color" content="${opts.backgroundColor}" />`);
|
|
173
|
+
if (opts.name)
|
|
174
|
+
lines.push(`<meta name="application-name" content="${name}" />`);
|
|
175
|
+
if (opts.shortName)
|
|
176
|
+
lines.push(`<meta name="apple-mobile-web-app-title" content="${shortName}" />`);
|
|
177
|
+
if (opts.display === 'standalone')
|
|
178
|
+
lines.push('<meta name="apple-mobile-web-app-capable" content="yes" />');
|
|
179
|
+
const icons = (opts.icons && opts.icons.length > 0)
|
|
180
|
+
? opts.icons
|
|
181
|
+
: [{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' }, { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }];
|
|
182
|
+
for (const icon of icons) {
|
|
183
|
+
if (icon.purpose && icon.purpose !== 'any')
|
|
184
|
+
continue;
|
|
185
|
+
lines.push(`<link rel="apple-touch-icon" href="${icon.src}" />`);
|
|
186
|
+
}
|
|
187
|
+
if (!opts.noServiceWorker)
|
|
188
|
+
lines.push(`<script src="${PWA_INIT_URL}" defer></script>`);
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Merge `tags` into an assembled head fragment without duplicating tags the
|
|
193
|
+
* page (or an earlier hook) already declared. Dedup keys on the `rel=` /
|
|
194
|
+
* `name=` / `src=` attribute value, so re-running against identical or slightly
|
|
195
|
+
* different markup stays idempotent.
|
|
196
|
+
*/
|
|
197
|
+
function dedupInsert(headHtml, tags) {
|
|
198
|
+
const present = new Set(headHtml.split('\n').map((s) => s.trim()));
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const raw of tags.split('\n')) {
|
|
201
|
+
const line = raw.trim();
|
|
202
|
+
if (!line)
|
|
203
|
+
continue;
|
|
204
|
+
let key = '';
|
|
205
|
+
const rel = line.indexOf('rel="');
|
|
206
|
+
const name = line.indexOf('name="');
|
|
207
|
+
const src = line.indexOf('src="');
|
|
208
|
+
if (rel >= 0)
|
|
209
|
+
key = line.slice(rel);
|
|
210
|
+
else if (name >= 0)
|
|
211
|
+
key = line.slice(name);
|
|
212
|
+
else if (src >= 0)
|
|
213
|
+
key = line.slice(src);
|
|
214
|
+
else
|
|
215
|
+
key = line;
|
|
216
|
+
if (present.has(line))
|
|
217
|
+
continue;
|
|
218
|
+
if (key && headHtml.includes(key))
|
|
219
|
+
continue;
|
|
220
|
+
present.add(line);
|
|
221
|
+
out.push(line);
|
|
222
|
+
}
|
|
223
|
+
if (out.length === 0)
|
|
224
|
+
return headHtml;
|
|
225
|
+
return (headHtml || '') + '\n' + out.join('\n');
|
|
226
|
+
}
|
|
227
|
+
// The VeskPlugin returned by the factory
|
|
228
|
+
export function pwaPlugin(opts = {}) {
|
|
229
|
+
const name = opts.name || DEFAULTS.name;
|
|
230
|
+
const shortName = opts.shortName || name;
|
|
231
|
+
const publicDir = opts.publicDir || resolve(process.cwd(), 'public');
|
|
232
|
+
function writeFiles() {
|
|
233
|
+
try {
|
|
234
|
+
mkdirSync(publicDir, { recursive: true });
|
|
235
|
+
writeFileSync(join(publicDir, 'manifest.webmanifest'), generateManifest(opts), 'utf-8');
|
|
236
|
+
if (!opts.icons || opts.icons.length === 0) {
|
|
237
|
+
writeFileSync(join(publicDir, 'icon-192.png'), solidPng(192, opts.themeColor || DEFAULTS.themeColor));
|
|
238
|
+
writeFileSync(join(publicDir, 'icon-512.png'), solidPng(512, opts.themeColor || DEFAULTS.themeColor));
|
|
239
|
+
}
|
|
240
|
+
if (!opts.noServiceWorker) {
|
|
241
|
+
writeFileSync(join(publicDir, 'sw.js'), generateServiceWorker(opts.swStrategy), 'utf-8');
|
|
242
|
+
writeFileSync(join(publicDir, 'pwa-init.js'), generatePwaInit(opts), 'utf-8');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
console.error(`[vesk/plugin-pwa] write error:`, e instanceof Error ? e.message : e);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const provides = {
|
|
250
|
+
pwaHeadTags: () => headTags(opts),
|
|
251
|
+
pwaManifestLink: () => `<link rel="manifest" href="${PWA_MANIFEST_URL}" />`,
|
|
252
|
+
pwaSwRegister: () => (opts.noServiceWorker ? '' : `<script src="${PWA_INIT_URL}" defer></script>`),
|
|
253
|
+
pwaMetaThemeColor: () => `<meta name="theme-color" content="${opts.themeColor || DEFAULTS.themeColor}" />`,
|
|
254
|
+
pwaName: () => name,
|
|
255
|
+
pwaShortName: () => shortName,
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
name: '@vesk/plugin-pwa',
|
|
259
|
+
dependencies: new Set(),
|
|
260
|
+
async onBuildStart() {
|
|
261
|
+
writeFiles();
|
|
262
|
+
},
|
|
263
|
+
async onFileWatch(filePath) {
|
|
264
|
+
const base = publicDir;
|
|
265
|
+
if (filePath === resolve(base, 'manifest.webmanifest') ||
|
|
266
|
+
filePath === resolve(base, 'sw.js') ||
|
|
267
|
+
filePath === resolve(base, 'pwa-init.js')) {
|
|
268
|
+
return { handled: true };
|
|
269
|
+
}
|
|
270
|
+
return { handled: false };
|
|
271
|
+
},
|
|
272
|
+
provides,
|
|
273
|
+
async onHead(headHtml) {
|
|
274
|
+
return dedupInsert(headHtml || '', headTags(opts));
|
|
275
|
+
},
|
|
276
|
+
async onRequest(ctx) {
|
|
277
|
+
const url = new URL(ctx.request.url);
|
|
278
|
+
if (url.pathname === PWA_MANIFEST_URL) {
|
|
279
|
+
try {
|
|
280
|
+
const manifestPath = join(publicDir, 'manifest.webmanifest');
|
|
281
|
+
if (existsSync(manifestPath)) {
|
|
282
|
+
ctx.set('pwaManifestBody', readFileSync(manifestPath, 'utf-8'));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch { /* ignore */ }
|
|
286
|
+
}
|
|
287
|
+
if (url.pathname === PWA_SW_URL) {
|
|
288
|
+
try {
|
|
289
|
+
const swPath = join(publicDir, 'sw.js');
|
|
290
|
+
if (existsSync(swPath)) {
|
|
291
|
+
ctx.set('pwaSwBody', readFileSync(swPath, 'utf-8'));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch { /* ignore */ }
|
|
295
|
+
}
|
|
296
|
+
if (url.pathname === PWA_INIT_URL) {
|
|
297
|
+
try {
|
|
298
|
+
const initPath = join(publicDir, 'pwa-init.js');
|
|
299
|
+
if (existsSync(initPath)) {
|
|
300
|
+
ctx.set('pwaInitBody', readFileSync(initPath, 'utf-8'));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch { /* ignore */ }
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
async onBuildEnd() {
|
|
307
|
+
writeFiles();
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// Legacy named-export default for backward compat
|
|
312
|
+
export default pwaPlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vesk/plugin-pwa",
|
|
3
|
+
"version": "0.2.15",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Progressive Web App plugin for Vesk — generates manifest.webmanifest and service worker",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.build.json",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"test": "tsx src/index.test.ts",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@vesk/types": "^0.2.15"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.8.0"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "opencode",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/emeraldlinks/veskTs.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/emeraldlinks/veskTs#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/emeraldlinks/veskTs/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20.0.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|