Skip to content

Commit 0334145

Browse files
committed
feat: initial typed links support
1 parent 4528d99 commit 0334145

File tree

11 files changed

+337
-356
lines changed

11 files changed

+337
-356
lines changed

packages/next/src/build/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1349,7 +1349,11 @@ export default async function build(
13491349
rewrites: config.rewrites,
13501350
})
13511351

1352-
await writeRouteTypesManifest(routeTypesManifest, routeTypesFilePath)
1352+
await writeRouteTypesManifest(
1353+
routeTypesManifest,
1354+
routeTypesFilePath,
1355+
config
1356+
)
13531357
})
13541358

13551359
// Turbopack already handles conflicting app and page routes.

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,6 @@ export default async function getBaseWebpackConfig(
389389

390390
const hasAppDir = !!appDir
391391
const disableOptimizedLoading = true
392-
const enableTypedRoutes = !!config.experimental.typedRoutes && hasAppDir
393392
const bundledReactChannel = needsExperimentalReact(config)
394393
? '-experimental'
395394
: ''
@@ -2152,7 +2151,7 @@ export default async function getBaseWebpackConfig(
21522151
dev,
21532152
isEdgeServer,
21542153
pageExtensions: config.pageExtensions,
2155-
typedRoutes: enableTypedRoutes,
2154+
typedRoutes: true, // TODO: I think this does more than just enable typed routes??
21562155
cacheLifeConfig: config.experimental.cacheLife,
21572156
originalRewrites,
21582157
originalRedirects,

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

Lines changed: 0 additions & 287 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import type { Rewrite, Redirect } from '../../../../lib/load-custom-routes'
2-
import type { Token } from 'next/dist/compiled/path-to-regexp'
32

43
import fs from 'fs/promises'
54
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
6-
import { parse } from 'next/dist/compiled/path-to-regexp'
75
import path from 'path'
86

97
import { WEBPACK_LAYERS } from '../../../../lib/constants'
@@ -15,7 +13,6 @@ import { isDynamicRoute } from '../../../../shared/lib/router/utils'
1513
import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths'
1614
import { getPageFromPath } from '../../../entries'
1715
import type { PageExtensions } from '../../../page-extensions-type'
18-
import { devPageFiles } from './shared'
1916
import { getProxiedPluginState } from '../../../build-context'
2017
import type { CacheLife } from '../../../../server/use-cache/cache-life'
2118

@@ -285,266 +282,6 @@ function formatRouteToRouteType(route: string) {
285282
}
286283
}
287284

288-
// Whether redirects and rewrites have been converted into routeTypes or not.
289-
let redirectsRewritesTypesProcessed = false
290-
291-
// Convert redirects and rewrites into routeTypes.
292-
function addRedirectsRewritesRouteTypes(
293-
rewrites: Rewrites | undefined,
294-
redirects: Redirect[] | undefined
295-
) {
296-
function addExtraRoute(source: string) {
297-
let tokens: Token[] | undefined
298-
try {
299-
tokens = parse(source)
300-
} catch {
301-
// Ignore invalid routes - they will be handled by other checks.
302-
}
303-
304-
if (Array.isArray(tokens)) {
305-
const possibleNormalizedRoutes = ['']
306-
let slugCnt = 1
307-
308-
function append(suffix: string) {
309-
for (let i = 0; i < possibleNormalizedRoutes.length; i++) {
310-
possibleNormalizedRoutes[i] += suffix
311-
}
312-
}
313-
314-
function fork(suffix: string) {
315-
const currentLength = possibleNormalizedRoutes.length
316-
for (let i = 0; i < currentLength; i++) {
317-
possibleNormalizedRoutes.push(possibleNormalizedRoutes[i] + suffix)
318-
}
319-
}
320-
321-
for (const token of tokens) {
322-
if (typeof token === 'object') {
323-
// Make sure the slug is always named.
324-
const slug =
325-
token.name || (slugCnt++ === 1 ? 'slug' : `slug${slugCnt}`)
326-
327-
if (token.modifier === '*') {
328-
append(`${token.prefix}[[...${slug}]]`)
329-
} else if (token.modifier === '+') {
330-
append(`${token.prefix}[...${slug}]`)
331-
} else if (token.modifier === '') {
332-
if (token.pattern === '[^\\/#\\?]+?') {
333-
// A safe slug
334-
append(`${token.prefix}[${slug}]`)
335-
} else if (token.pattern === '.*') {
336-
// An optional catch-all slug
337-
append(`${token.prefix}[[...${slug}]]`)
338-
} else if (token.pattern === '.+') {
339-
// A catch-all slug
340-
append(`${token.prefix}[...${slug}]`)
341-
} else {
342-
// Other regex patterns are not supported. Skip this route.
343-
return
344-
}
345-
} else if (token.modifier === '?') {
346-
if (/^[a-zA-Z0-9_/]*$/.test(token.pattern)) {
347-
// An optional slug with plain text only, fork the route.
348-
append(token.prefix)
349-
fork(token.pattern)
350-
} else {
351-
// Optional modifier `?` and regex patterns are not supported.
352-
return
353-
}
354-
}
355-
} else if (typeof token === 'string') {
356-
append(token)
357-
}
358-
}
359-
360-
for (const normalizedRoute of possibleNormalizedRoutes) {
361-
const { isDynamic, routeType } = formatRouteToRouteType(normalizedRoute)
362-
pluginState.routeTypes.extra[isDynamic ? 'dynamic' : 'static'].push(
363-
routeType
364-
)
365-
}
366-
}
367-
}
368-
369-
if (rewrites) {
370-
for (const rewrite of rewrites.beforeFiles) {
371-
addExtraRoute(rewrite.source)
372-
}
373-
for (const rewrite of rewrites.afterFiles) {
374-
addExtraRoute(rewrite.source)
375-
}
376-
for (const rewrite of rewrites.fallback) {
377-
addExtraRoute(rewrite.source)
378-
}
379-
}
380-
381-
if (redirects) {
382-
for (const redirect of redirects) {
383-
// Skip internal redirects
384-
// https://github.com/vercel/next.js/blob/8ff3d7ff57836c24088474175d595b4d50b3f857/packages/next/src/lib/load-custom-routes.ts#L704-L710
385-
if (!('internal' in redirect)) {
386-
addExtraRoute(redirect.source)
387-
}
388-
}
389-
}
390-
}
391-
392-
function serializeRouteTypes(routeTypes: string[]) {
393-
// route collection is not deterministic, this makes the output of the file deterministic
394-
return routeTypes
395-
.sort()
396-
.map((route) => `\n | \`${route}\``)
397-
.join('')
398-
}
399-
400-
function createRouteDefinitions() {
401-
let staticRouteTypes = []
402-
let dynamicRouteTypes = []
403-
404-
for (const type of ['edge', 'node', 'extra'] as const) {
405-
staticRouteTypes.push(...pluginState.routeTypes[type].static)
406-
dynamicRouteTypes.push(...pluginState.routeTypes[type].dynamic)
407-
}
408-
409-
const serializedStaticRouteTypes = serializeRouteTypes(staticRouteTypes)
410-
const serializedDynamicRouteTypes = serializeRouteTypes(dynamicRouteTypes)
411-
412-
// If both StaticRoutes and DynamicRoutes are empty, fallback to type 'string & {}'.
413-
const routeTypesFallback =
414-
!serializedStaticRouteTypes && !serializedDynamicRouteTypes
415-
? 'string & {}'
416-
: ''
417-
418-
return `// Type definitions for Next.js routes
419-
420-
/**
421-
* Internal types used by the Next.js router and Link component.
422-
* These types are not meant to be used directly.
423-
* @internal
424-
*/
425-
declare namespace __next_route_internal_types__ {
426-
type SearchOrHash = \`?\${string}\` | \`#\${string}\`
427-
type WithProtocol = \`\${string}:\${string}\`
428-
429-
type Suffix = '' | SearchOrHash
430-
431-
type SafeSlug<S extends string> = S extends \`\${string}/\${string}\`
432-
? never
433-
: S extends \`\${string}\${SearchOrHash}\`
434-
? never
435-
: S extends ''
436-
? never
437-
: S
438-
439-
type CatchAllSlug<S extends string> = S extends \`\${string}\${SearchOrHash}\`
440-
? never
441-
: S extends ''
442-
? never
443-
: S
444-
445-
type OptionalCatchAllSlug<S extends string> =
446-
S extends \`\${string}\${SearchOrHash}\` ? never : S
447-
448-
type StaticRoutes = ${serializedStaticRouteTypes || 'never'}
449-
type DynamicRoutes<T extends string = string> = ${
450-
serializedDynamicRouteTypes || 'never'
451-
}
452-
453-
type RouteImpl<T> = ${
454-
routeTypesFallback ||
455-
`
456-
${
457-
// This keeps autocompletion working for static routes.
458-
'| StaticRoutes'
459-
}
460-
| SearchOrHash
461-
| WithProtocol
462-
| \`\${StaticRoutes}\${SearchOrHash}\`
463-
| (T extends \`\${DynamicRoutes<infer _>}\${Suffix}\` ? T : never)
464-
`
465-
}
466-
}
467-
468-
declare module 'next' {
469-
export { default } from 'next/types.js'
470-
export * from 'next/types.js'
471-
472-
export type Route<T extends string = string> =
473-
__next_route_internal_types__.RouteImpl<T>
474-
}
475-
476-
declare module 'next/link' {
477-
import type { LinkProps as OriginalLinkProps } from 'next/dist/client/link.js'
478-
import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
479-
import type { UrlObject } from 'url'
480-
481-
type LinkRestProps = Omit<
482-
Omit<
483-
DetailedHTMLProps<
484-
AnchorHTMLAttributes<HTMLAnchorElement>,
485-
HTMLAnchorElement
486-
>,
487-
keyof OriginalLinkProps
488-
> &
489-
OriginalLinkProps,
490-
'href'
491-
>
492-
493-
export type LinkProps<RouteInferType> = LinkRestProps & {
494-
/**
495-
* The path or URL to navigate to. This is the only required prop. It can also be an object.
496-
* @see https://nextjs.org/docs/api-reference/next/link
497-
*/
498-
href: __next_route_internal_types__.RouteImpl<RouteInferType> | UrlObject
499-
}
500-
501-
export default function Link<RouteType>(props: LinkProps<RouteType>): JSX.Element
502-
}
503-
504-
declare module 'next/navigation' {
505-
export * from 'next/dist/client/components/navigation.js'
506-
507-
import type { NavigateOptions, AppRouterInstance as OriginalAppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
508-
interface AppRouterInstance extends OriginalAppRouterInstance {
509-
/**
510-
* Navigate to the provided href.
511-
* Pushes a new history entry.
512-
*/
513-
push<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void
514-
/**
515-
* Navigate to the provided href.
516-
* Replaces the current history entry.
517-
*/
518-
replace<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void
519-
/**
520-
* Prefetch the provided href.
521-
*/
522-
prefetch<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>): void
523-
}
524-
525-
export function useRouter(): AppRouterInstance;
526-
}
527-
528-
declare module 'next/form' {
529-
import type { FormProps as OriginalFormProps } from 'next/dist/client/form.js'
530-
531-
type FormRestProps = Omit<OriginalFormProps, 'action'>
532-
533-
export type FormProps<RouteInferType> = {
534-
/**
535-
* \`action\` can be either a \`string\` or a function.
536-
* - If \`action\` is a string, it will be interpreted as a path or URL to navigate to when the form is submitted.
537-
* The path will be prefetched when the form becomes visible.
538-
* - If \`action\` is a function, it will be called when the form is submitted. See the [React docs](https://react.dev/reference/react-dom/components/form#props) for more.
539-
*/
540-
action: __next_route_internal_types__.RouteImpl<RouteInferType> | ((formData: FormData) => void)
541-
} & FormRestProps
542-
543-
export default function Form<RouteType>(props: FormProps<RouteType>): JSX.Element
544-
}
545-
`
546-
}
547-
548285
function formatTimespan(seconds: number): string {
549286
if (seconds > 0) {
550287
if (seconds === 18748800) {
@@ -841,13 +578,6 @@ export class NextTypesPlugin {
841578
this.typedRoutes = options.typedRoutes
842579
this.cacheLifeConfig = options.cacheLifeConfig
843580
this.distDirAbsolutePath = path.join(this.dir, this.distDir)
844-
if (this.typedRoutes && !redirectsRewritesTypesProcessed) {
845-
redirectsRewritesTypesProcessed = true
846-
addRedirectsRewritesRouteTypes(
847-
options.originalRewrites,
848-
options.originalRedirects
849-
)
850-
}
851581
}
852582

853583
getRelativePathFromAppTypesDir(moduleRelativePathToAppDir: string) {
@@ -1109,23 +839,6 @@ export class NextTypesPlugin {
1109839
) as unknown as webpack.sources.RawSource
1110840
)
1111841

1112-
if (this.typedRoutes) {
1113-
if (this.dev && !this.isEdgeServer) {
1114-
devPageFiles.forEach((file) => {
1115-
this.collectPage(file)
1116-
})
1117-
}
1118-
1119-
const linkAssetPath = path.join(assetDirRelative, 'types/link.d.ts')
1120-
1121-
compilation.emitAsset(
1122-
linkAssetPath,
1123-
new sources.RawSource(
1124-
createRouteDefinitions()
1125-
) as unknown as webpack.sources.RawSource
1126-
)
1127-
}
1128-
1129842
if (this.cacheLifeConfig) {
1130843
const cacheLifeAssetPath = path.join(
1131844
assetDirRelative,

packages/next/src/lib/turbopack-warning.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ const unsupportedTurbopackNextConfigOptions = [
3535

3636
'experimental.sri.algorithm',
3737
'experimental.swcTraceProfiling',
38-
'experimental.typedRoutes',
3938

4039
// Left to be implemented (Might not be needed for Turbopack)
4140
'experimental.craCompat',

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,17 @@ export async function createRouteTypesManifest({
200200

201201
export async function writeRouteTypesManifest(
202202
manifest: RouteTypesManifest,
203-
filePath: string
203+
filePath: string,
204+
config: NextConfigComplete
204205
) {
205206
const dirname = path.dirname(filePath)
206207

207208
if (!fs.existsSync(dirname)) {
208209
await fs.promises.mkdir(dirname, { recursive: true })
209210
}
210211

211-
await fs.promises.writeFile(filePath, generateRouteTypesFile(manifest))
212+
await fs.promises.writeFile(
213+
filePath,
214+
generateRouteTypesFile(manifest, config)
215+
)
212216
}

0 commit comments

Comments
 (0)