Skip to content

Commit

Permalink
Freeze loaded manifests (#64313)
Browse files Browse the repository at this point in the history
Manifests in the runtime should be treated as immutable objects to
ensure that side effects aren't created that depend on the mutability of
these shared objects. Instead, mutable references should be used in
places where this is desirable.

This also introduces the new `DeepReadonly` utility type, which when
paired with existing manifest types, will modify every field to be read
only, ensuring that the type system will help catch accidental
modifications to these loaded manifest values as well.

Future work could eliminate these types by modifying the manifest types
themselves to be read only, only allowing code that generated them to be
writable.

Closes NEXT-3069
  • Loading branch information
wyattjoh authored Apr 12, 2024
1 parent 6bc9f79 commit b016c3c
Show file tree
Hide file tree
Showing 26 changed files with 327 additions and 84 deletions.
28 changes: 15 additions & 13 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ import { buildCustomRoute } from '../lib/build-custom-route'
import { createProgress } from './progress'
import { traceMemoryUsage } from '../lib/memory/trace'
import { generateEncryptionKeyBase64 } from '../server/app-render/encryption-utils'
import type { DeepReadonly } from '../shared/lib/deep-readonly'

interface ExperimentalBypassForInfo {
experimentalBypassFor?: RouteHas[]
Expand Down Expand Up @@ -337,27 +338,28 @@ async function readManifest<T extends object>(filePath: string): Promise<T> {

async function writePrerenderManifest(
distDir: string,
manifest: Readonly<PrerenderManifest>
manifest: DeepReadonly<PrerenderManifest>
): Promise<void> {
await writeManifest(path.join(distDir, PRERENDER_MANIFEST), manifest)
await writeEdgePartialPrerenderManifest(distDir, manifest)
}

async function writeEdgePartialPrerenderManifest(
distDir: string,
manifest: Readonly<Partial<PrerenderManifest>>
manifest: DeepReadonly<Partial<PrerenderManifest>>
): Promise<void> {
// We need to write a partial prerender manifest to make preview mode settings available in edge middleware.
// Use env vars in JS bundle and inject the actual vars to edge manifest.
const edgePartialPrerenderManifest: Partial<PrerenderManifest> = {
...manifest,
preview: {
previewModeId: 'process.env.__NEXT_PREVIEW_MODE_ID',
previewModeSigningKey: 'process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY',
previewModeEncryptionKey:
'process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY',
},
}
const edgePartialPrerenderManifest: DeepReadonly<Partial<PrerenderManifest>> =
{
...manifest,
preview: {
previewModeId: 'process.env.__NEXT_PREVIEW_MODE_ID',
previewModeSigningKey: 'process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY',
previewModeEncryptionKey:
'process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY',
},
}
await writeFileUtf8(
path.join(distDir, PRERENDER_MANIFEST.replace(/\.json$/, '.js')),
`self.__PRERENDER_MANIFEST=${JSON.stringify(
Expand All @@ -367,7 +369,7 @@ async function writeEdgePartialPrerenderManifest(
}

async function writeClientSsgManifest(
prerenderManifest: PrerenderManifest,
prerenderManifest: DeepReadonly<PrerenderManifest>,
{
buildId,
distDir,
Expand Down Expand Up @@ -3318,7 +3320,7 @@ export default async function build(
NextBuildContext.allowedRevalidateHeaderKeys =
config.experimental.allowedRevalidateHeaderKeys

const prerenderManifest: Readonly<PrerenderManifest> = {
const prerenderManifest: DeepReadonly<PrerenderManifest> = {
version: 4,
routes: finalPrerenderRoutes,
dynamicRoutes: finalDynamicRoutes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { SizeLimit } from '../../../../../types'
import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until'
import type { PAGE_TYPES } from '../../../../lib/page-types'
import type { NextRequestHint } from '../../../../server/web/adapter'
import type { DeepReadonly } from '../../../../shared/lib/deep-readonly'

export function getRender({
dev,
Expand Down Expand Up @@ -53,7 +54,7 @@ export function getRender({
renderToHTML?: any
Document: DocumentType
buildManifest: BuildManifest
prerenderManifest: PrerenderManifest
prerenderManifest: DeepReadonly<PrerenderManifest>
reactLoadableManifest: ReactLoadableManifest
subresourceIntegrityManifest?: Record<string, string>
interceptionRouteRewrites?: ManifestRewriteRoute[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export interface ManifestNode {
}

export type ClientReferenceManifest = {
moduleLoading: {
readonly moduleLoading: {
prefix: string
crossOrigin: string | null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/h
import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies'

import { createAsyncLocalStorage } from './async-local-storage'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

export interface RequestStore {
readonly headers: ReadonlyHeaders
readonly cookies: ReadonlyRequestCookies
readonly mutableCookies: ResponseCookies
readonly draftMode: DraftModeProvider
readonly reactLoadableManifest: Record<string, { files: string[] }>
readonly reactLoadableManifest: DeepReadonly<
Record<string, { files: string[] }>
>
readonly assetPrefix: string
}

Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { formatManifest } from '../build/manifests/formatter/format-manifest'
import { validateRevalidate } from '../server/lib/patch-fetch'
import { TurborepoAccessTraceResult } from '../build/turborepo-access-trace'
import { createProgress } from '../build/progress'
import type { DeepReadonly } from '../shared/lib/deep-readonly'

export class ExportError extends Error {
code = 'NEXT_EXPORT_ERROR'
Expand Down Expand Up @@ -188,7 +189,7 @@ export async function exportAppImpl(
!options.pages &&
(require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST)) as PagesManifest)

let prerenderManifest: PrerenderManifest | undefined
let prerenderManifest: DeepReadonly<PrerenderManifest> | undefined
try {
prerenderManifest = require(join(distDir, PRERENDER_MANIFEST))
} catch {}
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '../shared/lib/html-context.shared-runtime'
import type { HtmlProps } from '../shared/lib/html-context.shared-runtime'
import { encodeURIPath } from '../shared/lib/encode-uri-path'
import type { DeepReadonly } from '../shared/lib/deep-readonly'

export type { DocumentContext, DocumentInitialProps, DocumentProps }

Expand Down Expand Up @@ -360,7 +361,7 @@ function getAmpPath(ampPath: string, asPath: string): string {
}

function getNextFontLinkTags(
nextFontManifest: NextFontManifest | undefined,
nextFontManifest: DeepReadonly<NextFontManifest> | undefined,
dangerousAsPath: string,
assetPrefix: string = ''
) {
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import {
wrapClientComponentLoader,
} from '../client-component-renderer-logger'
import { createServerModuleMap } from './action-utils'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
Expand Down Expand Up @@ -137,7 +138,7 @@ export type AppRenderContext = AppRenderBaseContext & {
requestId: string
defaultRevalidate: Revalidate
pagePath: string
clientReferenceManifest: ClientReferenceManifest
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
assetPrefix: string
flightDataRendererErrorHandler: ErrorHandler
serverComponentsErrorHandler: ErrorHandler
Expand Down
13 changes: 7 additions & 6 deletions packages/next/src/server/app-render/encryption-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin'
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

// Keep the key in memory as it should never change during the lifetime of the server in
// both development and production.
Expand Down Expand Up @@ -116,8 +117,8 @@ export function setReferenceManifestsSingleton({
serverActionsManifest,
serverModuleMap,
}: {
clientReferenceManifest: ClientReferenceManifest
serverActionsManifest: ActionManifest
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
serverModuleMap: {
[id: string]: {
id: string
Expand Down Expand Up @@ -160,8 +161,8 @@ export function getClientReferenceManifestSingleton() {
const serverActionsManifestSingleton = (globalThis as any)[
SERVER_ACTION_MANIFESTS_SINGLETON
] as {
clientReferenceManifest: ClientReferenceManifest
serverActionsManifest: ActionManifest
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
}

if (!serverActionsManifestSingleton) {
Expand All @@ -181,8 +182,8 @@ export async function getActionEncryptionKey() {
const serverActionsManifestSingleton = (globalThis as any)[
SERVER_ACTION_MANIFESTS_SINGLETON
] as {
clientReferenceManifest: ClientReferenceManifest
serverActionsManifest: ActionManifest
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>
serverActionsManifest: DeepReadonly<ActionManifest>
}

if (!serverActionsManifestSingleton) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

/**
* Get external stylesheet link hrefs based on server CSS manifest.
*/
export function getLinkAndScriptTags(
clientReferenceManifest: ClientReferenceManifest,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
filePath: string,
injectedCSS: Set<string>,
injectedScripts: Set<string>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

/**
* Get hrefs for fonts to preload
Expand All @@ -8,7 +9,7 @@ import type { NextFontManifest } from '../../build/webpack/plugins/next-font-man
* Returns null if there are fonts but none to preload and at least some were previously preloaded
*/
export function getPreloadableFonts(
nextFontManifest: NextFontManifest | undefined,
nextFontManifest: DeepReadonly<NextFontManifest> | undefined,
filePath: string | undefined,
injectedFontPreloadTags: Set<string>
): string[] | null {
Expand Down
5 changes: 3 additions & 2 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ParsedUrlQuery } from 'querystring'
import type { AppPageModule } from '../future/route-modules/app-page/module'
import type { SwrDelta } from '../lib/revalidate'
import type { LoadingModuleData } from '../../shared/lib/app-router-context.shared-runtime'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

import s from 'next/dist/compiled/superstruct'

Expand Down Expand Up @@ -126,14 +127,14 @@ export interface RenderOptsPartial {
buildId: string
basePath: string
trailingSlash: boolean
clientReferenceManifest?: ClientReferenceManifest
clientReferenceManifest?: DeepReadonly<ClientReferenceManifest>
supportsDynamicHTML: boolean
runtime?: ServerRuntime
serverComponents?: boolean
enableTainting?: boolean
assetPrefix?: string
crossOrigin?: '' | 'anonymous' | 'use-credentials' | undefined
nextFontManifest?: NextFontManifest
nextFontManifest?: DeepReadonly<NextFontManifest>
isBot?: boolean
incrementalCache?: import('../lib/incremental-cache').IncrementalCache
isRevalidate?: boolean
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/server/app-render/use-flight-response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight
import type { BinaryStreamOf } from './app-render'

import { htmlEscapeJsonString } from '../htmlescape'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'

Expand All @@ -18,7 +19,7 @@ const encoder = new TextEncoder()
*/
export function useFlightStream<T>(
flightStream: BinaryStreamOf<T>,
clientReferenceManifest: ClientReferenceManifest,
clientReferenceManifest: DeepReadonly<ClientReferenceManifest>,
nonce?: string
): Promise<T> {
const response = flightResponses.get(flightStream)
Expand Down
13 changes: 8 additions & 5 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import { NextDataPathnameNormalizer } from './future/normalizers/request/next-da
import { getIsServerAction } from './lib/server-action-request-meta'
import { isInterceptionRouteAppPath } from './future/helpers/interception-routes'
import { toRoute } from './lib/to-route'
import type { DeepReadonly } from '../shared/lib/deep-readonly'

export type FindComponentsResult = {
components: LoadComponentsReturnType
Expand Down Expand Up @@ -285,9 +286,9 @@ export default abstract class Server<ServerOptions extends Options = Options> {
protected readonly renderOpts: BaseRenderOpts
protected readonly serverOptions: Readonly<ServerOptions>
protected readonly appPathRoutes?: Record<string, string[]>
protected readonly clientReferenceManifest?: ClientReferenceManifest
protected readonly clientReferenceManifest?: DeepReadonly<ClientReferenceManifest>
protected interceptionRoutePatterns: RegExp[]
protected nextFontManifest?: NextFontManifest
protected nextFontManifest?: DeepReadonly<NextFontManifest>
private readonly responseCache: ResponseCacheBase

protected abstract getPublicDir(): string
Expand All @@ -314,9 +315,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {
shouldEnsure?: boolean
url?: string
}): Promise<FindComponentsResult | null>
protected abstract getFontManifest(): FontManifest | undefined
protected abstract getPrerenderManifest(): PrerenderManifest
protected abstract getNextFontManifest(): NextFontManifest | undefined
protected abstract getFontManifest(): DeepReadonly<FontManifest> | undefined
protected abstract getPrerenderManifest(): DeepReadonly<PrerenderManifest>
protected abstract getNextFontManifest():
| DeepReadonly<NextFontManifest>
| undefined
protected abstract attachRequestMeta(
req: BaseNextRequest,
parsedUrl: NextUrlWithParsedQuery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AppConfig } from '../../../../build/utils'
import type { NextRequest } from '../../../web/spec-extension/request'
import type { PrerenderManifest } from '../../../../build'
import type { NextURL } from '../../../web/next-url'
import type { DeepReadonly } from '../../../../shared/lib/deep-readonly'

import {
RouteModule,
Expand Down Expand Up @@ -63,7 +64,7 @@ export type AppRouteModule =
*/
export interface AppRouteRouteHandlerContext extends RouteModuleHandleContext {
renderOpts: StaticGenerationContext['renderOpts']
prerenderManifest: PrerenderManifest
prerenderManifest: DeepReadonly<PrerenderManifest>
}

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/next/src/server/lib/incremental-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
IncrementalCacheKindHint,
} from '../../response-cache'
import type { Revalidate } from '../revalidate'
import type { DeepReadonly } from '../../../shared/lib/deep-readonly'

import FetchCache from './fetch-cache'
import FileSystemCache from './file-system-cache'
Expand Down Expand Up @@ -67,7 +68,7 @@ export class IncrementalCache implements IncrementalCacheType {
readonly disableForTestmode?: boolean
readonly cacheHandler?: CacheHandler
readonly hasCustomCacheHandler: boolean
readonly prerenderManifest: PrerenderManifest
readonly prerenderManifest: DeepReadonly<PrerenderManifest>
readonly requestHeaders: Record<string, undefined | string | string[]>
readonly requestProtocol?: 'http' | 'https'
readonly allowedRevalidateHeaderKeys?: string[]
Expand Down Expand Up @@ -115,7 +116,7 @@ export class IncrementalCache implements IncrementalCacheType {
allowedRevalidateHeaderKeys?: string[]
requestHeaders: IncrementalCache['requestHeaders']
maxMemoryCacheSize?: number
getPrerenderManifest: () => PrerenderManifest
getPrerenderManifest: () => DeepReadonly<PrerenderManifest>
fetchCacheKeyPrefix?: string
CurCacheHandler?: typeof CacheHandler
experimental: { ppr: boolean }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PrerenderManifest } from '../../../build'
import type { DeepReadonly } from '../../../shared/lib/deep-readonly'
import type { Revalidate } from '../revalidate'

/**
Expand All @@ -18,7 +19,9 @@ export class SharedRevalidateTimings {
* The prerender manifest that contains the initial revalidate timings for
* routes.
*/
private readonly prerenderManifest: Pick<PrerenderManifest, 'routes'>
private readonly prerenderManifest: DeepReadonly<
Pick<PrerenderManifest, 'routes'>
>
) {}

/**
Expand Down
Loading

0 comments on commit b016c3c

Please sign in to comment.