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
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"fuse.js",
"giget",
"h3-next",
"http-proxy-3",
"jiti",
"nitro",
"nitropack",
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"giget": "^2.0.0",
"h3": "^1.15.4",
"h3-next": "npm:h3@^2.0.1-rc.4",
"http-proxy-3": "^1.22.0",
"jiti": "^2.6.1",
"listhen": "^1.9.0",
"magicast": "^0.3.5",
Expand All @@ -71,7 +72,6 @@
"tsdown": "^0.15.9",
"typescript": "^5.9.3",
"ufo": "^1.6.1",
"undici": "^7.16.0",
"unplugin-purge-polyfills": "^0.1.0",
"vitest": "^3.2.4",
"youch": "^4.1.0-beta.11"
Expand Down
140 changes: 58 additions & 82 deletions packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { NuxtOptions } from '@nuxt/schema'
import type { ParsedArgs } from 'citty'
import type { ProxyTargetDetailed } from 'http-proxy-3/dist/lib/http-proxy'
import type { HTTPSOptions, ListenOptions } from 'listhen'
import type { ChildProcess } from 'node:child_process'
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { TLSSocket } from 'node:tls'
import type { NuxtDevContext, NuxtDevIPCMessage } from '../dev/utils'

import { fork } from 'node:child_process'
import process from 'node:process'

import { defineCommand } from 'citty'
import { isSocketSupported } from 'get-port-please'
import { createProxyServer } from 'http-proxy-3'
import { listen } from 'listhen'
import { getArgs as getListhenArgs, parseArgs as parseListhenArgs } from 'listhen/cli'
import { resolve } from 'pathe'
Expand All @@ -17,10 +21,8 @@ import { isBun, isDeno, isTest } from 'std-env'

import { initialize } from '../dev'
import { renderError } from '../dev/error'
import { createFetchHandler } from '../dev/fetch'
import { isSocketURL, parseSocketURL } from '../dev/socket'
import { resolveLoadingTemplate } from '../dev/utils'
import { connectToChildNetwork, connectToChildSocket } from '../dev/websocket'
import { showVersionsFromConfig } from '../utils/banner'
import { overrideEnv } from '../utils/env'
import { loadKit } from '../utils/kit'
Expand Down Expand Up @@ -131,14 +133,14 @@ const command = defineCommand({
}
}

// Start listener
const devHandler = await createDevHandler(cwd, nuxtOptions, listenOptions)
// Start proxy Listener
const devProxy = await createDevProxy(cwd, nuxtOptions, listenOptions)

const nuxtSocketEnv = process.env.NUXT_SOCKET ? process.env.NUXT_SOCKET === '1' : undefined

const useSocket = nuxtSocketEnv ?? (nuxtOptions._majorVersion === 4 && await isSocketSupported())

