Update revalidate handling for app (#49062)
This updates revalidate handling for app and moves some exports from `next/server` to `next/cache` x-ref: [slack thread](https://vercel.slack.com/archives/C042LHPJ1NX/p1682535961644979?thread_ts=1682368724.384619&cid=C042LHPJ1NX) ---------
This commit is contained in:
parent
02c5b5f6d6
commit
abc74fb92e
38 changed files with 592 additions and 63 deletions
3
packages/next/cache.d.ts
vendored
Normal file
3
packages/next/cache.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
|
||||
export { unstable_revalidatePath } from 'next/dist/server/web/spec-extension/unstable-revalidate-path'
|
||||
export { unstable_revalidateTag } from 'next/dist/server/web/spec-extension/unstable-revalidate-tag'
|
19
packages/next/cache.js
Normal file
19
packages/next/cache.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
const cacheExports = {
|
||||
unstable_cache: require('next/dist/server/web/spec-extension/unstable-cache')
|
||||
.unstable_cache,
|
||||
unstable_revalidateTag:
|
||||
require('next/dist/server/web/spec-extension/unstable-revalidate-tag')
|
||||
.unstable_revalidateTag,
|
||||
unstable_revalidatePath:
|
||||
require('next/dist/server/web/spec-extension/unstable-revalidate-path')
|
||||
.unstable_revalidatePath,
|
||||
}
|
||||
|
||||
// https://nodejs.org/api/esm.html#commonjs-namespaces
|
||||
// When importing CommonJS modules, the module.exports object is provided as the default export
|
||||
module.exports = cacheExports
|
||||
|
||||
// make import { xxx } from 'next/server' work
|
||||
exports.unstable_cache = cacheExports.unstable_cache
|
||||
exports.unstable_revalidatePath = cacheExports.unstable_revalidatePath
|
||||
exports.unstable_revalidateTag = cacheExports.unstable_revalidateTag
|
1
packages/next/index.d.ts
vendored
1
packages/next/index.d.ts
vendored
|
@ -2,6 +2,7 @@
|
|||
/// <reference path="./dist/styled-jsx/types/global.d.ts" />
|
||||
/// <reference path="./amp.d.ts" />
|
||||
/// <reference path="./app.d.ts" />
|
||||
/// <reference path="./cache.d.ts" />
|
||||
/// <reference path="./config.d.ts" />
|
||||
/// <reference path="./document.d.ts" />
|
||||
/// <reference path="./dynamic.d.ts" />
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
"client.js",
|
||||
"client.d.ts",
|
||||
"compat",
|
||||
"cache.js",
|
||||
"cache.d.ts",
|
||||
"config.js",
|
||||
"config.d.ts",
|
||||
"constants.js",
|
||||
|
|
3
packages/next/server.d.ts
vendored
3
packages/next/server.d.ts
vendored
|
@ -13,6 +13,3 @@ export { userAgent } from 'next/dist/server/web/spec-extension/user-agent'
|
|||
export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
|
||||
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
|
||||
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
|
||||
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
|
||||
export { unstable_revalidatePath } from 'next/dist/server/web/spec-extension/unstable-revalidate-path'
|
||||
export { unstable_revalidateTag } from 'next/dist/server/web/spec-extension/unstable-revalidate-tag'
|
||||
|
|
|
@ -5,14 +5,6 @@ const serverExports = {
|
|||
.NextResponse,
|
||||
ImageResponse: require('next/dist/server/web/spec-extension/image-response')
|
||||
.ImageResponse,
|
||||
unstable_cache: require('next/dist/server/web/spec-extension/unstable-cache')
|
||||
.unstable_cache,
|
||||
unstable_revalidateTag:
|
||||
require('next/dist/server/web/spec-extension/unstable-revalidate-tag')
|
||||
.unstable_revalidateTag,
|
||||
unstable_revalidatePath:
|
||||
require('next/dist/server/web/spec-extension/unstable-revalidate-path')
|
||||
.unstable_revalidatePath,
|
||||
userAgentFromString: require('next/dist/server/web/spec-extension/user-agent')
|
||||
.userAgentFromString,
|
||||
userAgent: require('next/dist/server/web/spec-extension/user-agent')
|
||||
|
@ -32,9 +24,6 @@ module.exports = serverExports
|
|||
exports.NextRequest = serverExports.NextRequest
|
||||
exports.NextResponse = serverExports.NextResponse
|
||||
exports.ImageResponse = serverExports.ImageResponse
|
||||
exports.unstable_cache = serverExports.unstable_cache
|
||||
exports.unstable_revalidatePath = serverExports.unstable_revalidatePath
|
||||
exports.unstable_revalidateTag = serverExports.unstable_revalidateTag
|
||||
exports.userAgentFromString = serverExports.userAgentFromString
|
||||
exports.userAgent = serverExports.userAgent
|
||||
exports.URLPattern = serverExports.URLPattern
|
||||
|
|
|
@ -2377,16 +2377,16 @@ export default async function build(
|
|||
initialHeaders?: SsgRoute['initialHeaders']
|
||||
} = {}
|
||||
|
||||
if (isRouteHandler) {
|
||||
const exportRouteMeta =
|
||||
exportConfig.initialPageMetaMap[route] || {}
|
||||
const exportRouteMeta: {
|
||||
status?: number
|
||||
headers?: Record<string, string>
|
||||
} = exportConfig.initialPageMetaMap[route] || {}
|
||||
|
||||
if (exportRouteMeta.status !== 200) {
|
||||
routeMeta.initialStatus = exportRouteMeta.status
|
||||
}
|
||||
if (Object.keys(exportRouteMeta.headers).length) {
|
||||
routeMeta.initialHeaders = exportRouteMeta.headers
|
||||
}
|
||||
if (exportRouteMeta.status !== 200) {
|
||||
routeMeta.initialStatus = exportRouteMeta.status
|
||||
}
|
||||
if (Object.keys(exportRouteMeta.headers || {}).length) {
|
||||
routeMeta.initialHeaders = exportRouteMeta.headers
|
||||
}
|
||||
|
||||
finalPrerenderRoutes[route] = {
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface StaticGenerationStore {
|
|||
readonly pathname: string
|
||||
readonly incrementalCache?: IncrementalCache
|
||||
readonly isRevalidate?: boolean
|
||||
readonly isMinimalMode?: boolean
|
||||
readonly isOnDemandRevalidate?: boolean
|
||||
readonly isPrerendering?: boolean
|
||||
|
||||
|
@ -17,6 +18,7 @@ export interface StaticGenerationStore {
|
|||
| 'force-no-store'
|
||||
| 'default-no-store'
|
||||
| 'only-no-store'
|
||||
|
||||
revalidate?: false | number
|
||||
forceStatic?: boolean
|
||||
dynamicShouldError?: boolean
|
||||
|
@ -26,6 +28,8 @@ export interface StaticGenerationStore {
|
|||
dynamicUsageStack?: string
|
||||
|
||||
nextFetchId?: number
|
||||
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export type StaticGenerationAsyncStorage =
|
||||
|
|
|
@ -46,6 +46,7 @@ import { isAppRouteRoute } from '../lib/is-app-route-route'
|
|||
import { toNodeHeaders } from '../server/web/utils'
|
||||
import { RouteModuleLoader } from '../server/future/helpers/module-loader/route-module-loader'
|
||||
import { NextRequestAdapter } from '../server/web/spec-extension/adapters/next-request'
|
||||
import * as ciEnvironment from '../telemetry/ci-info'
|
||||
|
||||
const envConfig = require('../shared/lib/runtime-config')
|
||||
|
||||
|
@ -322,6 +323,12 @@ export default async function exportPage({
|
|||
fontManifest: optimizeFonts ? requireFontManifest(distDir) : null,
|
||||
locale: locale as string,
|
||||
supportsDynamicHTML: false,
|
||||
...(ciEnvironment.hasNextSupport
|
||||
? {
|
||||
isMinimalMode: true,
|
||||
isRevalidate: true,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -400,6 +407,12 @@ export default async function exportPage({
|
|||
nextExport: true,
|
||||
supportsDynamicHTML: false,
|
||||
incrementalCache: curRenderOpts.incrementalCache,
|
||||
...(ciEnvironment.hasNextSupport
|
||||
? {
|
||||
isMinimalMode: true,
|
||||
isRevalidate: true,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -429,6 +442,12 @@ export default async function exportPage({
|
|||
|
||||
results.fromBuildExportRevalidate = revalidate
|
||||
const headers = toNodeHeaders(response.headers)
|
||||
const cacheTags = (context.staticGenerationContext as any)
|
||||
.fetchTags
|
||||
|
||||
if (cacheTags) {
|
||||
headers['x-next-cache-tags'] = cacheTags
|
||||
}
|
||||
|
||||
if (!headers['content-type'] && body.type) {
|
||||
headers['content-type'] = body.type
|
||||
|
@ -481,7 +500,26 @@ export default async function exportPage({
|
|||
results.fromBuildExportRevalidate = revalidate
|
||||
|
||||
if (revalidate !== 0) {
|
||||
const cacheTags = (curRenderOpts as any).fetchTags
|
||||
const headers = cacheTags
|
||||
? {
|
||||
'x-next-cache-tags': cacheTags,
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (ciEnvironment.hasNextSupport) {
|
||||
if (cacheTags) {
|
||||
results.fromBuildExportMeta = {
|
||||
headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await promises.writeFile(htmlFilepath, html ?? '', 'utf8')
|
||||
await promises.writeFile(
|
||||
htmlFilepath.replace(/\.html$/, '.meta'),
|
||||
JSON.stringify({ headers })
|
||||
)
|
||||
await promises.writeFile(
|
||||
htmlFilepath.replace(/\.html$/, '.rsc'),
|
||||
flightData
|
||||
|
|
|
@ -49,7 +49,7 @@ import {
|
|||
getURLFromRedirectError,
|
||||
isRedirectError,
|
||||
} from '../../client/components/redirect'
|
||||
import { patchFetch } from '../lib/patch-fetch'
|
||||
import { addImplicitTags, patchFetch } from '../lib/patch-fetch'
|
||||
import { AppRenderSpan } from '../lib/trace/constants'
|
||||
import { getTracer } from '../lib/trace/tracer'
|
||||
import { interopDefault } from './interop-default'
|
||||
|
@ -1571,6 +1571,8 @@ export async function renderToHTMLOrFlight(
|
|||
if (staticGenerationStore.pendingRevalidates) {
|
||||
await Promise.all(staticGenerationStore.pendingRevalidates)
|
||||
}
|
||||
addImplicitTags(staticGenerationStore)
|
||||
;(renderOpts as any).fetchTags = staticGenerationStore.tags?.join(',')
|
||||
|
||||
if (staticGenerationStore.isStaticGeneration) {
|
||||
const htmlResult = await streamToBufferedResult(renderResult)
|
||||
|
|
|
@ -38,6 +38,11 @@ export abstract class BaseNextResponse<Destination = any> {
|
|||
*/
|
||||
abstract setHeader(name: string, value: string | string[]): this
|
||||
|
||||
/**
|
||||
* Removes a header
|
||||
*/
|
||||
abstract removeHeader(name: string): this
|
||||
|
||||
/**
|
||||
* Appends value for the given header name
|
||||
*/
|
||||
|
|
|
@ -85,6 +85,11 @@ export class NodeNextResponse extends BaseNextResponse<Writable> {
|
|||
return this
|
||||
}
|
||||
|
||||
removeHeader(name: string): this {
|
||||
this._res.removeHeader(name)
|
||||
return this
|
||||
}
|
||||
|
||||
getHeaderValues(name: string): string[] | undefined {
|
||||
const values = this._res.getHeader(name)
|
||||
|
||||
|
|
|
@ -64,6 +64,11 @@ export class WebNextResponse extends BaseNextResponse<WritableStream> {
|
|||
return this
|
||||
}
|
||||
|
||||
removeHeader(name: string): this {
|
||||
this.headers.delete(name)
|
||||
return this
|
||||
}
|
||||
|
||||
getHeaderValues(name: string): string[] | undefined {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/get#example
|
||||
return this.getHeader(name)
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from '../shared/lib/utils'
|
||||
import type { PreviewData, ServerRuntime } from 'next/types'
|
||||
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
|
||||
import type { OutgoingHttpHeaders } from 'http2'
|
||||
import type { BaseNextRequest, BaseNextResponse } from './base-http'
|
||||
import type { PayloadOptions } from './send-payload'
|
||||
import type { PrerenderManifest } from '../build'
|
||||
|
@ -1586,8 +1587,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
| 'https',
|
||||
})
|
||||
|
||||
let isRevalidate = false
|
||||
|
||||
const doRender: () => Promise<ResponseCacheEntry | null> = async () => {
|
||||
// In development, we always want to generate dynamic HTML.
|
||||
const supportsDynamicHTML =
|
||||
|
@ -1605,6 +1604,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
staticGenerationContext: {
|
||||
supportsDynamicHTML,
|
||||
incrementalCache,
|
||||
isRevalidate: isSSG,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1612,6 +1612,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
// Handle the match and collect the response if it's a static response.
|
||||
const response = await this.handlers.handle(match, req, context)
|
||||
if (response) {
|
||||
const cacheTags = (context.staticGenerationContext as any).fetchTags
|
||||
|
||||
// If the request is for a static response, we can cache it so long
|
||||
// as it's not edge.
|
||||
if (isSSG && process.env.NEXT_RUNTIME !== 'edge') {
|
||||
|
@ -1619,6 +1621,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
|
||||
// Copy the headers from the response.
|
||||
const headers = toNodeHeaders(response.headers)
|
||||
|
||||
if (cacheTags) {
|
||||
headers['x-next-cache-tags'] = cacheTags
|
||||
}
|
||||
|
||||
if (!headers['content-type'] && blob.type) {
|
||||
headers['content-type'] = blob.type
|
||||
}
|
||||
|
@ -1669,6 +1676,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
let isrRevalidate: number | false
|
||||
let isNotFound: boolean | undefined
|
||||
let isRedirect: boolean | undefined
|
||||
let headers: OutgoingHttpHeaders | undefined
|
||||
|
||||
const origQuery = parseUrl(req.url || '', true).query
|
||||
|
||||
|
@ -1694,7 +1702,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
...(isAppPath && this.nextConfig.experimental.appDir
|
||||
? {
|
||||
incrementalCache,
|
||||
isRevalidate: this.minimalMode || isRevalidate,
|
||||
isRevalidate: isSSG,
|
||||
}
|
||||
: {}),
|
||||
isDataReq,
|
||||
|
@ -1717,6 +1725,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
|
||||
supportsDynamicHTML,
|
||||
isOnDemandRevalidate,
|
||||
isMinimalMode: this.minimalMode,
|
||||
}
|
||||
|
||||
const renderResult = await this.renderHTML(
|
||||
|
@ -1736,6 +1745,14 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
isNotFound = renderResultMeta.isNotFound
|
||||
isRedirect = renderResultMeta.isRedirect
|
||||
|
||||
const cacheTags = (renderOpts as any).fetchTags
|
||||
|
||||
if (cacheTags) {
|
||||
headers = {
|
||||
'x-next-cache-tags': cacheTags,
|
||||
}
|
||||
}
|
||||
|
||||
// we don't throw static to dynamic errors in dev as isSSG
|
||||
// is a best guess in dev since we don't have the prerender pass
|
||||
// to know whether the path is actually static or not
|
||||
|
@ -1771,7 +1788,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
if (body.isNull()) {
|
||||
return null
|
||||
}
|
||||
value = { kind: 'PAGE', html: body, pageData }
|
||||
value = { kind: 'PAGE', html: body, pageData, headers }
|
||||
}
|
||||
return { revalidate: isrRevalidate, value }
|
||||
}
|
||||
|
@ -1783,10 +1800,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
const isDynamicPathname = isDynamicRoute(pathname)
|
||||
const didRespond = hasResolved || res.sent
|
||||
|
||||
if (hadCache) {
|
||||
isRevalidate = true
|
||||
}
|
||||
|
||||
if (!staticPaths) {
|
||||
;({ staticPaths, fallbackMode } = hasStaticPaths
|
||||
? await this.getStaticPaths({
|
||||
|
@ -1815,6 +1828,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
return null
|
||||
}
|
||||
|
||||
if (hadCache?.isStale === -1) {
|
||||
isOnDemandRevalidate = true
|
||||
}
|
||||
|
||||
// only allow on-demand revalidate for fallback: true/blocking
|
||||
// or for prerendered fallback: false paths
|
||||
if (isOnDemandRevalidate && (fallbackMode !== false || hadCache)) {
|
||||
|
@ -1990,17 +2007,33 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
} else if (cachedData.kind === 'IMAGE') {
|
||||
throw new Error('invariant SSG should not return an image cache value')
|
||||
} else if (cachedData.kind === 'ROUTE') {
|
||||
const headers = { ...cachedData.headers }
|
||||
|
||||
if (!(this.minimalMode && isSSG)) {
|
||||
delete headers['x-next-cache-tags']
|
||||
}
|
||||
|
||||
await sendResponse(
|
||||
req,
|
||||
res,
|
||||
new Response(cachedData.body, {
|
||||
headers: fromNodeHeaders(cachedData.headers),
|
||||
headers: fromNodeHeaders(headers),
|
||||
status: cachedData.status || 200,
|
||||
})
|
||||
)
|
||||
return null
|
||||
} else {
|
||||
if (isAppPath) {
|
||||
if (
|
||||
this.minimalMode &&
|
||||
isSSG &&
|
||||
cachedData.headers?.['x-next-cache-tags']
|
||||
) {
|
||||
res.setHeader(
|
||||
'x-next-cache-tags',
|
||||
cachedData.headers['x-next-cache-tags'] as string
|
||||
)
|
||||
}
|
||||
if (isDataReq && typeof cachedData.pageData !== 'string') {
|
||||
throw new Error(
|
||||
'invariant: Expected pageData to be a string for app data request but received ' +
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
handleInternalServerErrorResponse,
|
||||
} from '../helpers/response-handlers'
|
||||
import { type HTTP_METHOD, HTTP_METHODS, isHTTPMethod } from '../../../web/http'
|
||||
import { patchFetch } from '../../../lib/patch-fetch'
|
||||
import { addImplicitTags, patchFetch } from '../../../lib/patch-fetch'
|
||||
import { getTracer } from '../../../lib/trace/tracer'
|
||||
import { AppRouteRouteHandlersSpan } from '../../../lib/trace/constants'
|
||||
import { getPathnameFromAbsolutePath } from './helpers/get-pathname-from-absolute-path'
|
||||
|
@ -349,6 +349,9 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
await Promise.all(
|
||||
staticGenerationStore.pendingRevalidates || []
|
||||
)
|
||||
addImplicitTags(staticGenerationStore)
|
||||
;(context.staticGenerationContext as any).fetchTags =
|
||||
staticGenerationStore.tags?.join(',')
|
||||
|
||||
// It's possible cookies were set in the handler, so we need
|
||||
// to merge the modified cookies and the returned response
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import type { OutgoingHttpHeaders } from 'http'
|
||||
import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from './'
|
||||
import LRUCache from 'next/dist/compiled/lru-cache'
|
||||
import { CacheFs } from '../../../shared/lib/utils'
|
||||
import path from '../../../shared/lib/isomorphic/path'
|
||||
import { CachedFetchValue } from '../../response-cache'
|
||||
import { CacheFs } from '../../../shared/lib/utils'
|
||||
import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from './'
|
||||
import { CACHE_ONE_YEAR } from '../../../lib/constants'
|
||||
|
||||
type FileSystemCacheContext = Omit<
|
||||
|
@ -199,12 +200,27 @@ export default class FileSystemCache implements CacheHandler {
|
|||
)
|
||||
).toString('utf8')
|
||||
)
|
||||
|
||||
let meta: { status?: number; headers?: OutgoingHttpHeaders } = {}
|
||||
|
||||
if (isAppPath) {
|
||||
try {
|
||||
meta = JSON.parse(
|
||||
(
|
||||
await this.fs.readFile(filePath.replace(/\.html$/, '.meta'))
|
||||
).toString('utf-8')
|
||||
)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
data = {
|
||||
lastModified: mtime.getTime(),
|
||||
value: {
|
||||
kind: 'PAGE',
|
||||
html: fileData,
|
||||
pageData,
|
||||
headers: meta.headers,
|
||||
status: meta.status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -216,11 +232,65 @@ export default class FileSystemCache implements CacheHandler {
|
|||
// unable to get data from disk
|
||||
}
|
||||
}
|
||||
let cacheTags: undefined | string[]
|
||||
|
||||
if (data?.value?.kind === 'PAGE') {
|
||||
const tagsHeader = data.value.headers?.['x-next-cache-tags']
|
||||
|
||||
if (typeof tagsHeader === 'string') {
|
||||
cacheTags = tagsHeader.split(',')
|
||||
}
|
||||
}
|
||||
|
||||
const getDerivedTags = (tags: string[]): string[] => {
|
||||
const derivedTags: string[] = []
|
||||
|
||||
for (const tag of tags || []) {
|
||||
if (tag.startsWith('/')) {
|
||||
const pathnameParts = tag.split('/')
|
||||
|
||||
// we automatically add the current path segments as tags
|
||||
// for revalidatePath handling
|
||||
for (let i = 1; i < pathnameParts.length + 1; i++) {
|
||||
const curPathname = pathnameParts.slice(0, i).join('/')
|
||||
|
||||
if (curPathname) {
|
||||
derivedTags.push(curPathname)
|
||||
|
||||
if (!derivedTags.includes(curPathname)) {
|
||||
derivedTags.push(curPathname)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!derivedTags.includes(tag)) {
|
||||
derivedTags.push(tag)
|
||||
}
|
||||
}
|
||||
return derivedTags
|
||||
}
|
||||
|
||||
if (data?.value?.kind === 'PAGE' && cacheTags?.length) {
|
||||
this.loadTagsManifest()
|
||||
const derivedTags = getDerivedTags(cacheTags || [])
|
||||
|
||||
const isStale = derivedTags.some((tag) => {
|
||||
return (
|
||||
tagsManifest?.items[tag]?.revalidatedAt &&
|
||||
tagsManifest?.items[tag].revalidatedAt >=
|
||||
(data?.lastModified || Date.now())
|
||||
)
|
||||
})
|
||||
if (isStale) {
|
||||
data.lastModified = -1
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data?.value?.kind === 'FETCH') {
|
||||
this.loadTagsManifest()
|
||||
const innerData = data.value.data
|
||||
const isStale = innerData.tags?.some((tag) => {
|
||||
const derivedTags = getDerivedTags(innerData.tags || [])
|
||||
|
||||
const isStale = derivedTags.some((tag) => {
|
||||
return (
|
||||
tagsManifest?.items[tag]?.revalidatedAt &&
|
||||
tagsManifest?.items[tag].revalidatedAt >=
|
||||
|
@ -274,6 +344,16 @@ export default class FileSystemCache implements CacheHandler {
|
|||
).filePath,
|
||||
isAppPath ? data.pageData : JSON.stringify(data.pageData)
|
||||
)
|
||||
|
||||
if (data.headers || data.status) {
|
||||
await this.fs.writeFile(
|
||||
htmlPath.replace(/\.html$/, '.meta'),
|
||||
JSON.stringify({
|
||||
headers: data.headers,
|
||||
status: data.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (data?.kind === 'FETCH') {
|
||||
const { filePath } = await this.getFsPath({
|
||||
pathname: key,
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from '../../response-cache'
|
||||
import { encode } from '../../../shared/lib/bloom-filter/base64-arraybuffer'
|
||||
import { encodeText } from '../../stream-utils/encode-decode'
|
||||
import { CACHE_ONE_YEAR } from '../../../lib/constants'
|
||||
|
||||
function toRoute(pathname: string): string {
|
||||
return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/'
|
||||
|
@ -21,9 +22,10 @@ export interface CacheHandlerContext {
|
|||
flushToDisk?: boolean
|
||||
serverDistDir?: string
|
||||
maxMemoryCacheSize?: number
|
||||
fetchCacheKeyPrefix?: string
|
||||
prerenderManifest?: PrerenderManifest
|
||||
_appDir: boolean
|
||||
_requestHeaders: IncrementalCache['requestHeaders']
|
||||
fetchCacheKeyPrefix?: string
|
||||
}
|
||||
|
||||
export interface CacheHandlerValue {
|
||||
|
@ -327,15 +329,24 @@ export class IncrementalCache {
|
|||
|
||||
const curRevalidate =
|
||||
this.prerenderManifest.routes[toRoute(pathname)]?.initialRevalidateSeconds
|
||||
const revalidateAfter = this.calculateRevalidate(
|
||||
pathname,
|
||||
cacheData?.lastModified || Date.now(),
|
||||
this.dev && !fetchCache
|
||||
)
|
||||
const isStale =
|
||||
revalidateAfter !== false && revalidateAfter < Date.now()
|
||||
? true
|
||||
: undefined
|
||||
|
||||
let isStale: boolean | -1 | undefined
|
||||
let revalidateAfter: false | number
|
||||
|
||||
if (cacheData?.lastModified === -1) {
|
||||
isStale = -1
|
||||
revalidateAfter = -1 * CACHE_ONE_YEAR
|
||||
} else {
|
||||
revalidateAfter = this.calculateRevalidate(
|
||||
pathname,
|
||||
cacheData?.lastModified || Date.now(),
|
||||
this.dev && !fetchCache
|
||||
)
|
||||
isStale =
|
||||
revalidateAfter !== false && revalidateAfter < Date.now()
|
||||
? true
|
||||
: undefined
|
||||
}
|
||||
|
||||
if (cacheData) {
|
||||
entry = {
|
||||
|
|
|
@ -7,6 +7,19 @@ import { CACHE_ONE_YEAR } from '../../lib/constants'
|
|||
|
||||
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
|
||||
|
||||
export function addImplicitTags(
|
||||
staticGenerationStore: ReturnType<StaticGenerationAsyncStorage['getStore']>
|
||||
) {
|
||||
if (!staticGenerationStore?.pathname) return
|
||||
|
||||
if (!Array.isArray(staticGenerationStore.tags)) {
|
||||
staticGenerationStore.tags = []
|
||||
}
|
||||
if (!staticGenerationStore.tags.includes(staticGenerationStore.pathname)) {
|
||||
staticGenerationStore.tags.push(staticGenerationStore.pathname)
|
||||
}
|
||||
}
|
||||
|
||||
// we patch fetch to collect cache information used for
|
||||
// determining if a page is static or not
|
||||
export function patchFetch({
|
||||
|
@ -81,7 +94,19 @@ export function patchFetch({
|
|||
// RequestInit doesn't keep extra fields e.g. next so it's
|
||||
// only available if init is used separate
|
||||
let curRevalidate = getNextField('revalidate')
|
||||
const tags: undefined | string[] = getNextField('tags')
|
||||
const tags: string[] = getNextField('tags') || []
|
||||
|
||||
if (Array.isArray(tags)) {
|
||||
if (!staticGenerationStore.tags) {
|
||||
staticGenerationStore.tags = []
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (!staticGenerationStore.tags.includes(tag)) {
|
||||
staticGenerationStore.tags.push(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
addImplicitTags(staticGenerationStore)
|
||||
|
||||
const isOnlyCache = staticGenerationStore.fetchCache === 'only-cache'
|
||||
const isForceCache = staticGenerationStore.fetchCache === 'force-cache'
|
||||
|
|
|
@ -268,6 +268,7 @@ export type RenderOptsPartial = {
|
|||
largePageDataBytes?: number
|
||||
isOnDemandRevalidate?: boolean
|
||||
strictNextHead: boolean
|
||||
isMinimalMode?: boolean
|
||||
}
|
||||
|
||||
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
|
||||
|
|
|
@ -38,6 +38,7 @@ export interface RequestMeta {
|
|||
_nextDataNormalizing?: boolean
|
||||
_nextMatch?: RouteMatch
|
||||
_nextIncrementalCache?: any
|
||||
_nextMinimalMode?: boolean
|
||||
}
|
||||
|
||||
export function getRequestMeta(
|
||||
|
|
|
@ -111,6 +111,8 @@ export default class ResponseCache {
|
|||
kind: 'PAGE',
|
||||
html: RenderResult.fromStatic(cachedResponse.value.html),
|
||||
pageData: cachedResponse.value.pageData,
|
||||
headers: cachedResponse.value.headers,
|
||||
status: cachedResponse.value.status,
|
||||
}
|
||||
: cachedResponse.value,
|
||||
})
|
||||
|
@ -121,7 +123,7 @@ export default class ResponseCache {
|
|||
}
|
||||
}
|
||||
|
||||
const cacheEntry = await responseGenerator(resolved, !!cachedResponse)
|
||||
const cacheEntry = await responseGenerator(resolved, cachedResponse)
|
||||
const resolveValue =
|
||||
cacheEntry === null
|
||||
? null
|
||||
|
@ -150,6 +152,8 @@ export default class ResponseCache {
|
|||
kind: 'PAGE',
|
||||
html: cacheEntry.value.html.toUnchunkedString(),
|
||||
pageData: cacheEntry.value.pageData,
|
||||
headers: cacheEntry.value.headers,
|
||||
status: cacheEntry.value.status,
|
||||
}
|
||||
: cacheEntry.value,
|
||||
cacheEntry.revalidate
|
||||
|
|
|
@ -35,6 +35,8 @@ interface CachedPageValue {
|
|||
// expects that type instead of a string
|
||||
html: RenderResult
|
||||
pageData: Object
|
||||
status?: number
|
||||
headers?: OutgoingHttpHeaders
|
||||
}
|
||||
|
||||
export interface CachedRouteValue {
|
||||
|
@ -61,13 +63,16 @@ interface IncrementalCachedPageValue {
|
|||
// the string value
|
||||
html: string
|
||||
pageData: Object
|
||||
headers?: OutgoingHttpHeaders
|
||||
status?: number
|
||||
}
|
||||
|
||||
export type IncrementalCacheEntry = {
|
||||
curRevalidate?: number | false
|
||||
// milliseconds to revalidate after
|
||||
revalidateAfter: number | false
|
||||
isStale?: boolean
|
||||
// -1 here dictates a blocking revalidate should be used
|
||||
isStale?: boolean | -1
|
||||
value: IncrementalCacheValue | null
|
||||
}
|
||||
|
||||
|
@ -87,13 +92,13 @@ export type ResponseCacheValue =
|
|||
export type ResponseCacheEntry = {
|
||||
revalidate?: number | false
|
||||
value: ResponseCacheValue | null
|
||||
isStale?: boolean
|
||||
isStale?: boolean | -1
|
||||
isMiss?: boolean
|
||||
}
|
||||
|
||||
export type ResponseGenerator = (
|
||||
hasResolved: boolean,
|
||||
hadCache: boolean
|
||||
cacheEntry?: IncrementalCacheItem
|
||||
) => Promise<ResponseCacheEntry | null>
|
||||
|
||||
export type IncrementalCacheItem = {
|
||||
|
@ -101,7 +106,7 @@ export type IncrementalCacheItem = {
|
|||
curRevalidate?: number | false
|
||||
revalidate?: number | false
|
||||
value: IncrementalCacheValue | null
|
||||
isStale?: boolean
|
||||
isStale?: boolean | -1
|
||||
isMiss?: boolean
|
||||
} | null
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class WebResponseCache {
|
|||
// same promise until we've fully finished our work.
|
||||
;(async () => {
|
||||
try {
|
||||
const cacheEntry = await responseGenerator(resolved, false)
|
||||
const cacheEntry = await responseGenerator(resolved)
|
||||
const resolveValue =
|
||||
cacheEntry === null
|
||||
? null
|
||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
|||
StaticGenerationStore,
|
||||
} from '../../../client/components/static-generation-async-storage'
|
||||
|
||||
import { unstable_revalidateTag } from './unstable-revalidate-tag'
|
||||
import { headers } from '../../../client/components/headers'
|
||||
import {
|
||||
PRERENDER_REVALIDATE_HEADER,
|
||||
|
@ -12,9 +13,14 @@ import {
|
|||
export function unstable_revalidatePath(
|
||||
path: string,
|
||||
ctx: {
|
||||
manualRevalidate?: boolean
|
||||
unstable_onlyGenerated?: boolean
|
||||
} = {}
|
||||
) {
|
||||
if (!ctx?.manualRevalidate) {
|
||||
return unstable_revalidateTag(path)
|
||||
}
|
||||
|
||||
const staticGenerationAsyncStorage = (
|
||||
fetch as any
|
||||
).__nextGetStaticStore?.() as undefined | StaticGenerationAsyncStorage
|
||||
|
|
|
@ -32,6 +32,22 @@ createNextDescribe(
|
|||
}
|
||||
})
|
||||
|
||||
it('should not have cache tags header for non-minimal mode', async () => {
|
||||
for (const path of [
|
||||
'/ssr-forced',
|
||||
'/ssr-forced',
|
||||
'/variable-revalidate/revalidate-3',
|
||||
'/variable-revalidate/revalidate-360',
|
||||
'/variable-revalidate/revalidate-360-isr',
|
||||
]) {
|
||||
const res = await fetchViaHTTP(next.url, path, undefined, {
|
||||
redirect: 'manual',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-next-cache-tags')).toBeFalsy()
|
||||
}
|
||||
})
|
||||
|
||||
if (isDev) {
|
||||
it('should error correctly for invalid params from generateStaticParams', async () => {
|
||||
await next.patchFile(
|
||||
|
@ -154,7 +170,7 @@ createNextDescribe(
|
|||
|
||||
// On-Demand Revalidate has not effect in dev since app routes
|
||||
// aren't considered static until prerendering
|
||||
if (!(global as any).isNextDev) {
|
||||
if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) {
|
||||
it.each([
|
||||
{
|
||||
type: 'edge route handler',
|
||||
|
@ -205,7 +221,9 @@ createNextDescribe(
|
|||
// On-Demand Revalidate has not effect in dev
|
||||
if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) {
|
||||
it('should revalidate all fetches during on-demand revalidate', async () => {
|
||||
const initRes = await next.fetch('/variable-revalidate/revalidate-360')
|
||||
const initRes = await next.fetch(
|
||||
'/variable-revalidate/revalidate-360-isr'
|
||||
)
|
||||
const html = await initRes.text()
|
||||
const $ = cheerio.load(html)
|
||||
const initLayoutData = $('#layout-data').text()
|
||||
|
@ -410,6 +428,7 @@ createNextDescribe(
|
|||
'partial-gen-params/[lang]/[slug]/page.js',
|
||||
'route-handler-edge/revalidate-360/route.js',
|
||||
'route-handler/post/route.js',
|
||||
'route-handler/revalidate-360-isr/route.js',
|
||||
'route-handler/revalidate-360/route.js',
|
||||
'ssg-draft-mode.html',
|
||||
'ssg-draft-mode.rsc',
|
||||
|
@ -633,6 +652,16 @@ createNextDescribe(
|
|||
initialRevalidateSeconds: 3,
|
||||
srcRoute: '/gen-params-dynamic-revalidate/[slug]',
|
||||
},
|
||||
'/route-handler/revalidate-360-isr': {
|
||||
dataRoute: null,
|
||||
initialHeaders: {
|
||||
'content-type': 'application/json',
|
||||
'x-next-cache-tags':
|
||||
'thankyounext,/route-handler/revalidate-360-isr',
|
||||
},
|
||||
initialRevalidateSeconds: false,
|
||||
srcRoute: '/route-handler/revalidate-360-isr',
|
||||
},
|
||||
'/variable-config-revalidate/revalidate-3': {
|
||||
dataRoute: '/variable-config-revalidate/revalidate-3.rsc',
|
||||
initialRevalidateSeconds: 3,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { NextRequest, NextResponse, unstable_revalidatePath } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { unstable_revalidatePath } from 'next/cache'
|
||||
|
||||
export const runtime = 'edge'
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { NextRequest, NextResponse, unstable_revalidatePath } from 'next/server'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { unstable_revalidatePath } from 'next/cache'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const path = req.nextUrl.searchParams.get('path') || '/'
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { NextResponse, unstable_revalidateTag } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { unstable_revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidate = 0
|
||||
export const runtime = 'edge'
|
||||
|
||||
export async function GET(req) {
|
||||
const tag = req.nextUrl.searchParams.get('tag')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { NextResponse, unstable_revalidateTag } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { unstable_revalidateTag } from 'next/cache'
|
||||
|
||||
export const revalidate = 0
|
||||
export const runtime = 'edge'
|
||||
|
||||
export async function GET(req) {
|
||||
const tag = req.nextUrl.searchParams.get('tag')
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
const data360 = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
next: {
|
||||
revalidate: 360,
|
||||
tags: ['thankyounext'],
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
const data10 = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random?a=10',
|
||||
{
|
||||
next: {
|
||||
revalidate: 10,
|
||||
tags: ['thankyounext'],
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({ data360, data10 })
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { unstable_cache } from 'next/server'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { unstable_cache } from 'next/server'
|
||||
import { unstable_cache } from 'next/cache'
|
||||
|
||||
export const revalidate = 0
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const revalidate = 3
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ slug: 'first' }]
|
||||
}
|
||||
|
||||
export async function GET(req, { params }) {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
next: {
|
||||
tags: ['isr-page'],
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({
|
||||
now: Date.now(),
|
||||
params,
|
||||
data,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export const revalidate = 0
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ slug: 'first' }]
|
||||
}
|
||||
|
||||
export async function GET(req, { params }) {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
next: {
|
||||
tags: ['ssr-page'],
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return NextResponse.json({
|
||||
now: Date.now(),
|
||||
params,
|
||||
data,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
export const revalidate = 3
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ slug: 'first' }]
|
||||
}
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
next: {
|
||||
tags: ['isr-page'],
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/isr/[slug]</p>
|
||||
<p id="params">{JSON.stringify(params)}</p>
|
||||
<p id="now">{Date.now()}</p>
|
||||
<p id="data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export const revalidate = 0
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const data = await fetch(
|
||||
'https://next-data-api-endpoint.vercel.app/api/random',
|
||||
{
|
||||
next: {
|
||||
tags: ['ssr-page'],
|
||||
},
|
||||
}
|
||||
).then((res) => res.text())
|
||||
|
||||
return (
|
||||
<>
|
||||
<p id="page">/ssr/[slug]</p>
|
||||
<p id="params">{JSON.stringify(params)}</p>
|
||||
<p id="now">{Date.now()}</p>
|
||||
<p id="data">{data}</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
import glob from 'glob'
|
||||
import fs from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import {
|
||||
fetchViaHTTP,
|
||||
findPort,
|
||||
initNextServerScript,
|
||||
killApp,
|
||||
} from 'next-test-utils'
|
||||
|
||||
describe('should set-up next', () => {
|
||||
let next: NextInstance
|
||||
let server
|
||||
let appPort
|
||||
|
||||
const setupNext = async ({
|
||||
nextEnv,
|
||||
minimalMode,
|
||||
}: {
|
||||
nextEnv?: boolean
|
||||
minimalMode?: boolean
|
||||
}) => {
|
||||
// test build against environment with next support
|
||||
process.env.NOW_BUILDER = nextEnv ? '1' : ''
|
||||
|
||||
next = await createNext({
|
||||
files: {
|
||||
app: new FileRef(join(__dirname, 'app')),
|
||||
lib: new FileRef(join(__dirname, 'lib')),
|
||||
'middleware.js': new FileRef(join(__dirname, 'middleware.js')),
|
||||
'data.txt': new FileRef(join(__dirname, 'data.txt')),
|
||||
'.env': new FileRef(join(__dirname, '.env')),
|
||||
'.env.local': new FileRef(join(__dirname, '.env.local')),
|
||||
'.env.production': new FileRef(join(__dirname, '.env.production')),
|
||||
},
|
||||
nextConfig: {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
output: 'standalone',
|
||||
},
|
||||
})
|
||||
await next.stop()
|
||||
|
||||
await fs.move(
|
||||
join(next.testDir, '.next/standalone'),
|
||||
join(next.testDir, 'standalone')
|
||||
)
|
||||
for (const file of await fs.readdir(next.testDir)) {
|
||||
if (file !== 'standalone') {
|
||||
await fs.remove(join(next.testDir, file))
|
||||
console.log('removed', file)
|
||||
}
|
||||
}
|
||||
const files = glob.sync('**/*', {
|
||||
cwd: join(next.testDir, 'standalone/.next/server/pages'),
|
||||
dot: true,
|
||||
})
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.json') || file.endsWith('.html')) {
|
||||
await fs.remove(join(next.testDir, '.next/server', file))
|
||||
}
|
||||
}
|
||||
|
||||
const testServer = join(next.testDir, 'standalone/server.js')
|
||||
await fs.writeFile(
|
||||
testServer,
|
||||
(await fs.readFile(testServer, 'utf8'))
|
||||
.replace('console.error(err)', `console.error('top-level', err)`)
|
||||
.replace('conf:', `minimalMode: ${minimalMode},conf:`)
|
||||
)
|
||||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
testServer,
|
||||
/Listening on/,
|
||||
{
|
||||
...process.env,
|
||||
PORT: appPort,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
cwd: next.testDir,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupNext({ nextEnv: true, minimalMode: true })
|
||||
})
|
||||
afterAll(async () => {
|
||||
await next.destroy()
|
||||
if (server) await killApp(server)
|
||||
})
|
||||
|
||||
it('should send cache tags in minimal mode for ISR', async () => {
|
||||
for (const [path, tags] of [
|
||||
['/isr/first', 'isr-page,/isr/[slug]'],
|
||||
['/isr/second', 'isr-page,/isr/[slug]'],
|
||||
['/api/isr/first', 'isr-page,/api/isr/[slug]'],
|
||||
['/api/isr/second', 'isr-page,/api/isr/[slug]'],
|
||||
]) {
|
||||
const res = await fetchViaHTTP(appPort, path, undefined, {
|
||||
redirect: 'manual',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-next-cache-tags')).toBe(tags)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not send cache tags in minimal mode for SSR', async () => {
|
||||
for (const path of [
|
||||
'/ssr/first',
|
||||
'/ssr/second',
|
||||
'/api/ssr/first',
|
||||
'/api/ssr/second',
|
||||
]) {
|
||||
const res = await fetchViaHTTP(appPort, path, undefined, {
|
||||
redirect: 'manual',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('x-next-cache-tags')).toBeFalsy()
|
||||
}
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue