@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
@@ -0,0 +1,2 @@
1
+ import 'next/server';
2
+ export { G as GET, P as POST, a as handleGetSubscriptions, b as handleSend, h as handleSubscribe } from '../../routes-DXA29sS_.mjs';
@@ -0,0 +1,149 @@
1
+ // src/pwa/server/routes.ts
2
+ import { NextResponse } from "next/server";
3
+
4
+ // src/pwa/server/push.ts
5
+ import webpush from "web-push";
6
+ import { consola } from "consola";
7
+ var vapidConfigured = false;
8
+ function getVapidKeys() {
9
+ const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
10
+ const privateKey = process.env.VAPID_PRIVATE_KEY;
11
+ const mailto = process.env.VAPID_MAILTO || "mailto:noreply@example.com";
12
+ return { publicKey, privateKey, mailto };
13
+ }
14
+ function configureVapid(options) {
15
+ const { publicKey, privateKey, mailto } = options || getVapidKeys();
16
+ if (!publicKey || !privateKey) {
17
+ consola.warn(
18
+ "\u26A0\uFE0F VAPID keys not configured!\n Generate keys: npx web-push generate-vapid-keys\n Add to .env.local:\n NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key\n VAPID_PRIVATE_KEY=your_private_key\n Push notifications will not work without VAPID keys."
19
+ );
20
+ return false;
21
+ }
22
+ try {
23
+ webpush.setVapidDetails(mailto, publicKey, privateKey);
24
+ vapidConfigured = true;
25
+ consola.success("\u2705 VAPID keys configured for push notifications");
26
+ return true;
27
+ } catch (error) {
28
+ consola.error("Failed to configure VAPID keys:", error.message);
29
+ return false;
30
+ }
31
+ }
32
+ async function sendPushNotification(subscription, notification) {
33
+ if (!vapidConfigured) {
34
+ const configured = configureVapid();
35
+ if (!configured) {
36
+ throw new Error("VAPID keys not configured. Cannot send push notification.");
37
+ }
38
+ }
39
+ const payload = JSON.stringify({
40
+ title: notification.title,
41
+ body: notification.body || "",
42
+ icon: notification.icon,
43
+ badge: notification.badge,
44
+ data: notification.data,
45
+ tag: notification.tag,
46
+ requireInteraction: notification.requireInteraction
47
+ });
48
+ const result = await webpush.sendNotification(subscription, payload);
49
+ console.log("\u2705 Push Sent to FCM:", {
50
+ statusCode: result.statusCode,
51
+ headers: result.headers,
52
+ bodyLength: result.body?.length
53
+ });
54
+ }
55
+ function validateSubscription(subscription) {
56
+ return subscription && typeof subscription === "object" && typeof subscription.endpoint === "string" && subscription.keys && typeof subscription.keys.p256dh === "string" && typeof subscription.keys.auth === "string";
57
+ }
58
+
59
+ // src/pwa/server/routes.ts
60
+ var subscriptions = /* @__PURE__ */ new Set();
61
+ async function handleSubscribe(request) {
62
+ try {
63
+ const subscription = await request.json();
64
+ if (!validateSubscription(subscription)) {
65
+ return NextResponse.json(
66
+ { error: "Invalid subscription format" },
67
+ { status: 400 }
68
+ );
69
+ }
70
+ subscriptions.add(JSON.stringify(subscription));
71
+ console.log("\u2705 Push subscription saved:", {
72
+ endpoint: subscription.endpoint.substring(0, 50) + "...",
73
+ total: subscriptions.size
74
+ });
75
+ return NextResponse.json({
76
+ success: true,
77
+ message: "Subscription saved",
78
+ totalSubscriptions: subscriptions.size
79
+ });
80
+ } catch (error) {
81
+ console.error("Subscription error:", error);
82
+ return NextResponse.json(
83
+ {
84
+ error: "Failed to save subscription",
85
+ details: error.message
86
+ },
87
+ { status: 500 }
88
+ );
89
+ }
90
+ }
91
+ async function handleGetSubscriptions() {
92
+ return NextResponse.json({
93
+ totalSubscriptions: subscriptions.size,
94
+ subscriptions: Array.from(subscriptions).map((sub) => {
95
+ const parsed = JSON.parse(sub);
96
+ return {
97
+ endpoint: parsed.endpoint.substring(0, 50) + "..."
98
+ };
99
+ })
100
+ });
101
+ }
102
+ async function handleSend(request) {
103
+ try {
104
+ const { subscription, notification } = await request.json();
105
+ if (!subscription) {
106
+ return NextResponse.json(
107
+ { error: "Subscription is required" },
108
+ { status: 400 }
109
+ );
110
+ }
111
+ if (!validateSubscription(subscription)) {
112
+ return NextResponse.json(
113
+ { error: "Invalid subscription format" },
114
+ { status: 400 }
115
+ );
116
+ }
117
+ configureVapid();
118
+ await sendPushNotification(subscription, {
119
+ title: notification?.title || "Test Notification",
120
+ body: notification?.body || "This is a test push notification",
121
+ icon: notification?.icon,
122
+ badge: notification?.badge,
123
+ data: notification?.data
124
+ });
125
+ return NextResponse.json({
126
+ success: true,
127
+ message: "Push notification sent"
128
+ });
129
+ } catch (error) {
130
+ console.error("Push notification error:", error);
131
+ return NextResponse.json(
132
+ {
133
+ error: "Failed to send push notification",
134
+ details: error.message
135
+ },
136
+ { status: 500 }
137
+ );
138
+ }
139
+ }
140
+ var POST = handleSubscribe;
141
+ var GET = handleGetSubscriptions;
142
+ export {
143
+ GET,
144
+ POST,
145
+ handleGetSubscriptions,
146
+ handleSend,
147
+ handleSubscribe
148
+ };
149
+ //# sourceMappingURL=routes.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/pwa/server/routes.ts","../../../src/pwa/server/push.ts"],"sourcesContent":["/**\n * Ready-to-use Push Notification Route Handlers\n *\n * Import these in your app/api/push/ routes\n */\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { configureVapid, sendPushNotification, validateSubscription } from './push';\n\n// In-memory storage для demo (в production используй БД)\nconst subscriptions = new Set<string>();\n\n/**\n * POST /api/push/subscribe\n * Save push subscription\n *\n * @example\n * ```typescript\n * // app/api/push/subscribe/route.ts\n * export { POST } from '@djangocfg/nextjs/pwa/server/routes';\n * ```\n */\nexport async function handleSubscribe(request: NextRequest) {\n try {\n const subscription = await request.json();\n\n if (!validateSubscription(subscription)) {\n return NextResponse.json(\n { error: 'Invalid subscription format' },\n { status: 400 }\n );\n }\n\n // Сохраняем subscription (в demo просто в памяти)\n subscriptions.add(JSON.stringify(subscription));\n\n console.log('✅ Push subscription saved:', {\n endpoint: subscription.endpoint.substring(0, 50) + '...',\n total: subscriptions.size,\n });\n\n return NextResponse.json({\n success: true,\n message: 'Subscription saved',\n totalSubscriptions: subscriptions.size,\n });\n } catch (error: any) {\n console.error('Subscription error:', error);\n\n return NextResponse.json(\n {\n error: 'Failed to save subscription',\n details: error.message,\n },\n { status: 500 }\n );\n }\n}\n\n/**\n * GET /api/push/subscribe\n * Get all subscriptions (for testing)\n */\nexport async function handleGetSubscriptions() {\n return NextResponse.json({\n totalSubscriptions: subscriptions.size,\n subscriptions: Array.from(subscriptions).map((sub) => {\n const parsed = JSON.parse(sub);\n return {\n endpoint: parsed.endpoint.substring(0, 50) + '...',\n };\n }),\n });\n}\n\n/**\n * POST /api/push/send\n * Send push notification\n *\n * @example\n * ```typescript\n * // app/api/push/send/route.ts\n * export { POST as handleSend as POST } from '@djangocfg/nextjs/pwa/server/routes';\n * ```\n */\nexport async function handleSend(request: NextRequest) {\n try {\n const { subscription, notification } = await request.json();\n\n if (!subscription) {\n return NextResponse.json(\n { error: 'Subscription is required' },\n { status: 400 }\n );\n }\n\n if (!validateSubscription(subscription)) {\n return NextResponse.json(\n { error: 'Invalid subscription format' },\n { status: 400 }\n );\n }\n\n // Configure VAPID if not already configured\n configureVapid();\n\n await sendPushNotification(subscription, {\n title: notification?.title || 'Test Notification',\n body: notification?.body || 'This is a test push notification',\n icon: notification?.icon,\n badge: notification?.badge,\n data: notification?.data,\n });\n\n return NextResponse.json({\n success: true,\n message: 'Push notification sent',\n });\n } catch (error: any) {\n console.error('Push notification error:', error);\n\n return NextResponse.json(\n {\n error: 'Failed to send push notification',\n details: error.message,\n },\n { status: 500 }\n );\n }\n}\n\n/**\n * Combined route handlers\n * Use like: export { POST, GET } from '@djangocfg/nextjs/pwa/server/routes'\n */\nexport const POST = handleSubscribe;\nexport const GET = handleGetSubscriptions;\n","/**\n * Server-side Push Notification Utilities\n *\n * VAPID-based Web Push notifications using web-push library\n */\n\nimport webpush, { PushSubscription } from 'web-push';\nimport { consola } from 'consola';\n\nlet vapidConfigured = false;\n\n/**\n * Check if VAPID keys are configured\n */\nexport function isVapidConfigured(): boolean {\n return vapidConfigured;\n}\n\n/**\n * Get VAPID keys from environment\n */\nexport function getVapidKeys() {\n const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;\n const privateKey = process.env.VAPID_PRIVATE_KEY;\n const mailto = process.env.VAPID_MAILTO || 'mailto:noreply@example.com';\n\n return { publicKey, privateKey, mailto };\n}\n\n/**\n * Configure VAPID keys for web-push\n * Call this once at app startup\n *\n * @example\n * ```typescript\n * // In your API route or middleware\n * import { configureVapid } from '@djangocfg/nextjs/pwa/server';\n *\n * configureVapid(); // Uses env vars automatically\n * ```\n */\nexport function configureVapid(options?: {\n publicKey?: string;\n privateKey?: string;\n mailto?: string;\n}): boolean {\n const { publicKey, privateKey, mailto } = options || getVapidKeys();\n\n if (!publicKey || !privateKey) {\n consola.warn(\n '⚠️ VAPID keys not configured!\\n' +\n ' Generate keys: npx web-push generate-vapid-keys\\n' +\n ' Add to .env.local:\\n' +\n ' NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key\\n' +\n ' VAPID_PRIVATE_KEY=your_private_key\\n' +\n ' Push notifications will not work without VAPID keys.'\n );\n return false;\n }\n\n try {\n webpush.setVapidDetails(mailto, publicKey, privateKey);\n vapidConfigured = true;\n consola.success('✅ VAPID keys configured for push notifications');\n return true;\n } catch (error: any) {\n consola.error('Failed to configure VAPID keys:', error.message);\n return false;\n }\n}\n\n/**\n * Send push notification to a subscription\n *\n * @example\n * ```typescript\n * await sendPushNotification(subscription, {\n * title: 'Hello!',\n * body: 'Test notification',\n * data: { url: '/page' },\n * });\n * ```\n */\nexport async function sendPushNotification(\n subscription: PushSubscription,\n notification: {\n title: string;\n body?: string;\n icon?: string;\n badge?: string;\n data?: any;\n tag?: string;\n requireInteraction?: boolean;\n }\n): Promise<void> {\n if (!vapidConfigured) {\n const configured = configureVapid();\n if (!configured) {\n throw new Error('VAPID keys not configured. Cannot send push notification.');\n }\n }\n\n const payload = JSON.stringify({\n title: notification.title,\n body: notification.body || '',\n icon: notification.icon,\n badge: notification.badge,\n data: notification.data,\n tag: notification.tag,\n requireInteraction: notification.requireInteraction,\n });\n\n const result = await webpush.sendNotification(subscription, payload);\n console.log('✅ Push Sent to FCM:', {\n statusCode: result.statusCode,\n headers: result.headers,\n bodyLength: result.body?.length\n });\n}\n\n/**\n * Send push notification to multiple subscriptions\n *\n * @example\n * ```typescript\n * const results = await sendPushToMultiple(subscriptions, {\n * title: 'Broadcast message',\n * body: 'Sent to all users',\n * });\n * console.log(`Sent: ${results.successful}, Failed: ${results.failed}`);\n * ```\n */\nexport async function sendPushToMultiple(\n subscriptions: PushSubscription[],\n notification: Parameters<typeof sendPushNotification>[1]\n): Promise<{\n successful: number;\n failed: number;\n errors: Array<{ subscription: PushSubscription; error: Error }>;\n}> {\n const results = await Promise.allSettled(\n subscriptions.map((sub) => sendPushNotification(sub, notification))\n );\n\n const successful = results.filter((r) => r.status === 'fulfilled').length;\n const failed = results.filter((r) => r.status === 'rejected').length;\n const errors = results\n .map((r, i) => (r.status === 'rejected' ? { subscription: subscriptions[i], error: r.reason } : null))\n .filter((e): e is NonNullable<typeof e> => e !== null);\n\n return { successful, failed, errors };\n}\n\n/**\n * Validate push subscription format\n */\nexport function validateSubscription(subscription: any): subscription is PushSubscription {\n return (\n subscription &&\n typeof subscription === 'object' &&\n typeof subscription.endpoint === 'string' &&\n subscription.keys &&\n typeof subscription.keys.p256dh === 'string' &&\n typeof subscription.keys.auth === 'string'\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Service Worker Utilities
3
+ *
4
+ * Ready-to-use Serwist configuration for Next.js PWA
5
+ */
6
+ interface ServiceWorkerOptions {
7
+ /**
8
+ * Offline fallback URL
9
+ * @default '/_offline'
10
+ */
11
+ offlineFallback?: string;
12
+ /**
13
+ * Skip waiting - activate new SW immediately
14
+ * @default true
15
+ */
16
+ skipWaiting?: boolean;
17
+ /**
18
+ * Take control of all clients immediately
19
+ * @default true
20
+ */
21
+ clientsClaim?: boolean;
22
+ /**
23
+ * Enable navigation preload for faster loads
24
+ * @default true
25
+ */
26
+ navigationPreload?: boolean;
27
+ /**
28
+ * Enable push notifications
29
+ * @default false
30
+ */
31
+ enablePushNotifications?: boolean;
32
+ /**
33
+ * Default notification icon
34
+ */
35
+ notificationIcon?: string;
36
+ /**
37
+ * Default notification badge
38
+ */
39
+ notificationBadge?: string;
40
+ }
41
+ /**
42
+ * Create and initialize Serwist service worker
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // app/sw.ts
47
+ * import { createServiceWorker } from '@djangocfg/nextjs/worker';
48
+ *
49
+ * createServiceWorker({
50
+ * offlineFallback: '/_offline',
51
+ * });
52
+ * ```
53
+ */
54
+ declare function createServiceWorker(options?: ServiceWorkerOptions): void;
55
+
56
+ export { type ServiceWorkerOptions, createServiceWorker };
@@ -0,0 +1,97 @@
1
+ // src/pwa/worker/index.ts
2
+ import { defaultCache } from "@serwist/next/worker";
3
+ import { Serwist } from "serwist";
4
+ function createServiceWorker(options = {}) {
5
+ const {
6
+ offlineFallback = "/_offline",
7
+ skipWaiting = true,
8
+ clientsClaim = true,
9
+ navigationPreload = true,
10
+ enablePushNotifications = false,
11
+ notificationIcon,
12
+ notificationBadge
13
+ } = options;
14
+ const self = globalThis;
15
+ console.log("SW: Worker Initialized \u{1F680}");
16
+ const serwist = new Serwist({
17
+ // Precache entries injected by Serwist build plugin
18
+ precacheEntries: self.__SW_MANIFEST,
19
+ // Skip waiting - activate new SW immediately
20
+ skipWaiting,
21
+ // Take control of all clients immediately
22
+ clientsClaim,
23
+ // Enable navigation preload for faster loads
24
+ navigationPreload,
25
+ // Use default Next.js runtime caching strategies
26
+ runtimeCaching: defaultCache,
27
+ // Fallback pages for offline
28
+ fallbacks: {
29
+ entries: [
30
+ {
31
+ url: offlineFallback,
32
+ matcher({ request }) {
33
+ return request.destination === "document";
34
+ }
35
+ }
36
+ ]
37
+ }
38
+ });
39
+ serwist.addEventListeners();
40
+ if (enablePushNotifications) {
41
+ self.addEventListener("push", (event) => {
42
+ let data;
43
+ try {
44
+ data = event.data?.json() || {};
45
+ console.log("SW: Push Received (JSON)", data);
46
+ } catch (err) {
47
+ console.log("SW: Push Received (Raw)", event.data?.text());
48
+ data = {};
49
+ }
50
+ const {
51
+ title = "Notification",
52
+ body = "",
53
+ icon = notificationIcon,
54
+ badge = notificationBadge,
55
+ data: notificationData = {},
56
+ tag,
57
+ requireInteraction = false,
58
+ vibrate = [200, 100, 200],
59
+ // Default vibration pattern
60
+ silent = false
61
+ // Default to playing sound
62
+ } = data;
63
+ event.waitUntil(
64
+ self.registration.showNotification(title, {
65
+ body,
66
+ icon,
67
+ badge,
68
+ data: notificationData,
69
+ tag,
70
+ requireInteraction,
71
+ vibrate,
72
+ silent
73
+ })
74
+ );
75
+ });
76
+ self.addEventListener("notificationclick", (event) => {
77
+ event.notification.close();
78
+ const urlToOpen = event.notification.data?.url || "/";
79
+ event.waitUntil(
80
+ self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => {
81
+ for (const client of clientList) {
82
+ if (client.url === urlToOpen && "focus" in client) {
83
+ return client.focus();
84
+ }
85
+ }
86
+ if (self.clients.openWindow) {
87
+ return self.clients.openWindow(urlToOpen);
88
+ }
89
+ })
90
+ );
91
+ });
92
+ }
93
+ }
94
+ export {
95
+ createServiceWorker
96
+ };
97
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/pwa/worker/index.ts"],"sourcesContent":["/**\n * Service Worker Utilities\n *\n * Ready-to-use Serwist configuration for Next.js PWA\n */\n\n/// <reference lib=\"webworker\" />\n\nimport { defaultCache } from '@serwist/next/worker';\nimport { Serwist } from 'serwist';\n\nexport interface ServiceWorkerOptions {\n /**\n * Offline fallback URL\n * @default '/_offline'\n */\n offlineFallback?: string;\n\n /**\n * Skip waiting - activate new SW immediately\n * @default true\n */\n skipWaiting?: boolean;\n\n /**\n * Take control of all clients immediately\n * @default true\n */\n clientsClaim?: boolean;\n\n /**\n * Enable navigation preload for faster loads\n * @default true\n */\n navigationPreload?: boolean;\n\n /**\n * Enable push notifications\n * @default false\n */\n enablePushNotifications?: boolean;\n\n /**\n * Default notification icon\n */\n notificationIcon?: string;\n\n /**\n * Default notification badge\n */\n notificationBadge?: string;\n}\n\n/**\n * Create and initialize Serwist service worker\n *\n * @example\n * ```ts\n * // app/sw.ts\n * import { createServiceWorker } from '@djangocfg/nextjs/worker';\n *\n * createServiceWorker({\n * offlineFallback: '/_offline',\n * });\n * ```\n */\nexport function createServiceWorker(options: ServiceWorkerOptions = {}) {\n const {\n offlineFallback = '/_offline',\n skipWaiting = true,\n clientsClaim = true,\n navigationPreload = true,\n enablePushNotifications = false,\n notificationIcon,\n notificationBadge,\n } = options;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const self = globalThis as any;\n console.log('SW: Worker Initialized 🚀');\n\n const 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,\n\n // Take control of all clients immediately\n clientsClaim,\n\n // Enable navigation preload for faster loads\n navigationPreload,\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: offlineFallback,\n matcher({ request }) {\n return request.destination === 'document';\n },\n },\n ],\n },\n });\n\n serwist.addEventListeners();\n\n // Push notification support\n if (enablePushNotifications) {\n // Handle push events\n self.addEventListener('push', (event: PushEvent) => {\n let data;\n try {\n data = event.data?.json() || {};\n console.log('SW: Push Received (JSON)', data);\n } catch (err) {\n console.log('SW: Push Received (Raw)', event.data?.text());\n data = {};\n }\n\n const {\n title = 'Notification',\n body = '',\n icon = notificationIcon,\n badge = notificationBadge,\n data: notificationData = {},\n tag,\n requireInteraction = false,\n vibrate = [200, 100, 200], // Default vibration pattern\n silent = false, // Default to playing sound\n } = data;\n\n event.waitUntil(\n self.registration.showNotification(title, {\n body,\n icon,\n badge,\n data: notificationData,\n tag,\n requireInteraction,\n vibrate,\n silent,\n })\n );\n });\n\n // Handle notification clicks\n self.addEventListener('notificationclick', (event: NotificationEvent) => {\n event.notification.close();\n\n const urlToOpen = event.notification.data?.url || '/';\n\n event.waitUntil(\n self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList: readonly WindowClient[]) => {\n // Check if there's already a window open with the URL\n for (const client of clientList) {\n if (client.url === urlToOpen && 'focus' in client) {\n return client.focus();\n }\n }\n // If not, open a new window\n if (self.clients.openWindow) {\n return self.clients.openWindow(urlToOpen);\n }\n })\n );\n });\n }\n}\n"],"mappings":";AAQA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AAyDjB,SAAS,oBAAoB,UAAgC,CAAC,GAAG;AACtE,QAAM;AAAA,IACJ,kBAAkB;AAAA,IAClB,cAAc;AAAA,IACd,eAAe;AAAA,IACf,oBAAoB;AAAA,IACpB,0BAA0B;AAAA,IAC1B;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,QAAM,OAAO;AACb,UAAQ,IAAI,kCAA2B;AAEvC,QAAM,UAAU,IAAI,QAAQ;AAAA;AAAA,IAE1B,iBAAiB,KAAK;AAAA;AAAA,IAGtB;AAAA;AAAA,IAGA;AAAA;AAAA,IAGA;AAAA;AAAA,IAGA,gBAAgB;AAAA;AAAA,IAGhB,WAAW;AAAA,MACT,SAAS;AAAA,QACP;AAAA,UACE,KAAK;AAAA,UACL,QAAQ,EAAE,QAAQ,GAAG;AACnB,mBAAO,QAAQ,gBAAgB;AAAA,UACjC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AAED,UAAQ,kBAAkB;AAG1B,MAAI,yBAAyB;AAE3B,SAAK,iBAAiB,QAAQ,CAAC,UAAqB;AAClD,UAAI;AACJ,UAAI;AACF,eAAO,MAAM,MAAM,KAAK,KAAK,CAAC;AAC9B,gBAAQ,IAAI,4BAA4B,IAAI;AAAA,MAC9C,SAAS,KAAK;AACZ,gBAAQ,IAAI,2BAA2B,MAAM,MAAM,KAAK,CAAC;AACzD,eAAO,CAAC;AAAA,MACV;AAEA,YAAM;AAAA,QACJ,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,MAAM,mBAAmB,CAAC;AAAA,QAC1B;AAAA,QACA,qBAAqB;AAAA,QACrB,UAAU,CAAC,KAAK,KAAK,GAAG;AAAA;AAAA,QACxB,SAAS;AAAA;AAAA,MACX,IAAI;AAEJ,YAAM;AAAA,QACJ,KAAK,aAAa,iBAAiB,OAAO;AAAA,UACxC;AAAA,UACA;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAGD,SAAK,iBAAiB,qBAAqB,CAAC,UAA6B;AACvE,YAAM,aAAa,MAAM;AAEzB,YAAM,YAAY,MAAM,aAAa,MAAM,OAAO;AAElD,YAAM;AAAA,QACJ,KAAK,QAAQ,SAAS,EAAE,MAAM,UAAU,qBAAqB,KAAK,CAAC,EAAE,KAAK,CAAC,eAAwC;AAEjH,qBAAW,UAAU,YAAY;AAC/B,gBAAI,OAAO,QAAQ,aAAa,WAAW,QAAQ;AACjD,qBAAO,OAAO,MAAM;AAAA,YACtB;AAAA,UACF;AAEA,cAAI,KAAK,QAAQ,YAAY;AAC3B,mBAAO,KAAK,QAAQ,WAAW,SAAS;AAAA,UAC1C;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -0,0 +1,68 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * Ready-to-use Push Notification Route Handlers
5
+ *
6
+ * Import these in your app/api/push/ routes
7
+ */
8
+
9
+ /**
10
+ * POST /api/push/subscribe
11
+ * Save push subscription
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // app/api/push/subscribe/route.ts
16
+ * export { POST } from '@djangocfg/nextjs/pwa/server/routes';
17
+ * ```
18
+ */
19
+ declare function handleSubscribe(request: NextRequest): Promise<NextResponse<{
20
+ error: string;
21
+ }> | NextResponse<{
22
+ success: boolean;
23
+ message: string;
24
+ totalSubscriptions: number;
25
+ }>>;
26
+ /**
27
+ * GET /api/push/subscribe
28
+ * Get all subscriptions (for testing)
29
+ */
30
+ declare function handleGetSubscriptions(): Promise<NextResponse<{
31
+ totalSubscriptions: number;
32
+ subscriptions: {
33
+ endpoint: string;
34
+ }[];
35
+ }>>;
36
+ /**
37
+ * POST /api/push/send
38
+ * Send push notification
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // app/api/push/send/route.ts
43
+ * export { POST as handleSend as POST } from '@djangocfg/nextjs/pwa/server/routes';
44
+ * ```
45
+ */
46
+ declare function handleSend(request: NextRequest): Promise<NextResponse<{
47
+ error: string;
48
+ }> | NextResponse<{
49
+ success: boolean;
50
+ message: string;
51
+ }>>;
52
+ /**
53
+ * Combined route handlers
54
+ * Use like: export { POST, GET } from '@djangocfg/nextjs/pwa/server/routes'
55
+ */
56
+ declare const POST: typeof handleSubscribe;
57
+ declare const GET: typeof handleGetSubscriptions;
58
+
59
+ declare const routes_GET: typeof GET;
60
+ declare const routes_POST: typeof POST;
61
+ declare const routes_handleGetSubscriptions: typeof handleGetSubscriptions;
62
+ declare const routes_handleSend: typeof handleSend;
63
+ declare const routes_handleSubscribe: typeof handleSubscribe;
64
+ declare namespace routes {
65
+ export { routes_GET as GET, routes_POST as POST, routes_handleGetSubscriptions as handleGetSubscriptions, routes_handleSend as handleSend, routes_handleSubscribe as handleSubscribe };
66
+ }
67
+
68
+ export { GET as G, POST as P, handleGetSubscriptions as a, handleSend as b, handleSubscribe as h, routes as r };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.35",
3
+ "version": "2.1.37",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -78,18 +78,42 @@
78
78
  "types": "./dist/scripts/index.d.mts",
