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
2 changes: 2 additions & 0 deletions packages/next/build/entries.ts
Expand Up @@ -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 = {
Expand All @@ -176,6 +177,7 @@ export function createEntrypoints(
absolute500Path: pages['/500'] || '',
absolutePagePath,
isServerComponent: isFlight,
serverComponents: hasServerComponents,
...defaultServerlessOptions,
} as any)}!`,
isServer: false,
Expand Down
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 = '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) {
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
2 changes: 2 additions & 0 deletions packages/next/server/base-server.ts
Expand Up @@ -203,6 +203,7 @@ export default abstract class Server {
domainLocales?: DomainLocale[]
distDir: string
concurrentFeatures?: boolean
serverComponents?: boolean
crossOrigin?: string
}
private compression?: ExpressMiddleware
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/dev/hot-reloader.ts
Expand Up @@ -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,
Expand Down
77 changes: 47 additions & 30 deletions packages/next/server/render.tsx
Expand Up @@ -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 (
<AppServer>
<Component {...props} />
</AppServer>
)
}

export type RenderOptsPartial = {
buildId: string
canonicalBase: string
Expand Down Expand Up @@ -219,6 +234,7 @@ export type RenderOptsPartial = {
disableOptimizedLoading?: boolean
supportsDynamicHTML?: boolean
concurrentFeatures?: boolean
serverComponents?: boolean
customServer?: boolean
crossOrigin?: string
}
Expand Down Expand Up @@ -342,14 +358,15 @@ 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 reqStream = renderToReadableStream(
<OriginalComponent {...props} />,
renderFlight(App, OriginalComponent, props),
serverComponentManifest
)

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

const Component = (props: any) => {
return (
<React.Suspense fallback={null}>
Expand Down Expand Up @@ -441,6 +459,7 @@ export async function renderToHTML(
? createServerComponentRenderer(
cachePrefix,
serverComponentsInlinedTransformStream!,
App,
OriginalComponent,
serverComponentManifest
)
Expand Down Expand Up @@ -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 (
<>
{/* <Head/> */}
Expand Down Expand Up @@ -1080,7 +1097,10 @@ export async function renderToHTML(

if (renderServerComponentData) {
const stream: ReadableStream = renderToReadableStream(
<OriginalComponent {...props.pageProps} {...serverComponentProps} />,
renderFlight(App, OriginalComponent, {
...props.pageProps,
...serverComponentProps,
}),
serverComponentManifest
)
const reader = stream.getReader()
Expand Down Expand Up @@ -1201,22 +1221,30 @@ export async function renderToHTML(
} else {
let bodyResult

const renderContent = () => {
return ctx.err && ErrorDebug ? (
<Body>
<ErrorDebug error={ctx.err} />
</Body>
) : (
<Body>
<AppContainerWithIsomorphicFiberStructure>
{renderOpts.serverComponents && (App as any).__next_rsc__ ? (
<Component {...props.pageProps} router={router} />
) : (
<App {...props} Component={Component} router={router} />
)}
</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 +1254,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>
}