Skip to content

Commit ad3bc40

Browse files
authored
Merge pull request #1130 from salesforcecli/sm/convert-source-behavior-confirmation
feat: confirmation and warnings for convert behavior
2 parents d36353d + e122e72 commit ad3bc40

File tree

6 files changed

+167
-391
lines changed

6 files changed

+167
-391
lines changed

messages/convert.source-behavior.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ Behavior to enable; the values correspond to the possible values of the "sourceB
1616

1717
# examples
1818

19-
- Update your Salesforce DX project to decompose custom labels:
19+
- Update your Salesforce DX project to decompose custom permission sets:
2020

21-
<%= config.bin %> <%= command.id %> --behavior decomposeCustomLabelsBeta
21+
<%= config.bin %> <%= command.id %> --behavior decomposePermissionSetBeta
2222

2323
- Display what the command would do, but don't change any existing files:
2424

25-
<%= config.bin %> <%= command.id %> --behavior decomposeCustomLabelsBeta --dry-run
25+
<%= config.bin %> <%= command.id %> --behavior decomposePermissionSetBeta --dry-run
2626

2727
- Keep the temporary directory that contains the interim metadata API formatted files:
2828

29-
<%= config.bin %> <%= command.id %> --behavior decomposeCustomLabelsBeta --dry-run --preserve-temp-dir
29+
<%= config.bin %> <%= command.id %> --behavior decomposePermissionSetBeta --dry-run --preserve-temp-dir
3030

3131
# flags.dry-run.summary
3232

@@ -51,15 +51,13 @@ Your project has a default org (target-org) that uses source tracking. This oper
5151
- Run this command again.
5252
- Create a new org ("sf org create scratch" or "sf org create sandbox") and deploy the modified source.
5353

54-
# error.packageDirectoryNeedsMainDefault
54+
# mainDefaultConfirmation
5555

56-
The package directory %s doesn't have a main/default structure.
57-
This command moves metadata into a main/default structure, but your package directories aren't ready for it.
56+
- This command puts components in a newly created `main/default` folder in each package directory. You might need to re-organize them into your preferred structure.
5857

59-
# error.packageDirectoryNeedsMainDefault.actions
58+
# basicConfirmation
6059

61-
- Update %s to have all its metadata inside a main/default directory structure.
62-
- Run the command again.
60+
- This command makes changes to your project. Be sure you've committed any source changes before continuing so you can easily revert if necessary.
6361

6462
# success.dryRun
6563

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,22 @@
55
"author": "Salesforce",
66
"bugs": "https://github.com/forcedotcom/cli/issues",
77
"dependencies": {
8-
"@oclif/core": "^4.0.12",
9-
"@salesforce/apex-node": "^8.1.1",
10-
"@salesforce/core": "^8.2.8",
11-
"@salesforce/kit": "^3.1.6",
12-
"@salesforce/plugin-info": "^3.3.24",
13-
"@salesforce/sf-plugins-core": "^11.3.0",
14-
"@salesforce/source-deploy-retrieve": "^12.1.11",
15-
"@salesforce/source-tracking": "^7.1.2",
8+
"@oclif/core": "^4.0.17",
9+
"@salesforce/apex-node": "^8.1.3",
10+
"@salesforce/core": "^8.4.0",
11+
"@salesforce/kit": "^3.2.1",
12+
"@salesforce/plugin-info": "^3.3.28",
13+
"@salesforce/sf-plugins-core": "^11.3.2",
14+
"@salesforce/source-deploy-retrieve": "^12.4.0",
15+
"@salesforce/source-tracking": "^7.1.7",
1616
"@salesforce/ts-types": "^2.0.12",
1717
"ansis": "^3.3.2"
1818
},
1919
"devDependencies": {
20-
"@oclif/plugin-command-snapshot": "^5.2.10",
21-
"@salesforce/cli-plugins-testkit": "^5.3.23",
20+
"@oclif/plugin-command-snapshot": "^5.2.12",
21+
"@salesforce/cli-plugins-testkit": "^5.3.25",
2222
"@salesforce/dev-scripts": "^10.2.9",
23-
"@salesforce/plugin-command-reference": "^3.1.13",
23+
"@salesforce/plugin-command-reference": "^3.1.16",
2424
"@salesforce/schemas": "^1.9.0",
2525
"@salesforce/source-testkit": "^2.2.39",
2626
"@salesforce/ts-sinon": "^1.4.23",

src/commands/project/convert/source-behavior.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77

88
import { rm, readFile, writeFile } from 'node:fs/promises';
9+
import { join } from 'node:path';
10+
import { existsSync } from 'node:fs';
911
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
1012
import { Messages } from '@salesforce/core';
1113
import {
@@ -17,6 +19,7 @@ import {
1719
PRESET_CHOICES,
1820
getPackageDirectoriesForPreset,
1921
convertBackToSource,
22+
ComponentSetAndPackageDirPath,
2023
} from '../../../utils/convertBehavior.js';
2124

2225
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
@@ -61,6 +64,15 @@ export default class ConvertSourceBehavior extends SfCommand<SourceBehaviorResul
6164
flags['dry-run'] ? readFile(projectJson.getPath()) : '',
6265
getPackageDirectoriesForPreset(this.project!, flags.behavior),
6366
]);
67+
68+
if (!packageDirsWithDecomposable.every(hasMainDefault(this.project!.getPath()))) {
69+
this.warn(messages.getMessage('mainDefaultConfirmation'));
70+
}
71+
72+
if (!flags['dry-run']) {
73+
this.warn(messages.getMessage('basicConfirmation'));
74+
await this.confirm({ message: 'Proceed' });
75+
}
6476
const filesToDelete = await convertToMdapi(packageDirsWithDecomposable);
6577

