@@ -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,224 @@ 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+ 				// Flag this project as being processed 
46+ 				this . processedProjects [ project . id ]  =  { 
47+ 					project, 
48+ 					// If a project is referenced multiple times in the dependency tree it is replaced 
49+ 					//	with the instance that is closest to the root. 
50+ 					// Here we track the parents referencing that project 
51+ 					parents : [ parent ] 
52+ 				} ; 
53+ 				return  true ; 
54+ 			} ) ; 
4655
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 ) ; 
56+ 			await  Promise . all ( projectsToProcess . map ( async  ( project )  =>  { 
57+ 				log . verbose ( `Processing project ${ project . id } ${ project . _level }  ) ; 
5058
51- 				// No further processing needed 
52- 				continue ; 
53- 			} 
59+ 				project . _level  =  level ; 
5460
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- 			} ; 
61+ 				if  ( project . dependencies  &&  project . dependencies . length )  { 
62+ 					// Do a dependency lookahead to apply any extensions that might affect this project 
63+ 					await  this . dependencyLookahead ( project ,  project . dependencies ) ; 
64+ 				} 
6265
63- 			configPromises . push ( this . configureProject ( project ) . then ( ( config )  =>  { 
64- 				if  ( ! config )  { 
66+ 				await  this . loadProjectConfiguration ( project ) ; 
67+ 				// this.applyShims(project); // shims not yet implemented 
68+ 				if  ( this . isConfigValid ( project ) )  { 
69+ 					await  this . applyType ( project ) ; 
70+ 					queue . push ( { 
71+ 						projects : project . dependencies , 
72+ 						parent : project , 
73+ 						level : level  +  1 
74+ 					} ) ; 
75+ 				}  else  { 
6576					if  ( project  ===  tree )  { 
6677						throw  new  Error ( `Failed to configure root project "${ project . id }  ) ; 
6778					} 
68- 
6979					// No config available 
7080					// => reject this project by removing it from its parents list of dependencies 
71- 					log . verbose ( `Ignoring project ${ project . id }  + 
81+ 					log . verbose ( `Ignoring project ${ project . id }    + 
7282						"(might be a non-UI5 dependency)" ) ; 
73- 					const  parents  =  processedProjects [ project . id ] . parents ; 
83+ 
84+ 					const  parents  =  this . processedProjects [ project . id ] . parents ; 
7485					for  ( let  i  =  parents . length  -  1 ;  i  >=  0 ;  i -- )  { 
7586						parents [ i ] . dependencies . splice ( parents [ i ] . dependencies . indexOf ( project ) ,  1 ) ; 
7687					} 
77- 					processedProjects [ project . id ]  =  { ignored : true } ; 
88+ 					this . processedProjects [ project . id ]  =  { ignored : true } ; 
7889				} 
7990			} ) ) ; 
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- 			} 
9091		} 
9192		return  Promise . all ( configPromises ) . then ( ( )  =>  { 
9293			if  ( log . isLevelEnabled ( "verbose" ) )  { 
9394				const  prettyHrtime  =  require ( "pretty-hrtime" ) ; 
9495				const  timeDiff  =  process . hrtime ( startTime ) ; 
95- 				log . verbose ( `Processed ${ Object . keys ( processedProjects ) . length } ${ prettyHrtime ( timeDiff ) }  ) ; 
96+ 				log . verbose ( `Processed ${ Object . keys ( this . processedProjects ) . length } ${ prettyHrtime ( timeDiff ) }  ) ; 
9697			} 
9798			return  tree ; 
9899		} ) ; 
99100	} 
100101
101- 	async  configureProject ( project )  { 
102- 		if  ( ! project . specVersion )  {  // Project might already be configured (e.g. via inline configuration) 
102+ 	async  dependencyLookahead ( parent ,  dependencies )  { 
103+ 		return  Promise . all ( dependencies . map ( async  ( project )  =>  { 
104+ 			if  ( this . isBeingProcessed ( parent ,  project ) )  { 
105+ 				return ; 
106+ 			} 
107+ 			log . verbose ( `Processing dependency lookahead for ${ parent . id } ${ project . id }  ) ; 
108+ 			// Temporarily flag project as being processed 
109+ 			this . processedProjects [ project . id ]  =  { 
110+ 				project, 
111+ 				parents : [ parent ] 
112+ 			} ; 
113+ 			const  { extensions}  =  await  this . loadProjectConfiguration ( project ) ; 
114+ 			if  ( extensions  &&  extensions . length )  { 
115+ 				// Project contains additional extensions 
116+ 				// => apply them 
117+ 				await  Promise . all ( extensions . map ( ( extProject )  =>  { 
118+ 					return  this . applyExtension ( extProject ) ; 
119+ 				} ) ) ; 
120+ 			} 
121+ 
122+ 			if  ( project . kind  ===  "extension" )  { 
123+ 				// Not a project but an extension 
124+ 				// => remove it as from any known projects that depend on it 
125+ 				const  parents  =  this . processedProjects [ project . id ] . parents ; 
126+ 				for  ( let  i  =  parents . length  -  1 ;  i  >=  0 ;  i -- )  { 
127+ 					parents [ i ] . dependencies . splice ( parents [ i ] . dependencies . indexOf ( project ) ,  1 ) ; 
128+ 				} 
129+ 				// Also ignore it from further processing by other projects depending on it 
130+ 				this . processedProjects [ project . id ]  =  { ignored : true } ; 
131+ 
132+ 				if  ( this . isConfigValid ( project ) )  { 
133+ 					// Finally apply the extension 
134+ 					await  this . applyExtension ( project ) ; 
135+ 				}  else  { 
136+ 					log . verbose ( `Ignoring extension ${ project . id }  ) ; 
137+ 				} 
138+ 			}  else  { 
139+ 				// Project is not an extension: Reset processing status of lookahead to allow the real processing 
140+ 				this . processedProjects [ project . id ]  =  null ; 
141+ 			} 
142+ 		} ) ) ; 
143+ 	} 
144+ 
145+ 	isBeingProcessed ( parent ,  project )  {  // Check whether a project is currently being or has already been processed 
146+ 		const  processedProject  =  this . processedProjects [ project . id ] ; 
147+ 		if  ( processedProject )  { 
148+ 			if  ( processedProject . ignored )  { 
149+ 				log . verbose ( `Dependency of project ${ parent . id } ${ project . id }  ) ; 
150+ 				parent . dependencies . splice ( parent . dependencies . indexOf ( project ) ,  1 ) ; 
151+ 				return  true ; 
152+ 			} 
153+ 			log . verbose ( `Dependency of project ${ parent . id } ${ project . id } ${ parent . _level  +  1 }  + 
154+ 				`replaced by project with same ID and distance to root of ${ processedProject . project . _level }  ) ; 
155+ 
156+ 			// Replace with the already processed project (closer to root -> preferred) 
157+ 			parent . dependencies [ parent . dependencies . indexOf ( project ) ]  =  processedProject . project ; 
158+ 			processedProject . parents . push ( parent ) ; 
159+ 
160+ 			// No further processing needed 
161+ 			return  true ; 
162+ 		} 
163+ 		return  false ; 
164+ 	} 
165+ 
166+ 	async  loadProjectConfiguration ( project )  { 
167+ 		if  ( project . specVersion )  {  // Project might already be configured 
103168			// Currently, specVersion is the indicator for configured projects 
104- 			const  projectConf  =  await  this . getProjectConfiguration ( project ) ; 
169+ 			this . normalizeConfig ( project ) ; 
170+ 			return  { } ; 
171+ 		} 
172+ 
173+ 		let  configs ; 
174+ 
175+ 		// A projects configPath property takes precedence over the default "<projectPath>/ui5.yaml" path 
176+ 		const  configPath  =  project . configPath  ||  path . join ( project . path ,  "/ui5.yaml" ) ; 
177+ 		try  { 
178+ 			configs  =  await  this . readConfigFile ( configPath ) ; 
179+ 		}  catch  ( err )  { 
180+ 			const  errorText  =  "Failed to read configuration for project "  + 
181+ 					`${ project . id } ${ configPath } ${ err . message }  ; 
105182
106- 			if  ( ! projectConf )  { 
107- 				return   null ; 
183+ 			if  ( err . code   !==   "ENOENT" )  {   // Something else than "File or directory does not exist" 
184+ 				throw   new   Error ( errorText ) ; 
108185			} 
186+ 			log . verbose ( errorText ) ; 
187+ 		} 
188+ 
189+ 		if  ( ! configs  ||  ! configs . length )  { 
190+ 			return  { } ; 
191+ 		} 
192+ 
193+ 		for  ( let  i  =  configs . length  -  1 ;  i  >=  0 ;  i -- )  { 
194+ 			this . normalizeConfig ( configs [ i ] ) ; 
195+ 		} 
196+ 
197+ 		const  projectConfigs  =  configs . filter ( ( config )  =>  { 
198+ 			return  config . kind  ===  "project" ; 
199+ 		} ) ; 
200+ 
201+ 		const  extensionConfigs  =  configs . filter ( ( config )  =>  { 
202+ 			return  config . kind  ===  "extension" ; 
203+ 		} ) ; 
204+ 
205+ 		const  projectClone  =  JSON . parse ( JSON . stringify ( project ) ) ; 
206+ 
207+ 		// While a project can contain multiple configurations, 
208+ 		//	from a dependency tree perspective it is always a single project 
209+ 		// This means it can represent one "project", plus multiple extensions or 
210+ 		//	one extension, plus multiple extensions 
211+ 
212+ 		if  ( projectConfigs . length  ===  1 )  { 
213+ 			// All well, this is the one. Merge config into project 
214+ 			Object . assign ( project ,  projectConfigs [ 0 ] ) ; 
215+ 		}  else  if  ( projectConfigs . length  >  1 )  { 
216+ 			throw  new  Error ( `Found ${ projectConfigs . length }   + 
217+ 								`project ${ project . id }  ) ; 
218+ 		}  else  if  ( projectConfigs . length  ===  0  &&  extensionConfigs . length )  { 
219+ 			// No project, but extensions 
220+ 			// => choose one to represent the project -> the first one 
221+ 			Object . assign ( project ,  extensionConfigs . shift ( ) ) ; 
222+ 		}  else  { 
223+ 			throw  new  Error ( `Found ${ configs . length }   + 
224+ 								`project ${ project . id }  ) ; 
225+ 		} 
226+ 
227+ 		const  extensionProjects  =  extensionConfigs . map ( ( config )  =>  { 
228+ 			// Clone original project 
229+ 			const  configuredProject  =  JSON . parse ( JSON . stringify ( projectClone ) ) ; 
230+ 
109231			// Enhance project with its configuration 
110- 			Object . assign ( project ,  projectConf ) ; 
232+ 			Object . assign ( configuredProject ,  config ) ; 
233+ 		} ) ; 
234+ 
235+ 		return  { extensions : extensionProjects } ; 
236+ 	} 
237+ 
238+ 	normalizeConfig ( config )  { 
239+ 		if  ( ! config . kind )  { 
240+ 			config . kind  =  "project" ;  // default 
111241		} 
242+ 	} 
112243
244+ 	isConfigValid ( project )  { 
113245		if  ( ! project . specVersion )  { 
114246			if  ( project . _level  ===  0 )  { 
115247				throw  new  Error ( `No specification version defined for root project ${ project . id }  ) ; 
116248			} 
117249			log . verbose ( `No specification version defined for project ${ project . id }  ) ; 
118- 			return ;  // return with empty config  
250+ 			return   false ;  // ignore this project  
119251		} 
120252
121253		if  ( project . specVersion  !==  "0.1" )  { 
@@ -128,52 +260,40 @@ class ProjectPreprocessor {
128260			if  ( project . _level  ===  0 )  { 
129261				throw  new  Error ( `No type configured for root project ${ project . id }  ) ; 
130262			} 
131- 			log . verbose ( `No type configured for project ${ project . id }  (neither in project configuration, nor in any shim) ` ) ; 
132- 			return ;  // return with empty config  
263+ 			log . verbose ( `No type configured for project ${ project . id }  ) ; 
264+ 			return   false ;  // ignore this project  
133265		} 
134266
135- 		if  ( project . type  ===  "application"  &&  project . _level  !==  0 )  { 
136- 			// There is only one project of type application allowed 
267+ 		if  ( project . kind  !==  "project"  &&  project . _level  ===  0 )  { 
268+ 			// This is arguable. It is not the concern of ui5-project to define the entry point of a project tree 
269+ 			// On the other hand, there is no known use case for anything else right now and failing early here 
270+ 			//	makes sense in that regard 
271+ 			throw  new  Error ( `Root project needs to be of kind "project". ${ project . id } ${ project . kind }  ) ; 
272+ 		} 
273+ 
274+ 		if  ( project . kind  ===  "project"  &&  project . type  ===  "application"  &&  project . _level  !==  0 )  { 
275+ 			// There is only one project project of type application allowed 
137276			// That project needs to be the root project 
138277			log . verbose ( `[Warn] Ignoring project ${ project . id }  + 
139278					` (distance to root: ${ project . _level }  ) ; 
140- 			return ;  // return with empty config  
279+ 			return   false ;  // ignore this project  
141280		} 
142281
143- 		// Apply type 
144- 		await  this . applyType ( project ) ; 
145- 		return  project ; 
282+ 		return  true ; 
146283	} 
147284
148- 	async  getProjectConfiguration ( project )  { 
149- 		// A projects configPath property takes precedence over the default "<projectPath>/ui5.yaml" path 
150- 		const  configPath  =  project . configPath  ||  path . join ( project . path ,  "/ui5.yaml" ) ; 
151- 
152- 		let  config ; 
285+ 	async  applyType ( project )  { 
286+ 		let  type ; 
153287		try  { 
154- 			config  =  await   this . readConfigFile ( configPath ) ; 
288+ 			type  =  typeRepository . getType ( project . type ) ; 
155289		}  catch  ( err )  { 
156- 			const  errorText  =  "Failed to read configuration for project "  + 
157- 					`${ project . id } ${ configPath } ${ err . message }  ; 
158- 
159- 			if  ( err . code  !==  "ENOENT" )  {  // Something else than "File or directory does not exist" 
160- 				throw  new  Error ( errorText ) ; 
161- 			} 
162- 			log . verbose ( errorText ) ; 
163- 
164- 			/* Disabled shimming until shim-plugin is available 
165- 			// If there is a config shim, use it as fallback 
166- 			if (configShims[project.id]) { 
167- 				// It's ok if there is no project configuration in the project if there is a shim for it 
168- 				log.verbose(`Applying shim for project ${project.id}...`); 
169- 				config = JSON.parse(JSON.stringify(configShims[project.id])); 
170- 			} else { 
171- 				// No configuration available -> return empty config 
172- 				return null; 
173- 			}*/ 
290+ 			throw  new  Error ( `Failed to retrieve type for project ${ project . id } ${ err . message }  ) ; 
174291		} 
292+ 		await  type . format ( project ) ; 
293+ 	} 
175294
176- 		return  config ; 
295+ 	async  applyExtension ( project )  { 
296+ 		// TOOD 
177297	} 
178298
179299	async  readConfigFile ( configPath )  { 
@@ -182,11 +302,6 @@ class ProjectPreprocessor {
182302			filename : path 
183303		} ) ; 
184304	} 
185- 
186- 	async  applyType ( project )  { 
187- 		let  type  =  typeRepository . getType ( project . type ) ; 
188- 		return  type . format ( project ) ; 
189- 	} 
190305} 
191306
192307/** 
0 commit comments