Skip to content

Commit 481684d

Browse files
committed
feat: rootParams
1 parent e08fe27 commit 481684d

File tree

37 files changed

+649
-6
lines changed

37 files changed

+649
-6
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,5 +611,7 @@
611611
"610": "Could not find a production build in the '%s' directory. Try building your app with 'next build' before starting the static export. https://nextjs.org/docs/messages/next-export-no-build-id",
612612
"611": "Route %s with \\`dynamic = \"error\"\\` couldn't be rendered statically because it used \\`request.%s\\`.",
613613
"612": "ServerPrerenderStreamResult cannot be consumed as a stream because it is not yet complete. status: %s",
614-
"613": "Expected the input to be `string | string[]`"
614+
"613": "Expected the input to be `string | string[]`",
615+
"614": "Route %s used \"unstable_rootParams\" inside \"use cache\". This is not currently supported.",
616+
"615": "Missing workStore in unstable_rootParams"
615617
}

packages/next/server.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
1414
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
1515
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
1616
export { after } from 'next/dist/server/after'
17+
export { unstable_rootParams } from 'next/dist/server/request/root-params'
1718
export { connection } from 'next/dist/server/request/connection'
1819
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
1920
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'

packages/next/server.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const serverExports = {
1313
.URLPattern,
1414
after: require('next/dist/server/after').after,
1515
connection: require('next/dist/server/request/connection').connection,
16+
unstable_rootParams: require('next/dist/server/request/root-params')
17+
.unstable_rootParams,
1618
}
1719

1820
// https://nodejs.org/api/esm.html#commonjs-namespaces
@@ -28,3 +30,4 @@ exports.userAgent = serverExports.userAgent
2830
exports.URLPattern = serverExports.URLPattern
2931
exports.after = serverExports.after
3032
exports.connection = serverExports.connection
33+
exports.unstable_rootParams = serverExports.unstable_rootParams

