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

272 lines (271 loc) • 10.5 kB
/* sfdx-hardis is managed in 3 layers, wit hthe following priority - project, stored in /config - branches, stored in /config/branches - user, stored in /config/users getConfig(layer) returns: - project + branches + user if layer is user - project + branches if layer is branch - project if layer is project */ import { SfError } from '@salesforce/core'; import axios from 'axios'; import c from 'chalk'; import { cosmiconfig } from 'cosmiconfig'; import fs from 'fs-extra'; import * as yaml from 'js-yaml'; import * as os from 'os'; import * as path from 'path'; import { getCurrentGitBranch, isCI, isGitRepo, uxLog } from '../common/utils/index.js'; import { prompts } from '../common/utils/prompts.js'; const moduleName = 'sfdx-hardis'; const projectConfigFiles = [ 'package.json', `.${moduleName}.yaml`, `.${moduleName}.yml`, `config/.${moduleName}.yaml`, `config/.${moduleName}.yml`, ]; const username = os.userInfo().username; const userConfigFiles = [`config/user/.${moduleName}.${username}.yaml`, `config/user/.${moduleName}.${username}.yml`]; const REMOTE_CONFIGS = {}; export const CONSTANTS = { DEFAULT_API_VERSION: '65.0', DOC_URL_ROOT: "https://sfdx-hardis.cloudity.com", WEBSITE_URL: "https://cloudity.com", CONTACT_URL: "https://cloudity.com/#form", NOT_IMPACTING_METADATA_TYPES: process.env.NOT_IMPACTING_METADATA_TYPES ?.split(",") .map((item) => item.trim()) .filter((item) => item.length > 0) ?? [ "ActionLinkGroupTemplate", "AnalyticSnapshot", "AppMenu", "Audience", "AuraDefinitionBundle", "Bot", "BotVersion", "BrandingSet", "ContentAsset", "CustomApplication", "CustomApplicationComponent", "CustomLabel", "CustomObjectTranslation", "CustomPageWebLink", "CustomSite", "CustomTab", "CustomValueSetTranslation", "Dashboard", "DashboardFolder", "Document", "EmailTemplate", "ExperienceBundle", "FlexiPage", "GlobalValueSetTranslation", "HomePageComponent", "HomePageLayout", "Layout", "Letterhead", "LightningExperienceTheme", "LightningComponentBundle", "LightningMessageChannel", "ListView", "NavigationMenu", "PathAssistant", "QuickAction", "ReportType", "Report", "ReportFolder", "SiteDotCom", "StandardValueSetTranslation", "StaticResource", "Translations", "WebLink", "CustomHelpMenuSection", "CustomFeedFilter" ] }; export const getApiVersion = (conn = null) => { // globalThis.currentOrgApiVersion is set during authentication check (so not set if --skipauth option is used) return process.env.SFDX_API_VERSION || globalThis.currentOrgApiVersion || (conn ? conn.getApiVersion() || CONSTANTS.DEFAULT_API_VERSION : CONSTANTS.DEFAULT_API_VERSION); }; export const getApiVersionNumber = (conn = null) => { const apiVersion = getApiVersion(conn); return parseFloat(apiVersion); }; async function getBranchConfigFiles() { if (!isGitRepo()) { return []; } const gitBranchFormatted = process.env.CONFIG_BRANCH || (await getCurrentGitBranch({ formatted: true })); const branchConfigFiles = [ `config/branches/.${moduleName}.${gitBranchFormatted}.yaml`, `config/branches/.${moduleName}.${gitBranchFormatted}.yml`, ]; return branchConfigFiles; } export const getConfig = async (layer = 'user') => { const defaultConfig = await loadFromConfigFile(projectConfigFiles); if (layer === 'project') { return defaultConfig; } let branchConfig = await loadFromConfigFile(await getBranchConfigFiles()); branchConfig = Object.assign(defaultConfig, branchConfig); if (layer === 'branch') { return branchConfig; } let userConfig = await loadFromConfigFile(userConfigFiles); userConfig = Object.assign(branchConfig, userConfig); return userConfig; }; // Set data in configuration file export const setConfig = async (layer, propValues) => { if (layer === 'user' && (fs.readdirSync(process.cwd()).length === 0 || !isGitRepo())) { if (process?.argv?.includes('--debug')) { uxLog("log", this, c.grey('Skipping update of user config file because the current directory is not a Salesforce project.')); } return; } const configSearchPlaces = layer === 'project' ? projectConfigFiles : layer === 'user' ? userConfigFiles : layer === 'branch' ? await getBranchConfigFiles() : []; return await setInConfigFile(configSearchPlaces, propValues); }; // Load configuration from file async function loadFromConfigFile(searchPlaces) { const configExplorer = await cosmiconfig(moduleName, { searchPlaces, }).search(); let config = configExplorer != null ? configExplorer.config : {}; if (config.extends) { const remoteConfig = await loadFromRemoteConfigFile(config.extends); config = Object.assign(remoteConfig, config); } return config; } async function loadFromRemoteConfigFile(url) { if (REMOTE_CONFIGS[url]) { return REMOTE_CONFIGS[url]; } const remoteConfigResp = await axios.get(url); if (remoteConfigResp.status !== 200) { throw new SfError('[sfdx-hardis] Unable to read remote configuration file at ' + url + '\n' + JSON.stringify(remoteConfigResp)); } const remoteConfig = yaml.load(remoteConfigResp.data); REMOTE_CONFIGS[url] = remoteConfig; return remoteConfig; } // Update configuration file export async function setInConfigFile(searchPlaces, propValues, configFile = '') { let explorer; if (configFile === '') { explorer = cosmiconfig(moduleName, { searchPlaces }); const configExplorer = await explorer.search(); configFile = configExplorer != null ? configExplorer.filepath : searchPlaces.slice(-1)[0]; } let doc = {}; if (fs.existsSync(configFile)) { doc = yaml.load(fs.readFileSync(configFile, 'utf-8')); } doc = Object.assign(doc, propValues); await fs.ensureDir(path.dirname(configFile)); await fs.writeFile(configFile, yaml.dump(doc)); if (explorer) { explorer.clearCaches(); } if (!isCI) { uxLog("other", this, c.magentaBright(`Updated config file ${c.bold(configFile)} with values: \n${JSON.stringify(propValues, null, 2)}`)); } return configFile; } // Check configuration of project so it works with sfdx-hardis export const checkConfig = async (options) => { // Skip hooks from other commands than hardis:scratch commands const commandId = options?.Command?.id || options?.id || ''; if (!commandId.startsWith('hardis')) { return; } let devHubAliasOk = false; // Check projectName is set. If not, request user to input it if (options.Command && (options.Command.requiresProject === true || options.Command.supportsDevhubUsername === true || options?.flags?.devhub === true || options.devHub === true)) { const configProject = await getConfig('project'); let projectName = process.env.PROJECT_NAME || configProject.projectName; devHubAliasOk = (process.env.DEVHUB_ALIAS || configProject.devHubAlias) != null; // If not found, prompt user project name and store it in user config file if (projectName == null) { projectName = promptForProjectName(); await setConfig('project', { projectName, devHubAlias: `DevHub_${projectName}`, }); devHubAliasOk = true; } } // Set DevHub username if not set if (devHubAliasOk === false && options.Command && options.Command.supportsDevhubUsername === true) { const configProject = await getConfig('project'); const devHubAlias = process.env.DEVHUB_ALIAS || configProject.devHubAlias; if (devHubAlias == null) { await setConfig('project', { devHubAlias: `DevHub_${configProject.projectName}`, }); } } }; export async function getReportDirectory() { const configProject = await getConfig('project'); const defaultReportDir = path.join(process.cwd(), 'hardis-report'); const reportDir = configProject.reportDirectory || defaultReportDir; await fs.ensureDir(reportDir); return reportDir; } export function getEnvVar(envVarName) { const varValue = process.env[envVarName] || null; // Avoid Azure cases that sends the expression as string if variable not defined if (varValue && varValue.includes(`(${envVarName}`)) { return null; } return varValue; } export function getEnvVarList(envVarName, separator = ',') { const varValue = getEnvVar(envVarName); if (varValue) { return varValue.split(separator).map((item) => item.trim()); } return null; } export async function promptForProjectName() { const projectRes = await prompts({ type: 'text', name: 'projectName', message: 'What is the name of your project ?', description: 'Used to generate environment variables and configuration files for your Salesforce project', placeholder: 'Ex: MyClient', }); const userProjectName = projectRes.projectName + ''; let projectName = projectRes.projectName.toLowerCase().replace(' ', '_'); // Make sure that projectName is compliant with the format of an environment variable projectName = projectName.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[^a-zA-Z_]+/, ''); if (projectName !== userProjectName) { uxLog("warning", this, c.yellow(`Project name has been changed to ${projectName} because it must be compliant with the format of an environment variable.`)); const promptResp = await prompts({ type: 'confirm', message: `Are you ok with updated project name "${projectName}" ?`, description: 'Confirms the use of the sanitized project name which must be compliant with environment variable format', }); if (promptResp.value === true) { return projectName; } return promptForProjectName(); } return projectName; } //# sourceMappingURL=index.js.map