From 9197676b3d09b77f51b17b63e5978db29e498ca4 Mon Sep 17 00:00:00 2001 From: shadcn Date: Mon, 15 Jun 2026 14:08:41 +0400 Subject: [PATCH] chore: replace node-fetch (#10905) * chore: replace node-fetch * fix(shadcn): surface network failure reason from fetch errors * fix(shadcn): avoid Error.cause typings for older TS lib targets * fix(shadcn): avoid stricter undici engine * fix(shadcn): use undici 7 proxy agent --- .changeset/lazy-buttons-fetch.md | 5 ++ packages/shadcn/package.json | 6 ++- packages/shadcn/src/registry/fetcher.ts | 10 +--- packages/shadcn/src/registry/github.ts | 12 ++--- packages/shadcn/src/registry/proxy.ts | 60 ++++++++++++++++++++++++ pnpm-lock.yaml | 61 ++++--------------------- 6 files changed, 83 insertions(+), 71 deletions(-) create mode 100644 .changeset/lazy-buttons-fetch.md create mode 100644 packages/shadcn/src/registry/proxy.ts diff --git a/.changeset/lazy-buttons-fetch.md b/.changeset/lazy-buttons-fetch.md new file mode 100644 index 0000000000..78e0e7761a --- /dev/null +++ b/.changeset/lazy-buttons-fetch.md @@ -0,0 +1,5 @@ +--- +"shadcn": patch +--- + +replace node-fetch with native fetch diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index 06bf69b91e..035d1b8e66 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -61,6 +61,9 @@ } }, "bin": "./dist/index.js", + "engines": { + "node": ">=20.18.1" + }, "scripts": { "dev": "tsup --watch", "build": "tsup", @@ -98,9 +101,7 @@ "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", - "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", - "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", @@ -111,6 +112,7 @@ "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", + "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" diff --git a/packages/shadcn/src/registry/fetcher.ts b/packages/shadcn/src/registry/fetcher.ts index 8bc76265df..9bed15b646 100644 --- a/packages/shadcn/src/registry/fetcher.ts +++ b/packages/shadcn/src/registry/fetcher.ts @@ -12,15 +12,10 @@ import { RegistryParseError, RegistryUnauthorizedError, } from "@/src/registry/errors" +import { fetchWithProxy } from "@/src/registry/proxy" import { registryItemSchema } from "@/src/schema" -import { HttpsProxyAgent } from "https-proxy-agent" -import fetch, { Headers } from "node-fetch" import { z } from "zod" -const agent = process.env.https_proxy - ? new HttpsProxyAgent(process.env.https_proxy) - : undefined - const registryCache = new Map>() export function clearRegistryCache() { @@ -59,8 +54,7 @@ export async function fetchRegistry( requestHeaders.set(key, value) } - const response = await fetch(url, { - agent, + const response = await fetchWithProxy(url, { headers: requestHeaders, }) diff --git a/packages/shadcn/src/registry/github.ts b/packages/shadcn/src/registry/github.ts index 5b06587898..9164593e59 100644 --- a/packages/shadcn/src/registry/github.ts +++ b/packages/shadcn/src/registry/github.ts @@ -5,21 +5,16 @@ import type { import { RegistryError, RegistrySourceFileError } from "@/src/registry/errors" import { resolveGitHubRef } from "@/src/registry/github-ref" import type { GitHubSource } from "@/src/registry/github-ref" +import { fetchWithProxy } from "@/src/registry/proxy" import { loadRegistryCatalogFromSource, loadRegistryItemFromSource, } from "@/src/registry/source" import type { RegistrySourceReader } from "@/src/registry/source" -import { HttpsProxyAgent } from "https-proxy-agent" -import fetch, { Headers } from "node-fetch" const GITHUB_RAW_URL = "https://raw.githubusercontent.com" const GITHUB_VALIDATION_CONCURRENCY = 8 -const agent = process.env.https_proxy - ? new HttpsProxyAgent(process.env.https_proxy) - : undefined - type GitHubItemAddress = Extract type GitHubRegistryValidationDiagnostic = { @@ -180,10 +175,9 @@ async function fetchGitHubSourceFile( filePath: string, address: GitHubSource ) { - let response: Awaited> + let response: Response try { - response = await fetch(url, { - agent, + response = await fetchWithProxy(url, { headers: new Headers({ "Accept-Encoding": "identity", "User-Agent": "shadcn", diff --git a/packages/shadcn/src/registry/proxy.ts b/packages/shadcn/src/registry/proxy.ts new file mode 100644 index 0000000000..9a28037f8f --- /dev/null +++ b/packages/shadcn/src/registry/proxy.ts @@ -0,0 +1,60 @@ +import { EnvHttpProxyAgent } from "undici" + +// Native fetch ignores the http.Agent-based `agent` option, so proxy support +// goes through an undici dispatcher instead. EnvHttpProxyAgent honors +// http_proxy, https_proxy and no_proxy (upper and lowercase). +const proxyDispatcher = + process.env.https_proxy || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.HTTP_PROXY + ? new EnvHttpProxyAgent() + : undefined + +export async function fetchWithProxy(url: string | URL, init?: RequestInit) { + try { + // The `dispatcher` option is supported by Node's fetch at runtime but + // missing from the ambient RequestInit type, hence the cast. + return await fetch(url, { + ...init, + dispatcher: proxyDispatcher, + } as RequestInit) + } catch (error) { + // Native fetch reports network failures as a generic "fetch failed" + // TypeError with the actual reason buried in `cause`. The casts are + // needed because the configured TS lib predates Error.cause. + const cause = + error instanceof TypeError + ? (error as TypeError & { cause?: unknown }).cause + : undefined + + if (cause) { + const enriched = new Error( + `Request to ${url} failed, reason: ${getFailureReason(cause)}` + ) as Error & { cause?: unknown } + enriched.cause = cause + throw enriched + } + + throw error + } +} + +function getFailureReason(cause: unknown): string { + // Connection failures surface as an AggregateError with an empty message + // and the per-address errors (e.g. ECONNREFUSED) in `errors`. + if (cause instanceof Error && "errors" in cause) { + const errors = (cause as Error & { errors: unknown }).errors + if (Array.isArray(errors) && errors.length) { + return getFailureReason(errors[0]) + } + } + + if (cause instanceof Error) { + return ( + cause.message || (cause as NodeJS.ErrnoException).code || "unknown error" + ) + } + + return String(cause) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf55ba6662..439fbbce68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,15 +428,9 @@ importers: fuzzysort: specifier: ^3.1.0 version: 3.1.0 - https-proxy-agent: - specifier: ^7.0.6 - version: 7.0.6 kleur: specifier: ^4.1.5 version: 4.1.5 - node-fetch: - specifier: ^3.3.2 - version: 3.3.2 open: specifier: ^11.0.0 version: 11.0.0 @@ -467,6 +461,9 @@ importers: tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + undici: + specifier: ^7.27.2 + version: 7.27.2 validate-npm-package-name: specifier: ^7.0.1 version: 7.0.1 @@ -4489,10 +4486,6 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -5145,10 +5138,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -5208,10 +5197,6 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -6545,11 +6530,6 @@ packages: sass: optional: true - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -6559,10 +6539,6 @@ packages: encoding: optional: true - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} @@ -8002,6 +7978,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.27.2: + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -8295,10 +8275,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -12241,8 +12217,6 @@ snapshots: dargs@8.1.0: {} - data-uri-to-buffer@4.0.1: {} - data-uri-to-buffer@6.0.2: {} data-view-buffer@1.0.2: @@ -13311,11 +13285,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -13389,10 +13358,6 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} fraction.js@4.3.7: {} @@ -14957,18 +14922,10 @@ snapshots: - '@babel/core' - babel-plugin-macros - node-domexception@1.0.0: {} - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-releases@2.0.21: {} normalize-path@3.0.0: {} @@ -16686,6 +16643,8 @@ snapshots: undici-types@6.21.0: {} + undici@7.27.2: {} + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} @@ -17029,8 +16988,6 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} - webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {}