@trieb.work/nextjs-turbo-redis-cache 1.8.1 → 1.9.0

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 (46) hide show
  1. package/.github/workflows/ci.yml +9 -2
  2. package/CHANGELOG.md +14 -0
  3. package/dist/index.d.mts +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1 -1
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +4 -3
  10. package/src/RedisStringsHandler.ts +2 -2
  11. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +1 -1
  12. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +1 -1
  13. package/test/integration/next-app-15-4-7/README.md +36 -0
  14. package/test/integration/next-app-15-4-7/eslint.config.mjs +16 -0
  15. package/test/integration/next-app-15-4-7/next.config.js +6 -0
  16. package/test/integration/next-app-15-4-7/package-lock.json +5969 -0
  17. package/test/integration/next-app-15-4-7/package.json +33 -0
  18. package/test/integration/next-app-15-4-7/pnpm-lock.yaml +3707 -0
  19. package/test/integration/next-app-15-4-7/postcss.config.mjs +5 -0
  20. package/test/integration/next-app-15-4-7/public/file.svg +1 -0
  21. package/test/integration/next-app-15-4-7/public/globe.svg +1 -0
  22. package/test/integration/next-app-15-4-7/public/next.svg +1 -0
  23. package/test/integration/next-app-15-4-7/public/vercel.svg +1 -0
  24. package/test/integration/next-app-15-4-7/public/window.svg +1 -0
  25. package/test/integration/next-app-15-4-7/src/app/api/cached-static-fetch/route.ts +18 -0
  26. package/test/integration/next-app-15-4-7/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  27. package/test/integration/next-app-15-4-7/src/app/api/revalidatePath/route.ts +15 -0
  28. package/test/integration/next-app-15-4-7/src/app/api/revalidateTag/route.ts +15 -0
  29. package/test/integration/next-app-15-4-7/src/app/api/revalidated-fetch/route.ts +17 -0
  30. package/test/integration/next-app-15-4-7/src/app/api/uncached-fetch/route.ts +15 -0
  31. package/test/integration/next-app-15-4-7/src/app/globals.css +26 -0
  32. package/test/integration/next-app-15-4-7/src/app/layout.tsx +59 -0
  33. package/test/integration/next-app-15-4-7/src/app/page.tsx +755 -0
  34. package/test/integration/next-app-15-4-7/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  35. package/test/integration/next-app-15-4-7/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  36. package/test/integration/next-app-15-4-7/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  37. package/test/integration/next-app-15-4-7/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  38. package/test/integration/next-app-15-4-7/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  39. package/test/integration/next-app-15-4-7/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  40. package/test/integration/next-app-15-4-7/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  41. package/test/integration/next-app-15-4-7/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  42. package/test/integration/next-app-15-4-7/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  43. package/test/integration/next-app-15-4-7/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  44. package/test/integration/next-app-15-4-7/src/app/revalidation-interface.tsx +267 -0
  45. package/test/integration/next-app-15-4-7/tsconfig.json +27 -0
  46. package/test/integration/nextjs-cache-handler.integration.test.ts +7 -5
