Skip to content

Commit 75b1cef

Browse files
styfleijjk
andauthored
fix(cache-tags): add /index to implicit tags and adjust revalidatePath() (#84586)
This fixes an issue where the tag was `/` but the build output was `/index` (and vice-versa) meaning you couldn't use `revalidatePath()` in some cases because the tag didn't match. --------- Co-authored-by: JJ Kasper <[email protected]>
1 parent 316c062 commit 75b1cef

File tree

4 files changed

+117
-11
lines changed

4 files changed

+117
-11
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { OpaqueFallbackRouteParams } from '../request/fallback-params'
2+
import { getImplicitTags } from './implicit-tags'
3+
4+
describe('getImplicitTags()', () => {
5+
it.each<{
6+
page: string
7+
url: { pathname: string; search: string }
8+
fallbackRouteParams: null | OpaqueFallbackRouteParams
9+
expectedTags: string[]
10+
}>([
11+
{
12+
page: '/',
13+
url: { pathname: '/', search: '' },
14+
fallbackRouteParams: null,
15+
expectedTags: ['_N_T_/layout', '_N_T_/', '_N_T_/index'],
16+
},
17+
{
18+
page: '',
19+
url: { pathname: '/', search: '' },
20+
fallbackRouteParams: null,
21+
expectedTags: ['_N_T_/layout', '_N_T_/', '_N_T_/index'],
22+
},
23+
{
24+
page: '/',
25+
url: { pathname: '', search: '' },
26+
fallbackRouteParams: null,
27+
expectedTags: ['_N_T_/layout'],
28+
},
29+
{
30+
page: '/page',
31+
url: { pathname: '', search: '' },
32+
fallbackRouteParams: null,
33+
expectedTags: ['_N_T_/layout', '_N_T_/page'],
34+
},
35+
{
36+
page: '/page',
37+
url: { pathname: '/', search: '' },
38+
fallbackRouteParams: null,
39+
expectedTags: ['_N_T_/layout', '_N_T_/page', '_N_T_/', '_N_T_/index'],
40+
},
41+
{
42+
page: '/page',
43+
url: { pathname: '/page', search: '' },
44+
fallbackRouteParams: null,
45+
expectedTags: ['_N_T_/layout', '_N_T_/page'],
46+
},
47+
{
48+
page: '/index',
49+
url: { pathname: '/', search: '' },
50+
fallbackRouteParams: null,
51+
expectedTags: [
52+
'_N_T_/layout',
53+
'_N_T_/index/layout',
54+
'_N_T_/',
55+
'_N_T_/index',
56+
],
57+
},
58+
{
59+
page: '/hello',
60+
url: { pathname: '/hello', search: '' },
61+
fallbackRouteParams: null,
62+
expectedTags: ['_N_T_/layout', '_N_T_/hello/layout', '_N_T_/hello'],
63+
},
64+
{
65+
page: '/foo/bar/baz',
66+
url: { pathname: '/foo/bar/baz', search: '' },
67+
fallbackRouteParams: null,
68+
expectedTags: [
69+
'_N_T_/layout',
70+
'_N_T_/foo/layout',
71+
'_N_T_/foo/bar/layout',
72+
'_N_T_/foo/bar/baz/layout',
73+
'_N_T_/foo/bar/baz',
74+
],
75+
},
76+
])(
77+
'for page $page with url $url and $fallback',
78+
async ({ page, url, fallbackRouteParams, expectedTags }) => {
79+
const result = await getImplicitTags(page, url, fallbackRouteParams)
80+
expect(result.tags).toEqual(expectedTags)
81+
}
82+
)
83+
})

packages/next/src/server/lib/implicit-tags.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ export async function getImplicitTags(
7979
},
8080
fallbackRouteParams: null | OpaqueFallbackRouteParams
8181
): Promise<ImplicitTags> {
82-
const tags: string[] = []
82+
const tags = new Set<string>()
8383

8484
// Add the derived tags from the page.
8585
const derivedTags = getDerivedTags(page)
8686
for (let tag of derivedTags) {
8787
tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}`
88-
tags.push(tag)
88+
tags.add(tag)
8989
}
9090

9191
// Add the tags from the pathname. If the route has unknown params, we don't
@@ -95,11 +95,20 @@ export async function getImplicitTags(
9595
(!fallbackRouteParams || fallbackRouteParams.size === 0)
9696
) {
9797
const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${url.pathname}`
98-
tags.push(tag)
98+
tags.add(tag)
9999
}
100100

101+
if (tags.has(`${NEXT_CACHE_IMPLICIT_TAG_ID}/`)) {
102+
tags.add(`${NEXT_CACHE_IMPLICIT_TAG_ID}/index`)
103+
}
104+
105+
if (tags.has(`${NEXT_CACHE_IMPLICIT_TAG_ID}/index`)) {
106+
tags.add(`${NEXT_CACHE_IMPLICIT_TAG_ID}/`)
107+
}
108+
109+
const tagsArray = Array.from(tags)
101110
return {
102-
tags,
103-
expirationsByCacheKind: createTagsExpirationsByCacheKind(tags),
111+
tags: tagsArray,
112+
expirationsByCacheKind: createTagsExpirationsByCacheKind(tagsArray),
104113
}
105114
}

