@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
text/typescript
/**
* 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);
}
};
}
}