Skip to content

Commit d509ed4

Browse files
committed
feat: rootParams
1 parent 491adda commit d509ed4

File tree

24 files changed

+435
-4
lines changed

24 files changed

+435
-4
lines changed

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 { unstable_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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const serverExports = {
1313
.URLPattern,
1414
unstable_after: require('next/dist/server/after').unstable_after,
1515
connection: require('next/dist/server/request/connection').connection,
16+
unstable_rootParams: require('next/dist/server/request/root-params'),
1617
}
1718

1819
// https://nodejs.org/api/esm.html#commonjs-namespaces
@@ -28,3 +29,4 @@ exports.userAgent = serverExports.userAgent
2829
exports.URLPattern = serverExports.URLPattern
2930
exports.unstable_after = serverExports.unstable_after
3031
exports.connection = serverExports.connection
32+
exports.unstable_rootParams = serverExports.unstable_rootParams

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ function formatRouteToRouteType(route: string) {
283283

284284
// Whether redirects and rewrites have been converted into routeTypes or not.
285285
let redirectsRewritesTypesProcessed = false
286+
let collectedRootParams: Map<string, string[]> = new Map()
286287

287288
// Convert redirects and rewrites into routeTypes.
288289
function addRedirectsRewritesRouteTypes(
@@ -584,6 +585,63 @@ function formatTimespanWithSeconds(seconds: undefined | number): string {
584585
return text + ' (' + descriptive + ')'
585586
}
586587

588+
function getRootParamsFromLayouts(layoutsMap: Map<string, string[]>) {
589+
let shortestLayoutLength = Infinity
590+
const rootLayouts: { route: string; params: string[] }[] = []
591+
const allRootParams = new Set<string>()
592+
593+
for (const [route, params] of layoutsMap.entries()) {
594+
const segments = route.split('/')
595+
if (segments.length <= shortestLayoutLength) {
596+
if (segments.length < shortestLayoutLength) {
597+
rootLayouts.length = 0 // Clear previous layouts if we found a shorter one
598+
shortestLayoutLength = segments.length
599+
}
600+
rootLayouts.push({ route, params })
601+
params.forEach((param) => allRootParams.add(param))
602+
}
603+
}
604+
605+
const result = Array.from(allRootParams).map((param) => ({
606+
param,
607+
// if we detected multiple root layouts and not all of them have the param,
608+
// then it needs to be marked optional in the type.
609+
optional: !rootLayouts.every((layout) => layout.params.includes(param)),
610+
}))
611+
612+
return result
613+
}
614+
615+
function createServerDefinitions(
616+
rootParams: { param: string; optional: boolean }[]
617+
) {
618+
return `
619+
declare module 'next/server' {
620+
621+
import type { AsyncLocalStorage as NodeAsyncLocalStorage } from 'async_hooks'
622+
declare global {
623+
var AsyncLocalStorage: typeof NodeAsyncLocalStorage
624+
}
625+
export { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event'
626+
export { NextRequest } from 'next/dist/server/web/spec-extension/request'
627+
export { NextResponse } from 'next/dist/server/web/spec-extension/response'
628+
export { NextMiddleware, MiddlewareConfig } from 'next/dist/server/web/types'
629+
export { userAgentFromString } from 'next/dist/server/web/spec-extension/user-agent'
630+
export { userAgent } from 'next/dist/server/web/spec-extension/user-agent'
631+
export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
632+
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
633+
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
634+
export { unstable_after } from 'next/dist/server/after'
635+
export { connection } from 'next/dist/server/request/connection'
636+
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
637+
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
638+
export function unstable_rootParams(): Promise<{ ${rootParams
639+
.map(({ param, optional }) => `${param}${optional ? '?' : ''}: string`)
640+
.join(', ')} }>
641+
}
642+
`
643+
}
644+
587645
function createCustomCacheLifeDefinitions(cacheLife: {
588646
[profile: string]: CacheLife
589647
}) {
@@ -855,6 +913,22 @@ export class NextTypesPlugin {
855913
if (!IS_IMPORTABLE) return
856914

857915
if (IS_LAYOUT) {
916+
const rootLayoutPath = normalizeAppPath(
917+
ensureLeadingSlash(
918+
getPageFromPath(
919+
path.relative(this.appDir, mod.resource),
920+
this.pageExtensions
921+
)
922+
)
923+
)
924+
925+
const foundParams = Array.from(
926+
rootLayoutPath.matchAll(/\[(.*?)\]/g),
927+
(match) => match[1]
928+
)
929+
930+
collectedRootParams.set(rootLayoutPath, foundParams)
931+
858932
const slots = await collectNamedSlots(mod.resource)
859933
assets[assetPath] = new sources.RawSource(
860934
createTypeGuardFile(mod.resource, relativeImportPath, {
@@ -933,6 +1007,20 @@ export class NextTypesPlugin {
9331007

9341008
await Promise.all(promises)
9351009

1010+
const rootParams = getRootParamsFromLayouts(collectedRootParams)
1011+
// If we discovered rootParams, we'll override the `next/server` types
1012+
// since we're able to determine the root params at build time.
1013+
if (rootParams.length > 0) {
1014+
const serverTypesPath = path.join(
1015+
assetDirRelative,
1016+
'types/server.d.ts'
1017+
)
1018+
1019+
assets[serverTypesPath] = new sources.RawSource(
1020+
createServerDefinitions(rootParams)
1021+
) as unknown as webpack.sources.RawSource
1022+
}
1023+
9361024
// Support `"moduleResolution": "Node16" | "NodeNext"` with `"type": "module"`
9371025

9381026
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
@@ -331,6 +331,10 @@ async function createComponentTreeInternal({
331331
// Resolve the segment param
332332
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
333333

334+
if (rootLayoutAtThisLevel) {
335+
workStore.rootParams = currentParams
336+
}
337+
334338
//
335339
// TODO: Combine this `map` traversal with the loop below that turns the array
336340
// 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 { workAsyncStorage } 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
@@ -669,7 +669,7 @@ export function createPatchedFetcher(
669669
)
670670
await handleUnlock()
671671

672-
// We we return a new Response to the caller.
672+
// We return a new Response to the caller.
673673
return new Response(bodyBuffer, {
674674
headers: res.headers,
675675
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
@@ -176,7 +176,7 @@ class DraftMode {
176176
return false
177177
}
178178
public enable() {
179-
// We we have a store we want to track dynamic data access to ensure we
179+
// We have a store we want to track dynamic data access to ensure we
180180
// don't statically generate routes that manipulate draft mode.
181181
trackDynamicDraftMode('draftMode().enable()')
182182
if (this._provider !== null) {
@@ -229,7 +229,7 @@ function trackDynamicDraftMode(expression: string) {
229229
const store = workAsyncStorage.getStore()
230230
const workUnitStore = workUnitAsyncStorage.getStore()
231231
if (store) {
232-
// We we have a store we want to track dynamic data access to ensure we
232+
// We have a store we want to track dynamic data access to ensure we
233233
// don't statically generate routes that manipulate draft mode.
234234
if (workUnitStore) {
235235
if (workUnitStore.type === 'cache') {

0 commit comments

Comments
 (0)