diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 39174b90bcdb..59a4b4fa10e8 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -63,130 +63,32 @@ type RenderRouteInfo = PrivateRouteInfo & { scroll?: { x: number; y: number } | null } type RenderErrorProps = Omit - -const data: typeof window['__NEXT_DATA__'] = JSON.parse( - document.getElementById('__NEXT_DATA__')!.textContent! -) -window.__NEXT_DATA__ = data +type RegisterFn = (input: [string, () => void]) => void export const version = process.env.__NEXT_VERSION +export let router: Router +export const emitter: MittEmitter = mitt() const looseToArray = (input: any): T[] => [].slice.call(input) -const { - props: hydrateProps, - err: hydrateErr, - page, - query, - buildId, - assetPrefix, - runtimeConfig, - dynamicIds, - isFallback, - locale, - locales, - domainLocales, - isPreview, - rsc, -} = data - -let { defaultLocale } = data - -const prefix: string = 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: runtimeConfig || {}, -}) - -let asPath: string = getURL() - -// make sure not to attempt stripping basePath for 404s -if (hasBasePath(asPath)) { - asPath = delBasePath(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 (locales) { - const parsedAs = parseRelativeUrl(asPath) - const localePathResult = normalizeLocalePath(parsedAs.pathname, 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 = 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 (data.scriptLoader) { - const { initScriptLoader } = require('./script') - initScriptLoader(data.scriptLoader) -} - -type RegisterFn = (input: [string, () => void]) => void - -const pageLoader: PageLoader = new PageLoader(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 - -const headManager: { +let initialData: NEXT_DATA +let defaultLocale: string | undefined = undefined +let asPath: string +let pageLoader: PageLoader +let appElement: HTMLElement | null +let headManager: { mountedInstances: Set updateHead: (head: JSX.Element[]) => void getIsSsr?: () => boolean -} = initHeadManager() -const appElement: HTMLElement | null = document.getElementById('__next') +} let lastRenderReject: (() => void) | null let webpackHMR: any -export let router: Router let CachedApp: AppComponent, onPerfEntry: (metric: any) => void +let CachedComponent: React.ComponentType let isAppRSC: boolean -headManager.getIsSsr = () => { - return router.isSsr -} - class Container extends React.Component<{ fn: (err: Error, info?: any) => void }> { @@ -206,15 +108,15 @@ class Container extends React.Component<{ // We don't update for 404 requests as this can modify // the asPath unexpectedly e.g. adding basePath when // it wasn't originally present - page !== '/404' && - page !== '/_error' && - (isFallback || - (data.nextExport && + initialData.page !== '/404' && + initialData.page !== '/_error' && + (initialData.isFallback || + (initialData.nextExport && (isDynamicRoute(router.pathname) || location.search || process.env.__NEXT_HAS_REWRITES)) || - (hydrateProps && - hydrateProps.__N_SSG && + (initialData.props && + initialData.props.__N_SSG && (location.search || process.env.__NEXT_HAS_REWRITES))) ) { // update query on mount for exported pages @@ -238,7 +140,7 @@ class Container extends React.Component<{ // not shallow. // Other pages (strictly updating query) happens shallowly, as data // requirements would already be present. - shallow: !isFallback, + shallow: !initialData.isFallback, } ) } @@ -273,18 +175,110 @@ class Container extends React.Component<{ } } -export const emitter: MittEmitter = mitt() -let CachedComponent: React.ComponentType - -export async function initNext( - opts: { webpackHMR?: any; beforeRender?: () => Promise } = {} -) { +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 } - let initialErr = hydrateErr + 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 = delBasePath(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 } +} + +export async function hydrate(opts?: { beforeRender?: () => Promise }) { + let initialErr = initialData.err try { const appEntrypoint = await pageLoader.routeLoader.whenEntrypoint('/_app') @@ -332,9 +326,9 @@ export async function initNext( 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' && hydrateErr - ? { error: hydrateErr } - : await pageLoader.routeLoader.whenEntrypoint(page) + process.env.NODE_ENV === 'development' && initialData.err + ? { error: initialData.err } + : await pageLoader.routeLoader.whenEntrypoint(initialData.page) if ('error' in pageEntrypoint) { throw pageEntrypoint.error } @@ -344,7 +338,7 @@ export async function initNext( const { isValidElementType } = require('next/dist/compiled/react-is') if (!isValidElementType(CachedComponent)) { throw new Error( - `The default export is not a React Component in page: "${page}"` + `The default export is not a React Component in page: "${initialData.page}"` ) } } @@ -360,7 +354,7 @@ export async function initNext( // Server-side runtime errors need to be re-thrown on the client-side so // that the overlay is rendered. if (initialErr) { - if (initialErr === hydrateErr) { + if (initialErr === initialData.err) { setTimeout(() => { let error try { @@ -377,7 +371,7 @@ export async function initNext( // Errors from the middleware are reported as client-side errors // since the middleware is compiled using the client compiler - if ('middleware' in hydrateErr) { + if (initialData.err && 'middleware' in initialData.err) { throw error } @@ -396,17 +390,17 @@ export async function initNext( } if (window.__NEXT_PRELOADREADY) { - await window.__NEXT_PRELOADREADY(dynamicIds) + await window.__NEXT_PRELOADREADY(initialData.dynamicIds) } - router = createRouter(page, query, asPath, { - initialProps: hydrateProps, + router = createRouter(initialData.page, initialData.query, asPath, { + initialProps: initialData.props, pageLoader, App: CachedApp, Component: CachedComponent, wrapApp, err: initialErr, - isFallback: Boolean(isFallback), + isFallback: Boolean(initialData.isFallback), subscription: (info, App, scroll) => render( Object.assign< @@ -418,23 +412,23 @@ export async function initNext( scroll, }) as RenderRouteInfo ), - locale, - locales, + locale: initialData.locale, + locales: initialData.locales, defaultLocale, - domainLocales, - isPreview, - isRsc: rsc, + domainLocales: initialData.domainLocales, + isPreview: initialData.isPreview, + isRsc: initialData.rsc, }) const renderCtx: RenderRouteInfo = { App: CachedApp, initial: true, Component: CachedComponent, - props: hydrateProps, + props: initialData.props, err: initialErr, } - if (opts.beforeRender) { + if (opts?.beforeRender) { await opts.beforeRender() } @@ -514,7 +508,13 @@ function renderError(renderErrorProps: RenderErrorProps): Promise { Component: ErrorComponent, AppTree, router, - ctx: { err, pathname: page, query, asPath, AppTree }, + ctx: { + err, + pathname: initialData.page, + query: initialData.query, + asPath, + AppTree, + }, } return Promise.resolve( renderErrorProps.props @@ -660,7 +660,7 @@ const wrapApp = const appProps: AppProps = { ...wrappedAppProps, Component: CachedComponent, - err: hydrateErr, + err: initialData.err, router, } return {renderApp(App, appProps)} @@ -831,7 +831,8 @@ function doRender(input: RenderRouteInfo): Promise { Component = Component || lastAppProps.Component props = props || lastAppProps.props - const isRSC = process.env.__NEXT_RSC && 'initial' in input ? !!rsc : !!__N_RSC + const isRSC = + process.env.__NEXT_RSC && 'initial' in input ? !!initialData.rsc : !!__N_RSC const appProps: AppProps = { ...props, diff --git a/packages/next/client/next-dev.js b/packages/next/client/next-dev.js index 65f2c08c6cc7..d3e4ad7c5754 100644 --- a/packages/next/client/next-dev.js +++ b/packages/next/client/next-dev.js @@ -1,4 +1,4 @@ -import { initNext, version, router, emitter } from './' +import { initialize, hydrate, version, router, emitter } from './' import initOnDemandEntries from './dev/on-demand-entries-client' import initWebpackHMR from './dev/webpack-hot-middleware-client' import initializeBuildWatcher from './dev/dev-build-watcher' @@ -9,15 +9,6 @@ import { urlQueryToSearchParams, } from '../shared/lib/router/utils/querystring' -const { - __NEXT_DATA__: { assetPrefix }, -} = window - -const prefix = assetPrefix || '' -const webpackHMR = initWebpackHMR() - -connectHMR({ assetPrefix: prefix, path: '/_next/webpack-hmr' }) - if (!window._nextSetupHydrationWarning) { const origConsoleError = window.console.error window.console.error = (...args) => { @@ -45,63 +36,71 @@ window.next = { }, emitter, } -initNext({ webpackHMR, beforeRender: displayContent }) - .then(() => { - initOnDemandEntries() - let buildIndicatorHandler = () => {} +const webpackHMR = initWebpackHMR() +initialize({ webpackHMR }) + .then(({ assetPrefix }) => { + connectHMR({ assetPrefix, path: '/_next/webpack-hmr' }) + + return hydrate({ beforeRender: displayContent }).then(() => { + initOnDemandEntries() - function devPagesManifestListener(event) { - if (event.data.indexOf('devPagesManifest') !== -1) { - fetch(`${prefix}/_next/static/development/_devPagesManifest.json`) - .then((res) => res.json()) - .then((manifest) => { - window.__DEV_PAGES_MANIFEST = manifest - }) - .catch((err) => { - console.log(`Failed to fetch devPagesManifest`, err) - }) - } else if (event.data.indexOf('middlewareChanges') !== -1) { - return window.location.reload() - } else if (event.data.indexOf('serverOnlyChanges') !== -1) { - const { pages } = JSON.parse(event.data) + let buildIndicatorHandler = () => {} - // Make sure to reload when the dev-overlay is showing for an - // API route - if (pages.includes(router.query.__NEXT_PAGE)) { + function devPagesManifestListener(event) { + if (event.data.indexOf('devPagesManifest') !== -1) { + fetch( + `${assetPrefix}/_next/static/development/_devPagesManifest.json` + ) + .then((res) => res.json()) + .then((manifest) => { + window.__DEV_PAGES_MANIFEST = manifest + }) + .catch((err) => { + console.log(`Failed to fetch devPagesManifest`, err) + }) + } else if (event.data.indexOf('middlewareChanges') !== -1) { return window.location.reload() - } + } else if (event.data.indexOf('serverOnlyChanges') !== -1) { + const { pages } = JSON.parse(event.data) + + // Make sure to reload when the dev-overlay is showing for an + // API route + if (pages.includes(router.query.__NEXT_PAGE)) { + return window.location.reload() + } - if (!router.clc && pages.includes(router.pathname)) { - console.log('Refreshing page data due to server-side change') + if (!router.clc && pages.includes(router.pathname)) { + console.log('Refreshing page data due to server-side change') - buildIndicatorHandler('building') + buildIndicatorHandler('building') - const clearIndicator = () => buildIndicatorHandler('built') + const clearIndicator = () => buildIndicatorHandler('built') - router - .replace( - router.pathname + - '?' + - String( - assign( - urlQueryToSearchParams(router.query), - new URLSearchParams(location.search) - ) - ), - router.asPath - ) - .finally(clearIndicator) + router + .replace( + router.pathname + + '?' + + String( + assign( + urlQueryToSearchParams(router.query), + new URLSearchParams(location.search) + ) + ), + router.asPath + ) + .finally(clearIndicator) + } } } - } - addMessageListener(devPagesManifestListener) + addMessageListener(devPagesManifestListener) - if (process.env.__NEXT_BUILD_INDICATOR) { - initializeBuildWatcher((handler) => { - buildIndicatorHandler = handler - }, process.env.__NEXT_BUILD_INDICATOR_POSITION) - } + if (process.env.__NEXT_BUILD_INDICATOR) { + initializeBuildWatcher((handler) => { + buildIndicatorHandler = handler + }, process.env.__NEXT_BUILD_INDICATOR_POSITION) + } + }) }) .catch((err) => { console.error('Error was not caught', err) diff --git a/packages/next/client/next.js b/packages/next/client/next.js index e17fdc100dd8..b0e36c94b515 100644 --- a/packages/next/client/next.js +++ b/packages/next/client/next.js @@ -1,4 +1,4 @@ -import { initNext, version, router, emitter } from './' +import { initialize, hydrate, version, router, emitter } from './' window.next = { version, @@ -9,4 +9,6 @@ window.next = { emitter, } -initNext().catch(console.error) +initialize({}) + .then(() => hydrate()) + .catch(console.error)