rsnext/packages/next/server/next.ts
Jiachi Liu b0f87fbc7c
Bundle ssr client layer excepts react externals (#41606)
Bundle the ssr client layer for RSC, this solves the problem when
there's an esm package is using on client components, but esm imports
the installed react instead of the built-in react version since esm
imports is not intercepted by require hook.

After bundling the ssr client layer and treating react as externals, now
react compiles as cjs externals and could be intercepted by require
hook, other code are bundled together which can also get optimized.

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

Co-authored-by: Shu Ding <g@shud.in>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-10-22 16:33:51 -07:00

226 lines
6.1 KiB
TypeScript

import type { Options as DevServerOptions } from './dev/next-dev-server'
import type { NodeRequestHandler } from './next-server'
import type { UrlWithParsedQuery } from 'url'
import './node-polyfill-fetch'
import { default as Server } from './next-server'
import * as log from '../build/output/log'
import loadConfig from './config'
import { resolve } from 'path'
import { NON_STANDARD_NODE_ENV } from '../lib/constants'
import { PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants'
import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants'
import { IncomingMessage, ServerResponse } from 'http'
import { NextUrlWithParsedQuery } from './request-meta'
import { shouldUseReactRoot } from './utils'
import {
loadRequireHook,
overrideBuiltInReactPackages,
} from '../build/webpack/require-hook'
loadRequireHook()
let ServerImpl: typeof Server
const getServerImpl = async () => {
if (ServerImpl === undefined) {
ServerImpl = (await Promise.resolve(require('./next-server'))).default
}
return ServerImpl
}
export type NextServerOptions = Partial<DevServerOptions>
export interface RequestHandler {
(
req: IncomingMessage,
res: ServerResponse,
parsedUrl?: NextUrlWithParsedQuery | undefined
): Promise<void>
}
export class NextServer {
private serverPromise?: Promise<Server>
private server?: Server
private reqHandlerPromise?: Promise<NodeRequestHandler>
private preparedAssetPrefix?: string
public options: NextServerOptions
constructor(options: NextServerOptions) {
this.options = options
}
get hostname() {
return this.options.hostname
}
get port() {
return this.options.port
}
getRequestHandler(): RequestHandler {
return async (
req: IncomingMessage,
res: ServerResponse,
parsedUrl?: UrlWithParsedQuery
) => {
const requestHandler = await this.getServerRequestHandler()
return requestHandler(req, res, parsedUrl)
}
}
getUpgradeHandler() {
return async (req: IncomingMessage, socket: any, head: any) => {
const server = await this.getServer()
// @ts-expect-error we mark this as protected so it
// causes an error here
return server.handleUpgrade.apply(server, [req, socket, head])
}
}
setAssetPrefix(assetPrefix: string) {
if (this.server) {
this.server.setAssetPrefix(assetPrefix)
} else {
this.preparedAssetPrefix = assetPrefix
}
}
logError(...args: Parameters<Server['logError']>) {
if (this.server) {
this.server.logError(...args)
}
}
async render(...args: Parameters<Server['render']>) {
const server = await this.getServer()
return server.render(...args)
}
async renderToHTML(...args: Parameters<Server['renderToHTML']>) {
const server = await this.getServer()
return server.renderToHTML(...args)
}
async renderError(...args: Parameters<Server['renderError']>) {
const server = await this.getServer()
return server.renderError(...args)
}
async renderErrorToHTML(...args: Parameters<Server['renderErrorToHTML']>) {
const server = await this.getServer()
return server.renderErrorToHTML(...args)
}
async render404(...args: Parameters<Server['render404']>) {
const server = await this.getServer()
return server.render404(...args)
}
async serveStatic(...args: Parameters<Server['serveStatic']>) {
const server = await this.getServer()
return server.serveStatic(...args)
}
async prepare() {
const server = await this.getServer()
return server.prepare()
}
async close() {
const server = await this.getServer()
return (server as any).close()
}
private async createServer(options: DevServerOptions): Promise<Server> {
if (options.dev) {
const DevServer = require('./dev/next-dev-server').default
return new DevServer(options)
}
const ServerImplementation = await getServerImpl()
return new ServerImplementation(options)
}
private async loadConfig() {
return loadConfig(
this.options.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
resolve(this.options.dir || '.'),
this.options.conf
)
}
private async getServer() {
if (!this.serverPromise) {
this.serverPromise = this.loadConfig().then(async (conf) => {
if (conf.experimental.appDir) {
process.env.NEXT_PREBUNDLED_REACT = '1'
overrideBuiltInReactPackages()
}
this.server = await this.createServer({
...this.options,
conf,
})
if (this.preparedAssetPrefix) {
this.server.setAssetPrefix(this.preparedAssetPrefix)
}
return this.server
})
}
return this.serverPromise
}
private async getServerRequestHandler() {
// Memoize request handler creation
if (!this.reqHandlerPromise) {
this.reqHandlerPromise = this.getServer().then((server) =>
server.getRequestHandler().bind(server)
)
}
return this.reqHandlerPromise
}
}
// This file is used for when users run `require('next')`
function createServer(options: NextServerOptions): NextServer {
// The package is used as a TypeScript plugin.
if (
options &&
'typescript' in options &&
'version' in (options as any).typescript
) {
return require('./next-typescript').createTSPlugin(options)
}
if (options == null) {
throw new Error(
'The server has not been instantiated properly. https://nextjs.org/docs/messages/invalid-server-options'
)
}
if (
!('isNextDevCommand' in options) &&
process.env.NODE_ENV &&
!['production', 'development', 'test'].includes(process.env.NODE_ENV)
) {
log.warn(NON_STANDARD_NODE_ENV)
}
if (options.dev && typeof options.dev !== 'boolean') {
console.warn(
"Warning: 'dev' is not a boolean which could introduce unexpected behavior. https://nextjs.org/docs/messages/invalid-server-options"
)
}
if (shouldUseReactRoot) {
;(process.env as any).__NEXT_REACT_ROOT = 'true'
}
return new NextServer(options)
}
// Support commonjs `require('next')`
module.exports = createServer
exports = module.exports
// Support `import next from 'next'`
export default createServer