router.ts 8.7 KB
Newer Older
1
import { IncomingMessage, ServerResponse } from 'http'
2 3
import { UrlWithParsedQuery } from 'url'

4
import pathMatch from '../lib/router/utils/path-match'
5
import { removePathTrailingSlash } from '../../client/normalize-trailing-slash'
6 7 8

export const route = pathMatch()

L
Lukáš Huvar 已提交
9
export type Params = { [param: string]: any }
10

J
Joe Haddad 已提交
11
export type RouteMatch = (pathname: string | null | undefined) => false | Params
J
Joe Haddad 已提交
12

13 14 15
type RouteResult = {
  finished: boolean
  pathname?: string
16
  query?: { [k: string]: string }
17 18
}

19
export type Route = {
J
Joe Haddad 已提交
20
  match: RouteMatch
21
  type: string
22
  check?: boolean
23 24
  statusCode?: number
  name: string
25
  requireBasePath?: false
26 27 28 29
  fn: (
    req: IncomingMessage,
    res: ServerResponse,
    params: Params,
30
    parsedUrl: UrlWithParsedQuery
31
  ) => Promise<RouteResult> | RouteResult
32 33
}

34 35 36 37
export type DynamicRoutes = Array<{ page: string; match: RouteMatch }>

export type PageChecker = (pathname: string) => Promise<boolean>

38 39 40
const customRouteTypes = new Set(['rewrite', 'redirect', 'header'])

function replaceBasePath(basePath: string, pathname: string) {
41
  // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
42 43 44
  return pathname!.replace(basePath, '') || '/'
}

45
export default class Router {
46
  basePath: string
47
  headers: Route[]
48
  fsRoutes: Route[]
49 50
  rewrites: Route[]
  redirects: Route[]
51 52 53
  catchAllRoute: Route
  pageChecker: PageChecker
  dynamicRoutes: DynamicRoutes
54
  useFileSystemPublicRoutes: boolean
55 56

  constructor({
57
    basePath = '',
58
    headers = [],
59
    fsRoutes = [],
60 61
    rewrites = [],
    redirects = [],
62 63 64
    catchAllRoute,
    dynamicRoutes = [],
    pageChecker,
65
    useFileSystemPublicRoutes,
66
  }: {
67
    basePath: string
68
    headers: Route[]
69
    fsRoutes: Route[]
70 71
    rewrites: Route[]
    redirects: Route[]
72 73 74
    catchAllRoute: Route
    dynamicRoutes: DynamicRoutes | undefined
    pageChecker: PageChecker
75
    useFileSystemPublicRoutes: boolean
76
  }) {
77
    this.basePath = basePath
78
    this.headers = headers
79
    this.fsRoutes = fsRoutes
80 81
    this.rewrites = rewrites
    this.redirects = redirects
82 83 84
    this.pageChecker = pageChecker
    this.catchAllRoute = catchAllRoute
    this.dynamicRoutes = dynamicRoutes
85
    this.useFileSystemPublicRoutes = useFileSystemPublicRoutes
86 87 88 89
  }

