diff --git a/package.json b/package.json index 6aba1150..f483f885 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@salesforce/kit": "^3.2.1", "@salesforce/plugin-info": "^3.3.28", "@salesforce/sf-plugins-core": "^11.3.2", - "@salesforce/source-deploy-retrieve": "^12.5.1", + "@salesforce/source-deploy-retrieve": "^12.6.0", "@salesforce/source-tracking": "^7.1.7", "@salesforce/ts-types": "^2.0.12", "ansis": "^3.3.2" diff --git a/schemas/project-deploy-cancel.json b/schemas/project-deploy-cancel.json index 9e321a42..9e5a0c41 100644 --- a/schemas/project-deploy-cancel.json +++ b/schemas/project-deploy-cancel.json @@ -8,6 +8,12 @@ "type": "object", "additionalProperties": false, "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/FileResponse" + } + }, "replacements": { "type": "object", "additionalProperties": { @@ -17,11 +23,11 @@ } } }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/FileResponse" - } + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" }, "id": { "type": "string" @@ -496,6 +502,12 @@ "$ref": "#/definitions/FileResponse" } }, + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" + }, "canceledBy": { "type": "string" }, diff --git a/schemas/project-deploy-quick.json b/schemas/project-deploy-quick.json index 9e321a42..9e5a0c41 100644 --- a/schemas/project-deploy-quick.json +++ b/schemas/project-deploy-quick.json @@ -8,6 +8,12 @@ "type": "object", "additionalProperties": false, "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/FileResponse" + } + }, "replacements": { "type": "object", "additionalProperties": { @@ -17,11 +23,11 @@ } } }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/FileResponse" - } + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" }, "id": { "type": "string" @@ -496,6 +502,12 @@ "$ref": "#/definitions/FileResponse" } }, + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" + }, "canceledBy": { "type": "string" }, diff --git a/schemas/project-deploy-report.json b/schemas/project-deploy-report.json index 9e321a42..9e5a0c41 100644 --- a/schemas/project-deploy-report.json +++ b/schemas/project-deploy-report.json @@ -8,6 +8,12 @@ "type": "object", "additionalProperties": false, "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/FileResponse" + } + }, "replacements": { "type": "object", "additionalProperties": { @@ -17,11 +23,11 @@ } } }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/FileResponse" - } + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" }, "id": { "type": "string" @@ -496,6 +502,12 @@ "$ref": "#/definitions/FileResponse" } }, + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" + }, "canceledBy": { "type": "string" }, diff --git a/schemas/project-deploy-resume.json b/schemas/project-deploy-resume.json index 9e321a42..9e5a0c41 100644 --- a/schemas/project-deploy-resume.json +++ b/schemas/project-deploy-resume.json @@ -8,6 +8,12 @@ "type": "object", "additionalProperties": false, "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/FileResponse" + } + }, "replacements": { "type": "object", "additionalProperties": { @@ -17,11 +23,11 @@ } } }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/FileResponse" - } + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" }, "id": { "type": "string" @@ -496,6 +502,12 @@ "$ref": "#/definitions/FileResponse" } }, + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" + }, "canceledBy": { "type": "string" }, diff --git a/schemas/project-deploy-start.json b/schemas/project-deploy-start.json index 9e321a42..9e5a0c41 100644 --- a/schemas/project-deploy-start.json +++ b/schemas/project-deploy-start.json @@ -8,6 +8,12 @@ "type": "object", "additionalProperties": false, "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/FileResponse" + } + }, "replacements": { "type": "object", "additionalProperties": { @@ -17,11 +23,11 @@ } } }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/FileResponse" - } + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" }, "id": { "type": "string" @@ -496,6 +502,12 @@ "$ref": "#/definitions/FileResponse" } }, + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" + }, "canceledBy": { "type": "string" }, diff --git a/schemas/project-deploy-validate.json b/schemas/project-deploy-validate.json index 9e321a42..9e5a0c41 100644 --- a/schemas/project-deploy-validate.json +++ b/schemas/project-deploy-validate.json @@ -8,6 +8,12 @@ "type": "object", "additionalProperties": false, "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/FileResponse" + } + }, "replacements": { "type": "object", "additionalProperties": { @@ -17,11 +23,11 @@ } } }, - "files": { - "type": "array", - "items": { - "$ref": "#/definitions/FileResponse" - } + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" }, "id": { "type": "string" @@ -496,6 +502,12 @@ "$ref": "#/definitions/FileResponse" } }, + "zipSize": { + "type": "number" + }, + "zipFileCount": { + "type": "number" + }, "canceledBy": { "type": "string" }, diff --git a/src/commands/project/deploy/start.ts b/src/commands/project/deploy/start.ts index a9467978..8b5531f7 100644 --- a/src/commands/project/deploy/start.ts +++ b/src/commands/project/deploy/start.ts @@ -7,14 +7,14 @@ import ansis from 'ansis'; import { EnvironmentVariable, Lifecycle, Messages, OrgConfigProperties, SfError } from '@salesforce/core'; -import { DeployVersionData } from '@salesforce/source-deploy-retrieve'; +import { type DeployVersionData, DeployZipData } from '@salesforce/source-deploy-retrieve'; import { Duration } from '@salesforce/kit'; import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core'; import { SourceConflictError } from '@salesforce/source-tracking'; import { AsyncDeployResultFormatter } from '../../../formatters/asyncDeployResultFormatter.js'; import { DeployResultFormatter } from '../../../formatters/deployResultFormatter.js'; +import { AsyncDeployResultJson, DeployResultJson, TestLevel } from '../../../utils/types.js'; import { DeployProgress } from '../../../utils/progressBar.js'; -import { DeployResultJson, TestLevel } from '../../../utils/types.js'; import { executeDeploy, resolveApi, validateTests, determineExitCode } from '../../../utils/deploy.js'; import { DeployCache } from '../../../utils/deployCache.js'; import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes.js'; @@ -22,6 +22,7 @@ import { ConfigVars } from '../../../configMeta.js'; import { coverageFormattersFlag, fileOrDirFlag, testLevelFlag, testsFlag } from '../../../utils/flags.js'; import { writeConflictTable } from '../../../utils/conflicts.js'; import { getOptionalProject } from '../../../utils/project.js'; +import { getZipFileSize } from '../../../utils/output.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata'); @@ -176,6 +177,9 @@ export default class DeployMetadata extends SfCommand { public static errorCodes = toHelpSection('ERROR CODES', DEPLOY_STATUS_CODES_DESCRIPTIONS); + private zipSize?: number; + private zipFileCount?: number; + public async run(): Promise { const { flags } = await this.parse(DeployMetadata); const project = await getOptionalProject(); @@ -214,6 +218,16 @@ export default class DeployMetadata extends SfCommand { ); }); + // eslint-disable-next-line @typescript-eslint/require-await + Lifecycle.getInstance().on('deployZipData', async (zipData: DeployZipData) => { + this.zipSize = zipData.zipSize; + if (flags.verbose && this.zipSize) this.log(`Deploy size: ${getZipFileSize(this.zipSize)} of ~39 MB limit`); + if (zipData.zipFileCount) { + this.zipFileCount = zipData.zipFileCount; + if (flags.verbose && this.zipSize) this.log(`Deployed files count: ${this.zipFileCount} of 10,000 limit`); + } + }); + const { deploy } = await executeDeploy( { ...flags, @@ -239,7 +253,8 @@ export default class DeployMetadata extends SfCommand { } const asyncFormatter = new AsyncDeployResultFormatter(deploy.id); if (!this.jsonEnabled()) asyncFormatter.display(); - return asyncFormatter.getJson(); + + return this.mixinZipMeta(await asyncFormatter.getJson()); } new DeployProgress(deploy, this.jsonEnabled()).start(); @@ -255,7 +270,7 @@ export default class DeployMetadata extends SfCommand { await DeployCache.update(deploy.id, { status: result.response.status }); - return formatter.getJson(); + return this.mixinZipMeta(await formatter.getJson()); } protected catch(error: Error | SfError): Promise { @@ -281,4 +296,14 @@ export default class DeployMetadata extends SfCommand { } return super.catch(error); } + + private mixinZipMeta(json: AsyncDeployResultJson | DeployResultJson): AsyncDeployResultJson | DeployResultJson { + if (this.zipSize) { + json.zipSize = this.zipSize; + } + if (this.zipFileCount) { + json.zipFileCount = this.zipFileCount; + } + return json; + } } diff --git a/src/utils/output.ts b/src/utils/output.ts index eaf075c3..3c5fad0b 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -38,6 +38,15 @@ export const exitCodeAsNumber = (): number | undefined => { } }; +export const getZipFileSize = (bytes: number): string => { + const units = ['B', 'KB', 'MB', 'GB']; + while (bytes > 1024 && units.length) { + bytes /= 1024; + units.shift(); + } + return parseFloat(bytes.toFixed(2)) + ' ' + units[0]; +}; + /** oclif table doesn't like "interface" but likes "type". SDR exports an interface */ export const getFileResponseSuccessProps = ( s: FileResponseSuccess diff --git a/src/utils/types.ts b/src/utils/types.ts index 6f37978f..103138e2 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -43,6 +43,8 @@ export type Verbosity = 'verbose' | 'concise' | 'normal'; export type AsyncDeployResultJson = Omit, 'status'> & { status: RequestStatus | 'Queued' | 'Nothing to deploy'; files: FileResponse[]; + zipSize?: number; + zipFileCount?: number; }; type ConvertEntry = { @@ -72,7 +74,12 @@ export type DeleteSourceJson = { export type CoverageResultsFileInfo = Record, string>; export type DeployResultJson = - | (MetadataApiDeployStatus & { files: FileResponse[] } & { replacements?: Record }) + | (MetadataApiDeployStatus & { + files: FileResponse[]; + replacements?: Record; + zipSize?: number; + zipFileCount?: number; + }) | AsyncDeployResultJson; export type MetadataRetrieveResultJson = Omit & { diff --git a/test/nuts/deploy/verbose.nut.ts b/test/nuts/deploy/verbose.nut.ts new file mode 100644 index 00000000..c3a2047b --- /dev/null +++ b/test/nuts/deploy/verbose.nut.ts @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 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 as pathJoin } from 'node:path'; +import { expect } from 'chai'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { type DeployResultJson } from '../../../src/utils/types.js'; + +describe('Deploy --verbose', () => { + let testkit: TestSession; + + before(async () => { + testkit = await TestSession.create({ + project: { gitClone: 'https://github.com/salesforcecli/sample-project-multiple-packages' }, + scratchOrgs: [{ setDefault: true, config: pathJoin('config', 'project-scratch-def.json') }], + devhubAuthStrategy: 'AUTO', + }); + }); + + after(async () => { + await testkit?.clean(); + }); + + it('should have zip file size and file count returned with --json', () => { + const cmdJson = execCmd( + 'project deploy start --source-dir force-app/main/default/apex --verbose --json', + { + ensureExitCode: 0, + } + ).jsonOutput; + + expect(cmdJson?.result.zipSize).to.be.within(1775, 1795); + expect(cmdJson?.result.zipFileCount).to.equal(5); + }); + + it('should have zip file size and file count in the output', () => { + const shellOutput = execCmd( + 'project deploy start --source-dir force-app/main/default/apex --verbose', + { + ensureExitCode: 0, + } + ).shellOutput; + + expect(shellOutput.stdout).to.contain('Deploy size: ').and.contain('KB of ~39 MB limit'); + expect(shellOutput.stdout).to.contain('Deployed files count: 5 of 10,000 limit'); + }); + + it('should have zip file size and file count returned with --json --async', () => { + const cmdJson = execCmd( + 'project deploy start --source-dir force-app/main/default/apex --verbose --async --json', + { + ensureExitCode: 0, + } + ).jsonOutput; + + expect(cmdJson?.result.zipSize).to.be.within(1775, 1795); + expect(cmdJson?.result.zipFileCount).to.equal(5); + }); + + it('should have zip file size and file count in the output with --async', () => { + const shellOutput = execCmd( + 'project deploy start --source-dir force-app/main/default/apex --verbose --async', + { + ensureExitCode: 0, + } + ).shellOutput; + + expect(shellOutput.stdout).to.contain('Deploy size: ').and.contain('KB of ~39 MB limit'); + expect(shellOutput.stdout).to.contain('Deployed files count: 5 of 10,000 limit'); + }); +}); diff --git a/test/utils/output.test.ts b/test/utils/output.test.ts index f30cfc8c..0f11bb6f 100644 --- a/test/utils/output.test.ts +++ b/test/utils/output.test.ts @@ -10,6 +10,7 @@ import sinon from 'sinon'; import { DeployMessage, DeployResult, FileResponse } from '@salesforce/source-deploy-retrieve'; import { Ux } from '@salesforce/sf-plugins-core'; import { getCoverageFormattersOptions } from '../../src/utils/coverage.js'; +import { getZipFileSize } from '../../src/utils/output.js'; import { DeployResultFormatter } from '../../src/formatters/deployResultFormatter.js'; import { getDeployResult } from './deployResponses.js'; @@ -223,3 +224,23 @@ describe('deployResultFormatter', () => { }); }); }); + +describe('output util functions', () => { + describe('getZipFileSize', () => { + it('should return correct number of Bytes if 0', () => { + expect(getZipFileSize(0)).to.equal('0 B'); + }); + it('should return correct number of Bytes', () => { + expect(getZipFileSize(724)).to.equal('724 B'); + }); + it('should return correct number of KiloBytes', () => { + expect(getZipFileSize(46_694)).to.equal('45.6 KB'); + }); + it('should return correct number of MegaBytes', () => { + expect(getZipFileSize(724_992_234)).to.equal('691.41 MB'); + }); + it('should return correct number of GigaBytes', () => { + expect(getZipFileSize(724_844_993_378)).to.equal('675.06 GB'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 74271a1f..895193d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1728,10 +1728,10 @@ string-width "^7.2.0" terminal-link "^3.0.0" -"@salesforce/source-deploy-retrieve@^12.4.0", "@salesforce/source-deploy-retrieve@^12.5.1": - version "12.5.1" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.5.1.tgz#55e915201b2c9320b9662b2c8500a191c8770ecf" - integrity sha512-jakBWFSIb8oZlUAf0QKHXaeFA/KuTQZwaKZVevdwaiuy43lJHzVVrSRfcNv/kjXxmg0oq5TAI8vUo2CC5Hq04A== +"@salesforce/source-deploy-retrieve@^12.4.0", "@salesforce/source-deploy-retrieve@^12.6.0": + version "12.6.0" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.6.0.tgz#0d2961fb2befeac0431fe8f6a70f439bc049fcea" + integrity sha512-gowMwjG93a8Bd5N05o46Z46TSsHZMeZXV3rQwMP3LXDnIhvSx3vxHJMzk0KKMOatfQFF2t+YI/6MR+47KWSblw== dependencies: "@salesforce/core" "^8.4.0" "@salesforce/kit" "^3.2.1" @@ -1741,6 +1741,7 @@ got "^11.8.6" graceful-fs "^4.2.11" ignore "^5.3.2" + isbinaryfile "^5.0.2" jszip "^3.10.1" mime "2.6.0" minimatch "^9.0.5" @@ -5410,6 +5411,11 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isbinaryfile@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.2.tgz#fe6e4dfe2e34e947ffa240c113444876ba393ae0" + integrity sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"