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
15 changes: 15 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,21 @@ export async function buildAppStaticPaths({
const newParams: Params[] = []

if (curGenerate.generateStaticParams) {
const curStore =
ComponentMod.staticGenerationAsyncStorage.getStore()

if (curStore) {
if (typeof curGenerate?.config?.fetchCache !== 'undefined') {
curStore.fetchCache = curGenerate.config.fetchCache
}
if (typeof curGenerate?.config?.revalidate !== 'undefined') {
curStore.revalidate = curGenerate.config.revalidate
}
if (curGenerate?.config?.dynamic === 'force-dynamic') {
curStore.forceDynamic = true
}
}

for (const params of paramsItems) {
const result = await curGenerate.generateStaticParams({
params,
Expand Down
127 changes: 72 additions & 55 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ function createPatchedFetcher(
return value || (isRequestInput ? (input as any)[field] : null)
}

let revalidate: number | undefined | false = undefined
let finalRevalidate: number | undefined | false = undefined
const getNextField = (field: 'revalidate' | 'tags') => {
return typeof init?.next?.[field] !== 'undefined'
? init?.next?.[field]
Expand All @@ -299,7 +299,7 @@ function createPatchedFetcher(
}
// RequestInit doesn't keep extra fields e.g. next so it's
// only available if init is used separate
let curRevalidate = getNextField('revalidate')
let currentFetchRevalidate = getNextField('revalidate')
const tags: string[] = validateTags(
getNextField('tags') || [],
`fetch ${input.toString()}`
Expand All @@ -317,49 +317,52 @@ function createPatchedFetcher(
}
const implicitTags = addImplicitTags(staticGenerationStore)

const fetchCacheMode = staticGenerationStore.fetchCache
const pageFetchCacheMode = staticGenerationStore.fetchCache
const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore

let _cache = getRequestMeta('cache')
let currentFetchCacheConfig = getRequestMeta('cache')
let cacheReason = ''

if (
typeof _cache === 'string' &&
typeof curRevalidate !== 'undefined'
typeof currentFetchCacheConfig === 'string' &&
typeof currentFetchRevalidate !== 'undefined'
) {
// when providing fetch with a Request input, it'll automatically set a cache value of 'default'
// we only want to warn if the user is explicitly setting a cache value
if (!(isRequestInput && _cache === 'default')) {
if (!(isRequestInput && currentFetchCacheConfig === 'default')) {
Log.warn(
`fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${_cache}" and "revalidate: ${curRevalidate}", only one should be specified.`
`fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.`
)
}
_cache = undefined
currentFetchCacheConfig = undefined
}

if (_cache === 'force-cache') {
curRevalidate = false
if (currentFetchCacheConfig === 'force-cache') {
currentFetchRevalidate = false
} else if (
_cache === 'no-cache' ||
_cache === 'no-store' ||
fetchCacheMode === 'force-no-store' ||
fetchCacheMode === 'only-no-store' ||
currentFetchCacheConfig === 'no-cache' ||
currentFetchCacheConfig === 'no-store' ||
pageFetchCacheMode === 'force-no-store' ||
pageFetchCacheMode === 'only-no-store' ||
// If no explicit fetch cache mode is set, but dynamic = `force-dynamic` is set,
// we shouldn't consider caching the fetch. This is because the `dynamic` cache
// is considered a "top-level" cache mode, whereas something like `fetchCache` is more
// fine-grained. Top-level modes are responsible for setting reasonable defaults for the
// other configurations.
(!fetchCacheMode && staticGenerationStore.forceDynamic)
(!pageFetchCacheMode && staticGenerationStore.forceDynamic)
) {
curRevalidate = 0
currentFetchRevalidate = 0
}

if (_cache === 'no-cache' || _cache === 'no-store') {
cacheReason = `cache: ${_cache}`
if (
currentFetchCacheConfig === 'no-cache' ||
currentFetchCacheConfig === 'no-store'
) {
cacheReason = `cache: ${currentFetchCacheConfig}`
}

revalidate = validateRevalidate(
curRevalidate,
finalRevalidate = validateRevalidate(
currentFetchRevalidate,
staticGenerationStore.urlPathname
)

Expand All @@ -379,20 +382,29 @@ function createPatchedFetcher(
// if there are authorized headers or a POST method and
// dynamic data usage was present above the tree we bail
// e.g. if cookies() is used before an authed/POST fetch
// or no user provided fetch cache config or revalidate
// is provided we don't cache
const autoNoCache =
(hasUnCacheableHeader || isUnCacheableMethod) &&
staticGenerationStore.revalidate === 0

switch (fetchCacheMode) {
// this condition is hit for null/undefined
// eslint-disable-next-line eqeqeq
(pageFetchCacheMode == undefined &&
// eslint-disable-next-line eqeqeq
currentFetchCacheConfig == undefined &&
// eslint-disable-next-line eqeqeq
currentFetchRevalidate == undefined) ||
((hasUnCacheableHeader || isUnCacheableMethod) &&
staticGenerationStore.revalidate === 0)

switch (pageFetchCacheMode) {
case 'force-no-store': {
cacheReason = 'fetchCache = force-no-store'
break
}
case 'only-no-store': {
if (
_cache === 'force-cache' ||
(typeof revalidate !== 'undefined' &&
(revalidate === false || revalidate > 0))
currentFetchCacheConfig === 'force-cache' ||
(typeof finalRevalidate !== 'undefined' &&
(finalRevalidate === false || finalRevalidate > 0))
) {
throw new Error(
`cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'`
Expand All @@ -402,17 +414,20 @@ function createPatchedFetcher(
break
}
case 'only-cache': {
if (_cache === 'no-store') {
if (currentFetchCacheConfig === 'no-store') {
throw new Error(
`cache: 'no-store' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-cache'`
)
}
break
}
case 'force-cache': {
if (typeof curRevalidate === 'undefined' || curRevalidate === 0) {
if (
typeof currentFetchRevalidate === 'undefined' ||
currentFetchRevalidate === 0
) {
cacheReason = 'fetchCache = force-cache'
revalidate = false
finalRevalidate = false
}
break
}
Expand All @@ -423,59 +438,59 @@ function createPatchedFetcher(
// simplify the switch case and ensure we have an exhaustive switch handling all modes
}

if (typeof revalidate === 'undefined') {
if (fetchCacheMode === 'default-cache') {
revalidate = false
if (typeof finalRevalidate === 'undefined') {
if (pageFetchCacheMode === 'default-cache' && !isUsingNoStore) {
finalRevalidate = false
cacheReason = 'fetchCache = default-cache'
} else if (autoNoCache) {
revalidate = 0
cacheReason = 'auto no cache'
} else if (fetchCacheMode === 'default-no-store') {
revalidate = 0
} else if (pageFetchCacheMode === 'default-no-store') {
finalRevalidate = 0
cacheReason = 'fetchCache = default-no-store'
} else if (isUsingNoStore) {
revalidate = 0
finalRevalidate = 0
cacheReason = 'noStore call'
} else if (autoNoCache) {
finalRevalidate = 0
cacheReason = 'auto no cache'
} else {
// TODO: should we consider this case an invariant?
cacheReason = 'auto cache'
revalidate =
finalRevalidate =
typeof staticGenerationStore.revalidate === 'boolean' ||
typeof staticGenerationStore.revalidate === 'undefined'
? false
: staticGenerationStore.revalidate
}
} else if (!cacheReason) {
cacheReason = `revalidate: ${revalidate}`
cacheReason = `revalidate: ${finalRevalidate}`
}

if (
// when force static is configured we don't bail from
// `revalidate: 0` values
!(staticGenerationStore.forceStatic && revalidate === 0) &&
// we don't consider autoNoCache to switch to dynamic during
// revalidate although if it occurs during build we do
!(staticGenerationStore.forceStatic && finalRevalidate === 0) &&
// we don't consider autoNoCache to switch to dynamic for ISR
!autoNoCache &&
// If the revalidate value isn't currently set or the value is less
// than the current revalidate value, we should update the revalidate
// value.
(typeof staticGenerationStore.revalidate === 'undefined' ||
(typeof revalidate === 'number' &&
(typeof finalRevalidate === 'number' &&
(staticGenerationStore.revalidate === false ||
(typeof staticGenerationStore.revalidate === 'number' &&
revalidate < staticGenerationStore.revalidate))))
finalRevalidate < staticGenerationStore.revalidate))))
) {
// If we were setting the revalidate value to 0, we should try to
// postpone instead first.
if (revalidate === 0) {
if (finalRevalidate === 0) {
trackDynamicFetch(staticGenerationStore, 'revalidate: 0')
}

staticGenerationStore.revalidate = revalidate
staticGenerationStore.revalidate = finalRevalidate
}

const isCacheableRevalidate =
(typeof revalidate === 'number' && revalidate > 0) ||
revalidate === false
(typeof finalRevalidate === 'number' && finalRevalidate > 0) ||
finalRevalidate === false

let cacheKey: string | undefined
if (staticGenerationStore.incrementalCache && isCacheableRevalidate) {
Expand All @@ -494,7 +509,7 @@ function createPatchedFetcher(
staticGenerationStore.nextFetchId = fetchIdx + 1

const normalizedRevalidate =
typeof revalidate !== 'number' ? CACHE_ONE_YEAR : revalidate
typeof finalRevalidate !== 'number' ? CACHE_ONE_YEAR : finalRevalidate

const doOriginalFetch = async (
isStale?: boolean,
Expand Down Expand Up @@ -552,7 +567,9 @@ function createPatchedFetcher(
url: fetchUrl,
cacheReason: cacheReasonOverride || cacheReason,
cacheStatus:
revalidate === 0 || cacheReasonOverride ? 'skip' : 'miss',
finalRevalidate === 0 || cacheReasonOverride
? 'skip'
: 'miss',
status: res.status,
method: clonedInit.method || 'GET',
})
Expand Down Expand Up @@ -580,7 +597,7 @@ function createPatchedFetcher(
},
{
fetchCache: true,
revalidate,
revalidate: finalRevalidate,
fetchUrl,
fetchIdx,
tags,
Expand Down Expand Up @@ -613,7 +630,7 @@ function createPatchedFetcher(
? null
: await staticGenerationStore.incrementalCache.get(cacheKey, {
kindHint: 'fetch',
revalidate,
revalidate: finalRevalidate,
fetchUrl,
fetchIdx,
tags,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-custom-cache-handler/app/layout.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const fetchCache = 'default-cache'

export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/app-dir/app-fetch-deduping/app/layout.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const fetchCache = 'default-cache'

export default function Layout({ children }) {
return (
<html lang="en">
Expand Down
Loading