Skip to content

Comments

feat: add package download button#1586

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

feat: add package download button#1586
Adebesin-Cell wants to merge 3 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 22, 2026 8:42pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 22, 2026 8:42pm
npmx-lunaria Ignored Ignored Feb 22, 2026 8:42pm

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.0103s 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. 🎉


📝 Walkthrough

Walkthrough

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

Suggested labels

front

Suggested reviewers

  • danielroe
  • graphieros
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description accurately describes the changes: adding a Download button to the package detail page with dropdown options for downloading package tarball and dependencies script.
Linked Issues check ✅ Passed All code changes meet the objective from issue #1528: a Download button is added to the package detail page with options to download the package tarball and fetch dependencies.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the download button feature. Type definitions are moved to shared types for proper code organization, which supports the feature implementation.

✏️ 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

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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