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
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -631,5 +631,6 @@
"630": "Invariant (SlowModuleDetectionPlugin): Module is recorded after the report is generated. This is a Next.js internal bug.",
"631": "Invariant (SlowModuleDetectionPlugin): Circular dependency detected in module graph. This is a Next.js internal bug.",
"632": "Invariant (SlowModuleDetectionPlugin): Module is missing a required debugId. This is a Next.js internal bug.",
"633": "Dynamic route not found"
"633": "Dynamic route not found",
"634": "Route %s used \"searchParams\" inside \"use cache\". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use \"searchParams\" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache"
}
2 changes: 1 addition & 1 deletion packages/next/src/build/segment-config/app/app-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
isAppRouteRouteModule,
isAppPageRouteModule,
} from '../../../server/route-modules/checks'
import { isClientReference } from '../../../lib/client-reference'
import { isClientReference } from '../../../lib/client-and-server-references'
import { getSegmentParam } from '../../../server/app-render/get-segment-param'
import {
getLayoutOrPageModule,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { Sema } from 'next/dist/compiled/async-sema'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { getRuntimeContext } from '../server/web/sandbox'
import { isClientReference } from '../lib/client-reference'
import { isClientReference } from '../lib/client-and-server-references'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { denormalizeAppPagePath } from '../shared/lib/page-path/denormalize-app-path'
import { RouteKind } from '../server/route-kind'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { hasBasePath } from '../../../has-base-path'
import {
extractInfoFromServerReferenceId,
omitUnusedArgs,
} from './server-reference-info'
} from '../../../../shared/lib/server-reference-info'
import { revalidateEntireCache } from '../../segment-cache/cache'

