2020-02-12 12:17:48 +00:00
import delay from 'delay' ;
2019-08-15 04:30:16 +00:00
import moment from 'moment' ;
import url from 'url' ;
import getRegistryUrl from 'registry-auth-token/registry-url' ;
import registryAuthToken from 'registry-auth-token' ;
2019-12-04 11:06:00 +00:00
import isBase64 from 'validator/lib/isBase64' ;
2019-08-15 04:30:16 +00:00
import { OutgoingHttpHeaders } from 'http' ;
2019-09-17 10:50:11 +00:00
import is from '@sindresorhus/is' ;
2019-08-15 04:30:16 +00:00
import { logger } from '../../logger' ;
2020-02-12 12:17:48 +00:00
import got , { GotJSONOptions } from '../../util/got' ;
2019-08-15 04:30:16 +00:00
import { maskToken } from '../../util/mask' ;
import { getNpmrc } from './npmrc' ;
2020-02-13 12:29:55 +00:00
import { DatasourceError , Release , ReleaseResult } from '../common' ;
2020-01-16 11:14:35 +00:00
import { DATASOURCE_NPM } from '../../constants/data-binary-source' ;
2019-01-05 07:00:07 +00:00
let memcache = { } ;
2019-11-26 15:13:07 +00:00
export function resetMemCache ( ) : void {
2019-01-05 07:00:07 +00:00
logger . debug ( 'resetMemCache()' ) ;
memcache = { } ;
}
2019-11-26 15:13:07 +00:00
export function resetCache ( ) : void {
2019-01-05 07:00:07 +00:00
resetMemCache ( ) ;
}
2019-08-15 04:30:16 +00:00
export interface NpmRelease extends Release {
canBeUnpublished? : boolean ;
gitRef? : string ;
}
export interface NpmDependency extends ReleaseResult {
releases : NpmRelease [ ] ;
deprecationSource? : string ;
name : string ;
homepage : string ;
latestVersion : string ;
sourceUrl : string ;
versions : Record < string , any > ;
'dist-tags' : string [ ] ;
'renovate-config' : any ;
sourceDirectory? : string ;
}
2019-08-19 14:44:12 +00:00
export async function getDependency (
2020-02-12 10:32:27 +00:00
name : string ,
retries = 3
2019-08-19 14:44:12 +00:00
) : Promise < NpmDependency | null > {
2019-01-05 07:00:07 +00:00
logger . trace ( ` npm.getDependency( ${ name } ) ` ) ;
// This is our datastore cache and is cleared at the end of each repo, i.e. we never requery/revalidate during a "run"
if ( memcache [ name ] ) {
logger . trace ( 'Returning cached result' ) ;
return JSON . parse ( memcache [ name ] ) ;
}
const scope = name . split ( '/' ) [ 0 ] ;
2019-08-15 04:30:16 +00:00
let regUrl : string ;
2019-01-05 07:00:07 +00:00
const npmrc = getNpmrc ( ) ;
try {
regUrl = getRegistryUrl ( scope , npmrc ) ;
} catch ( err ) {
regUrl = 'https://registry.npmjs.org' ;
}
const pkgUrl = url . resolve (
regUrl ,
encodeURIComponent ( name ) . replace ( /^%40/ , '@' )
) ;
2019-06-04 18:38:30 +00:00
// Now check the persistent cache
const cacheNamespace = 'datasource-npm' ;
2019-08-15 04:30:16 +00:00
const cachedResult = await renovateCache . get < NpmDependency > (
cacheNamespace ,
pkgUrl
) ;
2019-08-28 13:08:06 +00:00
// istanbul ignore if
2019-06-04 18:38:30 +00:00
if ( cachedResult ) {
return cachedResult ;
}
2019-01-05 07:00:07 +00:00
const authInfo = registryAuthToken ( regUrl , { npmrc } ) ;
2019-08-15 04:30:16 +00:00
const headers : OutgoingHttpHeaders = { } ;
2019-01-05 07:00:07 +00:00
if ( authInfo && authInfo . type && authInfo . token ) {
// istanbul ignore if
if ( npmrc && npmrc . massagedAuth && isBase64 ( authInfo . token ) ) {
logger . debug ( 'Massaging authorization type to Basic' ) ;
authInfo . type = 'Basic' ;
}
headers . authorization = ` ${ authInfo . type } ${ authInfo . token } ` ;
2019-01-27 06:36:00 +00:00
logger . trace (
2019-01-24 11:59:14 +00:00
{ token : maskToken ( authInfo . token ) , npmName : name } ,
'Using auth for npm lookup'
) ;
2019-01-05 07:00:07 +00:00
} else if ( process . env . NPM_TOKEN && process . env . NPM_TOKEN !== 'undefined' ) {
headers . authorization = ` Bearer ${ process . env . NPM_TOKEN } ` ;
}
2020-02-24 11:27:10 +00:00
const uri = url . parse ( pkgUrl ) ;
if ( uri . host === 'registry.npmjs.org' && ! uri . pathname . startsWith ( '/@' ) ) {
2019-01-05 07:00:07 +00:00
// Delete the authorization header for non-scoped public packages to improve http caching
// Otherwise, authenticated requests are not cacheable until the registry adds "public" to Cache-Control
// Ref: https://greenbytes.de/tech/webdav/rfc7234.html#caching.authenticated.responses
delete headers . authorization ;
}
// This tells our http layer not to serve responses directly from the cache and instead to revalidate them every time
headers [ 'Cache-Control' ] = 'no-cache' ;
try {
2020-02-12 12:17:48 +00:00
const useCache = retries === 3 ; // Disable cache if we're retrying
const opts : GotJSONOptions = {
2019-01-05 07:00:07 +00:00
json : true ,
2020-02-11 20:11:30 +00:00
retry : 5 ,
2019-01-05 07:00:07 +00:00
headers ,
2020-02-12 12:17:48 +00:00
useCache ,
2020-02-16 04:32:03 +00:00
readableHighWaterMark : 1024 * 1024 * 10 , // https://github.com/sindresorhus/got/issues/1062#issuecomment-586580036
2020-02-12 12:17:48 +00:00
} ;
const raw = await got ( pkgUrl , opts ) ;
2020-02-12 10:32:27 +00:00
// istanbul ignore if
if ( retries < 3 ) {
2020-02-24 07:43:01 +00:00
logger . debug ( { pkgUrl , retries } , 'Recovered from npm error' ) ;
2020-02-12 10:32:27 +00:00
}
2019-01-05 07:00:07 +00:00
const res = raw . body ;
// eslint-disable-next-line no-underscore-dangle
const returnedName = res . name ? res.name : res._id || '' ;
if ( returnedName . toLowerCase ( ) !== name . toLowerCase ( ) ) {
logger . warn (
{ lookupName : name , returnedName : res.name , regUrl } ,
'Returned name does not match with requested name'
) ;
return null ;
}
if ( ! res . versions || ! Object . keys ( res . versions ) . length ) {
// Registry returned a 200 OK but with no versions
2020-02-24 07:43:01 +00:00
logger . debug ( { dependency : name } , 'No versions returned' ) ;
2019-03-14 10:43:51 +00:00
return null ;
2019-01-05 07:00:07 +00:00
}
const latestVersion = res . versions [ res [ 'dist-tags' ] . latest ] ;
res . repository = res . repository || latestVersion . repository ;
res . homepage = res . homepage || latestVersion . homepage ;
// Determine repository URL
2019-08-15 04:30:16 +00:00
let sourceUrl : string ;
2019-01-05 07:00:07 +00:00
2019-12-20 08:27:58 +00:00
if ( res . repository ) {
if ( is . string ( res . repository ) ) {
sourceUrl = res . repository ;
} else if ( res . repository . url ) {
sourceUrl = res . repository . url ;
2019-01-05 07:00:07 +00:00
}
}
// Simplify response before caching and returning
2019-08-15 04:30:16 +00:00
const dep : NpmDependency = {
2019-01-05 07:00:07 +00:00
name : res.name ,
homepage : res.homepage ,
latestVersion : res [ 'dist-tags' ] . latest ,
sourceUrl ,
versions : { } ,
2019-08-15 04:30:16 +00:00
releases : null ,
2019-01-05 07:00:07 +00:00
'dist-tags' : res [ 'dist-tags' ] ,
'renovate-config' : latestVersion [ 'renovate-config' ] ,
} ;
2019-01-22 06:37:51 +00:00
if ( res . repository && res . repository . directory ) {
dep . sourceDirectory = res . repository . directory ;
}
2019-01-05 07:00:07 +00:00
if ( latestVersion . deprecated ) {
2019-06-07 04:34:57 +00:00
dep . deprecationMessage = ` On registry \` ${ regUrl } \` , the "latest" version (v ${ dep . latestVersion } ) of dependency \` ${ name } \` has the following deprecation notice: \ n \ n \` ${ latestVersion . deprecated } \` \ n \ nMarking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake. ` ;
2020-01-16 11:14:35 +00:00
dep . deprecationSource = DATASOURCE_NPM ;
2019-01-05 07:00:07 +00:00
}
dep . releases = Object . keys ( res . versions ) . map ( version = > {
2019-08-15 04:30:16 +00:00
const release : NpmRelease = {
2019-01-05 07:00:07 +00:00
version ,
gitRef : res.versions [ version ] . gitHead ,
} ;
if ( res . time && res . time [ version ] ) {
release . releaseTimestamp = res . time [ version ] ;
release . canBeUnpublished =
moment ( ) . diff ( moment ( release . releaseTimestamp ) , 'days' ) === 0 ;
}
if ( res . versions [ version ] . deprecated ) {
release . isDeprecated = true ;
}
return release ;
} ) ;
logger . trace ( { dep } , 'dep' ) ;
// serialize first before saving
memcache [ name ] = JSON . stringify ( dep ) ;
const cacheMinutes = process . env . RENOVATE_CACHE_NPM_MINUTES
? parseInt ( process . env . RENOVATE_CACHE_NPM_MINUTES , 10 )
: 5 ;
if ( ! name . startsWith ( '@' ) ) {
2019-06-04 18:38:30 +00:00
await renovateCache . set ( cacheNamespace , pkgUrl , dep , cacheMinutes ) ;
2019-01-05 07:00:07 +00:00
}
return dep ;
} catch ( err ) {
if ( err . statusCode === 401 || err . statusCode === 403 ) {
2020-02-24 07:43:01 +00:00
logger . debug (
2019-01-05 07:00:07 +00:00
{
pkgUrl ,
authInfoType : authInfo ? authInfo.type : undefined ,
authInfoToken : authInfo ? maskToken ( authInfo . token ) : undefined ,
err ,
statusCode : err.statusCode ,
depName : name ,
} ,
` Dependency lookup failure: unauthorized `
) ;
return null ;
}
2019-07-02 05:12:50 +00:00
// istanbul ignore if
if ( err . statusCode === 402 ) {
2020-02-24 07:43:01 +00:00
logger . debug (
2019-07-02 05:12:50 +00:00
{
pkgUrl ,
authInfoType : authInfo ? authInfo.type : undefined ,
authInfoToken : authInfo ? maskToken ( authInfo . token ) : undefined ,
err ,
statusCode : err.statusCode ,
depName : name ,
} ,
` Dependency lookup failure: payent required `
) ;
return null ;
}
2019-01-05 07:00:07 +00:00
if ( err . statusCode === 404 || err . code === 'ENOTFOUND' ) {
2020-02-24 07:43:01 +00:00
logger . debug ( { depName : name } , ` Dependency lookup failure: not found ` ) ;
2019-01-05 07:00:07 +00:00
logger . debug ( {
err ,
token : authInfo ? maskToken ( authInfo . token ) : 'none' ,
} ) ;
return null ;
}
2020-02-24 11:27:10 +00:00
if ( uri . host === 'registry.npmjs.org' ) {
2020-02-12 10:32:27 +00:00
// istanbul ignore if
2020-02-14 07:30:39 +00:00
if (
( err . name === 'ParseError' || err . code === 'ECONNRESET' ) &&
retries > 0
) {
2020-02-24 12:52:34 +00:00
logger . warn ( { pkgUrl , errName : err.name } , 'Retrying npm error' ) ;
2020-02-12 12:17:35 +00:00
await delay ( 5000 ) ;
2020-02-12 10:32:27 +00:00
return getDependency ( name , retries - 1 ) ;
}
2020-02-14 07:52:41 +00:00
// istanbul ignore if
if ( err . name === 'ParseError' && err . body ) {
2020-02-14 07:30:39 +00:00
err . body = 'err.body deleted by Renovate' ;
}
2020-02-13 12:29:55 +00:00
throw new DatasourceError ( err ) ;
2019-01-25 07:29:26 +00:00
}
2019-03-14 10:43:51 +00:00
// istanbul ignore next
return null ;
2019-01-05 07:00:07 +00:00
}
}