@ds-sfdc/sfparty 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![NPM](https://img.shields.io/npm/v/@ds-sfdc/sfparty.svg?label=@ds-sfdc/sfparty)](https://www.npmjs.com/package/@ds-sfdc/sfparty) [![Downloads/week](https://img.shields.io/npm/dw/@ds-sfdc/sfparty.svg)](https://npmjs.org/package/@ds-sfdc/sfparty) [![License](https://img.shields.io/badge/License-BSD%203--Clause-brightgreen.svg)](https://github.com/TimPaulaskasDS/sfparty/blob/main/LICENSE.md)
4
4
 
5
- ## Using the template
5
+ ## What is sfparty?
6
6
 
7
- This tool will split Salesforce metadata XML files into YAML parts (or JSON).
7
+ For those that are familiar with Salesforce metadata, you know that it uses large XML files. These XML files are difficult to diff, hard to read, and can cause conflicts and corrupted XML when merging. This tool will split Salesforce metadata XML files into YAML parts (or JSON), and combine them back into XML files. A great solution for your CI/CD needs.
8
8
 
9
9
  ## Install
10
10
 
@@ -13,6 +13,72 @@ npm i @ds-sfdc/sfparty
13
13
  ```
14
14
  ## Commands
15
15
 
16
- <!-- commands -->
16
+ ### Split
17
+ ```bash
18
+ sfparty split
19
+ ```
20
+
21
+ ### Combine
22
+ ```bash
23
+ sfparty combine
24
+ ```
25
+
26
+ ### Options
27
+
28
+ ```
29
+ -n, --name name of metadata file
30
+ -s, --source package directory path specified in sfdx-project.json
31
+ -t, --target target path to directory to create yaml/json files
32
+ -h, --help Show help
33
+ ```
34
+ ## Examples
35
+ ### Custom Labels
36
+ ```bash
37
+ sfparty split --type=label
38
+ ```
39
+
40
+ ### Permission Set
41
+ ```bash
42
+ sfparty split --type=permset
43
+ sfparty split --type=permset --name="My Permission Set"
44
+ ```
45
+ ### Profile
46
+ ```bash
47
+ sfparty split --type=profile
48
+ sfparty split --type=profile --name="My Profile"
49
+ ```
50
+ ### Workflow
51
+ ```bash
52
+ sfparty split --type=workflow
53
+ sfparty split --type=workflow --name="Workflow"
54
+ ```
55
+ ### Source Directory
56
+ The source directory will use your default package folder as specified in the sfdx-project.json file, and therefore must be executed from your Salesforce project directory. It will create the main/default folders if they do not exist.
17
57
 
18
- <!-- commandsstop -->
58
+ ```
59
+ {
60
+ "packageDirectories": [
61
+ {
62
+ "path": "force-app",
63
+ "default": true
64
+ },
65
+ {
66
+ "path": "my-package"
67
+ }
68
+ ],
69
+ "namespace": "",
70
+ "sfdcLoginUrl": "https://login.salesforce.com",
71
+ "sourceApiVersion": "53.0"
72
+ }
73
+ ```
74
+
75
+ ```bash
76
+ sfparty split --source="my-package"
77
+ ```
78
+
79
+ ### Target Directory
80
+ The source directory will use your default package folder as specified in the sfdx-project.json file, and append `-party` to the end. For example, if the default source path is `force-app`, then the default target directory will be `force-app-party` unless otherwise specified. The target does not need to be specified in the sfdx-project.json, however the combine command will not work on folders that are not specified in the sfdx-project.json.
81
+
82
+ ```bash
83
+ sfparty split --target="test"
84
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-sfdc/sfparty",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Salesforce metadata XML splitter for CI/CD",
5
5
  "type": "module",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ import chalk from 'chalk'
9
9
  import convertHrtime from 'convert-hrtime'
10
10
  import * as fileUtils from './lib/fileUtils.js'
11
11
  import * as pkgObj from '../package.json' assert { type: "json" }
12
+ import * as yargOptions from './meta/yargs.js'
12
13
  import * as metadataSplit from './party/split.js'
13
14
  import * as metadataCombine from './party/combine.js'
14
15
  import * as labelDefinition from './meta/CustomLabels.js'
@@ -16,8 +17,7 @@ import * as profileDefinition from './meta/Profiles.js'
16
17
  import * as permsetDefinition from './meta/PermissionSets.js'
17
18
  import * as workflowDefinition from './meta/Workflows.js'
18
19
 
19
- const startTime = process.hrtime.bigint()
20
-
20
+ const processStartTime = process.hrtime.bigint()
21
21
 
22
22
  global.logger = winston.createLogger({
23
23
  levels: winston.config.syslog.levels,
@@ -28,12 +28,6 @@ global.logger = winston.createLogger({
28
28
  ],
29
29
  });
30
30
 
31
- global.processed = {
32
- total: 0,
33
- errors: 0,
34
- current: 1,
35
- }
36
-
37
31
  global.icons = {
38
32
  "warn": '🔕',
39
33
  "success": chalk.greenBright('✔'),
@@ -68,6 +62,8 @@ const metaTypes = {
68
62
  },
69
63
  }
70
64
 
65
+ let types = []
66
+
71
67
  function getRootPath(packageDir) {
72
68
  let rootPath = fileUtils.find('sfdx-project.json')
73
69
  let defaultDir
@@ -100,7 +96,7 @@ let errorMessage = chalk.red('Please specify the action of ' + chalk.whiteBright
100
96
  displayHeader() // display header mast
101
97
 
102
98
  function displayHeader() {
103
- const table = {
99
+ const box = {
104
100
  topLeft: '╭',
105
101
  topRight: '╮',
106
102
  bottomLeft: '╰',
@@ -112,10 +108,10 @@ function displayHeader() {
112
108
  let titleMessage = `${global.icons.party} ${chalk.yellowBright(versionString)} ${global.icons.party}`
113
109
  titleMessage = titleMessage.padEnd((process.stdout.columns / 2) + versionString.length / 1.65)
114
110
  titleMessage = titleMessage.padStart(process.stdout.columns)
115
- titleMessage = chalk.blackBright(table.vertical) + ' ' + titleMessage + ' ' + chalk.blackBright(table.vertical)
116
- console.log(`${chalk.blackBright(table.topLeft + table.horizontal.repeat(process.stdout.columns - 2) + table.topRight)}`)
111
+ titleMessage = chalk.blackBright(box.vertical) + ' ' + titleMessage + ' ' + chalk.blackBright(box.vertical)
112
+ console.log(`${chalk.blackBright(box.topLeft + box.horizontal.repeat(process.stdout.columns - 2) + box.topRight)}`)
117
113
  console.log(titleMessage)
118
- console.log(`${chalk.blackBright(table.bottomLeft + table.horizontal.repeat(process.stdout.columns - 2) + table.bottomRight)}`)
114
+ console.log(`${chalk.blackBright(box.bottomLeft + box.horizontal.repeat(process.stdout.columns - 2) + box.bottomRight)}`)
119
115
  console.log()
120
116
  }
121
117
 
@@ -124,316 +120,33 @@ yargs(hideBin(process.argv))
124
120
  .command({
125
121
  command: '[split]',
126
122
  alias: 'split',
127
- description: 'splits metadata xml to json files',
123
+ description: 'splits metadata xml to yaml/json files',
128
124
  builder: (yargs) => {
129
125
  yargs
130
- .example([
131
- ['$0 split --type=profile --all'],
132
- ['$0 split --type=permset --name="Permission Set Name"'],
133
- ['--source=packageDir --target=dir/dir'],
134
- ['name portion of file: [name].profile-meta.xml'],
135
- ['Example: --name="Admin" for Admin.profile-meta.xml'],
136
- ['\nCommands not supporting name or all parameters:'],
137
- ['$0 split --type=label'],
138
- ])
139
- .options({
140
- type: {
141
- demand: true,
142
- alias: 'type',
143
- description: 'type of metadata to split',
144
- demandOption: true,
145
- type: 'string',
146
- },
147
- format: {
148
- demand: true,
149
- alias: 'format',
150
- default: 'yaml',
151
- description: 'type of output',
152
- demandOption: true,
153
- type: 'string',
154
- },
155
- name: {
156
- alias: 'n',
157
- description: 'name of metadata file to split',
158
- demandOption: false,
159
- type: 'string',
160
- },
161
- all: {
162
- alias: 'a',
163
- description: 'all metadata files of type will be split',
164
- demandOption: false,
165
- type: 'boolean',
166
- },
167
- source: {
168
- demand: false,
169
- alias: 's',
170
- description: 'package directory path specified in sfdx-project.json',
171
- type: 'string',
172
- },
173
- target: {
174
- demand: false,
175
- alias: 't',
176
- description: 'target path to directory to create json files',
177
- type: 'string',
178
- }
179
- })
180
- .choices('type', typeArray)
126
+ .example(yargOptions.splitExamples)
127
+ .options(yargOptions.splitOptions)
181
128
  .choices('format', ['json', 'yaml'])
182
- .check((argv, options) => {
183
- const name = argv.name
184
- const all = argv.all
185
-
186
- switch (argv.type) {
187
- case 'profile':
188
- case 'permset':
189
- case 'workflow':
190
- if ((typeof name != 'undefined' || name == '') && (typeof all != 'undefined' && all)) {
191
- throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' and ' + chalk.whiteBright.bgRedBright('--all') + ' at the same time.'))
192
- } else if (typeof name == 'undefined' && (typeof all == 'undefined' || !all)) {
193
- throw new Error(chalk.redBright('You must specify the ' + chalk.whiteBright.bgRedBright('--name') + ' parameter or use the ' + chalk.whiteBright.bgRedBright('--all') + ' switch.'))
194
- } else {
195
- return true // tell Yargs that the arguments passed the check
196
- }
197
- case 'label':
198
- if ((typeof name != 'undefined' && name != '') || (typeof all != 'undefined' && all)) {
199
- throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' or ' + chalk.whiteBright.bgRedBright('--all') + ' when using label.'))
200
- }
201
- return true
202
- }
203
- })
129
+ .check(yargCheck)
204
130
  },
205
131
  handler: (argv) => {
206
- if (!typeArray.includes(argv.type)) {
207
- global.logger.error('Metadata type not supported: ' + type)
208
- process.exit(1)
209
- }
210
- const fileList = []
211
- const typeObj = metaTypes[argv.type]
212
- const type = typeObj.type
213
- const metaExtension = `.${type}-meta.xml`
214
132
  global.format = argv.format
215
-
216
- let sourceDir = argv.source || ''
217
- let targetDir = argv.target || ''
218
- let name = argv.name
219
- let all = argv.all
220
- let packageDir = getRootPath(sourceDir)
221
-
222
- if (type == metaTypes.label.type) {
223
- name = metaTypes.label.definition.root
224
- }
225
- sourceDir = path.join(global.__basedir, packageDir, 'main', 'default', typeObj.definition.directory)
226
- if (targetDir == '') {
227
- targetDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', typeObj.definition.directory)
228
- } else {
229
- targetDir = path.join(targetDir, 'main', 'default', typeObj.definition.directory)
230
- }
231
- let metaDirPath = sourceDir
232
- console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
233
- console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
234
- console.log()
235
-
236
- if (!all) {
237
- let metaFilePath = path.join(metaDirPath, name)
238
- if (!fileUtils.fileExists(metaFilePath)) {
239
- name += metaExtension
240
- metaFilePath = path.join(metaDirPath, name)
241
- if (!fileUtils.fileExists(metaFilePath)) {
242
- global.logger.error('File not found: ' + metaFilePath)
243
- process.exit(1)
244
- }
245
- }
246
- fileList.push(name)
247
- } else {
248
- if (fileUtils.directoryExists(sourceDir)) {
249
- fileUtils.getFiles(sourceDir, metaExtension).forEach(file => {
250
- fileList.push(file)
251
- })
252
- }
253
- }
254
-
255
- global.processed.total = fileList.length
256
- console.log(`Splitting a total of ${fileList.length} file(s)`)
257
- console.log()
258
- const promList = []
259
- fileList.forEach(metaFile => {
260
- const metadataItem = new metadataSplit.Split({
261
- metadataDefinition: typeObj.definition,
262
- sourceDir: sourceDir,
263
- targetDir: targetDir,
264
- metaFilePath: path.join(sourceDir, metaFile),
265
- sequence: promList.length + 1,
266
- })
267
- const metadataItemProm = metadataItem.split()
268
- promList.push(metadataItemProm)
269
- metadataItemProm.then((resolve, reject) => {
270
- if (resolve == false) {
271
- global.processed.errors++
272
- global.processed.current--
273
- } else {
274
- global.processed.current++
275
- }
276
- })
277
- })
278
- Promise.allSettled(promList).then((results) => {
279
- let message = `Split ${chalk.bgBlackBright((global.processed.current > promList.length) ? promList.length : global.processed.current)} file(s) ${(global.processed.errors > 0) ? 'with ' + chalk.bgBlackBright.red(global.processed.errors) + ' error(s) ' : ''}in `
280
- displayMessageAndDuration(startTime, message)
281
- })
133
+ splitHandler(argv, processStartTime)
282
134
  }
283
135
  })
284
136
  .command({
285
137
  command: '[combine]',
286
138
  alias: 'combine',
287
- description: 'combines json files into metadata xml',
139
+ description: 'combines yaml/json files into metadata xml',
288
140
  builder: (yargs) => {
289
141
  yargs
290
- .example([
291
- ['$0 combine --type=profile --all'],
292
- ['$0 combine --type=permset --name="Permission Set Name"'],
293
- ['--source=packageDir --target=dir/dir'],
294
- ['name portion of file: [name].profile-meta.xml'],
295
- ['Example: --name="Admin" for Admin.profile-meta.xml'],
296
- ['\nCommands not supporting name or all parameters:'],
297
- ['$0 combine --type=label'],])
298
- .options({
299
- type: {
300
- demand: true,
301
- alias: 'type',
302
- description: 'type of metadata to combine',
303
- demandOption: true,
304
- type: 'string',
305
- },
306
- format: {
307
- demand: true,
308
- alias: 'format',
309
- default: 'yaml',
310
- description: 'type of output',
311
- demandOption: true,
312
- type: 'string',
313
- },
314
- name: {
315
- alias: 'n',
316
- description: 'name of metadata file to combine',
317
- demandOption: false,
318
- type: 'string',
319
- },
320
- all: {
321
- alias: 'a',
322
- description: 'all json files of type will be combined',
323
- demandOption: false,
324
- type: 'boolean',
325
- },
326
- source: {
327
- demand: false,
328
- alias: 's',
329
- description: 'package directory path specified in sfdx-project.json',
330
- type: 'string',
331
- },
332
- target: {
333
- demand: false,
334
- alias: 't',
335
- description: 'target path to directory to create xml files',
336
- type: 'string',
337
- }
338
- })
339
- .choices('type', typeArray)
142
+ .example(yargOptions.combineExamples)
143
+ .options(yargOptions.combineOptions)
340
144
  .choices('format', ['json', 'yaml'])
341
- .check((argv, options) => {
342
- const name = argv.name
343
- const all = argv.all
344
-
345
- switch (argv.type) {
346
- case 'profile':
347
- case 'permset':
348
- case 'workflow':
349
- if ((typeof name != 'undefined' || name == '') && (typeof all != 'undefined' && all)) {
350
- throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' and ' + chalk.whiteBright.bgRedBright('--all') + ' at the same time.'))
351
- } else if (typeof name == 'undefined' && (typeof all == 'undefined' || !all)) {
352
- throw new Error(chalk.redBright('You must specify the ' + chalk.whiteBright.bgRedBright('--name') + ' parameter or use the ' + chalk.whiteBright.bgRedBright('--all') + ' switch.'))
353
- } else {
354
- return true // tell Yargs that the arguments passed the check
355
- }
356
- case 'label':
357
- if ((typeof name != 'undefined' && name != '') || (typeof all != 'undefined' && all)) {
358
- throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' or ' + chalk.whiteBright.bgRedBright('--all') + ' when using label.'))
359
- }
360
- return true
361
- }
362
- })
145
+ .check(yargCheck)
363
146
  },
364
147
  handler: (argv) => {
365
- if (!typeArray.includes(argv.type)) {
366
- global.logger.error('Metadata type not supported: ' + type)
367
- process.exit(1)
368
- }
369
- let processList = []
370
148
  global.format = argv.format
371
- const typeObj = metaTypes[argv.type]
372
- const type = typeObj.type
373
-
374
- let sourceDir = argv.source || ''
375
- let targetDir = argv.target || ''
376
- let name = argv.name
377
- let all = argv.all
378
- let packageDir = getRootPath(sourceDir)
379
-
380
- sourceDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', typeObj.definition.directory)
381
- if (targetDir == '') {
382
- targetDir = path.join(global.__basedir, packageDir, 'main', 'default', typeObj.definition.directory)
383
- } else {
384
- targetDir = path.join(targetDir, 'main', 'default', typeObj.definition.directory)
385
- }
386
-
387
- console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
388
- console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
389
- console.log()
390
-
391
- if (type == metaTypes.label.type) {
392
- processList.push(metaTypes.label.definition.root)
393
- } else if (!all) {
394
- let metaDirPath = path.join(sourceDir, name)
395
- if (!fileUtils.directoryExists(metaDirPath)) {
396
- global.logger.error('Directory not found: ' + metaDirPath)
397
- process.exit(1)
398
- }
399
- processList.push(name)
400
- } else {
401
- processList = fileUtils.getDirectories(sourceDir)
402
- }
403
-
404
- global.processed.total = processList.length
405
- console.log(`Combining a total of ${global.processed.total} file(s)`)
406
- console.log()
407
-
408
- const promList = []
409
- processList.forEach(metaDir => {
410
- const metadataItem = new metadataCombine.Combine({
411
- metadataDefinition: typeObj.definition,
412
- sourceDir: sourceDir,
413
- targetDir: targetDir,
414
- metaDir: metaDir,
415
- sequence: promList.length + 1,
416
- })
417
- const metadataItemProm = metadataItem.combine()
418
- promList.push(metadataItemProm)
419
- metadataItemProm.then((resolve, reject) => {
420
- global.processed.current++
421
- })
422
- })
423
-
424
- Promise.allSettled(promList).then((results) => {
425
- let successes = 0
426
- let errors = 0
427
- results.forEach(result => {
428
- if (result.value == true) {
429
- successes++
430
- } else if (result.value == false) {
431
- errors++
432
- }
433
- })
434
- let message = `Combined ${chalk.bgBlackBright(successes)} file(s) ${(errors > 0) ? 'with ' + chalk.bgBlackBright(errors) + 'error(s) ' : ''}in `
435
- displayMessageAndDuration(startTime, message)
436
- })
149
+ combineHandler(argv, processStartTime)
437
150
  }
438
151
  })
