From 109adef0bd1510ac0a0b05478b19c8d14f2aedc5 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 10 May 2023 10:03:10 -0600 Subject: [PATCH 01/14] fix: deletes singular CL when from CLs when specified --- package.json | 3 ++- src/sourceTracking.ts | 34 +++++++++++++++++++++++++++++++++- yarn.lock | 7 +++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c0156202..4299612a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@salesforce/core": "^3.34.6", "@salesforce/kit": "^1.9.2", "@salesforce/source-deploy-retrieve": "^8.0.1", + "fast-xml-parser": "^4.2.2", "graceful-fs": "^4.2.11", "isomorphic-git": "1.23.0", "ts-retry-promise": "^0.7.0" @@ -158,4 +159,4 @@ "output": [] } } -} \ No newline at end of file +} diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index 48259465..845bcf51 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -28,6 +28,7 @@ import { // this is not exported by SDR (see the comments in SDR regarding its limitations) import { filePathsFromMetadataComponent } from '@salesforce/source-deploy-retrieve/lib/src/utils/filePathGenerator'; +import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { RemoteSourceTrackingService, remoteChangeElementToChangeResult } from './shared/remoteSourceTrackingService'; import { ShadowRepo } from './shared/localShadowRepo'; import { throwIfConflicts, findConflictsInComponentSet, getDedupedConflictsFromChanges } from './shared/conflicts'; @@ -351,7 +352,38 @@ export class SourceTracking extends AsyncCreatable { ); const filenames = Array.from(sourceComponentByFileName.keys()); // delete the files - await Promise.all(filenames.map((filename) => fs.promises.unlink(filename))); + await Promise.all( + filenames.map((filename) => { + if (sourceComponentByFileName.get(filename)?.type.id === 'customlabel') { + // for custom labels, we need to remove the individual label from the xml file + // so we'll parse the xml + const parser = new XMLParser({ + ignoreDeclaration: false, + ignoreAttributes: false, + attributeNamePrefix: '@_', + }); + const customLabels = parser.parse(fs.readFileSync(filename, 'utf8')) as { + CustomLabels: { labels: Array<{ fullName: string }> }; + }; + // delete the label from the json based on it's fullName + customLabels.CustomLabels.labels = customLabels.CustomLabels.labels.filter( + (label) => label.fullName !== sourceComponentByFileName.get(filename)?.fullName + ); + + const builder = new XMLBuilder({ + attributeNamePrefix: '@_', + ignoreAttributes: false, + format: true, + indentBy: ' ', + }); + // and then write that json back to xml and back to the fs + const xml = builder.build(customLabels) as string; + fs.writeFileSync(filename, xml); + } else { + return fs.promises.unlink(filename); + } + }) + ); // update the tracking files. We're simulating SDR-style fileResponse await Promise.all([ diff --git a/yarn.lock b/yarn.lock index b97536a6..3f5ab046 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3262,6 +3262,13 @@ fast-xml-parser@^4.1.4: dependencies: strnum "^1.0.5" +fast-xml-parser@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.2.tgz#cb7310d1e9cf42d22c687b0fae41f3c926629368" + integrity sha512-DLzIPtQqmvmdq3VUKR7T6omPK/VCRNqgFlGtbESfyhcH2R4I8EzK1/K6E8PkRCK2EabWrUHK32NjYRbEFnnz0Q== + dependencies: + strnum "^1.0.5" + fastest-levenshtein@^1.0.7: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" From e3bc831b3f1e83dd3f53fe25eb30806963a61de5 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 10 May 2023 12:06:13 -0600 Subject: [PATCH 02/14] chore: ensure array when deleting last CL in CLs --- src/sourceTracking.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index 845bcf51..b835881a 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import { resolve, sep, normalize } from 'path'; import { NamedPackageDir, Logger, Org, SfProject, Lifecycle } from '@salesforce/core'; -import { AsyncCreatable } from '@salesforce/kit'; +import { AsyncCreatable, ensureArray } from '@salesforce/kit'; import { isString } from '@salesforce/ts-types'; import { ComponentSet, @@ -366,7 +366,7 @@ export class SourceTracking extends AsyncCreatable { CustomLabels: { labels: Array<{ fullName: string }> }; }; // delete the label from the json based on it's fullName - customLabels.CustomLabels.labels = customLabels.CustomLabels.labels.filter( + customLabels.CustomLabels.labels = ensureArray(customLabels.CustomLabels.labels).filter( (label) => label.fullName !== sourceComponentByFileName.get(filename)?.fullName ); From 95a7d77269dd1447de50981a99d339a510fdf975 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 10 May 2023 14:19:11 -0600 Subject: [PATCH 03/14] chore: delete entire CL when last one deleted, return multiple FileResponses for multiple CL --- src/sourceTracking.ts | 58 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index b835881a..4fed25c5 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import { resolve, sep, normalize } from 'path'; import { NamedPackageDir, Logger, Org, SfProject, Lifecycle } from '@salesforce/core'; -import { AsyncCreatable, ensureArray } from '@salesforce/kit'; +import { AsyncCreatable } from '@salesforce/kit'; import { isString } from '@salesforce/ts-types'; import { ComponentSet, @@ -363,22 +363,30 @@ export class SourceTracking extends AsyncCreatable { attributeNamePrefix: '@_', }); const customLabels = parser.parse(fs.readFileSync(filename, 'utf8')) as { - CustomLabels: { labels: Array<{ fullName: string }> }; + CustomLabels: { labels: Array<{ fullName: string }> | { fullName: string } }; }; - // delete the label from the json based on it's fullName - customLabels.CustomLabels.labels = ensureArray(customLabels.CustomLabels.labels).filter( - (label) => label.fullName !== sourceComponentByFileName.get(filename)?.fullName - ); - - const builder = new XMLBuilder({ - attributeNamePrefix: '@_', - ignoreAttributes: false, - format: true, - indentBy: ' ', - }); - // and then write that json back to xml and back to the fs - const xml = builder.build(customLabels) as string; - fs.writeFileSync(filename, xml); + if ('fullName' in customLabels.CustomLabels.labels) { + // a single custom label remains, delete the entire file + return fs.promises.unlink(filename); + } else { + const customLabelsToDelete = changesToDelete + .filter((change) => change.type.name === 'CustomLabel') + .map((change) => change.fullName); + // delete the label from the json based on it's fullName + customLabels.CustomLabels.labels = customLabels.CustomLabels.labels.filter( + (label) => !customLabelsToDelete.includes(label.fullName) + ); + + const builder = new XMLBuilder({ + attributeNamePrefix: '@_', + ignoreAttributes: false, + format: true, + indentBy: ' ', + }); + // and then write that json back to xml and back to the fs + const xml = builder.build(customLabels) as string; + fs.writeFileSync(filename, xml); + } } else { return fs.promises.unlink(filename); } @@ -397,16 +405,14 @@ export class SourceTracking extends AsyncCreatable { true // skip polling because it's a pull ), ]); - return filenames.reduce((result, filename) => { - const component = sourceComponentByFileName.get(filename); - if (component) { - result.push({ - state: ComponentStatus.Deleted, - filePath: filename, - type: component.type.name, - fullName: component.fullName, - }); - } + + return changesToDelete.reduce((result, component) => { + result.push({ + state: ComponentStatus.Deleted, + filePath: component.content ?? (component.xml as string), + type: component.type.name, + fullName: component.fullName, + }); return result; }, []); From 688d59c73f5b47e7777d3095b0930c4b63532609 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 10 May 2023 15:53:05 -0600 Subject: [PATCH 04/14] chore: use promise array --- src/sourceTracking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index 4fed25c5..c0a82c03 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -385,7 +385,7 @@ export class SourceTracking extends AsyncCreatable { }); // and then write that json back to xml and back to the fs const xml = builder.build(customLabels) as string; - fs.writeFileSync(filename, xml); + return fs.promises.writeFile(filename, xml); } } else { return fs.promises.unlink(filename); From eca3efc5d3f1f38cc15815462f5efced07d04971 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 08:00:24 -0600 Subject: [PATCH 05/14] chore: small tweak, get CI running --- src/sourceTracking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index c0a82c03..dfbe8c4b 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -372,7 +372,7 @@ export class SourceTracking extends AsyncCreatable { const customLabelsToDelete = changesToDelete .filter((change) => change.type.name === 'CustomLabel') .map((change) => change.fullName); - // delete the label from the json based on it's fullName + // delete the labels from the json based on their fullName's customLabels.CustomLabels.labels = customLabels.CustomLabels.labels.filter( (label) => !customLabelsToDelete.includes(label.fullName) ); From 86737d54bfd445de840be723a4b6ff0aba6409c6 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 08:10:16 -0600 Subject: [PATCH 06/14] chore: bump sdr --- package.json | 2 +- yarn.lock | 69 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 4299612a..f976054f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dependencies": { "@salesforce/core": "^3.34.6", "@salesforce/kit": "^1.9.2", - "@salesforce/source-deploy-retrieve": "^8.0.1", + "@salesforce/source-deploy-retrieve": "^8.4.0", "fast-xml-parser": "^4.2.2", "graceful-fs": "^4.2.11", "isomorphic-git": "1.23.0", diff --git a/yarn.lock b/yarn.lock index 3f5ab046..c6c4b86e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,6 +913,27 @@ jsonwebtoken "9.0.0" ts-retry-promise "^0.7.0" +"@salesforce/core@^3.36.0": + version "3.36.0" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.0.tgz#cbff147d888eee0b921e368c1fdc1a1a3c2eacab" + integrity sha512-LOeSJgozf5B1S8C98/K3afewRsathfEm+HrXxaFXJjITFfIhxqcIDB5xC1cw0VikfRUlq7dQocmVsvezcK0Ghw== + dependencies: + "@salesforce/bunyan" "^2.0.0" + "@salesforce/kit" "^1.9.2" + "@salesforce/schemas" "^1.5.1" + "@salesforce/ts-types" "^1.7.2" + "@types/semver" "^7.3.13" + ajv "^8.12.0" + archiver "^5.3.0" + change-case "^4.1.2" + debug "^3.2.7" + faye "^1.4.0" + form-data "^4.0.0" + js2xmlparser "^4.0.1" + jsforce "^2.0.0-beta.21" + jsonwebtoken "9.0.0" + ts-retry-promise "^0.7.0" + "@salesforce/dev-config@^3.0.0", "@salesforce/dev-config@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-3.1.0.tgz#8eb5b35860ff60d1c1dc3fd9329b01a28475d5b9" @@ -977,16 +998,17 @@ resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.5.1.tgz#2d1bfdcf593caaa04cd4b3e6fe621097ff7f28fe" integrity sha512-MRqU+tn8w5IFvZ0Lm9YKLgxYxr2MQMI+fXXsTrwfUnijsps+ybF9IOTu6MOMxxl2vCUkO8XDjA435wXlWSLI6g== -"@salesforce/source-deploy-retrieve@^8.0.1": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-8.0.1.tgz#769d761cf2a3015282ad6a77e103d2a9c1e99052" - integrity sha512-uXgMwBXaCH0SM93e8Q/ptGc4xi+wna/CO+SFfZHvy3nnlGHi7MQOUNRnAn2L5NzqEg0ZX9HrXpx4ILf4hLFl3w== +"@salesforce/source-deploy-retrieve@^8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-8.4.0.tgz#10cadb7a02dc4657c5b941161b30a5017dabd788" + integrity sha512-eAOuhmbNAMoJdvhdzF1cBtqNIG/WhLR/GRCA89wevvPLidCdzMNzNGPUQWO0N+PSVWskRbbNeZG3n9ws4SV1vA== dependencies: - "@salesforce/core" "^3.34.6" + "@salesforce/core" "^3.36.0" "@salesforce/kit" "^1.9.2" "@salesforce/ts-types" "^1.7.2" archiver "^5.3.1" - fast-xml-parser "^4.1.4" + fast-levenshtein "^3.0.0" + fast-xml-parser "^4.2.2" got "^11.8.6" graceful-fs "^4.2.11" ignore "^5.2.4" @@ -3255,13 +3277,6 @@ fast-levenshtein@^3.0.0: dependencies: fastest-levenshtein "^1.0.7" -fast-xml-parser@^4.1.4: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.0.tgz#6db2ba33b95b8b4af93f94fe024d4b4d02a50855" - integrity sha512-+zVQv4aVTO+o8oRUyRL7PjgeVo1J6oP8Cw2+a8UTZQcj5V0yUK5T63gTN0ldgiHDPghUjKc4OpT6SwMTwnOQug== - dependencies: - strnum "^1.0.5" - fast-xml-parser@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.2.tgz#cb7310d1e9cf42d22c687b0fae41f3c926629368" @@ -4560,6 +4575,32 @@ jsforce@^2.0.0-beta.20: strip-ansi "^6.0.0" xml2js "^0.4.22" +jsforce@^2.0.0-beta.21: + version "2.0.0-beta.21" + resolved "https://registry.yarnpkg.com/jsforce/-/jsforce-2.0.0-beta.21.tgz#04c94d762d2536bf1af3062d5cca206656f5b12b" + integrity sha512-74GUF/96vYBNZo3SUccXtt4CmfvZ0iqTSc0Z3OB940Ec7oU6coOAGhlCZ+XprXaHOMMhXMXrZQ1PCd16yjIA7A== + dependencies: + "@babel/runtime" "^7.12.5" + "@babel/runtime-corejs3" "^7.12.5" + "@types/node" "^12.19.9" + abort-controller "^3.0.0" + base64url "^3.0.1" + commander "^4.0.1" + core-js "^3.6.4" + csv-parse "^4.8.2" + csv-stringify "^5.3.4" + faye "^1.4.0" + form-data "^4.0.0" + fs-extra "^8.1.0" + https-proxy-agent "^5.0.0" + inquirer "^7.0.0" + multistream "^3.1.0" + node-fetch "^2.6.1" + open "^7.0.0" + regenerator-runtime "^0.13.3" + strip-ansi "^6.0.0" + xml2js "^0.5.0" + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -7717,7 +7758,7 @@ write-file-atomic@^4.0.0: imurmurhash "^0.1.4" signal-exit "^3.0.7" -xml2js@0.5.0: +xml2js@0.5.0, xml2js@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7" integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA== From 8a45e6f77f54ea04d7f17ee74e7357d2b2556f68 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 08:30:06 -0600 Subject: [PATCH 07/14] chore: bump core --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f976054f..d963e491 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "/oclif.manifest.json" ], "dependencies": { - "@salesforce/core": "^3.34.6", + "@salesforce/core": "^3.36.0", "@salesforce/kit": "^1.9.2", "@salesforce/source-deploy-retrieve": "^8.4.0", "fast-xml-parser": "^4.2.2", From 99ef27bcba66f52bd5249f9ca3752cd8d326d898 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 10:19:04 -0600 Subject: [PATCH 08/14] chore: more bugs caught by extNuts fixed --- src/sourceTracking.ts | 50 +++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index dfbe8c4b..02295f25 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -350,6 +350,21 @@ export class SourceTracking extends AsyncCreatable { .filter((filename) => filename) .map((filename) => sourceComponentByFileName.set(filename, component)) ); + + // calculate what to return before we delete any files and .walkContent is no longer valid + const changedToBeDeleted = changesToDelete.reduce((result, component) => { + [...component.walkContent(), component.xml].flatMap((file) => { + result.push({ + state: ComponentStatus.Deleted, + filePath: file, + type: component.type.name, + fullName: component.fullName, + }); + }); + + return result; + }, []); + const filenames = Array.from(sourceComponentByFileName.keys()); // delete the files await Promise.all( @@ -377,15 +392,21 @@ export class SourceTracking extends AsyncCreatable { (label) => !customLabelsToDelete.includes(label.fullName) ); - const builder = new XMLBuilder({ - attributeNamePrefix: '@_', - ignoreAttributes: false, - format: true, - indentBy: ' ', - }); - // and then write that json back to xml and back to the fs - const xml = builder.build(customLabels) as string; - return fs.promises.writeFile(filename, xml); + if (customLabels.CustomLabels.labels.length === 0) { + // we've deleted everything, so let's delete the file + return fs.promises.unlink(filename); + } else { + // we need to write the file json back to xml back to the fs + const builder = new XMLBuilder({ + attributeNamePrefix: '@_', + ignoreAttributes: false, + format: true, + indentBy: ' ', + }); + // and then write that json back to xml and back to the fs + const xml = builder.build(customLabels) as string; + return fs.promises.writeFile(filename, xml); + } } } else { return fs.promises.unlink(filename); @@ -406,16 +427,7 @@ export class SourceTracking extends AsyncCreatable { ), ]); - return changesToDelete.reduce((result, component) => { - result.push({ - state: ComponentStatus.Deleted, - filePath: component.content ?? (component.xml as string), - type: component.type.name, - fullName: component.fullName, - }); - - return result; - }, []); + return changedToBeDeleted; } /** From 41d43f6d874330059f32dd3d443e7b6499af0063 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 11:52:46 -0600 Subject: [PATCH 09/14] chore: move CL delete to static method --- src/sourceTracking.ts | 90 +++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index 02295f25..3409b675 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -107,6 +107,54 @@ export class SourceTracking extends AsyncCreatable { this.subscribeSDREvents = options.subscribeSDREvents ?? false; } + /** + * A static method to help delete custom labels from a file, or the entire file if there are no more labels + * + * @param filename - a path to a custom labels file + * @param customLabels - an array of SourceComponents representing the custom labels to delete + */ + public static async deleteCustomLabels(filename: string, customLabels: SourceComponent[]): Promise { + // for custom labels, we need to remove the individual label from the xml file + // so we'll parse the xml + const parser = new XMLParser({ + ignoreDeclaration: false, + ignoreAttributes: false, + attributeNamePrefix: '@_', + }); + const cls = parser.parse(fs.readFileSync(filename, 'utf8')) as { + CustomLabels: { labels: Array<{ fullName: string }> | { fullName: string } }; + }; + if ('fullName' in cls.CustomLabels.labels) { + // a single custom label remains, delete the entire file + return fs.promises.unlink(filename); + } else { + // in theory, we should only have custom labels passed in, but filter just to make sure + const customLabelsToDelete = customLabels + .filter((label) => label.type.id === 'customlabel') + .map((change) => change.fullName); + // delete the labels from the json based on their fullName's + cls.CustomLabels.labels = cls.CustomLabels.labels.filter( + (label) => !customLabelsToDelete.includes(label.fullName) + ); + + if (cls.CustomLabels.labels.length === 0) { + // we've deleted everything, so let's delete the file + return fs.promises.unlink(filename); + } else { + // we need to write the file json back to xml back to the fs + const builder = new XMLBuilder({ + attributeNamePrefix: '@_', + ignoreAttributes: false, + format: true, + indentBy: ' ', + }); + // and then write that json back to xml and back to the fs + const xml = builder.build(cls) as string; + return fs.promises.writeFile(filename, xml); + } + } + } + // eslint-disable-next-line class-methods-use-this public async init(): Promise { await this.maybeSubscribeLifecycleEvents(); @@ -370,44 +418,10 @@ export class SourceTracking extends AsyncCreatable { await Promise.all( filenames.map((filename) => { if (sourceComponentByFileName.get(filename)?.type.id === 'customlabel') { - // for custom labels, we need to remove the individual label from the xml file - // so we'll parse the xml - const parser = new XMLParser({ - ignoreDeclaration: false, - ignoreAttributes: false, - attributeNamePrefix: '@_', - }); - const customLabels = parser.parse(fs.readFileSync(filename, 'utf8')) as { - CustomLabels: { labels: Array<{ fullName: string }> | { fullName: string } }; - }; - if ('fullName' in customLabels.CustomLabels.labels) { - // a single custom label remains, delete the entire file - return fs.promises.unlink(filename); - } else { - const customLabelsToDelete = changesToDelete - .filter((change) => change.type.name === 'CustomLabel') - .map((change) => change.fullName); - // delete the labels from the json based on their fullName's - customLabels.CustomLabels.labels = customLabels.CustomLabels.labels.filter( - (label) => !customLabelsToDelete.includes(label.fullName) - ); - - if (customLabels.CustomLabels.labels.length === 0) { - // we've deleted everything, so let's delete the file - return fs.promises.unlink(filename); - } else { - // we need to write the file json back to xml back to the fs - const builder = new XMLBuilder({ - attributeNamePrefix: '@_', - ignoreAttributes: false, - format: true, - indentBy: ' ', - }); - // and then write that json back to xml and back to the fs - const xml = builder.build(customLabels) as string; - return fs.promises.writeFile(filename, xml); - } - } + return SourceTracking.deleteCustomLabels( + filename, + changesToDelete.filter((change) => change.type.id === 'customlabel') + ); } else { return fs.promises.unlink(filename); } From 866825a69f0cc59cf6c22b546ab897aeb2df01e3 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 13:13:17 -0600 Subject: [PATCH 10/14] test: add UT for static method --- test/unit/sourceTracking.test.ts | 150 +++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 test/unit/sourceTracking.test.ts diff --git a/test/unit/sourceTracking.test.ts b/test/unit/sourceTracking.test.ts new file mode 100644 index 00000000..dd4deb0b --- /dev/null +++ b/test/unit/sourceTracking.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import * as fs from 'fs'; +import * as sinon from 'sinon'; +import { SourceComponent } from '@salesforce/source-deploy-retrieve'; +import { expect } from 'chai'; +import { SourceTracking } from '../../src'; + +describe('SourceTracking', () => { + const sandbox = sinon.createSandbox(); + let fsReadStub: sinon.SinonStub; + let fsWriteStub: sinon.SinonStub; + let fsUnlinkStub: sinon.SinonStub; + + beforeEach(() => { + fsWriteStub = sandbox.stub(fs.promises, 'writeFile'); + fsUnlinkStub = sandbox.stub(fs.promises, 'unlink'); + fsReadStub = sandbox + .stub(fs, 'readFileSync') + .returns( + '\n' + + '\n' + + ' \n' + + ' DeleteMe\n' + + ' en_US\n' + + ' true\n' + + ' DeleteMe\n' + + ' Test\n' + + ' \n' + + ' \n' + + ' KeepMe1\n' + + ' en_US\n' + + ' true\n' + + ' KeepMe1\n' + + ' Test\n' + + ' \n' + + ' \n' + + ' KeepMe2\n' + + ' en_US\n' + + ' true\n' + + ' KeepMe2\n' + + ' Test\n' + + ' \n' + + '\n' + ); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('deleteCustomLabels', () => { + it('will delete a singular custom label from a file', async () => { + const labels = [ + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'DeleteMe', + } as SourceComponent, + ]; + + await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(fsWriteStub.firstCall.args[1]).to.not.include('DeleteMe'); + expect(fsWriteStub.firstCall.args[1]).to.include('KeepMe1'); + expect(fsWriteStub.firstCall.args[1]).to.include('KeepMe2'); + expect(fsReadStub.callCount).to.equal(1); + }); + it('will delete a multiple custom labels from a file', async () => { + const labels = [ + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'KeepMe1', + }, + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'KeepMe2', + }, + ] as SourceComponent[]; + + await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(fsWriteStub.firstCall.args[1]).to.include('DeleteMe'); + expect(fsWriteStub.firstCall.args[1]).to.not.include('KeepMe1'); + expect(fsWriteStub.firstCall.args[1]).to.not.include('KeepMe2'); + expect(fsReadStub.callCount).to.equal(1); + }); + + it('will delete the file when everything is deleted', async () => { + const labels = [ + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'KeepMe1', + }, + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'KeepMe2', + }, + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'DeleteMe', + }, + ] as SourceComponent[]; + + await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(fsUnlinkStub.callCount).to.equal(1); + expect(fsReadStub.callCount).to.equal(1); + }); + + it('will delete the file when only a single label is present and deleted', async () => { + fsReadStub.returns( + '\n' + + '\n' + + ' \n' + + ' DeleteMe\n' + + ' en_US\n' + + ' true\n' + + ' DeleteMe\n' + + ' Test\n' + + ' \n' + + '\n' + ); + const labels = [ + { + type: { id: 'customlabel', name: 'CustomLabel' }, + fullName: 'DeleteMe', + }, + ] as SourceComponent[]; + + await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(fsUnlinkStub.callCount).to.equal(1); + expect(fsReadStub.callCount).to.equal(1); + }); + + it('will not delete custom labels', async () => { + const labels = [ + { + type: { id: 'apexclass', name: 'ApexClass' }, + fullName: 'DeleteMe', + }, + ] as SourceComponent[]; + + await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(fsUnlinkStub.callCount).to.equal(0); + expect(fsWriteStub.firstCall.args[1]).to.include('DeleteMe'); + expect(fsReadStub.callCount).to.equal(1); + }); + }); +}); From eb4148c93fe92b1fa7707fd1909fca31bb79f311 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 11 May 2023 15:23:24 -0600 Subject: [PATCH 11/14] chore: top level function, other review improvements --- src/index.ts | 2 +- src/shared/functions.ts | 51 +++++++++++++++++ src/sourceTracking.ts | 57 +------------------ ...ing.test.ts => deleteCustomLabels.test.ts} | 28 ++++----- 4 files changed, 68 insertions(+), 70 deletions(-) rename test/unit/{sourceTracking.test.ts => deleteCustomLabels.test.ts} (82%) diff --git a/src/index.ts b/src/index.ts index cf4d56cc..e99880e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,4 +16,4 @@ export { ConflictResponse, SourceConflictError, } from './shared/types'; -export { getKeyFromObject } from './shared/functions'; +export { getKeyFromObject, deleteCustomLabels } from './shared/functions'; diff --git a/src/shared/functions.ts b/src/shared/functions.ts index 7cab4f8e..0e23fdfc 100644 --- a/src/shared/functions.ts +++ b/src/shared/functions.ts @@ -6,8 +6,11 @@ */ import { sep, normalize, isAbsolute, relative } from 'path'; +import * as fs from 'fs'; import { isString } from '@salesforce/ts-types'; import { SourceComponent } from '@salesforce/source-deploy-retrieve'; +import { XMLBuilder, XMLParser } from 'fast-xml-parser'; +import { ensureArray } from '@salesforce/kit'; import { RemoteChangeElement, ChangeResult } from './types'; export const getMetadataKey = (metadataType: string, metadataName: string): string => @@ -45,3 +48,51 @@ export const chunkArray = (arr: T[], size: number): T[][] => export const ensureRelative = (filePath: string, projectPath: string): string => isAbsolute(filePath) ? relative(projectPath, filePath) : filePath; + +/** + * A method to help delete custom labels from a file, or the entire file if there are no more labels + * + * @param filename - a path to a custom labels file + * @param customLabels - an array of SourceComponents representing the custom labels to delete + */ +export const deleteCustomLabels = (filename: string, customLabels: SourceComponent[]): Promise => { + const customLabelsToDelete = customLabels + .filter((label) => label.type.id === 'customlabel') + .map((change) => change.fullName); + + // if we don't have custom labels, we don't need to do anything + if (!customLabelsToDelete.length) { + return Promise.resolve(); + } + // for custom labels, we need to remove the individual label from the xml file + // so we'll parse the xml + const parser = new XMLParser({ + ignoreDeclaration: false, + ignoreAttributes: false, + attributeNamePrefix: '@_', + }); + const cls = parser.parse(fs.readFileSync(filename, 'utf8')) as { + CustomLabels: { labels: Array<{ fullName: string }> }; + }; + + // delete the labels from the json based on their fullName's + cls.CustomLabels.labels = ensureArray(cls.CustomLabels.labels).filter( + (label) => !customLabelsToDelete.includes(label.fullName) + ); + + if (cls.CustomLabels.labels.length === 0) { + // we've deleted everything, so let's delete the file + return fs.promises.unlink(filename); + } else { + // we need to write the file json back to xml back to the fs + const builder = new XMLBuilder({ + attributeNamePrefix: '@_', + ignoreAttributes: false, + format: true, + indentBy: ' ', + }); + // and then write that json back to xml and back to the fs + const xml = builder.build(cls) as string; + return fs.promises.writeFile(filename, xml); + } +}; diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index 3409b675..a468bccf 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -27,8 +27,6 @@ import { } from '@salesforce/source-deploy-retrieve'; // this is not exported by SDR (see the comments in SDR regarding its limitations) import { filePathsFromMetadataComponent } from '@salesforce/source-deploy-retrieve/lib/src/utils/filePathGenerator'; - -import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { RemoteSourceTrackingService, remoteChangeElementToChangeResult } from './shared/remoteSourceTrackingService'; import { ShadowRepo } from './shared/localShadowRepo'; import { throwIfConflicts, findConflictsInComponentSet, getDedupedConflictsFromChanges } from './shared/conflicts'; @@ -42,7 +40,7 @@ import { RemoteChangeElement, } from './shared/types'; import { sourceComponentGuard } from './shared/guards'; -import { supportsPartialDelete, pathIsInFolder, ensureRelative } from './shared/functions'; +import { supportsPartialDelete, pathIsInFolder, ensureRelative, deleteCustomLabels } from './shared/functions'; import { registrySupportsType } from './shared/metadataKeys'; import { populateFilePaths } from './shared/populateFilePaths'; import { populateTypesAndNames } from './shared/populateTypesAndNames'; @@ -107,54 +105,6 @@ export class SourceTracking extends AsyncCreatable { this.subscribeSDREvents = options.subscribeSDREvents ?? false; } - /** - * A static method to help delete custom labels from a file, or the entire file if there are no more labels - * - * @param filename - a path to a custom labels file - * @param customLabels - an array of SourceComponents representing the custom labels to delete - */ - public static async deleteCustomLabels(filename: string, customLabels: SourceComponent[]): Promise { - // for custom labels, we need to remove the individual label from the xml file - // so we'll parse the xml - const parser = new XMLParser({ - ignoreDeclaration: false, - ignoreAttributes: false, - attributeNamePrefix: '@_', - }); - const cls = parser.parse(fs.readFileSync(filename, 'utf8')) as { - CustomLabels: { labels: Array<{ fullName: string }> | { fullName: string } }; - }; - if ('fullName' in cls.CustomLabels.labels) { - // a single custom label remains, delete the entire file - return fs.promises.unlink(filename); - } else { - // in theory, we should only have custom labels passed in, but filter just to make sure - const customLabelsToDelete = customLabels - .filter((label) => label.type.id === 'customlabel') - .map((change) => change.fullName); - // delete the labels from the json based on their fullName's - cls.CustomLabels.labels = cls.CustomLabels.labels.filter( - (label) => !customLabelsToDelete.includes(label.fullName) - ); - - if (cls.CustomLabels.labels.length === 0) { - // we've deleted everything, so let's delete the file - return fs.promises.unlink(filename); - } else { - // we need to write the file json back to xml back to the fs - const builder = new XMLBuilder({ - attributeNamePrefix: '@_', - ignoreAttributes: false, - format: true, - indentBy: ' ', - }); - // and then write that json back to xml and back to the fs - const xml = builder.build(cls) as string; - return fs.promises.writeFile(filename, xml); - } - } - } - // eslint-disable-next-line class-methods-use-this public async init(): Promise { await this.maybeSubscribeLifecycleEvents(); @@ -418,10 +368,7 @@ export class SourceTracking extends AsyncCreatable { await Promise.all( filenames.map((filename) => { if (sourceComponentByFileName.get(filename)?.type.id === 'customlabel') { - return SourceTracking.deleteCustomLabels( - filename, - changesToDelete.filter((change) => change.type.id === 'customlabel') - ); + return deleteCustomLabels(filename, changesToDelete); } else { return fs.promises.unlink(filename); } diff --git a/test/unit/sourceTracking.test.ts b/test/unit/deleteCustomLabels.test.ts similarity index 82% rename from test/unit/sourceTracking.test.ts rename to test/unit/deleteCustomLabels.test.ts index dd4deb0b..314a3d70 100644 --- a/test/unit/sourceTracking.test.ts +++ b/test/unit/deleteCustomLabels.test.ts @@ -8,9 +8,9 @@ import * as fs from 'fs'; import * as sinon from 'sinon'; import { SourceComponent } from '@salesforce/source-deploy-retrieve'; import { expect } from 'chai'; -import { SourceTracking } from '../../src'; +import { deleteCustomLabels } from '../../src/'; -describe('SourceTracking', () => { +describe('deleteCustomLabels', () => { const sandbox = sinon.createSandbox(); let fsReadStub: sinon.SinonStub; let fsWriteStub: sinon.SinonStub; @@ -54,7 +54,7 @@ describe('SourceTracking', () => { }); describe('deleteCustomLabels', () => { - it('will delete a singular custom label from a file', async () => { + it('will delete a singular custom label from a file', () => { const labels = [ { type: { id: 'customlabel', name: 'CustomLabel' }, @@ -62,13 +62,13 @@ describe('SourceTracking', () => { } as SourceComponent, ]; - await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); expect(fsWriteStub.firstCall.args[1]).to.not.include('DeleteMe'); expect(fsWriteStub.firstCall.args[1]).to.include('KeepMe1'); expect(fsWriteStub.firstCall.args[1]).to.include('KeepMe2'); expect(fsReadStub.callCount).to.equal(1); }); - it('will delete a multiple custom labels from a file', async () => { + it('will delete a multiple custom labels from a file', () => { const labels = [ { type: { id: 'customlabel', name: 'CustomLabel' }, @@ -80,14 +80,14 @@ describe('SourceTracking', () => { }, ] as SourceComponent[]; - await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); expect(fsWriteStub.firstCall.args[1]).to.include('DeleteMe'); expect(fsWriteStub.firstCall.args[1]).to.not.include('KeepMe1'); expect(fsWriteStub.firstCall.args[1]).to.not.include('KeepMe2'); expect(fsReadStub.callCount).to.equal(1); }); - it('will delete the file when everything is deleted', async () => { + it('will delete the file when everything is deleted', () => { const labels = [ { type: { id: 'customlabel', name: 'CustomLabel' }, @@ -103,12 +103,12 @@ describe('SourceTracking', () => { }, ] as SourceComponent[]; - await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); expect(fsUnlinkStub.callCount).to.equal(1); expect(fsReadStub.callCount).to.equal(1); }); - it('will delete the file when only a single label is present and deleted', async () => { + it('will delete the file when only a single label is present and deleted', () => { fsReadStub.returns( '\n' + '\n' + @@ -128,12 +128,12 @@ describe('SourceTracking', () => { }, ] as SourceComponent[]; - await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); expect(fsUnlinkStub.callCount).to.equal(1); expect(fsReadStub.callCount).to.equal(1); }); - it('will not delete custom labels', async () => { + it('no custom labels, quick exit and do nothing', () => { const labels = [ { type: { id: 'apexclass', name: 'ApexClass' }, @@ -141,10 +141,10 @@ describe('SourceTracking', () => { }, ] as SourceComponent[]; - await SourceTracking.deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); expect(fsUnlinkStub.callCount).to.equal(0); - expect(fsWriteStub.firstCall.args[1]).to.include('DeleteMe'); - expect(fsReadStub.callCount).to.equal(1); + expect(fsWriteStub.callCount).to.equal(0); + expect(fsReadStub.callCount).to.equal(0); }); }); }); From d06ac3fbf56fc606f4707e2ded84a5dfc72b1ff5 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 12 May 2023 10:34:53 -0500 Subject: [PATCH 12/14] chore: bump testkit --- package.json | 2 +- yarn.lock | 35 ++++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index d963e491..6fcd102d 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "ts-retry-promise": "^0.7.0" }, "devDependencies": { - "@salesforce/cli-plugins-testkit": "^3.3.2", + "@salesforce/cli-plugins-testkit": "^3.3.6", "@salesforce/dev-config": "^3.1.0", "@salesforce/dev-scripts": "^4.3.0", "@salesforce/prettier-config": "^0.0.2", diff --git a/yarn.lock b/yarn.lock index c6c4b86e..1bfa351f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -875,15 +875,15 @@ mv "~2" safe-json-stringify "~1" -"@salesforce/cli-plugins-testkit@^3.3.2": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@salesforce/cli-plugins-testkit/-/cli-plugins-testkit-3.3.2.tgz#5a5bd6f1c505962b97682463c0a204fb6b67b5bf" - integrity sha512-lXxm9tYNRD34GdcHtYVcFIccG9k5v8Ai/O1ytHE4zOoDowmeyB5l47O7Qo8blmeYbggRJUQIFfao2EPrJlTKCQ== +"@salesforce/cli-plugins-testkit@^3.3.6": + version "3.3.6" + resolved "https://registry.yarnpkg.com/@salesforce/cli-plugins-testkit/-/cli-plugins-testkit-3.3.6.tgz#89c17e87a58e1d0bd6006b65568a6883a17e7e5b" + integrity sha512-40o363ISrdX1FNyt0YLBouQMMMySXS0YLhozTqSjtZDHZn/9gfcVH1LOz9XsOUd1C4SwnVnVjHH2bdVfHzxRWA== dependencies: - "@salesforce/core" "^3.34.6" + "@salesforce/core" "^3.34.8" "@salesforce/kit" "^1.9.2" "@salesforce/ts-types" "^1.7.3" - "@types/shelljs" "^0.8.11" + "@types/shelljs" "^0.8.12" archiver "^5.2.0" debug "^4.3.1" shelljs "^0.8.4" @@ -913,6 +913,27 @@ jsonwebtoken "9.0.0" ts-retry-promise "^0.7.0" +"@salesforce/core@^3.34.8": + version "3.36.1" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.1.tgz#053a5e1079b9749b62e461e6ac3e630b5689694a" + integrity sha512-kcjyr9bj35nnL8Bqv8U39xeho3CrZYXJiS/X5X1eEHVNZLd9zckrmKrh1V7z8ElCFpsJrewT989SJsdvi9kE8w== + dependencies: + "@salesforce/bunyan" "^2.0.0" + "@salesforce/kit" "^1.9.2" + "@salesforce/schemas" "^1.5.1" + "@salesforce/ts-types" "^1.7.2" + "@types/semver" "^7.3.13" + ajv "^8.12.0" + archiver "^5.3.0" + change-case "^4.1.2" + debug "^3.2.7" + faye "^1.4.0" + form-data "^4.0.0" + js2xmlparser "^4.0.1" + jsforce "^2.0.0-beta.21" + jsonwebtoken "9.0.0" + ts-retry-promise "^0.7.0" + "@salesforce/core@^3.36.0": version "3.36.0" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.0.tgz#cbff147d888eee0b921e368c1fdc1a1a3c2eacab" @@ -1289,7 +1310,7 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== -"@types/shelljs@^0.8.11": +"@types/shelljs@^0.8.11", "@types/shelljs@^0.8.12": version "0.8.12" resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.12.tgz#79dc9632af7d5ca1b5afb65a6bfc1422d79b5fa0" integrity sha512-ZA8U81/gldY+rR5zl/7HSHrG2KDfEb3lzG6uCUDhW1DTQE9yC/VBQ45fXnXq8f3CgInfhZmjtdu/WOUlrXRQUg== From e9cd467f4bb1a0355a852e02ce76711d0979ed28 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 16 May 2023 10:43:19 -0600 Subject: [PATCH 13/14] chore: remove deleting CLs from other methods --- src/sourceTracking.ts | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/sourceTracking.ts b/src/sourceTracking.ts index a468bccf..7f19d68f 100644 --- a/src/sourceTracking.ts +++ b/src/sourceTracking.ts @@ -40,7 +40,7 @@ import { RemoteChangeElement, } from './shared/types'; import { sourceComponentGuard } from './shared/guards'; -import { supportsPartialDelete, pathIsInFolder, ensureRelative, deleteCustomLabels } from './shared/functions'; +import { supportsPartialDelete, pathIsInFolder, ensureRelative } from './shared/functions'; import { registrySupportsType } from './shared/metadataKeys'; import { populateFilePaths } from './shared/populateFilePaths'; import { populateTypesAndNames } from './shared/populateTypesAndNames'; @@ -348,32 +348,9 @@ export class SourceTracking extends AsyncCreatable { .filter((filename) => filename) .map((filename) => sourceComponentByFileName.set(filename, component)) ); - - // calculate what to return before we delete any files and .walkContent is no longer valid - const changedToBeDeleted = changesToDelete.reduce((result, component) => { - [...component.walkContent(), component.xml].flatMap((file) => { - result.push({ - state: ComponentStatus.Deleted, - filePath: file, - type: component.type.name, - fullName: component.fullName, - }); - }); - - return result; - }, []); - const filenames = Array.from(sourceComponentByFileName.keys()); // delete the files - await Promise.all( - filenames.map((filename) => { - if (sourceComponentByFileName.get(filename)?.type.id === 'customlabel') { - return deleteCustomLabels(filename, changesToDelete); - } else { - return fs.promises.unlink(filename); - } - }) - ); + await Promise.all(filenames.map((filename) => fs.promises.unlink(filename))); // update the tracking files. We're simulating SDR-style fileResponse await Promise.all([ @@ -387,8 +364,19 @@ export class SourceTracking extends AsyncCreatable { true // skip polling because it's a pull ), ]); + return filenames.reduce((result, filename) => { + const component = sourceComponentByFileName.get(filename); + if (component) { + result.push({ + state: ComponentStatus.Deleted, + filePath: filename, + type: component.type.name, + fullName: component.fullName, + }); + } - return changedToBeDeleted; + return result; + }, []); } /** From 31691a67c7dc3f963ae3eca42706fe732e6d00b2 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Wed, 17 May 2023 16:31:56 -0500 Subject: [PATCH 14/14] refactor: return file as object for consumers and tests (#388) * refactor: return file as object for consumers and tests * chore: dedupe pjson --- package.json | 4 +- src/shared/functions.ts | 22 ++++--- test/unit/deleteCustomLabels.test.ts | 33 +++++----- yarn.lock | 93 +--------------------------- 4 files changed, 34 insertions(+), 118 deletions(-) diff --git a/package.json b/package.json index e78ca556..84b4e94e 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,6 @@ "/oclif.manifest.json" ], "dependencies": { - "@salesforce/core": "^3.36.0", - "@salesforce/kit": "^1.9.2", - "@salesforce/source-deploy-retrieve": "^8.4.0", "@salesforce/core": "^3.36.1", "@salesforce/kit": "^1.9.2", "@salesforce/source-deploy-retrieve": "^8.4.0", @@ -60,6 +57,7 @@ "@salesforce/dev-scripts": "^4.3.0", "@salesforce/prettier-config": "^0.0.2", "@salesforce/ts-sinon": "^1.4.6", + "@types/graceful-fs": "^4.1.6", "@types/shelljs": "^0.8.11", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", diff --git a/src/shared/functions.ts b/src/shared/functions.ts index 0e23fdfc..dbf71635 100644 --- a/src/shared/functions.ts +++ b/src/shared/functions.ts @@ -49,20 +49,28 @@ export const chunkArray = (arr: T[], size: number): T[][] => export const ensureRelative = (filePath: string, projectPath: string): string => isAbsolute(filePath) ? relative(projectPath, filePath) : filePath; +export type ParsedCustomLabels = { + CustomLabels: { labels: Array<{ fullName: string }> }; +}; + /** * A method to help delete custom labels from a file, or the entire file if there are no more labels * * @param filename - a path to a custom labels file * @param customLabels - an array of SourceComponents representing the custom labels to delete + * @returns -json equivalent of the custom labels file's contents OR undefined if the file was deleted/not written */ -export const deleteCustomLabels = (filename: string, customLabels: SourceComponent[]): Promise => { +export const deleteCustomLabels = async ( + filename: string, + customLabels: SourceComponent[] +): Promise => { const customLabelsToDelete = customLabels .filter((label) => label.type.id === 'customlabel') .map((change) => change.fullName); // if we don't have custom labels, we don't need to do anything if (!customLabelsToDelete.length) { - return Promise.resolve(); + return undefined; } // for custom labels, we need to remove the individual label from the xml file // so we'll parse the xml @@ -71,9 +79,7 @@ export const deleteCustomLabels = (filename: string, customLabels: SourceCompone ignoreAttributes: false, attributeNamePrefix: '@_', }); - const cls = parser.parse(fs.readFileSync(filename, 'utf8')) as { - CustomLabels: { labels: Array<{ fullName: string }> }; - }; + const cls = parser.parse(fs.readFileSync(filename, 'utf8')) as ParsedCustomLabels; // delete the labels from the json based on their fullName's cls.CustomLabels.labels = ensureArray(cls.CustomLabels.labels).filter( @@ -82,7 +88,8 @@ export const deleteCustomLabels = (filename: string, customLabels: SourceCompone if (cls.CustomLabels.labels.length === 0) { // we've deleted everything, so let's delete the file - return fs.promises.unlink(filename); + await fs.promises.unlink(filename); + return undefined; } else { // we need to write the file json back to xml back to the fs const builder = new XMLBuilder({ @@ -93,6 +100,7 @@ export const deleteCustomLabels = (filename: string, customLabels: SourceCompone }); // and then write that json back to xml and back to the fs const xml = builder.build(cls) as string; - return fs.promises.writeFile(filename, xml); + await fs.promises.writeFile(filename, xml); + return cls; } }; diff --git a/test/unit/deleteCustomLabels.test.ts b/test/unit/deleteCustomLabels.test.ts index 314a3d70..ddee8e9b 100644 --- a/test/unit/deleteCustomLabels.test.ts +++ b/test/unit/deleteCustomLabels.test.ts @@ -54,7 +54,7 @@ describe('deleteCustomLabels', () => { }); describe('deleteCustomLabels', () => { - it('will delete a singular custom label from a file', () => { + it('will delete a singular custom label from a file', async () => { const labels = [ { type: { id: 'customlabel', name: 'CustomLabel' }, @@ -62,13 +62,12 @@ describe('deleteCustomLabels', () => { } as SourceComponent, ]; - void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); - expect(fsWriteStub.firstCall.args[1]).to.not.include('DeleteMe'); - expect(fsWriteStub.firstCall.args[1]).to.include('KeepMe1'); - expect(fsWriteStub.firstCall.args[1]).to.include('KeepMe2'); + const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + const resultLabels = result?.CustomLabels.labels.map((label) => label.fullName); + expect(resultLabels).to.deep.equal(['KeepMe1', 'KeepMe2']); expect(fsReadStub.callCount).to.equal(1); }); - it('will delete a multiple custom labels from a file', () => { + it('will delete a multiple custom labels from a file', async () => { const labels = [ { type: { id: 'customlabel', name: 'CustomLabel' }, @@ -80,14 +79,13 @@ describe('deleteCustomLabels', () => { }, ] as SourceComponent[]; - void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); - expect(fsWriteStub.firstCall.args[1]).to.include('DeleteMe'); - expect(fsWriteStub.firstCall.args[1]).to.not.include('KeepMe1'); - expect(fsWriteStub.firstCall.args[1]).to.not.include('KeepMe2'); + const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + const resultLabels = result?.CustomLabels.labels.map((label) => label.fullName); + expect(resultLabels).to.deep.equal(['DeleteMe']); expect(fsReadStub.callCount).to.equal(1); }); - it('will delete the file when everything is deleted', () => { + it('will delete the file when everything is deleted', async () => { const labels = [ { type: { id: 'customlabel', name: 'CustomLabel' }, @@ -103,12 +101,13 @@ describe('deleteCustomLabels', () => { }, ] as SourceComponent[]; - void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(result).to.equal(undefined); expect(fsUnlinkStub.callCount).to.equal(1); expect(fsReadStub.callCount).to.equal(1); }); - it('will delete the file when only a single label is present and deleted', () => { + it('will delete the file when only a single label is present and deleted', async () => { fsReadStub.returns( '\n' + '\n' + @@ -128,12 +127,13 @@ describe('deleteCustomLabels', () => { }, ] as SourceComponent[]; - void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(result).to.equal(undefined); expect(fsUnlinkStub.callCount).to.equal(1); expect(fsReadStub.callCount).to.equal(1); }); - it('no custom labels, quick exit and do nothing', () => { + it('no custom labels, quick exit and do nothing', async () => { const labels = [ { type: { id: 'apexclass', name: 'ApexClass' }, @@ -141,7 +141,8 @@ describe('deleteCustomLabels', () => { }, ] as SourceComponent[]; - void deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels); + expect(result).to.equal(undefined); expect(fsUnlinkStub.callCount).to.equal(0); expect(fsWriteStub.callCount).to.equal(0); expect(fsReadStub.callCount).to.equal(0); diff --git a/yarn.lock b/yarn.lock index 03f2ec8d..25eb2a00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -890,30 +890,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.7.0" -"@salesforce/core@^3.34.6": - version "3.34.7" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.34.7.tgz#445efe5c76fbab53e6c891563aa9b0dc5dd24179" - integrity sha512-C4zyXzLAV5ITMChC8dCP+6Kk3t5vloyP2eXpqBOw96OzF5OaCiN5/TayNN8YJl64pvFFny7FgAQPKk7omFXNSA== - dependencies: - "@salesforce/bunyan" "^2.0.0" - "@salesforce/kit" "^1.9.2" - "@salesforce/schemas" "^1.5.1" - "@salesforce/ts-types" "^1.7.2" - "@types/graceful-fs" "^4.1.6" - "@types/semver" "^7.3.13" - ajv "^8.12.0" - archiver "^5.3.0" - change-case "^4.1.2" - debug "^3.2.7" - faye "^1.4.0" - form-data "^4.0.0" - graceful-fs "^4.2.11" - js2xmlparser "^4.0.1" - jsforce "^2.0.0-beta.21" - jsonwebtoken "9.0.0" - ts-retry-promise "^0.7.0" - -"@salesforce/core@^3.34.8", "@salesforce/core@^3.36.0", "@salesforce/core@^3.36.1": +"@salesforce/core@^3.34.6", "@salesforce/core@^3.34.8", "@salesforce/core@^3.36.0", "@salesforce/core@^3.36.1": version "3.36.1" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.1.tgz#053a5e1079b9749b62e461e6ac3e630b5689694a" integrity sha512-kcjyr9bj35nnL8Bqv8U39xeho3CrZYXJiS/X5X1eEHVNZLd9zckrmKrh1V7z8ElCFpsJrewT989SJsdvi9kE8w== @@ -934,48 +911,6 @@ jsonwebtoken "9.0.0" ts-retry-promise "^0.7.0" -"@salesforce/core@^3.34.8": - version "3.36.1" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.1.tgz#053a5e1079b9749b62e461e6ac3e630b5689694a" - integrity sha512-kcjyr9bj35nnL8Bqv8U39xeho3CrZYXJiS/X5X1eEHVNZLd9zckrmKrh1V7z8ElCFpsJrewT989SJsdvi9kE8w== - dependencies: - "@salesforce/bunyan" "^2.0.0" - "@salesforce/kit" "^1.9.2" - "@salesforce/schemas" "^1.5.1" - "@salesforce/ts-types" "^1.7.2" - "@types/semver" "^7.3.13" - ajv "^8.12.0" - archiver "^5.3.0" - change-case "^4.1.2" - debug "^3.2.7" - faye "^1.4.0" - form-data "^4.0.0" - js2xmlparser "^4.0.1" - jsforce "^2.0.0-beta.21" - jsonwebtoken "9.0.0" - ts-retry-promise "^0.7.0" - -"@salesforce/core@^3.36.0": - version "3.36.0" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.0.tgz#cbff147d888eee0b921e368c1fdc1a1a3c2eacab" - integrity sha512-LOeSJgozf5B1S8C98/K3afewRsathfEm+HrXxaFXJjITFfIhxqcIDB5xC1cw0VikfRUlq7dQocmVsvezcK0Ghw== - dependencies: - "@salesforce/bunyan" "^2.0.0" - "@salesforce/kit" "^1.9.2" - "@salesforce/schemas" "^1.5.1" - "@salesforce/ts-types" "^1.7.2" - "@types/semver" "^7.3.13" - ajv "^8.12.0" - archiver "^5.3.0" - change-case "^4.1.2" - debug "^3.2.7" - faye "^1.4.0" - form-data "^4.0.0" - js2xmlparser "^4.0.1" - jsforce "^2.0.0-beta.21" - jsonwebtoken "9.0.0" - ts-retry-promise "^0.7.0" - "@salesforce/dev-config@^3.0.0", "@salesforce/dev-config@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-3.1.0.tgz#8eb5b35860ff60d1c1dc3fd9329b01a28475d5b9" @@ -4617,32 +4552,6 @@ jsforce@^2.0.0-beta.21: strip-ansi "^6.0.0" xml2js "^0.5.0" -jsforce@^2.0.0-beta.21: - version "2.0.0-beta.21" - resolved "https://registry.yarnpkg.com/jsforce/-/jsforce-2.0.0-beta.21.tgz#04c94d762d2536bf1af3062d5cca206656f5b12b" - integrity sha512-74GUF/96vYBNZo3SUccXtt4CmfvZ0iqTSc0Z3OB940Ec7oU6coOAGhlCZ+XprXaHOMMhXMXrZQ1PCd16yjIA7A== - dependencies: - "@babel/runtime" "^7.12.5" - "@babel/runtime-corejs3" "^7.12.5" - "@types/node" "^12.19.9" - abort-controller "^3.0.0" - base64url "^3.0.1" - commander "^4.0.1" - core-js "^3.6.4" - csv-parse "^4.8.2" - csv-stringify "^5.3.4" - faye "^1.4.0" - form-data "^4.0.0" - fs-extra "^8.1.0" - https-proxy-agent "^5.0.0" - inquirer "^7.0.0" - multistream "^3.1.0" - node-fetch "^2.6.1" - open "^7.0.0" - regenerator-runtime "^0.13.3" - strip-ansi "^6.0.0" - xml2js "^0.5.0" - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"