diff --git a/packages/next-env/index.ts b/packages/next-env/index.ts index ef5b4dcdd6beb..ccf0d3d87896b 100644 --- a/packages/next-env/index.ts +++ b/packages/next-env/index.ts @@ -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] + } } }) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index a263c11a070b1..6e98ad4987263 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -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')` } @@ -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) || diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts index a2bd730c9b506..3de43dc9f5ec8 100644 --- a/packages/next/src/cli/next-dev.ts +++ b/packages/next/src/cli/next-dev.ts @@ -121,7 +121,7 @@ function watchConfigFiles( type StartServerWorker = Worker & Pick -async function createRouterWorker(): Promise<{ +async function createRouterWorker(fullConfig: NextConfigComplete): Promise<{ worker: StartServerWorker cleanup: () => Promise }> { @@ -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'], @@ -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.' diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 426c1772a06b5..b42707e73c699 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1040,7 +1040,6 @@ export default abstract class Server { const useInvokePath = !useMatchedPathHeader && process.env.NEXT_RUNTIME !== 'edge' && - process.env.__NEXT_PRIVATE_RENDER_WORKER && invokePath if (useInvokePath) { @@ -1131,7 +1130,6 @@ export default abstract class Server { 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) diff --git a/packages/next/src/server/lib/render-server.ts b/packages/next/src/server/lib/render-server.ts index 5e572c25145e7..9c2eee1fd2086 100644 --- a/packages/next/src/server/lib/render-server.ts +++ b/packages/next/src/server/lib/render-server.ts @@ -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 diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index 0449217a9261e..2ee071b80cff9 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -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> @@ -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) { + 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, @@ -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 @@ -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, diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index d24cfd8dbee2d..5542da43ba545 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -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' @@ -46,7 +46,7 @@ export function getResolveRoutes( ReturnType >, config: NextConfigComplete, - opts: Parameters[0], + opts: Parameters[0], renderWorkers: { app?: RenderWorker pages?: RenderWorker diff --git a/packages/next/src/server/lib/server-ipc/index.ts b/packages/next/src/server/lib/server-ipc/index.ts index aaff45457e904..8e7bd60a8855d 100644 --- a/packages/next/src/server/lib/server-ipc/index.ts +++ b/packages/next/src/server/lib/server-ipc/index.ts @@ -1,5 +1,6 @@ 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' @@ -7,7 +8,6 @@ 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 @@ -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` } : {}), @@ -135,7 +130,8 @@ export const createWorker = async ( 'clearModuleContext', 'propagateServerField', ], - }) as any as RenderWorker + }) as any as RenderWorker & + InstanceType worker.getStderr().pipe(process.stderr) worker.getStdout().pipe(process.stdout) diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 9a24ef00a6685..0f78b15d6ef68 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/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' @@ -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, diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index b5ebb31e094ad..905e8a05c8c55 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -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, @@ -1033,7 +1024,7 @@ export default class NextNodeServer extends BaseServer { return { finished: true, } - } catch (_) {} + } catch {} throw err } diff --git a/test/development/basic/node-builtins.test.ts b/test/development/basic/node-builtins.test.ts index 168549f091688..27797f6c564d0 100644 --- a/test/development/basic/node-builtins.test.ts +++ b/test/development/basic/node-builtins.test.ts @@ -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) @@ -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) diff --git a/test/integration/disable-js/test/index.test.js b/test/integration/disable-js/test/index.test.js index 98cdb734ab646..336230e50bfaf 100644 --- a/test/integration/disable-js/test/index.test.js +++ b/test/integration/disable-js/test/index.test.js @@ -3,19 +3,16 @@ 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 = {} @@ -23,17 +20,14 @@ 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, '/') diff --git a/test/integration/future/test/index.test.js b/test/integration/future/test/index.test.js index 805b1d20efff6..9258cdba2abd1 100644 --- a/test/integration/future/test/index.test.js +++ b/test/integration/future/test/index.test.js @@ -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, '/') diff --git a/test/integration/production-build-dir/test/index.test.js b/test/integration/production-build-dir/test/index.test.js index 9e08798bb502a..d017a3974287c 100644 --- a/test/integration/production-build-dir/test/index.test.js +++ b/test/integration/production-build-dir/test/index.test.js @@ -2,10 +2,10 @@ import { join } from 'path' import { - nextServer, runNextCommand, - startApp, - stopApp, + nextStart, + killApp, + findPort, renderViaHTTP, } from 'next-test-utils' @@ -19,19 +19,13 @@ describe('Production Custom Build Directory', () => { }) expect(result.stderr).toBe('') - const app = nextServer({ - dir: join(__dirname, '../build'), - dev: false, - quiet: true, - }) - - const server = await startApp(app) - const appPort = server.address().port + const appPort = await findPort() + const app = await nextStart(join(__dirname, '../build'), appPort) const html = await renderViaHTTP(appPort, '/') expect(html).toMatch(/Hello World/) - await stopApp(server) + await killApp(app) }) }) })