Skip to content

refactor(dev): use internal listener#1382

Open
danielroe wants to merge 1 commit into
mainfrom
refactor/listhen
Open

refactor(dev): use internal listener#1382
danielroe wants to merge 1 commit into
mainfrom
refactor/listhen

Conversation

@danielroe

@danielroe danielroe commented Jul 24, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

📚 Description

this follows in nitro's footsteps by replacing listhen, which is a pretty hefty dependency, with our own listener. (this is also what vite, nitro, etc. do)

we only used a small part of that, which is feasible to maintain ourselves within nuxt/cli. the key benefit is about 3.5MB of unneeded dependencies we can lose.

Note

we'll likely migrate to srvx later on, and possibly even upstream some of these changes into a new listhen v2, if appropriate

(this won't make much difference in nuxt v3/4 because nitropack pulls them in as it has its own dependency on listhen, but in nuxt v5, it'll feel great!)

using mkcert not only removes a dependency on node-forge, but generates a locally-trusted certificate, which should be strictly better....

a quick feature comparison

Feature Before (listhen) After
Port resolution + fallback get-port-please Same, plus a warning when falling back to another port
--host / --public ✅ same, including bare --host = all interfaces
URL banner (Local/Network, colors) ✅ same layout and colors
--open ✅ zero-dep (spawns platform opener)
--clipboard ✅ (tinyclip) ✅ same
--qr (auto when public, centered) ✅ (uqr) ✅ same
--publicURL
--https with own cert/key/pfx
--https with no cert Self-signed via node-forge (untrusted) Locally-trusted via mkcert (PATH or one-time download); openssl self-signed fallback
--tunnel via untun Spawns cloudflared directly; ToS consent now persisted in user .nuxtrc
Graceful shutdown http-shutdown Native server.closeAllConnections()
--https.validityDays Only honored by the openssl fallback (mkcert fixes its own validity)
--name, --baseURL CLI flags ✅ (unused by nuxi) Dropped (baseURL still resolved from nuxt.config)
Offline cert generation ✅ (pure JS) Requires mkcert or openssl (error with hint otherwise)

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The development server no longer relies on listhen for listener types, argument parsing, or server startup. It adds local HTTP/HTTPS listening, certificate loading and generation, cached binary resolution with consent handling, and Cloudflare quick tunnels. The dev CLI now defines explicit listen and HTTPS options, while Nuxt server initialization, pooling, IPC types, URL handling, dependencies, and benchmarks are updated to use the new local listener contracts.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: replacing listhen with an internal listener implementation.
Description check ✅ Passed The description is directly related to the refactor and accurately describes the listener replacement and related features.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/listhen

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 24, 2026

Copy link
Copy Markdown
  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1382
    
    npm i https://pkg.pr.new/nuxi@1382
    
    npm i https://pkg.pr.new/@nuxt/cli@1382
    

commit: 8351819

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nuxi/src/commands/dev.ts`:
- Around line 221-238: The httpsOptions validityDays parsing must reject
non-numeric values instead of passing NaN to certificate generation. Update the
validityDays handling in the HTTPS options construction to validate the numeric
conversion and use the existing undefined/invalid-input behavior when conversion
is not finite, while preserving valid numeric values.
- Around line 245-259: Update the returned configuration in the dev command
around httpsOptions so overrides.https is undefined or otherwise falsy when
HTTPS is disabled, rather than always returning the possibly empty object.
Preserve the existing HTTPS options when httpsEnabled is true, or equivalently
pass the enabled-gated value through the resolveDevServerDefaults path used by
`#loadNuxtInstance`.

In `@packages/nuxi/src/dev/binaries.ts`:
- Around line 29-42: Update resolveTool and the download/extraction flow used by
downloadBinary so installations are written to a unique temporary path rather
than destination, then atomically rename the completed file into destination
using renameSync. Ensure archive extraction completes in a per-process temporary
location before the final rename, and clean up temporary artifacts on failure so
concurrent installs cannot leave a corrupted cache entry.
- Around line 100-107: Update downloadBinary to call fetch with an explicit
timeout signal, using the project’s established timeout mechanism or AbortSignal
timeout support. Keep the existing response validation, retry behavior, and
error fallback unchanged, including propagation of timeout failures through the
current catch flow.

In `@packages/nuxi/src/dev/cert.ts`:
- Around line 45-64: Update generateCertificate so the cache freshness threshold
incorporates options.validityDays rather than relying solely on the fixed
CERT_MAX_AGE_MS value. Bound the effective cache age by the configured
certificate validity, preserving the existing certificate generation flow and
optionally documenting that this also shortens mkcert certificate caching when
validityDays is set.

In `@packages/nuxi/src/dev/listen.ts`:
- Around line 78-81: Update the server error handling in the Promise around
server.listen, replacing the one-time error listener with a persistent listener
so later server-level errors remain handled throughout the dev session. Preserve
the existing Promise resolution and rejection behavior for the initial listen
operation.

In `@packages/nuxi/src/dev/tunnel.ts`:
- Line 27: Attach a child.on('error', ...) handler immediately after spawn in
the tunnel startup flow, using the existing tunnel error-handling behavior to
report the failure and disable or clean up the tunnel without allowing an
uncaught exception to terminate nuxi dev. Keep the normal stdout/stderr and
process-exit handling unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a94f6bc-9bb3-48f6-9c0d-83ee01d161c1

📥 Commits

