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
54 changes: 32 additions & 22 deletions packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import process from 'node:process'

import { defineCommand } from 'citty'
import { colors } from 'consola/utils'
import { getArgs as getListhenArgs } from 'listhen/cli'
import { getArgs as getListhenArgs, parseArgs as parseListhenArgs } from 'listhen/cli'
import { resolve } from 'pathe'
import { satisfies } from 'semver'
import { isBun, isTest } from 'std-env'
Expand Down Expand Up @@ -46,26 +46,22 @@ const command = defineCommand({
},
...{
...listhenArgs,
'port': {
port: {
...listhenArgs.port,
description: 'Port to listen on (default: `NUXT_PORT || NITRO_PORT || PORT || nuxtOptions.devServer.port`)',
alias: ['p'],
},
'open': {
open: {
...listhenArgs.open,
alias: ['o'],
default: false,
},
'host': {
host: {
...listhenArgs.host,
alias: ['h'],
description: 'Host to listen on (default: `NUXT_HOST || NITRO_HOST || HOST || nuxtOptions.devServer?.host`)',
},
'clipboard': { ...listhenArgs.clipboard, default: false },
'https.domains': {
Copy link
Contributor Author

@yamachi4416 yamachi4416 Nov 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed https.domains as it was identical to the original option's definition
listhen/cli.ts

...listhenArgs['https.domains'],
description: 'Comma separated list of domains and IPs, the autogenerated certificate should be valid for (https: true)',
},
clipboard: { ...listhenArgs.clipboard, default: false },
},
sslCert: {
type: 'string',
Expand Down Expand Up @@ -176,21 +172,35 @@ function resolveListenOverrides(args: ParsedArgs<ArgsT>) {
} as const
}

const _httpsCert = args['https.cert']
|| args.sslCert
|| process.env.NUXT_SSL_CERT
|| process.env.NITRO_SSL_CERT

const _httpsKey = args['https.key']
|| args.sslKey
|| process.env.NUXT_SSL_KEY
|| process.env.NITRO_SSL_KEY
const options = parseListhenArgs({
...args,
'host': args.host
|| process.env.NUXT_HOST
|| process.env.NITRO_HOST
|| process.env.HOST!,
'port': args.port
|| process.env.NUXT_PORT
|| process.env.NITRO_PORT
|| process.env.PORT!,
'https': args.https !== false,
'https.cert': args['https.cert']
|| args.sslCert
|| process.env.NUXT_SSL_CERT
|| process.env.NITRO_SSL_CERT!,
'https.key': args['https.key']
|| args.sslKey
|| process.env.NUXT_SSL_KEY
|| process.env.NITRO_SSL_KEY!,
})

return {
...args,
'open': (args.o as boolean) || args.open,
'https.cert': _httpsCert || '',
'https.key': _httpsKey || '',
...options,
// if the https flag is not present, https.xxx arguments are ignored.
// override if https is enabled in devServer config.
_https: args.https,
get https(): typeof options['https'] {
return this._https ? options.https : false
},
} as const
}

Expand Down
16 changes: 7 additions & 9 deletions packages/nuxi/src/dev/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
if (urls) {
// Pass hostname and https info for proper CORS and allowedHosts setup
const overrides = this.options.listenOverrides || {}
const hostname = overrides.hostname ?? (overrides as any).host
const hostname = overrides.hostname
const https = overrides.https

loadOptions.defaults = resolveDevServerDefaults({ hostname, https }, urls)
Expand Down Expand Up @@ -276,9 +276,7 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {

const port = overrides.port ?? nuxtConfig.devServer?.port

// CLI args use 'host', but ListenOptions uses 'hostname'
const rawHost = overrides.hostname ?? (overrides as any).host
const hostname = (rawHost === true ? '' : rawHost) ?? nuxtConfig.devServer?.host
const hostname = overrides.hostname ?? nuxtConfig.devServer?.host

// Resolve public flag
const isPublic = provider === 'codesandbox' || (overrides.public ?? (isPublicHostname(hostname) ? true : undefined))
Expand All @@ -288,12 +286,12 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
? nuxtConfig.devServer.https
: {}

const httpsEnabled = !!(overrides.https ?? nuxtConfig.devServer?.https)
;(overrides as any)._https ??= !!nuxtConfig.devServer?.https

const httpsOptions = httpsEnabled && {
...httpsFromConfig,
...(typeof overrides.https === 'object' ? overrides.https : {}),
}
const httpsOptions = overrides.https && defu(
(typeof overrides.https === 'object' ? overrides.https : {}),
httpsFromConfig,
)

// Resolve baseURL
const baseURL = nuxtConfig.app?.baseURL?.startsWith?.('./')
Expand Down
239 changes: 238 additions & 1 deletion packages/nuxt-cli/test/e2e/dev.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@ 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 { afterEach, describe, expect, it, vi } from 'vitest'
import { runCommand } from '../../src'

const fixtureDir = fileURLToPath(new URL('../fixtures/dev', import.meta.url))

const certsDir = fileURLToPath(new URL('../../../../playground/certs', import.meta.url))
const httpsCert = join(certsDir, 'cert.dummy')
const httpsKey = join(certsDir, 'key.dummy')
const httpsPfx = join(certsDir, 'pfx.dummy')

describe('dev server', () => {
afterEach(() => {
vi.unstubAllEnvs()
})

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'
Expand Down Expand Up @@ -92,4 +101,232 @@ describe('dev server', () => {
await close()
}
})

describe('https options', async () => {
const httpsCertValue = (await readFile(httpsCert, { encoding: 'ascii' })).split(/\r?\n/)
const httpsKeyValue = (await readFile(httpsKey, { encoding: 'ascii' })).split(/\r?\n/)

it('should be applied cert and key from commandline', { 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: 3601 })
const { result: { close } } = await runCommand('dev', [
`--https`,
`--https.cert=${httpsCert}`,
`--https.key=${httpsKey}`,
`--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 { https, ...options } = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse)
expect(options).toMatchObject({
host,
port,
url: `https://${host}:${port}/`,
})
expect(https).toBeTruthy()
expect(https.cert.split(/\r?\n/)).toEqual(httpsCertValue)
expect(https.key.split(/\r?\n/)).toEqual(httpsKeyValue)
})

it('should be applied pfx and passphrase from commandline', { 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: 3602 })
const { result: { close } } = await runCommand('dev', [
`--https`,
`--https.pfx=${httpsPfx}`,
`--https.passphrase=pass`,
`--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 { https, ...options } = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse)
expect(options).toMatchObject({
host,
port,
url: `https://${host}:${port}/`,
})
expect(https).toBeTruthy()
expect(https.cert.split(/\r?\n/)).toEqual(httpsCertValue)
expect(https.key.split(/\r?\n/)).toEqual(httpsKeyValue)
})

it('should be override from commandline', { 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: 3603 })
const { result: { close } } = await runCommand('dev', [
`--https.cert=${httpsCert}`,
`--https.key=${httpsKey}`,
`--host=${host}`,
`--port=${port}`,
`--cwd=${fixtureDir}`,
], {
overrides: {
devServer: {
https: {
cert: 'invalid-cert.pem',
key: 'invalid-key.pem',
host: 'localhost',
port: 3000,
},
},
modules: [
fileURLToPath(new URL('../fixtures/log-dev-server-options.ts', import.meta.url)),
],
},
}) as any
await close()
const { https, ...options } = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse)
expect(options).toMatchObject({
host,
port,
url: `https://${host}:${port}/`,
})
expect(https).toBeTruthy()
expect(https.cert.split(/\r?\n/)).toEqual(httpsCertValue)
expect(https.key.split(/\r?\n/)).toEqual(httpsKeyValue)
})

it('should be disabled from commandline', { 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: 3604 })
const { result: { close } } = await runCommand('dev', [
`--https=false`,
`--host=${host}`,
`--port=${port}`,
`--cwd=${fixtureDir}`,
], {
overrides: {
devServer: {
https: true,
host: 'localhost',
port: 3000,
},
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}/`,
})
})
})

describe('applied environment variables', async () => {
const httpsCertValue = (await readFile(httpsCert, { encoding: 'ascii' })).split(/\r?\n/)
const httpsKeyValue = (await readFile(httpsKey, { encoding: 'ascii' })).split(/\r?\n/)

it('should be applied from NUXT_ environment variables', { 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: 3701 })

vi.stubEnv('NUXT_HOST', host)
vi.stubEnv('NUXT_PORT', `${port}`)
vi.stubEnv('NUXT_SSL_CERT', httpsCert)
vi.stubEnv('NUXT_SSL_KEY', httpsKey)

const { result: { close } } = await runCommand('dev', [
`--https`,
`--cwd=${fixtureDir}`,
], {
overrides: {
modules: [
fileURLToPath(new URL('../fixtures/log-dev-server-options.ts', import.meta.url)),
],
},
}) as any
await close()
const { https, ...options } = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse)
expect(options).toMatchObject({
host,
port,
url: `https://${host}:${port}/`,
})
expect(https).toBeTruthy()
expect(https.cert.split(/\r?\n/)).toEqual(httpsCertValue)
expect(https.key.split(/\r?\n/)).toEqual(httpsKeyValue)
})

