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

390 lines 19.6 kB
import { SfError } from '@salesforce/core'; import c from 'chalk'; import extractZip from 'extract-zip'; import fs from 'fs-extra'; import * as path from 'path'; import sortArray from 'sort-array'; import { elapseEnd, elapseStart, execCommand, execSfdxJson, filterPackageXml, git, isGitRepo, sortCrossPlatform, uxLog, } from '../../common/utils/index.js'; import { getApiVersion } from '../../config/index.js'; import { PACKAGE_ROOT_DIR } from '../../settings.js'; import { getCache, setCache } from '../cache/index.js'; import { buildOrgManifest } from '../utils/deployUtils.js'; import { listMajorOrgs } from '../utils/orgConfigUtils.js'; import { GLOB_IGNORE_PATTERNS, isSfdxProject } from '../utils/projectUtils.js'; import { prompts } from '../utils/prompts.js'; import { parsePackageXmlFile } from '../utils/xmlUtils.js'; import { listMetadataTypes } from './metadataList.js'; import { glob } from 'glob'; class MetadataUtils { // Describe packageXml <=> metadata folder correspondance static listMetadatasNotManagedBySfdx() { return [ 'ApexEmailNotifications', 'AppMenu', 'AppointmentSchedulingPolicy', 'Audience', 'BlacklistedConsumer', 'ConnectedApp', 'CustomIndex', 'ForecastingType', 'IframeWhiteListUrlSettings', 'ManagedContentType', 'NotificationTypeConfig', 'Settings', 'TopicsForObjects', ]; } // Get default org that is currently selected for user static async getCurrentOrg() { const displayOrgCommand = 'sf org display'; const displayResult = await execSfdxJson(displayOrgCommand, this, { fail: false, output: false, }); if (displayResult?.result?.id) { return displayResult.result; } return null; } // List local orgs for user static async listLocalOrgs(type = 'any', options = {}) { const quickListParams = options?.quickOrgList === true ? ' --skip-connection-status' : ''; const orgListCommand = `sf org list${quickListParams}`; let orgListResult = await getCache(orgListCommand, null); if (orgListResult == null) { orgListResult = await execSfdxJson(orgListCommand, this); await setCache(orgListCommand, orgListResult); } // All orgs if (type === 'any') { return orgListResult?.result || []; } // Sandbox else if (type === 'sandbox') { return (orgListResult?.result?.nonScratchOrgs?.filter((org) => { return org.loginUrl.includes('--') || org.loginUrl.includes('test.salesforce.com'); }) || []); } // Sandbox else if (type === 'devSandbox') { const allSandboxes = orgListResult?.result?.nonScratchOrgs?.filter((org) => { return org.loginUrl.includes('--') || org.loginUrl.includes('test.salesforce.com'); }) || []; const majorOrgs = await listMajorOrgs(); const devSandboxes = allSandboxes.filter((org) => { return (majorOrgs.filter((majorOrg) => majorOrg.targetUsername === org.username || (majorOrg.instanceUrl === org.instanceUrl && !majorOrg.instanceUrl.includes('test.salesforce.com'))).length === 0); }); return devSandboxes; } // scratch else if (type === 'scratch') { return (orgListResult?.result?.scratchOrgs?.filter((org) => { return (org.status === 'Active' && (options.devHubUsername && org.devHubUsername !== options.devHubUsername ? false : true)); }) || []); } return []; } // List installed packages on a org static async listInstalledPackages(orgAlias = null, commandThis) { let listCommand = 'sf package installed list'; if (orgAlias != null) { listCommand += ` --target-org ${orgAlias}`; } try { const alreadyInstalled = await execSfdxJson(listCommand, commandThis, { fail: true, output: true, }); return alreadyInstalled?.result || []; } catch (e) { uxLog(this, c.yellow(`Unable to list installed packages: This is probably a @salesforce/cli bug !\n${e.message}\n${e.stack}`)); globalThis.workaroundCliPackages = true; return []; } } // Install package on existing org static async installPackagesOnOrg(packages, orgAlias = null, commandThis = null, context = 'none') { const alreadyInstalled = await MetadataUtils.listInstalledPackages(orgAlias, this); if (globalThis?.workaroundCliPackages === true) { uxLog(commandThis, c.yellow(`Skip packages installation because of a @salesforce/cli bug. Until it is solved, please install packages manually in target org if necessary. Issue tracking: https://github.com/forcedotcom/cli/issues/2426`)); return; } for (const package1 of packages) { if (alreadyInstalled.filter((installedPackage) => package1.SubscriberPackageVersionId === installedPackage.SubscriberPackageVersionId).length === 0) { if (context === 'scratch' && package1.installOnScratchOrgs === false) { uxLog(commandThis, c.cyan(`Skip installation of ${c.green(package1.SubscriberPackageName)} as it is configured to not be installed on scratch orgs`)); continue; } if (context === 'deploy' && package1.installDuringDeployments === false) { uxLog(commandThis, c.cyan(`Skip installation of ${c.green(package1.SubscriberPackageName)} as it is configured to not be installed on scratch orgs`)); continue; } uxLog(commandThis, c.cyan(`Installing package ${c.green(`${c.bold(package1.SubscriberPackageName || '')} - ${c.bold(package1.SubscriberPackageVersionName || '')}`)}...`)); if (package1.SubscriberPackageVersionId == null) { throw new SfError(c.red(`[sfdx-hardis] You must define ${c.bold('SubscriberPackageVersionId')} in .sfdx-hardis.yml (in installedPackages property)`)); } const securityType = package1.SecurityType || 'AdminsOnly'; let packageInstallCommand = 'sf package install' + ` --package ${package1.SubscriberPackageVersionId}` + ' --no-prompt' + ` --security-type ${securityType}` + ' --wait 60' + ' --json ' + (package1.installationkey != null && package1.installationkey != '' ? ` --installationkey ${package1.installationkey}` : ''); if (orgAlias != null) { packageInstallCommand += ` -u ${orgAlias}`; } elapseStart(`Install package ${package1.SubscriberPackageName}`); try { await execCommand(packageInstallCommand, null, { fail: true, output: true, }); } catch (ex) { if (ex.message.includes('Installation key not valid')) { uxLog(this, c.yellow(`${c.bold('Package requiring password')}: Please manually install package ${package1.SubscriberPackageName} in target org using its password, and define 'installDuringDeployments: false' in its .sfdx-hardis.yml reference`)); throw ex; } const ignoredErrors = [ 'Une version plus récente de ce package est installée.', 'A newer version of this package is currently installed.', ]; // If ex.message contains at least one of the ignoredError, don't rethrow exception if (!ignoredErrors.some((msg) => ex.message && ex.message.includes(msg))) { throw ex; } uxLog(this, c.yellow(`${c.bold('This is not a real error')}: A newer version of ${package1.SubscriberPackageName} has been found. You may update installedPackages property in .sfdx-hardis.yml`)); uxLog(this, c.yellow(`You can do that using command ${c.bold('sf hardis:org:retrieve:packageconfig')} in a minor git branch`)); } elapseEnd(`Install package ${package1.SubscriberPackageName}`); } else { uxLog(commandThis, c.cyan(`Skip installation of ${c.green(package1.SubscriberPackageName)} as it is already installed`)); } } } // Retrieve metadatas from a package.xml static async retrieveMetadatas(packageXml, metadataFolder, checkEmpty, filteredMetadatas, options = {}, commandThis, orgUsername, debug) { // Create output folder if not existing await fs.ensureDir(metadataFolder); // Build package.xml for all org await buildOrgManifest(orgUsername, 'package-full.xml'); await fs.copyFile('package-full.xml', 'package.xml'); // Filter managed items if requested if (options.filterManagedItems) { uxLog(commandThis, c.cyan('Filtering managed items from package.Xml manifest...')); // List installed packages & collect managed namespaces let namespaces = []; if (isSfdxProject()) { // Use sfdx command if possible const installedPackages = await this.listInstalledPackages(orgUsername, commandThis); for (const installedPackage of installedPackages) { if (installedPackage?.SubscriberPackageNamespace !== '' && installedPackage?.SubscriberPackageNamespace != null) { namespaces.push(installedPackage.SubscriberPackageNamespace); } } } else { // Get namespace list from package.xml const packageXmlContent = await parsePackageXmlFile('package-full.xml'); namespaces = packageXmlContent['InstalledPackage'] || []; } // Filter package XML to remove identified metadatas const packageXmlToRemove = fs.existsSync('./remove-items-package.xml') ? path.resolve('./remove-items-package.xml') : path.resolve(PACKAGE_ROOT_DIR + '/defaults/remove-items-package.xml'); const removeStandard = options.removeStandard === false ? false : true; const filterNamespaceRes = await filterPackageXml(packageXml, packageXml, { removeNamespaces: namespaces, removeStandard: removeStandard, removeFromPackageXmlFile: packageXmlToRemove, updateApiVersion: getApiVersion(), }); uxLog(commandThis, filterNamespaceRes.message); } // Filter package.xml only using locally defined remove-items-package.xml else if (fs.existsSync('./remove-items-package.xml')) { const filterNamespaceRes = await filterPackageXml(packageXml, packageXml, { removeFromPackageXmlFile: path.resolve('./remove-items-package.xml'), updateApiVersion: getApiVersion(), }); uxLog(commandThis, filterNamespaceRes.message); } // Filter package XML to remove identified metadatas const filterRes = await filterPackageXml(packageXml, packageXml, { removeMetadatas: filteredMetadatas, }); uxLog(commandThis, filterRes.message); // Filter package XML to keep only selected Metadata types if (options.keepMetadataTypes) { const filterRes2 = await filterPackageXml(packageXml, packageXml, { keepMetadataTypes: options.keepMetadataTypes, }); uxLog(commandThis, filterRes2.message); } // Retrieve metadatas if (fs.readdirSync(metadataFolder).length === 0 || checkEmpty === false) { uxLog(commandThis, c.cyan(`Retrieving metadatas in ${c.green(metadataFolder)}...`)); const retrieveCommand = 'sf project retrieve start' + ` --target-metadata-dir ${metadataFolder}` + ` --manifest ${packageXml}` + ` --wait ${process.env.SFDX_RETRIEVE_WAIT_MINUTES || '60'}` + (debug ? ' --verbose' : ''); const retrieveRes = await execSfdxJson(retrieveCommand, this, { output: false, fail: true, debug, }); if (debug) { uxLog(commandThis, retrieveRes); } // Unzip metadatas uxLog(commandThis, c.cyan('Unzipping metadatas...')); await extractZip(path.join(metadataFolder, 'unpackaged.zip'), { dir: metadataFolder, }); await fs.unlink(path.join(metadataFolder, 'unpackaged.zip')); } } // Prompt user to select a list of metadata types static async promptMetadataTypes() { const metadataTypes = sortArray(listMetadataTypes(), { by: ['xmlName'], order: ['asc'] }); const metadataResp = await prompts({ type: 'multiselect', message: c.cyanBright('Please select metadata types'), choices: metadataTypes.map((metadataType) => { return { title: c.cyan(`${metadataType.xmlName || 'no xml name'} (${metadataType.directoryName || 'no dir name'})`), value: metadataType, }; }), }); return metadataResp.value; } // List updated files and reformat them as string static async listChangedFiles() { if (!isGitRepo()) { return []; } const files = (await git().status(['--porcelain'])).files; const filesSorted = files.sort((a, b) => (a.path > b.path ? 1 : -1)); return filesSorted; } // List updated files and reformat them as string static async listChangedOrFromCurrentCommitFiles() { if (!isGitRepo()) { return []; } const changedFiles = await MetadataUtils.listChangedFiles(); const commitDetails = await git().show(['--name-only', '--pretty=format:']); const updatedFiles = commitDetails.trim().split('\n') .filter(file => { return file && !changedFiles.some(changedFile => changedFile.path === file); }) .map((file) => { return { path: file, index: 'x', working_dir: 'x' }; }); const files = [...changedFiles, ...updatedFiles]; const filesSorted = files.sort((a, b) => (a.path > b.path ? 1 : -1)); return filesSorted; } static getMetadataPrettyNames(metadataFilePaths, bold = false) { const metadataList = listMetadataTypes(); const metadataFilePathsHuman = new Map(); for (const fileRaw of metadataFilePaths) { const file = fileRaw.replace(/\\/g, '/').replace('force-app/main/default/', ''); let fileHuman = "" + file; for (const metadataDesc of metadataList) { if (file.includes(metadataDesc.directoryName || "THEREISNOT")) { const splits = file.split(metadataDesc.directoryName + "/"); const endOfPath = splits[1] || splits[0] || ""; const suffix = metadataDesc.suffix ?? "THEREISNOT"; let metadataName = endOfPath.includes("." + suffix + "-meta.xml") ? endOfPath.replace("." + suffix + "-meta.xml", "") : endOfPath.includes("." + suffix) ? endOfPath.replace("." + suffix, "") : endOfPath; if (bold) { metadataName = "*" + metadataName + "*"; } fileHuman = metadataDesc.xmlName + " " + metadataName; continue; } } metadataFilePathsHuman.set(fileRaw, fileHuman); } return metadataFilePathsHuman; } static async findMetaFileFromTypeAndName(packageXmlType, packageXmlName, packageDirectories = []) { // Handle default package directory if not provided as input if (packageDirectories.length === 0) { packageDirectories = [ { fullPath: path.join(process.cwd(), "force-app"), path: "force-app" } ]; } // Find metadata type from packageXmlName const metadataList = listMetadataTypes(); const metadataTypes = metadataList.filter(metadata => metadata.xmlName === packageXmlType); if (metadataTypes.length === 0) { // Strange, we shouldn't get here, or it means listMetadataTypes content is not up to date return null; } const metadataType = metadataTypes[0]; // Look for matching file in sources const globExpressions = [ `**/${metadataType.directoryName}/**/${packageXmlName}.${metadataType.suffix || ""}`, // Works for not-xml files `**/${metadataType.directoryName}/**/${packageXmlName}.${metadataType.suffix || ""}-meta.xml` // Works for all XML files ]; for (const packageDirectory of packageDirectories) { for (const globExpression of globExpressions) { const sourceFiles = await glob(globExpression, { cwd: packageDirectory.fullPath, ignore: GLOB_IGNORE_PATTERNS }); if (sourceFiles.length > 0) { const metaFile = path.join(packageDirectory.path, sourceFiles[0]); return metaFile.replace(/\\/g, "/"); } } } return null; } static async promptFlow() { const flowFiles = await glob("**/*.flow-meta.xml", { ignore: GLOB_IGNORE_PATTERNS }); sortCrossPlatform(flowFiles); const flowSelectRes = await prompts({ type: 'select', message: 'Please select the Flow you want to visually compare', choices: flowFiles.map(flowFile => { return { value: flowFile, title: path.basename(flowFile, ".flow-meta.xml") }; }) }); return flowSelectRes.value.replace(/\\/g, "/"); } static async promptMultipleFlows() { const flowFiles = await glob("**/*.flow-meta.xml", { ignore: GLOB_IGNORE_PATTERNS }); sortCrossPlatform(flowFiles); const flowSelectRes = await prompts({ type: 'multiselect', message: 'Please select the Flows you want to create the documentation', choices: flowFiles.map(flowFile => { return { value: flowFile, title: path.basename(flowFile, ".flow-meta.xml") }; }) }); return flowSelectRes.value.map(flowFile => flowFile.replace(/\\/g, "/")); } } export { MetadataUtils }; //# sourceMappingURL=index.js.map