  setDynamicRoutes(routes: DynamicRoutes = []) {
    this.dynamicRoutes = routes
90 91
  }

92 93
  addFsRoute(fsRoute: Route) {
    this.fsRoutes.unshift(fsRoute)
94 95
  }

96
  async execute(
97 98
    req: IncomingMessage,
    res: ServerResponse,
99
    parsedUrl: UrlWithParsedQuery
100
  ): Promise<boolean> {
101
    // memoize page check calls so we don't duplicate checks for pages
J
Jan Potoms 已提交
102
    const pageChecks: { [name: string]: Promise<boolean> } = {}
103 104 105 106
    const memoizedPageChecker = async (p: string): Promise<boolean> => {
      if (pageChecks[p]) {
        return pageChecks[p]
      }
J
Jan Potoms 已提交
107
      const result = this.pageChecker(p)
108 109 110 111
      pageChecks[p] = result
      return result
    }

112
    let parsedUrlUpdated = parsedUrl
113

114 115 116 117 118 119 120 121
    /*
      Desired routes order
      - headers
      - redirects
      - Check filesystem (including pages), if nothing found continue
      - User rewrites (checking filesystem and pages each match)
    */

122
    const allRoutes = [
123 124 125 126 127 128 129 130 131
      ...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',
132 133
              name: 'page checker',
              requireBasePath: false,
134
              match: route('/:path*'),
135
              fn: async (checkerReq, checkerRes, params, parsedCheckerUrl) => {
136 137
                let { pathname } = parsedCheckerUrl
                pathname = removePathTrailingSlash(pathname || '/')
138 139 140 141

                if (!pathname) {
                  return { finished: false }
                }
142

J
Jan Potoms 已提交
143
                if (await memoizedPageChecker(pathname)) {
144 145 146 147 148 149
                  return this.catchAllRoute.fn(
                    checkerReq,
                    checkerRes,
                    params,
                    parsedCheckerUrl
                  )
150 151 152 153 154 155 156 157 158 159 160
                }
                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] : []),
    ]
161 162
    const originallyHadBasePath =
      !this.basePath || (req as any)._nextHadBasePath
163

164
    for (const testRoute of allRoutes) {
165 166 167 168 169 170 171 172
      // if basePath is being used, the basePath will still be included
      // in the pathname here to allow custom-routes to require containing
      // it or not, filesystem routes and pages must always include the basePath
      // if it is set
      let currentPathname = parsedUrlUpdated.pathname
      const originalPathname = currentPathname
      const requireBasePath = testRoute.requireBasePath !== false
      const isCustomRoute = customRouteTypes.has(testRoute.type)
173 174
      const isPublicFolderCatchall = testRoute.name === 'public folder catchall'
      const keepBasePath = isCustomRoute || isPublicFolderCatchall
175

176
      if (!keepBasePath) {
177 178 179
        currentPathname = replaceBasePath(this.basePath, currentPathname!)
      }

180 181 182 183 184 185
      // re-add locale for custom-routes to allow matching against
      if (
        isCustomRoute &&
        (req as any).__nextStrippedLocale &&
        parsedUrl.query.__nextLocale
      ) {
186 187 188 189
        if (keepBasePath) {
          currentPathname = replaceBasePath(this.basePath, currentPathname!)
        }

190 191 192
        currentPathname = `/${parsedUrl.query.__nextLocale}${
          currentPathname === '/' ? '' : currentPathname
        }`
193 194 195 196

        if (keepBasePath) {
          currentPathname = `${this.basePath}${currentPathname}`
        }
197 198
      }

199
      const newParams = testRoute.match(currentPathname)
200 201 202

      // Check if the match function matched
      if (newParams) {
203 204
        // since we require basePath be present for non-custom-routes we
        // 404 here when we matched an fs route
205
        if (!keepBasePath) {
206 207 208 209 210 211 212 213 214 215 216 217 218
          if (!originallyHadBasePath && !(req as any)._nextDidRewrite) {
            if (requireBasePath) {
              // consider this a non-match so the 404 renders
              return false
            }
            // page checker occurs before rewrites so we need to continue
            // to check those since they don't always require basePath
            continue
          }

          parsedUrlUpdated.pathname = currentPathname
        }

219
        const result = await testRoute.fn(req, res, newParams, parsedUrlUpdated)
220 221 222 223 224 225

        // The response was handled
        if (result.finished) {
          return true
        }

226 227
        // since the fs route didn't match we need to re-add the basePath
        // to continue checking rewrites with the basePath present
228
        if (!keepBasePath) {
229 230 231
          parsedUrlUpdated.pathname = originalPathname
        }

232 233 234
        if (result.pathname) {
          parsedUrlUpdated.pathname = result.pathname
        }
235

236 237 238 239 240 241 242
        if (result.query) {
          parsedUrlUpdated.query = {
            ...parsedUrlUpdated.query,
            ...result.query,
          }
        }

243
        // check filesystem
244
        if (testRoute.check === true) {
245 246 247
          const originalFsPathname = parsedUrlUpdated.pathname
          const fsPathname = replaceBasePath(this.basePath, originalFsPathname!)

248
          for (const fsRoute of this.fsRoutes) {
249
            const fsParams = fsRoute.match(fsPathname)
250 251

            if (fsParams) {
252 253
              parsedUrlUpdated.pathname = fsPathname

254
              const fsResult = await fsRoute.fn(
255 256 257 258 259 260
                req,
                res,
                fsParams,
                parsedUrlUpdated
              )

261
              if (fsResult.finished) {
262 263
                return true
              }
264 265

              parsedUrlUpdated.pathname = originalFsPathname
266 267 268
            }
          }

269
          let matchedPage = await memoizedPageChecker(fsPathname)
270 271 272 273

          // If we didn't match a page check dynamic routes
          if (!matchedPage) {
            for (const dynamicRoute of this.dynamicRoutes) {
274
              if (dynamicRoute.match(fsPathname)) {
275 276 277 278 279 280 281
                matchedPage = true
              }
            }
          }

          // Matched a page or dynamic route so render it using catchAllRoute
          if (matchedPage) {
282 283
            parsedUrlUpdated.pathname = fsPathname

284 285 286 287 288 289 290 291 292 293 294 295 296
            const pageParams = this.catchAllRoute.match(
              parsedUrlUpdated.pathname
            )

            await this.catchAllRoute.fn(
              req,
              res,
              pageParams as Params,
              parsedUrlUpdated
            )
            return true
          }
        }
297 298
      }
    }
299
    return false
300 301
  }
}