Last active
October 11, 2023 09:21
-
-
Save ngseke/5e963dc85e1cb3b561564f8f6740cb7e to your computer and use it in GitHub Desktop.
Versatile Npm Copy
This file contains 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 Versatile Npm Copy | |
// @namespace https://ngseke.me/ | |
// @version 0.1 | |
// @description Easily copy various NPM installation commands with a single click. Supports NPM, Yarn, and pnpm. | |
// @author ngseke | |
// @match https://www.npmjs.com/package/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=npmjs.com | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
const placeholder = '<package>' | |
const customCommandTemplates = [ | |
`pnpm i ${placeholder}`, | |
`pnpm i -D ${placeholder}`, | |
`${placeholder} `, | |
] | |
const namespace = `versatile-npm-copy-${crypto.randomUUID()}` | |
const $ = (selector) => document.querySelector(selector) | |
function htmlToElement (html) { | |
const template = document.createElement('template') | |
html = html.trim() | |
template.innerHTML = html | |
return template.content.firstChild | |
} | |
function getNpmCommandInnerElement () { | |
return $('p.flex-auto.truncate.db.ma0 > code') | |
} | |
function getNpmCommandElement () { | |
return getNpmCommandInnerElement().parentElement.parentElement | |
} | |
const commandClassName = [namespace, 'command'].join('-') | |
const emphasisClassName = [namespace, 'emphasis'].join('-') | |
const notificationClassName = [namespace, 'notification'].join('-') | |
const dividerClassName = [namespace, 'divider'].join('-') | |
const injectStyles = () => { | |
const gradient = 'background-image: linear-gradient(to right, #ffecd2 0%, #fcb69f 100%);' | |
const css = ` | |
.${commandClassName} { | |
margin: .5rem 0; | |
} | |
.${commandClassName}:hover { | |
background-color: transparent; | |
border-color: transparent; | |
} | |
.${emphasisClassName} { | |
font-weight: bold; | |
background-image: linear-gradient(to right, #434343 0%, black 100%); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
} | |
.${notificationClassName} { | |
${gradient} | |
color: #1d1d1d; | |
border-color: white; | |
font-weight: bold; | |
} | |
.${dividerClassName} { | |
border: 0; | |
border-bottom: #cccccc 1px solid; | |
opacity: .7; | |
} | |
` | |
const style = document.createElement('style') | |
document.body.appendChild(style) | |
style.appendChild(document.createTextNode(css)) | |
} | |
function showNotification () { | |
let newNotification = htmlToElement(` | |
<div class="ee9e731a pa3 ph5-ns tl z-999 w-100 flex flex-row justify-between d76ab310 ${notificationClassName}"> | |
<div style="display: flex;"> | |
<p class="ma0" role="alert" aria-atomic="true">✔ Copied to clipboard!</p> | |
</div> | |
</div> | |
`) | |
const container = $('.list.ma0.pa0.tr.z-999 > .list.ma0.pa0.tr.z-999') | |
container.append(newNotification) | |
setTimeout(() => { | |
newNotification.remove() | |
newNotification = null | |
}, 1000) | |
} | |
function renderCustomCommand (command, emphasis = false) { | |
const $npmCommand = getNpmCommandElement() | |
const $customCommand = $npmCommand.cloneNode(true) | |
const $button = $customCommand.querySelector('button') | |
const $code = $customCommand.querySelector('code') | |
$code.innerText = '' | |
if (emphasis) { | |
const [first, ...rest] = command.split(' ') | |
const emphasisElement = document.createElement('strong') | |
emphasisElement.innerText = `${first} ` | |
emphasisElement.classList.add(emphasisClassName) | |
$code.append(emphasisElement, rest.join(' ')) | |
} else { | |
$code.innerText = command | |
} | |
$button.addEventListener('click', () => { | |
navigator.clipboard.writeText(command) | |
showNotification() | |
}) | |
$customCommand.classList.add(commandClassName) | |
$npmCommand.after($customCommand) | |
return $customCommand | |
} | |
function renderDivider () { | |
const $npmCommand = getNpmCommandElement() | |
const $divider = document.createElement('hr') | |
$divider.classList.add(dividerClassName) | |
$npmCommand.after($divider) | |
return $divider | |
} | |
injectStyles() | |
const renderedElements = [] | |
function removeRenderedElements () { | |
renderedElements.forEach(element => element.remove()) | |
renderedElements.length = 0 | |
} | |
function render () { | |
removeRenderedElements() | |
getNpmCommandElement().style.marginBottom = 0 | |
const packageName = getNpmCommandInnerElement() | |
.innerText | |
.replace(/^npm i /, '') | |
;[...customCommandTemplates].reverse() | |
.forEach((template) => { | |
const command = template.replaceAll(placeholder, packageName) | |
renderedElements.push( | |
renderCustomCommand(command, command.trim() !== packageName) | |
) | |
}) | |
renderedElements.push(renderDivider()) | |
} | |
render() | |
const observer = new MutationObserver((record) => { | |
render() | |
}) | |
observer.observe(getNpmCommandElement(), { | |
childList: true, | |
subtree: true, | |
characterData: true, | |
}) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment