Skip to content

Commit

Permalink
Merge pull request #1130 from salesforcecli/sm/convert-source-behavio…
Browse files Browse the repository at this point in the history
…r-confirmation

feat: confirmation and warnings for convert behavior
  • Loading branch information
shetzel authored Aug 16, 2024
2 parents d36353d + e122e72 commit ad3bc40
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 391 deletions.
18 changes: 8 additions & 10 deletions messages/convert.source-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ Behavior to enable; the values correspond to the possible values of the "sourceB

# examples

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

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

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

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

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

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

# flags.dry-run.summary

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

# error.packageDirectoryNeedsMainDefault
# mainDefaultConfirmation

The package directory %s doesn't have a main/default structure.
This command moves metadata into a main/default structure, but your package directories aren't ready for it.
- 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.

# error.packageDirectoryNeedsMainDefault.actions
# basicConfirmation

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

# success.dryRun

Expand Down
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
"author": "Salesforce",
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
"@oclif/core": "^4.0.12",
"@salesforce/apex-node": "^8.1.1",
"@salesforce/core": "^8.2.8",
"@salesforce/kit": "^3.1.6",
"@salesforce/plugin-info": "^3.3.24",
"@salesforce/sf-plugins-core": "^11.3.0",
"@salesforce/source-deploy-retrieve": "^12.1.11",
"@salesforce/source-tracking": "^7.1.2",
"@oclif/core": "^4.0.17",
"@salesforce/apex-node": "^8.1.3",
"@salesforce/core": "^8.4.0",
"@salesforce/kit": "^3.2.1",
"@salesforce/plugin-info": "^3.3.28",
"@salesforce/sf-plugins-core": "^11.3.2",
"@salesforce/source-deploy-retrieve": "^12.4.0",
"@salesforce/source-tracking": "^7.1.7",
"@salesforce/ts-types": "^2.0.12",
"ansis": "^3.3.2"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.10",
"@salesforce/cli-plugins-testkit": "^5.3.23",
"@oclif/plugin-command-snapshot": "^5.2.12",
"@salesforce/cli-plugins-testkit": "^5.3.25",
"@salesforce/dev-scripts": "^10.2.9",
"@salesforce/plugin-command-reference": "^3.1.13",
"@salesforce/plugin-command-reference": "^3.1.16",
"@salesforce/schemas": "^1.9.0",
"@salesforce/source-testkit": "^2.2.39",
"@salesforce/ts-sinon": "^1.4.23",
Expand Down
19 changes: 19 additions & 0 deletions src/commands/project/convert/source-behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/

import { rm, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import {
Expand All @@ -17,6 +19,7 @@ import {
PRESET_CHOICES,
getPackageDirectoriesForPreset,
convertBackToSource,
ComponentSetAndPackageDirPath,
} from '../../../utils/convertBehavior.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
Expand Down Expand Up @@ -61,6 +64,15 @@ export default class ConvertSourceBehavior extends SfCommand<SourceBehaviorResul
flags['dry-run'] ? readFile(projectJson.getPath()) : '',
getPackageDirectoriesForPreset(this.project!, flags.behavior),
]);

if (!packageDirsWithDecomposable.every(hasMainDefault(this.project!.getPath()))) {
this.warn(messages.getMessage('mainDefaultConfirmation'));
}

if (!flags['dry-run']) {
this.warn(messages.getMessage('basicConfirmation'));
await this.confirm({ message: 'Proceed' });
}
const filesToDelete = await convertToMdapi(packageDirsWithDecomposable);

// flip the preset in the sfdx-project.json, even for dry-run, since the registry will need for conversions
Expand Down Expand Up @@ -103,3 +115,10 @@ export default class ConvertSourceBehavior extends SfCommand<SourceBehaviorResul
};
}
}