const urls = await devHandler.listener.getURLs()
const urls = await devProxy.listener.getURLs()
// run initially in in no-fork mode
const { onRestart, onReady, close } = await initialize({
cwd,
Expand All @@ -147,16 +149,16 @@ const command = defineCommand({
public: listenOptions.public,
publicURLs: urls.map(r => r.url),
proxy: {
url: devHandler.listener.url,
url: devProxy.listener.url,
urls,
https: devHandler.listener.https,
addr: devHandler.listener.address,
https: devProxy.listener.https,
addr: devProxy.listener.address,
},
// 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 => devHandler.setAddress(address))
onReady(address => devProxy.setAddress(address))

// ... then fall back to pre-warmed fork if a hard restart is required
const fork = startSubprocess(cwd, ctx.args, ctx.rawArgs, listenOptions)
Expand All @@ -165,16 +167,16 @@ const command = defineCommand({
fork,
devServer.close().catch(() => {}),
])
await subprocess.initialize(devHandler, useSocket)
await subprocess.initialize(devProxy, useSocket)
})

return {
listener: devHandler.listener,
listener: devProxy.listener,
async close() {
await close()
const subprocess = await fork
subprocess.kill(0)
await devHandler.listener.close()
await devProxy.listener.close()
},
}
},
Expand All @@ -189,53 +191,42 @@ type ArgsT = Exclude<
undefined | ((...args: unknown[]) => unknown)
>

type DevHandler = Awaited<ReturnType<typeof createDevHandler>>
type DevProxy = Awaited<ReturnType<typeof createDevProxy>>

async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial<ListenOptions>) {
async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial<ListenOptions>) {
let loadingMessage = 'Nuxt dev server is starting...'
let error: Error | undefined
let address: string | undefined

let loadingTemplate = nuxtOptions.devServer.loadingTemplate

// Create fetch-based handler
const fetchHandler = createFetchHandler(
() => {
if (!address) {
return undefined
}

// Convert address string to DevAddress format
if (isSocketURL(address)) {
const { socketPath } = parseSocketURL(address)
return { socketPath }
}
const proxy = createProxyServer({})

// Parse network address
try {
const url = new URL(address)
return {
host: url.hostname,
port: Number.parseInt(url.port) || 80,
}
proxy.on('proxyReq', (proxyReq, req) => {
if (!proxyReq.hasHeader('x-forwarded-for')) {
const address = req.socket.remoteAddress
if (address) {
proxyReq.appendHeader('x-forwarded-for', address)
}
catch {
return undefined
}
},
// Error handler
async (req, res) => {
renderError(req, res, error)
},
// Loading handler
async (req, res) => {
if (res.headersSent) {
if (!res.writableEnded) {
res.end()
}
return
}
if (!proxyReq.hasHeader('x-forwarded-port')) {
const localPort = req?.socket?.localPort
if (localPort) {
proxyReq.setHeader('x-forwarded-port', localPort)
}
}
if (!proxyReq.hasHeader('x-forwarded-Proto')) {
const encrypted = (req?.connection as TLSSocket)?.encrypted
proxyReq.setHeader('x-forwarded-proto', encrypted ? 'https' : 'http')
}
})

const listener = await listen((req: IncomingMessage, res: ServerResponse) => {
if (error) {
renderError(req, res, error)
return
}
if (!address) {
res.statusCode = 503
res.setHeader('Content-Type', 'text/html')
res.setHeader('Cache-Control', 'no-store')
Expand All @@ -250,10 +241,10 @@ async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOpt
res.end(loadingTemplate({ loading: loadingMessage }))
}
return resolveLoadingMessage()
},
)

const listener = await listen(fetchHandler, listenOptions)
}
const target = isSocketURL(address) ? parseSocketURL(address) as ProxyTargetDetailed : address
proxy.web(req, res, { target })
}, listenOptions)

listener.server.on('upgrade', (req, socket, head) => {
if (!address) {
Expand All @@ -262,23 +253,8 @@ async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOpt
}
return
}
if (isSocketURL(address)) {
const { socketPath } = parseSocketURL(address)
connectToChildSocket(socketPath, req, socket, head)
}
else {
try {
const url = new URL(address)
const host = url.hostname
const port = Number.parseInt(url.port) || 80
connectToChildNetwork(host, port, req, socket, head)
}
catch {
if (!socket.destroyed) {
socket.end()
}
}
}
const target = isSocketURL(address) ? parseSocketURL(address) as ProxyTargetDetailed : address
return proxy.ws(req, socket, head, { target, xfwd: true })
})

return {
Expand All @@ -300,7 +276,7 @@ async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOpt

async function startSubprocess(cwd: string, args: { logLevel: string, clear: boolean, dotenv: string, envName: string, extends?: string }, rawArgs: string[], listenOptions: Partial<ListenOptions>) {
let childProc: ChildProcess | undefined
let devHandler: DevHandler
let devProxy: DevProxy
let ready: Promise<void> | undefined
const kill = (signal: NodeJS.Signals | number) => {
if (childProc) {
Expand All @@ -309,9 +285,9 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
}
}

async function initialize(handler: DevHandler, socket: boolean) {
devHandler = handler
const urls = await devHandler.listener.getURLs()
async function initialize(proxy: DevProxy, socket: boolean) {
devProxy = proxy
const urls = await devProxy.listener.getURLs()
await ready
childProc!.send({
type: 'nuxt:internal:dev:context',
Expand All @@ -323,16 +299,16 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
public: listenOptions.public,
publicURLs: urls.map(r => r.url),
proxy: {
url: devHandler.listener.url,
url: devProxy.listener.url,
urls,
https: devHandler.listener.https,
https: devProxy.listener.https,
},
} satisfies NuxtDevContext,
})
}

async function restart() {
devHandler?.clearError()
devProxy?.clearError()
if (!globalThis.__nuxt_cli__) {
return
}
Expand Down Expand Up @@ -367,19 +343,19 @@ async function startSubprocess(cwd: string, args: { logLevel: string, clear: boo
resolve()
}
else if (message.type === 'nuxt:internal:dev:ready') {
devHandler.setAddress(message.address)
devProxy.setAddress(message.address)
if (startTime) {
logger.debug(`Dev server ready for connections in ${Date.now() - startTime}ms`)
}
}
else if (message.type === 'nuxt:internal:dev:loading') {
devHandler.setAddress(undefined)
devHandler.setLoadingMessage(message.message)
devHandler.clearError()
devProxy.setAddress(undefined)
devProxy.setLoadingMessage(message.message)
devProxy.clearError()
}
else if (message.type === 'nuxt:internal:dev:loading:error') {
devHandler.setAddress(undefined)
devHandler.setError(message.error)
devProxy.setAddress(undefined)
devProxy.setError(message.error)
}
else if (message.type === 'nuxt:internal:dev:restart') {
restart()
Expand Down
Loading
Loading