feat: generate types for next/root-params during build and dev#90150
feat: generate types for next/root-params during build and dev#90150bgub wants to merge 1 commit intovercel:canaryfrom
Conversation
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
Collects root layout dynamic segments during type generation and emits a typed `declare module 'next/root-params'` declaration to .next/types/root-params.d.ts. The declaration is included via the routes.d.ts entry file when experimental.rootParams (or cacheComponents) is enabled. - simple [param] → Promise<string> - [...param] (catch-all) → Promise<string[]> - [[...param]] (optional catch-all) → Promise<string[] | undefined> - multiple root layouts with the same param name use the most permissive type Also adds a typecheck test that builds the existing root-params fixtures and verifies the generated types match expected return types.
There was a problem hiding this comment.
Pull request overview
This PR implements type generation for the next/root-params module, automatically creating typed getter functions based on dynamic segments found in root layouts. Root layouts are identified as the shallowest layout in each branch of the app directory tree. The generated types are written to .next/types/root-params.d.ts during both next build and next dev when experimental.rootParams or cacheComponents is enabled.
Changes:
- Adds
collectRootParamsFromLayoutsfunction to identify root layouts and extract their dynamic segments - Implements
generateRootParamsTypesandwriteRootParamsTypesutilities to create TypeScript declarations - Integrates root params type generation into both build and dev workflows
- Replaces magic number
0xfffffffewithINFINITE_CACHEconstant in cache-life utilities (from PR #90146)
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/e2e/app-dir/app-root-params-getters/typecheck.test.ts | Adds tests verifying generated types for simple and multiple-roots fixtures |
| test/e2e/app-dir/app-root-params-getters/fixtures/simple/type-tests.ts | TypeScript test file to validate generated type signatures |
| packages/next/src/server/lib/router-utils/setup-dev-bundler.ts | Integrates root params type generation in dev mode |
| packages/next/src/server/lib/router-utils/route-types-utils.ts | Adds root layout detection, param collection, and file generation logic |
| packages/next/src/server/lib/router-utils/root-params-type-utils.ts | Implements type generation and file writing for root params |
| packages/next/src/server/lib/router-utils/cache-life-type-utils.ts | Replaces magic number with INFINITE_CACHE constant |
| packages/next/src/build/index.ts | Integrates root params and cache-life type generation in build mode |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const kind: RootParamKind = | ||
| param.paramType === 'optional-catchall' | ||
| ? 'optional-catchall' | ||
| : param.paramType === 'catchall' | ||
| ? 'catchall' | ||
| : 'dynamic' |
There was a problem hiding this comment.
The current logic doesn't correctly handle intercepted route param types. When getSegmentParam returns param types like 'catchall-intercepted-(.)' or 'dynamic-intercepted-(...)', the direct equality checks will fail, causing intercepted catchall routes to be incorrectly classified as 'dynamic'.
Instead, import and use the existing getParamProperties helper from '../../../shared/lib/router/utils/get-segment-param':
Add to imports:
import { getSegmentParam, getParamProperties } from '../../../shared/lib/router/utils/get-segment-param'Replace lines 197-202 with:
const { repeat, optional } = getParamProperties(param.paramType)
const kind: RootParamKind =
repeat && optional
? 'optional-catchall'
: repeat
? 'catchall'
: 'dynamic'This handles all param type variants including intercepted routes.
| export function writeRootParamsTypesFile( | ||
| manifest: RouteTypesManifest, | ||
| filePath: string | ||
| ) { | ||
| writeRootParamsTypes(manifest.rootParams, filePath) | ||
| } |
There was a problem hiding this comment.
For consistency, this function should be declared as async to match the pattern used by writeCacheLifeTypesFile at line 529. Even though the underlying writeRootParamsTypes call is synchronous, using async maintains consistency with other file-writing functions in this module and allows for future async operations if needed.
| 'types', | ||
| 'root-params.d.ts' | ||
| ) | ||
| writeRootParamsTypesFile( |
There was a problem hiding this comment.
Missing await before writeRootParamsTypesFile. Even though the function is currently synchronous, it should be called with await for consistency with line 1425 where writeCacheLifeTypesFile is awaited, and to prevent issues if the function becomes async in the future.
| writeRootParamsTypesFile( | |
| await writeRootParamsTypesFile( |
7533724 to
0e17a84
Compare
Summary
Generates a typed `declare module 'next/root-params'` declaration during `next build` and `next dev`, replacing the current blank placeholder in `packages/next/root-params.d.ts`.
What this does:
Type mapping:
If the same param name appears across multiple root layouts with different segment types, the most permissive type is used.
Example output for `app/[lang]/[locale]/layout.tsx` + `app/catch-all/[...path]/layout.tsx`:
```ts
declare module 'next/root-params' {
export function lang(): Promise
export function locale(): Promise
export function path(): Promise<string[]>
}
```
Test plan