mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## 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`
140 lines
3.8 KiB
TypeScript
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'
|
|
)
|
|
})
|
|
})
|