@djangocfg/nextjs 2.1.35 → 2.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +146 -22
  2. package/dist/config/index.d.mts +7 -409
  3. package/dist/config/index.mjs +79 -394
  4. package/dist/config/index.mjs.map +1 -1
  5. package/dist/index.d.mts +2 -1
  6. package/dist/index.mjs +79 -394
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/plugin-DuRJ_Jq6.d.mts +100 -0
  9. package/dist/pwa/cli.d.mts +1 -0
  10. package/dist/pwa/cli.mjs +140 -0
  11. package/dist/pwa/cli.mjs.map +1 -0
  12. package/dist/pwa/index.d.mts +274 -0
  13. package/dist/pwa/index.mjs +327 -0
  14. package/dist/pwa/index.mjs.map +1 -0
  15. package/dist/pwa/server/index.d.mts +86 -0
  16. package/dist/pwa/server/index.mjs +175 -0
  17. package/dist/pwa/server/index.mjs.map +1 -0
  18. package/dist/pwa/server/routes.d.mts +2 -0
  19. package/dist/pwa/server/routes.mjs +149 -0
  20. package/dist/pwa/server/routes.mjs.map +1 -0
  21. package/dist/pwa/worker/index.d.mts +56 -0
  22. package/dist/pwa/worker/index.mjs +97 -0
  23. package/dist/pwa/worker/index.mjs.map +1 -0
  24. package/dist/routes-DXA29sS_.d.mts +68 -0
  25. package/package.json +38 -9
  26. package/src/config/createNextConfig.ts +9 -13
  27. package/src/config/index.ts +2 -19
  28. package/src/config/plugins/devStartup.ts +35 -36
  29. package/src/config/plugins/index.ts +1 -1
  30. package/src/config/utils/index.ts +0 -1
  31. package/src/index.ts +4 -0
  32. package/src/pwa/cli.ts +171 -0
  33. package/src/pwa/index.ts +9 -0
  34. package/src/pwa/manifest.ts +355 -0
  35. package/src/pwa/notifications.ts +192 -0
  36. package/src/pwa/plugin.ts +194 -0
  37. package/src/pwa/server/index.ts +23 -0
  38. package/src/pwa/server/push.ts +166 -0
  39. package/src/pwa/server/routes.ts +137 -0
  40. package/src/pwa/worker/index.ts +174 -0
  41. package/src/pwa/worker/package.json +3 -0
  42. package/bin/dev-with-browser.js +0 -114
  43. package/src/config/plugins/pwa.ts +0 -616
  44. package/src/config/utils/manifest.ts +0 -195
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dev Startup Webpack Plugin
3
3
  *
4
- * Handles banner display, version checking, package updates, and browser auto-open.
4
+ * Handles banner display, version checking, and package updates.
5
5
  */
6
6
 
7
7
  import type { Compiler } from 'webpack';
@@ -14,11 +14,8 @@ import { checkAndUpdatePackages } from '../packages/updater';
14
14
 
15
15
  // Track if startup tasks were already run (persists across HMR)
16
16
  let startupDone = false;
17
- let browserOpened = false;
18
17
 
19
18
  export interface DevStartupPluginOptions {
20
- /** Auto-open browser */
21
- openBrowser?: boolean;
22
19
  /** Check for missing optional packages (default: true) */
23
20
  checkPackages?: boolean;
24
21
  /** Auto-install missing packages without prompting */
@@ -49,12 +46,6 @@ export class DevStartupPlugin {
49
46
  startupDone = true;
50
47
  await this.runStartupTasks();
51
48
  }
52
-
53
- // Auto-open browser if enabled
54
- if (this.options.openBrowser && !browserOpened) {
55
- browserOpened = true;
56
- this.openBrowser();
57
- }
58
49
  });
59
50
  }
60
51
 
@@ -68,10 +59,13 @@ export class DevStartupPlugin {
68
59
  console.log(chalk.dim(` 📦 @djangocfg/nextjs v${version}`));
69
60
  }
70
61
 