packages/next/src/server/web/spec-extension/revalidate.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function unstable_expirePath(
3737
return
3838
}
3939

40-
let normalizedPath = `${NEXT_CACHE_IMPLICIT_TAG_ID}${originalPath}`
40+
let normalizedPath = `${NEXT_CACHE_IMPLICIT_TAG_ID}${originalPath || '/'}`
4141

4242
if (type) {
4343
normalizedPath += `${normalizedPath.endsWith('/') ? '' : '/'}${type}`
@@ -46,7 +46,13 @@ export function unstable_expirePath(
4646
`Warning: a dynamic page path "${originalPath}" was passed to "expirePath", but the "type" parameter is missing. This has no effect by default, see more info here https://nextjs.org/docs/app/api-reference/functions/unstable_expirePath`
4747
)
4848
}
49-
return revalidate([normalizedPath], `unstable_expirePath ${originalPath}`)
49+
const tags = [normalizedPath]
50+
if (normalizedPath === `${NEXT_CACHE_IMPLICIT_TAG_ID}/`) {
51+
tags.push(`${NEXT_CACHE_IMPLICIT_TAG_ID}/index`)
52+
} else if (normalizedPath === `${NEXT_CACHE_IMPLICIT_TAG_ID}/index`) {
53+
tags.push(`${NEXT_CACHE_IMPLICIT_TAG_ID}/`)
54+
}
55+
return revalidate(tags, `unstable_expirePath ${originalPath}`)
5056
}
5157

5258
/**
@@ -71,7 +77,7 @@ export function revalidatePath(originalPath: string, type?: 'layout' | 'page') {
7177
return
7278
}
7379

74-
let normalizedPath = `${NEXT_CACHE_IMPLICIT_TAG_ID}${originalPath}`
80+
let normalizedPath = `${NEXT_CACHE_IMPLICIT_TAG_ID}${originalPath || '/'}`
7581

7682
if (type) {
7783
normalizedPath += `${normalizedPath.endsWith('/') ? '' : '/'}${type}`
@@ -80,7 +86,15 @@ export function revalidatePath(originalPath: string, type?: 'layout' | 'page') {
8086
`Warning: a dynamic page path "${originalPath}" was passed to "revalidatePath", but the "type" parameter is missing. This has no effect by default, see more info here https://nextjs.org/docs/app/api-reference/functions/revalidatePath`
8187
)
8288
}
83-
return revalidate([normalizedPath], `revalidatePath ${originalPath}`)
89+
90+
const tags = [normalizedPath]
91+
if (normalizedPath === `${NEXT_CACHE_IMPLICIT_TAG_ID}/`) {
92+
tags.push(`${NEXT_CACHE_IMPLICIT_TAG_ID}/index`)
93+
} else if (normalizedPath === `${NEXT_CACHE_IMPLICIT_TAG_ID}/index`) {
94+
tags.push(`${NEXT_CACHE_IMPLICIT_TAG_ID}/`)
95+
}
96+
97+
return revalidate(tags, `revalidatePath ${originalPath}`)
8498
}
8599

86100
function revalidate(tags: string[], expression: string) {

test/e2e/app-dir/use-cache-custom-handler/use-cache-custom-handler.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('use-cache-custom-handler', () => {
2828
expect(cliOutput).toContain('ModernCustomCacheHandler::refreshTags')
2929

3030
expect(next.cliOutput.slice(outputIndex)).toMatch(
31-
/ModernCustomCacheHandler::get \["(development|[A-Za-z0-9_-]{21})","([0-9a-f]{2})+",\[\]\] \[ '_N_T_\/layout', '_N_T_\/page', '_N_T_\/' \]/
31+
/ModernCustomCacheHandler::get \["(development|[A-Za-z0-9_-]{21})","([0-9a-f]{2})+",\[\]\] \[ '_N_T_\/layout', '_N_T_\/page', '_N_T_\/', '_N_T_\/index' \]/
3232
)
3333

3434
expect(next.cliOutput.slice(outputIndex)).toMatch(
@@ -52,7 +52,7 @@ describe('use-cache-custom-handler', () => {
5252
// to compare the cache entries timestamp with the expiration of the
5353
// implicit tags.
5454
expect(next.cliOutput.slice(outputIndex)).toContain(
55-
`ModernCustomCacheHandler::getExpiration ["_N_T_/layout","_N_T_/page","_N_T_/"]`
55+
`ModernCustomCacheHandler::getExpiration ["_N_T_/layout","_N_T_/page","_N_T_/","_N_T_/index"]`
5656
)
5757

5858
// Because we use a low `revalidate` value for the "use cache" function, new

0 commit comments

Comments
 (0)