Skip to content

Comments

feat: add package download button#1586

Open
Adebesin-Cell wants to merge 4 commits intonpmx-dev:mainfrom
Adebesin-Cell:feat/download-button
Open

feat: add package download button#1586
Adebesin-Cell wants to merge 4 commits intonpmx-dev:mainfrom
Adebesin-Cell:feat/download-button

Conversation

@Adebesin-Cell
Copy link
Contributor

🔗 Linked issue

resolves #1528

🧭 Context

There was previously no way to directly download a package tarball or fetch all dependencies from the package detail page.

This PR introduces a Download button to make that happen.

📚 Description

This change adds a new Download button to the package detail page. The button includes a dropdown menu with two options:

  • Download the package .tgz tarball directly.
  • Generate and download a .sh script to fetch all dependencies.

Screenshot

Screenshot 2026-02-22 at 21 36 23

@vercel
Copy link

vercel bot commented Feb 22, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 23, 2026 10:37am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 23, 2026 10:37am
npmx-lunaria Ignored Ignored Feb 23, 2026 10:37am

Request Review

@github-actions
Copy link

github-actions bot commented Feb 22, 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.

@Adebesin-Cell Adebesin-Cell changed the title feat: Add package download button feat: add package download button Feb 22, 2026
@codecov
Copy link

codecov bot commented Feb 22, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1716 1 1715 6
View the top 1 failed test(s) by shortest run time
test/unit/a11y-component-coverage.spec.ts > a11y component test coverage > should have accessibility tests for all components (or be explicitly skipped)
Stack Traces | 0.0127s run time
AssertionError: Missing a11y tests for 1 component(s):
  - Package/DownloadButton.vue

To fix: Add tests in test/nuxt/a11y.spec.ts or add to SKIPPED_COMPONENTS in test/unit/a11y-component-coverage.spec.ts with justification.: expected 1 to equal +0

- Expected
+ Received

- 0
+ 1

 ❯ test/unit/a11y-component-coverage.spec.ts:166:12
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:145:14
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:915:28
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1243:24
 ❯ runWithTimeout node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1209:12
 ❯ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1653:42
 ❯ Traces.$ node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@.../dist/chunks/traces.CCmnQaNT.js:142:29
 ❯ trace node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@.../dist/chunks/test.B8ej_ZHS.js:239:23
 ❯ runTest node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab_@types+node_310e5dad6395fccdf5f424a1dccab9b9/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/index.js:1653:17

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3419bd and 07261cb.

📒 Files selected for processing (1)
  • test/unit/server/utils/dependency-analysis.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/unit/server/utils/dependency-analysis.spec.ts

📝 Walkthrough

Walkthrough

Adds a subtle variant to the Button component and adjusts its border/size class logic. Introduces a new Package DownloadButton Vue component that provides a teleport-ed dropdown menu to download package tarballs or a dependencies script with keyboard and focus handling. Integrates the download button into the package detail page. Extracts install-size types to shared/types, adds tarballUrl to resolved/dependency structures and tests, and adds i18n entries and schema for download-related labels.

Possibly related PRs

Suggested labels

front, a11y

Suggested reviewers

  • danielroe
  • graphieros
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the addition of a Download button to the package detail page with dropdown options for tarball and dependencies script downloads.
Linked Issues check ✅ Passed The PR successfully implements the requirements from issue #1528 by adding a download button allowing users to directly download packages and optionally their dependencies through UI controls on the package detail page.
Out of Scope Changes check ✅ Passed All changes are within scope: new DownloadButton component, Button variant extension to support 'subtle', shared types for install size, dependency resolver tarball URL tracking, and i18n translations—all directly supporting the download feature.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
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: 4

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

1144-1154: Consider showing the download button without waiting for install-size.
Right now the button only appears once install-size resolves; if the fetch is slow or fails, users lose direct tarball download. Consider rendering on displayVersion and disabling the dependencies action until installSize arrives.

Possible tweak
-            <PackageDownloadButton
-              v-if="displayVersion && installSize"
+            <PackageDownloadButton
+              v-if="displayVersion"
               :package-name="pkg.name"
               :version="displayVersion"
-              :install-size="installSize"
+              :install-size="installSize ?? undefined"
               size="small"
             />

Comment on lines +33 to +47
class="group gap-x-1 items-center justify-center font-mono border rounded-md transition-all duration-200 disabled:(opacity-40 cursor-not-allowed border-transparent)"
:class="{
'inline-flex': !block,
'flex': block,
'text-sm px-4 py-2': size === 'medium',
'text-xs px-2 py-0.5': size === 'small',
'text-xs px-2 py-0.5': size === 'small' && variant !== 'subtle',
'text-xs px-2 py-2': size === 'small' && variant === 'subtle',
'border-border': variant !== 'subtle',
'border-border-subtle': variant === 'subtle',
'bg-transparent text-fg hover:enabled:(bg-fg/10) focus-visible:enabled:(bg-fg/10) aria-pressed:(bg-fg/10 border-fg/20 hover:enabled:(bg-fg/20 text-fg/50))':
variant === 'secondary',
'text-bg bg-fg hover:enabled:(bg-fg/50) focus-visible:enabled:(bg-fg/50) aria-pressed:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
variant === 'primary',
'bg-bg-subtle text-fg-muted hover:enabled:(text-fg border-border-hover) focus-visible:enabled:(text-fg border-border-hover) active:enabled:scale-95':
variant === 'subtle',
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

Avoid per-button focus-visible utilities for the subtle variant.
Buttons already get focus-visible styling globally; adding inline focus-visible classes here diverges from the shared pattern. Consider dropping the inline focus-visible utility (or moving the styling to the global rule).

Suggested change
-      'bg-bg-subtle text-fg-muted hover:enabled:(text-fg border-border-hover) focus-visible:enabled:(text-fg border-border-hover) active:enabled:scale-95':
+      'bg-bg-subtle text-fg-muted hover:enabled:(text-fg border-border-hover) active:enabled:scale-95':
         variant === 'subtle',

Based on learnings: focus-visible styling for buttons and selects is applied globally via main.css (button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }); therefore individual buttons should not use inline focus-visible utility classes.

