salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
317 lines (315 loc) • 16 kB
JavaScript
;
/*
* 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.updateSourceTracking = exports.containsMdBundle = exports.checkForXmlParseError = exports.getSourceElementsInPath = exports.loadSourceElement = exports.getSourceElementsFromSourcePath = exports.getSourceElementForFile = exports.cleanupOutputDir = exports.createOutputDir = exports.validateManifestPath = exports.parseWaitParam = void 0;
const path = require("path");
const os = require("os");
const lodash_1 = require("lodash");
const core_1 = require("@salesforce/core");
const srcDevUtil = require("../core/srcDevUtil");
const consts = require("../core/constants");
const MdapiPackage = require("../source/mdapiPackage");
const aggregateSourceElement_1 = require("./aggregateSourceElement");
const metadataTypeFactory_1 = require("./metadataTypeFactory");
const inFolderMetadataType_1 = require("./metadataTypeImpl/inFolderMetadataType");
const nondecomposedTypesWithChildrenMetadataType_1 = require("./metadataTypeImpl/nondecomposedTypesWithChildrenMetadataType");
const customLabelsMetadataType_1 = require("./metadataTypeImpl/customLabelsMetadataType");
const aggregateSourceElements_1 = require("./aggregateSourceElements");
core_1.Messages.importMessagesDirectory(__dirname);
/**
* Validate the value for the 'wait' parameter and reset it as a number.
*
* @param flags The command parameters (aka flags)
* @param minWaitTime The minimum allowable time to wait
*/
exports.parseWaitParam = (flags, minWaitTime = consts.MIN_SRC_WAIT_MINUTES) => {
if (!lodash_1.isNil(flags.wait)) {
if (srcDevUtil.isInt(flags.wait)) {
const wait = (flags.wait = parseInt(flags.wait, 10)); // convert to a number
if (wait >= minWaitTime) {
return;
}
}
const errConfig = new core_1.SfdxErrorConfig('salesforce-alm', 'source', 'mdapiCliInvalidNumericParam');
errConfig.setErrorTokens(['wait']);
throw core_1.SfdxError.create(errConfig);
}
};
/**
* Validate that a manifest file path exists and is readable.
*
* @param manifestPath The path to the manifest file (package.xml)
*/
exports.validateManifestPath = async (manifestPath) => {
try {
await core_1.fs.access(manifestPath, core_1.fs.constants.R_OK);
}
catch (e) {
throw core_1.SfdxError.create('salesforce-alm', 'source', 'InvalidManifestError', [manifestPath]);
}
};
async function createOutputDir(cmdName) {
const logger = await core_1.Logger.child('SourceUtil');
const targetDir = process.env.SFDX_MDAPI_TEMP_DIR || os.tmpdir();
const tmpOutputDir = path.join(targetDir, `sdx_${cmdName}_${Date.now()}`);
await core_1.fs.mkdirp(tmpOutputDir, core_1.fs.DEFAULT_USER_DIR_MODE);
logger.info(`Created output directory '${tmpOutputDir}'`);
return tmpOutputDir;
}
exports.createOutputDir = createOutputDir;
async function cleanupOutputDir(outputDir) {
const logger = await core_1.Logger.child('SourceUtil');
if (outputDir && !outputDir.includes(process.env.SFDX_MDAPI_TEMP_DIR)) {
try {
await core_1.fs.remove(outputDir);
try {
if (await core_1.fs.stat(`${outputDir}.zip`)) {
await core_1.fs.unlink(`${outputDir}.zip`);
}
}
catch (err) {
if (err.code !== 'ENOENT') {
logger.warn(`Could not delete the MDAPI temporary zip file ${outputDir}.zip due to: ${err.message}`);
}
}
}
catch (err) {
logger.warn(`Could not delete the outputDir '${outputDir}' due to: ${err.message}`);
}
}
else {
logger.warn(`Did not delete the outputDir '${outputDir}' because it was set by the user`);
}
}
exports.cleanupOutputDir = cleanupOutputDir;
/**
* Return the aggregate source element for the specified file
*
* @param {string} sourcePath the file in the workspace
* @param sourceWorkspaceAdapter
* @returns {AggregateSourceElement}
*/
exports.getSourceElementForFile = async function (sourcePath, sourceWorkspaceAdapter, metadataType) {
let aggregateSourceElement;
const mdRegistry = sourceWorkspaceAdapter.metadataRegistry;
const sourceElementMetadataType = metadataType || metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromSourcePath(sourcePath, mdRegistry);
if (sourceElementMetadataType) {
// This will build an AggregateSourceElement with only the specified WorkspaceElement
// (child element) when the metadata type has a parent.
const _sourcePath = lodash_1.get(sourceElementMetadataType, 'typeDefObj.parent') ? sourcePath : path.dirname(sourcePath);
const aggregateMetadataName = sourceElementMetadataType.getAggregateMetadataName();
const aggregateFullName = sourceElementMetadataType.getAggregateFullNameFromFilePath(sourcePath);
const key = aggregateSourceElement_1.AggregateSourceElement.getKeyFromMetadataNameAndFullName(aggregateMetadataName, aggregateFullName);
// Get the AggregateSourceElement, which will only populate with the specified WorkspaceElement
// when sourcePath is part of an ASE.
const sourceElements = await sourceWorkspaceAdapter.getAggregateSourceElements(false, undefined, undefined, _sourcePath);
const packageName = core_1.SfdxProject.getInstance().getPackageNameFromPath(sourcePath);
aggregateSourceElement = exports.loadSourceElement(sourceElements, key, mdRegistry, packageName);
}
else {
throw core_1.SfdxError.create('salesforce-alm', 'source', 'SourcePathInvalid', [sourcePath]);
}
return aggregateSourceElement;
};
/**
* Get the source elements from the source path, whether for a particular file or a directory
*/
exports.getSourceElementsFromSourcePath = async function (optionsSourcePath, sourceWorkspaceAdapter) {
const aggregateSourceElements = new aggregateSourceElements_1.AggregateSourceElements();
const mdRegistry = sourceWorkspaceAdapter.metadataRegistry;
for (let sourcepath of optionsSourcePath.split(',')) {
// resolve to an absolute path
sourcepath = path.resolve(sourcepath.trim());
// Throw an error if the source path isn't accessible.
try {
await core_1.fs.access(sourcepath, core_1.fs.constants.R_OK);
}
catch (e) {
throw core_1.SfdxError.create('salesforce-alm', 'source', 'SourcePathInvalid', [sourcepath]);
}
// Get the MetadataType so we can resolve the path. Some paths such as individual static
// resources need to use a different path when getting source elements.
const metadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromSourcePath(sourcepath, mdRegistry);
if (metadataType) {
sourcepath = metadataType.resolveSourcePath(sourcepath);
}
// Get a single source element or a directory of source elements and add it to the map.
if (srcDevUtil.containsFileExt(sourcepath)) {
const ase = await exports.getSourceElementForFile(sourcepath, sourceWorkspaceAdapter, metadataType);
const aseKey = ase.getKey();
const pkg = ase.getPackageName();
if (aggregateSourceElements.has(pkg)) {
const sourceElements = aggregateSourceElements.get(pkg);
if (sourceElements.has(aseKey)) {
const _ase = sourceElements.get(aseKey);
_ase.addWorkspaceElement(ase.getWorkspaceElements()[0]);
sourceElements.set(aseKey, _ase);
}
else {
sourceElements.set(aseKey, ase);
}
}
else {
aggregateSourceElements.setIn(pkg, aseKey, ase);
}
}
else {
const sourceElementsInPath = await exports.getSourceElementsInPath(sourcepath, sourceWorkspaceAdapter);
aggregateSourceElements.merge(sourceElementsInPath);
}
}
return aggregateSourceElements;
};
/**
* Return the specified aggregate source element or error if it does not exist
*
* @param {AggregateSourceElements} sourceElements All the source elements in the workspace
* @param {string} key The key of the particular source element we are looking for
* @param {string} packageName
* @param {MetadataRegistry} metadataRegistry
* @returns {AggregateSourceElement}
*/
exports.loadSourceElement = function (sourceElements, key, metadataRegistry, packageName) {
const aggregateSourceElement = packageName
? sourceElements.getSourceElement(packageName, key)
: sourceElements.findSourceElementByKey(key);
if (!aggregateSourceElement) {
// Namespaces also contain '__' so only split on the first occurrence of '__'
let [mdType, ...rest] = key.split('__');
const mdName = rest.join('__');
const metadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(mdType, metadataRegistry);
if (!metadataType) {
throw core_1.SfdxError.create('salesforce-alm', 'source', 'MetadataTypeDoesNotExist', [mdType]);
}
const hasParentType = metadataType.getMetadataName() !== metadataType.getAggregateMetadataName();
if (hasParentType) {
// In this case, we are dealing with a decomposed subtype, so need to check for a parent
const parentName = metadataType.getAggregateMetadataName();
const parentMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getAggregateMetadataType(parentName, metadataRegistry);
const parentFullName = metadataType.getAggregateFullNameFromWorkspaceFullName(mdName);
const newKey = aggregateSourceElement_1.AggregateSourceElement.getKeyFromMetadataNameAndFullName(parentMetadataType.getAggregateMetadataName(), parentFullName);
// Get the parent AggregateSourceElement with all WorkspaceElements removed
// except for the child specified by the `key`.
return sourceElements.findParentElement(newKey, mdType, mdName);
}
else if (metadataType instanceof inFolderMetadataType_1.InFolderMetadataType) {
mdType = MdapiPackage.convertFolderTypeKey(mdType);
return exports.loadSourceElement(sourceElements, `${mdType}__${mdName}`, metadataRegistry, packageName);
}
else if (metadataType instanceof nondecomposedTypesWithChildrenMetadataType_1.NondecomposedTypesWithChildrenMetadataType) {
// eslint-disable-next-line @typescript-eslint/no-shadow
const mdType = metadataType.getMetadataName();
let name = `${mdType}__${mdName.split('.')[0]}`;
if (metadataType instanceof customLabelsMetadataType_1.CustomLabelsMetadataType) {
// for now, all CustomLabels are in CustomLabels.labels.meta-xml.
// the key for the ASE is then CustomLabels_CustomLabels
// it will deploy all CustomLabels, regardless of what is specified in the manifest
name = `${mdType}__${mdType}`;
}
if (name === key) {
// the name isn't changing which causes a max stack call size error,
const errConfig = new core_1.SfdxErrorConfig('salesforce-alm', 'source_deploy', 'SourceElementDoesNotExist');
errConfig.setErrorTokens([mdType, mdName]);
throw core_1.SfdxError.create(errConfig);
}
return exports.loadSourceElement(sourceElements, name, metadataRegistry, packageName);
}
else {
const errConfig = new core_1.SfdxErrorConfig('salesforce-alm', 'source_deploy', 'SourceElementDoesNotExist');
errConfig.setErrorTokens([mdType, mdName]);
throw core_1.SfdxError.create(errConfig);
}
}
return aggregateSourceElement;
};
/**
* Return the aggregate source elements found in the provided source path
*
* @param {Array<string>} sourcePath The path to look for source elements in
* @param sourceWorkspaceAdapter
* @returns {AggregateSourceElements}
*/
exports.getSourceElementsInPath = function (sourcePath, sourceWorkspaceAdapter) {
return sourceWorkspaceAdapter.getAggregateSourceElements(false, undefined, undefined, sourcePath);
};
/**
* Used to determine if an error is the result of parsing bad XML. If so return a new parsing error.
*
* @param path The file path.
* @param error The error to inspect.
*/
// eslint-disable-next-line @typescript-eslint/no-shadow
exports.checkForXmlParseError = function (path, error) {
if (path && error instanceof core_1.SfdxError && error.name === 'xmlParseErrorsReported') {
const data = error.data || [];
const message = `${path}:${os.EOL}${data.reduce(
// eslint-disable-next-line @typescript-eslint/no-shadow
(messages, message) => `${messages}${os.EOL}${message.message}`, '')}`;
return core_1.SfdxError.create('salesforce-alm', 'source', 'XmlParsingError', [message]);
}
return error;
};
/**
* @param options
*/
exports.containsMdBundle = function (options) {
if (options.metadata) {
// for retreiveFromMetadata
return options.metadata.indexOf('Bundle') >= 0;
}
else {
// for retrieveFromSourcepath
for (const pair of options) {
if (pair.type.indexOf('Bundle') >= 0) {
return true;
}
}
return false;
}
};
/**
* Filters the component success responses from a deploy or retrieve to exclude
* components that do not have SourceMembers created for them in the org, such
* as standard objects (e.g., Account) and standard fields before syncing with
* remote source tracking. Also modifies the fullName (e.g., MyApexClass) of
* certain metadata types to match their corresponding SourceMember names.
*
* Filtering rules applied:
* 1. Component successes without an `id` entry do not have `SourceMember`
* records created for them.
* E.g., standard objects, package.xml, CustomLabels, etc.
* 2. In-Folder types (E.g., Documents) will have the file extension removed
* since the SourceMember's MemberName does not include it.
* E.g., "MyDocFolder/MyDoc.png" --> "MyDocFolder/MyDoc"
* 3. Component success fullNames will be URI decoded.
* E.g., "Account %28Sales%29" --> "Account (Sales)"
*
* NOTE: Currently this is only called after a source:push.
*
* @param successes the component successes of a deploy/retrieve response
* @param remoteSourceTrackingService
* @param metadataRegistry
*/
exports.updateSourceTracking = async function (successes, remoteSourceTrackingService, metadataRegistry) {
const metadataTrackableElements = [];
successes.forEach((component) => {
// Assume only components with id's are trackable (SourceMembers are created)
if (component.id) {
const componentMetadataType = metadataTypeFactory_1.MetadataTypeFactory.getMetadataTypeFromMetadataName(component.componentType, metadataRegistry);
let fullName = component.fullName;
if (componentMetadataType instanceof inFolderMetadataType_1.InFolderMetadataType && component.fileName) {
// This removes any file extension from component.fullName
fullName = componentMetadataType.getFullNameFromFilePath(component.fileName);
}
metadataTrackableElements.push(decodeURIComponent(fullName));
}
});
// Sync source tracking for the trackable pushed components
await remoteSourceTrackingService.sync(metadataTrackableElements);
};
//# sourceMappingURL=sourceUtil.js.map