Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion METADATA_SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,15 +533,17 @@ 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|
|EngagementMessagingSettings|✅||
|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|
Expand Down
33 changes: 25 additions & 8 deletions src/client/metadataApiDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<string, string[]> = new Map<string, string[]>()
) {}

public getFileResponses(): FileResponse[] {
// this involves FS operations, so only perform once!
Expand Down Expand Up @@ -236,6 +236,7 @@ export class MetadataApiDeploy extends MetadataTransfer<MetadataApiDeployStatus,
},
};
private options: MetadataApiDeployOptions;
private replacements: Map<string, string[]> = 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.
Expand Down Expand Up @@ -310,6 +311,7 @@ export class MetadataApiDeploy extends MetadataTransfer<MetadataApiDeployStatus,
}

protected async pre(): Promise<AsyncResult> {
const LifecycleInstance = Lifecycle.getInstance();
const connection = await this.getConnection();
// store for use in the scopedPostDeploy event
this.orgId = connection.getAuthInfoFields().orgId;
Expand All @@ -320,11 +322,26 @@ export class MetadataApiDeploy extends MetadataTransfer<MetadataApiDeployStatus,
}
// only do event hooks if source, (NOT a metadata format) deploy
if (this.options.components) {
await Lifecycle.getInstance().emit('scopedPreDeploy', {
await LifecycleInstance.emit('scopedPreDeploy', {
componentSet: this.options.components,
orgId: this.orgId,
} as ScopedPreDeploy);
}

LifecycleInstance.on(
'replacement',
async (replacement: ReplacementEvent) =>
// 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;
Expand Down Expand Up @@ -370,7 +387,7 @@ export class MetadataApiDeploy extends MetadataTransfer<MetadataApiDeployStatus,
`Error trying to compile/send deploy telemetry data for deploy ID: ${this.id}\nError: ${error.message}`
);
}
const deployResult = new DeployResult(result, this.components);
const deployResult = new DeployResult(result, this.components, this.replacements);
// only do event hooks if source, (NOT a metadata format) deploy
if (this.options.components) {
await lifecycle.emit('scopedPostDeploy', { deployResult, orgId: this.orgId } as ScopedPostDeploy);
Expand Down
1 change: 0 additions & 1 deletion src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ interface FileResponseFailure extends FileResponseBase {
}

export type FileResponse = FileResponseSuccess | FileResponseFailure;

export interface MetadataTransferResult {
response: MetadataRequestStatus;
components: ComponentSet;
Expand Down
21 changes: 16 additions & 5 deletions src/convert/replacements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Lifecycle, SfError, SfProject } from '@salesforce/core';
import * as minimatch from 'minimatch';
import { SourcePath } from '../common';
import { SourceComponent } from '../resolve/sourceComponent';
import { MarkedReplacement, ReplacementConfig } from './types';
import { MarkedReplacement, ReplacementConfig, ReplacementEvent } from './types';

const fileContentsCache = new Map<string, string>();

Expand Down Expand Up @@ -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<string> => {
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;
};
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/convert/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type ConvertResult = {
export type MarkedReplacement = {
toReplace: string | RegExp;
replaceWith: string;
matchedFilename: string;
singleFile?: boolean;
};

Expand Down Expand Up @@ -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;
};
50 changes: 39 additions & 11 deletions test/convert/replacements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -124,13 +129,15 @@ describe('marking replacements on a component', () => {
expect(result).to.deep.equal({
[cmp.xml]: [
{
matchedFilename: cmp.xml,
toReplace: 'foo',
replaceWith: 'bar',
singleFile: false,
},
],
[cmp.content]: [
{
matchedFilename: cmp.content,
toReplace: 'foo',
replaceWith: 'bar',
singleFile: false,
Expand All @@ -146,13 +153,15 @@ describe('marking replacements on a component', () => {
expect(result).to.deep.equal({
[cmp.xml]: [
{
matchedFilename: cmp.xml,
toReplace: 'foo',
replaceWith: 'bar',
singleFile: true,
},
],
[cmp.content]: [
{
matchedFilename: cmp.content,
toReplace: 'foo',
replaceWith: 'bar',
singleFile: true,
Expand All @@ -164,71 +173,90 @@ 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');
});
});
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');
});
});

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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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
}
]
]
Loading