import { Package } from "../common/npmPackage" ; import { fatal , exec , execNoError } from "./utils" ; import { MonoRepo , MonoRepoKind } from "../common/monoRepo" ; import * as semver from "semver" ; export class VersionBag { private versionData : { [ key ] : string ; } = { } ; public add ( pkg , version ) { const existing = this . internalAdd ( pkg , version ) ; if ( existing ) { fatal ( `Inconsistent version for ${ pkg . name } ${ version } && ${ existing }` ) ; } } protected internalAdd ( pkg , version , override = false ) { const entryName = VersionBag . getEntryName ( pkg ) ; const existing = this . versionData [ entryName ] ; if ( existing !== version ) { if ( existing ) { if ( ! override ) { return existing ; } console . log ( `    Overriding ${ entryName } ${ existing } -> ${ version }` ) ; } this . versionData [ entryName ] = version ; return existing ; } } public get ( pkgOrMonoRepoName ) { let entryName = typeof pkgOrMonoRepoName === "string" ? pkgOrMonoRepoName : VersionBag . getEntryName ( pkgOrMonoRepoName ) ; return this . versionData [ entryName ] ; } public [ Symbol . iterator ] ( ) { return Object . entries ( this . versionData ) [ Symbol . iterator ] ( ) ; } protected static getEntryName ( pkg ) { return pkg . monoRepo ? MonoRepoKind [ pkg . monoRepo . kind ] : pkg . name ; } } /**
 * Keep track of all the dependency version information and detect conflicting dependencies.
 * Provide functionality to collect the dependencies information from published package as well.
 */ export class ReferenceVersionBag extends VersionBag { private readonly referenceData = new Map < string , { reference : string ; published : boolean ; } > ( ) ; private readonly nonDevDep = new Set < string > ( ) ; private readonly publishedPackage = new Set < string > ( ) ; private readonly publishedPackageRange = new Set < string > ( ) ; constructor ( private readonly repoRoot , private readonly fullPackageMap , public readonly repoVersions ) { super ( ) ; } param pkg param version param newReference public add ( pkg , version , dev = false , newReference ? , published = false ) { const entryName = VersionBag . getEntryName ( pkg ) ; const override = ! this . nonDevDep . has ( entryName ) && ( ! published || ! dev ) ; const existing = this . internalAdd ( pkg , version , override ) ; if ( ! dev ) { if ( existing ) { const existingReference = this . referenceData . get ( entryName ) ; const message = `Inconsistent dependency to ${ pkg . name }\n  ${ version . padStart ( 10 ) } in ${ newReference }\n  ${ existing . padStart ( 10 ) } in ${ existingReference ?. reference }` ; if ( existingReference ?. reference && this . publishedPackage . has ( existingReference . reference ) && newReference && this . publishedPackage . has ( newReference ) ) { console . warn ( `WARNING: ${ message }` ) ; } else { fatal ( message ) ; } } this . nonDevDep . add ( entryName ) ; } else if ( existing ) { console . log ( `      Ignored mismatched dev dependency ${ pkg . name }@${ version } vs ${ existing }` ) ; return ; } if ( newReference ) { this . referenceData . set ( entryName , { reference : newReference , published } ) ; } } private async getPublishedMatchingVersion ( rangeSpec , reference ) { const ret = await execNoError ( `npm view "${ rangeSpec }" version --json` , this . repoRoot ) ; if ( ! ret ) { if ( reference ) { fatal ( `Unable to get published version for ${ rangeSpec } referenced from ${ reference }.` ) ; } return undefined ; } let publishedVersions ; try { publishedVersions = JSON . parse ( ret ) ; } catch ( e ) { if ( reference ) { fatal ( `Unable to parse published version for ${ rangeSpec } referenced from ${ reference }.\nOutput: ${ ret }` ) ; } return undefined ; } return Array . isArray ( publishedVersions ) ? publishedVersions . sort ( semver . rcompare ) [ 0 ] : publishedVersions ; } private async getPublishedDependencies ( versionSpec , dev ) { const dep = dev ? "devDependencies" : "dependencies" ; const retDep = await exec ( `npm view ${ versionSpec } ${ dep } --json` , this . repoRoot , "look up dependencies" ) ; if ( retDep . trim ( ) === "" ) { return undefined ; } try { return JSON . parse ( retDep ) ; } catch ( e ) { fatal ( `Unable to parse dependencies for ${ versionSpec }.\nOutput: ${ retDep }` ) ; } } public static checkPrivate ( pkg , dep , dev ) { if ( dep . packageJson . private ) { if ( ! pkg . packageJson . private && ! dev ) { fatal ( `Private package not a dev dependency\n   ${ pkg . name }@${ pkg . version }\n  ${ dep . name }@${ dep . version }` ) ; } if ( ! MonoRepo . isSame ( pkg . monoRepo , dep . monoRepo ) ) { fatal ( `Private package not in the same monorepo\n   ${ pkg . name }@${ pkg . version }\n  ${ dep . name }@${ dep . version }` ) ; } return true ; } return false ; } param pkg param versionRange param repoRoot param fullPackageMap param reference public async collectPublishedPackageDependencies ( pkg , versionRange , dev , reference ? ) { const entryName = VersionBag . getEntryName ( pkg ) ; const rangeSpec = `${ pkg . name }@${ versionRange }` ; if ( this . publishedPackageRange . has ( rangeSpec ) ) { return ; } this . publishedPackageRange . add ( rangeSpec ) ; let matchedVersion = this . get ( entryName ) ; if ( ! matchedVersion || ! semver . satisfies ( matchedVersion , versionRange ) ) { matchedVersion = await this . getPublishedMatchingVersion ( rangeSpec , reference ) ; if ( ! matchedVersion ) { return ; } } console . log ( `    Found ${ rangeSpec } => ${ matchedVersion }` ) ; this . add ( pkg , matchedVersion , dev , reference , true ) ; const versionSpec = `${ pkg . name }@${ matchedVersion }` ; if ( this . publishedPackage . has ( versionSpec ) ) { return ; } this . publishedPackage . add ( versionSpec ) ; const pending = [ ] ; const addPublishedDependencies = async ( dev ) => { const dep = await this . getPublishedDependencies ( versionSpec , dev ) ; for ( const d in dep ) { const depPkg = this . fullPackageMap . get ( d ) ; if ( depPkg ) { if ( ReferenceVersionBag . checkPrivate ( pkg , depPkg , dev ) ) { continue ; } pending . push ( this . collectPublishedPackageDependencies ( depPkg , dep [ d ] , dev , versionSpec ) ) ; } } } ; await Promise . all ( [ addPublishedDependencies ( true ) , addPublishedDependencies ( false ) ] ) ; await Promise . all ( pending ) ; } public printRelease ( ) { console . log ( "Release Versions:" ) ; for ( const [ name ] of this . repoVersions ) { const depVersion = this . get ( name ) ?? "undefined" ; const state = this . needRelease ( name ) ? "(new)" : this . needBump ( name ) ? "(current)" : "(old)" ; console . log ( `${ name . padStart ( 40 ) }: ${ depVersion . padStart ( 10 ) } ${ state }` ) ; } console . log ( ) ; } public printPublished ( name ) { console . log ( `Current Versions from ${ name }:` ) ; for ( const [ name ] of this . repoVersions ) { const depVersion = this . get ( name ) ?? "undefined" ; console . log ( `${ name . padStart ( 40 ) }: ${ depVersion . padStart ( 10 ) } ${ depVersion === "undefined" ? "" : this . needRelease ( name ) ? "(local)" : "(published)" }` ) ; } console . log ( ) ; } public needBump ( name ) { return this . repoVersions . get ( name ) === this . get ( name ) ; } public needRelease ( name ) { if ( this . needBump ( name ) ) { const data = this . referenceData . get ( name ) ! ; return ! data || ! data . published ; } return false ; } } export function getRepoStateChange ( oldVersions , newVersions ) { let repoState = "" ; for ( const [ name , newVersion ] of newVersions ) { const oldVersion = oldVersions . get ( name ) ?? "undefined" ; if ( oldVersion !== newVersion ) { repoState += `\n${ name . padStart ( 40 ) }: ${ oldVersion . padStart ( 10 ) } -> ${ newVersion . padEnd ( 10 ) }` ; } else { repoState += `\n${ name . padStart ( 40 ) }: ${ newVersion . padStart ( 10 ) } (unchanged)` ; } } return repoState ; }