From a51803624638a626694b03c23b48e373eb586517 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 2 Mar 2022 19:29:54 +0100 Subject: [PATCH] Refactor export marks of server components (#34945) * refactor rsc export marks * fix failed test --- packages/next/build/utils.ts | 2 +- .../loaders/next-flight-server-loader.ts | 73 ++++++------------- .../next-middleware-ssr-loader/index.ts | 4 +- .../next-middleware-ssr-loader/render.ts | 7 +- packages/next/client/index.tsx | 7 +- packages/next/server/load-components.ts | 6 +- packages/next/server/render.tsx | 43 ++++++----- packages/next/shared/lib/router/router.ts | 6 +- .../app/pages/partial-hydration.server.js | 2 +- 9 files changed, 71 insertions(+), 79 deletions(-) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 4fd4d8c9a19a..081ca1edada7 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -881,7 +881,7 @@ export async function isPageStatic( throw new Error('INVALID_DEFAULT_EXPORT') } - const hasFlightData = !!(Comp as any).__next_rsc__ + const hasFlightData = !!(mod as any).__next_rsc__ const hasGetInitialProps = !!(Comp as any).getInitialProps const hasStaticProps = !!mod.getStaticProps const hasStaticPaths = !!mod.getStaticPaths 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 25b23bf5d47c..d66405948626 100644 --- a/packages/next/build/webpack/loaders/next-flight-server-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-server-loader.ts @@ -27,26 +27,26 @@ const createServerComponentFilter = (pageExtensions: string[]) => { async function parseImportsInfo({ resourcePath, source, - imports, isClientCompilation, isServerComponent, isClientComponent, }: { resourcePath: string source: string - imports: Array isClientCompilation: boolean isServerComponent: (name: string) => boolean isClientComponent: (name: string) => boolean }): Promise<{ source: string - defaultExportName: string + imports: string }> { const ast = await parse(source, { filename: resourcePath, isModule: true }) const { body } = ast + let transformedSource = '' let lastIndex = 0 - let defaultExportName + let imports = '' + for (let i = 0; i < body.length; i++) { const node = body[i] switch (node.type) { @@ -67,7 +67,7 @@ async function parseImportsInfo({ // A client component. It should be loaded as module reference. transformedSource += importDeclarations transformedSource += JSON.stringify(`${importSource}?__sc_client__`) - imports.push(`require(${JSON.stringify(importSource)})`) + imports += `require(${JSON.stringify(importSource)})\n` } else { // This is a special case to avoid the Duplicate React error. // Since we already include React in the SSR runtime, @@ -97,27 +97,12 @@ async function parseImportsInfo({ continue } - imports.push(`require(${JSON.stringify(importSource)})`) + imports += `require(${JSON.stringify(importSource)})\n` } lastIndex = node.source.span.end break } - case 'ExportDefaultDeclaration': { - const def = node.decl - if (def.type === 'Identifier') { - defaultExportName = def.name - } else if (def.type === 'FunctionExpression') { - defaultExportName = def.identifier.value - } - break - } - case 'ExportDefaultExpression': - const exp = node.expression - if (exp.type === 'Identifier') { - defaultExportName = exp.value - } - break default: break } @@ -127,7 +112,7 @@ async function parseImportsInfo({ transformedSource += source.substring(lastIndex) } - return { source: transformedSource, defaultExportName } + return { source: transformedSource, imports } } export default async function transformSource( @@ -162,44 +147,34 @@ export default async function transformSource( } } - const imports: string[] = [] - const { source: transformedSource, defaultExportName } = - await parseImportsInfo({ - resourcePath, - source, - imports, - isClientCompilation, - isServerComponent, - isClientComponent, - }) + const { source: transformedSource, imports } = await parseImportsInfo({ + resourcePath, + source, + isClientCompilation, + isServerComponent, + isClientComponent, + }) /** * For .server.js files, we handle this loader differently. * * Server compilation output: - * export default function ServerComponent() { ... } - * export const __rsc_noop__ = () => { ... } - * ServerComponent.__next_rsc__ = 1 - * ServerComponent.__webpack_require__ = __webpack_require__ + * (The content of the Server Component module will be kept.) + * export const __next_rsc__ = { __webpack_require__, _: () => { ... } } * * Client compilation output: - * The function body of Server Component will be removed + * (The content of the Server Component module will be removed.) + * export const __next_rsc__ = { __webpack_require__, _: () => { ... } } */ - const noop = `export const __rsc_noop__=()=>{${imports.join(';')}}` + let rscExports = `export const __next_rsc__={ + __webpack_require__, + _: () => {${imports}} + }` - let defaultExportNoop = '' if (isClientCompilation) { - defaultExportNoop = `export default function ${ - defaultExportName || 'ServerComponent' - }(){}\n${defaultExportName || 'ServerComponent'}.__next_rsc__=1;` - } else { - if (defaultExportName) { - // It's required to have the default export for pages. For other components, it's fine to leave it as is. - defaultExportNoop = `${defaultExportName}.__next_rsc__=1;${defaultExportName}.__webpack_require__=__webpack_require__;` - } + rscExports += '\nexport default function RSC () {}' } - const transformed = transformedSource + '\n' + noop + '\n' + defaultExportNoop - return transformed + return transformedSource + '\n' + rscExports } 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 870a6b901693..51caf137f478 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 @@ -28,9 +28,9 @@ export default async function middlewareSSRLoader(this: any) { import { getRender } from 'next/dist/build/webpack/loaders/next-middleware-ssr-loader/render' - import App from ${stringifiedAppPath} import Document from ${stringifiedDocumentPath} + const appMod = require(${stringifiedAppPath}) const pageMod = require(${stringifiedPagePath}) const errorMod = require(${stringifiedErrorPath}) const error500Mod = ${stringified500Path} ? require(${stringified500Path}) : null @@ -48,10 +48,10 @@ export default async function middlewareSSRLoader(this: any) { const render = getRender({ dev: ${dev}, page: ${JSON.stringify(page)}, + appMod, pageMod, errorMod, error500Mod, - App, Document, buildManifest, reactLoadableManifest, diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 8fa95529ea3a..77b553d2b6da 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -17,11 +17,11 @@ process.cwd = () => '' export function getRender({ dev, page, + appMod, pageMod, errorMod, error500Mod, Document, - App, buildManifest, reactLoadableManifest, serverComponentManifest, @@ -31,11 +31,11 @@ export function getRender({ }: { dev: boolean page: string + appMod: any pageMod: any errorMod: any error500Mod: any Document: DocumentType - App: AppType buildManifest: BuildManifest reactLoadableManifest: ReactLoadableManifest serverComponentManifest: any | null @@ -48,7 +48,8 @@ export function getRender({ buildManifest, reactLoadableManifest, Document, - App, + App: appMod.default as AppType, + AppMod: appMod, } const server = new WebServer({ diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index f7d90774551e..6c2de5960162 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -179,7 +179,10 @@ const appElement: HTMLElement | null = document.getElementById('__next') let lastRenderReject: (() => void) | null let webpackHMR: any export let router: Router + let CachedApp: AppComponent, onPerfEntry: (metric: any) => void +let isAppRSC: boolean + headManager.getIsSsr = () => { return router.isSsr } @@ -291,6 +294,7 @@ export async function initNext( const { component: app, exports: mod } = appEntrypoint CachedApp = app as AppComponent + isAppRSC = !!mod.__next_rsc__ const exportedReportWebVitals = mod && mod.reportWebVitals onPerfEntry = ({ id, @@ -419,6 +423,7 @@ export async function initNext( defaultLocale, domainLocales, isPreview, + isRsc: rsc, }) const renderCtx: RenderRouteInfo = { @@ -640,7 +645,7 @@ function AppContainer({ } function renderApp(App: AppComponent, appProps: AppProps) { - if (process.env.__NEXT_RSC && (App as any).__next_rsc__) { + if (process.env.__NEXT_RSC && isAppRSC) { const { Component, err: _, router: __, ...props } = appProps return } else { diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index b4d4b02a0ac0..8ccafc60a75d 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -36,6 +36,7 @@ export type LoadComponentsReturnType = { getStaticPaths?: GetStaticPaths getServerSideProps?: GetServerSideProps ComponentMod: any + AppMod: any } export async function loadDefaultErrorComponents( @@ -46,7 +47,8 @@ export async function loadDefaultErrorComponents( require(`next/dist/pages/_document` + (hasConcurrentFeatures ? '-concurrent' : '')) ) - const App = interopDefault(require('next/dist/pages/_app')) + const AppMod = require('next/dist/pages/_app') + const App = interopDefault(AppMod) const ComponentMod = require('next/dist/pages/_error') const Component = interopDefault(ComponentMod) @@ -58,6 +60,7 @@ export async function loadDefaultErrorComponents( buildManifest: require(join(distDir, `fallback-${BUILD_MANIFEST}`)), reactLoadableManifest: {}, ComponentMod, + AppMod, } } @@ -124,6 +127,7 @@ export async function loadComponents( reactLoadableManifest, pageConfig: ComponentMod.config || {}, ComponentMod, + AppMod, getServerSideProps, getStaticProps, getStaticPaths, diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 2dd64eb1b402..f86b9c3a10a3 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -183,13 +183,9 @@ function enhanceComponents( } } -function renderFlight( - App: AppType, - Component: React.ComponentType, - props: any -) { - const AppServer = (App as any).__next_rsc__ - ? (App as React.ComponentType) +function renderFlight(AppMod: any, Component: React.ComponentType, props: any) { + const AppServer = AppMod.__next_rsc__ + ? (AppMod.default as React.ComponentType) : React.Fragment return ( @@ -355,8 +351,9 @@ const useRSCResponse = createRSCHook() // Create the wrapper component for a Flight stream. function createServerComponentRenderer( - App: AppType, OriginalComponent: React.ComponentType, + AppMod: any, + ComponentMod: any, { cachePrefix, transformStream, @@ -374,14 +371,15 @@ function createServerComponentRenderer( // globally for react-server-dom-webpack. // This is a hack until we find a better way. // @ts-ignore - globalThis.__webpack_require__ = OriginalComponent.__webpack_require__ + globalThis.__webpack_require__ = + ComponentMod.__next_rsc__.__webpack_require__ } const writable = transformStream.writable const ServerComponentWrapper = (props: any) => { const id = (React as any).useId() const reqStream: ReadableStream = renderToReadableStream( - renderFlight(App, OriginalComponent, props), + renderFlight(AppMod, OriginalComponent, props), serverComponentManifest ) @@ -463,6 +461,8 @@ export async function renderToHTML( images, reactRoot, runtime, + ComponentMod, + AppMod, } = renderOpts const hasConcurrentFeatures = !!runtime @@ -473,7 +473,7 @@ export async function renderToHTML( const isServerComponent = !!serverComponentManifest && hasConcurrentFeatures && - (OriginalComponent as any).__next_rsc__ + ComponentMod.__next_rsc__ let Component: React.ComponentType<{}> | ((props: any) => JSX.Element) = renderOpts.Component @@ -485,12 +485,17 @@ export async function renderToHTML( if (isServerComponent) { serverComponentsInlinedTransformStream = new TransformStream() const search = stringifyQuery(query) - Component = createServerComponentRenderer(App, OriginalComponent, { - cachePrefix: pathname + (search ? `?${search}` : ''), - transformStream: serverComponentsInlinedTransformStream, - serverComponentManifest, - runtime, - }) + Component = createServerComponentRenderer( + OriginalComponent, + AppMod, + ComponentMod, + { + cachePrefix: pathname + (search ? `?${search}` : ''), + transformStream: serverComponentsInlinedTransformStream, + serverComponentManifest, + runtime, + } + ) } const getFontDefinition = (url: string): string => { @@ -1178,7 +1183,7 @@ export async function renderToHTML( if (renderServerComponentData) { const stream: ReadableStream = renderToReadableStream( - renderFlight(App, OriginalComponent, { + renderFlight(AppMod, OriginalComponent, { ...props.pageProps, ...serverComponentProps, }), @@ -1318,7 +1323,7 @@ export async function renderToHTML( ) : ( - {renderOpts.serverComponents && (App as any).__next_rsc__ ? ( + {renderOpts.serverComponents && AppMod.__next_rsc__ ? ( ) : ( diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index d3e172431880..30214fa6a073 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -656,6 +656,7 @@ export default class Router implements BaseRouter { defaultLocale, domainLocales, isPreview, + isRsc, }: { subscription: Subscription initialProps: any @@ -670,6 +671,7 @@ export default class Router implements BaseRouter { defaultLocale?: string domainLocales?: DomainLocale[] isPreview?: boolean + isRsc?: boolean } ) { // represents the current component key @@ -688,7 +690,7 @@ export default class Router implements BaseRouter { err, __N_SSG: initialProps && initialProps.__N_SSG, __N_SSP: initialProps && initialProps.__N_SSP, - __N_RSC: !!(Component as any)?.__next_rsc__, + __N_RSC: !!isRsc, } } @@ -1527,7 +1529,7 @@ export default class Router implements BaseRouter { styleSheets: res.styleSheets, __N_SSG: res.mod.__N_SSG, __N_SSP: res.mod.__N_SSP, - __N_RSC: !!(res.page as any).__next_rsc__, + __N_RSC: !!res.mod.__next_rsc__, }))) const { Component, __N_SSG, __N_SSP, __N_RSC } = routeInfo diff --git a/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js b/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js index 5214fccb70d3..afd39e8d601d 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/partial-hydration.server.js @@ -23,7 +23,7 @@ function Data() { throw promise } -export default function Page() { +export default function () { return ( <> Current Runtime:{' '}