Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rootParams #72837

Draft
wants to merge 1 commit into
base: canary
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions packages/next/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
export { unstable_after } from 'next/dist/server/after'
export { unstable_rootParams } from 'next/dist/server/request/root-params'
export { connection } from 'next/dist/server/request/connection'
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
3 changes: 3 additions & 0 deletions packages/next/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const serverExports = {
.URLPattern,
unstable_after: require('next/dist/server/after').unstable_after,
connection: require('next/dist/server/request/connection').connection,
unstable_rootParams: require('next/dist/server/request/root-params')
.unstable_rootParams,
}

// https://nodejs.org/api/esm.html#commonjs-namespaces
Expand All @@ -28,3 +30,4 @@ exports.userAgent = serverExports.userAgent
exports.URLPattern = serverExports.URLPattern
exports.unstable_after = serverExports.unstable_after
exports.connection = serverExports.connection
exports.unstable_rootParams = serverExports.unstable_rootParams
130 changes: 130 additions & 0 deletions packages/next/src/build/webpack/plugins/next-types-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ async function collectNamedSlots(layoutPath: string) {
// possible to provide the same experience for dynamic routes.

const pluginState = getProxiedPluginState({
collectedRootParams: {} as Record<string, string[]>,
routeTypes: {
edge: {
static: '',
Expand Down Expand Up @@ -584,6 +585,103 @@ function formatTimespanWithSeconds(seconds: undefined | number): string {
return text + ' (' + descriptive + ')'
}

function getRootParamsFromLayouts(layouts: Record<string, string[]>) {
// Sort layouts by depth (descending)
const sortedLayouts = Object.entries(layouts).sort(
(a, b) => b[0].split('/').length - a[0].split('/').length
)

if (!sortedLayouts.length) {
return []
}

// we assume the shorted layout path is the root layout
let rootLayout = sortedLayouts[sortedLayouts.length - 1][0]

let rootParams = new Set<string>()
let isMultipleRootLayouts = false

for (const [layoutPath, params] of sortedLayouts) {
const allSegmentsAreDynamic = layoutPath
.split('/')
.slice(1, -1)
// match dynamic params but not catch-all or optional catch-all
.every((segment) => /^\[[^[.\]]+\]$/.test(segment))

if (allSegmentsAreDynamic) {
if (isSubpath(rootLayout, layoutPath)) {
// Current path is a subpath of the root layout, update root
rootLayout = layoutPath
rootParams = new Set(params)
} else {
// Found another potential root layout
isMultipleRootLayouts = true
// Add any new params
for (const param of params) {
rootParams.add(param)
}
}
}
}

// Create result array
const result = Array.from(rootParams).map((param) => ({
param,
optional: isMultipleRootLayouts,
}))

return result
}

function isSubpath(parentLayoutPath: string, potentialChildLayoutPath: string) {
// we strip off the `layout` part of the path as those will always conflict with being a subpath
const parentSegments = parentLayoutPath.split('/').slice(1, -1)
const childSegments = potentialChildLayoutPath.split('/').slice(1, -1)

// child segments should be shorter or equal to parent segments to be a subpath
if (childSegments.length > parentSegments.length || !childSegments.length)
return false

// Verify all segment values are equal
return childSegments.every(
(childSegment, index) => childSegment === parentSegments[index]
)
}

function createServerDefinitions(
rootParams: { param: string; optional: boolean }[]
) {
return `
declare module 'next/server' {
import type { AsyncLocalStorage as NodeAsyncLocalStorage } from 'async_hooks'
declare global {
var AsyncLocalStorage: typeof NodeAsyncLocalStorage
}
export { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event'
export { NextRequest } from 'next/dist/server/web/spec-extension/request'
export { NextResponse } from 'next/dist/server/web/spec-extension/response'
export { NextMiddleware, MiddlewareConfig } from 'next/dist/server/web/types'
export { userAgentFromString } from 'next/dist/server/web/spec-extension/user-agent'
export { userAgent } from 'next/dist/server/web/spec-extension/user-agent'
export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
export { unstable_after } from 'next/dist/server/after'
export { connection } from 'next/dist/server/request/connection'
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
export function unstable_rootParams(): Promise<{ ${rootParams
.map(
({ param, optional }) =>
// ensure params with dashes are valid keys
`${param.includes('-') ? `'${param}'` : param}${optional ? '?' : ''}: string`
)
.join(', ')} }>
}
`
}

function createCustomCacheLifeDefinitions(cacheLife: {
[profile: string]: CacheLife
}) {
Expand Down Expand Up @@ -855,6 +953,22 @@ export class NextTypesPlugin {
if (!IS_IMPORTABLE) return

if (IS_LAYOUT) {
const rootLayoutPath = normalizeAppPath(
ensureLeadingSlash(
getPageFromPath(
path.relative(this.appDir, mod.resource),
this.pageExtensions
)
)
)

const foundParams = Array.from(
rootLayoutPath.matchAll(/\[(.*?)\]/g),
(match) => match[1]
)

pluginState.collectedRootParams[rootLayoutPath] = foundParams

const slots = await collectNamedSlots(mod.resource)
assets[assetPath] = new sources.RawSource(
createTypeGuardFile(mod.resource, relativeImportPath, {
Expand Down Expand Up @@ -933,6 +1047,22 @@ export class NextTypesPlugin {

await Promise.all(promises)

const rootParams = getRootParamsFromLayouts(
pluginState.collectedRootParams
)
// If we discovered rootParams, we'll override the `next/server` types
// since we're able to determine the root params at build time.
if (rootParams.length > 0) {
const serverTypesPath = path.join(
assetDirRelative,
'types/server.d.ts'
)

assets[serverTypesPath] = new sources.RawSource(
createServerDefinitions(rootParams)
) as unknown as webpack.sources.RawSource
}

// Support `"moduleResolution": "Node16" | "NodeNext"` with `"type": "module"`

const packageJsonAssetPath = path.join(
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@ async function createComponentTreeInternal({
// Resolve the segment param
const actualSegment = segmentParam ? segmentParam.treeSegment : segment

if (rootLayoutAtThisLevel) {
workStore.rootParams = currentParams
}

//
// TODO: Combine this `map` traversal with the loop below that turns the array
// into an object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { DeepReadonly } from '../../shared/lib/deep-readonly'
import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config'
import type { AfterContext } from '../after/after-context'
import type { CacheLife } from '../use-cache/cache-life'
import type { Params } from '../request/params'

// Share the instance module in the next-shared layer
import { workAsyncStorage } from './work-async-storage-instance' with { 'turbopack-transition': 'next-shared' }
Expand Down Expand Up @@ -69,6 +70,8 @@ export interface WorkStore {
Record<string, { files: string[] }>
>
readonly assetPrefix?: string

rootParams: Params
}

export type WorkAsyncStorage = AsyncLocalStorage<WorkStore>
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/async-storage/work-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export function createWorkStore({

isDraftMode: renderOpts.isDraftMode,

rootParams: {},

requestEndedState,
isPrefetchRequest,
buildId: renderOpts.buildId,
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ export function createPatchedFetcher(
)
await handleUnlock()

// We we return a new Response to the caller.
// We return a new Response to the caller.
return new Response(bodyBuffer, {
headers: res.headers,
status: res.status,
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/request/draft-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class DraftMode {
return false
}
public enable() {
// We we have a store we want to track dynamic data access to ensure we
// We have a store we want to track dynamic data access to ensure we
// don't statically generate routes that manipulate draft mode.
trackDynamicDraftMode('draftMode().enable()')
if (this._provider !== null) {
Expand Down Expand Up @@ -229,7 +229,7 @@ function trackDynamicDraftMode(expression: string) {
const store = workAsyncStorage.getStore()
const workUnitStore = workUnitAsyncStorage.getStore()
if (store) {
// We we have a store we want to track dynamic data access to ensure we
// We have a store we want to track dynamic data access to ensure we
// don't statically generate routes that manipulate draft mode.
if (workUnitStore) {
if (workUnitStore.type === 'cache') {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/request/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function createPrerenderParams(
prerenderStore
)
}
// remaining cases are prender-ppr and prerender-legacy
// remaining cases are prerender-ppr and prerender-legacy
// We aren't in a dynamicIO prerender but we do have fallback params at this
// level so we need to make an erroring exotic params object which will postpone
// if you access the fallback params
Expand Down
Loading
Loading