feat: nudge when a newer nuxt release is available#1393
Conversation
commit: |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds 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)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
README.mdpackages/nuxt-cli/src/main.tspackages/nuxt-cli/src/utils/update.tspackages/nuxt-cli/test/unit/utils/update.spec.ts
| /** | ||
| * 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) | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
🔒 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:
- 1: https://www.mintlify.com/unjs/ofetch/api/types/fetch-error
- 2: https://unjs-ofetch.mintlify.app/api/types/fetch-error
- 3: https://mintlify.wiki/unjs/ofetch/features/error-handling
- 4: https://unjs-ofetch.mintlify.app/features/error-handling
🏁 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 -SRepository: 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]);
}
}
JSRepository: 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:
- 1: https://www.mintlify.com/unjs/ofetch/api/types/fetch-error
- 2: https://unjs-ofetch.mintlify.app/api/types/fetch-error
- 3: https://unjs-ofetch.mintlify.app/features/error-handling
- 4: https://github.com/unjs/ofetch/blob/main/src/types.ts
- 5: https://www.mintlify.com/unjs/ofetch/api/options/fetch-options
- 6: https://npmx.dev/package-changelog/ofetch/v/1.5.1
- 7: https://github.com/unjs/ofetch/blob/main/src/fetch.ts
🏁 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}')
PYRepository: 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}')
PYRepository: 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)
PYRepository: 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}')
PYRepository: 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]}')
PYRepository: 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]}')
PYRepository: 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:
- 1: https://github.com/sxzz/obug/
- 2: https://www.npmjs.com/package/obug
- 3: https://github.com/sxzz/obug
- 4: https://jsr.io/@sxzz/obug/doc/~/DebugOptions
- 5: sxzz/obug@c9c724c
- 6: https://jsr.io/@sxzz/obug/doc/all_symbols
- 7: sxzz/obug@565f0f2
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.
🔗 Linked issue
resolves #320
📚 Description
nuxtnow 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 suggestingnuxt upgradeit 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=1skips one run.updateCheck.enabled=falsein your.nuxtrcopts out for good.