@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.
- package/README.md +146 -22
- package/dist/config/index.d.mts +7 -409
- package/dist/config/index.mjs +79 -394
- package/dist/config/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +79 -394
- package/dist/index.mjs.map +1 -1
- package/dist/plugin-DuRJ_Jq6.d.mts +100 -0
- package/dist/pwa/cli.d.mts +1 -0
- package/dist/pwa/cli.mjs +140 -0
- package/dist/pwa/cli.mjs.map +1 -0
- package/dist/pwa/index.d.mts +274 -0
- package/dist/pwa/index.mjs +327 -0
- package/dist/pwa/index.mjs.map +1 -0
- package/dist/pwa/server/index.d.mts +86 -0
- package/dist/pwa/server/index.mjs +175 -0
- package/dist/pwa/server/index.mjs.map +1 -0
- package/dist/pwa/server/routes.d.mts +2 -0
- package/dist/pwa/server/routes.mjs +149 -0
- package/dist/pwa/server/routes.mjs.map +1 -0
- package/dist/pwa/worker/index.d.mts +56 -0
- package/dist/pwa/worker/index.mjs +97 -0
- package/dist/pwa/worker/index.mjs.map +1 -0
- package/dist/routes-DXA29sS_.d.mts +68 -0
- package/package.json +38 -9
- package/src/config/createNextConfig.ts +9 -13
- package/src/config/index.ts +2 -19
- package/src/config/plugins/devStartup.ts +35 -36
- package/src/config/plugins/index.ts +1 -1
- package/src/config/utils/index.ts +0 -1
- package/src/index.ts +4 -0
- package/src/pwa/cli.ts +171 -0
- package/src/pwa/index.ts +9 -0
- package/src/pwa/manifest.ts +355 -0
- package/src/pwa/notifications.ts +192 -0
- package/src/pwa/plugin.ts +194 -0
- package/src/pwa/server/index.ts +23 -0
- package/src/pwa/server/push.ts +166 -0
- package/src/pwa/server/routes.ts +137 -0
- package/src/pwa/worker/index.ts +174 -0
- package/src/pwa/worker/package.json +3 -0
- package/bin/dev-with-browser.js +0 -114
- package/src/config/plugins/pwa.ts +0 -616
- 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
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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,
|
|
7
|
+
export { withPWA, defaultRuntimeCaching, type PWAPluginOptions } from '../../pwa/plugin';
|
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();
|
package/src/pwa/index.ts
ADDED
|
@@ -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
|
+
}
|