next.js/packages/next/src/client/resolve-href.ts
resolve-href.ts139 lines4.8 KB
import type { NextRouter, Url } from '../shared/lib/router/router'

import { searchParamsToUrlQuery } from '../shared/lib/router/utils/querystring'
import { formatWithValidation } from '../shared/lib/router/utils/format-url'
import { omit } from '../shared/lib/router/utils/omit'
import { normalizeRepeatedSlashes } from '../shared/lib/utils'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
import { isLocalURL } from '../shared/lib/router/utils/is-local-url'
import { isDynamicRoute } from '../shared/lib/router/utils'
import { interpolateAs } from '../shared/lib/router/utils/interpolate-as'
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'

/**
 * Resolves a given hyperlink with a certain router state (basePath not included).
 * Preserves absolute urls.
 */
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs: true
): [string, string] | [string]
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs?: false
): string
export function resolveHref(
  router: NextRouter,
  href: Url,
  resolveAs?: boolean
): [string, string] | [string] | string {
  // we use a dummy base url for relative urls
  let base: URL
  let urlAsString = typeof href === 'string' ? href : formatWithValidation(href)

  // repeated slashes and backslashes in the URL are considered
  // invalid and will never match a Next.js page/file
  // https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1
  const urlProtoMatch = urlAsString.match(/^[a-z][a-z0-9+.-]*:\/\//i)
  const urlAsStringNoProto = urlProtoMatch
    ? urlAsString.slice(urlProtoMatch[0].length)
    : urlAsString

  const urlParts = urlAsStringNoProto.split('?', 1)

  if ((urlParts[0] || '').match(/(\/\/|\\)/)) {
    console.error(
      `Invalid href '${urlAsString}' passed to next/router in page: '${router.pathname}'. Repeated forward-slashes (//) or backslashes \\ are not valid in the href.`
    )
    const normalizedUrl = normalizeRepeatedSlashes(urlAsStringNoProto)
    urlAsString = (urlProtoMatch ? urlProtoMatch[0] : '') + normalizedUrl
  }

  // Return because it cannot be routed by the Next.js router
  if (!isLocalURL(urlAsString)) {
    return (resolveAs ? [urlAsString] : urlAsString) as string
  }

  try {
    let baseBase = urlAsString.startsWith('#') ? router.asPath : router.pathname

    // If the provided href is only a query string, it is safer to use the asPath
    // considering rewrites.
    if (urlAsString.startsWith('?')) {
      baseBase = router.asPath

      // However, if is a dynamic route, we need to use the pathname to preserve the
      // query interpolation and rewrites (router.pathname will look like "/[slug]").
      if (isDynamicRoute(router.pathname)) {
        baseBase = router.pathname

        const routeRegex = getRouteRegex(router.pathname)
        const match = getRouteMatcher(routeRegex)(router.asPath)

        // For dynamic routes, if asPath doesn't match the pathname regex, it is a rewritten path.
        // In this case, should use asPath to preserve the current URL.
        if (!match) {
          baseBase = router.asPath
        }

        // Note: There is an edge case where the pathname is dynamic, and also a rewrite path to the same segment.
        // E.g. in "/[slug]" path, rewrite "/foo" -> "/bar"

        // In this case, it will be treated as a non-rewritten path and possibly interpolate the query string.
        // E.g., "/any?slug=foo" will become the content of "/foo", not rewritten as "/bar"

        // This is currently a trade-off of not resolving rewrite paths on every Router/Link call,
        // but using a lighter route regex pattern check.
      }
    }

    base = new URL(baseBase, 'http://n')
  } catch (_) {
    // fallback to / for invalid asPath values e.g. //
    base = new URL('/', 'http://n')
  }

  try {
    const finalUrl = new URL(urlAsString, base)
    finalUrl.pathname = normalizePathTrailingSlash(finalUrl.pathname)
    let interpolatedAs = ''

    if (
      isDynamicRoute(finalUrl.pathname) &&
      finalUrl.searchParams &&
      resolveAs
    ) {
      const query = searchParamsToUrlQuery(finalUrl.searchParams)

      const { result, params } = interpolateAs(
        finalUrl.pathname,
        finalUrl.pathname,
        query
      )

      if (result) {
        interpolatedAs = formatWithValidation({
          pathname: result,
          hash: finalUrl.hash,
          query: omit(query, params),
        })
      }
    }

    // if the origin didn't change, it means we received a relative href
    const resolvedHref =
      finalUrl.origin === base.origin
        ? finalUrl.href.slice(finalUrl.origin.length)
        : finalUrl.href

    return resolveAs
      ? [resolvedHref, interpolatedAs || resolvedHref]
      : resolvedHref
  } catch (_) {
    return resolveAs ? [urlAsString] : urlAsString
  }
}
Quest for Codev2.0.0
/
SIGN IN