Skip to content

Commit

Permalink
Implement web server as the request handler for edge SSR (#33635)
Browse files Browse the repository at this point in the history
(#31506 for context)

This PR implements the minimum viable web server on top of the Next.js base server, and integrates it into our middleware (edge) SSR runtime to handle all the requests.

This also addresses problems like missing dynamic routes support in our current handler.

Note that this is the initial implementation with the assumption that the web server is running under minimal mode. Also later we can refactor the `__server_context` environment to properly passing the context via the constructor or methods.
  • Loading branch information
shuding committed Jan 26, 2022
1 parent f0e31ee commit 99d4d6c
Show file tree
Hide file tree
Showing 13 changed files with 569 additions and 377 deletions.
4 changes: 2 additions & 2 deletions packages/next/build/entries.ts
Expand Up @@ -157,7 +157,6 @@ 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,11 +175,12 @@ export function createEntrypoints(
serverWeb[serverBundlePath] = finalizeEntrypoint({
name: '[name].js',
value: `next-middleware-ssr-loader?${stringify({
dev: false,
page,
stringifiedConfig: JSON.stringify(config),
absolute500Path: pages['/500'] || '',
absolutePagePath,
isServerComponent: isFlight,
serverComponents: hasServerComponents,
...defaultServerlessOptions,
} as any)}!`,
isServer: false,
Expand Down
Expand Up @@ -2,13 +2,16 @@ import { stringifyRequest } from '../../stringify-request'

export default async function middlewareSSRLoader(this: any) {
const {
dev,
page,
buildId,
absolutePagePath,
absoluteAppPath,
absoluteDocumentPath,
absolute500Path,
absoluteErrorPath,
isServerComponent,
...restRenderOpts
stringifiedConfig,
} = this.getOptions()

const stringifiedAbsolutePagePath = stringifyRequest(this, absolutePagePath)
Expand Down Expand Up @@ -42,16 +45,37 @@ export default async function middlewareSSRLoader(this: any) {
throw new Error('Your page must export a \`default\` component')
}
const render = getRender({
App,
Document,
pageMod,
errorMod,
// Set server context
self.__current_route = ${JSON.stringify(page)}
self.__server_context = {
Component: pageMod.default,
pageConfig: pageMod.config || {},
buildManifest,
reactLoadableManifest,
rscManifest,
Document,
App,
getStaticProps: pageMod.getStaticProps,
getServerSideProps: pageMod.getServerSideProps,
getStaticPaths: pageMod.getStaticPaths,
ComponentMod: undefined,
serverComponentManifest: ${isServerComponent} ? rscManifest : null,
// components
errorMod,
// renderOpts
buildId: ${JSON.stringify(buildId)},
dev: ${dev},
env: process.env,
supportsDynamicHTML: true,
concurrentFeatures: true,
disableOptimizedLoading: true,
}
const render = getRender({
Document,
isServerComponent: ${isServerComponent},
restRenderOpts: ${JSON.stringify(restRenderOpts)}
config: ${stringifiedConfig},
})
export default function rscMiddleware(opts) {
Expand Down
123 changes: 23 additions & 100 deletions packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts
@@ -1,8 +1,11 @@
import type { NextConfig } from '../../../../server/config-shared'

import { NextRequest } from '../../../../server/web/spec-extension/request'
import { renderToHTML } from '../../../../server/web/render'
import RenderResult from '../../../../server/render-result'
import { toNodeHeaders } from '../../../../server/web/utils'

import WebServer from '../../../../server/web-server'
import { WebNextRequest, WebNextResponse } from '../../../../server/base-http'

const createHeaders = (args?: any) => ({
...args,
'x-middleware-ssr': '1',
Expand All @@ -18,26 +21,22 @@ function sendError(req: any, error: Error) {
}

export function getRender({
App,
Document,
pageMod,
errorMod,
rscManifest,
buildManifest,
reactLoadableManifest,
isServerComponent,
restRenderOpts,
config,
}: {
App: any
Document: any
pageMod: any
errorMod: any
rscManifest: object
buildManifest: any
reactLoadableManifest: any
isServerComponent: boolean
restRenderOpts: any
config: NextConfig
}) {
// Polyfilled for `path-browserify`.
process.cwd = () => ''
const server = new WebServer({
conf: config,
minimalMode: true,
})
const requestHandler = server.getRequestHandler()

return async function render(request: NextRequest) {
const { nextUrl: url, cookies, headers } = request
const { pathname, searchParams } = url
Expand All @@ -56,6 +55,7 @@ export function getRender({
})
}

// @TODO: We should move this into server/render.
if (Document.getInitialProps) {
const err = new Error(
'`getInitialProps` in Document component is not supported with `concurrentFeatures` enabled.'
Expand All @@ -72,92 +72,15 @@ export function getRender({
? JSON.parse(query.__props__)
: undefined

delete query.__flight__
delete query.__props__

const renderOpts = {
...restRenderOpts,
// Locales are not supported yet.
// locales: i18n?.locales,
// locale: detectedLocale,
// defaultLocale,
// domainLocales: i18n?.domains,
dev: process.env.NODE_ENV !== 'production',
App,
Document,
buildManifest,
Component: pageMod.default,
pageConfig: pageMod.config || {},
getStaticProps: pageMod.getStaticProps,
getServerSideProps: pageMod.getServerSideProps,
getStaticPaths: pageMod.getStaticPaths,
reactLoadableManifest,
env: process.env,
supportsDynamicHTML: true,
concurrentFeatures: true,
// When streaming, opt-out the `defer` behavior for script tags.
disableOptimizedLoading: true,
// Extend the context.
Object.assign((self as any).__server_context, {
renderServerComponentData,
serverComponentProps,
serverComponentManifest: isServerComponent ? rscManifest : null,
ComponentMod: null,
}

const transformStream = new TransformStream()
const writer = transformStream.writable.getWriter()
const encoder = new TextEncoder()

let result: RenderResult | null
let renderError: any
try {
result = await renderToHTML(
req as any,
{} as any,
pathname,
query,
renderOpts
)
} catch (err: any) {
console.error(
'An error occurred while rendering the initial result:',
err
)
const errorRes = { statusCode: 500, err }
renderError = err
try {
req.url = '/_error'
result = await renderToHTML(
req as any,
errorRes as any,
'/_error',
query,
{
...renderOpts,
err,
Component: errorMod.default,
getStaticProps: errorMod.getStaticProps,
getServerSideProps: errorMod.getServerSideProps,
getStaticPaths: errorMod.getStaticPaths,
}
)
} catch (err2: any) {
return sendError(req, err2)
}
}

if (!result) {
return sendError(req, new Error('No result returned from render.'))
}

result.pipe({
write: (str: string) => writer.write(encoder.encode(str)),
end: () => writer.close(),
// Not implemented: cork/uncork/on/removeListener
} as any)

return new Response(transformStream.readable, {
headers: createHeaders(),
status: renderError ? 500 : 200,
})

const extendedReq = new WebNextRequest(request)
const extendedRes = new WebNextResponse()
requestHandler(extendedReq, extendedRes)
return await extendedRes.toResponse()
}
}
2 changes: 1 addition & 1 deletion packages/next/lib/chalk.ts
@@ -1,6 +1,6 @@
let chalk: typeof import('next/dist/compiled/chalk')

if (typeof window === 'undefined') {
if (!process.browser) {
chalk = require('next/dist/compiled/chalk')
} else {
chalk = require('./web/chalk').default
Expand Down
3 changes: 1 addition & 2 deletions packages/next/lib/web/chalk.ts
Expand Up @@ -4,8 +4,7 @@
// - chalk.red('error')
// - chalk.bold.cyan('message')
// - chalk.hex('#fff').underline('hello')
const log = console.log
const chalk: any = new Proxy(log, {
const chalk: any = new Proxy((s: string) => s, {
get(_, prop: string) {
if (
['hex', 'rgb', 'ansi256', 'bgHex', 'bgRgb', 'bgAnsi256'].includes(prop)
Expand Down

0 comments on commit 99d4d6c

Please sign in to comment.