Skip to content

Commit dcca20f

Browse files
authored
Sh/scope component sets (#43)
* feat: fix merge issues * test: adds unit tests for SourceCommand.createComponentSet() * fix: use latest core lib and use default package dir for retrieve output * refactor: remove commented import * test: adds more unit tests and small bug fixes * refactor: update typings * fix: use version 1.1.21 of the SDR lib * test: fix unit tests for updates
1 parent 88165ed commit dcca20f

File tree

9 files changed

+1964
-1326
lines changed

9 files changed

+1964
-1326
lines changed

messages/deploy.json

+3-5
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,17 @@
1414
"$ sfdx force:source:deploy -q 0Af9A00000FTM6pSAH`"
1515
],
1616
"flags": {
17-
"sourcePath": "comma-separated list of source file paths to retrieve",
18-
"manifest": "file path for manifest (package.xml) of components to retrieve",
17+
"sourcePath": "comma-separated list of source file paths to deploy",
18+
"manifest": "file path for manifest (package.xml) of components to deploy",
1919
"metadata": "comma-separated list of metadata component names",
2020
"wait": "wait time for command to finish in minutes",
21-
"packagename": "a comma-separated list of packages to retrieve",
22-
"verbose": "verbose output of retrieve result",
21+
"verbose": "verbose output of deploy result",
2322
"checkonly": "validate deploy but don’t save to the org",
2423
"testLevel": "deployment testing level",
2524
"runTests": "tests to run if --testlevel RunSpecifiedTests",
2625
"ignoreErrors": "ignore any errors and do not roll back deployment",
2726
"ignoreWarnings": "whether a warning will allow a deployment to complete successfully",
2827
"validateDeployRequestId": "request ID of the validated deployment to run a Quick Deploy"
2928
},
30-
"SourceRetrieveError": "Could not retrieve files in the sourcepath%s",
3129
"checkOnlySuccess": "\nSuccessfully validated the deployment"
3230
}

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"dependencies": {
88
"@oclif/config": "^1",
99
"@salesforce/command": "^3.1.0",
10-
"@salesforce/core": "^2.19.1",
11-
"@salesforce/source-deploy-retrieve": "^1.1.19",
10+
"@salesforce/core": "^2.20.5",
11+
"@salesforce/source-deploy-retrieve": "1.1.21",
1212
"chalk": "^4.1.0",
1313
"tslib": "^2"
1414
},

src/commands/force/source/deploy.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
78
import * as os from 'os';
89
import * as path from 'path';
910
import { flags, FlagsConfig } from '@salesforce/command';
1011
import { Lifecycle, Messages } from '@salesforce/core';
1112
import { DeployResult } from '@salesforce/source-deploy-retrieve';
1213
import { Duration } from '@salesforce/kit';
13-
import { asString, asArray } from '@salesforce/ts-types';
14+
import { asString, asArray, getBoolean } from '@salesforce/ts-types';
1415
import * as chalk from 'chalk';
1516
import { SourceCommand } from '../../../sourceCommand';
1617

