Files
next.js/test/unit/write-app-declarations.test.ts
Ben GublerandGitHub 46e2114ea2 feat: generate root params types (#91019)
## Summary

Generate type definitions for `next/root-params` from App Router root
layouts.

This adds a `root-params.d.ts` file under `.next/types` during build,
`next dev`, and `next typegen`, so imports from `next/root-params` get
accurate autocomplete and type checking.

## What Changed

- Collect root param metadata from layout routes in the route types
manifest
- Generate `.next/types/root-params.d.ts` for `next/root-params`
- Emit root param types in `next build/dev/typegen`
- Infer `undefined` only when a param is not present in every root
layout
- Model mixed root param shapes correctly as unions
- For example, `string | string[] | undefined` when the same param name
appears with different shapes across roots
- Remove stale `root-params.d.ts` when the feature is disabled or there
are no root params

## Why

`next/root-params` already works at runtime, but its types were not
tailored to the current app.

This change closes that gap by generating declarations from the app's
actual root layouts. Required params stay required for single-root apps,
while multi-root apps correctly expose params that may be missing or
have mixed segment shapes.

## Testing

- Added focused unit tests for root param type generation
- Added a typecheck e2e test for generated `root-params.d.ts`
2026-04-08 19:04:12 +02:00

140 lines
3.8 KiB
TypeScript

/* eslint-env jest */
import os from 'os'
import fs from 'fs-extra'
import { join } from 'path'
import { writeAppTypeDeclarations } from 'next/dist/lib/typescript/writeAppTypeDeclarations'
const fixtureDir = join(__dirname, 'fixtures/app-declarations')
const declarationFile = join(fixtureDir, 'next-env.d.ts')
const imageImportsEnabled = false
describe('find config', () => {
beforeEach(async () => {
await fs.ensureDir(fixtureDir)
})
afterEach(() => fs.remove(declarationFile))
it('should preserve CRLF EOL', async () => {
const eol = '\r\n'
const content =
'/// <reference types="next" />' +
eol +
(imageImportsEnabled
? '/// <reference types="next/image-types/global" />' + eol
: '') +
`import "./.next/types/routes.d.ts";` +
eol +
eol +
'// NOTE: This file should not be edited' +
eol +
'// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' +
eol
await fs.writeFile(declarationFile, content)
await writeAppTypeDeclarations({
baseDir: fixtureDir,
distDir: '.next',
imageImportsEnabled,
hasPagesDir: false,
hasAppDir: false,
strictRouteTypes: false,
typedRoutes: true,
rootParams: false,
})
expect(await fs.readFile(declarationFile, 'utf8')).toBe(content)
})
it('should preserve LF EOL', async () => {
const eol = '\n'
const content =
'/// <reference types="next" />' +
eol +
(imageImportsEnabled
? '/// <reference types="next/image-types/global" />' + eol
: '') +
`import "./.next/types/routes.d.ts";` +
eol +
eol +
'// NOTE: This file should not be edited' +
eol +
'// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' +
eol
await fs.writeFile(declarationFile, content)
await writeAppTypeDeclarations({
baseDir: fixtureDir,
distDir: '.next',
imageImportsEnabled,
hasPagesDir: false,
hasAppDir: false,
strictRouteTypes: false,
typedRoutes: true,
rootParams: false,
})
expect(await fs.readFile(declarationFile, 'utf8')).toBe(content)
})
it('should use OS EOL by default', async () => {
const eol = os.EOL
const content =
'/// <reference types="next" />' +
eol +
(imageImportsEnabled
? '/// <reference types="next/image-types/global" />' + eol
: '') +
`import "./.next/types/routes.d.ts";` +
eol +
eol +
'// NOTE: This file should not be edited' +
eol +
'// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.' +
eol
await writeAppTypeDeclarations({
baseDir: fixtureDir,
distDir: '.next',
imageImportsEnabled,
hasPagesDir: false,
hasAppDir: false,
strictRouteTypes: false,
typedRoutes: true,
rootParams: false,
})
expect(await fs.readFile(declarationFile, 'utf8')).toBe(content)
})
it('should include navigation types if app directory is enabled', async () => {
await writeAppTypeDeclarations({
baseDir: fixtureDir,
distDir: '.next',
imageImportsEnabled,
hasPagesDir: false,
hasAppDir: true,
strictRouteTypes: false,
typedRoutes: true,
rootParams: false,
})
await expect(fs.readFile(declarationFile, 'utf8')).resolves.not.toContain(
'next/navigation-types/compat/navigation'
)
await writeAppTypeDeclarations({
baseDir: fixtureDir,
distDir: '.next',
imageImportsEnabled,
hasPagesDir: true,
hasAppDir: true,
strictRouteTypes: false,
typedRoutes: true,
rootParams: false,
})
await expect(fs.readFile(declarationFile, 'utf8')).resolves.toContain(
'next/navigation-types/compat/navigation'
)
})
})