Skip to content

Commit

Permalink
feat: rootParams
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner committed Nov 15, 2024
1 parent 491adda commit d509ed4
Show file tree
Hide file tree
Showing 24 changed files with 435 additions and 4 deletions.
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'
2 changes: 2 additions & 0 deletions packages/next/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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'),
}

// https://nodejs.org/api/esm.html#commonjs-namespaces
Expand All @@ -28,3 +29,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
88 changes: 88 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 @@ -283,6 +283,7 @@ function formatRouteToRouteType(route: string) {

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

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

function getRootParamsFromLayouts(layoutsMap: Map<string, string[]>) {
let shortestLayoutLength = Infinity
const rootLayouts: { route: string; params: string[] }[] = []
const allRootParams = new Set<string>()

for (const [route, params] of layoutsMap.entries()) {
const segments = route.split('/')
if (segments.length <= shortestLayoutLength) {
if (segments.length < shortestLayoutLength) {
rootLayouts.length = 0 // Clear previous layouts if we found a shorter one
shortestLayoutLength = segments.length
}
rootLayouts.push({ route, params })
params.forEach((param) => allRootParams.add(param))
}
}

const result = Array.from(allRootParams).map((param) => ({
param,
// if we detected multiple root layouts and not all of them have the param,
// then it needs to be marked optional in the type.
optional: !rootLayouts.every((layout) => layout.params.includes(param)),
}))

return result
}

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 }) => `${param}${optional ? '?' : ''}: string`)
.join(', ')} }>
}
`
}

function createCustomCacheLifeDefinitions(cacheLife: {
[profile: string]: CacheLife
}) {
Expand Down Expand Up @@ -855,6 +913,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]
)

collectedRootParams.set(rootLayoutPath, foundParams)

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

await Promise.all(promises)

const rootParams = getRootParamsFromLayouts(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
Loading

0 comments on commit d509ed4

Please sign in to comment.