Skip to content

Commit b7f64a4

Browse files
committed
fix: third arg after rebase
1 parent 23552df commit b7f64a4

File tree

10 files changed

+247
-179
lines changed

10 files changed

+247
-179
lines changed

packages/next/src/build/webpack-config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2151,7 +2151,6 @@ export default async function getBaseWebpackConfig(
21512151
dev,
21522152
isEdgeServer,
21532153
pageExtensions: config.pageExtensions,
2154-
typedRoutes: true, // TODO: I think this does more than just enable typed routes??
21552154
cacheLifeConfig: config.experimental.cacheLife,
21562155
originalRewrites,
21572156
originalRedirects,

packages/next/src/build/webpack/plugins/next-types-plugin/index.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ describe('next-types-plugin', () => {
1111
dev: false,
1212
isEdgeServer: false,
1313
pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
14-
typedRoutes: false,
1514
cacheLifeConfig: undefined,
1615
originalRewrites: undefined,
1716
originalRedirects: undefined,
@@ -40,7 +39,6 @@ describe('next-types-plugin', () => {
4039
dev: false,
4140
isEdgeServer: false,
4241
pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
43-
typedRoutes: false,
4442
cacheLifeConfig: undefined,
4543
originalRewrites: undefined,
4644
originalRedirects: undefined,
@@ -61,7 +59,6 @@ describe('next-types-plugin', () => {
6159
dev: false,
6260
isEdgeServer: false,
6361
pageExtensions: ['tsx', 'ts', 'jsx', 'js'],
64-
typedRoutes: false,
6562
cacheLifeConfig: undefined,
6663
originalRewrites: undefined,
6764
originalRedirects: undefined,

packages/next/src/build/webpack/plugins/next-types-plugin/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ interface Options {
3131
dev: boolean
3232
isEdgeServer: boolean
3333
pageExtensions: PageExtensions
34-
typedRoutes: boolean
3534
cacheLifeConfig: undefined | { [profile: string]: CacheLife }
3635
originalRewrites: Rewrites | undefined
3736
originalRedirects: Redirect[] | undefined
@@ -563,7 +562,6 @@ export class NextTypesPlugin {
563562
isEdgeServer: boolean
564563
pageExtensions: string[]
565564
pagesDir: string
566-
typedRoutes: boolean
567565
cacheLifeConfig: undefined | { [profile: string]: CacheLife }
568566
distDirAbsolutePath: string
569567

@@ -575,7 +573,6 @@ export class NextTypesPlugin {
575573
this.isEdgeServer = options.isEdgeServer
576574
this.pageExtensions = options.pageExtensions
577575
this.pagesDir = path.join(this.appDir, '..', 'pages')
578-
this.typedRoutes = options.typedRoutes
579576
this.cacheLifeConfig = options.cacheLifeConfig
580577
this.distDirAbsolutePath = path.join(this.dir, this.distDir)
581578
}
@@ -599,8 +596,6 @@ export class NextTypesPlugin {
599596
}
600597

601598
collectPage(filePath: string) {
602-
if (!this.typedRoutes) return
603-
604599
const isApp = filePath.startsWith(this.appDir + path.sep)
605600
const isPages = !isApp && filePath.startsWith(this.pagesDir + path.sep)
606601

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() {

0 commit comments

Comments
 (0)