-
Notifications
You must be signed in to change notification settings - Fork 119
feat: nudge when a newer nuxt release is available #1393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+495
−1
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import process from 'node:process' | ||
|
|
||
| import { box } from '@clack/prompts' | ||
| import { $fetch } from 'ofetch' | ||
| import colors from 'picocolors' | ||
| import { readUser, updateUser } from 'rc9' | ||
| import { isCI, isTest, provider } from 'std-env' | ||
| import { joinURL } from 'ufo' | ||
| import { isGreater, tryParse } from 'verkit' | ||
|
|
||
| import { debug } from './logger' | ||
| import { detectNpmRegistry } from './registry' | ||
|
|
||
| const RC_FILE = '.nuxtrc' | ||
| const CACHE_KEY = 'updateCheck' | ||
| const CACHE_TTL = 24 * 60 * 60 * 1000 | ||
| const FETCH_TIMEOUT = 3000 | ||
|
|
||
| /** | ||
| * Patch releases within the same minor are only worth interrupting for once the | ||
| * user is meaningfully behind; a single patch is noise for anyone who upgrades | ||
| * regularly. | ||
| */ | ||
| const MIN_PATCH_DISTANCE = 5 | ||
|
|
||
| export interface NuxtUpdate { | ||
| current: string | ||
| latest: string | ||
| } | ||
|
|
||
| interface UpdateCache { | ||
| enabled?: boolean | ||
| latest?: string | ||
| checkedAt?: number | ||
| } | ||
|
|
||
| function readCache(): UpdateCache { | ||
| try { | ||
| return (readUser(RC_FILE)[CACHE_KEY] as UpdateCache | undefined) || {} | ||
| } | ||
| catch (error) { | ||
| debug('Failed to read update check cache:', error) | ||
| return {} | ||
| } | ||
| } | ||
|
|
||
| function writeCache(cache: UpdateCache) { | ||
| try { | ||
| updateUser({ [CACHE_KEY]: cache }, RC_FILE) | ||
| } | ||
| catch (error) { | ||
| debug('Failed to persist update check cache:', error) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * `NUXT_IGNORE_UPDATE_CHECK=1` (or the cross-tool `NO_UPDATE_NOTIFIER`) opts out | ||
| * for a single run and `updateCheck.enabled=false` in the user `.nuxtrc` opts | ||
| * out permanently. We also stay quiet where the nudge cannot be acted on | ||
| * interactively (CI, tests, StackBlitz, no TTY). | ||
| */ | ||
| export function isUpdateCheckEnabled(): boolean { | ||
| if (process.env.NUXT_IGNORE_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER) { | ||
| return false | ||
| } | ||
| if (readCache().enabled === false) { | ||
| return false | ||
| } | ||
| return !isCI && !isTest && provider !== 'stackblitz' && Boolean(process.stdout.isTTY) | ||
| } | ||
|
|
||
| async function resolveLatestVersion(): Promise<string | undefined> { | ||
| const cache = readCache() | ||
| if (cache.checkedAt && Date.now() - Number(cache.checkedAt) < CACHE_TTL) { | ||
| return cache.latest | ||
| } | ||
|
|
||
| let latest: string | undefined | ||
| try { | ||
| const { registry, authToken } = await detectNpmRegistry(null) | ||
| latest = (await $fetch<{ latest?: string }>(joinURL(registry, '-/package/nuxt/dist-tags'), { | ||
| headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined, | ||
| timeout: FETCH_TIMEOUT, | ||
| retry: 0, | ||
| })).latest | ||
| } | ||
| catch (error) { | ||
| debug('Failed to resolve the latest Nuxt version:', error) | ||
| } | ||
|
|
||
| // an unreachable or unauthenticated registry is recorded as a completed check | ||
| // so an offline user does not pay the request timeout on every command | ||
| writeCache({ latest, checkedAt: Date.now() }) | ||
| return latest | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the installed and latest Nuxt versions, returning nothing when the | ||
| * project is already current or when anything at all goes wrong (an offline | ||
| * user should never see an error from a check they did not ask for). | ||
| */ | ||
| export async function checkForNuxtUpdate(cwd: string): Promise<NuxtUpdate | undefined> { | ||
| try { | ||
| // imported lazily to keep package resolution off the CLI startup path | ||
| const [current, latest] = await Promise.all([ | ||
| import('./versions').then(({ getNuxtVersion }) => getNuxtVersion(cwd)).catch(() => undefined), | ||
| resolveLatestVersion(), | ||
| ]) | ||
|
|
||
| if (!current || !latest) { | ||
| return undefined | ||
| } | ||
|
|
||
| const installed = tryParse(current) | ||
| const published = tryParse(latest) | ||
| if (!installed || !published) { | ||
| return undefined | ||
| } | ||
|
|
||
| // prereleases are compared against `latest`, which is never the right | ||
| // baseline for someone tracking nightlies or release candidates | ||
| if (installed.prerelease) { | ||
| return undefined | ||
| } | ||
|
|
||
| if (!isGreater(latest, current)) { | ||
| return undefined | ||
| } | ||
|
|
||
| if ( | ||
| published.major === installed.major | ||
| && published.minor === installed.minor | ||
| && published.patch - installed.patch < MIN_PATCH_DISTANCE | ||
| ) { | ||
| return undefined | ||
| } | ||
|
|
||
| return { current, latest } | ||
| } | ||
| catch (error) { | ||
| debug('Failed to check for Nuxt updates:', error) | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| export function renderUpdateNudge({ current, latest }: NuxtUpdate): void { | ||
| const headline = `A new version of Nuxt is available: ${colors.green(latest)} ${colors.gray(`(you are on ${current})`)}` | ||
| const action = `Run ${colors.cyan('nuxt upgrade')} to update.` | ||
|
|
||
| process.stdout.write('\n') | ||
|
|
||
| // `box` cannot lay itself out without a known terminal width | ||
| const columns = process.stdout.columns | ||
| if (!columns || columns < 60) { | ||
| process.stdout.write(`${headline}\n${action}\n`) | ||
| return | ||
| } | ||
|
|
||
| box( | ||
| [ | ||
| headline, | ||
| '', | ||
| action, | ||
| ].join('\n'), | ||
| colors.green(' Update available '), | ||
| { | ||
| contentAlign: 'left', | ||
| titleAlign: 'left', | ||
| width: 'auto', | ||
| titlePadding: 2, | ||
| contentPadding: 2, | ||
| rounded: true, | ||
| withGuide: false, | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * 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) | ||
| } | ||
| }) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
optionsproperty of theFetchErrorinstance [1][2][3][4]. When a request fails, ofetch throws aFetchErrorobject [1][3]. This object provides access to the request configuration through itsoptionsproperty [1][3]. You can inspect these options—including any headers you originally provided in theheadersobject—by accessingerror.optionswithin acatchblock [3][4]. It is important to note that while the headers are available within theoptionsproperty, they are not directly exposed as individual properties on theFetchErrorinstance itself, nor are they included in the default string representation of theerror.message[3][4]. Theerror.messageis 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:
Repository: nuxt/cli
Length of output: 8022
🏁 Script executed:
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:
Repository: nuxt/cli
Length of output: 558
🏁 Script executed:
Repository: nuxt/cli
Length of output: 770
🏁 Script executed:
Repository: nuxt/cli
Length of output: 808
🏁 Script executed:
Repository: nuxt/cli
Length of output: 3620
🏁 Script executed:
Repository: nuxt/cli
Length of output: 764
🏁 Script executed:
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()passesAuthorization: Bearer ${authToken}to$fetch()/ofetch, whoseFetchErroris available aterror.options.headers. Sincedebug()receives the full error object, configure the formatter/log to omit request/option headers, or catchFetchErrorhere and log only the sanitized message.🤖 Prompt for AI Agents