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
210 lines (207 loc) • 10.9 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 sortArray from 'sort-array';
import { catchMatches, generateReports, uxLog, uxLogTable } from '../../../../common/utils/index.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
import { GLOB_IGNORE_PATTERNS } from '../../../../common/utils/projectUtils.js';
export default class CallInCallOut extends SfCommand {
static title = 'Audit Metadatas API Version';
static description = `This command identifies metadata with an apiVersion lower than the value specified in the --minimumapiversion parameter.
It can also update the apiVersion to a specific value:
- When --fix parameter is provided (updates to minimumapiversion)
- When --newapiversion is specified (updates to that version)
Example to handle [ApexClass / Trigger & ApexPage mandatory version upgrade](https://help.salesforce.com/s/articleView?id=sf.admin_locales_update_api.htm&type=5) :
\`sf hardis:project:audit:apiversion --metadatatype ApexClass,ApexTrigger,ApexPage --minimumapiversion 45 --newapiversion 50\`
`;
static examples = [
'$ sf hardis:project:audit:apiversion',
'$ sf hardis:project:audit:apiversion --metadatatype ApexClass,ApexTrigger,ApexPage --minimumapiversion 45',
'$ sf hardis:project:audit:apiversion --metadatatype ApexClass,ApexTrigger,ApexPage --minimumapiversion 45 --fix',
'$ sf hardis:project:audit:apiversion --metadatatype ApexClass,ApexTrigger,ApexPage --minimumapiversion 45 --newapiversion 50'
];
// public static args = [{name: 'file'}];
static flags = {
minimumapiversion: Flags.integer({
char: 'm',
default: 20,
description: messages.getMessage('minimumApiVersion'),
}),
failiferror: Flags.boolean({
char: 'f',
default: false,
description: messages.getMessage('failIfError'),
}),
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',
}),
metadatatype: Flags.string({
description: 'Metadata Types to fix. Comma separated. Supported Metadata types: ApexClass, ApexTrigger, ApexPage'
}),
fix: Flags.boolean({
// can't use "f", already use for failiferror
default: false,
description: 'Automatically update API versions in files that are below the minimum version threshold to match the minimum version',
}),
newapiversion: Flags.integer({
char: 'n',
description: 'Define an API version value to apply when updating files',
}),
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = true;
matchResults = [];
async run() {
const { flags } = await this.parse(CallInCallOut);
const minimumApiVersion = flags.minimumapiversion || false;
const failIfError = flags.failiferror || false;
const newApiVersion = flags.newapiversion;
// Apply fixes if either fix flag is present or a new API version is specified
const shouldFix = flags.fix || (newApiVersion !== undefined);
const fixApiVersion = newApiVersion || minimumApiVersion;
const metadataType = flags.metadatatype || '';
const fixAllowedExtensions = {
"ApexClass": "cls",
"ApexTrigger": "trigger",
"ApexPage": "page",
};
const fixAllowedMetadataTypes = Object.keys(fixAllowedExtensions);
const fixTargetedMetadataTypes = metadataType.trim() === '' ? [] : (metadataType || '').replace(/\s+/g, '').split(',');
const fixInvalidMetadataTypes = fixTargetedMetadataTypes.filter(value => !fixAllowedMetadataTypes.includes(value));
if (fixTargetedMetadataTypes.length > 0 && fixInvalidMetadataTypes.length > 0 && shouldFix) {
uxLog("warning", this, c.yellow(`[sfdx-hardis] WARNING: --fix Invalid Metadata Type(s) found: ${c.bold(fixInvalidMetadataTypes.join(', '))}. Only ${c.bold(fixAllowedMetadataTypes.join(', '))} Metadata Types are allowed for the fix.`));
if (failIfError) {
throw new SfError(c.red(`[sfdx-hardis] WARNING: --fix Invalid Metadata Type(s) found: ${c.bold(fixInvalidMetadataTypes.join(', '))}. Only ${c.bold(fixAllowedMetadataTypes.join(', '))} Metadata Types are allowed for the fix.`));
}
}
// Metadata Type Extensions to fix
const fixTargetedMetadataTypesExtensions = fixTargetedMetadataTypes.map(type => fixAllowedExtensions[type]);
const fixTargetedMetadataTypesPattern = new RegExp(`\\.(${fixTargetedMetadataTypesExtensions.join('|')})-meta\\.xml$`);
let pattern = '**/*.xml';
if (fixTargetedMetadataTypes.length > 0) {
// Check if there's only one extension type
if (fixTargetedMetadataTypesExtensions.length === 1) {
pattern = `**/*.${fixTargetedMetadataTypesExtensions[0]}-meta.xml`;
}
else {
pattern = `**/*.{${fixTargetedMetadataTypesExtensions.join(',')}}-meta.xml`;
}
}
const catchers = [
{
type: 'apiVersion',
subType: '',
regex: /<apiVersion>(.*?)<\/apiVersion>/gims,
detail: [{ name: 'apiVersion', regex: /<apiVersion>(.*?)<\/apiVersion>/gims }],
fixed: false,
},
];
const xmlFiles = await glob(pattern, { ignore: GLOB_IGNORE_PATTERNS });
this.matchResults = [];
uxLog("other", this, `Browsing ${xmlFiles.length} files`);
// Loop in files
for (const file of xmlFiles) {
try {
const fileText = await fs.readFile(file, 'utf8');
// Update ApiVersion on file
let fixed = false;
if (shouldFix && fixTargetedMetadataTypes.length > 0 && fixTargetedMetadataTypesPattern.test(file)) {
const apiVersionMatch = fileText.match(/<apiVersion>(.*?)<\/apiVersion>/);
if (apiVersionMatch && apiVersionMatch[1]) {
const currentApiVersion = parseFloat(apiVersionMatch[1]);
if (currentApiVersion < minimumApiVersion) {
const updatedContent = fileText.replace(/<apiVersion>(.*?)<\/apiVersion>/, `<apiVersion>${fixApiVersion}.0</apiVersion>`);
await fs.promises.writeFile(file, updatedContent, 'utf-8');
fixed = true;
uxLog("other", this, `Updated apiVersion in file: ${file} from ${currentApiVersion}.0 to ${fixApiVersion}.0`);
}
}
}
// Loop on criteria to find matches in this file
for (const catcher of catchers) {
const catcherMatchResults = await catchMatches(catcher, file, fileText, this);
// Add the "fixed" flag
const enrichedResults = catcherMatchResults.map(result => ({
...result,
fixed,
}));
this.matchResults.push(...enrichedResults);
}
}
catch (error) {
if (error instanceof Error) {
uxLog("warning", this, c.yellow(`Error processing file ${file}: ${error.message}`));
}
else {
uxLog("warning", this, c.yellow(`Error processing file ${file}: ${String(error)}`));
}
}
}
// Format result
const result = this.matchResults.map((item) => {
return {
type: item.type,
fileName: item.fileName,
nameSpace: item.fileName.includes('__') ? item.fileName.split('__')[0] : 'Custom',
apiVersion: parseFloat(item.detail['apiVersion']),
valid: parseFloat(item.detail['apiVersion']) >= (minimumApiVersion || 100) ? 'yes' : 'no',
fixed: item.fixed ? 'yes' : 'no',
};
});
// Sort array
const resultSorted = sortArray(result, {
by: ['type', 'subType', 'fileName'],
order: ['asc', 'asc', 'asc'],
});
// Display as table
uxLog("action", this, c.cyan(`Found ${c.bold(resultSorted.length)} metadata files with API Version.`));
const resultsLight = JSON.parse(JSON.stringify(resultSorted));
uxLogTable(this, resultsLight.map((item) => {
delete item.detail;
return item;
}));
const numberOfInvalid = result.filter((res) => res.valid === 'no').length;
const numberOfValid = result.length - numberOfInvalid;
if (numberOfInvalid > 0) {
uxLog("warning", this, c.yellow(`WARNING: Your sources contain ${c.bold(numberOfInvalid)} metadata files with API Version lesser than ${c.bold(minimumApiVersion)}`));
if (failIfError) {
throw new SfError(c.red(`${c.bold(numberOfInvalid)} metadata files with wrong API version detected`));
}
}
else {
uxLog("success", this, c.green(`SUCCESS: Your sources contain ${c.bold(numberOfValid)} metadata files with API Version superior to ${c.bold(minimumApiVersion)}`));
}
// Generate output files
const columns = [
{ key: 'type', header: 'IN/OUT' },
{ key: 'fileName', header: 'Apex' },
{ key: 'nameSpace', header: 'Namespace' },
{ key: 'apiVersion', header: 'API Version' },
{ key: 'valid', header: `Valid ( > ${minimumApiVersion} )` },
{ key: 'fixed', header: 'Fixed' },
];
const reportFiles = await generateReports(resultSorted, columns, this, {
logFileName: 'api-versions',
logLabel: 'Extract and Fix Metadata Api Versions',
});
// Return an object to be displayed with --json
return {
outputString: 'Processed apiVersion audit',
result: resultSorted,
reportFiles,
};
}
}
//# sourceMappingURL=apiversion.js.map