@djangocfg/nextjs 2.1.30 → 2.1.31

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.
@@ -0,0 +1,616 @@
1
+ /**
2
+ * PWA (Progressive Web App) Plugin
3
+ *
4
+ * Configures @ducanh2912/next-pwa for service worker and offline support
5
+ * Modern fork that supports Next.js 13+ with app directory
6
+ *
7
+ * @see https://www.npmjs.com/package/@ducanh2912/next-pwa
8
+ */
9
+
10
+ import type { NextConfig } from 'next';
11
+ import { consola } from 'consola';
12
+
13
+ /**
14
+ * Runtime caching handler strategies
15
+ */
16
+ export type CacheStrategy =
17
+ | 'CacheFirst'
18
+ | 'CacheOnly'
19
+ | 'NetworkFirst'
20
+ | 'NetworkOnly'
21
+ | 'StaleWhileRevalidate';
22
+
23
+ /**
24
+ * Runtime cache entry configuration
25
+ */
26
+ export interface RuntimeCacheEntry {
27
+ urlPattern: RegExp | string | ((context: { url: URL; request: Request }) => boolean);
28
+ handler: CacheStrategy;
29
+ options?: {
30
+ cacheName?: string;
31
+ expiration?: {
32
+ maxEntries?: number;
33
+ maxAgeSeconds?: number;
34
+ purgeOnQuotaError?: boolean;
35
+ };
36
+ cacheableResponse?: {
37
+ statuses?: number[];
38
+ headers?: Record<string, string>;
39
+ };
40
+ rangeRequests?: boolean;
41
+ backgroundSync?: {
42
+ name: string;
43
+ options?: {
44
+ maxRetentionTime?: number;
45
+ };
46
+ };
47
+ broadcastUpdate?: {
48
+ channelName?: string;
49
+ options?: {
50
+ headersToCheck?: string[];
51
+ };
52
+ };
53
+ matchOptions?: {
54
+ ignoreSearch?: boolean;
55
+ ignoreMethod?: boolean;
56
+ ignoreVary?: boolean;
57
+ };
58
+ networkTimeoutSeconds?: number;
59
+ plugins?: any[];
60
+ fetchOptions?: RequestInit;
61
+ };
62
+ }
63
+
64
+ export interface PWAPluginOptions {
65
+ /**
66
+ * Destination directory for service worker files
67
+ * @default 'public'
68
+ */
69
+ dest: string;
70
+
71
+ /**
72
+ * Disable PWA completely
73
+ * @default false in production, true in development
74
+ * @example disable: process.env.NODE_ENV === 'development'
75
+ */
76
+ disable?: boolean;
77
+
78
+ /**
79
+ * Auto-register service worker
80
+ * @default true
81
+ * @description Set to false if you want to register SW manually
82
+ */
83
+ register?: boolean;
84
+
85
+ /**
86
+ * URL scope for PWA
87
+ * @default '/'
88
+ * @example '/app' - only /app/** will be PWA
89
+ */
90
+ scope?: string;
91
+
92
+ /**
93
+ * Service worker file name
94
+ * @default 'sw.js'
95
+ */
96
+ sw?: string;
97
+
98
+ /**
99
+ * Skip waiting for service worker activation
100
+ * @default true
101
+ * @description Activate new SW immediately
102
+ */
103
+ skipWaiting?: boolean;
104
+
105
+ /**
106
+ * Client claim - take control of uncontrolled clients immediately
107
+ * @default true
108
+ */
109
+ clientsClaim?: boolean;
110
+
111
+ /**
112
+ * Cleanup outdated caches automatically
113
+ * @default true
114
+ */
115
+ cleanupOutdatedCaches?: boolean;
116
+
117
+ /**
118
+ * Runtime caching strategies
119
+ * @default defaultRuntimeCaching
120
+ * @see defaultRuntimeCaching for default configuration
121
+ */
122
+ runtimeCaching?: RuntimeCacheEntry[];
123
+
124
+ /**
125
+ * Exclude files from build directory precaching
126
+ * @example [/chunks\/images\/.*$/] - don't precache images
127
+ */
128
+ buildExcludes?: (string | RegExp | ((chunk: any) => boolean))[];
129
+
130
+ /**
131
+ * Exclude files from public directory precaching
132
+ * @default ['!noprecache/**\/*']
133
+ * @example ['!videos/**\/*', '!large-images/**\/*']
134
+ */
135
+ publicExcludes?: string[];
136
+
137
+ /**
138
+ * Cache start URL (_app or / route)
139
+ * @default true
140
+ */
141
+ cacheStartUrl?: boolean;
142
+
143
+ /**
144
+ * Dynamic start URL (returns different HTML for different states)
145
+ * @default true
146
+ * @description Set to false if start URL always returns same HTML
147
+ */
148
+ dynamicStartUrl?: boolean;
149
+
150
+ /**
151
+ * Redirect URL if start URL redirects (e.g., to /login)
152
+ * @example '/login'
153
+ */
154
+ dynamicStartUrlRedirect?: string;
155
+
156
+ /**
157
+ * Enable additional route caching on front-end navigation
158
+ * @default false
159
+ * @description Cache routes when navigating with next/link
160
+ */
161
+ cacheOnFrontEndNav?: boolean;
162
+
163
+ /**
164
+ * Subdomain prefix for static files
165
+ * @deprecated Use basePath in next.config.js instead
166
+ */
167
+ subdomainPrefix?: string;
168
+
169
+ /**
170
+ * Reload app when device goes back online
171
+ * @default true
172
+ */
173
+ reloadOnOnline?: boolean;
174
+
175
+ /**
176
+ * Custom worker directory
177
+ * @default 'worker'
178
+ * @description Directory containing custom worker implementation
179
+ */
180
+ customWorkerDir?: string;
181
+
182
+ /**
183
+ * Custom service worker source (for InjectManifest mode)
184
+ * @description Path to your custom service worker source
185
+ */
186
+ swSrc?: string;
187
+
188
+ /**
189
+ * Offline fallback pages
190
+ * @example { document: '/_offline', image: '/offline.jpg' }
191
+ */
192
+ fallbacks?: {
193
+ document?: string;
194
+ image?: string;
195
+ audio?: string;
196
+ video?: string;
197
+ font?: string;
198
+ };
199
+
200
+ /**
201
+ * Additional workbox options
202
+ * @see https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin
203
+ */
204
+ workboxOptions?: Record<string, any>;
205
+ }
206
+
207
+ /**
208
+ * Check if @ducanh2912/next-pwa is installed
209
+ *
210
+ * Note: @ducanh2912/next-pwa is included as a dependency,
211
+ * so this should always return true unless there's an installation issue
212
+ */
213
+ export function isPWAAvailable(): boolean {
214
+ try {
215
+ require.resolve('@ducanh2912/next-pwa');
216
+ return true;
217
+ } catch {
218
+ // Try legacy next-pwa (not recommended for Next.js 13+)
219
+ try {
220
+ require.resolve('next-pwa');
221
+ consola.warn(
222
+ 'Found legacy next-pwa. Please use @ducanh2912/next-pwa for Next.js 13+ support.\n' +
223
+ 'Run: pnpm remove next-pwa && pnpm add @ducanh2912/next-pwa'
224
+ );
225
+ return true;
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Add PWA configuration to Next.js config
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * // Basic usage
238
+ * export default withPWA(nextConfig, {
239
+ * dest: 'public',
240
+ * disable: process.env.NODE_ENV === 'development',
241
+ * });
242
+ *
243
+ * // With custom runtime caching
244
+ * export default withPWA(nextConfig, {
245
+ * dest: 'public',
246
+ * runtimeCaching: [
247
+ * {
248
+ * urlPattern: /^https:\/\/api\.example\.com\/.* /i,
249
+ * handler: 'NetworkFirst',
250
+ * options: {
251
+ * cacheName: 'api-cache',
252
+ * expiration: { maxEntries: 50, maxAgeSeconds: 300 },
253
+ * },
254
+ * },
255
+ * ],
256
+ * });
257
+ *
258
+ * // With offline fallbacks
259
+ * export default withPWA(nextConfig, {
260
+ * dest: 'public',
261
+ * fallbacks: {
262
+ * document: '/_offline',
263
+ * image: '/offline.jpg',
264
+ * },
265
+ * });
266
+ * ```
267
+ */
268
+ export function withPWA(
269
+ nextConfig: NextConfig,
270
+ options: Partial<PWAPluginOptions> = {}
271
+ ): NextConfig {
272
+ // Check if next-pwa is available
273
+ if (!isPWAAvailable()) {
274
+ consola.error(
275
+ '@ducanh2912/next-pwa is missing!\n' +
276
+ 'This is unexpected as it should be installed automatically.\n' +
277
+ 'Try: pnpm install --force\n' +
278
+ 'PWA features will be disabled.'
279
+ );
280
+ return nextConfig;
281
+ }
282
+
283
+ const isDev = process.env.NODE_ENV === 'development';
284
+
285
+ const defaultOptions: Partial<PWAPluginOptions> = {
286
+ dest: 'public',
287
+ disable: options.disable !== undefined ? options.disable : isDev,
288
+ register: true,
289
+ skipWaiting: true,
290
+ clientsClaim: true,
291
+ cleanupOutdatedCaches: true,
292
+ publicExcludes: ['!noprecache/**/*'],
293
+ buildExcludes: [/middleware-manifest\.json$/, /build-manifest\.json$/],
294
+ cacheStartUrl: true,
295
+ dynamicStartUrl: true,
296
+ reloadOnOnline: true,
297
+ ...options,
298
+ };
299
+
300
+ try {
301
+ // Use modern next-pwa (@ducanh2912/next-pwa) - supports Next.js 13+
302
+ const withPWAImport = require('@ducanh2912/next-pwa').default;
303
+ return withPWAImport(defaultOptions)(nextConfig);
304
+ } catch {
305
+ try {
306
+ // Fallback to legacy next-pwa (not recommended for Next.js 13+)
307
+ const withPWAImport = require('next-pwa');
308
+ consola.warn('Using legacy next-pwa. Upgrade to @ducanh2912/next-pwa for Next.js 13+ support');
309
+ return withPWAImport(defaultOptions)(nextConfig);
310
+ } catch (error) {
311
+ consola.error('Failed to load next-pwa:', error);
312
+ return nextConfig;
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Default runtime caching strategies
319
+ *
320
+ * Optimized caching rules for common assets:
321
+ * - Google Fonts (webfonts + stylesheets)
322
+ * - Static assets (images, fonts, audio, video)
323
+ * - Next.js resources (JS, CSS, data, images)
324
+ * - API routes excluded from page cache
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * // Use default caching
329
+ * withPWA(nextConfig, {
330
+ * dest: 'public',
331
+ * runtimeCaching: defaultRuntimeCaching,
332
+ * });
333
+ *
334
+ * // Extend with custom rules
335
+ * withPWA(nextConfig, {
336
+ * dest: 'public',
337
+ * runtimeCaching: [
338
+ * ...defaultRuntimeCaching,
339
+ * {
340
+ * urlPattern: /^https:\/\/api\.example\.com\/.* /i,
341
+ * handler: 'NetworkFirst',
342
+ * options: { cacheName: 'api-cache' },
343
+ * },
344
+ * ],
345
+ * });
346
+ * ```
347
+ */
348
+ export const defaultRuntimeCaching: RuntimeCacheEntry[] = [
349
+ {
350
+ urlPattern: /^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,
351
+ handler: 'CacheFirst',
352
+ options: {
353
+ cacheName: 'google-fonts-webfonts',
354
+ expiration: {
355
+ maxEntries: 4,
356
+ maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
357
+ },
358
+ },
359
+ },
360
+ {
361
+ urlPattern: /^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,
362
+ handler: 'StaleWhileRevalidate',
363
+ options: {
364
+ cacheName: 'google-fonts-stylesheets',
365
+ expiration: {
366
+ maxEntries: 4,
367
+ maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
368
+ },
369
+ },
370
+ },
371
+ {
372
+ urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
373
+ handler: 'StaleWhileRevalidate',
374
+ options: {
375
+ cacheName: 'static-font-assets',
376
+ expiration: {
377
+ maxEntries: 4,
378
+ maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
379
+ },
380
+ },
381
+ },
382
+ {
383
+ urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
384
+ handler: 'StaleWhileRevalidate',
385
+ options: {
386
+ cacheName: 'static-image-assets',
387
+ expiration: {
388
+ maxEntries: 64,
389
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
390
+ },
391
+ },
392
+ },
393
+ {
394
+ urlPattern: /\/_next\/image\?url=.+$/i,
395
+ handler: 'StaleWhileRevalidate',
396
+ options: {
397
+ cacheName: 'next-image',
398
+ expiration: {
399
+ maxEntries: 64,
400
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
401
+ },
402
+ },
403
+ },
404
+ {
405
+ urlPattern: /\.(?:mp3|wav|ogg)$/i,
406
+ handler: 'CacheFirst',
407
+ options: {
408
+ rangeRequests: true,
409
+ cacheName: 'static-audio-assets',
410
+ expiration: {
411
+ maxEntries: 32,
412
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
413
+ },
414
+ },
415
+ },
416
+ {
417
+ urlPattern: /\.(?:mp4)$/i,
418
+ handler: 'CacheFirst',
419
+ options: {
420
+ rangeRequests: true,
421
+ cacheName: 'static-video-assets',
422
+ expiration: {
423
+ maxEntries: 32,
424
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
425
+ },
426
+ },
427
+ },
428
+ {
429
+ urlPattern: /\.(?:js)$/i,
430
+ handler: 'StaleWhileRevalidate',
431
+ options: {
432
+ cacheName: 'static-js-assets',
433
+ expiration: {
434
+ maxEntries: 32,
435
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
436
+ },
437
+ },
438
+ },
439
+ {
440
+ urlPattern: /\.(?:css|less)$/i,
441
+ handler: 'StaleWhileRevalidate',
442
+ options: {
443
+ cacheName: 'static-style-assets',
444
+ expiration: {
445
+ maxEntries: 32,
446
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
447
+ },
448
+ },
449
+ },
450
+ {
451
+ urlPattern: /\/_next\/data\/.+\/.+\.json$/i,
452
+ handler: 'StaleWhileRevalidate',
453
+ options: {
454
+ cacheName: 'next-data',
455
+ expiration: {
456
+ maxEntries: 32,
457
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
458
+ },
459
+ },
460
+ },
461
+ {
462
+ urlPattern: /\.(?:json|xml|csv)$/i,
463
+ handler: 'NetworkFirst',
464
+ options: {
465
+ cacheName: 'static-data-assets',
466
+ expiration: {
467
+ maxEntries: 32,
468
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
469
+ },
470
+ },
471
+ },
472
+ {
473
+ urlPattern: ({ url }: { url: URL }) => {
474
+ const isSameOrigin = self.origin === url.origin;
475
+ if (!isSameOrigin) return false;
476
+ const pathname = url.pathname;
477
+ // Exclude /api/ routes
478
+ if (pathname.startsWith('/api/')) return false;
479
+ return true;
480
+ },
481
+ handler: 'NetworkFirst',
482
+ options: {
483
+ cacheName: 'pages',
484
+ expiration: {
485
+ maxEntries: 32,
486
+ maxAgeSeconds: 24 * 60 * 60, // 24 hours
487
+ },
488
+ },
489
+ },
490
+ ];
491
+
492
+ /**
493
+ * Helper: Create API caching rule
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * createApiCacheRule('https://api.example.com', {
498
+ * strategy: 'NetworkFirst',
499
+ * maxAge: 300, // 5 minutes
500
+ * });
501
+ * ```
502
+ */
503
+ export function createApiCacheRule(
504
+ apiUrl: string,
505
+ options: {
506
+ strategy?: CacheStrategy;
507
+ maxAge?: number;
508
+ maxEntries?: number;
509
+ cacheName?: string;
510
+ } = {}
511
+ ): RuntimeCacheEntry {
512
+ const {
513
+ strategy = 'NetworkFirst',
514
+ maxAge = 300,
515
+ maxEntries = 50,
516
+ cacheName = 'api-cache',
517
+ } = options;
518
+
519
+ return {
520
+ urlPattern: new RegExp(`^${apiUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/.*`, 'i'),
521
+ handler: strategy,
522
+ options: {
523
+ cacheName,
524
+ expiration: {
525
+ maxEntries,
526
+ maxAgeSeconds: maxAge,
527
+ },
528
+ networkTimeoutSeconds: 10,
529
+ },
530
+ };
531
+ }
532
+
533
+ /**
534
+ * Helper: Create static asset caching rule
535
+ *
536
+ * @example
537
+ * ```ts
538
+ * createStaticAssetRule(['jpg', 'png', 'webp'], {
539
+ * strategy: 'CacheFirst',
540
+ * maxAge: 86400, // 1 day
541
+ * });
542
+ * ```
543
+ */
544
+ export function createStaticAssetRule(
545
+ extensions: string[],
546
+ options: {
547
+ strategy?: CacheStrategy;
548
+ maxAge?: number;
549
+ maxEntries?: number;
550
+ cacheName?: string;
551
+ } = {}
552
+ ): RuntimeCacheEntry {
553
+ const {
554
+ strategy = 'CacheFirst',
555
+ maxAge = 86400,
556
+ maxEntries = 64,
557
+ cacheName = 'static-assets',
558
+ } = options;
559
+
560
+ const pattern = extensions.map((ext) => ext.replace('.', '')).join('|');
561
+
562
+ return {
563
+ urlPattern: new RegExp(`\\.(?:${pattern})$`, 'i'),
564
+ handler: strategy,
565
+ options: {
566
+ cacheName,
567
+ expiration: {
568
+ maxEntries,
569
+ maxAgeSeconds: maxAge,
570
+ },
571
+ },
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Helper: Create CDN caching rule
577
+ *
578
+ * @example
579
+ * ```ts
580
+ * createCdnCacheRule('https://cdn.example.com', {
581
+ * strategy: 'CacheFirst',
582
+ * maxAge: 2592000, // 30 days
583
+ * });
584
+ * ```
585
+ */
586
+ export function createCdnCacheRule(
587
+ cdnUrl: string,
588
+ options: {
589
+ strategy?: CacheStrategy;
590
+ maxAge?: number;
591
+ maxEntries?: number;
592
+ cacheName?: string;
593
+ } = {}
594
+ ): RuntimeCacheEntry {
595
+ const {
596
+ strategy = 'CacheFirst',
597
+ maxAge = 2592000, // 30 days
598
+ maxEntries = 100,
599
+ cacheName = 'cdn-cache',
600
+ } = options;
601
+
602
+ return {
603
+ urlPattern: new RegExp(`^${cdnUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/.*`, 'i'),
604
+ handler: strategy,
605
+ options: {
606
+ cacheName,
607
+ expiration: {
608
+ maxEntries,
609
+ maxAgeSeconds: maxAge,
610
+ },
611
+ cacheableResponse: {
612
+ statuses: [0, 200],
613
+ },
614
+ },
615
+ };
616
+ }
@@ -5,3 +5,4 @@
5
5
  export { deepMerge } from './deepMerge';
6
6
  export * from './env';
7
7
  export * from './version';
8
+ export * from './manifest';