From 212e3450c4c8654ddb95586f8b0cf08a5a492751 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 10 Jan 2022 15:57:40 +0100 Subject: [PATCH 01/10] rsc: app --- packages/next/build/entries.ts | 3 +- packages/next/build/webpack-config.ts | 2 +- packages/next/client/index.tsx | 25 +++++- packages/next/pages/_app.server.tsx | 5 ++ packages/next/server/base-server.ts | 2 + packages/next/server/dev/next-dev-server.ts | 9 ++- packages/next/server/render.tsx | 84 +++++++++++---------- packages/next/taskfile.js | 15 +++- 8 files changed, 100 insertions(+), 45 deletions(-) create mode 100644 packages/next/pages/_app.server.tsx diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 262d0da81425b..5eb9eb4d2ed9e 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -79,12 +79,13 @@ export function createPagesMapping( // allow falling back to the correct source file so // that HMR can work properly when a file is added/removed const documentPage = `_document${hasConcurrentFeatures ? '-web' : ''}` + const appPage = `_app${hasServerComponents ? '.server' : ''}` if (isDev) { pages['/_app'] = `${PAGES_DIR_ALIAS}/_app` pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` pages['/_document'] = `${PAGES_DIR_ALIAS}/_document` } else { - pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app' + pages['/_app'] = pages['/_app'] || `next/dist/pages/${appPage}` pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error' pages['/_document'] = pages['/_document'] || `next/dist/pages/${documentPage}` diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index bbac49486d94a..ffab7fb825617 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -546,7 +546,7 @@ export default async function getBaseWebpackConfig( prev.push(path.join(pagesDir, `_app.${ext}`)) return prev }, [] as string[]), - 'next/dist/pages/_app.js', + `next/dist/pages/_app${hasServerComponents ? '.server' : ''}.js`, ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ ...config.pageExtensions.reduce((prev, ext) => { diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 2b5fa798bdc74..924cd9cf54c7a 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -629,6 +629,25 @@ function AppContainer({ ) } +function renderApp( + App: AppComponent | React.ComponentType, + appProps: AppProps +) { + if (process.env.__NEXT_RSC) { + const { Component, router: _, err: __, ...props } = appProps + const AppServerComponent = App as React.ComponentType<{ + children: React.ReactNode + }> + return ( + + + + ) + } else { + return + } +} + const wrapApp = (App: AppComponent) => (wrappedAppProps: Record): JSX.Element => { @@ -640,7 +659,8 @@ const wrapApp = } return ( - + {renderApp(App, appProps)} + {/* */} ) } @@ -957,7 +977,8 @@ function doRender(input: RenderRouteInfo): Promise { <> - + {/* */} + {renderApp(App, appProps)} diff --git a/packages/next/pages/_app.server.tsx b/packages/next/pages/_app.server.tsx new file mode 100644 index 0000000000000..ab00268d6b6c7 --- /dev/null +++ b/packages/next/pages/_app.server.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export default function App({ children }: { children: React.ReactNode }) { + return children +} diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index e0feebf06e5b5..e3d8971ed2953 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1220,6 +1220,7 @@ export default abstract class Server { parsed: parsed, }) } catch (err) { + console.error('err', err) if (isError(err) && err.code === 'ENOENT') { await this.render404(req, res, parsed) return { finished: true } @@ -1403,6 +1404,7 @@ export default abstract class Server { finished: true, } } catch (err) { + console.error('n err', err) if (err instanceof NoFallbackError && bubbleNoFallback) { return { finished: false, diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 2b1f95c9517ef..3b5d5659335da 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -405,11 +405,15 @@ export default class DevServer extends Server { process.on('unhandledRejection', (reason) => { this.logErrorWithOriginalStack(reason, 'unhandledRejection').catch( - () => {} + (err) => { + console.error(err) + } ) }) process.on('uncaughtException', (err) => { - this.logErrorWithOriginalStack(err, 'uncaughtException').catch(() => {}) + this.logErrorWithOriginalStack(err, 'uncaughtException').catch((e) => { + console.error(e) + }) }) } @@ -667,6 +671,7 @@ export default class DevServer extends Server { } } + console.error(err) if (!usedOriginalStack) { if (type === 'warning') { Log.warn(err + '') diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 808c24aad59cc..d1b06c7deb2fc 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -182,6 +182,25 @@ function enhanceComponents( } } +function renderApp( + App: AppType | React.ComponentType, + Component: React.ComponentType, + router: ServerRouter, + props: any, + isServerComponent: boolean +) { + if (isServerComponent) { + let AppServerComponent = App as React.ComponentType + return ( + + + + ) + } else { + return + } +} + export type RenderOptsPartial = { buildId: string canonicalBase: string @@ -642,11 +661,7 @@ export async function renderToHTML( locales: renderOpts.locales, defaultLocale: renderOpts.defaultLocale, AppTree: (props: any) => { - return ( - - - - ) + return renderApp(App, Component, router, props, isServerComponent) }, defaultGetInitialProps: async ( docCtx: DocumentContext, @@ -714,11 +729,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 ( <> {/* */} @@ -1164,11 +1177,13 @@ export async function renderToHTML( const html = ReactDOMServer.renderToString( - + {renderApp( + EnhancedApp, + EnhancedComponent, + router, + props, + isServerComponent + )} ) @@ -1201,22 +1216,26 @@ export async function renderToHTML( } else { let bodyResult + const renderContent = () => { + return ctx.err && ErrorDebug ? ( + + + + ) : ( + + + {renderApp(App, Component, router, props, isServerComponent)} + + + ) + } + 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 +1245,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/packages/next/taskfile.js b/packages/next/taskfile.js index f0c348e9c57c9..caae935c10a6f 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1651,6 +1651,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 }) + .target('dist/pages') +} + export async function pages_error(task, opts) { await task .source('pages/_error.tsx') @@ -1674,7 +1681,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_error', + 'pages_document', + 'pages_document_server', + 'pages_app_server', + ], opts ) } From 645cd45b3216192d074d08d07857220a3a032b4f Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 10 Jan 2022 16:26:20 +0100 Subject: [PATCH 02/10] add test --- packages/next/build/entries.ts | 2 +- packages/next/build/webpack-config.ts | 2 +- packages/next/client/index.tsx | 8 +------ .../{_app.server.tsx => _app-server.tsx} | 2 ++ packages/next/server/base-server.ts | 2 -- packages/next/server/dev/next-dev-server.ts | 9 ++------ packages/next/taskfile.js | 2 +- .../app/components/container.client.js | 3 +++ .../test/index.test.js | 21 +++++++++++++++++++ 9 files changed, 32 insertions(+), 19 deletions(-) rename packages/next/pages/{_app.server.tsx => _app-server.tsx} (73%) create mode 100644 test/integration/react-streaming-and-server-components/app/components/container.client.js diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 5eb9eb4d2ed9e..e12ded708cb8e 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -79,7 +79,7 @@ export function createPagesMapping( // allow falling back to the correct source file so // that HMR can work properly when a file is added/removed const documentPage = `_document${hasConcurrentFeatures ? '-web' : ''}` - const appPage = `_app${hasServerComponents ? '.server' : ''}` + const appPage = `_app${hasServerComponents ? '-server' : ''}` if (isDev) { pages['/_app'] = `${PAGES_DIR_ALIAS}/_app` pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index ffab7fb825617..11da4d6dd2554 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -546,7 +546,7 @@ export default async function getBaseWebpackConfig( prev.push(path.join(pagesDir, `_app.${ext}`)) return prev }, [] as string[]), - `next/dist/pages/_app${hasServerComponents ? '.server' : ''}.js`, + `next/dist/pages/_app${hasServerComponents ? '-server' : ''}.js`, ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ ...config.pageExtensions.reduce((prev, ext) => { diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 924cd9cf54c7a..f560531d266cb 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -657,12 +657,7 @@ const wrapApp = err: hydrateErr, router, } - return ( - - {renderApp(App, appProps)} - {/* */} - - ) + return {renderApp(App, appProps)} } let RSCComponent: (props: any) => JSX.Element @@ -977,7 +972,6 @@ function doRender(input: RenderRouteInfo): Promise { <> - {/* */} {renderApp(App, appProps)} diff --git a/packages/next/pages/_app.server.tsx b/packages/next/pages/_app-server.tsx similarity index 73% rename from packages/next/pages/_app.server.tsx rename to packages/next/pages/_app-server.tsx index ab00268d6b6c7..a06d1c991894b 100644 --- a/packages/next/pages/_app.server.tsx +++ b/packages/next/pages/_app-server.tsx @@ -1,3 +1,5 @@ +// Default _app page for server components + import React from 'react' export default function App({ children }: { children: React.ReactNode }) { diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index e3d8971ed2953..e0feebf06e5b5 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1220,7 +1220,6 @@ export default abstract class Server { parsed: parsed, }) } catch (err) { - console.error('err', err) if (isError(err) && err.code === 'ENOENT') { await this.render404(req, res, parsed) return { finished: true } @@ -1404,7 +1403,6 @@ export default abstract class Server { finished: true, } } catch (err) { - console.error('n err', err) if (err instanceof NoFallbackError && bubbleNoFallback) { return { finished: false, diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 3b5d5659335da..2b1f95c9517ef 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -405,15 +405,11 @@ export default class DevServer extends Server { process.on('unhandledRejection', (reason) => { this.logErrorWithOriginalStack(reason, 'unhandledRejection').catch( - (err) => { - console.error(err) - } + () => {} ) }) process.on('uncaughtException', (err) => { - this.logErrorWithOriginalStack(err, 'uncaughtException').catch((e) => { - console.error(e) - }) + this.logErrorWithOriginalStack(err, 'uncaughtException').catch(() => {}) }) } @@ -671,7 +667,6 @@ export default class DevServer extends Server { } } - console.error(err) if (!usedOriginalStack) { if (type === 'warning') { Log.warn(err + '') diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index caae935c10a6f..4d4a09b11e7eb 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1653,7 +1653,7 @@ export async function pages_app(task, opts) { export async function pages_app_server(task, opts) { await task - .source('pages/_app.server.tsx') + .source('pages/_app-server.tsx') .swc('client', { dev: opts.dev }) .target('dist/pages') } diff --git a/test/integration/react-streaming-and-server-components/app/components/container.client.js b/test/integration/react-streaming-and-server-components/app/components/container.client.js new file mode 100644 index 0000000000000..e75ce0e55acbd --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/components/container.client.js @@ -0,0 +1,3 @@ +export default function Container({ children }) { + return
{children}
+} 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 2f28fcec96ee2..3b7176ec4c311 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 @@ -47,6 +47,13 @@ Document.getInitialProps = (ctx) => { } ` +const rscAppPage = ` +import Container from '../components/container.client' +export default function App({children}) { + return {children} +} +` + const appWithGlobalCss = ` import '../styles.css' @@ -175,6 +182,20 @@ describe('concurrentFeatures - prod', () => { runBasicTests(context, 'prod') }) +const customAppPageSuite = { + runTests: (context) => { + it('should render app page', async () => { + const html = await renderViaHTTP(context.appPort, '/') + expect(html).toContain('_app.server') + }) + }, + before: () => appPage.write(rscAppPage), + after: () => appPage.delete(), +} + +runSuite('Custom App', 'dev', customAppPageSuite) +runSuite('Custom App', 'prod', customAppPageSuite) + describe('concurrentFeatures - dev', () => { const context = { appDir } From d8fa67dc9a86f00e916bf60d8743e9731192c649 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 10 Jan 2022 16:45:14 +0100 Subject: [PATCH 03/10] fix app tree --- packages/next/server/render.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index d1b06c7deb2fc..beafbfd6b75b3 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -661,7 +661,11 @@ export async function renderToHTML( locales: renderOpts.locales, defaultLocale: renderOpts.defaultLocale, AppTree: (props: any) => { - return renderApp(App, Component, router, props, isServerComponent) + return ( + + {renderApp(App, Component, router, props, isServerComponent)} + + ) }, defaultGetInitialProps: async ( docCtx: DocumentContext, From 7f5f55b463b541c42c560c54441724ab4864ffea Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 10 Jan 2022 22:19:37 +0100 Subject: [PATCH 04/10] fix --- packages/next/client/index.tsx | 4 +--- packages/next/client/router.ts | 2 +- .../react-streaming-and-server-components/test/index.test.js | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index f560531d266cb..d86b41d757fad 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -635,9 +635,7 @@ function renderApp( ) { if (process.env.__NEXT_RSC) { const { Component, router: _, err: __, ...props } = appProps - const AppServerComponent = App as React.ComponentType<{ - children: React.ReactNode - }> + const AppServerComponent = App as React.ComponentType return ( diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts index d4021e05e730b..22a8ada7b4220 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/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 3b7176ec4c311..c0d9c4a66f96d 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 @@ -57,8 +57,8 @@ export default function App({children}) { const appWithGlobalCss = ` import '../styles.css' -function App({ Component, pageProps }) { - return +function App({ children }) { + return children } export default App From 6fe35ebc5ed0edd67a04acf0b45f50bfe5f80faa Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 11 Jan 2022 17:36:43 +0100 Subject: [PATCH 05/10] use defined variable as cond --- .../next-middleware-ssr-loader/index.ts | 2 +- packages/next/client/index.tsx | 5 +++-- packages/next/server/render.tsx | 19 ++++++------------- .../app/pages/{err.js => err/index.js} | 0 4 files changed, 10 insertions(+), 16 deletions(-) rename test/integration/react-streaming-and-server-components/app/pages/{err.js => err/index.js} (100%) 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 e4826c61b0f88..cc0b4435deec2 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 d86b41d757fad..f7e521ae8a4d1 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -634,11 +634,12 @@ function renderApp( appProps: AppProps ) { if (process.env.__NEXT_RSC) { - const { Component, router: _, err: __, ...props } = appProps + const { Component, ...props } = appProps const AppServerComponent = App as React.ComponentType + const ComponentType = Component as React.ComponentType return ( - + ) } else { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index beafbfd6b75b3..db599763ec0c2 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -186,14 +186,13 @@ function renderApp( App: AppType | React.ComponentType, Component: React.ComponentType, router: ServerRouter, - props: any, - isServerComponent: boolean + props: any ) { - if (isServerComponent) { + if (process.env.__NEXT_RSC) { let AppServerComponent = App as React.ComponentType return ( - + ) } else { @@ -663,7 +662,7 @@ export async function renderToHTML( AppTree: (props: any) => { return ( - {renderApp(App, Component, router, props, isServerComponent)} + {renderApp(App, Component, router, props)} ) }, @@ -1181,13 +1180,7 @@ export async function renderToHTML( const html = ReactDOMServer.renderToString( - {renderApp( - EnhancedApp, - EnhancedComponent, - router, - props, - isServerComponent - )} + {renderApp(EnhancedApp, EnhancedComponent, router, props)} ) @@ -1228,7 +1221,7 @@ export async function renderToHTML( ) : ( - {renderApp(App, Component, router, props, isServerComponent)} + {renderApp(App, Component, router, props)} ) 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 From c228bbf0db8e2babf5babf972b7f1e5bcfd49e15 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 11 Jan 2022 23:05:32 +0100 Subject: [PATCH 06/10] override runtime var --- packages/next/server/base-server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index e0feebf06e5b5..6eaa2471e320e 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -344,6 +344,9 @@ export default abstract class Server { if (this.renderOpts.optimizeCss) { process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) } + if (this.nextConfig.experimental.serverComponents) { + process.env.__NEXT_RSC = JSON.stringify(true) + } } public logError(err: Error): void { From afd9a4e27c30688503c48d78df7885d89d6ab17f Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 12 Jan 2022 15:13:49 +0100 Subject: [PATCH 07/10] ssr app tree --- packages/next/client/index.tsx | 15 +++---------- packages/next/server/render.tsx | 21 +++++++++++-------- .../app/components/container.client.js | 3 --- .../app/components/container.server.js | 3 +++ .../test/index.test.js | 15 +++++++------ 5 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 test/integration/react-streaming-and-server-components/app/components/container.client.js create mode 100644 test/integration/react-streaming-and-server-components/app/components/container.server.js diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index f7e521ae8a4d1..42cc067bea315 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -629,19 +629,10 @@ function AppContainer({ ) } -function renderApp( - App: AppComponent | React.ComponentType, - appProps: AppProps -) { +function renderApp(App: AppComponent, appProps: AppProps) { if (process.env.__NEXT_RSC) { - const { Component, ...props } = appProps - const AppServerComponent = App as React.ComponentType - const ComponentType = Component as React.ComponentType - return ( - - - - ) + const { Component, err: _, router: __, ...props } = appProps + return } else { return } diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index db599763ec0c2..f922f8ca8579f 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -183,18 +183,13 @@ function enhanceComponents( } function renderApp( - App: AppType | React.ComponentType, + App: AppType, Component: React.ComponentType, router: ServerRouter, props: any ) { if (process.env.__NEXT_RSC) { - let AppServerComponent = App as React.ComponentType - return ( - - - - ) + return } else { return } @@ -360,6 +355,7 @@ const useRSCResponse = createRSCHook() function createServerComponentRenderer( cachePrefix: string, transformStream: TransformStream, + App: React.ComponentType, OriginalComponent: React.ComponentType, serverComponentManifest: NonNullable ) { @@ -367,7 +363,9 @@ function createServerComponentRenderer( const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() const reqStream = renderToReadableStream( - , + + + , serverComponentManifest ) @@ -381,6 +379,7 @@ function createServerComponentRenderer( rscCache.delete(id) return root } + const Component = (props: any) => { return ( @@ -459,6 +458,7 @@ export async function renderToHTML( ? createServerComponentRenderer( cachePrefix, serverComponentsInlinedTransformStream!, + App as React.ComponentType, OriginalComponent, serverComponentManifest ) @@ -1095,8 +1095,11 @@ export async function renderToHTML( if (isResSent(res) && !isSSG) return null if (renderServerComponentData) { + const AppServerComponent = App as React.ComponentType const stream: ReadableStream = renderToReadableStream( - , + + + , serverComponentManifest ) const reader = stream.getReader() diff --git a/test/integration/react-streaming-and-server-components/app/components/container.client.js b/test/integration/react-streaming-and-server-components/app/components/container.client.js deleted file mode 100644 index e75ce0e55acbd..0000000000000 --- a/test/integration/react-streaming-and-server-components/app/components/container.client.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Container({ children }) { - return
{children}
-} 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 0000000000000..a6746b460b426 --- /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/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index c0d9c4a66f96d..26607be02f221 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 = ` @@ -48,7 +49,7 @@ Document.getInitialProps = (ctx) => { ` const rscAppPage = ` -import Container from '../components/container.client' +import Container from '../components/container.server' export default function App({children}) { return {children} } @@ -184,13 +185,15 @@ describe('concurrentFeatures - prod', () => { const customAppPageSuite = { runTests: (context) => { - it('should render app page', async () => { - const html = await renderViaHTTP(context.appPort, '/') - expect(html).toContain('_app.server') + 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: () => appPage.write(rscAppPage), - after: () => appPage.delete(), + before: () => appServerPage.write(rscAppPage), + after: () => appServerPage.delete(), } runSuite('Custom App', 'dev', customAppPageSuite) From 036a0ceba0e4e92766029dda5ec19ae0a879ad1c Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 13 Jan 2022 22:00:16 +0100 Subject: [PATCH 08/10] define rsc tag by loader --- packages/next/build/entries.ts | 3 +- packages/next/build/webpack-config.ts | 2 +- .../loaders/next-flight-server-loader.ts | 52 ++++++++++++++----- packages/next/client/index.tsx | 2 +- packages/next/pages/_app-server.tsx | 7 --- packages/next/server/render.tsx | 21 +++++--- packages/next/taskfile.js | 15 +----- .../app/components/container.server.js | 2 +- .../test/index.test.js | 8 +-- 9 files changed, 60 insertions(+), 52 deletions(-) delete mode 100644 packages/next/pages/_app-server.tsx diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index e12ded708cb8e..e864563c70eff 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -79,13 +79,12 @@ export function createPagesMapping( // allow falling back to the correct source file so // that HMR can work properly when a file is added/removed const documentPage = `_document${hasConcurrentFeatures ? '-web' : ''}` - const appPage = `_app${hasServerComponents ? '-server' : ''}` if (isDev) { pages['/_app'] = `${PAGES_DIR_ALIAS}/_app` pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` pages['/_document'] = `${PAGES_DIR_ALIAS}/_document` } else { - pages['/_app'] = pages['/_app'] || `next/dist/pages/${appPage}` + pages['/_app'] = pages['/_app'] || `next/dist/pages/_app` pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error' pages['/_document'] = pages['/_document'] || `next/dist/pages/${documentPage}` diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 11da4d6dd2554..bbac49486d94a 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -546,7 +546,7 @@ export default async function getBaseWebpackConfig( prev.push(path.join(pagesDir, `_app.${ext}`)) return prev }, [] as string[]), - `next/dist/pages/_app${hasServerComponents ? '-server' : ''}.js`, + 'next/dist/pages/_app.js', ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ ...config.pageExtensions.reduce((prev, ext) => { 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 732533120fccf..c4b4981e28271 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 = null 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) { @@ -83,6 +87,11 @@ async function parseImportsInfo( lastIndex = node.source.end imports.push(`require(${JSON.stringify(importSource)})`) continue + } + case 'ExportDefaultDeclaration': { + defaultExportName = node.declaration.id.name + break + } default: break } @@ -92,7 +101,7 @@ async function parseImportsInfo( transformedSource += source.substr(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/client/index.tsx b/packages/next/client/index.tsx index 42cc067bea315..c45ed2a4d468d 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -630,7 +630,7 @@ function AppContainer({ } function renderApp(App: AppComponent, appProps: AppProps) { - if (process.env.__NEXT_RSC) { + if (process.env.__NEXT_RSC && (App as any).__next_rsc__) { const { Component, err: _, router: __, ...props } = appProps return } else { diff --git a/packages/next/pages/_app-server.tsx b/packages/next/pages/_app-server.tsx deleted file mode 100644 index a06d1c991894b..0000000000000 --- a/packages/next/pages/_app-server.tsx +++ /dev/null @@ -1,7 +0,0 @@ -// Default _app page for server components - -import React from 'react' - -export default function App({ children }: { children: React.ReactNode }) { - return children -} diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index f922f8ca8579f..5d14d430cf50a 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -188,7 +188,7 @@ function renderApp( router: ServerRouter, props: any ) { - if (process.env.__NEXT_RSC) { + if (process.env.__NEXT_RSC && (App as any).__next_rsc__) { return } else { return @@ -355,17 +355,20 @@ const useRSCResponse = createRSCHook() function createServerComponentRenderer( cachePrefix: string, transformStream: TransformStream, - App: React.ComponentType, + App: AppType, OriginalComponent: React.ComponentType, serverComponentManifest: NonNullable ) { const writable = transformStream.writable const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() + const AppServer = (App as any).__next_rsc__ + ? (App as React.ComponentType) + : React.Fragment const reqStream = renderToReadableStream( - + - , + , serverComponentManifest ) @@ -458,7 +461,7 @@ export async function renderToHTML( ? createServerComponentRenderer( cachePrefix, serverComponentsInlinedTransformStream!, - App as React.ComponentType, + App, OriginalComponent, serverComponentManifest ) @@ -1095,11 +1098,13 @@ export async function renderToHTML( if (isResSent(res) && !isSSG) return null if (renderServerComponentData) { - const AppServerComponent = App as React.ComponentType + const AppServer = (App as any).__next_rsc__ + ? (App as React.ComponentType) + : React.Fragment const stream: ReadableStream = renderToReadableStream( - + - , + , serverComponentManifest ) const reader = stream.getReader() diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 4d4a09b11e7eb..f0c348e9c57c9 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1651,13 +1651,6 @@ 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 }) - .target('dist/pages') -} - export async function pages_error(task, opts) { await task .source('pages/_error.tsx') @@ -1681,13 +1674,7 @@ 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_server', - ], + ['pages_app', 'pages_error', 'pages_document', 'pages_document_server'], opts ) } 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 index a6746b460b426..ab63ebb0d37f0 100644 --- 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 @@ -1,3 +1,3 @@ export default function Container({ children }) { - return
{children}
+ return
{children}
} 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 26607be02f221..b5953a0c909cb 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 @@ -58,8 +58,8 @@ export default function App({children}) { const appWithGlobalCss = ` import '../styles.css' -function App({ children }) { - return children +function App({ Component, pageProps }) { + return } export default App @@ -188,8 +188,8 @@ const customAppPageSuite = { 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') + expect(indexHtml).toContain('container-server') + expect(indexFlight).toContain('container-server') }) }, before: () => appServerPage.write(rscAppPage), From 21bcdbdd478c9f7be8c90323cc22926b4f175c3d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 13 Jan 2022 22:07:33 +0100 Subject: [PATCH 09/10] replace substr with substring --- packages/next/build/entries.ts | 2 +- .../build/webpack/loaders/next-flight-server-loader.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index e864563c70eff..262d0da81425b 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -84,7 +84,7 @@ export function createPagesMapping( pages['/_error'] = `${PAGES_DIR_ALIAS}/_error` pages['/_document'] = `${PAGES_DIR_ALIAS}/_document` } else { - pages['/_app'] = pages['/_app'] || `next/dist/pages/_app` + pages['/_app'] = pages['/_app'] || 'next/dist/pages/_app' pages['/_error'] = pages['/_error'] || 'next/dist/pages/_error' pages['/_document'] = pages['/_document'] || `next/dist/pages/${documentPage}` 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 c4b4981e28271..16c6787c7cd2e 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -43,7 +43,7 @@ async function parseImportsInfo( let transformedSource = '' let lastIndex = 0 - let defaultExportName = null + let defaultExportName = 'Component' for (let i = 0; i < body.length; i++) { const node = body[i] @@ -61,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 { @@ -98,7 +98,7 @@ async function parseImportsInfo( } if (!isClientCompilation) { - transformedSource += source.substr(lastIndex) + transformedSource += source.substring(lastIndex) } return { source: transformedSource, defaultExportName } From e5548f326ed82fde4e6b5d0cff3fb206875bbdc5 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 14 Jan 2022 00:53:15 +0100 Subject: [PATCH 10/10] use loader option instead of assigning process env var --- packages/next/build/entries.ts | 2 + .../loaders/next-flight-server-loader.ts | 2 +- packages/next/server/base-server.ts | 5 +- packages/next/server/dev/hot-reloader.ts | 1 + packages/next/server/render.tsx | 48 ++++++++++--------- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 262d0da81425b..990fe3de983ab 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -154,6 +154,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 = { @@ -176,6 +177,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 16c6787c7cd2e..efc937d021bf7 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -43,7 +43,7 @@ async function parseImportsInfo( let transformedSource = '' let lastIndex = 0 - let defaultExportName = 'Component' + let defaultExportName = 'RSComponent' for (let i = 0; i < body.length; i++) { const node = body[i] diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 6eaa2471e320e..9b138c05635a7 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -203,6 +203,7 @@ export default abstract class Server { domainLocales?: DomainLocale[] distDir: string concurrentFeatures?: boolean + serverComponents?: boolean crossOrigin?: string } private compression?: ExpressMiddleware @@ -281,6 +282,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, @@ -344,9 +346,6 @@ export default abstract class Server { if (this.renderOpts.optimizeCss) { process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) } - if (this.nextConfig.experimental.serverComponents) { - process.env.__NEXT_RSC = JSON.stringify(true) - } } public logError(err: Error): void { diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 3f3ba14a94c4d..96eed91ad4572 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -535,6 +535,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 5d14d430cf50a..adb068deeef30 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -182,17 +182,19 @@ function enhanceComponents( } } -function renderApp( +function renderFlight( App: AppType, Component: React.ComponentType, - router: ServerRouter, props: any ) { - if (process.env.__NEXT_RSC && (App as any).__next_rsc__) { - return - } else { - return - } + const AppServer = (App as any).__next_rsc__ + ? (App as React.ComponentType) + : React.Fragment + return ( + + + + ) } export type RenderOptsPartial = { @@ -232,6 +234,7 @@ export type RenderOptsPartial = { disableOptimizedLoading?: boolean supportsDynamicHTML?: boolean concurrentFeatures?: boolean + serverComponents?: boolean customServer?: boolean crossOrigin?: string } @@ -362,13 +365,8 @@ function createServerComponentRenderer( const writable = transformStream.writable const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() - const AppServer = (App as any).__next_rsc__ - ? (App as React.ComponentType) - : React.Fragment const reqStream = renderToReadableStream( - - - , + renderFlight(App, OriginalComponent, props), serverComponentManifest ) @@ -665,7 +663,7 @@ export async function renderToHTML( AppTree: (props: any) => { return ( - {renderApp(App, Component, router, props)} + ) }, @@ -1098,13 +1096,11 @@ export async function renderToHTML( if (isResSent(res) && !isSSG) return null if (renderServerComponentData) { - const AppServer = (App as any).__next_rsc__ - ? (App as React.ComponentType) - : React.Fragment const stream: ReadableStream = renderToReadableStream( - - - , + renderFlight(App, OriginalComponent, { + ...props.pageProps, + ...serverComponentProps, + }), serverComponentManifest ) const reader = stream.getReader() @@ -1188,7 +1184,11 @@ export async function renderToHTML( const html = ReactDOMServer.renderToString( - {renderApp(EnhancedApp, EnhancedComponent, router, props)} + ) @@ -1229,7 +1229,11 @@ export async function renderToHTML( ) : ( - {renderApp(App, Component, router, props)} + {renderOpts.serverComponents && (App as any).__next_rsc__ ? ( + + ) : ( + + )} )