Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
18 changes: 13 additions & 5 deletions packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -128,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({
Expand All @@ -141,7 +144,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
}, {}, useSocket ? undefined : true)

onReady(address => devProxy.setAddress(address))

Expand All @@ -150,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 {
Expand Down Expand Up @@ -216,16 +221,18 @@ async function createDevProxy(nuxtOptions: NuxtOptions, listenOptions: Partial<L
}
return resolveLoadingMessage()
}
proxy.web(req, res, { target: address })
const target = isSocketURL(address) ? parseSocketURL(address) : address
proxy.web(req, res, { target })
}, listenOptions)

listener.server.on('upgrade', (req, socket, head) => {
if (!address) {
socket.destroy()
return
}
const target = isSocketURL(address) ? parseSocketURL(address) : address
// @ts-expect-error TODO: fix socket type in httpxy
return proxy.ws(req, socket, { target: address }, head)
return proxy.ws(req, socket, { target }, head)
})

return {
Expand Down Expand Up @@ -256,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,
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxi/src/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand All @@ -42,7 +42,7 @@ class IPC {

const ipc = new IPC()

export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}, listenOptions?: Partial<ListenOptions>) {
export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}, listenOptions?: true | Partial<ListenOptions>) {
const devServerOverrides = resolveDevServerOverrides({
public: devContext.public,
})
Expand Down
89 changes: 89 additions & 0 deletions packages/nuxi/src/dev/socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { RequestListener } from 'node:http'
import { existsSync, unlinkSync } from 'node:fs'
import { Server } from 'node:http'
import process from 'node:process'

function generateSocketPath(prefix: string): string {
const timestamp = Date.now()
const random = Math.random().toString(36).slice(2, 8)

if (process.platform === 'win32') {
// Windows named pipes
return `\\\\.\\pipe\\nuxt-${prefix}-${timestamp}-${random}`
}

// Unix domain sockets
return `/tmp/nuxt-${prefix}-${timestamp}-${random}.sock`
}

export function formatSocketURL(socketPath: string, ssl = false): string {
const protocol = ssl ? 'https' : 'http'
if (process.platform === 'win32') {
// Windows named pipes need special encoding
const encodedPath = encodeURIComponent(socketPath)
return `${protocol}+unix://${encodedPath}`
}

// Unix sockets can use the unix: protocol
return `${protocol}+unix://${socketPath.replace(/\//g, '%2F')}`
}

export function isSocketURL(url: string): boolean {
return url.startsWith('http+unix://') || url.startsWith('https+unix://')
}

export function parseSocketURL(url: string): { socketPath: string, protocol: 'https' | 'http' } {
if (!isSocketURL(url)) {
throw new Error(`Invalid socket URL: ${url}`)
}

const ssl = url.startsWith('https+unix://')
const path = url.slice(ssl ? 'https+unix://'.length : 'http+unix://'.length)
const socketPath = decodeURIComponent(path.replace(/%2F/g, '/'))

return { socketPath, protocol: ssl ? 'https' : 'http' }
}

export async function createSocketListener(handler: RequestListener, ssl = false) {
const socketPath = generateSocketPath('nuxt-dev')
const server = new Server(handler)

if (process.platform !== 'win32' && existsSync(socketPath)) {
try {
unlinkSync(socketPath)
}
catch {
// suppress errors if the socket file cannot be removed
}
}
await new Promise<void>(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<void>((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,
}
}
36 changes: 20 additions & 16 deletions packages/nuxi/src/dev/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ 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 }
= | { type: 'nuxt:internal:dev:context', context: NuxtDevContext, socket?: boolean }

export type NuxtDevIPCMessage
= | { type: 'nuxt:internal:dev:fork-ready' }
Expand Down Expand Up @@ -67,19 +68,21 @@ interface NuxtDevServerOptions {
devContext: NuxtDevContext
}

export async function createNuxtDevServer(options: NuxtDevServerOptions, listenOptions?: Partial<ListenOptions>) {
export async function createNuxtDevServer(options: NuxtDevServerOptions, listenOptions?: true | Partial<ListenOptions>) {
// Initialize dev server
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, typeof listenOptions === 'object'
? listenOptions
: { 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
Expand Down Expand Up @@ -119,7 +122,7 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
handler: RequestListener
listener: Pick<Listener, 'server' | 'getURLs' | 'https' | 'url' | 'close'> & {
_url?: string
address: AddressInfo & { socketPath?: string }
address: { socketPath: string, port: number, address: string } | AddressInfo
}

constructor(private options: NuxtDevServerOptions) {
Expand Down Expand Up @@ -317,9 +320,10 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
const addr = this.listener.address
this._currentNuxt.options.devServer.host = addr.address
this._currentNuxt.options.devServer.port = addr.port
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.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.')
Expand All @@ -343,7 +347,7 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
})

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() {
Expand All @@ -361,7 +365,7 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
}
}

function getAddressURL(addr: AddressInfo, https: boolean) {
function getAddressURL(addr: Pick<AddressInfo, 'address' | 'port'>, https: boolean) {
const proto = https ? 'https' : 'http'
let host = addr.address.includes(':') ? `[${addr.address}]` : addr.address
if (host === '[::]') {
Expand Down