Skip to content

Commit ec67a97

Browse files
committed
fix: third arg after rebase
1 parent 0528dbb commit ec67a97

File tree

7 files changed

+247
-170
lines changed

7 files changed

+247
-170
lines changed

packages/next/src/cli/next-typegen.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,11 @@ const nextTypegen = async (
129129
rewrites: nextConfig.rewrites,
130130
})
131131

132-
await writeRouteTypesManifest(routeTypesManifest, routeTypesFilePath)
132+
await writeRouteTypesManifest(
133+
routeTypesManifest,
134+
routeTypesFilePath,
135+
nextConfig
136+
)
133137

134138
console.log('✓ Route types generated successfully')
135139
}

packages/next/src/server/lib/router-utils/route-types-utils.ts

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import type { NextConfigComplete } from '../../config-shared'
77
import { isParallelRouteSegment } from '../../../shared/lib/segment'
88
import fs from 'fs'
9-
import { generateRouteTypesFile } from './typegen'
9+
import { generateRouteTypesFile, generateLinkTypesFile } from './typegen'
1010
import { tryToParsePath } from '../../../lib/try-to-parse-path'
1111

1212
interface RouteInfo {
@@ -27,46 +27,71 @@ export interface RouteTypesManifest {
2727
// Convert a custom-route source string (`/blog/:slug`, `/docs/:path*`, ...)
2828
// into the bracket-syntax used by other Next.js route helpers so that we can
2929
// reuse `getRouteRegex()` to extract groups.
30-
export function convertCustomRouteSource(source: string): string {
30+
export function convertCustomRouteSource(source: string): string[] {
3131
const parseResult = tryToParsePath(source)
3232

3333
if (parseResult.error || !parseResult.tokens) {
3434
// Fallback to original source if parsing fails
35-
return source.startsWith('/') ? source : '/' + source
35+
return source.startsWith('/') ? [source] : ['/' + source]
3636
}
3737

38-
let result = ''
38+
const possibleNormalizedRoutes = ['']
39+
let slugCnt = 1
40+
41+
function append(suffix: string) {
42+
for (let i = 0; i < possibleNormalizedRoutes.length; i++) {
43+
possibleNormalizedRoutes[i] += suffix
44+
}
45+
}
46+
47+
function fork(suffix: string) {
48+
const currentLength = possibleNormalizedRoutes.length
49+
for (let i = 0; i < currentLength; i++) {
50+
possibleNormalizedRoutes.push(possibleNormalizedRoutes[i] + suffix)
51+
}
52+
}
3953

4054
for (const token of parseResult.tokens) {
41-
if (typeof token === 'string') {
42-
// Literal path segment
43-
result += token
44-
} else {
45-
// Parameter token
46-
const { name, modifier, prefix } = token
47-
48-
// Add the prefix (usually '/')
49-
result += prefix
50-
51-
if (modifier === '*') {
52-
// Catch-all zero or more: :param* -> [[...param]]
53-
result += `[[...${name}]]`
54-
} else if (modifier === '+') {
55-
// Catch-all one or more: :param+ -> [...param]
56-
result += `[...${name}]`
57-
} else if (modifier === '?') {
58-
// Optional catch-all: :param? -> [[...param]]
59-
result += `[[...${name}]]`
60-
} else {
61-
// Standard dynamic segment: :param -> [param]
62-
result += `[${name}]`
55+
if (typeof token === 'object') {
56+
// Make sure the slug is always named.
57+
const slug = token.name || (slugCnt++ === 1 ? 'slug' : `slug${slugCnt}`)
58+
if (token.modifier === '*') {
59+
append(`${token.prefix}[[...${slug}]]`)
60+
} else if (token.modifier === '+') {
61+
append(`${token.prefix}[...${slug}]`)
62+
} else if (token.modifier === '') {
63+
if (token.pattern === '[^\\/#\\?]+?') {
64+
// A safe slug
65+
append(`${token.prefix}[${slug}]`)
66+
} else if (token.pattern === '.*') {
67+
// An optional catch-all slug
68+
append(`${token.prefix}[[...${slug}]]`)
69+
} else if (token.pattern === '.+') {
70+
// A catch-all slug
71+
append(`${token.prefix}[...${slug}]`)
72+
} else {
73+
// Other regex patterns are not supported. Skip this route.
74+
return []
75+
}
76+
} else if (token.modifier === '?') {
77+
if (/^[a-zA-Z0-9_/]*$/.test(token.pattern)) {
78+
// An optional slug with plain text only, fork the route.
79+
append(token.prefix)
80+
fork(token.pattern)
81+
} else {
82+
// Optional modifier `?` and regex patterns are not supported.
83+
return []
84+
}
6385
}
86+
} else if (typeof token === 'string') {
87+
append(token)
6488
}
6589
}
6690

6791
// Ensure leading slash
68-
if (!result.startsWith('/')) result = '/' + result
69-
return result
92+
return possibleNormalizedRoutes.map((route) =>
93+
route.startsWith('/') ? route : '/' + route
94+
)
7095
}
7196

7297
/**
@@ -165,11 +190,12 @@ export async function createRouteTypesManifest({
165190
const rd = await redirects()
166191

167192
for (const item of rd) {
168-
const source = convertCustomRouteSource(item.source)
169-
170-
manifest.redirectRoutes[source] = {
171-
path: source,
172-
groups: extractRouteParams(source),
193+
const possibleRoutes = convertCustomRouteSource(item.source)
194+
for (const route of possibleRoutes) {
195+
manifest.redirectRoutes[route] = {
196+
path: route,
197+
groups: extractRouteParams(route),
198+
}
173199
}
174200
}
175201
}
@@ -187,10 +213,12 @@ export async function createRouteTypesManifest({
187213
]
188214

189215
for (const item of allSources) {
190-
const source = convertCustomRouteSource(item.source)
191-
manifest.rewriteRoutes[source] = {
192-
path: source,
193-
groups: extractRouteParams(source),
216+
const possibleRoutes = convertCustomRouteSource(item.source)
217+
for (const route of possibleRoutes) {
218+
manifest.rewriteRoutes[route] = {
219+
path: route,
220+
groups: extractRouteParams(route),
221+
}
194222
}
195223
}
196224
}
@@ -209,8 +237,12 @@ export async function writeRouteTypesManifest(
209237
await fs.promises.mkdir(dirname, { recursive: true })
210238
}
211239

212-
await fs.promises.writeFile(
213-
filePath,
214-
generateRouteTypesFile(manifest, config)
215-
)
240+
// Write the main routes.d.ts file
241+
await fs.promises.writeFile(filePath, generateRouteTypesFile(manifest))
242+
243+
// Write the link.d.ts file if typedRoutes is enabled
244+
if (config.experimental?.typedRoutes === true) {
245+
const linkTypesPath = path.join(dirname, 'link.d.ts')
246+
await fs.promises.writeFile(linkTypesPath, generateLinkTypesFile(manifest))
247+
}
216248
}

packages/next/src/server/lib/router-utils/typegen.ts

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { RouteTypesManifest } from './route-types-utils'
22
import { isDynamicRoute } from '../../../shared/lib/router/utils/is-dynamic'
3-
import type { NextConfigComplete } from '../../config-shared'
43

54
function generateRouteTypes(routesManifest: RouteTypesManifest): string {
65
const appRoutes = Object.keys(routesManifest.appRoutes).sort()
@@ -175,19 +174,13 @@ function serializeRouteTypes(routeTypes: string[]) {
175174
.join('')
176175
}
177176

178-
export function generateRouteTypesFile(
179-
routesManifest: RouteTypesManifest,
180-
config: NextConfigComplete
177+
export function generateLinkTypesFile(
178+
routesManifest: RouteTypesManifest
181179
): string {
182-
const routeTypes = generateRouteTypes(routesManifest)
183-
const paramTypes = generateParamTypes(routesManifest)
184-
const layoutSlotMap = generateLayoutSlotMap(routesManifest)
185-
186180
// Generate serialized static and dynamic routes for the internal namespace
187181
const allRoutes = {
188182
...routesManifest.appRoutes,
189183
...routesManifest.pageRoutes,
190-
...routesManifest.layoutRoutes,
191184
...routesManifest.redirectRoutes,
192185
...routesManifest.rewriteRoutes,
193186
}
@@ -217,51 +210,6 @@ export function generateRouteTypesFile(
217210
return `// This file is generated automatically by Next.js
218211
// Do not edit this file manually
219212
220-
${routeTypes}
221-
222-
${paramTypes}
223-
224-
export type ParamsOf<Route extends Routes> = ParamMap[Route]
225-
226-
${layoutSlotMap}
227-
228-
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes }
229-
230-
declare global {
231-
/**
232-
* Props for Next.js App Router page components
233-
* @example
234-
* \`\`\`tsx
235-
* export default function Page(props: PageProps<'/blog/[slug]'>) {
236-
* const { slug } = await props.params
237-
* return <div>Blog post: {slug}</div>
238-
* }
239-
* \`\`\`
240-
*/
241-
interface PageProps<AppRoute extends AppRoutes> {
242-
params: Promise<ParamMap[AppRoute]>
243-
searchParams: Promise<Record<string, string | string[] | undefined>>
244-
}
245-
246-
/**
247-
* Props for Next.js App Router layout components
248-
* @example
249-
* \`\`\`tsx
250-
* export default function Layout(props: LayoutProps<'/dashboard'>) {
251-
* return <div>{props.children}</div>
252-
* }
253-
* \`\`\`
254-
*/
255-
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
256-
params: Promise<ParamMap[LayoutRoute]>
257-
children: React.ReactNode
258-
} & {
259-
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
260-
}
261-
}
262-
${
263-
config.experimental?.typedRoutes === true
264-
? `
265213
// Type definitions for Next.js routes
266214
267215
/**
@@ -390,7 +338,59 @@ declare module 'next/form' {
390338
export default function Form<RouteType>(props: FormProps<RouteType>): JSX.Element
391339
}
392340
`
393-
: ''
341+
}
342+
343+
export function generateRouteTypesFile(
344+
routesManifest: RouteTypesManifest
345+
): string {
346+
const routeTypes = generateRouteTypes(routesManifest)
347+
const paramTypes = generateParamTypes(routesManifest)
348+
const layoutSlotMap = generateLayoutSlotMap(routesManifest)
349+
350+
return `// This file is generated automatically by Next.js
351+
// Do not edit this file manually
352+
353+
${routeTypes}
354+
355+
${paramTypes}
356+
357+
export type ParamsOf<Route extends Routes> = ParamMap[Route]
358+
359+
${layoutSlotMap}
360+
361+
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes }
362+
363+
declare global {
364+
/**
365+
* Props for Next.js App Router page components
366+
* @example
367+
* \`\`\`tsx
368+
* export default function Page(props: PageProps<'/blog/[slug]'>) {
369+
* const { slug } = await props.params
370+
* return <div>Blog post: {slug}</div>
371+
* }
372+
* \`\`\`
373+
*/
374+
interface PageProps<AppRoute extends AppRoutes> {
375+
params: Promise<ParamMap[AppRoute]>
376+
searchParams: Promise<Record<string, string | string[] | undefined>>
377+
}
378+
379+
/**
380+
* Props for Next.js App Router layout components
381+
* @example
382+
* \`\`\`tsx
383+
* export default function Layout(props: LayoutProps<'/dashboard'>) {
384+
* return <div>{props.children}</div>
385+
* }
386+
* \`\`\`
387+
*/
388+
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
389+
params: Promise<ParamMap[LayoutRoute]>
390+
children: React.ReactNode
391+
} & {
392+
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
393+
}
394394
}
395395
`
396396
}

test/e2e/app-dir/typed-routes/next.config.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ const nextConfig = {
1717
destination: '/posts/:category/:slug*',
1818
permanent: false,
1919
},
20-
{
21-
source: '/optional/:param?',
22-
destination: '/fallback',
23-
permanent: false,
24-
},
2520
]
2621
},
2722
async rewrites() {

test/e2e/app-dir/typed-routes/typed-links.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('typed-links', () => {
1111
}
1212

1313
it('should generate types for next/link', async () => {
14-
const dts = await next.readFile('.next/types/routes.d.ts')
14+
const dts = await next.readFile('.next/types/link.d.ts')
1515
expect(dts).toContain(`declare module 'next/link'`)
1616
})
1717

test/e2e/app-dir/typed-routes/typed-routes.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const expectedDts = `
44
type AppRoutes = "/" | "/_shop/[[...category]]" | "/dashboard" | "/dashboard/settings" | "/docs/[...slug]" | "/gallery/photo/[id]" | "/project/[slug]"
55
type PageRoutes = "/about" | "/users/[id]"
66
type LayoutRoutes = "/" | "/dashboard"
7-
type RedirectRoutes = "/blog/[category]/[[...slug]]" | "/optional/[[...param]]" | "/project/[slug]"
7+
type RedirectRoutes = "/blog/[category]/[[...slug]]" | "/project/[slug]"
88
type RewriteRoutes = "/api-legacy/[version]/[[...endpoint]]" | "/docs-old/[...path]"
99
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
1010
`
@@ -36,9 +36,6 @@ describe('typed-routes', () => {
3636
// Test catch-all zero-or-more: :slug* -> [[...slug]]
3737
expect(dts).toContain('"/blog/[category]/[[...slug]]"')
3838
expect(dts).toContain('"/api-legacy/[version]/[[...endpoint]]"')
39-
40-
// Test optional parameter: :param? -> [[...param]]
41-
expect(dts).toContain('"/optional/[[...param]]"')
4239
})
4340

4441
if (isNextDev) {

0 commit comments

Comments
 (0)