mcdev 8.4.0 → 9.0.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.
Files changed (66) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/.github/workflows/code-test.yml +3 -2
  3. package/.husky/commit-msg +1 -1
  4. package/.husky/post-checkout +4 -6
  5. package/.prettierrc +1 -1
  6. package/@types/lib/Deployer.d.ts.map +1 -1
  7. package/@types/lib/index.d.ts.map +1 -1
  8. package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
  9. package/@types/lib/metadataTypes/Automation.d.ts.map +1 -1
  10. package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
  11. package/@types/lib/metadataTypes/Event.d.ts.map +1 -1
  12. package/@types/lib/metadataTypes/Folder.d.ts.map +1 -1
  13. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  14. package/@types/lib/metadataTypes/List.d.ts +1 -1
  15. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  16. package/@types/lib/util/auth.d.ts.map +1 -1
  17. package/@types/lib/util/cli.d.ts.map +1 -1
  18. package/@types/lib/util/file.d.ts.map +1 -1
  19. package/@types/lib/util/init.config.d.ts.map +1 -1
  20. package/@types/lib/util/init.d.ts +1 -1
  21. package/@types/lib/util/init.d.ts.map +1 -1
  22. package/@types/lib/util/init.git.d.ts.map +1 -1
  23. package/@types/lib/util/util.d.ts +1 -0
  24. package/@types/lib/util/util.d.ts.map +1 -1
  25. package/lib/Deployer.js +5 -4
  26. package/lib/index.js +14 -14
  27. package/lib/metadataTypes/Asset.js +15 -9
  28. package/lib/metadataTypes/Automation.js +8 -10
  29. package/lib/metadataTypes/DataExtension.js +4 -3
  30. package/lib/metadataTypes/DataExtensionField.js +3 -3
  31. package/lib/metadataTypes/Event.js +6 -5
  32. package/lib/metadataTypes/Folder.js +28 -14
  33. package/lib/metadataTypes/Journey.js +2 -2
  34. package/lib/metadataTypes/List.js +1 -1
  35. package/lib/metadataTypes/MetadataType.js +17 -21
  36. package/lib/metadataTypes/User.js +2 -2
  37. package/lib/metadataTypes/definitions/Folder.definition.js +1 -1
  38. package/lib/util/auth.js +20 -24
  39. package/lib/util/businessUnit.js +1 -1
  40. package/lib/util/cli.js +2 -1
  41. package/lib/util/file.js +2 -1
  42. package/lib/util/init.config.js +3 -1
  43. package/lib/util/init.git.js +2 -1
  44. package/lib/util/init.js +2 -1
  45. package/lib/util/util.js +5 -3
  46. package/package.json +19 -19
  47. package/test/mockRoot/.mcdevrc.json +1 -1
  48. package/test/mockRoot/deploy/testInstance/testBU/asset/block/test_slash.asset-block-meta.html +12 -0
  49. package/test/mockRoot/deploy/testInstance/testBU/asset/block/test_slash.asset-block-meta.json +29 -0
  50. package/test/resourceFactory.js +1 -1
  51. package/test/resources/9999999/asset/v1/content/assets/16992/get-response.json +60 -0
  52. package/test/resources/9999999/asset/v1/content/assets/post-response-key=test_slash.json +60 -0
  53. package/test/resources/9999999/asset/v1/content/assets/query/+post-response-assetType.idIN3,195,196,197,198,199,200,201,202,203,210,211,212,213-slashfolder.json +198 -0
  54. package/test/resources/9999999/asset/v1/content/categories/post-response.json +9 -0
  55. package/test/resources/9999999/asset-slashfolder-deploy/block/test_slash.asset-block-meta.html +12 -0
  56. package/test/resources/9999999/asset-slashfolder-deploy/block/test_slash.asset-block-meta.json +29 -0
  57. package/test/resources/9999999/dataFolder/+retrieve-ContentTypeINasset,asset-shared,cloudpages-slashfolder-response.xml +136 -0
  58. package/test/resources/9999999/dataFolder/create-ContentType=asset,Name=testFolder_samePath,ParentFolderID=89397-response.xml +33 -0
  59. package/test/resources/9999999/dataFolder/create-response.xml +33 -0
  60. package/test/resources/9999999/dataFolder/retrieve-samePathOtherBU-response.xml +564 -0
  61. package/test/resources/9999999/folder-deploy-samepath/Content Builder/testFolder_samePath.folder-meta.json +9 -0
  62. package/test/resources/9999999/folder-deploy-slash/Content Builder/Headers%2FFolders.folder-meta.json +9 -0
  63. package/test/type.folder.test.js +157 -0
  64. package/tsconfig.json +1 -1
  65. package/tsconfig.npmScripts.json +1 -1
  66. package/tsconfig.precommit.json +1 -1