packages/next/src/build/webpack/plugins/next-types-plugin/index.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ async function collectNamedSlots(layoutPath: string) {
237237
// possible to provide the same experience for dynamic routes.
238238

239239
const pluginState = getProxiedPluginState({
240+
collectedRootParams: {} as Record<string, string[]>,
240241
routeTypes: {
241242
edge: {
242243
static: '',
@@ -584,6 +585,103 @@ function formatTimespanWithSeconds(seconds: undefined | number): string {
584585
return text + ' (' + descriptive + ')'
585586
}
586587

588+
function getRootParamsFromLayouts(layouts: Record<string, string[]>) {
589+
// Sort layouts by depth (descending)
590+
const sortedLayouts = Object.entries(layouts).sort(
591+
(a, b) => b[0].split('/').length - a[0].split('/').length
592+
)
593+
594+
if (!sortedLayouts.length) {
595+
return []
596+
}
597+
598+
// we assume the shorted layout path is the root layout
599+
let rootLayout = sortedLayouts[sortedLayouts.length - 1][0]
600+
601+
let rootParams = new Set<string>()
602+
let isMultipleRootLayouts = false
603+
604+
for (const [layoutPath, params] of sortedLayouts) {
605+
const allSegmentsAreDynamic = layoutPath
606+
.split('/')
607+
.slice(1, -1)
608+
// match dynamic params but not catch-all or optional catch-all
609+
.every((segment) => /^\[[^[.\]]+\]$/.test(segment))
610+
611+
if (allSegmentsAreDynamic) {
612+
if (isSubpath(rootLayout, layoutPath)) {
613+
// Current path is a subpath of the root layout, update root
614+
rootLayout = layoutPath
615+
rootParams = new Set(params)
616+
} else {
617+
// Found another potential root layout
618+
isMultipleRootLayouts = true
619+
// Add any new params
620+
for (const param of params) {
621+
rootParams.add(param)
622+
}
623+
}
624+
}
625+
}
626+
627+
// Create result array
628+
const result = Array.from(rootParams).map((param) => ({
629+
param,
630+
optional: isMultipleRootLayouts,
631+
}))
632+
633+
return result
634+
}
635+
636+
function isSubpath(parentLayoutPath: string, potentialChildLayoutPath: string) {
637+
// we strip off the `layout` part of the path as those will always conflict with being a subpath
638+
const parentSegments = parentLayoutPath.split('/').slice(1, -1)
639+
const childSegments = potentialChildLayoutPath.split('/').slice(1, -1)
640+
641+
// child segments should be shorter or equal to parent segments to be a subpath
642+
if (childSegments.length > parentSegments.length || !childSegments.length)
643+
return false
644+
645+
// Verify all segment values are equal
646+
return childSegments.every(
647+
(childSegment, index) => childSegment === parentSegments[index]
648+
)
649+
}
650+
651+
function createServerDefinitions(
652+
rootParams: { param: string; optional: boolean }[]
653+
) {
654+
return `
655+
declare module 'next/server' {
656+
657+
import type { AsyncLocalStorage as NodeAsyncLocalStorage } from 'async_hooks'
658+
declare global {
659+
var AsyncLocalStorage: typeof NodeAsyncLocalStorage
660+
}
661+
export { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event'
662+
export { NextRequest } from 'next/dist/server/web/spec-extension/request'
663+
export { NextResponse } from 'next/dist/server/web/spec-extension/response'
664+
export { NextMiddleware, MiddlewareConfig } from 'next/dist/server/web/types'
665+
export { userAgentFromString } from 'next/dist/server/web/spec-extension/user-agent'
666+
export { userAgent } from 'next/dist/server/web/spec-extension/user-agent'
667+
export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
668+
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
669+
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
670+
export { unstable_after } from 'next/dist/server/after'
671+
export { connection } from 'next/dist/server/request/connection'
672+
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
673+
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
674+
export function unstable_rootParams(): Promise<{ ${rootParams
675+
.map(
676+
({ param, optional }) =>
677+
// ensure params with dashes are valid keys
678+
`${param.includes('-') ? `'${param}'` : param}${optional ? '?' : ''}: string`
679+
)
680+
.join(', ')} }>
681+
}
682+
`
683+
}
684+
587685
function createCustomCacheLifeDefinitions(cacheLife: {
588686
[profile: string]: CacheLife
589687
}) {
@@ -855,6 +953,22 @@ export class NextTypesPlugin {
855953
if (!IS_IMPORTABLE) return
856954

857955
if (IS_LAYOUT) {
956+
const rootLayoutPath = normalizeAppPath(
957+
ensureLeadingSlash(
958+
getPageFromPath(
959+
path.relative(this.appDir, mod.resource),
960+
this.pageExtensions
961+
)
962+
)
963+
)
964+
965+
const foundParams = Array.from(
966+
rootLayoutPath.matchAll(/\[(.*?)\]/g),
967+
(match) => match[1]
968+
)
969+
970+
pluginState.collectedRootParams[rootLayoutPath] = foundParams
971+
858972
const slots = await collectNamedSlots(mod.resource)
859973
assets[assetPath] = new sources.RawSource(
860974
createTypeGuardFile(mod.resource, relativeImportPath, {
@@ -933,6 +1047,22 @@ export class NextTypesPlugin {
9331047

9341048
await Promise.all(promises)
9351049

1050+
const rootParams = getRootParamsFromLayouts(
1051+
pluginState.collectedRootParams
1052+
)
1053+
// If we discovered rootParams, we'll override the `next/server` types
1054+
// since we're able to determine the root params at build time.
1055+
if (rootParams.length > 0) {
1056+
const serverTypesPath = path.join(
1057+
assetDirRelative,
1058+
'types/server.d.ts'
1059+
)
1060+
1061+
assets[serverTypesPath] = new sources.RawSource(
1062+
createServerDefinitions(rootParams)
1063+
) as unknown as webpack.sources.RawSource
1064+
}
1065+
9361066
// Support `"moduleResolution": "Node16" | "NodeNext"` with `"type": "module"`
9371067

9381068
const packageJsonAssetPath = path.join(

packages/next/src/server/app-render/create-component-tree.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ async function createComponentTreeInternal({
396396
// Resolve the segment param
397397
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
398398

399+
if (rootLayoutAtThisLevel) {
400+
workStore.rootParams = currentParams
401+
}
402+
399403
//
400404
// TODO: Combine this `map` traversal with the loop below that turns the array
401405
// into an object.

packages/next/src/server/app-render/work-async-storage.external.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { DeepReadonly } from '../../shared/lib/deep-readonly'
77
import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config'
88
import type { AfterContext } from '../after/after-context'
99
import type { CacheLife } from '../use-cache/cache-life'
10+
import type { Params } from '../request/params'
1011

1112
// Share the instance module in the next-shared layer
1213
import { workAsyncStorageInstance } from './work-async-storage-instance' with { 'turbopack-transition': 'next-shared' }
@@ -69,6 +70,8 @@ export interface WorkStore {
6970
Record<string, { files: string[] }>
7071
>
7172
readonly assetPrefix?: string
73+
74+
rootParams: Params
7275
}
7376

7477
export type WorkAsyncStorage = AsyncLocalStorage<WorkStore>

packages/next/src/server/async-storage/work-store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export function createWorkStore({
112112

113113
isDraftMode: renderOpts.isDraftMode,
114114

115+
rootParams: {},
116+
115117
requestEndedState,
116118
isPrefetchRequest,
117119
buildId: renderOpts.buildId,

packages/next/src/server/lib/patch-fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ export function createPatchedFetcher(
670670
)
671671
await handleUnlock()
672672

673-
// We we return a new Response to the caller.
673+
// We return a new Response to the caller.
674674
return new Response(bodyBuffer, {
675675
headers: res.headers,
676676
status: res.status,

packages/next/src/server/request/draft-mode.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class DraftMode {
171171
return false
172172
}
173173
public enable() {
174-
// We we have a store we want to track dynamic data access to ensure we
174+
// We have a store we want to track dynamic data access to ensure we
175175
// don't statically generate routes that manipulate draft mode.
176176
trackDynamicDraftMode('draftMode().enable()')
177177
if (this._provider !== null) {
@@ -224,7 +224,7 @@ function trackDynamicDraftMode(expression: string) {
224224
const store = workAsyncStorage.getStore()
225225
const workUnitStore = workUnitAsyncStorage.getStore()
226226
if (store) {
227-
// We we have a store we want to track dynamic data access to ensure we
227+
// We have a store we want to track dynamic data access to ensure we
228228
// don't statically generate routes that manipulate draft mode.
229229
if (workUnitStore) {
230230
if (workUnitStore.type === 'cache') {

packages/next/src/server/request/params.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ function createPrerenderParams(
160160
prerenderStore
161161
)
162162
}
163-
// remaining cases are prender-ppr and prerender-legacy
163+
// remaining cases are prerender-ppr and prerender-legacy
164164
// We aren't in a dynamicIO prerender but we do have fallback params at this
165165
// level so we need to make an erroring exotic params object which will postpone
166166
// if you access the fallback params

0 commit comments

Comments
 (0)