Opens npmjs.com package and search links through npmx.dev before navigation when possible.
// ==UserScript==
// @name npmx Link Router
// @name:en npmx Link Router
// @name:ru npmx Link Router — открывать npm-ссылки через npmx.dev
// @name:es npmx Link Router — abrir enlaces npm mediante npmx.dev
// @name:zh-CN npmx Link Router — 通过 npmx.dev 打开 npm 链接
// @name:pt-BR npmx Link Router — abrir links npm pelo npmx.dev
// @namespace https://github.com/wolfcreative/npmx-link-router
// @version 1.1.0
// @description Opens npmjs.com package and search links through npmx.dev before navigation when possible.
// @description:en Opens npmjs.com package and search links through npmx.dev before navigation when possible.
// @description:ru Открывает ссылки npmjs.com на пакеты и поиск через npmx.dev до перехода, когда это возможно.
// @description:es Abre enlaces de paquetes y búsqueda de npmjs.com mediante npmx.dev antes de navegar cuando es posible.
// @description:zh-CN 尽可能在跳转前通过 npmx.dev 打开 npmjs.com 的包和搜索链接。
// @description:pt-BR Abre links de pacotes e busca do npmjs.com pelo npmx.dev antes da navegação quando possível.
// @author wolfcreative
// @license MIT
// @homepageURL https://github.com/wolfcreative/npmx-link-router
// @source https://github.com/wolfcreative/npmx-link-router.git
// @supportURL https://github.com/wolfcreative/npmx-link-router/issues
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
'use strict';
const NPM_HOSTS = new Set(['npmjs.com', 'www.npmjs.com']);
const NPMX_HOST = 'npmx.dev';
function toNpmxUrl(inputUrl) {
let url;
try {
url = new URL(inputUrl, window.location.href);
} catch {
return null;
}
if (!NPM_HOSTS.has(url.hostname)) {
return null;
}
url.protocol = 'https:';
url.hostname = NPMX_HOST;
return url.toString();
}
function redirectCurrentPage() {
const nextUrl = toNpmxUrl(window.location.href);
if (nextUrl && nextUrl !== window.location.href) {
window.stop();
window.location.replace(nextUrl);
}
}
function rewriteAnchor(anchor) {
if (!anchor) {
return null;
}
const href = anchor.getAttribute('href');
if (
!href ||
href.startsWith('#') ||
href.startsWith('javascript:') ||
href.startsWith('mailto:')
) {
return null;
}
const nextUrl = toNpmxUrl(href);
if (!nextUrl) {
return null;
}
anchor.href = nextUrl;
anchor.dataset.npmxLinkRouter = 'true';
return nextUrl;
}
function findAnchor(event) {
return event.target?.closest?.('a[href]') || null;
}
function rewriteEventLink(event) {
rewriteAnchor(findAnchor(event));
}
function navigateEvent(event) {
const anchor = findAnchor(event);
const nextUrl = rewriteAnchor(anchor);
if (!nextUrl) {
return;
}
event.preventDefault();
event.stopPropagation();
const openInNewTab =
event.type === 'auxclick' ||
event.button === 1 ||
event.metaKey ||
event.ctrlKey ||
anchor.target === '_blank';
if (openInNewTab) {
window.open(nextUrl, '_blank', 'noopener,noreferrer');
return;
}
window.location.assign(nextUrl);
}
function rewriteLinks(root = document) {
if (root.nodeType === Node.ELEMENT_NODE && root.matches?.('a[href]')) {
rewriteAnchor(root);
}
root.querySelectorAll?.('a[href]').forEach(rewriteAnchor);
}
redirectCurrentPage();
const earlyRewriteEvents = [
'pointerover',
'mouseover',
'mousedown',
'touchstart',
'focus',
'contextmenu',
];
for (const eventName of earlyRewriteEvents) {
document.addEventListener(eventName, rewriteEventLink, true);
}
document.addEventListener('click', navigateEvent, true);
document.addEventListener('auxclick', navigateEvent, true);
function startObserver() {
rewriteLinks(document);
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (
mutation.type === 'attributes' &&
mutation.target.matches?.('a[href]')
) {
rewriteAnchor(mutation.target);
}
for (const node of mutation.addedNodes) {
rewriteLinks(node);
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href'],
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver, { once: true });
} else {
startObserver();
}
})();