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 (207 loc) • 9.22 kB
JavaScript
/* jscpd:ignore-start */
// External Libraries
import { glob } from 'glob';
import fs from 'fs-extra';
import * as xml2js from 'xml2js';
import * as path from 'path';
// Salesforce Specific
import { SfCommand, Flags, optionalOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
// Project Specific Utilities
import { NotifProvider } from '../../../common/notifProvider/index.js';
import { getNotificationButtons, getBranchMarkdown, getSeverityIcon } from '../../../common/utils/notifUtils.js';
import { generateCsvFile, generateReportPath } from '../../../common/utils/filesUtils.js';
import { uxLog } from '../../../common/utils/index.js';
import { GLOB_IGNORE_PATTERNS } from '../../../common/utils/projectUtils.js';
import { CONSTANTS } from '../../../config/index.js';
import { setConnectionVariables } from '../../../common/utils/orgUtils.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
/* jscpd:ignore-end */
export default class UnusedMetadatas extends SfCommand {
static title = 'check unused labels and custom permissions';
static description = `Check if elements (custom labels and custom permissions) are used in the project
This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/salesforce-monitoring-unused-metadata/) and can output Grafana, Slack and MsTeams Notifications.
`;
static examples = ['$ sf hardis:lint:unusedmetadatas'];
/* jscpd:ignore-start */
static flags = {
debug: Flags.boolean({
char: 'd',
default: false,
description: messages.getMessage('debugMode'),
}),
outputfile: Flags.string({
char: 'f',
description: 'Force the path and name of output report file. Must end with .csv',
}),
websocket: Flags.string({
description: messages.getMessage('websocket'),
}),
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
'target-org': optionalOrgFlagWithDeprecations,
};
/* jscpd:ignore-end */
unusedData = [];
outputFile;
outputFilesRes = {};
static supportsDevhubUsername = false;
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = true;
ignorePatterns = GLOB_IGNORE_PATTERNS;
projectFiles;
labelFilePattern = '**/CustomLabels.labels-meta.xml';
customPermissionFilePattern = '**/customPermissions/*.xml';
async run() {
const { flags } = await this.parse(UnusedMetadatas);
await this.setProjectFiles();
const unusedLabels = await this.verifyLabels();
const unusedCustomPermissions = await this.verifyCustomPermissions();
// Build notification
const branchMd = await getBranchMarkdown();
const notifButtons = await getNotificationButtons();
let notifSeverity = 'log';
let notifText = `No unused metadatas has been detected in ${branchMd}`;
const attachments = [];
if (unusedLabels.length > 0) {
attachments.push({
text: `*Unused Labels*\n${unusedLabels.map((label) => `• ${label.name}`).join('\n')}`,
});
}
if (unusedCustomPermissions.length > 0) {
attachments.push({
text: `*Unused Custom Permissions*\n${unusedCustomPermissions
.map((permission) => `• ${permission.name}`)
.join('\n')}`,
});
}
if (unusedLabels.length > 0 || unusedCustomPermissions.length > 0) {
notifSeverity = 'warning';
notifText = `${this.unusedData.length} unused metadatas have been detected in ${branchMd}`;
await this.buildCsvFile(unusedLabels, unusedCustomPermissions);
}
else {
uxLog(this, 'No unused labels or custom permissions detected.');
}
// Post notification
await setConnectionVariables(flags['target-org']?.getConnection()); // Required for some notifications providers like Email
await NotifProvider.postNotifications({
type: 'UNUSED_METADATAS',
text: notifText,
attachments: attachments,
buttons: notifButtons,
severity: notifSeverity,
sideImage: 'flow',
logElements: this.unusedData,
data: { metric: this.unusedData.length },
metrics: {
MetadatasNotUsed: this.unusedData.length,
},
});
return {};
}
/**
* @description Verify if custom labels are used in the project
* @returns
*/
async verifyLabels() {
const labelFiles = await glob(this.labelFilePattern, { ignore: this.ignorePatterns });
const labelFilePath = labelFiles[0];
if (!labelFilePath) {
console.warn('No label file found.');
return [];
}
return new Promise((resolve, reject) => {
fs.readFile(labelFilePath, 'utf-8', (errorReadingFile, data) => {
if (errorReadingFile) {
reject(errorReadingFile);
return;
}
xml2js.parseString(data, (errorParseString, result) => {
if (errorParseString) {
reject(errorParseString);
return;
}
const severityIconInfo = getSeverityIcon('info');
const labelsArray = result.CustomLabels.labels.map((label) => label.fullName[0]);
const unusedLabels = labelsArray
.filter((label) => {
const labelLower = `label.${label.toLowerCase()}`;
const cLower = `c.${label.toLowerCase()}`;
const auraPattern = `{!$Label.c.${label.toLowerCase()}}`;
return !this.projectFiles.some((filePath) => {
const fileContent = fs.readFileSync(filePath, 'utf-8').toLowerCase();
return (fileContent.includes(labelLower) || fileContent.includes(cLower) || fileContent.includes(auraPattern));
});
})
.map((label) => {
return {
name: label,
severity: 'info',
severityIcon: severityIconInfo,
};
});
resolve(unusedLabels);
});
});
});
}
/**
* @description Verify if custom permissions are used in the project
* @returns
*/
async verifyCustomPermissions() {
const foundLabels = new Map();
const customPermissionFiles = await glob(this.customPermissionFilePattern, {
ignore: this.ignorePatterns,
});
if (!customPermissionFiles) {
console.warn('No custom permission file found.');
return [];
}
for (const file of customPermissionFiles) {
const fileData = await fs.readFile(file, 'utf-8');
const fileName = path.basename(file, '.customPermission-meta.xml');
let label = '';
xml2js.parseString(fileData, (error, result) => {
if (error) {
console.error(`Error parsing XML: ${error}`);
return;
}
label = result.CustomPermission.label[0];
});
for (const filePath of this.projectFiles) {
const fileContent = fs.readFileSync(filePath, 'utf-8');
if (fileContent.includes(fileName) || fileContent.includes(label)) {
const currentCount = foundLabels.get(fileName) || 0;
foundLabels.set(fileName, currentCount + 1);
}
}
}
const severityIconInfo = getSeverityIcon('info');
const result = [...foundLabels.keys()]
.filter((key) => (foundLabels.get(key) || 0) < 2)
.map((name) => {
return {
name: name,
severity: 'info',
severityIcon: severityIconInfo,
};
});
return result;
}
async setProjectFiles() {
this.projectFiles = await glob('**/*.{cls,trigger,js,html,xml,cmp,email,page}', { ignore: this.ignorePatterns });
}
async buildCsvFile(unusedLabels, unusedCustomPermissions) {
this.outputFile = await generateReportPath('lint-unusedmetadatas', this.outputFile);
this.unusedData = [
...unusedLabels.map((label) => ({ type: 'Label', name: label })),
...unusedCustomPermissions.map((permission) => ({ type: 'Custom Permission', name: permission })),
];
this.outputFilesRes = await generateCsvFile(this.unusedData, this.outputFile);
}
}
//# sourceMappingURL=unusedmetadatas.js.map