From dc52060e44072a78571af7be27ab8ba3856f4db7 Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Wed, 3 Apr 2024 23:32:19 +0800 Subject: [PATCH] feat: support show latest version, github star count etc --- README.md | 5 +- src/apis.ts | 82 ++++++++++ src/codeLens/nodeVersion.ts | 36 +--- src/utils/pkg-hover-contents.ts | 154 +++++++++++++----- src/utils/pkg-info.ts | 80 +-------- test-workspace/pkg-json-codelens/index.ts | 1 + test-workspace/pkg-json-codelens/package.json | 1 + 7 files changed, 207 insertions(+), 152 deletions(-) create mode 100644 src/apis.ts diff --git a/README.md b/README.md index 151173e..b3d04fb 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,8 @@ useful when you refactor code from one package to another new package. - package hover tip - [ ] cnpm sync - - [ ] websites: npm trending, npm view,npm graph etc - [ ] changelog - - [ ] download count - - [ ] star count - - [ ] issue count + - [ ] websites: npm trending, npm view,npm graph etc - [ ] `pnpm why` visualization - [ ] `.npmrc` autocomplete diff --git a/src/apis.ts b/src/apis.ts new file mode 100644 index 0000000..551dde9 --- /dev/null +++ b/src/apis.ts @@ -0,0 +1,82 @@ +import allNodeVersions from 'all-node-versions'; +import axios from 'axios'; +import ExpiryMap from 'expiry-map'; +import pMemoize from 'p-memoize'; +import fetchPackageJson from 'package-json'; +import type { PackageJson } from 'type-fest'; + +const min = 1000 * 60; + +export type NodeVersions = + | { + satisfied: string; + latest: string; + } + | undefined; + +export const fetchNodeVersions = (() => { + const cache = new ExpiryMap(min * 2); + const request = async (version: string): Promise => { + const { versions, majors } = await allNodeVersions({ + mirror: 'https://npmmirror.com/mirrors/node', + }); + + // version not found + if (versions.every((v) => !v.node.startsWith(version))) { + return undefined; + } + + const majorNumber = Number(version.split('.')[0]); + const satisfied = majors.find((major) => major.major === majorNumber)!.latest; + const latest = majors.find((major) => major.lts)!.latest; + return { + satisfied, + latest, + }; + }; + return pMemoize(request, { cache }); +})(); + +export const fetchRemotePackageJson = (() => { + const cache = new ExpiryMap(min * 5); + const request = async (pkgName: string, pkgVersion?: string) => { + const pkgNameAndVersion = `${pkgName}${pkgVersion ? `@${pkgVersion}` : ''}`; + return fetchPackageJson(pkgNameAndVersion, { + fullMetadata: true, + }) as unknown as PackageJson | undefined; + }; + return pMemoize(request, { cache }); +})(); + +/** + * Fetch name@version by bundlephobia api + */ +export const fetchBundleSize = (() => { + const request = async (pkgNameAndVersion: string) => { + const url = `https://bundlephobia.com/api/size?package=${pkgNameAndVersion}`; + const { data } = await axios.get<{ gzip?: number; size?: number }>(url, { + timeout: 5 * 1000, + }); + if (data && typeof data.size === 'number') { + return { + gzip: data.gzip!, + normal: data.size, + }; + } + return undefined; + }; + // !: cache forever + return pMemoize(request); +})(); + +export async function tryFetch

>( + promise: P, +): Promise | undefined> { + let resp: Awaited

; + try { + resp = await promise; + } catch { + return undefined; + } + return resp; +} diff --git a/src/codeLens/nodeVersion.ts b/src/codeLens/nodeVersion.ts index 579540e..f27d5f2 100644 --- a/src/codeLens/nodeVersion.ts +++ b/src/codeLens/nodeVersion.ts @@ -1,42 +1,12 @@ -import allNodeVersions from 'all-node-versions'; -import ExpiryMap from 'expiry-map'; -import pMemoize from 'p-memoize'; import type { CancellationToken, ExtensionContext, TextDocument } from 'vscode'; import { CodeLens, Range } from 'vscode'; +import type { NodeVersions } from '../apis'; +import { fetchNodeVersions, tryFetch } from '../apis'; import { configuration, configurationKeys } from '../configuration'; import { commands } from '../utils/constants'; import { BaseCodeLensProvider } from './BaseCodeLensProvider'; -type NodeVersions = { - satisfied: string; - latest: string; -} | null; - -// 2 mins -const cache = new ExpiryMap(1000 * 60 * 2); -const fetchNodeVersions = pMemoize( - async (version: string): Promise => { - const { versions, majors } = await allNodeVersions({ - mirror: 'https://npmmirror.com/mirrors/node', - }); - - // version not found - if (versions.every((v) => !v.node.startsWith(version))) { - return null; - } - - const majorNumber = Number(version.split('.')[0]); - const satisfied = majors.find((major) => major.major === majorNumber)!.latest; - const latest = majors.find((major) => major.lts)!.latest; - return { - satisfied, - latest, - }; - }, - { cache }, -); - export class NodeVersionCodeLensProvider extends BaseCodeLensProvider { private _codelensData: | { @@ -68,7 +38,7 @@ export class NodeVersionCodeLensProvider extends BaseCodeLensProvider { const version = match[1]; this._codelensData = { version, - fetchNodeVersionsPromise: fetchNodeVersions(version), + fetchNodeVersionsPromise: tryFetch(fetchNodeVersions(version)), }; return [ new CodeLens( diff --git a/src/utils/pkg-hover-contents.ts b/src/utils/pkg-hover-contents.ts index 738ca12..690a49f 100644 --- a/src/utils/pkg-hover-contents.ts +++ b/src/utils/pkg-hover-contents.ts @@ -2,7 +2,7 @@ import { join } from 'node:path'; import hostedGitInfo from 'hosted-git-info'; import { isObject } from 'lodash-es'; -import { env, MarkdownString, Uri } from 'vscode'; +import { MarkdownString, Uri } from 'vscode'; import { spacing } from '.'; import { PACKAGE_JSON } from './constants'; @@ -19,13 +19,6 @@ function tryGetUrl(val: string | { url?: string | undefined } | undefined) { return undefined; } -function getPkgNameAndVersion(packageInfo: PackageInfo) { - return ( - packageInfo.name + - ((packageInfo as any).installedVersion ? `@${(packageInfo as any).installedVersion}` : '') - ); -} - function extractGitUrl(url: string) { let result: string | undefined; if (/^https?:\/\/.*/i.test(url)) { @@ -43,14 +36,41 @@ function extractGitUrl(url: string) { class PkgHoverContentsCreator { packageInfo!: PackageInfo; + get packageNameAndVersion() { + const { packageInfo } = this; + return ( + packageInfo.name + + ((packageInfo as any).installedVersion + ? `@${(packageInfo as any).installedVersion}` + : '') + ); + } + + get githubUserAndRepo() { + if (this.packageInfo.isBuiltinModule) return; + let repositoryUrl = tryGetUrl(this.packageInfo.packageJson.repository); + if (repositoryUrl) { + repositoryUrl = extractGitUrl(repositoryUrl); + } + + if (repositoryUrl?.startsWith('https://github')) { + return repositoryUrl.split('/').slice(-2).join('/'); + } + return undefined; + } + get pkgName() { + return this.packageInfo.name; + } + + get pkgNameLink() { const packageInfo = this.packageInfo; let packageName: string, showTextDocumentCmdUri: Uri | undefined; if (packageInfo.isBuiltinModule) { packageName = packageInfo.name; } else { - packageName = getPkgNameAndVersion(packageInfo); + packageName = this.packageNameAndVersion; const pkgJsonPath = packageInfo.installDir && join(packageInfo.installDir, PACKAGE_JSON); if (pkgJsonPath) { @@ -71,41 +91,36 @@ class PkgHoverContentsCreator { get pkgUrl() { const packageInfo = this.packageInfo; + if (packageInfo.isBuiltinModule) return; - let homepageUrl: string | undefined, - repositoryUrl: string | undefined, - npmUrl: string | undefined; - if (packageInfo.isBuiltinModule) { - homepageUrl = `https://nodejs.org/${env.language}/`; - repositoryUrl = 'https://github.com/nodejs/node'; - } else { - homepageUrl = tryGetUrl(packageInfo.packageJson.homepage); - repositoryUrl = tryGetUrl(packageInfo.packageJson.repository); + let homepageUrl: string | undefined, repositoryUrl: string | undefined; - if (repositoryUrl) { - repositoryUrl = extractGitUrl(repositoryUrl); - } + homepageUrl = tryGetUrl(packageInfo.packageJson.homepage); + repositoryUrl = tryGetUrl(packageInfo.packageJson.repository); - if (!repositoryUrl) { - let bugsUrl = tryGetUrl(packageInfo.packageJson.bugs); - if (bugsUrl) { - const idx = bugsUrl.indexOf('/issues'); - if (idx !== -1) { - bugsUrl = bugsUrl.slice(0, idx); - } - repositoryUrl = extractGitUrl(bugsUrl); - } - } + if (repositoryUrl) { + repositoryUrl = extractGitUrl(repositoryUrl); + } - if (repositoryUrl === homepageUrl) { - homepageUrl = undefined; + if (!repositoryUrl) { + let bugsUrl = tryGetUrl(packageInfo.packageJson.bugs); + if (bugsUrl) { + const idx = bugsUrl.indexOf('/issues'); + if (idx !== -1) { + bugsUrl = bugsUrl.slice(0, idx); + } + repositoryUrl = extractGitUrl(bugsUrl); } + } - npmUrl = `https://www.npmjs.com/package/${packageInfo.name}${ - packageInfo.installedVersion ? `/v/${packageInfo.installedVersion}` : '' - }`; + if (repositoryUrl === homepageUrl) { + homepageUrl = undefined; } + const npmUrl = `https://www.npmjs.com/package/${packageInfo.name}${ + packageInfo.installedVersion ? `/v/${packageInfo.installedVersion}` : '' + }`; + let result = ''; if (npmUrl) { result += `[NPM](${npmUrl})${spacing(4)}`; @@ -122,20 +137,71 @@ class PkgHoverContentsCreator { get bundleSize() { const packageInfo = this.packageInfo; - let result = ''; - if (!packageInfo.isBuiltinModule && packageInfo.webpackBundleSize) { - result = `[BundleSize](https://bundlephobia.com/package/${getPkgNameAndVersion(packageInfo)}):${spacing(1)}${formatSize( - packageInfo.webpackBundleSize.normal, - )}${spacing(1)}(gzip:${spacing(1)}${formatSize(packageInfo.webpackBundleSize.gzip)})`; + if (!packageInfo.isBuiltinModule && packageInfo.bundleSize) { + const { normal, gzip } = packageInfo.bundleSize; + const bundlephobiaWebsite = `https://bundlephobia.com/package/${this.packageNameAndVersion}`; + return `[![bundle size](https://img.shields.io/badge/bundle_size-${formatSize(normal)}_(gzip%3A_${formatSize(gzip)})-green)](${bundlephobiaWebsite})`; } - return result; + return undefined; + } + + get latestVersion() { + const badge = `![latest version](https://img.shields.io/npm/v/${this.pkgName}?label=latest)`; + return `[${badge}](https://www.npmjs.com/package/${this.pkgName})`; + } + + get downloadCountPerWeek() { + const badge = `![NPM Downloads](https://img.shields.io/npm/dw/${this.pkgName})`; + return `[${badge}](https://www.npmjs.com/package/${this.pkgName}?activeTab=versions)`; + } + + get githubStar() { + const { githubUserAndRepo } = this; + if (!githubUserAndRepo) return; + + const badge = `![GitHub Repo stars](https://img.shields.io/github/stars/${githubUserAndRepo})`; + return `[${badge}](https://github.com/${githubUserAndRepo})`; + } + + get githubIssueCount() { + const { githubUserAndRepo } = this; + if (!githubUserAndRepo) return; + + const badge = `![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/${githubUserAndRepo})`; + return `[${badge}](https://github.com/${githubUserAndRepo}/issues)`; + } + + get typeDefinition() { + const badge = `![NPM Type Definitions](https://img.shields.io/npm/types/${this.pkgName})`; + return `[${badge}](https://arethetypeswrong.github.io/?p=${this.packageNameAndVersion})`; + } + + get badgeInfos() { + return [ + this.latestVersion, + this.downloadCountPerWeek, + this.bundleSize, + this.githubStar, + this.githubIssueCount, + this.typeDefinition, + ] + .filter(Boolean) + .join(spacing(3)); } generate(packageInfo: PackageInfo): MarkdownString { this.packageInfo = packageInfo; - let markdown = `${this.pkgName}${spacing(2)}${this.pkgUrl}`; - markdown += `
${this.bundleSize}`; + let markdown = `${this.pkgNameLink}${spacing(2)}`; + if (this.packageInfo.isBuiltinModule) { + const homepageUrl = `https://nodejs.org/docs/latest/api/${this.pkgName}.html`; + const repositoryUrl = `https://github.com/nodejs/node/blob/main/lib/${this.pkgName}.js`; + markdown += `[HomePage](${homepageUrl})${spacing(4)}`; + markdown += `[Repository](${repositoryUrl})`; + } else { + markdown += this.pkgUrl; + markdown += `

${this.badgeInfos}`; + } const contents = new MarkdownString(markdown); contents.isTrusted = true; diff --git a/src/utils/pkg-info.ts b/src/utils/pkg-info.ts index 0a83cf9..b4a5fcf 100644 --- a/src/utils/pkg-info.ts +++ b/src/utils/pkg-info.ts @@ -1,13 +1,10 @@ import { resolve } from 'node:path'; -import axios from 'axios'; import isBuiltinModule from 'is-builtin-module'; -import { LRUCache } from 'lru-cache'; -import _fetchPackageJson from 'package-json'; import type { PackageJson } from 'type-fest'; import type { CancellationToken } from 'vscode'; -import { promiseDebounce } from '.'; +import { fetchBundleSize, fetchRemotePackageJson, tryFetch } from '../apis'; import { PACKAGE_JSON } from './constants'; import { readJsonFile } from './fs'; @@ -19,7 +16,7 @@ interface PackageJsonData { bugs?: string | { url?: string }; } -interface WebpackBundleSize { +interface BundleSize { gzip: number; normal: number; } @@ -32,70 +29,11 @@ type PackageInfo = isBuiltinModule: false; installedVersion?: string; installDir?: string; - webpackBundleSize?: WebpackBundleSize; + bundleSize?: BundleSize; packageJson: PackageJson; } | { isBuiltinModule: true; name: string }; -const fetchPackageJson = promiseDebounce(_fetchPackageJson, (pkgNameAndRangeVersion: string) => { - return pkgNameAndRangeVersion; -}); -const remotePkgMetadataCache = new LRUCache({ - max: 100, - // 10 mins - ttl: 1000 * 60 * 10, -}); -async function getRemotePackageJsonData(pkgName: string, pkgVersion?: string) { - const pkgNameAndVersion = `${pkgName}${pkgVersion ? `@${pkgVersion}` : ''}`; - if (!remotePkgMetadataCache.has(pkgNameAndVersion)) { - const pkgJsonData = (await fetchPackageJson(pkgNameAndVersion, { - fullMetadata: true, - })) as unknown as PackageJson | undefined; - if (pkgJsonData) { - remotePkgMetadataCache.set(pkgNameAndVersion, pkgJsonData); - return pkgJsonData; - } - return undefined; - } else { - return remotePkgMetadataCache.get(pkgNameAndVersion)!; - } -} - -const getBundlephobiaApiSize = promiseDebounce( - (pkgNameAndVersion: string) => { - const url = `https://bundlephobia.com/api/size?package=${pkgNameAndVersion}`; - return axios - .get<{ gzip?: number; size?: number }>(url, { - timeout: 5 * 1000, - }) - .catch(() => undefined); - }, - (pkgNameAndVersion: string) => pkgNameAndVersion, -); - -const pkgWebpackBundleSizeCache = new LRUCache({ - max: 100, - ttl: 1000 * 60 * 10, -}); - -async function getPkgWebpackBundleSize(pkgNameAndVersion: string) { - let bundleSizeInfo = pkgWebpackBundleSizeCache.get(pkgNameAndVersion); - if (!bundleSizeInfo) { - const resp = await getBundlephobiaApiSize(pkgNameAndVersion); - if (!resp) return; - - const { data } = resp; - if (data && typeof data.size === 'number') { - bundleSizeInfo = { - gzip: data.gzip!, - normal: data.size, - }; - pkgWebpackBundleSizeCache.set(pkgNameAndVersion, bundleSizeInfo); - } - } - return bundleSizeInfo; -} - const getPackageInfoDefaultOptions = { remoteFetch: true, fetchBundleSize: true, @@ -150,7 +88,7 @@ async function getPackageInfo( if (!result && options.remoteFetch) { const remotePackageJsonData = await (!options.token?.isCancellationRequested && - getRemotePackageJsonData(packageName, options.searchVersionRange)); + tryFetch(fetchRemotePackageJson(packageName, options.searchVersionRange))); if (remotePackageJsonData) { result = { name: packageName, @@ -163,10 +101,10 @@ async function getPackageInfo( if (result && options.fetchBundleSize) { const pkgNameAndVersion = `${result.name}@${(result as any).version}`; - const webpackBundleSize = await (!options.token?.isCancellationRequested && - getPkgWebpackBundleSize(pkgNameAndVersion)); - if (webpackBundleSize) { - (result as any).webpackBundleSize = webpackBundleSize; + const bundleSize = await (!options.token?.isCancellationRequested && + tryFetch(fetchBundleSize(pkgNameAndVersion))); + if (bundleSize) { + (result as any).bundleSize = bundleSize; } } @@ -174,4 +112,4 @@ async function getPackageInfo( } export { getPackageInfo }; -export type { PackageInfo, PackageJsonData, WebpackBundleSize }; +export type { BundleSize, PackageInfo, PackageJsonData }; diff --git a/test-workspace/pkg-json-codelens/index.ts b/test-workspace/pkg-json-codelens/index.ts index 6af3df1..79b8433 100644 --- a/test-workspace/pkg-json-codelens/index.ts +++ b/test-workspace/pkg-json-codelens/index.ts @@ -13,6 +13,7 @@ import type { } from 'vscode'; import * as e from 'execa' +import {resolve} from 'path' console.log(lodash); diff --git a/test-workspace/pkg-json-codelens/package.json b/test-workspace/pkg-json-codelens/package.json index f16b2f2..6d09465 100644 --- a/test-workspace/pkg-json-codelens/package.json +++ b/test-workspace/pkg-json-codelens/package.json @@ -12,6 +12,7 @@ "!src/exclude" ], "dependencies": { + "@yutengjing/eslint-config-typescript": "^0.6.0", "axios": "^1.6.8", "lodash": "^4.17.21", "bbb": "233",