Skip to content

Commit

Permalink
Custom app for server components (#33149)
Browse files Browse the repository at this point in the history
## Feature

Resolves #30996

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`
  • Loading branch information
huozhi committed Jan 14, 2022
1 parent 30ca6b3 commit 00a8432
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 55 deletions.
2 changes: 2 additions & 0 deletions packages/next/build/entries.ts
Expand Up @@ -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 = {
Expand All @@ -179,6 +180,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(
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 @@ -199,6 +199,7 @@ export default abstract class Server {
domainLocales?: DomainLocale[]
distDir: string
concurrentFeatures?: boolean
serverComponents?: boolean
crossOrigin?: string
}
private compression?: ExpressMiddleware
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/dev/hot-reloader.ts
Expand Up @@ -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,
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>
}

0 comments on commit 00a8432

Please sign in to comment.