it('should be applied from NITRO_ environment variables', { 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: 3702 })

vi.stubEnv('NITRO_HOST', host)
vi.stubEnv('NITRO_PORT', `${port}`)
vi.stubEnv('NITRO_SSL_CERT', httpsCert)
vi.stubEnv('NITRO_SSL_KEY', httpsKey)

const { result: { close } } = await runCommand('dev', [
`--https`,
`--cwd=${fixtureDir}`,
], {
overrides: {
modules: [
fileURLToPath(new URL('../fixtures/log-dev-server-options.ts', import.meta.url)),
],
},
}) as any
await close()
const { https, ...options } = await readFile(join(fixtureDir, '.nuxt/dev-server.json'), 'utf-8').then(JSON.parse)
expect(options).toMatchObject({
host,
port,
url: `https://${host}:${port}/`,
})
expect(https).toBeTruthy()
expect(https.cert.split(/\r?\n/)).toEqual(httpsCertValue)
expect(https.key.split(/\r?\n/)).toEqual(httpsKeyValue)
})

it('should be applied from HOST and PORT environment variables', { 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: 3703 })

vi.stubEnv('HOST', host)
vi.stubEnv('PORT', `${port}`)

const { result: { close } } = await runCommand('dev', [
`--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({
host,
port,
url: `http://${host}:${port}/`,
})
})
})
})
Loading
Loading