Skip to content

Commit

Permalink
feat: support show latest version, github star count etc
Browse files Browse the repository at this point in the history
  • Loading branch information
tjx666 committed Apr 3, 2024
1 parent 8aac267 commit dc52060
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 152 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
82 changes: 82 additions & 0 deletions src/apis.ts
Original file line number Diff line number Diff line change
@@ -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<NodeVersions> => {
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<P extends Promise<any>>(
promise: P,
): Promise<Awaited<P> | undefined> {
let resp: Awaited<P>;
try {
resp = await promise;
} catch {
return undefined;
}
return resp;
}
36 changes: 3 additions & 33 deletions src/codeLens/nodeVersion.ts
Original file line number Diff line number Diff line change
@@ -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<NodeVersions> => {
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:
| {
Expand Down Expand Up @@ -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(
Expand Down
154 changes: 110 additions & 44 deletions src/utils/pkg-hover-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)) {
Expand All @@ -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) {
Expand All @@ -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)}`;
Expand All @@ -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 += `<br/>${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 += `<br/><br/>${this.badgeInfos}`;
}

const contents = new MarkdownString(markdown);
contents.isTrusted = true;
Expand Down
Loading

0 comments on commit dc52060

Please sign in to comment.