diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index c3045f7ecadd0ac..3779caae4ebbbb9 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -157,6 +157,7 @@ export function createEntrypoints( const isFlight = isFlightPage(config, absolutePagePath) const webServerRuntime = !!config.experimental.concurrentFeatures + const hasServerComponents = !!config.experimental.serverComponents if (page.match(MIDDLEWARE_ROUTE)) { const loaderOpts: MiddlewareLoaderOptions = { @@ -179,6 +180,7 @@ export function createEntrypoints( absolute500Path: pages['/500'] || '', absolutePagePath, isServerComponent: isFlight, + serverComponents: hasServerComponents, ...defaultServerlessOptions, } as any)}!`, isServer: false, diff --git a/packages/next/build/webpack/loaders/next-flight-server-loader.ts b/packages/next/build/webpack/loaders/next-flight-server-loader.ts index 732533120fccfee..efc937d021bf7f2 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -32,7 +32,10 @@ async function parseImportsInfo( imports: Array, isClientCompilation: boolean, pageExtensions: string[] -): Promise { +): Promise<{ + source: string + defaultExportName: string +}> { const { body } = acorn.parse(source, { ecmaVersion: 11, sourceType: 'module', @@ -40,11 +43,12 @@ async function parseImportsInfo( let transformedSource = '' let lastIndex = 0 + let defaultExportName = 'RSComponent' for (let i = 0; i < body.length; i++) { const node = body[i] switch (node.type) { - case 'ImportDeclaration': + case 'ImportDeclaration': { const importSource = node.source.value if (!isClientCompilation) { @@ -57,9 +61,9 @@ async function parseImportsInfo( ) { continue } - transformedSource += source.substr( + transformedSource += source.substring( lastIndex, - node.source.start - lastIndex + node.source.start - 1 ) transformedSource += JSON.stringify(`${node.source.value}?flight`) } else { @@ -83,16 +87,21 @@ async function parseImportsInfo( lastIndex = node.source.end imports.push(`require(${JSON.stringify(importSource)})`) continue + } + case 'ExportDefaultDeclaration': { + defaultExportName = node.declaration.id.name + break + } default: break } } if (!isClientCompilation) { - transformedSource += source.substr(lastIndex) + transformedSource += source.substring(lastIndex) } - return transformedSource + return { source: transformedSource, defaultExportName } } export default async function transformSource( @@ -113,17 +122,32 @@ export default async function transformSource( } const imports: string[] = [] - const transformed = await parseImportsInfo( - source, - imports, - isClientCompilation, - getRawPageExtensions(pageExtensions) - ) - - const noop = `\nexport const __rsc_noop__=()=>{${imports.join(';')}}` + const { source: transformedSource, defaultExportName } = + await parseImportsInfo( + source, + imports, + isClientCompilation, + getRawPageExtensions(pageExtensions) + ) + + /** + * Server side component module output: + * + * export default function ServerComponent() { ... } + * + export const __rsc_noop__=()=>{ ... } + * + ServerComponent.__next_rsc__=1; + * + * Client side component module output: + * + * The function body of ServerComponent will be removed + */ + + const noop = `export const __rsc_noop__=()=>{${imports.join(';')}}` const defaultExportNoop = isClientCompilation - ? `\nexport default function Comp(){}\nComp.__next_rsc__=1` - : '' + ? `export default function ${defaultExportName}(){}\n${defaultExportName}.__next_rsc__=1;` + : `${defaultExportName}.__next_rsc__=1;` + + const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop - return transformed + noop + defaultExportNoop + return transformed } 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 e4826c61b0f88ff..cc0b4435deec2b4 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 @@ -50,7 +50,7 @@ export default async function middlewareSSRLoader(this: any) { buildManifest, reactLoadableManifest, rscManifest, - isServerComponent: ${JSON.stringify(isServerComponent)}, + isServerComponent: ${isServerComponent}, restRenderOpts: ${JSON.stringify(restRenderOpts)} }) diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 00298308369947d..aaa3fab2d8dd489 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -629,6 +629,15 @@ function AppContainer({ ) } +function renderApp(App: AppComponent, appProps: AppProps) { + if (process.env.__NEXT_RSC && (App as any).__next_rsc__) { + const { Component, err: _, router: __, ...props } = appProps + return + } else { + return + } +} + const wrapApp = (App: AppComponent) => (wrappedAppProps: Record): JSX.Element => { @@ -638,11 +647,7 @@ const wrapApp = err: hydrateErr, router, } - return ( - - - - ) + return {renderApp(App, appProps)} } let RSCComponent: (props: any) => JSX.Element @@ -957,7 +962,7 @@ function doRender(input: RenderRouteInfo): Promise { <> - + {renderApp(App, appProps)} diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts index d4021e05e730b68..22a8ada7b422044 100644 --- a/packages/next/client/router.ts +++ b/packages/next/client/router.ts @@ -142,7 +142,7 @@ export function useRouter(): NextRouter { // (do not use following exports inside the app) // Create a router and assign it as the singleton instance. -// This is used in client side when we are initilizing the app. +// This is used in client side when we are initializing the app. // This should **not** be used inside the server. export function createRouter(...args: RouterArgs): Router { singletonRouter.router = new Router(...args) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index de408ad8ecbcf6a..15310b11f99e90f 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -199,6 +199,7 @@ export default abstract class Server { domainLocales?: DomainLocale[] distDir: string concurrentFeatures?: boolean + serverComponents?: boolean crossOrigin?: string } private compression?: ExpressMiddleware @@ -291,6 +292,7 @@ export default abstract class Server { domainLocales: this.nextConfig.i18n?.domains, distDir: this.distDir, concurrentFeatures: this.nextConfig.experimental.concurrentFeatures, + serverComponents: this.nextConfig.experimental.serverComponents, crossOrigin: this.nextConfig.crossOrigin ? this.nextConfig.crossOrigin : undefined, diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index ec0d144b886148d..4a20caadeae2656 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -532,6 +532,7 @@ export default class HotReloader { absolute404Path: this.pagesMapping['/404'] || '', absolutePagePath, isServerComponent, + serverComponents: this.hasServerComponents, buildId: this.buildId, basePath: this.config.basePath, assetPrefix: this.config.assetPrefix, diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 808c24aad59cc18..adb068deeef3039 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -182,6 +182,21 @@ function enhanceComponents( } } +function renderFlight( + App: AppType, + Component: React.ComponentType, + props: any +) { + const AppServer = (App as any).__next_rsc__ + ? (App as React.ComponentType) + : React.Fragment + return ( + + + + ) +} + export type RenderOptsPartial = { buildId: string canonicalBase: string @@ -219,6 +234,7 @@ export type RenderOptsPartial = { disableOptimizedLoading?: boolean supportsDynamicHTML?: boolean concurrentFeatures?: boolean + serverComponents?: boolean customServer?: boolean crossOrigin?: string } @@ -342,6 +358,7 @@ const useRSCResponse = createRSCHook() function createServerComponentRenderer( cachePrefix: string, transformStream: TransformStream, + App: AppType, OriginalComponent: React.ComponentType, serverComponentManifest: NonNullable ) { @@ -349,7 +366,7 @@ function createServerComponentRenderer( const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() const reqStream = renderToReadableStream( - , + renderFlight(App, OriginalComponent, props), serverComponentManifest ) @@ -363,6 +380,7 @@ function createServerComponentRenderer( rscCache.delete(id) return root } + const Component = (props: any) => { return ( @@ -441,6 +459,7 @@ export async function renderToHTML( ? createServerComponentRenderer( cachePrefix, serverComponentsInlinedTransformStream!, + App, OriginalComponent, serverComponentManifest ) @@ -714,11 +733,9 @@ export async function renderToHTML( // not be useful. // https://github.com/facebook/react/pull/22644 const Noop = () => null - const AppContainerWithIsomorphicFiberStructure = ({ - children, - }: { + const AppContainerWithIsomorphicFiberStructure: React.FC<{ children: JSX.Element - }) => { + }> = ({ children }) => { return ( <> {/* */} @@ -1080,7 +1097,10 @@ export async function renderToHTML( if (renderServerComponentData) { const stream: ReadableStream = renderToReadableStream( - , + renderFlight(App, OriginalComponent, { + ...props.pageProps, + ...serverComponentProps, + }), serverComponentManifest ) const reader = stream.getReader() @@ -1201,22 +1221,30 @@ export async function renderToHTML( } else { let bodyResult + const renderContent = () => { + return ctx.err && ErrorDebug ? ( + + + + ) : ( + + + {renderOpts.serverComponents && (App as any).__next_rsc__ ? ( + + ) : ( + + )} + + + ) + } + if (concurrentFeatures) { bodyResult = async (suffix: string) => { // this must be called inside bodyResult so appWrappers is // up to date when getWrappedApp is called - const content = - ctx.err && ErrorDebug ? ( - - - - ) : ( - - - - - - ) + + const content = renderContent() return process.browser ? await renderToWebStream( content, @@ -1226,18 +1254,7 @@ export async function renderToHTML( : await renderToNodeStream(content, suffix, generateStaticHTML) } } else { - const content = - ctx.err && ErrorDebug ? ( - - - - ) : ( - - - - - - ) + const content = renderContent() // for non-concurrent rendering we need to ensure App is rendered // before _document so that updateHead is called/collected before // rendering _document's head diff --git a/test/integration/react-streaming-and-server-components/app/components/container.server.js b/test/integration/react-streaming-and-server-components/app/components/container.server.js new file mode 100644 index 000000000000000..ab63ebb0d37f0ff --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/components/container.server.js @@ -0,0 +1,3 @@ +export default function Container({ children }) { + return
{children}
+} diff --git a/test/integration/react-streaming-and-server-components/app/pages/err.js b/test/integration/react-streaming-and-server-components/app/pages/err/index.js similarity index 100% rename from test/integration/react-streaming-and-server-components/app/pages/err.js rename to test/integration/react-streaming-and-server-components/app/pages/err/index.js diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 2f28fcec96ee2dd..b5953a0c909cbca 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -25,6 +25,7 @@ const nativeModuleTestAppDir = join(__dirname, '../unsupported-native-module') const distDir = join(__dirname, '../app/.next') const documentPage = new File(join(appDir, 'pages/_document.jsx')) const appPage = new File(join(appDir, 'pages/_app.js')) +const appServerPage = new File(join(appDir, 'pages/_app.server.js')) const error500Page = new File(join(appDir, 'pages/500.js')) const documentWithGip = ` @@ -47,6 +48,13 @@ Document.getInitialProps = (ctx) => { } ` +const rscAppPage = ` +import Container from '../components/container.server' +export default function App({children}) { + return {children} +} +` + const appWithGlobalCss = ` import '../styles.css' @@ -175,6 +183,22 @@ describe('concurrentFeatures - prod', () => { runBasicTests(context, 'prod') }) +const customAppPageSuite = { + runTests: (context) => { + it('should render container in app', async () => { + const indexHtml = await renderViaHTTP(context.appPort, '/') + const indexFlight = await renderViaHTTP(context.appPort, '/?__flight__=1') + expect(indexHtml).toContain('container-server') + expect(indexFlight).toContain('container-server') + }) + }, + before: () => appServerPage.write(rscAppPage), + after: () => appServerPage.delete(), +} + +runSuite('Custom App', 'dev', customAppPageSuite) +runSuite('Custom App', 'prod', customAppPageSuite) + describe('concurrentFeatures - dev', () => { const context = { appDir }