Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle on-demand-entries and error overlay for server components #38480

Merged
Merged
31 changes: 23 additions & 8 deletions packages/next/build/webpack-config.ts
Expand Up @@ -571,14 +571,29 @@ export default async function getBaseWebpackConfig(
.replace(/\\/g, '/'),
...(config.experimental.appDir
? {
[CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT]:
`./` +
path
.relative(
dir,
path.join(NEXT_PROJECT_ROOT_DIST_CLIENT, 'app-next.js')
)
.replace(/\\/g, '/'),
[CLIENT_STATIC_FILES_RUNTIME_MAIN_ROOT]: dev
? [
require.resolve(
`next/dist/compiled/@next/react-refresh-utils/dist/runtime`
),
`./` +
path
.relative(
dir,
path.join(
NEXT_PROJECT_ROOT_DIST_CLIENT,
'app-next-dev.js'
)
)
.replace(/\\/g, '/'),
]
: `./` +
path
.relative(
dir,
path.join(NEXT_PROJECT_ROOT_DIST_CLIENT, 'app-next.js')
)
.replace(/\\/g, '/'),
}
: {}),
} as ClientEntries)
Expand Down
6 changes: 6 additions & 0 deletions packages/next/build/webpack/loaders/next-app-loader.ts
Expand Up @@ -140,6 +140,12 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{

export const AppRouter = require('next/dist/client/components/app-router.client.js').default
export const LayoutRouter = require('next/dist/client/components/layout-router.client.js').default
export const HotReloader = ${
// Disable HotReloader component in production
this.mode === 'development'
? `require('next/dist/client/components/hot-reloader.client.js').default`
: 'null'
}
export const hooksClientContext = require('next/dist/client/components/hooks-client-context.js')

export const __next_app_webpack_require__ = __webpack_require__
Expand Down
69 changes: 62 additions & 7 deletions packages/next/client/app-index.tsx
Expand Up @@ -8,6 +8,46 @@ import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-we

/// <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__ = __webpack_require__

// eslint-disable-next-line no-undef
// @ts-expect-error TODO: fix type
self.__next_chunk_load__ = (chunk) => {
if (chunk.endsWith('.css')) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
document.head.appendChild(link)
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)
}

export const version = process.env.__NEXT_VERSION

const appElement: HTMLElement | Document | null = document
Expand Down Expand Up @@ -119,7 +159,7 @@ function useInitialServerResponse(cacheKey: string) {
return newResponse
}

const ServerRoot = ({ cacheKey }: { cacheKey: string }) => {
function ServerRoot({ cacheKey }: { cacheKey: string }) {
React.useEffect(() => {
rscCache.delete(cacheKey)
})
Expand All @@ -128,6 +168,19 @@ const ServerRoot = ({ cacheKey }: { cacheKey: string }) => {
return root
}

function ErrorOverlay({
children,
}: React.PropsWithChildren<{}>): React.ReactElement {
if (process.env.NODE_ENV === 'production') {
return <>{children}</>
} else {
const {
ReactDevOverlay,
} = require('next/dist/compiled/@next/react-dev-overlay/dist/client')
return <ReactDevOverlay globalOverlay>{children}</ReactDevOverlay>
}
}

function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
if (process.env.__NEXT_TEST_MODE) {
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand All @@ -143,17 +196,19 @@ function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
return children as React.ReactElement
}

const RSCComponent = () => {
function RSCComponent() {
const cacheKey = getCacheKey()
return <ServerRoot cacheKey={cacheKey} />
}

export function hydrate() {
renderReactElement(appElement!, () => (
<React.StrictMode>
<Root>
<RSCComponent />
</Root>
</React.StrictMode>
<ErrorOverlay>
<React.StrictMode>
<Root>
<RSCComponent />
</Root>
</React.StrictMode>
</ErrorOverlay>
))
}
14 changes: 14 additions & 0 deletions packages/next/client/app-next-dev.js
@@ -0,0 +1,14 @@
import { hydrate, version } from './app-index'

// TODO: implement FOUC guard

// TODO: hydration warning

window.next = {
version,
appDir: true,
}

hydrate()

// TODO: build indicator
36 changes: 1 addition & 35 deletions packages/next/client/app-next.js
Expand Up @@ -6,41 +6,7 @@ import 'next/dist/client/components/layout-router.client.js'

window.next = {
version,
root: true,
}

// Override chunk URL mapping in the webpack runtime
// https://github.com/webpack/webpack/blob/2738eebc7880835d88c727d364ad37f3ec557593/lib/RuntimeGlobals.js#L204

// eslint-disable-next-line no-undef
const getChunkScriptFilename = __webpack_require__.u
const chunkFilenameMap = {}

// eslint-disable-next-line no-undef
__webpack_require__.u = (chunkId) => {
return chunkFilenameMap[chunkId] || getChunkScriptFilename(chunkId)
}

// Ignore the module ID transform in client.
// eslint-disable-next-line no-undef
self.__next_require__ = __webpack_require__

// eslint-disable-next-line no-undef
self.__next_chunk_load__ = (chunk) => {
if (chunk.endsWith('.css')) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
document.head.appendChild(link)
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)
appDir: true,
}

hydrate()
3 changes: 3 additions & 0 deletions packages/next/client/components/app-router.client.tsx
Expand Up @@ -50,10 +50,12 @@ export default function AppRouter({
initialTree,
initialCanonicalUrl,
children,
hotReloader,
}: {
initialTree: FlightRouterState
initialCanonicalUrl: string
children: React.ReactNode
hotReloader?: React.ReactNode
}) {
const [{ tree, cache, pushRef, canonicalUrl }, dispatch] = React.useReducer<
typeof reducer
Expand Down Expand Up @@ -238,6 +240,7 @@ export default function AppRouter({
}}
>
{children}
{hotReloader}
</AppTreeContext.Provider>
</AppRouterContext.Provider>
</FullAppTreeContext.Provider>
Expand Down