sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
181 lines • 8.77 kB
JavaScript
import { parsePackageXmlFile, writePackageXmlFile, removePackageXmlFilesContent, appendPackageXmlFilesContent } from './xmlUtils.js';
import { getFileAtCommit } from './gitUtils.js';
import fs from 'fs-extra';
import * as path from 'path';
import { createTempDir, uxLog } from './index.js';
import c from 'chalk';
let fullPackageTypes = null;
async function getAllTypes(fullPackageFile) {
if (fullPackageTypes) {
return fullPackageTypes;
}
fullPackageTypes = await parsePackageXmlFile(fullPackageFile);
return fullPackageTypes ?? new Map();
}
async function getAllLanguages(fullPackageFile) {
return (await getAllTypes(fullPackageFile))["Translations"] ?? [];
}
function addTypeIfMissing(types, typeToAdd) {
if (typeToAdd === null) {
return;
}
types[typeToAdd.name] = [...new Set([...(types[typeToAdd.name] ?? []), ...typeToAdd.members])];
}
// Generic processor factory
/* Config:
- separator: optional, if provided, the member must contain this separator and will be split on it
- skipCondition: optional, if provided, a function that takes the member and returns true if the member should be skipped
*/
function createProcessor(config) {
return async function (member, fullPackageFile) {
if (config.skipCondition && config.skipCondition(member)) {
return null;
}
if (config.separator) {
const parts = member.split(config.separator);
if (parts.length !== 2) {
return null;
}
}
const types = await getAllTypes(fullPackageFile);
const members = await config.memberGenerator(member, fullPackageFile, types);
return members.length ? { members, name: config.targetType } : null;
};
}
/* Extends a delta package.xml file with dependencies found in a full package.xml file.
For example, if the delta package.xml contains a CustomField, it will add the corresponding CustomObject and RecordTypes.
It also adds translations for all languages found in the full package.xml file.
*/
export async function extendPackageFileWithDependencies(deltaXmlFile, fullPackageFile, deltaDestructiveXmlFile) {
const languages = await getAllLanguages(fullPackageFile);
const modificationProcessors = listMetadataProcessors(languages, 'modified');
const deletionProcessors = listMetadataProcessors(languages, 'deleted');
const deltaToExtend = await parsePackageXmlFile(deltaXmlFile);
const destructiveTypes = deltaDestructiveXmlFile ? await parsePackageXmlFile(deltaDestructiveXmlFile) : {};
const clonedDeltaTypes = structuredClone(deltaToExtend);
await processMetadata(clonedDeltaTypes, deltaToExtend, modificationProcessors, fullPackageFile);
await processMetadata(destructiveTypes, deltaToExtend, deletionProcessors, fullPackageFile);
await writePackageXmlFile(deltaXmlFile, deltaToExtend);
}
async function processMetadata(typesToAnalyze, typesToExtend, metadataProcessors, fullPackageFile) {
for (const metadataType in typesToAnalyze) {
const members = typesToAnalyze[metadataType];
if (Object.hasOwn(metadataProcessors, metadataType)) {
for (const member of members) {
const processors = Array.isArray(metadataProcessors[metadataType]) ? metadataProcessors[metadataType] : [metadataProcessors[metadataType]];
for (const processor of processors) {
addTypeIfMissing(typesToExtend, await processor(member, fullPackageFile));
}
}
}
}
}
function listMetadataProcessors(languages, deltaAction) {
const allCustomFields = createProcessor({
targetType: "CustomField",
memberGenerator: async (member, _, allTypesMap) => {
const baseName = member.split('.')[0];
return allTypesMap["CustomField"]?.filter(field => field.startsWith(baseName)) ?? [];
}
});
const allCustomMetadataRecords = createProcessor({
skipCondition: (member) => !member.includes('__mdt'),
targetType: "CustomMetadata",
memberGenerator: async (member, _, allTypesMap) => {
const baseName = member.split('__mdt')[0];
return allTypesMap["CustomMetadata"]?.filter(member => member.startsWith(baseName)) ?? [];
}
});
const allObjectRecordTypes = createProcessor({
separator: '.',
skipCondition: (member) => member.includes("__mdt"),
targetType: "RecordType",
memberGenerator: async (member, _, allTypesMap) => {
const sobject = member.split('.')[0];
return allTypesMap["RecordType"]?.filter(member => member.startsWith(sobject + '.')) ?? [];
}
});
const dashSeparatedObjectToObjectTranslation = createProcessor({
separator: '-',
targetType: "CustomObjectTranslation",
memberGenerator: async (member) => {
const sobject = member.split('-')[0];
return languages.map(languageSuffix => sobject + "-" + languageSuffix);
}
});
const dotSeparatedObjectToObjectTranslation = createProcessor({
separator: '.',
skipCondition: (member) => member.includes("__mdt"),
targetType: "CustomObjectTranslation",
memberGenerator: async (member) => {
const sobject = member.split('.')[0];
return languages.map(languageSuffix => sobject + "-" + languageSuffix);
}
});
const globalTranslations = createProcessor({
targetType: "Translations",
memberGenerator: async () => languages
});
const leadConvertSettings = createProcessor({
skipCondition: (member) => !member.startsWith('Opportunity') && !member.includes('Account') && !member.includes('Contact') && !member.includes('Lead'),
targetType: "LeadConvertSettings",
memberGenerator: async (_, __, allTypesMap) => {
return allTypesMap["LeadConvertSettings"] ?? [];
}
});
const objectTranslations = createProcessor({
targetType: "CustomObjectTranslation",
memberGenerator: async (member) => {
return languages.map(suffix => member + "-" + suffix);
}
});
// Map of metadata types to their processors
if (deltaAction === "modified") {
return {
"CustomField": [allObjectRecordTypes, allCustomMetadataRecords, dotSeparatedObjectToObjectTranslation, leadConvertSettings],
"CustomLabel": globalTranslations,
"CustomMetadata": allCustomFields,
"CustomObject": objectTranslations,
"CustomPageWebLink": globalTranslations,
"CustomTab": globalTranslations,
"Layout": dashSeparatedObjectToObjectTranslation,
"QuickAction": dotSeparatedObjectToObjectTranslation,
"RecordType": dotSeparatedObjectToObjectTranslation,
"ReportType": globalTranslations,
"ValidationRule": dotSeparatedObjectToObjectTranslation,
};
}
else if (deltaAction === "deleted") {
return {
"Flow": globalTranslations,
"CustomApplication": globalTranslations,
"CustomLabel": globalTranslations,
"CustomTab": globalTranslations,
};
}
}
export async function appendPackageModifications(fromCommit, toCommit, sourcePackageFilename, targetPackageFilename) {
const packageFrom = (await getFileAtCommit(fromCommit, sourcePackageFilename)).toString();
const packageTo = (await getFileAtCommit(toCommit, sourcePackageFilename)).toString();
if (packageFrom == packageTo) {
uxLog("log", this, c.grey(c.italic(`Found no changes in ${sourcePackageFilename}`)));
return;
}
uxLog("action", this, c.cyan('[DeltaDeployment] Extending package.xml with manifest changes ...'));
const tmpDir = await createTempDir();
const tempFromFile = path.join(tmpDir, 'packageFrom.xml');
const tempToFile = path.join(tmpDir, 'packageTo.xml');
const tempDiffFile = path.join(tmpDir, 'packageDiff.xml');
await fs.writeFile(tempFromFile, packageFrom);
await fs.writeFile(tempToFile, packageTo);
const diffTypes = await removePackageXmlFilesContent(tempToFile, tempFromFile, { removedOnly: false, outputXmlFile: tempDiffFile });
if (diffTypes.length > 0) {
uxLog("log", this, c.grey(c.italic(`Found some added types in ${sourcePackageFilename}, adding them to final delta manifest.`)));
await appendPackageXmlFilesContent([tempDiffFile, targetPackageFilename], targetPackageFilename);
}
else {
uxLog("log", this, c.grey(c.italic(`Found no added types in ${sourcePackageFilename}`)));
}
fs.removeSync(tmpDir);
}
//# sourceMappingURL=deltaUtils.js.map