Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/kind-experts-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@10up/next-redis-cache-provider": minor
"@headstartwp/block-primitives": minor
"@headstartwp/epio-search": minor
"@headstartwp/core": minor
"@headstartwp/next": minor
---

Add support for Next.js 15
13,344 changes: 7,610 additions & 5,734 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
}
],
"scripts": {
"clean": "rm -rf node_modules && find . -name 'node_modules' -type d -prune -exec rm -rf '{}' \\;",
"build": "turbo run build",
"build:packages": "turbo run build --filter=./packages/*",
"build:wpnextjs": "turbo run build --filter=./projects/wp-nextjs",
Expand Down Expand Up @@ -59,6 +60,10 @@
"prettier": "3.2.5",
"turbo": "^2.0.11"
},
"overrides": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"nextBundleAnalysis": {
"buildOutputDirectory": "./projects/wp-nextjs/.next",
"budget": 148480,
Expand Down
2 changes: 2 additions & 0 deletions packages/block-primitives/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export default {
testPathIgnorePatterns: ['dist'],
collectCoverage: true,
setupFilesAfterEnv: ['./jest.setup.ts'],
extensionsToTreatAsEsm: ['.ts', '.tsx', '.mts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
1 change: 1 addition & 0 deletions packages/block-primitives/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@types/wordpress__block-editor": "^11.5.12"
},
"devDependencies": {
"@jest/globals": "29.0.3",
"@testing-library/dom": "^9.3.4",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
Expand Down
23 changes: 12 additions & 11 deletions packages/block-primitives/src/block-editor/__tests__/rich-text.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { render, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { RichText } from '../rich-text.js';
import * as WrapperModule from '../hooks/useBlockPrimitiveProps.js';
import { jest } from '@jest/globals';

describe('RichText', () => {
let attributes = {};
const setAttributes = jest.fn((newAttributes: Record<string, any>) => {
attributes = { ...attributes, ...newAttributes };
return attributes;
});

jest.spyOn(WrapperModule, 'useBlockPrimitiveProps').mockReturnValue({
let attributes = {};
const setAttributes = jest.fn((newAttributes: Record<string, any>) => {
attributes = { ...attributes, ...newAttributes };
return attributes;
});
jest.unstable_mockModule('../hooks/useBlockPrimitiveProps.js', () => ({
useBlockPrimitiveProps: () => ({
setAttributes,
attributes,
clientId: 'clientId',
isSelected: true,
});
}),
}));

const { RichText } = await import('../rich-text.js');
describe('RichText', () => {
it('supports inline editing', async () => {
const user = userEvent.setup();

Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"schema-dts": "^1.1.2"
},
"peerDependencies": {
"react": ">= 17.0.2 < 19.0.0",
"react-dom": ">= 17.0.2 < 19.0.0"
"react": ">= 17.0.2",
"react-dom": ">= 17.0.2"
}
}
4 changes: 2 additions & 2 deletions packages/epio-search/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@testing-library/jest-dom": "^6.6.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": ">= 18.0.0",
"react-dom": ">= 18.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/next-redis-cache-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@
"@types/ioredis-mock": "^8.2.5"
},
"peerDependencies": {
"next": ">= 13.2.0 < 15.0.0"
"next": ">= 15.0.0"
}
}
195 changes: 194 additions & 1 deletion packages/next-redis-cache-provider/src/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getRedisClient } from '..';
import { CacheHandlerContext } from 'next/dist/server/lib/incremental-cache';
import Redis from 'ioredis';
import RedisCache, { getRedisClient, initRedisClient } from '..';

// eslint-disable-next-line global-require
jest.mock('ioredis', () => require('ioredis-mock'));
Expand Down Expand Up @@ -80,3 +82,194 @@ describe('getRedisClient', () => {
});
});
});

describe('RedisCache', () => {
// Mock Date.now for consistent lastModified values in tests
const mockNow = 1625097600000; // July 1, 2021
const realDateNow = Date.now.bind(global.Date);

beforeEach(() => {
jest.resetModules();
global.Date.now = jest.fn(() => mockNow);

if (globalThis._nextRedisProviderRedisClient) {
delete globalThis._nextRedisProviderRedisClient;
}

const redis = new Redis();
initRedisClient();
return redis.flushall();
});

afterEach(() => {
global.Date.now = realDateNow;
});

const createMockContext = (options = {}): CacheHandlerContext => ({
flushToDisk: true,
serverDistDir: '/path/to/dist',
fs: {
readFile: jest.fn().mockResolvedValue('test-build-id'),
writeFile: jest.fn(),
mkdir: jest.fn(),
stat: jest.fn(),
existsSync: jest.fn(),
readFileSync: jest.fn(),
},
revalidatedTags: [],
_requestHeaders: {},
...options,
});

it('should set and get values correctly', async () => {
const mockContext = createMockContext();
const redisCache = new RedisCache(mockContext);

// Define test data with a structure that matches what the implementation expects
const testKey = 'test-cache-key';
const testData: any = {
kind: 'ROUTE',
data: { html: '<div>Test content</div>' },
revalidate: 60,
};
const testCtx = {};

await redisCache.set(testKey, testData, testCtx);

const result = await redisCache.get(testKey, { revalidate: 60 } as any);

expect(result).toEqual({ lastModified: mockNow, value: testData });

const redis = new Redis();
const storedData = await redis.get('test-build-id:test-cache-key');

expect(JSON.parse(storedData!)).toEqual({ lastModified: mockNow, value: testData });
});

it('should handle get when no data exists', async () => {
const mockContext = createMockContext();
const redisCache = new RedisCache(mockContext);

const result = await redisCache.get('non-existent-key', { revalidate: 60 } as any);

expect(result).toBeNull();
});

it('should not set data when flushToDisk is false', async () => {
const mockContext = createMockContext({ flushToDisk: false });
const redisCache = new RedisCache(mockContext);

const testData: any = {
kind: 'ROUTE',
data: { html: '<div>Test content</div>' },
revalidate: 60,
};

await redisCache.set('test-key', testData, {});

const redis = new Redis();
const storedData = await redis.get('test-build-id:test-key');

expect(storedData).toBeNull();
});

it('should handle revalidation in get method', async () => {
const mockContext = createMockContext();
const redisCache = new RedisCache(mockContext);

// Data that was cached 1 hour ago
const oldTimestamp = mockNow - 3600 * 1000;

// Create cached data
const cachedData: any = {
kind: 'ROUTE',
data: { html: '<div>Stale content</div>' },
revalidate: 60,
};

// Set up the database with stale data and associated tags
const redis = new Redis();
await redis.set(
'test-build-id:test-key',
JSON.stringify({ lastModified: oldTimestamp, value: cachedData }),
);
// Set up tag associations
await redis.sadd('test-build-id:tag:tag1', 'test-key');
await redis.sadd('test-build-id:tag:tag2', 'test-key');

// Get with revalidation context (revalidate after 30 minutes)
const result = await redisCache.get('test-key', {
revalidate: 1800, // 30 minutes
tags: ['tag1', 'tag2'],
} as any);

// It should return the stale data
expect(result).toEqual({
lastModified: oldTimestamp,
value: cachedData,
});

// But the data should be deleted from Redis
const storedData = await redis.get('test-build-id:test-key');
expect(storedData).toBeNull();

// And the tags should no longer have the key
const tag1Members = await redis.smembers('test-build-id:tag:tag1');
const tag2Members = await redis.smembers('test-build-id:tag:tag2');
expect(tag1Members).toEqual([]);
expect(tag2Members).toEqual([]);
});

it('should add tags when setting data with tags context', async () => {
const mockContext = createMockContext();
const redisCache = new RedisCache(mockContext);

// Test data
const testData: any = {
kind: 'ROUTE',
data: { html: '<div>Test content</div>' },
revalidate: 60,
};

// Set with tags
await redisCache.set('test-key', testData, {
tags: ['tag1', 'tag2'],
} as any);

// Verify tags were added to Redis
const redis = new Redis();
const tag1Members = await redis.smembers('test-build-id:tag:tag1');
const tag2Members = await redis.smembers('test-build-id:tag:tag2');

expect(tag1Members).toContain('test-key');
expect(tag2Members).toContain('test-key');
});

it('should revalidate tags correctly', async () => {
const mockContext = createMockContext();
const redisCache = new RedisCache(mockContext);

// Setup some test data with tags
const redis = new Redis();

// Set up keys
await redis.set('test-build-id:key1', 'value1');
await redis.set('test-build-id:key2', 'value2');

// Set up tag with members
await redis.sadd('test-build-id:tag:tag1', 'key1', 'key2');

// Revalidate the tag
await redisCache.revalidateTag('tag1');

// Verify all keys were deleted
const key1Value = await redis.get('test-build-id:key1');
const key2Value = await redis.get('test-build-id:key2');
expect(key1Value).toBeNull();
expect(key2Value).toBeNull();

// Verify tag set was deleted
const tag1Exists = await redis.exists('test-build-id:tag:tag1');
expect(tag1Exists).toBe(0);
});
});
Loading
Loading