rsnext/packages/next/next-server/server/router.ts
Tim Neutkens b124ed2e14
Added no-shadow rule to eslint (#13645)
Was going through _document and noticed some variable shadowing going on. Added a rule for it to our eslint configuration and went through all warnings with @Timer.
2020-06-01 21:00:22 +00:00

297 lines
8.3 KiB
TypeScript

import { IncomingMessage, ServerResponse } from 'http'
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
import { ParsedUrlQuery } from 'querystring'
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
import pathMatch from './lib/path-match'
export const route = pathMatch()
export type Params = { [param: string]: any }
export type RouteMatch = (pathname: string | null | undefined) => false | Params
type RouteResult = {
finished: boolean
pathname?: string
query?: { [k: string]: string }
}
export type Route = {
match: RouteMatch
type: string
check?: boolean
statusCode?: number
name: string
fn: (
req: IncomingMessage,
res: ServerResponse,
params: Params,
parsedUrl: UrlWithParsedQuery
) => Promise<RouteResult> | RouteResult
}
export type DynamicRoutes = Array<{ page: string; match: RouteMatch }>
export type PageChecker = (pathname: string) => Promise<boolean>
export const prepareDestination = (
destination: string,
params: Params,
query: ParsedUrlQuery,
appendParamsToQuery?: boolean
) => {
const parsedDestination = parseUrl(destination, true)
const destQuery = parsedDestination.query
let destinationCompiler = compilePathToRegex(
`${parsedDestination.pathname!}${parsedDestination.hash || ''}`,
// we don't validate while compiling the destination since we should
// have already validated before we got to this point and validating
// breaks compiling destinations with named pattern params from the source
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
// since compile validation is meant for reversing and not for inserting
// params from a separate path-regex into another
{ validate: false }
)
let newUrl
// update any params in query values
for (const [key, strOrArray] of Object.entries(destQuery)) {
let value = Array.isArray(strOrArray) ? strOrArray[0] : strOrArray
if (value) {
// the value needs to start with a forward-slash to be compiled
// correctly
value = `/${value}`
const queryCompiler = compilePathToRegex(value, { validate: false })
value = queryCompiler(params).substr(1)
}
destQuery[key] = value
}
// add path params to query if it's not a redirect and not
// already defined in destination query
if (appendParamsToQuery) {
for (const [name, value] of Object.entries(params)) {
if (!(name in destQuery)) {
destQuery[name] = value
}
}
}
try {
newUrl = encodeURI(destinationCompiler(params))
const [pathname, hash] = newUrl.split('#')
parsedDestination.pathname = pathname
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
parsedDestination.path = `${pathname}${parsedDestination.search}`
delete parsedDestination.search
} catch (err) {
if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
throw new Error(
`To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://err.sh/vercel/next.js/invalid-multi-match`
)
}
throw err
}
// Query merge order lowest priority to highest
// 1. initial URL query values
// 2. path segment values
// 3. destination specified query values
parsedDestination.query = {
...query,
...parsedDestination.query,
}
return {
newUrl,
parsedDestination,
}
}
export default class Router {
headers: Route[]
fsRoutes: Route[]
rewrites: Route[]
redirects: Route[]
catchAllRoute: Route
pageChecker: PageChecker
dynamicRoutes: DynamicRoutes
useFileSystemPublicRoutes: boolean
constructor({
headers = [],
fsRoutes = [],
rewrites = [],
redirects = [],
catchAllRoute,
dynamicRoutes = [],
pageChecker,
useFileSystemPublicRoutes,
}: {
headers: Route[]
fsRoutes: Route[]
rewrites: Route[]
redirects: Route[]
catchAllRoute: Route
dynamicRoutes: DynamicRoutes | undefined
pageChecker: PageChecker
useFileSystemPublicRoutes: boolean
}) {
this.headers = headers
this.fsRoutes = fsRoutes
this.rewrites = rewrites
this.redirects = redirects
this.pageChecker = pageChecker
this.catchAllRoute = catchAllRoute
this.dynamicRoutes = dynamicRoutes
this.useFileSystemPublicRoutes = useFileSystemPublicRoutes
}
setDynamicRoutes(routes: DynamicRoutes = []) {
this.dynamicRoutes = routes
}
addFsRoute(fsRoute: Route) {
this.fsRoutes.unshift(fsRoute)
}
async execute(
req: IncomingMessage,
res: ServerResponse,
parsedUrl: UrlWithParsedQuery
): Promise<boolean> {
// memoize page check calls so we don't duplicate checks for pages
const pageChecks: { [name: string]: Promise<boolean> } = {}
const memoizedPageChecker = async (p: string): Promise<boolean> => {
if (pageChecks[p]) {
return pageChecks[p]
}
const result = this.pageChecker(p)
pageChecks[p] = result
return result
}
let parsedUrlUpdated = parsedUrl
/*
Desired routes order
- headers
- redirects
- Check filesystem (including pages), if nothing found continue
- User rewrites (checking filesystem and pages each match)
*/
const allRoutes = [
...this.headers,
...this.redirects,
...this.fsRoutes,
// We only check the catch-all route if public page routes hasn't been
// disabled
...(this.useFileSystemPublicRoutes
? [
{
type: 'route',
name: 'Page checker',
match: route('/:path*'),
fn: async (checkerReq, checkerRes, params, parsedCheckerUrl) => {
const { pathname } = parsedCheckerUrl
if (!pathname) {
return { finished: false }
}
if (await memoizedPageChecker(pathname)) {
return this.catchAllRoute.fn(
checkerReq,
checkerRes,
params,
parsedCheckerUrl
)
}
return { finished: false }
},
} as Route,
]
: []),
...this.rewrites,
// We only check the catch-all route if public page routes hasn't been
// disabled
...(this.useFileSystemPublicRoutes ? [this.catchAllRoute] : []),
]
for (const testRoute of allRoutes) {
const newParams = testRoute.match(parsedUrlUpdated.pathname)
// Check if the match function matched
if (newParams) {
const result = await testRoute.fn(req, res, newParams, parsedUrlUpdated)
// The response was handled
if (result.finished) {
return true
}
if (result.pathname) {
parsedUrlUpdated.pathname = result.pathname
}
if (result.query) {
parsedUrlUpdated.query = {
...parsedUrlUpdated.query,
...result.query,
}
}
// check filesystem
if (testRoute.check === true) {
for (const fsRoute of this.fsRoutes) {
const fsParams = fsRoute.match(parsedUrlUpdated.pathname)
if (fsParams) {
const fsResult = await fsRoute.fn(
req,
res,
fsParams,
parsedUrlUpdated
)
if (fsResult.finished) {
return true
}
}
}
let matchedPage = await memoizedPageChecker(
parsedUrlUpdated.pathname!
)
// If we didn't match a page check dynamic routes
if (!matchedPage) {
for (const dynamicRoute of this.dynamicRoutes) {
if (dynamicRoute.match(parsedUrlUpdated.pathname)) {
matchedPage = true
}
}
}
// Matched a page or dynamic route so render it using catchAllRoute
if (matchedPage) {
const pageParams = this.catchAllRoute.match(
parsedUrlUpdated.pathname
)
await this.catchAllRoute.fn(
req,
res,
pageParams as Params,
parsedUrlUpdated
)
return true
}
}
}
}
return false
}
}