6678
// flip the preset in the sfdx-project.json, even for dry-run, since the registry will need for conversions
@@ -103,3 +115,10 @@ export default class ConvertSourceBehavior extends SfCommand<SourceBehaviorResul
103115
};
104116
}
105117
}
118+
119+
/** convert will put things in /main/default. If the packageDirs aren't configured that way, we'll need to warn the user
120+
* See https://salesforce.quip.com/va5IAgXmTMWF for details on that issue */
121+
const hasMainDefault =
122+
(projectDir: string) =>
123+
(i: ComponentSetAndPackageDirPath): boolean =>
124+
existsSync(join(projectDir, i.packageDirPath, 'main', 'default'));

src/utils/convertBehavior.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import { existsSync, readdirSync } from 'node:fs';
7+
import { readdirSync } from 'node:fs';
88
import { readFile } from 'node:fs/promises';
99
import { join, resolve } from 'node:path';
1010
import { fileURLToPath } from 'node:url';
@@ -38,20 +38,19 @@ export const getPackageDirectoriesForPreset = async (
3838
preset: string
3939
): Promise<ComponentSetAndPackageDirPath[]> => {
4040
const projectDir = project.getPath();
41+
const messages = loadMessages();
4142
const output = (
4243
await Promise.all(
4344
project
4445
.getPackageDirectories()
4546
.map((pd) => pd.path)
4647
.map(componentSetFromPackageDirectory(projectDir)(await getTypesFromPreset(preset)))
4748
)
48-
)
49-
.filter(componentSetIsNonEmpty)
50-
// we do this after filtering componentSets to reduce false positives (ex: dir does not have main/default but also has nothing to decompose)
51-
.map(validateMainDefault(projectDir));
49+
).filter(componentSetIsNonEmpty);
5250
if (output.length === 0) {
53-
loadMessages().createError('error.noTargetTypes', [preset]);
51+
messages.createError('error.noTargetTypes', [preset]);
5452
}
53+
5554
return output;
5655
};
5756

