@@ -13,57 +13,158 @@ import fs from 'fs/promises';
1313
1414import { run } from '@kbn/dev-cli-runner' ;
1515import { REPO_ROOT } from '@kbn/repo-info' ;
16- import { parseYarnLockFile } from './yarn_lock_v1' ;
16+ import type { PackageInfo } from './yarn_lock_v1' ;
17+ import { parseYarnLock } from './yarn_lock_v1' ;
1718
1819const options : RunOptions = {
1920 description :
2021 'Extracts declared dependency versions from a Yarn v1 lockfile into a versions file.\n' +
2122 'This can be useful to set up Moon task dependencies on package versions used in the repo.' ,
2223 flags : {
2324 string : [ 'collect' ] ,
25+ boolean : [ 'transitive' ] ,
2426 } ,
25- usage : `node scripts/extract_version_dependencies <output_file_path> --collect <package_name1,package_name2,...>` ,
27+ usage :
28+ `node scripts/extract_version_dependencies <output_file_path> ` +
29+ `--collect <package_name1> [--collect <package_name2> ...] [--transitive]` ,
2630} ;
2731
2832export async function runCli ( ) {
2933 return run ( async ( { flagsReader } ) => {
3034 const outputFilePath = flagsReader . getPositionals ( ) [ 0 ] ;
3135 const dependencies = flagsReader . arrayOfStrings ( 'collect' ) ;
36+ const transitive = flagsReader . boolean ( 'transitive' ) ;
3237 if ( typeof dependencies === 'undefined' ) {
3338 throw new Error ( '--collect flag is required and must specify at least one package name.' ) ;
3439 }
3540
36- await collectDependenciesAndWriteFile ( dependencies , outputFilePath ) ;
41+ await collectDependenciesAndWriteFile ( dependencies , outputFilePath , { transitive } ) ;
3742
3843 return ;
3944 } , options ) ;
4045}
4146
42- async function collectDependenciesAndWriteFile ( dependencies : string [ ] , outputFilePath : string ) {
43- const resolvedDependencyMap = new Map < string , string > ( ) ;
47+ async function collectDependenciesAndWriteFile (
48+ dependencies : string [ ] ,
49+ outputFilePath : string ,
50+ { transitive } : { transitive : boolean }
51+ ) {
4452 const rootPackageJson = path . join ( REPO_ROOT , 'package.json' ) ;
4553 const yarnLockPath = path . join ( REPO_ROOT , 'yarn.lock' ) ;
4654
47- const pkgJson = await fs . readFile ( rootPackageJson , 'utf-8' ) . then ( ( data ) => JSON . parse ( data ) ) ;
48- const yarnLockContent = parseYarnLockFile ( yarnLockPath , dependencies ) ;
49- const yarnLockEntries = Object . values ( yarnLockContent ) ;
55+ const [ packageJsonContent , yarnLockContent ] = await Promise . all ( [
56+ fs . readFile ( rootPackageJson , 'utf-8' ) ,
57+ fs . readFile ( yarnLockPath , 'utf-8' ) ,
58+ ] ) ;
59+
60+ const outputLines = collectDependencyVersionLines ( {
61+ dependencies,
62+ rootPackageJsonContent : packageJsonContent ,
63+ transitive,
64+ yarnLockContent,
65+ } ) ;
66+
67+ await fs . writeFile ( outputFilePath , outputLines . join ( '\n' ) + '\n' , 'utf-8' ) ;
68+ }
69+
70+ export const collectDependencyVersionLines = ( {
71+ dependencies,
72+ rootPackageJsonContent,
73+ transitive,
74+ yarnLockContent,
75+ } : {
76+ dependencies : string [ ] ;
77+ rootPackageJsonContent : string ;
78+ transitive : boolean ;
79+ yarnLockContent : string ;
80+ } ) => {
81+ const pkgJson = JSON . parse ( rootPackageJsonContent ) ;
82+ const yarnLockEntries = Object . values ( parseYarnLock ( yarnLockContent ) ) ;
83+ const requestedVersionIndex = createRequestedVersionIndex ( yarnLockEntries ) ;
5084
5185 const allRequestedDependencies = {
5286 ...pkgJson . devDependencies ,
5387 ...pkgJson . dependencies ,
5488 } ;
5589
56- dependencies . forEach ( ( dep ) => {
57- const declaredVersion = allRequestedDependencies [ dep ] ;
58- const yarnlockEntry = yarnLockEntries . find ( ( entry ) => {
59- return entry . name === dep && entry . requestedVersions . includes ( declaredVersion ) ;
60- } ) ;
61- resolvedDependencyMap . set ( dep , yarnlockEntry ! . resolvedVersion ! ) ;
90+ const rootDependencies = resolveRootDependencies (
91+ dependencies ,
92+ requestedVersionIndex ,
93+ allRequestedDependencies
94+ ) ;
95+
96+ const resolvedDependencyKeys = transitive
97+ ? collectTransitiveDependencies ( rootDependencies , requestedVersionIndex )
98+ : new Set ( rootDependencies . map ( ( pkg ) => `${ pkg . name } @${ pkg . resolvedVersion } ` ) ) ;
99+
100+ return Array . from ( resolvedDependencyKeys ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
101+ } ;
102+
103+ const createRequestedVersionIndex = ( packages : PackageInfo [ ] ) => {
104+ const index = new Map < string , PackageInfo > ( ) ;
105+
106+ for ( const pkg of packages ) {
107+ for ( const requestedVersion of pkg . requestedVersions ) {
108+ index . set ( `${ pkg . name } @${ requestedVersion } ` , pkg ) ;
109+ }
110+ }
111+
112+ return index ;
113+ } ;
114+
115+ const resolveDependency = (
116+ dependencyName : string ,
117+ requestedVersion : string ,
118+ requestedVersionIndex : Map < string , PackageInfo >
119+ ) => {
120+ const pkg = requestedVersionIndex . get ( `${ dependencyName } @${ requestedVersion } ` ) ;
121+
122+ if ( ! pkg ?. resolvedVersion ) {
123+ throw new Error (
124+ `Unable to resolve ${ dependencyName } @${ requestedVersion } from yarn.lock dependency graph`
125+ ) ;
126+ }
127+
128+ return pkg ;
129+ } ;
130+
131+ const resolveRootDependencies = (
132+ dependencies : string [ ] ,
133+ requestedVersionIndex : Map < string , PackageInfo > ,
134+ allRequestedDependencies : Record < string , string >
135+ ) => {
136+ return dependencies . map ( ( dependencyName ) => {
137+ const declaredVersion = allRequestedDependencies [ dependencyName ] ;
138+
139+ if ( ! declaredVersion ) {
140+ throw new Error ( `Unable to find ${ dependencyName } in the root package.json dependency list` ) ;
141+ }
142+
143+ return resolveDependency ( dependencyName , declaredVersion , requestedVersionIndex ) ;
62144 } ) ;
145+ } ;
63146
64- const outputLines = Array . from ( resolvedDependencyMap . entries ( ) )
65- . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) )
66- . map ( ( [ name , version ] ) => `${ name } @${ version } ` ) ;
147+ const collectTransitiveDependencies = (
148+ rootDependencies : PackageInfo [ ] ,
149+ requestedVersionIndex : Map < string , PackageInfo >
150+ ) => {
151+ const visited = new Set < string > ( ) ;
152+ const queue = [ ...rootDependencies ] ;
67153
68- await fs . writeFile ( outputFilePath , outputLines . join ( '\n' ) + '\n' , 'utf-8' ) ;
69- }
154+ while ( queue . length > 0 ) {
155+ const pkg = queue . shift ( ) ! ;
156+ const packageKey = `${ pkg . name } @${ pkg . resolvedVersion } ` ;
157+
158+ if ( visited . has ( packageKey ) ) {
159+ continue ;
160+ }
161+
162+ visited . add ( packageKey ) ;
163+
164+ for ( const [ dependencyName , requestedVersion ] of Object . entries ( pkg . dependencies ?? { } ) ) {
165+ queue . push ( resolveDependency ( dependencyName , requestedVersion , requestedVersionIndex ) ) ;
166+ }
167+ }
168+
169+ return visited ;
170+ } ;
0 commit comments