Skip to content

Commit e0584b2

Browse files
committed
feat: add Next.js 16.0.3 integration test environment and build artifacts for cache components
1 parent ffaa08a commit e0584b2

33 files changed

+6274
-125
lines changed

src/CacheComponentsHandler.ts

Lines changed: 471 additions & 0 deletions
Large diffs are not rendered by default.

src/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { describe, it } from 'vitest';
22

33
describe('Example Test', () => {
44
it('should work correctly', () => {
5-
console.log("TODO tests")
5+
console.log('TODO tests');
66
});
77
});

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ import CachedHandler from './CachedHandler';
22
export default CachedHandler;
33
import RedisStringsHandler from './RedisStringsHandler';
44
export { RedisStringsHandler };
5+
import {
6+
redisCacheHandler,
7+
getRedisCacheComponentsHandler,
8+
} from './CacheComponentsHandler';
9+
export { redisCacheHandler, getRedisCacheComponentsHandler };
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2+
import { spawn, ChildProcess } from 'child_process';
3+
import { createClient, RedisClientType } from 'redis';
4+
import path from 'path';
5+
6+
const PORT = 3055;
7+
const BASE_URL = `http://localhost:${PORT}`;
8+
9+
describe('Next.js 16 Cache Components Integration', () => {
10+
let nextProcess: ChildProcess;
11+
let redisClient: RedisClientType;
12+
let keyPrefix: string;
13+
14+
beforeAll(async () => {
15+
// Connect to Redis
16+
redisClient = createClient({
17+
url: process.env.REDIS_URL || 'redis://localhost:6379',
18+
database: 1,
19+
});
20+
await redisClient.connect();
21+
22+
// Generate unique key prefix for this test run
23+
keyPrefix = `cache-components-test-${Math.random().toString(36).substring(7)}`;
24+
process.env.VERCEL_URL = keyPrefix;
25+
26+
// Build and start Next.js app
27+
const appDir = path.join(__dirname, 'next-app-16-0-3-cache-components');
28+
29+
console.log('Building Next.js app...');
30+
await new Promise<void>((resolve, reject) => {
31+
const buildProcess = spawn('pnpm', ['build'], {
32+
cwd: appDir,
33+
stdio: 'inherit',
34+
});
35+
36+
buildProcess.on('close', (code) => {
37+
if (code === 0) resolve();
38+
else reject(new Error(`Build failed with code ${code}`));
39+
});
40+
});
41+
42+
console.log('Starting Next.js app...');
43+
nextProcess = spawn('pnpm', ['start', '-p', PORT.toString()], {
44+
cwd: appDir,
45+
env: { ...process.env, VERCEL_URL: keyPrefix },
46+
});
47+
48+
// Wait for server to be ready
49+
await new Promise((resolve) => setTimeout(resolve, 3000));
50+
}, 120000);
51+
52+
afterAll(async () => {
53+
// Clean up Redis keys
54+
const keys = await redisClient.keys(`${keyPrefix}*`);
55+
if (keys.length > 0) {
56+
await redisClient.del(keys);
57+
}
58+
await redisClient.quit();
59+
60+
// Kill Next.js process
61+
if (nextProcess) {
62+
nextProcess.kill();
63+
}
64+
});
65+
66+
describe('Basic use cache functionality', () => {
67+
it('should cache data and return same counter value on subsequent requests', async () => {
68+
// First request
69+
const res1 = await fetch(`${BASE_URL}/api/cached-static-fetch`);
70+
const data1 = await res1.json();
71+
72+
expect(data1.counter).toBe(1);
73+
74+
// Second request should return cached data
75+
const res2 = await fetch(`${BASE_URL}/api/cached-static-fetch`);
76+
const data2 = await res2.json();
77+
78+
expect(data2.counter).toBe(1); // Same counter value
79+
expect(data2.timestamp).toBe(data1.timestamp); // Same timestamp
80+
});
81+
82+
it('should store cache entry in Redis', async () => {
83+
await fetch(`${BASE_URL}/api/cached-static-fetch`);
84+
85+
// Check Redis for cache keys
86+
const keys = await redisClient.keys(`${keyPrefix}*`);
87+
expect(keys.length).toBeGreaterThan(0);
88+
});
89+
});
90+
91+
describe('cacheTag functionality', () => {
92+
it('should cache data with tags', async () => {
93+
const res1 = await fetch(`${BASE_URL}/api/cached-with-tag`);
94+
const data1 = await res1.json();
95+
96+
expect(data1.counter).toBeDefined();
97+
98+
// Second request should return cached data
99+
const res2 = await fetch(`${BASE_URL}/api/cached-with-tag`);
100+
const data2 = await res2.json();
101+
102+
expect(data2.counter).toBe(data1.counter);
103+
});
104+
105+
it('should invalidate cache when tag is revalidated (Stale while revalidate)', async () => {
106+
// Get initial cached data
107+
const res1 = await fetch(`${BASE_URL}/api/cached-with-tag`);
108+
const data1 = await res1.json();
109+
110+
// Revalidate the tag
111+
await fetch(`${BASE_URL}/api/revalidate-tag`, {
112+
method: 'POST',
113+
headers: { 'Content-Type': 'application/json' },
114+
body: JSON.stringify({ tag: 'test-tag' }),
115+
});
116+
117+
// The cache should be invalidated - verify by making multiple requests
118+
// until we get fresh data (with retries for async revalidation)
119+
let freshDataReceived = false;
120+
for (let i = 0; i < 5; i++) {
121+
await new Promise((resolve) => setTimeout(resolve, 200));
122+
const res = await fetch(`${BASE_URL}/api/cached-with-tag`);
123+
const data = await res.json();
124+
125+
if (data.counter > data1.counter && data.timestamp > data1.timestamp) {
126+
freshDataReceived = true;
127+
break;
128+
}
129+
}
130+
131+
expect(freshDataReceived).toBe(true);
132+
});
133+
});
134+
135+
describe('Redis cache handler integration', () => {
136+
it('should call cache handler get and set methods', async () => {
137+
// Make request to trigger cache (don't clear first)
138+
await fetch(`${BASE_URL}/api/cached-static-fetch`);
139+
140+
// Verify Redis has the cached data
141+
const redisKeys = await redisClient.keys(`${keyPrefix}*`);
142+
expect(redisKeys.length).toBeGreaterThan(0);
143+
144+
// Filter out hash keys (sharedTagsMap) and only check string keys (cache entries)
145+
// Try to get each key and verify at least one is a string value
146+
let foundStringKey = false;
147+
for (const key of redisKeys) {
148+
try {
149+
const type = await redisClient.type(key);
150+
if (type === 'string') {
151+
const cachedValue = await redisClient.get(key);
152+
if (cachedValue) {
153+
foundStringKey = true;
154+
expect(cachedValue).toBeTruthy();
155+
break;
156+
}
157+
}
158+
} catch (e) {
159+
// Skip non-string keys
160+
}
161+
}
162+
expect(foundStringKey).toBe(true);
163+
});
164+
});
165+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
# or
14+
bun dev
15+
```
16+
17+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
19+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20+
21+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22+
23+
## Learn More
24+
25+
To learn more about Next.js, take a look at the following resources:
26+
27+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29+
30+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31+
32+
## Deploy on Vercel
33+
34+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35+
36+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { redisCacheHandler } = require('@trieb.work/nextjs-turbo-redis-cache');
2+
3+
console.log('Cache handler module loaded!');
4+
5+
module.exports = redisCacheHandler;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineConfig, globalIgnores } from "eslint/config";
2+
import nextVitals from "eslint-config-next/core-web-vitals";
3+
import nextTs from "eslint-config-next/typescript";
4+
5+
const eslintConfig = defineConfig([
6+
...nextVitals,
7+
...nextTs,
8+
// Override default ignores of eslint-config-next.
9+
globalIgnores([
10+
// Default ignores of eslint-config-next:
11+
".next/**",
12+
"out/**",
13+
"build/**",
14+
"next-env.d.ts",
15+
]),
16+
]);
17+
18+
export default eslintConfig;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { NextConfig } from 'next';
2+
3+
const nextConfig: NextConfig = {
4+
cacheComponents: true,
5+
cacheHandlers: {
6+
default: require.resolve('./cache-handler.js'),
7+
},
8+
};
9+
10+
export default nextConfig;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "next-app-16-0-3",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "eslint"
10+
},
11+
"dependencies": {
12+
"@trieb.work/nextjs-turbo-redis-cache": "file:../../../",
13+
"next": "16.0.3",
14+
"react": "19.2.0",
15+
"react-dom": "19.2.0",
16+
"redis": "4.7.0"
17+
},
18+
"devDependencies": {
19+
"@tailwindcss/postcss": "^4",
20+
"@types/node": "^20",
21+
"@types/react": "^19",
22+
"@types/react-dom": "^19",
23+
"eslint": "^9",
24+
"eslint-config-next": "16.0.3",
25+
"tailwindcss": "^4",
26+
"typescript": "^5"
27+
}
28+
}

0 commit comments

Comments
 (0)