UNPKG

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

312 lines • 14 kB
// XML Utils functions import { SfError } from '@salesforce/core'; import c from 'chalk'; import fs from 'fs-extra'; import * as path from 'path'; import * as util from 'util'; import * as xml2js from 'xml2js'; import { sortCrossPlatform, uxLog } from './index.js'; import { getApiVersion } from '../../config/index.js'; export async function parseXmlFile(xmlFile) { const packageXmlString = await fs.readFile(xmlFile, 'utf8'); try { const parsedXml = await xml2js.parseStringPromise(packageXmlString); return parsedXml; } catch (e) { throw new SfError(`Error parsing ${xmlFile}: ${e.message}`); } } export async function writeXmlFile(xmlFile, xmlObject) { const builder = new xml2js.Builder({ renderOpts: { pretty: true, indent: process.env.SFDX_XML_INDENT || ' ', newline: '\n', }, xmldec: { version: '1.0', encoding: 'UTF-8' }, }); const updatedFileContent = builder.buildObject(xmlObject); await fs.ensureDir(path.dirname(xmlFile)); await fs.writeFile(xmlFile, updatedFileContent); } export async function writeXmlFileFormatted(xmlFile, xmlString) { const xmlObject = await xml2js.parseStringPromise(xmlString); await writeXmlFile(xmlFile, xmlObject); } export async function parsePackageXmlFile(packageXmlFile) { const targetOrgPackage = await parseXmlFile(packageXmlFile); const targetOrgContent = {}; if (!targetOrgPackage?.Package?.types) { uxLog(this, c.yellow(`File ${packageXmlFile} doesn't seem to respect package.xml format.`)); } for (const type of targetOrgPackage.Package.types || []) { const mdType = type.name[0]; const members = type.members || []; targetOrgContent[mdType] = members; } return targetOrgContent; } export async function countPackageXmlItems(packageXmlFile) { const packageXmlParsed = await parsePackageXmlFile(packageXmlFile); let counter = 0; for (const type of Object.keys(packageXmlParsed)) { counter += packageXmlParsed[type].length || 0; } return counter; } export async function writePackageXmlFile(packageXmlFile, packageXmlObject) { let packageXmlContent = { Package: { types: [], version: [getApiVersion()] } }; if (fs.existsSync(packageXmlFile)) { packageXmlContent = await parseXmlFile(packageXmlFile); } packageXmlContent.Package.types = Object.keys(packageXmlObject).map((typeKey) => { const type = { members: packageXmlObject[typeKey], name: [typeKey], }; return type; }); await writeXmlFile(packageXmlFile, packageXmlContent); } // Check if a package.xml is empty export async function isPackageXmlEmpty(packageXmlFile, options = { ignoreStandaloneParentItems: false }) { const packageXmlContent = await parseXmlFile(packageXmlFile); if (packageXmlContent && packageXmlContent.Package && packageXmlContent.Package.types && packageXmlContent.Package.types.length > 0) { if (options.ignoreStandaloneParentItems === true) { // Check if only contains SharingRules without SharingOwnerRule if (packageXmlContent.Package.types.length === 1 && packageXmlContent.Package.types[0].name[0] === 'SharingRules') { return true; } // Check if only contains SharingOwnerRule without SharingRules if (packageXmlContent.Package.types.length === 1 && packageXmlContent.Package.types[0].name[0] === 'SharingOwnerRule') { return true; } } // no standalone parent items found package.xml is considered not empty return false; } return true; } // Read package.xml files and build concatenated list of items export async function appendPackageXmlFilesContent(packageXmlFileList, outputXmlFile) { uxLog(this, c.cyan(`Appending ${packageXmlFileList.join(',')} into ${outputXmlFile}...`)); let firstPackageXmlContent = null; let allPackageXmlFilesTypes = {}; // loop on packageXml files for (const packageXmlFile of packageXmlFileList) { const result = await parseXmlFile(packageXmlFile); if (firstPackageXmlContent == null) { firstPackageXmlContent = result; } let packageXmlMetadatasTypeLs; // Get metadata types in current loop packageXml try { packageXmlMetadatasTypeLs = result.Package.types || []; } catch { throw new SfError('Unable to find Package XML element in ' + packageXmlFile); } // Add metadata members in concatenation list of items & store doublings for (const typePkg of packageXmlMetadatasTypeLs) { if (typePkg.name == null) { continue; } const nameKey = typePkg.name[0]; if (allPackageXmlFilesTypes[nameKey] != null && typePkg.members != null) { sortCrossPlatform(allPackageXmlFilesTypes[nameKey] = Array.from(new Set(allPackageXmlFilesTypes[nameKey].concat(typePkg.members)))); } else if (typePkg.members != null) { sortCrossPlatform(allPackageXmlFilesTypes[nameKey] = Array.from(new Set(typePkg.members))); } } } // Sort result allPackageXmlFilesTypes = sortObject(allPackageXmlFilesTypes); // Write output file const appendTypesXml = []; for (const packageXmlType of Object.keys(allPackageXmlFilesTypes)) { appendTypesXml.push({ members: allPackageXmlFilesTypes[packageXmlType], name: packageXmlType }); } firstPackageXmlContent.Package.types = appendTypesXml; await writeXmlFile(outputXmlFile, firstPackageXmlContent); } // Read package.xml files and remove the content of the export async function removePackageXmlFilesContent(packageXmlFile, removePackageXmlFile, { outputXmlFile = '', logFlag = false, removedOnly = false, keepEmptyTypes = false }) { // Read package.xml file to update const parsedPackageXml = await parseXmlFile(packageXmlFile); if (logFlag) { uxLog(this, `Parsed ${packageXmlFile} :\n` + util.inspect(parsedPackageXml, false, null)); } let packageXmlMetadatasTypeLs; // get metadata types in parse result try { packageXmlMetadatasTypeLs = parsedPackageXml.Package.types || []; } catch { throw new SfError('Unable to parse package Xml file ' + packageXmlFile); } // Read package.xml file to use for filtering first file const parsedPackageXmlRemove = await parseXmlFile(removePackageXmlFile); if (logFlag) { uxLog(this, c.grey(`Parsed ${removePackageXmlFile} :\n` + util.inspect(parsedPackageXmlRemove, false, null))); } let packageXmlRemoveMetadatasTypeLs; // get metadata types in parse result try { packageXmlRemoveMetadatasTypeLs = parsedPackageXmlRemove.Package.types || []; } catch { throw new SfError('Unable to parse package Xml file ' + removePackageXmlFile); } // Filter main package.xml file const processedTypes = []; for (const removeType of packageXmlRemoveMetadatasTypeLs) { const removeTypeName = removeType.name[0] || null; if (removeTypeName) { processedTypes.push(removeTypeName); } if (removeTypeName === null) { continue; } const removeTypeMembers = removeType.members || []; const types = packageXmlMetadatasTypeLs.filter((type1) => type1.name[0] === removeTypeName); if (types.length === 0) { continue; } const type = types[0]; let typeMembers = type.members || []; // Manage * case contained in target if (removedOnly === true && typeMembers.includes('*')) { typeMembers = removeTypeMembers; uxLog(this, c.grey(c.italic(`Found wildcard * on type ${c.bold(type.name)}, kept items: ${typeMembers.length}`))); } // Manage * case contained in source else if (removeTypeMembers[0] && removeTypeMembers[0] === '*') { typeMembers = typeMembers.filter(() => checkRemove(false, removedOnly)); uxLog(this, c.grey(c.italic(`Found wildcard * on type ${c.bold(type.name)} which have all been ${removedOnly ? 'kept' : 'removed'}`))); } else { // Filter members typeMembers = typeMembers.filter((member) => checkRemove(!removeTypeMembers.includes(member), removedOnly)); uxLog(this, c.grey(c.italic(`Found type ${c.bold(type.name)}, ${typeMembers.length} items have been ${removedOnly ? 'removed' : 'kept'}`))); } if (typeMembers.length > 0 || keepEmptyTypes === true) { // Update members for type packageXmlMetadatasTypeLs = packageXmlMetadatasTypeLs.map((type1) => { if (type1.name[0] === type.name[0]) { type1.members = typeMembers; } return type1; }); } else { // No more member, do not let empty type packageXmlMetadatasTypeLs = packageXmlMetadatasTypeLs.filter((type1) => { return type1.name[0] !== type.name[0]; }); } } // If removedOnly mode, remove types which were not present in removePackageXml if (removedOnly) { packageXmlMetadatasTypeLs = packageXmlMetadatasTypeLs.filter((type1) => processedTypes.includes(type1.name[0])); } // display in logs if requested if (logFlag) { uxLog(this, 'Package.xml remove results :\n' + util.inspect(packageXmlMetadatasTypeLs, false, null)); } // Write in output file if required if (outputXmlFile) { parsedPackageXml.Package.types = packageXmlMetadatasTypeLs; await writeXmlFile(outputXmlFile, parsedPackageXml); if (logFlag) { uxLog(this, 'Generated package.xml file: ' + outputXmlFile); } } return packageXmlMetadatasTypeLs; } export function sortObject(o) { return Object.keys(o) .sort() .reduce((r, k) => ((r[k] = o[k]), r), {}); } function checkRemove(boolRes, removedOnly = false) { if (removedOnly === true) { return !boolRes; } return boolRes; } export async function applyAllReplacementsDefinitions(allMatchingSourceFiles, referenceStrings, replacementDefinitions) { uxLog(this, c.cyan(`Initializing replacements in files for ${referenceStrings.join(',')}...`)); for (const ref of referenceStrings) { for (const replacementDefinition of replacementDefinitions) { replacementDefinition.refRegexes = replacementDefinition.refRegexes.map((refRegex) => { refRegex.regex = refRegex.regex.replace(new RegExp(`{{REF}}`), ref); return refRegex; }); await applyReplacementDefinition(replacementDefinition, allMatchingSourceFiles, ref); } } } export async function applyReplacementDefinition(replacementDefinition, allMatchingSourceFiles, ref) { for (const sourceFile of allMatchingSourceFiles.filter((file) => replacementDefinition.extensions.some((ext) => file.endsWith(ext)))) { let fileText = await fs.readFile(sourceFile, 'utf8'); let updated = false; // Replacement in all text if (replacementDefinition.replaceMode.includes('all')) { for (const regexReplace of replacementDefinition.refRegexes) { const updatedfileText = fileText.replace(new RegExp(regexReplace.regex, 'gm'), regexReplace.replace); if (updatedfileText !== fileText) { updated = true; fileText = updatedfileText; } } } // Replacement by line let fileLines = fileText.split(/\r?\n/); if (replacementDefinition.replaceMode.includes('line')) { const updatedFileLines = fileLines.map((line) => { const trimLine = line.trim(); if (trimLine.startsWith('/') || trimLine.startsWith('<!--')) { return line; } if ((replacementDefinition.type === 'code' && line.includes(ref)) || (replacementDefinition.type === 'xml' && (line.includes('>' + ref + '<') || line.includes('.' + ref + '<') || line.includes('>' + ref + '.')))) { updated = true; let regexReplaced = false; for (const regexReplace of replacementDefinition.refRegexes) { const updatedLine = line.replace(new RegExp(regexReplace.regex, 'gm'), regexReplace.replace); if (updatedLine !== line) { line = updatedLine; regexReplaced = true; break; } } if (regexReplaced) { return replacementDefinition.type === 'code' ? line + ' // Updated by sfdx-hardis purge-references' : line; } return replacementDefinition.type === 'code' ? '// ' + line + ' // Commented by sfdx-hardis purge-references' : replacementDefinition.type === 'xml' ? '<!-- ' + line + ' Commented by sfdx-hardis purge-references --> ' : line; } return line; }); fileLines = updatedFileLines; } // Apply updates on file if (updated) { const updatedFileText = fileLines.join('\n'); await fs.writeFile(sourceFile, updatedFileText); uxLog(this, c.grey(`- updated ${replacementDefinition.label}: ${sourceFile}`)); } } } //# sourceMappingURL=xmlUtils.js.map