From 124683e384bacc4b0ebf7ffdb205df41bc9841d7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 27 Oct 2025 21:10:52 +0100 Subject: [PATCH 1/3] fix(dev): use proxy instead of web fetch handler This reverts commit bc4b293def02709b5d181e3d2089d7fc0abdd1ad. --- packages/nuxi/package.json | 2 +- packages/nuxi/src/commands/dev.ts | 140 ++++----- packages/nuxi/src/dev/fetch.ts | 304 -------------------- packages/nuxi/src/dev/websocket.ts | 153 ---------- packages/nuxt-cli/package.json | 3 +- packages/nuxt-cli/test/e2e/runtimes.spec.ts | 2 +- pnpm-lock.yaml | 303 ++----------------- 7 files changed, 89 insertions(+), 818 deletions(-) delete mode 100644 packages/nuxi/src/dev/fetch.ts delete mode 100644 packages/nuxi/src/dev/websocket.ts diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index 717218788..a087204a3 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -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", @@ -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" diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 07675bd8a..e96c95f58 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -1,7 +1,10 @@ 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' @@ -9,6 +12,7 @@ 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' @@ -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' @@ -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, @@ -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) @@ -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() }, } }, @@ -189,53 +191,42 @@ type ArgsT = Exclude< undefined | ((...args: unknown[]) => unknown) > -type DevHandler = Awaited> +type DevProxy = Awaited> -async function createDevHandler(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial) { +async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptions: Partial) { 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') @@ -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) { @@ -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 { @@ -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) { let childProc: ChildProcess | undefined - let devHandler: DevHandler + let devProxy: DevProxy let ready: Promise | undefined const kill = (signal: NodeJS.Signals | number) => { if (childProc) { @@ -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', @@ -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 } @@ -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() diff --git a/packages/nuxi/src/dev/fetch.ts b/packages/nuxi/src/dev/fetch.ts deleted file mode 100644 index ad06005f4..000000000 --- a/packages/nuxi/src/dev/fetch.ts +++ /dev/null @@ -1,304 +0,0 @@ -import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http' -import { request as httpRequest } from 'node:http' -import { Readable } from 'node:stream' -import { pipeline } from 'node:stream/promises' -import { NodeRequest } from 'srvx/node' -import { isWindows } from 'std-env' -import { Agent } from 'undici' - -interface DevAddress { - socketPath?: string - host?: string - port?: number -} - -/** - * Create fetch options for socket-based communication - * Based on Nitro's fetchSocketOptions implementation - */ -function fetchSocketOptions(socketPath: string) { - if ('Bun' in globalThis) { - // https://bun.sh/guides/http/fetch-unix - return { unix: socketPath } - } - if ('Deno' in globalThis) { - // https://github.com/denoland/deno/pull/29154 - return { - client: (globalThis as any).Deno.createHttpClient({ - transport: 'unix', - path: socketPath, - }), - } - } - // https://github.com/nodejs/undici/issues/2970 - return { - dispatcher: new Agent({ connect: { socketPath } }), - } -} - -interface NodeHttpResponse { - status: number - statusText: string - headers: Headers - body: Readable -} -/** - * fetch using native Node.js http.request for Windows named pipes - * this bypasses undici's Web Streams which have buffering issues on Windows - */ -function fetchWithNodeHttp(socketPath: string, url: URL, init?: RequestInit & { duplex?: string }): Promise { - return new Promise((resolve, reject) => { - const headers: Record = {} - if (init?.headers) { - if (init.headers instanceof Headers) { - for (const [key, value] of init.headers.entries()) { - headers[key] = value - } - } - else if (Array.isArray(init.headers)) { - for (const [key, value] of init.headers) { - headers[key] = value - } - } - else { - Object.assign(headers, init.headers) - } - } - - const req = httpRequest({ - socketPath, - path: url.pathname + url.search, - method: init?.method || 'GET', - headers, - }, (res) => { - const responseHeaders = new Headers() - for (const [key, value] of Object.entries(res.headers)) { - if (value !== undefined) { - if (key.toLowerCase() === 'set-cookie') { - // `set-cookie` must never be joined! (RFC 6265) - if (Array.isArray(value)) { - for (const cookie of value) { - responseHeaders.append('set-cookie', cookie) - } - } - else { - responseHeaders.append('set-cookie', value) - } - } - else { - // Other headers can be joined with comma (per HTTP spec) - responseHeaders.set(key, Array.isArray(value) ? value.join(', ') : value) - } - } - } - - res.on('error', (err) => { - // Only reject if we haven't resolved yet - if (err && err.message && !err.message.includes('aborted')) { - reject(err) - } - }) - - resolve({ - status: res.statusCode || 200, - statusText: res.statusMessage || 'OK', - headers: responseHeaders, - body: res, - }) - }) - - req.on('error', reject) - - if (init?.body) { - if (typeof init.body === 'string') { - req.write(init.body) - } - else if (init.body instanceof ReadableStream) { - const reader = init.body.getReader() - const pump = async () => { - try { - while (true) { - const { done, value } = await reader.read() - if (done) { - break - } - req.write(value) - } - req.end() - } - catch (err) { - req.destroy(err as Error) - } - } - pump() - return - } - } - - req.end() - }) -} - -/** - * Fetch to a specific address (socket or network) - * Based on Nitro's fetchAddress implementation - */ -function fetchAddress( - addr: DevAddress, - input: string | URL | Request, - inputInit?: RequestInit, -): Promise { - let url: URL - let init: (RequestInit & { duplex?: string }) | undefined - - if (input instanceof Request) { - url = new URL(input.url) - init = { - method: input.method, - headers: input.headers, - body: input.body, - ...inputInit, - } - } - else { - url = new URL(input) - init = inputInit - } - - init = { - duplex: 'half', - redirect: 'manual', - ...init, - } - - if (addr.socketPath && isWindows) { - url.protocol = 'http:' - return fetchWithNodeHttp(addr.socketPath, url, init) - } - - if (addr.socketPath) { - url.protocol = 'http:' - return fetch(url, { - ...init, - ...fetchSocketOptions(addr.socketPath), - }) - } - - const origin = `http://${addr.host}${addr.port ? `:${addr.port}` : ''}` - const outURL = new URL(url.pathname + url.search, origin) - return fetch(outURL, init) -} - -/** - * Send Web API Response to Node.js ServerResponse - */ -async function sendWebResponse(res: ServerResponse, webResponse: Response | NodeHttpResponse): Promise { - // Set status - res.statusCode = webResponse.status - res.statusMessage = webResponse.statusText - - // Special handling for set-cookie header - const setCookies = webResponse.headers.getSetCookie?.() - if (setCookies && setCookies.length > 0) { - // Use appendHeader to add multiple set-cookie headers - for (const cookie of setCookies) { - res.appendHeader('set-cookie', cookie) - } - } - - // Set all other headers (skip set-cookie as it's handled above) - for (const [key, value] of webResponse.headers.entries()) { - if (key.toLowerCase() !== 'set-cookie') { - res.setHeader(key, value) - } - } - - // Stream body - if (webResponse.body) { - // handle node readable stream (from Windows named pipe fetch) - if (webResponse.body instanceof Readable) { - try { - await pipeline(webResponse.body, res, { end: true }) - } - catch (error) { - if (!res.writableEnded) { - res.end() - } - throw error - } - return - } - - const reader = webResponse.body.getReader() - try { - while (true) { - const { done, value } = await reader.read() - if (done) { - break - } - // backpressure - if (!res.write(value)) { - await new Promise(resolve => res.once('drain', resolve)) - } - } - } - catch (error) { - // If streaming fails, clean up and end the response - reader.releaseLock() - if (!res.writableEnded) { - res.end() - } - throw error - } - finally { - reader.releaseLock() - } - } - - res.end() -} - -export function createFetchHandler( - getAddress: () => DevAddress | undefined, - onError: (req: IncomingMessage, res: ServerResponse) => void | Promise, - onLoading: (req: IncomingMessage, res: ServerResponse) => void | Promise, -): RequestListener { - return async (req: IncomingMessage, res: ServerResponse) => { - try { - const address = getAddress() - - if (!address) { - await onLoading(req, res) - return - } - - const isWebSocketUpgrade = req.headers.upgrade?.toLowerCase() === 'websocket' - if (isWebSocketUpgrade) { - res.statusCode = 426 - res.setHeader('Connection', 'close') - res.end('Upgrade Required') - return - } - - const webRequest = new NodeRequest({ req, res }) - const webResponse = await fetchAddress(address, webRequest) - await sendWebResponse(res, webResponse) - } - catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - const isWebSocketError = errorMessage.toLowerCase().includes('websocket') - || errorMessage.toLowerCase().includes('upgrade') - - if (!isWebSocketError) { - console.error('Fetch handler error:', error) - } - - if (!res.headersSent) { - await onError(req, res) - } - else if (!res.writableEnded) { - res.end() - } - } - } -} diff --git a/packages/nuxi/src/dev/websocket.ts b/packages/nuxi/src/dev/websocket.ts deleted file mode 100644 index 6340f9141..000000000 --- a/packages/nuxi/src/dev/websocket.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type { Buffer } from 'node:buffer' -import type { IncomingMessage } from 'node:http' -import type { Duplex } from 'node:stream' -import { connect } from 'node:net' - -export function connectToChildSocket( - socketPath: string, - req: IncomingMessage, - clientSocket: Duplex, - head: Buffer, -): void { - const childSocket = connect(socketPath) - - let isConnected = false - - childSocket.on('error', (error) => { - const errorMessage = error.message || '' - if (!errorMessage.includes('ECONNRESET') && !errorMessage.includes('EPIPE') && !errorMessage.includes('premature close')) { - console.error('Child socket connection error:', error) - } - if (!clientSocket.destroyed) { - clientSocket.destroy() - } - }) - - clientSocket.on('error', (error) => { - const errorMessage = error.message || '' - if (!errorMessage.includes('ECONNRESET') && !errorMessage.includes('EPIPE') && !isConnected) { - console.error('Client socket error:', error) - } - if (!childSocket.destroyed) { - childSocket.destroy() - } - }) - - childSocket.on('connect', () => { - isConnected = true - // Forward the HTTP upgrade request - const requestLine = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n` - const headers = Object.entries(req.headers) - .map(([key, value]) => { - if (Array.isArray(value)) { - return value.map(v => `${key}: ${v}`).join('\r\n') - } - return `${key}: ${value}` - }) - .join('\r\n') - - const httpRequest = `${requestLine}${headers}\r\n\r\n` - - // Send HTTP upgrade request - childSocket.write(httpRequest) - - // Send any buffered data (head) - if (head && head.length > 0) { - childSocket.write(head) - } - - // Pipe data bidirectionally - clientSocket.pipe(childSocket) - childSocket.pipe(clientSocket) - }) - - // Clean up on close - const cleanup = () => { - if (!clientSocket.destroyed) { - clientSocket.destroy() - } - if (!childSocket.destroyed) { - childSocket.destroy() - } - } - - clientSocket.on('close', cleanup) - childSocket.on('close', cleanup) -} - -/** - * Connect to child process via network address for WebSocket upgrades - * Fallback for non-socket addresses - */ -export function connectToChildNetwork( - host: string, - port: number, - req: IncomingMessage, - clientSocket: Duplex, - head: Buffer, -): void { - const childSocket = connect(port, host) - - let isConnected = false - - childSocket.on('error', (error) => { - const errorMessage = error.message || '' - if (!errorMessage.includes('ECONNRESET') && !errorMessage.includes('EPIPE') && !errorMessage.includes('premature close')) { - console.error('Child network connection error:', error) - } - if (!clientSocket.destroyed) { - clientSocket.destroy() - } - }) - - clientSocket.on('error', (error) => { - const errorMessage = error.message || '' - if (!errorMessage.includes('ECONNRESET') && !errorMessage.includes('EPIPE') && !isConnected) { - console.error('Client socket error:', error) - } - if (!childSocket.destroyed) { - childSocket.destroy() - } - }) - - childSocket.on('connect', () => { - isConnected = true - // Forward the HTTP upgrade request - const requestLine = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n` - const headers = Object.entries(req.headers) - .map(([key, value]) => { - if (Array.isArray(value)) { - return value.map(v => `${key}: ${v}`).join('\r\n') - } - return `${key}: ${value}` - }) - .join('\r\n') - - const httpRequest = `${requestLine}${headers}\r\n\r\n` - - // Send HTTP upgrade request - childSocket.write(httpRequest) - - // Send any buffered data (head) - if (head && head.length > 0) { - childSocket.write(head) - } - - // Pipe data bidirectionally - clientSocket.pipe(childSocket) - childSocket.pipe(clientSocket) - }) - - // Clean up on close - const cleanup = () => { - if (!clientSocket.destroyed) { - clientSocket.destroy() - } - if (!childSocket.destroyed) { - childSocket.destroy() - } - } - - clientSocket.on('close', cleanup) - childSocket.on('close', cleanup) -} diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index 140aa77d9..87c247d3f 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -43,6 +43,7 @@ "fuse.js": "^7.1.0", "get-port-please": "^3.2.0", "giget": "^2.0.0", + "http-proxy-3": "^1.22.0", "jiti": "^2.6.1", "listhen": "^1.9.0", "nypm": "^0.6.2", @@ -57,7 +58,6 @@ "std-env": "^3.10.0", "tinyexec": "^1.0.1", "ufo": "^1.6.1", - "undici": "^7.16.0", "youch": "^4.1.0-beta.11" }, "devDependencies": { @@ -72,6 +72,7 @@ "rollup-plugin-visualizer": "^6.0.5", "tsdown": "^0.15.9", "typescript": "^5.9.3", + "undici": "^7.16.0", "unplugin-purge-polyfills": "^0.1.0", "vitest": "^3.2.4", "youch": "^4.1.0-beta.11" diff --git a/packages/nuxt-cli/test/e2e/runtimes.spec.ts b/packages/nuxt-cli/test/e2e/runtimes.spec.ts index e743d7d41..0dbdb1896 100644 --- a/packages/nuxt-cli/test/e2e/runtimes.spec.ts +++ b/packages/nuxt-cli/test/e2e/runtimes.spec.ts @@ -49,7 +49,7 @@ function createIt(runtimeName: typeof runtimes[number], _socketsEnabled: boolean deno: { start: !platform.windows, fetching: !platform.windows, - websockets: !platform.windows && !platform.macos, + websockets: !platform.windows, }, } const status = supportMatrix[runtimeName] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8171022e4..27942a185 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,9 @@ importers: h3-next: specifier: npm:h3@^2.0.1-rc.4 version: h3@2.0.1-rc.4(crossws@0.4.1(srvx@0.8.16)) + http-proxy-3: + specifier: ^1.22.0 + version: 1.22.0 jiti: specifier: ^2.6.1 version: 2.6.1 @@ -172,10 +175,10 @@ importers: version: 0.3.5 nitro: specifier: ^3.0.1-alpha.0 - version: 3.0.1-alpha.0(chokidar@4.0.3)(ioredis@5.8.1)(rolldown@1.0.0-beta.44)(vite@7.1.9(@types/node@22.18.12)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)) + version: 3.0.1-alpha.0(chokidar@4.0.3)(ioredis@5.8.2)(rolldown@1.0.0-beta.44)(vite@7.1.9(@types/node@22.18.12)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)) nitropack: specifier: latest - version: 2.12.7(rolldown@1.0.0-beta.44) + version: 2.12.8(rolldown@1.0.0-beta.44) nypm: specifier: ^0.6.2 version: 0.6.2 @@ -224,9 +227,6 @@ importers: ufo: specifier: ^1.6.1 version: 1.6.1 - undici: - specifier: ^7.16.0 - version: 7.16.0 unplugin-purge-polyfills: specifier: ^0.1.0 version: 0.1.0 @@ -269,6 +269,9 @@ importers: giget: specifier: ^2.0.0 version: 2.0.0 + http-proxy-3: + specifier: ^1.22.0 + version: 1.22.0 jiti: specifier: ^2.6.1 version: 2.6.1 @@ -311,9 +314,6 @@ importers: ufo: specifier: ^1.6.1 version: 1.6.1 - undici: - specifier: ^7.16.0 - version: 7.16.0 youch: specifier: ^4.1.0-beta.11 version: 4.1.0-beta.11 @@ -351,6 +351,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + undici: + specifier: ^7.16.0 + version: 7.16.0 unplugin-purge-polyfills: specifier: ^0.1.0 version: 0.1.0 @@ -1808,15 +1811,6 @@ packages: rollup: optional: true - '@rollup/plugin-commonjs@28.0.6': - resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/plugin-commonjs@28.0.9': resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -1844,15 +1838,6 @@ packages: rollup: optional: true - '@rollup/plugin-node-resolve@16.0.2': - resolution: {integrity: sha512-tCtHJ2BlhSoK4cCs25NMXfV7EALKr0jyasmqVCq3y9cBrKdmJhtsy1iTz36Xhk/O+pDJbzawxF4K6ZblqCnITQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/plugin-node-resolve@16.0.3': resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} engines: {node: '>=14.0.0'} @@ -2222,11 +2207,6 @@ packages: cpu: [x64] os: [win32] - '@vercel/nft@0.30.2': - resolution: {integrity: sha512-pquXF3XZFg/T3TBor08rUhIGgOhdSilbn7WQLVP/aVSSO+25Rs4H/m3nxNDQ2x3znX7Z3yYjryN8xaLwypcwQg==} - engines: {node: '>=18'} - hasBin: true - '@vercel/nft@0.30.3': resolution: {integrity: sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w==} engines: {node: '>=18'} @@ -3532,6 +3512,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-proxy-3@1.22.0: + resolution: {integrity: sha512-qyYYKjmPW7kDiRBGzydmD5f5ckuniL9fY45EWP05YVDoR/02JjrVMGqdrO5O+OURU7imhF3WyiKwp++4A3KEbw==} + engines: {node: '>=18'} + http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -3587,10 +3571,6 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ioredis@5.8.1: - resolution: {integrity: sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==} - engines: {node: '>=12.22.0'} - ioredis@5.8.2: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} @@ -4139,16 +4119,6 @@ packages: xml2js: optional: true - nitropack@2.12.7: - resolution: {integrity: sha512-HWyzMBj2d8b14J6Cfnxv97ztnuHIgXNcrGiWCruLfb2ZfKsp6OCbZYJm5T9sv/ZKl8LedhatrMKG66HWJux9Rg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - xml2js: ^0.6.2 - peerDependenciesMeta: - xml2js: - optional: true - nitropack@2.12.8: resolution: {integrity: sha512-k4KT/6CMiX+aAI2LWEdVhvI4PPPWt6NTz70TcxrGUgvMpt8Pv4/iG0KTwBJ58KdwFp59p3Mlp8QyGVmIVP6GvQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7044,18 +7014,6 @@ snapshots: optionalDependencies: rollup: 4.52.5 - '@rollup/plugin-commonjs@28.0.6(rollup@4.52.5)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.3) - is-reference: 1.2.1 - magic-string: 0.30.19 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.52.5 - '@rollup/plugin-commonjs@28.0.9(rollup@4.52.5)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.52.5) @@ -7082,16 +7040,6 @@ snapshots: optionalDependencies: rollup: 4.52.5 - '@rollup/plugin-node-resolve@16.0.2(rollup@4.52.5)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.10 - optionalDependencies: - rollup: 4.52.5 - '@rollup/plugin-node-resolve@16.0.3(rollup@4.52.5)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.52.5) @@ -7408,25 +7356,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/nft@0.30.2(rollup@4.52.5)': - dependencies: - '@mapbox/node-pre-gyp': 2.0.0 - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) - async-sema: 3.1.1 - bindings: 1.5.0 - estree-walker: 2.0.2 - glob: 10.4.5 - graceful-fs: 4.2.11 - node-gyp-build: 4.8.4 - picomatch: 4.0.3 - resolve-from: 5.0.0 - transitivePeerDependencies: - - encoding - - rollup - - supports-color - '@vercel/nft@0.30.3(rollup@4.52.5)': dependencies: '@mapbox/node-pre-gyp': 2.0.0 @@ -7781,7 +7710,7 @@ snapshots: axios@1.12.2: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.4.3) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -8718,7 +8647,9 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.11: {} + follow-redirects@1.15.11(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 foreground-child@3.3.1: dependencies: @@ -8893,6 +8824,13 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-proxy-3@1.22.0: + dependencies: + debug: 4.4.3 + follow-redirects: 1.15.11(debug@4.4.3) + transitivePeerDependencies: + - supports-color + http-shutdown@1.2.2: {} https-proxy-agent@7.0.6: @@ -8939,20 +8877,6 @@ snapshots: ini@4.1.1: {} - ioredis@5.8.1: - dependencies: - '@ioredis/commands': 1.4.0 - cluster-key-slot: 1.1.2 - debug: 4.4.3 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - ioredis@5.8.2: dependencies: '@ioredis/commands': 1.4.0 @@ -9633,57 +9557,6 @@ snapshots: nf3@0.1.1: {} - nitro@3.0.1-alpha.0(chokidar@4.0.3)(ioredis@5.8.1)(rolldown@1.0.0-beta.44)(vite@7.1.9(@types/node@22.18.12)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)): - dependencies: - consola: 3.4.2 - cookie-es: 2.0.0 - crossws: 0.4.1(srvx@0.8.16) - db0: 0.3.4 - esbuild: 0.25.11 - fetchdts: 0.1.7 - h3: 1.15.4 - jiti: 2.6.1 - nf3: 0.1.1 - ofetch: 1.4.1 - ohash: 2.0.11 - rendu: 0.0.6 - rollup: 4.52.5 - srvx: 0.8.16 - undici: 7.16.0 - unenv: 2.0.0-rc.21 - unstorage: 2.0.0-alpha.3(chokidar@4.0.3)(db0@0.3.4)(ioredis@5.8.1)(ofetch@1.4.1) - optionalDependencies: - rolldown: 1.0.0-beta.44 - vite: 7.1.9(@types/node@22.18.12)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - better-sqlite3 - - chokidar - - drizzle-orm - - idb-keyval - - ioredis - - lru-cache - - mongodb - - mysql2 - - sqlite3 - - uploadthing - nitro@3.0.1-alpha.0(chokidar@4.0.3)(ioredis@5.8.2)(rolldown@1.0.0-beta.44)(vite@7.1.9(@types/node@22.18.12)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)): dependencies: consola: 3.4.2 @@ -9735,107 +9608,6 @@ snapshots: - sqlite3 - uploadthing - nitropack@2.12.7(rolldown@1.0.0-beta.44): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.0 - '@rollup/plugin-alias': 5.1.1(rollup@4.52.5) - '@rollup/plugin-commonjs': 28.0.6(rollup@4.52.5) - '@rollup/plugin-inject': 5.0.5(rollup@4.52.5) - '@rollup/plugin-json': 6.1.0(rollup@4.52.5) - '@rollup/plugin-node-resolve': 16.0.2(rollup@4.52.5) - '@rollup/plugin-replace': 6.0.2(rollup@4.52.5) - '@rollup/plugin-terser': 0.4.4(rollup@4.52.5) - '@vercel/nft': 0.30.2(rollup@4.52.5) - archiver: 7.0.1 - c12: 3.3.1(magicast@0.3.5) - chokidar: 4.0.3 - citty: 0.1.6 - compatx: 0.2.0 - confbox: 0.2.2 - consola: 3.4.2 - cookie-es: 2.0.0 - croner: 9.1.0 - crossws: 0.3.5 - db0: 0.3.4 - defu: 6.1.4 - destr: 2.0.5 - dot-prop: 10.1.0 - esbuild: 0.25.10 - escape-string-regexp: 5.0.0 - etag: 1.8.1 - exsolve: 1.0.7 - globby: 15.0.0 - gzip-size: 7.0.0 - h3: 1.15.4 - hookable: 5.5.3 - httpxy: 0.1.7 - ioredis: 5.8.1 - jiti: 2.6.1 - klona: 2.0.6 - knitwork: 1.2.0 - listhen: 1.9.0 - magic-string: 0.30.19 - magicast: 0.3.5 - mime: 4.1.0 - mlly: 1.8.0 - node-fetch-native: 1.6.7 - node-mock-http: 1.0.3 - ofetch: 1.4.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.0.0 - pkg-types: 2.3.0 - pretty-bytes: 7.1.0 - radix3: 1.1.2 - rollup: 4.52.5 - rollup-plugin-visualizer: 6.0.5(rolldown@1.0.0-beta.44)(rollup@4.52.5) - scule: 1.3.0 - semver: 7.7.3 - serve-placeholder: 2.0.2 - serve-static: 2.2.0 - source-map: 0.7.6 - std-env: 3.10.0 - ufo: 1.6.1 - ultrahtml: 1.6.0 - uncrypto: 0.1.3 - unctx: 2.4.1 - unenv: 2.0.0-rc.21 - unimport: 5.4.1 - unplugin-utils: 0.3.1 - unstorage: 1.17.1(db0@0.3.4)(ioredis@5.8.1) - untyped: 2.0.0 - unwasm: 0.3.11 - youch: 4.1.0-beta.11 - youch-core: 0.3.3 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - better-sqlite3 - - drizzle-orm - - encoding - - idb-keyval - - mysql2 - - react-native-b4a - - rolldown - - sqlite3 - - supports-color - - uploadthing - nitropack@2.12.8(rolldown@1.0.0-beta.44): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 @@ -10009,7 +9781,7 @@ snapshots: mlly: 1.8.0 mocked-exports: 0.1.1 nanotar: 0.2.0 - nitropack: 2.12.7(rolldown@1.0.0-beta.44) + nitropack: 2.12.8(rolldown@1.0.0-beta.44) nypm: 0.6.2 ofetch: 1.4.1 ohash: 2.0.11 @@ -11236,20 +11008,6 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.1(db0@0.3.4)(ioredis@5.8.1): - dependencies: - anymatch: 3.1.3 - chokidar: 4.0.3 - destr: 2.0.5 - h3: 1.15.4 - lru-cache: 10.4.3 - node-fetch-native: 1.6.7 - ofetch: 1.4.1 - ufo: 1.6.1 - optionalDependencies: - db0: 0.3.4 - ioredis: 5.8.1 - unstorage@1.17.1(db0@0.3.4)(ioredis@5.8.2): dependencies: anymatch: 3.1.3 @@ -11264,13 +11022,6 @@ snapshots: db0: 0.3.4 ioredis: 5.8.2 - unstorage@2.0.0-alpha.3(chokidar@4.0.3)(db0@0.3.4)(ioredis@5.8.1)(ofetch@1.4.1): - optionalDependencies: - chokidar: 4.0.3 - db0: 0.3.4 - ioredis: 5.8.1 - ofetch: 1.4.1 - unstorage@2.0.0-alpha.3(chokidar@4.0.3)(db0@0.3.4)(ioredis@5.8.2)(ofetch@1.4.1): optionalDependencies: chokidar: 4.0.3 From c1c287621e27eb04ff38d4874126ff247b6c3705 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 27 Oct 2025 21:15:46 +0100 Subject: [PATCH 2/3] chore: update knip config --- knip.json | 1 + 1 file changed, 1 insertion(+) diff --git a/knip.json b/knip.json index b9b81125a..4b14a1e92 100644 --- a/knip.json +++ b/knip.json @@ -33,6 +33,7 @@ "fuse.js", "giget", "h3-next", + "http-proxy-3", "jiti", "nitro", "nitropack", From 9d8001ac6006c4ff522ae96e3fe53e22025fa0ef Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 27 Oct 2025 21:18:41 +0100 Subject: [PATCH 3/3] test: update deno compatibility --- packages/nuxt-cli/test/e2e/runtimes.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt-cli/test/e2e/runtimes.spec.ts b/packages/nuxt-cli/test/e2e/runtimes.spec.ts index 0dbdb1896..e743d7d41 100644 --- a/packages/nuxt-cli/test/e2e/runtimes.spec.ts +++ b/packages/nuxt-cli/test/e2e/runtimes.spec.ts @@ -49,7 +49,7 @@ function createIt(runtimeName: typeof runtimes[number], _socketsEnabled: boolean deno: { start: !platform.windows, fetching: !platform.windows, - websockets: !platform.windows, + websockets: !platform.windows && !platform.macos, }, } const status = supportMatrix[runtimeName]