import { BaseCommand , WorkspaceRequiredError } from "@yarnpkg/cli" ; import { Cache , Configuration , Descriptor , FormatType , formatUtils , Locator , MessageName , Project , Report , StreamReport , structUtils , Workspace , } from "@yarnpkg/core" ; import { Command , Option , Usage , UsageError } from "clipanion" ; import micromatch from "micromatch" ; import semver from "semver" ; import t from "typanion" ; import { DependencyFetcher } from "./DependencyFetcher" ; import { DependencyTable } from "./DependencyTable" ; import { DependencyInfo , dependencyTypes , OutdatedDependency , severities , Severity , } from "./types" ; import { isVersionOutdated , truthy } from "./utils" ; export class OutdatedCommand extends BaseCommand { static paths = [ [ "outdated" ] ] ; static usage : Usage = Command . Usage ( { description : "view outdated dependencies" , details : `
      This command finds outdated dependencies in a project and prints the result in a table or JSON format.

      This command accepts glob patterns as arguments to filter the output. Make sure to escape the patterns, to prevent your own shell from trying to expand them.
    ` , examples : [ [ "View outdated dependencies" , "yarn outdated" ] , [ "View outdated dependencies with the `@babel` scope" , "yarn outdated '@babel/*'" , ] , [ "Filter results to only include devDependencies" , "yarn outdated --type devDependencies" , ] , [ "Filter results to only include major version updates" , "yarn outdated --severity major" , ] , ] , } ) ; patterns = Option . Rest ( ) ; all = Option . Boolean ( "-a,--all" , false , { description : "Include outdated dependencies from all workspaces" , } ) ; check = Option . Boolean ( "-c,--check" , false , { description : "Exit with exit code 1 when outdated dependencies are found" , } ) ; json = Option . Boolean ( "--json" , false , { description : "Format the output as JSON" , } ) ; severity = Option . String ( "-s,--severity" , { description : "Filter results based on the severity of the update" , validator : t . isEnum ( severities ) , } ) ; type = Option . String ( "-t,--type" , { description : "Filter results based on the dependency type" , validator : t . isEnum ( dependencyTypes ) , } ) ; url = Option . Boolean ( "--url" , false , { description : "Include the homepage URL of each package in the output" , } ) ; async execute ( ) { const { cache , configuration , project , workspace } = await this . loadProject ( ) ; const fetcher = new DependencyFetcher ( configuration , project , workspace , cache ) ; const workspaces = this . getWorkspaces ( project , workspace ) ; const dependencies = this . getDependencies ( configuration , workspaces ) ; if ( this . json ) { const outdated = await this . getOutdatedDependencies ( fetcher , dependencies ) ; this . context . stdout . write ( JSON . stringify ( outdated ) + "\n" ) ; return ; } const report = await StreamReport . start ( { configuration , stdout : this . context . stdout } , async ( report ) => { await this . checkOutdatedDependencies ( configuration , dependencies , fetcher , report ) ; } ) ; return report . exitCode ( ) ; } async checkOutdatedDependencies ( configuration , dependencies , fetcher , report ) { let outdated = null ! ; await report . startTimerPromise ( "Checking for outdated dependencies" , async ( ) => { const count = dependencies . length ; const progress = StreamReport . progressViaCounter ( count ) ; report . reportProgress ( progress ) ; outdated = await this . getOutdatedDependencies ( fetcher , dependencies , progress ) ; } ) ; report . reportSeparator ( ) ; if ( outdated . length ) { const table = new DependencyTable ( report , configuration , outdated , { url : this . url , workspace : this . all , } ) ; table . print ( ) ; report . reportSeparator ( ) ; this . printOutdatedCount ( report , outdated . length ) ; } else { this . printUpToDate ( configuration , report ) ; } } /**
     * Loads the project and current workspace.
     */ async loadProject ( ) { const configuration = await Configuration . find ( this . context . cwd , this . context . plugins ) ; const [ cache , { project , workspace } ] = await Promise . all ( [ Cache . find ( configuration ) , Project . find ( configuration , this . context . cwd ) , ] ) ; await project . restoreInstallState ( ) ; if ( ! workspace ) { throw new WorkspaceRequiredError ( project . cwd , this . context . cwd ) ; } return { cache , configuration , project , workspace , } ; } /**
     * If the user passed the `--all` CLI flag, then we will load dependencies
     * from all workspaces instead of just the current workspace.
     */ getWorkspaces ( project , workspace ) { return this . all ? project . workspaces : [ workspace ] ; } get dependencyTypes ( ) { return this . type ? [ this . type ] : dependencyTypes ; } /**
     * Collect all dependencies and devDependencies from all workspaces into an
     * array which we can process more easily.
     */ getDependencies ( configuration , workspaces ) { const dependencies = [ ] ; for ( const workspace of workspaces ) { const { anchoredLocator , project } = workspace ; const root = project . storedPackages . get ( anchoredLocator . locatorHash ) ; if ( ! root ) this . throw ( configuration , anchoredLocator ) ; for ( const dependencyType of this . dependencyTypes ) { for ( const descriptor of workspace . manifest [ dependencyType ] . values ( ) ) { const { range } = descriptor ; if ( range . includes ( ":" ) && ! /(npm|patch):/ . test ( range ) ) { continue ; } const dependency = root . dependencies . get ( descriptor . identHash ) ; if ( ! dependency ) this . throw ( configuration , descriptor ) ; const res = project . storedResolutions . get ( dependency . descriptorHash ) ; if ( ! res ) this . throw ( configuration , dependency ) ; const pkg = project . storedPackages . get ( res ) ; if ( ! pkg ) this . throw ( configuration , dependency ) ; dependencies . push ( { dependencyType , name : structUtils . stringifyIdent ( descriptor ) , pkg , workspace , } ) ; } } } if ( ! this . patterns . length ) { return dependencies ; } const filteredDependencies = dependencies . filter ( ( { name } ) => micromatch . isMatch ( name , this . patterns ) ) ; if ( ! filteredDependencies . length ) { throw new UsageError ( `Pattern ${ formatUtils . prettyList ( configuration , this . patterns , FormatType . CODE ) } doesn't match any packages referenced by any workspace` ) ; } return filteredDependencies ; } throw ( configuration , item ) { const name = structUtils . prettyIdent ( configuration , item ) ; throw new Error ( `Package for ${ name } not found in the project` ) ; } getSeverity ( currentVersion , latestVersion ) { const current = semver . coerce ( currentVersion ) ! ; const latest = semver . coerce ( latestVersion ) ! ; return latest . major > current . major ? "major" : latest . minor > current . minor ? "minor" : "patch" ; } /**
     * Iterates through the dependencies to find the outdated dependencies and
     * sort them in ascending order.
     */ async getOutdatedDependencies ( fetcher , dependencies , progress ? ) { const outdated = dependencies . map ( async ( { dependencyType , name , pkg , workspace } ) => { if ( workspace . project . tryWorkspaceByLocator ( pkg ) ) { return ; } const { url , version : latest } = await fetcher . fetch ( { pkg , range : "latest" , url : this . url , } ) ; progress ?. tick ( ) ; if ( isVersionOutdated ( pkg . version ! , latest ) ) { return { current : pkg . version ! , latest , name , severity : this . getSeverity ( pkg . version ! , latest ) , type : dependencyType , url , workspace : this . all ? this . getWorkspaceName ( workspace ) : undefined , } ; } } ) ; return ( await Promise . all ( outdated ) ) . filter ( truthy ) . filter ( ( { severity } ) => ! this . severity || severity === this . severity ) . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ; } getWorkspaceName ( workspace ) { return workspace . manifest . name ? structUtils . stringifyIdent ( workspace . manifest . name ) : workspace . computeCandidateName ( ) ; } printOutdatedCount ( report , count ) { const args = [ MessageName . UNNAMED , count === 1 ? "1 dependency is out of date" : `${ count } dependencies are out of date` , ] as const ; if ( this . check ) { report . reportError ( ... args ) ; } else { report . reportWarning ( ... args ) ; } } printUpToDate ( configuration , report ) { const message = "✨ All your dependencies are up to date!" ; report . reportInfo ( MessageName . UNNAMED , formatUtils . pretty ( configuration , message , "green" ) ) ; } }