diff --git a/packages/next/src/server/lib/freeze.ts b/packages/next/src/server/lib/freeze.ts new file mode 100644 index 0000000000000..16b445c86bebd --- /dev/null +++ b/packages/next/src/server/lib/freeze.ts @@ -0,0 +1,34 @@ +/** + * Recursively freezes an object and all of its properties. This prevents the + * object from being modified at runtime. When the JS runtime is running in + * strict mode, any attempts to modify a frozen object will throw an error. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze + * @param obj The object to freeze. + */ +export function freeze(obj: object): void { + // `null` is an object, if we get this, we should just return it. + if (obj === null) return + + // An array is an object, but we also want to freeze each element in the array + // as well. + if (Array.isArray(obj)) { + for (const item of obj) { + if (!item || typeof item !== 'object') continue + freeze(item) + } + + Object.freeze(obj) + return + } + + for (const name of Object.keys(obj)) { + const value = obj[name as keyof typeof obj] + + if (!value || typeof value !== 'object') continue + freeze(value) + } + + Object.freeze(obj) + return +} diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 58d47c8324197..cf5aa9dd4a2bc 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -1,15 +1,15 @@ import type { CacheFs } from '../../../shared/lib/utils' -import type { PrerenderManifest } from '../../../build' +import type { PrerenderManifest, SsgRoute } from '../../../build' import type { IncrementalCacheValue, IncrementalCacheEntry, IncrementalCache as IncrementalCacheType, IncrementalCacheKindHint, } from '../../response-cache' +import type { Revalidate } from '../revalidate' import FetchCache from './fetch-cache' import FileSystemCache from './file-system-cache' -import path from '../../../shared/lib/isomorphic/path' import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' import { @@ -189,6 +189,31 @@ export class IncrementalCache implements IncrementalCacheType { } } + private readonly revalidateSeconds = new Map() + + private getRevalidateSeconds(route: string): Revalidate | undefined { + let revalidateSeconds = this.revalidateSeconds.get(route) + + // If we already have an entry for this pathname, return it. + if (typeof revalidateSeconds === 'number') { + return revalidateSeconds + } + + // Otherwise, source it from the prerender manifest. + const ssgRoute: SsgRoute | undefined = this.prerenderManifest.routes[route] + + // If there's no entry, return undefined. + if (typeof ssgRoute === 'undefined') { + return undefined + } + + // If there is an entry, store it in the cache and return it. + revalidateSeconds = ssgRoute.initialRevalidateSeconds + this.revalidateSeconds.set(route, revalidateSeconds) + + return revalidateSeconds + } + private calculateRevalidate( pathname: string, fromTime: number, @@ -199,12 +224,10 @@ export class IncrementalCache implements IncrementalCacheType { if (dev) return new Date().getTime() - 1000 // if an entry isn't present in routes we fallback to a default - // of revalidating after 1 second - const { initialRevalidateSeconds } = this.prerenderManifest.routes[ - toRoute(pathname) - ] || { - initialRevalidateSeconds: 1, - } + // of revalidating after 1 second. + const initialRevalidateSeconds = + this.getRevalidateSeconds(toRoute(pathname)) ?? 1 + const revalidateAfter = typeof initialRevalidateSeconds === 'number' ? initialRevalidateSeconds * 1000 + fromTime @@ -485,8 +508,7 @@ export class IncrementalCache implements IncrementalCacheType { } } - const curRevalidate = - this.prerenderManifest.routes[toRoute(cacheKey)]?.initialRevalidateSeconds + const curRevalidate = this.getRevalidateSeconds(toRoute(cacheKey)) let isStale: boolean | -1 | undefined let revalidateAfter: false | number @@ -584,22 +606,12 @@ export class IncrementalCache implements IncrementalCacheType { pathname = this._getPathname(pathname, ctx.fetchCache) try { - // we use the prerender manifest memory instance - // to store revalidate timings for calculating - // revalidateAfter values so we update this on set + // Set the value for the revalidate seconds so if it changes we can + // update the cache with the new value. if (typeof ctx.revalidate !== 'undefined' && !ctx.fetchCache) { - this.prerenderManifest.routes[pathname] = { - experimentalPPR: undefined, - dataRoute: path.posix.join( - '/_next/data', - `${normalizePagePath(pathname)}.json` - ), - srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter - initialRevalidateSeconds: ctx.revalidate, - // Pages routes do not have a prefetch data route. - prefetchDataRoute: undefined, - } + this.revalidateSeconds.set(pathname, ctx.revalidate) } + await this.cacheHandler?.set(pathname, data, ctx) } catch (error) { console.warn('Failed to update prerender cache for', pathname, error) diff --git a/packages/next/src/server/load-manifest.ts b/packages/next/src/server/load-manifest.ts index 82b2ae1d0272d..722744e06bae0 100644 --- a/packages/next/src/server/load-manifest.ts +++ b/packages/next/src/server/load-manifest.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'fs' import { runInNewContext } from 'vm' +import { freeze } from './lib/freeze' const cache = new Map() @@ -8,13 +9,17 @@ export function loadManifest( shouldCache: boolean = true ): unknown { const cached = shouldCache && cache.get(path) - if (cached) { return cached } const manifest = JSON.parse(readFileSync(path, 'utf8')) + // Freeze the manifest so it cannot be modified if we're caching it. + if (shouldCache) { + freeze(manifest) + } + if (shouldCache) { cache.set(path, manifest) } @@ -27,7 +32,6 @@ export function evalManifest( shouldCache: boolean = true ): unknown { const cached = shouldCache && cache.get(path) - if (cached) { return cached } @@ -40,6 +44,11 @@ export function evalManifest( const contextObject = {} runInNewContext(content, contextObject) + // Freeze the context object so it cannot be modified if we're caching it. + if (shouldCache) { + freeze(contextObject) + } + if (shouldCache) { cache.set(path, contextObject) } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index d9a2292ad5f30..87e0cc7972648 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1768,11 +1768,11 @@ export default class NextNodeServer extends BaseServer { return this._cachedPreviewManifest } - const manifest = loadManifest( + this._cachedPreviewManifest = loadManifest( join(this.distDir, PRERENDER_MANIFEST) ) as PrerenderManifest - return (this._cachedPreviewManifest = manifest) + return this._cachedPreviewManifest } protected getRoutesManifest(): NormalizedRouteManifest | undefined {