diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js index ed423ac137e1b..ada0c358d17c9 100644 --- a/lib/all-badge-examples.js +++ b/lib/all-badge-examples.js @@ -640,6 +640,13 @@ const allBadgeExamples = [ 'node' ] }, + { + title: 'npm (custom registry)', + previewUri: '/npm/v/npm.svg?registry_uri=https://registry.npmjs.com', + keywords: [ + 'node' + ] + }, { title: 'npm (scoped with tag)', previewUri: '/npm/v/@cycle/core/canary.svg', @@ -647,6 +654,13 @@ const allBadgeExamples = [ 'node' ] }, + { + title: 'npm (scoped with tag, custom registry)', + previewUri: '/npm/v/@cycle/core/canary.svg?registry_uri=https://registry.npmjs.com', + keywords: [ + 'node' + ] + }, { title: 'node', previewUri: '/node/v/passport.svg', @@ -675,6 +689,13 @@ const allBadgeExamples = [ 'node' ] }, + { + title: 'node (scoped with tag, custom registry)', + previewUri: '/node/v/@stdlib/stdlib/latest.svg?registry_uri=https://registry.npmjs.com', + keywords: [ + 'node' + ] + }, { title: 'PyPI', previewUri: '/pypi/v/nine.svg', @@ -1116,6 +1137,13 @@ const allBadgeExamples = [ 'node' ] }, + { + title: 'npm (custom registry)', + previewUri: '/npm/l/express.svg?registry_uri=https://registry.npmjs.com', + keywords: [ + 'node' + ] + }, { title: 'apm', previewUri: '/apm/l/vim-mode.svg', diff --git a/lib/npm-badge-helpers.js b/lib/npm-badge-helpers.js new file mode 100644 index 0000000000000..c809630b9920a --- /dev/null +++ b/lib/npm-badge-helpers.js @@ -0,0 +1,7 @@ +'use strict'; + +const defaultNpmRegistryUri = 'https://registry.npmjs.org'; + +module.exports = { + defaultNpmRegistryUri, +}; diff --git a/server.js b/server.js index 718513ff380d8..5b0cddcd402ca 100644 --- a/server.js +++ b/server.js @@ -70,6 +70,9 @@ const { const { mapNpmDownloads } = require('./lib/npm-provider'); +const { + defaultNpmRegistryUri +} = require('./lib/npm-badge-helpers'); const { teamcityBadge } = require('./lib/teamcity-badge-helpers'); @@ -1580,163 +1583,175 @@ cache(function (data, match, sendBadge, request) { // npm version integration. camp.route(/^\/npm\/v\/(?:@([^/]+))?\/?([^/]*)\/?([^/]*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - // e.g. cycle, core, next, svg - const [, scope, packageName, tag, format] = match; - const pkg = encodeURIComponent(scope ? `@${scope}/${packageName}` : packageName); - const apiUrl = `https://registry.npmjs.org/-/package/${pkg}/dist-tags`; - const name = tag ? `npm@${tag}` : 'npm'; - const badgeData = getBadgeData(name, data); - // Using the Accept header because of this bug: - // - request(apiUrl, { headers: { 'Accept': '*/*' } }, (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - const version = data[tag || 'latest']; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// npm license integration. -camp.route(/^\/npm\/l\/(?:@([^/]+)\/)?([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const scope = match[1]; // "user" (when a scope "@user" is supplied) - const packageName = match[2]; // "express" - const format = match[3]; // "svg" - let apiUrl; - if (scope === undefined) { - // e.g. https://registry.npmjs.org/express/latest - // Use this endpoint as an optimization. It covers the vast majority of - // these badges, and the response is smaller. - apiUrl = `https://registry.npmjs.org/${packageName}/latest`; - } else { - // e.g. https://registry.npmjs.org/@cedx%2Fgulp-david - // because https://registry.npmjs.org/@cedx%2Fgulp-david/latest does not work - const path = encodeURIComponent(`${scope}/${packageName}`); - apiUrl = `https://registry.npmjs.org/@${path}`; - } - const badgeData = getBadgeData('license', data); - request(apiUrl, { headers: { 'Accept': '*/*' } }, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - let license; - if (scope === undefined) { - license = data.license; - } else { - const latestVersion = data['dist-tags'].latest; - license = data.versions[latestVersion].license; +cache({ + queryParams: ['registry_uri'], + handler: function(queryParams, match, sendBadge, request) { + // e.g. cycle, core, next, svg + const [, scope, packageName, tag, format] = match; + const registryUri = queryParams.registry_uri || defaultNpmRegistryUri; + const pkg = encodeURIComponent(scope ? `@${scope}/${packageName}` : packageName); + const apiUrl = `${registryUri}/-/package/${pkg}/dist-tags`; + + const name = tag ? `npm@${tag}` : 'npm'; + const badgeData = getBadgeData(name, queryParams); + // Using the Accept header because of this bug: + // + request(apiUrl, { headers: { 'Accept': '*/*' } }, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible'; + sendBadge(format, badgeData); + return; } - if (Array.isArray(license)) { - license = license.join(', '); - } else if (typeof license == 'object') { - license = license.type; + try { + const data = JSON.parse(buffer); + const version = data[tag || 'latest']; + badgeData.text[1] = versionText(version); + badgeData.colorscheme = versionColor(version); + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); } - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); + }); + } })); -// npm node version integration. -camp.route(/^\/node\/v\/(?:@([^/]+))?\/?([^/]*)\/?([^/]*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - // e.g. @stdlib, stdlib, next, svg - const [, scope, packageName, tag, format] = match; - const registryTag = tag || 'latest'; - let apiUrl; - if (scope === undefined) { +// npm license integration. +camp.route(/^\/npm\/l\/(?:@([^/]+)\/)?([^/]+)\.(svg|png|gif|jpg|json)$/, +cache({ + queryParams: ['registry_uri'], + handler: function(queryParams, match, sendBadge, request) { + // e.g. cycle, core, svg + const [, scope, packageName, format ] = match; + const registryUri = queryParams.registry_uri || defaultNpmRegistryUri; + let apiUrl; + if (scope === undefined) { // e.g. https://registry.npmjs.org/express/latest // Use this endpoint as an optimization. It covers the vast majority of // these badges, and the response is smaller. - apiUrl = `https://registry.npmjs.org/${packageName}/${registryTag}`; - } else { - // e.g. https://registry.npmjs.org/@cedx%2Fgulp-david - // because https://registry.npmjs.org/@cedx%2Fgulp-david/latest does not work - const path = encodeURIComponent(`${scope}/${packageName}`); - apiUrl = `https://registry.npmjs.org/@${path}`; - } - const name = tag ? `node@${tag}` : 'node'; - const badgeData = getBadgeData(name, data); - // Using the Accept header because of this bug: - // - request(apiUrl, { headers: { 'Accept': '*/*' } }, (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; + apiUrl = `${registryUri}/${packageName}/latest`; + } else { + // e.g. https://registry.npmjs.org/@cedx%2Fgulp-david + // because https://registry.npmjs.org/@cedx%2Fgulp-david/latest does not work + const path = encodeURIComponent(`${scope}/${packageName}`); + apiUrl = `${registryUri}/@${path}`; } - try { - const data = JSON.parse(buffer); - if (data.error === 'not_found') { - badgeData.text[1] = 'package not found'; + const badgeData = getBadgeData('license', queryParams); + request(apiUrl, { headers: { 'Accept': '*/*' } }, function(err, res, buffer) { + if (err != null) { + badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } - let releaseData; - if (scope === undefined) { - releaseData = data; - } else { - const version = data['dist-tags'][registryTag]; - releaseData = data.versions[version]; + try { + const data = JSON.parse(buffer); + let license; + if (scope === undefined) { + license = data.license; + } else { + const latestVersion = data['dist-tags'].latest; + license = data.versions[latestVersion].license; + } + if (Array.isArray(license)) { + license = license.join(', '); + } else if (typeof license == 'object') { + license = license.type; + } + badgeData.text[1] = license; + badgeData.colorscheme = 'blue'; + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); } - const versionRange = (releaseData.engines || {}).node; - if (! versionRange) { - badgeData.text[1] = 'not specified'; + }); + } +})); + +// npm node version integration. +camp.route(/^\/node\/v\/(?:@([^/]+))?\/?([^/]*)\/?([^/]*)\.(svg|png|gif|jpg|json)$/, +cache({ + queryParams: ['registry_uri'], + handler: function(queryParams, match, sendBadge, request) { + // e.g. @stdlib, stdlib, next, svg + const [, scope, packageName, tag, format] = match; + const registryUri = queryParams.registry_uri || defaultNpmRegistryUri; + const registryTag = tag || 'latest'; + let apiUrl; + if (scope === undefined) { + // e.g. https://registry.npmjs.org/express/latest + // Use this endpoint as an optimization. It covers the vast majority of + // these badges, and the response is smaller. + apiUrl = `${registryUri}/${packageName}/${registryTag}`; + } else { + // e.g. https://registry.npmjs.org/@cedx%2Fgulp-david + // because https://registry.npmjs.org/@cedx%2Fgulp-david/latest does not work + const path = encodeURIComponent(`${scope}/${packageName}`); + apiUrl = `${registryUri}/@${path}`; + } + const name = tag ? `node@${tag}` : 'node'; + const badgeData = getBadgeData(name, queryParams); + // Using the Accept header because of this bug: + // + request(apiUrl, { headers: { 'Accept': '*/*' } }, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible'; sendBadge(format, badgeData); return; } - badgeData.text[1] = versionRange; - regularUpdate('http://nodejs.org/dist/latest/SHASUMS256.txt', - (24 * 3600 * 1000), - shasums => { - // tarball index start, tarball index end - const taris = shasums.indexOf('node-v'); - const tarie = shasums.indexOf('\n', taris); - const tarball = shasums.slice(taris, tarie); - const version = tarball.split('-')[1]; - return version; - }, (err, version) => { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - if (semver.satisfies(version, versionRange)) { - badgeData.colorscheme = 'brightgreen'; - } else if (semver.gtr(version, versionRange)) { - badgeData.colorscheme = 'yellow'; - } else { - badgeData.colorscheme = 'orange'; - } - } catch(e) { } + try { + const data = JSON.parse(buffer); + if (data.error === 'not_found') { + badgeData.text[1] = 'package not found'; sendBadge(format, badgeData); - }); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); + return; + } + let releaseData; + if (scope === undefined) { + releaseData = data; + } else { + const version = data['dist-tags'][registryTag]; + releaseData = data.versions[version]; + } + const versionRange = (releaseData.engines || {}).node; + if (! versionRange) { + badgeData.text[1] = 'not specified'; + sendBadge(format, badgeData); + return; + } + badgeData.text[1] = versionRange; + regularUpdate('http://nodejs.org/dist/latest/SHASUMS256.txt', + (24 * 3600 * 1000), + shasums => { + // tarball index start, tarball index end + const taris = shasums.indexOf('node-v'); + const tarie = shasums.indexOf('\n', taris); + const tarball = shasums.slice(taris, tarie); + const version = tarball.split('-')[1]; + return version; + }, (err, version) => { + if (err != null) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + return; + } + try { + if (semver.satisfies(version, versionRange)) { + badgeData.colorscheme = 'brightgreen'; + } else if (semver.gtr(version, versionRange)) { + badgeData.colorscheme = 'yellow'; + } else { + badgeData.colorscheme = 'orange'; + } + } catch(e) { } + sendBadge(format, badgeData); + }); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); + } })); // Anaconda Cloud / conda package manager integration diff --git a/service-tests/node.js b/service-tests/node.js index 856f713d2b1bc..675d8625a2bf0 100644 --- a/service-tests/node.js +++ b/service-tests/node.js @@ -30,6 +30,11 @@ t.create("gets the tagged release's node version version of ionic") .expectJSONTypes(Joi.object({ name: 'node@next' }).unknown()) .afterJSON(json => { assertIsSemverRange(json.value); }); +t.create('gets the node version of passport from a custom registry') + .get('/v/passport.json?registry_uri=https://registry.npmjs.com') + .expectJSONTypes(Joi.object({ name: 'node' }).unknown()) + .afterJSON(json => { assertIsSemverRange(json.value); }); + t.create("gets the tagged release's node version of @cycle/core") .get('/v/@cycle/core/canary.json') .expectJSONTypes(Joi.object({ name: 'node@canary' }).unknown()) diff --git a/service-tests/npm.js b/service-tests/npm.js index 1f051e32fb9c5..0bb5143ce01fc 100644 --- a/service-tests/npm.js +++ b/service-tests/npm.js @@ -19,10 +19,26 @@ t.create('gets the tagged package version of npm') .get('/v/npm/next.json') .expectJSONTypes(Joi.object().keys({ name: 'npm@next', value: isSemver })); +t.create('gets the package version of left-pad from a custom registry') + .get('/v/left-pad.json?registry_uri=https://registry.npmjs.com') + .expectJSONTypes(Joi.object().keys({ name: 'npm', value: isSemver })); + t.create('gets the tagged package version of @cycle/core') .get('/v/@cycle/core/canary.json') .expectJSONTypes(Joi.object().keys({ name: 'npm@canary', value: isSemver })); +t.create('gets the tagged package version of @cycle/core from a custom registry') + .get('/v/@cycle/core/canary.json?registry_uri=https://registry.npmjs.com') + .expectJSONTypes(Joi.object().keys({ name: 'npm@canary', value: isSemver })); + +t.create('gets the license of express') + .get('/l/express.json') + .expectJSONTypes(Joi.object().keys({ name: 'license', value: 'MIT' })); + +t.create('gets the license of express from a custom registry') + .get('/l/express.json?registry_uri=https://registry.npmjs.com') + .expectJSONTypes(Joi.object().keys({ name: 'license', value: 'MIT' })); + t.create('invalid package name') .get('/v/frodo-is-not-a-package.json') .expectJSON({ name: 'npm', value: 'invalid' });