Skip to content

Commit ea82130

Browse files
committed
refactor: fetch JSON with native fetch instead of ofetch
1 parent 669bc5e commit ea82130

16 files changed

Lines changed: 96 additions & 51 deletions

File tree

packages/nuxt-cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
"magicast": "^0.5.3",
6464
"nypm": "^0.6.8",
6565
"obug": "^2.1.1",
66-
"ofetch": "^1.5.1",
6766
"ohash": "^2.0.11",
6867
"pathe": "^2.0.3",
6968
"perfect-debounce": "^2.1.0",

packages/nuxt-cli/src/commands/init.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { box, cancel, confirm, intro, isCancel, outro, S_BAR, select, spinner, t
1313
import { defineCommand, showUsage } from 'citty'
1414
import { downloadTemplate, startShell } from 'giget'
1515
import { detectPackageManager } from 'nypm'
16-
import { $fetch } from 'ofetch'
1716
import { basename, join, relative, resolve } from 'pathe'
1817
import colors from 'picocolors'
1918
import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types'
@@ -22,6 +21,7 @@ import { x } from 'tinyexec'
2221

2322
import { runCommandDef as runCommand } from '../run-command'
2423
import { nuxtIcon, themeColor } from '../utils/ascii'
24+
import { fetchJson } from '../utils/fetch'
2525
import { createInstallLog, resolvePackageManagerDescriptor, runInstall, takeUnreportedIgnoredBuilds } from '../utils/install'
2626
import { debug, logger } from '../utils/logger'
2727
import { classifyNetworkError, describeNetworkError, logNetworkError, probeNetworkError } from '../utils/network'
@@ -429,7 +429,7 @@ export default defineCommand({
429429
const nightlySpinner = spinner()
430430
nightlySpinner.start('Fetching nightly version info')
431431

432-
const response = await $fetch<{ 'dist-tags': Record<string, string> }>(NIGHTLY_DIST_TAGS_URL).catch((err) => {
432+
const response = await fetchJson<{ 'dist-tags': Record<string, string> }>(NIGHTLY_DIST_TAGS_URL).catch((err) => {
433433
nightlySpinner.error('Failed to fetch nightly version info')
434434
logNetworkError(err, { url: NIGHTLY_DIST_TAGS_URL })
435435
process.exit(1)
@@ -772,7 +772,7 @@ export default defineCommand({
772772
async function getModuleDependencies(moduleName: string) {
773773
const url = `https://registry.npmjs.org/${moduleName}/latest`
774774
try {
775-
const response = await $fetch(url)
775+
const response = await fetchJson<{ dependencies?: Record<string, string> }>(url)
776776
const dependencies = response.dependencies || {}
777777
return Object.keys(dependencies)
778778
}

packages/nuxt-cli/src/commands/module/_utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import type { PackageJson } from 'pkg-types'
44
import { existsSync } from 'node:fs'
55

66
import { confirm, isCancel } from '@clack/prompts'
7-
import { $fetch } from 'ofetch'
87
import { resolve } from 'pathe'
98
import colors from 'picocolors'
109
import { satisfies } from 'verkit'
1110

11+
import { fetchJson } from '../../utils/fetch'
1212
import { logger } from '../../utils/logger'
1313
import { relativeToProcess } from '../../utils/paths'
1414
import { cwdArgs, logLevelArgs } from '../_shared'
@@ -109,7 +109,7 @@ export interface NuxtModule {
109109
export const MODULES_API_URL = 'https://api.nuxt.com/modules?version=all'
110110

111111
export async function fetchModules(): Promise<NuxtModule[]> {
112-
const { modules } = await $fetch<NuxtApiModulesResponse>(MODULES_API_URL)
112+
const { modules } = await fetchJson<NuxtApiModulesResponse>(MODULES_API_URL)
113113
return modules
114114
}
115115

packages/nuxt-cli/src/commands/module/add.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import process from 'node:process'
88
import { cancel, confirm, isCancel, select, spinner } from '@clack/prompts'
99
import { defineCommand } from 'citty'
1010
import { detectPackageManager, packageManagers } from 'nypm'
11-
import { $fetch } from 'ofetch'
1211
import { resolve } from 'pathe'
1312
import colors from 'picocolors'
1413
import { readPackageJSON } from 'pkg-types'
@@ -17,6 +16,7 @@ import { satisfies } from 'verkit'
1716

1817
import { runCommandDef as runCommand } from '../../run-command'
1918
import { updateConfig } from '../../utils/config'
19+
import { fetchJson } from '../../utils/fetch'
2020
import { createInstallLog, resolvePackageManagerDescriptor, runInstall, takeUnreportedIgnoredBuilds } from '../../utils/install'
2121
import { logger } from '../../utils/logger'
2222
import { logNetworkError } from '../../utils/network'
@@ -447,7 +447,7 @@ async function resolveModule(moduleName: string, cwd: string): Promise<ModuleRes
447447

448448
// TODO: spinner
449449
const pkgUrl = joinURL(meta.registry, `${pkgName}`)
450-
const pkgDetails = await $fetch(pkgUrl, { headers }).catch((err: unknown) => {
450+
const pkgDetails = await fetchJson<any>(pkgUrl, { headers }).catch((err: unknown) => {
451451
logNetworkError(err, { url: pkgUrl, prefix: `Failed to fetch package details for ${colors.cyan(pkgName)}.` })
452452
return null
453453
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/** Status codes worth a second attempt: transient overload, throttling and gateway faults. */
2+
const RETRY_STATUS_CODES = new Set([408, 409, 425, 429, 500, 502, 503, 504])
3+
4+
export interface FetchJsonOptions {
5+
headers?: Record<string, string>
6+
/** Milliseconds before the request is aborted. Unlimited when omitted. */
7+
timeout?: number
8+
/** Extra attempts after the first. Defaults to one. */
9+
retry?: number
10+
}
11+
12+
interface FetchJsonError extends Error {
13+
status: number
14+
response: Response
15+
}
16+
17+
/**
18+
* `GET` a JSON document, throwing on a non-2xx response.
19+
*
20+
* Errors carry `status` and `response` so `classifyNetworkError` can tell an HTTP
21+
* failure from a transport failure. Transport errors and the status codes above
22+
* are retried, since `fetch` itself will not.
23+
*
24+
* This module deliberately has no imports: it is loaded directly by
25+
* `scripts/generate-data.ts` under Node's type stripping, where extensionless
26+
* relative specifiers do not resolve.
27+
*/
28+
export async function fetchJson<T>(url: string, options: FetchJsonOptions = {}): Promise<T> {
29+
const attempts = (options.retry ?? 1) + 1
30+
let lastError: unknown
31+
for (let attempt = 0; attempt < attempts; attempt++) {
32+
try {
33+
const response = await fetch(url, {
34+
headers: options.headers,
35+
signal: options.timeout ? AbortSignal.timeout(options.timeout) : undefined,
36+
})
37+
if (response.ok) {
38+
return await response.json() as T
39+
}
40+
lastError = Object.assign(
41+
new Error(`Request to ${url} failed with ${response.status} ${response.statusText}`),
42+
{ status: response.status, response },
43+
) satisfies FetchJsonError
44+
if (!RETRY_STATUS_CODES.has(response.status)) {
45+
break
46+
}
47+
}
48+
catch (error) {
49+
lastError = error
50+
// A timeout is a deliberate deadline, so retrying would silently double it.
51+
const name = (error as Error | undefined)?.name
52+
if (name === 'TimeoutError' || name === 'AbortError') {
53+
break
54+
}
55+
}
56+
}
57+
throw lastError
58+
}

packages/nuxt-cli/src/utils/starter-templates.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import process from 'node:process'
2-
import { $fetch } from 'ofetch'
2+
3+
import { fetchJson } from './fetch.ts'
34

45
export const hiddenTemplates = [
56
'doc-driven',
@@ -26,7 +27,6 @@ const fetchOptions = {
2627
// than 3s. The template list is prefetched and has a static fallback, so a
2728
// slightly longer deadline only ever delays the prompt on a broken network.
2829
timeout: 5000,
29-
responseType: 'json',
3030
headers: {
3131
'user-agent': '@nuxt/cli',
3232
...process.env.GITHUB_TOKEN ? { authorization: `token ${process.env.GITHUB_TOKEN}` } : {},
@@ -45,7 +45,7 @@ export async function getTemplates() {
4545
export async function fetchTemplates() {
4646
const templates = {} as Record<string, TemplateData>
4747

48-
const files = await $fetch<Array<{ name: string, type: string, download_url?: string }>>(
48+
const files = await fetchJson<Array<{ name: string, type: string, download_url?: string }>>(
4949
TEMPLATES_API_URL,
5050
fetchOptions,
5151
)
@@ -59,7 +59,7 @@ export async function fetchTemplates() {
5959
return
6060
}
6161
templates[templateName] = undefined as unknown as TemplateData
62-
templates[templateName] = await $fetch(file.download_url, fetchOptions)
62+
templates[templateName] = await fetchJson<TemplateData>(file.download_url, fetchOptions)
6363
}))
6464

6565
return templates

packages/nuxt-cli/src/utils/update.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import process from 'node:process'
22

33
import { box } from '@clack/prompts'
4-
import { $fetch } from 'ofetch'
54
import colors from 'picocolors'
65
import { readUser, updateUser } from 'rc9'
76
import { isCI, isTest, provider } from 'std-env'
87
import { joinURL } from 'ufo'
98
import { isGreater, tryParse } from 'verkit'
109

10+
import { fetchJson } from './fetch'
1111
import { debug } from './logger'
1212
import { detectNpmRegistry } from './registry'
1313

@@ -78,7 +78,7 @@ async function resolveLatestVersion(): Promise<string | undefined> {
7878
let latest: string | undefined
7979
try {
8080
const { registry, authToken } = await detectNpmRegistry(null)
81-
latest = (await $fetch<{ latest?: string }>(joinURL(registry, '-/package/nuxt/dist-tags'), {
81+
latest = (await fetchJson<{ latest?: string }>(joinURL(registry, '-/package/nuxt/dist-tags'), {
8282
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined,
8383
timeout: FETCH_TIMEOUT,
8484
retry: 0,

packages/nuxt-cli/src/utils/versions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { readFileSync } from 'node:fs'
22
import { resolveModulePath } from 'exsolve'
3-
import { $fetch } from 'ofetch'
43
import { readPackageJSON } from 'pkg-types'
54
import { joinURL } from 'ufo'
65
import { coerce, findMaxSatisfying } from 'verkit'
76

87
import { resolveCatalogEntry } from './catalog'
8+
import { fetchJson } from './fetch'
99
import { tryResolveNuxt } from './kit'
1010
import { debug } from './logger'
1111
import { detectNpmRegistry } from './registry'
@@ -45,7 +45,7 @@ export async function resolveRegistryVersion(pkg: string, range: string): Promis
4545
const scope = pkg.startsWith('@') ? pkg.split('/')[0]! : null
4646
const { registry, authToken } = await detectNpmRegistry(scope)
4747

48-
packument = await $fetch(joinURL(registry, pkg), {
48+
packument = await fetchJson(joinURL(registry, pkg), {
4949
headers: {
5050
// The abbreviated packument is a fraction of the size of the full one and
5151
// still carries every version and dist-tag.

packages/nuxt-cli/test/unit/commands/add.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ vi.mock('pkg-types', async () => {
4040
}
4141
})
4242

43-
vi.mock('ofetch', async () => {
43+
vi.mock('../../../src/utils/fetch', async () => {
4444
return {
45-
$fetch: mock$fetch,
45+
fetchJson: mock$fetch,
4646
}
4747
})
4848

packages/nuxt-cli/test/unit/commands/module/add-config.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ vi.mock('nypm', () => ({
2222
vi.mock('pkg-types', () => ({
2323
readPackageJSON: () => Promise.resolve({ devDependencies: { nuxt: '3.0.0' } }),
2424
}))
25-
vi.mock('ofetch', () => ({
26-
$fetch: vi.fn(() => Promise.resolve({
25+
vi.mock('../../../../src/utils/fetch', () => ({
26+
fetchJson: vi.fn(() => Promise.resolve({
2727
'dist-tags': { latest: '1.0.0' },
2828
'versions': { '1.0.0': manifest },
2929
})),

0 commit comments

Comments
 (0)