Skip to content

Commit cd5d221

Browse files
authored
Allow for parsing app route strings with and without slot names. (#92327)
The old impl has a boolean arg that was always true. To support some fixes to routing I need to make the parser sometimes allow for slots but still not things like route groups. I am generalizing the function implementation to have a filter type that decides whether the input string is allowed to have certain types such as route groups and slot names. All the existing uses still disallow any non-url path segments.
1 parent e296bcb commit cd5d221

9 files changed

Lines changed: 172 additions & 143 deletions

File tree

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1148,5 +1148,7 @@
11481148
"1147": "Turbopack database compaction is not supported on this platform",
11491149
"1148": "webToReadable cannot be used in the edge runtime",
11501150
"1149": "Node.js Readable cannot be teed in the edge runtime",
1151-
"1150": "Node.js Readable cannot be converted to a web stream in the edge runtime"
1151+
"1150": "Node.js Readable cannot be converted to a web stream in the edge runtime",
1152+
"1151": "%s is being parsed as a normalized route, but it has a route group segment.",
1153+
"1152": "%s is being parsed as a normalized route, but it has a parallel route segment."
11521154
}

packages/next/src/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.test.ts

Lines changed: 70 additions & 69 deletions
Large diffs are not rendered by default.

packages/next/src/build/static-paths/utils.test.ts

Lines changed: 45 additions & 44 deletions
Large diffs are not rendered by default.

packages/next/src/build/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ import type {
8080
} from '../server/route-modules/app-route/module'
8181
import type { FunctionsConfigManifest, ManifestRoute } from './index'
8282
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
83-
import { parseAppRoute } from '../shared/lib/router/routes/app'
83+
import { parseNormalizedAppRoute } from '../shared/lib/router/routes/app'
8484
import { fillMetadataSegment } from '../lib/metadata/get-metadata-route'
8585
import { STATIC_METADATA_IMAGES } from '../lib/metadata/is-metadata-route'
8686

