Skip to content

Commit

Permalink
fix: add convert command (#64)
Browse files Browse the repository at this point in the history
* fix: add convert command

* fix: remove SfdxProjectJson dep

* fix: rootdir working
  • Loading branch information
WillieRuemmele authored Apr 7, 2021
1 parent c00e432 commit 26e36b2
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 0 deletions.
16 changes: 16 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
[
{
"command": "force:source:convert",
"plugin": "@salesforce/plugin-source",
"flags": [
"apiversion",
"json",
"loglevel",
"manifest",
"metadata",
"outputdir",
"packagename",
"rootdir",
"sourcepath",
"targetusername"
]
},
{
"command": "force:source:deploy",
"plugin": "@salesforce/plugin-source",
Expand Down
16 changes: 16 additions & 0 deletions messages/convert.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"description": "convert source into Metadata API format",
"examples": [
"$ sfdx force:source:convert -r path/to/source",
"$ sfdx force:source:convert -r path/to/source -d path/to/outputdir -n 'My Package'"
],
"flags": {
"rootdir": "a source directory other than the default package to convert",
"outputdir": "output directory to store the Metadata API–formatted files in",
"packagename": "name of the package to associate with the metadata-formatted files",
"manifest": "file path to manifest (package.xml) of metadata types to convert.",
"sourcepath": "comma-separated list of paths to the local source files to convert",
"metadata": "comma-separated list of metadata component names to convert"
},
"success": "Source was successfully converted to Metadata API format and written to the location: %s"
}
96 changes: 96 additions & 0 deletions src/commands/force/source/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 { resolve } from 'path';
import { flags, FlagsConfig } from '@salesforce/command';
import { Messages } from '@salesforce/core';
import { MetadataConverter } from '@salesforce/source-deploy-retrieve';
import { asArray, asString } from '@salesforce/ts-types';
import { SourceCommand } from '../../../sourceCommand';

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

type ConvertResult = {
location: string;
};

export class Convert 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 requiresUsername = true;
public static readonly flagsConfig: FlagsConfig = {
rootdir: flags.directory({
char: 'r',
description: messages.getMessage('flags.rootdir'),
}),
outputdir: flags.directory({
default: './',
char: 'd',
description: messages.getMessage('flags.outputdir'),
}),
packagename: flags.string({
char: 'n',
description: messages.getMessage('flags.packagename'),
}),
manifest: flags.string({
char: 'x',
description: messages.getMessage('flags.manifest'),
}),
sourcepath: flags.array({
char: 'p',
description: messages.getMessage('flags.sourcepath'),
exclusive: ['manifest', 'metadata'],
}),
metadata: flags.array({
char: 'm',
description: messages.getMessage('flags.metadata'),
exclusive: ['manifest', 'sourcepath'],
}),
};

public async run(): Promise<ConvertResult> {
const paths: string[] = [];

if (this.flags.sourcepath) {
paths.push(...this.flags.sourcepath);
}

// rootdir behaves exclusively to sourcepath, metadata, and manifest... to maintain backwards compatibility
// we will check here, instead of adding the exclusive option to the flag definition so we don't break scripts
if (this.flags.rootdir && !this.flags.sourcepath && !this.flags.metadata && !this.flags.manifest) {
// only rootdir option passed
paths.push(this.flags.rootdir);
}

// no options passed, convert the default package (usually force-app)
if (!this.flags.sourcepath && !this.flags.metadata && !this.flags.manifest && !this.flags.rootdir) {
paths.push(this.project.getDefaultPackage().path);
}

const cs = await this.createComponentSet({
sourcepath: paths,
manifest: asString(this.flags.manifest),
metadata: asArray<string>(this.flags.metadata),
});

const converter = new MetadataConverter();
const res = await converter.convert(cs.getSourceComponents().toArray(), 'metadata', {
type: 'directory',
outputDirectory: asString(this.flags.outputdir),
packageName: asString(this.flags.packagename),
});

this.ux.log(messages.getMessage('success', [res.packagePath]));

return {
location: resolve(res.packagePath),
};
}
}
133 changes: 133 additions & 0 deletions test/commands/source/convert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 { join, resolve } from 'path';
import { Dictionary } from '@salesforce/ts-types';
import { DeployResult, MetadataConverter } from '@salesforce/source-deploy-retrieve';
import * as sinon from 'sinon';
import { expect } from 'chai';
import { Convert } from '../../../src/commands/force/source/convert';
import { FlagOptions } from '../../../src/sourceCommand';

describe('force:source:convert', () => {
let createComponentSetStub: sinon.SinonStub;
let deployStub: sinon.SinonStub;

const defaultDir = join('my', 'default', 'package');
const myApp = join('new', 'package', 'directory');

const sandbox = sinon.createSandbox();
const packageXml = 'package.xml';

const run = async (flags: Dictionary<boolean | string | number | string[]> = {}): Promise<DeployResult> => {
// Run the command
return Convert.prototype.run.call({
flags: Object.assign({}, flags),
ux: {
log: () => {},
styledHeader: () => {},
table: () => {},
},
logger: {
debug: () => {},
},
project: {
getDefaultPackage: () => {
return { path: defaultDir };
},
},
createComponentSet: createComponentSetStub,
}) as Promise<DeployResult>;
};

// Ensure SourceCommand.createComponentSet() args
const ensureCreateComponentSetArgs = (overrides?: Partial<FlagOptions>) => {
const defaultArgs = {
sourcepath: [],
manifest: undefined,
metadata: undefined,
};
const expectedArgs = { ...defaultArgs, ...overrides };

expect(createComponentSetStub.calledOnce).to.equal(true);
expect(createComponentSetStub.firstCall.args[0]).to.deep.equal(expectedArgs);
};

beforeEach(() => {
sandbox.stub(MetadataConverter.prototype, 'convert').resolves({ packagePath: 'temp' });
createComponentSetStub = sandbox.stub().returns({
deploy: deployStub,
getPackageXml: () => packageXml,
getSourceComponents: () => {
return {
toArray: () => {},
};
},
});
});

afterEach(() => {
sandbox.restore();
});

it('should pass along sourcepath', async () => {
const sourcepath = ['somepath'];
const result = await run({ sourcepath, json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ sourcepath });
});

it('should call default package dir if no args', async () => {
const result = await run({ json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
});

it('should call with metadata', async () => {
const result = await run({ metadata: ['ApexClass'], json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ metadata: ['ApexClass'] });
});

it('should call with package.xml', async () => {
const result = await run({ json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
});

it('should call default package dir if no args', async () => {
const result = await run({ json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
});

it('should call root dir with rootdir flag', async () => {
const result = await run({ rootdir: myApp, json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ sourcepath: [myApp] });
});

describe('rootdir should be overwritten by any other flag', () => {
it('sourcepath', async () => {
const result = await run({ rootdir: myApp, sourcepath: [defaultDir], json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
});

it('metadata', async () => {
const result = await run({ rootdir: myApp, metadata: ['ApexClass', 'CustomObject'], json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ metadata: ['ApexClass', 'CustomObject'] });
});

it('package', async () => {
const result = await run({ rootdir: myApp, manifest: packageXml, json: true });
expect(result).to.deep.equal({ location: resolve('temp') });
ensureCreateComponentSetArgs({ manifest: packageXml });
});
});
});

0 comments on commit 26e36b2

Please sign in to comment.