@@ -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 } " is flagged as ignored.` ) ;
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 } ": Distance to root of ${ level } . Will be ` +
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 } on level ${ 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 } ". Please check verbose log for details.` ) ;
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 } with missing configuration ` +
81+ log . verbose ( `Ignoring project ${ project . id } with missing configuration ` +
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 } projects in ${ prettyHrtime ( timeDiff ) } ` ) ;
96+ log . verbose ( `Processed ${ Object . keys ( this . processedProjects ) . length } projects in ${ 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 } with missing configuration` ) ;
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 } " is flagged as ignored.` ) ;
150+ parent . dependencies . splice ( parent . dependencies . indexOf ( project ) , 1 ) ;
151+ return true ;
152+ }
153+ log . verbose ( `Dependency of project ${ parent . id } , "${ project . id } ": Distance to root of ${ parent . _level + 1 } . Will be ` +
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 } at "${ configPath } ". Error: ${ 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 } configurations of kind 'project' for ` +
217+ `project ${ project . id } . There is only one project per configuration allowed.` ) ;
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 } configurations for ` +
224+ `project ${ project . id } . None are of valid kind.` ) ;
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 } is of kind ${ 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 } with type application` +
139278 ` (distance to root: ${ project . _level } ). Type application is only allowed for the root project` ) ;
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 } at "${ configPath } ". Error: ${ 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