@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
356 lines • 17.7 kB
JavaScript
"use strict";
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigAggregator = void 0;
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const messages_1 = require("../messages");
const lifecycleEvents_1 = require("../lifecycleEvents");
const envVars_1 = require("./envVars");
const config_1 = require("./config");
;
const messages = new messages_1.Messages('@salesforce/core', 'config', new Map([["unknownConfigKey", "Unknown config name: %s."], ["deprecatedConfigKey", "Deprecated config name: %s. Please use %s instead."], ["invalidWrite", "The writeSync method is not allowed on SfdxConfig. Use the async write method instead."], ["invalidConfigValue", "Invalid config value: %s."], ["invalidInstanceUrl", "Specify a valid Salesforce instance URL."], ["invalidApiVersion", "Specify a valid Salesforce API version, for example, 42.0."], ["invalidCustomOrgMetadataTemplates", "Specify a valid repository URL or directory for the custom org metadata templates."], ["invalidIsvDebuggerSid", "Specify a valid Debugger SID."], ["invalidIsvDebuggerUrl", "Specify a valid Debugger URL."], ["invalidNumberConfigValue", "Specify a valid positive integer, for example, 150000."], ["invalidBooleanConfigValue", "The config value can only be set to true or false."], ["invalidProjectWorkspace", "This directory does not contain a valid Salesforce DX project."], ["schemaValidationError", "The config file \"%s\" is not schema valid.\nDue to: %s"], ["schemaValidationError.actions", ["Fix the invalid entries at %s."]], ["missingDefaultPath", "In sfdx-project.json, be sure to specify which package directory (path) is the default. Example: `[{ \"path\": \"packageDirectory1\", \"default\": true }, { \"path\": \"packageDirectory2\" }]`"], ["missingPackageDirectory", "The path \"%s\", specified in sfdx-project.json, does not exist. Be sure this directory is included in your project root."], ["invalidPackageDirectory", "The path \"%s\", specified in sfdx-project.json, must be indicated as a relative path to the project root."], ["multipleDefaultPaths", "In sfdx-project.json, indicate only one package directory (path) as the default."], ["singleNonDefaultPackage", "The sfdx-project.json file must include one, and only one, default package directory (path). Because your sfdx-project.json file contains only one package directory, it must be the default. Remove the `\"default\": false` key and try again."], ["target-org", "Username or alias of the org that all commands run against by default. (sf only)"], ["target-dev-hub", "Username or alias of your default Dev Hub org. (sf only)"], ["defaultUsername", "Username or alias of the org that all commands run against by default. (sfdx only)"], ["defaultDevHubUsername", "Username or alias of your default Dev Hub org. (sfdx only)"], ["isvDebuggerSid", "ISV debugger SID (sfdx only)"], ["isvDebuggerUrl", "ISV debugger URL (sfdx only)"], ["org-isv-debugger-sid", "ISV debugger SID."], ["org-isv-debugger-url", "ISV debugger URL."], ["apiVersion", "API version of your project. Default: API version of your Dev Hub org. (sfdx only)"], ["org-api-version", "API version of your project. Default: API version of your Dev Hub org."], ["disableTelemetry", "Disables the collection of usage and user environment information, etc. Default: false. (sfdx only)"], ["disable-telemetry", "Disables the collection of usage and user environment information, etc. Default: false."], ["maxQueryLimit", "Maximum number of Salesforce records returned by a CLI command. Default: 10,000. (sfdx only)"], ["org-max-query-limit", "Maximum number of Salesforce records returned by a CLI command. Default: 10,000."], ["restDeploy", "Whether deployments use the Metadata REST API (true) or SOAP API (false, default value). (sfdx only)"], ["instanceUrl", "URL of the Salesforce instance hosting your org. Default: https://login.salesforce.com. (sfdx only)"], ["org-instance-url", "URL of the Salesforce instance hosting your org. Default: https://login.salesforce.com."], ["customOrgMetadataTemplates", "A valid repository URL or directory for the custom org metadata templates."], ["org-custom-metadata-templates", "A valid repository URL or directory for the custom org metadata templates."], ["org-capitalize-record-types", "Whether record types are capitalized on scratch org creation."], ["invalidId", "The given id %s is not a valid 15 or 18 character Salesforce ID."]]));
/**
* Aggregate global and local project config files, as well as environment variables for
* `config.json`. The resolution happens in the following bottom-up order:
*
* 1. Environment variables (`SF_LOG_LEVEL`)
* 1. Workspace settings (`<workspace-root>/.sf/config.json`)
* 1. Global settings (`$HOME/.sf/config.json`)
*
* Use {@link ConfigAggregator.create} to instantiate the aggregator.
*
* ```
* const aggregator = await ConfigAggregator.create();
* console.log(aggregator.getPropertyValue('target-org'));
* ```
*/
class ConfigAggregator extends kit_1.AsyncOptionalCreatable {
static instance;
static encrypted = true;
// Initialized in loadProperties
allowedProperties;
localConfig;
globalConfig;
envVars = {};
/**
* **Do not directly construct instances of this class -- use {@link ConfigAggregator.create} instead.**
*
* @ignore
*/
constructor(options) {
super(options ?? {});
// Don't throw an project error with the aggregator, since it should resolve to global if
// there is no project.
try {
this.localConfig = new config_1.Config(config_1.Config.getDefaultOptions(false));
}
catch (err) {
if (err.name !== 'InvalidProjectWorkspaceError') {
throw err;
}
}
this.globalConfig = new config_1.Config(config_1.Config.getDefaultOptions(true));
this.setAllowedProperties(config_1.Config.getAllowedProperties());
}
get config() {
return this.resolveProperties(this.globalConfig.getContents(), this.localConfig?.getContents());
}
// Use typing from AsyncOptionalCreatable to support extending ConfigAggregator.
// We really don't want ConfigAggregator extended but typescript doesn't support a final.
static async create(options) {
let config = ConfigAggregator.instance;
if (!config) {
config = ConfigAggregator.instance = new this(options);
await config.init();
}
if (ConfigAggregator.encrypted) {
await config.loadProperties();
}
if (options?.customConfigMeta) {
config_1.Config.addAllowedProperties(options.customConfigMeta);
}
return ConfigAggregator.instance;
}
/**
* Get the info for a given key. If the ConfigAggregator was not asynchronously created OR
* the {@link ConfigAggregator.reload} was not called, the config value may be encrypted.
*
* @param key The config key.
*/
static getValue(key) {
return this.getInstance().getInfo(key);
}
/**
* Get the static ConfigAggregator instance. If one doesn't exist, one will be created with
* the **encrypted** config values. Encrypted config values need to be resolved
* asynchronously by calling {@link ConfigAggregator.reload}
*/
// Use typing from AsyncOptionalCreatable to support extending ConfigAggregator.
// We really don't want ConfigAggregator extended but typescript doesn't support a final.
static getInstance() {
if (!ConfigAggregator.instance) {
ConfigAggregator.instance = new this();
ConfigAggregator.instance.loadPropertiesSync();
}
return ConfigAggregator.instance;
}
/**
* Initialize this instances async dependencies.
*/
async init() {
await this.loadProperties();
}
/**
* Get a resolved config property.
* If you use a deprecated property, a warning will be emitted and it will attempt to resolve the new property's value
*
* **Throws** *{@link SfError}{ name: 'UnknownConfigKeyError' }* An attempt to get a property that's not supported.
*
* @param key The key of the property.
*/
getPropertyValue(key) {
const match = this.getAllowedProperties().find((element) => key === element.key);
if (match?.deprecated && match.newKey) {
void lifecycleEvents_1.Lifecycle.getInstance().emitWarning(messages.getMessage('deprecatedConfigKey', [key, match.newKey]));
const newKeyMatch = this.getAllowedProperties().find((element) => match.newKey === element.key);
if (newKeyMatch) {
return this.getConfig()[newKeyMatch.key] ?? this.getConfig()[match.key];
}
}
if (this.getAllowedProperties().some((element) => key === element.key || key === element.newKey)) {
return this.getConfig()[key];
}
else {
throw messages.createError('unknownConfigKey', [key]);
}
}
/**
* Get a resolved config property meta.
* If the property is deprecated, it will return the new key's meta, if it exists, with a deprecation warning
*
* **Throws** *{@link SfError}{ name: 'UnknownConfigKeyError' }* An attempt to get a property that's not supported.
*
* @param key The key of the property.
*/
getPropertyMeta(key) {
const match = this.getAllowedProperties().find((element) => key === element.key);
if (match) {
if (match.deprecated && match.newKey) {
void lifecycleEvents_1.Lifecycle.getInstance().emitWarning(messages.getMessage('deprecatedConfigKey', [key, match.newKey]));
const newKeyMatch = this.getAllowedProperties().find((element) => key === element.newKey);
if (newKeyMatch) {
return newKeyMatch ?? match;
}
}
return match;
}
const matchFromNewKey = this.getAllowedProperties().find((element) => key === element.newKey);
if (matchFromNewKey) {
return matchFromNewKey;
}
throw messages.createError('unknownConfigKey', [key]);
}
/**
* Get a resolved config property.
* If a property is deprecated, it will try to use the the new key, if there is a config there.
*
* @param key The key of the property.
* @param throwOnDeprecation True, if you want an error throw when reading a deprecated config
*/
getInfo(key, throwOnDeprecation = false) {
const meta = this.getPropertyMeta(key);
if (meta.deprecated && meta.newKey) {
if (throwOnDeprecation) {
throw messages.createError('deprecatedConfigKey', [key, meta.newKey]);
}
// we don't need to emit a deprecatedConfigKey warning here because getPropertyMeta does that
}
const location = meta.newKey ? this.getLocation(meta.newKey) : this.getLocation(key);
return {
key: meta.newKey ?? key,
location,
value: this.getPropertyValue(meta.newKey ?? key),
path: this.getPath(meta.newKey ?? key),
isLocal: () => location === "Local" /* ConfigAggregator.Location.LOCAL */,
isGlobal: () => location === "Global" /* ConfigAggregator.Location.GLOBAL */,
isEnvVar: () => location === "Environment" /* ConfigAggregator.Location.ENVIRONMENT */,
deprecated: meta.deprecated ?? false,
};
}
/**
* Gets a resolved config property location.
*
* For example, `getLocation('logLevel')` will return:
* 1. `Location.GLOBAL` if resolved to an environment variable.
* 1. `Location.LOCAL` if resolved to local project config.
* 1. `Location.ENVIRONMENT` if resolved to the global config.
*
* @param key The key of the property.
*/
getLocation(key) {
// envs populate old and new automatically
if (this.envVars[key] != null) {
return "Environment" /* ConfigAggregator.Location.ENVIRONMENT */;
}
if (this.localConfig?.get(key)) {
return "Local" /* ConfigAggregator.Location.LOCAL */;
}
if (this.globalConfig?.get(key)) {
return "Global" /* ConfigAggregator.Location.GLOBAL */;
}
}
/**
* Get a resolved file path or environment variable name of the property.
*
* For example, `getPath('logLevel')` will return:
* 1. `$SF_LOG_LEVEL` if resolved to an environment variable.
* 1. `./.sf/config.json` if resolved to the local config.
* 1. `~/.sf/config.json` if resolved to the global config.
* 1. `undefined`, if not resolved.
*
* **Note:** that the path returned may be the absolute path instead of
* relative paths such as `./` and `~/`.
*
* @param key The key of the property.
*/
getPath(key) {
// TODO: make EnvVars always prefer the "new" regardless of CLI
if (this.envVars[key] != null) {
return `$${envVars_1.EnvVars.propertyToEnvName(key)}`;
}
if (this.localConfig?.getContents()[key] != null) {
return this.localConfig.getPath();
}
if (this.globalConfig.getContents()[key] != null) {
return this.globalConfig.getPath();
}
}
/**
* Get all resolved config property keys, values, locations, and paths.
*
* ```
* > console.log(aggregator.getConfigInfo());
* [
* { key: 'logLevel', val: 'INFO', location: 'Environment', path: '$SF_LOG_LEVEL'}
* { key: 'target-org', val: '<username>', location: 'Local', path: './.sf/config.json'}
* ]
* ```
*/
getConfigInfo() {
const infos = Object.keys(this.getConfig())
.filter((key) => this.getAllowedProperties().some((element) => key === element.key || key === element.newKey))
.map((key) => this.getInfo(key))
.filter((info) => !!info);
return (0, kit_1.sortBy)(infos, 'key');
}
/**
* Get the local project config instance.
*/
getLocalConfig() {
return this.localConfig;
}
/**
* Get the global config instance.
*/
getGlobalConfig() {
return this.globalConfig;
}
/**
* Get the resolved config object from the local, global and environment config instances.
*/
getConfig() {
return this.config;
}
async unsetByValue(key) {
const hasLocalWrites = this.localConfig
?.getKeysByValue(key)
.map((k) => this.localConfig?.unset(k))
.some(Boolean);
if (hasLocalWrites)
await this.localConfig?.write();
const hasGlobalWrites = this.globalConfig
?.getKeysByValue(key)
.map((k) => this.globalConfig?.unset(k))
.some(Boolean);
if (hasGlobalWrites)
await this.globalConfig?.write();
}
/**
* Get the config properties that are environment variables.
*/
getEnvVars() {
return this.envVars;
}
/**
* Re-read all property configurations from disk.
*/
async reload() {
await this.loadProperties();
return this;
}
/**
* Add an allowed config property.
*/
addAllowedProperties(configMetas) {
if ((0, ts_types_1.isArray)(configMetas)) {
this.allowedProperties.push(...configMetas);
}
else {
this.allowedProperties.push(configMetas);
}
}
/**
* Set the allowed properties.
*
* @param properties The properties to set.
*/
setAllowedProperties(properties) {
this.allowedProperties = properties;
}
/**
* Get the allowed properties.
*/
getAllowedProperties() {
return this.allowedProperties;
}
/**
* Loads all the properties and aggregates them according to location.
*/
async loadProperties() {
this.resolveProperties(await this.globalConfig.read(), this.localConfig && (await this.localConfig.read()));
ConfigAggregator.encrypted = false;
}
/**
* Loads all the properties and aggregates them according to location.
*/
loadPropertiesSync() {
this.resolveProperties(this.globalConfig.readSync(), this.localConfig?.readSync());
}
resolveProperties(globalConfig, localConfig) {
const envVars = new envVars_1.EnvVars();
for (const property of this.getAllowedProperties()) {
const key = property.newKey ? property.newKey : property.key;
const value = envVars.getPropertyFromEnv(property.key);
if (value)
this.envVars[key] = value;
}
// Global config must be read first so it is on the left hand of the
// object assign and is overwritten by the local config.
const configs = [globalConfig];
// We might not be in a project workspace
if (localConfig) {
configs.push(localConfig);
}
configs.push(this.envVars);
const json = {};
return configs.filter(ts_types_1.isJsonMap).reduce((acc, el) => (0, kit_1.merge)(acc, el), json);
}
}
exports.ConfigAggregator = ConfigAggregator;
//# sourceMappingURL=configAggregator.js.map