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
61 changes: 46 additions & 15 deletions packages/next/cache.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
const cacheExports = {
unstable_cache: require('next/dist/server/web/spec-extension/unstable-cache')
.unstable_cache,
let cacheExports

updateTag: require('next/dist/server/web/spec-extension/revalidate')
.updateTag,
if (process.env.NEXT_RUNTIME === '') {
const notAvailableInClient = (name) => {
return function notAvailable() {
throw new Error(`\`${name}\` is only available in a Server Component.`)
}
}

cacheExports = {
unstable_cache: function unstable_cache(cb) {
// Legacy behavior: allow importing/using unstable_cache from client bundles
// without pulling in server internals.
if (typeof cb !== 'function') return cb
return function cached() {
return cb.apply(this, arguments)
}
},
unstable_noStore: function unstable_noStore() {},

updateTag: notAvailableInClient('updateTag'),
revalidateTag: notAvailableInClient('revalidateTag'),
revalidatePath: notAvailableInClient('revalidatePath'),
refresh: notAvailableInClient('refresh'),
cacheLife: notAvailableInClient('cacheLife'),
cacheTag: notAvailableInClient('cacheTag'),
}
} else {
// Keep server requires in this branch so browser builds can DCE them.
cacheExports = {
unstable_cache:
require('next/dist/server/web/spec-extension/unstable-cache')
.unstable_cache,

updateTag: require('next/dist/server/web/spec-extension/revalidate')
.updateTag,

revalidateTag: require('next/dist/server/web/spec-extension/revalidate')
.revalidateTag,
revalidatePath: require('next/dist/server/web/spec-extension/revalidate')
.revalidatePath,
revalidateTag: require('next/dist/server/web/spec-extension/revalidate')
.revalidateTag,
revalidatePath: require('next/dist/server/web/spec-extension/revalidate')
.revalidatePath,

refresh: require('next/dist/server/web/spec-extension/revalidate').refresh,
refresh: require('next/dist/server/web/spec-extension/revalidate').refresh,

unstable_noStore:
require('next/dist/server/web/spec-extension/unstable-no-store')
.unstable_noStore,
cacheLife: require('next/dist/server/use-cache/cache-life').cacheLife,
cacheTag: require('next/dist/server/use-cache/cache-tag').cacheTag,
unstable_noStore:
require('next/dist/server/web/spec-extension/unstable-no-store')
.unstable_noStore,
cacheLife: require('next/dist/server/use-cache/cache-life').cacheLife,
cacheTag: require('next/dist/server/use-cache/cache-tag').cacheTag,
}
}

let didWarnCacheLife = false
Expand Down
14 changes: 14 additions & 0 deletions test/production/app-dir/browser-chunks/app/cache-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import { unstable_cache } from 'next/cache'

// Importing next/cache in a Client Component should not pull server internals
// into browser chunks. The bundler sets NEXT_RUNTIME='' for client builds,
// which allows cache.js to DCE the server require() branch.
const getCachedData = unstable_cache(async () => {
return { data: 'hello' }
})

export function CacheClient() {
return <button onClick={() => getCachedData()}>Fetch cached</button>
}
9 changes: 8 additions & 1 deletion test/production/app-dir/browser-chunks/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { CacheClient } from './cache-client'

export default function Page() {
return <p>hello world</p>
return (
<div>
<p>hello world</p>
<CacheClient />
</div>
)
}
59 changes: 56 additions & 3 deletions test/production/app-dir/browser-chunks/browser-chunks.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
import { nextTestSetup } from 'e2e-utils'
import { join } from 'path'
import { readdir, readFile } from 'fs/promises'

async function readFilesRecursive(
dir: string,
predicate: (filename: string) => boolean
): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true })
const results: string[] = []

for (const entry of entries) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
results.push(...(await readFilesRecursive(fullPath, predicate)))
} else if (predicate(entry.name)) {
results.push(await readFile(fullPath, 'utf8'))
}
}

return results
}

describe('browser-chunks', () => {
const { next } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})

let sources = []
let sources: string[] = []
let jsContents: string[] = []
beforeAll(async () => {
const sourcemaps = await next.readFiles('.next/static/chunks', (filename) =>
const chunksDir = join(next.testDir, '.next/static/chunks')

const sourcemaps = await readFilesRecursive(chunksDir, (filename) =>
filename.endsWith('.js.map')
)

sources = sourcemaps.flatMap((sourcemap) => JSON.parse(sourcemap).sources)

jsContents = await readFilesRecursive(chunksDir, (filename) =>
filename.endsWith('.js')
)
})

it('must not bundle any server modules into browser chunks', () => {
const serverSources = sources.filter(
(source) =>
Expand Down Expand Up @@ -62,4 +90,29 @@ describe('browser-chunks', () => {
)
}
})

it('must not pull server internals from next/cache into browser chunks', () => {
// When a Client Component imports from next/cache, the bundler should
// DCE the server require() branch (via process.env.NEXT_RUNTIME === '')
// and only include lightweight client stubs. Pre-compiled dist/ modules
// don't appear in sourcemaps, so we check the actual JS content.
const serverOnlyPatterns = [
// IncrementalCache is a class from next/dist/server used by unstable_cache
'IncrementalCache',
]

for (const pattern of serverOnlyPatterns) {
const chunksWithPattern = jsContents.filter((content) =>
content.includes(pattern)
)

if (chunksWithPattern.length > 0) {
throw new Error(
`Found server-only pattern "${pattern}" in ${chunksWithPattern.length} browser chunk(s). ` +
`This likely means next/cache is pulling server internals into the client bundle. ` +
`Ensure the server require() calls in packages/next/cache.js are behind a DCE-able branch.`
)
}
}
})
})
Loading