1
+ import BluebirdPromise from "bluebird-lst-c"
2
+ import * as path from "path"
3
+ import { readJson , lstat , realpath , readdir } from "fs-extra-p"
4
+
5
+ export interface Dependency {
6
+ name : string
7
+ path : string
8
+ extraneous : boolean
9
+ optional : boolean
10
+
11
+ dependencies : { [ name : string ] : Dependency }
12
+ }
13
+
14
+ export async function readInstalled ( folder : string ) : Promise < Map < string , Dependency > > {
15
+ const opts = {
16
+ depth : Infinity ,
17
+ dev : false ,
18
+ }
19
+
20
+ const findUnmetSeen = new Set < any > ( )
21
+ const pathToDep = new Map < string , Dependency > ( )
22
+ const obj = await _readInstalled ( folder , null , null , 0 , opts , pathToDep , findUnmetSeen )
23
+
24
+ unmarkExtraneous ( obj , opts . dev , true )
25
+ return pathToDep
26
+ }
27
+
28
+ async function _readInstalled ( folder : string , parent : any | null , name : string | null , depth : number , opts : any , realpathSeen : Map < string , Dependency > , findUnmetSeen : Set < any > ) : Promise < any > {
29
+ const realDir = await realpath ( folder )
30
+
31
+ const processed = realpathSeen . get ( realDir )
32
+ if ( processed != null ) {
33
+ return processed
34
+ }
35
+
36
+ const obj = await readJson ( path . resolve ( folder , "package.json" ) )
37
+ obj . realPath = realDir
38
+ obj . path = obj . path || folder
39
+ //noinspection ES6MissingAwait
40
+ if ( ( await lstat ( folder ) ) . isSymbolicLink ( ) ) {
41
+ obj . link = realDir
42
+ }
43
+
44
+ obj . realName = name || obj . name
45
+ obj . dependencyNames = obj . dependencies == null ? null : new Set ( Object . keys ( obj . dependencies ) )
46
+
47
+ // Mark as extraneous at this point.
48
+ // This will be un-marked in unmarkExtraneous, where we mark as not-extraneous everything that is required in some way from the root object.
49
+ obj . extraneous = true
50
+ obj . optional = true
51
+
52
+ if ( parent != null && obj . link == null ) {
53
+ obj . parent = parent
54
+ }
55
+
56
+ realpathSeen . set ( realDir , obj )
57
+
58
+ if ( depth > opts . depth ) {
59
+ return obj
60
+ }
61
+
62
+ const deps = await BluebirdPromise . map ( await readScopedDir ( path . join ( folder , "node_modules" ) ) , pkg => _readInstalled ( path . join ( folder , "node_modules" , pkg ) , obj , pkg , depth + 1 , opts , realpathSeen , findUnmetSeen ) , { concurrency : 8 } )
63
+ if ( obj . dependencies != null ) {
64
+ for ( const dep of deps ) {
65
+ obj . dependencies [ dep . realName ] = dep
66
+ }
67
+
68
+ // any strings in the obj.dependencies are unmet deps. However, if it's optional, then that's fine, so just delete it.
69
+ if ( obj . optionalDependencies != null ) {
70
+ for ( const dep of Object . keys ( obj . optionalDependencies ) ) {
71
+ if ( typeof obj . dependencies [ dep ] === "string" ) {
72
+ delete obj . dependencies [ dep ]
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ return obj
79
+ }
80
+
81
+ function unmark ( deps : Iterable < string > , obj : any , dev : boolean , unsetOptional : boolean ) {
82
+ for ( const name of deps ) {
83
+ const dep = findDep ( obj , name )
84
+ if ( dep != null ) {
85
+ if ( unsetOptional ) {
86
+ dep . optional = false
87
+ }
88
+ if ( dep . extraneous ) {
89
+ unmarkExtraneous ( dep , dev , false )
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ function unmarkExtraneous ( obj : any , dev : boolean , isRoot : boolean ) {
96
+ // Mark all non-required deps as extraneous.
97
+ // start from the root object and mark as non-extraneous all modules
98
+ // that haven't been previously flagged as extraneous then propagate to all their dependencies
99
+
100
+ obj . extraneous = false
101
+
102
+ if ( obj . dependencyNames != null ) {
103
+ unmark ( obj . dependencyNames , obj , dev , true )
104
+ }
105
+
106
+ if ( dev && obj . devDependencies != null && ( isRoot || obj . link ) ) {
107
+ unmark ( Object . keys ( obj . devDependencies ) , obj , dev , true )
108
+ }
109
+
110
+ if ( obj . peerDependencies != null ) {
111
+ unmark ( Object . keys ( obj . peerDependencies ) , obj , dev , true )
112
+ }
113
+
114
+ if ( obj . optionalDependencies != null ) {
115
+ unmark ( Object . keys ( obj . optionalDependencies ) , obj , dev , false )
116
+ }
117
+ }
118
+
119
+ // find the one that will actually be loaded by require() so we can make sure it's valid
120
+ function findDep ( obj : any , name : string ) {
121
+ let r = obj
122
+ let found = null
123
+ while ( r != null && found == null ) {
124
+ // if r is a valid choice, then use that.
125
+ // kinda weird if a pkg depends on itself, but after the first iteration of this loop, it indicates a dep cycle.
126
+ const dependency = r . dependencies == null ? null : r . dependencies [ name ]
127
+ if ( typeof dependency === "object" ) {
128
+ found = dependency
129
+ }
130
+ if ( found == null && r . realName === name ) {
131
+ found = r
132
+ }
133
+ r = r . link ? null : r . parent
134
+ }
135
+ return found
136
+ }
137
+
138
+ async function readScopedDir ( dir : string ) {
139
+ let files : Array < string >
140
+ try {
141
+ files = ( await readdir ( dir ) ) . filter ( it => ! it . startsWith ( "." ) )
142
+ }
143
+ catch ( e ) {
144
+ // error indicates that nothing is installed here
145
+ return [ ]
146
+ }
147
+
148
+ files . sort ( )
149
+
150
+ const scopes = files . filter ( it => it . startsWith ( "@" ) )
151
+ if ( scopes . length === 0 ) {
152
+ return files
153
+ }
154
+
155
+ const result = files . filter ( it => ! it . startsWith ( "@" ) )
156
+ const scopeFileList = await BluebirdPromise . map ( scopes , it => readdir ( path . join ( dir , it ) ) )
157
+ for ( let i = 0 ; i < scopes . length ; i ++ ) {
158
+ for ( const file of scopeFileList [ i ] ) {
159
+ if ( ! file . startsWith ( "." ) ) {
160
+ result . push ( `${ scopes [ i ] } /${ file } ` )
161
+ }
162
+ }
163
+ }
164
+
165
+ result . sort ( )
166
+ return result
167
+ }
0 commit comments