Skip to content

Commit df2f5e0

Browse files
fix: add progress bar to deploy (#65)
1 parent ea064da commit df2f5e0

File tree

4 files changed

+120
-16
lines changed

4 files changed

+120
-16
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@salesforce/core": "^2.20.8",
1111
"@salesforce/source-deploy-retrieve": "1.1.21",
1212
"chalk": "^4.1.0",
13+
"cli-ux": "^5.5.1",
1314
"tslib": "^2"
1415
},
1516
"devDependencies": {

src/commands/force/source/deploy.ts

+68-14
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import * as os from 'os';
99
import * as path from 'path';
1010
import { flags, FlagsConfig } from '@salesforce/command';
1111
import { Lifecycle, Messages } from '@salesforce/core';
12-
import { DeployResult } from '@salesforce/source-deploy-retrieve';
12+
import { DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve';
1313
import { Duration } from '@salesforce/kit';
1414
import { asString, asArray, getBoolean, JsonCollection } from '@salesforce/ts-types';
1515
import * as chalk from 'chalk';
16+
import cli from 'cli-ux';
17+
import { env } from '@salesforce/kit';
1618
import { SourceCommand } from '../../../sourceCommand';
1719

1820
Messages.importMessagesDirectory(__dirname);
1921
const messages = Messages.loadMessages('@salesforce/plugin-source', 'deploy');
2022

23+
type TestLevel = 'NoTestRun' | 'RunSpecifiedTests' | 'RunLocalTests' | 'RunAllTestsInOrg';
24+
2125
export class Deploy extends SourceCommand {
2226
public static readonly description = messages.getMessage('description');
2327
public static readonly examples = messages.getMessage('examples').split(os.EOL);
@@ -111,19 +115,24 @@ export class Deploy extends SourceCommand {
111115

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

114-
const results = await cs
115-
.deploy({
116-
usernameOrConnection: this.org.getUsername(),
117-
apiOptions: {
118-
ignoreWarnings: getBoolean(this.flags, 'ignorewarnings', false),
119-
rollbackOnError: !getBoolean(this.flags, 'ignoreerrors', false),
120-
checkOnly: getBoolean(this.flags, 'checkonly', false),
121-
runTests: asArray<string>(this.flags.runtests),
122-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
123-
testLevel: this.flags.testlevel,
124-
},
125-
})
126-
.start();
118+
const deploy = cs.deploy({
119+
usernameOrConnection: this.org.getUsername(),
120+
apiOptions: {
121+
ignoreWarnings: getBoolean(this.flags, 'ignorewarnings', false),
122+
rollbackOnError: !getBoolean(this.flags, 'ignoreerrors', false),
123+
checkOnly: getBoolean(this.flags, 'checkonly', false),
124+
runTests: asArray<string>(this.flags.runtests),
125+
testLevel: this.flags.testlevel as TestLevel,
126+
},
127+
});
128+
129+
// if SFDX_USE_PROGRESS_BAR is true and no --json flag use progress bar, if not, skip
130+
if (env.getBoolean('SFDX_USE_PROGRESS_BAR', true) && !this.flags.json) {
131+
this.progress(deploy);
132+
}
133+
134+
const results = await deploy.start();
135+
127136
await hookEmitter.emit('postdeploy', results);
128137

129138
// skip a lot of steps that would do nothing
@@ -134,6 +143,51 @@ export class Deploy extends SourceCommand {
134143
return results;
135144
}
136145

146+
private progress(deploy: MetadataApiDeploy): void {
147+
// cli.progress doesn't have typings
148+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
149+
const progressBar = cli.progress({
150+
format: 'SOURCE PROGRESS | {bar} | {value}/{total} Components',
151+
barCompleteChar: '\u2588',
152+
barIncompleteChar: '\u2591',
153+
linewrap: true,
154+
});
155+
let printOnce = true;
156+
deploy.onUpdate((data) => {
157+
// the numCompTot. isn't computed right away, wait to start until we know how many we have
158+
if (data.numberComponentsTotal && printOnce) {
159+
this.ux.log(`Job ID | ${data.id}`);
160+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
161+
progressBar.start(data.numberComponentsTotal + data.numberTestsTotal);
162+
printOnce = false;
163+
}
164+
165+
// the numTestsTot. isn't computed until validated as tests by the server, update the PB once we know
166+
if (data.numberTestsTotal) {
167+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
168+
progressBar.setTotal(data.numberComponentsTotal + data.numberTestsTotal);
169+
}
170+
171+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
172+
progressBar.update(data.numberComponentsDeployed + data.numberTestsCompleted);
173+
});
174+
175+
deploy.onFinish(() => {
176+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
177+
progressBar.stop();
178+
});
179+
180+
deploy.onCancel(() => {
181+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
182+
progressBar.stop();
183+
});
184+
185+
deploy.onError(() => {
186+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
187+
progressBar.stop();
188+
});
189+
}
190+
137191
private printComponentFailures(result: DeployResult): void {
138192
if (result.response.status === 'Failed' && result.components) {
139193
// sort by filename then fullname

test/commands/source/deploy.test.ts

+49
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('force:source:deploy', () => {
2424

2525
// Stubs
2626
let createComponentSetStub: sinon.SinonStub;
27+
let progressStub: sinon.SinonStub;
2728
let deployStub: sinon.SinonStub;
2829
let startStub: sinon.SinonStub;
2930
let lifecycleEmitStub: sinon.SinonStub;
@@ -44,12 +45,15 @@ describe('force:source:deploy', () => {
4445
getUsername: () => username,
4546
},
4647
createComponentSet: createComponentSetStub,
48+
progress: progressStub,
49+
print: () => {},
4750
}) as Promise<DeployResult>;
4851
};
4952

5053
beforeEach(() => {
5154
startStub = sandbox.stub().returns(stubbedResults);
5255
deployStub = sandbox.stub().returns({ start: startStub });
56+
progressStub = sandbox.stub();
5357
createComponentSetStub = sandbox.stub().returns({
5458
deploy: deployStub,
5559
getPackageXml: () => packageXml,
@@ -107,13 +111,18 @@ describe('force:source:deploy', () => {
107111
expect(lifecycleEmitStub.secondCall.args[1]).to.deep.equal(stubbedResults);
108112
};
109113

114+
const ensureProgressBar = (callCount: number) => {
115+
expect(progressStub.callCount).to.equal(callCount);
116+
};
117+
110118
it('should pass along sourcepath', async () => {
111119
const sourcepath = ['somepath'];
112120
const result = await run({ sourcepath, json: true });
113121
expect(result).to.deep.equal(stubbedResults);
114122
ensureCreateComponentSetArgs({ sourcepath });
115123
ensureDeployArgs();
116124
ensureHookArgs();
125+
ensureProgressBar(0);
117126
});
118127

119128
it('should pass along metadata', async () => {
@@ -123,6 +132,7 @@ describe('force:source:deploy', () => {
123132
ensureCreateComponentSetArgs({ metadata });
124133
ensureDeployArgs();
125134
ensureHookArgs();
135+
ensureProgressBar(0);
126136
});
127137

128138
it('should pass along manifest', async () => {
@@ -132,6 +142,7 @@ describe('force:source:deploy', () => {
132142
ensureCreateComponentSetArgs({ manifest });
133143
ensureDeployArgs();
134144
ensureHookArgs();
145+
ensureProgressBar(0);
135146
});
136147

137148
it('should pass along apiversion', async () => {
@@ -142,6 +153,7 @@ describe('force:source:deploy', () => {
142153
ensureCreateComponentSetArgs({ apiversion, manifest });
143154
ensureDeployArgs();
144155
ensureHookArgs();
156+
ensureProgressBar(0);
145157
});
146158

147159
it('should pass along all deploy options', async () => {
@@ -170,5 +182,42 @@ describe('force:source:deploy', () => {
170182
},
171183
});
172184
ensureHookArgs();
185+
ensureProgressBar(0);
186+
});
187+
188+
it('should NOT call progress bar because of environment variable', async () => {
189+
try {
190+
process.env.SFDX_USE_PROGRESS_BAR = 'false';
191+
const sourcepath = ['somepath'];
192+
const result = await run({ sourcepath });
193+
expect(result).to.deep.equal(stubbedResults);
194+
ensureCreateComponentSetArgs({ sourcepath });
195+
ensureDeployArgs();
196+
ensureHookArgs();
197+
ensureProgressBar(0);
198+
} finally {
199+
delete process.env.SFDX_USE_PROGRESS_BAR;
200+
}
201+
});
202+
203+
it('should NOT call progress bar because of --json', async () => {
204+
const sourcepath = ['somepath'];
205+
const result = await run({ sourcepath, json: true });
206+
expect(result).to.deep.equal(stubbedResults);
207+
expect(progressStub.called).to.be.false;
208+
ensureCreateComponentSetArgs({ sourcepath });
209+
ensureDeployArgs();
210+
ensureHookArgs();
211+
ensureProgressBar(0);
212+
});
213+
214+
it('should call progress bar', async () => {
215+
const sourcepath = ['somepath'];
216+
const result = await run({ sourcepath });
217+
expect(result).to.deep.equal(stubbedResults);
218+
ensureCreateComponentSetArgs({ sourcepath });
219+
ensureDeployArgs();
220+
ensureHookArgs();
221+
ensureProgressBar(1);
173222
});
174223
});

yarn.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -1722,9 +1722,9 @@ cli-ux@^4.9.3:
17221722
treeify "^1.1.0"
17231723
tslib "^1.9.3"
17241724

1725-
cli-ux@^5.2.1:
1725+
cli-ux@^5.2.1, cli-ux@^5.5.1:
17261726
version "5.5.1"
1727-
resolved "https://registry.npmjs.org/cli-ux/-/cli-ux-5.5.1.tgz#99d28dae0c3ef7845fa2ea56e066a1d5fcceca9e"
1727+
resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-5.5.1.tgz#99d28dae0c3ef7845fa2ea56e066a1d5fcceca9e"
17281728
integrity sha512-t3DT1U1C3rArLGYLpKa3m9dr/8uKZRI8HRm/rXKL7UTjm4c+Yd9zHNWg1tP8uaJkUbhmvx5SQHwb3VWpPUVdHQ==
17291729
dependencies:
17301730
"@oclif/command" "^1.6.0"

0 commit comments

Comments
 (0)