Skip to content

Commit

Permalink
Wr/manifest generate (#168)
Browse files Browse the repository at this point in the history
* fix: working on source:manifest:generate

* fix: manifest:create command working

* chore: manifest:create NUTs

* chore: update NUTs

* chore: unique flag descriptions

* chore: update messages

* fix: bump SDR to 4.0.2 (#169)

* fix: bump SDR to 4.0.2

* chore: disable sfdx NUTs

* chore: disable sfdx executible in generateNut

* chore(release): 1.0.7 [ci skip]

* chore: code review I - onto apiversion

* chore: apiversion flag

* chore: learn about flags.builtin

* fix: working on source:manifest:generate

* fix: manifest:create command working

* chore: manifest:create NUTs

* chore: update NUTs

* chore: unique flag descriptions

* chore: update messages

* chore: code review I - onto apiversion

* chore: apiversion flag

* chore: learn about flags.builtin

Co-authored-by: SF-CLI-BOT <[email protected]>
  • Loading branch information
WillieRuemmele and SF-CLI-BOT authored Aug 12, 2021
1 parent 12f1301 commit cdad337
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 10 deletions.
7 changes: 6 additions & 1 deletion command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@
{
"command": "force:source:deploy:report",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait", "verbose"]
"flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "verbose", "wait"]
},
{
"command": "force:source:manifest:create",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "json", "loglevel", "manifestname", "manifesttype", "metadata", "outputdir", "sourcepath"]
},
{
"command": "force:source:open",
Expand Down
17 changes: 17 additions & 0 deletions messages/create.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "create a project manifest that lists the metadata components you want to deploy or retrieve \n Create a manifest from a list of metadata components (--metadata) or from one or more local directories that contain source files (--sourcepath). You can specify either of these parameters, not both.\n\nUse --manifesttype to specify the type of manifest you want to create. The resulting manifest files have specific names, such as the standard package.xml or destructiveChanges.xml to delete metadata. Valid values for this parameter, and their respective file names, are:\n\n package : package.xml (default)\n pre : destructiveChangesPre.xml\n post : destructiveChangesPost.xml\n destroy : destructiveChanges.xml\n\nSee https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy_deleting_files.htm for information about these destructive manifest files. \n\nUse --manifestname to specify a custom name for the generated manifest if the pre-defined ones don’t suit your needs. You can specify either --manifesttype or --manifestname, but not both.\n",
"examples": [
"$ sfdx force:source:manifest:create -m ApexClass",
"$ sfdx force:source:manifest:create -m ApexClass:MyApexClass --manifesttype destroy",
"$ sfdx force:source:manifest:create --sourcepath force-app --manifestname myNewManifest"
],
"flags": {
"manifesttype": "type of manifest to create; the type determines the name of the created file",
"manifestname": "name of a custom manifest file to create",
"outputdir": "directory to save the created manifest",
"sourcepath": "comma-separated list of paths to the local source files to include in the manifest",
"metadata": "comma-separated list of names of metadata components to include in the manifest"
},
"success": "successfully wrote %s",
"successOutputDir": "successfully wrote %s to %s"
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@
"subtopics": {
"deploy": {
"description": "interact with an active deploy request"
},
"manifest": {
"description": "create a manifest to use in a deploy/retrieve"
}
}
}
Expand Down Expand Up @@ -129,6 +132,7 @@
"test:nuts:convert": "PLUGIN_SOURCE_SEED_FILTER=\"convert\" ts-node ./test/nuts/generateNuts.ts && nyc mocha \"**/*.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"test:nuts:deploy": "PLUGIN_SOURCE_SEED_FILTER=\"deploy\" ts-node ./test/nuts/generateNuts.ts && nyc mocha \"**/*.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"test:nuts:retrieve": "PLUGIN_SOURCE_SEED_FILTER=\"retrieve\" ts-node ./test/nuts/generateNuts.ts && nyc mocha \"**/*.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"test:nuts:manifest:create": "nyc mocha \"test/nuts/create.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
"version": "oclif-dev readme"
},
"husky": {
Expand Down
12 changes: 5 additions & 7 deletions src/commands/force/source/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import * as os from 'os';
import { flags, FlagsConfig } from '@salesforce/command';
import { Messages, SfdxError } from '@salesforce/core';
import { Messages } from '@salesforce/core';
import { AsyncResult, DeployResult } from '@salesforce/source-deploy-retrieve';
import { Duration } from '@salesforce/kit';
import { getString, isString } from '@salesforce/ts-types';
Expand All @@ -24,7 +24,6 @@ Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-source', 'deploy');

// One of these flags must be specified for a valid deploy.
const requiredFlags = ['manifest', 'metadata', 'sourcepath', 'validateddeployrequestid'];

type TestLevel = 'NoTestRun' | 'RunSpecifiedTests' | 'RunLocalTests' | 'RunAllTestsInOrg';

Expand Down Expand Up @@ -100,6 +99,7 @@ export class Deploy extends DeployCommand {
exclusive: ['metadata', 'sourcepath'],
}),
};
protected xorFlags = ['manifest', 'metadata', 'sourcepath', 'validateddeployrequestid'];
protected readonly lifecycleEventNames = ['predeploy', 'postdeploy'];

