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

Custom app for server components #33149

Merged
merged 12 commits into from Jan 14, 2022
3 changes: 2 additions & 1 deletion packages/next/build/entries.ts
Expand Up @@ -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}`
Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/webpack-config.ts
Expand Up @@ -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) => {
Expand Down
Expand Up @@ -50,7 +50,7 @@ export default async function middlewareSSRLoader(this: any) {
buildManifest,
reactLoadableManifest,
rscManifest,
isServerComponent: ${JSON.stringify(isServerComponent)},
isServerComponent: ${isServerComponent},
restRenderOpts: ${JSON.stringify(restRenderOpts)}
})

Expand Down
17 changes: 11 additions & 6 deletions packages/next/client/index.tsx
Expand Up @@ -629,6 +629,15 @@ function AppContainer({
)
}

function renderApp(App: AppComponent, appProps: AppProps) {
if (process.env.__NEXT_RSC) {
const { Component, err: _, router: __, ...props } = appProps
return <Component {...props} />
} else {
return <App {...appProps} />
}
}

huozhi marked this conversation as resolved.
Show resolved Hide resolved
const wrapApp =
(App: AppComponent) =>
(wrappedAppProps: Record<string, any>): JSX.Element => {
Expand All @@ -638,11 +647,7 @@ const wrapApp =
err: hydrateErr,
router,
}
return (
<AppContainer>
<App {...appProps} />
</AppContainer>
)
return <AppContainer>{renderApp(App, appProps)}</AppContainer>
}

let RSCComponent: (props: any) => JSX.Element
Expand Down Expand Up @@ -957,7 +962,7 @@ function doRender(input: RenderRouteInfo): Promise<any> {
<>
<Head callback={onHeadCommit} />
<AppContainer>
<App {...appProps} />
{renderApp(App, appProps)}
<Portal type="next-route-announcer">
<RouteAnnouncer />
</Portal>
Expand Down
2 changes: 1 addition & 1 deletion packages/next/client/router.ts
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions packages/next/pages/_app-server.tsx
@@ -0,0 +1,7 @@
// Default _app page for server components

import React from 'react'

export default function App({ children }: { children: React.ReactNode }) {
return children
}
3 changes: 3 additions & 0 deletions packages/next/server/base-server.ts
Expand Up @@ -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 {
Expand Down
80 changes: 44 additions & 36 deletions packages/next/server/render.tsx
Expand Up @@ -182,6 +182,19 @@ function enhanceComponents(
}
}

function renderApp(
App: AppType,
Component: React.ComponentType,
router: ServerRouter,
props: any
) {
if (process.env.__NEXT_RSC) {
huozhi marked this conversation as resolved.
Show resolved Hide resolved
return <Component {...props.pageProps} router={router} />
} else {
return <App {...props} Component={Component} router={router} />
}
}

export type RenderOptsPartial = {
buildId: string
canonicalBase: string
Expand Down Expand Up @@ -342,14 +355,17 @@ const useRSCResponse = createRSCHook()
function createServerComponentRenderer(
cachePrefix: string,
transformStream: TransformStream,
App: React.ComponentType,
OriginalComponent: React.ComponentType,
serverComponentManifest: NonNullable<RenderOpts['serverComponentManifest']>
) {
const writable = transformStream.writable
const ServerComponentWrapper = (props: any) => {
const id = (React as any).useId()
const reqStream = renderToReadableStream(
<OriginalComponent {...props} />,
<App>
<OriginalComponent {...props} />
</App>,
serverComponentManifest
)

Expand All @@ -363,6 +379,7 @@ function createServerComponentRenderer(
rscCache.delete(id)
return root
}

const Component = (props: any) => {
return (
<React.Suspense fallback={null}>
Expand Down Expand Up @@ -441,6 +458,7 @@ export async function renderToHTML(
? createServerComponentRenderer(
cachePrefix,
serverComponentsInlinedTransformStream!,
App as React.ComponentType,
OriginalComponent,
serverComponentManifest
)
Expand Down Expand Up @@ -644,7 +662,7 @@ export async function renderToHTML(
AppTree: (props: any) => {
return (
<AppContainerWithIsomorphicFiberStructure>
<App {...props} Component={Component} router={router} />
{renderApp(App, Component, router, props)}
</AppContainerWithIsomorphicFiberStructure>
)
},
Expand Down Expand Up @@ -714,11 +732,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 (
<>
{/* <Head/> */}
Expand Down Expand Up @@ -1079,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(
<OriginalComponent {...props.pageProps} {...serverComponentProps} />,
<AppServerComponent>
<OriginalComponent {...props.pageProps} {...serverComponentProps} />
</AppServerComponent>,
serverComponentManifest
)
const reader = stream.getReader()
Expand Down Expand Up @@ -1164,11 +1183,7 @@ export async function renderToHTML(
const html = ReactDOMServer.renderToString(
<Body>
<AppContainerWithIsomorphicFiberStructure>
<EnhancedApp
Component={EnhancedComponent}
router={router}
{...props}
/>
{renderApp(EnhancedApp, EnhancedComponent, router, props)}
</AppContainerWithIsomorphicFiberStructure>
</Body>
)
Expand Down Expand Up @@ -1201,22 +1216,26 @@ export async function renderToHTML(
} else {
let bodyResult

const renderContent = () => {
return ctx.err && ErrorDebug ? (
<Body>
<ErrorDebug error={ctx.err} />
</Body>
) : (
<Body>
<AppContainerWithIsomorphicFiberStructure>
{renderApp(App, Component, router, props)}
</AppContainerWithIsomorphicFiberStructure>
</Body>
)
}

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 ? (
<Body>
<ErrorDebug error={ctx.err} />
</Body>
) : (
<Body>
<AppContainerWithIsomorphicFiberStructure>
<App {...props} Component={Component} router={router} />
</AppContainerWithIsomorphicFiberStructure>
</Body>
)

const content = renderContent()
return process.browser
? await renderToWebStream(
content,
Expand All @@ -1226,18 +1245,7 @@ export async function renderToHTML(
: await renderToNodeStream(content, suffix, generateStaticHTML)
}
} else {
const content =
ctx.err && ErrorDebug ? (
<Body>
<ErrorDebug error={ctx.err} />
</Body>
) : (
<Body>
<AppContainerWithIsomorphicFiberStructure>
<App {...props} Component={Component} router={router} />
</AppContainerWithIsomorphicFiberStructure>
</Body>
)
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
Expand Down
15 changes: 14 additions & 1 deletion packages/next/taskfile.js
Expand Up @@ -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')
Expand All @@ -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
)
}
Expand Down
@@ -0,0 +1,3 @@
export default function Container({ children }) {
return <div className="container.server">{children}</div>
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Up @@ -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 = `
Expand All @@ -47,11 +48,18 @@ Document.getInitialProps = (ctx) => {
}
`

const rscAppPage = `
import Container from '../components/container.server'
export default function App({children}) {
return <Container>{children}</Container>
}
`

const appWithGlobalCss = `
import '../styles.css'

function App({ Component, pageProps }) {
return <Component {...pageProps} />
function App({ children }) {
return children
}

export default App
Expand Down Expand Up @@ -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 }

Expand Down