79
79
  "import": "./dist/scripts/index.mjs",
80
80
  "default": "./dist/scripts/index.mjs"
81
+ },
82
+ "./pwa": {
83
+ "types": "./dist/pwa/index.d.mts",
84
+ "import": "./dist/pwa/index.mjs",
85
+ "default": "./dist/pwa/index.mjs"
86
+ },
87
+ "./pwa/worker": {
88
+ "types": "./dist/pwa/worker/index.d.mts",
89
+ "import": "./dist/pwa/worker/index.mjs",
90
+ "default": "./dist/pwa/worker/index.mjs"
91
+ },
92
+ "./worker": {
93
+ "types": "./dist/pwa/worker/index.d.mts",
94
+ "import": "./dist/pwa/worker/index.mjs",
95
+ "default": "./dist/pwa/worker/index.mjs"
96
+ },
97
+ "./pwa/server": {
98
+ "types": "./dist/pwa/server/index.d.mts",
99
+ "import": "./dist/pwa/server/index.mjs",
100
+ "default": "./dist/pwa/server/index.mjs"
101
+ },
102
+ "./pwa/server/routes": {
103
+ "types": "./dist/pwa/server/routes.d.mts",
104
+ "import": "./dist/pwa/server/routes.mjs",
105
+ "default": "./dist/pwa/server/routes.mjs"
81
106
  }
