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

174 lines (164 loc) • 9.99 kB
/* jscpd:ignore-start */ import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages, SfError } from '@salesforce/core'; import c from 'chalk'; import fs from 'fs-extra'; import ora from 'ora'; import * as path from 'path'; import { execCommand, uxLog } from '../../../common/utils/index.js'; import { prompts } from '../../../common/utils/prompts.js'; import { MetadataUtils } from '../../../common/metadata-utils/index.js'; import { glob } from 'glob'; import { GLOB_IGNORE_PATTERNS } from '../../../common/utils/projectUtils.js'; import { applyAllReplacementsDefinitions } from '../../../common/utils/xmlUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class PurgeRef extends SfCommand { static title = 'Purge References'; static description = ` ## Command Behavior **Purges references to specified strings within your Salesforce metadata files before deployment.** This command is a powerful, yet dangerous, tool designed to modify your local Salesforce metadata by removing or altering references to specific strings. It's primarily intended for advanced use cases, such as refactoring a custom field's API name (e.g., changing a Master-Detail relationship to a Lookup) where direct string replacement across many files is necessary. **USE WITH EXTREME CAUTION AND CAREFULLY READ ALL MESSAGES!** Incorrect usage can lead to data loss or metadata corruption. Key functionalities: - **Reference String Input:** You can provide a comma-separated list of strings (e.g., \`Affaire__c,MyField__c\`) that you want to find and modify within your metadata. - **Automatic Related Field Inclusion:** If a custom field API name (ending with \`__c\`) is provided, it automatically includes its relationship name (ending with \`__r\`) in the list of references to purge, ensuring comprehensive cleanup. - **Source Synchronization Check:** Prompts you to confirm if your local sources are up-to-date with the target org, offering to retrieve metadata if needed. - **Targeted File Scan:** Scans \`.cls\`, \`.trigger\`, and \`.xml\` files within your SFDX project to identify occurrences of the specified reference strings. - **Configurable Replacements:** Applies predefined replacement rules based on file type (e.g., Apex classes, XML files) to modify the content where references are found. <details markdown="1"> <summary>Technical explanations</summary> The command's technical implementation involves: - **Interactive Input:** Uses \`prompts\` to get the list of reference strings from the user if not provided via flags. - **Metadata Retrieval:** If the user indicates that local sources are not up-to-date, it executes \`sf project retrieve start\` to fetch the latest metadata from the target org. - **File System Scan:** It uses \`glob\` to efficiently find all relevant source files (\`.cls\`, \`.trigger\`, \`.xml\`) within the project's package directories. - **Content Matching:** Reads the content of each source file and checks for the presence of any of the specified reference strings. The core utility function for replacements is called \`applyAllReplacementsDefinitions\`. It is responsible for iterating through the identified files and applying the defined replacement rules. These rules are structured to target specific patterns (for example, \`,{{REF}},\` or \`{{REF}}[ |=].+\` in Apex code) and replace them with a desired string (often an empty string or a modified version). - **Regular Expressions:** The replacement rules heavily rely on regular expressions (\`regex\`) to precisely match and modify the content. - **User Feedback:** Provides real-time feedback using \`ora\` for spinners and \`uxLog\` for logging messages about the progress and results of the operation. </details> `; static examples = ['$ sf hardis:misc:purge-references']; static flags = { references: Flags.string({ char: 'r', description: 'Comma-separated list of references to find in metadatas', }), 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', }), 'target-org': requiredOrgFlagWithDeprecations, }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = true; /* jscpd:ignore-end */ ignorePatterns = GLOB_IGNORE_PATTERNS; referenceStrings = []; referenceStringsLabel; allMatchingSourceFiles = []; spinnerCustom; async run() { uxLog("warning", this, c.yellow(c.bold(PurgeRef.description))); const { flags } = await this.parse(PurgeRef); // Collect input parameters this.referenceStrings = (flags?.references || '').split(','); if (this.referenceStrings.length == 1 && this.referenceStrings[0] === '') { const refPromptResult = await prompts({ type: 'text', message: 'Please input a comma-separated list of strings you want to purge (example: Affaire__c)', description: 'Enter the reference strings to purge from your metadata files.', placeholder: 'Ex: Affaire__c,MyField__c,CustomObject__c', }); this.referenceStrings = refPromptResult.value.split(','); } if (this.referenceStrings.length == 1 && this.referenceStrings[0] === '') { throw new SfError('You must input at least one string to check for references'); } for (const refString of this.referenceStrings) { if (refString.endsWith('__c') && !this.referenceStrings.includes(refString.replace('__c', '__r'))) { this.referenceStrings.push(refString.replace('__c', '__r')); } } this.referenceStringsLabel = this.referenceStrings.join(','); // Retrieve metadatas if necessary const retrieveNeedRes = await prompts({ type: 'select', message: `Are your local sources up to date with target org ${flags['target-org'].getUsername()}, or do you need to retrieve some of them?`, description: 'Confirm whether your local metadata is synchronized with the target org.', placeholder: 'Select an option', choices: [ { value: true, title: 'My local sfdx sources are up to date with the target org' }, { value: false, title: 'I need to retrieve metadatas ' }, ], }); if (retrieveNeedRes.value === false) { const metadatas = await MetadataUtils.promptMetadataTypes(); const metadataArg = metadatas.map((metadataType) => metadataType.xmlName).join(' '); await execCommand(`sf project retrieve start --ignore-conflicts --metadata ${metadataArg}`, this, { fail: true }); } // Find sources that contain references this.spinnerCustom = ora({ text: `Browsing sources to find references to ${this.referenceStringsLabel}...`, spinner: 'moon', }).start(); const packageDirectories = this.project?.getPackageDirectories() || []; this.allMatchingSourceFiles = []; for (const packageDirectory of packageDirectories) { const sourceFiles = await glob('*/**/*.{cls,trigger,xml}', { ignore: this.ignorePatterns, cwd: packageDirectory.fullPath, }); const matchingSourceFiles = sourceFiles .filter((sourceFile) => { sourceFile = path.join(packageDirectory.path, sourceFile); const fileContent = fs.readFileSync(sourceFile, 'utf8'); return this.referenceStrings.some((refString) => fileContent.includes(refString)); }) .map((sourceFile) => path.join(packageDirectory.path, sourceFile)); this.allMatchingSourceFiles.push(...matchingSourceFiles); } this.spinnerCustom.succeed(`Found ${this.allMatchingSourceFiles.length} sources with references`); this.allMatchingSourceFiles.sort(); uxLog("other", this, 'Matching files:\n' + c.grey(this.allMatchingSourceFiles.join('\n'))); // Handling Apex classes await applyAllReplacementsDefinitions(this.allMatchingSourceFiles, this.referenceStrings, this.getAllReplacements()); return { message: 'Command completed' }; } getAllReplacements() { return [ // Apex { extensions: ['.cls', '.trigger'], label: 'Apex', type: 'code', replaceMode: ['line'], refRegexes: [ // , REF , { regex: `,{{REF}},`, replace: ',' }, { regex: `, {{REF}},`, replace: ',' }, { regex: `,{{REF}} ,`, replace: ',' }, { regex: `, {{REF}} ,`, replace: ',' }, // , REF = xxx , { regex: `,{{REF}}[ |=].+\\,`, replace: ',' }, { regex: `, {{REF}}[ |=].+\\,`, replace: ',' }, { regex: `,{{REF}}[ |=].+\\, `, replace: ',' }, { regex: `, {{REF}}[ |=].+\\ ,`, replace: ',' }, // , REF = xxx ) { regex: `,{{REF}}[ |=].+\\)`, replace: ')' }, { regex: `, {{REF}}[ |=].+\\)`, replace: ')' }, // REF = xxx , { regex: `{{REF}}[ |=].+\\)`, replace: ')' }, ], }, ]; } } //# sourceMappingURL=purge-references.js.map