@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
Markdown
# @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.