@@ -836,7 +836,7 @@ export async function isPageStatic({
836836
appConfig.revalidate = 0
837837
}
838838

839-
const route = parseAppRoute(page, true)
839+
const route = parseNormalizedAppRoute(page)
840840

841841
// If the page is dynamic and we're not in edge runtime, then we need to
842842
// build the static paths. The edge runtime doesn't support static

packages/next/src/build/validate-app-paths.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
} from '../shared/lib/router/utils/get-segment-param'
55
import {
66
isInterceptionAppRoute,
7-
parseAppRoute,
7+
parseNormalizedAppRoute,
88
type NormalizedAppRoute,
99
type NormalizedAppRouteSegment,
1010
} from '../shared/lib/router/routes/app'
@@ -143,8 +143,8 @@ function validateAppRoute(route: NormalizedAppRoute): void {
143143
*/
144144
function parseAndValidateAppPath(path: string): NormalizedAppRoute {
145145
// Fast parse the route information. We're expecting this to be a normalized
146-
// route, so we pass `true` to the `parseAppRoute` function.
147-
const route = parseAppRoute(path, true)
146+
// route.
147+
const route = parseNormalizedAppRoute(path)
148148

149149
// Slow walk the data from the route in order to validate it.
150150
validateAppRoute(route)

packages/next/src/server/dev/static-paths-worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { collectRootParamKeys } from '../../build/segment-config/app/collect-roo
2626
import { buildAppStaticPaths } from '../../build/static-paths/app'
2727
import { buildPagesStaticPaths } from '../../build/static-paths/pages'
2828
import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'
29-
import { parseAppRoute } from '../../shared/lib/router/routes/app'
29+
import { parseNormalizedAppRoute } from '../../shared/lib/router/routes/app'
3030

3131
type RuntimeConfig = {
3232
pprConfig: ExperimentalPPRConfig | undefined
@@ -119,7 +119,7 @@ export async function loadStaticPaths({
119119
routeModule as AppPageRouteModule | AppRouteRouteModule
120120
)
121121

122-
const route = parseAppRoute(pathname, true)
122+
const route = parseNormalizedAppRoute(pathname)
123123
if (route.dynamicSegments.length === 0) {
124124
throw new InvariantError(
125125
`Expected a dynamic route, but got a static route: ${pathname}`

packages/next/src/server/request/fallback-params.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FallbackRouteParam } from '../../build/static-paths/types'
33
import type { DynamicParamTypesShort } from '../../shared/lib/app-router-types'
44
import { dynamicParamTypes } from '../app-render/get-short-dynamic-param-type'
55
import type AppPageRouteModule from '../route-modules/app-page/module'
6-
import { parseAppRoute } from '../../shared/lib/router/routes/app'
6+
import { parseNormalizedAppRoute } from '../../shared/lib/router/routes/app'
77
import { extractPathnameRouteParamSegmentsFromLoaderTree } from '../../build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree'
88
import { getParamProperties } from '../../shared/lib/router/utils/get-segment-param'
99

@@ -119,7 +119,7 @@ export function getFallbackRouteParams(
119119
page: string,
120120
routeModule: AppPageRouteModule
121121
) {
122-
const route = parseAppRoute(page, true)
122+
const route = parseNormalizedAppRoute(page)
123123

124124
// Extract the pathname-contributing segments from the loader tree. This
125125
// mirrors the logic in buildAppStaticPaths where we determine which segments

packages/next/src/shared/lib/router/routes/app.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,17 @@ export function isInterceptionAppRoute(
173173
)
174174
}
175175

176-
export function parseAppRoute(
176+
// Bitmask for which non-URL segment types to allow during parsing.
177+
// By default, route groups and parallel routes are rejected because
178+
// they should have been stripped by normalizeAppPath. These flags
179+
// let callers opt in to allowing specific types.
180+
const OnlyRoutableSegments = /* */ 0b00
181+
const AllowParallelSegments = /* */ 0b01
182+
const AllowGroupSegments = /* */ 0b10
183+
184+
function parseAppRouteImpl(
177185
pathname: string,
178-
normalized: true
179-
): NormalizedAppRoute
180-
export function parseAppRoute(pathname: string, normalized: false): AppRoute
181-
export function parseAppRoute(
182-
pathname: string,
183-
normalized: boolean
186+
allowedTypes: number
184187
): AppRoute | NormalizedAppRoute {
185188
const pathnameSegments = pathname.split('/').filter(Boolean)
186189

@@ -202,12 +205,20 @@ export function parseAppRoute(
202205
}
203206

204207
if (
205-
normalized &&
206-
(appSegment.type === 'route-group' ||
207-
appSegment.type === 'parallel-route')
208+
appSegment.type === 'route-group' &&
209+
!(allowedTypes & AllowGroupSegments)
210+
) {
211+
throw new InvariantError(
212+
`${pathname} is being parsed as a normalized route, but it has a route group segment.`
213+
)
214+
}
215+
216+
if (
217+
appSegment.type === 'parallel-route' &&
218+
!(allowedTypes & AllowParallelSegments)
208219
) {
209220
throw new InvariantError(
210-
`${pathname} is being parsed as a normalized route, but it has a route group or parallel route segment.`
221+
`${pathname} is being parsed as a normalized route, but it has a parallel route segment.`
211222
)
212223
}
213224

@@ -219,12 +230,8 @@ export function parseAppRoute(
219230
throw new Error(`Invalid interception route: ${pathname}`)
220231
}
221232

222-
interceptingRoute = normalized
223-
? parseAppRoute(parts[0], true)
224-
: parseAppRoute(parts[0], false)
225-
interceptedRoute = normalized
226-
? parseAppRoute(parts[1], true)
227-
: parseAppRoute(parts[1], false)
233+
interceptingRoute = parseAppRouteImpl(parts[0], allowedTypes)
234+
interceptedRoute = parseAppRouteImpl(parts[1], allowedTypes)
228235
interceptionMarker = appSegment.interceptionMarker
229236
}
230237
}
@@ -234,7 +241,7 @@ export function parseAppRoute(
234241
)
235242

236243
return {
237-
normalized,
244+
normalized: allowedTypes === OnlyRoutableSegments,
238245
pathname,
239246
segments,
240247
dynamicSegments,
@@ -243,3 +250,21 @@ export function parseAppRoute(
243250
interceptedRoute,
244251
}
245252
}
253+
254+
/**
255+
* Parse an app route that has been fully normalized (no @slot or ()
256+
* group segments). Throws if either is present.
257+
*/
258+
export function parseNormalizedAppRoute(pathname: string): NormalizedAppRoute {
259+
return parseAppRouteImpl(pathname, OnlyRoutableSegments) as NormalizedAppRoute
260+
}
261+
262+
/**
263+
* Parse an app route that may contain @slot segments but not ()
264+
* group segments. Slot segments are preserved as parallel-route
265+
* type segments so callers can distinguish routes in different
266+
* parallel slots.
267+
*/
268+
export function parseAppRouteWithSlots(pathname: string): AppRoute {
269+
return parseAppRouteImpl(pathname, AllowParallelSegments) as AppRoute
270+
}

packages/next/src/shared/lib/router/utils/get-dynamic-param.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Params } from '../../../../server/request/params'
55
import type { DynamicParamTypesShort } from '../../app-router-types'
66
import { InvariantError } from '../../invariant-error'
77
import { parseLoaderTree } from './parse-loader-tree'
8-
import { parseAppRoute, parseAppRouteSegment } from '../routes/app'
8+
import { parseNormalizedAppRoute, parseAppRouteSegment } from '../routes/app'
99
import { resolveParamValue } from './resolve-param-value'
1010

1111
/**
@@ -53,7 +53,7 @@ export function interpolateParallelRouteParams(
5353
]
5454

5555
// Parse the route from the provided page path.
56-
const route = parseAppRoute(pagePath, true)
56+
const route = parseNormalizedAppRoute(pagePath)
5757

5858
while (stack.length > 0) {
5959
const { tree, depth } = stack.pop()!

0 commit comments

Comments
 (0)