Skip to content

feat(ui): add health score widget powered by npm Pulse#2333

Closed
hamdibenjarrar wants to merge 10 commits intonpmx-dev:mainfrom
hamdibenjarrar:feat/package-health-score
Closed

feat(ui): add health score widget powered by npm Pulse#2333
hamdibenjarrar wants to merge 10 commits intonpmx-dev:mainfrom
hamdibenjarrar:feat/package-health-score

Conversation

@hamdibenjarrar
Copy link
Copy Markdown
Contributor

@hamdibenjarrar hamdibenjarrar commented Mar 30, 2026

Summary

Adds a Package Health Score sidebar widget to every package page, powered by npm Pulse.

  • Overall score (0–100) with letter grade (A–F) displayed in a CollapsibleSection matching the existing sidebar layout
  • Algorithm weights shown inline for transparency: Maintenance×30% · Quality×25% · Security×25% · Popularity×20%
  • Four color-coded dimension bars (green → red) with per-bar weight labels
  • Data fetched lazily client-side from npm Pulse so page load is never blocked
  • Footer link points to the npm Pulse homepage

Changes

File What changed
app/components/Package/HealthScore.vue New sidebar widget wrapped in CollapsibleSection
app/pages/package/[[org]]/[name].vue Inject <PackageHealthScore> above download stats
modules/security-headers.ts Add https://npm-pulse.vercel.app to connectSrc CSP
i18n/locales/en.json Add package.health_score translation keys
i18n/locales/ar-EG.json Arabic (Egyptian) translations
i18n/locales/fr-FR.json French translations
i18n/schema.json Regenerated to include new health_score keys
test/nuxt/a11y.spec.ts A11y test for PackageHealthScore in loading state

How it works

The component calls https://npm-pulse.vercel.app/api/v1/score/{package} and renders:

  1. A large numeric score with a letter grade badge
  2. Four color-coded progress bars with dimension name, weight %, and score
  3. A "Powered by npm Pulse" footer linking to the homepage

Follows existing patterns: UnoCSS, var(--fg)/var(--border) CSS custom properties, $t() i18n, useFetch composable, CollapsibleSection wrapper, Lucide icons.

hamdibenjarrar and others added 5 commits March 29, 2026 12:31
Co-authored-by: Philippe Serhal <philippe.serhal@gmail.com>
Adds a new PackageHealthScore component to the package sidebar that
displays an overall health score (0-100) with letter grade (A-F) and
four dimension bars: Maintenance (30%), Quality (25%), Security (25%),
and Popularity (20%).

Data is fetched client-side from https://npm-pulse.vercel.app/api/v1/score/{package}
and rendered lazily to avoid blocking page load.

- app/components/Package/HealthScore.vue: new sidebar widget
- app/pages/package/[[org]]/[name].vue: inject component above download stats
- i18n/locales/en.json: add package.health_score translation keys
- i18n/locales/ar-EG.json: add Arabic (Egyptian) translations
- i18n/locales/fr-FR.json: add French translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 30, 2026 4:31pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 30, 2026 4:31pm
npmx-lunaria Ignored Ignored Mar 30, 2026 4:31pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 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
i18n/locales/ar-EG.json Localization changed, will be marked as complete.
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/fr-FR.json Localization changed, will be marked as complete.
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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 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

Adds a new Vue 3 component app/components/Package/HealthScore.vue that fetches and displays an npm Pulse health score (numeric score, grade badge and four dimension progress bars) for a required packageName prop and optional version prop. Integrates the component into the package page sidebar (app/pages/package/[[org]]/[name].vue) before weekly download stats. Adds i18n keys for the health score in i18n/locales/en.json, fr-FR.json and ar-EG.json. Updates modules/security-headers.ts to allow connect-src to https://npm-pulse.vercel.app. Adds an accessibility test for PackageHealthScore and updates i18n/schema.json to include the new health_score block.

Possibly related PRs