71
- // 3. Print AI docs hint
62
+ // 3. Check PWA setup
63
+ this.checkPWASetup();
64
+
65
+ // 4. Print AI docs hint
72
66
  console.log(chalk.magenta(` ${AI_DOCS_HINT}\n`));
73
67
 
74
- // 4. Check for package updates
68
+ // 5. Check for package updates
75
69
  if (this.options.checkUpdates !== false) {
76
70
  try {
77
71
  await checkAndUpdatePackages({
@@ -84,7 +78,7 @@ export class DevStartupPlugin {
84
78
  }
85
79
  }
86
80
 
87
- // 5. Check for missing optional packages
81
+ // 6. Check for missing optional packages
88
82
  if (this.options.checkPackages !== false) {
89
83
  await checkAndInstallPackages({
90
84
  autoInstall: this.options.autoInstall,
@@ -92,29 +86,35 @@ export class DevStartupPlugin {
92
86
  }
93
87
  }
94
88
 
95
- private openBrowser(): void {
96
- // Delay to ensure server is ready
97
- setTimeout(async () => {
98
- try {
99
- const { exec } = await import('child_process');
100
- const port = process.env.PORT || '3000';
101
- const url = `http://localhost:${port}`;
102
-
103
- const command = process.platform === 'darwin'
104
- ? 'open'
105
- : process.platform === 'win32'
106
- ? 'start'
107
- : 'xdg-open';
108
-
109
- exec(`${command} ${url}`, (error) => {
110
- if (error) {
111
- console.warn(`Failed to open browser: ${error.message}`);
112
- }
113
- });
114
- } catch (error) {
115
- console.warn('Failed to open browser');
89
+ private checkPWASetup(): void {
90
+ const fs = require('fs');
91
+ const path = require('path');
92
+
93
+ const cwd = process.cwd();
94
+ const swPath = path.join(cwd, 'app', 'sw.ts');
95
+ const manifestPath = path.join(cwd, 'app', 'manifest.ts');
96
+
97
+ const hasSW = fs.existsSync(swPath);
98
+ const hasManifest = fs.existsSync(manifestPath);
99
+
100
+ if (hasSW || hasManifest) {
101
+ console.log(chalk.cyan(' 📱 PWA Configuration:'));
102
+
103
+ if (hasSW) {
104
+ console.log(chalk.green(' ✓ Service Worker: app/sw.ts'));
105
+ } else {
106
+ console.log(chalk.yellow(' ⚠ Service Worker: not found'));
107
+ }
108
+
109
+ if (hasManifest) {
110
+ console.log(chalk.green(' ✓ Manifest: app/manifest.ts'));
111
+ } else {
112
+ console.log(chalk.yellow(' ⚠ Manifest: not found'));
116
113
  }
117
- }, 2000);
114
+
115
+ console.log(chalk.dim(' → Check: DevTools → Application → Service Workers'));
116
+ console.log(chalk.dim(' → Test push: import from @djangocfg/nextjs/pwa'));
117
+ }
118
118
  }
119
119
  }
120
120
 
@@ -123,5 +123,4 @@ export class DevStartupPlugin {
123
123
  */
124
124
  export function resetDevStartupState(): void {
125
125
  startupDone = false;
126
- browserOpened = false;
127
126
  }
@@ -4,4 +4,4 @@
4
4
 
5
5
  export { DevStartupPlugin, resetDevStartupState, type DevStartupPluginOptions } from './devStartup';
6
6
  export { addCompressionPlugins, isCompressionAvailable, type CompressionPluginOptions } from './compression';
7
- export { withPWA, isPWAAvailable, defaultRuntimeCaching, type PWAPluginOptions } from './pwa';
7
+ export { withPWA, defaultRuntimeCaching, type PWAPluginOptions } from '../../pwa/plugin';
@@ -5,4 +5,3 @@
5
5
  export { deepMerge } from './deepMerge';
6
6
  export * from './env';
7
7
  export * from './version';
8
- export * from './manifest';
package/src/index.ts CHANGED
@@ -25,3 +25,7 @@ export * from './ai';
25
25
  // Types
26
26
  export type * from './types';
27
27
 
28
+ // PWA worker utilities (separate export for tree-shaking)
29
+ // Import via: import { createServiceWorker } from '@djangocfg/nextjs/worker'
30
+ // Note: worker/ has its own package.json with "type": "module"
31
+
package/src/pwa/cli.ts ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DjangoCFG PWA CLI
4
+ *
5
+ * Usage:
6
+ * pnpm pwa vapid # Generate VAPID keys
7
+ * pnpm pwa send # Send test push notification
8
+ * pnpm pwa status # Show PWA status
9
+ * pnpm pwa info # Show help
10
+ */
11
+
12
+ import { consola } from 'consola';
13
+ import webpush from 'web-push';
14
+
15
+ const args = process.argv.slice(2);
16
+ const command = args[0];
17
+
18
+ async function generateVapidKeys() {
19
+ consola.box('VAPID Keys Generator');
20
+ consola.info('Generating new VAPID key pair...\n');
21
+
22
+ const vapidKeys = webpush.generateVAPIDKeys();
23
+
24
+ consola.success('✅ VAPID keys generated!\n');
25
+ consola.log('Add these to your .env.local:\n');
26
+ consola.log(`NEXT_PUBLIC_VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`);
27
+ consola.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`);
28
+ consola.log(`VAPID_MAILTO=mailto:your-email@example.com\n`);
29
+
30
+ consola.info('Public key (share with clients):');
31
+ consola.log(` ${vapidKeys.publicKey}\n`);
32
+ consola.info('Private key (keep secret):');
33
+ consola.log(` ${vapidKeys.privateKey}\n`);
34
+ }
35
+
36
+ async function sendTestPush() {
37
+ consola.box('Send Test Push Notification');
38
+
39
+ const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
40
+ const privateKey = process.env.VAPID_PRIVATE_KEY;
41
+ const mailto = process.env.VAPID_MAILTO || 'mailto:test@example.com';
42
+
43
+ if (!publicKey || !privateKey) {
44
+ consola.error('❌ VAPID keys not configured!');
45
+ consola.info('\nGenerate keys with:');
46
+ consola.log(' pnpm pwa vapid\n');
47
+ consola.info('Then add to .env.local:');
48
+ consola.log(' NEXT_PUBLIC_VAPID_PUBLIC_KEY=...');
49
+ consola.log(' VAPID_PRIVATE_KEY=...');
50
+ process.exit(1);
51
+ }
52
+
53
+ // Get subscription from args or use example
54
+ const subscriptionArg = args[1];
55
+ if (!subscriptionArg) {
56
+ consola.error('❌ Subscription required!\n');
57
+ consola.info('Usage:');
58
+ consola.log(' pnpm pwa send \'{"endpoint":"...","keys":{...}}\'\n');
59
+ consola.info('To subscribe and get the JSON, visit the playground:');
60
+ consola.log(' http://djangocfg.com/layouts/a2hs-hint\n');
61
+ consola.info('Or use the helper command:');
62
+ consola.log(' pnpm exec djangocfg-pwa status\n');
63
+ process.exit(1);
64
+ }
65
+
66
+ try {
67
+ const subscription = JSON.parse(subscriptionArg);
68
+
69
+ webpush.setVapidDetails(mailto, publicKey, privateKey);
70
+
71
+ const payload = JSON.stringify({
72
+ title: 'Test Push from CLI',
73
+ body: 'This is a test notification sent from @djangocfg/nextjs CLI',
74
+ icon: '/static/logos/192x192.png',
75
+ badge: '/static/logos/192x192.png',
76
+ });
77
+
78
+ consola.info('Sending push notification...\n');
79
+ await webpush.sendNotification(subscription, payload);
80
+ consola.success('✅ Push notification sent successfully!');
81
+ } catch (err) {
82
+ consola.error('❌ Failed to send push:', err instanceof Error ? err.message : err);
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ async function showStatus() {
88
+ consola.box('PWA Status');
89
+
90
+ const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
91
+ const privateKey = process.env.VAPID_PRIVATE_KEY;
92
+ const mailto = process.env.VAPID_MAILTO;
93
+
94
+ consola.log('Environment Variables:');
95
+ consola.log(` NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${publicKey ? '✅ Set' : '❌ Not set'}`);
96
+ consola.log(` VAPID_PRIVATE_KEY: ${privateKey ? '✅ Set' : '❌ Not set'}`);
97
+ consola.log(` VAPID_MAILTO: ${mailto || '⚠️ Not set (optional)'}\n`);
98
+
99
+ if (!publicKey || !privateKey) {
100
+ consola.warn('⚠️ VAPID keys not configured. Run: pnpm pwa vapid\n');
101
+ } else {
102
+ consola.success('✅ VAPID keys configured\n');
103
+ consola.log('Public Key:');
104
+ consola.log(` ${publicKey.slice(0, 40)}...\n`);
105
+ }
106
+
107
+ consola.log('Quick Start:');
108
+ consola.log(' 1. Generate VAPID keys: pnpm pwa vapid');
109
+ consola.log(' 2. Add to .env.local');
110
+ consola.log(' 3. Visit playground: http://djangocfg.com/layouts/a2hs-hint');
111
+ consola.log(' 4. Send test push: pnpm pwa send \'<subscription>\'\n');
112
+ }
113
+
114
+ async function showInfo() {
115
+ consola.box('DjangoCFG PWA CLI');
116
+ consola.log('Commands:');
117
+ consola.log(' vapid Generate VAPID keys');
118
+ consola.log(' send <subscription> Send test push notification');
119
+ consola.log(' status Show PWA configuration status');
120
+ consola.log(' info Show this help\n');
121
+
122
+ consola.log('Examples:');
123
+ consola.log(' # Generate VAPID keys');
124
+ consola.log(' pnpm pwa vapid\n');
125
+
126
+ consola.log(' # Check status');
127
+ consola.log(' pnpm pwa status\n');
128
+
129
+ consola.log(' # Subscribe');
130
+ consola.log(' Visit: http://djangocfg.com/layouts/a2hs-hint\n');
131
+
132
+ consola.log(' # Send test push');
133
+ consola.log(' pnpm pwa send \'{"endpoint":"...","keys":{"p256dh":"...","auth":"..."}}\'\n');
134
+
135
+ consola.log('Documentation:');
136
+ consola.log(' https://djangocfg.com/docs/pwa');
137
+ }
138
+
139
+ async function main() {
140
+ switch (command) {
141
+ case 'vapid':
142
+ case 'v':
143
+ case 'keys':
144
+ await generateVapidKeys();
145
+ break;
146
+
147
+ case 'send':
148
+ case 's':
149
+ case 'push':
150
+ await sendTestPush();
151
+ break;
152
+
153
+ case 'status':
154
+ case 'st':
155
+ await showStatus();
156
+ break;
157
+
158
+ case 'info':
159
+ case 'i':
160
+ case 'help':
161
+ case 'h':
162
+ await showInfo();
163
+ break;
164
+
165
+ default:
166
+ await showInfo();
167
+ break;
168
+ }
169
+ }
170
+
171
+ main();
@@ -0,0 +1,9 @@
1
+ /**
2
+ * PWA Utilities
3
+ *
4
+ * Client-side utilities for Progressive Web Apps
5
+ */
6
+
7
+ export * from './notifications';
8
+ export * from './manifest';
9
+ export * from './plugin';
@@ -0,0 +1,355 @@
1
+ /**
2
+ * PWA Manifest Metadata Utilities
3
+ *
4
+ * Helper functions for creating Next.js metadata for PWA manifest
5
+ */
6
+
7
+ import type { Metadata, MetadataRoute, Viewport } from 'next';
8
+
9
+ export interface ManifestConfig {
10
+ name: string;
11
+ shortName?: string;
12
+ description?: string;
13
+ themeColor?: string;
14
+ backgroundColor?: string;
15
+ display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';
16
+ orientation?: 'portrait' | 'landscape' | 'any';
17
+ startUrl?: string;
18
+ scope?: string;
19
+ lang?: string;
20
+ dir?: 'ltr' | 'rtl' | 'auto';
21
+ icons?: {
22
+ src: string;
23
+ sizes: string;
24
+ type?: string;
25
+ purpose?: string;
26
+ }[];
27
+ }
28
+
29
+ /**
30
+ * Icon paths configuration
31
+ */
32
+ export interface IconPaths {
33
+ logo192?: string;
34
+ logo384?: string;
35
+ logo512?: string;
36
+ }
37
+
38
+ /**
39
+ * Protocol handler configuration
40
+ * Allows your PWA to register as a handler for custom protocols
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * {
45
+ * protocol: "web+music",
46
+ * url: "/play?track=%s"
47
+ * }
48
+ * ```
49
+ */
50
+ export interface ProtocolHandler {
51
+ /** Protocol scheme (e.g., "web+music", "mailto", "magnet") */
52
+ protocol: string;
53
+ /** URL template with %s placeholder for the protocol parameter */
54
+ url: string;
55
+ }
56
+
57
+ /**
58
+ * Create viewport configuration for Next.js app
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * export const viewport: Viewport = createViewport({
63
+ * themeColor: '#ffffff',
64
+ * });
65
+ * ```
66
+ */
67
+ export function createViewport(config: { themeColor?: string }): Viewport {
68
+ return {
69
+ width: 'device-width',
70
+ initialScale: 1,
71
+ maximumScale: 1,
72
+ themeColor: config.themeColor || '#000000',
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Create manifest metadata for Next.js app
78
+ *
79
+ * Note: themeColor and viewport should be exported separately using createViewport()
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * export const metadata: Metadata = {
84
+ * ...createManifestMetadata({
85
+ * name: 'My App',
86
+ * shortName: 'App',
87
+ * description: 'My awesome app',
88
+ * }),
89
+ * };
90
+ *
91
+ * export const viewport: Viewport = createViewport({
92
+ * themeColor: '#ffffff',
93
+ * });
94
+ * ```
95
+ */
96
+ export function createManifestMetadata(config: ManifestConfig): Metadata {
97
+ return {
98
+ manifest: '/manifest.json',
99
+ appleWebApp: {
100
+ capable: true,
101
+ statusBarStyle: 'default',
102
+ title: config.shortName || config.name,
103
+ },
104
+ applicationName: config.name,
105
+ formatDetection: {
106
+ telephone: false,
107
+ },
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Create Next.js manifest function
113
+ *
114
+ * Use this in your app/manifest.ts file
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * // app/manifest.ts
119
+ * import { createManifest } from '@djangocfg/nextjs/config';
120
+ * import { settings } from '@core/settings';
121
+ *
122
+ * export default createManifest({
123
+ * name: settings.app.name,
124
+ * description: settings.app.description,
125
+ * icons: {
126
+ * logo192: settings.app.icons.logo192,
127
+ * logo384: settings.app.icons.logo384,
128
+ * logo512: settings.app.icons.logo512,
129
+ * },
130
+ * });
131
+ * ```
132
+ */
133
+ export interface ScreenshotConfig {
134
+ src: string;
135
+ sizes: string;
136
+ type?: string;
137
+ form_factor?: 'narrow' | 'wide';
138
+ label?: string;
139
+ }
140
+
141
+ /**
142
+ * Smart screenshot configuration
143
+ * Automatically detects everything from path or uses defaults
144
+ */
145
+ export interface SmartScreenshotInput {
146
+ src: string;
147
+ /** Form factor (auto-detected from filename if contains 'desktop'/'mobile', or use default) */
148
+ form_factor?: 'narrow' | 'wide';
149
+ /** Optional label (auto-generated from form_factor) */
150
+ label?: string;
151
+ /** Optional width (defaults based on form_factor) */
152
+ width?: number;
153
+ /** Optional height (defaults based on form_factor) */
154
+ height?: number;
155
+ }
156
+
157
+ /**
158
+ * Create screenshot config with smart defaults
159
+ * Automatically detects type, sizes, form_factor from path or uses sensible defaults
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * // Minimal - everything auto-detected
164
+ * createScreenshot({ src: '/screenshots/desktop-view.png' })
165
+ * // → form_factor: 'wide', sizes: '1920x1080', type: 'image/png', label: 'Desktop screenshot'
166
+ *
167
+ * createScreenshot({ src: '/screenshots/mobile.png' })
168
+ * // → form_factor: 'narrow', sizes: '390x844', type: 'image/png', label: 'Mobile screenshot'
169
+ *
170
+ * // With custom dimensions
171
+ * createScreenshot({ src: '/screenshots/tablet.png', width: 1024, height: 768 })
172
+ * ```
173
+ */
174
+ export function createScreenshot(input: SmartScreenshotInput | string): ScreenshotConfig {
175
+ // Allow string shorthand
176
+ const config = typeof input === 'string' ? { src: input } : input;
177
+ let { src, width, height, label, form_factor } = config;
178
+
179
+ // Auto-detect image type from extension
180
+ const ext = src.split('.').pop()?.toLowerCase();
181
+ const typeMap: Record<string, string> = {
182
+ png: 'image/png',
183
+ jpg: 'image/jpeg',
184
+ jpeg: 'image/jpeg',
185
+ webp: 'image/webp',
186
+ svg: 'image/svg+xml',
187
+ };
188
+ const type = ext ? typeMap[ext] || 'image/png' : 'image/png';
189
+
190
+ // Try to parse dimensions from filename (e.g., "1920x1080.png" or "screenshot-390x844.png")
191
+ const filename = src.toLowerCase();
192
+ const dimensionMatch = filename.match(/(\d{3,4})x(\d{3,4})/);
193
+ if (dimensionMatch && !width && !height) {
194
+ width = parseInt(dimensionMatch[1], 10);
195
+ height = parseInt(dimensionMatch[2], 10);
196
+ }
197
+
198
+ // Auto-detect form_factor from filename if not provided
199
+ let autoFormFactor: 'narrow' | 'wide' = 'wide'; // Default to wide
200
+ if (filename.includes('mobile') || filename.includes('phone') || filename.includes('narrow')) {
201
+ autoFormFactor = 'narrow';
202
+ } else if (filename.includes('desktop') || filename.includes('laptop') || filename.includes('wide')) {
203
+ autoFormFactor = 'wide';
204
+ } else if (width && height) {
205
+ // Calculate from dimensions if provided or parsed
206
+ const aspectRatio = width / height;
207
+ autoFormFactor = aspectRatio > 1.2 ? 'wide' : 'narrow';
208
+ }
209
+
210
+ const finalFormFactor = form_factor || autoFormFactor;
211
+
212
+ // Default dimensions based on form_factor (only if not parsed from filename)
213
+ const defaultDimensions = finalFormFactor === 'wide'
214
+ ? { width: 1920, height: 1080 } // Desktop default
215
+ : { width: 390, height: 844 }; // Mobile default (iPhone 14)
216
+
217
+ const finalWidth = width || defaultDimensions.width;
218
+ const finalHeight = height || defaultDimensions.height;
219
+
220
+ // Auto-generate label
221
+ const autoLabel = finalFormFactor === 'wide'
222
+ ? 'Desktop screenshot'
223
+ : 'Mobile screenshot';
224
+
225
+ return {
226
+ src,
227
+ sizes: `${finalWidth}x${finalHeight}`,
228
+ type,
229
+ form_factor: finalFormFactor,
230
+ label: label || autoLabel,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Create multiple screenshots from array
236
+ * Supports string shorthand or full config objects
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * // Minimal - just paths
241
+ * createScreenshots([
242
+ * '/screenshots/desktop.png', // Auto: wide, 1920x1080
243
+ * '/screenshots/mobile.png', // Auto: narrow, 390x844
244
+ * ])
245
+ *
246
+ * // Mixed
247
+ * createScreenshots([
248
+ * '/screenshots/desktop.png',
249
+ * { src: '/screenshots/tablet.png', width: 1024, height: 768 },
250
+ * ])
251
+ * ```
252
+ */
253
+ export function createScreenshots(inputs: Array<SmartScreenshotInput | string>): ScreenshotConfig[] {
254
+ return inputs.map(createScreenshot);
255
+ }
256
+
257
+ export function createManifest(config: {
258
+ name: string;
259
+ shortName?: string;
260
+ description?: string;
261
+ themeColor?: string;
262
+ backgroundColor?: string;
263
+ display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';
264
+ orientation?: 'portrait' | 'landscape' | 'any';
265
+ id?: string;
266
+ startUrl?: string;
267
+ scope?: string;
268
+ lang?: string;
269
+ dir?: 'ltr' | 'rtl' | 'auto';
270
+ icons?: IconPaths | ManifestConfig['icons'];
271
+ screenshots?: ScreenshotConfig[];
272
+ protocol_handlers?: ProtocolHandler[];
273
+ }): () => MetadataRoute.Manifest {
274
+ return () => {
275
+ // Convert IconPaths to manifest icons format
276
+ let manifestIcons: MetadataRoute.Manifest['icons'];
277
+
278
+ if (Array.isArray(config.icons)) {
279
+ // Already in manifest format
280
+ manifestIcons = config.icons as MetadataRoute.Manifest['icons'];
281
+ } else if (config.icons) {
282
+ // Convert IconPaths to manifest icons
283
+ const { logo192, logo384, logo512 } = config.icons as IconPaths;
284
+ manifestIcons = [
285
+ ...(logo192
286
+ ? [
287
+ {
288
+ src: logo192,
289
+ sizes: '192x192',
290
+ type: 'image/png',
291
+ purpose: 'maskable' as const,
292
+ },
293
+ ]
294
+ : []),
295
+ ...(logo384
296
+ ? [
297
+ {
298
+ src: logo384,
299
+ sizes: '384x384',
300
+ type: 'image/png',
301
+ },
302
+ ]
303
+ : []),
304
+ ...(logo512
305
+ ? [
306
+ {
307
+ src: logo512,
308
+ sizes: '512x512',
309
+ type: 'image/png',
310
+ },
311
+ ]
312
+ : []),
313
+ ];
314
+ }
315
+
316
+ const manifest: MetadataRoute.Manifest = {
317
+ name: config.name,
318
+ short_name: config.shortName || config.name,
319
+ description: config.description || config.name,
320
+ id: config.id || config.startUrl || '/',
321
+ start_url: config.startUrl || '/',
322
+ scope: config.scope || '/',
323
+ display: config.display || 'standalone',
324
+ orientation: config.orientation || 'portrait',
325
+ background_color: config.backgroundColor || '#000000',
326
+ theme_color: config.themeColor || '#ffffff',
327
+ lang: config.lang || 'en',
328
+ dir: config.dir || 'ltr',
329
+ icons: manifestIcons,
330
+ // Removed forced gcm_sender_id to avoid potential conflicts with VAPID
331
+ // gcm_sender_id: '103953800507',
332
+ };
333
+
334
+ // Add screenshots if provided (for Richer PWA Install UI)
335
+ if (config.screenshots && config.screenshots.length > 0) {
336
+ (manifest as any).screenshots = config.screenshots;
337
+ }
338
+
339
+ // Add protocol handlers if provided
340
+ if (config.protocol_handlers && config.protocol_handlers.length > 0) {
341
+ (manifest as any).protocol_handlers = config.protocol_handlers;
342
+ }
343
+
344
+ return manifest;
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Generate manifest.json content (legacy)
350
+ *
351
+ * @deprecated Use createManifest() instead
352
+ */
353
+ export function generateManifest(config: ManifestConfig): Record<string, any> {
354
+ return createManifest(config)();
355
+ }