@@ -3,19 +3,22 @@ const fs = require("graceful-fs");
3
3
const path = require ( "path" ) ;
4
4
const { promisify} = require ( "util" ) ;
5
5
const readFile = promisify ( fs . readFile ) ;
6
- const parseYaml = require ( "js-yaml" ) . safeLoad ;
6
+ const parseYaml = require ( "js-yaml" ) . safeLoadAll ;
7
7
const typeRepository = require ( "@ui5/builder" ) . types . typeRepository ;
8
8
9
9
class ProjectPreprocessor {
10
+ constructor ( ) {
11
+ this . processedProjects = { } ;
12
+ }
13
+
10
14
/*
11
15
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
13
17
- Add configuration to projects
14
18
*/
15
19
async processTree ( tree ) {
16
- const processedProjects = { } ;
17
20
const queue = [ {
18
- project : tree ,
21
+ projects : [ tree ] ,
19
22
parent : null ,
20
23
level : 0
21
24
} ] ;
@@ -27,95 +30,224 @@ class ProjectPreprocessor {
27
30
28
31
// Breadth-first search to prefer projects closer to root
29
32
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 ;
43
44
}
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
+ } ) ;
46
55
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 } ...` ) ;
50
58
51
- // No further processing needed
52
- continue ;
53
- }
59
+ project . _level = level ;
54
60
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
+ }
62
65
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 {
65
76
if ( project === tree ) {
66
77
throw new Error ( `Failed to configure root project "${ project . id } ". Please check verbose log for details.` ) ;
67
78
}
68
-
69
79
// No config available
70
80
// => 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 ` +
72
82
"(might be a non-UI5 dependency)" ) ;
73
- const parents = processedProjects [ project . id ] . parents ;
83
+
84
+ const parents = this . processedProjects [ project . id ] . parents ;
74
85
for ( let i = parents . length - 1 ; i >= 0 ; i -- ) {
75
86
parents [ i ] . dependencies . splice ( parents [ i ] . dependencies . indexOf ( project ) , 1 ) ;
76
87
}
77
- processedProjects [ project . id ] = { ignored : true } ;
88
+ this . processedProjects [ project . id ] = { ignored : true } ;
78
89
}
79
90
} ) ) ;
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
- }
90
91
}
91
92
return Promise . all ( configPromises ) . then ( ( ) => {
92
93
if ( log . isLevelEnabled ( "verbose" ) ) {
93
94
const prettyHrtime = require ( "pretty-hrtime" ) ;
94
95
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 ) } ` ) ;
96
97
}
97
98
return tree ;
98
99
} ) ;
99
100
}
100
101
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
103
168
// 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 } ` ;
105
182
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 ) ;
108
185
}
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
+
109
231
// 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
111
241
}
242
+ }
112
243
244
+ isConfigValid ( project ) {
113
245
if ( ! project . specVersion ) {
114
246
if ( project . _level === 0 ) {
115
247
throw new Error ( `No specification version defined for root project ${ project . id } ` ) ;
116
248
}
117
249
log . verbose ( `No specification version defined for project ${ project . id } ` ) ;
118
- return ; // return with empty config
250
+ return false ; // ignore this project
119
251
}
120
252
121
253
if ( project . specVersion !== "0.1" ) {
@@ -128,52 +260,40 @@ class ProjectPreprocessor {
128
260
if ( project . _level === 0 ) {
129
261
throw new Error ( `No type configured for root project ${ project . id } ` ) ;
130
262
}
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
133
265
}
134
266
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
137
276
// That project needs to be the root project
138
277
log . verbose ( `[Warn] Ignoring project ${ project . id } with type application` +
139
278
` (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
141
280
}
142
281
143
- // Apply type
144
- await this . applyType ( project ) ;
145
- return project ;
282
+ return true ;
146
283
}
147
284
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 ;
153
287
try {
154
- config = await this . readConfigFile ( configPath ) ;
288
+ type = typeRepository . getType ( project . type ) ;
155
289
} 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 } ` ) ;
174
291
}
292
+ await type . format ( project ) ;
293
+ }
175
294
176
- return config ;
295
+ async applyExtension ( project ) {
296
+ // TOOD
177
297
}
178
298
179
299
async readConfigFile ( configPath ) {
@@ -182,11 +302,6 @@ class ProjectPreprocessor {
182
302
filename : path
183
303
} ) ;
184
304
}
185
-
186
- async applyType ( project ) {
187
- let type = typeRepository . getType ( project . type ) ;
188
- return type . format ( project ) ;
189
- }
190
305
}
191
306
192
307
/**
0 commit comments