rsnext/packages/next/shared/lib/router/utils/route-regex.ts
Javi Velasco f354f46b3f
Deprecate nested Middleware in favor of root middleware (#36772)
This PR deprecates declaring a middleware under `pages` in favour of the project root naming it after `middleware` instead of `_middleware`. This is in the context of having a simpler execution model for middleware and also ships some refactor work. There is a ton of a code to be simplified after this deprecation but I think it is best to do it progressively.

With this PR, when in development, we will **fail** whenever we find a nested middleware but we do **not** include it in the compiler so if the project is using it, it will no longer work. For production we will **fail** too so it will not be possible to build and deploy a deprecated middleware. The error points to a page that should also be reviewed as part of **documentation**.

Aside from the deprecation, this migrates all middleware tests to work with a single middleware. It also splits tests into multiple folders to make them easier to isolate and work with. Finally it ships some small code refactor and simplifications.
2022-05-19 15:46:21 +00:00

199 lines
5.8 KiB
TypeScript

import { escapeStringRegexp } from '../../escape-regexp'
import { removeTrailingSlash } from './remove-trailing-slash'
export interface Group {
pos: number
repeat: boolean
optional: boolean
}
export interface RouteRegex {
groups: { [groupName: string]: Group }
re: RegExp
}
/**
* From a normalized route this function generates a regular expression and
* a corresponding groups object inteded to be used to store matching groups
* from the regular expression.
*/
export function getRouteRegex(normalizedRoute: string): RouteRegex {
const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute)
return {
re: new RegExp(`^${parameterizedRoute}(?:/)?$`),
groups: groups,
}
}
/**
* This function extends `getRouteRegex` generating also a named regexp where
* each group is named along with a routeKeys object that indexes the assigned
* named group with its corresponding key.
*/
export function getNamedRouteRegex(normalizedRoute: string) {
const result = getNamedParametrizedRoute(normalizedRoute)
return {
...getRouteRegex(normalizedRoute),
namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`,
routeKeys: result.routeKeys,
}
}
function getParametrizedRoute(route: string) {
const segments = removeTrailingSlash(route).slice(1).split('/')
const groups: { [groupName: string]: Group } = {}
let groupIndex = 1
return {
parameterizedRoute: segments
.map((segment) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
groups[key] = { pos: groupIndex++, repeat, optional }
return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'
} else {
return `/${escapeStringRegexp(segment)}`
}
})
.join(''),
groups,
}
}
function getNamedParametrizedRoute(route: string) {
const segments = removeTrailingSlash(route).slice(1).split('/')
const getSafeRouteKey = buildGetSafeRouteKey()
const routeKeys: { [named: string]: string } = {}
return {
namedParameterizedRoute: segments
.map((segment) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
// replace any non-word characters since they can break
// the named regex
let cleanedKey = key.replace(/\W/g, '')
let invalidKey = false
// check if the key is still invalid and fallback to using a known
// safe key
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true
}
if (!isNaN(parseInt(cleanedKey.slice(0, 1)))) {
invalidKey = true
}
if (invalidKey) {
cleanedKey = getSafeRouteKey()
}
routeKeys[cleanedKey] = key
return repeat
? optional
? `(?:/(?<${cleanedKey}>.+?))?`
: `/(?<${cleanedKey}>.+?)`
: `/(?<${cleanedKey}>[^/]+?)`
} else {
return `/${escapeStringRegexp(segment)}`
}
})
.join(''),
routeKeys,
}
}
/**
* Parses a given parameter from a route to a data structure that can be used
* to generate the parametrized route. Examples:
* - `[...slug]` -> `{ name: 'slug', repeat: true, optional: true }`
* - `[foo]` -> `{ name: 'foo', repeat: false, optional: true }`
* - `bar` -> `{ name: 'bar', repeat: false, optional: false }`
*/
function parseParameter(param: string) {
const optional = param.startsWith('[') && param.endsWith(']')
if (optional) {
param = param.slice(1, -1)
}
const repeat = param.startsWith('...')
if (repeat) {
param = param.slice(3)
}
return { key: param, repeat, optional }
}
/**
* Builds a function to generate a minimal routeKey using only a-z and minimal
* number of characters.
*/
function buildGetSafeRouteKey() {
let routeKeyCharCode = 97
let routeKeyCharLength = 1
return () => {
let routeKey = ''
for (let i = 0; i < routeKeyCharLength; i++) {
routeKey += String.fromCharCode(routeKeyCharCode)
routeKeyCharCode++
if (routeKeyCharCode > 122) {
routeKeyCharLength++
routeKeyCharCode = 97
}
}
return routeKey
}
}
/**
* From a middleware normalized route this function generates a regular
* expression for it. Temporarly we are using this to generate Edge Function
* routes too. In such cases the route should not include a trailing catch-all.
* For these cases the option `catchAll` should be set to false.
*/
export function getMiddlewareRegex(
normalizedRoute: string,
options?: {
catchAll?: boolean
}
): RouteRegex {
const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute)
const { catchAll = true } = options ?? {}
if (parameterizedRoute === '/') {
let catchAllRegex = catchAll ? '(?!_next).*' : ''
return {
groups: {},
re: new RegExp(`^/${catchAllRegex}$`),
}
}
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''
return {
groups: groups,
re: new RegExp(`^${parameterizedRoute}${catchAllGroupedRegex}$`),
}
}
/**
* A server version for getMiddlewareRegex that generates a named regexp.
* This is intended to be using for build time only.
*/
export function getNamedMiddlewareRegex(
normalizedRoute: string,
options: {
catchAll?: boolean
}
) {
const { parameterizedRoute } = getParametrizedRoute(normalizedRoute)
const { catchAll = true } = options
if (parameterizedRoute === '/') {
let catchAllRegex = catchAll ? '(?!_next).*' : ''
return {
namedRegex: `^/${catchAllRegex}$`,
}
}
const { namedParameterizedRoute } = getNamedParametrizedRoute(normalizedRoute)
let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : ''
return {
namedRegex: `^${namedParameterizedRoute}${catchAllGroupedRegex}$`,
}
}