From b553818d3729674f1f9c998d228fa81274400b18 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 18 May 2022 13:18:28 +0200 Subject: [PATCH] Simplify the logic for static flight response generation (#36984) * code refactor * simplify static data * htmlEscapeJsonString in view-render --- .../webpack/plugins/flight-manifest-plugin.ts | 259 +++++++++--------- .../webpack/plugins/middleware-plugin.ts | 7 +- packages/next/server/load-components.ts | 15 +- packages/next/server/render.tsx | 185 +++++-------- packages/next/server/view-render.tsx | 89 +++--- packages/next/shared/lib/constants.ts | 1 + 6 files changed, 259 insertions(+), 297 deletions(-) diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index f2cf30ba3821d7..197dbbdf47a09d 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -10,6 +10,7 @@ import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { MIDDLEWARE_FLIGHT_MANIFEST, EDGE_RUNTIME_WEBPACK, + NEXT_CLIENT_SSR_ENTRY_SUFFIX, } from '../../../shared/lib/constants' import { clientComponentRegex } from '../loaders/utils' import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' @@ -54,8 +55,6 @@ export class FlightManifestPlugin { } apply(compiler: any) { - const context = (this as any).context - compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation: any, { normalModuleFactory }: any) => { @@ -74,151 +73,151 @@ export class FlightManifestPlugin { compiler.hooks.finishMake.tapAsync( PLUGIN_NAME, async (compilation: any, callback: any) => { - const promises: any = [] + this.createClientEndpoints(compilation, callback) + } + ) - // For each SC server compilation entry, we need to create its corresponding - // client component entry. - for (const [name, entry] of compilation.entries.entries()) { - if (name === 'pages/_app.server') continue + compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => { + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + // @ts-ignore TODO: Remove ignore when webpack 5 is stable + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + (assets: any) => this.createAsset(assets, compilation) + ) + }) + } - // Check if the page entry is a server component or not. - const entryDependency = entry.dependencies?.[0] - const request = entryDependency?.request + async createClientEndpoints(compilation: any, callback: () => void) { + const context = (this as any).context + const promises: any = [] - if (request && entry.options?.layer === 'sc_server') { - const visited = new Set() - const clientComponentImports: string[] = [] + // For each SC server compilation entry, we need to create its corresponding + // client component entry. + for (const [name, entry] of compilation.entries.entries()) { + if (name === 'pages/_app.server') continue - function filterClientComponents(dependency: any) { - const module = - compilation.moduleGraph.getResolvedModule(dependency) - if (!module) return + // Check if the page entry is a server component or not. + const entryDependency = entry.dependencies?.[0] + const request = entryDependency?.request - if (visited.has(module.userRequest)) return - visited.add(module.userRequest) + if (request && entry.options?.layer === 'sc_server') { + const visited = new Set() + const clientComponentImports: string[] = [] - if (clientComponentRegex.test(module.userRequest)) { - clientComponentImports.push(module.userRequest) - } + function filterClientComponents(dependency: any) { + const module = compilation.moduleGraph.getResolvedModule(dependency) + if (!module) return - compilation.moduleGraph - .getOutgoingConnections(module) - .forEach((connection: any) => { - filterClientComponents(connection.dependency) - }) - } + if (visited.has(module.userRequest)) return + visited.add(module.userRequest) - // Traverse the module graph to find all client components. - filterClientComponents(entryDependency) + if (clientComponentRegex.test(module.userRequest)) { + clientComponentImports.push(module.userRequest) + } - const entryModule = - compilation.moduleGraph.getResolvedModule(entryDependency) - const routeInfo = entryModule.buildInfo.route || { - page: denormalizePagePath(name.replace(/^pages/, '')), - absolutePagePath: entryModule.resource, - } + compilation.moduleGraph + .getOutgoingConnections(module) + .forEach((connection: any) => { + filterClientComponents(connection.dependency) + }) + } - // Parse gSSP and gSP exports from the page source. - const pageStaticInfo = this.isEdgeServer - ? {} - : await getPageStaticInfo( - routeInfo.absolutePagePath, - {}, - this.dev - ) - - const clientLoader = `next-flight-client-entry-loader?${stringify({ - modules: clientComponentImports, - runtime: this.isEdgeServer ? 'edge' : 'nodejs', - ssr: pageStaticInfo.ssr, - // Adding name here to make the entry key unique. - name, - })}!` - - const bundlePath = 'pages' + normalizePagePath(routeInfo.page) - - // Inject the entry to the client compiler. - if (this.dev) { - const pageKey = 'client' + routeInfo.page - if (!entries[pageKey]) { - entries[pageKey] = { - bundlePath, - absolutePagePath: routeInfo.absolutePagePath, - clientLoader, - dispose: false, - lastActiveTime: Date.now(), - } as any - const invalidator = getInvalidator() - if (invalidator) { - invalidator.invalidate() - } - } - } else { - injectedClientEntries.set( - bundlePath, - `next-client-pages-loader?${stringify({ - isServerComponent: true, - page: denormalizePagePath(bundlePath.replace(/^pages/, '')), - absolutePagePath: clientLoader, - })}!` + clientLoader - ) - } + // Traverse the module graph to find all client components. + filterClientComponents(entryDependency) + + const entryModule = + compilation.moduleGraph.getResolvedModule(entryDependency) + const routeInfo = entryModule.buildInfo.route || { + page: denormalizePagePath(name.replace(/^pages/, '')), + absolutePagePath: entryModule.resource, + } - // Inject the entry to the server compiler. - const clientComponentEntryDep = ( - webpack as any - ).EntryPlugin.createDependency( + // Parse gSSP and gSP exports from the page source. + const pageStaticInfo = this.isEdgeServer + ? {} + : await getPageStaticInfo(routeInfo.absolutePagePath, {}, this.dev) + + const clientLoader = `next-flight-client-entry-loader?${stringify({ + modules: clientComponentImports, + runtime: this.isEdgeServer ? 'edge' : 'nodejs', + ssr: pageStaticInfo.ssr, + // Adding name here to make the entry key unique. + name, + })}!` + + const bundlePath = 'pages' + normalizePagePath(routeInfo.page) + + // Inject the entry to the client compiler. + if (this.dev) { + const pageKey = 'client' + routeInfo.page + if (!entries[pageKey]) { + entries[pageKey] = { + bundlePath, + absolutePagePath: routeInfo.absolutePagePath, clientLoader, - name + '.__sc_client__' - ) - promises.push( - new Promise((res, rej) => { - compilation.addEntry( - context, - clientComponentEntryDep, - this.isEdgeServer - ? { - name: name + '.__sc_client__', - library: { - name: ['self._CLIENT_ENTRY'], - type: 'assign', - }, - runtime: EDGE_RUNTIME_WEBPACK, - asyncChunks: false, - } - : { - name: name + '.__sc_client__', - runtime: 'webpack-runtime', - }, - (err: any) => { - if (err) { - rej(err) - } else { - res() - } - } - ) - }) - ) + dispose: false, + lastActiveTime: Date.now(), + } as any + const invalidator = getInvalidator() + if (invalidator) { + invalidator.invalidate() + } } + } else { + injectedClientEntries.set( + bundlePath, + `next-client-pages-loader?${stringify({ + isServerComponent: true, + page: denormalizePagePath(bundlePath.replace(/^pages/, '')), + absolutePagePath: clientLoader, + })}!` + clientLoader + ) } - Promise.all(promises) - .then(() => callback()) - .catch(callback) + // Inject the entry to the server compiler. + const clientComponentEntryDep = ( + webpack as any + ).EntryPlugin.createDependency( + clientLoader, + name + NEXT_CLIENT_SSR_ENTRY_SUFFIX + ) + promises.push( + new Promise((res, rej) => { + compilation.addEntry( + context, + clientComponentEntryDep, + this.isEdgeServer + ? { + name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, + library: { + name: ['self._CLIENT_ENTRY'], + type: 'assign', + }, + runtime: EDGE_RUNTIME_WEBPACK, + asyncChunks: false, + } + : { + name: name + NEXT_CLIENT_SSR_ENTRY_SUFFIX, + runtime: 'webpack-runtime', + }, + (err: any) => { + if (err) { + rej(err) + } else { + res() + } + } + ) + }) + ) } - ) + } - compiler.hooks.make.tap(PLUGIN_NAME, (compilation: any) => { - compilation.hooks.processAssets.tap( - { - name: PLUGIN_NAME, - // @ts-ignore TODO: Remove ignore when webpack 5 is stable - stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - (assets: any) => this.createAsset(assets, compilation) - ) - }) + Promise.all(promises) + .then(() => callback()) + .catch(callback) } createAsset(assets: any, compilation: any) { diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 37c38d20c411bc..aa923c3671b015 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -10,6 +10,7 @@ import { MIDDLEWARE_FLIGHT_MANIFEST, MIDDLEWARE_MANIFEST, MIDDLEWARE_REACT_LOADABLE_MANIFEST, + NEXT_CLIENT_SSR_ENTRY_SUFFIX, } from '../../../shared/lib/constants' export interface MiddlewareManifest { @@ -395,7 +396,11 @@ function getEntryFiles(entryFiles: string[], meta: EntryMetadata) { (file) => file.startsWith('pages/') && !file.endsWith('.hot-update.js') ) - .map((file) => 'server/' + file.replace('.js', '.__sc_client__.js')) + .map( + (file) => + 'server/' + + file.replace('.js', NEXT_CLIENT_SSR_ENTRY_SUFFIX + '.js') + ) ) } diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index 67a31e5ef140be..b8cebe97259cc9 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -13,6 +13,7 @@ import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, MIDDLEWARE_FLIGHT_MANIFEST, + NEXT_CLIENT_SSR_ENTRY_SUFFIX, } from '../shared/lib/constants' import { join } from 'path' import { requirePage, getPagePath } from './require' @@ -69,7 +70,7 @@ export async function loadComponents( distDir: string, pathname: string, serverless: boolean, - serverComponents?: boolean, + hasServerComponents?: boolean, rootEnabled?: boolean ): Promise { if (serverless) { @@ -115,7 +116,7 @@ export async function loadComponents( Promise.resolve().then(() => requirePage(pathname, distDir, serverless, rootEnabled) ), - serverComponents + hasServerComponents ? Promise.resolve().then(() => requirePage('/_app.server', distDir, serverless, rootEnabled) ) @@ -126,23 +127,23 @@ export async function loadComponents( await Promise.all([ require(join(distDir, BUILD_MANIFEST)), require(join(distDir, REACT_LOADABLE_MANIFEST)), - serverComponents + hasServerComponents ? require(join(distDir, 'server', MIDDLEWARE_FLIGHT_MANIFEST + '.json')) : null, ]) - if (serverComponents) { + if (hasServerComponents) { try { // Make sure to also load the client entry in cache. await requirePage( - normalizePagePath(pathname) + '.__sc_client__', + normalizePagePath(pathname) + NEXT_CLIENT_SSR_ENTRY_SUFFIX, distDir, serverless, rootEnabled ) } catch (_) { - // This page might not be a server component page, so there is no __sc_client__ - // bundle to load. + // This page might not be a server component page, so there is no + // client entry to load. } } diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 08bfa4f6971bd0..f5d421ff47e04a 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -322,92 +322,77 @@ function checkRedirectValues( const rscCache = new Map() -function createFlightHook() { - return ({ - id, - req, - inlinedDataWritable, - staticDataWritable, - bootstrap, - }: { - id: string - req: ReadableStream - bootstrap: boolean - inlinedDataWritable: WritableStream - staticDataWritable: WritableStream | null - }) => { - let entry = rscCache.get(id) - if (!entry) { - const [renderStream, forwardStream] = readableStreamTee(req) - entry = createFromReadableStream(renderStream) - rscCache.set(id, entry) - - let bootstrapped = false - - const forwardReader = forwardStream.getReader() - const inlinedDataWriter = inlinedDataWritable.getWriter() - const staticDataWriter = staticDataWritable - ? staticDataWritable.getWriter() - : null - - function process() { - forwardReader.read().then(({ done, value }) => { - if (bootstrap && !bootstrapped) { - bootstrapped = true - inlinedDataWriter.write( - encodeText( - `` - ) +function useFlightResponse({ + id, + req, + pageData, + inlinedDataWritable, +}: { + id: string + req: ReadableStream + pageData: { current: string } | null + inlinedDataWritable: WritableStream +}) { + let entry = rscCache.get(id) + if (!entry) { + const [renderStream, forwardStream] = readableStreamTee(req) + entry = createFromReadableStream(renderStream) + rscCache.set(id, entry) + + let bootstrapped = false + + const forwardReader = forwardStream.getReader() + const inlinedDataWriter = inlinedDataWritable.getWriter() + + function process() { + forwardReader.read().then(({ done, value }) => { + if (!bootstrapped) { + bootstrapped = true + inlinedDataWriter.write( + encodeText( + `` ) - } - if (done) { - rscCache.delete(id) - inlinedDataWriter.close() - if (staticDataWriter) { - staticDataWriter.close() - } - } else { - inlinedDataWriter.write( - encodeText( - `` - ) + ) + } + if (done) { + rscCache.delete(id) + inlinedDataWriter.close() + } else { + const decodedValue = decodeText(value) + inlinedDataWriter.write( + encodeText( + `` ) - if (staticDataWriter) { - staticDataWriter.write(value) - } - process() + ) + if (pageData) { + pageData.current += decodedValue } - }) - } - process() + process() + } + }) } - return entry + process() } + return entry } -function escapeJSONForFlightScript(input: unknown): string { - return htmlEscapeJsonString(JSON.stringify(input)) -} - -const useFlightResponse = createFlightHook() - // Create the wrapper component for a Flight stream. function createServerComponentRenderer( App: any, Component: any, { cachePrefix, + pageData, inlinedTransformStream, - staticTransformStream, serverComponentManifest, }: { cachePrefix: string + pageData: { current: string } | null inlinedTransformStream: TransformStream - staticTransformStream: null | TransformStream serverComponentManifest: NonNullable } ) { @@ -424,11 +409,8 @@ function createServerComponentRenderer( const response = useFlightResponse({ id: cachePrefix + ',' + id, req: reqStream, + pageData, inlinedDataWritable: inlinedTransformStream.writable, - staticDataWritable: staticTransformStream - ? staticTransformStream.writable - : null, - bootstrap: true, }) const root = response.readRoot() @@ -507,12 +489,10 @@ export async function renderToHTML( Uint8Array, Uint8Array > | null = null - let serverComponentsPageDataTransformStream: TransformStream< - Uint8Array, - Uint8Array - > | null = + + const serverComponentsStaticPageData: { current: string } | null = isServerComponent && process.env.NEXT_RUNTIME !== 'edge' - ? new TransformStream() + ? { current: '' } : null if (isServerComponent) { @@ -527,7 +507,7 @@ export async function renderToHTML( Component = createServerComponentRenderer(App, Component, { cachePrefix: pathname + (search ? `?${search}` : ''), inlinedTransformStream: serverComponentsInlinedTransformStream, - staticTransformStream: serverComponentsPageDataTransformStream, + pageData: serverComponentsStaticPageData, serverComponentManifest, }) } @@ -1189,7 +1169,7 @@ export async function renderToHTML( if ((isDataReq && !isSSG) || (renderOpts as any).isRedirect) { // For server components, we still need to render the page to get the flight // data. - if (!serverComponentsPageDataTransformStream) { + if (!serverComponentsStaticPageData) { return RenderResult.fromStatic(JSON.stringify(props)) } } @@ -1459,38 +1439,6 @@ export async function renderToHTML( return flushed } - // Handle static data for server components. - async function generateStaticFlightDataIfNeeded() { - if (serverComponentsPageDataTransformStream) { - // If it's a server component with the Node.js runtime, we also - // statically generate the page data. - let data = '' - - const readable = serverComponentsPageDataTransformStream.readable - const reader = readable.getReader() - const textDecoder = new TextDecoder() - - while (true) { - const { done, value } = await reader.read() - if (done) { - break - } - data += decodeText(value, textDecoder) - } - - ;(renderOpts as any).pageData = { - ...(renderOpts as any).pageData, - __flight__: data, - } - return data - } - } - - // @TODO: A potential improvement would be to reuse the inlined - // data stream, or pass a callback inside as this doesn't need to - // be streamed. - // Do not use `await` here. - generateStaticFlightDataIfNeeded() return continueFromInitialStream(initialStream, { suffix, dataStream: serverComponentsInlinedTransformStream?.readable, @@ -1698,11 +1646,23 @@ export async function renderToHTML( await documentResult.bodyResult(renderTargetSuffix), ] + // After the page is fully rendered, we can assign the flight response to + // page data of `renderOpts` now. + function updateServerComponentPageData() { + if (serverComponentsStaticPageData) { + ;(renderOpts as any).pageData = { + ...(renderOpts as any).pageData, + __flight__: serverComponentsStaticPageData.current, + } + } + } + if ( - serverComponentsPageDataTransformStream && + serverComponentsStaticPageData && ((isDataReq && !isSSG) || (renderOpts as any).isRedirect) ) { await streamToString(streams[1]) + updateServerComponentPageData() return RenderResult.fromStatic((renderOpts as any).pageData) } @@ -1711,6 +1671,7 @@ export async function renderToHTML( if (generateStaticHTML) { const html = await streamToString(chainStreams(streams)) + updateServerComponentPageData() const optimizedHtml = await postOptimize(html) return new RenderResult(optimizedHtml) } diff --git a/packages/next/server/view-render.tsx b/packages/next/server/view-render.tsx index a4f6c3d840b0c6..3e1e1f10f510f8 100644 --- a/packages/next/server/view-render.tsx +++ b/packages/next/server/view-render.tsx @@ -18,6 +18,7 @@ import { } from './node-web-streams-helper' import { isDynamicRoute } from '../shared/lib/router/utils' import { tryGetPreviewData } from './api-utils/node' +import { htmlEscapeJsonString } from './htmlescape' const ReactDOMServer = process.env.__NEXT_REACT_ROOT ? require('react-dom/server.browser') @@ -98,57 +99,52 @@ function preloadDataFetchingRecord( return record } -function createFlightHook() { - return ( - writable: WritableStream, - id: string, - req: ReadableStream, - bootstrap: boolean - ) => { - let entry = rscCache.get(id) - if (!entry) { - const [renderStream, forwardStream] = readableStreamTee(req) - entry = createFromReadableStream(renderStream) - rscCache.set(id, entry) - - let bootstrapped = false - const forwardReader = forwardStream.getReader() - const writer = writable.getWriter() - function process() { - forwardReader.read().then(({ done, value }) => { - if (bootstrap && !bootstrapped) { - bootstrapped = true - writer.write( - encodeText( - `` - ) +function useFlightResponse( + writable: WritableStream, + id: string, + req: ReadableStream +) { + let entry = rscCache.get(id) + if (!entry) { + const [renderStream, forwardStream] = readableStreamTee(req) + entry = createFromReadableStream(renderStream) + rscCache.set(id, entry) + + let bootstrapped = false + const forwardReader = forwardStream.getReader() + const writer = writable.getWriter() + function process() { + forwardReader.read().then(({ done, value }) => { + if (!bootstrapped) { + bootstrapped = true + writer.write( + encodeText( + `` ) - } - if (done) { - rscCache.delete(id) - writer.close() - } else { - writer.write( - encodeText( - `` - ) + ) + } + if (done) { + rscCache.delete(id) + writer.close() + } else { + writer.write( + encodeText( + `` ) - process() - } - }) - } - process() + ) + process() + } + }) } - return entry + process() } + return entry } -const useFlightResponse = createFlightHook() - // Create the wrapper component for a Flight stream. function createServerComponentRenderer( ComponentToRender: React.ComponentType, @@ -186,8 +182,7 @@ function createServerComponentRenderer( const response = useFlightResponse( writable, cachePrefix + ',' + id, - reqStream, - true + reqStream ) const root = response.readRoot() rscCache.delete(id) diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index 714a997dec5d3f..4a8448d1ec334c 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -34,6 +34,7 @@ export const MODERN_BROWSERSLIST_TARGET = [ 'safari 11', ] export const NEXT_BUILTIN_DOCUMENT = '__NEXT_BUILTIN_DOCUMENT__' +export const NEXT_CLIENT_SSR_ENTRY_SUFFIX = '.__sc_client__' // server/middleware-flight-manifest.js export const MIDDLEWARE_FLIGHT_MANIFEST = 'middleware-flight-manifest'