/
npm-base.js
135 lines (124 loc) · 3.86 KB
/
npm-base.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
'use strict'
const Joi = require('@hapi/joi')
const serverSecrets = require('../../lib/server-secrets')
const { BaseJsonService, InvalidResponse, NotFound } = require('..')
const { optionalUrl } = require('../validators')
const { isDependencyMap } = require('../package-json-helpers')
const deprecatedLicenseObjectSchema = Joi.object({
type: Joi.string().required(),
})
const packageDataSchema = Joi.object({
dependencies: isDependencyMap,
devDependencies: isDependencyMap,
peerDependencies: isDependencyMap,
engines: Joi.object().pattern(/./, Joi.string()),
license: Joi.alternatives().try(
Joi.string(),
deprecatedLicenseObjectSchema,
Joi.array().items(
Joi.alternatives(Joi.string(), deprecatedLicenseObjectSchema)
)
),
maintainers: Joi.array()
// We don't need the keys here, just the length.
.items(Joi.object({}))
.required(),
types: Joi.string(),
files: Joi.array()
.items(Joi.string())
.default([]),
}).required()
const queryParamSchema = Joi.object({
registry_uri: optionalUrl,
}).required()
// Abstract class for NPM badges which display data about the latest version
// of a package.
module.exports = class NpmBase extends BaseJsonService {
static buildRoute(base, { withTag } = {}) {
if (withTag) {
return {
base,
pattern: ':scope(@[^/]+)?/:packageName/:tag?',
queryParamSchema,
}
} else {
return {
base,
pattern: ':scope(@[^/]+)?/:packageName',
queryParamSchema,
}
}
}
static unpackParams(
{ scope, packageName, tag },
{ registry_uri: registryUrl = 'https://registry.npmjs.org' }
) {
return {
scope,
packageName,
tag,
registryUrl,
}
}
static encodeScopedPackage({ scope, packageName }) {
const scopeWithoutAt = scope.replace(/^@/, '')
// e.g. https://registry.npmjs.org/@cedx%2Fgulp-david
const encoded = encodeURIComponent(`${scopeWithoutAt}/${packageName}`)
return `@${encoded}`
}
async _requestJson(data) {
// Use a custom Accept header because of this bug:
// <https://github.com/npm/npmjs.org/issues/163>
const headers = { Accept: '*/*' }
if (serverSecrets.npm_token) {
headers.Authorization = `Bearer ${serverSecrets.npm_token}`
}
return super._requestJson({
...data,
options: { headers },
})
}
async fetchPackageData({ registryUrl, scope, packageName, tag }) {
registryUrl = registryUrl || this.constructor.defaultRegistryUrl
let url
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.
url = `${registryUrl}/${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 scoped = this.constructor.encodeScopedPackage({
scope,
packageName,
})
url = `${registryUrl}/${scoped}`
}
const json = await this._requestJson({
// We don't validate here because we need to pluck the desired subkey first.
schema: Joi.any(),
url,
errorMessages: { 404: 'package not found' },
})
let packageData
if (scope === undefined) {
packageData = json
} else {
const registryTag = tag || 'latest'
let latestVersion
try {
latestVersion = json['dist-tags'][registryTag]
} catch (e) {
throw new NotFound({ prettyMessage: 'tag not found' })
}
try {
packageData = json.versions[latestVersion]
} catch (e) {
throw new InvalidResponse({ prettyMessage: 'invalid json response' })
}
}
return this.constructor._validate(packageData, packageDataSchema)
}
}
module.exports.queryParamSchema = queryParamSchema