439
152
  .demandCommand(1, errorMessage)
@@ -447,6 +160,35 @@ yargs(hideBin(process.argv))
447
160
  .argv
448
161
  .parse
449
162
 
163
+ function yargCheck(argv, options) {
164
+ const name = argv.name
165
+ types = (argv.type !== undefined) ? argv.type.split(',') : typeArray
166
+ types.forEach(type => {
167
+ type = type.trim()
168
+ if (!typeArray.includes(type)) {
169
+ throw new Error(`Invalid type: ${type}`)
170
+ }
171
+ })
172
+
173
+ if (types.length > 1) {
174
+ // if using multiple types you cannot specify name
175
+ if ((typeof name != 'undefined' && name != '')) {
176
+ throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' when using multiple types.'))
177
+ }
178
+ return true
179
+ } else {
180
+ switch (argv.type) {
181
+ case 'label':
182
+ if ((typeof name != 'undefined' && name != '')) {
183
+ throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' when using label.'))
184
+ }
185
+ break
186
+ default:
187
+ return true
188
+ }
189
+ }
190
+ }
191
+
450
192
  function displayMessageAndDuration(startTime, message) {
451
193
  const diff = process.hrtime.bigint() - BigInt(startTime)
452
194
  let durationMessage
@@ -471,4 +213,215 @@ process.on('SIGINT', function () {
471
213
  }
472
214
 
473
215
  callAmount++;
474
- })
216
+ })
217
+
218
+ function splitHandler(argv) {
219
+ const split = processSplit(types[0], argv)
220
+ split.then((resolve) => {
221
+ types.shift() // remove first item from array
222
+ if (types.length > 0) {
223
+ console.log()
224
+ splitHandler(argv)
225
+ } else {
226
+ if (argv.type === undefined || argv.type.split(',').length > 1) {
227
+ let message = `Split completed in `
228
+ displayMessageAndDuration(startTime, message)
229
+ }
230
+ }
231
+ })
232
+ }
233
+
234
+ function processSplit(typeItem, argv) {
235
+ return new Promise((resolve, reject) => {
236
+ const processed = {
237
+ total: 0,
238
+ errors: 0,
239
+ current: 1,
240
+ }
241
+ const startTime = process.hrtime.bigint()
242
+
243
+ if (!typeArray.includes(typeItem)) {
244
+ global.logger.error('Metadata type not supported: ' + typeItem)
245
+ process.exit(1)
246
+ }
247
+
248
+ const fileList = []
249
+ const typeObj = metaTypes[typeItem]
250
+ const type = typeObj.type
251
+ const metaExtension = `.${type}-meta.xml`
252
+
253
+ let sourceDir = argv.source || ''
254
+ let targetDir = argv.target || ''
255
+ let name = argv.name
256
+ let all = (argv.type === undefined || argv.type.split(',').length > 1) ? true : argv.all
257
+ let packageDir = getRootPath(sourceDir)
258
+
259
+ if (type == metaTypes.label.type) {
260
+ name = metaTypes.label.definition.root
261
+ }
262
+ sourceDir = path.join(global.__basedir, packageDir, 'main', 'default', typeObj.definition.directory)
263
+ if (targetDir == '') {
264
+ targetDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', typeObj.definition.directory)
265
+ } else {
266
+ targetDir = path.join(targetDir, 'main', 'default', typeObj.definition.directory)
267
+ }
268
+ let metaDirPath = sourceDir
269
+
270
+ if (!all) {
271
+ let metaFilePath = path.join(metaDirPath, name)
272
+ if (!fileUtils.fileExists(metaFilePath)) {
273
+ name += metaExtension
274
+ metaFilePath = path.join(metaDirPath, name)
275
+ if (!fileUtils.fileExists(metaFilePath)) {
276
+ global.logger.error('File not found: ' + metaFilePath)
277
+ process.exit(1)
278
+ }
279
+ }
280
+ fileList.push(name)
281
+ } else {
282
+ if (fileUtils.directoryExists(sourceDir)) {
283
+ fileUtils.getFiles(sourceDir, metaExtension).forEach(file => {
284
+ fileList.push(file)
285
+ })
286
+ }
287
+ }
288
+
289
+ processed.total = fileList.length
290
+
291
+ console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
292
+ console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
293
+ console.log()
294
+ console.log(`Splitting a total of ${processed.total} file(s)`)
295
+ console.log()
296
+
297
+ const promList = []
298
+ fileList.forEach(metaFile => {
299
+ const metadataItem = new metadataSplit.Split({
300
+ metadataDefinition: typeObj.definition,
301
+ sourceDir: sourceDir,
302
+ targetDir: targetDir,
303
+ metaFilePath: path.join(sourceDir, metaFile),
304
+ sequence: promList.length + 1,
305
+ total: processed.total,
306
+ })
307
+ const metadataItemProm = metadataItem.split()
308
+ promList.push(metadataItemProm)
309
+ metadataItemProm.then((resolve, reject) => {
310
+ if (resolve == false) {
311
+ processed.errors++
312
+ processed.current--
313
+ } else {
314
+ processed.current++
315
+ }
316
+ })
317
+ })
318
+ Promise.allSettled(promList).then((results) => {
319
+ let message = `Split ${chalk.bgBlackBright((processed.current > promList.length) ? promList.length : processed.current)} file(s) ${(processed.errors > 0) ? 'with ' + chalk.bgBlackBright.red(processed.errors) + ' error(s) ' : ''}in `
320
+ displayMessageAndDuration(startTime, message)
321
+ resolve(true)
322
+ })
323
+ })
324
+ }
325
+
326
+ function combineHandler(argv, startTime) {
327
+ const combine = processCombine(types[0], argv)
328
+ combine.then((resolve) => {
329
+ types.shift() // remove first item from array
330
+ if (types.length > 0) {
331
+ console.log()
332
+ combineHandler(argv, startTime)
333
+ } else {
334
+ if (argv.type === undefined || argv.type.split(',').length > 1) {
335
+ let message = `Split completed in `
336
+ displayMessageAndDuration(startTime, message)
337
+ }
338
+ }
339
+ })
340
+
341
+ }
342
+
343
+ function processCombine(typeItem, argv) {
344
+ return new Promise((resolve, reject) => {
345
+ const processed = {
346
+ total: 0,
347
+ errors: 0,
348
+ current: 1,
349
+ }
350
+ const startTime = process.hrtime.bigint()
351
+
352
+ if (!typeArray.includes(typeItem)) {
353
+ global.logger.error('Metadata type not supported: ' + typeItem)
354
+ process.exit(1)
355
+ }
356
+
357
+ let processList = []
358
+ const typeObj = metaTypes[typeItem]
359
+ const type = typeObj.type
360
+
361
+ let sourceDir = argv.source || ''
362
+ let targetDir = argv.target || ''
363
+ let name = argv.name
364
+ let all = (argv.type === undefined || argv.type.split(',').length > 1) ? true : argv.all
365
+ let packageDir = getRootPath(sourceDir)
366
+
367
+ sourceDir = path.join(global.__basedir, packageDir + '-party', 'main', 'default', typeObj.definition.directory)
368
+ if (targetDir == '') {
369
+ targetDir = path.join(global.__basedir, packageDir, 'main', 'default', typeObj.definition.directory)
370
+ } else {
371
+ targetDir = path.join(targetDir, 'main', 'default', typeObj.definition.directory)
372
+ }
373
+
374
+ console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
375
+ console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
376
+ console.log()
377
+
378
+ if (type == metaTypes.label.type) {
379
+ processList.push(metaTypes.label.definition.root)
380
+ } else if (!all) {
381
+ let metaDirPath = path.join(sourceDir, name)
382
+ if (!fileUtils.directoryExists(metaDirPath)) {
383
+ global.logger.error('Directory not found: ' + metaDirPath)
384
+ process.exit(1)
385
+ }
386
+ processList.push(name)
387
+ } else {
388
+ processList = fileUtils.getDirectories(sourceDir)
389
+ }
390
+
391
+ processed.total = processList.length
392
+ console.log(`Combining a total of ${processed.total} file(s)`)
393
+ console.log()
394
+
395
+ const promList = []
396
+ processList.forEach(metaDir => {
397
+ const metadataItem = new metadataCombine.Combine({
398
+ metadataDefinition: typeObj.definition,
399
+ sourceDir: sourceDir,
400
+ targetDir: targetDir,
401
+ metaDir: metaDir,
402
+ sequence: promList.length + 1,
403
+ total: processed.total,
404
+ })
405
+ const metadataItemProm = metadataItem.combine()
406
+ promList.push(metadataItemProm)
407
+ metadataItemProm.then((resolve, reject) => {
408
+ processed.current++
409
+ })
410
+ })
411
+
412
+ Promise.allSettled(promList).then((results) => {
413
+ let successes = 0
414
+ let errors = 0
415
+ results.forEach(result => {
416
+ if (result.value == true) {
417
+ successes++
418
+ } else if (result.value == false) {
419
+ errors++
420
+ }
421
+ })
422
+ let message = `Combined ${chalk.bgBlackBright(successes)} file(s) ${(errors > 0) ? 'with ' + chalk.bgBlackBright(errors) + 'error(s) ' : ''}in `
423
+ displayMessageAndDuration(startTime, message)
424
+ resolve(true)
425
+ })
426
+ })
427
+ }
@@ -0,0 +1,75 @@
1
+ const options = {
2
+ type: {
3
+ demand: false,
4
+ alias: 'type',
5
+ description: 'type of metadata to $1',
6
+ demandOption: false,
7
+ type: 'string',
8
+ },
9
+ format: {
10
+ demand: true,
11
+ alias: 'format',
12
+ default: 'yaml',
13
+ description: 'type of output',
14
+ demandOption: true,
15
+ type: 'string',
16
+ },
17
+ name: {
18
+ alias: 'n',
19
+ description: 'name of metadata file to $1',
20
+ demandOption: false,
21
+ type: 'string',
22
+ },
23
+ source: {
24
+ demand: false,
25
+ alias: 's',
26
+ description: 'package directory path specified in sfdx-project.json',
27
+ type: 'string',
28
+ },
29
+ target: {
30
+ demand: false,
31
+ alias: 't',
32
+ description: 'target path to directory to create yaml/json files',
33
+ type: 'string',
34
+ }
35
+ }
36
+
37
+ function getOptions(type) {
38
+ let optionObj = {... options}
39
+ Object.keys(optionObj).forEach(key => {
40
+ Object.keys(optionObj[key]).forEach(subKey => {
41
+ if (typeof optionObj[key][subKey] == 'string') {
42
+ optionObj[key][subKey] = optionObj[key][subKey].replaceAll('$1', type)
43
+ }
44
+ })
45
+ })
46
+
47
+ return optionObj
48
+ }
49
+
50
+ export const splitOptions = getOptions('split')
51
+ export const combineOptions = getOptions('combine')
52
+
53
+ const examples = [
54
+ ['$0 $1'],
55
+ ['$0 $1 --type=profile'],
56
+ ['$0 $1 --type="profile,label"'],
57
+ ['$0 $1 --type=permset --name="Permission Set Name"'],
58
+ ['--source=packageDir --target=dir/dir'],
59
+ ['name portion of file: [name].profile-meta.xml'],
60
+ ['Example: --name="Admin" for Admin.profile-meta.xml'],
61
+ ['\nCommands not supporting name or all parameters:'],
62
+ ['$0 $1 --type=label'],
63
+ ]
64
+
65
+ function getExamples(type) {
66
+ let exArr = [...examples]
67
+ exArr.forEach(arrItem => {
68
+ arrItem[0] = arrItem[0].replaceAll('$1', type)
69
+ })
70
+
71
+ return exArr
72
+ }
73
+
74
+ export const splitExamples = getExamples('split')
75
+ export const combineExamples = getExamples('combine')
@@ -8,6 +8,12 @@ import * as xml2js from 'xml2js'
8
8
  import * as fileUtils from '../lib/fileUtils.js'
