Skip to content

Commit 144e806

Browse files
committed
feat: add poc for github stars, github issues & created at comparison
1 parent 0164064 commit 144e806

File tree

6 files changed

+186
-3
lines changed

6 files changed

+186
-3
lines changed

app/composables/useFacetSelection.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ export function useFacetSelection(queryParam = 'facets') {
8989
description: t(`compare.facets.items.deprecated.description`),
9090
chartable: false,
9191
},
92+
githubStars: {
93+
label: t(`compare.facets.items.github_stars.label`),
94+
description: t(`compare.facets.items.github_stars.description`),
95+
chartable: true,
96+
},
97+
githubIssues: {
98+
label: t(`compare.facets.items.github_issues.label`),
99+
description: t(`compare.facets.items.github_issues.description`),
100+
chartable: true,
101+
},
102+
createdAt: {
103+
label: t(`compare.facets.items.created_at.label`),
104+
description: t(`compare.facets.items.created_at.description`),
105+
chartable: false,
106+
},
92107
}),
93108
)
94109

app/composables/usePackageComparison.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,14 @@ export interface PackageComparisonData {
4444
* but a maintainer was removed last week, this would show the '3 years ago' time.
4545
*/
4646
lastUpdated?: string
47+
/** Creation date of the package (ISO 8601 date-time string) */
48+
createdAt?: string
4749
engines?: { node?: string; npm?: string }
48-
deprecated?: string
50+
deprecated?: string,
51+
github?: {
52+
stars?: number
53+
issues?: number
54+
}
4955
}
5056
/** Whether this is a binary-only package (CLI without library entry points) */
5157
isBinaryOnly?: boolean
@@ -115,12 +121,11 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
115121
try {
116122
// Fetch basic package info first (required)
117123
const { data: pkgData } = await $npmRegistry<Packument>(`/${encodePackageName(name)}`)
118-
119124
const latestVersion = pkgData['dist-tags']?.latest
120125
if (!latestVersion) return null
121126

122127
// Fetch fast additional data in parallel (optional - failures are ok)
123-
const [downloads, analysis, vulns, likes] = await Promise.all([
128+
const [downloads, analysis, vulns, likes, ghStars, ghIssues] = await Promise.all([
124129
$fetch<{ downloads: number }>(
125130
`https://api.npmjs.org/downloads/point/last-week/${encodePackageName(name)}`,
126131
).catch(() => null),
@@ -133,6 +138,8 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
133138
$fetch<PackageLikes>(`/api/social/likes/${encodePackageName(name)}`).catch(
134139
() => null,
135140
),
141+
$fetch<{ repo: { stars: number } }>(`https://ungh.cc/repos/${parseRepositoryInfo(pkgData.repository)?.owner}/${parseRepositoryInfo(pkgData.repository)?.repo}`).then(res => res?.repo.stars || 0).catch(() => null),
142+
$fetch<{issues: number}>(`/api/github/issues/${parseRepositoryInfo(pkgData.repository)?.owner}/${parseRepositoryInfo(pkgData.repository)?.repo}`).then(res => res?.issues || 0).catch(() => null),
136143
])
137144
const versionData = pkgData.versions[latestVersion]
138145
const packageSize = versionData?.dist?.unpackedSize
@@ -179,8 +186,13 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
179186
// Use version-specific publish time, NOT time.modified (which can be
180187
// updated by metadata changes like maintainer additions)
181188
lastUpdated: pkgData.time?.[latestVersion],
189+
createdAt: pkgData.time?.created,
182190
engines: analysis?.engines,
183191
deprecated: versionData?.deprecated,
192+
github: {
193+
stars: ghStars ?? undefined,
194+
issues: ghIssues ?? undefined,
195+
},
184196
},
185197
isBinaryOnly: isBinary,
186198
totalLikes: likes?.totalLikes,
@@ -252,6 +264,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
252264

253265
return packagesData.value.map(pkg => {
254266
if (!pkg) return null
267+
console.log(pkg.package.name, {pkg})
255268
return computeFacetValue(
256269
facet,
257270
pkg,
@@ -538,6 +551,43 @@ function computeFacetValue(
538551
status: totalDepCount > 50 ? 'warning' : 'neutral',
539552
}
540553
}
554+
case 'githubStars': {
555+
const stars = data.metadata?.github?.stars
556+
if (stars == null) return null
557+
return {
558+
raw: stars,
559+
display: formatCompactNumber(stars),
560+
status: stars > 1000 ? 'good' : 'neutral',
561+
}
562+
}
563+
case 'githubIssues': {
564+
const issues = data.metadata?.github?.issues
565+
if (issues == null) return null
566+
const stars = data.metadata?.github?.stars
567+
const ratio = stars && issues > 0 ? issues / stars : null
568+
return {
569+
raw: issues,
570+
display: formatCompactNumber(issues),
571+
// High issues-to-stars ratio suggests the project is struggling relative to its popularity
572+
status: ratio == null || ratio < 0.1 ? 'good' : ratio < 0.5 ? 'neutral' : 'warning',
573+
}
574+
}
575+
case 'createdAt': {
576+
const createdAt = data.metadata?.createdAt
577+
const resolved = createdAt ? resolveNoDependencyDisplay(createdAt, t) : null
578+
if (resolved) return { raw: 0, ...resolved }
579+
if (!createdAt) return null
580+
const date = new Date(createdAt)
581+
const oneMonthAgo = new Date()
582+
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1)
583+
return {
584+
raw: date.getTime(),
585+
display: createdAt,
586+
// Package is rated "good" if it was created more than a month ago (not brand new)
587+
status: date < oneMonthAgo ? 'good' : 'neutral',
588+
type: 'date',
589+
}
590+
}
541591
default: {
542592
return null
543593
}

i18n/locales/en.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,18 @@
13431343
"vulnerabilities": {
13441344
"label": "Vulnerabilities",
13451345
"description": "Known security vulnerabilities"
1346+
},
1347+
"github_stars": {
1348+
"label": "GitHub Stars",
1349+
"description": "Number of stars on the GitHub repository"
1350+
},
1351+
"github_issues": {
1352+
"label": "GitHub Issues",
1353+
"description": "Number of issues on the GitHub repository"
1354+
},
1355+
"created_at": {
1356+
"label": "Created At",
1357+
"description": "When the package was created"
13461358
}
13471359
},
13481360
"values": {

i18n/schema.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4035,6 +4035,39 @@
40354035
}
40364036
},
40374037
"additionalProperties": false
4038+
},
4039+
"github_stars": {
4040+
"type": "object",
4041+
"properties": {
4042+
"label": {
4043+
"type": "string"
4044+
},
4045+
"description": {
4046+
"type": "string"
4047+
}
4048+
}
4049+
},
4050+
"github_issues": {
4051+
"type": "object",
4052+
"properties": {
4053+
"label": {
4054+
"type": "string"
4055+
},
4056+
"description": {
4057+
"type": "string"
4058+
}
4059+
}
4060+
},
4061+
"created_at": {
4062+
"type": "object",
4063+
"properties": {
4064+
"label": {
4065+
"type": "string"
4066+
},
4067+
"description": {
4068+
"type": "string"
4069+
}
4070+
}
40384071
}
40394072
},
40404073
"additionalProperties": false
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
2+
3+
const GITHUB_HEADERS = {
4+
'Accept': 'application/vnd.github.v3+json',
5+
'User-Agent': 'npmx',
6+
'X-GitHub-Api-Version': '2022-11-28',
7+
} as const
8+
9+
interface GitHubSearchResponse {
10+
total_count: number
11+
}
12+
13+
export interface GithubIssueCountResponse {
14+
owner: string
15+
repo: string
16+
issues: number
17+
}
18+
19+
export default defineCachedEventHandler(
20+
async (event): Promise<GithubIssueCountResponse> => {
21+
const owner = getRouterParam(event, 'owner')
22+
const repo = getRouterParam(event, 'repo')
23+
24+
if (!owner || !repo) {
25+
throw createError({
26+
statusCode: 400,
27+
statusMessage: 'Owner and repo are required parameters.',
28+
})
29+
}
30+
31+
const query = `repo:${owner}/${repo} is:issue is:open`
32+
const url = `https://api.github.com/search/issues?q=${encodeURIComponent(query)}&per_page=1`
33+
34+
try {
35+
const data = await $fetch<GitHubSearchResponse>(url, {
36+
headers: GITHUB_HEADERS,
37+
})
38+
return {
39+
owner,
40+
repo,
41+
issues: data.total_count,
42+
}
43+
} catch (error: any) {
44+
throw createError({
45+
statusCode: error.response?.status || 500,
46+
statusMessage: error.response?._data?.message || 'Failed to fetch issue count from GitHub',
47+
})
48+
}
49+
},
50+
{
51+
maxAge: CACHE_MAX_AGE_ONE_HOUR,
52+
swr: true,
53+
name: 'github-issue-count',
54+
getKey: (event) => {
55+
const owner = getRouterParam(event, 'owner')
56+
const repo = getRouterParam(event, 'repo')
57+
return `${owner}/${repo}`
58+
},
59+
},
60+
)

shared/types/comparison.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export type ComparisonFacet =
1717
| 'totalDependencies'
1818
| 'deprecated'
1919
| 'totalLikes'
20+
| 'githubStars'
21+
| 'githubIssues'
22+
| 'createdAt'
23+
2024

2125
/** Facet metadata for UI display */
2226
export interface FacetInfo {
@@ -56,6 +60,15 @@ export const FACET_INFO: Record<ComparisonFacet, Omit<FacetInfo, 'id'>> = {
5660
deprecated: {
5761
category: 'health',
5862
},
63+
githubStars: {
64+
category: 'health',
65+
},
66+
githubIssues: {
67+
category: 'health',
68+
},
69+
createdAt: {
70+
category: 'health',
71+
},
5972
// Compatibility
6073
engines: {
6174
category: 'compatibility',

0 commit comments

Comments
 (0)