Skip to content

Commit

Permalink
feat: rootParams
Browse files Browse the repository at this point in the history
  • Loading branch information
ztanner authored and wyattjoh committed Dec 16, 2024
1 parent e9415fc commit 96ada8c
Show file tree
Hide file tree
Showing 37 changed files with 650 additions and 7 deletions.
6 changes: 4 additions & 2 deletions packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -611,5 +611,7 @@
"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",
"611": "Route %s with \\`dynamic = \"error\"\\` couldn't be rendered statically because it used \\`request.%s\\`.",
"612": "ServerPrerenderStreamResult cannot be consumed as a stream because it is not yet complete. status: %s",
"613": "Expected the input to be `string | string[]`"
}
"613": "Expected the input to be `string | string[]`",
"614": "Route %s used \"unstable_rootParams\" inside \"use cache\". This is not currently supported.",
"615": "Missing workStore in unstable_rootParams"
}
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 { 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,
after: require('next/dist/server/after').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.after = serverExports.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 @@ -396,6 +396,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 { workAsyncStorageInstance } 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 @@ -670,7 +670,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 @@ -171,7 +171,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 @@ -224,7 +224,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

0 comments on commit 96ada8c

Please sign in to comment.