first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import { ReactNode } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,34 @@
import Image from 'next/image'
export default function Page() {
return (
<div>
<p>hello world</p>
{/* Each unique width/quality combination creates a separate cache entry */}
<Image
id="image-small"
src="/test.png"
width={100}
height={100}
quality={75}
alt="small image"
/>
<Image
id="image-medium"
src="/test.png"
width={200}
height={200}
quality={75}
alt="medium image"
/>
<Image
id="image-large"
src="/test.png"
width={400}
height={400}
quality={75}
alt="large image"
/>
</div>
)
}

View File

@@ -0,0 +1,105 @@
/**
* Simple LRU cache handler with max entries eviction policy.
* When the cache exceeds maxEntries, the least recently used entries are evicted.
*/
const MAX_IMAGE_ENTRIES = parseInt(
process.env.MAX_IMAGE_CACHE_ENTRIES || '2',
10
)
class LRUCache {
constructor(maxEntries) {
this.maxEntries = maxEntries
this.cache = new Map()
}
get(key) {
if (!this.cache.has(key)) {
return undefined
}
// Move to end (most recently used)
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
set(key, value) {
// If key exists, delete it first (will be re-added at end)
if (this.cache.has(key)) {
this.cache.delete(key)
}
// Evict oldest entries if at capacity
while (this.cache.size >= this.maxEntries) {
const oldestKey = this.cache.keys().next().value
console.log('cache-handler evicting', oldestKey)
this.cache.delete(oldestKey)
}
this.cache.set(key, value)
}
has(key) {
return this.cache.has(key)
}
get size() {
return this.cache.size
}
keys() {
return Array.from(this.cache.keys())
}
}
// Separate caches for different kinds
const imageCache = new LRUCache(MAX_IMAGE_ENTRIES)
const pageCache = new LRUCache(100) // Higher limit for pages
class CacheHandler {
constructor(options) {
this.options = options
console.log('initialized custom cache-handler')
console.log('max image cache entries:', MAX_IMAGE_ENTRIES)
}
async get(key, ctx) {
const kind = ctx?.kind
console.log('cache-handler get', key, 'kind:', kind)
const cache = kind === 'IMAGE' ? imageCache : pageCache
const entry = cache.get(key)
if (entry) {
console.log('cache-handler hit', key)
return entry
}
console.log('cache-handler miss', key)
return null
}
async set(key, data, ctx) {
const kind = data?.kind
console.log('cache-handler set', key, 'kind:', kind)
const cache = kind === 'IMAGE' ? imageCache : pageCache
cache.set(key, {
value: data,
lastModified: Date.now(),
})
if (kind === 'IMAGE') {
console.log('cache-handler image cache size:', imageCache.size)
console.log(
'cache-handler image cache keys:',
imageCache.keys().join(', ')
)
}
}
async revalidateTag(tags) {
console.log('cache-handler revalidateTag', tags)
}
}
module.exports = CacheHandler

View File

@@ -0,0 +1,101 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
describe('custom-cache-handler-image', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
env: {
// Set max cache entries to 2 to easily test eviction
MAX_IMAGE_CACHE_ENTRIES: '2',
},
})
if (skipped) {
return
}
it('should use custom cache handler for image optimization', async () => {
// First, render the page to get the image URLs
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
const smallImgSrc = $('#image-small').attr('src')
expect(smallImgSrc).toContain('/_next/image')
// Fetch the optimized image to trigger cache handler
const imageRes = await next.fetch(smallImgSrc)
expect(imageRes.status).toBe(200)
// Verify cache handler was called for the image
await retry(() => {
expect(next.cliOutput).toContain('initialized custom cache-handler')
expect(next.cliOutput).toContain('cache-handler set')
expect(next.cliOutput).toMatch(/kind:.*IMAGE/)
})
})
it('should evict oldest entries when cache exceeds max size', async () => {
const $ = await next.render$('/')
const smallImgSrc = $('#image-small').attr('src')
const mediumImgSrc = $('#image-medium').attr('src')
const largeImgSrc = $('#image-large').attr('src')
// Request all three images sequentially
// With MAX_IMAGE_CACHE_ENTRIES=2, the first image should be evicted
// when the third one is added
// Request image 1 (small)
await next.fetch(smallImgSrc)
await retry(() => {
expect(next.cliOutput).toContain('cache-handler image cache size: 1')
})
// Request image 2 (medium)
await next.fetch(mediumImgSrc)
await retry(() => {
expect(next.cliOutput).toContain('cache-handler image cache size: 2')
})
// Request image 3 (large) - this should trigger eviction of image 1
await next.fetch(largeImgSrc)
await retry(() => {
expect(next.cliOutput).toContain('cache-handler evicting')
// Cache size should still be 2 after eviction
const sizeMatches = next.cliOutput.match(
/cache-handler image cache size: (\d+)/g
)
const lastSize = sizeMatches?.[sizeMatches.length - 1]
expect(lastSize).toContain('size: 2')
})
})
it('should miss cache for evicted entries', async () => {
const $ = await next.render$('/')
const smallImgSrc = $('#image-small').attr('src')
const mediumImgSrc = $('#image-medium').attr('src')
const largeImgSrc = $('#image-large').attr('src')
// Fill the cache and cause eviction
await next.fetch(smallImgSrc) // Entry 1
await next.fetch(mediumImgSrc) // Entry 2
await next.fetch(largeImgSrc) // Entry 3, evicts entry 1
// Clear output to make assertions cleaner
const outputBefore = next.cliOutput
// Request the evicted image again - should be a cache miss
await next.fetch(smallImgSrc)
await retry(() => {
// Get output after the last request
const newOutput = next.cliOutput.slice(outputBefore.length)
// Should have a cache miss for the small image (it was evicted)
expect(newOutput).toContain('cache-handler miss')
// Should set it again
expect(newOutput).toContain('cache-handler set')
})
})
})

View File

@@ -0,0 +1,12 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
cacheHandler: process.cwd() + '/cache-handler.js',
images: {
imageSizes: [100, 200, 400],
customCacheHandler: true,
},
}
module.exports = nextConfig

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB