@@ -6,6 +6,7 @@ const {promisify} = require("util");
66const fs = require ( "fs" ) ;
77const realpath = promisify ( fs . realpath ) ;
88const resolveModulePath = promisify ( require ( "resolve" ) ) ;
9+ const parentNameRegExp = new RegExp ( / : ( [ ^ : ] + ) : $ / i) ;
910
1011class NpmTranslator {
1112 constructor ( ) {
@@ -17,12 +18,18 @@ class NpmTranslator {
1718 /*
1819 Returns a promise with an array of projects
1920 */
20- async processPkg ( data , parentName ) {
21+ async processPkg ( data , parentPath ) {
2122 const cwd = data . path ;
2223 const moduleName = data . name ;
2324 const pkg = data . pkg ;
25+ const parentName = parentPath && this . getParentNameFromPath ( parentPath ) || "nothing - root project" ;
2426
25- log . verbose ( "Analyzing %s (%s) (dependency of %s)" , moduleName , cwd , parentName || "nothing - root project" ) ;
27+ log . verbose ( "Analyzing %s (%s) (dependency of %s)" , moduleName , cwd , parentName ) ;
28+
29+ if ( ! parentPath ) {
30+ parentPath = ":" ;
31+ }
32+ parentPath += `${ moduleName } :` ;
2633
2734 /*
2835 * Inject collection definitions for some known projects
@@ -76,7 +83,7 @@ class NpmTranslator {
7683 if ( ! pkg . collection ) {
7784 return this . getDepProjects ( {
7885 cwd,
79- parentName : moduleName ,
86+ parentPath ,
8087 dependencies,
8188 optionalDependencies : pkg . optionalDependencies
8289 } ) . then ( ( depProjects ) => {
@@ -98,6 +105,7 @@ class NpmTranslator {
98105 // our dependencies later on.
99106 this . registerPendingDependencies ( {
100107 parentProject : project ,
108+ parentPath,
101109 dependencies : optDependencies
102110 } ) ;
103111 return [ project ] ;
@@ -112,7 +120,7 @@ class NpmTranslator {
112120 log . verbose ( "Ignoring module with same name as parent: " + parentName ) ;
113121 return null ;
114122 }
115- return this . readProject ( { modulePath, moduleName : depName , parentName : moduleName } ) ;
123+ return this . readProject ( { modulePath, moduleName : depName , parentPath } ) ;
116124 } )
117125 ) . then ( ( projects ) => {
118126 // Array needs to be flattened because:
@@ -123,16 +131,25 @@ class NpmTranslator {
123131 }
124132 }
125133
126- getDepProjects ( { cwd, dependencies, optionalDependencies, parentName} ) {
134+ getParentNameFromPath ( parentPath ) {
135+ const parentNameMatch = parentPath . match ( parentNameRegExp ) ;
136+ if ( parentNameMatch ) {
137+ return parentNameMatch [ 1 ] ;
138+ } else {
139+ log . error ( `Failed to get parent name from path ${ parentPath } ` ) ;
140+ }
141+ }
142+
143+ getDepProjects ( { cwd, dependencies, optionalDependencies, parentPath} ) {
127144 return Promise . all (
128145 Object . keys ( dependencies ) . map ( ( moduleName ) => {
129146 return this . findModulePath ( cwd , moduleName ) . then ( ( modulePath ) => {
130- return this . readProject ( { modulePath, moduleName, parentName } ) ;
147+ return this . readProject ( { modulePath, moduleName, parentPath } ) ;
131148 } , ( err ) => {
132149 // Due to normalization done by by the "read-pkg-up" module the values
133- // in optionalDependencies get added to dependencies. Also as described here:
150+ // in " optionalDependencies" get added to the modules " dependencies" . Also described here:
134151 // https://github.com/npm/normalize-package-data#what-normalization-currently-entails
135- // Resolution errors of optionalDependencies are being ignored
152+ // Ignore resolution errors for optional dependencies
136153 if ( optionalDependencies && optionalDependencies [ moduleName ] ) {
137154 return null ;
138155 } else {
@@ -143,17 +160,32 @@ class NpmTranslator {
143160 ) . then ( ( depProjects ) => {
144161 // Array needs to be flattened because:
145162 // readProject returns an array + Promise.all returns an array = array filled with arrays
146- // Filter out null values of ignored packages
163+ // Also filter out null values of ignored packages
147164 return Array . prototype . concat . apply ( [ ] , depProjects . filter ( ( p ) => p !== null ) ) ;
148165 } ) ;
149166 }
150167
151- readProject ( { modulePath, moduleName, parentName } ) {
168+ readProject ( { modulePath, moduleName, parentPath } ) {
152169 if ( this . projectCache [ modulePath ] ) {
153- return this . projectCache [ modulePath ] ;
170+ const cache = this . projectCache [ modulePath ] ;
171+ // Check whether modules has already been processed in the current subtree (indicates a loop)
172+ if ( parentPath . indexOf ( `:${ moduleName } :` ) !== - 1 ) {
173+ // This is a loop => abort further processing
174+ return cache . pPkg . then ( ( pkg ) => {
175+ return [ {
176+ id : moduleName ,
177+ version : pkg . version ,
178+ path : modulePath ,
179+ dependencies : [ ] ,
180+ deduped : true
181+ } ] ;
182+ } ) ;
183+ } else {
184+ return cache . pProject ;
185+ }
154186 }
155187
156- return this . projectCache [ modulePath ] = readPkg ( modulePath ) . catch ( ( err ) => {
188+ const pPkg = readPkg ( modulePath ) . catch ( ( err ) => {
157189 // Failed to read package
158190 // If dependency shim is available, fake the package
159191
@@ -171,12 +203,14 @@ class NpmTranslator {
171203 };
172204 }*/
173205 throw err ;
174- } ) . then ( ( pkg ) => {
206+ } ) ;
207+
208+ const pProject = pPkg . then ( ( pkg ) => {
175209 return this . processPkg ( {
176210 name : moduleName ,
177211 pkg : pkg ,
178212 path : modulePath
179- } , parentName ) . then ( ( projects ) => {
213+ } , parentPath ) . then ( ( projects ) => {
180214 // Flatten the array of project arrays (yes, because collections)
181215 return Array . prototype . concat . apply ( [ ] , projects . filter ( ( p ) => p !== null ) ) ;
182216 } ) ;
@@ -189,6 +223,12 @@ class NpmTranslator {
189223 dependencies : [ ]
190224 } ] ;
191225 } ) ;
226+
227+ this . projectCache [ modulePath ] = {
228+ pPkg,
229+ pProject
230+ } ;
231+ return pProject ;
192232 }
193233
194234 /* Returns path to a module
@@ -234,13 +274,19 @@ class NpmTranslator {
234274 } ) ;
235275 }
236276
237- registerPendingDependencies ( { dependencies, parentProject} ) {
277+ registerPendingDependencies ( { dependencies, parentProject, parentPath } ) {
238278 Object . keys ( dependencies ) . forEach ( ( moduleName ) => {
239279 if ( this . pendingDeps [ moduleName ] ) {
240- this . pendingDeps [ moduleName ] . parents . push ( parentProject ) ;
280+ this . pendingDeps [ moduleName ] . parents . push ( {
281+ project : parentProject ,
282+ path : parentPath
283+ } ) ;
241284 } else {
242285 this . pendingDeps [ moduleName ] = {
243- parents : [ parentProject ]
286+ parents : [ {
287+ project : parentProject ,
288+ path : parentPath ,
289+ } ]
244290 } ;
245291 }
246292 } ) ;
@@ -263,7 +309,21 @@ class NpmTranslator {
263309
264310 if ( this . pendingDeps [ project . id ] ) {
265311 for ( let i = this . pendingDeps [ project . id ] . parents . length - 1 ; i >= 0 ; i -- ) {
266- this . pendingDeps [ project . id ] . parents [ i ] . dependencies . push ( project ) ;
312+ const parent = this . pendingDeps [ project . id ] . parents [ i ] ;
313+ // Check whether modules has already been processed in the current subtree (indicates a loop)
314+ if ( parent . path . indexOf ( `:${ project . id } :` ) !== - 1 ) {
315+ // This is a loop => abort further processing
316+ const dedupedProject = {
317+ id : project . id ,
318+ version : project . version ,
319+ path : project . path ,
320+ dependencies : [ ] ,
321+ deduped : true
322+ } ;
323+ parent . project . dependencies . push ( dedupedProject ) ;
324+ } else {
325+ parent . project . dependencies . push ( project ) ;
326+ }
267327 }
268328 this . pendingDeps [ project . id ] = null ;
269329 }
0 commit comments