Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: confirmation and warnings for convert behavior #1130

Merged
merged 12 commits into from
Aug 16, 2024
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
Loading