82
107
  },
83
108
  "files": [
84
109
  "dist",
85
110
  "src",
86
- "bin",
87
111
  "README.md",
88
112
  "LICENSE"
89
113
  ],
90
114
  "bin": {
91
115
  "djangocfg-docs": "./dist/ai/cli.mjs",
92
- "nextjs-dev": "./bin/dev-with-browser.js"
116
+ "djangocfg-pwa": "./dist/pwa/cli.mjs"
93
117
  },
94
118
  "scripts": {
95
119
  "build": "tsup",
@@ -98,26 +122,31 @@
98
122
  "lint": "eslint .",
99
123
  "check": "tsc --noEmit",
100
124
  "check-links": "tsx src/scripts/check-links.ts",
101
- "ai-docs": "tsx src/ai/cli.ts"
125
+ "ai-docs": "tsx src/ai/cli.ts",
126
+ "pwa": "tsx src/pwa/cli.ts"
102
127
  },
103
128
  "peerDependencies": {
104
129
  "next": "^16.0.10"
105
130
  },
106
131
  "dependencies": {
107
- "@ducanh2912/next-pwa": "^10.2.9",
132
+ "@serwist/next": "^9.2.3",
133
+ "@serwist/sw": "^9.2.3",
108
134
  "chalk": "^5.3.0",
109
135
  "conf": "^15.0.2",
110
136
  "consola": "^3.4.2",
111
- "semver": "^7.7.3"
137
+ "semver": "^7.7.3",
138
+ "serwist": "^9.2.3",
139
+ "web-push": "^3.6.7"
112
140
  },
