Created
July 5, 2025 14:22
-
-
Save sreyemnayr/a0c8707589216640ff81e9a2c0fc2f72 to your computer and use it in GitHub Desktop.
Userscript: VSCode Extension Marketplace – Direct VSIX Download Button
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name VSCode Marketplace – Direct VSIX Download | |
// @namespace https://github.com/sreyemnayr | |
// @version 0.2.1 | |
// @description Adds a “Download VSIX” button to VS Code Marketplace pages, keeps it there, and saves the file with a proper name. | |
// @author Ryan Meyers (sreyemnayr) | |
// @match https://marketplace.visualstudio.com/items* | |
// @run-at document-idle | |
// @grant none | |
// ==/UserScript== | |
async function injectDownloadButton () { | |
/*—-1-— make sure we’re on an extension-detail page —*/ | |
const params = new URLSearchParams(location.search); | |
const itemName = params.get('itemName'); // e.g. Balastrong.close-tabs | |
if (!itemName || !itemName.includes('.')) return; | |
/* If the button is already in the DOM, nothing to do */ | |
if (document.getElementById('vsix-download-container')) return; | |
const [publisher, extension] = itemName.split('.'); | |
/*—-2-— pull metadata for latest VSIX —*/ | |
const apiUrl = `https://marketplace.visualstudio.com/_apis/public/gallery/vscode/${publisher}/${extension}/latest`; | |
let latest, version, vsixUrl; | |
try { | |
const json = await fetch(apiUrl).then(r => r.json()); | |
latest = json?.versions?.[0]; | |
version = latest?.version ?? '0.0.0'; | |
vsixUrl = latest?.files?.find(f => f.assetType === 'Microsoft.VisualStudio.Services.VSIXPackage')?.source; | |
if (!vsixUrl) return; | |
} catch (e) { | |
console.error('[VSIX] metadata fetch failed:', e); | |
return; | |
} | |
/*—-3-— find the Install button so we can clone its look —*/ | |
const install = document.querySelector('.install'); | |
const container = install?.closest('.ux-oneclick-install-button-container')?.parentNode; | |
if (!install || !container) return; | |
const dlBtn = install.cloneNode(true); | |
dlBtn.id = 'vsix-download-button'; | |
dlBtn.querySelector('*').textContent = 'Download VSIX'; | |
dlBtn.removeAttribute('href'); // we handle click ourselves | |
dlBtn.style.pointerEvents = 'auto'; | |
dlBtn.addEventListener('click', async ev => { | |
ev.preventDefault(); | |
const label = dlBtn.querySelector('*'); | |
const originalText = label.textContent; | |
label.textContent = 'Fetching…'; | |
try { | |
const buffer = await fetch(vsixUrl).then(r => r.arrayBuffer()); | |
const blob = new Blob([buffer], { type: 'application/octet-stream' }); | |
const url = URL.createObjectURL(blob); | |
const tmp = document.createElement('a'); | |
tmp.href = url; | |
tmp.download = `${publisher}.${extension}.${version}.vsix`; | |
document.body.appendChild(tmp); | |
tmp.click(); | |
tmp.remove(); | |
URL.revokeObjectURL(url); | |
label.textContent = 'Downloaded ✔'; | |
setTimeout(() => (label.textContent = originalText), 2500); | |
} catch (err) { | |
console.error('[VSIX] download failed:', err); | |
label.textContent = 'Error – try again'; | |
setTimeout(() => (label.textContent = originalText), 3000); | |
} | |
}); | |
const wrap = document.createElement('div'); | |
wrap.id = 'vsix-download-container'; | |
wrap.appendChild(dlBtn); | |
container.appendChild(wrap); | |
console.log('[VSIX] Download button added →', vsixUrl); | |
} | |
/*—-4-— keep the button present even if the page rewrites the DOM —*/ | |
setInterval(injectDownloadButton, 1000); // every second | |
window.addEventListener('popstate', injectDownloadButton); // SPA navigation support |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment