diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 1eedcade4..9f8d528cd 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -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' @@ -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': { - ...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', @@ -176,21 +172,35 @@ function resolveListenOverrides(args: ParsedArgs) { } 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 } diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 31e740b60..fc2b2f79b 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -228,7 +228,7 @@ export class NuxtDevServer extends EventEmitter { 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) @@ -276,9 +276,7 @@ export class NuxtDevServer extends EventEmitter { 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)) @@ -288,12 +286,12 @@ export class NuxtDevServer extends EventEmitter { ? 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?.('./') diff --git a/packages/nuxt-cli/test/e2e/dev.spec.ts b/packages/nuxt-cli/test/e2e/dev.spec.ts index 0b7ca2cef..e96e63c68 100644 --- a/packages/nuxt-cli/test/e2e/dev.spec.ts +++ b/packages/nuxt-cli/test/e2e/dev.spec.ts @@ -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' @@ -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}/`, + }) + }) + }) }) diff --git a/playground/certs/cert.dummy b/playground/certs/cert.dummy new file mode 100644 index 000000000..4c6e93016 --- /dev/null +++ b/playground/certs/cert.dummy @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID2DCCAsCgAwIBAgIFNTkzOTIwDQYJKoZIhvcNAQELBQAwdTESMBAGA1UEAxMJ +bG9jYWxob3N0MQswCQYDVQQGEwJVUzERMA8GA1UECBMITWljaGlnYW4xEDAOBgNV +BAcTB0JlcmtsZXkxFTATBgNVBAoTDFRlc3RpbmcgQ29ycDEWMBQGA1UECxMNSVQg +ZGVwYXJ0bWVudDAgFw0yNTExMTYwMzIxNDZaGA8yMTI1MTAyMzAzMjE0NlowdTES +MBAGA1UEAxMJbG9jYWxob3N0MQswCQYDVQQGEwJVUzERMA8GA1UECBMITWljaGln +YW4xEDAOBgNVBAcTB0JlcmtsZXkxFTATBgNVBAoTDFRlc3RpbmcgQ29ycDEWMBQG +A1UECxMNSVQgZGVwYXJ0bWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALltbdcjx7u+018fT9dOIUZbZUt9CoHpZWbWSohMz9YfVabB99SL+7AiM30/ +l4XCXO5DhhUSxqwUlQ2pAcjnEHxmVpvuiXwTHwItfiF+UDxyCIlfMrj8Tc8ltB41 +KPpfgUuc1mb6fsISjEgRkLNW0VIXJDNenlvpx/uXf4MG+6My1T7TFVAhEulZu3wv +b62Q5WR6Ud1G7pMDrEcRcPHkuh3CeQ3OW11hCrwMh2IaLPCd+PDTkwfOaKXYY3/m +raLSmCb/PXVsN7qcUBd1+bwlMblteeJykoZfYawVgn8/+sVdtxBsQftAYOComz4a +a4iJsuKEPoLf8VwaVUaIMpZEVtcCAwEAAaNtMGswDAYDVR0TAQH/BAIwADAOBgNV +HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCwGA1Ud +EQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG +9w0BAQsFAAOCAQEAZsnDKhb3OjHgBg1rz2yZg0FFD7oPeyNJNpxXScIOmCIsh0f0 +oYx8b17/4dbEg812EHpdUJeceWfvKg+vfylmL6hZ04eY4ymUjf5jrW9iuDd3mmLY ++wsARPjvBvw/iA4BPYg16HUSqUEWfwG/IeQqpu+C75RCie604nt36nDoTQv8QLCx +Et6wvXH9Ucz4CA5MjFJbqaM62CJU+Qeu4sVFgY/KW3UQEJboD51hutxKBMWkkZGL +l5svZhTqUoPSFgdQTHeu4Tku5ZSdwgEBBE2cf8HPOET99aFhPvCVg52IysfW2UNs +BKo0D/Vy+dCX8CGldS59CLx5zKta30fSLlq6vw== +-----END CERTIFICATE----- diff --git a/playground/certs/key.dummy b/playground/certs/key.dummy new file mode 100644 index 000000000..ab1ed8138 --- /dev/null +++ b/playground/certs/key.dummy @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuW1t1yPHu77TXx9P104hRltlS30KgellZtZKiEzP1h9VpsH3 +1Iv7sCIzfT+XhcJc7kOGFRLGrBSVDakByOcQfGZWm+6JfBMfAi1+IX5QPHIIiV8y +uPxNzyW0HjUo+l+BS5zWZvp+whKMSBGQs1bRUhckM16eW+nH+5d/gwb7ozLVPtMV +UCES6Vm7fC9vrZDlZHpR3UbukwOsRxFw8eS6HcJ5Dc5bXWEKvAyHYhos8J348NOT +B85opdhjf+atotKYJv89dWw3upxQF3X5vCUxuW154nKShl9hrBWCfz/6xV23EGxB ++0Bg4KibPhpriImy4oQ+gt/xXBpVRogylkRW1wIDAQABAoIBAB9dZLZ+7WKTATL2 +W216YEOD4yr1MClQXuAZwEq033UDINxPtAmGUiD1cAswDgPIoCqHTm9TGTrzUlEY +tN4UQ6QfNWgz3ZqYq2aVZl/o+052JX6DFVPYDZtL798qM8/CBt9Q3K1XkshmFcd8 +/SJwvYBqvKtZxmSas0KZ2i5CKJ9uiHEWi/ZCs6pUJdRojxfkfeHLMujA7EDBtZic +AKqH698jo7+m8hddczM7XOlBVe6MAn2fWs4VlgVK4gfw4V0xtZt5wvNhyh69YTUZ +wtMTaEULPwPtmxSTdWvb0Z4kUftXLRS3jd/ibe7utQLoR579ZPoNL0Ixma8sIA9j +TV+b9akCgYEA5wJufOg7uFXD1bL/TsnU9ciNs7rof6SK+Hc7IrLFVk7a5SeAjcAJ +oar70pnaGE0TgLLMXysi/2XZL0y45a3ihgl3CoLGJBGOv4QcgccmFrZqYUGCV0Cv +oz7PskhcmcneyWnoI/twbbyYxkQNww+5LSTN8iAaVcb+3G1YZPaQ2AkCgYEAzXym +btesxvDXHurQdviBKEjp7yhvSo44Q54ahEW8n1RiGfjgwFcjF1jZXxqGml/bIiu4 +AYsSeUqBrFXAXq9AlA6DThFJtC5EK385do1WUhcaU53FlvWt2atmRXuUl+rgaBrS +b43uvLyFJZezQILzvYkaHJjh8ioKOkPzLW2ar98CgYB0oJ6lgx27d9lSB3esEGvq +1qDrz35YCvt6a7+4SeclJtSOgr39UqnKLCfM8I3SXP9up1ZU6dNWe9YFckea9Yn6 +v8aQ0Os2BIM8H3fA8YlCSEA277rdUDQcR7bWPIA7yFYo+8YOfIALdv7ugicshsCn +kQBEsH57Necv5CiPeIgx+QKBgG+BY6MkX/p4eJOrYkIc6aFdp6wCqhmwATIYGlWK +ridbl/x2BCf7YOxrZ1FnSIF+4J+zT59uwzCUULeetMvsl8N/+JqlYPRoYs+jsx/0 +5FGZfczAAZfAa32Bt/aeb+zcJLf5ThYA0/sQ5cOXhUrNhMxmGIhKIdnSHEiv1Mbj +AhzLAoGBAKDYjXW1FNIR3le4Ngs8adCPN18iJ7rXSYPnen5e40g3i1vQGKwxMQpg +aEKByb1RWSH4fZDToiC+CPqrimgu24mgYJNMD9W7M8iizR2/5vKNJ/ABqL1/SV0L +OsvTc2QFx005TGOLteaw1mhqjSGptNHzHhPQ01v2gcIkobBk0N5y +-----END RSA PRIVATE KEY----- diff --git a/playground/certs/pfx.dummy b/playground/certs/pfx.dummy new file mode 100644 index 000000000..d993b5cde Binary files /dev/null and b/playground/certs/pfx.dummy differ diff --git a/playground/package.json b/playground/package.json index ddbdda412..cbcbd4085 100644 --- a/playground/package.json +++ b/playground/package.json @@ -4,6 +4,8 @@ "private": true, "scripts": { "dev": "nuxt dev", + "dev:https:certs": "nuxt dev --https --https.cert=certs/cert.dummy --https.key=certs/key.dummy", + "dev:https:pfx": "nuxt dev --https --https.pfx=certs/pfx.dummy --https.passphrase=pass", "dev:prepare": "nuxt prepare", "test": "vitest" },