f354f46b3f
This PR deprecates declaring a middleware under `pages` in favour of the project root naming it after `middleware` instead of `_middleware`. This is in the context of having a simpler execution model for middleware and also ships some refactor work. There is a ton of a code to be simplified after this deprecation but I think it is best to do it progressively. With this PR, when in development, we will **fail** whenever we find a nested middleware but we do **not** include it in the compiler so if the project is using it, it will no longer work. For production we will **fail** too so it will not be possible to build and deploy a deprecated middleware. The error points to a page that should also be reviewed as part of **documentation**. Aside from the deprecation, this migrates all middleware tests to work with a single middleware. It also splits tests into multiple folders to make them easier to isolate and work with. Finally it ships some small code refactor and simplifications.
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 undefined
|
|
}
|
|
protected getFontManifest() {
|
|
return undefined
|
|
}
|
|
protected getPagesManifest() {
|
|
return {
|
|
[this.serverOptions.webServerConfig.page]: '',
|
|
}
|
|
}
|
|
protected getViewPathsManifest() {
|
|
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,
|
|
}
|
|
}
|
|
}
|