diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 9cfbb276b8d4c5..806a178e255335 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -15,7 +15,12 @@ import { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-lo import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' import { LoadedEnvFiles } from '@next/env' import { parse } from '../build/swc' -import { isCustomErrorPage, isFlightPage, isReservedPage } from './utils' +import { + getRawPageExtensions, + isCustomErrorPage, + isFlightPage, + isReservedPage, +} from './utils' import { ssrEntries } from './webpack/plugins/middleware-plugin' import { MIDDLEWARE_RUNTIME_WEBPACK, @@ -28,7 +33,14 @@ export type PagesMapping = { } export function getPageFromPath(pagePath: string, extensions: string[]) { - let page = pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '') + const rawExtensions = getRawPageExtensions(extensions) + const pickedExtensions = pagePath.includes('/_app.server.') + ? rawExtensions + : extensions + let page = pagePath.replace( + new RegExp(`\\.+(${pickedExtensions.join('|')})$`), + '' + ) page = page.replace(/\\/g, '/').replace(/\/index$/, '') return page === '' ? '/' : page } @@ -85,10 +97,17 @@ export function createPagesMapping( pages['/_app'] = `${PAGES_DIR_ALIAS}/_app` pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` pages['/_document'] = `${PAGES_DIR_ALIAS}/_document` + if (hasServerComponents) { + pages['/_app.server'] = `${PAGES_DIR_ALIAS}/_app.server` + } } else { pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app' pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error' pages['/_document'] = pages['/_document'] || `next/dist/pages/_document` + if (hasServerComponents) { + pages['/_app.server'] = + pages['/_app.server'] || 'next/dist/pages/_app.server' + } } return pages } @@ -220,6 +239,7 @@ export async function createEntrypoints( const defaultServerlessOptions = { absoluteAppPath: pages['/_app'], + absoluteAppServerPath: pages['/_app.server'], absoluteDocumentPath: pages['/_document'], absoluteErrorPath: pages['/_error'], absolute404Path: pages['/404'] || '', @@ -312,6 +332,7 @@ export async function createEntrypoints( } else if ( isLikeServerless && page !== '/_app' && + page !== '/_app.server' && page !== '/_document' && !isEdgeRuntime ) { @@ -325,7 +346,7 @@ export async function createEntrypoints( )}!` } - if (page === '/_document') { + if (page === '/_document' || page === '/_app.server') { return } diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 110ef701c36ca1..9f1bf289ce8dd7 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -170,6 +170,7 @@ export async function printTreeView( !( e === '/_document' || e === '/_error' || + e === '/_app.server' || (!hasCustomApp && e === '/_app') ) ) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 33b3bdf0a34e89..b90e4974bb9ea2 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -554,12 +554,19 @@ export default async function getBaseWebpackConfig( if (dev) { customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [ - ...config.pageExtensions.reduce((prev, ext) => { + ...rawPageExtensions.reduce((prev, ext) => { prev.push(path.join(pagesDir, `_app.${ext}`)) return prev }, [] as string[]), 'next/dist/pages/_app.js', ] + customAppAliases[`${PAGES_DIR_ALIAS}/_app.server`] = [ + ...rawPageExtensions.reduce((prev, ext) => { + prev.push(path.join(pagesDir, `_app.server.${ext}`)) + return prev + }, [] as string[]), + 'next/dist/pages/_app.server.js', + ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ ...config.pageExtensions.reduce((prev, ext) => { prev.push(path.join(pagesDir, `_error.${ext}`)) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 3e07e9ebb93349..a3b57409678ad8 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -7,6 +7,7 @@ export default async function middlewareSSRLoader(this: any) { buildId, absolutePagePath, absoluteAppPath, + absoluteAppServerPath, absoluteDocumentPath, absolute500Path, absoluteErrorPath, @@ -16,6 +17,8 @@ export default async function middlewareSSRLoader(this: any) { const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const stringifiedAppPath = stringifyRequest(this, absoluteAppPath) + const stringifiedAppServerPath = stringifyRequest(this, absoluteAppServerPath) + const stringifiedErrorPath = stringifyRequest(this, absoluteErrorPath) const stringifiedDocumentPath = stringifyRequest(this, absoluteDocumentPath) const stringified500Path = absolute500Path @@ -31,6 +34,7 @@ export default async function middlewareSSRLoader(this: any) { import Document from ${stringifiedDocumentPath} const appMod = require(${stringifiedAppPath}) + const appServerMod = require(${stringifiedAppServerPath}) const pageMod = require(${stringifiedPagePath}) const errorMod = require(${stringifiedErrorPath}) const error500Mod = ${stringified500Path} ? require(${stringified500Path}) : null @@ -56,6 +60,8 @@ export default async function middlewareSSRLoader(this: any) { buildManifest, reactLoadableManifest, serverComponentManifest: ${isServerComponent} ? rscManifest : null, + appServerMod: appServerMod, + isServerComponent: ${isServerComponent}, config: ${stringifiedConfig}, buildId: ${JSON.stringify(buildId)}, }) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 44f08b11c9ea3e..09143b480c9b2f 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -27,6 +27,7 @@ export function getRender({ serverComponentManifest, config, buildId, + appServerMod, }: { dev: boolean page: string @@ -37,7 +38,9 @@ export function getRender({ Document: DocumentType buildManifest: BuildManifest reactLoadableManifest: ReactLoadableManifest - serverComponentManifest: any | null + serverComponentManifest: any + appServerMod: any + isServerComponent: boolean config: NextConfig buildId: string }) { @@ -48,6 +51,7 @@ export function getRender({ Document, App: appMod.default as AppType, AppMod: appMod, + AppServerMod: appServerMod, } const server = new WebServer({ diff --git a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts index e562239c529581..723e841c8145a3 100644 --- a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts @@ -49,6 +49,10 @@ export default class PagesManifestPlugin implements webpack.Plugin { file.endsWith('.js') ) + // Skip _app.server entry which is empty + if (!files.length) { + continue + } // Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency. pages[pagePath] = files[files.length - 1] diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 941c51bcd9dbe0..832cb4914539de 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -88,6 +88,7 @@ let webpackHMR: any let CachedApp: AppComponent, onPerfEntry: (metric: any) => void let CachedComponent: React.ComponentType let isAppRSC: boolean +let isRSCPage: boolean class Container extends React.Component<{ fn: (err: Error, info?: any) => void @@ -333,6 +334,7 @@ export async function hydrate(opts?: { beforeRender?: () => Promise }) { throw pageEntrypoint.error } CachedComponent = pageEntrypoint.component + isRSCPage = !!pageEntrypoint.exports.__next_rsc__ if (process.env.NODE_ENV !== 'production') { const { isValidElementType } = require('next/dist/compiled/react-is') @@ -646,7 +648,8 @@ function AppContainer({ } function renderApp(App: AppComponent, appProps: AppProps) { - if (process.env.__NEXT_RSC && isAppRSC) { + console.log('isRSCPage', isRSCPage) + if (process.env.__NEXT_RSC && isRSCPage) { const { Component, err: _, router: __, ...props } = appProps return } else { diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 0f241d21ffe6fd..9eff2e8a40e7dc 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -239,7 +239,12 @@ export default async function exportApp( continue } - if (page === '/_document' || page === '/_app' || page === '/_error') { + if ( + page === '/_document' || + page === '/_app.server' || + page === '/_app' || + page === '/_error' + ) { continue } diff --git a/packages/next/pages/_app.server.tsx b/packages/next/pages/_app.server.tsx new file mode 100644 index 00000000000000..993aef49d48d71 --- /dev/null +++ b/packages/next/pages/_app.server.tsx @@ -0,0 +1,6 @@ +import React from 'react' + +export type AppProps = { children: React.ReactNode } +export default function AppServer({ children }: AppProps) { + return children +} diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index ed5d7ee5aea342..8f09bbc49a6770 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -564,6 +564,7 @@ export default class HotReloader { page, stringifiedConfig: JSON.stringify(this.config), absoluteAppPath: this.pagesMapping['/_app'], + absoluteAppServerPath: this.pagesMapping['/_app.server'], absoluteDocumentPath: this.pagesMapping['/_document'], absoluteErrorPath: this.pagesMapping['/_error'], absolute404Path: this.pagesMapping['/404'] || '', diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 9f0fd64aba5615..641088376cf843 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -680,6 +680,7 @@ export default class DevServer extends Server { } } + console.error(err) if (!usedOriginalStack) { if (type === 'warning') { Log.warn(err + '') @@ -937,9 +938,11 @@ export default class DevServer extends Server { try { await this.hotReloader!.ensurePage(pathname) + const serverComponents = this.nextConfig.experimental.serverComponents + // When the new page is compiled, we need to reload the server component // manifest. - if (this.nextConfig.experimental.serverComponents) { + if (serverComponents) { this.serverComponentManifest = super.getServerComponentManifest() } diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 361f0183899d55..3ee2170fcaefe0 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -31,7 +31,7 @@ export type LoadComponentsReturnType = { pageConfig: PageConfig buildManifest: BuildManifest reactLoadableManifest: ReactLoadableManifest - serverComponentManifest?: any | null + serverComponentManifest?: any Document: DocumentType App: AppType getStaticProps?: GetStaticProps @@ -39,6 +39,7 @@ export type LoadComponentsReturnType = { getServerSideProps?: GetServerSideProps ComponentMod: any AppMod: any + AppServerMod: any } export async function loadDefaultErrorComponents(distDir: string) { @@ -57,6 +58,8 @@ export async function loadDefaultErrorComponents(distDir: string) { reactLoadableManifest: {}, ComponentMod, AppMod, + // TODO detect server components + AppServerMod: AppMod, } } @@ -99,10 +102,11 @@ export async function loadComponents( } as LoadComponentsReturnType } - const [DocumentMod, AppMod, ComponentMod] = await Promise.all([ + const [DocumentMod, AppMod, ComponentMod, AppServerMod] = await Promise.all([ requirePage('/_document', distDir, serverless), requirePage('/_app', distDir, serverless), requirePage(pathname, distDir, serverless), + serverComponents ? requirePage('/_app.server', distDir, serverless) : null, ]) const [buildManifest, reactLoadableManifest, serverComponentManifest] = @@ -129,6 +133,7 @@ export async function loadComponents( pageConfig: ComponentMod.config || {}, ComponentMod, AppMod, + AppServerMod, getServerSideProps, getStaticProps, getStaticPaths, diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index c4fb8055765680..a48877b378eb08 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -658,7 +658,8 @@ export default class NextNodeServer extends BaseServer { const components = await loadComponents( this.distDir, pagePath!, - !this.renderOpts.dev && this._isLikeServerless + !this.renderOpts.dev && this._isLikeServerless, + this.renderOpts.serverComponents ) if ( diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 2b5f36910aef78..fd256516b72a74 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -77,6 +77,7 @@ import { } from './node-web-streams-helper' import { ImageConfigContext } from '../shared/lib/image-config-context' import { FlushEffectsContext } from '../shared/lib/flush-effects' +import { interopDefault } from '../lib/interop-default' let optimizeAmp: typeof import('./optimize-amp').default let getFontDefinitionFromManifest: typeof import('./font-utils').getFontDefinitionFromManifest @@ -193,10 +194,13 @@ function enhanceComponents( } } -function renderFlight(AppMod: any, Component: React.ComponentType, props: any) { - const AppServer = AppMod.__next_rsc__ - ? (AppMod.default as React.ComponentType) +function renderFlight(AppMod: any, ComponentMod: any, props: any) { + const App = interopDefault(AppMod) + const Component = interopDefault(ComponentMod) + const AppServer = !!ComponentMod.__next_rsc__ + ? (App as React.ComponentType) : React.Fragment + return ( @@ -360,8 +364,9 @@ const useFlightResponse = createFlightHook() // Create the wrapper component for a Flight stream. function createServerComponentRenderer( - OriginalComponent: React.ComponentType, + // OriginalComponent: React.ComponentType, AppMod: any, + // App: any, ComponentMod: any, { cachePrefix, @@ -377,12 +382,13 @@ function createServerComponentRenderer( // react-server-dom-webpack. This is a hack until we find a better way. // @ts-ignore globalThis.__webpack_require__ = ComponentMod.__next_rsc__.__webpack_require__ + const Component = interopDefault(ComponentMod) const writable = transformStream.writable - const ServerComponentWrapper = (props: any) => { + function ServerComponentWrapper(props: any) { const id = (React as any).useId() const reqStream: ReadableStream = renderToReadableStream( - renderFlight(AppMod, OriginalComponent, props), + renderFlight(AppMod, ComponentMod, props), serverComponentManifest ) @@ -397,10 +403,6 @@ function createServerComponentRenderer( return root } - const Component = (props: any) => { - return - } - // Although it's not allowed to attach some static methods to Component, // we still re-assign all the component APIs to keep the behavior unchanged. for (const methodName of [ @@ -409,13 +411,13 @@ function createServerComponentRenderer( 'getServerSideProps', 'getStaticPaths', ]) { - const method = (OriginalComponent as any)[methodName] + const method = (Component as any)[methodName] if (method) { - ;(Component as any)[methodName] = method + ;(ServerComponentWrapper as any)[methodName] = method } } - return Component + return ServerComponentWrapper } export async function renderToHTML( @@ -439,7 +441,6 @@ export async function renderToHTML( err, dev = false, ampPath = '', - App, pageConfig = {}, buildManifest, fontManifest, @@ -459,13 +460,13 @@ export async function renderToHTML( reactRoot, runtime: globalRuntime, ComponentMod, - AppMod, + AppMod: AppClientMod, + AppServerMod, } = renderOpts const hasConcurrentFeatures = reactRoot let Document = renderOpts.Document - const OriginalComponent = renderOpts.Component // We don't need to opt-into the flight inlining logic if the page isn't a RSC. const isServerComponent = @@ -475,6 +476,10 @@ export async function renderToHTML( let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component + + const AppMod = isServerComponent ? AppServerMod : AppClientMod + const App = interopDefault(AppMod) + let serverComponentsInlinedTransformStream: TransformStream< Uint8Array, Uint8Array @@ -483,16 +488,11 @@ export async function renderToHTML( if (isServerComponent) { serverComponentsInlinedTransformStream = new TransformStream() const search = stringifyQuery(query) - Component = createServerComponentRenderer( - OriginalComponent, - AppMod, - ComponentMod, - { - cachePrefix: pathname + (search ? `?${search}` : ''), - transformStream: serverComponentsInlinedTransformStream, - serverComponentManifest, - } - ) + Component = createServerComponentRenderer(AppMod, ComponentMod, { + cachePrefix: pathname + (search ? `?${search}` : ''), + transformStream: serverComponentsInlinedTransformStream, + serverComponentManifest, + }) } const getFontDefinition = (url: string): string => { @@ -707,7 +707,8 @@ export async function renderToHTML( AppTree: (props: any) => { return ( - + {/* */} + {renderFlight(AppMod, ComponentMod, { ...props, router })} ) }, @@ -1186,7 +1187,7 @@ export async function renderToHTML( if (renderServerComponentData) { const stream: ReadableStream = renderToReadableStream( - renderFlight(AppMod, OriginalComponent, { + renderFlight(AppMod, ComponentMod, { ...props.pageProps, ...serverComponentProps, }), @@ -1342,7 +1343,7 @@ export async function renderToHTML( ) : ( - {isServerComponent && AppMod.__next_rsc__ ? ( + {isServerComponent && AppServerMod.__next_rsc__ ? ( // _app.server.js is used. ) : ( @@ -1503,7 +1504,6 @@ export async function renderToHTML( optimizeFonts: renderOpts.optimizeFonts, nextScriptWorkers: renderOpts.nextScriptWorkers, runtime: globalRuntime, - hasConcurrentFeatures, } const document = ( diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index f90ee195447f79..8d0ddaa7c19d3b 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1829,6 +1829,13 @@ export async function pages_app(task, opts) { .target('dist/pages') } +export async function pages_app_server(task, opts) { + await task + .source('pages/_app.server.tsx') + .swc('client', { dev: opts.dev, keepImportAssertions: true }) + .target('dist/pages') +} + export async function pages_error(task, opts) { await task .source('pages/_error.tsx') @@ -1852,7 +1859,13 @@ export async function pages_document_server(task, opts) { export async function pages(task, opts) { await task.parallel( - ['pages_app', 'pages_error', 'pages_document', 'pages_document_server'], + [ + 'pages_app', + 'pages_app_server', + 'pages_error', + 'pages_document', + 'pages_document_server', + ], opts ) } diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.server.js new file mode 100644 index 00000000000000..877caf3c9c6fc4 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.server.js @@ -0,0 +1,7 @@ +export default function AppServer({ children }) { + return ( +
+ {children} +
+ ) +}