diff --git a/services/github/github-common-fetch.js b/services/github/github-common-fetch.js index 6eb60e43af4cbfe9f6e78c0f72dd7de72b742e03..aff6d6ec1dc46a2c8f00e04c19fe921e6630562c 100644 --- a/services/github/github-common-fetch.js +++ b/services/github/github-common-fetch.js @@ -40,7 +40,9 @@ async function fetchJsonFromRepo( schema: contentSchema, url, options, - errorMessages: errorMessagesFor(`${filename} missing or repo not found`), + errorMessages: errorMessagesFor( + `repo not found, branch not found, or ${filename} missing` + ), }) let decoded diff --git a/services/github/github-manifest.tester.js b/services/github/github-manifest.tester.js index 75cebbe57d6dcbe1195211de1e3139d925add1cf..55a77a5043b01eb6586f5100062ba7e3fd0f81ca 100644 --- a/services/github/github-manifest.tester.js +++ b/services/github/github-manifest.tester.js @@ -40,5 +40,5 @@ t.create('Manifest invalid json response') .get('/v/RedSparr0w/not-a-real-project.json') .expectJSON({ name: 'version', - value: 'manifest.json missing or repo not found', + value: 'repo not found, branch not found, or manifest.json missing', }) diff --git a/services/github/github-package-json.service.js b/services/github/github-package-json.service.js index 87ad2276c5f752cf77c13e12ffe60827b2fd4e85..870289905d4f685186273fb58aef5f8f7f2cdbe6 100644 --- a/services/github/github-package-json.service.js +++ b/services/github/github-package-json.service.js @@ -6,11 +6,17 @@ const { transformAndValidate, renderDynamicBadge, } = require('../dynamic-common') +const { + isPackageJsonWithDependencies, + getDependencyVersion, +} = require('../package-json-helpers') const { semver } = require('../validators') const { ConditionalGithubAuthService } = require('./github-auth-service') const { fetchJsonFromRepo } = require('./github-common-fetch') const { documentation } = require('./github-helpers') +const keywords = ['npm', 'node'] + const versionSchema = Joi.object({ version: semver, }).required() @@ -35,6 +41,7 @@ class GithubPackageJsonVersion extends ConditionalGithubAuthService { namedParams: { user: 'IcedFrisby', repo: 'IcedFrisby' }, staticPreview: this.render({ version: '2.0.0-alpha.2' }), documentation, + keywords, }, { title: 'GitHub package.json version (branch)', @@ -46,6 +53,7 @@ class GithubPackageJsonVersion extends ConditionalGithubAuthService { }, staticPreview: this.render({ version: '2.0.0-alpha.2' }), documentation, + keywords, }, ] } @@ -70,6 +78,99 @@ class GithubPackageJsonVersion extends ConditionalGithubAuthService { } } +class GithubPackageJsonDependencyVersion extends ConditionalGithubAuthService { + static get category() { + return 'platform-support' + } + + static get route() { + return { + base: 'github/package-json/dependency-version', + pattern: + ':user/:repo/:kind(dev|peer)?/:scope(@[^/]+)?/:packageName/:branch*', + } + } + + static get examples() { + return [ + { + title: 'GitHub package.json dependency version (prod)', + pattern: ':user/:repo/:packageName', + namedParams: { + user: 'developit', + repo: 'microbundle', + packageName: 'rollup', + }, + staticPreview: this.render({ + dependency: 'rollup', + range: '^0.67.3', + }), + documentation, + keywords, + }, + { + title: 'GitHub package.json dependency version (dev dep on branch)', + pattern: ':user/:repo/dev/:scope?/:packageName/:branch*', + namedParams: { + user: 'zeit', + repo: 'next.js', + branch: 'canary', + scope: '@babel', + packageName: 'preset-react', + }, + staticPreview: this.render({ + dependency: '@babel/preset-react', + range: '7.0.0', + }), + documentation, + keywords, + }, + ] + } + + static get defaultBadgeData() { + return { + label: 'dependency', + } + } + + static render({ dependency, range }) { + return { + label: dependency, + message: range, + color: 'blue', + } + } + + async handle({ user, repo, kind, branch = 'master', scope, packageName }) { + const { + dependencies, + devDependencies, + peerDependencies, + } = await fetchJsonFromRepo(this, { + schema: isPackageJsonWithDependencies, + user, + repo, + branch, + filename: 'package.json', + }) + + const wantedDependency = scope ? `${scope}/${packageName}` : packageName + const { range } = getDependencyVersion({ + kind, + wantedDependency, + dependencies, + devDependencies, + peerDependencies, + }) + + return this.constructor.render({ + dependency: wantedDependency, + range, + }) + } +} + class DynamicGithubPackageJson extends ConditionalGithubAuthService { static get category() { return 'other' @@ -98,6 +199,7 @@ class DynamicGithubPackageJson extends ConditionalGithubAuthService { value: ['bundle', 'rollup', 'micro library'], }), documentation, + keywords, }, { title: 'GitHub package.json dynamic', @@ -114,6 +216,7 @@ class DynamicGithubPackageJson extends ConditionalGithubAuthService { branch: 'master', }), documentation, + keywords, }, ] } @@ -151,5 +254,6 @@ class DynamicGithubPackageJson extends ConditionalGithubAuthService { module.exports = { GithubPackageJsonVersion, + GithubPackageJsonDependencyVersion, DynamicGithubPackageJson, } diff --git a/services/github/github-package-json.tester.js b/services/github/github-package-json.tester.js index 83f69266e76a4ae2481048fe76079d75fd519e4a..d376bd1b02a035269d7c9a8b8cc674b5e6334bbc 100644 --- a/services/github/github-package-json.tester.js +++ b/services/github/github-package-json.tester.js @@ -3,6 +3,7 @@ const Joi = require('joi') const ServiceTester = require('../service-tester') const { isSemver } = require('../test-validators') +const { semverRange } = require('../validators') const t = (module.exports = new ServiceTester({ id: 'GithubPackageJson', @@ -23,7 +24,7 @@ t.create('Package version (repo not found)') .get('/v/badges/helmets.json') .expectJSON({ name: 'version', - value: 'package.json missing or repo not found', + value: 'repo not found, branch not found, or package.json missing', }) t.create('Package name') @@ -46,3 +47,57 @@ t.create('Package array') t.create('Package object') .get('/dependencies/badges/shields.json') .expectJSON({ name: 'package.json', value: 'invalid key value' }) + +t.create('Peer dependency version') + .get('/dependency-version/paulmelnikow/react-boxplot/peer/react.json') + .expectJSONTypes( + Joi.object({ + name: 'react', + value: semverRange, + }) + ) + +t.create('Dev dependency version') + .get( + '/dependency-version/paulmelnikow/react-boxplot/dev/react.json?label=react%20tested' + ) + .expectJSONTypes( + Joi.object({ + name: 'react tested', + value: semverRange, + }) + ) + +t.create('Prod prod dependency version') + .get('/dependency-version/paulmelnikow/react-boxplot/simple-statistics.json') + .expectJSONTypes( + Joi.object({ + name: 'simple-statistics', + value: semverRange, + }) + ) + +t.create('Scoped dependency') + .get('/dependency-version/badges/shields/dev/@babel/core.json') + .expectJSONTypes( + Joi.object({ + name: '@babel/core', + value: semverRange, + }) + ) + +t.create('Scoped dependency on branch') + .get('/dependency-version/zeit/next.js/dev/babel-eslint/alpha.json') + .expectJSONTypes( + Joi.object({ + name: 'babel-eslint', + value: semverRange, + }) + ) + +t.create('Unknown dependency') + .get('/dependency-version/paulmelnikow/react-boxplot/dev/i-made-this-up.json') + .expectJSON({ + name: 'dependency', + value: 'dev dependency not found', + }) diff --git a/services/npm/npm-base.js b/services/npm/npm-base.js index af016a278264f3a632b1e4ff1916835ef552e4de..2d7d239fe9f3d7de8d1eb9fb5eb9a21d1e5fbaf9 100644 --- a/services/npm/npm-base.js +++ b/services/npm/npm-base.js @@ -4,26 +4,15 @@ const Joi = require('joi') const serverSecrets = require('../../lib/server-secrets') const BaseJsonService = require('../base-json') const { InvalidResponse, NotFound } = require('../errors') -const { semverRange } = require('../validators') +const { isDependencyMap } = require('../package-json-helpers') const deprecatedLicenseObjectSchema = Joi.object({ type: Joi.string().required(), }) -const dependencyMap = Joi.object() - .pattern( - /./, - Joi.alternatives().try( - semverRange, - Joi.string() - .uri() - .required() - ) - ) - .default({}) -const schema = Joi.object({ - dependencies: dependencyMap, - devDependencies: dependencyMap, - peerDependencies: dependencyMap, +const packageDataSchema = Joi.object({ + dependencies: isDependencyMap, + devDependencies: isDependencyMap, + peerDependencies: isDependencyMap, engines: Joi.object().pattern(/./, Joi.string()), license: Joi.alternatives().try( Joi.string(), @@ -135,6 +124,6 @@ module.exports = class NpmBase extends BaseJsonService { } } - return this.constructor._validate(packageData, schema) + return this.constructor._validate(packageData, packageDataSchema) } } diff --git a/services/npm/npm-dependency-version.service.js b/services/npm/npm-dependency-version.service.js index a741aa7af814f1125f351d7a4ec2786c7b6e1739..82c3fa4a488e6ed0fc57177ecb8439b32a1b8a5b 100644 --- a/services/npm/npm-dependency-version.service.js +++ b/services/npm/npm-dependency-version.service.js @@ -1,6 +1,6 @@ 'use strict' -const { InvalidParameter } = require('../errors') +const { getDependencyVersion } = require('../package-json-helpers') const NpmBase = require('./npm-base') const keywords = ['node'] @@ -78,30 +78,6 @@ module.exports = class NpmDependencyVersion extends NpmBase { } } - transform({ - kind, - wantedDependency, - dependencies, - devDependencies, - peerDependencies, - }) { - let dependenciesOfKind - if (kind === 'peer') { - dependenciesOfKind = peerDependencies - } else if (kind === 'dev') { - dependenciesOfKind = devDependencies - } else { - dependenciesOfKind = dependencies - } - - const range = dependenciesOfKind[wantedDependency] - if (range === undefined) { - throw new InvalidParameter({ prettyMessage: 'not found' }) - } - - return { range } - } - async handle(namedParams, queryParams) { const { scope, packageName, registryUrl } = this.constructor.unpackParams( namedParams, @@ -119,7 +95,7 @@ module.exports = class NpmDependencyVersion extends NpmBase { registryUrl, }) - const { range } = this.transform({ + const { range } = getDependencyVersion({ kind, wantedDependency, dependencies, diff --git a/services/npm/npm-dependency-version.tester.js b/services/npm/npm-dependency-version.tester.js index 5232feda50312280e338bca11b91184c5e88baf3..5a998e8a25bb43af2b9d6c8df64c4bd9fb785ae6 100644 --- a/services/npm/npm-dependency-version.tester.js +++ b/services/npm/npm-dependency-version.tester.js @@ -44,5 +44,5 @@ t.create('unknown dependency') .get('/react-boxplot/dev/i-made-this-up.json') .expectJSON({ name: 'dependency', - value: 'not found', + value: 'dev dependency not found', }) diff --git a/services/package-json-helpers.js b/services/package-json-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..9ad97dc03cee3e1cc26e8fec2869fdee56e3f6cc --- /dev/null +++ b/services/package-json-helpers.js @@ -0,0 +1,54 @@ +'use strict' + +const Joi = require('joi') +const { InvalidParameter } = require('./errors') + +const isDependencyMap = Joi.object() + .pattern( + /./, + // This accepts a semver range, a URL, and many other possible values. + Joi.string() + .min(1) + .required() + ) + .default({}) + +const isPackageJsonWithDependencies = Joi.object({ + dependencies: isDependencyMap, + devDependencies: isDependencyMap, + peerDependencies: isDependencyMap, +}).required() + +function getDependencyVersion({ + kind = 'prod', + wantedDependency, + dependencies, + devDependencies, + peerDependencies, +}) { + let dependenciesOfKind + if (kind === 'peer') { + dependenciesOfKind = peerDependencies + } else if (kind === 'dev') { + dependenciesOfKind = devDependencies + } else if (kind === 'prod') { + dependenciesOfKind = dependencies + } else { + throw Error(`Not very kind: ${kind}`) + } + + const range = dependenciesOfKind[wantedDependency] + if (range === undefined) { + throw new InvalidParameter({ + prettyMessage: `${kind} dependency not found`, + }) + } + + return { range } +} + +module.exports = { + isDependencyMap, + isPackageJsonWithDependencies, + getDependencyVersion, +}