2608e7d865
x-ref: #31506 This PR migrates existing SSR on edge from middleware to edge functions implmentation. So that we can get rid of limitation of middleware and resolve the conflicts between middleware and edge SSR routes. * Adding edge functions matching route in middleware catch all route,keep the order as `middleware catch all` -> redirects/rewrites -> `edge catch all` -> others * Dropping middleware related code for edge SSR: removing client info and preflight request handling
215 lines
5.4 KiB
TypeScript
215 lines
5.4 KiB
TypeScript
import type { WebNextRequest, WebNextResponse } from './base-http/web'
|
|
import type { RenderOpts } from './render'
|
|
import type RenderResult from './render-result'
|
|
import type { NextParsedUrlQuery } from './request-meta'
|
|
import type { Params } from '../shared/lib/router/utils/route-matcher'
|
|
import type { PayloadOptions } from './send-payload'
|
|
import type { LoadComponentsReturnType } from './load-components'
|
|
import type { Options } from './base-server'
|
|
|
|
import BaseServer from './base-server'
|
|
import { renderToHTML } from './render'
|
|
import { byteLength, generateETag } from './api-utils/web'
|
|
|
|
interface WebServerOptions extends Options {
|
|
webServerConfig: {
|
|
page: string
|
|
loadComponent: (
|
|
pathname: string
|
|
) => Promise<LoadComponentsReturnType | null>
|
|
extendRenderOpts: Partial<BaseServer['renderOpts']> &
|
|
Pick<BaseServer['renderOpts'], 'buildId'>
|
|
}
|
|
}
|
|
|
|
export default class NextWebServer extends BaseServer<WebServerOptions> {
|
|
constructor(options: WebServerOptions) {
|
|
super(options)
|
|
|
|
// Extend `renderOpts`.
|
|
Object.assign(this.renderOpts, options.webServerConfig.extendRenderOpts)
|
|
}
|
|
|
|
protected generateRewrites() {
|
|
// @TODO: assuming minimal mode right now
|
|
return {
|
|
beforeFiles: [],
|
|
afterFiles: [],
|
|
fallback: [],
|
|
}
|
|
}
|
|
protected handleCompression() {
|
|
// For the web server layer, compression is automatically handled by the
|
|
// upstream proxy (edge runtime or node server) and we can simply skip here.
|
|
}
|
|
protected getRoutesManifest() {
|
|
return {
|
|
headers: [],
|
|
rewrites: {
|
|
fallback: [],
|
|
afterFiles: [],
|
|
beforeFiles: [],
|
|
},
|
|
redirects: [],
|
|
}
|
|
}
|
|
protected getPagePath() {
|
|
// @TODO
|
|
return ''
|
|
}
|
|
protected getPublicDir() {
|
|
// Public files are not handled by the web server.
|
|
return ''
|
|
}
|
|
protected getBuildId() {
|
|
return this.serverOptions.webServerConfig.extendRenderOpts.buildId
|
|
}
|
|
protected loadEnvConfig() {
|
|
// The web server does not need to load the env config. This is done by the
|
|
// runtime already.
|
|
}
|
|
protected getHasStaticDir() {
|
|
return false
|
|
}
|
|
protected generateImageRoutes() {
|
|
return []
|
|
}
|
|
protected generateStaticRoutes() {
|
|
return []
|
|
}
|
|
protected generateFsStaticRoutes() {
|
|
return []
|
|
}
|
|
protected generatePublicRoutes() {
|
|
return []
|
|
}
|
|
protected generateCatchAllMiddlewareRoute() {
|
|
return []
|
|
}
|
|
protected getFontManifest() {
|
|
return undefined
|
|
}
|
|
protected getPagesManifest() {
|
|
return {
|
|
[this.serverOptions.webServerConfig.page]: '',
|
|
}
|
|
}
|
|
protected getAppPathsManifest() {
|
|
return {
|
|
[this.serverOptions.webServerConfig.page]: '',
|
|
}
|
|
}
|
|
protected getFilesystemPaths() {
|
|
return new Set<string>()
|
|
}
|
|
protected getPrerenderManifest() {
|
|
return {
|
|
version: 3 as const,
|
|
routes: {},
|
|
dynamicRoutes: {},
|
|
notFoundRoutes: [],
|
|
preview: {
|
|
previewModeId: '',
|
|
previewModeSigningKey: '',
|
|
previewModeEncryptionKey: '',
|
|
},
|
|
}
|
|
}
|
|
protected getServerComponentManifest() {
|
|
// @TODO: Need to return `extendRenderOpts.serverComponentManifest` here.
|
|
return undefined
|
|
}
|
|
protected async renderHTML(
|
|
req: WebNextRequest,
|
|
_res: WebNextResponse,
|
|
pathname: string,
|
|
query: NextParsedUrlQuery,
|
|
renderOpts: RenderOpts
|
|
): Promise<RenderResult | null> {
|
|
return renderToHTML(
|
|
{
|
|
url: req.url,
|
|
cookies: req.cookies,
|
|
headers: req.headers,
|
|
} as any,
|
|
{} as any,
|
|
pathname,
|
|
query,
|
|
{
|
|
...renderOpts,
|
|
disableOptimizedLoading: true,
|
|
runtime: 'edge',
|
|
}
|
|
)
|
|
}
|
|
protected async sendRenderResult(
|
|
_req: WebNextRequest,
|
|
res: WebNextResponse,
|
|
options: {
|
|
result: RenderResult
|
|
type: 'html' | 'json'
|
|
generateEtags: boolean
|
|
poweredByHeader: boolean
|
|
options?: PayloadOptions | undefined
|
|
}
|
|
): Promise<void> {
|
|
res.setHeader('X-Edge-Runtime', '1')
|
|
|
|
// Add necessary headers.
|
|
// @TODO: Share the isomorphic logic with server/send-payload.ts.
|
|
if (options.poweredByHeader && options.type === 'html') {
|
|
res.setHeader('X-Powered-By', 'Next.js')
|
|
}
|
|
if (!res.getHeader('Content-Type')) {
|
|
res.setHeader(
|
|
'Content-Type',
|
|
options.type === 'json'
|
|
? 'application/json'
|
|
: 'text/html; charset=utf-8'
|
|
)
|
|
}
|
|
|
|
if (options.result.isDynamic()) {
|
|
const writer = res.transformStream.writable.getWriter()
|
|
options.result.pipe({
|
|
write: (chunk: Uint8Array) => writer.write(chunk),
|
|
end: () => writer.close(),
|
|
destroy: (err: Error) => writer.abort(err),
|
|
cork: () => {},
|
|
uncork: () => {},
|
|
// Not implemented: on/removeListener
|
|
} as any)
|
|
} else {
|
|
const payload = await options.result.toUnchunkedString()
|
|
res.setHeader('Content-Length', String(byteLength(payload)))
|
|
if (options.generateEtags) {
|
|
res.setHeader('ETag', await generateETag(payload))
|
|
}
|
|
res.body(payload)
|
|
}
|
|
|
|
res.send()
|
|
}
|
|
protected async runApi() {
|
|
// @TODO
|
|
return true
|
|
}
|
|
protected async findPageComponents(
|
|
pathname: string,
|
|
query?: NextParsedUrlQuery,
|
|
params?: Params | null
|
|
) {
|
|
const result = await this.serverOptions.webServerConfig.loadComponent(
|
|
pathname
|
|
)
|
|
if (!result) return null
|
|
|
|
return {
|
|
query: {
|
|
...(query || {}),
|
|
...(params || {}),
|
|
},
|
|
components: result,
|
|
}
|
|
}
|
|
}
|