@@ -56,8 +56,7 @@ class Automation extends MetadataType {
56
56
  static async retrieve(retrieveDir, _, __, key) {
57
57
  let metadataMap;
58
58
  if (key && this._cachedMetadataMap?.[key]) {
59
- metadataMap = {};
60
- metadataMap[key] = this._cachedMetadataMap[key];
59
+ metadataMap = { [key]: this._cachedMetadataMap[key] };
61
60
  delete this._cachedMetadataMap;
62
61
  } else if (!key && this._cachedMetadataMap) {
63
62
  metadataMap = this._cachedMetadataMap;
@@ -849,8 +848,7 @@ class Automation extends MetadataType {
849
848
  }
850
849
  );
851
850
  if (response?.id === automationLegacyId) {
852
- const item = {};
853
- item[this.definition.keyField] = key;
851
+ const item = { [this.definition.keyField]: key };
854
852
  Util.logger.info(
855
853
  ` - ${mode === 'schedule' ? '✅ activated' : '🛑 paused'} scheduled ${Util.getTypeKeyName(this.definition, item)}${mode === 'schedule' ? ' (' + description + ')' : ''}`
856
854
  );
@@ -1089,12 +1087,12 @@ class Automation extends MetadataType {
1089
1087
  durationUnits: waitUnit,
1090
1088
  });
1091
1089
  } else if (!waitUnit) {
1092
- // convert 24 hrs based time in waitDuration back into 12 hrs based time
1093
- let waitTime12 = waitDuration;
1094
- waitTime12 =
1095
- Number(waitTime12) > 12
1096
- ? (Number(waitTime12) - 12).toString() + ' AM'
1097
- : waitTime12 + ' PM';
1090
+ // // convert 24 hrs based time in waitDuration back into 12 hrs based time
1091
+ // let waitTime12 = waitDuration;
1092
+ // waitTime12 =
1093
+ // Number(waitTime12) > 12
1094
+ // ? (Number(waitTime12) - 12).toString() + ' AM'
1095
+ // : waitTime12 + ' PM';
1098
1096
  // @ts-expect-error - serializedObject is only used to create/update wait activities
1099
1097
  activity.serializedObject = JSON.stringify({
1100
1098
  specifiedTime: waitDuration,
@@ -320,7 +320,7 @@ class DataExtension extends MetadataType {
320
320
  // reset here to get the correct field list
321
321
  item.Fields = Object.keys(existingFields)
322
322
  .map((el) => existingFields[el])
323
- .sort((a, b) => a.Ordinal - b.Ordinal);
323
+ .toSorted((a, b) => a.Ordinal - b.Ordinal);
324
324
  } else if (existingFields) {
325
325
  // get list of updated fields
326
326
  /** @type {DataExtensionFieldItem[]} */ // @ts-ignore Fields.Field is a special case that cannot be properly typed; only required for SOAP API
@@ -344,7 +344,7 @@ class DataExtension extends MetadataType {
344
344
  field.FieldType = existingField.FieldType;
345
345
  return field;
346
346
  })
347
- .sort((a, b) => a.Ordinal - b.Ordinal);
347
+ .toSorted((a, b) => a.Ordinal - b.Ordinal);
348
348
 
349
349
  // get list of new fields
350
350
  /** @type {DataExtensionFieldItem[]} */ // @ts-ignore Fields.Field is a special case that cannot be properly typed; only required for SOAP API
@@ -1128,7 +1128,8 @@ class DataExtension extends MetadataType {
1128
1128
  // A workaround for cross-BU deploy exists but it's likely not beneficial to explain to users:
1129
1129
  // Create a DE based on the not-supported template on the target BU, retrieve it, copy the Template.CustomerKey into the to-be-deployed DE (or use mcdev-templating), done
1130
1130
  throw new Error(
1131
- `Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`
1131
+ `Could not find specified DataExtension Template. Please note that DataExtensions based on SMSMessageTracking and SMSSubscriptionLog cannot be deployed automatically across BUs at this point.`,
1132
+ { cause: ex }
1132
1133
  );
1133
1134
  }
1134
1135
  }
@@ -103,7 +103,7 @@ class DataExtensionField extends MetadataType {
103
103
  Object.keys(fieldsObj)
104
104
  .map((field) => fieldsObj[field])
105
105
  // the API returns the fields not sorted
106
- .sort(this.sortDeFields)
106
+ .toSorted(this.sortDeFields)
107
107
  );
108
108
  }
109
109
 
@@ -352,7 +352,7 @@ class DataExtensionField extends MetadataType {
352
352
  Util.logger.info(
353
353
  ` - Found ${deletionQueue.length} Data Extensions with field ${fieldName} in your BU:\n - ${deletionQueue
354
354
  .map((item) => item.deKey)
355
- .sort()
355
+ .toSorted()
356
356
  .join('\n - ')}`
357
357
  );
358
358
  if (deletionQueue.length > 0 && !Util.skipInteraction) {
@@ -436,7 +436,7 @@ class DataExtensionField extends MetadataType {
436
436
  Util.getGrayMsg(
437
437
  `To refresh your local files, run mcdev r ${this.buObject.credential}/${this.buObject.businessUnit} -m ${uniqueDEKeys
438
438
  .map((key) => 'dataExtension:"' + key + '"')
439
- .sort()
439
+ .toSorted()
440
440
  .join(' ')}`
441
441
  )
442
442
  );
@@ -695,7 +695,7 @@ class Event extends MetadataType {
695
695
  ...new Set(
696
696
  Object.values(this.sfObjects.referencedObjects[objectAPIName])
697
697
  .map((el) => el.referenceObjectName)
698
- .sort()
698
+ .toSorted()
699
699
  ),
700
700
  ]
701
701
  : [];
@@ -793,7 +793,8 @@ class Event extends MetadataType {
793
793
  } catch (ex) {
794
794
  if (ex.code === 'ERR_BAD_RESPONSE') {
795
795
  throw new Error(
796
- `Could not find Salesforce entry object ${objectAPIName} on connected org.`
796
+ `Could not find Salesforce entry object ${objectAPIName} on connected org.`,
797
+ { cause: ex }
797
798
  );
798
799
  }
799
800
  }
@@ -861,7 +862,7 @@ class Event extends MetadataType {
861
862
  */
862
863
  static checkSalesforceEntryEvents(ca, isPublished) {
863
864
  // 1 check eventDataConfig
864
- const edcObjects = ca.eventDataConfig.objects.sort((a, b) =>
865
+ const edcObjects = ca.eventDataConfig.objects.toSorted((a, b) =>
865
866
  a.dePrefix.localeCompare(b.dePrefix)
866
867
  );
867
868
  const warnings = [];
@@ -1156,7 +1157,7 @@ class Event extends MetadataType {
1156
1157
  ca.eventDataSummary =
1157
1158
  'string' === typeof ca.eventDataSummary
1158
1159
  ? // @ts-expect-error transforming this from API-string-format to from mcdev-format
1159
- ca.eventDataSummary.split('; ').filter(Boolean).sort()
1160
+ ca.eventDataSummary.split('; ').filter(Boolean).toSorted()
1160
1161
  : ca.eventDataSummary;
1161
1162
  ca.passThroughArgument =
1162
1163
  'string' === typeof ca.passThroughArgument
@@ -1204,7 +1205,7 @@ class Event extends MetadataType {
1204
1205
  eventDataSummary =
1205
1206
  'string' === typeof eventDataSummary
1206
1207
  ? // @ts-expect-error transforming this from API-string-format to from mcdev-format
1207
- eventDataSummary.split('; ').filter(Boolean).sort()
1208
+ eventDataSummary.split('; ').filter(Boolean).toSorted()
1208
1209
  : eventDataSummary;
1209
1210
 
1210
1211
  const errors = [];
@@ -93,12 +93,18 @@ class Folder extends MetadataType {
93
93
  if (idMap[idMap[id].ParentFolder.ID].Path) {
94
94
  const parent = idMap[idMap[id].ParentFolder.ID];
95
95
  // we use / here not system separator as it is important to keep metadata consistent
96
- idMap[id].Path = [parent.Path, idMap[id].Name].join(
97
- Util.standardizedSplitChar
98
- );
96
+ // replace '/' in folder names with escape char to avoid confusion with the path separator
97
+ idMap[id].Path = [
98
+ parent.Path,
99
+ idMap[id].Name.replaceAll('/', Util.folderNameSlashEscapeChar),
100
+ ].join(Util.standardizedSplitChar);
99
101
  idMap[id].ParentFolder.Path = parent.Path;
100
102
  } else {
101
- idMap[id].Path = idMap[id].Name;
103
+ // replace '/' in folder names with escape char to avoid confusion with the path separator
104
+ idMap[id].Path = idMap[id].Name.replaceAll(
105
+ '/',
106
+ Util.folderNameSlashEscapeChar
107
+ );
102
108
  }
103
109
  } else {
104
110
  Util.logger.info(
@@ -224,9 +230,12 @@ class Folder extends MetadataType {
224
230
  let existingId;
225
231
  try {
226
232
  // perform a secondary check based on path
227
-
233
+ // do not allow mapping folders from other BUs to avoid
234
+ // treating a same-path folder from another BU as already existing
228
235
  const cachedVersion = cache.getFolderByPath(
229
- deployableMetadata.Path
236
+ deployableMetadata.Path,
237
+ undefined,
238
+ false
230
239
  );
231
240
  existingId = cachedVersion?.ID;
232
241
  if (
@@ -309,11 +318,12 @@ class Folder extends MetadataType {
309
318
  parsed[normalizedKey] = parsed['undefined'];
310
319
  delete parsed['undefined'];
311
320
  }
312
- const newObject = {};
313
- newObject[normalizedKey] = Object.assign(
314
- beforeMetadata,
315
- parsed[normalizedKey]
316
- );
321
+ const newObject = {
322
+ [normalizedKey]: Object.assign(
323
+ beforeMetadata,
324
+ parsed[normalizedKey]
325
+ ),
326
+ };
317
327
  cache.mergeMetadata('folder', newObject);
318
328
 
319
329
  upsertResults[metadataKey] = beforeMetadata;
@@ -642,9 +652,13 @@ class Folder extends MetadataType {
642
652
  );
643
653
 
644
654
  if (fileContent.Name === fileNameWithoutEnding) {
655
+ // replace '/' in folder names with escape char to match how paths are built during retrieve
645
656
  fileContent.Path =
646
657
  (standardSubDir ? standardSubDir + '/' : '') +
647
- fileNameWithoutEnding;
658
+ fileNameWithoutEnding.replaceAll(
659
+ '/',
660
+ Util.folderNameSlashEscapeChar
661
+ );
648
662
  fileContent.ParentFolder = {
649
663
  Path: standardSubDir,
650
664
  };
@@ -678,7 +692,7 @@ class Folder extends MetadataType {
678
692
  return fileName2FileContent;
679
693
  } catch (ex) {
680
694
  Util.metadataLogger('error', this.definition.type, 'getJsonFromFS', ex);
681
- throw new Error(ex);
695
+ throw ex;
682
696
  }
683
697
  }
684
698
 
@@ -697,7 +711,7 @@ class Folder extends MetadataType {
697
711
  leftOperand: 'ContentType',
698
712
  operator: contentTypeList.length === 1 ? 'equals' : 'IN',
699
713
  rightOperand:
700
- contentTypeList.length === 1 ? contentTypeList[0] : contentTypeList.sort(),
714
+ contentTypeList.length === 1 ? contentTypeList[0] : contentTypeList.toSorted(),
701
715
  };
702
716
  options.filter = options.filter
703
717
  ? {
@@ -1323,7 +1323,7 @@ class Journey extends MetadataType {
1323
1323
  }
1324
1324
 
1325
1325
  // apply sorting by activity key to work around the API shuffling activities around
1326
- metadata.activities = metadata.activities.sort((a, b) => a.key.localeCompare(b.key));
1326
+ metadata.activities = metadata.activities.toSorted((a, b) => a.key.localeCompare(b.key));
1327
1327
  }
1328
1328
 
1329
1329
  /**
@@ -2362,7 +2362,7 @@ class Journey extends MetadataType {
2362
2362
  }
2363
2363
 
2364
2364
  // good practice to return the published keys in alphabetical order
2365
- return executedKeyArr.filter(Boolean).sort();
2365
+ return executedKeyArr.filter(Boolean).toSorted();
2366
2366
  }
2367
2367
 
2368
2368
  /**
@@ -92,7 +92,7 @@ class List extends MetadataType {
92
92
  }
93
93
 
94
94
  /**
95
- * helper for @link retrieveForCache and @link retrieve
95
+ * helper for {@link retrieveForCache} and {@link retrieve}
96
96
  *
97
97
  * @private
98
98
  * @param {MetadataTypeMapObj} results metadata from retrieve for current BU
@@ -108,7 +108,7 @@ class MetadataType {
108
108
  } catch (ex) {
109
109
  // this will catch issues with readdirSync
110
110
  Util.metadataLogger('debug', this.definition.type, 'getJsonFromFS', ex);
111
- throw new Error(ex);
111
+ throw ex;
112
112
  }
113
113
  return fileName2FileContent;
114
114
  }
@@ -229,7 +229,8 @@ class MetadataType {
229
229
  // postRetrieveTasks will be run automatically on this via super.saveResult
230
230
  } catch (ex) {
231
231
  throw new Error(
232
- `Could not get details for new ${this.definition.type} ${id} from server (${ex.message})`
232
+ `Could not get details for new ${this.definition.type} ${id} from server (${ex.message})`,
233
+ { cause: ex }
233
234
  );
234
235
  }
235
236
  }
@@ -451,7 +452,7 @@ class MetadataType {
451
452
 
452
453
  return { metadata: metadata, type: this.definition.type };
453
454
  } catch (ex) {
454
- throw new Error(`${this.definition.type}:: ${ex.message}`);
455
+ throw new Error(`${this.definition.type}:: ${ex.message}`, { cause: ex });
455
456
  }
456
457
  }
457
458
 
@@ -845,8 +846,7 @@ class MetadataType {
845
846
 
846
847
  // make this newly created item available in cache for other itmes that might reference it
847
848
  /** @type {MetadataTypeMap} */
848
- const newObject = {};
849
- newObject[metadataKey] = metadataMap[metadataKey];
849
+ const newObject = { [metadataKey]: metadataMap[metadataKey] };
850
850
  cache.mergeMetadata(this.definition.type, newObject);
851
851
  }
852
852
  } else if (action === 'update' && !Util.OPTIONS.noUpdate) {
@@ -872,8 +872,10 @@ class MetadataType {
872
872
  updateResults.push(result);
873
873
 
874
874
  // make this newly created item available in cache for other itmes that might reference it
875
- const newObject = {};
876
- newObject[metadataKey] = structuredClone(metadataMap[metadataKey]);
875
+ const newObject = {
876
+ [metadataKey]: structuredClone(metadataMap[metadataKey]),
877
+ };
878
+
877
879
  if (result.objectID) {
878
880
  // required for assets
879
881
  newObject[metadataKey].objectID = result.objectID;
@@ -1704,8 +1706,7 @@ class MetadataType {
1704
1706
  (typeof singleRetrieve === 'string' || typeof singleRetrieve === 'number')
1705
1707
  ) {
1706
1708
  // in case we really just wanted one entry but couldnt do so in the api call, filter it here
1707
- const single = {};
1708
- single[singleRetrieve] = metadataStructure[singleRetrieve];
1709
+ const single = { [singleRetrieve]: metadataStructure[singleRetrieve] };
1709
1710
  return single;
1710
1711
  } else if (singleRetrieve) {
1711
1712
  return {};
@@ -2547,8 +2548,7 @@ class MetadataType {
2547
2548
  * @returns {Promise.<boolean>} deletion success flag
2548
2549
  */
2549
2550
  static async deleteByKeySOAP(key, overrideKeyField, codeNotFound, handleOutside) {
2550
- const metadata = {};
2551
- metadata[overrideKeyField || this.definition.keyField] = key;
2551
+ const metadata = { [overrideKeyField || this.definition.keyField]: key };
2552
2552
  const soapType = this.definition.soapType || this.definition.type;
2553
2553
  try {
2554
2554
  await this.client.soap.delete(Util.capitalizeFirstLetter(soapType), metadata, null);
@@ -2639,16 +2639,12 @@ class MetadataType {
2639
2639
  static async readBUMetadataForType(readDir, listBadKeys, buMetadata) {
2640
2640
  buMetadata ||= {};
2641
2641
  readDir = File.normalizePath([readDir, this.definition.type]);
2642
- try {
2643
- if (await File.pathExists(readDir)) {
2644
- // check if folder name is a valid metadataType, then check if the user limited to a certain type in the command params
2645
- buMetadata[this.definition.type] = await this.getJsonFromFS(readDir, listBadKeys);
2646
- return buMetadata;
2647
- } else {
2648
- throw new Error(`Directory '${readDir}' does not exist.`);
2649
- }
2650
- } catch (ex) {
2651
- throw new Error(ex.message);
2642
+ if (await File.pathExists(readDir)) {
2643
+ // check if folder name is a valid metadataType, then check if the user limited to a certain type in the command params
2644
+ buMetadata[this.definition.type] = await this.getJsonFromFS(readDir, listBadKeys);
2645
+ return buMetadata;
2646
+ } else {
2647
+ throw new Error(`Directory '${readDir}' does not exist.`);
2652
2648
  }
2653
2649
  }
2654
2650
 
@@ -1055,7 +1055,7 @@ class User extends MetadataType {
1055
1055
  associatedBus = [
1056
1056
  ...new Set(user.c__AssociatedBusinessUnits.map((mid) => this._getBuName(mid))),
1057
1057
  ]
1058
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
1058
+ .toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0))
1059
1059
  .join(',<br> ');
1060
1060
  }
1061
1061
  const defaultBUName = this._getBuName(user.DefaultBusinessUnit);
@@ -1215,7 +1215,7 @@ class User extends MetadataType {
1215
1215
  // individual role (which are not manageable nor visible in the GUI)
1216
1216
  (roleName) => !roleName.startsWith('Individual role for ')
1217
1217
  )
1218
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1218
+ .toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1219
1219
  } else {
1220
1220
  // set to empty array
1221
1221
  roles = [];
@@ -57,7 +57,7 @@ export default {
57
57
  'triggered_send',
58
58
  ],
59
59
  deployFolderTypesEmailRest: ['automations', 'journey', 'triggered_send_journeybuilder'],
60
- deployFolderTypesAssetRest: ['cloudpages'],
60
+ deployFolderTypesAssetRest: ['cloudpages', 'asset', 'asset-shared'],
61
61
  deployFolderBlacklist: [
62
62
  // lower-case values!
63
63
  'shared data extensions',
package/lib/util/auth.js CHANGED
@@ -40,32 +40,28 @@ const Auth = {
40
40
  */
41
41
  async saveCredential(authObject, credential) {
42
42
  const sdk = setupSDK(credential, authObject);
43
- try {
44
- // check credentials to allow clear log output and stop execution
45
- const test = await sdk.auth.getAccessToken();
46
- if (test.error) {
47
- throw new Error(test.error_description);
48
- } else if (test.scope) {
49
- // find missing rights
50
- const missingAccess = sdk.auth
51
- .getSupportedScopes()
52
- .filter((element) => !test.scope.includes(element));
43
+ // check credentials to allow clear log output and stop execution
44
+ const test = await sdk.auth.getAccessToken();
45
+ if (test.error) {
46
+ throw new Error(test.error_description);
47
+ } else if (test.scope) {
48
+ // find missing rights
49
+ const missingAccess = sdk.auth
50
+ .getSupportedScopes()
51
+ .filter((element) => !test.scope.includes(element));
53
52
 
54
- if (missingAccess.length) {
55
- Util.logger.warn(
56
- 'Installed package has insufficient access. You might encounter malfunctions!'
57
- );
58
- Util.logger.warn('Missing scope: ' + missingAccess.join(', '));
59
- }
60
- const existingAuth = (await File.pathExists(Util.authFileName))
61
- ? await File.readJSON(Util.authFileName)
62
- : {};
63
- existingAuth[credential] = authObject;
64
- await File.writeJSONToFile('./', Util.authFileName.split('.json')[0], existingAuth);
65
- authfile = existingAuth;
53
+ if (missingAccess.length) {
54
+ Util.logger.warn(
55
+ 'Installed package has insufficient access. You might encounter malfunctions!'
56
+ );
57
+ Util.logger.warn('Missing scope: ' + missingAccess.join(', '));
66
58
  }
67
- } catch (ex) {
68
- throw new Error(ex.message);
59
+ const existingAuth = (await File.pathExists(Util.authFileName))
60
+ ? await File.readJSON(Util.authFileName)
61
+ : {};
62
+ existingAuth[credential] = authObject;
63
+ await File.writeJSONToFile('./', Util.authFileName.split('.json')[0], existingAuth);
64
+ authfile = existingAuth;
69
65
  }
70
66
  },
71
67
 
@@ -60,7 +60,7 @@ const BusinessUnit = {
60
60
  element.ID = Number.parseInt(element.ID);
61
61
  element.ParentID = Number.parseInt(element.ParentID);
62
62
  return element;
63
- }).sort((a, b) => {
63
+ }).toSorted((a, b) => {
64
64
  if (a.ParentID === 0) {
65
65
  return -1;
66
66
  }
package/lib/util/cli.js CHANGED
@@ -304,7 +304,8 @@ const Cli = {
304
304
  async _setCredential(properties, credName, refreshBUs = true) {
305
305
  const skipInteraction = Util.skipInteraction;
306
306
  // Get user input
307
- let credentialsGood = null;
307
+ /** @type {boolean} */
308
+ let credentialsGood;
308
309
  let inputData;
309
310
  do {
310
311
  if (skipInteraction) {
package/lib/util/file.js CHANGED
@@ -271,7 +271,8 @@ const File = {
271
271
  */
272
272
  _beautify_prettier: async function (directory, filename, filetype, content) {
273
273
  const properties = await config.getProperties();
274
- let formatted = '';
274
+ /** @type {string} */
275
+ let formatted;
275
276
  try {
276
277
  if (!FileFs.prettierConfig) {
277
278
  // either no prettier config in project directory or initPrettier was not run before this
@@ -444,13 +444,15 @@ const Init = {
444
444
  );
445
445
  boilerplateFileContent ||= await File.readFile(boilerplateFileName, 'utf8');
446
446
 
447
- let todo = null;
447
+ /** @type {string} */
448
+ let todo;
448
449
 
449
450
  if (await File.pathExists(fileName)) {
450
451
  if (relevantForced.deletes.includes(path.normalize(fileName))) {
451
452
  Util.logger.info(
452
453
  `- ✋ ${fileName} found but it is required to delete it. Commencing rename instead for your convenience:`
453
454
  );
455
+ // eslint-disable-next-line no-useless-assignment
454
456
  todo = 'delete';
455
457
  } else {
456
458
  const existingFileContent = await File.readFile(fileName, 'utf8');
@@ -30,7 +30,8 @@ const Init = {
30
30
  }
31
31
  // 3. test if in git repo
32
32
  const gitRepoFoundInCWD = await File.pathExists('.git');
33
- let newRepoInitialized = null;
33
+ /** @type {boolean} */
34
+ let newRepoInitialized;
34
35
  if (gitRepoFoundInCWD) {
35
36
  Util.logger.info(`✔️ Git repository found`);
36
37
  newRepoInitialized = false;
package/lib/util/init.js CHANGED
@@ -91,6 +91,7 @@ const Init = {
91
91
  }
92
92
  } while (error && !skipInteraction);
93
93
  Util.logger.debug('reloading config');
94
+ // eslint-disable-next-line no-useless-assignment
94
95
  properties = await config.getProperties(true);
95
96
  } else if (missingCredentials.length) {
96
97
  // forced update-credential mode - user likely cloned repo and is missing mcdev-auth.json
@@ -284,7 +285,7 @@ const Init = {
284
285
  },
285
286
 
286
287
  /**
287
- * helper for @initProject that optionally creates markets and market lists for all BUs
288
+ * helper for {@link Init.initProject} that optionally creates markets and market lists for all BUs
288
289
  */
289
290
  async _initMarkets() {
290
291
  const skipInteraction = Util.skipInteraction;
package/lib/util/util.js CHANGED
@@ -45,6 +45,8 @@ export const Util = {
45
45
  defaultGitBranch: 'main',
46
46
  parentBuName: '_ParentBU_',
47
47
  standardizedSplitChar: '/',
48
+ /** used to replace '/' in folder names to avoid confusion with the path separator */
49
+ folderNameSlashEscapeChar: '\u2215',
48
50
  /** @type {SkipInteraction} */
49
51
  skipInteraction: null,
50
52
  packageJsonMcdev: readJsonSync(path.join(__dirname, '../../package.json')),
@@ -1018,7 +1020,7 @@ export const Util = {
1018
1020
  if (subTypeArr && subTypeArr.length > 0) {
1019
1021
  Util.logger.info(
1020
1022
  Util.getGrayMsg(
1021
- `${indent} - Subtype${subTypeArr.length > 1 ? 's' : ''}: ${[...subTypeArr].sort().join(', ')}`
1023
+ `${indent} - Subtype${subTypeArr.length > 1 ? 's' : ''}: ${[...subTypeArr].toSorted().join(', ')}`
1022
1024
  )
1023
1025
  );
1024
1026
  }
@@ -1341,7 +1343,7 @@ export const Util = {
1341
1343
  values.push(...this.findLeafVals(object[k], key));
1342
1344
  }
1343
1345
  });
1344
- return [...new Set(values.sort())];
1346
+ return [...new Set(values.toSorted())];
1345
1347
  },
1346
1348
  /**
1347
1349
  * helper that returns a new object with sorted attributes of the given object
@@ -1351,7 +1353,7 @@ export const Util = {
1351
1353
  */
1352
1354
  sortObjectAttributes(obj) {
1353
1355
  return Object.keys(obj)
1354
- .sort()
1356
+ .toSorted()
1355
1357
  .reduce((acc, key) => {
1356
1358
  acc[key] = obj[key];
1357
1359
  return acc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcdev",
3
- "version": "8.4.0",
3
+ "version": "9.0.1",
4
4
  "description": "Accenture Salesforce Marketing Cloud DevTools",
5
5
  "author": "Accenture: joern.berkefeld, douglas.midgley, robert.zimmermann, maciej.barnas",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "mcdev": "./lib/cli.js"
46
46
  },
47
47
  "engines": {
48
- "node": "^18.20.0 || ^20.10.0 || >=21.0.0"
48
+ "node": "^20.19.0 || ^22.13.0 || >=24"
49
49
  },
50
50
  "scripts": {
51
51
  "start": "node lib/cli.js",
@@ -70,47 +70,47 @@
70
70
  "beauty-amp-core2": "0.4.9",
71
71
  "cli-progress": "3.12.0",
72
72
  "command-exists": "1.2.9",
73
- "conf": "13.1.0",
73
+ "conf": "15.1.0",
74
74
  "console.table": "0.10.0",
75
75
  "deep-equal": "2.2.3",
76
76
  "fs-extra": "11.3.3",
77
- "inquirer": "12.6.0",
77
+ "inquirer": "13.3.0",
78
78
  "json-to-table": "4.2.1",
79
79
  "mustache": "4.2.0",
80
- "p-limit": "6.2.0",
80
+ "p-limit": "7.3.0",
81
81
  "prettier": "3.8.1",
82
82
  "prettier-plugin-sql": "0.19.2",
83
83
  "semver": "7.7.4",
84
- "sfmc-sdk": "2.1.4",
85
- "simple-git": "3.30.0",
84
+ "sfmc-sdk": "3.0.0",
85
+ "simple-git": "3.32.3",
86
86
  "toposort": "2.0.2",
87
87
  "update-notifier": "7.3.1",
88
88
  "winston": "3.19.0",
89
- "yargs": "17.7.2",
89
+ "yargs": "18.0.0",
90
90
  "yocto-spinner": "1.1.0"
91
91
  },
92
92
  "devDependencies": {
93
- "@eslint/js": "9.39.2",
93
+ "@eslint/js": "10.0.1",
94
94
  "@types/fs-extra": "11.0.4",
95
- "@types/inquirer": "9.0.7",
95
+ "@types/inquirer": "9.0.9",
96
96
  "@types/mocha": "10.0.8",
97
- "@types/node": "25.2.3",
98
- "@types/yargs": "17.0.33",
97
+ "@types/node": "25.3.3",
98
+ "@types/yargs": "17.0.35",
99
99
  "assert": "2.1.0",
100
100
  "axios-mock-adapter": "2.0.0",
101
- "c8": "10.1.3",
101
+ "c8": "11.0.0",
102
102
  "chai": "6.2.2",
103
103
  "chai-files": "1.4.0",
104
- "eslint": "9.39.2",
104
+ "eslint": "10.0.2",
105
105
  "eslint-config-ssjs": "2.0.0",
106
- "eslint-plugin-jsdoc": "50.7.1",
106
+ "eslint-plugin-jsdoc": "62.7.1",
107
107
  "eslint-plugin-mocha": "11.2.0",
108
108
  "eslint-plugin-prettier": "5.5.5",
109
- "eslint-plugin-unicorn": "59.0.1",
110
- "fast-xml-parser": "5.3.5",
111
- "globals": "17.3.0",
109
+ "eslint-plugin-unicorn": "63.0.0",
110
+ "fast-xml-parser": "5.4.1",
111
+ "globals": "17.4.0",
112
112
  "husky": "9.1.7",
113
- "lint-staged": "16.2.7",
113
+ "lint-staged": "16.3.1",
114
114
  "mocha": "11.7.5",
115
115
  "mock-fs": "5.3.0",
116
116
  "npm-run-all": "4.1.5",
@@ -178,5 +178,5 @@
178
178
  "verification"
179
179
  ]
180
180
  },
181
- "version": "8.4.0"
181
+ "version": "9.0.1"
182
182
  }