diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js index 0459ad76b7452..cd5eda48c1e63 100644 --- a/lib/all-badge-examples.js +++ b/lib/all-badge-examples.js @@ -781,6 +781,20 @@ const allBadgeExamples = [ 'node' ] }, + { + title: 'npm bundle size (minified)', + previewUri: '/bundlephobia/min/react.svg', + keywords: [ + 'node' + ] + }, + { + title: 'npm bundle size (minified + gzip)', + previewUri: '/bundlephobia/minzip/react.svg', + keywords: [ + 'node' + ] + }, { title: 'node', previewUri: '/node/v/passport.svg', diff --git a/server.js b/server.js index ae6d2429aad1e..a99a393d8edc9 100644 --- a/server.js +++ b/server.js @@ -7613,6 +7613,61 @@ camp.route(/^\/nsp\/npm\/(?:@([^/]+)?\/)?([^/]+)?(?:\/([^/]+)?)?\.(svg|png|gif|j } })); +// bundle size for npm packages +camp.route(/^\/bundlephobia\/(min|minzip)\/(?:@([^/]+)?\/)?([^/]+)?(?:\/([^/]+)?)?\.(svg|png|gif|jpg|json)?$/, + cache((data, match, sendBadge, request) => { + // A: /bundlephobia/(min|minzip)/:package.:format + // B: /bundlephobia/(min|minzip)/:package/:version.:format + // C: /bundlephobia/(min|minzip)/@:scope/:package.:format + // D: /bundlephobia/(min|minzip)/@:scope/:package/:version.:format + const resultType = match[1]; + const scope = match[2]; + const packageName = match[3]; + const packageVersion = match[4]; + const format = match[5]; + const showMin = resultType === 'min'; + + const badgeData = getBadgeData(showMin ? 'minified size' : 'minzipped size', data); + + let packageString = typeof scope === 'string' ? + `@${scope}/${packageName}` : packageName; + + if(packageVersion) { + packageString += `@${packageVersion}`; + } + + const requestOptions = { + url: 'https://bundlephobia.com/api/size', + qs: { + package: packageString, + }, + json: true, + }; + + /** + * `ErrorCode` => `error code` + * @param {string} code + * @returns {string} + */ + const formatErrorCode = (code) => + code.replace(/([A-Z])/g, ' $1').trim().toLowerCase(); + + request(requestOptions, (error, response, body) => { + if(typeof body !== 'object' || body === null) { + badgeData.text[1] = 'error'; + badgeData.colorscheme = 'red'; + } else if (error !== null || body.error) { + badgeData.text[1] = 'code' in body.error ? + formatErrorCode(body.error.code) : 'error'; + badgeData.colorscheme = 'red'; + } else { + badgeData.text[1] = prettyBytes(showMin ? body.size : body.gzip); + badgeData.colorscheme = 'blue'; + } + sendBadge(format, badgeData); + }); +})); + // Redmine plugin rating. camp.route(/^\/redmine\/plugin\/(rating|stars)\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { diff --git a/service-tests/bundlephobia.js b/service-tests/bundlephobia.js new file mode 100644 index 0000000000000..041a895f38154 --- /dev/null +++ b/service-tests/bundlephobia.js @@ -0,0 +1,78 @@ +'use strict' + +const Joi = require('joi') +const ServiceTester = require('./runner/service-tester') +const { isFileSize } = require('./helpers/validators') + +const t = new ServiceTester({ + id: 'bundlephobia', title: 'NPM package bundle size', +}) + +module.exports = t + +const formats = { + A: '/bundlephobia/:type/:package.:format', + B: '/bundlephobia/:type/:package/:version.:format', + C: '/bundlephobia/:type/@:scope/:package.:format', + D: '/bundlephobia/:type/@:scope/:package/:version.:format', +} + +const data = [ + { + format: formats.A, + get: '/min/preact.json', + expect: { name: 'minified size', value: isFileSize }, + }, + { + format: formats.B, + get: '/min/preact/8.0.0.json', + expect: { name: 'minified size', value: '7.94 kB' }, + }, + { + format: formats.C, + get: '/min/@cycle/core.json', + expect: { name: 'minified size', value: isFileSize }, + }, + { + format: formats.D, + get: '/min/@cycle/core/7.0.0.json', + expect: { name: 'minified size', value: '3.51 kB' }, + }, + { + format: formats.A, + get: '/minzip/preact.json', + expect: { name: 'minzipped size', value: isFileSize }, + }, + { + format: formats.B, + get: '/minzip/preact/8.0.0.json', + expect: { name: 'minzipped size', value: '3.35 kB' }, + }, + { + format: formats.C, + get: '/minzip/@cycle/core.json', + expect: { name: 'minzipped size', value: isFileSize }, + }, + { + format: formats.D, + get: '/minzip/@cycle/core/7.0.0.json', + expect: { name: 'minzipped size', value: '1.23 kB' }, + }, + { + format: formats.A, + get: '/min/some-no-exist.json', + expect: { name: 'minified size', value: 'package not found error' }, + }, + { + format: formats.C, + get: '/min/@some-no-exist/some-no-exist.json', + expect: { name: 'minified size', value: 'package not found error' }, + } +] + +data.forEach( ({format, get, expect }) => { + t.create(`Testing format '${format}' against '${get}'`) + .get(get) + .expectJSONTypes(Joi.object().keys(expect)) + } +)