Skip to content

Commit 26e36b2

Browse files
fix: add convert command (#64)
* fix: add convert command * fix: remove SfdxProjectJson dep * fix: rootdir working
1 parent c00e432 commit 26e36b2

File tree

4 files changed

+261
-0
lines changed

4 files changed

+261
-0
lines changed

command-snapshot.json

+16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
[
2+
{
3+
"command": "force:source:convert",
4+
"plugin": "@salesforce/plugin-source",
5+
"flags": [
6+
"apiversion",
7+
"json",
8+
"loglevel",
9+
"manifest",
10+
"metadata",
11+
"outputdir",
12+
"packagename",
13+
"rootdir",
14+
"sourcepath",
15+
"targetusername"
16+
]
17+
},
218
{
319
"command": "force:source:deploy",
420
"plugin": "@salesforce/plugin-source",

messages/convert.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"description": "convert source into Metadata API format",
3+
"examples": [
4+
"$ sfdx force:source:convert -r path/to/source",
5+
"$ sfdx force:source:convert -r path/to/source -d path/to/outputdir -n 'My Package'"
6+
],
7+
"flags": {
8+
"rootdir": "a source directory other than the default package to convert",
9+
"outputdir": "output directory to store the Metadata API–formatted files in",
10+
"packagename": "name of the package to associate with the metadata-formatted files",
11+
"manifest": "file path to manifest (package.xml) of metadata types to convert.",
12+
"sourcepath": "comma-separated list of paths to the local source files to convert",
13+
"metadata": "comma-separated list of metadata component names to convert"
14+
},
15+
"success": "Source was successfully converted to Metadata API format and written to the location: %s"
16+
}

src/commands/force/source/convert.ts

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import * as os from 'os';
9+
import { resolve } from 'path';
10+
import { flags, FlagsConfig } from '@salesforce/command';
11+
import { Messages } from '@salesforce/core';
12+
import { MetadataConverter } from '@salesforce/source-deploy-retrieve';
13+
import { asArray, asString } from '@salesforce/ts-types';
14+
import { SourceCommand } from '../../../sourceCommand';
15+
16+
Messages.importMessagesDirectory(__dirname);
17+
const messages = Messages.loadMessages('@salesforce/plugin-source', 'convert');
18+
19+
type ConvertResult = {
20+
location: string;
21+
};
22+
23+
export class Convert extends SourceCommand {
24+
public static readonly description = messages.getMessage('description');
25+
public static readonly examples = messages.getMessage('examples').split(os.EOL);
26+
public static readonly requiresProject = true;
27+
public static readonly requiresUsername = true;
28+
public static readonly flagsConfig: FlagsConfig = {
29+
rootdir: flags.directory({
30+
char: 'r',
31+
description: messages.getMessage('flags.rootdir'),
32+
}),
33+
outputdir: flags.directory({
34+
default: './',
35+
char: 'd',
36+
description: messages.getMessage('flags.outputdir'),
37+
}),
38+
packagename: flags.string({
39+
char: 'n',
40+
description: messages.getMessage('flags.packagename'),
41+
}),
42+
manifest: flags.string({
43+
char: 'x',
44+
description: messages.getMessage('flags.manifest'),
45+
}),
46+
sourcepath: flags.array({
47+
char: 'p',
48+
description: messages.getMessage('flags.sourcepath'),
49+
exclusive: ['manifest', 'metadata'],
50+
}),
51+
metadata: flags.array({
52+
char: 'm',
53+
description: messages.getMessage('flags.metadata'),
54+
exclusive: ['manifest', 'sourcepath'],
55+
}),
56+
};
57+
58+
public async run(): Promise<ConvertResult> {
59+
const paths: string[] = [];
60+
61+
if (this.flags.sourcepath) {
62+
paths.push(...this.flags.sourcepath);
63+
}
64+
65+
// rootdir behaves exclusively to sourcepath, metadata, and manifest... to maintain backwards compatibility
66+
// we will check here, instead of adding the exclusive option to the flag definition so we don't break scripts
67+
if (this.flags.rootdir && !this.flags.sourcepath && !this.flags.metadata && !this.flags.manifest) {
68+
// only rootdir option passed
69+
paths.push(this.flags.rootdir);
70+
}
71+
72+
// no options passed, convert the default package (usually force-app)
73+
if (!this.flags.sourcepath && !this.flags.metadata && !this.flags.manifest && !this.flags.rootdir) {
74+
paths.push(this.project.getDefaultPackage().path);
75+
}
76+
77+
const cs = await this.createComponentSet({
78+
sourcepath: paths,
79+
manifest: asString(this.flags.manifest),
80+
metadata: asArray<string>(this.flags.metadata),
81+
});
82+
83+
const converter = new MetadataConverter();
84+
const res = await converter.convert(cs.getSourceComponents().toArray(), 'metadata', {
85+
type: 'directory',
86+
outputDirectory: asString(this.flags.outputdir),
87+
packageName: asString(this.flags.packagename),
88+
});
89+
90+
this.ux.log(messages.getMessage('success', [res.packagePath]));
91+
92+
return {
93+
location: resolve(res.packagePath),
94+
};
95+
}
96+
}

test/commands/source/convert.test.ts

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { join, resolve } from 'path';
9+
import { Dictionary } from '@salesforce/ts-types';
10+
import { DeployResult, MetadataConverter } from '@salesforce/source-deploy-retrieve';
11+
import * as sinon from 'sinon';
12+
import { expect } from 'chai';
13+
import { Convert } from '../../../src/commands/force/source/convert';
14+
import { FlagOptions } from '../../../src/sourceCommand';
15+
16+
describe('force:source:convert', () => {
17+
let createComponentSetStub: sinon.SinonStub;
18+
let deployStub: sinon.SinonStub;
19+
20+
const defaultDir = join('my', 'default', 'package');
21+
const myApp = join('new', 'package', 'directory');
22+
23+
const sandbox = sinon.createSandbox();
24+
const packageXml = 'package.xml';
25+
26+
const run = async (flags: Dictionary<boolean | string | number | string[]> = {}): Promise<DeployResult> => {
27+
// Run the command
28+
return Convert.prototype.run.call({
29+
flags: Object.assign({}, flags),
30+
ux: {
31+
log: () => {},
32+
styledHeader: () => {},
33+
table: () => {},
34+
},
35+
logger: {
36+
debug: () => {},
37+
},
38+
project: {
39+
getDefaultPackage: () => {
40+
return { path: defaultDir };
41+
},
42+
},
43+
createComponentSet: createComponentSetStub,
44+
}) as Promise<DeployResult>;
45+
};
46+
47+
// Ensure SourceCommand.createComponentSet() args
48+
const ensureCreateComponentSetArgs = (overrides?: Partial<FlagOptions>) => {
49+
const defaultArgs = {
50+
sourcepath: [],
51+
manifest: undefined,
52+
metadata: undefined,
53+
};
54+
const expectedArgs = { ...defaultArgs, ...overrides };
55+
56+
expect(createComponentSetStub.calledOnce).to.equal(true);
57+
expect(createComponentSetStub.firstCall.args[0]).to.deep.equal(expectedArgs);
58+
};
59+
60+
beforeEach(() => {
61+
sandbox.stub(MetadataConverter.prototype, 'convert').resolves({ packagePath: 'temp' });
62+
createComponentSetStub = sandbox.stub().returns({
63+
deploy: deployStub,
64+
getPackageXml: () => packageXml,
65+
getSourceComponents: () => {
66+
return {
67+
toArray: () => {},
68+
};
69+
},
70+
});
71+
});
72+
73+
afterEach(() => {
74+
sandbox.restore();
75+
});
76+
77+
it('should pass along sourcepath', async () => {
78+
const sourcepath = ['somepath'];
79+
const result = await run({ sourcepath, json: true });
80+
expect(result).to.deep.equal({ location: resolve('temp') });
81+
ensureCreateComponentSetArgs({ sourcepath });
82+
});
83+
84+
it('should call default package dir if no args', async () => {
85+
const result = await run({ json: true });
86+
expect(result).to.deep.equal({ location: resolve('temp') });
87+
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
88+
});
89+
90+
it('should call with metadata', async () => {
91+
const result = await run({ metadata: ['ApexClass'], json: true });
92+
expect(result).to.deep.equal({ location: resolve('temp') });
93+
ensureCreateComponentSetArgs({ metadata: ['ApexClass'] });
94+
});
95+
96+
it('should call with package.xml', async () => {
97+
const result = await run({ json: true });
98+
expect(result).to.deep.equal({ location: resolve('temp') });
99+
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
100+
});
101+
102+
it('should call default package dir if no args', async () => {
103+
const result = await run({ json: true });
104+
expect(result).to.deep.equal({ location: resolve('temp') });
105+
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
106+
});
107+
108+
it('should call root dir with rootdir flag', async () => {
109+
const result = await run({ rootdir: myApp, json: true });
110+
expect(result).to.deep.equal({ location: resolve('temp') });
111+
ensureCreateComponentSetArgs({ sourcepath: [myApp] });
112+
});
113+
114+
describe('rootdir should be overwritten by any other flag', () => {
115+
it('sourcepath', async () => {
116+
const result = await run({ rootdir: myApp, sourcepath: [defaultDir], json: true });
117+
expect(result).to.deep.equal({ location: resolve('temp') });
118+
ensureCreateComponentSetArgs({ sourcepath: [defaultDir] });
119+
});
120+
121+
it('metadata', async () => {
122+
const result = await run({ rootdir: myApp, metadata: ['ApexClass', 'CustomObject'], json: true });
123+
expect(result).to.deep.equal({ location: resolve('temp') });
124+
ensureCreateComponentSetArgs({ metadata: ['ApexClass', 'CustomObject'] });
125+
});
126+
127+
it('package', async () => {
128+
const result = await run({ rootdir: myApp, manifest: packageXml, json: true });
129+
expect(result).to.deep.equal({ location: resolve('temp') });
130+
ensureCreateComponentSetArgs({ manifest: packageXml });
131+
});
132+
});
133+
});

0 commit comments

Comments
 (0)