From 3324d3363862d0a8a63723a9ac6fff87538ea4c1 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Wed, 20 Nov 2024 17:39:34 +0100 Subject: [PATCH] remove inheritance from NextCustomServer --- packages/next/src/server/lib/render-server.ts | 37 +-- packages/next/src/server/next.ts | 214 +++++++++++++----- 2 files changed, 170 insertions(+), 81 deletions(-) diff --git a/packages/next/src/server/lib/render-server.ts b/packages/next/src/server/lib/render-server.ts index 206b238018c83d..656dde66c1acce 100644 --- a/packages/next/src/server/lib/render-server.ts +++ b/packages/next/src/server/lib/render-server.ts @@ -1,23 +1,18 @@ -import type { NextServer, RequestHandler } from '../next' +import type { NextServer, RequestHandler, UpgradeHandler } from '../next' import type { DevBundlerService } from './dev-bundler-service' import type { PropagateToWorkersField } from './router-utils/types' import next from '../next' import type { Span } from '../../trace' -let initializations: Record< - string, - | Promise<{ - requestHandler: ReturnType< - InstanceType['getRequestHandler'] - > - upgradeHandler: ReturnType< - InstanceType['getUpgradeHandler'] - > - app: ReturnType - }> - | undefined -> = {} +type InitializationResult = { + requestHandler: RequestHandler + upgradeHandler: UpgradeHandler + app: NextServer +} + +let initializations: Record | undefined> = + {} let sandboxContext: undefined | typeof import('../web/sandbox/context') @@ -93,7 +88,7 @@ async function initializeImpl(opts: { } let requestHandler: RequestHandler - let upgradeHandler: any + let upgradeHandler: UpgradeHandler const app = next({ ...opts, @@ -101,7 +96,7 @@ async function initializeImpl(opts: { customServer: false, httpServer: opts.server, port: opts.port, - }) + }) as NextServer // should return a NextServer when `customServer: false` requestHandler = app.getRequestHandler() upgradeHandler = app.getUpgradeHandler() @@ -116,15 +111,7 @@ async function initializeImpl(opts: { export async function initialize( opts: Parameters[0] -): Promise<{ - requestHandler: ReturnType< - InstanceType['getRequestHandler'] - > - upgradeHandler: ReturnType< - InstanceType['getUpgradeHandler'] - > - app: NextServer -}> { +): Promise { // if we already setup the server return as we only need to do // this on first worker boot if (initializations[opts.dir]) { diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index ed57c8f34fb040..76456ba56c2d2d 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -5,13 +5,14 @@ import type { } from './next-server' import type { UrlWithParsedQuery } from 'url' import type { IncomingMessage, ServerResponse } from 'http' +import type { Duplex } from 'stream' import type { NextUrlWithParsedQuery } from './request-meta' import type { WorkerRequestHandler, WorkerUpgradeHandler } from './lib/types' import './require-hook' import './node-polyfill-crypto' -import type { default as Server } from './next-server' +import type { default as NextNodeServer } from './next-server' import * as log from '../build/output/log' import loadConfig from './config' import path, { resolve } from 'path' @@ -26,7 +27,7 @@ import { NextServerSpan } from './lib/trace/constants' import { formatUrl } from '../shared/lib/router/utils/format-url' import type { ServerFields } from './lib/router-utils/setup-dev-bundler' -let ServerImpl: typeof Server +let ServerImpl: typeof NextNodeServer const getServerImpl = async () => { if (ServerImpl === undefined) { @@ -42,26 +43,69 @@ export type NextServerOptions = Omit< > & Partial> -export interface RequestHandler { - ( - req: IncomingMessage, - res: ServerResponse, - parsedUrl?: NextUrlWithParsedQuery | undefined - ): Promise -} +export type RequestHandler = ( + req: IncomingMessage, + res: ServerResponse, + parsedUrl?: NextUrlWithParsedQuery | undefined +) => Promise + +export type UpgradeHandler = ( + req: IncomingMessage, + socket: Duplex, + head: Buffer +) => Promise const SYMBOL_LOAD_CONFIG = Symbol('next.load_config') -export class NextServer { - private serverPromise?: Promise - private server?: Server +export interface NextWrapperServer { + // NOTE: the methods/properties here are the public API for custom servers. + // Consider backwards compatibilty when changing something here! + + options: NextServerOptions + hostname: string | undefined + port: number | undefined + + getRequestHandler(): RequestHandler + prepare(serverFields?: ServerFields): Promise + setAssetPrefix(assetPrefix: string): void + close(): Promise + + // used internally + getUpgradeHandler(): UpgradeHandler + + // legacy methods that we left exposed in the past + + logError(...args: Parameters): void + + render( + ...args: Parameters + ): ReturnType + + renderToHTML( + ...args: Parameters + ): ReturnType + + renderError( + ...args: Parameters + ): ReturnType + + renderErrorToHTML( + ...args: Parameters + ): ReturnType + + render404( + ...args: Parameters + ): ReturnType +} + +/** The wrapper server used by `next start` */ +export class NextServer implements NextWrapperServer { + private serverPromise?: Promise + private server?: NextNodeServer private reqHandler?: NodeRequestHandler private reqHandlerPromise?: Promise private preparedAssetPrefix?: string - protected cleanupListeners: (() => Promise)[] = [] - protected standaloneMode?: boolean - public options: NextServerOptions constructor(options: NextServerOptions) { @@ -89,7 +133,7 @@ export class NextServer { } } - getUpgradeHandler() { + getUpgradeHandler(): UpgradeHandler { return async (req: IncomingMessage, socket: any, head: any) => { const server = await this.getServer() // @ts-expect-error we mark this as protected so it @@ -106,40 +150,40 @@ export class NextServer { } } - logError(...args: Parameters) { + logError(...args: Parameters) { if (this.server) { this.server.logError(...args) } } - async render(...args: Parameters) { + async render(...args: Parameters) { const server = await this.getServer() return server.render(...args) } - async renderToHTML(...args: Parameters) { + async renderToHTML(...args: Parameters) { const server = await this.getServer() return server.renderToHTML(...args) } - async renderError(...args: Parameters) { + async renderError(...args: Parameters) { const server = await this.getServer() return server.renderError(...args) } - async renderErrorToHTML(...args: Parameters) { + async renderErrorToHTML( + ...args: Parameters + ) { const server = await this.getServer() return server.renderErrorToHTML(...args) } - async render404(...args: Parameters) { + async render404(...args: Parameters) { const server = await this.getServer() return server.render404(...args) } async prepare(serverFields?: ServerFields) { - if (this.standaloneMode) return - const server = await this.getServer() if (serverFields) { @@ -153,21 +197,16 @@ export class NextServer { } async close() { - await Promise.all( - [ - async () => { - const server = await this.getServer() - await (server as any).close() - }, - ...this.cleanupListeners, - ].map((f) => f()) - ) + if (this.server) { + // BaseServer.close() is protected + await this.server['close']() + } } private async createServer( options: ServerOptions | DevServerOptions - ): Promise { - let ServerImplementation: typeof Server + ): Promise { + let ServerImplementation: typeof NextNodeServer if (options.dev) { ServerImplementation = require('./dev/next-dev-server') .default as typeof import('./dev/next-dev-server').default @@ -214,10 +253,6 @@ export class NextServer { private async getServer() { if (!this.serverPromise) { this.serverPromise = this[SYMBOL_LOAD_CONFIG]().then(async (conf) => { - if (this.standaloneMode) { - process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(conf) - } - if (!this.options.dev) { if (conf.output === 'standalone') { if (!process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) { @@ -263,16 +298,49 @@ export class NextServer { } } -class NextCustomServer extends NextServer { - protected standaloneMode = true +/** The wrapper server used for `import next from "next" (in a custom server)` */ +class NextCustomServer implements NextWrapperServer { private didWebSocketSetup: boolean = false + protected cleanupListeners: (() => Promise)[] = [] + + protected init?: { + requestHandler: WorkerRequestHandler + upgradeHandler: WorkerUpgradeHandler + renderServer: NextServer + } + + public options: NextServerOptions + + constructor(options: NextServerOptions) { + this.options = options + } - // @ts-expect-error These are initialized in prepare() - protected requestHandler: WorkerRequestHandler - // @ts-expect-error These are initialized in prepare() - protected upgradeHandler: WorkerUpgradeHandler - // @ts-expect-error These are initialized in prepare() - protected renderServer: NextServer + protected getInit() { + if (!this.init) { + throw new Error( + 'prepare() must be called before performing this operation' + ) + } + return this.init + } + + protected get requestHandler() { + return this.getInit().requestHandler + } + protected get upgradeHandler() { + return this.getInit().upgradeHandler + } + protected get renderServer() { + return this.getInit().renderServer + } + + get hostname() { + return this.options.hostname + } + + get port() { + return this.options.port + } async prepare() { const { getRequestHandlers } = @@ -287,9 +355,11 @@ class NextCustomServer extends NextServer { minimalMode: this.options.minimalMode, quiet: this.options.quiet, }) - this.requestHandler = initResult[0] - this.upgradeHandler = initResult[1] - this.renderServer = initResult[2] + this.init = { + requestHandler: initResult[0], + upgradeHandler: initResult[1], + renderServer: initResult[2], + } } private setupWebSocketHandler( @@ -308,7 +378,7 @@ class NextCustomServer extends NextServer { } } - getRequestHandler() { + getRequestHandler(): RequestHandler { return async ( req: IncomingMessage, res: ServerResponse, @@ -324,9 +394,9 @@ class NextCustomServer extends NextServer { } } - async render(...args: Parameters) { + async render(...args: Parameters) { let [req, res, pathname, query, parsedUrl] = args - this.setupWebSocketHandler(this.options.httpServer, req as any) + this.setupWebSocketHandler(this.options.httpServer, req as IncomingMessage) if (!pathname.startsWith('/')) { console.error(`Cannot render page with path "${pathname}"`) @@ -340,14 +410,46 @@ class NextCustomServer extends NextServer { query, }) - await this.requestHandler(req as any, res as any) + await this.requestHandler(req as IncomingMessage, res as ServerResponse) return } setAssetPrefix(assetPrefix: string): void { - super.setAssetPrefix(assetPrefix) this.renderServer.setAssetPrefix(assetPrefix) } + + getUpgradeHandler(): UpgradeHandler { + return this.renderServer.getUpgradeHandler() + } + + logError(...args: Parameters) { + this.renderServer.logError(...args) + } + + async renderToHTML(...args: Parameters) { + return this.renderServer.renderToHTML(...args) + } + + async renderError(...args: Parameters) { + return this.renderServer.renderError(...args) + } + + async renderErrorToHTML( + ...args: Parameters + ) { + return this.renderServer.renderErrorToHTML(...args) + } + + async render404(...args: Parameters) { + return this.renderServer.render404(...args) + } + + async close() { + await Promise.all([ + this.init?.renderServer.close(), + ...this.cleanupListeners.map((f) => f()), + ]) + } } // This file is used for when users run `require('next')` @@ -356,7 +458,7 @@ function createServer( turbo?: boolean turbopack?: boolean } -): NextServer { +): NextWrapperServer { if (options && (options.turbo || options.turbopack)) { process.env.TURBOPACK = '1' }