Skip to content

feat: nudge when a newer nuxt release is available#1393

Merged
danielroe merged 2 commits into
mainfrom
feat/update-notifier
Jul 25, 2026
Merged

feat: nudge when a newer nuxt release is available#1393
danielroe merged 2 commits into
mainfrom
feat/update-notifier

Conversation

@danielroe

Copy link
Copy Markdown
Member

🔗 Linked issue

resolves #320

📚 Description

nuxt now checks the latest dist-tag once a day in the background, caches it in the user-level .nuxtrc, and prints a box after the command's own output suggesting nuxt upgrade

it works behind mirrors, and it stays quiet for prereleases, for patch bumps within the same minor unless you're 5+ patches behind (inspired by astro), in CI, on StackBlitz, and when stdout isn't a TTY

disabling update checks

NUXT_IGNORE_UPDATE_CHECK=1 skips one run.
updateCheck.enabled=false in your .nuxtrc opts out for good.

@pkg-pr-new

pkg-pr-new Bot commented Jul 25, 2026

Copy link
Copy Markdown
  • nuxt-cli-playground

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

commit: 8cc5b00

@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing feat/update-notifier (8cc5b00) with main (571cc3a)

Open in CodSpeed

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a95c0d45-d7f4-4f57-97b5-cf548e44185d

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbd53c and 8cc5b00.

📒 Files selected for processing (3)
  • README.md
  • packages/nuxt-cli/src/utils/update.ts
  • packages/nuxt-cli/test/unit/utils/update.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/nuxt-cli/test/unit/utils/update.spec.ts
  • packages/nuxt-cli/src/utils/update.ts

📝 Walkthrough

Walkthrough

Adds a cached Nuxt release checker to the CLI, with environment, terminal, CI, StackBlitz, and configuration opt-outs. It fetches registry metadata, filters prereleases and minor patch updates, renders width-aware notifications on process exit, and integrates scheduling into command setup. Unit tests cover detection, caching, scheduling, rendering, and failure handling. README documentation describes the behavior and configuration.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: notifying users when a newer Nuxt release is available.
Description check ✅ Passed The description matches the PR changes and explains the update-check behavior and opt-out options.
Linked Issues check ✅ Passed The PR satisfies #320 by adding a cached CLI check for the latest Nuxt release and showing an upgrade nudge.
Out of Scope Changes check ✅ Passed The README and test additions are directly tied to the update-check feature and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 feat/update-notifier

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.

@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: 1

