salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
274 lines (272 loc) • 15.5 kB
JavaScript
"use strict";
/*
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceDeployApi = void 0;
const path = require("path");
const os = require("os");
const cli_ux_1 = require("cli-ux");
// Node
const fsExtra = require("fs-extra");
// Local
const core_1 = require("@salesforce/core");
const MdapiDeployApi = require("../mdapi/mdapiDeployApi");
const consts = require("../core/constants");
const MetadataRegistry = require("./metadataRegistry");
const syncCommandHelper = require("./syncCommandHelper");
const workspaceFileState_1 = require("./workspaceFileState");
const metadataTypeFactory_1 = require("./metadataTypeFactory");
const sourceDeployApiBase_1 = require("./sourceDeployApiBase");
const sourceUtil_1 = require("./sourceUtil");
const aggregateSourceElements_1 = require("./aggregateSourceElements");
const PathUtils = require("./sourcePathUtil");
const sourceWorkspaceAdapter_1 = require("./sourceWorkspaceAdapter");
const sourceElementsResolver_1 = require("./sourceElementsResolver");
class SourceDeployApi extends sourceDeployApiBase_1.SourceDeployApiBase {
constructor() {
super(...arguments);
this.DELETE_NOT_SUPPORTED_IN_CONTENT = ['StaticResource'];
}
// @todo we shouldn't cross the command api separation by re-using cli options as dependencies for the api.
async doDeploy(options) {
let aggregateSourceElements = new aggregateSourceElements_1.AggregateSourceElements();
this.isDelete = options.delete;
this.logger = await core_1.Logger.child('SourceDeployApi');
this.isAsync = options.wait === consts.MIN_SRC_DEPLOY_WAIT_MINUTES;
// Only put SWA in stateless mode when sourcepath param is used.
const mode = options.sourcepath && sourceWorkspaceAdapter_1.SourceWorkspaceAdapter.modes.STATELESS;
this.logger.debug(`mode: ${mode}`);
const sfdxProject = core_1.SfdxProject.getInstance();
const swaOptions = {
org: this.orgApi,
metadataRegistryImpl: MetadataRegistry,
defaultPackagePath: sfdxProject.getDefaultPackage().name,
fromConvert: true,
sourceMode: mode,
};
this.swa = await sourceWorkspaceAdapter_1.SourceWorkspaceAdapter.create(swaOptions);
const packageNames = sfdxProject.getUniquePackageNames();
const tmpOutputDir = await sourceUtil_1.createOutputDir('sourceDeploy');
try {
const sourceElementsResolver = new sourceElementsResolver_1.SourceElementsResolver(this.orgApi, this.swa);
if (options.sourcepath) {
this.logger.info(`Deploying metadata in sourcepath '${options.sourcepath}' to org: '${this.orgApi.name}'`);
aggregateSourceElements = await sourceUtil_1.getSourceElementsFromSourcePath(options.sourcepath, this.swa);
// sourcepaths can be outside of a packageDirectory, in which case the packageName will be undefined.
// Add `undefined` as a valid package to deploy for this case.
packageNames.push(undefined);
}
else if (options.manifest) {
this.logger.info(`Deploying metadata in manifest '${options.manifest}' to org: '${this.orgApi.name}'`);
aggregateSourceElements = await sourceElementsResolver.getSourceElementsFromManifest(options.manifest);
}
else if (options.metadata) {
aggregateSourceElements = await sourceElementsResolver.getSourceElementsFromMetadata(options, aggregateSourceElements, tmpOutputDir);
}
else if (options.validateddeployrequestid) {
// this is a quick deploy
return new MdapiDeployApi(this.orgApi).deploy(options);
}
else {
// This should never happen but just a little OC - 'else if' without an 'else'
throw core_1.SfdxError.create('salesforce-alm', 'source', 'missingScopeOption');
}
SourceDeployApi.packagesDeployed = aggregateSourceElements.size;
let _handleDeleteResult = false;
if (this.isDelete) {
if (options.sourcepath) {
_handleDeleteResult = await this._handleDelete(options.noprompt, aggregateSourceElements, options.sourcepath);
}
else {
// if it is the metadata option, options.sourcepath was empty. Create a path to the "source" from the MD name
_handleDeleteResult = await this._handleDelete(options.noprompt, aggregateSourceElements, path.join(this.swa.defaultSrcDir, 'aura', options.metadata.split(':').pop()));
}
if (!_handleDeleteResult) {
return { outboundFiles: [], userCanceled: true };
}
}
if (isNaN(options.wait)) {
options.wait = this.force.config.getConfigContent().defaultSrcWaitMinutes;
}
const results = { outboundFiles: [], deploys: [] };
if (aggregateSourceElements.size > 0) {
// Deploy AggregateSourceElements in the order specified within the project config.
for (const pkgName of packageNames) {
const aseMap = aggregateSourceElements.get(pkgName);
if (aseMap && aseMap.size) {
this.logger.info('deploying package:', pkgName);
let tmpPkgOutputDir;
// Clone the options object passed to this.doDeploy so options don't
// leak from 1 package deploy to the next.
const deployOptions = Object.assign({}, options);
try {
// Create a temp directory
tmpPkgOutputDir = await sourceUtil_1.createOutputDir('sourceDeploy_pkg');
deployOptions.deploydir = tmpPkgOutputDir;
// change the manifest path to point to the package.xml from the
// package tmp deploy dir
deployOptions.manifest = path.join(tmpPkgOutputDir, 'package.xml');
deployOptions.ignorewarnings = deployOptions.ignorewarnings || this.isDelete;
const _ases = new aggregateSourceElements_1.AggregateSourceElements().set(pkgName, aseMap);
if (!deployOptions.checkonly) {
await this._doLocalDelete(_ases);
}
let result = await this.convertAndDeploy(deployOptions, this.swa, _ases, this.isDelete);
// If we are only checking the metadata deploy, return what `mdapi:deploy` returns.
// Otherwise process results and return similar to `source:push`
if (!deployOptions.checkonly && !this.isAsync) {
result = await this._processResults(result, _ases, deployOptions.deploydir);
}
// NOTE: This object assign is unfortunate and wrong, but we have to do it to maintain
// JSON output backwards compatibility between pre-mpd and mpd deploys.
const outboundFiles = results.outboundFiles;
Object.assign(results, result);
if (result.outboundFiles && result.outboundFiles.length) {
results.outboundFiles = [...outboundFiles, ...result.outboundFiles];
}
results.deploys.push(result);
}
finally {
// Remove the sourcePathInfos.json file and delete any temp dirs
this.orgApi.getSourcePathInfos().delete();
await sourceUtil_1.cleanupOutputDir(tmpPkgOutputDir);
await sourceUtil_1.cleanupOutputDir(this.tmpBackupDeletions);
}
}
}
}
return results;
}
finally {
await sourceUtil_1.cleanupOutputDir(tmpOutputDir);
}
}
async _doLocalDelete(ases) {
this.tmpBackupDeletions = await sourceUtil_1.createOutputDir('sourceDelete');
const cleanedCache = new Map();
ases.getAllSourceElements().forEach((ase) => {
ase
.getPendingDeletedWorkspaceElements()
.forEach((we) => fsExtra.copySync(we.getSourcePath(), path.join(this.tmpBackupDeletions, path.basename(we.getSourcePath()))));
ase.commitDeletes([]);
const dirname = path.dirname(ase.getMetadataFilePath());
if (!cleanedCache.get(dirname)) {
const contentPaths = ase.getContentPaths(ase.getMetadataFilePath());
contentPaths.forEach((content) => {
if (content.includes('__tests__') && content.includes('lwc')) {
core_1.fs.unlinkSync(content);
}
});
// This should only be called once per type. For example if there are 1000 static resources then
// cleanEmptyDirs should be called once not 1000 times.
PathUtils.cleanEmptyDirs(dirname);
cleanedCache.set(dirname, true);
}
});
}
async _handleDelete(noprompt, ases, sourcepath) {
let pendingDelPathsForPrompt = [];
const typedefObj = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromSourcePath(sourcepath, this.swa.metadataRegistry);
const metadataType = typedefObj ? typedefObj.getMetadataName() : null;
/** delete of static resources file is not supported by cli */
if (this.DELETE_NOT_SUPPORTED_IN_CONTENT.includes(metadataType)) {
const data = fsExtra.statSync(sourcepath);
if (data.isFile()) {
throw core_1.SfdxError.create('salesforce-alm', 'source', 'StaticResourceDeleteError');
}
}
ases.getAllSourceElements().forEach((ase) => {
ase.getWorkspaceElements().some((we) => {
const type = we.getMetadataName();
const sourceMemberMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(type, this.swa.metadataRegistry);
const shouldDeleteWorkspaceAggregate = sourceMemberMetadataType.shouldDeleteWorkspaceAggregate(type);
if (shouldDeleteWorkspaceAggregate) {
ase.markForDelete();
return true;
}
else {
// the type is decomposed and we only want to delete components of an aggregate element
const sourcepaths = sourcepath.split(',');
if (sourcepaths.some((sp) => we.getSourcePath().includes(sp.trim()))) {
we.setState(workspaceFileState_1.WorkspaceFileState.DELETED);
ase.addPendingDeletedWorkspaceElement(we);
}
}
});
pendingDelPathsForPrompt = pendingDelPathsForPrompt.concat(ase.getPendingDeletedWorkspaceElements().map((el) => `${os.EOL}${el.getSourcePath()}`));
});
if (noprompt || pendingDelPathsForPrompt.length === 0) {
return true;
}
return this._handlePrompt(pendingDelPathsForPrompt);
}
async _handlePrompt(pathsToPrompt) {
// @todo this prompt should no be in the API. Need to remove.
const messages = core_1.Messages.loadMessages('salesforce-alm', 'source_delete');
// the pathsToPrompt looks like [ '\n/path/to/metadata', '\n/path/to/metadata/two']
// move the \n from the front to in between each entry for proper output
const paths = pathsToPrompt.map((p) => p.substr(2)).join('\n');
const promptMessage = messages.getMessage('sourceDeletePrompt', [paths]);
const answer = await cli_ux_1.default.prompt(promptMessage);
return answer.toUpperCase() === 'YES' || answer.toUpperCase() === 'Y';
}
async _processResults(result, aggregateSourceElements, deployDir) {
if (result.success && result.details.componentFailures) {
this.removeFailedAggregates(result.details.componentFailures, aggregateSourceElements);
}
// We need to check both success and status because a status of 'SucceededPartial' returns success === true even though rollbackOnError is set.
if (result.success && result.status === 'Succeeded') {
const isNonDestructiveChangeDelete = this.isDelete && !fsExtra.existsSync(`${deployDir}${path.sep}destructiveChangesPost.xml`);
result.outboundFiles = this.getOutboundFiles(aggregateSourceElements, isNonDestructiveChangeDelete);
return result;
}
else {
// throw the error that is created by _setupDeployFail
throw await this._setupDeployFail(result, aggregateSourceElements);
}
}
async _setupDeployFail(result, aggSourceElements) {
const deployFailed = new Error();
if (result.timedOut) {
deployFailed.name = 'PollingTimeout';
}
else {
deployFailed.name = 'DeployFailed';
deployFailed.failures = syncCommandHelper.getDeployFailures(result, aggSourceElements, this.swa.metadataRegistry);
}
if (result.success && result.status === 'SucceededPartial') {
deployFailed.outboundFiles = this.getOutboundFiles(aggSourceElements);
}
if (this.isDelete) {
await this._revertDeletions(aggSourceElements);
const messages = core_1.Messages.loadMessages('salesforce-alm', 'source_delete');
deployFailed.message = messages.getMessage('sourceDeleteFailure');
}
return deployFailed;
}
// Revert all deletions since something went wrong and they were not deleted server side.
// This copies all the files from the temporary location back to their original location.
async _revertDeletions(ases) {
for (const ase of ases.getAllSourceElements()) {
const parentDir = path.dirname(ase.getMetadataFilePath());
try {
await core_1.fs.access(parentDir, core_1.fs.constants.R_OK);
}
catch (e) {
// If the parent directory does not exist, re-create it
await core_1.fs.mkdirp(parentDir, core_1.fs.DEFAULT_USER_DIR_MODE);
}
// Re-create each workspace element
for (const we of ase.getWorkspaceElements()) {
const backupPath = path.join(this.tmpBackupDeletions, path.basename(we.getSourcePath()));
fsExtra.copySync(backupPath, we.getSourcePath());
}
}
}
}
exports.SourceDeployApi = SourceDeployApi;
//# sourceMappingURL=sourceDeployApi.js.map