private isAsync = false;
Expand All @@ -112,11 +112,6 @@ export class Deploy extends DeployCommand {
});

public async run(): Promise<DeployCommandResult | DeployCommandAsyncResult> {
// verify that the user defined one of: manifest, metadata, sourcepath, validateddeployrequestid
if (!Object.keys(this.flags).some((flag) => requiredFlags.includes(flag))) {
throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'MissingRequiredParam', [requiredFlags.join(', ')]);
}

await this.deploy();
this.resolveSuccess();
return this.formatResult();
Expand All @@ -127,6 +122,9 @@ export class Deploy extends DeployCommand {
// 2. asynchronous - deploy metadata and immediately return.
// 3. recent validation - deploy metadata that's already been validated by the org
protected async deploy(): Promise<void> {
// verify that the user defined one of: manifest, metadata, sourcepath, validateddeployrequestid
this.validateFlags();

const waitDuration = this.getFlag<Duration>('wait');
this.isAsync = waitDuration.quantity === 0;
this.isRest = await this.isRestDeploy();
Expand Down
117 changes: 117 additions & 0 deletions src/commands/force/source/manifest/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* 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 * as os from 'os';
import { join } from 'path';
import { flags, FlagsConfig } from '@salesforce/command';
import { fs } from '@salesforce/core';
import { Messages } from '@salesforce/core';
import { SourceCommand } from '../../../../sourceCommand';
import { ComponentSetBuilder } from '../../../../componentSetBuilder';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-source', 'create');

const manifestTypes: Record<string, string> = {
pre: 'destructiveChangesPre.xml',
post: 'destructiveChangesPost.xml',
destroy: 'destructiveChanges.xml',
package: 'package.xml',
};

interface CreateCommandResult {
name: string;
path: string;
}

export class create extends SourceCommand {
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessage('examples').split(os.EOL);
public static readonly requiresProject = true;
public static readonly flagsConfig: FlagsConfig = {
apiversion: flags.builtin({}),
metadata: flags.array({
char: 'm',
description: messages.getMessage('flags.metadata'),
exclusive: ['sourcepath'],
}),
sourcepath: flags.array({
char: 'p',
description: messages.getMessage('flags.sourcepath'),
exclusive: ['metadata'],
}),
manifestname: flags.string({
char: 'n',
description: messages.getMessage('flags.manifestname'),
exclusive: ['manifesttype'],
}),
manifesttype: flags.enum({
description: messages.getMessage('flags.manifesttype'),
options: Object.keys(manifestTypes),
char: 't',
}),
outputdir: flags.string({
char: 'o',
description: messages.getMessage('flags.outputdir'),
}),
};
protected xorFlags = ['metadata', 'sourcepath'];
private manifestName: string;
private outputDir: string;
private outputPath: string;

public async run(): Promise<CreateCommandResult> {
await this.createManifest();
this.resolveSuccess();
return this.formatResult();
}

protected async createManifest(): Promise<void> {
this.validateFlags();
// convert the manifesttype into one of the "official" manifest names
// if no manifesttype flag passed, use the manifestname flag
// if no manifestname flag, default to 'package.xml'
this.manifestName =
manifestTypes[this.getFlag<string>('manifesttype')] || this.getFlag<string>('manifestname') || 'package.xml';
this.outputDir = this.getFlag<string>('outputdir');

const componentSet = await ComponentSetBuilder.build({
apiversion: this.getFlag('apiversion'),
sourcepath: this.getFlag<string[]>('sourcepath'),
metadata: this.flags.metadata && {
metadataEntries: this.getFlag<string[]>('metadata'),
directoryPaths: this.getPackageDirs(),
},
});

// add the .xml suffix if the user just provided a file name
this.manifestName = this.manifestName.endsWith('.xml') ? this.manifestName : this.manifestName + '.xml';

if (this.outputDir) {
fs.mkdirSync(this.outputDir, { recursive: true });
this.outputPath = join(this.outputDir, this.manifestName);
} else {
this.outputPath = this.manifestName;
}

return fs.writeFile(this.outputPath, componentSet.getPackageXml());
}

// noop this method because any errors will be reported by the createManifest method
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected resolveSuccess(): void {}

protected formatResult(): CreateCommandResult {
if (!this.isJsonOutput()) {
if (this.outputDir) {
this.ux.log(messages.getMessage('successOutputDir', [this.manifestName, this.outputDir]));
} else {
this.ux.log(messages.getMessage('success', [this.manifestName]));
}
}
return { path: this.outputPath, name: this.manifestName };
}
}
10 changes: 9 additions & 1 deletion src/sourceCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { SfdxCommand } from '@salesforce/command';
import { Lifecycle } from '@salesforce/core';
import { Lifecycle, SfdxError } from '@salesforce/core';
import { ComponentSet } from '@salesforce/source-deploy-retrieve';
import { get, getBoolean, getString, Optional } from '@salesforce/ts-types';
import cli from 'cli-ux';
Expand All @@ -21,6 +21,7 @@ export type ProgressBar = {

export abstract class SourceCommand extends SfdxCommand {
public static readonly DEFAULT_SRC_WAIT_MINUTES = 33;
protected xorFlags: string[] = [];
protected progressBar?: ProgressBar;
protected lifecycle = Lifecycle.getInstance();

Expand All @@ -30,6 +31,13 @@ export abstract class SourceCommand extends SfdxCommand {
return getBoolean(this.flags, 'json', false);
}

protected validateFlags(): void {
// verify that the user defined one of the flag names specified in requiredFlags property
if (!Object.keys(this.flags).some((flag) => this.xorFlags.includes(flag))) {
throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'MissingRequiredParam', [this.xorFlags.join(', ')]);
}
}

protected getFlag<T>(flagName: string, defaultVal?: unknown): T {
return get(this.flags, flagName, defaultVal) as T;
}
Expand Down
96 changes: 96 additions & 0 deletions test/nuts/create.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* 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 { join } from 'path';
import { expect } from '@salesforce/command/lib/test';
import { TestSession } from '@salesforce/cli-plugins-testkit';
import { execCmd } from '@salesforce/cli-plugins-testkit';
import { fs } from '@salesforce/core';
import { Dictionary } from '@salesforce/ts-types';

const apexManifest =
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<Package xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
' <types>\n' +
' <members>GeocodingService</members>\n' +
' <members>GeocodingServiceTest</members>\n' +
' <members>PagedResult</members>\n' +
' <members>PropertyController</members>\n' +
' <members>SampleDataController</members>\n' +
' <members>TestPropertyController</members>\n' +
' <members>TestSampleDataController</members>\n' +
' <name>ApexClass</name>\n' +
' </types>\n' +
' <version>51.0</version>\n' +
'</Package>';

describe('force:source:manifest:create', () => {
let session: TestSession;

before(async () => {
session = await TestSession.create({
project: {
gitClone: 'https://github.com/trailheadapps/dreamhouse-lwc.git',
},
});
});

after(async () => {
await session?.clean();
});

it('should produce a manifest (package.xml) for ApexClass', () => {
const result = execCmd<Dictionary>('force:source:manifest:create --metadata ApexClass --json', {
ensureExitCode: 0,
}).jsonOutput.result;
expect(result).to.be.ok;
expect(result).to.include({ path: 'package.xml', name: 'package.xml' });
});

it('should produce a manifest (destructiveChanges.xml) for ApexClass in a new directory', () => {
const output = join('abc', 'def');
const outputFile = join(output, 'destructiveChanges.xml');
const result = execCmd<Dictionary>(
`force:source:manifest:create --metadata ApexClass --manifesttype destroy --outputdir ${output} --apiversion=51.0 --json`,
{
ensureExitCode: 0,
}
).jsonOutput.result;
expect(result).to.be.ok;
expect(result).to.include({ path: `${outputFile}`, name: 'destructiveChanges.xml' });
const file = fs.readFileSync(join(session.project.dir, outputFile), 'utf-8');
expect(file).to.include(apexManifest);
});

it('should produce a custom manifest (myNewManifest.xml) for a sourcepath', () => {
const output = join('abc', 'def');
const outputFile = join(output, 'myNewManifest.xml');
const result = execCmd<Dictionary>(
`force:source:manifest:create --metadata ApexClass --manifestname myNewManifest --outputdir ${output} --json`,
{
ensureExitCode: 0,
}
).jsonOutput.result;
expect(result).to.be.ok;
expect(result).to.include({ path: `${outputFile}`, name: 'myNewManifest.xml' });
});

it('should produce a manifest in a directory with stdout output', () => {
const output = join('abc', 'def');
const result = execCmd<Dictionary>(`force:source:manifest:create --metadata ApexClass --outputdir ${output}`, {
ensureExitCode: 0,
}).shellOutput;
expect(result).to.include(`successfully wrote package.xml to ${output}`);
});

it('should produce a manifest with stdout output', () => {
const result = execCmd<Dictionary>('force:source:manifest:create --metadata ApexClass', {
ensureExitCode: 0,
}).shellOutput;
expect(result).to.include('successfully wrote package.xml');
});
});
2 changes: 1 addition & 1 deletion test/nuts/open.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('force:source:open', () => {
gitClone: 'https://github.com/trailheadapps/dreamhouse-lwc.git',
},
setupCommands: [
'sfdx force:org:create -f config/project-scratch-def.json --setdefaultusername --wait 10',
'sfdx force:org:create -f config/project-scratch-def.json --setdefaultusername --wait 10 --durationdays 1',
'sfdx force:source:deploy -p force-app',
],
});
Expand Down

0 comments on commit cdad337

Please sign in to comment.