@ds-sfdc/sfparty 1.3.4 → 1.3.5

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
@@ -159,3 +159,10 @@ Optional:
159
159
  ```
160
160
  sfparty combine --git=HEAD~1..HEAD --append --delta --package=deploy/package.xml --destructive=deploy/destructiveChanges/destructiveChanges.xml
161
161
  ```
162
+ #### Previous Commit to Current and output to different target directory
163
+
164
+ The default target is the package file specified in the `sfdx-project.json` file. You can use the `--target` parameter if you want the files to be created in a different location. sfparty will create the /main/default/* directories accordingly.
165
+
166
+ ```
167
+ sfparty combine --git=HEAD~1..HEAD --append --delta --package=deploy/package.xml --destructive=deploy/destructiveChanges/destructiveChanges.xml --target=deployDir/force-app
168
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-sfdc/sfparty",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Salesforce metadata XML splitter for CI/CD",
5
5
  "type": "module",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "axios": "^1.2.2",
27
27
  "chai-as-promised": "^7.1.1",
28
- "chalk": "^5.2.0",
28
+ "cli-color": "^2.0.3",
29
29
  "cli-spinners": "^2.7.0",
30
30
  "convert-hrtime": "^5.0.0",
31
31
  "js-yaml": "^4.1.0",
package/src/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
- import { exec } from 'child_process'
3
+ import { exec, spawnSync } from 'child_process'
4
4
  import { readFileSync } from 'fs'
5
5
  import path from 'path'
6
6
  import yargs from 'yargs'
7
7
  import { hideBin } from 'yargs/helpers'
8
8
  import winston from 'winston'
9
- import chalk from 'chalk'
9
+ import clc from 'cli-color'
10
10
  import convertHrtime from 'convert-hrtime'
11
11
  import axios from 'axios'
12
12
  import { marked } from 'marked'
@@ -51,7 +51,7 @@ global.logger = winston.createLogger({
51
51
 
52
52
  global.icons = {
53
53
  "warn": '🔕',
54
- "success": chalk.greenBright('✔'),
54
+ "success": clc.greenBright('✔'),
55
55
  "fail": '❗',
56
56
  "working": '⏳',
57
57
  "party": '🎉',
@@ -110,7 +110,7 @@ global.metaTypes = {
110
110
  let types = []
111
111
  const packageDir = getRootPath()
112
112
 
113
- let errorMessage = chalk.red('Please specify the action of ' + chalk.whiteBright.bgRedBright('split') + ' or ' + chalk.whiteBright.bgRedBright('combine') + '.')
113
+ let errorMessage = clc.red('Please specify the action of ' + clc.whiteBright.bgRedBright('split') + ' or ' + clc.whiteBright.bgRedBright('combine') + '.')
114
114
 
115
115
  displayHeader() // display header mast
116
116
 
@@ -128,7 +128,7 @@ yargs(hideBin(process.argv))
128
128
  alias: 'test',
129
129
  handler: (argv) => {
130
130
  // THIS IS A PLACE TO TEST NEW CODE
131
- global.logger.info(chalk.magentaBright(`${global.icons.party} TEST ${global.icons.party}`))
131
+ global.logger.info(clc.magentaBright(`${global.icons.party} TEST ${global.icons.party}`))
132
132
  }
133
133
  })
134
134
  .command({
@@ -139,7 +139,7 @@ yargs(hideBin(process.argv))
139
139
  .check(yargCheck)
140
140
  },
141
141
  handler: (argv) => {
142
- checkVersion(axios, exec, pkgObj.default.version, true)
142
+ checkVersion(axios, spawnSync, pkgObj.default.version, true)
143
143
  }
144
144
  })
145
145
  .command({
@@ -154,7 +154,7 @@ yargs(hideBin(process.argv))
154
154
  .check(yargCheck)
155
155
  },
156
156
  handler: (argv) => {
157
- checkVersion(axios, exec, pkgObj.default.version)
157
+ checkVersion(axios, spawnSync, pkgObj.default.version)
158
158
  global.format = argv.format
159
159
  splitHandler(argv, processStartTime)
160
160
  }
@@ -171,7 +171,7 @@ yargs(hideBin(process.argv))
171
171
  .check(yargCheck)
172
172
  },
173
173
  handler: (argv) => {
174
- checkVersion(axios, exec, pkgObj.default.version)
174
+ checkVersion(axios, spawnSync, pkgObj.default.version)
175
175
  global.format = argv.format
176
176
  const startProm = new Promise((resolve, reject) => {
177
177
  if (argv.git !== undefined) {
@@ -185,10 +185,10 @@ yargs(hideBin(process.argv))
185
185
  global.git.latest = data.latestCommit
186
186
  global.git.last = data.lastCommit
187
187
  if (data.last === undefined) {
188
- console.log(`${chalk.yellowBright('git mode')} ${chalk.bgMagentaBright('not active:')} no prior commit - processing all`)
188
+ console.log(`${clc.yellowBright('git mode')} ${clc.bgMagentaBright('not active:')} no prior commit - processing all`)
189
189
  resolve(false)
190
190
  } else {
191
- console.log(`${chalk.yellowBright('git mode')} ${chalk.magentaBright('active:')} ${chalk.bgBlackBright(data.lastCommit) + '..' + chalk.bgBlackBright(data.latestCommit)}`)
191
+ console.log(`${clc.yellowBright('git mode')} ${clc.magentaBright('active:')} ${clc.bgBlackBright(data.lastCommit) + '..' + clc.bgBlackBright(data.latestCommit)}`)
192
192
  console.log()
193
193
  const diff = git.diff(global.__basedir, `${data.lastCommit}..${data.latestCommit}`)
194
194
  diff
@@ -207,7 +207,7 @@ yargs(hideBin(process.argv))
207
207
  throw error
208
208
  })
209
209
  } else {
210
- console.log(`${chalk.yellowBright('git mode')} ${chalk.magentaBright('active:')} ${chalk.bgBlackBright(gitRef)}`)
210
+ console.log(`${clc.yellowBright('git mode')} ${clc.magentaBright('active:')} ${clc.bgBlackBright(gitRef)}`)
211
211
  console.log()
212
212
  const diff = git.diff(global.__basedir, gitRef)
213
213
  diff
@@ -254,7 +254,7 @@ function yargCheck(argv, options) {
254
254
  )
255
255
 
256
256
  if (invalidKeys.length > 0) {
257
- const invalidKeysWithColor = invalidKeys.map(key => chalk.redBright(key))
257
+ const invalidKeysWithColor = invalidKeys.map(key => clc.redBright(key))
258
258
  throw new Error(`Invalid options specified: ${invalidKeysWithColor.join(', ')}`)
259
259
  }
260
260
 
@@ -270,13 +270,13 @@ function yargCheck(argv, options) {
270
270
  if (types.length > 1) {
271
271
  // if using multiple types you cannot specify name
272
272
  if ((typeof name != 'undefined' && name != '')) {
273
- throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' when using multiple types.'))
273
+ throw new Error(clc.redBright('You cannot specify ' + clc.whiteBright.bgRedBright('--name') + ' when using multiple types.'))
274
274
  }
275
275
  } else {
276
276
  switch (argv.type) {
277
277
  case 'label':
278
278
  if ((typeof name != 'undefined' && name != '')) {
279
- throw new Error(chalk.redBright('You cannot specify ' + chalk.whiteBright.bgRedBright('--name') + ' when using label.'))
279
+ throw new Error(clc.redBright('You cannot specify ' + clc.whiteBright.bgRedBright('--name') + ' when using label.'))
280
280
  }
281
281
  break
282
282
  }
@@ -291,11 +291,11 @@ function displayMessageAndDuration(startTime, message) {
291
291
  let minutes = Math.floor((executionTime.seconds + Math.round(executionTime.milliseconds / 100000)) / 60)
292
292
  let seconds = Math.round((executionTime.seconds + Math.round(executionTime.milliseconds / 100000)) % 60)
293
293
  if (minutes == 0 && seconds == 0) {
294
- durationMessage = message + chalk.magentaBright(`<1s`)
294
+ durationMessage = message + clc.magentaBright(`<1s`)
295
295
  } else if (minutes > 0) {
296
- durationMessage = message + chalk.magentaBright(`${minutes}m ${seconds}s`)
296
+ durationMessage = message + clc.magentaBright(`${minutes}m ${seconds}s`)
297
297
  } else {
298
- durationMessage = message + chalk.magentaBright(`${seconds}s`)
298
+ durationMessage = message + clc.magentaBright(`${seconds}s`)
299
299
  }
300
300
  console.log('\n' + durationMessage)
301
301
  }
@@ -384,8 +384,8 @@ function processSplit(typeItem, argv) {
384
384
 
385
385
  if (processed.total == 0) resolve(true)
386
386
 
387
- console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
388
- console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
387
+ console.log(`${clc.bgBlackBright('Source path:')} ${sourceDir}`)
388
+ console.log(`${clc.bgBlackBright('Target path:')} ${targetDir}`)
389
389
  console.log()
390
390
  console.log(`Splitting a total of ${processed.total} file(s)`)
391
391
  console.log()
@@ -412,7 +412,7 @@ function processSplit(typeItem, argv) {
412
412
  })
413
413
  })
414
414
  Promise.allSettled(promList).then((results) => {
415
- 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 `
415
+ let message = `Split ${clc.bgBlackBright((processed.current > promList.length) ? promList.length : processed.current)} file(s) ${(processed.errors > 0) ? 'with ' + clc.bgBlackBright.red(processed.errors) + ' error(s) ' : ''}in `
416
416
  displayMessageAndDuration(startTime, message)
417
417
  resolve(true)
418
418
  })
@@ -496,7 +496,7 @@ function processCombine(typeItem, argv) {
496
496
  }
497
497
 
498
498
  processed.total = processList.length
499
- console.log(`${chalk.bgBlackBright(processed.total)} ${typeItem} file(s) to process`)
499
+ console.log(`${clc.bgBlackBright(processed.total)} ${typeItem} file(s) to process`)
500
500
 
501
501
  // Abort if there are no files to process
502
502
  if (processed.total == 0) {
@@ -505,8 +505,8 @@ function processCombine(typeItem, argv) {
505
505
  }
506
506
 
507
507
  console.log()
508
- console.log(`${chalk.bgBlackBright('Source path:')} ${sourceDir}`)
509
- console.log(`${chalk.bgBlackBright('Target path:')} ${targetDir}`)
508
+ console.log(`${clc.bgBlackBright('Source path:')} ${sourceDir}`)
509
+ console.log(`${clc.bgBlackBright('Target path:')} ${targetDir}`)
510
510
  console.log()
511
511
 
512
512
  const promList = []
@@ -538,7 +538,7 @@ function processCombine(typeItem, argv) {
538
538
  errors++
539
539
  }
540
540
  })
541
- let message = `Combined ${chalk.bgBlackBright(successes)} file(s) ${(errors > 0) ? 'with ' + chalk.bgBlackBright(errors) + 'error(s) ' : ''}in `
541
+ let message = `Combined ${clc.bgBlackBright(successes)} file(s) ${(errors > 0) ? 'with ' + clc.bgBlackBright(errors) + 'error(s) ' : ''}in `
542
542
  displayMessageAndDuration(startTime, message)
543
543
  resolve(true)
544
544
  })
@@ -603,13 +603,13 @@ function displayHeader() {
603
603
  vertical: '│',
604
604
  }
605
605
  let versionString = `sfparty v${pkgObj.default.version}${(process.stdout.columns > pkgObj.default.description.length + 15) ? ' - ' + pkgObj.default.description : ''}`
606
- let titleMessage = `${global.icons.party} ${chalk.yellowBright(versionString)} ${global.icons.party}`
606
+ let titleMessage = `${global.icons.party} ${clc.yellowBright(versionString)} ${global.icons.party}`
607
607
  titleMessage = titleMessage.padEnd((process.stdout.columns / 2) + versionString.length / 1.65)
608
608
  titleMessage = titleMessage.padStart(process.stdout.columns)
609
- titleMessage = chalk.blackBright(box.vertical) + ' ' + titleMessage + ' ' + chalk.blackBright(box.vertical)
610
- console.log(`${chalk.blackBright(box.topLeft + box.horizontal.repeat(process.stdout.columns - 2) + box.topRight)}`)
609
+ titleMessage = clc.blackBright(box.vertical) + ' ' + titleMessage + ' ' + clc.blackBright(box.vertical)
610
+ console.log(`${clc.blackBright(box.topLeft + box.horizontal.repeat(process.stdout.columns - 2) + box.topRight)}`)
611
611
  console.log(titleMessage)
612
- console.log(`${chalk.blackBright(box.bottomLeft + box.horizontal.repeat(process.stdout.columns - 2) + box.bottomRight)}`)
612
+ console.log(`${clc.blackBright(box.bottomLeft + box.horizontal.repeat(process.stdout.columns - 2) + box.bottomRight)}`)
613
613
  console.log()
614
614
  }
615
615
 
@@ -1,42 +1,78 @@
1
- export async function checkVersion(axios, exec, currentVersion, update = false) {
1
+ import clc from 'cli-color'
2
+
3
+ class NpmNotInstalledError extends Error {
4
+ constructor(message) {
5
+ super(message)
6
+ this.name = "NpmNotInstalledError"
7
+ }
8
+ }
9
+
10
+ class PackageNotFoundError extends Error {
11
+ constructor(message) {
12
+ super(message)
13
+ this.name = "PackageNotFoundError"
14
+ }
15
+ }
16
+
17
+ class UpdateError extends Error {
18
+ constructor(message) {
19
+ super(message)
20
+ this.name = "UpdateError"
21
+ }
22
+ }
23
+
24
+ export async function checkVersion(axios, spawnSync, currentVersion, update = false) {
2
25
  try {
3
- const { data } = await axios.get('https://registry.npmjs.org/@ds-sfdc/sfparty')
4
- const command = 'npm i -g @ds-sfdc/sfparty@latest'
26
+ const { data } = await axios.get('https://registry.npmjs.org/@ds-sfdc/sfparty', {
27
+ params: {
28
+ field: 'dist-tags.latest'
29
+ }
30
+ })
5
31
  if (currentVersion !== data['dist-tags'].latest) {
6
- console.log(`${(update) ? global.icons.working : global.icons.fail} A newer version ${chalk.bgCyanBright(data['dist-tags'].latest)} is available.`)
32
+ let icon
33
+ const version = clc.bgCyanBright(data['dist-tags'].latest)
34
+ if (update) {
35
+ icon = global.icons.working
36
+ } else {
37
+ icon = global.icons.fail
38
+ }
39
+ console.log(`${icon} A newer version ${version} is available.`)
7
40
  if (!update) {
8
- console.log(`Please upgrade by running ${chalk.cyanBright('sfparty update')}`)
41
+ console.log(`Please upgrade by running ${clc.cyanBright('sfparty update')}`)
9
42
  return 'A newer version'
10
43
  } else {
11
- console.log(`Updating the application using ${chalk.cyanBright(command)}`)
12
- exec('npm -v', (error, stdout, stderr) => {
13
- if (error) {
14
- global.logger.error("npm is not installed on this system. Please install npm and run the command again.")
15
- return 'npm is not installed'
16
- } else {
17
- exec(command, (error, stdout, stderr) => {
18
- if (error) {
19
- global.logger.error(error)
20
- reject(error)
21
- } else {
22
- console.log(stdout)
23
- console.log(stderr)
24
- resolve(true)
25
- }
26
- })
44
+ let command = 'npm i -g @ds-sfdc/sfparty'.split(' ')
45
+ console.log(`Updating the application using ${clc.cyanBright(command.join(' '))}`)
46
+ try {
47
+ const npmVersion = spawnSync('npm', ['-v'])
48
+ if (npmVersion.stderr.toString().trim() === 'command not found') {
49
+ throw new NpmNotInstalledError("npm is not installed on this system. Please install npm and run the command again.")
50
+ }
51
+ const update = spawnSync(command[0], command.slice(1))
52
+ if (update.status !== 0) {
53
+ throw new UpdateError("Error updating the application.")
27
54
  }
28
- })
55
+ console.log("Application updated successfully.")
56
+ } catch (err) {
57
+ if (err instanceof NpmNotInstalledError) {
58
+ console.error(err)
59
+ } else if (err instanceof UpdateError) {
60
+ console.error(err)
61
+ }
62
+ throw err
63
+ }
29
64
  }
30
65
  } else {
31
66
  if (update) {
32
67
  console.log(`${global.icons.success} You are on the latest version.`)
33
- return 'You are on the latest version'
34
68
  }
69
+ return 'You are on the latest version'
35
70
  }
36
71
  } catch (error) {
37
- global.logger.error(error)
72
+ if (error.response && error.response.status === 404) {
73
+ error = new PackageNotFoundError("Package not found on the npm registry")
74
+ }
75
+ console.error(error)
38
76
  throw error
39
77
  }
40
- }
41
-
42
-
78
+ }
@@ -133,6 +133,7 @@ async function convertXML(data) {
133
133
  try {
134
134
  let parser = new Parser()
135
135
  parser.parseString(data, function (err, result) {
136
+ if (err) throw err
136
137
  resolve(result)
137
138
  })
138
139
  } catch (error) {
@@ -1,7 +1,4 @@
1
1
  import path from 'path'
2
- import chalk from 'chalk'
3
- import * as xml2js from 'xml2js'
4
- import * as fileUtils from './fileUtils.js'
5
2
  import * as packageDefinition from '../meta/Package.js'
6
3
 
7
4
  export class Package {
@@ -12,30 +9,45 @@ export class Package {
12
9
  this.packageJSON = undefined
13
10
  }
14
11
 
15
- getPackageXML() {
12
+ getPackageXML(fileUtils) {
16
13
  const that = this
17
14
  return new Promise((resolve, reject) => {
18
- if (that.xmlPath === undefined) throw new Error('Package not initialized')
19
-
20
- let fileName = path.resolve(that.xmlPath)
21
- if (fileUtils.fileExists(fileName) && global.git.append) {
22
- let data = fileUtils.readFile(fileName)
23
- data
24
- .then((json) => {
15
+ try {
16
+ if (that.xmlPath === undefined) throw new Error('Package not initialized')
17
+
18
+ let fileName = path.resolve(that.xmlPath)
19
+ if (fileUtils.fileExists(fileName) && global.git.append) {
20
+ let data = fileUtils.readFile(fileName)
21
+ data
22
+ .then((json) => {
23
+ try {
24
+ if (json === undefined || Object.keys(json).length === 0 ) json = packageDefinition.metadataDefinition.emptyPackage
25
+ processJSON(that, json, fileUtils)
26
+ resolve('existing')
27
+ } catch (error) {
28
+ console.error(error)
29
+ reject(error)
30
+ }
31
+ })
32
+ .catch((error) => {
33
+ reject(error)
34
+ })
35
+ } else {
36
+ try {
37
+ let json = JSON.parse(JSON.stringify(packageDefinition.metadataDefinition.emptyPackage))
25
38
  processJSON(that, json)
26
- resolve('existing')
27
- })
28
- .catch((error) => {
39
+ resolve('not found')
40
+ } catch (error) {
29
41
  reject(error)
30
- })
31
- } else {
32
- let json = JSON.parse(JSON.stringify(packageDefinition.metadataDefinition.emptyPackage))
33
- processJSON(that, json)
34
- resolve('not found')
42
+ }
43
+ }
44
+ } catch (error) {
45
+ reject(error)
35
46
  }
47
+
36
48
  })
37
49
 
38
- function processJSON(that, json) {
50
+ function processJSON(that, json, fileUtils) {
39
51
  try {
40
52
  json.Package.version = fileUtils.readFile(path.join(global.__basedir, 'sfdx-project.json')).sourceApiVersion
41
53
  } catch (error) {
@@ -49,8 +61,9 @@ export class Package {
49
61
  addMember(type, member) {
50
62
  const that = this
51
63
  if (that.packageJSON === undefined) throw new Error('getPackageXML must be called before adding members')
52
- if (type === undefined) throw new Error('An undefined type was received when attempting to add a member')
53
- if (member === undefined) throw new Error('An undefined member was received when attempting to add a member')
64
+ if (type === undefined || type.trim() == '') throw new Error('An undefined type was received when attempting to add a member')
65
+ if (member === undefined || member.trim() == '') throw new Error('An undefined member was received when attempting to add a member')
66
+ if (member.indexOf(`.${global.format}`) !== -1) throw new Error('Part file received as member is not allowed')
54
67
 
55
68
  const packageJSON = that.packageJSON
56
69
  let foundMember = false
@@ -77,82 +90,99 @@ export class Package {
77
90
  })
78
91
  }
79
92
  } catch (error) {
80
- global.displayError(error, true)
93
+ throw error
81
94
  }
82
95
  })
83
96
 
84
97
  // exit if member already exists
85
98
  if (foundMember) return
86
99
  if (foundAsterisk) {
87
- // global.logger.warn(`Found ${chalk.bgBlackBright('*')} in type: ${type}.`)
88
100
  return
89
101
  }
90
102
 
91
- if (typeJSON !== undefined) {
92
- typeJSON.members.push(member)
93
- typeJSON.members.sort()
94
- } else {
95
- typeJSON = JSON.parse(JSON.stringify(packageDefinition.metadataDefinition.emptyNode))
96
- typeJSON.name = type
97
- typeJSON.members.push(member)
98
-
99
- packageJSON.Package.types.push(typeJSON)
103
+ try {
104
+ if (typeJSON !== undefined) {
105
+ typeJSON.members.push(member)
106
+ typeJSON.members.sort()
107
+ } else {
108
+ typeJSON = JSON.parse(JSON.stringify(packageDefinition.metadataDefinition.emptyNode))
109
+ typeJSON.name = type
110
+ typeJSON.members.push(member)
111
+
112
+ packageJSON.Package.types.push(typeJSON)
113
+ }
114
+
115
+ packageJSON.Package.types.sort((a, b) => {
116
+ if (a.name < b.name) return -1
117
+ if (a.name > b.name) return 1
118
+ return 0
119
+ })
120
+ } catch (error) {
121
+ throw error
100
122
  }
101
-
102
- packageJSON.Package.types.sort((a, b) => {
103
- if (a.name < b.name) return -1
104
- if (a.name > b.name) return 1
105
- return 0
106
- })
107
123
  }
108
124
 
109
- savePackage() {
125
+ savePackage(xml2js, fileUtils) {
110
126
  let that = this
111
127
  let json = that.packageJSON.Package
112
- json.$.xmlns = json.$.xmlns.replace('http:', 'https:')
113
-
114
- const builder = new xml2js.Builder(
115
- {
116
- cdata: false,
117
- rootName: 'Package',
118
- xmldec: { 'version': '1.0', 'encoding': 'UTF-8' }
119
- }
120
- )
121
- let fileName = that.xmlPath
122
- fileUtils.createDirectory(path.dirname(fileName))
123
-
124
- const xml = builder.buildObject(json)
125
-
126
- fileUtils.writeFile(fileName, xml)
128
+ try {
129
+ json.$.xmlns = json.$.xmlns.replace('http:', 'https:')
130
+ const version = json.version
131
+ delete json.version
132
+ json.version = version
133
+
134
+ const builder = new xml2js.Builder(
135
+ {
136
+ cdata: false,
137
+ rootName: 'Package',
138
+ xmldec: { 'version': '1.0', 'encoding': 'UTF-8' }
139
+ }
140
+ )
141
+ let fileName = that.xmlPath
142
+ fileUtils.createDirectory(path.dirname(fileName))
143
+
144
+ const xml = builder.buildObject(json)
145
+
146
+ fileUtils.writeFile(fileName, xml)
147
+
148
+ } catch (error) {
149
+ throw error
150
+ }
127
151
  }
128
152
  }
129
153
 
130
154
  function transformJSON(json) {
131
- json.forEach(typesItem => {
132
- Object.keys(typesItem).forEach(key => {
133
- let jsonString = JSON.stringify(typesItem[key], (name, value) => {
134
- if (key == 'members') {
135
- return value
136
- } else {
137
- return xml2json(value)
138
- }
155
+ try {
156
+ json.forEach(typesItem => {
157
+ Object.keys(typesItem).forEach(key => {
158
+ let jsonString = JSON.stringify(typesItem[key], (name, value) => {
159
+ if (key == 'members') {
160
+ return value
161
+ } else {
162
+ return xml2json(value)
163
+ }
164
+ })
165
+ typesItem[key] = JSON.parse(jsonString)
139
166
  })
140
- typesItem[key] = JSON.parse(jsonString)
141
167
  })
142
- })
143
-
144
- return
145
- }
168
+
169
+ return
170
+ } catch (error) {
171
+ throw error
172
+ }
146
173
 
147
- function xml2json(currentValue) {
148
- if (Array.isArray(currentValue)) {
149
- if (currentValue.length == 1) {
150
- currentValue = currentValue[0].toString().trim()
174
+ function xml2json(currentValue) {
175
+ try {
176
+ if (Array.isArray(currentValue)) {
177
+ if (currentValue.length == 1) {
178
+ currentValue = currentValue[0].toString().trim()
179
+ }
180
+ }
181
+ if (currentValue == 'true') currentValue = true
182
+ if (currentValue == 'false') currentValue = false
183
+ return currentValue
184
+ } catch (error) {
185
+ throw error
151
186
  }
152
187
  }
153
- if (currentValue == 'true') currentValue = true
154
- if (currentValue == 'false') currentValue = false
155
- return currentValue
156
188
  }
157
-
158
- // Create JEST tests that cover 100% of the code
@@ -1,6 +1,6 @@
1
1
  import path from 'path'
2
2
  import logUpdate from 'log-update'
3
- import chalk from 'chalk'
3
+ import clc from 'cli-color'
4
4
  import convertHrtime from 'convert-hrtime'
5
5
  import cliSpinners from 'cli-spinners'
6
6
  import os from 'node:os'
@@ -110,8 +110,8 @@ export class Combine {
110
110
  if (global.git.enabled) {
111
111
  that.#addPkg = new packageUtil.Package(that.addManifest)
112
112
  that.#desPkg = new packageUtil.Package(that.desManifest)
113
- const prom1 = that.#addPkg.getPackageXML()
114
- const prom2 = that.#desPkg.getPackageXML()
113
+ const prom1 = that.#addPkg.getPackageXML(fileUtils)
114
+ const prom2 = that.#desPkg.getPackageXML(fileUtils)
115
115
 
116
116
  Promise.allSettled([prom1, prom2])
117
117
  .then((results) => {
@@ -139,7 +139,7 @@ export class Combine {
139
139
  } else {
140
140
  logUpdate(that.#spinnerMessage
141
141
  .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
142
- .replace('[%2]', `. ${chalk.redBright('source not found - removing XML file')}`)
142
+ .replace('[%2]', `. ${clc.redBright('source not found - removing XML file')}`)
143
143
  .replace('[%3]', ``)
144
144
  .replace('[%4]', `${global.icons.delete} `)
145
145
  .replace('[%5]', that.#fileName.shortName)
@@ -155,8 +155,8 @@ export class Combine {
155
155
  }
156
156
 
157
157
  function savePackageXML(that) {
158
- that.#addPkg.savePackage()
159
- that.#desPkg.savePackage()
158
+ that.#addPkg.savePackage(xml2js, fileUtils)
159
+ that.#desPkg.savePackage(xml2js, fileUtils)
160
160
  }
161
161
 
162
162
  function getXML(that) {
@@ -167,14 +167,14 @@ export class Combine {
167
167
  processed.current++
168
168
 
169
169
  that.#startTime = process.hrtime.bigint()
170
- that.#spinnerMessage = `[%1] of ${that.total} - ${that.#root}: [%4]${chalk.yellowBright('[%5]')}[%2][%3]`
170
+ that.#spinnerMessage = `[%1] of ${that.total} - ${that.#root}: [%4]${clc.yellowBright('[%5]')}[%2][%3]`
171
171
 
172
172
  try {
173
173
  that.#types.forEach(key => {
174
174
  // display message
175
175
  logUpdate(that.#spinnerMessage
176
176
  .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
177
- .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
177
+ .replace('[%2]', `\n${clc.magentaBright(nextFrame(that))} ${key}`)
178
178
  .replace('[%3]', `${that.#errorMessage}`)
179
179
  .replace('[%4]', `${global.icons.working} `)
180
180
  .replace('[%5]', `${that.#fileName.shortName} `)
@@ -199,7 +199,7 @@ export class Combine {
199
199
  } else if (that.metadataDefinition.directories.includes(key)) {
200
200
  processDirectory(that, key)
201
201
  } else {
202
- global.logger.warn(`Unexpected metadata type: ${chalk.redBright(key)}`)
202
+ global.logger.warn(`Unexpected metadata type: ${clc.redBright(key)}`)
203
203
  }
204
204
  })
205
205
  return true
@@ -234,7 +234,7 @@ export class Combine {
234
234
  fileList.forEach((file, index) => {
235
235
  logUpdate(that.#spinnerMessage
236
236
  .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
237
- .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key} - ${index + 1} of ${fileList.length} - ${chalk.magentaBright(file)}`)
237
+ .replace('[%2]', `\n${clc.magentaBright(nextFrame(that))} ${key} - ${index + 1} of ${fileList.length} - ${clc.magentaBright(file)}`)
238
238
  .replace('[%3]', `${that.#errorMessage}`)
239
239
  .replace('[%4]', `${global.icons.working} `)
240
240
  .replace('[%5]', `${that.#fileName.shortName} `)
@@ -263,7 +263,7 @@ export class Combine {
263
263
  fileObj.shortName === undefined ||
264
264
  fileObj.fullName === undefined
265
265
  ) {
266
- global.displayError(`${global.icons.warn} Invalid file information passed ${chalk.redBright(fileObj)}`, true)
266
+ global.displayError(`${global.icons.warn} Invalid file information passed ${clc.redBright(fileObj)}`, true)
267
267
  }
268
268
 
269
269
  if (!fileUtils.fileExists(fileObj.fullName)) {
@@ -4,7 +4,7 @@ import path from 'path'
4
4
  import { readFile } from 'fs'
5
5
  import { Parser } from 'xml2js'
6
6
  import logUpdate from 'log-update'
7
- import chalk from 'chalk'
7
+ import clc from 'cli-color'
8
8
  import convertHrtime from 'convert-hrtime'
9
9
  import cliSpinners from 'cli-spinners'
10
10
  import * as fileUtils from '../lib/fileUtils.js'
@@ -128,7 +128,7 @@ export class Split {
128
128
  })
129
129
 
130
130
  function processJSON(that, json, baseDir) {
131
- that.#spinnerMessage = `[%1] of ${that.total} - ${that.#root}: [%4]${chalk.yellowBright(that.#fileName.shortName)}[%2][%3]`
131
+ that.#spinnerMessage = `[%1] of ${that.total} - ${that.#root}: [%4]${clc.yellowBright(that.#fileName.shortName)}[%2][%3]`
132
132
 
133
133
  let targetDir = baseDir
134
134
  if (processed.type != that.#root) {
@@ -140,7 +140,7 @@ export class Split {
140
140
  that.sequence = processed.current
141
141
  logUpdate(that.#spinnerMessage
142
142
  .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
143
- .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
143
+ .replace('[%2]', `\n${clc.magentaBright(nextFrame(that))} ${key}`)
144
144
  .replace('[%3]', `${that.#errorMessage}`)
145
145
  .replace('[%4]', `${global.icons.working} `)
146
146
  )
@@ -176,7 +176,7 @@ export class Split {
176
176
  that.sequence = processed.current
177
177
  logUpdate(that.#spinnerMessage
178
178
  .replace('[%1]', that.sequence.toString().padStart(that.total.toString().length, ' '))
179
- .replace('[%2]', `\n${chalk.magentaBright(nextFrame(that))} ${key}`)
179
+ .replace('[%2]', `\n${clc.magentaBright(nextFrame(that))} ${key}`)
180
180
  .replace('[%3]', `${that.#errorMessage}`)
181
181
  .replace('[%4]', `${global.icons.working} `)
182
182
  )
@@ -0,0 +1,69 @@
1
+ import axios from 'axios'
2
+ import { spawnSync } from 'child_process'
3
+ import clc from 'cli-color'
4
+ import { checkVersion } from '../src/lib/checkVersion.js'
5
+
6
+ global.icons = {
7
+ "success": clc.greenBright('✔'),
8
+ "fail": '❗',
9
+ "working": '⏳',
10
+ }
11
+
12
+ jest.mock('axios')
13
+ jest.mock('child_process', () => ({ spawnSync: jest.fn() }))
14
+
15
+ describe('checkVersion', () => {
16
+ let spy
17
+ beforeEach(() => {
18
+ jest.clearAllMocks()
19
+ spy = jest.spyOn(console, 'log')
20
+ })
21
+ afterEach(() => {
22
+ spy.mockRestore()
23
+ })
24
+
25
+ it('should return "A newer version" if a newer version is available', async () => {
26
+ axios.get.mockResolvedValue({ data: { 'dist-tags': { latest: '2.0.0' } } })
27
+ const result = await checkVersion(axios, spawnSync, '1.0.0')
28
+ expect(result).toBe('A newer version')
29
+ })
30
+
31
+ it('should return "You are on the latest version" if the current version is the latest version', async () => {
32
+ axios.get.mockResolvedValue({ data: { 'dist-tags': { latest: '1.0.0' } } })
33
+ const result = await checkVersion(axios, spawnSync, '1.0.0')
34
+ expect(result).toBe('You are on the latest version')
35
+ })
36
+
37
+ it('should throw a NpmNotInstalledError if npm is not installed', async () => {
38
+ axios.get.mockResolvedValue({ data: { 'dist-tags': { latest: '2.0.0' } } })
39
+ spawnSync.mockReturnValue({ status: 1, stderr: { toString: () => 'command not found' } })
40
+ try {
41
+ await checkVersion(axios, spawnSync, '1.0.0', true)
42
+ } catch (err) {
43
+ expect(err.name).toBe('NpmNotInstalledError')
44
+ expect(err.message).toBe('npm is not installed on this system. Please install npm and run the command again.')
45
+ }
46
+ })
47
+
48
+ it('should throw a PackageNotFoundError if the package is not found on the npm registry', async () => {
49
+ axios.get.mockRejectedValue({ response: { status: 404 } });
50
+ try {
51
+ await checkVersion(axios, spawnSync, '1.0.0');
52
+ } catch (err) {
53
+ expect(err.name).toBe('PackageNotFoundError');
54
+ expect(err.message).toBe('Package not found on the npm registry');
55
+ }
56
+ });
57
+
58
+ it('should throw a UpdateError if an error occurs while updating the package', async () => {
59
+ axios.get.mockResolvedValue({ data: { 'dist-tags': { latest: '2.0.0' } } });
60
+ spawnSync.mockReturnValue({ status: 1, stderr: { toString: () => 'Update error' } });
61
+ try {
62
+ await checkVersion(axios, spawnSync, '1.0.0', true);
63
+ } catch (err) {
64
+ expect(err.name).toBe('UpdateError');
65
+ expect(err.message).toBe('Error updating the application.');
66
+ }
67
+ });
68
+
69
+ })
@@ -10,45 +10,43 @@ jest.mock('child_process', () => ({
10
10
  }))
11
11
  const gitRef = "HEAD~1..HEAD"
12
12
 
13
- describe('diff', () => {
14
- beforeEach(() => {
15
- jest.clearAllMocks()
16
- })
13
+ beforeEach(() => {
14
+ jest.clearAllMocks()
15
+ })
17
16
 
18
- test('rejects if directory is not a git repository', async () => {
19
- existsSync.mockReturnValueOnce(false)
20
- try {
21
- await diff('/path/to/dir')
22
- fail('Expected function to throw an error', gitRef, existsSync)
23
- } catch (error) {
24
- expect(error.message).toEqual('The directory "/path/to/dir" is not a git repository')
25
- }
26
- })
17
+ test('rejects if directory is not a git repository', async () => {
18
+ existsSync.mockReturnValueOnce(false)
19
+ try {
20
+ await diff('/path/to/dir')
21
+ fail('Expected function to throw an error', gitRef, existsSync)
22
+ } catch (error) {
23
+ expect(error.message).toEqual('The directory "/path/to/dir" is not a git repository')
24
+ }
25
+ })
27
26
 
28
- test('resolves with files when git diff command is successful', async () => {
29
- existsSync.mockReturnValueOnce(true).mockReturnValueOnce(true)
30
- execSync.mockReturnValueOnce(
31
- `A\tfile1.txt
27
+ test('resolves with files when git diff command is successful', async () => {
28
+ existsSync.mockReturnValueOnce(true).mockReturnValueOnce(true)
29
+ execSync.mockReturnValueOnce(
30
+ `A\tfile1.txt
32
31
  M\tfile2.txt
33
32
  D\tfile3.txt`)
34
33
 
35
- const files = await diff('/path/to/dir', gitRef, existsSync, execSync)
36
- expect(files).toEqual([
37
- { type: 'add', path: 'file1.txt', action: 'add' },
38
- { type: 'modify', path: 'file2.txt', action: 'add' },
39
- { type: 'delete', path: 'file3.txt', action: 'delete' }
40
- ])
41
- })
34
+ const files = await diff('/path/to/dir', gitRef, existsSync, execSync)
35
+ expect(files).toEqual([
36
+ { type: 'add', path: 'file1.txt', action: 'add' },
37
+ { type: 'modify', path: 'file2.txt', action: 'add' },
38
+ { type: 'delete', path: 'file3.txt', action: 'delete' }
39
+ ])
40
+ })
42
41
 
43
- test('rejects when git diff command fails', async () => {
44
- existsSync.mockReturnValueOnce(true).mockReturnValueOnce(true)
45
- execSync.mockImplementation(() => { throw new Error('Command failed') })
42
+ test('rejects when git diff command fails', async () => {
43
+ existsSync.mockReturnValueOnce(true).mockReturnValueOnce(true)
44
+ execSync.mockImplementation(() => { throw new Error('Command failed') })
46
45
 
47
- try {
48
- await diff('/path/to/dir', gitRef, existsSync, execSync)
49
- fail('Expected function to throw an error')
50
- } catch (error) {
51
- expect(error.message).toEqual('Command failed')
52
- }
53
- })
46
+ try {
47
+ await diff('/path/to/dir', gitRef, existsSync, execSync)
48
+ fail('Expected function to throw an error')
49
+ } catch (error) {
50
+ expect(error.message).toEqual('Command failed')
51
+ }
54
52
  })
@@ -8,26 +8,24 @@ jest.mock('child_process', () => {
8
8
  }
9
9
  })
10
10
 
11
- describe('log', () => {
12
- beforeEach(() => {
13
- jest.resetAllMocks()
14
- })
11
+ beforeEach(() => {
12
+ jest.resetAllMocks()
13
+ })
15
14
 
16
- it('should return an array of git commit hashes', () => {
17
- const commits = ['1234567890abcdef', '234567890abcdef1', '34567890abcdef12']
18
- execSync.mockReturnValue(commits.join(os.EOL))
19
- const dir = process.cwd()
20
- const gitRef = 'HEAD~1..HEAD'
21
- const result = log(dir, gitRef, execSync)
22
- expect(execSync).toHaveBeenCalledWith(`git log --format=format:%H ${gitRef}`, { cwd: dir, encoding: 'utf-8' })
23
- expect(result).toEqual(commits)
24
- })
15
+ it('should return an array of git commit hashes', () => {
16
+ const commits = ['1234567890abcdef', '234567890abcdef1', '34567890abcdef12']
17
+ execSync.mockReturnValue(commits.join(os.EOL))
18
+ const dir = process.cwd()
19
+ const gitRef = 'HEAD~1..HEAD'
20
+ const result = log(dir, gitRef, execSync)
21
+ expect(execSync).toHaveBeenCalledWith(`git log --format=format:%H ${gitRef}`, { cwd: dir, encoding: 'utf-8' })
22
+ expect(result).toEqual(commits)
23
+ })
25
24
 
26
- it('should throw an error if git is not installed or no entry found in path', () => {
27
- const error = { message: 'ENOENT' }
28
- execSync.mockImplementation(() => { throw error })
29
- const dir = process.cwd()
30
- const gitRef = 'HEAD~1..HEAD'
31
- expect(() => log(dir, gitRef, execSync)).toThrowError('git not installed or no entry found in path')
32
- })
25
+ it('should throw an error if git is not installed or no entry found in path', () => {
26
+ const error = { message: 'ENOENT' }
27
+ execSync.mockImplementation(() => { throw error })
28
+ const dir = process.cwd()
29
+ const gitRef = 'HEAD~1..HEAD'
30
+ expect(() => log(dir, gitRef, execSync)).toThrowError('git not installed or no entry found in path')
33
31
  })
@@ -0,0 +1,49 @@
1
+ import * as packageDefinition from '../../../src/meta/Package.js'
2
+ import { Package } from '../../../src/lib/packageUtil.js'
3
+
4
+ let pkg;
5
+ beforeEach(() => {
6
+ pkg = new Package('xmlPath');
7
+ pkg.packageJSON = JSON.parse(JSON.stringify(packageDefinition.metadataDefinition.emptyPackage));
8
+ });
9
+
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+
15
+ it('should add a member to the pkg JSON', () => {
16
+ pkg.addMember('type', 'member');
17
+ expect(pkg.packageJSON.Package.types[0].name).toBe('type');
18
+ expect(pkg.packageJSON.Package.types[0].members).toEqual(['member']);
19
+ });
20
+
21
+ it('should throw an error if packageJSON is undefined', () => {
22
+ pkg.packageJSON = undefined;
23
+ expect(() => pkg.addMember('type', 'member')).toThrowError('getPackageXML must be called before adding members');
24
+ });
25
+
26
+ it('should throw an error if type is undefined', () => {
27
+ expect(() => pkg.addMember(undefined, 'member')).toThrowError('An undefined type was received when attempting to add a member');
28
+ });
29
+
30
+ it('should throw an error if member is undefined', () => {
31
+ expect(() => pkg.addMember('type', undefined)).toThrowError('An undefined member was received when attempting to add a member');
32
+ });
33
+
34
+ it('should throw an error if member is a part file', () => {
35
+ global.format = "part"
36
+ expect(() => pkg.addMember('type', 'member.part')).toThrowError('Part file received as member is not allowed');
37
+ });
38
+
39
+ it('should not add the member if it already exists', () => {
40
+ pkg.packageJSON.Package.types = [{ name: "type", members: ["member"] }];
41
+ pkg.addMember('type', 'member');
42
+ expect(pkg.packageJSON.Package.types[0].members).toEqual(["member"]);
43
+ });
44
+
45
+ it('should not add the member if type already has an asterisk', () => {
46
+ pkg.packageJSON.Package.types = [{ name: "type", members: ["*"] }];
47
+ pkg.addMember('type', 'member');
48
+ expect(pkg.packageJSON.Package.types[0].members).toEqual(["*"]);
49
+ });
@@ -0,0 +1,57 @@
1
+ import * as packageDefinition from '../../../src/meta/Package.js'
2
+ import { Package } from '../../../src/lib/packageUtil.js'
3
+
4
+ let pkg;
5
+ const fileUtils = {
6
+ fileExists: jest.fn(),
7
+ readFile: jest.fn(),
8
+ }
9
+ beforeEach(() => {
10
+ pkg = new Package('xmlPath');
11
+ });
12
+ global.__basedir = '.'
13
+ afterEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ it('should default the package if the json is empty', async () => {
18
+ fileUtils.fileExists.mockReturnValue(true);
19
+ fileUtils.readFile.mockResolvedValue({});
20
+ global.git = { append: true }
21
+ const result = await pkg.getPackageXML(fileUtils);
22
+ expect(result).toBe('existing');
23
+ expect(fileUtils.fileExists).toHaveBeenCalled();
24
+ expect(fileUtils.readFile).toHaveBeenCalled();
25
+ expect(pkg.packageJSON).toEqual(packageDefinition.metadataDefinition.emptyPackage);
26
+ });
27
+
28
+ it('should read an existing file and call processJSON', async () => {
29
+ fileUtils.fileExists.mockReturnValue(true);
30
+ fileUtils.readFile.mockResolvedValue(packageDefinition.metadataDefinition.emptyPackage);
31
+ global.git = { append: true }
32
+ const result = await pkg.getPackageXML(fileUtils);
33
+ expect(result).toBe('existing');
34
+ expect(fileUtils.fileExists).toHaveBeenCalled();
35
+ expect(fileUtils.readFile).toHaveBeenCalled();
36
+ });
37
+
38
+ it('should create an empty pkg JSON and call processJSON', async () => {
39
+ fileUtils.fileExists.mockReturnValue(false);
40
+ const finalJSON = JSON.parse(JSON.stringify(packageDefinition.metadataDefinition.emptyPackage))
41
+ finalJSON.Package.version = packageDefinition.metadataDefinition.fallbackVersion
42
+ const result = await pkg.getPackageXML(fileUtils);
43
+ expect(result).toBe('not found');
44
+ expect(fileUtils.fileExists).toHaveBeenCalled();
45
+ expect(pkg.packageJSON).toEqual(finalJSON);
46
+ });
47
+
48
+ it('should throw an error if xmlPath is undefined', async () => {
49
+ pkg.xmlPath = undefined;
50
+ await expect(pkg.getPackageXML(fileUtils)).rejects.toThrowError('Package not initialized');
51
+ });
52
+
53
+ it('should throw an error if error occurs during processing', async () => {
54
+ fileUtils.fileExists.mockReturnValue(true);
55
+ fileUtils.readFile.mockRejectedValue(new Error('Error'));
56
+ await expect(pkg.getPackageXML(fileUtils)).rejects.toThrowError('Error');
57
+ });
@@ -0,0 +1,57 @@
1
+ import * as xml2js from 'xml2js'
2
+ import path from 'path'
3
+ import fs from 'fs'
4
+ import { Package } from '../../../src/lib/packageUtil.js'
5
+
6
+ const fileUtils = {
7
+ createDirectory: jest.fn(),
8
+ writeFile: jest.fn()
9
+ }
10
+
11
+ describe('savePackage', () => {
12
+ let pkg
13
+ beforeEach(() => {
14
+ pkg = new Package('path/to/file.xml')
15
+ pkg.packageJSON = {
16
+ Package: {
17
+ $: { xmlns: 'http://www.example.com' },
18
+ version: '1.0'
19
+ }
20
+ }
21
+ })
22
+
23
+ afterEach(() => {
24
+ jest.resetAllMocks()
25
+ })
26
+
27
+ it('should replace http with https in xmlns property', () => {
28
+ pkg.savePackage(xml2js, fileUtils)
29
+ expect(pkg.packageJSON.Package.$.xmlns).toBe('https://www.example.com')
30
+ })
31
+
32
+ it('should save version property to variable, delete it from json and set it again', () => {
33
+ const version = pkg.packageJSON.Package.version
34
+ pkg.savePackage(xml2js, fileUtils)
35
+ expect(version).toBe('1.0')
36
+ expect(pkg.packageJSON.Package.version).toBe(version)
37
+ })
38
+
39
+ it('should build xml object from json and write it to a file', () => {
40
+ pkg.addMember('type', 'member');
41
+ expect(pkg.packageJSON.Package.types[0].name).toBe('type');
42
+ expect(pkg.packageJSON.Package.types[0].members).toEqual(['member']);
43
+ pkg.savePackage(xml2js, fileUtils)
44
+ const xml = '<?xml version="1.0" encoding="UTF-8"?>\n<Package xmlns="https://www.example.com">\n <types>\n <members>member</members>\n <name>type</name>\n </types>\n <version>1.0</version>\n</Package>'
45
+ expect(fileUtils.createDirectory).toHaveBeenCalledWith(path.dirname('path/to/file.xml'))
46
+ expect(fileUtils.writeFile).toHaveBeenCalledWith('path/to/file.xml', xml)
47
+ })
48
+
49
+ it('should throw an error if it occurs', () => {
50
+ fileUtils.createDirectory.mockImplementation(() => {
51
+ throw new Error('createDirectory error')
52
+ })
53
+ expect(() => {
54
+ pkg.savePackage(xml2js, fileUtils)
55
+ }).toThrowError('createDirectory error')
56
+ })
57
+ })