🤖 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/nuxt-cli/src/utils/update.ts`:
- Around line 174-200: Sanitize fetch failure details before they reach debug
logging in scheduleUpdateNudge and the update-check flow, especially errors from
resolveLatestVersion whose options.headers may contain Authorization
credentials. Configure the relevant formatter to omit request/option headers, or
catch FetchError and pass only a sanitized message to debug while preserving the
existing nudge behavior.
🪄 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: 4fc6660e-d999-4283-aa6c-150a62cf8832

📥 Commits

Reviewing files that changed from the base of the PR and between 571cc3a and 5bbd53c.

📒 Files selected for processing (4)
  • README.md
  • packages/nuxt-cli/src/main.ts
  • packages/nuxt-cli/src/utils/update.ts
  • packages/nuxt-cli/test/unit/utils/update.spec.ts

Comment on lines +174 to +200
/**
* Check for a newer Nuxt release without blocking the command, deferring the
* nudge to process exit so it can never interleave with the command's own
* output (including the long-lived `dev` server's banner and logs).
*
* `upgrade` is excluded because the installed version is read when the command
* starts, so a nudge would still be pending once the upgrade has succeeded.
*/
export async function scheduleUpdateNudge(cwd: string, command?: string): Promise<void> {
if (command === 'upgrade' || !isUpdateCheckEnabled()) {
return
}

const update = await checkForNuxtUpdate(cwd)
if (!update) {
return
}

process.once('exit', () => {
try {
renderUpdateNudge(update)
}
catch (error) {
debug('Failed to render update nudge:', error)
}
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does ofetch's FetchError include request headers or options (e.g., Authorization) when thrown, and are they exposed via its message or properties?

💡 Result:

Yes, ofetch's FetchError includes the request options used for the failed request, and these options (which contain the headers, such as Authorization) are exposed via the options property of the FetchError instance [1][2][3][4]. When a request fails, ofetch throws a FetchError object [1][3]. This object provides access to the request configuration through its options property [1][3]. You can inspect these options—including any headers you originally provided in the headers object—by accessing error.options within a catch block [3][4]. It is important to note that while the headers are available within the options property, they are not directly exposed as individual properties on the FetchError instance itself, nor are they included in the default string representation of the error.message [3][4]. The error.message is formatted specifically as [METHOD] "url": status statusText errorMessage [1][3]. Example usage: try { await ofetch('/api/protected', { method: 'POST', headers: { Authorization: 'Bearer token' } }); } catch (error) { if (error instanceof FetchError) { console.log(error.options.headers); // Access the headers here } }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files matching update/logger/update versions:\n'
fd -a '^(update|logger)\.(ts|js)$|package\.json$' . | sed 's#^\./##' | head -100

printf '\nupdate.ts outline:\n'
ast-grep outline packages/nuxt-cli/src/utils/update.ts 2>/dev/null || true

printf '\nlogger.ts outline:\n'
fd -a 'logger\.ts$' packages/nuxt-cli/src packages | while read -r f; do
  printf '\n--- %s ---\n' "$f"
  ast-grop outline "$f" 2>/dev/null || ast-grep outline "$f" 2>/dev/null || true
done

printf '\nRelevant update.ts lines:\n'
cat -n packages/nuxt-cli/src/utils/update.ts | sed -n '130,220p'

printf '\nRelevant logger files:\n'
fd -a 'logger\.ts$' packages/nuxt-cli/src | while read -r f; do
  echo "--- $f ---"
  cat -n "$f" | sed -n '1,220p'
done

printf '\nSearch debug/logger usage and ofetch calls with Authorization:\n'
rg -n "debug\\(|ofetch\\(|Authorization|resolveLatestVersion|checkForNuxtUpdate" packages/nuxt-cli/src -S

Repository: nuxt/cli

Length of output: 8022


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'resolveLatestVersion function:\n'
cat -n packages/nuxt-cli/src/utils/update.ts | sed -n '66,97p'

printf '\nPackage metadata for dependencies:\n'
python3 - <<'PY'
import json
from pathlib import Path
p=Path('packages/nuxt-cli/package.json')
j=json.loads(p.read_text())
for k in ('dependencies','devDependencies'):
    print(f'[{k}]')
    for dep in ['ofetch','obug','`@clack/prompts`']:
        if dep in j.get(k,{}):
            print(f'{dep}: {j[k][dep]}')
PY

printf '\nRead package-lock / lockfiles for exact versions if present:\n'
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '"ofetch"|"obug"|`@nuxt/cli`|nuxt-cli' "$f" | head -80 || true
  fi
done

printf '\nFetchError definition from lockfile references (if accessible via npx package metadata not repo files):\n'
node - <<'JS'
const fs = require('fs');
for (const pkg of ['ofetch','obug','`@clack/prompts`']) {
  console.log(pkg);
  try {
    const p = require.resolve(`${pkg}/package.json`);
    console.log(p);
  } catch (e) {
    console.log('not resolved:', e.message.split('\n')[0]);
  }
}
JS

Repository: nuxt/cli

Length of output: 2062


🌐 Web query:

ofetch 1.5.1 source FetchError options headers property

💡 Result:

In ofetch 1.5.1, the FetchError class provides a structured way to handle request failures [1][2]. When a request fails, the resulting FetchError instance includes an options property, which contains the resolved fetch options that were used for that specific request [3]. The FetchError interface is defined as follows: export interface IFetchError<T = any> extends Error { request?: FetchRequest; options?: FetchOptions; // Resolved request options response?: FetchResponse; data?: T; status?: number; statusText?: string; statusCode?: number; statusMessage?: string; } Within this options property, the headers are present as part of the resolved FetchOptions [4][5]. As of ofetch 1.4 and onwards, these headers are normalized into a Headers object within the request context [6]. If you are inspecting the FetchError, the options.headers property will reflect these normalized headers used during the execution of the request [3][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Check whether npx can retrieve ofetch source from registry:\n'
npm view ofetch@1.5.1 dist.tarball --json 2>&1 | head -20

printf '\nProgrammatic extraction from fetched ofetch source bundle if npm registry access is available:\n'
python3 - <<'PY'
import json, subprocess, tarfile, io
try:
    r = subprocess.run(['npm','view','ofetch@1.5.1','dist.tarball'], text=True, capture_output=True, timeout=20)
    url = r.stdout.strip().splitlines()[-1] if r.stdout.strip() else ''
    if not url:
        print('npm view did not provide dist.tarball')
        raise SystemExit(0)
    print('tarball:', url)
    s = subprocess.run(['curl','-fsSL', url], capture_output=True, check=True)
    tf = tarfile.open(fileobj=io.BytesIO(s.stdout), mode='r:gz')
    for name in tf.getnames():
        if name in ['dist/index.mjs','dist/index.cjs','src/types.ts','src/fetch.ts']:
            f = tf.extractfile(name)
            text = f.read().decode('utf-8','replace')
            print(f'\n--- {name} ---')
            for i, line in enumerate(text.splitlines(), 1):
                if 'class FetchError' in line or 'IFetchError' in line or 'options' in line or 'headers' in line or 'toString' in line:
                    print(f'{i}: {line}')
except Exception as e:
    print(f'could not extract remote source: {type(e).__name__}: {e}')
PY

Repository: nuxt/cli

Length of output: 558


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import subprocess, tarfile, io, re, sys

url = subprocess.check_output(['npm','view','ofetch@1.5.1','dist.tarball'], text=True).strip().splitlines()[-1].splitlines()[-1]
print('tarball:', url)
data = subprocess.check_output(['curl','-fsSL', url])
tf = tarfile.open(fileobj=io.BytesIO(data), mode='r:gz')
files = {n: tf.extractfile(n).read().decode('utf-8','replace') for n in ('dist/index.mjs','dist/index.cjs','src/types.ts','src/fetch.ts')}
for path, text in files.items():
    if not text:
        continue
    print(f'\n--- {path} ---')
    for i, line in enumerate(text.splitlines(), 1):
        if any(tok in line for tok in ['class FetchError', 'throw new FetchError', 'IFetchError', 'request:', 'options:', 'response:', 'data:', 'statusCode:', 'statusMessage:', 'message:', 'toString']):
            print(f'{i}: {line}')
PY

Repository: nuxt/cli

Length of output: 770


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import subprocess, tarfile, io, sys

url = subprocess.check_output(['npm','view','ofetch@1.5.1','dist.tarball'], text=True).strip().splitlines()[-1].splitlines()[-1]
data = subprocess.check_output(['curl','-fsSL', url])
tf = tarfile.open(fileobj=io.BytesIO(data), mode='r:gz')
for name in tf.getnames():
    print(name)
PY

Repository: nuxt/cli

Length of output: 808


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import subprocess, tarfile, io, re

url = subprocess.check_output(['npm','view','ofetch@1.5.1','dist.tarball'], text=True).strip().splitlines()[-1].splitlines()[-1]
data = subprocess.check_output(['curl','-fsSL', url])
tf = tarfile.open(fileobj=io.BytesIO(data), mode='r:gz')

interesting = [
    'package/dist/index.mjs',
    'package/dist/index.cjs',
    'package/dist/index.d.ts',
    'package/dist/shared/ofetch.BbrTaNPp.d.mts',
    'package/dist/shared/ofetch.CWycOUEr.mjs',
]

for name in interesting:
    try:
        text = tf.extractfile(name).read().decode('utf-8','replace')
    except Exception as e:
        print(f'--- {name}: ERROR {e} ---')
        continue
    print(f'\n--- {name} ---')
    for i, line in enumerate(text.splitlines(), 1):
        if any(tok in line for tok in [
            'class FetchError', 'throw new FetchError', 'IFetchError',
            'FetchError', 'request:', 'options:', 'response:', 'statusCode:',
            'statusMessage:', 'getMessage', 'toString', 'headers'
        ]):
            print(f'{i}: {line}')
PY

Repository: nuxt/cli

Length of output: 3620


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import subprocess, tarfile, io

url = subprocess.check_output(['npm','view','ofetch@1.5.1','dist.tarball'], text=True).strip().splitlines()[-1].splitlines()[-1]
data = subprocess.check_output(['curl','-fsSL', url])
tf = tarfile.open(fileobj=io.BytesIO(data), mode='r:gz')
text = tf.extractfile('package/dist/shared/ofetch.CWycOUEr.mjs').read().decode('utf-8','replace').splitlines()

for idx, (start, depth, text) in enumerate([
    (4, 1, 'class FetchError'),
    (13, 0, 'function createFetchError'),
], start=0):
    print(f'\n--- {text} ---')
    base = getattr(range(start, min(start+60, len(text)+1)), '__len__', lambda: range(start, min(start+60, len(text)+1)))
PY

python3 - <<'PY'
import subprocess, tarfile, io

url = subprocess.check_output(['npm','view','ofetch@1.5.1','dist.tarball'], text=True).strip().splitlines()[-1].splitlines()[-1]
data = subprocess.check_output(['curl','-fsSL', url])
tf = tarfile.open(fileobj=io.BytesIO(data), mode='r:.gz')
text = tf.extractfile('package/dist/shared/ofetch.CWycOUEr.mjs').read().decode('utf-8','replace')
lines = text.splitlines()

print('FetchError class:')
for i in range(4, 14):
    print(f'{i}: {lines[i-1]}')

print('\ncreateFetchError function:')
for i in range(13, 36):
    print(f'{i}: {lines[i-1]}')
PY

Repository: nuxt/cli

Length of output: 764


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import subprocess, tarfile, io

url = subprocess.check_output(['npm','view','ofetch@1.5.1','dist.tarball'], text=True).strip().splitlines()[-1].splitlines()[-1]
data = subprocess.check_output(['curl','-fsSL', url])
tf = tarfile.open(fileobj=io.BytesIO(data), mode='r:gz')

lines = tf.extractfile('package/dist/shared/ofetch.CWycOUEr.mjs').read().decode('utf-8','replace').splitlines()
print('FetchError class:')
for i in range(4, 13):
    print(f'{i}: {lines[i-1]}')

print('\nfetchError.message construction:')
for i in range(13, 24):
    print(f'{i}: {lines[i-1]}')

print('\nthrow FetchError call:')
for i in range(185, 210):
    print(f'{i}: {lines[i-1]}')
PY

Repository: nuxt/cli

Length of output: 1985


🌐 Web query:

obug createDebug default log function argument serialization

💡 Result:

In the obug library, argument serialization is primarily handled through the formatArgs function, which is configurable within the DebugOptions object provided to createDebug [1][2]. By default, obug uses an internal mechanism to process and format arguments before they are passed to the log function [1][2]. The default log function typically writes to stderr in Node.js and to the console in browser environments [1][3]. Key points regarding argument handling and serialization: - Custom Serialization: Users can define a custom formatArgs function in the DebugOptions to control how arguments are serialized or transformed before being logged [1][4]. - Formatters: The library also supports a formatters object within DebugOptions, which allows for custom handling of specific format specifiers (e.g., %n) used in debug strings [5][6]. - Default Behavior: When not overridden, obug manages formatting automatically, including adding timestamps and namespace prefixes [1][7]. In Node.js, this default behavior can be further adjusted using inspectOpts, which provides options for object inspection depth and style [1][3]. - Overriding Logging: While formatArgs handles the serialization of the arguments array, the log option itself can be overridden to redirect output or change how the serialized result is ultimately presented (e.g., sending logs to a custom endpoint or file) [1][2]. For most use cases, the default serialization is sufficient, relying on standard console output methods, but the modularity of the library allows for granular control over both the preparation (formatArgs) and the final output (log) of debug messages [1][3].

Citations:


Redact auth headers before logging fetch failures.

resolveLatestVersion() passes Authorization: Bearer ${authToken} to $fetch()/ofetch, whose FetchError is available at error.options.headers. Since debug() receives the full error object, configure the formatter/log to omit request/option headers, or catch FetchError here and log only the sanitized message.

🤖 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/nuxt-cli/src/utils/update.ts` around lines 174 - 200, Sanitize fetch
failure details before they reach debug logging in scheduleUpdateNudge and the
update-check flow, especially errors from resolveLatestVersion whose
options.headers may contain Authorization credentials. Configure the relevant
formatter to omit request/option headers, or catch FetchError and pass only a
sanitized message to debug while preserving the existing nudge behavior.

@danielroe
danielroe merged commit 47b8387 into main Jul 25, 2026
19 checks passed
@danielroe
danielroe deleted the feat/update-notifier branch July 25, 2026 23:24
@github-actions github-actions Bot mentioned this pull request Jul 25, 2026
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.

Automatically check latest nuxt release and inform in CLI

1 participant