@jager-ai/holy-pwa
v1.0.0
Published
Progressive Web App (PWA) utilities and templates extracted from Holy Habit project with manifest generation, service worker management, and offline support
Maintainers
Readme
@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
npm install @mvp-factory/holy-pwaQuick Start
1. Generate PWA Manifest
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
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
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:
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:
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:
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:
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:
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
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
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
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
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:
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>:
<!-- 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
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
# Run tests
npm test
# Test PWA features
npm run test:pwa
# Lighthouse PWA audit
npx lighthouse https://your-app.com --viewDevelopment vs Production
Development
// 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
// 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
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © MVP Factory
Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Documentation: Full Documentation
Extracted from Holy Habit project - Battle-tested PWA system used in production with advanced offline support, push notifications, and seamless app-like experience.
