Skip to content

feat: add version history page#2025

Open
ShroXd wants to merge 27 commits intonpmx-dev:mainfrom
ShroXd:linkable-version-page
Open

feat: add version history page#2025
ShroXd wants to merge 27 commits intonpmx-dev:mainfrom
ShroXd:linkable-version-page

Conversation

@ShroXd
Copy link
Contributor

@ShroXd ShroXd commented Mar 10, 2026

🔗 Linked issue

Resolves #1846

🧭 Context

Like the original issues mentioned, it would be useful to have a full version history page with filtering functionality.

📚 Description

Since it's common for a package to have hundreds of versions, some package like typescript even have 3,700 versions. To provide good performance and user experience, I did some optimization and trade off during developing.

1. Two phase data fetching

Phase 1 (on page load) calls getVersions to fetch only a lightweight summary — version strings, dist-tags, and publish times. This is enough to immediately render the "Current Tags" section and all group headers without waiting for full metadata.
Phase 2 (lazy, on first group expand) calls fetchAllPackageVersions to retrieve full metadata (deprecated status, provenance). The result is cached in fullVersionMap as a shallowRef — once loaded it's reused across all subsequent group expansions, never re-fetched.

The tradeoff: a short one-time loading delay on first expand, in exchange for a significantly faster initial page load.

2. Virtual scrolling + SSR fallback

Since WindowVirtualizer is client-only, it's wrapped in <ClientOnly>, with a #fallback slot providing a static SSR substitute — just the first 20 group headers, no interactivity.

The SSR fallback iterates versionGroups directly, a flat list of group objects, each containing an array of versions
The client-side virtualizer consumes flatItems — a flattened array that interleaves group headers and their expanded version rows into a single indexed list.

This follows existing pattern in package list page.

3. Debounced version filter

Since the filtering work is done entirely on the frontend, it can put significant performance pressure on packages with a large number of versions, such as typescript. And to support virtual scrolling, version groups are flattened into a flatItems array — a single indexed list of group headers and version rows that WindowVirtualizer can render efficiently. Therefore, a debounce on the filter input is necessary to avoid triggering expensive recomputation on every keystroke.

Some further optimization options include chunked flatItems construction with requestIdleCallback and moving the filtering work into a Web Worker. However, both require non-trivial design and implementation to handle potential race conditions. We also plan to add changelog support to this page, which will introduce additional matching overhead. For now, the current implementation is "good enough" — once the full feature set is complete, we can do thorough performance profiling to find the best optimization approach.

4. Disable prefetching for package page of each version

Per-version links in the history list have prefetching disabled, as expanding a group could generate a lot of simultaneous prefetch requests and unnecessarily strain the network and significantly impact page performance. Although this makes package page navigation slightly slower, considering most user behavior on this page is browsing and filtering, the tradeoff is acceptable.

5. Some interesting cases

📸 Screenshots

Scenario Screenshot
Light mode version-history-page-light
Dark mode version-history-page-dark
Expand version group version-history-expand-group
Filter version-history-filtering

@vercel
Copy link

vercel bot commented Mar 10, 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 14, 2026 0:54am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 14, 2026 0:54am
npmx-lunaria Ignored Ignored Mar 14, 2026 0:54am

Request Review

@codecov
Copy link

codecov bot commented Mar 13, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/pages/package/[[org]]/[name]/versions.vue 89.43% 13 Missing ⚠️
app/components/Package/Versions.vue 71.42% 2 Missing ⚠️
app/pages/package/[[org]]/[name].vue 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ShroXd ShroXd force-pushed the linkable-version-page branch from 35bdb74 to 03ed3c3 Compare March 14, 2026 07:23
@github-actions
Copy link

github-actions bot commented Mar 14, 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/en.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.

@ShroXd ShroXd force-pushed the linkable-version-page branch from 03ed3c3 to bba4380 Compare March 14, 2026 12:52
@ShroXd ShroXd marked this pull request as ready for review March 14, 2026 13:11
@ShroXd ShroXd requested a review from shuuji3 March 14, 2026 13:12
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces a dedicated page route for viewing package version history. A new page component renders versions with semantic grouping, expandable groups with lazy-loaded metadata, search filtering with semver range support, and virtualization for long lists. The main package page conditionally gates rendering when on the versions route. The Versions component header gains a new "View all versions" link button. Internationalisation strings and schema are extended to support the new UI elements including page title, current tags section, and filter controls.

Possibly related PRs

Suggested labels

front

Suggested reviewers

  • danielroe
  • alexdln
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description comprehensively explains the feature implementation, design decisions, and optimizations related to adding a version history page.
Linked Issues check ✅ Passed All coding objectives from issue #1846 are met: the PR implements a linkable, dedicated versions page with version filtering, full version discovery without menu expansion.
Out of Scope Changes check ✅ Passed All changes are directly related to the version history page feature. Minor adjustments to the package page and Versions component support the new feature; no unrelated changes detected.

✏️ 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
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

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

🧹 Nitpick comments (1)
app/pages/package/[[org]]/[name]/versions.vue (1)

377-387: Consider adding safety check for array access.

item.versions[0] is accessed without a guard. While the grouping logic ensures each group has at least one version, adding a safety check would satisfy strict type-safety guidelines and prevent potential runtime issues if the logic changes.

🔧 Suggested fix
                     <span class="ms-auto flex items-center gap-3 shrink-0">