type FetchServerActionResult = {
Expand Down
33 changes: 33 additions & 0 deletions packages/next/src/lib/client-and-server-references.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { extractInfoFromServerReferenceId } from '../shared/lib/server-reference-info'

// Only contains the properties we're interested in.
export interface ServerReference {
$$typeof: Symbol
$$id: string
}

export type ServerFunction = ServerReference &
((...args: unknown[]) => Promise<unknown>)

export function isServerReference<T>(
value: T & Partial<ServerReference>
): value is T & ServerFunction {
return value.$$typeof === Symbol.for('react.server.reference')
}

export function isUseCacheFunction<T>(
value: T & Partial<ServerReference>
): value is T & ServerFunction {
if (!isServerReference(value)) {
return false
}

const { type } = extractInfoFromServerReferenceId(value.$$id)

return type === 'use-cache'
}

export function isClientReference(mod: any): boolean {
const defaultExport = mod?.default || mod
return defaultExport?.$$typeof === Symbol.for('react.client.reference')
}
4 changes: 0 additions & 4 deletions packages/next/src/lib/client-reference.ts

This file was deleted.

49 changes: 39 additions & 10 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { CacheNodeSeedData, PreloadCallbacks } from './types'
import React from 'react'
import { isClientReference } from '../../lib/client-reference'
import {
isClientReference,
isUseCacheFunction,
} from '../../lib/client-and-server-references'
import { getLayoutOrPageModule } from '../lib/app-dir-module'
import type { LoaderTree } from '../lib/app-dir-module'
import { interopDefault } from './interop-default'
Expand All @@ -19,6 +22,7 @@ import type { Params } from '../request/params'
import { workUnitAsyncStorage } from './work-unit-async-storage.external'
import { OUTLET_BOUNDARY_NAME } from '../../lib/metadata/metadata-constants'
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
import type { UseCachePageComponentProps } from '../use-cache/use-cache-wrapper'

/**
* Use the provided loader tree to create the React Component tree.
Expand Down Expand Up @@ -652,19 +656,44 @@ async function createComponentTreeInternal({
)
}
} else {
// If we are passing searchParams to a server component Page we need to track their usage in case
// the current render mode tracks dynamic API usage.
// If we are passing params to a server component Page we need to track
// their usage in case the current render mode tracks dynamic API usage.
const params = createServerParamsForServerSegment(
currentParams,
workStore
)
const searchParams = createServerSearchParamsForServerPage(
query,
workStore
)
pageElement = (
<PageComponent params={params} searchParams={searchParams} />
)

// TODO(useCache): Should we use this trick also if dynamicIO is enabled,
// instead of relying on the searchParams being a hanging promise?
if (!experimental.dynamicIO && isUseCacheFunction(PageComponent)) {
const UseCachePageComponent: React.ComponentType<UseCachePageComponentProps> =
PageComponent

// The "use cache" wrapper takes care of converting this into an
// erroring search params promise when passing it to the original
// function.
const searchParams = Promise.resolve({})

pageElement = (
<UseCachePageComponent
params={params}
searchParams={searchParams}
$$isPageComponent
/>
)
} else {
// If we are passing searchParams to a server component Page we need to
// track their usage in case the current render mode tracks dynamic API
// usage.
const searchParams = createServerSearchParamsForServerPage(
query,
workStore
)

pageElement = (
<PageComponent params={params} searchParams={searchParams} />
)
}
}
return [
actualSegment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface WorkStore {
readonly assetPrefix?: string

rootParams: Params
dynamicIOEnabled: boolean
}

export type WorkAsyncStorage = AsyncLocalStorage<WorkStore>
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/async-storage/work-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function createWorkStore({
assetPrefix: renderOpts?.assetPrefix || '',

afterContext: createAfterContext(renderOpts),
dynamicIOEnabled: renderOpts.experimental.dynamicIO,
}

// TODO: remove this when we resolve accessing the store outside the execution context
Expand Down
174 changes: 82 additions & 92 deletions packages/next/src/server/request/search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
describeHasCheckingStringProperty,
throwWithStaticGenerationBailoutErrorWithDynamicError,
wellKnownProperties,
throwForSearchParamsAccessInUseCache,
} from './utils'
import { scheduleImmediate } from '../../lib/scheduler'

Expand Down Expand Up @@ -167,6 +168,11 @@ function createRenderSearchParams(
interface CacheLifetime {}
const CachedSearchParams = new WeakMap<CacheLifetime, Promise<SearchParams>>()

const CachedSearchParamsForUseCache = new WeakMap<
CacheLifetime,
Promise<SearchParams>
>()

function makeAbortingExoticSearchParams(
route: string,
prerenderStore: PrerenderStoreModern
Expand Down Expand Up @@ -203,31 +209,9 @@ function makeAbortingExoticSearchParams(
annotateDynamicAccess(expression, prerenderStore)
return ReflectAdapter.get(target, prop, receiver)
}
// Object prototype
case 'hasOwnProperty':
case 'isPrototypeOf':
case 'propertyIsEnumerable':
case 'toString':
case 'valueOf':
case 'toLocaleString':

// Promise prototype
// fallthrough
case 'catch':
case 'finally':

// Common tested properties
// fallthrough
case 'toJSON':
case '$$typeof':
case '__esModule': {
// These properties cannot be shadowed because they need to be the
// true underlying value for Promises to work correctly at runtime
return ReflectAdapter.get(target, prop, receiver)
}

default: {
if (typeof prop === 'string') {
if (typeof prop === 'string' && !wellKnownProperties.has(prop)) {
const expression = describeStringPropertyAccess(
'searchParams',
prop
Expand Down Expand Up @@ -306,28 +290,6 @@ function makeErroringExoticSearchParams(
}

switch (prop) {
// Object prototype
case 'hasOwnProperty':
case 'isPrototypeOf':
case 'propertyIsEnumerable':
case 'toString':
case 'valueOf':
case 'toLocaleString':

// Promise prototype
// fallthrough
case 'catch':
case 'finally':

// Common tested properties
// fallthrough
case 'toJSON':
case '$$typeof':
case '__esModule': {
// These properties cannot be shadowed because they need to be the
// true underlying value for Promises to work correctly at runtime
return ReflectAdapter.get(target, prop, receiver)
}
case 'then': {
const expression =
'`await searchParams`, `searchParams.then`, or similar'
Expand Down Expand Up @@ -379,7 +341,7 @@ function makeErroringExoticSearchParams(
return
}
default: {
if (typeof prop === 'string') {
if (typeof prop === 'string' && !wellKnownProperties.has(prop)) {
const expression = describeStringPropertyAccess(
'searchParams',
prop
Expand Down Expand Up @@ -469,6 +431,63 @@ function makeErroringExoticSearchParams(
return proxiedPromise
}

/**
* This is a variation of `makeErroringExoticSearchParams` that always throws an
* error on access, because accessing searchParams inside of `"use cache"` is
* not allowed.
*/
export function makeErroringExoticSearchParamsForUseCache(
workStore: WorkStore
): Promise<SearchParams> {
const cachedSearchParams = CachedSearchParamsForUseCache.get(workStore)
if (cachedSearchParams) {
return cachedSearchParams
}

const promise = Promise.resolve({})

const proxiedPromise = new Proxy(promise, {
get(target, prop, receiver) {
if (Object.hasOwn(promise, prop)) {
// The promise has this property directly. we must return it. We know it
// isn't a dynamic access because it can only be something that was
// previously written to the promise and thus not an underlying
// searchParam value
return ReflectAdapter.get(target, prop, receiver)
}

if (
typeof prop === 'string' &&
(prop === 'then' || !wellKnownProperties.has(prop))
) {
throwForSearchParamsAccessInUseCache(workStore.route)
}

return ReflectAdapter.get(target, prop, receiver)
},
has(target, prop) {
// We don't expect key checking to be used except for testing the existence of
// searchParams so we make all has tests throw an error. this means that `promise.then`
// can resolve to the then function on the Promise prototype but 'then' in promise will assume
// you are testing whether the searchParams has a 'then' property.
if (
typeof prop === 'string' &&
(prop === 'then' || !wellKnownProperties.has(prop))
) {
throwForSearchParamsAccessInUseCache(workStore.route)
}

return ReflectAdapter.has(target, prop)
},
ownKeys() {
throwForSearchParamsAccessInUseCache(workStore.route)
},
})

CachedSearchParamsForUseCache.set(workStore, proxiedPromise)
return proxiedPromise
}

function makeUntrackedExoticSearchParams(
underlyingSearchParams: SearchParams,
store: WorkStore
Expand All @@ -485,52 +504,23 @@ function makeUntrackedExoticSearchParams(
CachedSearchParams.set(underlyingSearchParams, promise)

Object.keys(underlyingSearchParams).forEach((prop) => {
switch (prop) {
// Object prototype
case 'hasOwnProperty':
case 'isPrototypeOf':
case 'propertyIsEnumerable':
case 'toString':
case 'valueOf':
case 'toLocaleString':

// Promise prototype
// fallthrough
case 'then':
case 'catch':
case 'finally':

// React Promise extension
// fallthrough
case 'status':

// Common tested properties
// fallthrough
case 'toJSON':
case '$$typeof':
case '__esModule': {
// These properties cannot be shadowed because they need to be the
// true underlying value for Promises to work correctly at runtime
break
}
default: {
Object.defineProperty(promise, prop, {
get() {
const workUnitStore = workUnitAsyncStorage.getStore()
trackDynamicDataInDynamicRender(store, workUnitStore)
return underlyingSearchParams[prop]
},
set(value) {
Object.defineProperty(promise, prop, {
value,
writable: true,
enumerable: true,
})
},
enumerable: true,
configurable: true,
})
}
if (!wellKnownProperties.has(prop)) {
Object.defineProperty(promise, prop, {
get() {
const workUnitStore = workUnitAsyncStorage.getStore()
trackDynamicDataInDynamicRender(store, workUnitStore)
return underlyingSearchParams[prop]
},
set(value) {
Object.defineProperty(promise, prop, {
value,
writable: true,
enumerable: true,
})
},
enumerable: true,
configurable: true,
})
}
})

Expand Down
Loading