// ==UserScript==
// @name npm_plus
// @namespace npm_plus
// @description 增强 npm 的搜索体验, 添加下载和 github 直链, 使用 npms.io 的数据替换 npm 评分
// @version 1.0.0
// @author roojay
// @license http://opensource.org/licenses/MIT
// @include *://*npmjs.com/search*
// @run-at document-end
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js
// @noframes
// @connect *
// @downloadURL https://update.greasyfork.icu/scripts/399821/npm_plus.user.js
// @updateURL https://update.greasyfork.icu/scripts/399821/npm_plus.meta.js
// ==/UserScript==
GM_addStyle(`
.github-icon {
width: 12px;
height: 12px;
fill: rgba(0, 0, 0, 0.45);
}
.download-number {
color: #d14;
font-size: 12px;
font-weight: normal;
font-family: Arial;
font-style: italic;
}
.download-icon {
width: 8px;
height: 12px;
fill: rgba(0, 0, 0, 0.45);
}
.npm-plus-title-wrap {
position: relative;
flex: 1;
display: inline-flex;
justify-content: flex-end;
align-items: center;
}
.npm-plus-title-wrap > span {
margin-left: 8px;
}
.score-number-wrap {
position: relative;
}
.score-icon {
position: relative;
width: 26px;
height: 26px;
float: left;
cursor: default;
fill: rgb(95, 149, 122);
overflow: hidden;
}
.score-number {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
transform: translate(-50%, -50%);
color: #fff;
font-size: 12px;
font-weight: 900;
align-items: center;
font-family: Arial;
}
`);
$(function() {
// 包列表
const DOM_PACKAGE_LIST = 'div.ph3.pt2-ns';
// 单个包
const DOM_PACKAGE_ITEM = 'section.flex.pl1-ns';
// 包信息
const DOM_PACKAGE_INFO = 'div.w-80';
// 包评分
const DOM_PACKAGE_SCORE = 'div.w-20';
// 包名容器
const DOM_PACKAGE_TITLE_WRAP = '.flex.flex-row.items-end.pr3'
// 包名链接
const DOM_PACKAGE_LINK = '.flex-row a';
const API_NPMS = 'https://api.npms.io/v2'
const API_NPM_REGISTRY = 'https://registry.npmjs.org';
const ICON_SCORE = '';
const ICON_DOWNLOAD = '';
const ICON_GITHUB = '';
function initScript() {
const observer = new MutationObserver((mutations, observer) => runAll());
observer.observe(document.querySelector(DOM_PACKAGE_LIST), {
childList: true, // 子节点的变动
attributes: false, // 属性的变动
characterData: true, // 节点内容或节点文本的变动
subtree: false // 是否将该观察器应用于该节点的所有后代节点
});
runAll();
}
const renderGithub = (_githubUrl) => `
${ICON_GITHUB}
`;
const renderDownload = (_downloadUrl, _downloadNumber) => `
${ICON_DOWNLOAD}
${_downloadNumber}
`;
const renderScore = (_score) => `
${ICON_SCORE}
${_score}
`;
/**
* 获取包的详细信息
* @param {string} packageUrl
* @param {jQuery object} titleWrap
*/
async function getPackageDetails(packageUrl, titleWrap) {
const fullName = packageUrl.replace('/package/', '');
const _url = `${API_NPMS}/package/${encodeURIComponent(fullName)}`;
const name = fullName.split('/').slice(-1)[0];
GM_xmlhttpRequest({
url: _url,
method: 'get',
onload: function(xhr) {
try {
const res = JSON.parse(xhr.response);
const downNum = res.collected.npm.downloads.slice(-1)[0].count;
const scoreNum = ~~(res.score.final * 100);
const version = res.collected.metadata.version;
const githubUrl = res.collected.metadata.links.repository;
const downLoadUrl = `${API_NPM_REGISTRY}/${fullName}/-/${name}-${version}.tgz`;
const toolWrap = `
${githubUrl ? renderGithub(githubUrl) : ''}
${renderDownload(downLoadUrl, downNum)}
${renderScore(scoreNum)}
`;
titleWrap.append(toolWrap);
} catch (e) {
console.log('getPackageDetails -> e', e);
}
}
});
}
// 执行所有替换方法
async function runAll() {
const promises = $(DOM_PACKAGE_ITEM).map((index, val) => {
const $this = $(val);
// 去掉右侧原有评分
$this.find(DOM_PACKAGE_SCORE).remove();
$this.find(DOM_PACKAGE_INFO).removeClass('w-80').addClass('w-100');
const packageUrl = $this.find(DOM_PACKAGE_LINK).attr('href');
const titleWrap = $this.find(DOM_PACKAGE_TITLE_WRAP);
return getPackageDetails(packageUrl, titleWrap);
});
Promise.all(promises);
}
initScript();
});