rsnext/packages/next/client/index.tsx

909 lines
27 KiB
TypeScript
Raw Normal View History

/* global location */
import '../build/polyfills/polyfill-module'
import React from 'react'
import { HeadManagerContext } from '../shared/lib/head-manager-context'
import mitt, { MittEmitter } from '../shared/lib/mitt'
import { RouterContext } from '../shared/lib/router-context'
import type Router from '../shared/lib/router/router'
import {
AppComponent,
AppProps,
PrivateRouteInfo,
} from '../shared/lib/router/router'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import {
urlQueryToSearchParams,
assign,
} from '../shared/lib/router/utils/querystring'
import { setConfig } from '../shared/lib/runtime-config'
import {
getURL,
loadGetInitialProps,
NextWebVitalsMetric,
NEXT_DATA,
ST,
} from '../shared/lib/utils'
import { Portal } from './portal'
import initHeadManager from './head-manager'
import PageLoader, { StyleSheetTuple } from './page-loader'
import measureWebVitals from './performance-relayer'
import { RouteAnnouncer } from './route-announcer'
import { createRouter, makePublicRouterInstance } from './router'
import { getProperError } from '../lib/is-error'
import { ImageConfigContext } from '../shared/lib/image-config-context'
import { ImageConfigComplete } from '../shared/lib/image-config'
import { removeBasePath } from './remove-base-path'
import { hasBasePath } from './has-base-path'
const ReactDOM = process.env.__NEXT_REACT_ROOT
? require('react-dom/client')
: require('react-dom')
/// <reference types="react-dom/experimental" />
declare let __webpack_public_path__: string
declare global {
interface Window {
/* test fns */
__NEXT_HYDRATED?: boolean
__NEXT_HYDRATED_CB?: () => void
/* prod */
__NEXT_PRELOADREADY?: (ids?: (string | number)[]) => void
__NEXT_DATA__: NEXT_DATA
__NEXT_P: any[]
}
}
type RenderRouteInfo = PrivateRouteInfo & {
App: AppComponent
scroll?: { x: number; y: number } | null
}
type RenderErrorProps = Omit<RenderRouteInfo, 'Component' | 'styleSheets'>
type RegisterFn = (input: [string, () => void]) => void
export const version = process.env.__NEXT_VERSION
export let router: Router
export const emitter: MittEmitter<string> = mitt()
const looseToArray = <T extends {}>(input: any): T[] => [].slice.call(input)
let initialData: NEXT_DATA
let defaultLocale: string | undefined = undefined
let asPath: string
let pageLoader: PageLoader
let appElement: HTMLElement | null
let headManager: {
mountedInstances: Set<unknown>
updateHead: (head: JSX.Element[]) => void
getIsSsr?: () => boolean
}
let initialMatchesMiddleware = false
let lastAppProps: AppProps
2017-04-05 13:43:34 +02:00
let lastRenderReject: (() => void) | null
let webpackHMR: any
let CachedApp: AppComponent, onPerfEntry: (metric: any) => void
let CachedComponent: React.ComponentType
// Ignore the module ID transform in client.
// @ts-ignore
;(self as any).__next_require__ = __webpack_require__
class Container extends React.Component<{
fn: (err: Error, info?: any) => void
}> {
componentDidCatch(componentErr: Error, info: any) {
this.props.fn(componentErr, info)
}
componentDidMount() {
this.scrollToHash()
// We need to replace the router state if:
// - the page was (auto) exported and has a query string or search (hash)
// - it was auto exported and is a dynamic route (to provide params)
// - if it is a client-side skeleton (fallback render)
// - if middleware matches the current page (may have rewrite params)
// - if rewrites in next.config.js match (may have rewrite params)
if (
router.isSsr &&
// We don't update for 404 requests as this can modify
// the asPath unexpectedly e.g. adding basePath when
// it wasn't originally present
initialData.page !== '/404' &&
initialData.page !== '/_error' &&
(initialData.isFallback ||
(initialData.nextExport &&
(isDynamicRoute(router.pathname) ||
location.search ||
process.env.__NEXT_HAS_REWRITES ||
initialMatchesMiddleware)) ||
(initialData.props &&
initialData.props.__N_SSG &&
(location.search ||
process.env.__NEXT_HAS_REWRITES ||
initialMatchesMiddleware)))
) {
// update query on mount for exported pages
router
.replace(
router.pathname +
'?' +
String(
assign(
urlQueryToSearchParams(router.query),
new URLSearchParams(location.search)
)
),
asPath,
{
// @ts-ignore
// WARNING: `_h` is an internal option for handing Next.js
// client-side hydration. Your app should _never_ use this property.
// It may change at any time without notice.
_h: 1,
// Fallback pages must trigger the data fetch, so the transition is
// not shallow.
// Other pages (strictly updating query) happens shallowly, as data
// requirements would already be present.
shallow: !initialData.isFallback && !initialMatchesMiddleware,
}
)
.catch((err) => {
if (!err.cancelled) throw err
})
}
}
componentDidUpdate() {
this.scrollToHash()
}
scrollToHash() {
let { hash } = location
hash = hash && hash.substring(1)
if (!hash) return
const el: HTMLElement | null = document.getElementById(hash)
if (!el) return
// If we call scrollIntoView() in here without a setTimeout
// it won't scroll properly.
setTimeout(() => el.scrollIntoView(), 0)
}
render() {
if (process.env.NODE_ENV === 'production') {
return this.props.children
} else {
const {
ReactDevOverlay,
} = require('next/dist/compiled/@next/react-dev-overlay/dist/client')
2020-05-15 20:14:44 +02:00
return <ReactDevOverlay>{this.props.children}</ReactDevOverlay>
}
}
}
export async function initialize(opts: { webpackHMR?: any } = {}): Promise<{
assetPrefix: string
}> {
// This makes sure this specific lines are removed in production
if (process.env.NODE_ENV === 'development') {
webpackHMR = opts.webpackHMR
}
initialData = JSON.parse(
document.getElementById('__NEXT_DATA__')!.textContent!
)
window.__NEXT_DATA__ = initialData
defaultLocale = initialData.defaultLocale
const prefix: string = initialData.assetPrefix || ''
// With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time
// So, this is how we do it in the client side at runtime
__webpack_public_path__ = `${prefix}/_next/` //eslint-disable-line
// Initialize next/config with the environment configuration
setConfig({
serverRuntimeConfig: {},
publicRuntimeConfig: initialData.runtimeConfig || {},
})
asPath = getURL()
// make sure not to attempt stripping basePath for 404s
if (hasBasePath(asPath)) {
asPath = removeBasePath(asPath)
}
if (process.env.__NEXT_I18N_SUPPORT) {
const { normalizeLocalePath } =
require('../shared/lib/i18n/normalize-locale-path') as typeof import('../shared/lib/i18n/normalize-locale-path')
const { detectDomainLocale } =
require('../shared/lib/i18n/detect-domain-locale') as typeof import('../shared/lib/i18n/detect-domain-locale')
const { parseRelativeUrl } =
require('../shared/lib/router/utils/parse-relative-url') as typeof import('../shared/lib/router/utils/parse-relative-url')
const { formatUrl } =
require('../shared/lib/router/utils/format-url') as typeof import('../shared/lib/router/utils/format-url')
if (initialData.locales) {
const parsedAs = parseRelativeUrl(asPath)
const localePathResult = normalizeLocalePath(
parsedAs.pathname,
initialData.locales
)
if (localePathResult.detectedLocale) {
parsedAs.pathname = localePathResult.pathname
asPath = formatUrl(parsedAs)
} else {
// derive the default locale if it wasn't detected in the asPath
// since we don't prerender static pages with all possible default
// locales
defaultLocale = initialData.locale
}
// attempt detecting default locale based on hostname
const detectedDomain = detectDomainLocale(
process.env.__NEXT_I18N_DOMAINS as any,
window.location.hostname
)
// TODO: investigate if defaultLocale needs to be populated after
// hydration to prevent mismatched renders
if (detectedDomain) {
defaultLocale = detectedDomain.defaultLocale
}
}
}
if (initialData.scriptLoader) {
const { initScriptLoader } = require('./script')
initScriptLoader(initialData.scriptLoader)
}
pageLoader = new PageLoader(initialData.buildId, prefix)
const register: RegisterFn = ([r, f]) =>
pageLoader.routeLoader.onEntrypoint(r, f)
if (window.__NEXT_P) {
// Defer page registration for another tick. This will increase the overall
// latency in hydrating the page, but reduce the total blocking time.
window.__NEXT_P.map((p) => setTimeout(() => register(p), 0))
}
window.__NEXT_P = []
;(window.__NEXT_P as any).push = register
headManager = initHeadManager()
headManager.getIsSsr = () => {
return router.isSsr
}
appElement = document.getElementById('__next')
return { assetPrefix: prefix }
}
function renderApp(App: AppComponent, appProps: AppProps) {
return <App {...appProps} />
}
function AppContainer({
children,
}: React.PropsWithChildren<{}>): React.ReactElement {
return (
<Container
fn={(error) =>
// TODO: Fix disabled eslint rule
// eslint-disable-next-line @typescript-eslint/no-use-before-define
renderError({ App: CachedApp, err: error }).catch((err) =>
console.error('Error rendering page: ', err)
)
}
>
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={headManager}>
<ImageConfigContext.Provider
value={process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete}
>
{children}
</ImageConfigContext.Provider>
</HeadManagerContext.Provider>
</RouterContext.Provider>
</Container>
)
}
const wrapApp =
(App: AppComponent) =>
(wrappedAppProps: Record<string, any>): JSX.Element => {
const appProps: AppProps = {
...wrappedAppProps,
Component: CachedComponent,
err: initialData.err,
router,
}
return <AppContainer>{renderApp(App, appProps)}</AppContainer>
}
// This method handles all runtime and debug errors.
// 404 and 500 errors are special kind of errors
// and they are still handle via the main render method.
function renderError(renderErrorProps: RenderErrorProps): Promise<any> {
let { App, err } = renderErrorProps
2020-05-15 20:14:44 +02:00
// In development runtime errors are caught by our overlay
2019-07-11 23:23:07 +02:00
// In production we catch runtime errors using componentDidCatch which will trigger renderError
if (process.env.NODE_ENV !== 'production') {
2020-05-15 20:14:44 +02:00
// A Next.js rendering runtime error is always unrecoverable
// FIXME: let's make this recoverable (error in GIP client-transition)
webpackHMR.onUnrecoverableError()
// We need to render an empty <App> so that the `<ReactDevOverlay>` can
// render itself.
// TODO: Fix disabled eslint rule
// eslint-disable-next-line @typescript-eslint/no-use-before-define
2020-05-15 20:14:44 +02:00
return doRender({
App: () => null,
props: {},
Component: () => null,
styleSheets: [],
2020-05-15 20:14:44 +02:00
})
}
// Make sure we log the error to the console, otherwise users can't track down issues.
console.error(err)
console.error(
`A client-side exception has occurred, see here for more info: https://nextjs.org/docs/messages/client-side-exception-occurred`
)
return pageLoader
.loadPage('/_error')
.then(({ page: ErrorComponent, styleSheets }) => {
return lastAppProps?.Component === ErrorComponent
? import('../pages/_error')
.then((errorModule) => {
return import('../pages/_app').then((appModule) => {
App = appModule.default as any as AppComponent
renderErrorProps.App = App
return errorModule
})
})
.then((m) => ({
ErrorComponent: m.default as React.ComponentType<{}>,
styleSheets: [],
}))
: { ErrorComponent, styleSheets }
})
.then(({ ErrorComponent, styleSheets }) => {
// In production we do a normal render with the `ErrorComponent` as component.
// If we've gotten here upon initial render, we can use the props from the server.
// Otherwise, we need to call `getInitialProps` on `App` before mounting.
const AppTree = wrapApp(App)
const appCtx = {
Component: ErrorComponent,
AppTree,
router,
ctx: {
err,
pathname: initialData.page,
query: initialData.query,
asPath,
AppTree,
},
}
return Promise.resolve(
renderErrorProps.props?.err
? renderErrorProps.props
: loadGetInitialProps(App, appCtx)
).then((initProps) =>
// TODO: Fix disabled eslint rule
// eslint-disable-next-line @typescript-eslint/no-use-before-define
doRender({
...renderErrorProps,
err,
Component: ErrorComponent,
styleSheets,
props: initProps,
})
)
})
}
// Dummy component that we render as a child of Root so that we can
// toggle the correct styles before the page is rendered.
function Head({ callback }: { callback: () => void }): null {
// We use `useLayoutEffect` to guarantee the callback is executed
// as soon as React flushes the update.
React.useLayoutEffect(() => callback(), [callback])
return null
}
let reactRoot: any = null
// On initial render a hydrate should always happen
let shouldHydrate: boolean = true
function clearMarks(): void {
;['beforeRender', 'afterHydrate', 'afterRender', 'routeChange'].forEach(
(mark) => performance.clearMarks(mark)
)
}
function markHydrateComplete(): void {
if (!ST) return
performance.mark('afterHydrate') // mark end of hydration
performance.measure(
'Next.js-before-hydration',
'navigationStart',
'beforeRender'
)
performance.measure('Next.js-hydration', 'beforeRender', 'afterHydrate')
if (onPerfEntry) {
performance.getEntriesByName('Next.js-hydration').forEach(onPerfEntry)
}
clearMarks()
}
function markRenderComplete(): void {
if (!ST) return
performance.mark('afterRender') // mark end of render
const navStartEntries: PerformanceEntryList = performance.getEntriesByName(
'routeChange',
'mark'
)
if (!navStartEntries.length) return
performance.measure(
'Next.js-route-change-to-render',
navStartEntries[0].name,
'beforeRender'
)
performance.measure('Next.js-render', 'beforeRender', 'afterRender')
if (onPerfEntry) {
performance.getEntriesByName('Next.js-render').forEach(onPerfEntry)
performance
.getEntriesByName('Next.js-route-change-to-render')
.forEach(onPerfEntry)
}
clearMarks()
2020-05-18 21:24:37 +02:00
;['Next.js-route-change-to-render', 'Next.js-render'].forEach((measure) =>
performance.clearMeasures(measure)
)
}
function renderReactElement(
domEl: HTMLElement,
fn: (cb: () => void) => JSX.Element
): void {
// mark start of hydrate/render
if (ST) {
performance.mark('beforeRender')
}
const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete)
if (process.env.__NEXT_REACT_ROOT) {
if (!reactRoot) {
// Unlike with createRoot, you don't need a separate root.render() call here
reactRoot = ReactDOM.hydrateRoot(domEl, reactEl)
// TODO: Remove shouldHydrate variable when React 18 is stable as it can depend on `reactRoot` existing
shouldHydrate = false
} else {
const startTransition = (React as any).startTransition
startTransition(() => {
reactRoot.render(reactEl)
})
}
} else {
// The check for `.hydrate` is there to support React alternatives like preact
if (shouldHydrate) {
ReactDOM.hydrate(reactEl, domEl)
shouldHydrate = false
} else {
ReactDOM.render(reactEl, domEl)
}
}
}
function Root({
callbacks,
children,
}: React.PropsWithChildren<{
callbacks: Array<() => void>
}>): React.ReactElement {
// We use `useLayoutEffect` to guarantee the callbacks are executed
// as soon as React flushes the update
React.useLayoutEffect(
() => callbacks.forEach((callback) => callback()),
[callbacks]
)
// We should ask to measure the Web Vitals after rendering completes so we
// don't cause any hydration delay:
React.useEffect(() => {
measureWebVitals(onPerfEntry)
}, [])
if (process.env.__NEXT_TEST_MODE) {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
window.__NEXT_HYDRATED = true
if (window.__NEXT_HYDRATED_CB) {
window.__NEXT_HYDRATED_CB()
}
}, [])
}
return children as React.ReactElement
}
function doRender(input: RenderRouteInfo): Promise<any> {
let { App, Component, props, err }: RenderRouteInfo = input
let styleSheets: StyleSheetTuple[] | undefined =
'initial' in input ? undefined : input.styleSheets
Component = Component || lastAppProps.Component
props = props || lastAppProps.props
const appProps: AppProps = {
...props,
Component,
err,
router,
}
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
lastAppProps = appProps
let canceled: boolean = false
let resolvePromise: () => void
const renderPromise = new Promise<void>((resolve, reject) => {
if (lastRenderReject) {
lastRenderReject()
}
resolvePromise = () => {
lastRenderReject = null
resolve()
}
lastRenderReject = () => {
canceled = true
lastRenderReject = null
const error: any = new Error('Cancel rendering route')
error.cancelled = true
reject(error)
}
})
// This function has a return type to ensure it doesn't start returning a
// Promise. It should remain synchronous.
function onStart(): boolean {
if (
!styleSheets ||
// We use `style-loader` in development, so we don't need to do anything
// unless we're in production:
process.env.NODE_ENV !== 'production'
) {
return false
}
const currentStyleTags: HTMLStyleElement[] = looseToArray<HTMLStyleElement>(
document.querySelectorAll('style[data-n-href]')
)
const currentHrefs: Set<string | null> = new Set(
currentStyleTags.map((tag) => tag.getAttribute('data-n-href'))
)
const noscript: Element | null = document.querySelector(
'noscript[data-n-css]'
)
const nonce: string | null | undefined =
noscript?.getAttribute('data-n-css')
styleSheets.forEach(({ href, text }: { href: string; text: any }) => {
if (!currentHrefs.has(href)) {
const styleTag = document.createElement('style')
styleTag.setAttribute('data-n-href', href)
styleTag.setAttribute('media', 'x')
if (nonce) {
styleTag.setAttribute('nonce', nonce)
}
document.head.appendChild(styleTag)
styleTag.appendChild(document.createTextNode(text))
}
})
return true
}
function onHeadCommit(): void {
if (
// We use `style-loader` in development, so we don't need to do anything
// unless we're in production:
process.env.NODE_ENV === 'production' &&
// We can skip this during hydration. Running it wont cause any harm, but
// we may as well save the CPU cycles:
styleSheets &&
// Ensure this render was not canceled
!canceled
) {
const desiredHrefs: Set<string> = new Set(styleSheets.map((s) => s.href))
const currentStyleTags: HTMLStyleElement[] =
looseToArray<HTMLStyleElement>(
document.querySelectorAll('style[data-n-href]')
)
const currentHrefs: string[] = currentStyleTags.map(
(tag) => tag.getAttribute('data-n-href')!
)
// Toggle `<style>` tags on or off depending on if they're needed:
for (let idx = 0; idx < currentHrefs.length; ++idx) {
if (desiredHrefs.has(currentHrefs[idx])) {
currentStyleTags[idx].removeAttribute('media')
} else {
currentStyleTags[idx].setAttribute('media', 'x')
}
}
// Reorder styles into intended order:
let referenceNode: Element | null = document.querySelector(
'noscript[data-n-css]'
)
if (
// This should be an invariant:
referenceNode
) {
styleSheets.forEach(({ href }: { href: string }) => {
const targetTag: Element | null = document.querySelector(
`style[data-n-href="${href}"]`
)
if (
// This should be an invariant:
targetTag
) {
referenceNode!.parentNode!.insertBefore(
targetTag,
referenceNode!.nextSibling
)
referenceNode = targetTag
}
})
}
// Finally, clean up server rendered stylesheets:
looseToArray<HTMLLinkElement>(
document.querySelectorAll('link[data-n-p]')
).forEach((el) => {
el.parentNode!.removeChild(el)
})
}
if (input.scroll) {
const htmlElement = document.documentElement
const existing = htmlElement.style.scrollBehavior
htmlElement.style.scrollBehavior = 'auto'
window.scrollTo(input.scroll.x, input.scroll.y)
htmlElement.style.scrollBehavior = existing
}
}
function onRootCommit(): void {
resolvePromise()
}
onStart()
const elem: JSX.Element = (
<>
<Head callback={onHeadCommit} />
<AppContainer>
{renderApp(App, appProps)}
<Portal type="next-route-announcer">
<RouteAnnouncer />
</Portal>
</AppContainer>
</>
)
// We catch runtime errors using componentDidCatch which will trigger renderError
renderReactElement(appElement!, (callback) => (
<Root callbacks={[callback, onRootCommit]}>
{process.env.__NEXT_STRICT_MODE ? (
<React.StrictMode>{elem}</React.StrictMode>
) : (
elem
)}
</Root>
))
return renderPromise
}
async function render(renderingProps: RenderRouteInfo): Promise<void> {
if (renderingProps.err) {
await renderError(renderingProps)
return
}
try {
await doRender(renderingProps)
} catch (err) {
const renderErr = getProperError(err)
// bubble up cancelation errors
if ((renderErr as Error & { cancelled?: boolean }).cancelled) {
throw renderErr
}
if (process.env.NODE_ENV === 'development') {
// Ensure this error is displayed in the overlay in development
setTimeout(() => {
throw renderErr
})
}
await renderError({ ...renderingProps, err: renderErr })
}
}
export async function hydrate(opts?: { beforeRender?: () => Promise<void> }) {
let initialErr = initialData.err
try {
const appEntrypoint = await pageLoader.routeLoader.whenEntrypoint('/_app')
if ('error' in appEntrypoint) {
throw appEntrypoint.error
}
const { component: app, exports: mod } = appEntrypoint
CachedApp = app as AppComponent
if (mod && mod.reportWebVitals) {
onPerfEntry = ({
id,
name,
startTime,
value,
duration,
entryType,
entries,
add attribution to web vitals (#39368) This commit implements the main proposal presented in https://github.com/vercel/next.js/issues/39241 to add attribution to web vitals. Attribution adds more specific debugging info to web vitals, for example in the case of Cumulative Layout Shift (CLS), we might want to know > What's the first element that shifted when the single largest layout shift occurred? on in the case of Largest Contentful Paint (LCP), > What's the element corresponding to the LCP for the page? > If it is an image, what's the URL of the image resource? Attribution is *disabled* by default because it could potentially generate a lot data and overwhelm the RUM backend. It is enabled *per metric* (LCP, FCP, CLS, etc) As part of this change, `web-vitals` has been upgraded to v3.0.0 This version contains minor bug fixes, please see changelog at https://github.com/GoogleChrome/web-vitals/commit/9fe3cc02c875cb70ac0f1803f5e11b428e7a4014 Fixes #39241 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-10-04 02:17:30 +02:00
attribution,
}: any): void => {
// Combines timestamp with random number for unique ID
const uniqueID: string = `${Date.now()}-${
Math.floor(Math.random() * (9e12 - 1)) + 1e12
}`
let perfStartEntry: string | undefined
if (entries && entries.length) {
perfStartEntry = entries[0].startTime
}
const webVitals: NextWebVitalsMetric = {
id: id || uniqueID,
name,
startTime: startTime || perfStartEntry,
value: value == null ? duration : value,
label:
entryType === 'mark' || entryType === 'measure'
? 'custom'
: 'web-vital',
}
add attribution to web vitals (#39368) This commit implements the main proposal presented in https://github.com/vercel/next.js/issues/39241 to add attribution to web vitals. Attribution adds more specific debugging info to web vitals, for example in the case of Cumulative Layout Shift (CLS), we might want to know > What's the first element that shifted when the single largest layout shift occurred? on in the case of Largest Contentful Paint (LCP), > What's the element corresponding to the LCP for the page? > If it is an image, what's the URL of the image resource? Attribution is *disabled* by default because it could potentially generate a lot data and overwhelm the RUM backend. It is enabled *per metric* (LCP, FCP, CLS, etc) As part of this change, `web-vitals` has been upgraded to v3.0.0 This version contains minor bug fixes, please see changelog at https://github.com/GoogleChrome/web-vitals/commit/9fe3cc02c875cb70ac0f1803f5e11b428e7a4014 Fixes #39241 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-10-04 02:17:30 +02:00
if (attribution) {
webVitals.attribution = attribution
}
mod.reportWebVitals(webVitals)
}
}
const pageEntrypoint =
// The dev server fails to serve script assets when there's a hydration
// error, so we need to skip waiting for the entrypoint.
process.env.NODE_ENV === 'development' && initialData.err
? { error: initialData.err }
: await pageLoader.routeLoader.whenEntrypoint(initialData.page)
if ('error' in pageEntrypoint) {
throw pageEntrypoint.error
}
CachedComponent = pageEntrypoint.component
if (process.env.NODE_ENV !== 'production') {
const { isValidElementType } = require('next/dist/compiled/react-is')
if (!isValidElementType(CachedComponent)) {
throw new Error(
`The default export is not a React Component in page: "${initialData.page}"`
)
}
}
} catch (error) {
// This catches errors like throwing in the top level of a module
initialErr = getProperError(error)
}
if (process.env.NODE_ENV === 'development') {
const {
getServerError,
} = require('next/dist/compiled/@next/react-dev-overlay/dist/client')
// Server-side runtime errors need to be re-thrown on the client-side so
// that the overlay is rendered.
if (initialErr) {
if (initialErr === initialData.err) {
setTimeout(() => {
let error
try {
// Generate a new error object. We `throw` it because some browsers
// will set the `stack` when thrown, and we want to ensure ours is
// not overridden when we re-throw it below.
throw new Error(initialErr!.message)
} catch (e) {
error = e as Error
}
error.name = initialErr!.name
error.stack = initialErr!.stack
throw getServerError(error, initialErr!.source)
})
}
// We replaced the server-side error with a client-side error, and should
// no longer rewrite the stack trace to a Node error.
else {
setTimeout(() => {
throw initialErr
})
}
}
}
if (window.__NEXT_PRELOADREADY) {
await window.__NEXT_PRELOADREADY(initialData.dynamicIds)
}
router = createRouter(initialData.page, initialData.query, asPath, {
initialProps: initialData.props,
pageLoader,
App: CachedApp,
Component: CachedComponent,
wrapApp,
err: initialErr,
isFallback: Boolean(initialData.isFallback),
subscription: (info, App, scroll) =>
render(
Object.assign<
{},
Omit<RenderRouteInfo, 'App' | 'scroll'>,
Pick<RenderRouteInfo, 'App' | 'scroll'>
>({}, info, {
App,
scroll,
}) as RenderRouteInfo
),
locale: initialData.locale,
locales: initialData.locales,
defaultLocale,
domainLocales: initialData.domainLocales,
isPreview: initialData.isPreview,
})
initialMatchesMiddleware = await router._initialMatchesMiddlewarePromise
const renderCtx: RenderRouteInfo = {
App: CachedApp,
initial: true,
Component: CachedComponent,
props: initialData.props,
err: initialErr,
}
if (opts?.beforeRender) {
await opts.beforeRender()
}
render(renderCtx)
}