@@ -30,7 +31,7 @@ export class Deploy extends SourceCommand {
3031
wait: flags.minutes({
3132
char: 'w',
3233
default: Duration.minutes(SourceCommand.DEFAULT_SRC_WAIT_MINUTES),
33-
min: Duration.minutes(SourceCommand.MINIMUM_SRC_WAIT_MINUTES),
34+
min: Duration.minutes(0), // wait=0 means deploy is asynchronous
3435
description: messages.getMessage('flags.wait'),
3536
}),
3637
testlevel: flags.enum({
@@ -90,20 +91,30 @@ export class Deploy extends SourceCommand {
9091
public async run(): Promise<DeployResult> {
9192
if (this.flags.validatedeployrequestid) {
9293
// TODO: return this.doDeployRecentValidation();
94+
throw Error('NOT IMPLEMENTED YET');
9395
}
9496
const hookEmitter = Lifecycle.getInstance();
9597

9698
const cs = await this.createComponentSet({
9799
sourcepath: asArray<string>(this.flags.sourcepath),
98100
manifest: asString(this.flags.manifest),
99101
metadata: asArray<string>(this.flags.metadata),
102+
apiversion: asString(this.flags.apiversion),
100103
});
101104

102105
await hookEmitter.emit('predeploy', { packageXmlPath: cs.getPackageXml() });
103106

104107
const results = await cs
105108
.deploy({
106109
usernameOrConnection: this.org.getUsername(),
110+
apiOptions: {
111+
ignoreWarnings: getBoolean(this.flags, 'ignorewarnings', false),
112+
rollbackOnError: !getBoolean(this.flags, 'ignoreerrors', false),
113+
checkOnly: getBoolean(this.flags, 'checkonly', false),
114+
runTests: asArray<string>(this.flags.runtests),
115+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
116+
testLevel: this.flags.testlevel,
117+
},
107118
})
108119
.start();
109120
await hookEmitter.emit('postdeploy', results);
@@ -121,7 +132,7 @@ export class Deploy extends SourceCommand {
121132
// sort by filename then fullname
122133
const failures = result.getFileResponses().sort((i, j) => {
123134
if (i.filePath === j.filePath) {
124-
// if the have the same directoryName then sort by fullName
135+
// if they have the same directoryName then sort by fullName
125136
return i.fullName < j.fullName ? 1 : -1;
126137
}
127138
return i.filePath < j.filePath ? 1 : -1;
@@ -177,7 +188,7 @@ export class Deploy extends SourceCommand {
177188
this.printComponentFailures(result);
178189
// TODO: this.printTestResults(result); <- this has WI @W-8903671@
179190
if (result.response.success && this.flags.checkonly) {
180-
this.log(messages.getMessage('checkOnlySuccess'));
191+
this.ux.log(messages.getMessage('checkOnlySuccess'));
181192
}
182193

183194
return result;

src/commands/force/source/retrieve.ts

+10-13
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
import * as os from 'os';
88

99
import { flags, FlagsConfig } from '@salesforce/command';
10-
import { Lifecycle, Messages, SfdxError, SfdxProjectJson } from '@salesforce/core';
10+
import { Lifecycle, Messages, SfdxError } from '@salesforce/core';
1111
import { Duration } from '@salesforce/kit';
1212
import { asArray, asString } from '@salesforce/ts-types';
1313
import { blue } from 'chalk';
14-
import { MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve';
14+
import { MetadataApiRetrieveStatus, RetrieveResult } from '@salesforce/source-deploy-retrieve';
1515
import { FileProperties } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
1616
import { SourceCommand } from '../../../sourceCommand';
1717

1818
Messages.importMessagesDirectory(__dirname);
1919
const messages = Messages.loadMessages('@salesforce/plugin-source', 'retrieve');
2020

21-
export class retrieve extends SourceCommand {
21+
export class Retrieve extends SourceCommand {
2222
public static readonly description = messages.getMessage('description');
2323
public static readonly examples = messages.getMessage('examples').split(os.EOL);
2424
public static readonly requiresProject = true;
@@ -32,7 +32,7 @@ export class retrieve extends SourceCommand {
3232
wait: flags.minutes({
3333
char: 'w',
3434
default: Duration.minutes(SourceCommand.DEFAULT_SRC_WAIT_MINUTES),
35-
min: SourceCommand.MINIMUM_SRC_WAIT_MINUTES,
35+
min: Duration.minutes(1),
3636
description: messages.getMessage('flags.wait'),
3737
}),
3838
manifest: flags.filepath({
@@ -53,30 +53,27 @@ export class retrieve extends SourceCommand {
5353
protected readonly lifecycleEventNames = ['preretrieve', 'postretrieve'];
5454

5555
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56-
public async run(): Promise<any> {
56+
public async run(): Promise<RetrieveResult> {
5757
const hookEmitter = Lifecycle.getInstance();
5858

59-
const proj = await SfdxProjectJson.create({});
60-
const packages = await proj.getPackageDirectories();
61-
const defaultPackage = packages.find((pkg) => pkg.default);
59+
const defaultPackagePath = this.project.getDefaultPackage().fullPath;
6260

6361
const cs = await this.createComponentSet({
64-
// safe to cast from the flags as an array of strings
6562
packagenames: asArray<string>(this.flags.packagenames),
6663
sourcepath: asArray<string>(this.flags.sourcepath),
6764
manifest: asString(this.flags.manifest),
6865
metadata: asArray<string>(this.flags.metadata),
66+
apiversion: asString(this.flags.apiversion),
6967
});
7068

71-
// emit pre retrieve event
72-
// needs to be a path to the temp dir package.xml
69+
// Emit the preretrieve event, which needs the package.xml from the ComponentSet
7370
await hookEmitter.emit('preretrieve', { packageXmlPath: cs.getPackageXml() });
7471

7572
const mdapiResult = await cs
7673
.retrieve({
7774
usernameOrConnection: this.org.getUsername(),
7875
merge: true,
79-
output: (this.flags.sourcepath as string) ?? defaultPackage.path,
76+
output: defaultPackagePath,
8077
packageNames: asArray<string>(this.flags.packagenames),
8178
})
8279
.start();
@@ -94,7 +91,7 @@ export class retrieve extends SourceCommand {
9491
this.printTable(results, true);
9592
}
9693

97-
return results;
94+
return mdapiResult;
9895
}
9996

10097
/**

src/sourceCommand.ts

+53-20
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
import * as path from 'path';
88
import { SfdxCommand } from '@salesforce/command';
99
import { ComponentSet } from '@salesforce/source-deploy-retrieve';
10-
import { fs, SfdxError } from '@salesforce/core';
10+
import { fs, SfdxError, Logger } from '@salesforce/core';
1111
import { ComponentLike } from '@salesforce/source-deploy-retrieve/lib/src/common';
1212

1313
export type FlagOptions = {
1414
packagenames?: string[];
1515
sourcepath: string[];
1616
manifest: string;
1717
metadata: string[];
18+
apiversion?: string;
1819
};
1920

2021
export abstract class SourceCommand extends SfdxCommand {
21-
public static MINIMUM_SRC_WAIT_MINUTES = 1;
2222
public static DEFAULT_SRC_WAIT_MINUTES = 33;
2323
/**
2424
* will create one ComponentSet to be deployed/retrieved
@@ -27,11 +27,13 @@ export abstract class SourceCommand extends SfdxCommand {
2727
* @param options: FlagOptions where to create ComponentSets from
2828
*/
2929
protected async createComponentSet(options: FlagOptions): Promise<ComponentSet> {
30+
const logger = Logger.childFromRoot(this.constructor.name);
3031
const setAggregator: ComponentLike[] = [];
3132

3233
// go through options to create a list of ComponentSets
3334
// we'll then combine all of those to deploy/retrieve
3435
if (options.sourcepath) {
36+
logger.debug(`Building ComponentSet from sourcepath: ${options.sourcepath.toString()}`);
3537
options.sourcepath.forEach((filepath) => {
3638
if (fs.fileExistsSync(filepath)) {
3739
setAggregator.push(...ComponentSet.fromSource(path.resolve(filepath)));
@@ -41,38 +43,69 @@ export abstract class SourceCommand extends SfdxCommand {
4143
});
4244
}
4345

46+
// Return empty ComponentSet and use packageNames in the library via `.retrieve` options
4447
if (options.packagenames) {
45-
// return ComponentSet and use packageNames in the library via `.retrieve` options
48+
logger.debug(`Building ComponentSet for packagenames: ${options.packagenames.toString()}`);
4649
setAggregator.push(...new ComponentSet([]));
4750
}
4851

52+
// Resolve manifest with source in package directories.
4953
if (options.manifest) {
50-
setAggregator.push(
51-
...(await ComponentSet.fromManifestFile(options.manifest, {
52-
// to create a link to the actual source component we need to have it resolve through all packages
53-
// to find the matching source metadata
54-
// this allows us to deploy after
55-
resolve: process.cwd(),
56-
}))
57-
);
54+
logger.debug(`Building ComponentSet from manifest: ${options.manifest}`);
55+
const packageDirs = this.project.getUniquePackageDirectories().map((pDir) => pDir.fullPath);
56+
for (const packageDir of packageDirs) {
57+
logger.debug(`Searching in packageDir: ${packageDir} for matching metadata`);
58+
const compSet = await ComponentSet.fromManifestFile(options.manifest, { resolve: packageDir });
59+
setAggregator.push(...compSet);
60+
}
5861
}
5962

63+
// Resolve metadata entries with source in package directories.
6064
if (options.metadata) {
65+
logger.debug(`Building ComponentSet from metadata: ${options.metadata.toString()}`);
66+
67+
// Build a Set of metadata entries
68+
const filter = new ComponentSet();
6169
options.metadata.forEach((entry) => {
6270
const splitEntry = entry.split(':');
63-
const metadata: ComponentLike = {
71+
filter.add({
6472
type: splitEntry[0],
65-
// either -m ApexClass or -m ApexClass:MyApexClass
6673
fullName: splitEntry.length === 1 ? '*' : splitEntry[1],
67-
};
68-
const cs = new ComponentSet([metadata]);
69-
// we need to search the entire project for the matching metadata component
70-
// no better way than to have it search than process.cwd()
71-
cs.resolveSourceComponents(process.cwd(), { filter: cs });
72-
setAggregator.push(...cs);
74+
});
7375
});
76+
77+
// Search the packages directories for matching metadata
78+
const packageDirs = this.project.getUniquePackageDirectories().map((pDir) => pDir.fullPath);
79+
logger.debug(`Searching for matching metadata in packageDirs: ${packageDirs.toString()}`);
80+
setAggregator.push(...ComponentSet.fromSource({ inclusiveFilter: filter, fsPaths: packageDirs }));
81+
}
82+
83+
const componentSet = new ComponentSet(setAggregator);
84+
85+
// This is only for debug output of matched files based on the command flags.
86+
// It will log up to 20 file matches.
87+
// TODO: add logger.debugEnabled
88+
if (componentSet.size) {
89+
logger.debug(`Matching metadata files (${componentSet.size}):`);
90+
const components = componentSet.getSourceComponents().toArray();
91+
for (let i = 0; i < componentSet.size; i++) {
92+
if (components[i]?.content) {
93+
logger.debug(components[i].content);
94+
} else if (components[i]?.xml) {
95+
logger.debug(components[i].xml);
96+
}
97+
98+
if (i > 18) {
99+
logger.debug(`(showing 20 of ${componentSet.size} matches)`);
100+
break;
101+
}
102+
}
103+
}
104+
105+
if (options.apiversion) {
106+
componentSet.apiVersion = options.apiversion;
74107
}
75108

76-
return new ComponentSet(setAggregator);
109+
return componentSet;
77110
}
78111
}

0 commit comments

Comments
 (0)