diff --git a/services/node/node.service.js b/services/node/node-base.js similarity index 82% rename from services/node/node.service.js rename to services/node/node-base.js index 31cfef4b1cbbe..2af26cb1069dc 100644 --- a/services/node/node.service.js +++ b/services/node/node-base.js @@ -1,23 +1,24 @@ 'use strict' const NPMBase = require('../npm/npm-base') -const { versionColorForRange } = require('./node-version-color') const keywords = ['npm'] -module.exports = class NodeVersion extends NPMBase { +module.exports = class NodeVersionBase extends NPMBase { static get category() { return 'platform-support' } static get route() { - return this.buildRoute('node/v', { withTag: true }) + return this.buildRoute(`node/${this.path}`, { withTag: true }) } static get examples() { + const type = this.type + const prefix = `node-${type}` return [ { - title: 'node', + title: `${prefix}`, pattern: ':packageName', namedParams: { packageName: 'passport' }, staticPreview: this.renderStaticPreview({ @@ -26,7 +27,7 @@ module.exports = class NodeVersion extends NPMBase { keywords, }, { - title: 'node (scoped)', + title: `${prefix} (scoped)`, pattern: '@:scope/:packageName', namedParams: { scope: 'stdlib', packageName: 'stdlib' }, staticPreview: this.renderStaticPreview({ @@ -35,7 +36,7 @@ module.exports = class NodeVersion extends NPMBase { keywords, }, { - title: 'node (tag)', + title: `${prefix} (tag)`, pattern: ':packageName/:tag', namedParams: { packageName: 'passport', tag: 'latest' }, staticPreview: this.renderStaticPreview({ @@ -45,7 +46,7 @@ module.exports = class NodeVersion extends NPMBase { keywords, }, { - title: 'node (scoped with tag)', + title: `${prefix} (scoped with tag)`, pattern: '@:scope/:packageName/:tag', namedParams: { scope: 'stdlib', packageName: 'stdlib', tag: 'latest' }, staticPreview: this.renderStaticPreview({ @@ -55,7 +56,7 @@ module.exports = class NodeVersion extends NPMBase { keywords, }, { - title: 'node (scoped with tag, custom registry)', + title: `${prefix} (scoped with tag, custom registry)`, pattern: '@:scope/:packageName/:tag', namedParams: { scope: 'stdlib', packageName: 'stdlib', tag: 'latest' }, queryParams: { registry_uri: 'https://registry.npmjs.com' }, @@ -68,16 +69,12 @@ module.exports = class NodeVersion extends NPMBase { ] } - static get defaultBadgeData() { - return { label: 'node' } - } - static renderStaticPreview({ tag, nodeVersionRange }) { // Since this badge has an async `render()` function, but `get examples()` has to // be synchronous, this method exists. It should return the same value as the // real `render()`. There's a unit test to check that. return { - label: tag ? `node@${tag}` : undefined, + label: tag ? `${this.defaultBadgeData.label}@${tag}` : undefined, message: nodeVersionRange, color: 'brightgreen', } @@ -86,7 +83,7 @@ module.exports = class NodeVersion extends NPMBase { static async render({ tag, nodeVersionRange }) { // Atypically, the `render()` function of this badge is `async` because it needs to pull // data from the server. - const label = tag ? `node@${tag}` : undefined + const label = tag ? `${this.defaultBadgeData.label}@${tag}` : undefined if (nodeVersionRange === undefined) { return { @@ -98,7 +95,7 @@ module.exports = class NodeVersion extends NPMBase { return { label, message: nodeVersionRange, - color: await versionColorForRange(nodeVersionRange), + color: await this.colorResolver(nodeVersionRange), } } } diff --git a/services/node/node-current.service.js b/services/node/node-current.service.js new file mode 100644 index 0000000000000..27ace3ba4cf3d --- /dev/null +++ b/services/node/node-current.service.js @@ -0,0 +1,22 @@ +'use strict' + +const NodeVersionBase = require('./node-base') +const { versionColorForRangeCurrent } = require('./node-version-color') + +module.exports = class NodeCurrentVersion extends NodeVersionBase { + static get path() { + return 'v' + } + + static get defaultBadgeData() { + return { label: 'node' } + } + + static get type() { + return 'current' + } + + static get colorResolver() { + return versionColorForRangeCurrent + } +} diff --git a/services/node/node-current.spec.js b/services/node/node-current.spec.js new file mode 100644 index 0000000000000..a0a9e46e4b605 --- /dev/null +++ b/services/node/node-current.spec.js @@ -0,0 +1,23 @@ +'use strict' + +const { test, given } = require('sazerac') +const NodeVersion = require('./node-current.service') + +describe('node static renderStaticPreview', function() { + it('should have parity with render()', async function() { + const nodeVersionRange = '>= 6.0.0' + + const expectedNoTag = await NodeVersion.renderStaticPreview({ + nodeVersionRange, + }) + const expectedLatestTag = await NodeVersion.renderStaticPreview({ + nodeVersionRange, + tag: 'latest', + }) + + test(NodeVersion.renderStaticPreview.bind(NodeVersion), () => { + given({ nodeVersionRange }).expect(expectedNoTag) + given({ nodeVersionRange, tag: 'latest' }).expect(expectedLatestTag) + }) + }) +}) diff --git a/services/node/node-current.tester.js b/services/node/node-current.tester.js new file mode 100644 index 0000000000000..b9ae1d9e55cb1 --- /dev/null +++ b/services/node/node-current.tester.js @@ -0,0 +1,157 @@ +'use strict' + +const { expect } = require('chai') +const { Range } = require('semver') +const t = (module.exports = require('../tester').createServiceTester()) +const { mockPackageData, mockCurrentSha } = require('./testUtils/test-utils') + +function expectSemverRange(message) { + expect(() => new Range(message)).not.to.throw() +} + +t.create('gets the node version of passport') + .get('/passport.json') + .expectBadge({ label: 'node' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies current node version') + .get('/passport.json') + .intercept( + mockPackageData({ + packageName: 'passport', + engines: '>=0.4.0', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ label: 'node', message: `>=0.4.0`, color: `brightgreen` }) + +t.create('engines does not satisfy current node version') + .get('/passport.json') + .intercept( + mockPackageData({ + packageName: 'passport', + engines: '12', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ label: 'node', message: `12`, color: `yellow` }) + +t.create('gets the node version of @stdlib/stdlib') + .get('/@stdlib/stdlib.json') + .expectBadge({ label: 'node' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies current node version - scoped') + .get('/@stdlib/stdlib.json') + .intercept( + mockPackageData({ + packageName: 'stdlib', + engines: '>=0.4.0', + scope: '@stdlib', + tag: '', + registry: '', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ label: 'node', message: `>=0.4.0`, color: `brightgreen` }) + +t.create('engines does not satisfy current node version - scoped') + .get('/@stdlib/stdlib.json') + .intercept( + mockPackageData({ + packageName: 'stdlib', + engines: '12', + scope: '@stdlib', + tag: '', + registry: '', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ label: 'node', message: `12`, color: `yellow` }) + +t.create("gets the tagged release's node version version of ionic") + .get('/ionic/testing.json') + .expectBadge({ label: 'node@testing' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies current node version - tagged') + .get('/ionic/testing.json') + .intercept( + mockPackageData({ + packageName: 'ionic', + engines: '>=0.4.0', + tag: 'testing', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ + label: 'node@testing', + message: `>=0.4.0`, + color: `brightgreen`, + }) + +t.create('engines does not satisfy current node version - tagged') + .get('/ionic/testing.json') + .intercept( + mockPackageData({ + packageName: 'ionic', + engines: '12', + tag: 'testing', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ label: 'node@testing', message: `12`, color: `yellow` }) + +t.create("gets the tagged release's node version of @cycle/core") + .get('/@cycle/core/canary.json') + .expectBadge({ label: 'node@canary' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies current node version - scoped and tagged') + .get('/@cycle/core/canary.json') + .intercept( + mockPackageData({ + packageName: 'core', + engines: '>=0.4.0', + scope: '@cycle', + tag: 'canary', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ + label: 'node@canary', + message: `>=0.4.0`, + color: `brightgreen`, + }) + +t.create('engines does not satisfy current node version - scoped and tagged') + .get('/@cycle/core/canary.json') + .intercept( + mockPackageData({ + packageName: 'core', + engines: '12', + scope: '@cycle', + tag: 'canary', + }) + ) + .intercept(mockCurrentSha(13)) + .expectBadge({ label: 'node@canary', message: `12`, color: `yellow` }) + +t.create('gets the node version of passport from a custom registry') + .get('/passport.json?registry_uri=https://registry.npmjs.com') + .expectBadge({ label: 'node' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('invalid package name') + .get('/frodo-is-not-a-package.json') + .expectBadge({ label: 'node', message: 'package not found' }) diff --git a/services/node/node-lts.service.js b/services/node/node-lts.service.js new file mode 100644 index 0000000000000..73477191eb36d --- /dev/null +++ b/services/node/node-lts.service.js @@ -0,0 +1,22 @@ +'use strict' + +const NodeVersionBase = require('./node-base') +const { versionColorForRangeLts } = require('./node-version-color') + +module.exports = class NodeLtsVersion extends NodeVersionBase { + static get path() { + return 'v-lts' + } + + static get defaultBadgeData() { + return { label: 'node-lts' } + } + + static get type() { + return 'lts' + } + + static get colorResolver() { + return versionColorForRangeLts + } +} diff --git a/services/node/node.spec.js b/services/node/node-lts.spec.js similarity index 75% rename from services/node/node.spec.js rename to services/node/node-lts.spec.js index 542b412adb1b8..7dfc5ad0bc8cd 100644 --- a/services/node/node.spec.js +++ b/services/node/node-lts.spec.js @@ -1,9 +1,9 @@ 'use strict' const { test, given } = require('sazerac') -const NodeVersion = require('./node.service') +const NodeVersion = require('./node-lts.service') -describe('renderStaticPreview', function() { +describe('node-lts renderStaticPreview', function() { it('should have parity with render()', async function() { const nodeVersionRange = '>= 6.0.0' @@ -15,7 +15,7 @@ describe('renderStaticPreview', function() { tag: 'latest', }) - test(NodeVersion.renderStaticPreview, () => { + test(NodeVersion.renderStaticPreview.bind(NodeVersion), () => { given({ nodeVersionRange }).expect(expectedNoTag) given({ nodeVersionRange, tag: 'latest' }).expect(expectedLatestTag) }) diff --git a/services/node/node-lts.tester.js b/services/node/node-lts.tester.js new file mode 100644 index 0000000000000..f2d47331fee54 --- /dev/null +++ b/services/node/node-lts.tester.js @@ -0,0 +1,217 @@ +'use strict' + +const { expect } = require('chai') +const { Range } = require('semver') +const t = (module.exports = require('../tester').createServiceTester()) +const { + mockPackageData, + mockReleaseSchedule, + mockVersionsSha, +} = require('./testUtils/test-utils') + +function expectSemverRange(message) { + expect(() => new Range(message)).not.to.throw() +} + +t.create('gets the node version of passport') + .get('/passport.json') + .expectBadge({ label: 'node-lts' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies all lts node versions') + .get('/passport.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'passport', + engines: '10 - 12', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts', message: `10 - 12`, color: `brightgreen` }) + +t.create('engines does not satisfy all lts node versions') + .get('/passport.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'passport', + engines: '8', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts', message: `8`, color: `orange` }) + +t.create('engines satisfies some lts node versions') + .get('/passport.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'passport', + engines: '10', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts', message: `10`, color: `yellow` }) + +t.create('gets the node version of @stdlib/stdlib') + .get('/@stdlib/stdlib.json') + .expectBadge({ label: 'node-lts' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies all lts node versions - scoped') + .get('/@stdlib/stdlib.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'stdlib', + engines: '10 - 12', + scope: '@stdlib', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts', message: `10 - 12`, color: `brightgreen` }) + +t.create('engines does not satisfy all lts node versions - scoped') + .get('/@stdlib/stdlib.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'stdlib', + engines: '8', + scope: '@stdlib', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts', message: `8`, color: `orange` }) + +t.create('engines satisfies some lts node versions - scoped') + .get('/@stdlib/stdlib.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'stdlib', + engines: '10', + scope: '@stdlib', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts', message: `10`, color: `yellow` }) + +t.create("gets the tagged release's node version version of ionic") + .get('/ionic/testing.json') + .expectBadge({ label: 'node-lts@testing' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies all lts node versions - tagged') + .get('/ionic/testing.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'ionic', + engines: '10 - 12', + tag: 'testing', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ + label: 'node-lts@testing', + message: `10 - 12`, + color: `brightgreen`, + }) + +t.create('engines does not satisfy all lts node versions - tagged') + .get('/ionic/testing.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'ionic', + engines: '8', + tag: 'testing', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts@testing', message: `8`, color: `orange` }) + +t.create('engines satisfies some lts node versions - tagged') + .get('/ionic/testing.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'ionic', + engines: '10', + tag: 'testing', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts@testing', message: `10`, color: `yellow` }) + +t.create("gets the tagged release's node version of @cycle/core") + .get('/@cycle/core/canary.json') + .expectBadge({ label: 'node-lts@canary' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('engines satisfies all lts node versions - scoped and tagged') + .get('/@cycle/core/canary.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'core', + engines: '10 - 12', + scope: '@cycle', + tag: 'canary', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ + label: 'node-lts@canary', + message: `10 - 12`, + color: `brightgreen`, + }) + +t.create('engines does not satisfy all lts node versions - scoped and tagged') + .get('/@cycle/core/canary.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'core', + engines: '8', + scope: '@cycle', + tag: 'canary', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts@canary', message: `8`, color: `orange` }) + +t.create('engines satisfies some lts node versions - scoped and tagged') + .get('/@cycle/core/canary.json') + .intercept(mockReleaseSchedule()) + .intercept( + mockPackageData({ + packageName: 'core', + engines: '10', + scope: '@cycle', + tag: 'canary', + }) + ) + .intercept(mockVersionsSha()) + .expectBadge({ label: 'node-lts@canary', message: `10`, color: `yellow` }) + +t.create('gets the node version of passport from a custom registry') + .get('/passport.json?registry_uri=https://registry.npmjs.com') + .expectBadge({ label: 'node-lts' }) + .afterJSON(json => { + expectSemverRange(json.message) + }) + +t.create('invalid package name') + .get('/frodo-is-not-a-package.json') + .expectBadge({ label: 'node-lts', message: 'package not found' }) diff --git a/services/node/node-version-color.js b/services/node/node-version-color.js index 3e0c5b128e757..9208bcbce0593 100644 --- a/services/node/node-version-color.js +++ b/services/node/node-version-color.js @@ -1,12 +1,19 @@ 'use strict' const { promisify } = require('util') +const moment = require('moment') const semver = require('semver') const { regularUpdate } = require('../../core/legacy/regular-update') -function getLatestVersion() { +const dateFormat = 'YYYY-MM-DD' + +function getVersion(version) { + let semver = `` + if (version) { + semver = `-${version}.x` + } return promisify(regularUpdate)({ - url: 'https://nodejs.org/dist/latest/SHASUMS256.txt', + url: `https://nodejs.org/dist/latest${semver}/SHASUMS256.txt`, intervalMillis: 24 * 3600 * 1000, json: false, scraper: shasums => { @@ -14,14 +21,57 @@ function getLatestVersion() { 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 + return tarball.split('-')[1] }, }) } -async function versionColorForRange(range) { - const latestVersion = await getLatestVersion() +function ltsVersionsScraper(versions) { + const currentDate = moment().format(dateFormat) + return Object.keys(versions).filter(function(version) { + const data = versions[version] + return data.lts && data.lts < currentDate && data.end > currentDate + }) +} + +async function getCurrentVersion() { + return getVersion() +} + +async function getLtsVersions() { + const versions = await promisify(regularUpdate)({ + url: + 'https://raw.githubusercontent.com/nodejs/Release/master/schedule.json', + intervalMillis: 24 * 3600 * 1000, + json: true, + scraper: ltsVersionsScraper, + }) + return Promise.all(versions.map(getVersion)) +} + +async function versionColorForRangeLts(range) { + const ltsVersions = await getLtsVersions() + try { + const matchesAll = ltsVersions.reduce(function(satisfies, version) { + return satisfies && semver.satisfies(version, range) + }, true) + const matchesSome = ltsVersions.reduce(function(satisfies, version) { + return satisfies || semver.satisfies(version, range) + }, false) + if (matchesAll) { + return 'brightgreen' + } else if (matchesSome) { + return 'yellow' + } else { + return 'orange' + } + } catch (e) { + return 'lightgray' + } +} + +async function versionColorForRangeCurrent(range) { + const latestVersion = await getCurrentVersion() try { if (semver.satisfies(latestVersion, range)) { return 'brightgreen' @@ -36,6 +86,6 @@ async function versionColorForRange(range) { } module.exports = { - getLatestVersion, - versionColorForRange, + versionColorForRangeCurrent, + versionColorForRangeLts, } diff --git a/services/node/node.tester.js b/services/node/node.tester.js deleted file mode 100644 index 52a756c46d273..0000000000000 --- a/services/node/node.tester.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict' - -const { expect } = require('chai') -const { Range } = require('semver') -const t = (module.exports = require('../tester').createServiceTester()) - -function expectSemverRange(message) { - expect(() => new Range(message)).not.to.throw() -} - -t.create('gets the node version of passport') - .get('/passport.json') - .expectBadge({ label: 'node' }) - .afterJSON(json => { - expectSemverRange(json.message) - }) - -t.create('gets the node version of @stdlib/stdlib') - .get('/@stdlib/stdlib.json') - .expectBadge({ label: 'node' }) - .afterJSON(json => { - expectSemverRange(json.message) - }) - -t.create("gets the tagged release's node version version of ionic") - .get('/ionic/next.json') - .expectBadge({ label: 'node@next' }) - .afterJSON(json => { - expectSemverRange(json.message) - }) - -t.create('gets the node version of passport from a custom registry') - .get('/passport.json?registry_uri=https://registry.npmjs.com') - .expectBadge({ label: 'node' }) - .afterJSON(json => { - expectSemverRange(json.message) - }) - -t.create("gets the tagged release's node version of @cycle/core") - .get('/@cycle/core/canary.json') - .expectBadge({ label: 'node@canary' }) - .afterJSON(json => { - expectSemverRange(json.message) - }) - -t.create('invalid package name') - .get('/frodo-is-not-a-package.json') - .expectBadge({ label: 'node', message: 'package not found' }) diff --git a/services/node/testUtils/packageJsonTemplate.json b/services/node/testUtils/packageJsonTemplate.json new file mode 100644 index 0000000000000..e69cdb52be1ea --- /dev/null +++ b/services/node/testUtils/packageJsonTemplate.json @@ -0,0 +1,11 @@ +{ + "engines": { + "node": ">= 0.4.0" + }, + "maintainers": [ + { + "name": "jaredhanson", + "email": "jaredhanson@gmail.com" + } + ] +} diff --git a/services/node/testUtils/packageJsonVersionsTemplate.json b/services/node/testUtils/packageJsonVersionsTemplate.json new file mode 100644 index 0000000000000..818c864975af4 --- /dev/null +++ b/services/node/testUtils/packageJsonVersionsTemplate.json @@ -0,0 +1,29 @@ +{ + "dist-tags": { + "latest": "0.0.91" + }, + "versions": { + "0.0.90": { + "engines": { + "node": ">= 0.4.0" + }, + "maintainers": [ + { + "name": "jaredhanson", + "email": "jaredhanson@gmail.com" + } + ] + }, + "0.0.91": { + "engines": { + "node": ">= 0.4.0" + }, + "maintainers": [ + { + "name": "jaredhanson", + "email": "jaredhanson@gmail.com" + } + ] + } + } +} diff --git a/services/node/testUtils/test-utils.js b/services/node/testUtils/test-utils.js new file mode 100644 index 0000000000000..bc0512539d519 --- /dev/null +++ b/services/node/testUtils/test-utils.js @@ -0,0 +1,169 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const moment = require('moment') + +const dateFormat = 'YYYY-MM-DD' + +const templates = { + packageJsonVersionsTemplate: fs.readFileSync( + path.join(__dirname, `packageJsonVersionsTemplate.json`), + 'utf-8' + ), + packageJsonTemplate: fs.readFileSync( + path.join(__dirname, `packageJsonTemplate.json`), + 'utf-8' + ), +} + +const getTemplate = template => JSON.parse(templates[template]) + +const mockPackageData = ({ packageName, engines, scope, tag }) => nock => { + let packageJson + let urlPath + if (scope || tag) { + if (scope) { + urlPath = `/${scope}%2F${packageName}` + } else { + urlPath = `/${packageName}` + } + packageJson = getTemplate('packageJsonVersionsTemplate') + packageJson['dist-tags'][tag || 'latest'] = '0.0.91' + packageJson.versions['0.0.91'].engines.node = engines + } else { + urlPath = `/${packageName}/latest` + packageJson = getTemplate('packageJsonTemplate') + packageJson.engines.node = engines + } + return nock('https://registry.npmjs.org/') + .get(urlPath) + .reply(200, packageJson) +} + +const mockCurrentSha = latestVersion => nock => { + const latestSha = `node-v${latestVersion}.12.0-aix-ppc64.tar.gz` + return nock('https://nodejs.org/dist/') + .get(`/latest/SHASUMS256.txt`) + .reply(200, latestSha) +} + +const mockVersionsSha = () => nock => { + let scope = nock('https://nodejs.org/dist/') + for (const version of [10, 12]) { + const latestSha = `node-v${version}.12.0-aix-ppc64.tar.gz` + scope = scope + .get(`/latest-v${version}.x/SHASUMS256.txt`) + .reply(200, latestSha) + } + return scope +} + +const mockReleaseSchedule = () => nock => { + const currentDate = moment() + const schedule = { + 'v0.10': { + start: '2013-03-11', + end: '2016-10-31', + }, + 'v0.12': { + start: '2015-02-06', + end: '2016-12-31', + }, + v4: { + start: '2015-09-08', + lts: '2015-10-12', + maintenance: '2017-04-01', + end: '2018-04-30', + codename: 'Argon', + }, + v5: { + start: '2015-10-29', + maintenance: '2016-04-30', + end: '2016-06-30', + }, + v6: { + start: '2016-04-26', + lts: '2016-10-18', + maintenance: '2018-04-30', + end: '2019-04-30', + codename: 'Boron', + }, + v7: { + start: '2016-10-25', + maintenance: '2017-04-30', + end: '2017-06-30', + }, + v8: { + start: '2017-05-30', + lts: '2017-10-31', + maintenance: '2019-01-01', + end: '2019-12-31', + codename: 'Carbon', + }, + v9: { + start: '2017-10-01', + maintenance: '2018-04-01', + end: '2018-06-30', + }, + v10: { + start: '2018-04-24', + lts: currentDate + .clone() + .subtract(6, 'month') + .format(dateFormat), + maintenance: '2020-04-30', + end: currentDate + .clone() + .add(1, 'month') + .format(dateFormat), + codename: 'Dubnium', + }, + v11: { + start: '2018-10-23', + maintenance: '2019-04-22', + end: '2019-06-01', + }, + v12: { + start: '2019-04-23', + lts: currentDate + .clone() + .subtract(1, 'month') + .format(dateFormat), + maintenance: '2020-10-20', + end: currentDate + .clone() + .add(6, 'month') + .format(dateFormat), + codename: 'Erbium', + }, + v13: { + start: '2019-10-22', + maintenance: '2020-04-01', + end: '2020-06-01', + }, + v14: { + start: '2020-04-21', + lts: currentDate + .clone() + .add(4, 'month') + .format(dateFormat), + maintenance: '2021-10-19', + end: currentDate + .clone() + .add(12, 'month') + .format(dateFormat), + codename: '', + }, + } + return nock('https://raw.githubusercontent.com/') + .get(`/nodejs/Release/master/schedule.json`) + .reply(200, schedule) +} + +module.exports = { + mockPackageData, + mockCurrentSha, + mockVersionsSha, + mockReleaseSchedule, +} diff --git a/services/npm/npm-base.js b/services/npm/npm-base.js index bded61a3910e1..3ca60ccc52408 100644 --- a/services/npm/npm-base.js +++ b/services/npm/npm-base.js @@ -98,11 +98,15 @@ module.exports = class NpmBase extends BaseJsonService { async fetchPackageData({ registryUrl, scope, packageName, tag }) { registryUrl = registryUrl || this.constructor.defaultRegistryUrl let url - if (scope === undefined) { + if (scope === undefined && tag === 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. url = `${registryUrl}/${packageName}/latest` + } else if (scope === undefined && tag !== undefined) { + // e.g. https://registry.npmjs.org/express + // because https://registry.npmjs.org/express/canary does not work + url = `${registryUrl}/${packageName}` } else { // e.g. https://registry.npmjs.org/@cedx%2Fgulp-david // because https://registry.npmjs.org/@cedx%2Fgulp-david/latest does not work @@ -120,7 +124,7 @@ module.exports = class NpmBase extends BaseJsonService { }) let packageData - if (scope === undefined) { + if (scope === undefined && tag === undefined) { packageData = json } else { const registryTag = tag || 'latest'