Reviewing files that changed from the base of the PR and between 882749f and 1b97a6c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • knip.json
  • packages/nuxi/package.json
  • packages/nuxi/src/commands/dev.ts
  • packages/nuxi/src/dev/binaries.ts
  • packages/nuxi/src/dev/cert.ts
  • packages/nuxi/src/dev/index.ts
  • packages/nuxi/src/dev/listen.ts
  • packages/nuxi/src/dev/pool.ts
  • packages/nuxi/src/dev/tunnel.ts
  • packages/nuxi/src/dev/utils.ts
  • packages/nuxt-cli/package.json
  • packages/nuxt-cli/test/bench/dev.bench.ts

Comment thread packages/nuxi/src/commands/dev.ts
Comment thread packages/nuxi/src/commands/dev.ts
Comment thread packages/nuxi/src/dev/binaries.ts
Comment thread packages/nuxi/src/dev/binaries.ts Outdated
Comment thread packages/nuxi/src/dev/cert.ts Outdated
Comment on lines +45 to +64
const CERT_MAX_AGE_MS = 27 * 24 * 60 * 60 * 1000

async function generateCertificate(options: HTTPSOptions): Promise<ResolvedCertificate> {
const domains = options.domains?.length ? options.domains : ['localhost', '127.0.0.1', '::1']
const hash = createHash('sha256').update(domains.join(',')).digest('hex').slice(0, 8)
const dir = getCacheDir('certs', hash)
const certPath = join(dir, 'cert.pem')
const keyPath = join(dir, 'key.pem')

const fresh = existsSync(certPath)
&& existsSync(keyPath)
&& Date.now() - statSync(certPath).mtimeMs < CERT_MAX_AGE_MS

if (!fresh) {
const generated = await generateWithMkcert(certPath, keyPath, domains)
|| generateWithOpenssl(certPath, keyPath, domains, options.validityDays)
if (!generated) {
throw new Error('Could not generate a development certificate. Install `mkcert` (https://github.com/FiloSottile/mkcert) or provide `--https.cert` and `--https.key`.')
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Hardcoded 27-day cache TTL ignores --https.validityDays; can serve an expired self-signed cert.

CERT_MAX_AGE_MS is fixed at 27 days and only checks the cert file's mtime, but validityDays (used by the openssl fallback, Line 126) is user-configurable and can be set below 27. If someone runs with --https.validityDays 10, then reruns nuxt dev between day 10 and day 27, this code treats the cache as "fresh" and serves a certificate that has actually already expired, breaking HTTPS in the browser.

🔧 Suggested fix: bound the cache window by the configured validity
-  const fresh = existsSync(certPath)
-    && existsSync(keyPath)
-    && Date.now() - statSync(certPath).mtimeMs < CERT_MAX_AGE_MS
+  const maxAgeMs = Math.min(CERT_MAX_AGE_MS, (options.validityDays ?? Infinity) * 24 * 60 * 60 * 1000)
+  const fresh = existsSync(certPath)
+    && existsSync(keyPath)
+    && Date.now() - statSync(certPath).mtimeMs < maxAgeMs

Note: this also shortens the cache lifetime for mkcert-issued certs when validityDays happens to be set alongside --https, even though mkcert ignores that option — an acceptable trade-off, but worth a comment if you go this route.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const CERT_MAX_AGE_MS = 27 * 24 * 60 * 60 * 1000
async function generateCertificate(options: HTTPSOptions): Promise<ResolvedCertificate> {
const domains = options.domains?.length ? options.domains : ['localhost', '127.0.0.1', '::1']
const hash = createHash('sha256').update(domains.join(',')).digest('hex').slice(0, 8)
const dir = getCacheDir('certs', hash)
const certPath = join(dir, 'cert.pem')
const keyPath = join(dir, 'key.pem')
const fresh = existsSync(certPath)
&& existsSync(keyPath)
&& Date.now() - statSync(certPath).mtimeMs < CERT_MAX_AGE_MS
if (!fresh) {
const generated = await generateWithMkcert(certPath, keyPath, domains)
|| generateWithOpenssl(certPath, keyPath, domains, options.validityDays)
if (!generated) {
throw new Error('Could not generate a development certificate. Install `mkcert` (https://github.com/FiloSottile/mkcert) or provide `--https.cert` and `--https.key`.')
}
}
const CERT_MAX_AGE_MS = 27 * 24 * 60 * 60 * 1000
async function generateCertificate(options: HTTPSOptions): Promise<ResolvedCertificate> {
const domains = options.domains?.length ? options.domains : ['localhost', '127.0.0.1', '::1']
const hash = createHash('sha256').update(domains.join(',')).digest('hex').slice(0, 8)
const dir = getCacheDir('certs', hash)
const certPath = join(dir, 'cert.pem')
const keyPath = join(dir, 'key.pem')
const maxAgeMs = Math.min(CERT_MAX_AGE_MS, (options.validityDays ?? Infinity) * 24 * 60 * 60 * 1000)
const fresh = existsSync(certPath)
&& existsSync(keyPath)
&& Date.now() - statSync(certPath).mtimeMs < maxAgeMs
if (!fresh) {
const generated = await generateWithMkcert(certPath, keyPath, domains)
|| generateWithOpenssl(certPath, keyPath, domains, options.validityDays)
if (!generated) {
throw new Error('Could not generate a development certificate. Install `mkcert` (https://github.com/FiloSottile/mkcert) or provide `--https.cert` and `--https.key`.')
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxi/src/dev/cert.ts` around lines 45 - 64, Update
generateCertificate so the cache freshness threshold incorporates
options.validityDays rather than relying solely on the fixed CERT_MAX_AGE_MS
value. Bound the effective cache age by the configured certificate validity,
preserving the existing certificate generation flow and optionally documenting
that this also shortens mkcert certificate caching when validityDays is set.

Comment thread packages/nuxi/src/dev/listen.ts
Comment thread packages/nuxi/src/dev/tunnel.ts
@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing refactor/listhen (8351819) with main (882749f)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant