nexa-pwa 0.7.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.
@@ -0,0 +1,7 @@
1
+ export { useOffline } from './useOffline.js';
2
+ export { useInstallPrompt } from './useInstallPrompt.js';
3
+ export { usePWAUpdate } from './usePWAUpdate.js';
4
+ export { nexaPWA } from './plugin.js';
5
+ export type { PWAPluginOptions, CacheStrategy } from './plugin.js';
6
+ export type { InstallPromptState } from './useInstallPrompt.js';
7
+ export type { PWAUpdateState } from './usePWAUpdate.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { useOffline } from './useOffline.js';
2
+ export { useInstallPrompt } from './useInstallPrompt.js';
3
+ export { usePWAUpdate } from './usePWAUpdate.js';
4
+ export { nexaPWA } from './plugin.js';
@@ -0,0 +1,19 @@
1
+ export type CacheStrategy = 'cache-first' | 'network-first' | 'stale-while-revalidate';
2
+ export interface PWAPluginOptions {
3
+ name: string;
4
+ shortName?: string;
5
+ description?: string;
6
+ themeColor?: string;
7
+ backgroundColor?: string;
8
+ display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';
9
+ startUrl?: string;
10
+ icons?: Array<{
11
+ src: string;
12
+ sizes: string;
13
+ type?: string;
14
+ }>;
15
+ cacheStrategy?: CacheStrategy;
16
+ cacheRoutes?: string[];
17
+ swPath?: string;
18
+ }
19
+ export declare function nexaPWA(opts: PWAPluginOptions): any;
package/dist/plugin.js ADDED
@@ -0,0 +1,123 @@
1
+ const defaultIcons = [
2
+ { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
3
+ { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
4
+ ];
5
+ function buildManifest(opts) {
6
+ return JSON.stringify({
7
+ name: opts.name,
8
+ short_name: opts.shortName ?? opts.name,
9
+ description: opts.description ?? '',
10
+ theme_color: opts.themeColor ?? '#6366f1',
11
+ background_color: opts.backgroundColor ?? '#0f0f14',
12
+ display: opts.display ?? 'standalone',
13
+ start_url: opts.startUrl ?? '/',
14
+ icons: opts.icons ?? defaultIcons,
15
+ }, null, 2);
16
+ }
17
+ function buildServiceWorker(opts) {
18
+ const strategy = opts.cacheStrategy ?? 'network-first';
19
+ const cacheName = `nexa-cache-v1`;
20
+ const routes = opts.cacheRoutes ?? ['/'];
21
+ return `
22
+ const CACHE_NAME = '${cacheName}';
23
+ const PRECACHE_ROUTES = ${JSON.stringify(routes)};
24
+
25
+ self.addEventListener('install', (event) => {
26
+ event.waitUntil(
27
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_ROUTES))
28
+ );
29
+ self.skipWaiting();
30
+ });
31
+
32
+ self.addEventListener('activate', (event) => {
33
+ event.waitUntil(
34
+ caches.keys().then((keys) =>
35
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
36
+ )
37
+ );
38
+ self.clients.claim();
39
+ });
40
+
41
+ self.addEventListener('message', (event) => {
42
+ if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
43
+ });
44
+
45
+ self.addEventListener('fetch', (event) => {
46
+ if (event.request.method !== 'GET') return;
47
+ ${strategy === 'cache-first' ? buildCacheFirst() :
48
+ strategy === 'stale-while-revalidate' ? buildStaleWhileRevalidate() :
49
+ buildNetworkFirst()}
50
+ });
51
+ `.trim();
52
+ }
53
+ const buildCacheFirst = () => `
54
+ event.respondWith(
55
+ caches.match(event.request).then((cached) =>
56
+ cached ?? fetch(event.request).then((res) => {
57
+ const clone = res.clone();
58
+ caches.open(CACHE_NAME).then((c) => c.put(event.request, clone));
59
+ return res;
60
+ })
61
+ )
62
+ );`;
63
+ const buildNetworkFirst = () => `
64
+ event.respondWith(
65
+ fetch(event.request)
66
+ .then((res) => {
67
+ const clone = res.clone();
68
+ caches.open(CACHE_NAME).then((c) => c.put(event.request, clone));
69
+ return res;
70
+ })
71
+ .catch(() => caches.match(event.request))
72
+ );`;
73
+ const buildStaleWhileRevalidate = () => `
74
+ event.respondWith(
75
+ caches.open(CACHE_NAME).then((cache) =>
76
+ cache.match(event.request).then((cached) => {
77
+ const networkFetch = fetch(event.request).then((res) => {
78
+ cache.put(event.request, res.clone());
79
+ return res;
80
+ });
81
+ return cached ?? networkFetch;
82
+ })
83
+ )
84
+ );`;
85
+ export function nexaPWA(opts) {
86
+ const swPath = opts.swPath ?? '/sw.js';
87
+ return {
88
+ name: 'vite-plugin-nexa-pwa',
89
+ enforce: 'post',
90
+ configureServer(server) {
91
+ server.middlewares.use('/manifest.webmanifest', (_req, res) => {
92
+ res.setHeader('Content-Type', 'application/manifest+json');
93
+ res.end(buildManifest(opts));
94
+ });
95
+ server.middlewares.use(swPath, (_req, res) => {
96
+ res.setHeader('Content-Type', 'application/javascript');
97
+ res.end(buildServiceWorker(opts));
98
+ });
99
+ },
100
+ generateBundle() {
101
+ this.emitFile({
102
+ type: 'asset',
103
+ fileName: 'manifest.webmanifest',
104
+ source: buildManifest(opts),
105
+ });
106
+ this.emitFile({
107
+ type: 'asset',
108
+ fileName: swPath.replace(/^\//, ''),
109
+ source: buildServiceWorker(opts),
110
+ });
111
+ },
112
+ transformIndexHtml(html) {
113
+ const tags = [
114
+ `<link rel="manifest" href="/manifest.webmanifest">`,
115
+ `<meta name="theme-color" content="${opts.themeColor ?? '#6366f1'}">`,
116
+ `<meta name="mobile-web-app-capable" content="yes">`,
117
+ `<meta name="apple-mobile-web-app-capable" content="yes">`,
118
+ `<script>if('serviceWorker' in navigator) navigator.serviceWorker.register('${swPath}')</script>`,
119
+ ].join('\n ');
120
+ return html.replace('</head>', ` ${tags}\n </head>`);
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,7 @@
1
+ import { signal } from 'nexa-reactivity';
2
+ export interface InstallPromptState {
3
+ canInstall: ReturnType<typeof signal<boolean>>;
4
+ prompt: () => Promise<'accepted' | 'dismissed' | 'unavailable'>;
5
+ destroy: () => void;
6
+ }
7
+ export declare function useInstallPrompt(): InstallPromptState;
@@ -0,0 +1,26 @@
1
+ import { signal } from 'nexa-reactivity';
2
+ export function useInstallPrompt() {
3
+ const canInstall = signal(false);
4
+ let deferredEvent = null;
5
+ const onBeforeInstall = (e) => {
6
+ e.preventDefault();
7
+ deferredEvent = e;
8
+ canInstall.value = true;
9
+ };
10
+ window.addEventListener('beforeinstallprompt', onBeforeInstall);
11
+ return {
12
+ canInstall,
13
+ async prompt() {
14
+ if (!deferredEvent)
15
+ return 'unavailable';
16
+ deferredEvent.prompt();
17
+ const { outcome } = await deferredEvent.userChoice;
18
+ deferredEvent = null;
19
+ canInstall.value = false;
20
+ return outcome;
21
+ },
22
+ destroy() {
23
+ window.removeEventListener('beforeinstallprompt', onBeforeInstall);
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,4 @@
1
+ export declare function useOffline(): {
2
+ isOffline: import("nexa-reactivity").Signal<boolean>;
3
+ destroy(): void;
4
+ };
@@ -0,0 +1,15 @@
1
+ import { signal } from 'nexa-reactivity';
2
+ export function useOffline() {
3
+ const isOffline = signal(!navigator.onLine);
4
+ const onOnline = () => { isOffline.value = false; };
5
+ const onOffline = () => { isOffline.value = true; };
6
+ window.addEventListener('online', onOnline);
7
+ window.addEventListener('offline', onOffline);
8
+ return {
9
+ isOffline,
10
+ destroy() {
11
+ window.removeEventListener('online', onOnline);
12
+ window.removeEventListener('offline', onOffline);
13
+ }
14
+ };
15
+ }
@@ -0,0 +1,7 @@
1
+ import { signal } from 'nexa-reactivity';
2
+ export interface PWAUpdateState {
3
+ updateAvailable: ReturnType<typeof signal<boolean>>;
4
+ applyUpdate: () => void;
5
+ destroy: () => void;
6
+ }
7
+ export declare function usePWAUpdate(): PWAUpdateState;
@@ -0,0 +1,40 @@
1
+ import { signal } from 'nexa-reactivity';
2
+ export function usePWAUpdate() {
3
+ const updateAvailable = signal(false);
4
+ let waitingWorker = null;
5
+ const onControllerChange = () => {
6
+ window.location.reload();
7
+ };
8
+ const checkRegistration = async () => {
9
+ if (!('serviceWorker' in navigator))
10
+ return;
11
+ const reg = await navigator.serviceWorker.getRegistration();
12
+ if (!reg)
13
+ return;
14
+ const onUpdateFound = () => {
15
+ const installing = reg.installing;
16
+ if (!installing)
17
+ return;
18
+ installing.addEventListener('statechange', () => {
19
+ if (installing.state === 'installed' && navigator.serviceWorker.controller) {
20
+ waitingWorker = installing;
21
+ updateAvailable.value = true;
22
+ }
23
+ });
24
+ };
25
+ reg.addEventListener('updatefound', onUpdateFound);
26
+ navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
27
+ };
28
+ checkRegistration();
29
+ return {
30
+ updateAvailable,
31
+ applyUpdate() {
32
+ if (waitingWorker) {
33
+ waitingWorker.postMessage({ type: 'SKIP_WAITING' });
34
+ }
35
+ },
36
+ destroy() {
37
+ navigator.serviceWorker?.removeEventListener('controllerchange', onControllerChange);
38
+ }
39
+ };
40
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "nexa-pwa",
3
+ "version": "0.7.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "nexa-reactivity": "0.7.0"
17
+ },
18
+ "peerDependencies": {
19
+ "vite": "^5.0.0 || ^6.0.0"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "clean": "rm -rf dist"
30
+ }
31
+ }