UNPKG

@jager-ai/holy-pwa

Version:

Progressive Web App (PWA) utilities and templates extracted from Holy Habit project with manifest generation, service worker management, and offline support

756 lines (615 loc) 20.4 kB
# @mvp-factory/holy-pwa Progressive Web App (PWA) utilities and templates extracted from Holy Habit project with manifest generation, service worker management, and offline support. ## Features - 🏗️ **Manifest Generator** - Template-based PWA manifest.json generation - ⚙️ **Service Worker Generator** - Configurable service worker with caching strategies - 📱 **PWA Manager** - Complete PWA lifecycle management and installation handling - 🔧 **Multiple Templates** - Pre-configured templates for different app types - 🎯 **TypeScript Support** - Full TypeScript definitions and type safety - 💾 **Offline Support** - Advanced caching strategies and offline functionality - 🔔 **Push Notifications** - Built-in push notification support - 🔄 **Background Sync** - Offline data synchronization capabilities - 🎨 **Icon Management** - Automated PWA icon generation and management - 📊 **Update Management** - Automatic PWA update detection and application ## Installation ```bash npm install @mvp-factory/holy-pwa ``` ## Quick Start ### 1. Generate PWA Manifest ```typescript import { HolyPWA } from '@mvp-factory/holy-pwa'; // Create manifest with template const manifestGenerator = HolyPWA.manifestTemplates.productivity({ name: 'My Productivity App', shortName: 'ProductivityApp', description: 'A powerful productivity application', themeColor: '#3b82f6', backgroundColor: '#ffffff', icons: { sizes: [72, 96, 128, 144, 152, 192, 384, 512], basePath: '/assets/icons' } }); // Generate manifest.json content const manifestJSON = manifestGenerator.generateJSON(); console.log(manifestJSON); ``` ### 2. Generate Service Worker ```typescript import { HolyPWA } from '@mvp-factory/holy-pwa'; // Create service worker with advanced template const serviceWorkerGenerator = HolyPWA.serviceWorkerTemplates.advanced({ cacheName: 'my-app', version: '1.0.0', urlsToCache: [ '/', '/assets/css/style.css', '/assets/js/main.js', '/offline.html' ], enableBackgroundSync: true, enablePushNotifications: true }); // Generate service worker JavaScript code const serviceWorkerCode = serviceWorkerGenerator.generate(); console.log(serviceWorkerCode); ``` ### 3. Initialize PWA Manager ```typescript import { HolyPWA } from '@mvp-factory/holy-pwa'; // Create PWA manager with lifecycle events const pwaManager = HolyPWA.createManager({ onInstall: () => console.log('PWA installed'), onUpdateAvailable: (version) => console.log(`Update available: ${version}`), onOffline: () => console.log('App is offline'), onOnline: () => console.log('App is back online') }); // Initialize with service worker await pwaManager.initialize('/service-worker.js'); // Check installation state const installState = pwaManager.getInstallationState(); if (installState.isInstallable) { const userChoice = await pwaManager.promptInstall(); console.log('User choice:', userChoice); } ``` ## Core Components ### ManifestGenerator Template-based PWA manifest generation: ```typescript import { ManifestGenerator, PWAConfig } from '@mvp-factory/holy-pwa'; const config: PWAConfig = { name: 'My App', shortName: 'MyApp', description: 'My awesome application', themeColor: '#3b82f6', backgroundColor: '#ffffff', startUrl: '/', display: 'standalone', icons: { sizes: [192, 512], basePath: '/icons' }, shortcuts: [ { name: 'New Document', short_name: 'New Doc', description: 'Create new document', url: '/new' } ] }; const generator = new ManifestGenerator(config); const manifest = generator.generate(); ``` ### ServiceWorkerGenerator Configurable service worker with caching strategies: ```typescript import { ServiceWorkerGenerator, ServiceWorkerConfig } from '@mvp-factory/holy-pwa'; const config: ServiceWorkerConfig = { cacheName: 'my-pwa', version: '1.0.0', urlsToCache: ['/', '/app.css', '/app.js'], networkFirst: ['/api/'], cacheFirst: ['/assets/', '/images/'], staleWhileRevalidate: ['/data/'], enableBackgroundSync: true, enablePushNotifications: true, offlinePageUrl: '/offline.html' }; const generator = new ServiceWorkerGenerator(config); const serviceWorkerCode = generator.generate(); // Write to file import fs from 'fs'; fs.writeFileSync('public/service-worker.js', serviceWorkerCode); ``` ### PWA Manager Complete PWA lifecycle management: ```typescript import { PWAManager, PWALifecycleEvents } from '@mvp-factory/holy-pwa'; const events: PWALifecycleEvents = { onInstall: () => { console.log('PWA installed successfully'); showInstallSuccessMessage(); }, onUpdateAvailable: (version) => { showUpdateNotification(version); }, onUpdateApplied: () => { showUpdateAppliedMessage(); }, onOffline: () => { showOfflineBanner(); }, onOnline: () => { hideOfflineBanner(); syncOfflineData(); } }; const pwaManager = new PWAManager(events); // Initialize await pwaManager.initialize(); // Check capabilities const capabilities = pwaManager.getCapabilities(); console.log('PWA Capabilities:', capabilities); // Handle installation if (capabilities.installPrompt) { document.getElementById('install-button').addEventListener('click', async () => { try { const result = await pwaManager.promptInstall(); console.log('Install result:', result); } catch (error) { console.error('Install failed:', error); } }); } // Handle updates document.getElementById('update-button').addEventListener('click', async () => { const hasUpdate = await pwaManager.checkForUpdates(); if (hasUpdate) { await pwaManager.applyUpdate(); } }); // Push notifications if (capabilities.pushNotifications) { const subscription = await pwaManager.subscribeToPush('YOUR_VAPID_KEY'); console.log('Push subscription:', subscription); } ``` ## Templates ### Manifest Templates Pre-configured manifest templates for different app types: ```typescript import { ManifestGenerator } from '@mvp-factory/holy-pwa'; // Productivity app const productivityManifest = ManifestGenerator.createTemplates().productivity({ name: 'Task Manager', shortName: 'TaskManager', description: 'Manage your tasks efficiently' }); // Social app const socialManifest = ManifestGenerator.createTemplates().social({ name: 'Social Connect', shortName: 'SocialApp', description: 'Connect with friends' }); // E-commerce app const ecommerceManifest = ManifestGenerator.createTemplates().ecommerce({ name: 'Online Store', shortName: 'Store', description: 'Shop your favorite products' }); ``` ### Service Worker Templates Pre-configured service worker templates: ```typescript import { ServiceWorkerGenerator } from '@mvp-factory/holy-pwa'; // Basic service worker const basicSW = ServiceWorkerGenerator.createTemplates().basic({ cacheName: 'basic-app', version: '1.0.0', urlsToCache: ['/', '/app.css', '/app.js'] }); // Advanced service worker with all features const advancedSW = ServiceWorkerGenerator.createTemplates().advanced({ cacheName: 'advanced-app', version: '1.0.0', enableBackgroundSync: true, enablePushNotifications: true }); // Offline-first service worker const offlineSW = ServiceWorkerGenerator.createTemplates().offlineFirst({ cacheName: 'offline-app', version: '1.0.0' }); ``` ## Configuration Options ### PWA Configuration | Option | Type | Default | Description | |--------|------|---------|-------------| | `name` | string | Required | Full app name | | `shortName` | string | Required | Short app name | | `description` | string | Required | App description | | `themeColor` | string | Required | Theme color (hex) | | `backgroundColor` | string | Required | Background color (hex) | | `startUrl` | string | `'/'` | App start URL | | `display` | string | `'standalone'` | Display mode | | `orientation` | string | `'portrait'` | Screen orientation | | `scope` | string | `'/'` | App scope | | `lang` | string | `'en-US'` | App language | | `categories` | string[] | `['productivity']` | App categories | ### Service Worker Configuration | Option | Type | Default | Description | |--------|------|---------|-------------| | `cacheName` | string | Required | Cache name prefix | | `version` | string | Required | Cache version | | `urlsToCache` | string[] | Required | URLs to cache on install | | `networkFirst` | string[] | `[]` | Network-first patterns | | `cacheFirst` | string[] | `[]` | Cache-first patterns | | `staleWhileRevalidate` | string[] | `[]` | Stale-while-revalidate patterns | | `networkOnly` | string[] | `[]` | Network-only patterns | | `enableBackgroundSync` | boolean | `false` | Enable background sync | | `enablePushNotifications` | boolean | `false` | Enable push notifications | | `offlinePageUrl` | string | `'/offline.html'` | Offline page URL | ## Advanced Usage ### Custom Caching Strategies ```typescript const serviceWorker = new ServiceWorkerGenerator({ cacheName: 'custom-app', version: '2.1.0', urlsToCache: ['/', '/app.css'], // Network first for API calls networkFirst: ['/api/', '/data/'], // Cache first for static assets cacheFirst: ['/assets/', '/images/', '/fonts/'], // Stale while revalidate for dynamic content staleWhileRevalidate: ['/posts/', '/news/'], // Network only for real-time features networkOnly: ['/websocket/', '/stream/'], // Enable advanced features enableBackgroundSync: true, enablePushNotifications: true, skipWaiting: true, clientsClaim: true }); ``` ### PWA Installation Flow ```typescript class PWAInstaller { private pwaManager: PWAManager; constructor() { this.pwaManager = new PWAManager({ onInstall: this.onInstalled.bind(this), onUpdateAvailable: this.onUpdateAvailable.bind(this) }); } async initialize() { await this.pwaManager.initialize(); this.setupInstallButton(); this.checkForUpdates(); } private setupInstallButton() { const installButton = document.getElementById('install-pwa'); const state = this.pwaManager.getInstallationState(); if (state.isInstalled) { installButton.style.display = 'none'; } else if (state.isInstallable) { installButton.style.display = 'block'; installButton.addEventListener('click', this.handleInstall.bind(this)); } } private async handleInstall() { try { const result = await this.pwaManager.promptInstall(); if (result === 'accepted') { this.showSuccessMessage('App installed successfully!'); } } catch (error) { this.showErrorMessage('Installation failed. Please try again.'); } } private async checkForUpdates() { const hasUpdate = await this.pwaManager.checkForUpdates(); if (hasUpdate) { this.showUpdatePrompt(); } } private onInstalled() { document.getElementById('install-pwa').style.display = 'none'; this.showSuccessMessage('Welcome to the app!'); } private onUpdateAvailable(version: string) { this.showUpdateNotification(`New version (${version}) available!`); } } // Initialize PWA installer const installer = new PWAInstaller(); installer.initialize(); ``` ### Push Notifications Setup ```typescript class PushNotificationManager { private pwaManager: PWAManager; private vapidKey = 'YOUR_VAPID_PUBLIC_KEY'; constructor(pwaManager: PWAManager) { this.pwaManager = pwaManager; } async setupPushNotifications() { try { // Request permission const permission = await this.pwaManager.requestNotificationPermission(); if (permission === 'granted') { // Subscribe to push notifications const subscription = await this.pwaManager.subscribeToPush(this.vapidKey); // Send subscription to server await this.sendSubscriptionToServer(subscription); console.log('Push notifications enabled'); } } catch (error) { console.error('Push notification setup failed:', error); } } private async sendSubscriptionToServer(subscription: PushSubscription) { await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subscription: subscription.toJSON() }) }); } async unsubscribe() { const success = await this.pwaManager.unsubscribeFromPush(); if (success) { // Notify server await fetch('/api/push/unsubscribe', { method: 'POST' }); } return success; } } ``` ### Offline Data Management ```typescript class OfflineDataManager { private pwaManager: PWAManager; constructor(pwaManager: PWAManager) { this.pwaManager = pwaManager; this.setupOfflineHandling(); } private setupOfflineHandling() { // Listen for online/offline events window.addEventListener('online', this.handleOnline.bind(this)); window.addEventListener('offline', this.handleOffline.bind(this)); } private handleOffline() { console.log('App went offline'); this.showOfflineBanner(); this.enableOfflineMode(); } private handleOnline() { console.log('App is back online'); this.hideOfflineBanner(); this.syncOfflineData(); } private async syncOfflineData() { const offlineData = this.getOfflineData(); for (const item of offlineData) { try { await this.syncItem(item); this.removeOfflineItem(item.id); } catch (error) { console.error('Sync failed for item:', item.id, error); } } } private getOfflineData(): any[] { const data = localStorage.getItem('offline-data'); return data ? JSON.parse(data) : []; } private async syncItem(item: any) { const response = await fetch('/api/sync', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(item) }); if (!response.ok) { throw new Error(`Sync failed: ${response.statusText}`); } } private removeOfflineItem(id: string) { const data = this.getOfflineData(); const filtered = data.filter(item => item.id !== id); localStorage.setItem('offline-data', JSON.stringify(filtered)); } } ``` ## Complete Example Here's a complete example of setting up a PWA: ```typescript import { HolyPWA, PWAConfig, ServiceWorkerConfig } from '@mvp-factory/holy-pwa'; import fs from 'fs'; // 1. Generate manifest.json const manifestConfig: PWAConfig = { name: 'My Awesome PWA', shortName: 'AwesomePWA', description: 'An awesome progressive web application', themeColor: '#3b82f6', backgroundColor: '#ffffff', icons: { sizes: [72, 96, 128, 144, 152, 192, 384, 512], basePath: '/assets/icons' }, shortcuts: [ { name: 'New Task', short_name: 'New Task', description: 'Create a new task', url: '/new-task' } ] }; const manifestGenerator = new HolyPWA.ManifestGenerator(manifestConfig); const manifestJSON = manifestGenerator.generateJSON(); // Write manifest.json fs.writeFileSync('public/manifest.json', manifestJSON); // 2. Generate service worker const swConfig: ServiceWorkerConfig = { cacheName: 'awesome-pwa', version: '1.0.0', urlsToCache: [ '/', '/assets/css/app.css', '/assets/js/app.js', '/offline.html' ], networkFirst: ['/api/'], cacheFirst: ['/assets/', '/images/'], enableBackgroundSync: true, enablePushNotifications: true, offlinePageUrl: '/offline.html' }; const swGenerator = new HolyPWA.ServiceWorkerGenerator(swConfig); const serviceWorkerCode = swGenerator.generate(); // Write service worker fs.writeFileSync('public/service-worker.js', serviceWorkerCode); // 3. Initialize PWA in client-side code const pwaManager = HolyPWA.createManager({ onInstall: () => console.log('PWA installed!'), onUpdateAvailable: (version) => console.log(`Update available: ${version}`), onOffline: () => document.body.classList.add('offline'), onOnline: () => document.body.classList.remove('offline') }); // Initialize on page load document.addEventListener('DOMContentLoaded', async () => { await pwaManager.initialize('/service-worker.js'); // Setup install button const installButton = document.getElementById('install-btn'); const state = pwaManager.getInstallationState(); if (state.isInstallable) { installButton.style.display = 'block'; installButton.addEventListener('click', async () => { await pwaManager.promptInstall(); }); } }); ``` ## HTML Integration Add to your HTML `<head>`: ```html <!-- PWA Manifest --> <link rel="manifest" href="/manifest.json"> <!-- Theme colors --> <meta name="theme-color" content="#3b82f6"> <meta name="apple-mobile-web-app-status-bar-style" content="default"> <!-- PWA meta tags --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-title" content="My PWA"> <meta name="mobile-web-app-capable" content="yes"> <!-- Icons --> <link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png"> <link rel="icon" type="image/png" sizes="192x192" href="/assets/icons/icon-192x192.png"> <!-- Service Worker Registration --> <script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('SW registered: ', registration); }) .catch(registrationError => { console.log('SW registration failed: ', registrationError); }); }); } </script> ``` ## Error Handling ```typescript import { PWAError, ManifestError, ServiceWorkerError } from '@mvp-factory/holy-pwa'; try { const pwaManager = new PWAManager(); await pwaManager.initialize(); } catch (error) { if (error instanceof PWAError) { switch (error.code) { case 'SERVICE_WORKER_FAILED': console.error('Service Worker failed:', error.message); break; case 'INSTALL_FAILED': console.error('Installation failed:', error.message); break; case 'NOTIFICATION_DENIED': console.error('Notifications denied:', error.message); break; default: console.error('PWA error:', error.message); } } } ``` ## Testing ```bash # Run tests npm test # Test PWA features npm run test:pwa # Lighthouse PWA audit npx lighthouse https://your-app.com --view ``` ## Development vs Production ### Development ```typescript // Enable debugging and verbose logging const pwaManager = HolyPWA.createManager({ onInstall: () => console.log('[DEV] PWA installed'), onUpdateAvailable: (v) => console.log(`[DEV] Update: ${v}`), onOffline: () => console.log('[DEV] Offline'), onOnline: () => console.log('[DEV] Online') }); // Use development service worker const swGenerator = HolyPWA.serviceWorkerTemplates.advanced({ cacheName: 'dev-pwa', version: `dev-${Date.now()}`, // Unique version for development enableBackgroundSync: false, // Disable for development skipWaiting: true, // Always update immediately clientsClaim: true }); ``` ### Production ```typescript // Production PWA manager const pwaManager = HolyPWA.createManager({ onInstall: () => analytics.track('pwa_installed'), onUpdateAvailable: showUpdatePrompt, onOffline: enableOfflineMode, onOnline: syncOfflineData }); // Production service worker const swGenerator = HolyPWA.serviceWorkerTemplates.advanced({ cacheName: 'prod-pwa', version: process.env.APP_VERSION, enableBackgroundSync: true, enablePushNotifications: true, skipWaiting: false, // Wait for user confirmation clientsClaim: false }); ``` ## Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## License MIT © MVP Factory ## Support - 📧 Email: support@mvp-factory.dev - 🐛 Issues: [GitHub Issues](https://github.com/mvp-factory/modules/issues) - 📖 Documentation: [Full Documentation](https://docs.mvp-factory.dev/modules/holy-pwa) --- **Extracted from Holy Habit project** - Battle-tested PWA system used in production with advanced offline support, push notifications, and seamless app-like experience.