@@ -3,19 +3,22 @@ const fs = require("graceful-fs");
33const  path  =  require ( "path" ) ; 
44const  { promisify}  =  require ( "util" ) ; 
55const  readFile  =  promisify ( fs . readFile ) ; 
6- const  parseYaml  =  require ( "js-yaml" ) . safeLoad ; 
6+ const  parseYaml  =  require ( "js-yaml" ) . safeLoadAll ; 
77const  typeRepository  =  require ( "@ui5/builder" ) . types . typeRepository ; 
88
99class  ProjectPreprocessor  { 
10+ 	constructor ( )  { 
11+ 		this . processedProjects  =  { } ; 
12+ 	} 
13+ 
1014	/* 
1115		Adapt and enhance the project tree: 
12- 			- Replace duplicate projects further away from the root with those closed  to the root 
16+ 			- Replace duplicate projects further away from the root with those closer  to the root 
1317			- Add configuration to projects 
1418	*/ 
1519	async  processTree ( tree )  { 
16- 		const  processedProjects  =  { } ; 
1720		const  queue  =  [ { 
18- 			project :  tree , 
21+ 			projects :  [ tree ] , 
1922			parent : null , 
2023			level : 0 
2124		} ] ; 
@@ -27,95 +30,210 @@ class ProjectPreprocessor {
2730
2831		// Breadth-first search to prefer projects closer to root 
2932		while  ( queue . length )  { 
30- 			const  { project,  parent,  level}  =  queue . shift ( ) ;  // Get and remove first entry from queue 
31- 			if  ( ! project . id )  { 
32- 				throw  new  Error ( "Encountered project with missing id" ) ; 
33- 			} 
34- 			project . _level  =  level ; 
35- 
36- 			// Check whether project ID is already known 
37- 			const  processedProject  =  processedProjects [ project . id ] ; 
38- 			if  ( processedProject )  { 
39- 				if  ( processedProject . ignored )  { 
40- 					log . verbose ( `Dependency of project ${ parent . id } ${ project . id }  ) ; 
41- 					parent . dependencies . splice ( parent . dependencies . indexOf ( project ) ,  1 ) ; 
42- 					continue ; 
33+ 			const  { projects,  parent,  level}  =  queue . shift ( ) ;  // Get and remove first entry from queue 
34+ 
35+ 			// Before processing all projects on a level concurrently, we need to set all of them as being processed. 
36+ 			// This prevents transitive dependencies pointing to the same projects from being processed first 
37+ 			//	 by the dependency lookahead 
38+ 			const  projectsToProcess  =  projects . filter ( ( project )  =>  { 
39+ 				if  ( ! project . id )  { 
40+ 					throw  new  Error ( "Encountered project with missing id" ) ; 
41+ 				} 
42+ 				if  ( this . isBeingProcessed ( parent ,  project ) )  { 
43+ 					return  false ; 
4344				} 
44- 				log . verbose ( `Dependency of project ${ parent . id } ${ project . id } ${ level }  + 
45- 					`replaced by project with same ID and distance to root of ${ processedProject . project . _level }  ) ; 
45+ 				this . processedProjects [ project . id ]  =  { 
46+ 					project, 
47+ 					// If a project is referenced multiple times in the dependency tree it is replaced 
48+ 					//	with the instance that is closest to the root. 
49+ 					// Here we track the parents referencing that project 
50+ 					parents : [ parent ] 
51+ 				} ; 
52+ 				return  true ; 
53+ 			} ) ; 
4654
47- 				// Replace with the already processed project (closer to root -> preferred) 
48- 				parent . dependencies [ parent . dependencies . indexOf ( project ) ]  =  processedProject . project ; 
49- 				processedProject . parents . push ( parent ) ; 
55+ 			await  Promise . all ( projectsToProcess . map ( async  ( project )  =>  { 
56+ 				log . verbose ( `Processing project ${ project . id } ${ project . _level }  ) ; 
5057
51- 				// No further processing needed 
52- 				continue ; 
53- 			} 
58+ 				project . _level  =  level ; 
5459
55- 			processedProjects [ project . id ]  =  { 
56- 				project, 
57- 				// If a project is referenced multiple times in the dependency tree, 
58- 				//	it is replaced with the occurrence closest to the root. 
59- 				// Here we collect the different parents, this single project configuration then has 
60- 				parents : [ parent ] 
61- 			} ; 
60+ 				if  ( project . dependencies  &&  project . dependencies . length )  { 
61+ 					await  this . dependencyLookahead ( project ,  project . dependencies ) ; 
62+ 				} 
6263
63- 			configPromises . push ( this . configureProject ( project ) . then ( ( config )  =>  { 
64- 				if  ( ! config )  { 
64+ 				await  this . loadProjectConfiguration ( project ) ; 
65+ 				// this.applyShims(project); // shims not yet implemented 
66+ 				if  ( this . isConfigValid ( project ) )  { 
67+ 					await  this . applyType ( project ) ; 
68+ 					queue . push ( { 
69+ 						projects : project . dependencies , 
70+ 						parent : project , 
71+ 						level : level  +  1 
72+ 					} ) ; 
73+ 				}  else  { 
6574					if  ( project  ===  tree )  { 
6675						throw  new  Error ( `Failed to configure root project "${ project . id }  ) ; 
6776					} 
68- 
6977					// No config available 
7078					// => reject this project by removing it from its parents list of dependencies 
71- 					log . verbose ( `Ignoring project ${ project . id }  + 
79+ 					log . verbose ( `Ignoring project ${ project . id }    + 
7280						"(might be a non-UI5 dependency)" ) ; 
73- 					const  parents  =  processedProjects [ project . id ] . parents ; 
81+ 
82+ 					const  parents  =  this . processedProjects [ project . id ] . parents ; 
7483					for  ( let  i  =  parents . length  -  1 ;  i  >=  0 ;  i -- )  { 
7584						parents [ i ] . dependencies . splice ( parents [ i ] . dependencies . indexOf ( project ) ,  1 ) ; 
7685					} 
77- 					processedProjects [ project . id ]  =  { ignored : true } ; 
86+ 					this . processedProjects [ project . id ]  =  { ignored : true } ; 
7887				} 
7988			} ) ) ; 
80- 
81- 			if  ( project . dependencies )  { 
82- 				queue . push ( ...project . dependencies . map ( ( depProject )  =>  { 
83- 					return  { 
84- 						project : depProject , 
85- 						parent : project , 
86- 						level : level  +  1 
87- 					} ; 
88- 				} ) ) ; 
89- 			} 
9089		} 
9190		return  Promise . all ( configPromises ) . then ( ( )  =>  { 
9291			if  ( log . isLevelEnabled ( "verbose" ) )  { 
9392				const  prettyHrtime  =  require ( "pretty-hrtime" ) ; 
9493				const  timeDiff  =  process . hrtime ( startTime ) ; 
95- 				log . verbose ( `Processed ${ Object . keys ( processedProjects ) . length } ${ prettyHrtime ( timeDiff ) }  ) ; 
94+ 				log . verbose ( `Processed ${ Object . keys ( this . processedProjects ) . length } ${ prettyHrtime ( timeDiff ) }  ) ; 
9695			} 
9796			return  tree ; 
9897		} ) ; 
9998	} 
10099
101- 	async  configureProject ( project )  { 
102- 		if  ( ! project . specVersion )  {  // Project might already be configured (e.g. via inline configuration) 
100+ 	async  dependencyLookahead ( parent ,  dependencies )  { 
101+ 		return  Promise . all ( dependencies . map ( async  ( project )  =>  { 
102+ 			if  ( this . isBeingProcessed ( project ,  project ) )  { 
103+ 				return ; 
104+ 			} 
105+ 			log . verbose ( `Processing dependency lookahead for ${ parent . id } ${ project . id }  ) ; 
106+ 			this . processedProjects [ project . id ]  =  { 
107+ 				project, 
108+ 				parents : [ parent ] 
109+ 			} ; 
110+ 			const  extensions  =  await  this . loadProjectConfiguration ( project ) ; 
111+ 			if  ( extensions  &&  extensions . length )  { 
112+ 				await  Promise . all ( extensions . map ( ( extProject )  =>  { 
113+ 					return  this . applyExtension ( extProject ) ; 
114+ 				} ) ) ; 
115+ 			} 
116+ 
117+ 			if  ( project . kind  ===  "extension"  &&  this . isConfigValid ( project ) )  { 
118+ 				const  parents  =  this . processedProjects [ project . id ] . parents ; 
119+ 				for  ( let  i  =  parents . length  -  1 ;  i  >=  0 ;  i -- )  { 
120+ 					parents [ i ] . dependencies . splice ( parents [ i ] . dependencies . indexOf ( project ) ,  1 ) ; 
121+ 				} 
122+ 				this . processedProjects [ project . id ]  =  { ignored : true } ; 
123+ 				await  this . applyExtension ( project ) ; 
124+ 			}  else  { 
125+ 				// No extension: Reset processing status of lookahead to allow the real processing 
126+ 				this . processedProjects [ project . id ]  =  null ; 
127+ 			} 
128+ 		} ) ) ; 
129+ 	} 
130+ 
131+ 	isBeingProcessed ( parent ,  project )  {  // Check whether a project is currently being or has already been processed 
132+ 		const  processedProject  =  this . processedProjects [ project . id ] ; 
133+ 		if  ( processedProject )  { 
134+ 			if  ( processedProject . ignored )  { 
135+ 				log . verbose ( `Dependency of project ${ parent . id } ${ project . id }  ) ; 
136+ 				parent . dependencies . splice ( parent . dependencies . indexOf ( project ) ,  1 ) ; 
137+ 				return  true ; 
138+ 			} 
139+ 			log . verbose ( `Dependency of project ${ parent . id } ${ project . id } ${ parent . _level  +  1 }  + 
140+ 				`replaced by project with same ID and distance to root of ${ processedProject . project . _level }  ) ; 
141+ 
142+ 			// Replace with the already processed project (closer to root -> preferred) 
143+ 			parent . dependencies [ parent . dependencies . indexOf ( project ) ]  =  processedProject . project ; 
144+ 			processedProject . parents . push ( parent ) ; 
145+ 
146+ 			// No further processing needed 
147+ 			return  true ; 
148+ 		} 
149+ 		return  false ; 
150+ 	} 
151+ 
152+ 	async  loadProjectConfiguration ( project )  { 
153+ 		if  ( project . specVersion )  {  // Project might already be configured 
103154			// Currently, specVersion is the indicator for configured projects 
104- 			const  projectConf  =  await  this . getProjectConfiguration ( project ) ; 
155+ 			this . normalizeConfig ( project ) ; 
156+ 			return ; 
157+ 		} 
105158
106- 			if  ( ! projectConf )  { 
107- 				return  null ; 
159+ 		let  configs ; 
160+ 
161+ 		// A projects configPath property takes precedence over the default "<projectPath>/ui5.yaml" path 
162+ 		const  configPath  =  project . configPath  ||  path . join ( project . path ,  "/ui5.yaml" ) ; 
163+ 		try  { 
164+ 			configs  =  await  this . readConfigFile ( configPath ) ; 
165+ 		}  catch  ( err )  { 
166+ 			const  errorText  =  "Failed to read configuration for project "  + 
167+ 					`${ project . id } ${ configPath } ${ err . message }  ; 
168+ 
169+ 			if  ( err . code  !==  "ENOENT" )  {  // Something else than "File or directory does not exist" 
170+ 				throw  new  Error ( errorText ) ; 
108171			} 
172+ 			log . verbose ( errorText ) ; 
173+ 		} 
174+ 
175+ 		if  ( ! configs  ||  ! configs . length )  { 
176+ 			return ; 
177+ 		} 
178+ 
179+ 		for  ( let  i  =  configs . length  -  1 ;  i  >=  0 ;  i -- )  { 
180+ 			this . normalizeConfig ( configs [ i ] ) ; 
181+ 		} 
182+ 
183+ 		const  projectConfigs  =  configs . filter ( ( config )  =>  { 
184+ 			return  config . kind  ===  "project" ; 
185+ 		} ) ; 
186+ 
187+ 		const  extensionConfigs  =  configs . filter ( ( config )  =>  { 
188+ 			return  config . kind  ===  "extension" ; 
189+ 		} ) ; 
190+ 
191+ 		const  projectClone  =  JSON . parse ( JSON . stringify ( project ) ) ; 
192+ 
193+ 		// While a project can contain multiple configurations, 
194+ 		//	from a dependency tree perspective it is always a single project 
195+ 		// This means it can represent one "project", plus multiple extensions or 
196+ 		//	one extension, plus multiple extensions 
197+ 
198+ 		if  ( projectConfigs . length  ===  1 )  { 
199+ 			// All well, this is the one 
200+ 			Object . assign ( project ,  projectConfigs [ 0 ] ) ; 
201+ 		}  else  if  ( projectConfigs . length  >  1 )  { 
202+ 			throw  new  Error ( `Found ${ projectConfigs . length }   + 
203+ 								`project ${ project . id }  ) ; 
204+ 		}  else  if  ( projectConfigs . length  ===  0  &&  extensionConfigs . length )  { 
205+ 			// No project, but extensions 
206+ 			// => choose one to represent the project (the first one) 
207+ 			Object . assign ( project ,  extensionConfigs . shift ( ) ) ; 
208+ 		}  else  { 
209+ 			throw  new  Error ( `Found ${ configs . length }   + 
210+ 								`project ${ project . id }  ) ; 
211+ 		} 
212+ 
213+ 		const  extensionProjects  =  extensionConfigs . map ( ( config )  =>  { 
214+ 			// Clone original project 
215+ 			const  configuredProject  =  JSON . parse ( JSON . stringify ( projectClone ) ) ; 
216+ 
109217			// Enhance project with its configuration 
110- 			Object . assign ( project ,  projectConf ) ; 
218+ 			Object . assign ( configuredProject ,  config ) ; 
219+ 		} ) ; 
220+ 
221+ 		return  extensionProjects ; 
222+ 	} 
223+ 
224+ 	normalizeConfig ( config )  { 
225+ 		if  ( ! config . kind )  { 
226+ 			config . kind  =  "project" ;  // default 
111227		} 
228+ 	} 
112229
230+ 	isConfigValid ( project )  { 
113231		if  ( ! project . specVersion )  { 
114232			if  ( project . _level  ===  0 )  { 
115233				throw  new  Error ( `No specification version defined for root project ${ project . id }  ) ; 
116234			} 
117235			log . verbose ( `No specification version defined for project ${ project . id }  ) ; 
118- 			return ;  // return with empty config  
236+ 			return   false ;  // ignore this project  
119237		} 
120238
121239		if  ( project . specVersion  !==  "0.1" )  { 
@@ -128,21 +246,40 @@ class ProjectPreprocessor {
128246			if  ( project . _level  ===  0 )  { 
129247				throw  new  Error ( `No type configured for root project ${ project . id }  ) ; 
130248			} 
131- 			log . verbose ( `No type configured for project ${ project . id }  (neither in project configuration, nor in any shim) ` ) ; 
132- 			return ;  // return with empty config  
249+ 			log . verbose ( `No type configured for project ${ project . id }  ) ; 
250+ 			return   false ;  // ignore this project  
133251		} 
134252
135- 		if  ( project . type  ===  "application"  &&  project . _level  !==  0 )  { 
136- 			// There is only one project of type application allowed 
253+ 		if  ( project . kind  !==  "project"  &&  project . _level  ===  0 )  { 
254+ 			// This is arguable. It is not the concern of ui5-project to define the entry point of a project tree 
255+ 			// On the other hand, there is no known use case for anything else right now and failing early here 
256+ 			//	makes sense in that regard 
257+ 			throw  new  Error ( `Root project needs to be of kind "project". ${ project . id } ${ project . kind }  ) ; 
258+ 		} 
259+ 
260+ 		if  ( project . kind  ===  "project"  &&  project . type  ===  "application"  &&  project . _level  !==  0 )  { 
261+ 			// There is only one project project of type application allowed 
137262			// That project needs to be the root project 
138263			log . verbose ( `[Warn] Ignoring project ${ project . id }  + 
139264					` (distance to root: ${ project . _level }  ) ; 
140- 			return ;  // return with empty config 
265+ 			return  false ;  // ignore this project 
266+ 		} 
267+ 
268+ 		return  true ; 
269+ 	} 
270+ 
271+ 	async  applyType ( project )  { 
272+ 		let  type ; 
273+ 		try  { 
274+ 			type  =  typeRepository . getType ( project . type ) ; 
275+ 		}  catch  ( err )  { 
276+ 			throw  new  Error ( `Failed to retrieve type for project ${ project . id } ${ err . message }  ) ; 
141277		} 
278+ 		await  type . format ( project ) ; 
279+ 	} 
142280
143- 		// Apply type 
144- 		await  this . applyType ( project ) ; 
145- 		return  project ; 
281+ 	async  applyExtension ( project )  { 
282+ 		// TOOD 
146283	} 
147284
148285	async  getProjectConfiguration ( project )  { 
@@ -182,11 +319,6 @@ class ProjectPreprocessor {
182319			filename : path 
183320		} ) ; 
184321	} 
185- 
186- 	async  applyType ( project )  { 
187- 		let  type  =  typeRepository . getType ( project . type ) ; 
188- 		return  type . format ( project ) ; 
189- 	} 
190322} 
191323
192324/** 
0 commit comments