Skip to content

Commit

Permalink
fix: compiler error when next/cache is used in a client module (verce…
Browse files Browse the repository at this point in the history
…l#75979)

Most functions in `next/cache` don't work on the client, and produce
confusing error messages when called there. It's better to just ban them
at compile time.

NOTE: For legacy/compat reasons, we're allowing `unstable_cache` and
`unstable_noStore`, since they don't currently throw when called.
  • Loading branch information
lubieowoce authored Feb 12, 2025
1 parent 319a338 commit eac707f
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,22 @@ impl ReactServerComponentValidator {

invalid_client_imports: vec![Atom::from("server-only"), Atom::from("next/headers")],

invalid_client_lib_apis_mapping: FxHashMap::from_iter([("next/server", vec!["after"])]),
invalid_client_lib_apis_mapping: FxHashMap::from_iter([
("next/server", vec!["after"]),
(
"next/cache",
vec![
"revalidatePath",
"revalidateTag",
// "unstable_cache", // useless in client, but doesn't technically error
"unstable_cacheLife",
"unstable_cacheTag",
"unstable_expirePath",
"unstable_expireTag",
// "unstable_noStore" // no-op in client, but allowed for legacy reasons
],
),
]),
imports: ImportMap::default(),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { revalidatePath } from 'next/cache'

console.log({ revalidatePath })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { revalidateTag } from 'next/cache'

console.log({ revalidateTag })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { unstable_cache } from 'next/cache'

console.log({ unstable_cache })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { unstable_cacheLife } from 'next/cache'

console.log({ unstable_cacheLife })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { unstable_cacheTag } from 'next/cache'

console.log({ unstable_cacheTag })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { unstable_expirePath } from 'next/cache'

console.log({ unstable_expirePath })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { unstable_expireTag } from 'next/cache'

console.log({ unstable_expireTag })

export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client'
import { unstable_noStore } from 'next/cache'

console.log({ unstable_noStore })

export default function Page() {
return null
}
35 changes: 35 additions & 0 deletions test/development/acceptance-app/rsc-build-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,41 @@ describe('Error overlay - RSC build errors', () => {
)
})

describe("importing 'next/cache' APIs in a client component", () => {
test.each([
'revalidatePath',
'revalidateTag',
'unstable_cacheLife',
'unstable_cacheTag',
'unstable_expirePath',
'unstable_expireTag',
])('%s is not allowed', async (api) => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/next-cache-in-client/${api.toLowerCase()}`
)
const { session } = sandbox
await session.assertHasRedbox()
expect(await session.getRedboxSource()).toInclude(
`You're importing a component that needs "${api}". That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.`
)
})

test.each([
'unstable_cache', // useless in client, but doesn't technically error
'unstable_noStore', // no-op in client, but allowed for legacy reasons
])('%s is allowed', async (api) => {
await using sandbox = await createSandbox(
next,
undefined,
`/server-with-errors/next-cache-in-client/${api.toLowerCase()}`
)
const { session } = sandbox
await session.assertNoRedbox()
})
})

it('should error for invalid undefined module retuning from next dynamic', async () => {
await using sandbox = await createSandbox(
next,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,57 @@ describe('Error Overlay for server components compiler errors in pages', () => {
`)
}
})

describe("importing 'next/cache' APIs in pages", () => {
test.each([
'revalidatePath',
'revalidateTag',
'unstable_cacheLife',
'unstable_cacheTag',
'unstable_expirePath',
'unstable_expireTag',
])('%s is not allowed', async (api) => {
await using sandbox = await createSandbox(next, initialFiles)
const { session } = sandbox

await next.patchFile(
'components/Comp.js',
outdent`
import { ${api} } from 'next/cache'
export default function Page() {
return 'hello world'
}
`
)

await session.assertHasRedbox()
await expect(session.getRedboxSource()).resolves.toMatch(
`You're importing a component that needs "${api}". That only works in a Server Component which is not supported in the pages/ directory.`
)
})

test.each([
'unstable_cache', // useless in client, but doesn't technically error
'unstable_noStore', // no-op in client, but allowed for legacy reasons
])('%s is allowed', async (api) => {
await using sandbox = await createSandbox(next, initialFiles)
const { session } = sandbox

await next.patchFile(
'components/Comp.js',
outdent`
import { ${api} } from 'next/cache'
export default function Page() {
return 'hello world'
}
`
)

await session.assertNoRedbox()
})
})
})

const takeUpToString = (text: string, str: string): string =>
Expand Down

0 comments on commit eac707f

Please sign in to comment.