113
141
  "devDependencies": {
114
- "@djangocfg/imgai": "^2.1.35",
115
- "@djangocfg/layouts": "^2.1.35",
116
- "@djangocfg/typescript-config": "^2.1.35",
142
+ "@djangocfg/imgai": "^2.1.37",
143
+ "@djangocfg/layouts": "^2.1.37",
144
+ "@djangocfg/typescript-config": "^2.1.37",
117
145
  "@types/node": "^24.7.2",
118
146
  "@types/react": "19.2.2",
119
147
  "@types/react-dom": "19.2.1",
120
148
  "@types/semver": "^7.7.1",
149
+ "@types/web-push": "^3.6.4",
121
150
  "@types/webpack": "^5.28.5",
122
151
  "@vercel/og": "^0.8.5",
123
152
  "eslint": "^9.37.0",
@@ -29,7 +29,7 @@ import { deepMerge } from './utils/deepMerge';
29
29
  import { isStaticBuild, isDev, getBasePath, getApiUrl, getSiteUrl } from './utils/env';
30
30
  import { DevStartupPlugin } from './plugins/devStartup';
31
31
  import { addCompressionPlugins } from './plugins/compression';
32
- import { withPWA, type PWAPluginOptions } from './plugins/pwa';
32
+ import { withPWA, type PWAPluginOptions } from '../pwa/plugin';
33
33
 
34
34
  // ─────────────────────────────────────────────────────────────────────────
35
35
  // Configuration Options
@@ -42,12 +42,6 @@ export interface BaseNextConfigOptions {
42
42
  transpilePackages?: string[];
43
43
  /** Additional optimize package imports (merged with defaults) */
44
44
  optimizePackageImports?: string[];
45
- /**
46
- * Automatically open browser in dev mode (default: false)
47
- * NOTE: Only works with webpack mode in Next.js 16+ (Turbopack doesn't support webpack plugins)
48
- * For Turbopack compatibility, use a custom dev script instead of this option
49
- */
50
- openBrowser?: boolean;
51
45
  /** Check for @djangocfg/* package updates on startup (default: true) */
52
46
  checkUpdates?: boolean;
53
47
  /** Auto-update outdated packages without prompting (default: false) */
@@ -69,6 +63,8 @@ export interface BaseNextConfigOptions {
69
63
  * @default { enabled: true (in production), disable: true (in development) }
70
64
  */
71
65
  pwa?: PWAPluginOptions | false;
66
+ /** Turbopack configuration (Next.js 16+ default bundler) */
67
+ turbopack?: NextConfig['turbopack'];
72
68
  /** Custom webpack configuration function (called after base webpack logic) */
73
69
  webpack?: (
74
70
  config: WebpackConfig,
@@ -179,6 +175,10 @@ export function createBaseNextConfig(
179
175
  ...(options.transpilePackages || []),
180
176
  ],
181
177
 
178
+ // Turbopack configuration (Next.js 16+ default bundler)
179
+ // Always set turbopack config to silence Next.js 16 warning about webpack config
180
+ turbopack: options.turbopack || {},
181
+
182
182
  // Experimental features
183
183
  experimental: {
184
184
  // Optimize package imports (only in production)
@@ -197,20 +197,16 @@ export function createBaseNextConfig(
197
197
  },
198
198
 
199
199
  // Webpack configuration
200
- // NOTE: Next.js 16 uses Turbopack by default in dev mode, which doesn't support webpack plugins.
201
- // DevStartupPlugin only runs in webpack mode (next dev --webpack).
202
- // For Turbopack compatibility, consider using a custom dev script for browser auto-open.
203
200
  webpack: (config: WebpackConfig, webpackOptions: { isServer: boolean; dev: boolean; [key: string]: any }) => {
204
201
  const { isServer, dev } = webpackOptions;
205
202
 
206
- // Add dev startup plugin (client-side only in dev, webpack mode only)
203
+ // Add dev startup plugin (client-side only in dev)
207
204
  if (dev && !isServer) {
208
205
  if (!config.plugins) {
209
206
  config.plugins = [];
210
207
  }
211
208
  config.plugins.push(
212
209
  new DevStartupPlugin({
213
- openBrowser: options.openBrowser,
214
210
  checkUpdates: options.checkUpdates,
215
211
  autoUpdate: options.autoUpdate,
216
212
  forceCheckWorkspace: options.forceCheckWorkspace,
@@ -256,13 +252,13 @@ export function createBaseNextConfig(
256
252
  // Cleanup: Remove custom options that are not part of NextConfig
257
253
  delete (finalConfig as any).optimizePackageImports;
258
254
  delete (finalConfig as any).isDefaultCfgAdmin;
259
- delete (finalConfig as any).openBrowser;
260
255
  delete (finalConfig as any).checkUpdates;
261
256
  delete (finalConfig as any).autoUpdate;
262
257
  delete (finalConfig as any).forceCheckWorkspace;
263
258
  delete (finalConfig as any).checkPackages;
264
259
  delete (finalConfig as any).autoInstall;
265
260
  delete (finalConfig as any).allowIframeFrom;
261
+ // Note: turbopack is a valid NextConfig option, don't delete it
266
262
 
267
263
  // Apply PWA wrapper if enabled
268
264
  if (options.pwa !== false) {
@@ -6,8 +6,8 @@
6
6
  * import { createBaseNextConfig } from '@djangocfg/nextjs/config';
7
7
  *
8
8
  * export default createBaseNextConfig({
9
- * openBrowser: true,
10
9
  * checkPackages: true,
10
+ * checkUpdates: true,
11
11
  * });
12
12
  * ```
13
13
  */
@@ -97,21 +97,4 @@ export {
97
97
  isCompressionAvailable,
98
98
  type CompressionPluginOptions,
99
99
  } from './plugins/compression';
100
- export {
101
- withPWA,
102
- isPWAAvailable,
103
- defaultRuntimeCaching,
104
- createApiCacheRule,
105
- createStaticAssetRule,
106
- createCdnCacheRule,
107
- type PWAPluginOptions,
108
- type CacheStrategy,
109
- type RuntimeCacheEntry,
110
- } from './plugins/pwa';
111
- export {
112
- createManifestMetadata,
113
- generateManifest,
114
- createManifest,
115
- type ManifestConfig,
116
- type IconPaths,
117
- } from './utils/manifest';
100
+