Skip to content

Commit 99d4d6c

Browse files
authored
Implement web server as the request handler for edge SSR (#33635)
(#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.
1 parent f0e31ee commit 99d4d6c

File tree

13 files changed

+569
-377
lines changed

13 files changed

+569
-377
lines changed

packages/next/build/entries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ export function createEntrypoints(
157157
const isFlight = isFlightPage(config, absolutePagePath)
158158

159159
const webServerRuntime = !!config.experimental.concurrentFeatures
160-
const hasServerComponents = !!config.experimental.serverComponents
161160

162161
if (page.match(MIDDLEWARE_ROUTE)) {
163162
const loaderOpts: MiddlewareLoaderOptions = {
@@ -176,11 +175,12 @@ export function createEntrypoints(
176175
serverWeb[serverBundlePath] = finalizeEntrypoint({
177176
name: '[name].js',
178177
value: `next-middleware-ssr-loader?${stringify({
178+
dev: false,
179179
page,
180+
stringifiedConfig: JSON.stringify(config),
180181
absolute500Path: pages['/500'] || '',
181182
absolutePagePath,
182183
isServerComponent: isFlight,
183-
serverComponents: hasServerComponents,
184184
...defaultServerlessOptions,
185185
} as any)}!`,
186186
isServer: false,

packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { stringifyRequest } from '../../stringify-request'
22

33
export default async function middlewareSSRLoader(this: any) {
44
const {
5+
dev,
6+
page,
7+
buildId,
58
absolutePagePath,
69
absoluteAppPath,
710
absoluteDocumentPath,
811
absolute500Path,
912
absoluteErrorPath,
1013
isServerComponent,
11-
...restRenderOpts
14+
stringifiedConfig,
1215
} = this.getOptions()
1316

1417
const stringifiedAbsolutePagePath = stringifyRequest(this, absolutePagePath)
@@ -42,16 +45,37 @@ export default async function middlewareSSRLoader(this: any) {
4245
throw new Error('Your page must export a \`default\` component')
4346
}
4447
45-
const render = getRender({
46-
App,
47-
Document,
48-
pageMod,
49-
errorMod,
48+
// Set server context
49+
self.__current_route = ${JSON.stringify(page)}
50+
self.__server_context = {
51+
Component: pageMod.default,
52+
pageConfig: pageMod.config || {},
5053
buildManifest,
5154
reactLoadableManifest,
52-
rscManifest,
55+
Document,
56+
App,
57+
getStaticProps: pageMod.getStaticProps,
58+
getServerSideProps: pageMod.getServerSideProps,
59+
getStaticPaths: pageMod.getStaticPaths,
60+
ComponentMod: undefined,
61+
serverComponentManifest: ${isServerComponent} ? rscManifest : null,
62+
63+
// components
64+
errorMod,
65+
66+
// renderOpts
67+
buildId: ${JSON.stringify(buildId)},
68+
dev: ${dev},
69+
env: process.env,
70+
supportsDynamicHTML: true,
71+
concurrentFeatures: true,
72+
disableOptimizedLoading: true,
73+
}
74+
75+
const render = getRender({
76+
Document,
5377
isServerComponent: ${isServerComponent},
54-
restRenderOpts: ${JSON.stringify(restRenderOpts)}
78+
config: ${stringifiedConfig},
5579
})
5680
5781
export default function rscMiddleware(opts) {
Lines changed: 23 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import type { NextConfig } from '../../../../server/config-shared'
2+
13
import { NextRequest } from '../../../../server/web/spec-extension/request'
2-
import { renderToHTML } from '../../../../server/web/render'
3-
import RenderResult from '../../../../server/render-result'
44
import { toNodeHeaders } from '../../../../server/web/utils'
55

6+
import WebServer from '../../../../server/web-server'
7+
import { WebNextRequest, WebNextResponse } from '../../../../server/base-http'
8+
69
const createHeaders = (args?: any) => ({
710
...args,
811
'x-middleware-ssr': '1',
@@ -18,26 +21,22 @@ function sendError(req: any, error: Error) {
1821
}
1922

2023
export function getRender({
21-
App,
2224
Document,
23-
pageMod,
24-
errorMod,
25-
rscManifest,
26-
buildManifest,
27-
reactLoadableManifest,
2825
isServerComponent,
29-
restRenderOpts,
26+
config,
3027
}: {
31-
App: any
3228
Document: any
33-
pageMod: any
34-
errorMod: any
35-
rscManifest: object
36-
buildManifest: any
37-
reactLoadableManifest: any
3829
isServerComponent: boolean
39-
restRenderOpts: any
30+
config: NextConfig
4031
}) {
32+
// Polyfilled for `path-browserify`.
33+
process.cwd = () => ''
34+
const server = new WebServer({
35+
conf: config,
36+
minimalMode: true,
37+
})
38+
const requestHandler = server.getRequestHandler()
39+
4140
return async function render(request: NextRequest) {
4241
const { nextUrl: url, cookies, headers } = request
4342
const { pathname, searchParams } = url
@@ -56,6 +55,7 @@ export function getRender({
5655
})
5756
}
5857

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

75-
delete query.__flight__
76-
delete query.__props__
77-
78-
const renderOpts = {
79-
...restRenderOpts,
80-
// Locales are not supported yet.
81-
// locales: i18n?.locales,
82-
// locale: detectedLocale,
83-
// defaultLocale,
84-
// domainLocales: i18n?.domains,
85-
dev: process.env.NODE_ENV !== 'production',
86-
App,
87-
Document,
88-
buildManifest,
89-
Component: pageMod.default,
90-
pageConfig: pageMod.config || {},
91-
getStaticProps: pageMod.getStaticProps,
92-
getServerSideProps: pageMod.getServerSideProps,
93-
getStaticPaths: pageMod.getStaticPaths,
94-
reactLoadableManifest,
95-
env: process.env,
96-
supportsDynamicHTML: true,
97-
concurrentFeatures: true,
98-
// When streaming, opt-out the `defer` behavior for script tags.
99-
disableOptimizedLoading: true,
75+
// Extend the context.
76+
Object.assign((self as any).__server_context, {
10077
renderServerComponentData,
10178
serverComponentProps,
102-
serverComponentManifest: isServerComponent ? rscManifest : null,
103-
ComponentMod: null,
104-
}
105-
106-
const transformStream = new TransformStream()
107-
const writer = transformStream.writable.getWriter()
108-
const encoder = new TextEncoder()
109-
110-
let result: RenderResult | null
111-
let renderError: any
112-
try {
113-
result = await renderToHTML(
114-
req as any,
115-
{} as any,
116-
pathname,
117-
query,
118-
renderOpts
119-
)
120-
} catch (err: any) {
121-
console.error(
122-
'An error occurred while rendering the initial result:',
123-
err
124-
)
125-
const errorRes = { statusCode: 500, err }
126-
renderError = err
127-
try {
128-
req.url = '/_error'
129-
result = await renderToHTML(
130-
req as any,
131-
errorRes as any,
132-
'/_error',
133-
query,
134-
{
135-
...renderOpts,
136-
err,
137-
Component: errorMod.default,
138-
getStaticProps: errorMod.getStaticProps,
139-
getServerSideProps: errorMod.getServerSideProps,
140-
getStaticPaths: errorMod.getStaticPaths,
141-
}
142-
)
143-
} catch (err2: any) {
144-
return sendError(req, err2)
145-
}
146-
}
147-
148-
if (!result) {
149-
return sendError(req, new Error('No result returned from render.'))
150-
}
151-
152-
result.pipe({
153-
write: (str: string) => writer.write(encoder.encode(str)),
154-
end: () => writer.close(),
155-
// Not implemented: cork/uncork/on/removeListener
156-
} as any)
157-
158-
return new Response(transformStream.readable, {
159-
headers: createHeaders(),
160-
status: renderError ? 500 : 200,
16179
})
80+
81+
const extendedReq = new WebNextRequest(request)
82+
const extendedRes = new WebNextResponse()
83+
requestHandler(extendedReq, extendedRes)
84+
return await extendedRes.toResponse()
16285
}
16386
}

packages/next/lib/chalk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
let chalk: typeof import('next/dist/compiled/chalk')
22

3-
if (typeof window === 'undefined') {
3+
if (!process.browser) {
44
chalk = require('next/dist/compiled/chalk')
55
} else {
66
chalk = require('./web/chalk').default

packages/next/lib/web/chalk.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
// - chalk.red('error')
55
// - chalk.bold.cyan('message')
66
// - chalk.hex('#fff').underline('hello')
7-
const log = console.log
8-
const chalk: any = new Proxy(log, {
7+
const chalk: any = new Proxy((s: string) => s, {
98
get(_, prop: string) {
109
if (
1110
['hex', 'rgb', 'ansi256', 'bgHex', 'bgRgb', 'bgAnsi256'].includes(prop)

0 commit comments

Comments
 (0)