Suggested reviewers

  • danielroe
  • alexdln
  • graphieros
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description comprehensively describes the changeset, including the new health score widget feature, affected files, implementation details, and alignment with existing UI patterns.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
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

🧹 Nitpick comments (5)
i18n/locales/ar-EG.json (1)

1-1: UTF-8 BOM character detected at start of file.

The file begins with a UTF-8 Byte Order Mark (). While some tools handle this gracefully, BOMs in JSON files can cause parsing failures in certain environments and are generally discouraged per JSON specification (RFC 8259 recommends UTF-8 without BOM).

Consider removing the BOM to ensure maximum compatibility.

app/components/Package/HealthScore.vue (3)

1-1: UTF-8 BOM character detected.

The file begins with a UTF-8 BOM (). This is unusual for Vue SFC files and may cause issues with some tooling. Consider removing it.


60-69: Add defensive checks for potentially missing dimension properties.

The code accesses data.value.dimensions and its nested properties without verifying they exist. If the API response is malformed or changes, this will throw a runtime error.

As per coding guidelines, ensure type-safe code by checking when accessing properties that might be undefined.

🛡️ Proposed defensive check
 const dimensions = computed(() => {
-  if (!data.value) return []
-  const d = data.value.dimensions
+  if (!data.value?.dimensions) return []
+  const d = data.value.dimensions
   return [
-    { key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: d.maintenance.score, weight: d.maintenance.weight },
-    { key: 'quality', label: $t('package.health_score.dimension_quality'), score: d.quality.score, weight: d.quality.weight },
-    { key: 'security', label: $t('package.health_score.dimension_security'), score: d.security.score, weight: d.security.weight },
-    { key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: d.popularity.score, weight: d.popularity.weight },
+    { key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: d.maintenance?.score ?? 0, weight: d.maintenance?.weight ?? 0 },
+    { key: 'quality', label: $t('package.health_score.dimension_quality'), score: d.quality?.score ?? 0, weight: d.quality?.weight ?? 0 },
+    { key: 'security', label: $t('package.health_score.dimension_security'), score: d.security?.score ?? 0, weight: d.security?.weight ?? 0 },
+    { key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: d.popularity?.score ?? 0, weight: d.popularity?.weight ?? 0 },
   ]
 })

149-157: Footer link uses raw packageName instead of props.packageName.

While this works due to Vue's template context, using props.packageName (as done in line 28) would be more consistent. This is a minor nitpick.

♻️ Consistency improvement
       <a
-        :href="`https://npm-pulse.vercel.app/api/v1/score/${packageName}`"
+        :href="`https://npm-pulse.vercel.app/api/v1/score/${props.packageName}`"
         target="_blank"
app/pages/package/[[org]]/[name].vue (1)

1-1: UTF-8 BOM character added to file.

A UTF-8 BOM () has been added to the start of this file. This is inconsistent with typical Vue/TypeScript file conventions and may cause issues with some tooling. Consider removing it.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c6cb7da-0ec1-412e-9a39-935c94a1d017

📥 Commits

Reviewing files that changed from the base of the PR and between b3da028 and 60833c7.

📒 Files selected for processing (5)
  • app/components/Package/HealthScore.vue
  • app/pages/package/[[org]]/[name].vue
  • i18n/locales/ar-EG.json
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json

@hamdibenjarrar hamdibenjarrar changed the title feat(package): add health score widget powered by npm Pulse feat(ui): add health score widget powered by npm Pulse Mar 30, 2026
…-score

# Conflicts:
#	i18n/locales/ar-EG.json
#	i18n/locales/fr-FR.json
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/components/Package/HealthScore.vue 75.92% 10 Missing and 3 partials ⚠️
app/pages/package/[[org]]/[name].vue 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87891e0e-5a96-4709-b93b-cb08a4f02b4b

📥 Commits

Reviewing files that changed from the base of the PR and between 60833c7 and bcf3455.

📒 Files selected for processing (5)
  • app/components/Package/HealthScore.vue
  • app/pages/package/[[org]]/[name].vue
  • i18n/locales/ar-EG.json
  • i18n/locales/fr-FR.json
  • modules/security-headers.ts
