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

208 lines (202 loc) • 9.45 kB
/* jscpd:ignore-start */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages, SfError } from '@salesforce/core'; import c from 'chalk'; import fs from 'fs-extra'; import { glob } from 'glob'; import * as path from 'path'; import sortArray from 'sort-array'; import * as xmldom from '@xmldom/xmldom'; import * as xpath from 'xpath'; import { isCI, uxLog } from '../../../../common/utils/index.js'; import { prompts } from '../../../../common/utils/prompts.js'; import { writeXmlFileFormatted } from '../../../../common/utils/xmlUtils.js'; import { getConfig, setConfig } from '../../../../config/index.js'; import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class CleanXml extends SfCommand { static title = 'Clean retrieved empty items in dx sources'; static description = `Remove XML elements using Glob patterns and XPath expressions This can be very useful to avoid to always remove manually the same elements in the same XML file. - **globpattern** can be any glob pattern allowing to identify the XML files to update, for example \`/**/*.flexipage-meta.xml\` - **xpath** can be any xpath following the format \`//ns:PARENT-TAG-NAME//ns:TAG-NAME[contains(text(),'TAG-VALUE')]\`. If an element is found, the whole **PARENT-TAG-NAME** (with its subtree) will be removed. ![How to build cleaning XPath](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/doc-clean-xml.jpg) Note: If globpattern and xpath are not sent, elements defined in property **cleanXmlPatterns** in **.sfdx-hardis.yml** config file will be used `; static examples = [ '$ sf hardis:project:clean:xml', `$ sf hardis:project:clean:xml --globpattern "/**/*.flexipage-meta.xml" --xpath "//ns:flexiPageRegions//ns:name[contains(text(),'dashboardName')]"`, ]; static flags = { folder: Flags.string({ char: 'f', default: 'force-app', description: 'Root folder', }), globpattern: Flags.string({ char: 'p', description: 'Glob pattern to find files to clean. Ex: /**/*.flexipage-meta.xml', dependsOn: ['xpath'], }), xpath: Flags.string({ char: 'x', description: "XPath to use to detect the elements to remove. Ex: //ns:flexiPageRegions//ns:name[contains(text(),'dashboardName')]", dependsOn: ['globpattern'], }), namespace: Flags.string({ char: 'n', default: 'http://soap.sforce.com/2006/04/metadata', description: 'XML Namespace to use', }), debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = true; folder; globPattern; namespace; xpath; debugMode = false; async run() { const { flags } = await this.parse(CleanXml); this.folder = flags.folder || './force-app'; this.globPattern = flags.globpattern; this.xpath = flags.xpath; this.namespace = flags.namespace || 'http://soap.sforce.com/2006/04/metadata'; this.debugMode = flags.debug || false; // Delete standard files when necessary uxLog("log", this, c.grey(`Clean XML elements matching patterns`)); /* jscpd:ignore-end */ const rootFolder = path.resolve(this.folder); const cleanXmlPatterns = await this.buildCleanXmlPatterns(); let counter = 0; const xpathSelect = xpath.useNamespaces({ ns: this.namespace }); // iterate on removePatterns for (const cleanXmlPattern of cleanXmlPatterns) { const findPattern = rootFolder + cleanXmlPattern.globPattern; const matchingXmlFiles = await glob(findPattern, { cwd: process.cwd(), ignore: GLOB_IGNORE_PATTERNS }); // Iterate on matching files for (const xmlFile of matchingXmlFiles) { let updated = false; const xml = await fs.readFile(xmlFile, 'utf8'); const doc = new xmldom.DOMParser().parseFromString(xml, 'text/xml'); // Iterate on xpaths for (const xpathItem of cleanXmlPattern.xpaths) { const nodes = xpathSelect(xpathItem, doc); for (const node of nodes) { await this.removeXPath(xpathItem, doc, node); uxLog("log", this, c.grey(`Removed xpath ${xpathItem} from ${xmlFile}`)); updated = true; counter++; } } if (updated) { const updatedXml = new xmldom.XMLSerializer().serializeToString(doc); await writeXmlFileFormatted(xmlFile, updatedXml); } } } // Summary const msg = `Updated ${c.green(c.bold(counter))} XML files`; uxLog("log", this, c.grey(msg)); // Propose to add in permanent configuration if (this.globPattern && this.xpath) { await this.manageAddToPermanentConfig(this.globPattern, this.xpath); } // Return an object to be displayed with --json return { outputString: msg }; } async buildCleanXmlPatterns() { // Input parameters if (this.globPattern && this.xpath) { uxLog("log", this, c.grey('Using configuration from input arguments...')); return [ { globPattern: this.globPattern, xpaths: [this.xpath], }, ]; } // Stored config uxLog("log", this, c.grey(`Using configuration from property ${c.bold('cleanXmlPatterns')} in .sfdx-hardis.yml config file...`)); const config = await getConfig('branch'); return config.cleanXmlPatterns || []; } async removeXPath(xPathItem, doc, node) { const parentNodeName = this.findRemoveParentNodeName(xPathItem); const parentNode = this.findParentNode(node, parentNodeName); if (parentNode && parentNode.parentNode) { parentNode.parentNode.removeChild(parentNode); } } findRemoveParentNodeName(xpathItem) { const splits = xpathItem.split('//ns:').filter((str) => str !== ''); if (splits[0]) { return splits[0]; } throw new SfError(`[sfdx-hardis] xpath should start with //ns:PARENT-TAG-NAME//ns:`); } findParentNode(node, parentNodeName) { if (node == null) { throw new SfError(`[sfdx-hardis] Parent node named ${parentNodeName} not found`); } if (node.localName === parentNodeName) { return node; } return this.findParentNode(node.parentNode, parentNodeName); } // Propose user to perform such cleaning at each future hardis:work:save command async manageAddToPermanentConfig(globPattern, xpath) { if (!isCI) { const config = await getConfig('project'); let cleanXmlPatterns = config.cleanXmlPatterns || []; const alreadyDefined = cleanXmlPatterns.filter((item) => item.globPattern === globPattern && item.xpaths.includes(xpath)); if (alreadyDefined.length > 0) { return; } // prompt user const addConfigRes = await prompts({ type: 'confirm', message: c.cyanBright(`Do you want to ALWAYS apply removal of xpath ${xpath} from files of pattern ${globPattern} ?`), description: 'Choose whether to save this xpath removal as a permanent cleaning rule', }); if (addConfigRes.value === true) { let updated = false; cleanXmlPatterns = cleanXmlPatterns.map((item) => { // Check if glob pattern is already existing in the config and update it if found if (item.globPattern === globPattern) { item.xpaths.push(xpath); item.xpaths.sort(); updated = true; } return item; }); if (!updated) { // globPattern not existing yet: add it with single xpath cleanXmlPatterns.push({ globPattern: globPattern, xpaths: [xpath], }); } // Update config with sorted new value cleanXmlPatterns = sortArray(cleanXmlPatterns, { by: ['globPattern'], order: ['asc'], }); await setConfig('project', { cleanXmlPatterns: cleanXmlPatterns }); } } } } //# sourceMappingURL=xml.js.map