mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 15:22:55 +01:00
472 lines
12 KiB
Markdown
472 lines
12 KiB
Markdown
# CONFIG-005: PWA Manifest Generation
|
|
|
|
## Overview
|
|
|
|
Generate a valid `manifest.json` file and auto-scale app icons from a single source image for Progressive Web App support.
|
|
|
|
**Estimated effort:** 10-14 hours
|
|
**Dependencies:** CONFIG-001, CONFIG-002
|
|
**Blocks:** None (enables Phase 5 Capacitor integration)
|
|
|
|
---
|
|
|
|
## Objectives
|
|
|
|
1. Generate `manifest.json` from app config
|
|
2. Auto-scale icons from single 512x512 source
|
|
3. Copy icons to build output
|
|
4. Validate PWA requirements
|
|
5. Generate service worker stub (optional)
|
|
|
|
---
|
|
|
|
## Generated manifest.json
|
|
|
|
```json
|
|
{
|
|
"name": "My Amazing App",
|
|
"short_name": "My App",
|
|
"description": "A visual programming app that makes building easy",
|
|
"start_url": "/",
|
|
"display": "standalone",
|
|
"background_color": "#ffffff",
|
|
"theme_color": "#d21f3c",
|
|
"icons": [
|
|
{
|
|
"src": "/icons/icon-72x72.png",
|
|
"sizes": "72x72",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-96x96.png",
|
|
"sizes": "96x96",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-128x128.png",
|
|
"sizes": "128x128",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-144x144.png",
|
|
"sizes": "144x144",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-152x152.png",
|
|
"sizes": "152x152",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-192x192.png",
|
|
"sizes": "192x192",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-384x384.png",
|
|
"sizes": "384x384",
|
|
"type": "image/png"
|
|
},
|
|
{
|
|
"src": "/icons/icon-512x512.png",
|
|
"sizes": "512x512",
|
|
"type": "image/png"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Files to Create
|
|
|
|
### 1. Manifest Generator
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/utils/compilation/build/processors/manifest-processor.ts`
|
|
|
|
```typescript
|
|
import * as path from 'path';
|
|
import * as fs from 'fs-extra';
|
|
import { ProjectModel } from '@noodl-models/projectmodel';
|
|
import { AppConfig } from '@noodl/runtime/src/config/types';
|
|
|
|
// Standard PWA icon sizes
|
|
const ICON_SIZES = [72, 96, 128, 144, 152, 192, 384, 512];
|
|
|
|
export interface ManifestProcessorOptions {
|
|
baseUrl?: string;
|
|
outputDir: string;
|
|
}
|
|
|
|
export class ManifestProcessor {
|
|
constructor(public readonly project: ProjectModel) {}
|
|
|
|
public async process(options: ManifestProcessorOptions): Promise<void> {
|
|
const config = this.project.getAppConfig();
|
|
|
|
// Only generate if PWA is enabled
|
|
if (!config.pwa?.enabled) {
|
|
return;
|
|
}
|
|
|
|
const baseUrl = options.baseUrl || '/';
|
|
|
|
// Generate icons from source
|
|
await this.generateIcons(config, options.outputDir, baseUrl);
|
|
|
|
// Generate manifest.json
|
|
const manifest = this.generateManifest(config, baseUrl);
|
|
const manifestPath = path.join(options.outputDir, 'manifest.json');
|
|
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
}
|
|
|
|
private generateManifest(config: AppConfig, baseUrl: string): object {
|
|
const manifest: Record<string, any> = {
|
|
name: config.identity.appName,
|
|
short_name: config.pwa?.shortName || config.identity.appName,
|
|
description: config.identity.description || '',
|
|
start_url: config.pwa?.startUrl || '/',
|
|
display: config.pwa?.display || 'standalone',
|
|
background_color: config.pwa?.backgroundColor || '#ffffff',
|
|
theme_color: config.seo.themeColor || '#000000',
|
|
icons: this.generateIconsManifest(baseUrl)
|
|
};
|
|
|
|
// Optional fields
|
|
if (config.identity.description) {
|
|
manifest.description = config.identity.description;
|
|
}
|
|
|
|
return manifest;
|
|
}
|
|
|
|
private generateIconsManifest(baseUrl: string): object[] {
|
|
return ICON_SIZES.map(size => ({
|
|
src: `${baseUrl}icons/icon-${size}x${size}.png`,
|
|
sizes: `${size}x${size}`,
|
|
type: 'image/png'
|
|
}));
|
|
}
|
|
|
|
private async generateIcons(
|
|
config: AppConfig,
|
|
outputDir: string,
|
|
baseUrl: string
|
|
): Promise<void> {
|
|
const sourceIcon = config.pwa?.sourceIcon;
|
|
|
|
if (!sourceIcon) {
|
|
console.warn('PWA enabled but no source icon provided. Using placeholder.');
|
|
await this.generatePlaceholderIcons(outputDir);
|
|
return;
|
|
}
|
|
|
|
// Resolve source icon path
|
|
const sourcePath = this.resolveAssetPath(sourceIcon);
|
|
|
|
if (!await fs.pathExists(sourcePath)) {
|
|
console.warn(`Source icon not found: ${sourcePath}. Using placeholder.`);
|
|
await this.generatePlaceholderIcons(outputDir);
|
|
return;
|
|
}
|
|
|
|
// Ensure icons directory exists
|
|
const iconsDir = path.join(outputDir, 'icons');
|
|
await fs.ensureDir(iconsDir);
|
|
|
|
// Generate each size
|
|
for (const size of ICON_SIZES) {
|
|
const outputPath = path.join(iconsDir, `icon-${size}x${size}.png`);
|
|
await this.resizeIcon(sourcePath, outputPath, size);
|
|
}
|
|
}
|
|
|
|
private async resizeIcon(
|
|
sourcePath: string,
|
|
outputPath: string,
|
|
size: number
|
|
): Promise<void> {
|
|
// Use sharp for image resizing
|
|
const sharp = require('sharp');
|
|
|
|
try {
|
|
await sharp(sourcePath)
|
|
.resize(size, size, {
|
|
fit: 'contain',
|
|
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
|
})
|
|
.png()
|
|
.toFile(outputPath);
|
|
} catch (error) {
|
|
console.error(`Failed to resize icon to ${size}x${size}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async generatePlaceholderIcons(outputDir: string): Promise<void> {
|
|
const iconsDir = path.join(outputDir, 'icons');
|
|
await fs.ensureDir(iconsDir);
|
|
|
|
// Generate simple colored placeholder icons
|
|
const sharp = require('sharp');
|
|
|
|
for (const size of ICON_SIZES) {
|
|
const outputPath = path.join(iconsDir, `icon-${size}x${size}.png`);
|
|
|
|
// Create a simple colored square as placeholder
|
|
await sharp({
|
|
create: {
|
|
width: size,
|
|
height: size,
|
|
channels: 4,
|
|
background: { r: 100, g: 100, b: 100, alpha: 1 }
|
|
}
|
|
})
|
|
.png()
|
|
.toFile(outputPath);
|
|
}
|
|
}
|
|
|
|
private resolveAssetPath(assetPath: string): string {
|
|
if (path.isAbsolute(assetPath)) {
|
|
return assetPath;
|
|
}
|
|
return path.join(this.project._retainedProjectDirectory, assetPath);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Icon Validation
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/views/panels/AppSetupPanel/utils/icon-validation.ts`
|
|
|
|
```typescript
|
|
export interface IconValidationResult {
|
|
valid: boolean;
|
|
errors: string[];
|
|
warnings: string[];
|
|
dimensions?: { width: number; height: number };
|
|
}
|
|
|
|
export async function validatePWAIcon(filePath: string): Promise<IconValidationResult> {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
try {
|
|
const sharp = require('sharp');
|
|
const metadata = await sharp(filePath).metadata();
|
|
|
|
const { width, height, format } = metadata;
|
|
|
|
// Check format
|
|
if (format !== 'png') {
|
|
errors.push('Icon must be a PNG file');
|
|
}
|
|
|
|
// Check dimensions
|
|
if (!width || !height) {
|
|
errors.push('Could not read image dimensions');
|
|
} else {
|
|
// Check if square
|
|
if (width !== height) {
|
|
errors.push(`Icon must be square. Current: ${width}x${height}`);
|
|
}
|
|
|
|
// Check minimum size
|
|
if (width < 512 || height < 512) {
|
|
errors.push(`Icon must be at least 512x512 pixels. Current: ${width}x${height}`);
|
|
}
|
|
|
|
// Warn if not exactly 512x512
|
|
if (width !== 512 && width > 512) {
|
|
warnings.push(`Icon will be scaled down from ${width}x${height} to required sizes`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
dimensions: width && height ? { width, height } : undefined
|
|
};
|
|
|
|
} catch (error) {
|
|
return {
|
|
valid: false,
|
|
errors: [`Failed to read image: ${error.message}`],
|
|
warnings: []
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Service Worker Stub (Optional)
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/utils/compilation/build/templates/sw.js`
|
|
|
|
```javascript
|
|
// Basic service worker for PWA install prompt
|
|
// This is a minimal stub - users can replace with their own
|
|
|
|
const CACHE_NAME = 'app-cache-v1';
|
|
const ASSETS_TO_CACHE = [
|
|
'/',
|
|
'/index.html'
|
|
];
|
|
|
|
// Install event - cache basic assets
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME)
|
|
.then((cache) => cache.addAll(ASSETS_TO_CACHE))
|
|
);
|
|
});
|
|
|
|
// Fetch event - network first, fallback to cache
|
|
self.addEventListener('fetch', (event) => {
|
|
event.respondWith(
|
|
fetch(event.request)
|
|
.catch(() => caches.match(event.request))
|
|
);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Files to Modify
|
|
|
|
### 1. Build Pipeline
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/utils/compilation/build/build.ts` (or equivalent)
|
|
|
|
Add manifest processing to build pipeline:
|
|
|
|
```typescript
|
|
import { ManifestProcessor } from './processors/manifest-processor';
|
|
|
|
async function buildProject(project: ProjectModel, options: BuildOptions): Promise<void> {
|
|
// ... existing build steps ...
|
|
|
|
// Generate PWA manifest and icons
|
|
const manifestProcessor = new ManifestProcessor(project);
|
|
await manifestProcessor.process({
|
|
baseUrl: options.baseUrl,
|
|
outputDir: options.outputDir
|
|
});
|
|
|
|
// ... rest of build ...
|
|
}
|
|
```
|
|
|
|
### 2. Deploy Build
|
|
|
|
Ensure the deploy process also runs manifest generation:
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/utils/compilation/deploy/`
|
|
|
|
```typescript
|
|
// Include manifest generation in deploy build
|
|
await manifestProcessor.process({
|
|
baseUrl: deployConfig.baseUrl || '/',
|
|
outputDir: deployOutputDir
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Icon Size Reference
|
|
|
|
| Size | Usage |
|
|
|------|-------|
|
|
| 72x72 | Android home screen (legacy) |
|
|
| 96x96 | Android home screen |
|
|
| 128x128 | Chrome Web Store |
|
|
| 144x144 | IE/Edge pinned sites |
|
|
| 152x152 | iPad home screen |
|
|
| 192x192 | Android home screen (modern), Chrome install |
|
|
| 384x384 | Android splash screen |
|
|
| 512x512 | Android splash screen, PWA install prompt |
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
### Manifest Generation
|
|
|
|
- [ ] manifest.json created when PWA enabled
|
|
- [ ] manifest.json not created when PWA disabled
|
|
- [ ] name field matches appName
|
|
- [ ] short_name matches config or falls back to name
|
|
- [ ] description present if configured
|
|
- [ ] start_url correct
|
|
- [ ] display mode correct
|
|
- [ ] background_color correct
|
|
- [ ] theme_color correct
|
|
- [ ] icons array has all 8 sizes
|
|
|
|
### Icon Generation
|
|
|
|
- [ ] All 8 icon sizes generated
|
|
- [ ] Icons are valid PNG files
|
|
- [ ] Icons maintain aspect ratio
|
|
- [ ] Icons have correct dimensions
|
|
- [ ] Placeholder icons generated when no source
|
|
- [ ] Warning shown when source icon missing
|
|
- [ ] Error shown for invalid source format
|
|
|
|
### Validation
|
|
|
|
- [ ] Non-square icons rejected
|
|
- [ ] Non-PNG icons rejected
|
|
- [ ] Too-small icons rejected
|
|
- [ ] Valid icons pass validation
|
|
- [ ] Warnings for oversized icons
|
|
|
|
### Integration
|
|
|
|
- [ ] manifest.json linked in HTML head
|
|
- [ ] Icons accessible at correct paths
|
|
- [ ] PWA install prompt appears in Chrome
|
|
- [ ] App installs correctly
|
|
- [ ] Installed app shows correct icon
|
|
- [ ] Splash screen displays correctly
|
|
|
|
---
|
|
|
|
## Notes for Implementer
|
|
|
|
### Sharp Dependency
|
|
|
|
The `sharp` library is required for image processing. It should already be available in the editor dependencies. If not:
|
|
|
|
```bash
|
|
npm install sharp --save
|
|
```
|
|
|
|
### Service Worker Considerations
|
|
|
|
The basic service worker stub is intentionally minimal. Advanced features like:
|
|
- Offline caching strategies
|
|
- Push notifications
|
|
- Background sync
|
|
|
|
...should be implemented by users through custom code or future Noodl features.
|
|
|
|
### Capacitor Integration (Phase 5)
|
|
|
|
The icon generation logic will be reused for Capacitor builds:
|
|
- iOS requires specific sizes: 1024, 180, 167, 152, 120, 76, 60, 40, 29, 20
|
|
- Android uses adaptive icons with foreground/background layers
|
|
|
|
Consider making icon generation configurable for different target platforms.
|
|
|
|
### Testing PWA Installation
|
|
|
|
Test PWA installation on:
|
|
- Chrome (desktop and mobile)
|
|
- Edge (desktop)
|
|
- Safari (iOS - via Add to Home Screen)
|
|
- Firefox (Android)
|
|
|
|
Use Chrome DevTools > Application > Manifest to verify manifest is valid.
|