✅ Files skipped from review due to trivial changes (3)
  • modules/security-headers.ts
  • app/pages/package/[[org]]/[name].vue
  • i18n/locales/fr-FR.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/ar-EG.json

Comment on lines +60 to +68
const dimensions = computed(() => {
if (!data.value?.dimensions) return []
const d = data.value.dimensions
return [
{ key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: d.maintenance?.score ?? 0, weight: d.maintenance?.weight ?? 0 },
{ key: 'quality', label: $t('package.health_score.dimension_quality'), score: d.quality?.score ?? 0, weight: d.quality?.weight ?? 0 },
{ key: 'security', label: $t('package.health_score.dimension_security'), score: d.security?.score ?? 0, weight: d.security?.weight ?? 0 },
{ key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: d.popularity?.score ?? 0, weight: d.popularity?.weight ?? 0 },
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clamp score values to 0–100 before binding to progress UI.

Scores from a remote API are trusted as-is. If a value is out of range, aria-valuenow and width can become invalid (>100 or <0), causing accessibility and rendering issues.

Suggested fix
+function clampScore(score: number): number {
+  if (!Number.isFinite(score))
+    return 0
+  return Math.max(0, Math.min(100, score))
+}
+
 const dimensions = computed(() => {
   if (!data.value?.dimensions) return []
   const d = data.value.dimensions
   return [
-    { key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: d.maintenance?.score ?? 0, weight: d.maintenance?.weight ?? 0 },
-    { key: 'quality', label: $t('package.health_score.dimension_quality'), score: d.quality?.score ?? 0, weight: d.quality?.weight ?? 0 },
-    { key: 'security', label: $t('package.health_score.dimension_security'), score: d.security?.score ?? 0, weight: d.security?.weight ?? 0 },
-    { key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: d.popularity?.score ?? 0, weight: d.popularity?.weight ?? 0 },
+    { key: 'maintenance', label: $t('package.health_score.dimension_maintenance'), score: clampScore(d.maintenance?.score ?? 0), weight: d.maintenance?.weight ?? 0 },
+    { key: 'quality', label: $t('package.health_score.dimension_quality'), score: clampScore(d.quality?.score ?? 0), weight: d.quality?.weight ?? 0 },
+    { key: 'security', label: $t('package.health_score.dimension_security'), score: clampScore(d.security?.score ?? 0), weight: d.security?.weight ?? 0 },
+    { key: 'popularity', label: $t('package.health_score.dimension_popularity'), score: clampScore(d.popularity?.score ?? 0), weight: d.popularity?.weight ?? 0 },
   ]
 })

Also applies to: 134-143

Copy link
Copy Markdown
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

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

A few preliminary remarks:

  • This feature should be transparent on the algorithm it uses. We wanted originally to have a custom algorithm that we control, and which we can be fully transparent about. How would you provide more transparency about the way the score is calculated ?

  • The link opens a json file (is this intentional ?)

  • The block containing the score component is not aligned horizontally in the side bar, and should be treated like other sections (inside a dropdown).

Image
  • Can you please add tests ?

hamdibenjarrar and others added 2 commits March 30, 2026 17:16
- Wrap component in CollapsibleSection for proper sidebar alignment
- Fix footer link to point to npm Pulse homepage (not raw JSON API)
- Add algorithm subtitle showing weights inline for transparency
- Add a11y test for PackageHealthScore
- Fix formatting to pass vp fmt check
- Remove BOM from locale files to fix i18n validator
@hamdibenjarrar
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @graphieros! Here's what I've addressed in the latest commit (88bc6b1):

1. Transparency of the algorithm
The component now shows the scoring weights directly in the section subtitle: Maintenance×30% · Quality×25% · Security×25% · Popularity×20%. Each dimension bar also shows its weight inline (e.g. "Maintenance (30%)"), so the full formula is visible without following any link. The score is: score = maintenance×0.30 + quality×0.25 + security×0.25 + popularity×0.20 on a 0–100 scale.

If you'd prefer a fully self-hosted algorithm built entirely on data already in npmx.dev (downloads, last publish date, vulnerability count, etc.), I'm happy to reimplement it that way — just let me know.

2. Link fixed
The footer link now points to https://npm-pulse.vercel.app (the homepage) instead of the raw JSON API endpoint.

3. Alignment
The component is now wrapped in <CollapsibleSection id="health-score">, matching the pattern used by all other sidebar sections (Downloads, Keywords, Compatibility, Maintainers, etc.).

4. Tests
Added an a11y test for PackageHealthScore in test/nuxt/a11y.spec.ts.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
test/nuxt/a11y.spec.ts (1)

1231-1239: LGTM!

The accessibility test for PackageHealthScore follows the established patterns in this file. The test correctly mounts the component with the required packageName prop and validates there are no axe violations in the loading state.

Consider adding test cases for improved coverage in a follow-up:

  • Test with the optional version prop: props: { packageName: 'vue', version: '3.5.0' }
  • If API mocking is feasible, tests for the loaded state with score data and the error state would strengthen accessibility coverage further

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb72eeaf-c34c-419c-9437-cc6dacc625a8

📥 Commits

Reviewing files that changed from the base of the PR and between bcf3455 and 056fcc3.

📒 Files selected for processing (6)
  • app/components/Package/HealthScore.vue
  • app/pages/package/[[org]]/[name].vue
  • i18n/locales/ar-EG.json
  • i18n/locales/en.json
  • i18n/locales/fr-FR.json
  • test/nuxt/a11y.spec.ts
✅ Files skipped from review due to trivial changes (4)
  • app/pages/package/[[org]]/[name].vue
  • i18n/locales/en.json
  • i18n/locales/ar-EG.json
  • app/components/Package/HealthScore.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • i18n/locales/fr-FR.json

Copy link
Copy Markdown
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

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

@hamdibenjarrar

While AI tools can be helpful for coding assistance, our project requires genuine human involvement. We've outlined this clearly in our contribution guidelines.

If you're genuinely interested in contributing to npmx.dev:

  • Engage with the community first (issues, discussions)
  • Understand the codebase and what we actually need
  • Submit thoughtful, individual PRs with your own descriptions
  • Follow up on feedback and participate in reviews
  • Use AI as a tool to assist you, not replace you

As for the actual solution proposed, we would really prefer to opt for an algorithm we control, instead of linking to a paid solution.

Should you be interested in this challenge, while contributing to npmx following our guidelines, you are most welcome to improve your solution.

@graphieros graphieros added the automation This PR might have been created by an AI, which goes against our code of conduct. label Mar 30, 2026
@hamdibenjarrar
Copy link
Copy Markdown
Contributor Author

hamdibenjarrar commented Mar 30, 2026

Hey @graphieros fair feedback, and I appreciate the directness.
You're right on both points. I leaned heavily on AI tooling to move fast, and it shows in ways that don't reflect genuine engagement with the project. That's on me.
On the architecture: linking to an external service I own was the wrong call for an open-source contribution. The right approach is building the algorithm inside npmx using data you already fetch — npm registry, downloads, vulnerability counts, analysis data. Everything needed is already there. No external deps, no third-party links, fully under your control.
I'd like to rebuild this properly:

Start by engaging on the existing issues and understanding what the team actually wants from a health score
Implement it using only internal data sources
Submit a cleaner PR with real context and my own reasoning

Would it make sense to open a discussion issue first to align on the algorithm before writing code?

@graphieros graphieros closed this Mar 30, 2026
@graphieros
Copy link
Copy Markdown
Contributor

@hamdibenjarrar

Sounds good, as long as it's with your own words.
An existing issue already exists: #1099

You are welcome to participate.
This PR was closed in the meanwhile.
Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation This PR might have been created by an AI, which goes against our code of conduct.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants