2020-05-01 16:03:48 +00:00
import url from 'url' ;
import is from '@sindresorhus/is' ;
2019-08-15 04:30:16 +00:00
import { logger } from '../../logger' ;
2020-06-23 09:44:52 +00:00
import { ExternalHostError } from '../../types/errors/external-host-error' ;
2020-06-25 06:32:55 +00:00
import * as packageCache from '../../util/cache/package' ;
2020-04-03 11:45:55 +00:00
import { Http , HttpOptions } from '../../util/http' ;
2021-03-02 20:44:55 +00:00
import type { Release , ReleaseResult } from '../types' ;
2020-03-01 07:01:12 +00:00
import { id } from './common' ;
2021-03-20 08:27:33 +00:00
import { resolvePackage } from './npmrc' ;
import { NpmResponse } from './types' ;
2019-01-05 07:00:07 +00:00
2020-04-03 11:45:55 +00:00
const http = new Http ( id ) ;
2020-05-07 08:23:45 +00:00
let memcache : Record < string , string > = { } ;
2019-01-05 07:00:07 +00:00
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 {
gitRef? : string ;
}
export interface NpmDependency extends ReleaseResult {
releases : NpmRelease [ ] ;
deprecationSource? : string ;
name : string ;
homepage : string ;
sourceUrl : string ;
versions : Record < string , any > ;
2020-05-07 08:23:45 +00:00
'dist-tags' : Record < string , string > ;
2019-08-15 04:30:16 +00:00
sourceDirectory? : string ;
}
2019-08-19 14:44:12 +00:00
export async function getDependency (
2021-03-17 13:40:50 +00:00
packageName : string
2019-08-19 14:44:12 +00:00
) : Promise < NpmDependency | null > {
2020-02-27 21:04:10 +00:00
logger . trace ( ` npm.getDependency( ${ packageName } ) ` ) ;
2019-01-05 07:00:07 +00:00
// This is our datastore cache and is cleared at the end of each repo, i.e. we never requery/revalidate during a "run"
2020-02-27 21:04:10 +00:00
if ( memcache [ packageName ] ) {
2019-01-05 07:00:07 +00:00
logger . trace ( 'Returning cached result' ) ;
2020-09-25 06:59:05 +00:00
return JSON . parse ( memcache [ packageName ] ) as NpmDependency ;
2019-01-05 07:00:07 +00:00
}
2021-03-20 08:27:33 +00:00
const { headers , packageUrl , registryUrl } = resolvePackage ( packageName ) ;
2019-06-04 18:38:30 +00:00
// Now check the persistent cache
const cacheNamespace = 'datasource-npm' ;
2020-06-25 06:32:55 +00:00
const cachedResult = await packageCache . get < NpmDependency > (
2019-08-15 04:30:16 +00:00
cacheNamespace ,
2021-03-20 08:27:33 +00:00
packageUrl
2019-08-15 04:30:16 +00:00
) ;
2019-08-28 13:08:06 +00:00
// istanbul ignore if
2020-06-17 08:07:22 +00:00
if ( cachedResult ) {
2019-06-04 18:38:30 +00:00
return cachedResult ;
}
2019-01-05 07:00:07 +00:00
2021-03-20 08:27:33 +00:00
const uri = url . parse ( packageUrl ) ;
2020-02-24 11:27:10 +00:00
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 ;
}
try {
2020-04-03 11:45:55 +00:00
const opts : HttpOptions = {
2019-01-05 07:00:07 +00:00
headers ,
2020-02-12 12:17:48 +00:00
} ;
2021-03-20 08:27:33 +00:00
const raw = await http . getJson < NpmResponse > ( packageUrl , opts ) ;
2019-01-05 07:00:07 +00:00
const res = raw . body ;
if ( ! res . versions || ! Object . keys ( res . versions ) . length ) {
// Registry returned a 200 OK but with no versions
2020-02-27 21:04:10 +00:00
logger . debug ( { dependency : packageName } , '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 ,
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' ] ,
2021-03-20 08:27:33 +00:00
registryUrl ,
2019-01-05 07:00:07 +00:00
} ;
2020-08-10 14:18:08 +00:00
if ( res . repository ? . directory ) {
2019-01-22 06:37:51 +00:00
dep . sourceDirectory = res . repository . directory ;
}
2019-01-05 07:00:07 +00:00
if ( latestVersion . deprecated ) {
2021-03-20 08:27:33 +00:00
dep . deprecationMessage = ` On registry \` ${ registryUrl } \` , the "latest" version of dependency \` ${ packageName } \` 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-03-01 07:01:12 +00:00
dep . deprecationSource = id ;
2019-01-05 07:00:07 +00:00
}
2020-04-12 16:09:36 +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 ,
2021-03-01 07:59:57 +00:00
dependencies : res.versions [ version ] . dependencies ,
devDependencies : res.versions [ version ] . devDependencies ,
2019-01-05 07:00:07 +00:00
} ;
2020-08-10 14:18:08 +00:00
if ( res . time ? . [ version ] ) {
2019-01-05 07:00:07 +00:00
release . releaseTimestamp = res . time [ version ] ;
}
if ( res . versions [ version ] . deprecated ) {
release . isDeprecated = true ;
}
return release ;
} ) ;
logger . trace ( { dep } , 'dep' ) ;
// serialize first before saving
2020-02-27 21:04:10 +00:00
memcache [ packageName ] = JSON . stringify ( dep ) ;
2019-01-05 07:00:07 +00:00
const cacheMinutes = process . env . RENOVATE_CACHE_NPM_MINUTES
? parseInt ( process . env . RENOVATE_CACHE_NPM_MINUTES , 10 )
2020-08-14 12:38:51 +00:00
: 15 ;
2020-04-24 15:12:20 +00:00
// TODO: use dynamic detection of public repos instead of a static list
const whitelistedPublicScopes = [
'@graphql-codegen' ,
'@storybook' ,
'@types' ,
'@typescript-eslint' ,
] ;
if (
2021-03-09 15:32:24 +00:00
! raw . authorization &&
2021-03-20 08:27:33 +00:00
( whitelistedPublicScopes . includes ( packageName . split ( '/' ) [ 0 ] ) ||
! packageName . startsWith ( '@' ) )
2020-04-24 15:12:20 +00:00
) {
2021-03-20 08:27:33 +00:00
await packageCache . set ( cacheNamespace , packageUrl , 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
{
2021-03-20 08:27:33 +00:00
packageUrl ,
2019-01-05 07:00:07 +00:00
err ,
statusCode : err.statusCode ,
2020-02-27 21:04:10 +00:00
packageName ,
2019-01-05 07:00:07 +00:00
} ,
` Dependency lookup failure: unauthorized `
) ;
return null ;
}
2019-07-02 05:12:50 +00:00
if ( err . statusCode === 402 ) {
2020-02-24 07:43:01 +00:00
logger . debug (
2019-07-02 05:12:50 +00:00
{
2021-03-20 08:27:33 +00:00
packageUrl ,
2019-07-02 05:12:50 +00:00
err ,
statusCode : err.statusCode ,
2020-02-27 21:04:10 +00:00
packageName ,
2019-07-02 05:12:50 +00:00
} ,
` Dependency lookup failure: payent required `
) ;
return null ;
}
2019-01-05 07:00:07 +00:00
if ( err . statusCode === 404 || err . code === 'ENOTFOUND' ) {
2021-03-20 08:27:33 +00:00
logger . debug (
{ err , packageName } ,
` Dependency lookup failure: not found `
) ;
2019-01-05 07:00:07 +00:00
return null ;
}
2020-02-24 11:27:10 +00:00
if ( uri . host === 'registry.npmjs.org' ) {
2020-06-16 05:11:21 +00:00
// istanbul ignore if
2020-02-14 07:52:41 +00:00
if ( err . name === 'ParseError' && err . body ) {
2020-02-14 07:30:39 +00:00
err . body = 'err.body deleted by Renovate' ;
}
2020-06-22 19:28:02 +00:00
throw new ExternalHostError ( err ) ;
2019-01-25 07:29:26 +00:00
}
2019-03-14 10:43:51 +00:00
return null ;
2019-01-05 07:00:07 +00:00
}
}