9
9
 
10
10
  const spinner = cliSpinners['dots']
11
+ const processed = {
12
+ total: 0,
13
+ errors: 0,
14
+ current: 0,
15
+ type: undefined,
16
+ }
11
17
 
12
18
  export class Combine {
13
19
  #type = undefined
@@ -33,6 +39,7 @@ export class Combine {
33
39
  this.targetDir = config.targetDir
34
40
  this.metaDir = config.metaDir
35
41
  this.sequence = config.sequence
42
+ this.total = config.total
36
43
  }
37
44
 
38
45
  get metadataDefinition() {
@@ -102,13 +109,19 @@ export class Combine {
102
109
  })
103
110
 
104
111
  function getXML(that) {
112
+ if (processed.type != that.#root) {
113
+ processed.current = 0
114
+ processed.type = that.#root
115
+ }
116
+ processed.current++
117
+
105
118
  that.#startTime = process.hrtime.bigint()
106
- that.#spinnerMessage = `[%1] of ${global.processed.total} - ${that.#root}: [%4]${chalk.yellowBright('[%5]')}[%2][%3]`
119
+ that.#spinnerMessage = `[%1] of ${that.total} - ${that.#root}: [%4]${chalk.yellowBright('[%5]')}[%2][%3]`
107
120
 
108
121
  that.#types.forEach(key => {
109
122
  // display message
110
123
  logUpdate(that.#spinnerMessage
111
- .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
124
+ .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
112
125
  .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
113
126
  .replace('[%3]', `${that.#errorMessage}`)
114
127
  .replace('[%4]', `${global.icons.working} `)
@@ -157,7 +170,7 @@ export class Combine {
157
170
  // iterate over fileList
158
171
  fileList.forEach((file, index) => {
159
172
  logUpdate(that.#spinnerMessage
160
- .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
173
+ .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
161
174
  .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key} - ${index + 1} of ${fileList.length} - ${chalk.magentaBright(file)}`)
162
175
  .replace('[%3]', `${that.#errorMessage}`)
163
176
  .replace('[%4]', `${global.icons.working} `)
@@ -292,7 +305,7 @@ export class Combine {
292
305
  let stateIcon = (that.#errorMessage == '') ? global.icons.success : global.icons.fail
293
306
 
294
307
  logUpdate(that.#spinnerMessage
295
- .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
308
+ .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
296
309
  .replace('[%2]', `. Processed in ${durationMessage}.`)
297
310
  .replace('[%3]', `${that.#errorMessage}`)
298
311
  .replace('[%4]', `${stateIcon} `)
@@ -12,6 +12,12 @@ import cliSpinners from 'cli-spinners'
12
12
  import * as fileUtils from '../lib/fileUtils.js'
13
13
 
14
14
  const spinner = cliSpinners['dots']
15
+ const processed = {
16
+ total: 0,
17
+ errors: 0,
18
+ current: 0,
19
+ type: undefined,
20
+ }
15
21
 
16
22
  export class Split {
17
23
  #type = undefined
@@ -33,6 +39,7 @@ export class Split {
33
39
  this.targetDir = config.targetDir
34
40
  this.metaFilePath = config.metaFilePath
35
41
  this.sequence = config.sequence
42
+ this.total = config.total
36
43
  }
37
44
 
38
45
  get metadataDefinition() {
@@ -117,13 +124,18 @@ export class Split {
117
124
  })
118
125
 
119
126
  function processJSON(that, json, baseDir) {
120
- that.#spinnerMessage = `[%1] of ${global.processed.total} - Workflow: [%4]${chalk.yellowBright(that.#fileName.shortName)}[%2][%3]`
127
+ that.#spinnerMessage = `[%1] of ${that.total} - ${that.#root}: [%4]${chalk.yellowBright(that.#fileName.shortName)}[%2][%3]`
121
128
 
122
129
  let targetDir = baseDir
130
+ if (processed.type != that.#root) {
131
+ processed.current = 0
132
+ processed.type = that.#root
133
+ }
134
+ processed.current++
123
135
  Object.keys(json).forEach(key => {
124
- that.sequence = global.processed.current
136
+ that.sequence = processed.current
125
137
  logUpdate(that.#spinnerMessage
126
- .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
138
+ .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
127
139
  .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
128
140
  .replace('[%3]', `${that.#errorMessage}`)
129
141
  .replace('[%4]', `${global.icons.working} `)
@@ -157,9 +169,9 @@ export class Split {
157
169
  }
158
170
  mainInfo.main.name = that.#fileName.shortName
159
171
  that.metadataDefinition.main.forEach(key => {
160
- that.sequence = global.processed.current
172
+ that.sequence = processed.current
161
173
  logUpdate(that.#spinnerMessage
162
- .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
174
+ .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
163
175
  .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
164
176
  .replace('[%3]', `${that.#errorMessage}`)
165
177
  .replace('[%4]', `${global.icons.working} `)
@@ -182,7 +194,7 @@ export class Split {
182
194
  let durationMessage = `${executionTime.seconds}.${executionTime.milliseconds}s`
183
195
  let stateIcon = (that.#errorMessage == '') ? global.icons.success : global.icons.fail
184
196
  logUpdate(that.#spinnerMessage
185
- .replace('[%1]', that.sequence.toString().padStart(global.processed.total.toString().length, ' '))
197
+ .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
186
198
  .replace('[%2]', `. Processed in ${durationMessage}.`)
187
199
  .replace('[%3]', `${that.#errorMessage}`)
188
200
  .replace('[%4]', `${stateIcon} `)