-                      <span class="text-xs text-fg-muted" dir="ltr">{{ item.versions[0] }}</span>
+                      <span v-if="item.versions[0]" class="text-xs text-fg-muted" dir="ltr">{{ item.versions[0] }}</span>
                       <DateTime
-                        v-if="getVersionTime(item.versions[0])"
-                        :datetime="getVersionTime(item.versions[0])!"
+                        v-if="item.versions[0] && getVersionTime(item.versions[0])"
+                        :datetime="getVersionTime(item.versions[0])"
                         class="text-xs text-fg-subtle hidden sm:block"

As per coding guidelines: "ensure you always check when accessing an array value by index".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9477326f-b47e-43c8-81a2-07b1d0698711

📥 Commits

Reviewing files that changed from the base of the PR and between a170292 and bba4380.

📒 Files selected for processing (7)
  • app/components/Package/Versions.vue
  • app/pages/package/[[org]]/[name].vue
  • app/pages/package/[[org]]/[name]/versions.vue
  • i18n/locales/en.json
  • i18n/schema.json
  • test/nuxt/components/PackageVersions.spec.ts
  • test/nuxt/pages/PackageVersionsPage.spec.ts

Comment on lines +533 to +540
<LinkBase
:to="versionsPageRoute"
variant="button-secondary"
class="text-fg-subtle hover:text-fg transition-colors min-w-6 min-h-6 p-1 rounded"
:title="$t('package.versions.view_all_versions')"
classicon="i-lucide:history"
data-testid="view-all-versions-link"
>
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate LinkBase implementation"
fd -p 'Base.vue' app/components/Link

LINK_BASE_FILE="$(fd -p 'Base.vue' app/components/Link | head -n 1)"
echo "Inspecting: ${LINK_BASE_FILE}"

echo
echo "=== defineProps / attrs forwarding signals ==="
rg -n "defineProps|\\$attrs|inheritAttrs|v-bind=|data-testid|title" "$LINK_BASE_FILE"

echo
echo "=== file excerpt ==="
sed -n '1,260p' "$LINK_BASE_FILE"

Repository: npmx-dev/npmx.dev

Length of output: 270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Inspecting LinkBase implementation ==="
cat -n app/components/Link/Base.vue

Repository: npmx-dev/npmx.dev

Length of output: 5803


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Versions.vue lines 530-545 ==="
sed -n '530,545p' app/components/Package/Versions.vue

echo
echo "=== Search for data-testid usage in Versions.vue ==="
rg -n 'data-testid' app/components/Package/Versions.vue | head -20

echo
echo "=== Search for tests using view-all-versions-link ==="
rg -r 'view-all-versions-link' --type ts --type tsx --type js --type jsx --type vue

Repository: npmx-dev/npmx.dev

Length of output: 1543


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for test files referencing view-all-versions-link ==="
fd -e spec -e test | xargs grep -l 'view-all-versions-link' 2>/dev/null || echo "No test files found with this selector"

echo
echo "=== Check for any __tests__ or test directories ==="
fd -type d -name '__tests__' -o -name 'tests' -o -name 'test' | head -10

echo
echo "=== Search all files for view-all-versions-link ==="
rg 'view-all-versions-link' --type-list | head -5
rg 'view-all-versions-link' 2>/dev/null || echo "Pattern not found in other files"

Repository: npmx-dev/npmx.dev

Length of output: 405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verify if LinkBase ever renders disabled state in practice ==="
rg -c 'disabled' app/components/Link/Base.vue

echo
echo "=== Check Versions.vue to see if disabled is ever passed ==="
rg -n 'LinkBase' app/components/Package/Versions.vue | grep -i disabled || echo "No disabled prop passed"

echo
echo "=== Verify title is also not declared (from learnings) ==="
rg -n "title" app/components/Link/Base.vue || echo "title not found in LinkBase"

Repository: npmx-dev/npmx.dev

Length of output: 303


data-testid and title attributes are not forwarded by LinkBase.

LinkBase does not declare data-testid or title as props. The component uses a v-if/v-else fragment where the non-disabled branch renders <NuxtLink v-bind="props">. Since v-bind="props" only forwards declared props, these HTML attributes are silently dropped and will not appear on the rendered DOM element, making the test selector ineffective and the title attribute unavailable to assistive technologies.

To fix this, add both data-testid and title to LinkBase's defineProps, and forward them explicitly to the <NuxtLink> (the <span> branch can also receive them if needed for consistency).

Comment on lines +210 to +211
// Gates template rendering only — data fetches intentionally still run.
const isVersionsRoute = computed(() => route.name === 'package-versions')
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

Versions route still triggers the full package data pipeline.
This gates rendering only, but all heavyweight fetch/composable initialisation still runs on /versions. That adds avoidable load and weakens the intended versions-page optimisation. Please gate expensive package-detail data hooks for this route, or move the versions page into a sibling route shell that does not initialise main-page data.

Also applies to: 521-535

Comment on lines +346 to +348
expandedGroups.has(item.groupKey)
? $t('package.versions.collapse', { tag: item.label })
: $t('package.versions.expand', { tag: item.label })
Copy link
Member

@knowler knowler Mar 14, 2026

Choose a reason for hiding this comment

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

Don’t worry about updating these labels. Using aria-expanded is already sufficient for AT users to know what state the button is in and consequently what state it will change to if they activate it. Doing both can be confusing (e.g. screen reader might read Collapse 4.x versions, button expanded or Expand 4.x versions, button collapsed).

This goes for anywhere else in the PR that includes the words “expand” or “collapse” inside of the aria-label.

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.

Linkable package page to show list of versions

3 participants