Comment on lines +105 to +115
function downloadPackage() {
const tarballUrl = props.version.dist.tarball
if (!tarballUrl) return

const link = document.createElement('a')
link.href = tarballUrl
link.download = `${props.packageName}-${props.version.version}.tgz`
document.body.appendChild(link)
link.click()
document.body.removeChild(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 | 🟠 Major

link.download is ignored for cross-origin URLs — tarball downloads will behave unreliably across browsers.

The HTML download attribute doesn't work with cross-origin URLs due to browser security restrictions implemented in Chrome 65 and other modern browsers; it is ignored for cross-origin URLs to prevent security issues. Since registry.npmjs.org is cross-origin from npmx.dev, browsers will ignore the download for cross-origin resources; as a result, no download is triggered and the resource is simply treated as another navigation destination.

The custom filename set at line 111 will never be applied. Whether the file downloads at all depends entirely on whether the npm registry sends Content-Disposition: attachment, which is outside this code's control.

The reliable fix is a Nuxt server route that proxies the tarball and sets the Content-Disposition: attachment; filename=... header itself, or fetching it as a Blob first:

🔧 Blob-based workaround (client-side, loads entire file into memory)
-function downloadPackage() {
-  const tarballUrl = props.version.dist.tarball
-  if (!tarballUrl) return
-
-  const link = document.createElement('a')
-  link.href = tarballUrl
-  link.download = `${props.packageName}-${props.version.version}.tgz`
-  document.body.appendChild(link)
-  link.click()
-  document.body.removeChild(link)
-}
+async function downloadPackage() {
+  const tarballUrl = props.version.dist.tarball
+  if (!tarballUrl) return
+
+  const response = await fetch(tarballUrl)
+  const blob = await response.blob()
+  const blobUrl = URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = blobUrl
+  link.download = `${props.packageName.replace(/\//g, '__')}-${props.version.version}.tgz`
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
+  URL.revokeObjectURL(blobUrl)
+}

Note the blob-based fix also applies the /__ replacement for scoped packages (e.g. @types/react), keeping the filename consistent with downloadDependenciesScript at lines 132 and 139, which already does this. The current line 111 is missing that replacement.

]

// Add root package
const rootTarball = (props.version.dist as any).tarball
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

Remove the unnecessary as any cast — dist.tarball is already typed on SlimPackumentVersion.

downloadPackage() on line 106 accesses props.version.dist.tarball directly without any cast, proving the field is properly typed on SlimPackumentVersion. The as any here silently bypasses TypeScript for no reason. As per the coding guidelines, strictly type-safe code is required.

🔧 Proposed fix
-  const rootTarball = (props.version.dist as any).tarball
+  const rootTarball = props.version.dist.tarball
📝 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
const rootTarball = (props.version.dist as any).tarball
const rootTarball = props.version.dist.tarball

Comment on lines +136 to +140
// Add dependencies
props.installSize.dependencies.forEach((dep: any) => {
lines.push(`# ${dep.name}@${dep.version}`)
lines.push(`curl -L ${dep.tarballUrl} -o ${dep.name.replace(/\//g, '__')}-${dep.version}.tgz`)
})
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

Replace dep: any with DependencySize; add a guard for empty tarballUrl; quote URLs in the curl commands.

Three related issues in this block:

  1. dep: any (line 137)installSize.dependencies is typed as DependencySize[], so dep is already statically typed. Annotating it as any defeats type checking on dep.tarballUrl, dep.name, and dep.version. As per the coding guidelines, strictly type-safe code is required.

  2. Missing tarballUrl guard (line 139)DependencySize.tarballUrl is a string (not optional), but an empty string is valid at the type level. If dep.tarballUrl is "", the generated curl command becomes curl -L -o … which silently fails.

  3. Unquoted URL in curl (lines 132, 139) — interpolating an unquoted URL into a shell command is fragile. Quoting removes any ambiguity.

🔧 Proposed fix
-  props.installSize.dependencies.forEach((dep: any) => {
-    lines.push(`# ${dep.name}@${dep.version}`)
-    lines.push(`curl -L ${dep.tarballUrl} -o ${dep.name.replace(/\//g, '__')}-${dep.version}.tgz`)
-  })
+  props.installSize.dependencies.forEach((dep) => {
+    if (!dep.tarballUrl) return
+    lines.push(`# ${dep.name}@${dep.version}`)
+    lines.push(`curl -L "${dep.tarballUrl}" -o ${dep.name.replace(/\//g, '__')}-${dep.version}.tgz`)
+  })

Apply the same quoting fix to the root package curl command (line 132):

-    lines.push(
-      `curl -L ${rootTarball} -o ${props.packageName.replace(/\//g, '__')}-${props.version.version}.tgz`,
-    )
+    lines.push(
+      `curl -L "${rootTarball}" -o ${props.packageName.replace(/\//g, '__')}-${props.version.version}.tgz`,
+    )
📝 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
// Add dependencies
props.installSize.dependencies.forEach((dep: any) => {
lines.push(`# ${dep.name}@${dep.version}`)
lines.push(`curl -L ${dep.tarballUrl} -o ${dep.name.replace(/\//g, '__')}-${dep.version}.tgz`)
})
// Add dependencies
props.installSize.dependencies.forEach((dep) => {
if (!dep.tarballUrl) return
lines.push(`# ${dep.name}@${dep.version}`)
lines.push(`curl -L "${dep.tarballUrl}" -o ${dep.name.replace(/\//g, '__')}-${dep.version}.tgz`)
})

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.

Proposal: Add a button to direct download a dependency

1 participant