Skip to content

Commit

Permalink
Split router utils into smaller modules (#45451)Co-authored-by: Jan K…
Browse files Browse the repository at this point in the history
…aifer <[email protected]>

This makes sure that the entire module `shared/lib/router/router` isn't
included in the final bundle, by splitting it into smaller utils.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

---------

Co-authored-by: Jan Kaifer <[email protected]>
  • Loading branch information
shuding and jankaifer authored Feb 1, 2023
1 parent b1f154d commit 074b7e4
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 233 deletions.
2 changes: 1 addition & 1 deletion packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { createInfinitePromise } from './infinite-promise'
import { ErrorBoundary } from './error-boundary'
import { matchSegment } from './match-segments'
import { useRouter } from './navigation'
import { handleSmoothScroll } from '../../shared/lib/router/router'
import { handleSmoothScroll } from '../../shared/lib/router/utils/handle-smooth-scroll'

/**
* Add refetch marker to router state at the point of the current layout segment.
Expand Down
14 changes: 8 additions & 6 deletions packages/next/src/client/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/* global location */
import '../build/polyfills/polyfill-module'

import type Router from '../shared/lib/router/router'
import type {
AppComponent,
AppProps,
PrivateRouteInfo,
} from '../shared/lib/router/router'

import React from 'react'
// @ts-expect-error upgrade react types to react 18
import ReactDOM from 'react-dom/client'
import { HeadManagerContext } from '../shared/lib/head-manager-context'
import mitt, { MittEmitter } from '../shared/lib/mitt'
import { RouterContext } from '../shared/lib/router-context'
import {
AppComponent,
AppProps,
handleSmoothScroll,
PrivateRouteInfo,
} from '../shared/lib/router/router'
import { handleSmoothScroll } from '../shared/lib/router/utils/handle-smooth-scroll'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import {
urlQueryToSearchParams,
Expand Down
11 changes: 6 additions & 5 deletions packages/next/src/client/link.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client'

import React from 'react'
import { UrlObject } from 'url'
import {
isLocalURL,
import type {
NextRouter,
PrefetchOptions as RouterPrefetchOptions,
resolveHref,
} from '../shared/lib/router/router'

import React from 'react'
import { UrlObject } from 'url'
import { resolveHref } from '../shared/lib/router/utils/resolve-href'
import { isLocalURL } from '../shared/lib/router/utils/is-local-url'
import { formatUrl } from '../shared/lib/router/utils/format-url'
import { addLocale } from './add-locale'
import { RouterContext } from '../shared/lib/router-context'
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/client/page-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ComponentType } from 'react'
import type { RouteLoader } from './route-loader'
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
import { addBasePath } from './add-base-path'
import { interpolateAs } from '../shared/lib/router/router'
import { interpolateAs } from '../shared/lib/router/utils/interpolate-as'
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
import { addLocale } from './add-locale'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
Expand Down
221 changes: 6 additions & 215 deletions packages/next/src/shared/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { RouterEvent } from '../../../client/router'
import type { StyleSheetTuple } from '../../../client/page-loader'
import type { UrlObject } from 'url'
import type PageLoader from '../../../client/page-loader'
import { normalizePathTrailingSlash } from '../../../client/normalize-trailing-slash'
import { removeTrailingSlash } from './utils/remove-trailing-slash'
import {
getClientBuildManifest,
Expand All @@ -24,15 +23,12 @@ import {
getLocationOrigin,
getURL,
loadGetInitialProps,
normalizeRepeatedSlashes,
NextPageContext,
ST,
NEXT_DATA,
isAbsoluteUrl,
} from '../utils'
import { isDynamicRoute } from './utils/is-dynamic'
import { parseRelativeUrl } from './utils/parse-relative-url'
import { searchParamsToUrlQuery } from './utils/querystring'
import resolveRewrites from './utils/resolve-rewrites'
import { getRouteMatcher } from './utils/route-matcher'
import { getRouteRegex } from './utils/route-regex'
Expand All @@ -48,7 +44,12 @@ import { isAPIRoute } from '../../../lib/is-api-route'
import { getNextPathnameInfo } from './utils/get-next-pathname-info'
import { formatNextPathnameInfo } from './utils/format-next-pathname-info'
import { compareRouterStates } from './utils/compare-states'
import { isLocalURL } from './utils/is-local-url'
import { isBot } from './utils/is-bot'
import { omit } from './utils/omit'
import { resolveHref } from './utils/resolve-href'
import { interpolateAs } from './utils/interpolate-as'
import { handleSmoothScroll } from './utils/handle-smooth-scroll'

declare global {
interface Window {
Expand Down Expand Up @@ -123,195 +124,6 @@ function stripOrigin(url: string) {
return url.startsWith(origin) ? url.substring(origin.length) : url
}

function omit<T extends { [key: string]: unknown }, K extends keyof T>(
object: T,
keys: K[]
): Omit<T, K> {
const omitted: { [key: string]: unknown } = {}
Object.keys(object).forEach((key) => {
if (!keys.includes(key as K)) {
omitted[key] = object[key]
}
})
return omitted as Omit<T, K>
}

/**
* Detects whether a given url is routable by the Next.js router (browser only).
*/
export function isLocalURL(url: string): boolean {
// prevent a hydration mismatch on href for url with anchor refs
if (!isAbsoluteUrl(url)) return true
try {
// absolute urls can be local if they are on the same origin
const locationOrigin = getLocationOrigin()
const resolved = new URL(url, locationOrigin)
return resolved.origin === locationOrigin && hasBasePath(resolved.pathname)
} catch (_) {
return false
}
}

export function interpolateAs(
route: string,
asPathname: string,
query: ParsedUrlQuery
) {
let interpolatedRoute = ''

const dynamicRegex = getRouteRegex(route)
const dynamicGroups = dynamicRegex.groups
const dynamicMatches =
// Try to match the dynamic route against the asPath
(asPathname !== route ? getRouteMatcher(dynamicRegex)(asPathname) : '') ||
// Fall back to reading the values from the href
// TODO: should this take priority; also need to change in the router.
query

interpolatedRoute = route
const params = Object.keys(dynamicGroups)

if (
!params.every((param) => {
let value = dynamicMatches[param] || ''
const { repeat, optional } = dynamicGroups[param]

// support single-level catch-all
// TODO: more robust handling for user-error (passing `/`)
let replaced = `[${repeat ? '...' : ''}${param}]`
if (optional) {
replaced = `${!value ? '/' : ''}[${replaced}]`
}
if (repeat && !Array.isArray(value)) value = [value]

return (
(optional || param in dynamicMatches) &&
// Interpolate group into data URL if present
(interpolatedRoute =
interpolatedRoute!.replace(
replaced,
repeat
? (value as string[])
.map(
// these values should be fully encoded instead of just
// path delimiter escaped since they are being inserted
// into the URL and we expect URL encoded segments
// when parsing dynamic route params
(segment) => encodeURIComponent(segment)
)
.join('/')
: encodeURIComponent(value as string)
) || '/')
)
})
) {
interpolatedRoute = '' // did not satisfy all requirements

// n.b. We ignore this error because we handle warning for this case in
// development in the `<Link>` component directly.
}
return {
params,
result: interpolatedRoute,
}
}

/**
* 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
const urlProtoMatch = urlAsString.match(/^[a-zA-Z]{1,}:\/\//)
const urlAsStringNoProto = urlProtoMatch
? urlAsString.slice(urlProtoMatch[0].length)
: urlAsString

const urlParts = urlAsStringNoProto.split('?')

if ((urlParts[0] || '').match(/(\/\/|\\)/)) {
console.error(
`Invalid href passed to next/router: ${urlAsString}, 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 {
base = new URL(
urlAsString.startsWith('#') ? router.asPath : router.pathname,
'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
}
}

function prepareUrlAs(router: NextRouter, url: Url, as?: Url) {
// If url and as provided as an object representation,
// we'll format them into the string version here.
Expand Down Expand Up @@ -538,7 +350,7 @@ async function withMiddlewareEffects<T extends FetchDataOutput>(
}
}

type Url = UrlObject | string
export type Url = UrlObject | string

export type BaseRouter = {
route: string
Expand Down Expand Up @@ -672,27 +484,6 @@ interface FetchNextDataParams {
unstable_skipClientCache?: boolean
}

/**
* Run function with `scroll-behavior: auto` applied to `<html/>`.
* This css change will be reverted after the function finishes.
*/
export function handleSmoothScroll(
fn: () => void,
options: { dontForceLayout?: boolean } = {}
) {
const htmlElement = document.documentElement
const existing = htmlElement.style.scrollBehavior
htmlElement.style.scrollBehavior = 'auto'
if (!options.dontForceLayout) {
// In Chrome-based browsers we need to force reflow before calling `scrollTo`.
// Otherwise it will not pickup the change in scrollBehavior
// More info here: https://github.com/vercel/next.js/issues/40719#issuecomment-1336248042
htmlElement.getClientRects()
}
fn()
htmlElement.style.scrollBehavior = existing
}

function tryToParseAsJSON(text: string) {
try {
return JSON.parse(text)
Expand Down
20 changes: 20 additions & 0 deletions packages/next/src/shared/lib/router/utils/handle-smooth-scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Run function with `scroll-behavior: auto` applied to `<html/>`.
* This css change will be reverted after the function finishes.
*/
export function handleSmoothScroll(
fn: () => void,
options: { dontForceLayout?: boolean } = {}
) {
const htmlElement = document.documentElement
const existing = htmlElement.style.scrollBehavior
htmlElement.style.scrollBehavior = 'auto'
if (!options.dontForceLayout) {
// In Chrome-based browsers we need to force reflow before calling `scrollTo`.
// Otherwise it will not pickup the change in scrollBehavior
// More info here: https://github.com/vercel/next.js/issues/40719#issuecomment-1336248042
htmlElement.getClientRects()
}
fn()
htmlElement.style.scrollBehavior = existing
}
Loading

0 comments on commit 074b7e4

Please sign in to comment.