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
JavaScript
/*
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