Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge app renderer process #54143

Merged
merged 23 commits into from Aug 22, 2023
8 changes: 6 additions & 2 deletions packages/next-env/index.ts
Expand Up @@ -26,8 +26,12 @@ type Log = {

function replaceProcessEnv(sourceEnv: Env) {
Object.keys(process.env).forEach((key) => {
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
delete process.env[key]
// Allow mutating internal Next.js env variables after the server has initiated.
// This is necessary for dynamic things like the IPC server port.
if (!key.startsWith('__NEXT_PRIVATE')) {
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
delete process.env[key]
}
}
})

Expand Down
10 changes: 6 additions & 4 deletions packages/next/src/build/utils.ts
Expand Up @@ -1935,14 +1935,12 @@ export async function copyTracedFiles(
serverOutputPath,
`${
moduleType
? `import http from 'http'
import path from 'path'
? `import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
import { startServer } from 'next/dist/server/lib/start-server.js'
`
: `
const http = require('http')
const path = require('path')
const { startServer } = require('next/dist/server/lib/start-server')`
}
Expand All @@ -1961,13 +1959,17 @@ if (!process.env.NEXT_MANUAL_SIG_HANDLE) {

const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || 'localhost'
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);

let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = ${JSON.stringify({
...serverConfig,
distDir: `./${path.relative(dir, distDir)}`,
})}

process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = nextConfig.experimental && nextConfig.experimental.serverActions
? 'experimental'
: 'next'

if (
Number.isNaN(keepAliveTimeout) ||
Expand Down
7 changes: 5 additions & 2 deletions packages/next/src/cli/next-dev.ts
Expand Up @@ -121,7 +121,7 @@ function watchConfigFiles(
type StartServerWorker = Worker &
Pick<typeof import('../server/lib/start-server'), 'startServer'>

async function createRouterWorker(): Promise<{
async function createRouterWorker(fullConfig: NextConfigComplete): Promise<{
worker: StartServerWorker
cleanup: () => Promise<void>
}> {
Expand All @@ -143,6 +143,9 @@ async function createRouterWorker(): Promise<{
: {}),
WATCHPACK_WATCHER_LIMIT: '20',
EXPERIMENTAL_TURBOPACK: process.env.EXPERIMENTAL_TURBOPACK,
__NEXT_PRIVATE_PREBUNDLED_REACT: !!fullConfig.experimental.serverActions
? 'experimental'
: 'next',
},
},
exposedMethods: ['startServer'],
Expand Down Expand Up @@ -394,7 +397,7 @@ const nextDev: CliCommand = async (argv) => {
} else {
const runDevServer = async (reboot: boolean) => {
try {
const workerInit = await createRouterWorker()
const workerInit = await createRouterWorker(config)
if (!!args['--experimental-https']) {
Log.warn(
'Self-signed certificates are currently an experimental feature, use at your own risk.'
Expand Down
2 changes: 0 additions & 2 deletions packages/next/src/server/base-server.ts
Expand Up @@ -1040,7 +1040,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const useInvokePath =
!useMatchedPathHeader &&
process.env.NEXT_RUNTIME !== 'edge' &&
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
invokePath

if (useInvokePath) {
Expand Down Expand Up @@ -1131,7 +1130,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {

if (
process.env.NEXT_RUNTIME !== 'edge' &&
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
req.headers['x-middleware-invoke']
) {
const nextDataResult = await this.normalizeNextData(req, res, parsedUrl)
Expand Down
6 changes: 4 additions & 2 deletions packages/next/src/server/lib/render-server.ts
Expand Up @@ -80,8 +80,10 @@ export async function initialize(opts: {
return result
}

const type = process.env.__NEXT_PRIVATE_RENDER_WORKER!
process.title = 'next-render-worker-' + type
const type = process.env.__NEXT_PRIVATE_RENDER_WORKER
if (type) {
process.title = 'next-render-worker-' + type
}

let requestHandler: RequestHandler
let upgradeHandler: any
Expand Down
50 changes: 29 additions & 21 deletions packages/next/src/server/lib/router-server.ts
Expand Up @@ -38,15 +38,14 @@ import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-requ

const debug = setupDebug('next:router-server:main')

export type RenderWorker = InstanceType<
typeof import('next/dist/compiled/jest-worker').Worker
> & {
initialize: typeof import('./render-server').initialize
deleteCache: typeof import('./render-server').deleteCache
deleteAppClientCache: typeof import('./render-server').deleteAppClientCache
clearModuleContext: typeof import('./render-server').clearModuleContext
propagateServerField: typeof import('./render-server').propagateServerField
}
export type RenderWorker = Pick<
typeof import('./render-server'),
| 'initialize'
| 'deleteCache'
| 'clearModuleContext'
| 'deleteAppClientCache'
| 'propagateServerField'
>

export interface RenderWorkers {
app?: Awaited<ReturnType<typeof createWorker>>
Expand Down Expand Up @@ -185,16 +184,19 @@ export async function initialize(opts: {
},
} as any)

const { initialEnv } = require('@next/env') as typeof import('@next/env')
if (!!config.experimental.appDir) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config no longer exists so let's remove it.

process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT = ipcPort + ''
process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY = ipcValidationKey
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = config.experimental
.serverActions
? 'experimental'
: 'next'

renderWorkers.app = await createWorker(
ipcPort,
ipcValidationKey,
opts.isNodeDebugging,
'app',
config,
initialEnv
)
renderWorkers.app =
require('./render-server') as typeof import('./render-server')
}

const { initialEnv } = require('@next/env') as typeof import('@next/env')
renderWorkers.pages = await createWorker(
ipcPort,
ipcValidationKey,
Expand Down Expand Up @@ -272,10 +274,16 @@ export async function initialize(opts: {
}
}

const logError = async (
type: 'uncaughtException' | 'unhandledRejection',
err: Error | undefined
) => {
await devInstance?.logErrorWithOriginalStack(err, type)
}

const cleanup = () => {
debug('router-server process cleanup')
for (const curWorker of [
...((renderWorkers.app as any)?._workerPool?._workers || []),
...((renderWorkers.pages as any)?._workerPool?._workers || []),
] as {
_child?: import('child_process').ChildProcess
Expand All @@ -290,8 +298,8 @@ export async function initialize(opts: {
process.on('exit', cleanup)
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
process.on('uncaughtException', cleanup)
process.on('unhandledRejection', cleanup)
process.on('uncaughtException', logError.bind(null, 'uncaughtException'))
process.on('unhandledRejection', logError.bind(null, 'unhandledRejection'))

const resolveRoutes = getResolveRoutes(
fsChecker,
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/lib/router-utils/resolve-routes.ts
Expand Up @@ -2,10 +2,10 @@ import type { TLSSocket } from 'tls'
import type { FsOutput } from './filesystem'
import type { IncomingMessage } from 'http'
import type { NextConfigComplete } from '../../config-shared'
import type { RenderWorker, initialize } from '../router-server'

import url from 'url'
import { Redirect } from '../../../../types'
import { RenderWorker } from '../router-server'
import setupDebug from 'next/dist/compiled/debug'
import { getCloneableBody } from '../../body-streams'
import { filterReqHeaders, ipcForbiddenHeaders } from '../server-ipc/utils'
Expand Down Expand Up @@ -46,7 +46,7 @@ export function getResolveRoutes(
ReturnType<typeof import('./filesystem').setupFsCheck>
>,
config: NextConfigComplete,
opts: Parameters<typeof import('../router-server').initialize>[0],
opts: Parameters<typeof initialize>[0],
renderWorkers: {
app?: RenderWorker
pages?: RenderWorker
Expand Down
14 changes: 5 additions & 9 deletions packages/next/src/server/lib/server-ipc/index.ts
@@ -1,13 +1,13 @@
import type NextServer from '../../next-server'
import type { NextConfigComplete } from '../../config-shared'
import type { RenderWorker } from '../router-server'

import { getNodeOptionsWithoutInspect } from '../utils'
import { errorToJSON } from '../../render'
import crypto from 'crypto'
import isError from '../../../lib/is-error'
import { genRenderExecArgv } from '../worker-utils'
import { deserializeErr } from './request-utils'
import { RenderWorker } from '../router-server'
import type { Env } from '@next/env'

// we can't use process.send as jest-worker relies on
Expand Down Expand Up @@ -115,13 +115,8 @@ export const createWorker = async (
__NEXT_PRIVATE_STANDALONE_CONFIG:
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG,
NODE_ENV: process.env.NODE_ENV,
...(type === 'app'
? {
__NEXT_PRIVATE_PREBUNDLED_REACT: useServerActions
? 'experimental'
: 'next',
}
: {}),
__NEXT_PRIVATE_PREBUNDLED_REACT:
type === 'app' ? (useServerActions ? 'experimental' : 'next') : '',
...(process.env.NEXT_CPU_PROF
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.${type}-renderer` }
: {}),
Expand All @@ -135,7 +130,8 @@ export const createWorker = async (
'clearModuleContext',
'propagateServerField',
],
}) as any as RenderWorker
}) as any as RenderWorker &
InstanceType<typeof import('next/dist/compiled/jest-worker').Worker>

worker.getStderr().pipe(process.stderr)
worker.getStdout().pipe(process.stdout)
Expand Down
9 changes: 7 additions & 2 deletions packages/next/src/server/lib/start-server.ts
@@ -1,4 +1,5 @@
import '../node-polyfill-fetch'
import '../require-hook'

import type { IncomingMessage, ServerResponse } from 'http'

Expand Down Expand Up @@ -220,11 +221,15 @@ export async function startServer({
server.close()
process.exit(0)
}
const exception = (err: Error) => {
// This is the render worker, we keep the process alive
console.error(err)
}
process.on('exit', cleanup)
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
process.on('uncaughtException', cleanup)
process.on('unhandledRejection', cleanup)
process.on('uncaughtException', exception)
process.on('unhandledRejection', exception)

const initResult = await getRequestHandlers({
dir,
Expand Down
11 changes: 1 addition & 10 deletions packages/next/src/server/next-server.ts
Expand Up @@ -491,15 +491,6 @@ export default class NextNodeServer extends BaseServer {
)
}

private streamResponseChunk(res: ServerResponse, chunk: any) {
res.write(chunk)
// When streaming is enabled, we need to explicitly
// flush the response to avoid it being buffered.
if ('flush' in res) {
;(res as any).flush()
}
}

protected async imageOptimizer(
req: NodeNextRequest,
res: NodeNextResponse,
Expand Down Expand Up @@ -1033,7 +1024,7 @@ export default class NextNodeServer extends BaseServer {
return {
finished: true,
}
} catch (_) {}
} catch {}

throw err
}
Expand Down
2 changes: 0 additions & 2 deletions test/development/basic/node-builtins.test.ts
Expand Up @@ -138,7 +138,6 @@ createNextDescribe(
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-render-worker-app')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
Expand All @@ -164,7 +163,6 @@ createNextDescribe(
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-render-worker-app')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
Expand Down
18 changes: 6 additions & 12 deletions test/integration/disable-js/test/index.test.js
Expand Up @@ -3,37 +3,31 @@
import { join } from 'path'
import cheerio from 'cheerio'
import {
nextServer,
nextBuild,
startApp,
stopApp,
renderViaHTTP,
findPort,
launchApp,
killApp,
nextStart,
} from 'next-test-utils'

const appDir = join(__dirname, '../')
let appPort
let server
let app

const context = {}

describe('disabled runtime JS', () => {
describe('production mode', () => {
beforeAll(async () => {
appPort = await findPort()

await nextBuild(appDir)
app = nextServer({
dir: join(__dirname, '../'),
dev: false,
quiet: true,
})
app = await nextStart(appDir, appPort)

server = await startApp(app)
context.appPort = appPort = server.address().port
context.appPort = appPort
})
afterAll(() => stopApp(server))
afterAll(() => killApp(app))

it('should render the page', async () => {
const html = await renderViaHTTP(appPort, '/')
Expand Down
20 changes: 7 additions & 13 deletions test/integration/future/test/index.test.js
Expand Up @@ -4,30 +4,24 @@ import webdriver from 'next-webdriver'
import { join } from 'path'
import {
nextBuild,
nextServer,
startApp,
stopApp,
nextStart,
killApp,
findPort,
renderViaHTTP,
} from 'next-test-utils'

let appDir = join(__dirname, '../')
let server
let appPort
let app

describe('excludeDefaultMomentLocales', () => {
beforeAll(async () => {
appPort = await findPort()
await nextBuild(appDir)
const app = nextServer({
dir: appDir,
dev: false,
quiet: true,
})
server = await startApp(app)
appPort = server.address().port
// wait for it to start up:
app = await nextStart(appDir, appPort)
await renderViaHTTP(appPort, '/')
})
afterAll(() => stopApp(server))
afterAll(() => killApp(app))

it('should load momentjs', async () => {
const browser = await webdriver(appPort, '/')
Expand Down