/** convert will put things in /main/default. If the packageDirs aren't configured that way, we'll need to warn the user
* See https://salesforce.quip.com/va5IAgXmTMWF for details on that issue */
const hasMainDefault =
(projectDir: string) =>
(i: ComponentSetAndPackageDirPath): boolean =>
existsSync(join(projectDir, i.packageDirPath, 'main', 'default'));
27 changes: 6 additions & 21 deletions src/utils/convertBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 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 { existsSync, readdirSync } from 'node:fs';
import { readdirSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
Expand Down Expand Up @@ -38,20 +38,19 @@ export const getPackageDirectoriesForPreset = async (
preset: string
): Promise<ComponentSetAndPackageDirPath[]> => {
const projectDir = project.getPath();
const messages = loadMessages();
const output = (
await Promise.all(
project
.getPackageDirectories()
.map((pd) => pd.path)
.map(componentSetFromPackageDirectory(projectDir)(await getTypesFromPreset(preset)))
)
)
.filter(componentSetIsNonEmpty)
// we do this after filtering componentSets to reduce false positives (ex: dir does not have main/default but also has nothing to decompose)
.map(validateMainDefault(projectDir));
).filter(componentSetIsNonEmpty);
if (output.length === 0) {
loadMessages().createError('error.noTargetTypes', [preset]);
messages.createError('error.noTargetTypes', [preset]);
}

return output;
};

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

/** convert will put things in /main/default. If the packageDirs aren't configured that way, we don't want to make a mess.
* See https://salesforce.quip.com/va5IAgXmTMWF for details on that issue */
const validateMainDefault =
(projectDir: string) =>
(i: ComponentSetAndPackageDirPath): ComponentSetAndPackageDirPath => {
if (!existsSync(join(projectDir, i.packageDirPath, 'main', 'default'))) {
throw loadMessages().createError(
'error.packageDirectoryNeedsMainDefault',
[i.packageDirPath],
[i.packageDirPath]
);
}
return i;
};

const getComponentSetFiles = (cs: ComponentSet): string[] =>
cs
.getSourceComponents()
Expand Down
49 changes: 24 additions & 25 deletions test/nuts/convert/decompose.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@

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

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'convert.source-behavior');

config.truncateThreshold = 0;

describe('source behavior changes', () => {
let session: TestSession;
before(async () => {
Expand Down Expand Up @@ -54,7 +60,7 @@ describe('source behavior changes', () => {
await fs.promises.rm(path.join(session.project.dir, DRY_RUN_DIR), { recursive: true });
});

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

const result = execCmd('project convert source-behavior --behavior decomposeCustomLabelsBeta --json', {
ensureExitCode: 1,
});
expect(result.jsonOutput?.name).to.equal('PackageDirectoryNeedsMainDefaultError');
// put stuff back the way it was
await fs.promises.rm(newDir, { recursive: true });
await fs.promises.writeFile(
path.join(session.project.dir, 'sfdx-project.json'),
JSON.stringify(originalProject, null, 2)
);
});

it.skip('produces actual output and makes expected changes', async () => {
const result = execCmd<SourceBehaviorResult>(
'project convert source-behavior --behavior decomposeCustomLabelsBeta --json',
const result = await execInteractiveCmd(
'project convert source-behavior --behavior decomposeCustomLabelsBeta',
{ Proceed: ['y', Interaction.ENTER] },
{
ensureExitCode: 0,
}
);
expect(result.jsonOutput?.result.deletedFiles).to.deep.equal([
path.join(session.project.dir, 'force-app', 'main', 'default', 'labels', 'CustomLabels.labels-meta.xml'),
]);
expect(result.jsonOutput?.result.createdFiles).to.have.length(4);
expect(result.stderr).to.include(messages.getMessage('basicConfirmation'));
expect(result.stderr).to.include(messages.getMessage('mainDefaultConfirmation'));

expect(result.stdout).to.include('Deleted Files');
expect(result.stdout).to.include('Created Files');
expect(result.stdout).to.include(
path.join(session.project.dir, 'force-app', 'main', 'default', 'labels', 'CustomLabels.labels-meta.xml')
);
// it modified the project json
expect((await getProject(session))[PRESETS_PROP]).to.deep.equal(['decomposeCustomLabelsBeta']);

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

it.skip("throws on repeated preset that's already done", () => {
const err = execCmd<SourceBehaviorResult>(
it("throws on repeated preset that's already done", async () => {
const err = await execInteractiveCmd(
'project convert source-behavior --behavior decomposeCustomLabelsBeta --json',
{},
{
ensureExitCode: 1,
}
);
expect(err.jsonOutput?.name).to.equal('sourceBehaviorOptionAlreadyExists');
expect(err.stdout).to.include('sourceBehaviorOptionAlreadyExists');
});

after(async () => {
Expand Down
Loading

0 comments on commit ad3bc40

Please sign in to comment.