From fc7036b132ae10144c81849c03532d247591602c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 18 Jun 2025 11:15:31 +0100 Subject: [PATCH 1/5] wip --- packages/nuxi/src/commands/dev.ts | 7 ++- packages/nuxi/src/dev/socket.ts | 89 +++++++++++++++++++++++++++++++ packages/nuxi/src/dev/utils.ts | 20 +++---- 3 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 packages/nuxi/src/dev/socket.ts diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 8375c0957..68712caed 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -18,6 +18,7 @@ import { isBun, isTest } from 'std-env' import { initialize } from '../dev' import { renderError } from '../dev/error' +import { isSocketURL, parseSocketURL } from '../dev/socket' import { showVersions } from '../utils/banner' import { overrideEnv } from '../utils/env' import { loadKit } from '../utils/kit' @@ -216,7 +217,8 @@ async function createDevProxy(nuxtOptions: NuxtOptions, listenOptions: Partial { @@ -224,8 +226,9 @@ async function createDevProxy(nuxtOptions: NuxtOptions, listenOptions: Partial(resolve => server.listen({ path: socketPath }, resolve)) + const url = formatSocketURL(socketPath, ssl) + return { + url, + address: { + socketPath, + address: 'localhost', + port: 3000, + }, + async close() { + try { + server.removeAllListeners() + await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve())) + } + finally { + // Clean up socket file on Unix systems + if (process.platform !== 'win32') { + try { + unlinkSync(socketPath) + } + catch { + // suppress errors + } + } + } + }, + getURLs: async () => [{ url, type: 'network' as const }], + https: false as const, + server, + } +} diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index fbbf8cee7..0a4fc61f3 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -23,6 +23,7 @@ import { loadKit } from '../utils/kit' import { loadNuxtManifest, resolveNuxtManifest, writeNuxtManifest } from '../utils/nuxt' import { renderError } from './error' +import { createSocketListener, formatSocketURL } from './socket' export type NuxtParentIPCMessage = | { type: 'nuxt:internal:dev:context', context: NuxtDevContext } @@ -71,14 +72,9 @@ export async function createNuxtDevServer(options: NuxtDevServerOptions, listenO const devServer = new NuxtDevServer(options) // Attach internal listener - devServer.listener = await listen( - devServer.handler, - listenOptions || { - port: options.port ?? 0, - hostname: '127.0.0.1', - showURL: false, - }, - ) + devServer.listener = listenOptions + ? await listen(devServer.handler, listenOptions) + : await createSocketListener(devServer.handler) // Merge interface with public context devServer.listener._url = devServer.listener.url @@ -118,7 +114,7 @@ export class NuxtDevServer extends EventEmitter { handler: RequestListener listener: Pick & { _url?: string - address: AddressInfo & { socketPath?: string } + address: { socketPath: string, port?: number, address?: string } | AddressInfo } constructor(private options: NuxtDevServerOptions) { @@ -314,8 +310,8 @@ export class NuxtDevServer extends EventEmitter { // Sync internal server info to the internals // It is important for vite-node to use the internal URL but public proto const addr = this.listener.address - this._currentNuxt.options.devServer.host = addr.address - this._currentNuxt.options.devServer.port = addr.port + this._currentNuxt.options.devServer.host = addr.address || 'localhost' + this._currentNuxt.options.devServer.port = addr.port || 0 this._currentNuxt.options.devServer.url = getAddressURL(addr, !!this.listener.https) this._currentNuxt.options.devServer.https = this.options.devContext.proxy ?.https as boolean | { key: string, cert: string } @@ -340,7 +336,7 @@ export class NuxtDevServer extends EventEmitter { }) this._handler = toNodeListener(this._currentNuxt.server.app) - this.emit('ready', `http://127.0.0.1:${addr.port}`) + this.emit('ready', 'socketPath' in addr ? formatSocketURL(addr.socketPath, !!this.listener.https) : `http://127.0.0.1:${addr.port}`) } async _watchConfig() { From 4f7e1b4c9a304cd6cf5903ebb337fb9fac773019 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 19 Jun 2025 09:52:48 +0100 Subject: [PATCH 2/5] fix: point vite-node to 'public' url --- packages/nuxi/src/dev/index.ts | 2 +- packages/nuxi/src/dev/socket.ts | 2 +- packages/nuxi/src/dev/utils.ts | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index b870989f7..ce904a538 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -42,7 +42,7 @@ class IPC { const ipc = new IPC() -export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}, listenOptions?: Partial) { +export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}, listenOptions?: true | Partial) { const devServerOverrides = resolveDevServerOverrides({ public: devContext.public, }) diff --git a/packages/nuxi/src/dev/socket.ts b/packages/nuxi/src/dev/socket.ts index 925a22cb1..36a204c9c 100644 --- a/packages/nuxi/src/dev/socket.ts +++ b/packages/nuxi/src/dev/socket.ts @@ -3,7 +3,7 @@ import { existsSync, unlinkSync } from 'node:fs' import { Server } from 'node:http' import process from 'node:process' -export function generateSocketPath(prefix: string): string { +function generateSocketPath(prefix: string): string { const timestamp = Date.now() const random = Math.random().toString(36).slice(2, 8) diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 0a4fc61f3..e1704727f 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -67,13 +67,13 @@ interface NuxtDevServerOptions { devContext: NuxtDevContext } -export async function createNuxtDevServer(options: NuxtDevServerOptions, listenOptions?: Partial) { +export async function createNuxtDevServer(options: NuxtDevServerOptions, listenOptions?: true | Partial) { // Initialize dev server const devServer = new NuxtDevServer(options) // Attach internal listener devServer.listener = listenOptions - ? await listen(devServer.handler, listenOptions) + ? await listen(devServer.handler, typeof listenOptions === 'object' ? listenOptions : { port: options.port ?? 0, hostname: '127.0.0.1', showURL: false }) : await createSocketListener(devServer.handler) // Merge interface with public context @@ -114,7 +114,7 @@ export class NuxtDevServer extends EventEmitter { handler: RequestListener listener: Pick & { _url?: string - address: { socketPath: string, port?: number, address?: string } | AddressInfo + address: { socketPath: string, port: number, address: string } | AddressInfo } constructor(private options: NuxtDevServerOptions) { @@ -310,11 +310,12 @@ export class NuxtDevServer extends EventEmitter { // Sync internal server info to the internals // It is important for vite-node to use the internal URL but public proto const addr = this.listener.address - this._currentNuxt.options.devServer.host = addr.address || 'localhost' - this._currentNuxt.options.devServer.port = addr.port || 0 - this._currentNuxt.options.devServer.url = getAddressURL(addr, !!this.listener.https) - this._currentNuxt.options.devServer.https = this.options.devContext.proxy - ?.https as boolean | { key: string, cert: string } + this._currentNuxt.options.devServer.host = addr.address + this._currentNuxt.options.devServer.port = addr.port + this._currentNuxt.options.devServer.url = 'socketPath' in addr + ? this.options.devContext.proxy?.url || getAddressURL(addr, !!this.listener.https) + : getAddressURL(addr, !!this.listener.https) + this._currentNuxt.options.devServer.https = this.options.devContext.proxy?.https as boolean | { key: string, cert: string } if (this.listener.https && !process.env.NODE_TLS_REJECT_UNAUTHORIZED) { console.warn('You might need `NODE_TLS_REJECT_UNAUTHORIZED=0` environment variable to make https work.') @@ -354,7 +355,7 @@ export class NuxtDevServer extends EventEmitter { } } -function getAddressURL(addr: AddressInfo, https: boolean) { +function getAddressURL(addr: Pick, https: boolean) { const proto = https ? 'https' : 'http' let host = addr.address.includes(':') ? `[${addr.address}]` : addr.address if (host === '[::]') { From 4742d66cebe735ab62c3bcd105ebb2427f74c312 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 1 Jul 2025 13:26:48 +0100 Subject: [PATCH 3/5] fix: only listen on socket in nuxt v4 or if `NUXT_SOCKET=1` --- packages/nuxi/src/commands/dev.ts | 4 +++- packages/nuxi/src/dev/utils.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 68712caed..c4753d2df 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -142,7 +142,9 @@ const command = defineCommand({ urls, https: devProxy.listener.https, }, - }) + // if running with nuxt v4 or `NUXT_SOCKET=1`, we use the socket listener + // otherwise pass 'true' to listen on a random port instead + }, {}, nuxtOptions._majorVersion === 4 || process.env.NUXT_SOCKET ? undefined : true) onReady(address => devProxy.setAddress(address)) diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 0d5f75a5a..935b001b7 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -74,7 +74,9 @@ export async function createNuxtDevServer(options: NuxtDevServerOptions, listenO // Attach internal listener devServer.listener = listenOptions - ? await listen(devServer.handler, typeof listenOptions === 'object' ? listenOptions : { port: options.port ?? 0, hostname: '127.0.0.1', showURL: false }) + ? await listen(devServer.handler, typeof listenOptions === 'object' + ? listenOptions + : { port: options.port ?? 0, hostname: '127.0.0.1', showURL: false }) : await createSocketListener(devServer.handler) // Merge interface with public context From 62a682069868665153770f9b53c695b160b83ff8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 1 Jul 2025 13:34:58 +0100 Subject: [PATCH 4/5] test: run benchmark with socket --- .github/workflows/bench.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index d27e4219c..7d6783b0b 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -32,6 +32,8 @@ jobs: - name: Run benchmarks uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0 + env: + NUXT_SOCKET: 1 with: run: pnpm vitest bench token: ${{ secrets.CODSPEED_TOKEN }} From 56455187135dfc27a18bc45cf4b453d61c86965f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 1 Jul 2025 14:16:12 +0100 Subject: [PATCH 5/5] fix: also respect socket option on restart --- packages/nuxi/src/commands/dev.ts | 9 ++++++--- packages/nuxi/src/dev/index.ts | 2 +- packages/nuxi/src/dev/utils.ts | 7 ++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index c4753d2df..d041352b5 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -129,6 +129,8 @@ const command = defineCommand({ // Start proxy Listener const devProxy = await createDevProxy(nuxtOptions, listenOptions) + const useSocket = nuxtOptions._majorVersion === 4 || !!process.env.NUXT_SOCKET + const urls = await devProxy.listener.getURLs() // run initially in in no-fork mode const { onRestart, onReady, close } = await initialize({ @@ -144,7 +146,7 @@ const command = defineCommand({ }, // if running with nuxt v4 or `NUXT_SOCKET=1`, we use the socket listener // otherwise pass 'true' to listen on a random port instead - }, {}, nuxtOptions._majorVersion === 4 || process.env.NUXT_SOCKET ? undefined : true) + }, {}, useSocket ? undefined : true) onReady(address => devProxy.setAddress(address)) @@ -153,7 +155,7 @@ const command = defineCommand({ onRestart(async (devServer) => { await devServer.close() const subprocess = await fork - await subprocess.initialize(devProxy) + await subprocess.initialize(devProxy, useSocket) }) return { @@ -261,12 +263,13 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo } } - async function initialize(proxy: DevProxy) { + async function initialize(proxy: DevProxy, socket: boolean) { devProxy = proxy const urls = await devProxy.listener.getURLs() await ready childProc!.send({ type: 'nuxt:internal:dev:context', + socket, context: { cwd, args, diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index ce904a538..994d3e663 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -27,7 +27,7 @@ class IPC { }) process.on('message', (message: NuxtParentIPCMessage) => { if (message.type === 'nuxt:internal:dev:context') { - initialize(message.context) + initialize(message.context, {}, message.socket ? undefined : true) } }) this.send({ type: 'nuxt:internal:dev:fork-ready' }) diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 935b001b7..98bdce37b 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -27,7 +27,7 @@ import { renderError } from './error' import { createSocketListener, formatSocketURL } from './socket' export type NuxtParentIPCMessage - = | { type: 'nuxt:internal:dev:context', context: NuxtDevContext } + = | { type: 'nuxt:internal:dev:context', context: NuxtDevContext, socket?: boolean } export type NuxtDevIPCMessage = | { type: 'nuxt:internal:dev:fork-ready' } @@ -79,6 +79,11 @@ export async function createNuxtDevServer(options: NuxtDevServerOptions, listenO : { port: options.port ?? 0, hostname: '127.0.0.1', showURL: false }) : await createSocketListener(devServer.handler) + if (process.env.DEBUG) { + // eslint-disable-next-line no-console + console.debug(`Using ${listenOptions ? 'network' : 'socket'} listener for Nuxt dev server.`) + } + // Merge interface with public context devServer.listener._url = devServer.listener.url if (options.devContext.proxy?.url) {