6
6
*/
7
7
import { WriteInfo , WriterFormat } from './types' ;
8
8
import { MetadataComponent , SourceComponent } from '../resolve' ;
9
- import { join } from 'path' ;
9
+ import { basename , dirname , join , resolve , sep } from 'path' ;
10
10
import { JsToXml } from './streams' ;
11
- import { XML_NS_KEY , XML_NS_URL } from '../common' ;
12
- import { JsonArray , JsonMap } from '@salesforce/ts-types' ;
11
+ import { META_XML_SUFFIX , XML_NS_KEY , XML_NS_URL } from '../common' ;
12
+ import { getString , JsonArray , JsonMap } from '@salesforce/ts-types' ;
13
13
import { ComponentSet } from '../collections' ;
14
+ import { RecompositionStrategy } from '../registry/types' ;
15
+ import { isEmpty } from '@salesforce/kit' ;
14
16
15
17
abstract class ConvertTransactionFinalizer < T > {
16
18
protected abstract _state : T ;
@@ -23,7 +25,7 @@ abstract class ConvertTransactionFinalizer<T> {
23
25
return this . _state ;
24
26
}
25
27
26
- public abstract finalize ( ) : Promise < WriterFormat [ ] > ;
28
+ public abstract finalize ( defaultDirectory ?: string ) : Promise < WriterFormat [ ] > ;
27
29
}
28
30
29
31
export interface RecompositionState {
@@ -66,13 +68,16 @@ class RecompositionFinalizer extends ConvertTransactionFinalizer<RecompositionSt
66
68
}
67
69
68
70
private async recompose ( children : ComponentSet , parent : SourceComponent ) : Promise < JsonMap > {
69
- const parentXmlObj : JsonMap = await parent . parseXml ( ) ;
71
+ const parentXmlObj =
72
+ parent . type . strategies . recomposition === RecompositionStrategy . StartEmpty
73
+ ? { }
74
+ : await parent . parseXml ( ) ;
70
75
71
76
for ( const child of children ) {
72
77
const { directoryName : groupName } = child . type ;
73
78
const { name : parentName } = child . parent . type ;
74
79
const xmlObj = await ( child as SourceComponent ) . parseXml ( ) ;
75
- const childContents = xmlObj [ child . type . name ] ;
80
+ const childContents = xmlObj [ child . type . name ] || xmlObj ;
76
81
77
82
if ( ! parentXmlObj [ parentName ] ) {
78
83
parentXmlObj [ parentName ] = { [ XML_NS_KEY ] : XML_NS_URL } ;
@@ -88,7 +93,6 @@ class RecompositionFinalizer extends ConvertTransactionFinalizer<RecompositionSt
88
93
89
94
group . push ( childContents ) ;
90
95
}
91
-
92
96
return parentXmlObj ;
93
97
}
94
98
}
@@ -125,17 +129,223 @@ class DecompositionFinalizer extends ConvertTransactionFinalizer<DecompositionSt
125
129
}
126
130
}
127
131
132
+ export interface NonDecompositionState {
133
+ claimed : ChildIndex ;
134
+ unclaimed : ChildIndex ;
135
+ }
136
+
137
+ type ChildIndex = {
138
+ [ componentKey : string ] : {
139
+ parent : SourceComponent ;
140
+ children : {
141
+ [ childName : string ] : JsonMap ;
142
+ } ;
143
+ } ;
144
+ } ;
145
+
146
+ /**
147
+ * Merges child components that share the same parent in the conversion pipeline
148
+ * into a single file.
149
+ *
150
+ * Inserts unclaimed child components into the parent that belongs to the default package
151
+ */
152
+ class NonDecompositionFinalizer extends ConvertTransactionFinalizer < NonDecompositionState > {
153
+ protected _state : NonDecompositionState = {
154
+ unclaimed : { } ,
155
+ claimed : { } ,
156
+ } ;
157
+
158
+ public async finalize ( defaultDirectory : string ) : Promise < WriterFormat [ ] > {
159
+ await this . finalizeState ( defaultDirectory ) ;
160
+
161
+ const writerData : WriterFormat [ ] = [ ] ;
162
+ for ( const { parent, children } of Object . values ( this . state . claimed ) ) {
163
+ const recomposedXmlObj = await this . recompose ( Object . values ( children ) , parent ) ;
164
+
165
+ writerData . push ( {
166
+ component : parent ,
167
+ writeInfos : [ { source : new JsToXml ( recomposedXmlObj ) , output : parent . xml } ] ,
168
+ } ) ;
169
+ }
170
+
171
+ for ( const { parent, children } of Object . values ( this . state . unclaimed ) ) {
172
+ const recomposedXmlObj = await this . recompose ( Object . values ( children ) , parent ) ;
173
+ writerData . push ( {
174
+ component : parent ,
175
+ writeInfos : [
176
+ { source : new JsToXml ( recomposedXmlObj ) , output : this . getDefaultOutput ( parent ) } ,
177
+ ] ,
178
+ } ) ;
179
+ }
180
+
181
+ return writerData ;
182
+ }
183
+
184
+ /**
185
+ * This method finalizes the state by:
186
+ * - finding any "unprocessed components" (nondecomposed metadata types can exist in multiple locations under the same name
187
+ * so we have to find all components that could potentially claim children)
188
+ * - removing any children from the unclaimed state that have been claimed by the unprocessed components
189
+ * - removing any children from the unclaimed state that have already been claimed by a prent in the claimed state
190
+ * - merging the remaining unclaimed children into the default parent component (either the component that matches the
191
+ * defaultDirectory or the first parent component)
192
+ */
193
+ private async finalizeState ( defaultDirectory : string ) : Promise < void > {
194
+ if ( isEmpty ( this . state . claimed ) ) {
195
+ return ;
196
+ }
197
+
198
+ const unprocessedComponents = this . getUnprocessedComponents ( defaultDirectory ) ;
199
+ const parentPaths = Object . keys ( this . state . claimed ) . concat (
200
+ unprocessedComponents . map ( ( c ) => c . xml )
201
+ ) ;
202
+
203
+ const defaultComponentKey =
204
+ parentPaths . find ( ( p ) => p . startsWith ( defaultDirectory ) ) || parentPaths [ 0 ] ;
205
+
206
+ const claimedChildren = [
207
+ ...this . getClaimedChildrenNames ( ) ,
208
+ ...( await this . getChildrenOfUnprocessedComponents ( unprocessedComponents ) ) ,
209
+ ] ;
210
+
211
+ // merge unclaimed children into default parent component
212
+ for ( const [ key , childIndex ] of Object . entries ( this . state . unclaimed ) ) {
213
+ const pruned = Object . entries ( childIndex . children ) . reduce ( ( result , [ childName , childXml ] ) => {
214
+ return ! claimedChildren . includes ( childName )
215
+ ? Object . assign ( result , { [ childName ] : childXml } )
216
+ : result ;
217
+ } , { } ) ;
218
+ delete this . state . unclaimed [ key ] ;
219
+ if ( this . state . claimed [ defaultComponentKey ] ) {
220
+ this . state . claimed [ defaultComponentKey ] . children = Object . assign (
221
+ { } ,
222
+ this . state . claimed [ defaultComponentKey ] . children ,
223
+ pruned
224
+ ) ;
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Returns the "unprocessed components"
231
+ *
232
+ * An unprocessed component is a component that was not resolved during component resolution.
233
+ * This typically only happens when a specific source path was resolved. This is problematic for
234
+ * nondecomposed metadata types (like CustomLabels) because we need to know the location of each
235
+ * child type before recomposing the final xml. So in order for each of the children to be properly
236
+ * claimed, we have to create new ComponentSet that will have all the parent components.
237
+ */
238
+ private getUnprocessedComponents ( defaultDirectory : string ) : SourceComponent [ ] {
239
+ if ( isEmpty ( this . state . unclaimed ) ) {
240
+ return [ ] ;
241
+ }
242
+ const parents = this . getParentsOfClaimedChildren ( ) ;
243
+ const filterSet = new ComponentSet ( parents ) ;
244
+
245
+ const { tree } = parents [ 0 ] ;
246
+ const projectDir = resolve ( dirname ( defaultDirectory ) ) ;
247
+ const parentDirs = Object . keys ( this . state . claimed ) . map ( ( k ) => {
248
+ const parts = k . split ( sep ) ;
249
+ const partIndex = parts . findIndex ( ( p ) => basename ( projectDir ) === p ) ;
250
+ return parts [ partIndex + 1 ] ;
251
+ } ) ;
252
+
253
+ const fsPaths = tree
254
+ . readDirectory ( projectDir )
255
+ . map ( ( p ) => join ( projectDir , p ) )
256
+ . filter ( ( p ) => {
257
+ const dirName = basename ( p ) ;
258
+ // Only return directories that are likely to be a project directory
259
+ return (
260
+ tree . isDirectory ( p ) &&
261
+ ! dirName . startsWith ( '.' ) &&
262
+ dirName !== 'config' &&
263
+ dirName !== 'node_modules' &&
264
+ ! parentDirs . includes ( dirName )
265
+ ) ;
266
+ } ) ;
267
+
268
+ const unprocessedComponents = ComponentSet . fromSource ( { fsPaths, include : filterSet } )
269
+ . getSourceComponents ( )
270
+ . filter ( ( component ) => ! this . state . claimed [ component . xml ] ) ;
271
+ return unprocessedComponents . toArray ( ) ;
272
+ }
273
+
274
+ /**
275
+ * Returns the children of "unprocessed components"
276
+ */
277
+ private async getChildrenOfUnprocessedComponents (
278
+ unprocessedComponents : SourceComponent [ ]
279
+ ) : Promise < string [ ] > {
280
+ const childrenOfUnprocessed = [ ] ;
281
+ for ( const component of unprocessedComponents ) {
282
+ for ( const child of component . getChildren ( ) ) {
283
+ const xml = await child . parseXml ( ) ;
284
+ const childName = getString ( xml , child . type . uniqueIdElement ) ;
285
+ childrenOfUnprocessed . push ( childName ) ;
286
+ }
287
+ }
288
+ return childrenOfUnprocessed ;
289
+ }
290
+
291
+ private async recompose ( children : JsonMap [ ] , parent : SourceComponent ) : Promise < JsonMap > {
292
+ const parentXmlObj =
293
+ parent . type . strategies . recomposition === RecompositionStrategy . StartEmpty
294
+ ? { }
295
+ : await parent . parseXml ( ) ;
296
+ const groupName = parent . type . directoryName ;
297
+ const parentName = parent . type . name ;
298
+ for ( const child of children ) {
299
+ if ( ! parentXmlObj [ parentName ] ) {
300
+ parentXmlObj [ parentName ] = { [ XML_NS_KEY ] : XML_NS_URL } ;
301
+ }
302
+
303
+ const parent = parentXmlObj [ parentName ] as JsonMap ;
304
+
305
+ if ( ! parent [ groupName ] ) {
306
+ parent [ groupName ] = [ ] ;
307
+ }
308
+
309
+ const group = parent [ groupName ] as JsonArray ;
310
+
311
+ group . push ( child ) ;
312
+ }
313
+
314
+ return parentXmlObj ;
315
+ }
316
+
317
+ private getDefaultOutput ( component : SourceComponent ) : string {
318
+ const { fullName } = component ;
319
+ const [ baseName ] = fullName . split ( '.' ) ;
320
+ const output = `${ baseName } .${ component . type . suffix } ${ META_XML_SUFFIX } ` ;
321
+
322
+ return join ( component . getPackageRelativePath ( '' , 'source' ) , output ) ;
323
+ }
324
+
325
+ private getClaimedChildrenNames ( ) : string [ ] {
326
+ return Object . values ( this . state . claimed ) . reduce (
327
+ ( x , y ) => x . concat ( Object . keys ( y . children ) ) ,
328
+ [ ]
329
+ ) ;
330
+ }
331
+
332
+ private getParentsOfClaimedChildren ( ) : SourceComponent [ ] {
333
+ return Object . values ( this . state . claimed ) . reduce ( ( x , y ) => x . concat ( [ y . parent ] ) , [ ] ) ;
334
+ }
335
+ }
336
+
128
337
/**
129
338
* A state manager over the course of a single metadata conversion call.
130
339
*/
131
340
export class ConvertContext {
132
341
public readonly decomposition = new DecompositionFinalizer ( ) ;
133
342
public readonly recomposition = new RecompositionFinalizer ( ) ;
343
+ public readonly nonDecomposition = new NonDecompositionFinalizer ( ) ;
134
344
135
- public async * executeFinalizers ( ) : AsyncIterable < WriterFormat [ ] > {
345
+ public async * executeFinalizers ( defaultDirectory ?: string ) : AsyncIterable < WriterFormat [ ] > {
136
346
for ( const member of Object . values ( this ) ) {
137
347
if ( member instanceof ConvertTransactionFinalizer ) {
138
- yield member . finalize ( ) ;
348
+ yield member . finalize ( defaultDirectory ) ;
139
349
}
140
350
}
141
351
}
0 commit comments