@@ -0,0 +1,19 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ export default async function TestPage() {
4
+ const res = await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/cached-static-fetch`,
6
+ );
7
+ const data = await res.json();
8
+ return (
9
+ <main
10
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
11
+ >
12
+ <h1>Test Page</h1>
13
+ <p>Counter: {data.counter}</p>
14
+ <p>This is a test page for integration testing.</p>
15
+ <p>Timestamp: {Date.now()}</p>
16
+ <p>Slug: /test-page</p>
17
+ </main>
18
+ );
19
+ }
@@ -0,0 +1,34 @@
1
+ export default async function TestPage() {
2
+ let res;
3
+ try {
4
+ res = await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/cached-static-fetch`,
6
+ {
7
+ next: {
8
+ revalidate: 15,
9
+ tags: ['cached-static-fetch-revalidate15-default-page'],
10
+ },
11
+ },
12
+ );
13
+ } catch (e) {
14
+ // ECONNREFUSED is expected during build
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ if (((e as Error).cause as any)?.code !== 'ECONNREFUSED') {
17
+ throw e;
18
+ }
19
+ }
20
+
21
+ const data = res?.ok ? await res.json() : { counter: -1 };
22
+
23
+ return (
24
+ <main
25
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
26
+ >
27
+ <h1>Test Page</h1>
28
+ <p>Counter: {data.counter}</p>
29
+ <p>This is a test page for integration testing.</p>
30
+ <p>Timestamp: {Date.now()}</p>
31
+ <p>Slug: /test-page</p>
32
+ </main>
33
+ );
34
+ }
@@ -0,0 +1,25 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ export default async function TestPage() {
4
+ const res = await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/cached-static-fetch`,
6
+ {
7
+ next: {
8
+ revalidate: 15,
9
+ tags: ['cached-static-fetch-revalidate15-force-dynamic-page'],
10
+ },
11
+ },
12
+ );
13
+ const data = await res.json();
14
+ return (
15
+ <main
16
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
17
+ >
18
+ <h1>Test Page</h1>
19
+ <p>Counter: {data.counter}</p>
20
+ <p>This is a test page for integration testing.</p>
21
+ <p>Timestamp: {Date.now()}</p>
22
+ <p>Slug: /test-page</p>
23
+ </main>
24
+ );
25
+ }
@@ -0,0 +1,55 @@
1
+ export default async function TestPage() {
2
+ await new Promise((r) => setTimeout(r, 1000));
3
+ const rdm = Math.random();
4
+ return (
5
+ <main
6
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
7
+ >
8
+ <h1>Test Page</h1>
9
+ <p>Random number: {rdm}</p>
10
+ <p>This is a test page for integration testing.</p>
11
+ <p>Timestamp: {Date.now()}</p>
12
+ </main>
13
+ );
14
+ }
15
+
16
+ /**
17
+ * TODO
18
+ * Test cases:
19
+ * 0. Store timestamp of the first call in a variable so we can later check if TTL in redis is correct
20
+ * 1. Call the page twice
21
+ * 2. Extract the Timestamp from both results
22
+ * 3. Compare the two timestamps
23
+ * 4. The timestamps should be the same, meaning that the page was deduplicated
24
+ *
25
+ * 5. Connect to redis and check if the page was cached in redis and if TTL is set correctly
26
+ *
27
+ * 6. Call the page again, but wait 3 seconds before calling it
28
+ * 7. Extract the Timestamp
29
+ * 8. Compare the timestamp to previous timestamp
30
+ * 9. The timestamp should be the same, meaning that the page was cached (By in-memory cache which is set to 10 seconds by default)
31
+ *
32
+ * 10. Call the page again, but wait 11 seconds before calling it
33
+ * 11. Extract the Timestamp
34
+ * 12. Compare the timestamp to previous timestamp
35
+ * 13. The timestamp should be the same, meaning that the page was cached (By redis cache which becomes active after in-memory cache expires)
36
+ *
37
+ * 14. Connect to redis and check if the page was cached in redis and if TTL is set correctly
38
+ *
39
+ * 15. Check expiration time of the page in redis
40
+ *
41
+ * 16. Call the page again after TTL expiration time
42
+ * 17. Extract the Timestamp
43
+ * 18. Compare the timestamp to previous timestamp
44
+ * 19. The timestamp should be different, meaning that the page was recreated
45
+ *
46
+ * 20. call API which will invalidate the page via a revalidatePage action
47
+ * 21. Call the page again
48
+ * 18. Compare the timestamp to previous timestamp
49
+ * 19. The timestamp should be different, meaning that the page was recreated
50
+ *
51
+ * 20. Connect to redis and delete the page from redis
52
+ * 21. Call the page again, but wait 11 seconds before calling it
53
+ * 22. Compare the timestamp to previous timestamp
54
+ * 23. The timestamp should be different, meaning that the page was recreated
55
+ */
@@ -0,0 +1,19 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ export default async function TestPage() {
4
+ const res = await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/revalidated-fetch`,
6
+ );
7
+ const data = await res.json();
8
+ return (
9
+ <main
10
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
11
+ >
12
+ <h1>Test Page</h1>
13
+ <p>Counter: {data.counter}</p>
14
+ <p>This is a test page for integration testing.</p>
15
+ <p>Timestamp: {Date.now()}</p>
16
+ <p>Slug: /test-page</p>
17
+ </main>
18
+ );
19
+ }
@@ -0,0 +1,35 @@
1
+ // This page will inherit the revalidate from it's subsequent fetch request
2
+ // meaning that the page will be revalidated after 15 seconds
3
+
4
+ export default async function TestPage() {
5
+ try {
6
+ const res = await fetch(
7
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/revalidated-fetch`,
8
+ {
9
+ next: {
10
+ revalidate: 15,
11
+ tags: ['revalidated-fetch-revalidate15-default-page'],
12
+ },
13
+ },
14
+ );
15
+ const data = await res.json();
16
+ return (
17
+ <main
18
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
19
+ >
20
+ <h1>Test Page</h1>
21
+ <p>Counter: {data.counter}</p>
22
+ <p>This is a test page for integration testing.</p>
23
+ <p>Timestamp: {Date.now()}</p>
24
+ <p>Slug: /test-page</p>
25
+ </main>
26
+ );
27
+ } catch (e) {
28
+ return (
29
+ <p>
30
+ Error: {JSON.stringify(e)} (an error here is normal during build since
31
+ API is not available yet)
32
+ </p>
33
+ );
34
+ }
35
+ }
@@ -0,0 +1,25 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ export default async function TestPage() {
4
+ const res = await await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/revalidated-fetch`,
6
+ {
7
+ next: {
8
+ revalidate: 15,
9
+ tags: ['revalidated-fetch-revalidate15-force-dynamic-page'],
10
+ },
11
+ },
12
+ );
13
+ const data = await res.json();
14
+ return (
15
+ <main
16
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
17
+ >
18
+ <h1>Test Page</h1>
19
+ <p>Counter: {data.counter}</p>
20
+ <p>This is a test page for integration testing.</p>
21
+ <p>Timestamp: {Date.now()}</p>
22
+ <p>Slug: /test-page</p>
23
+ </main>
24
+ );
25
+ }
@@ -0,0 +1,19 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ export default async function TestPage() {
4
+ const res = await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/uncached-fetch`,
6
+ );
7
+ const data = await res.json();
8
+ return (
9
+ <main
10
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
11
+ >
12
+ <h1>Test Page</h1>
13
+ <p>Counter: {data.counter}</p>
14
+ <p>This is a test page for integration testing.</p>
15
+ <p>Timestamp: {Date.now()}</p>
16
+ <p>Slug: /test-page</p>
17
+ </main>
18
+ );
19
+ }
@@ -0,0 +1,32 @@
1
+ export default async function TestPage() {
2
+ try {
3
+ const res = await fetch(
4
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/uncached-fetch`,
5
+ {
6
+ next: {
7
+ revalidate: 15,
8
+ tags: ['uncached-fetch-revalidate15-default-page'],
9
+ },
10
+ },
11
+ );
12
+ const data = await res.json();
13
+ return (
14
+ <main
15
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
16
+ >
17
+ <h1>Test Page</h1>
18
+ <p>Counter: {data.counter}</p>
19
+ <p>This is a test page for integration testing.</p>
20
+ <p>Timestamp: {Date.now()}</p>
21
+ <p>Slug: /test-page</p>
22
+ </main>
23
+ );
24
+ } catch (e) {
25
+ return (
26
+ <p>
27
+ Error: {JSON.stringify(e)} (an error here is normal during build since
28
+ API is not available yet)
29
+ </p>
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,25 @@
1
+ export const dynamic = 'force-dynamic';
2
+
3
+ export default async function TestPage() {
4
+ const res = await fetch(
5
+ `http://localhost:${process.env.NEXT_START_PORT || 3000}/api/uncached-fetch`,
6
+ {
7
+ next: {
8
+ revalidate: 15,
9
+ tags: ['uncached-fetch-revalidate15-force-dynamic-page'],
10
+ },
11
+ },
12
+ );
13
+ const data = await res.json();
14
+ return (
15
+ <main
16
+ style={{ padding: 32, fontFamily: 'sans-serif', textAlign: 'center' }}
17
+ >
18
+ <h1>Test Page</h1>
19
+ <p>Counter: {data.counter}</p>
20
+ <p>This is a test page for integration testing.</p>
21
+ <p>Timestamp: {Date.now()}</p>
22
+ <p>Slug: /test-page</p>
23
+ </main>
24
+ );
25
+ }
@@ -0,0 +1,267 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ // Define API routes for path-based revalidation
6
+ const apiRoutePaths: Record<string, string> = {
7
+ 'Cached Static Fetch': '/api/cached-static-fetch',
8
+ 'Uncached Fetch': '/api/uncached-fetch',
9
+ 'Revalidated Fetch': '/api/revalidated-fetch',
10
+ 'Nested Fetch in API Route':
11
+ '/api/nested-fetch-in-api-route/revalidated-fetch',
12
+ };
13
+
14
+ // Define API routes with tags for tag-based revalidation
15
+ const apiRouteTags: Record<string, string> = {
16
+ 'Revalidated Fetch in Nested API Route':
17
+ 'revalidated-fetch-revalidate3-nested-fetch-in-api-route',
18
+ 'Revalidated Fetch API': 'revalidated-fetch-api',
19
+ 'Cached Static Fetch API': 'cached-static-fetch-api',
20
+ 'Uncached Fetch API': 'uncached-fetch-api',
21
+ };
22
+
23
+ // Revalidation Interface Component
24
+ export function RevalidationInterface() {
25
+ const [selectedPathRoute, setSelectedPathRoute] = useState('');
26
+ const [selectedTagRoute, setSelectedTagRoute] = useState('');
27
+ const [revalidationStatus, setRevalidationStatus] = useState('');
28
+ const [isLoading, setIsLoading] = useState(false);
29
+
30
+ const handlePathRevalidate = async () => {
31
+ if (!selectedPathRoute) {
32
+ setRevalidationStatus('Please select an API route to revalidate by path');
33
+ return;
34
+ }
35
+
36
+ setIsLoading(true);
37
+ setRevalidationStatus('Revalidating API route by path...');
38
+
39
+ try {
40
+ const path = apiRoutePaths[selectedPathRoute];
41
+ const response = await fetch(`/api/revalidatePath?path=${path}`);
42
+ const data = await response.json();
43
+
44
+ if (response.ok) {
45
+ setRevalidationStatus(
46
+ `Successfully revalidated API route by path: ${selectedPathRoute} (${path})`,
47
+ );
48
+ } else {
49
+ setRevalidationStatus(`Error: ${data.error || 'Unknown error'}`);
50
+ }
51
+ } catch (error: unknown) {
52
+ const errorMessage =
53
+ error instanceof Error ? error.message : 'Unknown error';
54
+ setRevalidationStatus(`Error: ${errorMessage}`);
55
+ } finally {
56
+ setIsLoading(false);
57
+ }
58
+ };
59
+
60
+ const handleTagRevalidate = async () => {
61
+ if (!selectedTagRoute) {
62
+ setRevalidationStatus('Please select an API route to revalidate by tag');
63
+ return;
64
+ }
65
+
66
+ setIsLoading(true);
67
+ setRevalidationStatus('Revalidating API route by tag...');
68
+
69
+ try {
70
+ const tag = apiRouteTags[selectedTagRoute];
71
+ const response = await fetch(`/api/revalidateTag?tag=${tag}`);
72
+ const data = await response.json();
73
+
74
+ if (response.ok) {
75
+ setRevalidationStatus(
76
+ `Successfully revalidated API route by tag: ${selectedTagRoute} (tag: ${tag})`,
77
+ );
78
+ } else {
79
+ setRevalidationStatus(`Error: ${data.error || 'Unknown error'}`);
80
+ }
81
+ } catch (error: unknown) {
82
+ const errorMessage =
83
+ error instanceof Error ? error.message : 'Unknown error';
84
+ setRevalidationStatus(`Error: ${errorMessage}`);
85
+ } finally {
86
+ setIsLoading(false);
87
+ }
88
+ };
89
+
90
+ return (
91
+ <div
92
+ style={{
93
+ border: '1px solid #eaeaea',
94
+ padding: '25px',
95
+ borderRadius: '8px',
96
+ boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
97
+ backgroundColor: '#fff',
98
+ marginBottom: '30px',
99
+ }}
100
+ >
101
+ <h3
102
+ style={{
103
+ fontSize: '1.4rem',
104
+ color: '#0070f3',
105
+ marginTop: '0',
106
+ marginBottom: '15px',
107
+ borderBottom: '1px solid #eaeaea',
108
+ paddingBottom: '10px',
109
+ }}
110
+ >
111
+ API Route Revalidation Interface
112
+ </h3>
113
+
114
+ {/* Path-based revalidation section */}
115
+ <div
116
+ style={{
117
+ padding: '15px',
118
+ marginBottom: '20px',
119
+ backgroundColor: '#f9f9f9',
120
+ borderRadius: '5px',
121
+ border: '1px solid #eaeaea',
122
+ }}
123
+ >
124
+ <h4
125
+ style={{
126
+ fontSize: '1.1rem',
127
+ color: '#0070f3',
128
+ marginTop: '0',
129
+ marginBottom: '15px',
130
+ }}
131
+ >
132
+ Revalidate API Route by Path
133
+ </h4>
134
+
135
+ <div style={{ marginBottom: '15px' }}>
136
+ <label
137
+ style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
138
+ >
139
+ Select API Route:
140
+ </label>
141
+ <select
142
+ value={selectedPathRoute}
143
+ onChange={(e) => setSelectedPathRoute(e.target.value)}
144
+ style={{
145
+ width: '100%',
146
+ padding: '10px',
147
+ borderRadius: '5px',
148
+ border: '1px solid #ddd',
149
+ fontSize: '1rem',
150
+ }}
151
+ >
152
+ <option value="">-- Select an API route --</option>
153
+ {Object.keys(apiRoutePaths).map((route) => (
154
+ <option key={route} value={route}>
155
+ {route}
156
+ </option>
157
+ ))}
158
+ </select>
159
+ </div>
160
+
161
+ <button
162
+ onClick={handlePathRevalidate}
163
+ disabled={isLoading || !selectedPathRoute}
164
+ style={{
165
+ backgroundColor: '#0070f3',
166
+ color: 'white',
167
+ border: 'none',
168
+ padding: '10px 20px',
169
+ borderRadius: '5px',
170
+ fontSize: '1rem',
171
+ fontWeight: '500',
172
+ cursor: isLoading || !selectedPathRoute ? 'not-allowed' : 'pointer',
173
+ opacity: isLoading || !selectedPathRoute ? 0.7 : 1,
174
+ transition: 'all 0.2s',
175
+ }}
176
+ >
177
+ {isLoading ? 'Revalidating...' : 'Revalidate by Path'}
178
+ </button>
179
+ </div>
180
+
181
+ {/* Tag-based revalidation section */}
182
+ <div
183
+ style={{
184
+ padding: '15px',
185
+ marginBottom: '20px',
186
+ backgroundColor: '#f9f9f9',
187
+ borderRadius: '5px',
188
+ border: '1px solid #eaeaea',
189
+ }}
190
+ >
191
+ <h4
192
+ style={{
193
+ fontSize: '1.1rem',
194
+ color: '#0070f3',
195
+ marginTop: '0',
196
+ marginBottom: '15px',
197
+ }}
198
+ >
199
+ Revalidate API Route by Tag
200
+ </h4>
201
+
202
+ <div style={{ marginBottom: '15px' }}>
203
+ <label
204
+ style={{ display: 'block', marginBottom: '8px', fontWeight: '500' }}
205
+ >
206
+ Select API Route with Tag:
207
+ </label>
208
+ <select
209
+ value={selectedTagRoute}
210
+ onChange={(e) => setSelectedTagRoute(e.target.value)}
211
+ style={{
212
+ width: '100%',
213
+ padding: '10px',
214
+ borderRadius: '5px',
215
+ border: '1px solid #ddd',
216
+ fontSize: '1rem',
217
+ }}
218
+ >
219
+ <option value="">-- Select an API route with tag --</option>
220
+ {Object.keys(apiRouteTags).map((route) => (
221
+ <option key={route} value={route}>
222
+ {route}
223
+ </option>
224
+ ))}
225
+ </select>
226
+ </div>
227
+
228
+ <button
229
+ onClick={handleTagRevalidate}
230
+ disabled={isLoading || !selectedTagRoute}
231
+ style={{
232
+ backgroundColor: '#0070f3',
233
+ color: 'white',
234
+ border: 'none',
235
+ padding: '10px 20px',
236
+ borderRadius: '5px',
237
+ fontSize: '1rem',
238
+ fontWeight: '500',
239
+ cursor: isLoading || !selectedTagRoute ? 'not-allowed' : 'pointer',
240
+ opacity: isLoading || !selectedTagRoute ? 0.7 : 1,
241
+ transition: 'all 0.2s',
242
+ }}
243
+ >
244
+ {isLoading ? 'Revalidating...' : 'Revalidate by Tag'}
245
+ </button>
246
+ </div>
247
+
248
+ {/* Status display */}
249
+ {revalidationStatus && (
250
+ <div
251
+ style={{
252
+ marginTop: '15px',
253
+ padding: '10px',
254
+ borderRadius: '5px',
255
+ backgroundColor: revalidationStatus.includes('Error')
256
+ ? '#ffebee'
257
+ : '#e3f2fd',
258
+ color: revalidationStatus.includes('Error') ? '#c62828' : '#0070f3',
259
+ border: `1px solid ${revalidationStatus.includes('Error') ? '#ffcdd2' : '#bbdefb'}`,
260
+ }}
261
+ >
262
+ {revalidationStatus}
263
+ </div>
264
+ )}
265
+ </div>
266
+ );
267
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }
@@ -6,8 +6,10 @@ import { join } from 'path';
6
6
  import { CacheEntry } from '../../src/RedisStringsHandler';
7
7
  import { revalidate as next1503_revalidatedFetch_route } from './next-app-15-0-3/src/app/api/revalidated-fetch/route';
8
8
 
9
- // const NEXT_APP_DIR = join(__dirname, 'next-app-15-0-3');
10
- const NEXT_APP_DIR = join(__dirname, 'next-app-15-3-2');
9
+ // Select which Next.js test app to use. Can be overridden via NEXT_TEST_APP env var
10
+ // Examples: next-app-15-0-3, next-app-15-3-2, next-app-15-4-7
11
+ const NEXT_TEST_APP = process.env.NEXT_TEST_APP || 'next-app-15-4-7';
12
+ const NEXT_APP_DIR = join(__dirname, NEXT_TEST_APP);
11
13
  console.log('NEXT_APP_DIR', NEXT_APP_DIR);
12
14
  const NEXT_START_PORT = 3055;
13
15
  const NEXT_START_URL = `http://localhost:${NEXT_START_PORT}`;
@@ -574,8 +576,9 @@ describe('Next.js Turbo Redis Cache Integration', () => {
574
576
  expect(data).toBeDefined();
575
577
  const cacheEntry: CacheEntry = JSON.parse(data);
576
578
 
577
- // The format should be as expected
578
- expect(cacheEntry).toEqual({
579
+ // The format should be as expected. We intentionally do not assert on an optional status field here
580
+ // so that different Next.js versions (which may include or omit it) are both supported.
581
+ expect(cacheEntry).toMatchObject({
579
582
  value: {
580
583
  kind: 'APP_PAGE',
581
584
  html: expect.any(String),
@@ -587,7 +590,6 @@ describe('Next.js Turbo Redis Cache Integration', () => {
587
590
  'x-next-cache-tags':
588
591
  '_N_T_/layout,_N_T_/pages/layout,_N_T_/pages/no-fetch/layout,_N_T_/pages/no-fetch/default-page/layout,_N_T_/pages/no-fetch/default-page/page,_N_T_/pages/no-fetch/default-page',
589
592
  },
590
- status: 200,
591
593
  },
592
594
  lastModified: expect.any(Number),
593
595
  tags: [