diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c303f370..90cf8371f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -52,12 +52,10 @@ workflows: - linux - windows node_version: - - latest + # - latest - lts - maintenance exclude: - - os: windows - node_version: lts - os: windows node_version: maintenance - release-management/test-nut: @@ -79,6 +77,7 @@ workflows: - 'yarn test:nuts:retrieve' - 'yarn test:nuts:specialTypes' - 'yarn test:nuts:deploy:destructive' + - 'yarn test:nuts:tracking' - release-management/release-package: sign: true github-release: true diff --git a/.eslintrc.js b/.eslintrc.js index 83ad149c3..339133558 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,4 +6,5 @@ */ module.exports = { extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license'], + ignorePatterns: ['test/nuts/ebikes-lwc/**'], }; diff --git a/.gitignore b/.gitignore index 263ca065b..8ed064959 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ node_modules # os specific files .DS_Store .idea + +# ignore generated nut tests +test/nuts/generated/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4429489b2..6deadfe29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,9 @@ All notable changes to this project will be documented in this file. See [standa ### [1.2.6](https://github.com/salesforcecli/plugin-source/compare/v1.2.5...v1.2.6) (2021-10-21) - ### Bug Fixes -* use cross-env for test:nuts script ([#260](https://github.com/salesforcecli/plugin-source/issues/260)) ([76627fb](https://github.com/salesforcecli/plugin-source/commit/76627fb21c62a4fb140c87ccfc266accd79af3fd)) +- use cross-env for test:nuts script ([#260](https://github.com/salesforcecli/plugin-source/issues/260)) ([76627fb](https://github.com/salesforcecli/plugin-source/commit/76627fb21c62a4fb140c87ccfc266accd79af3fd)) ### [1.2.5](https://github.com/salesforcecli/plugin-source/compare/v1.2.4...v1.2.5) (2021-10-21) diff --git a/README.md b/README.md index 668829747..7e39174df 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ sfdx plugins # Usage + ```sh-session $ npm install -g @salesforce/plugin-source $ sfdx COMMAND @@ -86,27 +87,29 @@ USAGE $ sfdx COMMAND ... ``` + # Commands -* [`sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceconvert--r-directory--d-directory--n-string--p-array---x-string---m-array---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx `](#sfdx-) -* [`sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [--predestructivechanges ] [--postdestructivechanges ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploy---soapdeploy--w-minutes--q-id---x-filepath---m-array---p-array---c---l-notestrunrunspecifiedtestsrunlocaltestsrunalltestsinorg---r-array---o---g---predestructivechanges-filepath----postdestructivechanges-filepath---u-string---apiversion-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploycancel--w-minutes--i-id--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeployreport--w-minutes--i-id--u-string---apiversion-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:manifest:create [-m | -p ] [-n | -t pre|post|destroy|package] [-o ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcemanifestcreate--m-array---p-array--n-string---t-prepostdestroypackage--o-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:open -f [-r] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceopen--f-filepath--r--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceretrieve--p-array---x-filepath---m-array--w-minutes--n-array--u-string--a-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) + +- [`sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceconvert--r-directory--d-directory--n-string--p-array---x-string---m-array---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx `](#sfdx-) +- [`sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [--predestructivechanges ] [--postdestructivechanges ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploy---soapdeploy--w-minutes--q-id---x-filepath---m-array---p-array---c---l-notestrunrunspecifiedtestsrunlocaltestsrunalltestsinorg---r-array---o---g---predestructivechanges-filepath----postdestructivechanges-filepath---u-string---apiversion-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploycancel--w-minutes--i-id--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeployreport--w-minutes--i-id--u-string---apiversion-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:manifest:create [-m | -p ] [-n | -t pre|post|destroy|package] [-o ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcemanifestcreate--m-array---p-array--n-string---t-prepostdestroypackage--o-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:open -f [-r] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceopen--f-filepath--r--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceretrieve--p-array---x-filepath---m-array--w-minutes--n-array--u-string--a-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) ## `sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` -convert source into Metadata API format +convert source into Metadata API format ``` -convert source into Metadata API format - Converts source-formatted files into metadata that you can deploy using Metadata API. +convert source into Metadata API format + Converts source-formatted files into metadata that you can deploy using Metadata API. To convert source-formatted files into the metadata format, so that you can deploy them using Metadata API, run "sfdx force:source:convert". Then deploy the metadata using "sfdx force:mdapi:deploy". @@ -115,7 +118,7 @@ To convert Metadata API–formatted files into the source format, run "sfdx forc To specify a package name that includes spaces, enclose the name in single quotes. USAGE - $ sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] + $ sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -145,7 +148,7 @@ OPTIONS this command invocation DESCRIPTION - Converts source-formatted files into metadata that you can deploy using Metadata API. + Converts source-formatted files into metadata that you can deploy using Metadata API. To convert source-formatted files into the metadata format, so that you can deploy them using Metadata API, run "sfdx force:source:convert". Then deploy the metadata using "sfdx force:mdapi:deploy". @@ -162,10 +165,10 @@ _See code: [src/commands/force/source/convert.ts](https://github.com/salesforcec ## `sfdx ` -delete source from your project and from a non-source-tracked org +delete source from your project and from a non-source-tracked org ``` -delete source from your project and from a non-source-tracked org +delete source from your project and from a non-source-tracked org Use this command to delete components from orgs that don’t have source tracking. To remove deleted items from scratch orgs, which have change tracking, use "sfdx force:source:push". @@ -235,9 +238,9 @@ If the comma-separated list you’re supplying contains spaces, enclose the enti If you use the --manifest, --predestructivechanges, or --postdestructivechanges parameters, run the force:source:manifest:create command to easily generate the different types of manifest files. USAGE - $ sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l - NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [--predestructivechanges - ] [--postdestructivechanges ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel + $ sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l + NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [--predestructivechanges + ] [--postdestructivechanges ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -302,22 +305,22 @@ DESCRIPTION To take advantage of change tracking with scratch orgs, use "sfdx force:source:push". To deploy metadata that’s in metadata format, use "sfdx force:mdapi:deploy". - The source you deploy overwrites the corresponding metadata in your org. This command does not attempt to merge your + The source you deploy overwrites the corresponding metadata in your org. This command does not attempt to merge your source with the versions in your org. - To run the command asynchronously, set --wait to 0, which immediately returns the job ID. This way, you can continue + To run the command asynchronously, set --wait to 0, which immediately returns the job ID. This way, you can continue to use the CLI. To check the status of the job, use force:source:deploy:report. - If the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of + If the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of double quotes. On Windows, if the list contains commas, also enclose the entire list in one set of double quotes. - If you use the --manifest, --predestructivechanges, or --postdestructivechanges parameters, run the + If you use the --manifest, --predestructivechanges, or --postdestructivechanges parameters, run the force:source:manifest:create command to easily generate the different types of manifest files. EXAMPLES To deploy the source files in a directory: $ sfdx force:source:deploy -p path/to/source - To deploy a specific Apex class and the objects whose source is in a directory: + To deploy a specific Apex class and the objects whose source is in a directory: $ sfdx force:source:deploy -p "path/to/apex/classes/MyClass.cls,path/to/source/objects" To deploy source files in a comma-separated list that contains spaces: $ sfdx force:source:deploy -p "path/to/objects/MyCustomObject/fields/MyField.field-meta.xml, path/to/apex/classes" @@ -357,7 +360,7 @@ To run the command asynchronously, set --wait to 0, which immediately returns th To check the status of the job, use force:source:deploy:report. USAGE - $ sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel + $ sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -380,10 +383,10 @@ OPTIONS this command invocation DESCRIPTION - Use this command to cancel a specified asynchronous source deployment. You can also specify a wait time (in minutes) + Use this command to cancel a specified asynchronous source deployment. You can also specify a wait time (in minutes) to check for updates to the canceled deploy status. - To run the command asynchronously, set --wait to 0, which immediately returns the job ID. This way, you can continue + To run the command asynchronously, set --wait to 0, which immediately returns the job ID. This way, you can continue to use the CLI. To check the status of the job, use force:source:deploy:report. @@ -402,14 +405,14 @@ _See code: [src/commands/force/source/deploy/cancel.ts](https://github.com/sales ## `sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` -check the status of a metadata deployment +check the status of a metadata deployment ``` -check the status of a metadata deployment +check the status of a metadata deployment Specify the job ID for the deploy you want to check. You can also specify a wait time (minutes) to check for updates to the deploy status. USAGE - $ sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--verbose] [--json] + $ sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -434,7 +437,7 @@ OPTIONS --verbose verbose output of deploy result DESCRIPTION - Specify the job ID for the deploy you want to check. You can also specify a wait time (minutes) to check for updates + Specify the job ID for the deploy you want to check. You can also specify a wait time (minutes) to check for updates to the deploy status. EXAMPLES @@ -452,10 +455,10 @@ _See code: [src/commands/force/source/deploy/report.ts](https://github.com/sales ## `sfdx force:source:manifest:create [-m | -p ] [-n | -t pre|post|destroy|package] [-o ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` -create a project manifest that lists the metadata components you want to deploy or retrieve +create a project manifest that lists the metadata components you want to deploy or retrieve ``` -create a project manifest that lists the metadata components you want to deploy or retrieve +create a project manifest that lists the metadata components you want to deploy or retrieve Create a manifest from a list of metadata components (--metadata) or from one or more local directories that contain source files (--sourcepath). You can specify either of these parameters, not both. Use --manifesttype to specify the type of manifest you want to create. The resulting manifest files have specific names, such as the standard package.xml or destructiveChanges.xml to delete metadata. Valid values for this parameter, and their respective file names, are: @@ -465,14 +468,14 @@ Use --manifesttype to specify the type of manifest you want to create. The resul post : destructiveChangesPost.xml destroy : destructiveChanges.xml -See https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy_deleting_files.htm for information about these destructive manifest files. +See https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy_deleting_files.htm for information about these destructive manifest files. Use --manifestname to specify a custom name for the generated manifest if the pre-defined ones don’t suit your needs. You can specify either --manifesttype or --manifestname, but not both. USAGE - $ sfdx force:source:manifest:create [-m | -p ] [-n | -t pre|post|destroy|package] [-o - ] [--apiversion ] [--json] [--loglevel + $ sfdx force:source:manifest:create [-m | -p ] [-n | -t pre|post|destroy|package] [-o + ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -503,11 +506,11 @@ OPTIONS this command invocation DESCRIPTION - Create a manifest from a list of metadata components (--metadata) or from one or more local directories that contain + Create a manifest from a list of metadata components (--metadata) or from one or more local directories that contain source files (--sourcepath). You can specify either of these parameters, not both. - Use --manifesttype to specify the type of manifest you want to create. The resulting manifest files have specific - names, such as the standard package.xml or destructiveChanges.xml to delete metadata. Valid values for this parameter, + Use --manifesttype to specify the type of manifest you want to create. The resulting manifest files have specific + names, such as the standard package.xml or destructiveChanges.xml to delete metadata. Valid values for this parameter, and their respective file names, are: package : package.xml (default) @@ -515,10 +518,10 @@ DESCRIPTION post : destructiveChangesPost.xml destroy : destructiveChanges.xml - See https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy_deleting_files.htm for - information about these destructive manifest files. + See https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy_deleting_files.htm for + information about these destructive manifest files. - Use --manifestname to specify a custom name for the generated manifest if the pre-defined ones don’t suit your needs. + Use --manifestname to specify a custom name for the generated manifest if the pre-defined ones don’t suit your needs. You can specify either --manifesttype or --manifestname, but not both. EXAMPLES @@ -542,7 +545,7 @@ If no browser-based editor is available for the selected file, this command open To generate a URL for the browser-based editor but not open the editor, use --urlonly. USAGE - $ sfdx force:source:open -f [-r] [-u ] [--apiversion ] [--json] [--loglevel + $ sfdx force:source:open -f [-r] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -563,8 +566,8 @@ OPTIONS this command invocation DESCRIPTION - Opens the specified Lightning Page in Lightning App Builder. Lightning Page files have the suffix .flexipage-meta.xml, - and are stored in the flexipages directory. If you specify a different type of file, this command opens your org’s + Opens the specified Lightning Page in Lightning App Builder. Lightning Page files have the suffix .flexipage-meta.xml, + and are stored in the flexipages directory. If you specify a different type of file, this command opens your org’s home page. The file opens in your default browser. @@ -581,10 +584,10 @@ _See code: [src/commands/force/source/open.ts](https://github.com/salesforcecli/ ## `sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` -retrieve source from an org +retrieve source from an org ``` -retrieve source from an org +retrieve source from an org Use this command to retrieve source (metadata that’s in source format) from an org. To take advantage of change tracking with scratch orgs, use "sfdx force:source:pull". To retrieve metadata that’s in metadata format, use "sfdx force:mdapi:retrieve". @@ -594,7 +597,7 @@ The source you retrieve overwrites the corresponding source files in your local If the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of double quotes. On Windows, if the list contains commas, also enclose it in one set of double quotes. USAGE - $ sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a + $ sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -631,10 +634,10 @@ DESCRIPTION To take advantage of change tracking with scratch orgs, use "sfdx force:source:pull". To retrieve metadata that’s in metadata format, use "sfdx force:mdapi:retrieve". - The source you retrieve overwrites the corresponding source files in your local project. This command does not attempt + The source you retrieve overwrites the corresponding source files in your local project. This command does not attempt to merge the source from your org with your local source files. - If the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of + If the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of double quotes. On Windows, if the list contains commas, also enclose it in one set of double quotes. EXAMPLES @@ -643,7 +646,7 @@ EXAMPLES To retrieve a specific Apex class and the objects whose source is in a directory: $ sfdx force:source:retrieve -p "path/to/apex/classes/MyClass.cls,path/to/source/objects" To retrieve source files in a comma-separated list that contains spaces: - $ sfdx force:source:retrieve -p "path/to/objects/MyCustomObject/fields/MyField.field-meta.xml, + $ sfdx force:source:retrieve -p "path/to/objects/MyCustomObject/fields/MyField.field-meta.xml, path/to/apex/classes To retrieve all Apex classes: $ sfdx force:source:retrieve -m ApexClass @@ -658,7 +661,7 @@ EXAMPLES To retrieve metadata from a package or multiple packages: $ sfdx force:source:retrieve -n MyPackageName $ sfdx force:source:retrieve -n "Package1, PackageName With Spaces, Package3" - To retrieve all metadata from a package and specific components that aren’t in the package, specify both -n | + To retrieve all metadata from a package and specific components that aren’t in the package, specify both -n | --packagenames and one other scoping parameter: $ sfdx force:source:retrieve -n MyPackageName -p path/to/apex/classes $ sfdx force:source:retrieve -n MyPackageName -m ApexClass:MyApexClass @@ -666,4 +669,5 @@ EXAMPLES ``` _See code: [src/commands/force/source/retrieve.ts](https://github.com/salesforcecli/plugin-source/blob/v1.2.5/src/commands/force/source/retrieve.ts)_ + diff --git a/command-snapshot.json b/command-snapshot.json index ebd5fbb17..a875344a6 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,4 +1,29 @@ [ + { + "command": "force:source:beta:pull", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "forceoverwrite", "json", "loglevel", "targetusername", "wait"] + }, + { + "command": "force:source:beta:push", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "forceoverwrite", "ignorewarnings", "json", "loglevel", "quiet", "targetusername", "wait"] + }, + { + "command": "force:source:beta:status", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "json", "local", "loglevel", "remote", "targetusername"] + }, + { + "command": "force:source:beta:tracking:clear", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "json", "loglevel", "noprompt", "targetusername"] + }, + { + "command": "force:source:beta:tracking:reset", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "json", "loglevel", "noprompt", "revision", "targetusername"] + }, { "command": "force:source:convert", "plugin": "@salesforce/plugin-source", diff --git a/messages/pull.json b/messages/pull.json new file mode 100644 index 000000000..b307365ee --- /dev/null +++ b/messages/pull.json @@ -0,0 +1,17 @@ +{ + "description": "pull source from the scratch org to the project", + "descriptionLong": "Pulls changed source from the scratch org to your project to keep them in sync.", + "help": "If the command detects a conflict, it displays the conflicts but does not complete the process. After reviewing the conflict, rerun the command with the --forceoverwrite parameter.", + "flags": { + "forceoverwrite": "ignore conflict warnings and overwrite changes to the project", + "forceoverwriteLong": "Runs the pull command even if conflicts exist. Changes in the scratch org overwrite changes in the project.", + "waitLong": "The number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you. The default is 33 minutes." + }, + "NonScratchOrgPull": "We can\"t retrieve your changes. \"force:source:pull\" is only available for orgs that have source tracking enabled. Use \"force:source:retrieve\" or \"force:mdapi:retrieve\" instead.", + "sourceConflictDetected": "Source conflict(s) detected.", + "pull": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time.", + "retrievedSourceHeader": "Retrieved Source", + "NoResultsFound": "No results found", + "retrievedSourceWarningsHeader": "Retrieved Source Warnings", + "retrieveTimeout": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time." +} diff --git a/messages/push.json b/messages/push.json new file mode 100644 index 000000000..7f6b72560 --- /dev/null +++ b/messages/push.json @@ -0,0 +1,17 @@ +{ + "description": "push source to a scratch org from the project", + "descriptionLong": "Pushes changed source from your project to a scratch org to keep them in sync.", + "help": "If the command detects a conflict, it displays the conflicts but does not complete the process. After reviewing the conflict, rerun the command with the --forceoverwrite parameter.", + "flags": { + "waitLong": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you. The default is 33 minutes.", + "forceoverwrite": "ignore conflict warnings and overwrite changes to scratch org", + "forceoverwriteLong": "Runs the push command even if conflicts exist. Changes in the project overwrite changes in the scratch org.", + "replacetokens": "replace tokens in source files prior to deployment", + "replacetokensLong": "Replaces tokens in source files prior to deployment.", + "ignorewarnings": "deploy changes even if warnings are generated", + "ignorewarningsLong": "Completes the deployment even if warnings are generated.", + "quiet": "minimize json and sdtout output on success" + }, + "sourcepushFailed": "Push failed.", + "conflictMsg": "We couldn't complete the push operation due to conflicts. Verify that you want to keep the local versions, then run \"sfdx force:source:push -f\" with the --forceoverwrite (-f) option." +} diff --git a/messages/status.json b/messages/status.json new file mode 100644 index 000000000..946a6a1fc --- /dev/null +++ b/messages/status.json @@ -0,0 +1,19 @@ +{ + "description": "list local changes and/or changes in a scratch org", + "LongDescription": "Lists changes that have been made locally, in a scratch org, or both.", + "examples": [ + "sfdx force:source:status -l", + "sfdx force:source:status -r", + "sfdx force:source:status -a", + "sfdx force:source:status -a -u me@example.com --json" + ], + "flags": { + "all": "list all the changes that have been made", + "allLong": "Lists all the changes that have been made.", + "local": "list the changes that have been made locally", + "localLong": "Lists the changes that have been made locally.", + "remote": "list the changes that have been made in the scratch org", + "remoteLong": "Lists the changes that have been made in the scratch org." + }, + "humanSuccess": "Source Status" +} diff --git a/messages/tracking.json b/messages/tracking.json new file mode 100644 index 000000000..0ca7dd5b9 --- /dev/null +++ b/messages/tracking.json @@ -0,0 +1,7 @@ +{ + "resetDescription": "reset local and remote source tracking\n\n WARNING: This command deletes or overwrites all existing source tracking files. Use with extreme caution. \n\nResets local and remote source tracking so that the CLI no longer registers differences between your local files and those in the org. When you next run force:source:status, the CLI returns no results, even though conflicts might actually exist. The CLI then resumes tracking new source changes as usual.\n\nUse the --revision parameter to reset source tracking to a specific revision number of an org source member. To get the revision number, query the SourceMember Tooling API object with the force:data:soql:query command. For example:\n $ sfdx force:data:soql:query -q \"SELECT MemberName, MemberType, RevisionCounter FROM SourceMember\" -t", + "clearDescription": "clear all local source tracking information\n\nWARNING: This command deletes or overwrites all existing source tracking files. Use with extreme caution.\n\nClears all local source tracking information. When you next run force:source:status, the CLI displays all local and remote files as changed, and any files with the same name are listed as conflicts.", + "nopromptDescription": "do not prompt for source tracking override confirmation", + "revisionDescription": "reset to a specific SourceMember revision counter number", + "promptMessage": "WARNING: This operation will modify all your local source tracking files. The operation can have unintended consequences on all the force:source commands. Are you sure you want to proceed (y/n)?" +} diff --git a/package.json b/package.json index 9b5f2d694..6f2221350 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "@oclif/config": "^1", "@salesforce/command": "^4.1.3", "@salesforce/core": "^2.28.0", - "@salesforce/source-deploy-retrieve": "^5.0.0", + "@salesforce/source-deploy-retrieve": "^5.1.0", + "@salesforce/source-tracking": "^0.4.1", "chalk": "^4.1.2", "cli-ux": "^5.6.3", "open": "^8.2.1", @@ -142,6 +143,7 @@ "test:nuts:delete": "mocha \"test/nuts/delete.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", "test:nuts:deploy": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy PLUGIN_SOURCE_SEED_EXCLUDE=async ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", "test:nuts:deploy:async": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.async ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", + "test:nuts:deploy:destructive": "mocha \"test/nuts/deployDestructive.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0", "test:nuts:deploy:manifest": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.manifest ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", "test:nuts:deploy:metadata": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.metadata ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", "test:nuts:deploy:quick": "cross-env PLUGIN_SOURCE_SEED_FILTER=deploy.quick ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", @@ -152,7 +154,12 @@ "test:nuts:retrieve": "cross-env PLUGIN_SOURCE_SEED_FILTER=retrieve ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0", "test:nuts:specialTypes": "mocha \"test/nuts/territory2.nut.ts\" \"test/nuts/folderTypes.nut.ts\" --slow 4500 --timeout 600000 --retries 0 --parallel", "test:nuts:territory2": "mocha \"test/nuts/territory2.nut.ts\" --slow 4500 --timeout 600000 --retries 0", - "test:nuts:deploy:destructive": "mocha \"test/nuts/deployDestructive.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0", + "test:nuts:tracking": "mocha \"test/nuts/trackingCommands/*.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0", + "test:nuts:tracking:basics": "mocha \"test/nuts/trackingCommands/basics.nut.ts\" --slow 3000 --timeout 600000 --retries 0", + "test:nuts:tracking:conflicts": "mocha \"test/nuts/trackingCommands/conflicts.nut.ts\" --slow 3000 --timeout 600000 --retries 0", + "test:nuts:tracking:forceignore": "mocha \"test/nuts/trackingCommands/forceIgnore.nut.ts\" --slow 3000 --timeout 600000 --retries 0", + "test:nuts:tracking:remote": "mocha \"test/nuts/trackingCommands/remoteChanges.nut.ts\" --slow 3000 --timeout 600000 --retries 0", + "test:nuts:tracking:resetClear": "mocha \"test/nuts/trackingCommands/resetClear.nut.ts\" --slow 3000 --timeout 600000 --retries 0", "version": "oclif-dev readme" }, "husky": { diff --git a/src/commands/force/source/beta/pull.ts b/src/commands/force/source/beta/pull.ts new file mode 100644 index 000000000..29cdb0377 --- /dev/null +++ b/src/commands/force/source/beta/pull.ts @@ -0,0 +1,185 @@ +/* + * 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 { FlagsConfig, flags } from '@salesforce/command'; +import { Duration } from '@salesforce/kit'; +import { Messages } from '@salesforce/core'; +import { + FileResponse, + SourceComponent, + ComponentSet, + RetrieveResult, + RequestStatus, + ComponentStatus, +} from '@salesforce/source-deploy-retrieve'; +import { SourceTracking, throwIfInvalid, replaceRenamedCommands, ChangeResult } from '@salesforce/source-tracking'; +import { processConflicts } from '../../../../formatters/conflicts'; +import { SourceCommand } from '../../../../sourceCommand'; +import { PullResponse, PullResultFormatter } from '../../../../formatters/pullFormatter'; +Messages.importMessagesDirectory(__dirname); +const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'pull'); + +export default class Pull extends SourceCommand { + public static description = messages.getMessage('description'); + public static help = messages.getMessage('help'); + protected static readonly flagsConfig: FlagsConfig = { + forceoverwrite: flags.boolean({ + char: 'f', + description: messages.getMessage('flags.forceoverwrite'), + }), + // TODO: use shared flags from plugin-source + wait: flags.minutes({ + char: 'w', + default: Duration.minutes(33), + min: Duration.minutes(0), // wait=0 means deploy is asynchronous + description: messages.getMessage('flags.waitLong'), + }), + }; + + protected static requiresUsername = true; + protected static requiresProject = true; + protected readonly lifecycleEventNames = ['preretrieve', 'postretrieve']; + protected tracking: SourceTracking; + protected retrieveResult: RetrieveResult; + protected deleteFileResponses: FileResponse[]; + + public async run(): Promise { + await this.preChecks(); + await this.retrieve(); + // do not parallelize delete and retrieve...we only get to delete IF retrieve was successful + await this.doDeletes(); // deletes includes its tracking file operations + await this.updateTrackingFilesWithRetrieve(); + this.ux.stopSpinner(); + + return this.formatResult(); + } + + protected async preChecks(): Promise { + // checks the source tracking file version and throws if they're toolbelt's old version + throwIfInvalid({ + org: this.org, + projectPath: this.project.getPath(), + toValidate: 'plugin-source', + command: replaceRenamedCommands('force:source:pull'), + }); + + this.ux.startSpinner('Loading source tracking information'); + this.tracking = await SourceTracking.create({ + org: this.org, + project: this.project, + }); + + await this.tracking.ensureRemoteTracking(true); + + if (!this.flags.forceoverwrite) { + this.ux.setSpinnerStatus('Checking for conflicts'); + processConflicts(await this.tracking.getConflicts(), this.ux, messages.getMessage('sourceConflictDetected')); + } + } + + protected async doDeletes(): Promise { + this.ux.setSpinnerStatus('Checking for deletes from the org and updating source tracking files'); + const changesToDelete = await this.tracking.getChanges({ + origin: 'remote', + state: 'delete', + format: 'SourceComponent', + }); + this.deleteFileResponses = await this.tracking.deleteFilesAndUpdateTracking(changesToDelete); + } + + protected async updateTrackingFilesWithRetrieve(): Promise { + this.ux.setSpinnerStatus('Updating source tracking files'); + + // might not exist if we exited from retrieve early + if (!this.retrieveResult) { + return; + } + const successes = this.retrieveResult + .getFileResponses() + .filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed); + + await Promise.all([ + // commit the local file successes that the retrieve modified + this.tracking.updateLocalTracking({ + files: successes.map((fileResponse) => fileResponse.filePath).filter(Boolean), + }), + this.tracking.updateRemoteTracking( + successes.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath })), + true // skip polling because it's a pull + ), + ]); + } + + protected async retrieve(): Promise { + const componentSet = new ComponentSet(); + ( + await this.tracking.getChanges({ + origin: 'remote', + state: 'nondelete', + format: 'ChangeResult', + }) + ).map((component) => { + if (component.type && component.name) { + componentSet.add({ + type: component.type, + fullName: component.name, + }); + } + }); + + if (componentSet.size === 0) { + return; + } + componentSet.sourceApiVersion = await this.getSourceApiVersion(); + if (this.getFlag('apiversion')) { + componentSet.apiVersion = this.getFlag('apiversion'); + } + + const mdapiRetrieve = await componentSet.retrieve({ + usernameOrConnection: this.org.getUsername(), + merge: true, + output: this.project.getDefaultPackage().path, + }); + + this.ux.setSpinnerStatus('Retrieving metadata from the org'); + + // assume: remote deletes that get deleted locally don't fire hooks? + await this.lifecycle.emit('preretrieve', componentSet.toArray()); + this.retrieveResult = await mdapiRetrieve.pollStatus(1000, this.getFlag('wait').seconds); + + // Assume: remote deletes that get deleted locally don't fire hooks. + await this.lifecycle.emit('postretrieve', this.retrieveResult.getFileResponses()); + } + + protected resolveSuccess(): void { + // there might not be a retrieveResult if we don't have anything to retrieve + if (this.retrieveResult && this.retrieveResult.response.status !== RequestStatus.Succeeded) { + this.setExitCode(1); + } + } + + protected formatResult(): PullResponse[] { + const formatterOptions = { + verbose: this.getFlag('verbose', false), + }; + + const formatter = new PullResultFormatter( + this.logger, + this.ux, + formatterOptions, + this.retrieveResult, + this.deleteFileResponses + ); + + // Only display results to console when JSON flag is unset. + if (!this.isJsonOutput()) { + formatter.display(); + } + + return formatter.getJson(); + } +} diff --git a/src/commands/force/source/beta/push.ts b/src/commands/force/source/beta/push.ts new file mode 100644 index 000000000..e21949986 --- /dev/null +++ b/src/commands/force/source/beta/push.ts @@ -0,0 +1,156 @@ +/* + * 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 { FlagsConfig, flags } from '@salesforce/command'; +import { Duration, env } from '@salesforce/kit'; +import { Messages } from '@salesforce/core'; +import { RequestStatus, ComponentStatus } from '@salesforce/source-deploy-retrieve'; + +import { SourceTracking, throwIfInvalid, replaceRenamedCommands } from '@salesforce/source-tracking'; +import { DeployCommand } from '../../../../deployCommand'; +import { PushResponse, PushResultFormatter } from '../../../../formatters/pushResultFormatter'; +import { ProgressFormatter } from '../../../../formatters/progressFormatter'; +import { DeployProgressBarFormatter } from '../../../../formatters/deployProgressBarFormatter'; +import { DeployProgressStatusFormatter } from '../../../../formatters/deployProgressStatusFormatter'; +import { processConflicts } from '../../../../formatters/conflicts'; + +Messages.importMessagesDirectory(__dirname); +const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'push'); + +export default class Push extends DeployCommand { + public static description = messages.getMessage('description'); + public static help = messages.getMessage('help'); + protected static readonly flagsConfig: FlagsConfig = { + forceoverwrite: flags.boolean({ + char: 'f', + description: messages.getMessage('flags.forceoverwrite'), + longDescription: messages.getMessage('flags.forceoverwriteLong'), + }), + // TODO: use shared flags from plugin-source? + wait: flags.minutes({ + char: 'w', + default: Duration.minutes(33), + min: Duration.minutes(1), + description: messages.getMessage('flags.waitLong'), + longDescription: messages.getMessage('flags.waitLong'), + }), + ignorewarnings: flags.boolean({ + char: 'g', + description: messages.getMessage('flags.ignorewarnings'), + longDescription: messages.getMessage('flags.ignorewarningsLong'), + }), + quiet: flags.builtin({ + description: messages.getMessage('flags.quiet'), + }), + }; + protected static requiresUsername = true; + protected static requiresProject = true; + protected readonly lifecycleEventNames = ['predeploy', 'postdeploy']; + + private isRest = false; + + public async run(): Promise { + await this.deploy(); + this.resolveSuccess(); + return this.formatResult(); + } + + protected async deploy(): Promise { + throwIfInvalid({ + org: this.org, + projectPath: this.project.getPath(), + toValidate: 'plugin-source', + command: replaceRenamedCommands('force:source:push'), + }); + const waitDuration = this.getFlag('wait'); + this.isRest = await this.isRestDeploy(); + + const tracking = await SourceTracking.create({ + org: this.org, + project: this.project, + apiVersion: this.flags.apiversion as string, + }); + if (!this.flags.forceoverwrite) { + processConflicts(await tracking.getConflicts(), this.ux, messages.getMessage('conflictMsg')); + } + const componentSet = await tracking.localChangesAsComponentSet(); + componentSet.sourceApiVersion = await this.getSourceApiVersion(); + + // there might have been components in local tracking, but they might be ignored by SDR or unresolvable. + // SDR will throw when you try to resolve them, so don't + if (componentSet.size === 0) { + this.logger.warn('There are no changes to deploy'); + return; + } + + // fire predeploy event for sync and async deploys + await this.lifecycle.emit('predeploy', componentSet.toArray()); + this.ux.log(`*** Pushing with ${this.isRest ? 'REST' : 'SOAP'} API v${componentSet.sourceApiVersion} ***`); + + const deploy = await componentSet.deploy({ + usernameOrConnection: this.org.getUsername(), + apiOptions: { + ignoreWarnings: this.getFlag('ignorewarnings', false), + rest: this.isRest, + testLevel: 'NoTestRun', + }, + }); + + // we're not print JSON output + if (!this.isJsonOutput()) { + const progressFormatter: ProgressFormatter = env.getBoolean('SFDX_USE_PROGRESS_BAR', true) + ? new DeployProgressBarFormatter(this.logger, this.ux) + : new DeployProgressStatusFormatter(this.logger, this.ux); + progressFormatter.progress(deploy); + } + this.deployResult = await deploy.pollStatus(500, waitDuration.seconds); + + const successes = this.deployResult + .getFileResponses() + .filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed); + const successNonDeletes = successes.filter((fileResponse) => fileResponse.state !== ComponentStatus.Deleted); + const successDeletes = successes.filter((fileResponse) => fileResponse.state === ComponentStatus.Deleted); + + if (this.deployResult) { + // Only fire the postdeploy event when we have results. I.e., not async. + await this.lifecycle.emit('postdeploy', this.deployResult); + } + + await Promise.all([ + tracking.updateLocalTracking({ + files: successNonDeletes.map((fileResponse) => fileResponse.filePath), + deletedFiles: successDeletes.map((fileResponse) => fileResponse.filePath), + }), + tracking.updateRemoteTracking(successes), + ]); + } + + protected resolveSuccess(): void { + // there might not be a deployResult if we exited early with an empty componentSet + if (this.deployResult && this.deployResult.response.status !== RequestStatus.Succeeded) { + this.setExitCode(1); + } + } + + protected formatResult(): PushResponse[] { + if (!this.deployResult) { + this.ux.log('No results found'); + } + const formatterOptions = { + quiet: this.getFlag('quiet', false), + }; + + const formatter = new PushResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); + + // Only display results to console when JSON flag is unset. + if (!this.isJsonOutput()) { + formatter.display(); + } + + return formatter.getJson(); + } +} diff --git a/src/commands/force/source/beta/status.ts b/src/commands/force/source/beta/status.ts new file mode 100644 index 000000000..082733de8 --- /dev/null +++ b/src/commands/force/source/beta/status.ts @@ -0,0 +1,116 @@ +/* + * 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 { FlagsConfig, flags, SfdxCommand } from '@salesforce/command'; +import { Messages } from '@salesforce/core'; +import { + SourceTracking, + throwIfInvalid, + replaceRenamedCommands, + ChangeResult, + StatusOutputRow, +} from '@salesforce/source-tracking'; +import { StatusResult, StatusFormatter } from '../../../../formatters/statusFormatter'; + +Messages.importMessagesDirectory(__dirname); +const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'status'); + +export default class Status extends SfdxCommand { + public static description = messages.getMessage('description'); + public static readonly examples = replaceRenamedCommands(messages.getMessage('examples')).split(os.EOL); + protected static flagsConfig: FlagsConfig = { + local: flags.boolean({ + char: 'l', + description: messages.getMessage('flags.local'), + longDescription: messages.getMessage('flags.localLong'), + exclusive: ['remote'], + }), + remote: flags.boolean({ + char: 'r', + description: messages.getMessage('flags.remote'), + longDescription: messages.getMessage('flags.remoteLong'), + exclusive: ['local'], + }), + }; + protected static requiresUsername = true; + protected static requiresProject = true; + protected results = new Array(); + protected localAdds: ChangeResult[] = []; + + public async run(): Promise { + throwIfInvalid({ + org: this.org, + projectPath: this.project.getPath(), + toValidate: 'plugin-source', + command: replaceRenamedCommands('force:source:status'), + }); + + const wantsLocal = (this.flags.local as boolean) || (!this.flags.remote && !this.flags.local); + const wantsRemote = (this.flags.remote as boolean) || (!this.flags.remote && !this.flags.local); + + this.logger.debug( + `project is ${this.project.getPath()} and pkgDirs are ${this.project + .getPackageDirectories() + .map((dir) => dir.path) + .join(',')}` + ); + const tracking = await SourceTracking.create({ + org: this.org, + project: this.project, + apiVersion: this.flags.apiversion as string, + }); + const stlStatusResult = await tracking.getStatus({ local: wantsLocal, remote: wantsRemote }); + this.results = stlStatusResult.map((result) => resultConverter(result)); + + return this.formatResult(); + } + + protected formatResult(): StatusResult[] { + const formatter = new StatusFormatter(this.logger, this.ux, {}, this.results); + + if (!this.flags.json) { + formatter.display(); + } + + return formatter.getJson(); + } +} + +/** + * STL provides a more useful json output. + * This function makes it consistent with the Status command's json. + */ +const resultConverter = (input: StatusOutputRow): StatusResult => { + const { fullName, type, ignored, filePath, conflict } = input; + const origin = originMap.get(input.origin); + const actualState = stateMap.get(input.state); + return { + fullName, + type, + // this string became the place to store information. + // The JSON now breaks out that info but preserves this property for backward compatibility + state: `${origin} ${actualState}${conflict ? ' (Conflict)' : ''}`, + ignored, + filePath, + origin, + actualState, + conflict, + }; +}; + +const originMap = new Map([ + ['local', 'Local'], + ['remote', 'Remote'], +]); + +const stateMap = new Map([ + ['delete', 'Deleted'], + ['add', 'Add'], + ['modify', 'Changed'], + ['nondelete', 'Changed'], +]); diff --git a/src/commands/force/source/beta/tracking/clear.ts b/src/commands/force/source/beta/tracking/clear.ts new file mode 100644 index 000000000..d88e68089 --- /dev/null +++ b/src/commands/force/source/beta/tracking/clear.ts @@ -0,0 +1,56 @@ +/* + * 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 { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; +import { Messages } from '@salesforce/core'; +import * as chalk from 'chalk'; +import { SourceTracking, throwIfInvalid, replaceRenamedCommands } from '@salesforce/source-tracking'; + +Messages.importMessagesDirectory(__dirname); +const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'tracking'); + +export type SourceTrackingClearResult = { + clearedFiles: string[]; +}; + +export class Clear extends SfdxCommand { + public static readonly description = replaceRenamedCommands(messages.getMessage('clearDescription')); + + public static readonly requiresProject = true; + public static readonly requiresUsername = true; + + public static readonly flagsConfig: FlagsConfig = { + noprompt: flags.boolean({ + char: 'p', + description: messages.getMessage('nopromptDescription'), + required: false, + }), + }; + + public async run(): Promise { + throwIfInvalid({ + org: this.org, + projectPath: this.project.getPath(), + toValidate: 'plugin-source', + command: replaceRenamedCommands('force:source:tracking:clear'), + }); + let clearedFiles: string[] = []; + if ( + this.flags.noprompt || + (await this.ux.confirm(chalk.dim(replaceRenamedCommands(messages.getMessage('promptMessage'))))) + ) { + const sourceTracking = await SourceTracking.create({ + project: this.project, + org: this.org, + apiVersion: this.flags.apiversion as string, + }); + clearedFiles = await Promise.all([sourceTracking.clearLocalTracking(), sourceTracking.clearRemoteTracking()]); + this.ux.log('Cleared local tracking files.'); + } + return { clearedFiles }; + } +} diff --git a/src/commands/force/source/beta/tracking/reset.ts b/src/commands/force/source/beta/tracking/reset.ts new file mode 100644 index 000000000..c0d4546f2 --- /dev/null +++ b/src/commands/force/source/beta/tracking/reset.ts @@ -0,0 +1,77 @@ +/* + * 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 { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; +import { Messages } from '@salesforce/core'; +import * as chalk from 'chalk'; +import { SourceTracking, throwIfInvalid, replaceRenamedCommands } from '@salesforce/source-tracking'; + +Messages.importMessagesDirectory(__dirname); +const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'tracking'); + +export type SourceTrackingResetResult = { + sourceMembersSynced: number; + localPathsSynced: number; +}; + +export class Reset extends SfdxCommand { + public static readonly description = replaceRenamedCommands(messages.getMessage('resetDescription')); + + public static readonly requiresProject = true; + public static readonly requiresUsername = true; + + public static readonly flagsConfig: FlagsConfig = { + revision: flags.integer({ + char: 'r', + description: messages.getMessage('revisionDescription'), + min: 0, + }), + noprompt: flags.boolean({ + char: 'p', + description: messages.getMessage('nopromptDescription'), + }), + }; + + public async run(): Promise { + throwIfInvalid({ + org: this.org, + projectPath: this.project.getPath(), + toValidate: 'plugin-source', + command: replaceRenamedCommands('force:source:tracking:clear'), + }); + + if ( + this.flags.noprompt || + (await this.ux.confirm(chalk.dim(replaceRenamedCommands(messages.getMessage('promptMessage'))))) + ) { + const sourceTracking = await SourceTracking.create({ + project: this.project, + org: this.org, + apiVersion: this.flags.apiversion as string, + }); + + const [remoteResets, localResets] = await Promise.all([ + sourceTracking.resetRemoteTracking(this.flags.revision as number), + sourceTracking.resetLocalTracking(), + ]); + + this.ux.log( + `Reset local tracking files${this.flags.revision ? ` to revision ${this.flags.revision as number}` : ''}.` + ); + + return { + sourceMembersSynced: remoteResets, + localPathsSynced: localResets.length, + }; + } + + return { + sourceMembersSynced: 0, + localPathsSynced: 0, + }; + } +} diff --git a/src/commands/force/source/open.ts b/src/commands/force/source/open.ts index 4055a572f..2381cf015 100644 --- a/src/commands/force/source/open.ts +++ b/src/commands/force/source/open.ts @@ -10,26 +10,12 @@ import * as path from 'path'; import * as fs from 'fs'; import * as open from 'open'; import { getString } from '@salesforce/ts-types'; -import { AuthInfo, SfdcUrl } from '@salesforce/core'; import { flags, FlagsConfig } from '@salesforce/command'; -import { Messages, sfdc, SfdxError } from '@salesforce/core'; +import { Messages, sfdc, SfdxError, AuthInfo, SfdcUrl } from '@salesforce/core'; import { SourceComponent, MetadataResolver } from '@salesforce/source-deploy-retrieve'; import { OpenResultFormatter, OpenCommandResult } from '../../../formatters/openResultFormatter'; import { SourceCommand } from '../../../sourceCommand'; -export interface DnsLookupObject { - address: string; - family: number; -} - -export interface FlexiPageRecord { - attributes: { - type: string; - url: string; - }; - Id: string; -} - Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-source', 'open'); @@ -75,7 +61,7 @@ export class Open extends SourceCommand { private async doOpen(): Promise { const typeName = this.getTypeNameDefinitionByFileName(path.resolve(this.flags.sourcefile)); - const openPath = typeName === 'FlexiPage' ? await this.handleSupportedTypes() : await this.handleUnsupportedTypes(); + const openPath = typeName === 'FlexiPage' ? await this.setUpOpenPath() : await this.buildFrontdoorUrl(); this.openResult = await this.open(openPath); } @@ -86,70 +72,48 @@ export class Open extends SourceCommand { const components: SourceComponent[] = metadataResolver.getComponentsFromPath(fsPath); return components[0].type.name; } - return undefined; - } - - private async handleSupportedTypes(): Promise { - return await this.setUpOpenPath(); - } - - private async handleUnsupportedTypes(): Promise { - return await this.buildFrontdoorUrl(); - } - - private async getUrl(retURL: string): Promise { - const frontDoorUrl: string = await this.buildFrontdoorUrl(); - return `${frontDoorUrl}&retURL=${encodeURIComponent(decodeURIComponent(retURL))}`; } private async buildFrontdoorUrl(): Promise { - const connection = this.org.getConnection(); - const { username } = connection.getAuthInfoFields(); - const authInfo = await AuthInfo.create({ username }); - const url = authInfo.getOrgFrontDoorUrl(); - return url; + const authInfo = await AuthInfo.create({ username: this.org.getUsername() }); + return authInfo.getOrgFrontDoorUrl(); } - private async open(src: string, urlonly?: boolean): Promise { - const connection = this.org.getConnection(); - const { username, orgId } = connection.getAuthInfoFields(); - const url = await this.getUrl(src); - const act = (): OpenCommandResult => - this.flags.urlonly || urlonly ? { url, username, orgId } : this.openBrowser(url, { url, username, orgId }); - if (sfdc.isInternalUrl(url)) { - return act(); - } - - try { - const result = await new SfdcUrl(url).checkLightningDomain(); - - if (result) { - return act(); + private async open(src: string): Promise { + const url = `${await this.buildFrontdoorUrl()}&retURL=${encodeURIComponent(decodeURIComponent(src))}`; + const result: OpenCommandResult = { + url, + username: this.org.getUsername(), + orgId: this.org.getOrgId(), + }; + + if (!sfdc.isInternalUrl(url)) { + try { + await new SfdcUrl(url).checkLightningDomain(); + } catch (error) { + throw SfdxError.create('@salesforce/plugin-source', 'open', 'SourceOpenCommandTimeoutError'); } - } catch (error) { - throw SfdxError.create('@salesforce/plugin-source', 'open', 'SourceOpenCommandTimeoutError'); } - } - private async deriveFlexipageURL(flexipage: string): Promise { - const connection = this.org.getConnection(); - const queryResult = await connection.tooling.query(`SELECT id FROM flexipage WHERE DeveloperName='${flexipage}'`); - if (queryResult.totalSize === 1 && queryResult.records) { - const record = queryResult.records[0] as FlexiPageRecord; - return record.Id; - } - return; + return this.flags.urlonly ? result : this.openBrowser(url, result); } private async setUpOpenPath(): Promise { - const id = await this.deriveFlexipageURL(path.basename(this.flags.sourcefile, '.flexipage-meta.xml')); - - if (id) { - return `/visualEditor/appBuilder.app?pageId=${id}`; + try { + const flexipage = await this.org + .getConnection() + .singleRecordQuery<{ Id: string }>( + `SELECT id FROM flexipage WHERE DeveloperName='${path.basename( + this.flags.sourcefile, + '.flexipage-meta.xml' + )}'`, + { tooling: true } + ); + return `/visualEditor/appBuilder.app?pageId=${flexipage.Id}`; + } catch (error) { + return '_ui/flexipage/ui/FlexiPageFilterListPage'; } - return '_ui/flexipage/ui/FlexiPageFilterListPage'; } - private openBrowser(url: string, options: OpenCommandResult): OpenCommandResult { void open(url); return options; diff --git a/src/formatters/conflicts.ts b/src/formatters/conflicts.ts new file mode 100644 index 000000000..4a2182344 --- /dev/null +++ b/src/formatters/conflicts.ts @@ -0,0 +1,40 @@ +/* + * 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 { UX } from '@salesforce/command'; +import { SfdxError } from '@salesforce/core'; +import { ChangeResult } from '@salesforce/source-tracking'; + +const writeConflictTable = (conflicts: ChangeResult[], ux: UX): void => { + ux.table( + conflicts.map((conflict) => ({ ...conflict, state: 'Conflict' })), + { + columns: [ + { label: 'STATE', key: 'state' }, + { label: 'FULL NAME', key: 'name' }, + { label: 'TYPE', key: 'type' }, + { label: 'PROJECT PATH', key: 'filenames' }, + ], + } + ); +}; + +/** + * Write a table (if not json) and throw an error that includes a custom message and the conflict data + * + * @param conflicts + * @param ux + * @param message + */ +export const processConflicts = (conflicts: ChangeResult[], ux: UX, message: string): void => { + if (conflicts.length === 0) { + return; + } + writeConflictTable(conflicts, ux); + const err = new SfdxError(message); + err.setData(conflicts); + throw err; +}; diff --git a/src/formatters/pullFormatter.ts b/src/formatters/pullFormatter.ts new file mode 100644 index 000000000..7d7b6cad7 --- /dev/null +++ b/src/formatters/pullFormatter.ts @@ -0,0 +1,132 @@ +/* + * 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 { blue, yellow } from 'chalk'; +import { UX } from '@salesforce/command'; +import { Logger, Messages, SfdxError } from '@salesforce/core'; +import { get, getString, getNumber } from '@salesforce/ts-types'; +import { + RetrieveResult, + ComponentStatus, + FileResponse, + RequestStatus, + RetrieveMessage, +} from '@salesforce/source-deploy-retrieve'; +import { ResultFormatter, ResultFormatterOptions, toArray } from './resultFormatter'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-source', 'pull'); + +export type PullResponse = Pick; + +export class PullResultFormatter extends ResultFormatter { + protected result: RetrieveResult; + protected fileResponses: FileResponse[]; + protected warnings: RetrieveMessage[]; + + public constructor( + logger: Logger, + ux: UX, + options: ResultFormatterOptions, + retrieveResult: RetrieveResult, + deleteResult: FileResponse[] = [] + ) { + super(logger, ux, options); + this.result = retrieveResult; + this.fileResponses = (retrieveResult?.getFileResponses ? retrieveResult.getFileResponses() : []).concat( + deleteResult + ); + const warnMessages = retrieveResult?.response?.messages ?? ([] as RetrieveMessage | RetrieveMessage[]); + this.warnings = toArray(warnMessages); + if (this.result?.response?.zipFile) { + // zipFile can become massive and unwieldy with JSON parsing/terminal output and, isn't useful + delete this.result.response.zipFile; + } + } + + /** + * Get the JSON output from the PullCommandResult. + * + * @returns RetrieveCommandResult + */ + public getJson(): PullResponse[] { + return this.fileResponses.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath })); + } + + /** + * Displays retrieve results in human format. + */ + public display(): void { + if (this.hasStatus(RequestStatus.InProgress)) { + const commandWaitTime = getNumber(this.options, 'waitTime', 33); + this.ux.log(messages.getMessage('retrieveTimeout', [commandWaitTime])); + return; + } + + if (this.isSuccess()) { + this.ux.styledHeader(blue(messages.getMessage('retrievedSourceHeader'))); + const retrievedFiles = this.fileResponses.filter((fr) => fr.state !== ComponentStatus.Failed); + if (retrievedFiles?.length) { + this.displaySuccesses(retrievedFiles); + } else { + this.ux.log(messages.getMessage('NoResultsFound')); + } + if (this.warnings.length) { + this.displayWarnings(); + } + } else { + this.displayErrors(); + } + } + + protected hasStatus(status: RequestStatus): boolean { + return getString(this.result, 'response.status') === status; + } + + protected hasComponents(): boolean { + return getNumber(this.result, 'components.size', 0) === 0; + } + + private displayWarnings(): void { + this.ux.styledHeader(yellow(messages.getMessage('retrievedSourceWarningsHeader'))); + const columns = [ + { key: 'fileName', label: 'FILE NAME' }, + { key: 'problem', label: 'PROBLEM' }, + ]; + this.ux.table(this.warnings, { columns }); + this.ux.log(); + } + + private displaySuccesses(retrievedFiles: FileResponse[]): void { + this.sortFileResponses(retrievedFiles); + this.asRelativePaths(retrievedFiles); + this.ux.table(retrievedFiles, { + columns: [ + { label: 'STATE', key: 'state' }, + { label: 'FULL NAME', key: 'fullName' }, + { label: 'TYPE', key: 'type' }, + { label: 'PROJECT PATH', key: 'filePath' }, + ], + }); + } + + private displayErrors(): void { + // an invalid packagename retrieval will end up with a message in the `errorMessage` entry + if (!this.result) { + return; + } + const errorMessage = getString(this.result.response, 'errorMessage'); + if (errorMessage) { + throw new SfdxError(errorMessage); + } + const unknownMsg: RetrieveMessage[] = [{ fileName: 'unknown', problem: 'unknown' }]; + const responseMsgs = get(this.result, 'response.messages', unknownMsg) as RetrieveMessage | RetrieveMessage[]; + const errMsgs = toArray(responseMsgs); + const errMsgsForDisplay = errMsgs.reduce((p, c) => `${p}\n${c.fileName}: ${c.problem}`, ''); + this.ux.log(`Retrieve Failed due to: ${errMsgsForDisplay}`); + } +} diff --git a/src/formatters/pushResultFormatter.ts b/src/formatters/pushResultFormatter.ts new file mode 100644 index 000000000..692921e8b --- /dev/null +++ b/src/formatters/pushResultFormatter.ts @@ -0,0 +1,138 @@ +/* + * 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 chalk from 'chalk'; +import { UX } from '@salesforce/command'; +import { Logger, Messages, SfdxError } from '@salesforce/core'; +import { getString } from '@salesforce/ts-types'; +import { + DeployResult, + FileResponse, + RequestStatus, + DeployMessage, + ComponentStatus, +} from '@salesforce/source-deploy-retrieve'; +import { ResultFormatter, ResultFormatterOptions, toArray } from './resultFormatter'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-source', 'push'); + +export type PushResponse = Pick; + +export class PushResultFormatter extends ResultFormatter { + protected result: DeployResult; + protected fileResponses: FileResponse[]; + + public constructor(logger: Logger, ux: UX, options: ResultFormatterOptions, result: DeployResult) { + super(logger, ux, options); + this.result = result; + this.fileResponses = result?.getFileResponses ? result.getFileResponses() : []; + } + + /** + * Get the JSON output from the DeployResult. + * + * @returns a JSON formatted result matching the provided type. + */ + public getJson(): PushResponse[] { + // quiet returns only failures + const toReturn = this.isQuiet() + ? this.fileResponses.filter((fileResponse) => fileResponse.state === ComponentStatus.Failed) + : this.fileResponses; + + return toReturn.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath })); + } + + /** + * Displays deploy results in human readable format. Output can vary based on: + * + * 1. Verbose option + * 3. Checkonly deploy (checkonly=true) + * 4. Deploy with test results + * 5. Canceled status + */ + public display(): void { + this.displaySuccesses(); + this.displayFailures(); + + // Throw a DeployFailed error unless the deployment was successful. + if (!this.isSuccess()) { + throw new SfdxError(messages.getMessage('sourcepushFailed'), 'PushFailed'); + } + } + + protected hasStatus(status: RequestStatus): boolean { + return getString(this.result, 'response.status') === status; + } + + protected displaySuccesses(): void { + if (this.isQuiet()) { + return; + } + if (this.isSuccess() && this.fileResponses?.length) { + const successes = this.fileResponses.filter((f) => f.state !== 'Failed'); + if (!successes.length) { + return; + } + this.sortFileResponses(successes); + this.asRelativePaths(successes); + + this.ux.log(''); + this.ux.styledHeader(chalk.blue('Pushed Source')); + this.ux.table(successes, { + columns: [ + { key: 'state', label: 'STATE' }, + { key: 'fullName', label: 'FULL NAME' }, + { key: 'type', label: 'TYPE' }, + { key: 'filePath', label: 'PROJECT PATH' }, + ], + }); + } + } + + protected displayFailures(): void { + if (this.hasStatus(RequestStatus.Failed)) { + const failures: Array = []; + const fileResponseFailures: Map = new Map(); + + if (this.fileResponses?.length) { + const fileResponses: FileResponse[] = []; + this.fileResponses + .filter((f) => f.state === 'Failed') + .map((f: FileResponse & { error: string }) => { + fileResponses.push(f); + fileResponseFailures.set(`${f.type}#${f.fullName}`, f.error); + }); + this.sortFileResponses(fileResponses); + this.asRelativePaths(fileResponses); + failures.push(...fileResponses); + } + + const deployMessages = toArray(this.result?.response?.details?.componentFailures); + if (deployMessages.length > failures.length) { + // if there's additional failures in the API response, find the failure and add it to the output + deployMessages.map((deployMessage) => { + if (!fileResponseFailures.has(`${deployMessage.componentType}#${deployMessage.fullName}`)) { + // duplicate the problem message to the error property for displaying in the table + failures.push(Object.assign(deployMessage, { error: deployMessage.problem })); + } + }); + } + + this.ux.log(''); + this.ux.styledHeader(chalk.red(`Component Failures [${failures.length}]`)); + this.ux.table(failures, { + columns: [ + { key: 'problemType', label: 'Type' }, + { key: 'fullName', label: 'Name' }, + { key: 'error', label: 'Problem' }, + ], + }); + this.ux.log(''); + } + } +} diff --git a/src/formatters/resultFormatter.ts b/src/formatters/resultFormatter.ts index f1b1914d1..2038bc3aa 100644 --- a/src/formatters/resultFormatter.ts +++ b/src/formatters/resultFormatter.ts @@ -13,6 +13,7 @@ import { getBoolean, getNumber } from '@salesforce/ts-types'; export interface ResultFormatterOptions { verbose?: boolean; + quiet?: boolean; waitTime?: number; } @@ -44,6 +45,10 @@ export abstract class ResultFormatter { return getBoolean(this.options, 'verbose', false); } + public isQuiet(): boolean { + return getBoolean(this.options, 'quiet', false); + } + // Sort by type > filePath > fullName protected sortFileResponses(fileResponses: FileResponse[]): void { fileResponses.sort((i, j) => { diff --git a/src/formatters/retrieveResultFormatter.ts b/src/formatters/retrieveResultFormatter.ts index dd770e3b0..acbc12a34 100644 --- a/src/formatters/retrieveResultFormatter.ts +++ b/src/formatters/retrieveResultFormatter.ts @@ -51,7 +51,7 @@ export class RetrieveResultFormatter extends ResultFormatter { const warnMessages = get(result, 'response.messages', []) as RetrieveMessage | RetrieveMessage[]; this.warnings = toArray(warnMessages); this.packages = options.packages || []; - // zipFile can become massive and unweildy with JSON parsing/terminal output and, isn't useful + // zipFile can become massive and unwieldy with JSON parsing/terminal output and, isn't useful delete this.result.response.zipFile; } diff --git a/src/formatters/statusFormatter.ts b/src/formatters/statusFormatter.ts new file mode 100644 index 000000000..a68b17fe5 --- /dev/null +++ b/src/formatters/statusFormatter.ts @@ -0,0 +1,66 @@ +/* + * 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 { UX } from '@salesforce/command'; +import { Logger, Messages } from '@salesforce/core'; +import { ResultFormatter, ResultFormatterOptions } from './resultFormatter'; + +Messages.importMessagesDirectory(__dirname); +const messages: Messages = Messages.loadMessages('@salesforce/plugin-source', 'status'); + +type StatusActualState = 'Deleted' | 'Add' | 'Changed' | 'Unchanged'; +type StatusOrigin = 'Local' | 'Remote'; +type StatusStateString = `${StatusOrigin} ${StatusActualState}` | `${StatusOrigin} ${StatusActualState} (Conflict)`; +export interface StatusResult { + state: StatusStateString; + fullName: string; + type: string; + filePath?: string; + ignored?: boolean; + conflict?: boolean; + actualState?: StatusActualState; + origin: StatusOrigin; +} + +// sort order is state, type, fullname +const rowSortFunction = (a: StatusResult, b: StatusResult): number => { + if (a.state.toLowerCase() === b.state.toLowerCase()) { + if (a.type.toLowerCase() === b.type.toLowerCase()) { + return a.fullName.toLowerCase() < b.fullName.toLowerCase() ? -1 : 1; + } + return a.type.toLowerCase() < b.type.toLowerCase() ? -1 : 1; + } + return a.state.toLowerCase() < b.state.toLowerCase() ? -1 : 1; +}; + +export class StatusFormatter extends ResultFormatter { + public constructor(logger: Logger, ux: UX, options: ResultFormatterOptions, private statusRows: StatusResult[]) { + super(logger, ux, options); + } + + public getJson(): StatusResult[] { + return this.statusRows; + } + + public display(): void { + if (this.statusRows.length === 0) { + this.ux.log('No results found'); + return; + } + this.ux.log(messages.getMessage('humanSuccess')); + const baseColumns = [ + { label: 'STATE', key: 'state' }, + { label: 'FULL NAME', key: 'fullName' }, + { label: 'TYPE', key: 'type' }, + { label: 'PROJECT PATH', key: 'filePath' }, + ]; + this.ux.table(this.statusRows.sort(rowSortFunction), { + columns: this.statusRows.some((row) => row.ignored) + ? [{ label: 'IGNORED', key: 'ignored' }, ...baseColumns] + : baseColumns, + }); + } +} diff --git a/test/commands/source/open.test.ts b/test/commands/source/open.test.ts index ee0ab9044..57d47d1fa 100644 --- a/test/commands/source/open.test.ts +++ b/test/commands/source/open.test.ts @@ -81,29 +81,15 @@ describe('force:source:open', () => { const orgStub = fromStub( stubInterface(sandbox, { getUsername: () => username, + getOrgId: () => orgId, getConnection: () => ({ getAuthInfoFields: () => ({ username, orgId, }), - tooling: { - query: () => ({ - size: 1, - totalSize: 1, - done: true, - queryLocator: null, - entityTypeName: 'FlexiPage', - records: [ - { - attributes: { - type: 'FlexiPage', - url: '/services/data/v52.0/tooling/sobjects/FlexiPage/0M0J0000000Q0vmKAC', - }, - Id: recordId, - }, - ], - }), - }, + singleRecordQuery: () => ({ + Id: recordId, + }), }), }) ); diff --git a/test/formatters/pushResultFormater.test.ts b/test/formatters/pushResultFormater.test.ts new file mode 100644 index 000000000..cc624593c --- /dev/null +++ b/test/formatters/pushResultFormater.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { expect } from 'chai'; +import { Logger } from '@salesforce/core'; +import { UX } from '@salesforce/command'; +import * as sinon from 'sinon'; +import { stubInterface } from '@salesforce/ts-sinon'; +import { getDeployResult } from '../commands/source/deployResponses'; +import { PushResultFormatter } from '../../src/formatters/pushResultFormatter'; + +describe('PushResultFormatter', () => { + const logger = Logger.childFromRoot('deployTestLogger').useMemoryLogging(); + const deployResultSuccess = getDeployResult('successSync'); + const deployResultFailure = getDeployResult('failed'); + + const sandbox = sinon.createSandbox(); + + let uxMock; + let tableStub: sinon.SinonStub; + beforeEach(() => { + tableStub = sandbox.stub(); + uxMock = stubInterface(sandbox, { + table: tableStub, + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('json', () => { + it('returns expected json for success', () => { + const formatter = new PushResultFormatter(logger, new UX(logger), {}, deployResultSuccess); + // expect(formatter.getJson()).to.have.lengthOf(1); + expect(formatter.getJson()).to.deep.equal([ + { + filePath: 'classes/ProductController.cls', + fullName: 'ProductController', + state: 'Changed', + type: 'ApexClass', + }, + ]); + }); + describe('json with quiet', () => { + it('honors quiet flag for json successes', () => { + const formatter = new PushResultFormatter(logger, new UX(logger), { quiet: true }, deployResultSuccess); + expect(formatter.getJson()).to.deep.equal([]); + }); + it('honors quiet flag for json successes', () => { + const formatter = new PushResultFormatter(logger, new UX(logger), { quiet: true }, deployResultFailure); + expect(formatter.getJson()).to.have.length(1); + }); + }); + }); + + describe('human output', () => { + it('returns expected output for success', () => { + const formatter = new PushResultFormatter(logger, uxMock, {}, deployResultSuccess); + formatter.display(); + expect(tableStub.callCount).to.equal(1); + }); + describe('quiet', () => { + it('does not display successes for quiet', () => { + const formatter = new PushResultFormatter(logger, uxMock, { quiet: true }, deployResultSuccess); + formatter.display(); + expect(tableStub.callCount).to.equal(0); + }); + it('displays errors for quiet', () => { + const formatter = new PushResultFormatter(logger, uxMock, { quiet: true }, deployResultFailure); + formatter.display(); + expect(tableStub.callCount).to.equal(1); + }); + }); + }); +}); diff --git a/test/nuts/trackingCommands/aura.nut.ts b/test/nuts/trackingCommands/aura.nut.ts new file mode 100644 index 000000000..8d2fe6a3a --- /dev/null +++ b/test/nuts/trackingCommands/aura.nut.ts @@ -0,0 +1,16 @@ +/* + * 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 + */ + +describe('aura', () => { + it('sees aura css changes in local status'); + it('pushes aura css change'); + it("Deleting an aura sub-component should show the sub-component as 'Deleted'"); + it('pushes aura subcomponent delete'); + it('Each change to an aura subcomponent should be expressed in its own line'); + it('bundle shows as changed?'); + it('detects remote subcomponent conflicts'); +}); diff --git a/test/nuts/trackingCommands/basics.nut.ts b/test/nuts/trackingCommands/basics.nut.ts new file mode 100644 index 000000000..e7685e843 --- /dev/null +++ b/test/nuts/trackingCommands/basics.nut.ts @@ -0,0 +1,195 @@ +/* + * 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 + */ + +/* eslint-disable no-console */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { expect } from 'chai'; +import * as shelljs from 'shelljs'; +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { ComponentStatus } from '@salesforce/source-deploy-retrieve'; +import { replaceRenamedCommands } from '@salesforce/source-tracking'; +import { PushResponse } from '../../../src/formatters/pushResultFormatter'; +import { StatusResult } from '../../../src/formatters/statusFormatter'; +import { PullResponse } from '../../../src/formatters/pullFormatter'; + +let session: TestSession; +describe('end-to-end-test for tracking with an org (single packageDir)', () => { + before(async () => { + session = await TestSession.create({ + project: { + gitClone: 'https://github.com/trailheadapps/ebikes-lwc', + }, + setupCommands: [ + // 'git checkout 652b954921f51c79371c224760dd5bdf6a277db5', + `sfdx force:org:create -d 1 -s -f ${path.join('config', 'project-scratch-def.json')}`, + ], + }); + + // we also need to remove profiles from the forceignore + const originalForceIgnore = await fs.promises.readFile(path.join(session.project.dir, '.forceignore'), 'utf8'); + const newForceIgnore = originalForceIgnore.replace('**/profiles/**', ''); + await fs.promises.writeFile(path.join(session.project.dir, '.forceignore'), newForceIgnore); + }); + + after(async () => { + await session?.zip(undefined, 'artifacts'); + await session?.clean(); + }); + + describe('basic status and pull', () => { + it('detects the initial metadata status', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result).to.be.an.instanceof(Array); + // the fields should be populated + expect(result.every((row) => row.type && row.fullName)).to.equal(true); + }); + it('pushes the initial metadata to the org', () => { + const result = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result).to.be.an.instanceof(Array); + expect(result, JSON.stringify(result)).to.have.lengthOf(234); + expect( + result.every((r) => r.state !== ComponentStatus.Failed), + JSON.stringify(result) + ).to.equal(true); + }); + it('sees no local changes (all were committed from push), but profile updated in remote', () => { + const localResult = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(localResult).to.deep.equal([]); + + const remoteResult = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(remoteResult.some((item) => item.type === 'Profile')).to.equal(true); + }); + + it('can pull the remote profile', () => { + const pullResult = execCmd(replaceRenamedCommands('force:source:pull --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect( + pullResult.some((item) => item.type === 'Profile'), + JSON.stringify(pullResult) + ).to.equal(true); + }); + + it('sees no local or remote changes', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect( + result.filter((r) => r.type === 'Profile'), + JSON.stringify(result) + ).to.have.length(0); + }); + + it('sees a local delete in local status', async () => { + const classDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'classes'); + await Promise.all([ + fs.promises.unlink(path.join(classDir, 'TestOrderController.cls')), + fs.promises.unlink(path.join(classDir, 'TestOrderController.cls-meta.xml')), + ]); + const result = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result).to.deep.equal([ + { + type: 'ApexClass', + state: 'Local Deleted', + fullName: 'TestOrderController', + filePath: path.normalize('force-app/main/default/classes/TestOrderController.cls'), + ignored: false, + actualState: 'Deleted', + origin: 'Local', + }, + { + type: 'ApexClass', + state: 'Local Deleted', + fullName: 'TestOrderController', + filePath: path.normalize('force-app/main/default/classes/TestOrderController.cls-meta.xml'), + ignored: false, + actualState: 'Deleted', + origin: 'Local', + }, + ]); + }); + it('does not see any change in remote status', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect( + result.filter((r) => r.fullName === 'TestOrderController'), + JSON.stringify(result) + ).to.have.length(0); + }); + + it('pushes the local delete to the org', () => { + const result = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result, JSON.stringify(result)).to.be.an.instanceof(Array).with.length(2); + }); + it('sees no local changes', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result, JSON.stringify(result)).to.be.an.instanceof(Array).with.length(0); + }); + }); + + describe('non-successes', () => { + it('should throw an err when attempting to pull from a non scratch-org', () => { + const hubUsername = ( + JSON.parse(shelljs.exec('sfdx force:config:get defaultdevhubusername --json', { silent: true })) as { + result: [{ location: string; value: string }]; + } + ).result.find((config) => config.location === 'Local').value; + const failure = execCmd(replaceRenamedCommands(`force:source:status -u ${hubUsername} --remote --json`), { + ensureExitCode: 1, + }).jsonOutput as unknown as { name: string }; + expect(failure.name).to.equal('NonSourceTrackedOrgError'); + }); + + describe('push failures', () => { + it('writes a bad class', async () => { + const classdir = path.join(session.project.dir, 'force-app', 'main', 'default', 'classes'); + // add a file in the local source + await Promise.all([ + fs.promises.writeFile(path.join(classdir, 'badClass.cls'), 'bad'), + fs.promises.writeFile( + path.join(classdir, 'badClass.cls-meta.xml'), + ` + + 53.0 +` + ), + ]); + }); + it('fails to push', () => { + const result = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 1, + }).jsonOutput.result; + expect(result.every((r) => r.type === 'ApexClass' && r.state === 'Failed')).to.equal(true); + }); + it('classes that failed to deploy are still in local status', () => { + it('sees no local changes', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result, JSON.stringify(result)).to.be.an.instanceof(Array).with.length(2); + }); + }); + }); + }); +}); diff --git a/test/nuts/trackingCommands/conflicts.nut.ts b/test/nuts/trackingCommands/conflicts.nut.ts new file mode 100644 index 000000000..902e925f5 --- /dev/null +++ b/test/nuts/trackingCommands/conflicts.nut.ts @@ -0,0 +1,136 @@ +/* + * 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 + */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { expect } from 'chai'; + +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { Connection, AuthInfo } from '@salesforce/core'; +import { ComponentStatus } from '@salesforce/source-deploy-retrieve'; +import { replaceRenamedCommands } from '@salesforce/source-tracking'; +import { PushResponse } from '../../../src/formatters/pushResultFormatter'; +import { StatusResult } from '../../../src/formatters/statusFormatter'; +import { PullResponse } from '../../../src/formatters/pullFormatter'; + +let session: TestSession; +describe('conflict detection and resolution', () => { + before(async () => { + session = await TestSession.create({ + project: { + gitClone: 'https://github.com/trailheadapps/ebikes-lwc', + }, + setupCommands: [ + 'git checkout 652b954921f51c79371c224760dd5bdf6a277db5', + `sfdx force:org:create -d 1 -s -f ${path.join('config', 'project-scratch-def.json')}`, + ], + }); + }); + + after(async () => { + await session?.zip(undefined, 'artifacts'); + await session?.clean(); + }); + + it('pushes to initiate the remote', () => { + // This would go in setupCommands but we want it to use the bin/run version + const pushResult = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(pushResult, JSON.stringify(pushResult)).to.have.lengthOf(234); + expect( + pushResult.every((r) => r.state !== ComponentStatus.Failed), + JSON.stringify(pushResult) + ).to.equal(true); + }); + + it('edits a remote file', async () => { + const conn = await Connection.create({ + authInfo: await AuthInfo.create({ + username: (session.setup[1] as { result: { username: string } }).result?.username, + }), + }); + const app = await conn.singleRecordQuery<{ Id: string; Metadata: any }>( + "select Id, Metadata from CustomApplication where DeveloperName = 'EBikes'", + { + tooling: true, + } + ); + await conn.tooling.sobject('CustomApplication').update({ + ...app, + Metadata: { + ...app.Metadata, + description: 'modified', + }, + }); + const result = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect( + result.filter((r) => r.type === 'CustomApplication'), + JSON.stringify(result) + ).to.have.lengthOf(1); + }); + it('edits a local file', async () => { + const filePath = path.join( + session.project.dir, + 'force-app', + 'main', + 'default', + 'applications', + 'EBikes.app-meta.xml' + ); + await fs.promises.writeFile( + filePath, + (await fs.promises.readFile(filePath, { encoding: 'utf-8' })).replace('Lightning App Builder', 'App Builder') + ); + }); + it('can see the conflict in status', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json'), { + ensureExitCode: 0, + }).jsonOutput.result.filter((app) => app.type === 'CustomApplication'); + // json is not sorted. This relies on the implementation of getConflicts() + expect(result).to.deep.equal([ + { + type: 'CustomApplication', + state: 'Local Changed (Conflict)', + fullName: 'EBikes', + filePath: path.normalize('force-app/main/default/applications/EBikes.app-meta.xml'), + ignored: false, + conflict: true, + origin: 'Local', + actualState: 'Changed', + }, + { + type: 'CustomApplication', + state: 'Remote Changed (Conflict)', + fullName: 'EBikes', + filePath: path.normalize('force-app/main/default/applications/EBikes.app-meta.xml'), + ignored: false, + conflict: true, + origin: 'Remote', + actualState: 'Changed', + }, + ]); + }); + + it('gets conflict error on push', () => { + execCmd(replaceRenamedCommands('force:source:push --json'), { ensureExitCode: 1 }); + }); + it('gets conflict error on pull', () => { + execCmd(replaceRenamedCommands('force:source:pull --json'), { ensureExitCode: 1 }); + }); + it('can push with forceoverwrite', () => { + execCmd(replaceRenamedCommands('force:source:push --json --forceoverwrite'), { + ensureExitCode: 0, + }); + }); +}); diff --git a/test/nuts/trackingCommands/forceIgnore.nut.ts b/test/nuts/trackingCommands/forceIgnore.nut.ts new file mode 100644 index 000000000..922be652c --- /dev/null +++ b/test/nuts/trackingCommands/forceIgnore.nut.ts @@ -0,0 +1,152 @@ +/* + * 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 + */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { expect } from 'chai'; +import * as shell from 'shelljs'; + +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { Connection, AuthInfo } from '@salesforce/core'; +import { ComponentStatus } from '@salesforce/source-deploy-retrieve'; +import { replaceRenamedCommands } from '@salesforce/source-tracking'; +import { PushResponse } from '../../../src/formatters/pushResultFormatter'; +import { PullResponse } from '../../../src/formatters/pullFormatter'; +import { StatusResult } from '../../../src/formatters/statusFormatter'; + +let session: TestSession; +const classdir = 'force-app/main/default/classes'; +let originalForceIgnore: string; +let conn: Connection; + +describe('forceignore changes', () => { + before(async () => { + session = await TestSession.create({ + project: { + name: 'forceIngoreTest', + }, + setupCommands: [ + `sfdx force:org:create -d 1 -s -f ${path.join('config', 'project-scratch-def.json')}`, + `sfdx force:apex:class:create -n IgnoreTest --outputdir ${classdir}`, + ], + }); + originalForceIgnore = await fs.promises.readFile(path.join(session.project.dir, '.forceignore'), 'utf8'); + conn = await Connection.create({ + authInfo: await AuthInfo.create({ + username: (session.setup[0] as { result: { username: string } }).result?.username, + }), + }); + }); + + after(async () => { + await session?.zip(undefined, 'artifacts'); + await session?.clean(); + }); + + describe('local', () => { + it('will not push a file that was created, then ignored', async () => { + // setup a forceIgnore with some file + const newForceIgnore = originalForceIgnore + '\n' + `${classdir}/IgnoreTest.cls`; + await fs.promises.writeFile(path.join(session.project.dir, '.forceignore'), newForceIgnore); + // nothing should push + const output = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(output).to.deep.equal([]); + }); + + it('shows the file in status as ignored', () => { + const output = execCmd(replaceRenamedCommands('force:source:status --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(output, JSON.stringify(output)).to.deep.include({ + state: 'Local Add', + fullName: 'IgnoreTest', + type: 'ApexClass', + origin: 'Local', + filePath: path.join(classdir, 'IgnoreTest.cls'), + ignored: true, + conflict: false, + actualState: 'Add', + }); + }); + + it('will ignore a class in the ignore file before it was created', async () => { + // setup a forceIgnore with some file + const newForceIgnore = + originalForceIgnore + '\n' + `${classdir}/UnIgnoreTest.cls` + '\n' + `${classdir}/IgnoreTest.cls`; + await fs.promises.writeFile(path.join(session.project.dir, '.forceignore'), newForceIgnore); + + // add a file in the local source + shell.exec(`sfdx force:apex:class:create -n UnIgnoreTest --outputdir ${classdir}`, { + cwd: session.project.dir, + silent: true, + }); + // pushes with no results + const ignoredOutput = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + // nothing should have been pushed + expect(ignoredOutput).to.deep.equal([]); + }); + + it('will push files that are now un-ignored', async () => { + // un-ignore the file + await fs.promises.writeFile(path.join(session.project.dir, '.forceignore'), originalForceIgnore); + + // verify file pushed in results + const unIgnoredOutput = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + + // all 4 files should have been pushed + expect(unIgnoredOutput).to.have.length(4); + unIgnoredOutput.map((result) => { + expect(result.type === 'ApexClass'); + expect(result.state === ComponentStatus.Created); + }); + }); + }); + + describe('remote', () => { + it('adds on the server', async () => { + const createResult = await conn.tooling.create('ApexClass', { + Name: 'CreatedClass', + Body: 'public class CreatedClass {}', + Status: 'Active', + }); + if (!Array.isArray(createResult) && createResult.success) { + expect(createResult.id).to.be.a('string'); + } + }); + + it('will not pull a remote file added to the ignore AFTER it is being tracked', async () => { + // add that type to the forceignore + await fs.promises.writeFile( + path.join(session.project.dir, '.forceignore'), + originalForceIgnore + '\n' + classdir + ); + + // gets file into source tracking + const statusOutput = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(statusOutput.some((result) => result.fullName === 'CreatedClass')).to.equal(true); + + // pull doesn't retrieve that change + const pullOutput = execCmd(replaceRenamedCommands('force:source:pull --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(pullOutput.some((result) => result.fullName === 'CreatedClass')).to.equal(false); + }); + }); +}); diff --git a/test/nuts/trackingCommands/hooks.nut.ts b/test/nuts/trackingCommands/hooks.nut.ts new file mode 100644 index 000000000..57f2cbb4a --- /dev/null +++ b/test/nuts/trackingCommands/hooks.nut.ts @@ -0,0 +1,10 @@ +/* + * 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 + */ +describe('something about hooks', () => { + it('fires hooks from push'); + it('fires hooks from pull'); +}); diff --git a/test/nuts/trackingCommands/lwc.nut.ts b/test/nuts/trackingCommands/lwc.nut.ts new file mode 100644 index 000000000..3d3cbb648 --- /dev/null +++ b/test/nuts/trackingCommands/lwc.nut.ts @@ -0,0 +1,15 @@ +/* + * 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 + */ +describe('lwc', () => { + it('sees lwc css changes in local status'); + it('pushes lwc css change'); + it("Deleting an lwc sub-component should show the sub-component as 'Deleted'"); + it('pushes lwc subcomponent delete'); + it('Each change to an lwc subcomponent should be expressed in its own line'); + it('bundle shows as changed?'); + it('detects remote subcomponent conflicts'); +}); diff --git a/test/nuts/trackingCommands/remoteChanges.nut.ts b/test/nuts/trackingCommands/remoteChanges.nut.ts new file mode 100644 index 000000000..25a43bcb9 --- /dev/null +++ b/test/nuts/trackingCommands/remoteChanges.nut.ts @@ -0,0 +1,184 @@ +/* + * 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 + */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import * as path from 'path'; +import * as fs from 'fs'; +import { expect } from 'chai'; + +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { Connection, AuthInfo } from '@salesforce/core'; +import { ComponentStatus } from '@salesforce/source-deploy-retrieve'; +import { replaceRenamedCommands } from '@salesforce/source-tracking'; +import { PushResponse } from '../../../src/formatters/pushResultFormatter'; +import { StatusResult } from '../../../src/formatters/statusFormatter'; +import { PullResponse } from '../../../src/formatters/pullFormatter'; + +let session: TestSession; +let conn: Connection; + +describe('remote changes', () => { + before(async () => { + session = await TestSession.create({ + project: { + gitClone: 'https://github.com/trailheadapps/ebikes-lwc', + }, + setupCommands: [ + 'git checkout 652b954921f51c79371c224760dd5bdf6a277db5', + `sfdx force:org:create -d 1 -s -f ${path.join('config', 'project-scratch-def.json')}`, + ], + }); + conn = await Connection.create({ + authInfo: await AuthInfo.create({ + username: (session.setup[1] as { result: { username: string } }).result?.username, + }), + }); + }); + + after(async () => { + await session?.zip(undefined, 'artifacts'); + await session?.clean(); + }); + + describe('remote changes: delete', () => { + it('pushes to initiate the remote', () => { + const pushResult = execCmd(replaceRenamedCommands('force:source:push --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(pushResult, JSON.stringify(pushResult)).to.have.lengthOf(234); + expect( + pushResult.every((r) => r.state !== ComponentStatus.Failed), + JSON.stringify(pushResult) + ).to.equal(true); + }); + + it('deletes on the server', async () => { + const testClass = await conn.singleRecordQuery<{ Id: string }>( + "select Id from ApexClass where Name = 'TestOrderController'", + { + tooling: true, + } + ); + const deleteResult = await conn.tooling.delete('ApexClass', testClass.Id); + if (!Array.isArray(deleteResult) && deleteResult.success) { + expect(deleteResult.id).to.be.a('string'); + } + }); + it('local file is present', () => { + expect( + fs.existsSync( + path.join(session.project.dir, 'force-app', 'main', 'default', 'classes', 'TestOrderController.cls') + ) + ).to.equal(true); + expect( + fs.existsSync( + path.join(session.project.dir, 'force-app', 'main', 'default', 'classes', 'TestOrderController.cls-meta.xml') + ) + ).to.equal(true); + }); + it('can see the delete in status', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + // it shows up as one class on the server, but 2 files when pulled + expect( + result.filter((r) => r.state.includes('Delete')), + JSON.stringify(result) + ).to.have.length(1); + }); + it('does not see any change in local status', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result).to.deep.equal([]); + }); + it('can pull the delete', () => { + const result = execCmd(replaceRenamedCommands('force:source:pull --json'), { ensureExitCode: 0 }) + .jsonOutput.result; + // the 2 files for the apexClass, and possibly one for the Profile (depending on whether it got created in time) + expect(result, JSON.stringify(result)).to.have.length.greaterThanOrEqual(2); + expect(result, JSON.stringify(result)).to.have.length.lessThanOrEqual(4); + result.filter((r) => r.fullName === 'TestOrderController').map((r) => expect(r.state).to.equal('Deleted')); + }); + it('local file was deleted', () => { + expect( + fs.existsSync( + path.join(session.project.dir, 'force-app', 'main', 'default', 'classes', 'TestOrderController.cls') + ) + ).to.equal(false); + expect( + fs.existsSync( + path.join(session.project.dir, 'force-app', 'main', 'default', 'classes', 'TestOrderController.cls-meta.xml') + ) + ).to.equal(false); + }); + it('sees correct local and remote status', () => { + const remoteResult = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(remoteResult.filter((r) => r.state.includes('Remote Deleted'))).to.deep.equal([]); + + const localStatus = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(localStatus).to.deep.equal([]); + }); + }); + + describe('remote changes: add', () => { + const className = 'CreatedClass'; + it('adds on the server', async () => { + const createResult = await conn.tooling.create('ApexClass', { + Name: className, + Body: 'public class CreatedClass {}', + Status: 'Active', + }); + if (!Array.isArray(createResult) && createResult.success) { + expect(createResult.id).to.be.a('string'); + } + }); + it('can see the add in status', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect( + result.some((r) => r.fullName === className), + JSON.stringify(result) + ).to.equal(true); + }); + it('can pull the add', () => { + const result = execCmd(replaceRenamedCommands('force:source:pull --json'), { ensureExitCode: 0 }) + .jsonOutput.result; + // SDR marks all retrieves as 'Changed' even if it creates new local files. This is different from toolbelt, which marked those as 'Created' + result.filter((r) => r.fullName === className).map((r) => expect(r.state, JSON.stringify(r)).to.equal('Created')); + }); + it('sees correct local and remote status', () => { + const remoteResult = execCmd(replaceRenamedCommands('force:source:status --json --remote'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect( + remoteResult.filter((r) => r.fullName === className), + JSON.stringify(remoteResult) + ).deep.equal([]); + + const localStatus = execCmd(replaceRenamedCommands('force:source:status --json --local'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(localStatus).to.deep.equal([]); + }); + }); + + describe('remote changes: mixed', () => { + it('all three types of changes on the server'); + it('can see the changes in status'); + it('can pull the changes'); + it('sees correct local and remote status'); + }); +}); diff --git a/test/nuts/trackingCommands/resetClear.nut.ts b/test/nuts/trackingCommands/resetClear.nut.ts new file mode 100644 index 000000000..b358489a0 --- /dev/null +++ b/test/nuts/trackingCommands/resetClear.nut.ts @@ -0,0 +1,160 @@ +/* + * 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 + */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable no-console */ + +import * as path from 'path'; +import * as fs from 'fs'; + +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import { Connection, AuthInfo } from '@salesforce/core'; +import { replaceRenamedCommands } from '@salesforce/source-tracking'; +import { StatusResult } from '../../../src/formatters/statusFormatter'; +import { SourceTrackingClearResult } from '../../../src/commands/force/source/beta/tracking/clear'; + +let session: TestSession; +let orgId: string; +let trackingFileFolder: string; +let conn: Connection; + +// copy/pasted here to avoid exporting this type from STL +type MemberRevision = { + serverRevisionCounter: number; + lastRetrievedFromServer: number | null; + memberType: string; + isNameObsolete: boolean; +}; + +const getRevisionsAsArray = async (): Promise => { + const revisionFile = JSON.parse( + await fs.promises.readFile(path.join(trackingFileFolder, 'maxRevision.json'), 'utf8') + ); + return Reflect.ownKeys(revisionFile.sourceMembers).map((key) => revisionFile.sourceMembers[key] as MemberRevision); +}; + +describe('reset and clear', () => { + before(async () => { + session = await TestSession.create({ + project: { + gitClone: 'https://github.com/trailheadapps/ebikes-lwc', + }, + setupCommands: [ + 'git checkout 652b954921f51c79371c224760dd5bdf6a277db5', + `sfdx force:org:create -d 1 -s -f ${path.join('config', 'project-scratch-def.json')}`, + ], + }); + orgId = (session.setup[1] as { result: { orgId: string } }).result?.orgId; + trackingFileFolder = path.join(session?.project.dir, '.sfdx', 'orgs', orgId); + conn = await Connection.create({ + authInfo: await AuthInfo.create({ + username: (session.setup[1] as { result: { username: string } }).result?.username, + }), + }); + }); + + after(async () => { + await session?.zip(undefined, 'artifacts'); + await session?.clean(); + }); + + describe('clearing tracking', () => { + it('runs status to start tracking', () => { + const result = execCmd(replaceRenamedCommands('force:source:status --json'), { + ensureExitCode: 0, + }).jsonOutput.result; + expect(result).to.have.length.greaterThan(100); // ebikes is big + }); + + it('local tracking file exists', () => { + expect(fs.existsSync(path.join(trackingFileFolder, 'localSourceTracking'))).to.equal(true); + }); + it('remote tracking file exists', () => { + expect(fs.existsSync(path.join(trackingFileFolder, 'maxRevision.json'))).to.equal(true); + }); + it('runs clear', () => { + const clearResult = execCmd( + replaceRenamedCommands('force:source:tracking:clear --noprompt --json'), + { + ensureExitCode: 0, + } + ).jsonOutput.result; + expect(clearResult.clearedFiles.some((file) => file.includes('maxRevision.json'))).to.equal(true); + }); + it('local tracking is gone', () => { + expect(fs.existsSync(path.join(trackingFileFolder, 'localSourceTracking'))).to.equal(false); + }); + it('remote tracking is gone', () => { + expect(fs.existsSync(path.join(trackingFileFolder, 'maxRevision.json'))).to.equal(false); + }); + }); + + describe('reset remote tracking', () => { + let lowestRevision: number; + it('creates 2 apex classes to get some tracking going', async () => { + const createResult = await conn.tooling.create('ApexClass', { + Name: 'CreatedClass', + Body: 'public class CreatedClass {}', + Status: 'Active', + }); + const createResult2 = await conn.tooling.create('ApexClass', { + Name: 'CreatedClass2', + Body: 'public class CreatedClass2 {}', + Status: 'Active', + }); + [createResult, createResult2].map((result) => { + if (!Array.isArray(result)) { + expect(result.success).to.equal(true); + } + }); + // gets tracking files from server + execCmd(replaceRenamedCommands('force:source:status --json --remote'), { ensureExitCode: 0 }); + const revisions = await getRevisionsAsArray(); + const revisionFile = JSON.parse( + await fs.promises.readFile(path.join(trackingFileFolder, 'maxRevision.json'), 'utf8') + ); + lowestRevision = revisions.reduce( + (previousValue, revision) => Math.min(previousValue, revision.serverRevisionCounter), + revisionFile.serverMaxRevisionCounter + ); + expect(lowestRevision).lessThan(revisionFile.serverMaxRevisionCounter); + // revisions are not retrieved + revisions.map((revision) => { + expect(revision.serverRevisionCounter !== revision.lastRetrievedFromServer).to.equal(true); + expect(revision.lastRetrievedFromServer).to.equal(null); + }); + }); + it('can reset to a known revision', async () => { + execCmd(replaceRenamedCommands(`force:source:tracking:reset --revision ${lowestRevision} --noprompt`), { + ensureExitCode: 0, + }); + const revisions = await getRevisionsAsArray(); + + revisions.map((revision) => { + if (revision.serverRevisionCounter === lowestRevision) { + expect(revision.serverRevisionCounter === revision.lastRetrievedFromServer).to.equal(true); + } else { + expect(revision.serverRevisionCounter !== revision.lastRetrievedFromServer).to.equal(true); + } + }); + }); + + it('can reset to a non-specified revision (resets everything)', async () => { + execCmd(replaceRenamedCommands(`force:source:tracking:reset --revision ${lowestRevision} --noprompt`), { + ensureExitCode: 0, + }); + const revisions = await getRevisionsAsArray(); + + revisions.map((revision) => { + expect(revision.serverRevisionCounter === revision.lastRetrievedFromServer).to.equal(true); + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 12618d122..a99f03279 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,13 @@ "extends": "@salesforce/dev-config/tsconfig", "compilerOptions": { "outDir": "lib", - "rootDir": "src" + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@salesforce/kit": ["node_modules/@salesforce/kit"], + "@salesforce/source-deploy-retrieve": ["node_modules/@salesforce/source-deploy-retrieve"], + "@salesforce/core": ["node_modules/@salesforce/core"] + } }, "include": ["./src/**/*.ts"] } diff --git a/yarn.lock b/yarn.lock index 9ae9b3d39..24e5a5126 100644 --- a/yarn.lock +++ b/yarn.lock @@ -823,10 +823,26 @@ unzipper "0.10.11" xmldom-sfdx-encoding "^0.1.29" -"@salesforce/source-deploy-retrieve@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-5.0.0.tgz#a27ec48f6ffeadf8b32da9945eaaf48a53110695" - integrity sha512-okHGiAuXVQKNTOu56CYMKg05Oe5EtIbGl8lju6WANzw/fyjkT1v1blTlyupP/cC70gQVEfNEdSO/QytnBEoi7A== +"@salesforce/source-deploy-retrieve@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-5.0.1.tgz#b4add87b3de0cb27e2432e64638ef145ae81d561" + integrity sha512-dcQv0ehKyXs+TzX/gHLhs1pV+cC2VmwsYe+sRjrqTxKsQ5qo+ByF8FhkbwtlYLdUOpRkaIS+/JwCpEwKgPKvKg== + dependencies: + "@salesforce/core" "2.28.0" + "@salesforce/kit" "^1.5.0" + "@salesforce/ts-types" "^1.4.2" + archiver "^5.3.0" + fast-xml-parser "^3.17.4" + graceful-fs "^4.2.8" + ignore "^5.1.8" + mime "2.4.6" + unzipper "0.10.11" + xmldom-sfdx-encoding "^0.1.29" + +"@salesforce/source-deploy-retrieve@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-5.1.0.tgz#0c06825e24499dcb14ef76a8b76a66580fb177af" + integrity sha512-rdBKsnn59G6MuTmValYIvV5w3veQ5pry6X6qZvgA9qdG+w6biWFop9IF/wtMbeZghsnmKsOvAgPCYOlYyVhT4A== dependencies: "@salesforce/core" "2.28.0" "@salesforce/kit" "^1.5.0" @@ -856,6 +872,17 @@ sinon "^10.0.0" strip-ansi "^6.0.0" +"@salesforce/source-tracking@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@salesforce/source-tracking/-/source-tracking-0.4.1.tgz#eba5175923c7109e6c8a1587db3ee59653c7dc28" + integrity sha512-B1PQWPPXpm6uQ+6aVapW4fIuuF7m7CNEf9Xic9Ke8kjR6QBrbuKICpiRA05bKUwyk1uV1WaCZAwy5YIQ+hBBow== + dependencies: + "@salesforce/core" "^2.28.0" + "@salesforce/kit" "^1.5.17" + "@salesforce/source-deploy-retrieve" "^5.0.1" + isomorphic-git "^1.9.2" + ts-retry-promise "^0.6.0" + "@salesforce/ts-sinon@1.3.21": version "1.3.21" resolved "https://registry.npmjs.org/@salesforce/ts-sinon/-/ts-sinon-1.3.21.tgz#e8aaac2a80a9e802337d08080714ed76f452cacd" @@ -1445,6 +1472,11 @@ astral-regex@^2.0.0: resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-lock@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.3.0.tgz#0fba111bea8b9693020857eba4f9adca173df3e5" + integrity sha512-8A7SkiisnEgME2zEedtDYPxUPzdv3x//E7n5IFktPAtMYSEAV7eNJF0rMwrVyUFj6d/8rgajLantbjcNRQYXIg== + async@0.9.x: version "0.9.2" resolved "https://registry.npmjs.org/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -1843,6 +1875,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-git-ref@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/clean-git-ref/-/clean-git-ref-2.0.1.tgz#dcc0ca093b90e527e67adb5a5e55b1af6816dcd9" + integrity sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2433,6 +2470,13 @@ decode-uri-component@^0.2.0: resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + dedent@0.7.0, dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -2521,6 +2565,11 @@ detect-indent@^6.0.0: resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +diff3@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/diff3/-/diff3-0.0.3.tgz#d4e5c3a4cdf4e5fe1211ab42e693fcb4321580fc" + integrity sha1-1OXDpM305f4SEatC5pP8tDIVgPw= + diff@5.0.0, diff@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" @@ -4457,6 +4506,23 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-git@^1.9.2: + version "1.10.0" + resolved "https://registry.yarnpkg.com/isomorphic-git/-/isomorphic-git-1.10.0.tgz#59a4604d1190d1e7fc52172085da25e6a428bc07" + integrity sha512-CijspEYaOQAnsHWXyq8ICZXzLJ/1wYQAa0jdfLcugA/68oNzrxykjGZz8Up7B8huA1VfkFHm4VviExtj/zpViw== + dependencies: + async-lock "^1.1.0" + clean-git-ref "^2.0.1" + crc-32 "^1.2.0" + diff3 "0.0.3" + ignore "^5.1.4" + minimisted "^2.0.0" + pako "^1.0.10" + pify "^4.0.1" + readable-stream "^3.4.0" + sha.js "^2.4.9" + simple-get "^3.0.2" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -5292,6 +5358,11 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -5318,6 +5389,13 @@ minimist@1.2.5, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2 resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimisted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/minimisted/-/minimisted-2.0.1.tgz#d059fb905beecf0774bc3b308468699709805cb1" + integrity sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA== + dependencies: + minimist "^1.2.5" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -5861,6 +5939,11 @@ paged-request@^2.0.1: dependencies: axios "^0.21.1" +pako@^1.0.10: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6678,6 +6761,14 @@ sfdx-faye@^1.0.9: tough-cookie "~2.4.3" tunnel-agent "~0.6.0" +sha.js@^2.4.9: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -6740,6 +6831,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + sinon@10.0.0: version "10.0.0" resolved "https://registry.npmjs.org/sinon/-/sinon-10.0.0.tgz#52279f97e35646ff73d23207d0307977c9b81430"