@djangocfg/nextjs 2.1.45 → 2.1.47

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.
@@ -232,6 +232,9 @@ function withPWA(nextConfig, options = {}) {
232
232
  ...options
233
233
  };
234
234
  try {
235
+ if (!process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING) {
236
+ process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING = "1";
237
+ }
235
238
  const withSerwistInit = __require("@serwist/next").default;
236
239
  const withSerwist = withSerwistInit({
237
240
  swSrc: defaultOptions.swSrc,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pwa/notifications.ts","../../src/pwa/manifest.ts","../../src/pwa/plugin.ts"],"sourcesContent":["/**\n * PWA Push Notifications Client Utilities\n *\n * Functions for requesting permission, subscribing to push notifications,\n * and sending test notifications\n */\n\n/**\n * Check if push notifications are supported\n */\nexport function isPushNotificationSupported(): boolean {\n return (\n 'serviceWorker' in navigator &&\n 'PushManager' in window &&\n 'Notification' in window\n );\n}\n\n/**\n * Get current notification permission status\n */\nexport function getNotificationPermission(): NotificationPermission {\n if (!('Notification' in window)) {\n return 'denied';\n }\n return Notification.permission;\n}\n\n/**\n * Request notification permission from user\n */\nexport async function requestNotificationPermission(): Promise<NotificationPermission> {\n if (!('Notification' in window)) {\n throw new Error('Notifications are not supported');\n }\n\n const permission = await Notification.requestPermission();\n return permission;\n}\n\n/**\n * Convert base64 VAPID key to Uint8Array\n */\nfunction urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');\n\n const rawData = window.atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray;\n}\n\nexport interface PushSubscriptionOptions {\n /**\n * VAPID public key (base64 encoded)\n * Generate with: npx web-push generate-vapid-keys\n */\n vapidPublicKey: string;\n\n /**\n * User visible only (required for Chrome)\n * @default true\n */\n userVisibleOnly?: boolean;\n}\n\n/**\n * Subscribe to push notifications\n *\n * @example\n * ```ts\n * const subscription = await subscribeToPushNotifications({\n * vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,\n * });\n *\n * // Send subscription to your backend\n * await fetch('/api/push/subscribe', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify(subscription),\n * });\n * ```\n */\nexport async function subscribeToPushNotifications(\n options: PushSubscriptionOptions\n): Promise<PushSubscription> {\n if (!isPushNotificationSupported()) {\n throw new Error('Push notifications are not supported');\n }\n\n // Request permission first\n const permission = await requestNotificationPermission();\n if (permission !== 'granted') {\n throw new Error(`Notification permission ${permission}`);\n }\n\n // Get service worker registration\n const registration = await navigator.serviceWorker.ready;\n\n // Check if already subscribed\n let subscription = await registration.pushManager.getSubscription();\n\n if (subscription) {\n return subscription;\n }\n\n // Subscribe to push notifications\n const convertedVapidKey = urlBase64ToUint8Array(options.vapidPublicKey);\n\n subscription = await registration.pushManager.subscribe({\n userVisibleOnly: options.userVisibleOnly ?? true,\n applicationServerKey: convertedVapidKey as BufferSource,\n });\n\n return subscription;\n}\n\n/**\n * Unsubscribe from push notifications\n */\nexport async function unsubscribeFromPushNotifications(): Promise<boolean> {\n if (!isPushNotificationSupported()) {\n return false;\n }\n\n const registration = await navigator.serviceWorker.ready;\n const subscription = await registration.pushManager.getSubscription();\n\n if (subscription) {\n return await subscription.unsubscribe();\n }\n\n return false;\n}\n\n/**\n * Get current push subscription\n */\nexport async function getPushSubscription(): Promise<PushSubscription | null> {\n if (!isPushNotificationSupported()) {\n return null;\n }\n\n const registration = await navigator.serviceWorker.ready;\n return await registration.pushManager.getSubscription();\n}\n\n/**\n * Show a local notification (doesn't require push)\n *\n * @example\n * ```ts\n * await showLocalNotification({\n * title: 'Hello!',\n * body: 'This is a test notification',\n * icon: '/icon.png',\n * data: { url: '/some-page' },\n * });\n * ```\n */\nexport async function showLocalNotification(options: {\n title: string;\n body?: string;\n icon?: string;\n badge?: string;\n tag?: string;\n data?: any;\n requireInteraction?: boolean;\n}): Promise<void> {\n if (!('Notification' in window)) {\n throw new Error('Notifications are not supported');\n }\n\n const permission = await requestNotificationPermission();\n if (permission !== 'granted') {\n throw new Error(`Notification permission ${permission}`);\n }\n\n const registration = await navigator.serviceWorker.ready;\n await registration.showNotification(options.title, {\n body: options.body,\n icon: options.icon,\n badge: options.badge,\n tag: options.tag,\n data: options.data,\n requireInteraction: options.requireInteraction,\n });\n}\n","/**\n * PWA Manifest Metadata Utilities\n *\n * Helper functions for creating Next.js metadata for PWA manifest\n */\n\nimport type { Metadata, MetadataRoute, Viewport } from 'next';\n\nexport interface ManifestConfig {\n name: string;\n shortName?: string;\n description?: string;\n themeColor?: string;\n backgroundColor?: string;\n display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';\n orientation?: 'portrait' | 'landscape' | 'any';\n startUrl?: string;\n scope?: string;\n lang?: string;\n dir?: 'ltr' | 'rtl' | 'auto';\n icons?: {\n src: string;\n sizes: string;\n type?: string;\n purpose?: string;\n }[];\n}\n\n/**\n * Icon paths configuration\n */\nexport interface IconPaths {\n logo192?: string;\n logo384?: string;\n logo512?: string;\n}\n\n/**\n * Protocol handler configuration\n * Allows your PWA to register as a handler for custom protocols\n *\n * @example\n * ```typescript\n * {\n * protocol: \"web+music\",\n * url: \"/play?track=%s\"\n * }\n * ```\n */\nexport interface ProtocolHandler {\n /** Protocol scheme (e.g., \"web+music\", \"mailto\", \"magnet\") */\n protocol: string;\n /** URL template with %s placeholder for the protocol parameter */\n url: string;\n}\n\n/**\n * Create viewport configuration for Next.js app\n *\n * @example\n * ```typescript\n * export const viewport: Viewport = createViewport({\n * themeColor: '#ffffff',\n * });\n * ```\n */\nexport function createViewport(config: { themeColor?: string }): Viewport {\n return {\n width: 'device-width',\n initialScale: 1,\n maximumScale: 1,\n themeColor: config.themeColor || '#000000',\n };\n}\n\n/**\n * Create manifest metadata for Next.js app\n *\n * Note: themeColor and viewport should be exported separately using createViewport()\n *\n * @example\n * ```typescript\n * export const metadata: Metadata = {\n * ...createManifestMetadata({\n * name: 'My App',\n * shortName: 'App',\n * description: 'My awesome app',\n * }),\n * };\n *\n * export const viewport: Viewport = createViewport({\n * themeColor: '#ffffff',\n * });\n * ```\n */\nexport function createManifestMetadata(config: ManifestConfig): Metadata {\n return {\n manifest: '/manifest.json',\n appleWebApp: {\n capable: true,\n statusBarStyle: 'default',\n title: config.shortName || config.name,\n },\n applicationName: config.name,\n formatDetection: {\n telephone: false,\n },\n };\n}\n\n/**\n * Create Next.js manifest function\n *\n * Use this in your app/manifest.ts file\n *\n * @example\n * ```typescript\n * // app/manifest.ts\n * import { createManifest } from '@djangocfg/nextjs/config';\n * import { settings } from '@core/settings';\n *\n * export default createManifest({\n * name: settings.app.name,\n * description: settings.app.description,\n * icons: {\n * logo192: settings.app.icons.logo192,\n * logo384: settings.app.icons.logo384,\n * logo512: settings.app.icons.logo512,\n * },\n * });\n * ```\n */\nexport interface ScreenshotConfig {\n src: string;\n sizes: string;\n type?: string;\n form_factor?: 'narrow' | 'wide';\n label?: string;\n}\n\n/**\n * Smart screenshot configuration\n * Automatically detects everything from path or uses defaults\n */\nexport interface SmartScreenshotInput {\n src: string;\n /** Form factor (auto-detected from filename if contains 'desktop'/'mobile', or use default) */\n form_factor?: 'narrow' | 'wide';\n /** Optional label (auto-generated from form_factor) */\n label?: string;\n /** Optional width (defaults based on form_factor) */\n width?: number;\n /** Optional height (defaults based on form_factor) */\n height?: number;\n}\n\n/**\n * Create screenshot config with smart defaults\n * Automatically detects type, sizes, form_factor from path or uses sensible defaults\n *\n * @example\n * ```typescript\n * // Minimal - everything auto-detected\n * createScreenshot({ src: '/screenshots/desktop-view.png' })\n * // → form_factor: 'wide', sizes: '1920x1080', type: 'image/png', label: 'Desktop screenshot'\n *\n * createScreenshot({ src: '/screenshots/mobile.png' })\n * // → form_factor: 'narrow', sizes: '390x844', type: 'image/png', label: 'Mobile screenshot'\n *\n * // With custom dimensions\n * createScreenshot({ src: '/screenshots/tablet.png', width: 1024, height: 768 })\n * ```\n */\nexport function createScreenshot(input: SmartScreenshotInput | string): ScreenshotConfig {\n // Allow string shorthand\n const config = typeof input === 'string' ? { src: input } : input;\n let { src, width, height, label, form_factor } = config;\n\n // Auto-detect image type from extension\n const ext = src.split('.').pop()?.toLowerCase();\n const typeMap: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n };\n const type = ext ? typeMap[ext] || 'image/png' : 'image/png';\n\n // Try to parse dimensions from filename (e.g., \"1920x1080.png\" or \"screenshot-390x844.png\")\n const filename = src.toLowerCase();\n const dimensionMatch = filename.match(/(\\d{3,4})x(\\d{3,4})/);\n if (dimensionMatch && !width && !height) {\n width = parseInt(dimensionMatch[1], 10);\n height = parseInt(dimensionMatch[2], 10);\n }\n\n // Auto-detect form_factor from filename if not provided\n let autoFormFactor: 'narrow' | 'wide' = 'wide'; // Default to wide\n if (filename.includes('mobile') || filename.includes('phone') || filename.includes('narrow')) {\n autoFormFactor = 'narrow';\n } else if (filename.includes('desktop') || filename.includes('laptop') || filename.includes('wide')) {\n autoFormFactor = 'wide';\n } else if (width && height) {\n // Calculate from dimensions if provided or parsed\n const aspectRatio = width / height;\n autoFormFactor = aspectRatio > 1.2 ? 'wide' : 'narrow';\n }\n\n const finalFormFactor = form_factor || autoFormFactor;\n\n // Default dimensions based on form_factor (only if not parsed from filename)\n const defaultDimensions = finalFormFactor === 'wide'\n ? { width: 1920, height: 1080 } // Desktop default\n : { width: 390, height: 844 }; // Mobile default (iPhone 14)\n\n const finalWidth = width || defaultDimensions.width;\n const finalHeight = height || defaultDimensions.height;\n\n // Auto-generate label\n const autoLabel = finalFormFactor === 'wide'\n ? 'Desktop screenshot'\n : 'Mobile screenshot';\n\n return {\n src,\n sizes: `${finalWidth}x${finalHeight}`,\n type,\n form_factor: finalFormFactor,\n label: label || autoLabel,\n };\n}\n\n/**\n * Create multiple screenshots from array\n * Supports string shorthand or full config objects\n *\n * @example\n * ```typescript\n * // Minimal - just paths\n * createScreenshots([\n * '/screenshots/desktop.png', // Auto: wide, 1920x1080\n * '/screenshots/mobile.png', // Auto: narrow, 390x844\n * ])\n *\n * // Mixed\n * createScreenshots([\n * '/screenshots/desktop.png',\n * { src: '/screenshots/tablet.png', width: 1024, height: 768 },\n * ])\n * ```\n */\nexport function createScreenshots(inputs: Array<SmartScreenshotInput | string>): ScreenshotConfig[] {\n return inputs.map(createScreenshot);\n}\n\nexport function createManifest(config: {\n name: string;\n shortName?: string;\n description?: string;\n themeColor?: string;\n backgroundColor?: string;\n display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';\n orientation?: 'portrait' | 'landscape' | 'any';\n id?: string;\n startUrl?: string;\n scope?: string;\n lang?: string;\n dir?: 'ltr' | 'rtl' | 'auto';\n icons?: IconPaths | ManifestConfig['icons'];\n screenshots?: ScreenshotConfig[];\n protocol_handlers?: ProtocolHandler[];\n}): () => MetadataRoute.Manifest {\n return () => {\n // Convert IconPaths to manifest icons format\n let manifestIcons: MetadataRoute.Manifest['icons'];\n\n if (Array.isArray(config.icons)) {\n // Already in manifest format\n manifestIcons = config.icons as MetadataRoute.Manifest['icons'];\n } else if (config.icons) {\n // Convert IconPaths to manifest icons\n const { logo192, logo384, logo512 } = config.icons as IconPaths;\n manifestIcons = [\n ...(logo192\n ? [\n {\n src: logo192,\n sizes: '192x192',\n type: 'image/png',\n purpose: 'maskable' as const,\n },\n ]\n : []),\n ...(logo384\n ? [\n {\n src: logo384,\n sizes: '384x384',\n type: 'image/png',\n },\n ]\n : []),\n ...(logo512\n ? [\n {\n src: logo512,\n sizes: '512x512',\n type: 'image/png',\n },\n ]\n : []),\n ];\n }\n\n const manifest: MetadataRoute.Manifest = {\n name: config.name,\n short_name: config.shortName || config.name,\n description: config.description || config.name,\n id: config.id || config.startUrl || '/',\n start_url: config.startUrl || '/',\n scope: config.scope || '/',\n display: config.display || 'standalone',\n orientation: config.orientation || 'portrait',\n background_color: config.backgroundColor || '#000000',\n theme_color: config.themeColor || '#ffffff',\n lang: config.lang || 'en',\n dir: config.dir || 'ltr',\n icons: manifestIcons,\n // Removed forced gcm_sender_id to avoid potential conflicts with VAPID\n // gcm_sender_id: '103953800507',\n };\n\n // Add screenshots if provided (for Richer PWA Install UI)\n if (config.screenshots && config.screenshots.length > 0) {\n (manifest as any).screenshots = config.screenshots;\n }\n\n // Add protocol handlers if provided\n if (config.protocol_handlers && config.protocol_handlers.length > 0) {\n (manifest as any).protocol_handlers = config.protocol_handlers;\n }\n\n return manifest;\n };\n}\n\n/**\n * Generate manifest.json content (legacy)\n *\n * @deprecated Use createManifest() instead\n */\nexport function generateManifest(config: ManifestConfig): Record<string, any> {\n return createManifest(config)();\n}\n","/**\n * PWA (Progressive Web App) Plugin\n *\n * Configures Serwist for service worker and offline support\n * Modern PWA solution for Next.js 15+ with App Router\n *\n * @see https://serwist.pages.dev/\n */\n\nimport type { NextConfig } from 'next';\nimport { consola } from 'consola';\n\nexport interface PWAPluginOptions {\n /**\n * Destination directory for service worker files\n * @default 'public'\n * @deprecated Use swDest instead\n */\n dest?: string;\n\n /**\n * Path to service worker source file (relative to project root)\n * @default 'app/sw.ts'\n */\n swSrc?: string;\n\n /**\n * Destination for compiled service worker\n * @default 'public/sw.js'\n */\n swDest?: string;\n\n /**\n * Disable PWA completely\n * @default false in production, true in development\n * @example disable: process.env.NODE_ENV === 'development'\n */\n disable?: boolean;\n\n /**\n * Cache on navigation - cache pages when navigating\n * @default true\n */\n cacheOnNavigation?: boolean;\n\n /**\n * Reload app when device goes back online\n * @default true\n */\n reloadOnOnline?: boolean;\n\n /**\n * Additional Serwist options\n * @see https://serwist.pages.dev/docs/next/configuring\n */\n serwistOptions?: Record<string, any>;\n}\n\n/**\n * Add PWA configuration to Next.js config using Serwist\n *\n * @example Basic usage\n * ```ts\n * import { createBaseNextConfig, withPWA } from '@djangocfg/nextjs/config';\n *\n * const nextConfig = createBaseNextConfig({...});\n *\n * export default withPWA(nextConfig, {\n * swSrc: 'app/sw.ts',\n * disable: process.env.NODE_ENV === 'development',\n * });\n * ```\n *\n * @example Integrated with createBaseNextConfig\n * ```ts\n * import { createBaseNextConfig } from '@djangocfg/nextjs/config';\n *\n * const config = createBaseNextConfig({\n * pwa: {\n * swSrc: 'app/sw.ts',\n * disable: false,\n * },\n * });\n *\n * export default config;\n * ```\n */\nexport function withPWA(\n nextConfig: NextConfig,\n options: PWAPluginOptions = {}\n): NextConfig {\n const isDev = process.env.NODE_ENV === 'development';\n const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';\n\n // Determine if PWA should be disabled:\n // - Explicitly disabled via options\n // - In development mode (default)\n // - Static build (output: 'export' doesn't support service workers)\n const shouldDisable = options.disable !== undefined\n ? options.disable\n : (isDev || isStaticBuild);\n\n const defaultOptions: PWAPluginOptions = {\n swSrc: 'app/sw.ts',\n swDest: 'public/sw.js',\n disable: shouldDisable,\n cacheOnNavigation: true,\n reloadOnOnline: true,\n ...options,\n };\n\n try {\n const withSerwistInit = require('@serwist/next').default;\n\n const withSerwist = withSerwistInit({\n swSrc: defaultOptions.swSrc,\n swDest: defaultOptions.swDest,\n disable: defaultOptions.disable,\n cacheOnNavigation: defaultOptions.cacheOnNavigation,\n reloadOnOnline: defaultOptions.reloadOnOnline,\n ...defaultOptions.serwistOptions,\n });\n\n return withSerwist(nextConfig);\n } catch (error) {\n consola.error('Failed to configure Serwist:', error);\n return nextConfig;\n }\n}\n\n/**\n * Get service worker template content\n *\n * Returns ready-to-use service worker code for app/sw.ts\n *\n * @example\n * ```ts\n * import { getServiceWorkerTemplate } from '@djangocfg/nextjs/config';\n *\n * // Copy this to your app/sw.ts file\n * console.log(getServiceWorkerTemplate());\n * ```\n */\nexport function getServiceWorkerTemplate(): string {\n return `/**\n * Service Worker (Serwist)\n *\n * Modern PWA service worker using Serwist\n */\n\nimport { defaultCache } from '@serwist/next/worker';\nimport { Serwist } from 'serwist';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ndeclare const self: any;\n\nconst serwist = new Serwist({\n // Precache entries injected by Serwist build plugin\n precacheEntries: self.__SW_MANIFEST,\n\n // Skip waiting - activate new SW immediately\n skipWaiting: true,\n\n // Take control of all clients immediately\n clientsClaim: true,\n\n // Enable navigation preload for faster loads\n navigationPreload: true,\n\n // Use default Next.js runtime caching strategies\n runtimeCaching: defaultCache,\n\n // Fallback pages for offline\n fallbacks: {\n entries: [\n {\n url: '/_offline',\n matcher({ request }) {\n return request.destination === 'document';\n },\n },\n ],\n },\n});\n\nserwist.addEventListeners();\n`;\n}\n\n// Backward compatibility exports (deprecated)\nexport const defaultRuntimeCaching = [];\nexport function createApiCacheRule() {\n consola.warn('createApiCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');\n return {};\n}\nexport function createStaticAssetRule() {\n consola.warn('createStaticAssetRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');\n return {};\n}\nexport function createCdnCacheRule() {\n consola.warn('createCdnCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');\n return {};\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/pwa/notifications.ts","../../src/pwa/manifest.ts","../../src/pwa/plugin.ts"],"sourcesContent":["/**\n * PWA Push Notifications Client Utilities\n *\n * Functions for requesting permission, subscribing to push notifications,\n * and sending test notifications\n */\n\n/**\n * Check if push notifications are supported\n */\nexport function isPushNotificationSupported(): boolean {\n return (\n 'serviceWorker' in navigator &&\n 'PushManager' in window &&\n 'Notification' in window\n );\n}\n\n/**\n * Get current notification permission status\n */\nexport function getNotificationPermission(): NotificationPermission {\n if (!('Notification' in window)) {\n return 'denied';\n }\n return Notification.permission;\n}\n\n/**\n * Request notification permission from user\n */\nexport async function requestNotificationPermission(): Promise<NotificationPermission> {\n if (!('Notification' in window)) {\n throw new Error('Notifications are not supported');\n }\n\n const permission = await Notification.requestPermission();\n return permission;\n}\n\n/**\n * Convert base64 VAPID key to Uint8Array\n */\nfunction urlBase64ToUint8Array(base64String: string): Uint8Array {\n const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');\n\n const rawData = window.atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray;\n}\n\nexport interface PushSubscriptionOptions {\n /**\n * VAPID public key (base64 encoded)\n * Generate with: npx web-push generate-vapid-keys\n */\n vapidPublicKey: string;\n\n /**\n * User visible only (required for Chrome)\n * @default true\n */\n userVisibleOnly?: boolean;\n}\n\n/**\n * Subscribe to push notifications\n *\n * @example\n * ```ts\n * const subscription = await subscribeToPushNotifications({\n * vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,\n * });\n *\n * // Send subscription to your backend\n * await fetch('/api/push/subscribe', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify(subscription),\n * });\n * ```\n */\nexport async function subscribeToPushNotifications(\n options: PushSubscriptionOptions\n): Promise<PushSubscription> {\n if (!isPushNotificationSupported()) {\n throw new Error('Push notifications are not supported');\n }\n\n // Request permission first\n const permission = await requestNotificationPermission();\n if (permission !== 'granted') {\n throw new Error(`Notification permission ${permission}`);\n }\n\n // Get service worker registration\n const registration = await navigator.serviceWorker.ready;\n\n // Check if already subscribed\n let subscription = await registration.pushManager.getSubscription();\n\n if (subscription) {\n return subscription;\n }\n\n // Subscribe to push notifications\n const convertedVapidKey = urlBase64ToUint8Array(options.vapidPublicKey);\n\n subscription = await registration.pushManager.subscribe({\n userVisibleOnly: options.userVisibleOnly ?? true,\n applicationServerKey: convertedVapidKey as BufferSource,\n });\n\n return subscription;\n}\n\n/**\n * Unsubscribe from push notifications\n */\nexport async function unsubscribeFromPushNotifications(): Promise<boolean> {\n if (!isPushNotificationSupported()) {\n return false;\n }\n\n const registration = await navigator.serviceWorker.ready;\n const subscription = await registration.pushManager.getSubscription();\n\n if (subscription) {\n return await subscription.unsubscribe();\n }\n\n return false;\n}\n\n/**\n * Get current push subscription\n */\nexport async function getPushSubscription(): Promise<PushSubscription | null> {\n if (!isPushNotificationSupported()) {\n return null;\n }\n\n const registration = await navigator.serviceWorker.ready;\n return await registration.pushManager.getSubscription();\n}\n\n/**\n * Show a local notification (doesn't require push)\n *\n * @example\n * ```ts\n * await showLocalNotification({\n * title: 'Hello!',\n * body: 'This is a test notification',\n * icon: '/icon.png',\n * data: { url: '/some-page' },\n * });\n * ```\n */\nexport async function showLocalNotification(options: {\n title: string;\n body?: string;\n icon?: string;\n badge?: string;\n tag?: string;\n data?: any;\n requireInteraction?: boolean;\n}): Promise<void> {\n if (!('Notification' in window)) {\n throw new Error('Notifications are not supported');\n }\n\n const permission = await requestNotificationPermission();\n if (permission !== 'granted') {\n throw new Error(`Notification permission ${permission}`);\n }\n\n const registration = await navigator.serviceWorker.ready;\n await registration.showNotification(options.title, {\n body: options.body,\n icon: options.icon,\n badge: options.badge,\n tag: options.tag,\n data: options.data,\n requireInteraction: options.requireInteraction,\n });\n}\n","/**\n * PWA Manifest Metadata Utilities\n *\n * Helper functions for creating Next.js metadata for PWA manifest\n */\n\nimport type { Metadata, MetadataRoute, Viewport } from 'next';\n\nexport interface ManifestConfig {\n name: string;\n shortName?: string;\n description?: string;\n themeColor?: string;\n backgroundColor?: string;\n display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';\n orientation?: 'portrait' | 'landscape' | 'any';\n startUrl?: string;\n scope?: string;\n lang?: string;\n dir?: 'ltr' | 'rtl' | 'auto';\n icons?: {\n src: string;\n sizes: string;\n type?: string;\n purpose?: string;\n }[];\n}\n\n/**\n * Icon paths configuration\n */\nexport interface IconPaths {\n logo192?: string;\n logo384?: string;\n logo512?: string;\n}\n\n/**\n * Protocol handler configuration\n * Allows your PWA to register as a handler for custom protocols\n *\n * @example\n * ```typescript\n * {\n * protocol: \"web+music\",\n * url: \"/play?track=%s\"\n * }\n * ```\n */\nexport interface ProtocolHandler {\n /** Protocol scheme (e.g., \"web+music\", \"mailto\", \"magnet\") */\n protocol: string;\n /** URL template with %s placeholder for the protocol parameter */\n url: string;\n}\n\n/**\n * Create viewport configuration for Next.js app\n *\n * @example\n * ```typescript\n * export const viewport: Viewport = createViewport({\n * themeColor: '#ffffff',\n * });\n * ```\n */\nexport function createViewport(config: { themeColor?: string }): Viewport {\n return {\n width: 'device-width',\n initialScale: 1,\n maximumScale: 1,\n themeColor: config.themeColor || '#000000',\n };\n}\n\n/**\n * Create manifest metadata for Next.js app\n *\n * Note: themeColor and viewport should be exported separately using createViewport()\n *\n * @example\n * ```typescript\n * export const metadata: Metadata = {\n * ...createManifestMetadata({\n * name: 'My App',\n * shortName: 'App',\n * description: 'My awesome app',\n * }),\n * };\n *\n * export const viewport: Viewport = createViewport({\n * themeColor: '#ffffff',\n * });\n * ```\n */\nexport function createManifestMetadata(config: ManifestConfig): Metadata {\n return {\n manifest: '/manifest.json',\n appleWebApp: {\n capable: true,\n statusBarStyle: 'default',\n title: config.shortName || config.name,\n },\n applicationName: config.name,\n formatDetection: {\n telephone: false,\n },\n };\n}\n\n/**\n * Create Next.js manifest function\n *\n * Use this in your app/manifest.ts file\n *\n * @example\n * ```typescript\n * // app/manifest.ts\n * import { createManifest } from '@djangocfg/nextjs/config';\n * import { settings } from '@core/settings';\n *\n * export default createManifest({\n * name: settings.app.name,\n * description: settings.app.description,\n * icons: {\n * logo192: settings.app.icons.logo192,\n * logo384: settings.app.icons.logo384,\n * logo512: settings.app.icons.logo512,\n * },\n * });\n * ```\n */\nexport interface ScreenshotConfig {\n src: string;\n sizes: string;\n type?: string;\n form_factor?: 'narrow' | 'wide';\n label?: string;\n}\n\n/**\n * Smart screenshot configuration\n * Automatically detects everything from path or uses defaults\n */\nexport interface SmartScreenshotInput {\n src: string;\n /** Form factor (auto-detected from filename if contains 'desktop'/'mobile', or use default) */\n form_factor?: 'narrow' | 'wide';\n /** Optional label (auto-generated from form_factor) */\n label?: string;\n /** Optional width (defaults based on form_factor) */\n width?: number;\n /** Optional height (defaults based on form_factor) */\n height?: number;\n}\n\n/**\n * Create screenshot config with smart defaults\n * Automatically detects type, sizes, form_factor from path or uses sensible defaults\n *\n * @example\n * ```typescript\n * // Minimal - everything auto-detected\n * createScreenshot({ src: '/screenshots/desktop-view.png' })\n * // → form_factor: 'wide', sizes: '1920x1080', type: 'image/png', label: 'Desktop screenshot'\n *\n * createScreenshot({ src: '/screenshots/mobile.png' })\n * // → form_factor: 'narrow', sizes: '390x844', type: 'image/png', label: 'Mobile screenshot'\n *\n * // With custom dimensions\n * createScreenshot({ src: '/screenshots/tablet.png', width: 1024, height: 768 })\n * ```\n */\nexport function createScreenshot(input: SmartScreenshotInput | string): ScreenshotConfig {\n // Allow string shorthand\n const config = typeof input === 'string' ? { src: input } : input;\n let { src, width, height, label, form_factor } = config;\n\n // Auto-detect image type from extension\n const ext = src.split('.').pop()?.toLowerCase();\n const typeMap: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n };\n const type = ext ? typeMap[ext] || 'image/png' : 'image/png';\n\n // Try to parse dimensions from filename (e.g., \"1920x1080.png\" or \"screenshot-390x844.png\")\n const filename = src.toLowerCase();\n const dimensionMatch = filename.match(/(\\d{3,4})x(\\d{3,4})/);\n if (dimensionMatch && !width && !height) {\n width = parseInt(dimensionMatch[1], 10);\n height = parseInt(dimensionMatch[2], 10);\n }\n\n // Auto-detect form_factor from filename if not provided\n let autoFormFactor: 'narrow' | 'wide' = 'wide'; // Default to wide\n if (filename.includes('mobile') || filename.includes('phone') || filename.includes('narrow')) {\n autoFormFactor = 'narrow';\n } else if (filename.includes('desktop') || filename.includes('laptop') || filename.includes('wide')) {\n autoFormFactor = 'wide';\n } else if (width && height) {\n // Calculate from dimensions if provided or parsed\n const aspectRatio = width / height;\n autoFormFactor = aspectRatio > 1.2 ? 'wide' : 'narrow';\n }\n\n const finalFormFactor = form_factor || autoFormFactor;\n\n // Default dimensions based on form_factor (only if not parsed from filename)\n const defaultDimensions = finalFormFactor === 'wide'\n ? { width: 1920, height: 1080 } // Desktop default\n : { width: 390, height: 844 }; // Mobile default (iPhone 14)\n\n const finalWidth = width || defaultDimensions.width;\n const finalHeight = height || defaultDimensions.height;\n\n // Auto-generate label\n const autoLabel = finalFormFactor === 'wide'\n ? 'Desktop screenshot'\n : 'Mobile screenshot';\n\n return {\n src,\n sizes: `${finalWidth}x${finalHeight}`,\n type,\n form_factor: finalFormFactor,\n label: label || autoLabel,\n };\n}\n\n/**\n * Create multiple screenshots from array\n * Supports string shorthand or full config objects\n *\n * @example\n * ```typescript\n * // Minimal - just paths\n * createScreenshots([\n * '/screenshots/desktop.png', // Auto: wide, 1920x1080\n * '/screenshots/mobile.png', // Auto: narrow, 390x844\n * ])\n *\n * // Mixed\n * createScreenshots([\n * '/screenshots/desktop.png',\n * { src: '/screenshots/tablet.png', width: 1024, height: 768 },\n * ])\n * ```\n */\nexport function createScreenshots(inputs: Array<SmartScreenshotInput | string>): ScreenshotConfig[] {\n return inputs.map(createScreenshot);\n}\n\nexport function createManifest(config: {\n name: string;\n shortName?: string;\n description?: string;\n themeColor?: string;\n backgroundColor?: string;\n display?: 'standalone' | 'fullscreen' | 'minimal-ui' | 'browser';\n orientation?: 'portrait' | 'landscape' | 'any';\n id?: string;\n startUrl?: string;\n scope?: string;\n lang?: string;\n dir?: 'ltr' | 'rtl' | 'auto';\n icons?: IconPaths | ManifestConfig['icons'];\n screenshots?: ScreenshotConfig[];\n protocol_handlers?: ProtocolHandler[];\n}): () => MetadataRoute.Manifest {\n return () => {\n // Convert IconPaths to manifest icons format\n let manifestIcons: MetadataRoute.Manifest['icons'];\n\n if (Array.isArray(config.icons)) {\n // Already in manifest format\n manifestIcons = config.icons as MetadataRoute.Manifest['icons'];\n } else if (config.icons) {\n // Convert IconPaths to manifest icons\n const { logo192, logo384, logo512 } = config.icons as IconPaths;\n manifestIcons = [\n ...(logo192\n ? [\n {\n src: logo192,\n sizes: '192x192',\n type: 'image/png',\n purpose: 'maskable' as const,\n },\n ]\n : []),\n ...(logo384\n ? [\n {\n src: logo384,\n sizes: '384x384',\n type: 'image/png',\n },\n ]\n : []),\n ...(logo512\n ? [\n {\n src: logo512,\n sizes: '512x512',\n type: 'image/png',\n },\n ]\n : []),\n ];\n }\n\n const manifest: MetadataRoute.Manifest = {\n name: config.name,\n short_name: config.shortName || config.name,\n description: config.description || config.name,\n id: config.id || config.startUrl || '/',\n start_url: config.startUrl || '/',\n scope: config.scope || '/',\n display: config.display || 'standalone',\n orientation: config.orientation || 'portrait',\n background_color: config.backgroundColor || '#000000',\n theme_color: config.themeColor || '#ffffff',\n lang: config.lang || 'en',\n dir: config.dir || 'ltr',\n icons: manifestIcons,\n // Removed forced gcm_sender_id to avoid potential conflicts with VAPID\n // gcm_sender_id: '103953800507',\n };\n\n // Add screenshots if provided (for Richer PWA Install UI)\n if (config.screenshots && config.screenshots.length > 0) {\n (manifest as any).screenshots = config.screenshots;\n }\n\n // Add protocol handlers if provided\n if (config.protocol_handlers && config.protocol_handlers.length > 0) {\n (manifest as any).protocol_handlers = config.protocol_handlers;\n }\n\n return manifest;\n };\n}\n\n/**\n * Generate manifest.json content (legacy)\n *\n * @deprecated Use createManifest() instead\n */\nexport function generateManifest(config: ManifestConfig): Record<string, any> {\n return createManifest(config)();\n}\n","/**\n * PWA (Progressive Web App) Plugin\n *\n * Configures Serwist for service worker and offline support\n * Modern PWA solution for Next.js 15+ with App Router\n *\n * @see https://serwist.pages.dev/\n */\n\nimport type { NextConfig } from 'next';\nimport { consola } from 'consola';\n\nexport interface PWAPluginOptions {\n /**\n * Destination directory for service worker files\n * @default 'public'\n * @deprecated Use swDest instead\n */\n dest?: string;\n\n /**\n * Path to service worker source file (relative to project root)\n * @default 'app/sw.ts'\n */\n swSrc?: string;\n\n /**\n * Destination for compiled service worker\n * @default 'public/sw.js'\n */\n swDest?: string;\n\n /**\n * Disable PWA completely\n * @default false in production, true in development\n * @example disable: process.env.NODE_ENV === 'development'\n */\n disable?: boolean;\n\n /**\n * Cache on navigation - cache pages when navigating\n * @default true\n */\n cacheOnNavigation?: boolean;\n\n /**\n * Reload app when device goes back online\n * @default true\n */\n reloadOnOnline?: boolean;\n\n /**\n * Additional Serwist options\n * @see https://serwist.pages.dev/docs/next/configuring\n */\n serwistOptions?: Record<string, any>;\n}\n\n/**\n * Add PWA configuration to Next.js config using Serwist\n *\n * @example Basic usage\n * ```ts\n * import { createBaseNextConfig, withPWA } from '@djangocfg/nextjs/config';\n *\n * const nextConfig = createBaseNextConfig({...});\n *\n * export default withPWA(nextConfig, {\n * swSrc: 'app/sw.ts',\n * disable: process.env.NODE_ENV === 'development',\n * });\n * ```\n *\n * @example Integrated with createBaseNextConfig\n * ```ts\n * import { createBaseNextConfig } from '@djangocfg/nextjs/config';\n *\n * const config = createBaseNextConfig({\n * pwa: {\n * swSrc: 'app/sw.ts',\n * disable: false,\n * },\n * });\n *\n * export default config;\n * ```\n */\nexport function withPWA(\n nextConfig: NextConfig,\n options: PWAPluginOptions = {}\n): NextConfig {\n const isDev = process.env.NODE_ENV === 'development';\n const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';\n\n // Determine if PWA should be disabled:\n // - Explicitly disabled via options\n // - In development mode (default)\n // - Static build (output: 'export' doesn't support service workers)\n const shouldDisable = options.disable !== undefined\n ? options.disable\n : (isDev || isStaticBuild);\n\n const defaultOptions: PWAPluginOptions = {\n swSrc: 'app/sw.ts',\n swDest: 'public/sw.js',\n disable: shouldDisable,\n cacheOnNavigation: true,\n reloadOnOnline: true,\n ...options,\n };\n\n try {\n // Suppress Turbopack warning - it only applies to dev mode, not production builds\n // The warning is misleading when running `next build` with Turbopack\n // See: https://github.com/serwist/serwist/issues/54\n if (!process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING) {\n process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING = '1';\n }\n\n const withSerwistInit = require('@serwist/next').default;\n\n const withSerwist = withSerwistInit({\n swSrc: defaultOptions.swSrc,\n swDest: defaultOptions.swDest,\n disable: defaultOptions.disable,\n cacheOnNavigation: defaultOptions.cacheOnNavigation,\n reloadOnOnline: defaultOptions.reloadOnOnline,\n ...defaultOptions.serwistOptions,\n });\n\n return withSerwist(nextConfig);\n } catch (error) {\n consola.error('Failed to configure Serwist:', error);\n return nextConfig;\n }\n}\n\n/**\n * Get service worker template content\n *\n * Returns ready-to-use service worker code for app/sw.ts\n *\n * @example\n * ```ts\n * import { getServiceWorkerTemplate } from '@djangocfg/nextjs/config';\n *\n * // Copy this to your app/sw.ts file\n * console.log(getServiceWorkerTemplate());\n * ```\n */\nexport function getServiceWorkerTemplate(): string {\n return `/**\n * Service Worker (Serwist)\n *\n * Modern PWA service worker using Serwist\n */\n\nimport { defaultCache } from '@serwist/next/worker';\nimport { Serwist } from 'serwist';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ndeclare const self: any;\n\nconst serwist = new Serwist({\n // Precache entries injected by Serwist build plugin\n precacheEntries: self.__SW_MANIFEST,\n\n // Skip waiting - activate new SW immediately\n skipWaiting: true,\n\n // Take control of all clients immediately\n clientsClaim: true,\n\n // Enable navigation preload for faster loads\n navigationPreload: true,\n\n // Use default Next.js runtime caching strategies\n runtimeCaching: defaultCache,\n\n // Fallback pages for offline\n fallbacks: {\n entries: [\n {\n url: '/_offline',\n matcher({ request }) {\n return request.destination === 'document';\n },\n },\n ],\n },\n});\n\nserwist.addEventListeners();\n`;\n}\n\n// Backward compatibility exports (deprecated)\nexport const defaultRuntimeCaching = [];\nexport function createApiCacheRule() {\n consola.warn('createApiCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');\n return {};\n}\nexport function createStaticAssetRule() {\n consola.warn('createStaticAssetRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');\n return {};\n}\nexport function createCdnCacheRule() {\n consola.warn('createCdnCacheRule is deprecated with Serwist. Use defaultCache from @serwist/next/worker');\n return {};\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.45",
3
+ "version": "2.1.47",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -139,9 +139,9 @@
139
139
  "web-push": "^3.6.7"
140
140
  },
141
141
  "devDependencies": {
142
- "@djangocfg/imgai": "^2.1.45",
143
- "@djangocfg/layouts": "^2.1.45",
144
- "@djangocfg/typescript-config": "^2.1.45",
142
+ "@djangocfg/imgai": "^2.1.47",
143
+ "@djangocfg/layouts": "^2.1.47",
144
+ "@djangocfg/typescript-config": "^2.1.47",
145
145
  "@types/node": "^24.7.2",
146
146
  "@types/react": "19.2.2",
147
147
  "@types/react-dom": "19.2.1",
package/src/pwa/plugin.ts CHANGED
@@ -110,6 +110,13 @@ export function withPWA(
110
110
  };
111
111
 
112
112
  try {
113
+ // Suppress Turbopack warning - it only applies to dev mode, not production builds
114
+ // The warning is misleading when running `next build` with Turbopack
115
+ // See: https://github.com/serwist/serwist/issues/54
116
+ if (!process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING) {
117
+ process.env.SERWIST_SUPPRESS_TURBOPACK_WARNING = '1';
118
+ }
119
+
113
120
  const withSerwistInit = require('@serwist/next').default;
114
121
 
115
122
  const withSerwist = withSerwistInit({