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
58 changes: 41 additions & 17 deletions packages/next/build/webpack/loaders/next-flight-server-loader.ts
Expand Up @@ -32,19 +32,23 @@ async function parseImportsInfo(
imports: Array<string>,
isClientCompilation: boolean,
pageExtensions: string[]
): Promise<string> {
): Promise<{
source: string
defaultExportName: string
}> {
const { body } = acorn.parse(source, {
ecmaVersion: 11,
sourceType: 'module',
}) as any

let transformedSource = ''
let lastIndex = 0
let defaultExportName = 'Component'

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) {
Expand All @@ -57,9 +61,9 @@ async function parseImportsInfo(
) {
continue
}
transformedSource += source.substr(
transformedSource += source.substring(
shuding marked this conversation as resolved.
Show resolved Hide resolved
lastIndex,
node.source.start - lastIndex
node.source.start - 1
)
transformedSource += JSON.stringify(`${node.source.value}?flight`)
} else {
Expand All @@ -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(
Expand All @@ -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
}
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 && (App as any).__next_rsc__) {
const { Component, err: _, router: __, ...props } = appProps
return <Component {...props} />
} else {
return <App {...appProps} />
}
}

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
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
85 changes: 49 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 && (App as any).__next_rsc__) {
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,20 @@ const useRSCResponse = createRSCHook()
function createServerComponentRenderer(
cachePrefix: string,
transformStream: TransformStream,
App: AppType,
OriginalComponent: React.ComponentType,
serverComponentManifest: NonNullable<RenderOpts['serverComponentManifest']>
) {
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(
<OriginalComponent {...props} />,
<AppServer>
<OriginalComponent {...props} />
</AppServer>,
serverComponentManifest
)

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

const Component = (props: any) => {
return (
<React.Suspense fallback={null}>
Expand Down Expand Up @@ -441,6 +461,7 @@ export async function renderToHTML(
? createServerComponentRenderer(
cachePrefix,
serverComponentsInlinedTransformStream!,
App,
OriginalComponent,
serverComponentManifest
)
Expand Down Expand Up @@ -644,7 +665,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 +735,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 +1098,13 @@ 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(
<OriginalComponent {...props.pageProps} {...serverComponentProps} />,
<AppServer>
<OriginalComponent {...props.pageProps} {...serverComponentProps} />
</AppServer>,
serverComponentManifest
)
const reader = stream.getReader()
Expand Down Expand Up @@ -1164,11 +1188,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 +1221,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 +1250,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
@@ -0,0 +1,3 @@
export default function Container({ children }) {
return <div className="container-server">{children}</div>
}
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,6 +48,13 @@ 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'

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