@@ -144,6 +143,7 @@ const convertToSource = async ({
144143
}): Promise<ConvertResult[]> => {
145144
// mdapi=>source convert the target dir back to the project
146145
// it's a new converter because the project has changed and it should reload the project's registry.
146+
SfProject.clearInstances(); // break the singleton so SDR will re-read to get the new preset
147147
const converter = new MetadataConverter(new RegistryAccess(undefined, projectDir));
148148
return Promise.all(
149149
packageDirsWithPreset.map(async (pd) =>
@@ -179,21 +179,6 @@ export const getTypesFromPreset = async (preset: string): Promise<string[]> =>
179179
(JSON.parse(await readFile(join(PRESET_DIR, `${preset}.json`), 'utf-8')) as MetadataRegistry).types
180180
).map((t) => t.name);
181181

182-
/** convert will put things in /main/default. If the packageDirs aren't configured that way, we don't want to make a mess.
183-
* See https://salesforce.quip.com/va5IAgXmTMWF for details on that issue */
184-
const validateMainDefault =
185-
(projectDir: string) =>
186-
(i: ComponentSetAndPackageDirPath): ComponentSetAndPackageDirPath => {
187-
if (!existsSync(join(projectDir, i.packageDirPath, 'main', 'default'))) {
188-
throw loadMessages().createError(
189-
'error.packageDirectoryNeedsMainDefault',
190-
[i.packageDirPath],
191-
[i.packageDirPath]
192-
);
193-
}
194-
return i;
195-
};
196-
197182
const getComponentSetFiles = (cs: ComponentSet): string[] =>
198183
cs
199184
.getSourceComponents()

test/nuts/convert/decompose.nut.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77

88
import path from 'node:path';
99
import fs from 'node:fs';
10-
import { expect } from 'chai';
11-
import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit';
10+
import { expect, config } from 'chai';
11+
import { Interaction, TestSession, execCmd, execInteractiveCmd } from '@salesforce/cli-plugins-testkit';
1212
import { type ProjectJson } from '@salesforce/schemas';
13+
import { Messages } from '@salesforce/core/messages';
1314
import { SourceBehaviorResult } from '../../../src/commands/project/convert/source-behavior.js';
1415
import { DRY_RUN_DIR, PRESETS_PROP } from '../../../src/utils/convertBehavior.js';
1516

17+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
18+
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'convert.source-behavior');
19+
20+
config.truncateThreshold = 0;
21+
1622
describe('source behavior changes', () => {
1723
let session: TestSession;
1824
before(async () => {
@@ -54,7 +60,7 @@ describe('source behavior changes', () => {
5460
await fs.promises.rm(path.join(session.project.dir, DRY_RUN_DIR), { recursive: true });
5561
});
5662

57-
it('throws on a packageDir not using main/default', async () => {
63+
it('warns on a packageDir not using main/default', async () => {
5864
const newDir = path.join(session.project.dir, 'other-dir');
5965
// create the new packageDir
6066
await fs.promises.mkdir(path.join(newDir, 'labels'), { recursive: true });
@@ -74,44 +80,37 @@ describe('source behavior changes', () => {
7480
)
7581
);
7682

77-
const result = execCmd('project convert source-behavior --behavior decomposeCustomLabelsBeta --json', {
78-
ensureExitCode: 1,
79-
});
80-
expect(result.jsonOutput?.name).to.equal('PackageDirectoryNeedsMainDefaultError');
81-
// put stuff back the way it was
82-
await fs.promises.rm(newDir, { recursive: true });
83-
await fs.promises.writeFile(
84-
path.join(session.project.dir, 'sfdx-project.json'),
85-
JSON.stringify(originalProject, null, 2)
86-
);
87-
});
88-
89-
it.skip('produces actual output and makes expected changes', async () => {
90-
const result = execCmd<SourceBehaviorResult>(
91-
'project convert source-behavior --behavior decomposeCustomLabelsBeta --json',
83+
const result = await execInteractiveCmd(
84+
'project convert source-behavior --behavior decomposeCustomLabelsBeta',
85+
{ Proceed: ['y', Interaction.ENTER] },
9286
{
9387
ensureExitCode: 0,
9488
}
9589
);
96-
expect(result.jsonOutput?.result.deletedFiles).to.deep.equal([
97-
path.join(session.project.dir, 'force-app', 'main', 'default', 'labels', 'CustomLabels.labels-meta.xml'),
98-
]);
99-
expect(result.jsonOutput?.result.createdFiles).to.have.length(4);
90+
expect(result.stderr).to.include(messages.getMessage('basicConfirmation'));
91+
expect(result.stderr).to.include(messages.getMessage('mainDefaultConfirmation'));
92+
93+
expect(result.stdout).to.include('Deleted Files');
94+
expect(result.stdout).to.include('Created Files');
95+
expect(result.stdout).to.include(
96+
path.join(session.project.dir, 'force-app', 'main', 'default', 'labels', 'CustomLabels.labels-meta.xml')
97+
);
10098
// it modified the project json
10199
expect((await getProject(session))[PRESETS_PROP]).to.deep.equal(['decomposeCustomLabelsBeta']);
102100

103101
// no dry run dir
104102
expect(fs.existsSync(path.join(session.project.dir, DRY_RUN_DIR))).to.be.false;
105103
});
106104

107-
it.skip("throws on repeated preset that's already done", () => {
108-
const err = execCmd<SourceBehaviorResult>(
105+
it("throws on repeated preset that's already done", async () => {
106+
const err = await execInteractiveCmd(
109107
'project convert source-behavior --behavior decomposeCustomLabelsBeta --json',
108+
{},
110109
{
111110
ensureExitCode: 1,
112111
}
113112
);
114-
expect(err.jsonOutput?.name).to.equal('sourceBehaviorOptionAlreadyExists');
113+
expect(err.stdout).to.include('sourceBehaviorOptionAlreadyExists');
115114
});
116115

117116
after(async () => {

0 commit comments

Comments
 (0)