import cheerio from 'cheerio' import got from '../libs/got' import { millify, version, versionColor } from '../libs/utils' import { badgenServe, BadgenServeMeta as Meta, BadgenServeHandlers as Handlers, BadgenServeHandlerArgs as Args } from '../libs/badgen-serve' // https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md // https://github.com/npm/registry/blob/master/docs/download-counts.md // https://unpkg.com/ export const meta: Meta = { title: 'npm', examples: { '/npm/v/express': 'version', '/npm/v/ava': 'version', '/npm/v/ava/next': 'version (tag)', '/npm/v/next/canary': 'version (tag)', '/npm/v/@babel/core': 'version (scoped package)', '/npm/v/@nestjs/core/beta': 'version (scoped & tag)', '/npm/dw/express': 'weekly downloads', '/npm/dm/express': 'monthly downloads', '/npm/dy/express': 'yearly downloads', '/npm/dt/express': 'total downloads', '/npm/license/lodash': 'license', '/npm/node/next': 'node version', '/npm/dependents/got': 'dependents' } } export const handlers: Handlers = { '/npm/:topic/:scope<@.+>/:pkg/:tag?': handler, '/npm/:topic/:pkg/:tag?': handler } export default badgenServe(handlers) async function handler ({ topic, scope, pkg, tag }: Args) { const npmName = scope ? `${scope}/${pkg}` : pkg switch (topic) { case 'v': return unpkg('version', npmName, tag) case 'license': return unpkg('license', npmName, tag) case 'node': return unpkg('node', npmName, tag) case 'dt': return download('total', npmName) case 'dd': return download('last-day', npmName) // might deprecate this case 'dw': return download('last-week', npmName) case 'dm': return download('last-month', npmName) case 'dy': return download('last-year', npmName) case 'dependents': return dependents(npmName) default: return { subject: 'npm', status: 'unknown', color: 'grey' } } } async function unpkg (topic, pkg, tag = 'latest') { const endpoint = `https://unpkg.com/${pkg}@${tag}/package.json` const meta = await got(endpoint).then(res => res.body) switch (topic) { case 'version': { return { subject: `npm${tag === 'latest' ? '' : '@' + tag}`, status: version(meta.version), color: tag === 'latest' ? versionColor(meta.version) : 'cyan' } } case 'license': { return { subject: 'license', status: meta.license || 'unknown', color: 'blue' } } case 'node': { return { subject: 'node', status: (meta.engines && meta.engines.node) || '*', color: 'green' } } default: { return { subject: 'npm', status: 'unknown', color: 'grey' } } } } const download = async (period, npmName, tag = 'latest') => { const endpoint = ['https://api.npmjs.org/downloads'] const isTotal = period === 'total' if (isTotal) { endpoint.push(`range/2005-01-01:${new Date().getFullYear() + 1}-01-01`) } else { endpoint.push(`point/${period}`) } endpoint.push(npmName) // endpoint.push(tag) const { downloads } = await got(endpoint.join('/')).then( res => res.body, err => err.response!.statusCode === 404 && { downloads: 0 } ) const count = typeof downloads === 'number' ? downloads : downloads.reduce((accu, { downloads }) => { return accu + downloads }, 0) const per = isTotal ? '' : period.replace('last-', '/') return { subject: 'downloads', status: millify(count) + per, color: 'green' } } // https://github.com/astur/check-npm-dependents/blob/master/index.js async function dependents (name) { const html = await got(`https://www.npmjs.com/package/${name}`, { // @ts-ignore json: false }).then(res => res.body) return { subject: 'dependents', status: parseDependents(html), color: 'green' } } const parseDependents = html => { const $ = cheerio.load(html) const depLink = $('a[href="?activeTab=dependents"]') if (depLink.length !== 1) return -1 return depLink.text().replace(/[^0-9]/g, '') }