Skip to content

Comments

feat: add GitHub contributors graph#1445

Merged
graphieros merged 9 commits intonpmx-dev:mainfrom
Tobbe:tobbe-feat-gh-contrib-graph
Feb 13, 2026
Merged

feat: add GitHub contributors graph#1445
graphieros merged 9 commits intonpmx-dev:mainfrom
Tobbe:tobbe-feat-gh-contrib-graph

Conversation

@Tobbe
Copy link
Contributor

@Tobbe Tobbe commented Feb 12, 2026

I started working on this new graph. WDYT?
I wanted to be able to see how "healthy", from a number-of-contibutors perspective a package is.
Like, even if a package has hundreds of contributors, maybe those are all from years past, and the package is now basically only kept alive by one person, which isn't too good from a reliability/bus factor point of view
It uses the GitHub API, so only works for packages that have a github repo linked in the package metadata

image

@vercel
Copy link

vercel bot commented Feb 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Feb 13, 2026 6:56pm
npmx.dev Ready Ready Preview, Comment Feb 13, 2026 6:56pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Feb 13, 2026 6:56pm

Request Review

@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/en-GB.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov
Copy link

codecov bot commented Feb 12, 2026

Codecov Report

❌ Patch coverage is 26.85185% with 158 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useCharts.ts 4.23% 87 Missing and 26 partials ⚠️
app/components/Package/TrendsChart.vue 56.38% 28 Missing and 13 partials ⚠️
app/pages/compare.vue 0.00% 3 Missing ⚠️
app/pages/package/[[org]]/[name].vue 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds per-package repository resolution and a new "contributors" metric to the package trends chart. It introduces a repoRef?: RepoRef | null prop, per-package repoRefsByPackage state and fetching (fetchRepoRefsForPackages / loadRepoRefsForPackages), and changes metric APIs to use MetricContext = { packageName, repoRef }. The METRICS definitions and metric state were extended to include contributors, with multi-package flows fetching per-package contributor evolutions when a GitHub repo is available. Server-side support for GitHub contributors stats (API route and fixture handling), client-side contributor-evolution fetching and caching, localization keys for Contributors, and minor UI/granularity adjustments were also added.

Possibly related PRs

Suggested labels

front

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (77 files):

