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
JavaScript
/* 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.

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