013a58d58e
As @timneutkens pointed out [here](https://github.com/vercel/next.js/pull/43819#discussion_r1042408158), changing the key will make Flight's module cache grow indefinitely. While I've thought about updating Flight's logic to clean the cache at some point, but that's tricky to do correctly as everything is asynchronous and we have to trigger clean up from outside (Webpack). So currently, a better way I can think of is to just give Flight a proxied object as `moduleExports`. Even if Flight is getting the same object from cache, it will always require the latest module from Webpack. ## 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)
282 lines
8.7 KiB
TypeScript
282 lines
8.7 KiB
TypeScript
/* global location */
|
|
import '../build/polyfills/polyfill-module'
|
|
// @ts-ignore react-dom/client exists when using React 18
|
|
import ReactDOMClient from 'react-dom/client'
|
|
import React, { use } from 'react'
|
|
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack/client'
|
|
|
|
import { HeadManagerContext } from '../shared/lib/head-manager-context'
|
|
import { GlobalLayoutRouterContext } from '../shared/lib/app-router-context'
|
|
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/no-ssr-error'
|
|
|
|
/// <reference types="react-dom/experimental" />
|
|
|
|
// Override chunk URL mapping in the webpack runtime
|
|
// https://github.com/webpack/webpack/blob/2738eebc7880835d88c727d364ad37f3ec557593/lib/RuntimeGlobals.js#L204
|
|
|
|
declare global {
|
|
const __webpack_require__: any
|
|
}
|
|
|
|
// eslint-disable-next-line no-undef
|
|
const getChunkScriptFilename = __webpack_require__.u
|
|
const chunkFilenameMap: any = {}
|
|
|
|
// eslint-disable-next-line no-undef
|
|
__webpack_require__.u = (chunkId: any) => {
|
|
return chunkFilenameMap[chunkId] || getChunkScriptFilename(chunkId)
|
|
}
|
|
|
|
// Ignore the module ID transform in client.
|
|
// eslint-disable-next-line no-undef
|
|
// @ts-expect-error TODO: fix type
|
|
self.__next_require__ =
|
|
process.env.NODE_ENV !== 'production'
|
|
? (id: string) => {
|
|
const mod = __webpack_require__(id)
|
|
if (typeof mod === 'object') {
|
|
// Return a proxy to flight client to make sure it's always getting
|
|
// the latest module, instead of being cached.
|
|
return new Proxy(mod, {
|
|
get(_target, prop) {
|
|
return __webpack_require__(id)[prop]
|
|
},
|
|
})
|
|
}
|
|
|
|
return mod
|
|
}
|
|
: __webpack_require__
|
|
|
|
// eslint-disable-next-line no-undef
|
|
;(self as any).__next_chunk_load__ = (chunk: string) => {
|
|
if (!chunk) return Promise.resolve()
|
|
const [chunkId, chunkFileName] = chunk.split(':')
|
|
chunkFilenameMap[chunkId] = `static/chunks/${chunkFileName}.js`
|
|
|
|
// @ts-ignore
|
|
// eslint-disable-next-line no-undef
|
|
return __webpack_chunk_load__(chunkId)
|
|
}
|
|
|
|
const appElement: HTMLElement | Document | null = document
|
|
|
|
const getCacheKey = () => {
|
|
const { pathname, search } = location
|
|
return pathname + search
|
|
}
|
|
|
|
const encoder = new TextEncoder()
|
|
|
|
let initialServerDataBuffer: string[] | undefined = undefined
|
|
let initialServerDataWriter: ReadableStreamDefaultController | undefined =
|
|
undefined
|
|
let initialServerDataLoaded = false
|
|
let initialServerDataFlushed = false
|
|
|
|
function nextServerDataCallback(
|
|
seg: [isBootStrap: 0] | [isNotBootstrap: 1, responsePartial: string]
|
|
): void {
|
|
if (seg[0] === 0) {
|
|
initialServerDataBuffer = []
|
|
} else {
|
|
if (!initialServerDataBuffer)
|
|
throw new Error('Unexpected server data: missing bootstrap script.')
|
|
|
|
if (initialServerDataWriter) {
|
|
initialServerDataWriter.enqueue(encoder.encode(seg[1]))
|
|
} else {
|
|
initialServerDataBuffer.push(seg[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// There might be race conditions between `nextServerDataRegisterWriter` and
|
|
// `DOMContentLoaded`. The former will be called when React starts to hydrate
|
|
// the root, the latter will be called when the DOM is fully loaded.
|
|
// For streaming, the former is called first due to partial hydration.
|
|
// For non-streaming, the latter can be called first.
|
|
// Hence, we use two variables `initialServerDataLoaded` and
|
|
// `initialServerDataFlushed` to make sure the writer will be closed and
|
|
// `initialServerDataBuffer` will be cleared in the right time.
|
|
function nextServerDataRegisterWriter(ctr: ReadableStreamDefaultController) {
|
|
if (initialServerDataBuffer) {
|
|
initialServerDataBuffer.forEach((val) => {
|
|
ctr.enqueue(encoder.encode(val))
|
|
})
|
|
if (initialServerDataLoaded && !initialServerDataFlushed) {
|
|
ctr.close()
|
|
initialServerDataFlushed = true
|
|
initialServerDataBuffer = undefined
|
|
}
|
|
}
|
|
|
|
initialServerDataWriter = ctr
|
|
}
|
|
|
|
// When `DOMContentLoaded`, we can close all pending writers to finish hydration.
|
|
const DOMContentLoaded = function () {
|
|
if (initialServerDataWriter && !initialServerDataFlushed) {
|
|
initialServerDataWriter.close()
|
|
initialServerDataFlushed = true
|
|
initialServerDataBuffer = undefined
|
|
}
|
|
initialServerDataLoaded = true
|
|
}
|
|
// It's possible that the DOM is already loaded.
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false)
|
|
} else {
|
|
DOMContentLoaded()
|
|
}
|
|
|
|
function onRecoverableError(err: any) {
|
|
// Using default react onRecoverableError
|
|
// x-ref: https://github.com/facebook/react/blob/d4bc16a7d69eb2ea38a88c8ac0b461d5f72cdcab/packages/react-dom/src/client/ReactDOMRoot.js#L83
|
|
const defaultOnRecoverableError =
|
|
typeof reportError === 'function'
|
|
? // In modern browsers, reportError will dispatch an error event,
|
|
// emulating an uncaught JavaScript error.
|
|
reportError
|
|
: (error: any) => {
|
|
window.console.error(error)
|
|
}
|
|
|
|
// Skip certain custom errors which are not expected to be reported on client
|
|
if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) return
|
|
defaultOnRecoverableError(err)
|
|
}
|
|
|
|
const nextServerDataLoadingGlobal = ((self as any).__next_f =
|
|
(self as any).__next_f || [])
|
|
nextServerDataLoadingGlobal.forEach(nextServerDataCallback)
|
|
nextServerDataLoadingGlobal.push = nextServerDataCallback
|
|
|
|
function createResponseCache() {
|
|
return new Map<string, any>()
|
|
}
|
|
const rscCache = createResponseCache()
|
|
|
|
function useInitialServerResponse(cacheKey: string): Promise<JSX.Element> {
|
|
const response = rscCache.get(cacheKey)
|
|
if (response) return response
|
|
|
|
const readable = new ReadableStream({
|
|
start(controller) {
|
|
nextServerDataRegisterWriter(controller)
|
|
},
|
|
})
|
|
|
|
const newResponse = createFromReadableStream(readable)
|
|
|
|
rscCache.set(cacheKey, newResponse)
|
|
return newResponse
|
|
}
|
|
|
|
function ServerRoot({ cacheKey }: { cacheKey: string }): JSX.Element {
|
|
React.useEffect(() => {
|
|
rscCache.delete(cacheKey)
|
|
})
|
|
const response = useInitialServerResponse(cacheKey)
|
|
const root = use(response)
|
|
return root
|
|
}
|
|
|
|
const StrictModeIfEnabled = process.env.__NEXT_STRICT_MODE_APP
|
|
? React.StrictMode
|
|
: React.Fragment
|
|
|
|
function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
|
|
React.useEffect(() => {
|
|
if (process.env.__NEXT_ANALYTICS_ID) {
|
|
require('./performance-relayer-app')()
|
|
}
|
|
}, [])
|
|
|
|
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 RSCComponent(props: any): JSX.Element {
|
|
const cacheKey = getCacheKey()
|
|
return <ServerRoot {...props} cacheKey={cacheKey} />
|
|
}
|
|
|
|
export function hydrate() {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const rootLayoutMissingTagsError = (self as any)
|
|
.__next_root_layout_missing_tags_error
|
|
const HotReload: typeof import('./components/react-dev-overlay/hot-reloader-client').default =
|
|
require('./components/react-dev-overlay/hot-reloader-client')
|
|
.default as typeof import('./components/react-dev-overlay/hot-reloader-client').default
|
|
|
|
// Don't try to hydrate if root layout is missing required tags, render error instead
|
|
if (rootLayoutMissingTagsError) {
|
|
const reactRootElement = document.createElement('div')
|
|
document.body.appendChild(reactRootElement)
|
|
const reactRoot = (ReactDOMClient as any).createRoot(reactRootElement, {
|
|
onRecoverableError,
|
|
})
|
|
|
|
reactRoot.render(
|
|
<GlobalLayoutRouterContext.Provider
|
|
value={{
|
|
tree: rootLayoutMissingTagsError.tree,
|
|
changeByServerResponse: () => {},
|
|
focusAndScrollRef: {
|
|
apply: false,
|
|
},
|
|
}}
|
|
>
|
|
<HotReload
|
|
assetPrefix={rootLayoutMissingTagsError.assetPrefix}
|
|
// initialState={{
|
|
// rootLayoutMissingTagsError: {
|
|
// missingTags: rootLayoutMissingTagsError.missingTags,
|
|
// },
|
|
// }}
|
|
/>
|
|
</GlobalLayoutRouterContext.Provider>
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
const reactEl = (
|
|
<StrictModeIfEnabled>
|
|
<HeadManagerContext.Provider
|
|
value={{
|
|
appDir: true,
|
|
}}
|
|
>
|
|
<Root>
|
|
<RSCComponent />
|
|
</Root>
|
|
</HeadManagerContext.Provider>
|
|
</StrictModeIfEnabled>
|
|
)
|
|
|
|
const options = {
|
|
onRecoverableError,
|
|
}
|
|
const isError = document.documentElement.id === '__next_error__'
|
|
const reactRoot = isError
|
|
? (ReactDOMClient as any).createRoot(appElement, options)
|
|
: (React as any).startTransition(() =>
|
|
(ReactDOMClient as any).hydrateRoot(appElement, reactEl, options)
|
|
)
|
|
if (isError) {
|
|
reactRoot.render(reactEl)
|
|
}
|
|
}
|