⚔️ .gitignore (content)
⚔️ CONTRIBUTING.md (content)
⚔️ README.md (content)
⚔️ app/app.vue (content)
⚔️ app/components/AppFooter.vue (content)
⚔️ app/components/AppHeader.vue (content)
⚔️ app/components/CallToAction.vue (content)
⚔️ app/components/Compare/FacetSelector.vue (content)
⚔️ app/components/Header/AccountMenu.client.vue (content)
⚔️ app/components/Header/AuthModal.client.vue (content)
⚔️ app/components/Header/ConnectorModal.vue (content)
⚔️ app/components/Header/SearchBox.vue (content)
⚔️ app/components/Link/Base.vue (content)
⚔️ app/components/Package/Card.vue (content)
⚔️ app/components/Package/Keywords.vue (content)
⚔️ app/components/Package/Maintainers.vue (content)
⚔️ app/components/Package/ManagerSelect.vue (content)
⚔️ app/components/Package/TrendsChart.vue (content)
⚔️ app/components/Package/VersionDistribution.vue (content)
⚔️ app/components/Package/Versions.vue (content)
⚔️ app/components/Package/WeeklyDownloadStats.vue (content)
⚔️ app/components/Readme.vue (content)
⚔️ app/components/Terminal/Install.vue (content)
⚔️ app/composables/useCharts.ts (content)
⚔️ app/composables/useMarkdown.ts (content)
⚔️ app/composables/usePackageAnalysis.ts (content)
⚔️ app/composables/useStructuredFilters.ts (content)
⚔️ app/pages/about.vue (content)
⚔️ app/pages/compare.vue (content)
⚔️ app/pages/index.vue (content)
⚔️ app/pages/package/[[org]]/[name].vue (content)
⚔️ app/pages/search.vue (content)
⚔️ app/utils/frameworks.ts (content)
⚔️ app/utils/install-command.ts (content)
⚔️ app/utils/prehydrate.ts (content)
⚔️ app/utils/versions.ts (content)
⚔️ i18n/locales/de-DE.json (content)
⚔️ i18n/locales/en.json (content)
⚔️ i18n/locales/es.json (content)
⚔️ i18n/locales/fr-FR.json (content)
⚔️ i18n/locales/ja-JP.json (content)
⚔️ i18n/locales/pl-PL.json (content)
⚔️ i18n/locales/zh-CN.json (content)
⚔️ i18n/schema.json (content)
⚔️ lunaria/files/de-DE.json (content)
⚔️ lunaria/files/en-GB.json (content)
⚔️ lunaria/files/en-US.json (content)
⚔️ lunaria/files/es-419.json (content)
⚔️ lunaria/files/es-ES.json (content)
⚔️ lunaria/files/fr-FR.json (content)
⚔️ lunaria/files/ja-JP.json (content)
⚔️ lunaria/files/pl-PL.json (content)
⚔️ lunaria/files/zh-CN.json (content)
⚔️ lunaria/lunaria.ts (content)
⚔️ lunaria/prepare-json-files.ts (content)
⚔️ modules/runtime/server/cache.ts (content)
⚔️ nuxt.config.ts (content)
⚔️ package.json (content)
⚔️ pnpm-lock.yaml (content)
⚔️ scripts/compare-translations.ts (content)
⚔️ server/api/contributors.get.ts (content)
⚔️ server/middleware/canonical-redirects.global.ts (content)
⚔️ server/utils/readme-loaders.ts (content)
⚔️ server/utils/readme.ts (content)
⚔️ shared/utils/constants.ts (content)
⚔️ shared/utils/package-analysis.ts (content)
⚔️ test/nuxt/a11y.spec.ts (content)
⚔️ test/nuxt/components/PackageVersions.spec.ts (content)
⚔️ test/nuxt/composables/use-markdown.spec.ts (content)
⚔️ test/unit/app/utils/install-command.spec.ts (content)
⚔️ test/unit/app/utils/versions.spec.ts (content)
⚔️ test/unit/server/utils/readme-loaders.spec.ts (content)
⚔️ test/unit/server/utils/readme.spec.ts (content)
⚔️ test/unit/shared/utils/package-analysis.spec.ts (content)
⚔️ uno-preset-a11y.ts (content)
⚔️ uno-preset-rtl.ts (content)
⚔️ vercel.json (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is directly related to the changeset, describing a new GitHub contributors graph feature to assess package health from a contributor perspective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Comment on lines +1368 to +1372
<PackageWeeklyDownloadStats
:packageName
:createdIso="pkg?.time?.created ?? null"
:repoRef="repoRef"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix repoRef nullability for PackageWeeklyDownloadStats.

repoRef can be null from useRepoMeta, but the child prop expects RepoRef | undefined, causing the reported type check failure. Normalise null to undefined (or widen the prop type).

🛠️ Suggested fix
-            :repoRef="repoRef"
+            :repoRef="repoRef ?? undefined"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PackageWeeklyDownloadStats
:packageName
:createdIso="pkg?.time?.created ?? null"
:repoRef="repoRef"
/>
<PackageWeeklyDownloadStats
:packageName
:createdIso="pkg?.time?.created ?? null"
:repoRef="repoRef ?? undefined"
/>
🧰 Tools
🪛 GitHub Check: 💪 Type check

[failure] 1371-1371:
Type 'RepoRef | null' is not assignable to type 'RepoRef | undefined'.

@Tobbe Tobbe changed the title feat: Add GitHub contributors graph feat: add GitHub contributors graph Feb 12, 2026
@graphieros
Copy link
Contributor

graphieros commented Feb 12, 2026

When switching to monthly or yearly, the component is designed to display estimations for incomplete periods.
It was designed originally to chart downloads. However this KPI is not compatible with estimations. The component must be adapted to show estimations for compatible facets.

The facet does not appear to be working on the compare page.

I would also move the fetching to the useCharts composable

@graphieros graphieros requested a review from serhalp February 12, 2026 21:51
@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 12, 2026

When switching to monthly or yearly, the component is designed to display estimations for incomplete periods. It was designed originally to chart downloads. However this KPI is not compatible with estimations. The component must be adapted to show estimations for compatible facets.

Yes, I did find it a bit weird that it was estimating how many contributors a package will have 😀 How should we handle the incomplete period though? 🤔

@graphieros
Copy link
Contributor

When switching to monthly or yearly, the component is designed to display estimations for incomplete periods. It was designed originally to chart downloads. However this KPI is not compatible with estimations. The component must be adapted to show estimations for compatible facets.

Yes, I did find it a bit weird that it was estimating how many contributors a package will have 😀 How should we handle the incomplete period though? 🤔

We are dealing with absolutes. n contributors today does not add up with n yesterday. So weekly, monthly, yearly should be averages.

@graphieros graphieros marked this pull request as draft February 12, 2026 22:07
@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 12, 2026

The facet does not appear to be working on the compare page.

Yeah, I hadn't gotten around to fully testing that one yet.
I've made it work now (locally)
image

But I also need to find a package that doesn't have a GitHub repo in its package.json to test with.
You don't happen to know of one offhand, do you?

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 12, 2026

I faked one not having a github link, just to be able to test how it'd work.
It now prints a small notice for packages not included

image

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 12, 2026

When switching to monthly or yearly, the component is designed to display estimations for incomplete periods. It was designed originally to chart downloads. However this KPI is not compatible with estimations. The component must be adapted to show estimations for compatible facets.

Yes, I did find it a bit weird that it was estimating how many contributors a package will have 😀 How should we handle the incomplete period though? 🤔

We are dealing with absolutes. n contributors today does not add up with n yesterday. So weekly, monthly, yearly should be averages.

Likes also isn't additive, right? So maybe likes and contributors should be handled differently compared to downloads.
But I don't want to touch the logic for existing graphs in this PR. So how about we leave it like it is for all three facets for now, and then have another PR to fix this logic?

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

@graphieros Can we mark this as ready to review again?

@graphieros
Copy link
Contributor

I don't think it should be shipped with estimations for contributors.
I opened it for review again, would love to have more opinions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/Package/TrendsChart.vue (1)

1662-1667: ⚠️ Potential issue | 🟡 Minor

Remove inline focus-visible utility on the reset button.

Please rely on the global button focus-visible styling and drop the per‑element focus-visible:outline-accent/70 class here.

🛠️ Suggested fix
-          class="self-end flex items-center justify-center px-2.5 py-1.75 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border focus-visible:outline-accent/70 sm:mb-0"
+          class="self-end flex items-center justify-center px-2.5 py-1.75 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border sm:mb-0"

Based on learnings: "In the npmx.dev project, ensure that focus-visible styling for button and select elements is implemented globally in app/assets/main.css ... Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements."

🧹 Nitpick comments (1)
app/pages/compare.vue (1)

253-258: Edge case: fallback may show a loading spinner when status is 'success' but data is absent.

The v-else branch displays a loading spinner for any state not explicitly handled. If status is 'success' yet packagesData remains null or all-null (an unlikely but possible race or upstream bug), users would see an indefinite spinner rather than an informative message.

Consider whether a "No data available" state or an additional explicit check for status === 'success' would provide clearer feedback.

Comment on lines +625 to +686
const METRICS = computed<MetricDef[]>(() => {
const metrics: MetricDef[] = [
{
id: 'downloads',
label: $t('package.trends.items.downloads'),
fetch: ({ packageName }, opts) =>
fetchPackageDownloadEvolution(
packageName,
props.createdIso ?? null,
opts,
) as Promise<EvolutionData>,
supportsMulti: true,
},
{
id: 'likes',
label: $t('package.trends.items.likes'),
fetch: ({ packageName }, opts) => fetchPackageLikesEvolution(packageName, opts),
supportsMulti: true,
},
]

if (hasContributorsFacet.value) {
metrics.push({
id: 'contributors',
label: $t('package.trends.items.contributors'),
fetch: ({ repoRef }, opts) => fetchRepoContributorsEvolution(repoRef, opts),
supportsMulti: true,
})
}

return metrics
})

const selectedMetric = usePermalink<MetricId>('facet', DEFAULT_METRIC_ID, {
permanent: props.permalink,
})

const effectivePackageNamesForMetric = computed<string[]>(() => {
if (!isMultiPackageMode.value) return effectivePackageNames.value
if (selectedMetric.value !== 'contributors') return effectivePackageNames.value
return effectivePackageNames.value.filter(
name => repoRefsByPackage.value[name]?.provider === 'github',
)
})

const skippedPackagesWithoutGitHub = computed(() => {
if (!isMultiPackageMode.value) return []
if (selectedMetric.value !== 'contributors') return []
if (!effectivePackageNames.value.length) return []

return effectivePackageNames.value.filter(
name => repoRefsByPackage.value[name]?.provider !== 'github',
)
})

const availableGranularities = computed<ChartTimeGranularity[]>(() => {
if (selectedMetric.value === 'contributors') {
return ['weekly', 'monthly', 'yearly']
}

return ['daily', 'weekly', 'monthly', 'yearly']
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Disable estimation/extrapolation for contributors.

Contributors are absolute counts, so the monthly/yearly extrapolation and estimation overlay will inflate the latest period and mislead. Gate those behaviours on selectedMetric !== 'contributors'.

🛠️ Suggested fix
-const isEstimationGranularity = computed(
-  () => displayedGranularity.value === 'monthly' || displayedGranularity.value === 'yearly',
-)
+const isEstimationGranularity = computed(
+  () =>
+    selectedMetric.value !== 'contributors'
+    && (displayedGranularity.value === 'monthly' || displayedGranularity.value === 'yearly'),
+)
 function extrapolateLastValue(lastValue: number) {
+  if (selectedMetric.value === 'contributors') return lastValue
   if (displayedGranularity.value !== 'monthly' && displayedGranularity.value !== 'yearly')
     return lastValue

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

I don't think it should be shipped with estimations for contributors.

I also agree that it's wrong.

You said

We are dealing with absolutes. n contributors today does not add up with n yesterday. So weekly, monthly, yearly should be averages.

So just to make sure I understand what you mean, if I select "yearly" as the period, it should show the average so far for 2026 as the value for the full year? And same for "monthly", it should show the average so far for February as the value for this month?
Or did I misunderstand you @graphieros?

Copy link
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with @graphieros we should fix the data prediction issue before merging, especially as there isn't a lot of time before recharging begins - will defer to him on that since he is the graph guy :p

but very exciting!!

@graphieros
Copy link
Contributor

I don't think it should be shipped with estimations for contributors.

I also agree that it's wrong.

You said

We are dealing with absolutes. n contributors today does not add up with n yesterday. So weekly, monthly, yearly should be averages.

So just to make sure I understand what you mean, if I select "yearly" as the period, it should show the average so far for 2026 as the value for the full year? And same for "monthly", it should show the average so far for February as the value for this month? Or did I misunderstand you @graphieros?

Yes that's right. An average of unique contributors, if this data is available of course.

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

@graphieros I removed the extrapolation for Contributors.
I want each period in the chart be the total number of unique contributors for that period. So for Monthly "January" should be the total number of unique contributors for January. For February (which we're only halfway through now), the total number of unique contributors will not go down. So I just made it show the current total number of contributors (i.e. not try to guess any future value at all).

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

@ghostdevv you requested changes. Is this good enough? I.e. no estimation, just current total

image image

@graphieros
Copy link
Contributor

Looks good to me now !
And you are right, estimations should not be applied on likes.

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

@graphieros Thanks for working with me on this one 🙏

@graphieros
Copy link
Contributor

@Tobbe I recommend you merge main in your branch, since vue-data-ui version was bumped and minor config details updated on the chart file

@graphieros
Copy link
Contributor

Ok, looks mergeable to me

@graphieros graphieros enabled auto-merge February 13, 2026 19:01
@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

Ok, looks mergeable to me

Yeah, I was just going to say it looks good :)

image

@Tobbe
Copy link
Contributor Author

Tobbe commented Feb 13, 2026

@graphieros What's the setup like here, do we need to wait for @ghostdevv to review again?

@graphieros
Copy link
Contributor

Yep ;)

@graphieros graphieros requested a review from ghostdevv February 13, 2026 19:19
@graphieros graphieros added this pull request to the merge queue Feb 13, 2026
Merged via the queue into npmx-dev:main with commit 6755779 Feb 13, 2026
17 checks passed
WarningImHack3r pushed a commit to WarningImHack3r/npmx.dev that referenced this pull request Feb 14, 2026
alex-key pushed a commit to alex-key/npmx.dev that referenced this pull request Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants