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

328 lines (300 loc) 8.95 kB
/** * Manifest Generator * * PWA manifest.json generator with template support * Extracted from Holy Habit manifest configuration */ import { PWAManifest, PWAConfig, PWAIcon, PWAShortcut, PWAScreenshot, ManifestError } from '../types/PWA'; export class ManifestGenerator { private config: PWAConfig; constructor(config: PWAConfig) { this.config = config; this.validateConfig(); } /** * Generate complete PWA manifest * * @returns PWA manifest object */ generate(): PWAManifest { return { name: this.config.name, short_name: this.config.shortName, description: this.config.description, start_url: this.config.startUrl || '/', display: this.config.display || 'standalone', background_color: this.config.backgroundColor, theme_color: this.config.themeColor, orientation: this.config.orientation || 'portrait', scope: this.config.scope || '/', lang: this.config.lang || 'en-US', icons: this.generateIcons(), shortcuts: this.generateShortcuts(), categories: this.config.categories || ['productivity'], iarc_rating_id: this.generateIarcRatingId(), edge_side_panel: this.generateEdgeSidePanel(), file_handlers: this.generateFileHandlers(), protocol_handlers: this.generateProtocolHandlers(), screenshots: this.generateScreenshots(), related_applications: [], prefer_related_applications: false }; } /** * Generate icons array with all standard sizes * * @returns Array of PWA icons */ private generateIcons(): PWAIcon[] { const standardSizes = this.config.icons.sizes.length > 0 ? this.config.icons.sizes : [72, 96, 128, 144, 152, 192, 384, 512]; return standardSizes.map(size => ({ src: `${this.config.icons.basePath}/icon-${size}x${size}.png`, sizes: `${size}x${size}`, type: 'image/png', purpose: 'any maskable' })); } /** * Generate shortcuts for PWA * * @returns Array of PWA shortcuts */ private generateShortcuts(): PWAShortcut[] | undefined { if (!this.config.shortcuts) return undefined; return this.config.shortcuts.map(shortcut => ({ ...shortcut, icons: [{ src: `${this.config.icons.basePath}/shortcut-${shortcut.name.toLowerCase().replace(/\s+/g, '-')}-icon.png`, sizes: '96x96', type: 'image/png' }] })); } /** * Generate screenshots for app stores * * @returns Array of PWA screenshots */ private generateScreenshots(): PWAScreenshot[] | undefined { if (!this.config.screenshots) return undefined; return this.config.screenshots.map((screenshot, index) => ({ src: `${this.config.icons.basePath}/screenshot-${index + 1}.png`, sizes: screenshot.sizes, type: screenshot.type, label: screenshot.label, platform: screenshot.platform })); } /** * Generate IARC rating ID * * @returns IARC rating ID */ private generateIarcRatingId(): string { return this.config.name.toLowerCase().replace(/\s+/g, '-'); } /** * Generate Edge side panel configuration * * @returns Edge side panel config */ private generateEdgeSidePanel() { return { preferred_width: 480 }; } /** * Generate file handlers * * @returns Array of file handlers */ private generateFileHandlers() { // Common file handlers for productivity apps return [ { action: '/editor', accept: { 'text/plain': ['.txt', '.md'], 'application/json': ['.json'] } } ]; } /** * Generate protocol handlers * * @returns Array of protocol handlers */ private generateProtocolHandlers() { const protocolName = this.config.name.toLowerCase().replace(/\s+/g, ''); return [ { protocol: `web+${protocolName}`, url: '/?url=%s' } ]; } /** * Generate JSON string with formatting * * @param indent - Indentation spaces (default: 2) * @returns Formatted JSON string */ generateJSON(indent: number = 2): string { const manifest = this.generate(); return JSON.stringify(manifest, null, indent); } /** * Validate configuration * * @throws ManifestError if configuration is invalid */ private validateConfig(): void { const required = ['name', 'shortName', 'description', 'themeColor', 'backgroundColor']; for (const field of required) { if (!this.config[field as keyof PWAConfig]) { throw new ManifestError(`Missing required field: ${field}`); } } // Validate color format const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; if (!colorRegex.test(this.config.themeColor)) { throw new ManifestError(`Invalid theme color format: ${this.config.themeColor}`); } if (!colorRegex.test(this.config.backgroundColor)) { throw new ManifestError(`Invalid background color format: ${this.config.backgroundColor}`); } // Validate icons configuration if (!this.config.icons || !this.config.icons.basePath) { throw new ManifestError('Icons configuration is required'); } } /** * Update configuration * * @param newConfig - New configuration to merge */ updateConfig(newConfig: Partial<PWAConfig>): void { this.config = { ...this.config, ...newConfig }; this.validateConfig(); } /** * Get current configuration * * @returns Current PWA configuration */ getConfig(): PWAConfig { return { ...this.config }; } /** * Create manifest for specific use cases */ static createTemplates() { return { /** * Create manifest for productivity app */ productivity: (config: Partial<PWAConfig>): ManifestGenerator => { const productivityConfig: PWAConfig = { name: 'Productivity App', shortName: 'ProductivityApp', description: 'A powerful productivity application', themeColor: '#3b82f6', backgroundColor: '#ffffff', display: 'standalone', categories: ['productivity', 'business'], icons: { sizes: [72, 96, 128, 144, 152, 192, 384, 512], basePath: '/assets/icons' }, shortcuts: [ { name: 'New Document', short_name: 'New Doc', description: 'Create a new document', url: '/new' }, { name: 'Dashboard', short_name: 'Dashboard', description: 'View your dashboard', url: '/dashboard' } ], ...config }; return new ManifestGenerator(productivityConfig); }, /** * Create manifest for social app */ social: (config: Partial<PWAConfig>): ManifestGenerator => { const socialConfig: PWAConfig = { name: 'Social App', shortName: 'SocialApp', description: 'Connect with friends and family', themeColor: '#1da1f2', backgroundColor: '#ffffff', display: 'standalone', categories: ['social', 'lifestyle'], icons: { sizes: [72, 96, 128, 144, 152, 192, 384, 512], basePath: '/assets/icons' }, shortcuts: [ { name: 'New Post', short_name: 'Post', description: 'Create a new post', url: '/compose' }, { name: 'Messages', short_name: 'Messages', description: 'View your messages', url: '/messages' } ], ...config }; return new ManifestGenerator(socialConfig); }, /** * Create manifest for e-commerce app */ ecommerce: (config: Partial<PWAConfig>): ManifestGenerator => { const ecommerceConfig: PWAConfig = { name: 'Shop App', shortName: 'ShopApp', description: 'Shop your favorite products', themeColor: '#059669', backgroundColor: '#ffffff', display: 'standalone', categories: ['shopping', 'business'], icons: { sizes: [72, 96, 128, 144, 152, 192, 384, 512], basePath: '/assets/icons' }, shortcuts: [ { name: 'Browse Products', short_name: 'Browse', description: 'Browse all products', url: '/products' }, { name: 'My Cart', short_name: 'Cart', description: 'View your shopping cart', url: '/cart' } ], ...config }; return new ManifestGenerator(ecommerceConfig); } }; } }