diff --git a/knip.json b/knip.json index 74faba98b..a3fc7daec 100644 --- a/knip.json +++ b/knip.json @@ -15,13 +15,13 @@ "pages/**", "server/**", "some-layer/**" - ], - "ignoreDependencies": [ - "@nuxt/test-utils", - "nuxt" ] }, "packages/nuxt-cli": { + "entry": [ + "src/index.ts", + "test/fixtures/*" + ], "ignoreDependencies": [ "c12", "clipboardy", @@ -34,7 +34,6 @@ "h3", "httpxy", "jiti", - "listhen", "nypm", "ofetch", "ohash", diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 6a4f36d66..82bade3ce 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -149,6 +149,7 @@ const command = defineCommand({ url: devProxy.listener.url, urls, 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 diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index 7d3e89583..b8a2f5d8e 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -1,10 +1,12 @@ import type { NuxtConfig } from '@nuxt/schema' import type { ListenOptions } from 'listhen' -import type { NuxtDevContext, NuxtDevIPCMessage, NuxtDevServer, NuxtParentIPCMessage } from './utils' +import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './utils' import process from 'node:process' import defu from 'defu' -import { createNuxtDevServer, resolveDevServerDefaults, resolveDevServerOverrides } from './utils' +import { listen } from 'listhen' +import { createSocketListener } from './socket' +import { NuxtDevServer, resolveDevServerDefaults, resolveDevServerOverrides } from './utils' const start = Date.now() @@ -52,22 +54,48 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti https: devContext.proxy?.https, }, devContext.publicURLs) - // _PORT is used by `@nuxt/test-utils` to launch the dev server on a specific port - const listenOptions = _listenOptions === true || process.env._PORT - ? { port: process.env._PORT ?? 0, hostname: '127.0.0.1', showURL: false } - : _listenOptions - - // Init Nuxt dev - const devServer = await createNuxtDevServer({ + // Initialize dev server + const devServer = new NuxtDevServer({ cwd: devContext.cwd, - overrides: defu(ctx.data?.overrides, devServerOverrides), + overrides: defu( + ctx.data?.overrides, + ({ extends: devContext.args.extends } satisfies NuxtConfig) as NuxtConfig, + devServerOverrides, + ), defaults: devServerDefaults, logLevel: devContext.args.logLevel as 'silent' | 'info' | 'verbose', clear: !!devContext.args.clear, dotenv: { cwd: devContext.cwd, fileName: devContext.args.dotenv }, envName: devContext.args.envName, - devContext, - }, listenOptions) + devContext: { + proxy: devContext.proxy, + }, + }) + + // _PORT is used by `@nuxt/test-utils` to launch the dev server on a specific port + const listenOptions = _listenOptions === true || process.env._PORT + ? { port: process.env._PORT ?? 0, hostname: '127.0.0.1', showURL: false } + : _listenOptions + + // Attach internal listener + devServer.listener = listenOptions + ? await listen(devServer.handler, listenOptions) + : await createSocketListener(devServer.handler, devContext.proxy?.addr) + + 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 (devContext.proxy?.url) { + devServer.listener.url = devContext.proxy.url + } + if (devContext.proxy?.urls) { + const _getURLs = devServer.listener.getURLs.bind(devServer.listener) + devServer.listener.getURLs = async () => Array.from(new Set([...devContext.proxy?.urls || [], ...(await _getURLs())])) + } let address: string diff --git a/packages/nuxi/src/dev/socket.ts b/packages/nuxi/src/dev/socket.ts index 400a4bc04..f1ee0bea8 100644 --- a/packages/nuxi/src/dev/socket.ts +++ b/packages/nuxi/src/dev/socket.ts @@ -1,4 +1,5 @@ import type { RequestListener } from 'node:http' +import type { AddressInfo } from 'node:net' import { existsSync, unlinkSync } from 'node:fs' import { Server } from 'node:http' import os from 'node:os' @@ -43,7 +44,7 @@ export function parseSocketURL(url: string): { socketPath: string, protocol: 'ht return { socketPath, protocol: ssl ? 'https' : 'http' } } -export async function createSocketListener(handler: RequestListener, ssl = false) { +export async function createSocketListener(handler: RequestListener, proxyAddress?: AddressInfo) { const socketPath = generateSocketPath('nuxt-dev') const server = new Server(handler) @@ -56,14 +57,10 @@ export async function createSocketListener(handler: RequestListener, ssl = false } } await new Promise(resolve => server.listen({ path: socketPath }, resolve)) - const url = formatSocketURL(socketPath, ssl) + const url = formatSocketURL(socketPath) return { url, - address: { - socketPath, - address: 'localhost', - port: 3000, - }, + address: { address: 'localhost', port: 3000, ...proxyAddress, socketPath }, async close() { try { server.removeAllListeners() diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 641efbcfc..46c7c27f5 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -15,7 +15,6 @@ import { pathToFileURL } from 'node:url' import defu from 'defu' import { resolveModulePath } from 'exsolve' import { toNodeListener } from 'h3' -import { listen } from 'listhen' import { resolve } from 'pathe' import { debounce } from 'perfect-debounce' import { provider } from 'std-env' @@ -26,7 +25,7 @@ import { loadKit } from '../utils/kit' import { loadNuxtManifest, resolveNuxtManifest, writeNuxtManifest } from '../utils/nuxt' import { renderError } from './error' -import { createSocketListener, formatSocketURL } from './socket' +import { formatSocketURL } from './socket' export type NuxtParentIPCMessage = | { type: 'nuxt:internal:dev:context', context: NuxtDevContext, socket?: boolean } @@ -55,6 +54,7 @@ export interface NuxtDevContext { url?: string urls?: ListenURL[] https?: boolean | HTTPSOptions + addr?: AddressInfo } } @@ -66,38 +66,8 @@ interface NuxtDevServerOptions { clear?: boolean defaults: NuxtConfig overrides: NuxtConfig - port?: string | number loadingTemplate?: ({ loading }: { loading: string }) => string - devContext: NuxtDevContext -} - -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, 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 - if (options.devContext.proxy?.url) { - devServer.listener.url = options.devContext.proxy.url - } - if (options.devContext.proxy?.urls) { - const _getURLs = devServer.listener.getURLs.bind(devServer.listener) - devServer.listener.getURLs = async () => Array.from(new Set([...options.devContext.proxy?.urls || [], ...(await _getURLs())])) - } - - return devServer + devContext: Pick } // https://regex101.com/r/7HkR5c/1 @@ -125,7 +95,7 @@ export class NuxtDevServer extends EventEmitter { handler: RequestListener listener: Pick & { _url?: string - address: { socketPath: string, port: number, address: string } | AddressInfo + address: Omit & { socketPath: string } | AddressInfo } constructor(private options: NuxtDevServerOptions) { @@ -236,7 +206,6 @@ export class NuxtDevServer extends EventEmitter { defaults: defu(this.options.defaults, devServerDefaults), overrides: { logLevel: this.options.logLevel as 'silent' | 'info' | 'verbose', - ...(this.options.devContext.args.extends && { extends: this.options.devContext.args.extends }), ...this.options.overrides, vite: { clearScreen: this.options.clear, @@ -319,9 +288,7 @@ export class NuxtDevServer extends EventEmitter { const addr = this.listener.address 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.url = 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) { diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index 364a8ec9f..40c3deeec 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -60,6 +60,7 @@ "youch": "^4.1.0-beta.10" }, "devDependencies": { + "@nuxt/kit": "^4.0.0", "@nuxt/schema": "^4.0.0", "@types/node": "^22.16.3", "get-port-please": "^3.2.0", diff --git a/packages/nuxt-cli/test/e2e/dev.spec.ts b/packages/nuxt-cli/test/e2e/dev.spec.ts new file mode 100644 index 000000000..c7beda739 --- /dev/null +++ b/packages/nuxt-cli/test/e2e/dev.spec.ts @@ -0,0 +1,56 @@ +import { readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { getPort } from 'get-port-please' +import { describe, expect, it } from 'vitest' +import { runCommand } from '../../src' + +const fixtureDir = fileURLToPath(new URL('../fixtures/dev', import.meta.url)) + +describe('dev server', () => { + it('should expose dev server address to nuxt options', { timeout: 50_000 }, async () => { + await rm(join(fixtureDir, '.nuxt'), { recursive: true, force: true }) + const host = '127.0.0.1' + const port = await getPort({ host, port: 3031 }) + const { result: { close } } = await runCommand('dev', [`--host=${host}`, `--port=${port}`, `--cwd=${fixtureDir}`], { + overrides: { + modules: [ + fileURLToPath(new URL('../fixtures/log-dev-server-options.ts', import.meta.url)), + ], + }, + }) as any + await close() + const options = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse) + expect(options).toMatchObject({ + https: false, + host, + port, + url: `http://${host}:${port}/`, + }) + }) + + it('should respect configured devServer options', { timeout: 50_000 }, async () => { + await rm(join(fixtureDir, '.nuxt'), { recursive: true, force: true }) + const host = '127.0.0.1' + const port = await getPort({ host, port: 3050 }) + const { result: { close } } = await runCommand('dev', [`--cwd=${fixtureDir}`], { + overrides: { + devServer: { + host, + port, + }, + modules: [ + fileURLToPath(new URL('../fixtures/log-dev-server-options.ts', import.meta.url)), + ], + }, + }) as any + await close() + const options = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse) + expect(options).toMatchObject({ + https: false, + host, + port, + url: `http://${host}:${port}/`, + }) + }) +}) diff --git a/packages/nuxt-cli/test/fixtures/dev/package.json b/packages/nuxt-cli/test/fixtures/dev/package.json new file mode 100644 index 000000000..42fa2e37c --- /dev/null +++ b/packages/nuxt-cli/test/fixtures/dev/package.json @@ -0,0 +1,12 @@ +{ + "name": "fixtures-dev", + "version": "1.0.0", + "private": true, + "scripts": { + "dev:prepare": "nuxt prepare", + "test": "vitest" + }, + "dependencies": { + "nuxt": "^4.0.0" + } +} diff --git a/packages/nuxt-cli/test/fixtures/log-dev-server-options.ts b/packages/nuxt-cli/test/fixtures/log-dev-server-options.ts new file mode 100644 index 000000000..7122f38d1 --- /dev/null +++ b/packages/nuxt-cli/test/fixtures/log-dev-server-options.ts @@ -0,0 +1,18 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { defineNuxtModule, useNuxt } from '@nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'nuxt-cli-test-module', + }, + setup() { + const nuxt = useNuxt() + + nuxt.hook('build:before', async () => { + await mkdir('.nuxt', { recursive: true }) + await writeFile(join(nuxt.options.rootDir, '.nuxt/dev-server.json'), JSON.stringify(nuxt.options.devServer)) + await nuxt.close() + }) + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a594a868..b91257bea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -294,6 +294,9 @@ importers: specifier: ^4.1.0-beta.10 version: 4.1.0-beta.10 devDependencies: + '@nuxt/kit': + specifier: ^4.0.0 + version: 4.0.0(magicast@0.3.5) '@nuxt/schema': specifier: 4.0.0 version: 4.0.0 @@ -322,6 +325,12 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.3)(jiti@2.4.2)(terser@5.43.1)(yaml@2.8.0) + packages/nuxt-cli/test/fixtures/dev: + dependencies: + nuxt: + specifier: ^4.0.0 + version: 4.0.0(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.16.3)(@vue/compiler-sfc@3.5.17)(db0@0.3.2)(eslint@9.31.0(jiti@2.4.2))(ioredis@5.6.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.45.0)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(terser@5.43.1)(yaml@2.8.0))(yaml@2.8.0) + playground: dependencies: nuxt: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0e90cf81d..11881ced9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - packages/* + - packages/nuxt-cli/test/fixtures/* - playground