router.ts 9.0 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
import { normalizeLocalePath } from '../lib/i18n/normalize-locale-path'
7 8 9

export const route = pathMatch()

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

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

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

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

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

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

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

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

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

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

  setDynamicRoutes(routes: DynamicRoutes = []) {
    this.dynamicRoutes = routes
95 96
  }

97 98
  addFsRoute(fsRoute: Route) {
    this.fsRoutes.unshift(fsRoute)
99 100
  }

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

111 112 113
      if (pageChecks[p]) {
        return pageChecks[p]
      }
J
Jan Potoms 已提交
114
      const result = this.pageChecker(p)
115 116 117 118
      pageChecks[p] = result
      return result
    }

119
    let parsedUrlUpdated = parsedUrl
120

121 122 123 124 125 126 127 128
    /*
      Desired routes order
      - headers
      - redirects
      - Check filesystem (including pages), if nothing found continue
      - User rewrites (checking filesystem and pages each match)
    */

129
    const allRoutes = [
130 131 132 133 134 135 136 137 138
      ...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',
139 140
              name: 'page checker',
              requireBasePath: false,
141
              match: route('/:path*'),
142
              fn: async (checkerReq, checkerRes, params, parsedCheckerUrl) => {
143 144
                let { pathname } = parsedCheckerUrl
                pathname = removePathTrailingSlash(pathname || '/')
145 146 147 148

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

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

171
    for (const testRoute of allRoutes) {
172 173 174 175 176 177 178 179
      // 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)
180 181
      const isPublicFolderCatchall = testRoute.name === 'public folder catchall'
      const keepBasePath = isCustomRoute || isPublicFolderCatchall
182

183
      if (!keepBasePath) {
184 185 186
        currentPathname = replaceBasePath(this.basePath, currentPathname!)
      }

187
      // re-add locale for custom-routes to allow matching against
188
      if (isCustomRoute && parsedUrl.query.__nextLocale) {
189 190 191 192
        if (keepBasePath) {
          currentPathname = replaceBasePath(this.basePath, currentPathname!)
        }

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

197 198 199 200 201 202 203
        if (
          (req as any).__nextHadTrailingSlash &&
          !currentPathname.endsWith('/')
        ) {
          currentPathname += '/'
        }

204 205 206
        if (keepBasePath) {
          currentPathname = `${this.basePath}${currentPathname}`
        }
207 208
      }

209
      const newParams = testRoute.match(currentPathname)
210 211 212

      // Check if the match function matched
      if (newParams) {
213 214
        // since we require basePath be present for non-custom-routes we
        // 404 here when we matched an fs route
215
        if (!keepBasePath) {
216 217 218 219 220 221 222 223 224 225 226 227 228
          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
        }

229
        const result = await testRoute.fn(req, res, newParams, parsedUrlUpdated)
230 231 232 233 234 235

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

236 237
        // since the fs route didn't match we need to re-add the basePath
        // to continue checking rewrites with the basePath present
238
        if (!keepBasePath) {
239 240 241
          parsedUrlUpdated.pathname = originalPathname
        }

242 243 244
        if (result.pathname) {
          parsedUrlUpdated.pathname = result.pathname
        }
245

246 247 248 249 250 251 252
        if (result.query) {
          parsedUrlUpdated.query = {
            ...parsedUrlUpdated.query,
            ...result.query,
          }
        }

253
        // check filesystem
254
        if (testRoute.check === true) {
255 256 257
          const originalFsPathname = parsedUrlUpdated.pathname
          const fsPathname = replaceBasePath(this.basePath, originalFsPathname!)

258
          for (const fsRoute of this.fsRoutes) {
259
            const fsParams = fsRoute.match(fsPathname)
260 261

            if (fsParams) {
262 263
              parsedUrlUpdated.pathname = fsPathname

264
              const fsResult = await fsRoute.fn(
265 266 267 268 269 270
                req,
                res,
                fsParams,
                parsedUrlUpdated
              )

271
              if (fsResult.finished) {
272 273
                return true
              }
274 275

              parsedUrlUpdated.pathname = originalFsPathname
276 277 278
            }
          }

279
          let matchedPage = await memoizedPageChecker(fsPathname)
280 281 282 283

          // If we didn't match a page check dynamic routes
          if (!matchedPage) {
            for (const dynamicRoute of this.dynamicRoutes) {
284
              if (dynamicRoute.match(fsPathname)) {
285 286 287 288 289 290 291
                matchedPage = true
              }
            }
          }

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

294 295 296 297 298 299 300 301 302 303 304 305 306
            const pageParams = this.catchAllRoute.match(
              parsedUrlUpdated.pathname
            )

            await this.catchAllRoute.fn(
              req,
              res,
              pageParams as Params,
              parsedUrlUpdated
            )
            return true
          }
        }
307 308
      }
    }
309
    return false
310 311
  }
}