Skip to content

Instantly share code, notes, and snippets.

@ngseke
Last active October 11, 2023 09:21
Show Gist options
  • Save ngseke/5e963dc85e1cb3b561564f8f6740cb7e to your computer and use it in GitHub Desktop.
Save ngseke/5e963dc85e1cb3b561564f8f6740cb7e to your computer and use it in GitHub Desktop.
Versatile Npm Copy
// ==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