diff --git a/METADATA_SUPPORT.md b/METADATA_SUPPORT.md index 7172c7051e..53a953104f 100644 --- a/METADATA_SUPPORT.md +++ b/METADATA_SUPPORT.md @@ -533,6 +533,7 @@ v57 introduces the following new types. Here's their current level of support |ActionableListDefinition|❌|Not supported, but support could be added| |AffinityScoreDefinition|❌|Not supported, but support could be added| |ClauseCatgConfiguration|❌|Not supported, but support could be added| +|CommerceRuleSettings|✅|| |DisclosureDefinition|❌|Not supported, but support could be added| |DisclosureDefinitionVersion|❌|Not supported, but support could be added| |DisclosureType|❌|Not supported, but support could be added| @@ -540,8 +541,9 @@ v57 introduces the following new types. Here's their current level of support |ExternalClientAppSettings|✅|| |ExternalClientApplication|✅|| |ExternalDocStorageConfig|❌|Not supported, but support could be added| -|ExtlClntAppMobileSet|❌|Not supported, but support could be added| +|ExtlClntAppMobileSettings|✅|| |ExtlClntAppOauthPlcyCnfg|❌|Not supported, but support could be added| +|ExtlClntAppOauthSettings|✅|| |IdentityProviderSettings|✅|| |IntegrationProviderDef|❌|Not supported, but support could be added| |LocationUse|❌|Not supported, but support could be added| diff --git a/src/client/metadataApiDeploy.ts b/src/client/metadataApiDeploy.ts index e5d50d89f3..f1fadbfa5d 100644 --- a/src/client/metadataApiDeploy.ts +++ b/src/client/metadataApiDeploy.ts @@ -10,6 +10,7 @@ import { create as createArchive } from 'archiver'; import * as fs from 'graceful-fs'; import { Lifecycle, Messages, SfError } from '@salesforce/core'; import { ensureArray } from '@salesforce/kit'; +import { ReplacementEvent } from '../convert/types'; import { MetadataConverter } from '../convert'; import { ComponentLike, SourceComponent } from '../resolve'; import { ComponentSet } from '../collections'; @@ -31,16 +32,15 @@ Messages.importMessagesDirectory(__dirname); const messages = Messages.load('@salesforce/source-deploy-retrieve', 'sdr', ['error_no_job_id']); export class DeployResult implements MetadataTransferResult { - public readonly response: MetadataApiDeployStatus; - public readonly components: ComponentSet; private readonly diagnosticUtil = new DiagnosticUtil('metadata'); private fileResponses: FileResponse[]; private readonly shouldConvertPaths = sep !== posix.sep; - public constructor(response: MetadataApiDeployStatus, components: ComponentSet) { - this.response = response; - this.components = components; - } + public constructor( + public readonly response: MetadataApiDeployStatus, + public readonly components: ComponentSet, + public readonly replacements: Map = new Map() + ) {} public getFileResponses(): FileResponse[] { // this involves FS operations, so only perform once! @@ -236,6 +236,7 @@ export class MetadataApiDeploy extends MetadataTransfer = new Map(); private orgId: string; // Keep track of rest deploys separately since Connection.deploy() removes it // from the apiOptions and we need it for telemetry. @@ -310,6 +311,7 @@ export class MetadataApiDeploy extends MetadataTransfer { + const LifecycleInstance = Lifecycle.getInstance(); const connection = await this.getConnection(); // store for use in the scopedPostDeploy event this.orgId = connection.getAuthInfoFields().orgId; @@ -320,11 +322,26 @@ export class MetadataApiDeploy extends MetadataTransfer + // lifecycle have to be async, so wrapped in a promise + new Promise((resolve) => { + if (!this.replacements.has(replacement.filename)) { + this.replacements.set(replacement.filename, [replacement.replaced]); + } else { + this.replacements.get(replacement.filename).push(replacement.replaced); + } + resolve(); + }) + ); + const [zipBuffer] = await Promise.all([this.getZipBuffer(), this.maybeSaveTempDirectory('metadata')]); // SDR modifies what the mdapi expects by adding a rest param const { rest, ...optionsWithoutRest } = this.options.apiOptions; @@ -370,7 +387,7 @@ export class MetadataApiDeploy extends MetadataTransfer(); @@ -50,20 +50,30 @@ class ReplacementStream extends Transform { * emits warnings when an expected replacement target isn't found */ export const replacementIterations = async (input: string, replacements: MarkedReplacement[]): Promise => { + const lifecycleInstance = Lifecycle.getInstance(); let output = input; for (const replacement of replacements) { // TODO: node 16+ has String.replaceAll for non-regex scenarios const regex = typeof replacement.toReplace === 'string' ? new RegExp(replacement.toReplace, 'g') : replacement.toReplace; const replaced = output.replace(regex, replacement.replaceWith); - if (replacement.singleFile && replaced === output) { + + if (replaced !== output) { + output = replaced; + // eslint-disable-next-line no-await-in-loop + await lifecycleInstance.emit('replacement', { + filename: replacement.matchedFilename, + replaced: replacement.toReplace.toString(), + } as ReplacementEvent); + } else if (replacement.singleFile) { // replacements need to be done sequentially // eslint-disable-next-line no-await-in-loop - await Lifecycle.getInstance().emitWarning( - `Your sfdx-project.json specifies that ${replacement.toReplace.toString()} should be replaced, but it was not found.` + await lifecycleInstance.emitWarning( + `Your sfdx-project.json specifies that ${replacement.toReplace.toString()} should be replaced in ${ + replacement.matchedFilename + }, but it was not found.` ); } - output = replaced; } return output; }; @@ -144,6 +154,7 @@ export const getReplacements = async ( // filter out any that don't match the current file .filter((r) => matchesFile(f, r)) .map(async (r) => ({ + matchedFilename: f, // used during replacement stream to limit warnings to explicit filenames, not globs singleFile: Boolean(r.filename), // Config is json which might use the regex. If so, turn it into an actual regex diff --git a/src/convert/types.ts b/src/convert/types.ts index 026959dc92..2be5c60053 100644 --- a/src/convert/types.ts +++ b/src/convert/types.ts @@ -110,6 +110,7 @@ export type ConvertResult = { export type MarkedReplacement = { toReplace: string | RegExp; replaceWith: string; + matchedFilename: string; singleFile?: boolean; }; @@ -138,3 +139,8 @@ type ReplacementTarget = /** When putting regex into json, you have to use an extra backslash to escape your regex backslashes because JSON also treats backslash as an escape character */ regexToReplace: string; }; + +export type ReplacementEvent = { + filename: string; + replaced: string; +}; diff --git a/test/convert/replacements.test.ts b/test/convert/replacements.test.ts index ea992f9ed4..f7d575fb2f 100644 --- a/test/convert/replacements.test.ts +++ b/test/convert/replacements.test.ts @@ -63,6 +63,7 @@ describe('marking replacements on a component', () => { expect(result).to.deep.equal({ [cmp.xml]: [ { + matchedFilename: cmp.xml, toReplace: 'foo', replaceWith: 'bar', singleFile: true, @@ -75,6 +76,7 @@ describe('marking replacements on a component', () => { expect(result).to.deep.equal({ [cmp.xml]: [ { + matchedFilename: cmp.xml, toReplace: 'foo', replaceWith: 'bar', singleFile: true, @@ -90,6 +92,7 @@ describe('marking replacements on a component', () => { expect(result).to.deep.equal({ [cmp.xml]: [ { + matchedFilename: cmp.xml, toReplace: /.*foo.*/g, replaceWith: 'bar', singleFile: true, @@ -105,11 +108,13 @@ describe('marking replacements on a component', () => { expect(result).to.deep.equal({ [cmp.xml]: [ { + matchedFilename: cmp.xml, toReplace: 'foo', replaceWith: 'bar', singleFile: true, }, { + matchedFilename: cmp.xml, toReplace: 'baz', replaceWith: 'bar', singleFile: true, @@ -124,6 +129,7 @@ describe('marking replacements on a component', () => { expect(result).to.deep.equal({ [cmp.xml]: [ { + matchedFilename: cmp.xml, toReplace: 'foo', replaceWith: 'bar', singleFile: false, @@ -131,6 +137,7 @@ describe('marking replacements on a component', () => { ], [cmp.content]: [ { + matchedFilename: cmp.content, toReplace: 'foo', replaceWith: 'bar', singleFile: false, @@ -146,6 +153,7 @@ describe('marking replacements on a component', () => { expect(result).to.deep.equal({ [cmp.xml]: [ { + matchedFilename: cmp.xml, toReplace: 'foo', replaceWith: 'bar', singleFile: true, @@ -153,6 +161,7 @@ describe('marking replacements on a component', () => { ], [cmp.content]: [ { + matchedFilename: cmp.content, toReplace: 'foo', replaceWith: 'bar', singleFile: true, @@ -164,24 +173,27 @@ describe('marking replacements on a component', () => { }); describe('executes replacements on a string', () => { + const matchedFilename = 'foo'; describe('string', () => { it('basic replacement', async () => { expect( - await replacementIterations('ThisIsATest', [{ toReplace: 'This', replaceWith: 'That', singleFile: true }]) + await replacementIterations('ThisIsATest', [ + { matchedFilename, toReplace: 'This', replaceWith: 'That', singleFile: true }, + ]) ).to.equal('ThatIsATest'); }); it('same replacement occuring multiple times', async () => { expect( await replacementIterations('ThisIsATestWithThisAndThis', [ - { toReplace: 'This', replaceWith: 'That', singleFile: true }, + { matchedFilename, toReplace: 'This', replaceWith: 'That', singleFile: true }, ]) ).to.equal('ThatIsATestWithThatAndThat'); }); it('multiple replacements', async () => { expect( await replacementIterations('ThisIsATestWithThisAndThis', [ - { toReplace: 'This', replaceWith: 'That' }, - { toReplace: 'ATest', replaceWith: 'AnAwesomeTest' }, + { matchedFilename, toReplace: 'This', replaceWith: 'That' }, + { matchedFilename, toReplace: 'ATest', replaceWith: 'AnAwesomeTest' }, ]) ).to.equal('ThatIsAnAwesomeTestWithThatAndThat'); }); @@ -189,21 +201,23 @@ describe('executes replacements on a string', () => { describe('regex', () => { it('basic replacement', async () => { expect( - await replacementIterations('ThisIsATest', [{ toReplace: /Is/g, replaceWith: 'IsNot', singleFile: true }]) + await replacementIterations('ThisIsATest', [ + { toReplace: /Is/g, replaceWith: 'IsNot', singleFile: true, matchedFilename }, + ]) ).to.equal('ThisIsNotATest'); }); it('same replacement occuring multiple times', async () => { expect( await replacementIterations('ThisIsATestWithThisAndThis', [ - { toReplace: /s/g, replaceWith: 'S', singleFile: true }, + { toReplace: /s/g, replaceWith: 'S', singleFile: true, matchedFilename }, ]) ).to.equal('ThiSISATeStWithThiSAndThiS'); }); it('multiple replacements', async () => { expect( await replacementIterations('This Is A Test With This And This', [ - { toReplace: /^T.{2}s/, replaceWith: 'That', singleFile: false }, - { toReplace: /T.{2}s$/, replaceWith: 'Stuff', singleFile: false }, + { toReplace: /^T.{2}s/, replaceWith: 'That', singleFile: false, matchedFilename }, + { toReplace: /T.{2}s$/, replaceWith: 'Stuff', singleFile: false, matchedFilename }, ]) ).to.equal('That Is A Test With This And Stuff'); }); @@ -211,24 +225,38 @@ describe('executes replacements on a string', () => { describe('warning when no replacement happened', () => { let warnSpy: Sinon.SinonSpy; + let emitSpy: Sinon.SinonSpy; beforeEach(() => { + // everything is an emit. Warn calls emit, too. warnSpy = Sinon.spy(Lifecycle.getInstance(), 'emitWarning'); + emitSpy = Sinon.spy(Lifecycle.getInstance(), 'emit'); }); afterEach(() => { warnSpy.restore(); + emitSpy.restore(); }); it('emits warning only when no change', async () => { - await replacementIterations('ThisIsATest', [{ toReplace: 'Nope', replaceWith: 'Nah', singleFile: true }]); + await replacementIterations('ThisIsATest', [ + { toReplace: 'Nope', replaceWith: 'Nah', singleFile: true, matchedFilename }, + ]); expect(warnSpy.callCount).to.equal(1); + expect(emitSpy.callCount).to.equal(1); }); it('no warning when string is replaced', async () => { - await replacementIterations('ThisIsATest', [{ toReplace: 'Test', replaceWith: 'SpyTest', singleFile: true }]); + await replacementIterations('ThisIsATest', [ + { toReplace: 'Test', replaceWith: 'SpyTest', singleFile: true, matchedFilename }, + ]); expect(warnSpy.callCount).to.equal(0); + // because it emits the replacement event + expect(emitSpy.callCount).to.equal(1); }); it('no warning when no replacement but not a single file (ex: glob)', async () => { - await replacementIterations('ThisIsATest', [{ toReplace: 'Nope', replaceWith: 'Nah', singleFile: false }]); + await replacementIterations('ThisIsATest', [ + { toReplace: 'Nope', replaceWith: 'Nah', singleFile: false, matchedFilename }, + ]); expect(warnSpy.callCount).to.equal(0); + expect(emitSpy.callCount).to.equal(0); }); }); }); diff --git a/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/eda.json b/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/eda.json index 0f72f60780..bca290887b 100644 --- a/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/eda.json +++ b/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/eda.json @@ -1,18 +1,18 @@ [ { "name": "componentSetCreate", - "duration": 217.24560199998086 + "duration": 217.22331100000883 }, { "name": "sourceToMdapi", - "duration": 6034.749643999967 + "duration": 5528.8957320000045 }, { "name": "sourceToZip", - "duration": 4773.798967999988 + "duration": 4973.436105999979 }, { "name": "mdapiToSource", - "duration": 3753.1327789999777 + "duration": 3836.91319500003 } -] +] \ No newline at end of file diff --git a/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClasses.json b/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClasses.json index 3b8eae7955..57c2e7e73b 100644 --- a/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClasses.json +++ b/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClasses.json @@ -1,18 +1,18 @@ [ { "name": "componentSetCreate", - "duration": 445.2519559999928 + "duration": 412.3087709999527 }, { "name": "sourceToMdapi", - "duration": 8093.97652299999 + "duration": 7418.153514000005 }, { "name": "sourceToZip", - "duration": 6444.346208999981 + "duration": 6142.985255000007 }, { "name": "mdapiToSource", - "duration": 4924.401616999996 + "duration": 4409.731972999987 } -] +] \ No newline at end of file diff --git a/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClassesOneDir.json b/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClassesOneDir.json index 2862ad2241..64679058f6 100644 --- a/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClassesOneDir.json +++ b/test/nuts/perfResults/x64-linux-2xIntel-Xeon-Platinum-8272CL-CPU-2-60GHz/lotsOfClassesOneDir.json @@ -1,18 +1,18 @@ [ { "name": "componentSetCreate", - "duration": 751.4006550000049 + "duration": 702.3643789999769 }, { "name": "sourceToMdapi", - "duration": 11578.667857999972 + "duration": 11670.428872999968 }, { "name": "sourceToZip", - "duration": 9852.379058999999 + "duration": 10227.489082999993 }, { "name": "mdapiToSource", - "duration": 8332.220241000003 + "duration": 10798.030672999972 } -] +] \ No newline at end of file