@salesforce/command
Version:
Salesforce CLI base command class
489 lines • 20.4 kB
JavaScript
/*
* 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.buildSfdxFlags = exports.optionalBuiltinFlags = exports.requiredBuiltinFlags = exports.flags = void 0;
const url_1 = require("url");
const core_1 = require("@oclif/core");
const core_2 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
core_2.Messages.importMessagesDirectory(__dirname);
const messages = core_2.Messages.load('@salesforce/command', 'flags', [
'error.UnknownBuiltinFlagType',
'error.FormattingMessageArrayValue',
'error.FormattingMessageArrayOption',
'error.FormattingMessageDate',
'error.FormattingMessageId',
'error.FormattingMessageId',
'error.InvalidFlagType',
'flags.json.description.long',
'flags.json.description',
'error.InvalidLoggerLevel',
'error.InvalidApiVersion',
'flags.apiversion.description',
'flags.apiversion.description.long',
'flags.concise.description',
'flags.long.description.long',
'flags.loglevel.description',
'flags.loglevel.description.long',
'flags.quiet.description',
'flags.quiet.description.long',
'flags.targetdevhubusername.description',
'flags.targetdevhubusername.description.long',
'flags.targetusername.description',
'flags.targetusername.description.long',
'flags.verbose.description',
'flags.verbose.description.long',
'error.InvalidFlagName',
'error.InvalidFlagChar',
'error.MissingOrInvalidFlagDescription',
'error.InvalidLongDescriptionFormat',
]);
function validateValue(isValid, value, kind, correct) {
if (isValid)
return value;
throw messages.createError('error.InvalidFlagType', [value, kind, correct ?? '']);
}
function toValidatorFn(validator) {
return (val) => {
if ((0, ts_types_1.isString)(validator))
return new RegExp(validator).test(val);
if ((0, ts_types_1.isInstance)(validator, RegExp))
return validator.test(val);
if ((0, ts_types_1.isFunction)(validator))
return !!validator(val);
return true;
};
}
function merge(kind, flag, describable) {
if ((0, ts_types_1.has)(flag, 'validate') && (0, ts_types_1.hasFunction)(flag, 'parse')) {
const parse = flag.parse.bind(flag);
flag.parse = (val, ctx) => {
validateValue(toValidatorFn(flag.validate)(val), val, kind);
return parse(val, ctx);
};
}
// @ts-ignore
return {
kind,
...flag,
description: describable.description,
longDescription: describable.longDescription,
};
}
function option(kind, options, parse) {
const flag = core_1.Flags.option({ ...options, parse });
return merge(kind, flag, options);
}
// oclif
function buildBoolean(options) {
const flag = core_1.Flags.boolean(options);
return merge('boolean', flag, options);
}
function buildEnum(options) {
return {
kind: 'enum',
...core_1.Flags.enum(options),
options: options.options,
description: options.description,
longDescription: options.longDescription,
};
}
function buildHelp(options) {
const flag = core_1.Flags.help(options);
return merge('help', core_1.Flags.help(options), {
description: (0, ts_types_1.ensure)(flag.description),
});
}
function buildFilepath(options) {
return option('filepath', options, (val) => Promise.resolve(validateValue(core_2.sfdc.validatePathDoesNotContainInvalidChars(val), val, 'filepath')));
}
function buildDirectory(options) {
return option('directory', options, (val) => Promise.resolve(validateValue(core_2.sfdc.validatePathDoesNotContainInvalidChars(val), val, 'directory')));
}
function validateBounds(kind, value, bounds, extract) {
if (bounds.min != null && value < extract(bounds.min)) {
throw new core_2.SfError(`Expected ${kind} greater than or equal to ${extract(bounds.min)} but received ${value}`, 'InvalidFlagNumericBoundsError');
}
if (bounds.max != null && value > extract(bounds.max)) {
throw new core_2.SfError(`Expected ${kind} less than or equal to ${extract(bounds.max)} but received ${value}`, 'InvalidFlagNumericBoundsError');
}
return value;
}
function buildInteger(options) {
const kind = 'integer';
return option(kind, options, async (val) => {
const parsed = (0, kit_1.toNumber)(val);
validateValue(Number.isInteger(parsed), val, kind);
return validateBounds(kind, parsed, options, (t) => t);
});
}
function buildOption(options) {
const optsFlag = core_1.Flags.option(options);
return merge('option', optsFlag, options);
}
function buildString(options) {
return option('string', options, options.parse ?? ((val) => Promise.resolve(val)));
}
function buildVersion(options) {
const flag = core_1.Flags.version(options);
return merge('version', flag, {
description: (0, ts_types_1.ensure)(flag.description),
});
}
// sfdx
function validateArrayValues(kind, raw, vals, validator) {
validateValue(vals.every(toValidatorFn(validator)), raw, kind, ` ${messages.getMessage('error.FormattingMessageArrayValue')}`);
}
function validateArrayOptions(kind, raw, vals, allowed) {
validateValue(allowed.size === 0 || vals.every((t) => allowed.has(t)), raw, kind, ` ${messages.getMessage('error.FormattingMessageArrayOption', [Array.from(allowed).toString()])}`);
}
const convertArrayFlagToArray = (flagValue, delimiter = ',') => {
// don't split on delimiter if it's inside a single or double-quoted substring
// eslint-disable-next-line no-useless-escape
const regex = new RegExp(`"(.*?)"|\'(.*?)\'|${delimiter}`);
return flagValue
.split(regex)
.filter((i) => !!i)
.map((i) => i.trim());
};
function buildMappedArray(kind, options) {
const { options: values, ...rest } = options;
const allowed = new Set(values);
return option(kind, rest, (val) => {
const vals = convertArrayFlagToArray(val, options.delimiter);
validateArrayValues(kind, val, vals, options.validate);
const mappedVals = vals.map(options.map);
validateArrayOptions(kind, val, mappedVals, allowed);
return Promise.resolve(mappedVals);
});
}
function buildStringArray(kind, options) {
const { options: values, ...rest } = options;
const allowed = new Set(values);
return option(kind, rest, (val) => {
const vals = convertArrayFlagToArray(val, options.delimiter);
validateArrayValues(kind, val, vals, options.validate);
validateArrayOptions(kind, val, vals, allowed);
return Promise.resolve(vals);
});
}
function buildArray(options) {
const kind = 'array';
return 'map' in options ? buildMappedArray(kind, options) : buildStringArray(kind, options);
}
function buildDate(options) {
const kind = 'date';
return option(kind, options, (val) => {
const parsed = Date.parse(val);
validateValue(!isNaN(parsed), val, kind, ` ${messages.getMessage('error.FormattingMessageDate')}`);
return Promise.resolve(new Date(parsed));
});
}
function buildDatetime(options) {
const kind = 'datetime';
return option(kind, options, (val) => {
const parsed = Date.parse(val);
validateValue(!isNaN(parsed), val, kind, ` ${messages.getMessage('error.FormattingMessageDate')}`);
return Promise.resolve(new Date(parsed));
});
}
function buildEmail(options) {
return option('email', options, (val) => Promise.resolve(validateValue(core_2.sfdc.validateEmail(val), val, 'email')));
}
function buildId(options) {
return option('id', options, (val) => Promise.resolve(validateValue(core_2.sfdc.validateSalesforceId(val), val, 'id', ` ${messages.getMessage('error.FormattingMessageId')}`)));
}
function buildMilliseconds(options) {
const kind = 'milliseconds';
return option(kind, options, async (val) => {
const parsed = (0, kit_1.toNumber)(val);
validateValue(Number.isInteger(parsed), val, kind);
return Promise.resolve(kit_1.Duration.milliseconds(validateBounds(kind, parsed, options, (v) => ((0, ts_types_1.isNumber)(v) ? v : v[kind]))));
});
}
function buildMinutes(options) {
const kind = 'minutes';
return option(kind, options, async (val) => {
const parsed = (0, kit_1.toNumber)(val);
validateValue(Number.isInteger(parsed), val, kind);
return Promise.resolve(kit_1.Duration.minutes(validateBounds(kind, parsed, options, (v) => ((0, ts_types_1.isNumber)(v) ? v : v[kind]))));
});
}
function buildNumber(options) {
const kind = 'number';
return option(kind, options, async (val) => {
const parsed = (0, kit_1.toNumber)(val);
validateValue(isFinite(parsed), val, kind);
return validateBounds(kind, parsed, options, (t) => t);
});
}
function buildSeconds(options) {
const kind = 'seconds';
return option(kind, options, async (val) => {
const parsed = (0, kit_1.toNumber)(val);
validateValue(Number.isInteger(parsed), val, kind);
return Promise.resolve(kit_1.Duration.seconds(validateBounds(kind, parsed, options, (v) => ((0, ts_types_1.isNumber)(v) ? v : v[kind]))));
});
}
function buildUrl(options) {
return option('url', options, (val) => {
try {
return Promise.resolve(new url_1.URL(val));
}
catch (err) {
const correct = ` ${messages.getMessage('error.FormattingMessageId')}`;
throw messages.createError('error.InvalidFlagType', [val, 'url', correct || '']);
}
});
}
function buildBuiltin(options = {}) {
return { ...options, type: 'builtin' };
}
exports.flags = {
// oclif
/**
* A flag type whose presence indicates a `true` boolean value. Produces false when not present.
*/
boolean: buildBoolean,
/**
* A flag type with a fixed enumeration of possible option values. Produces a validated string from the `options` list.
*/
enum: buildEnum,
/**
* A flag type useful for overriding the short `char` trigger for emitting CLI help. Emits help and exits the CLI.
*/
help: buildHelp,
/**
* A flag type that accepts basic integer values. For floats, binary, octal, and hex, see {@link flags.number}.
* Produces an integer `number`.
*/
integer: buildInteger,
/**
* A flag type for custom string processing. Accepts a `parse` function that converts a `string` value to a type `T`.
* Produces a type `T`.
*/
option: buildOption,
/**
* A flag type for returning a raw `string` value without further preprocessing. Produces a string.
*/
string: buildString,
/**
* A flag type for emitting CLI version information. Emits the CLI version and exits the CLI.
*/
version: buildVersion,
/**
* A flag type for valid file paths. Produces a validated string.
*
* **See** [@salesforce/core#sfdc.validatePathDoesNotContainInvalidChars](https://forcedotcom.github.io/sfdx-core/globals.html#sfdc), e.g. "this/is/my/path".
*/
filepath: buildFilepath,
/**
* A flag type for valid directory paths. Produces a validated string.
*
* **See** [@salesforce/core#sfdc.validatePathDoesNotContainInvalidChars](https://forcedotcom.github.io/sfdx-core/globals.html#sfdc), e.g. "this/is/my/path".
*/
directory: buildDirectory,
// sfdx
/**
* A flag type for a delimited list of strings with the delimiter defaulting to `,`, e.g., "one,two,three". Accepts
* an optional `delimiter` `string` and/or a custom `map` function for converting parsed `string` values into
* a type `T`. Produces a parsed (and possibly mapped) array of type `T` where `T` defaults to `string` if no
* custom `map` function was provided.
*/
array: buildArray,
/**
* A flag type for a valid date, e.g., "01-02-2000" or "01/02/2000 01:02:34". Produces a parsed `Date`.
*/
date: buildDate,
/**
* A flag type for a valid datetime, e.g., "01-02-2000" or "01/02/2000 01:02:34". Produces a parsed `Date`.
*/
datetime: buildDatetime,
/**
* A flag type for valid email addresses. Produces a validated string.
*
* **See** [@salesforce/core#sfdc.validateEmail](https://forcedotcom.github.io/sfdx-core/globals.html#sfdc), e.g., "me@my.org".
*/
email: buildEmail,
/**
* A flag type for valid Salesforce IDs. Produces a validated string.
*
* **See** [@salesforce/core#sfdc.validateSalesforceId](https://forcedotcom.github.io/sfdx-core/globals.html#sfdc), e.g., "00Dxxxxxxxxxxxx".
*/
id: buildId,
/**
* A flag type for a valid `Duration` in milliseconds, e.g., "5000".
*/
milliseconds: buildMilliseconds,
/**
* A flag type for a valid `Duration` in minutes, e.g., "2".
*/
minutes: buildMinutes,
/**
* A flag type for valid integer or floating point number, e.g., "42". Additionally supports binary, octal, and hex
* notation. Produces a parsed `number`.
*/
number: buildNumber,
/**
* A flag type for a valid `Duration` in seconds, e.g., "5".
*/
seconds: buildSeconds,
/**
* A flag type for a valid url, e.g., "http://www.salesforce.com". Produces a parsed `URL` instance.
*/
url: buildUrl,
// builtins
/**
* Declares a flag definition to be one of the builtin types, for automatic configuration.
*/
builtin: buildBuiltin,
};
exports.requiredBuiltinFlags = {
json() {
return exports.flags.boolean({
description: messages.getMessage('flags.json.description'),
longDescription: messages.getMessage('flags.json.description.long'),
});
},
loglevel() {
return exports.flags.enum({
options: core_2.Logger.LEVEL_NAMES.concat(core_2.Logger.LEVEL_NAMES.map((l) => l.toUpperCase())),
default: core_2.LoggerLevel[core_2.Logger.DEFAULT_LEVEL].toLowerCase(),
required: false,
description: messages.getMessage('flags.loglevel.description'),
longDescription: messages.getMessage('flags.loglevel.description.long'),
parse: (val) => {
val = val.toLowerCase();
if (core_2.Logger.LEVEL_NAMES.includes(val))
return Promise.resolve(val);
throw messages.createError('error.InvalidLoggerLevel', [val]);
},
});
},
};
function resolve(opts, key, def) {
return (0, ts_types_1.hasString)(opts, key) ? opts[key] : def;
}
exports.optionalBuiltinFlags = {
apiversion(opts) {
return Object.assign(opts ?? {}, exports.flags.string({
description: resolve(opts, 'description', messages.getMessage('flags.apiversion.description')),
longDescription: resolve(opts, 'longDescription', messages.getMessage('flags.apiversion.description.long')),
parse: (val) => {
if (core_2.sfdc.validateApiVersion(val))
return Promise.resolve(val);
throw messages.createError('error.InvalidApiVersion', [val]);
},
}));
},
concise(opts) {
return Object.assign(opts ?? {}, exports.flags.boolean({
description: resolve(opts, 'description', messages.getMessage('flags.concise.description')),
longDescription: resolve(opts, 'longDescription', messages.getMessage('flags.long.description.long')),
}));
},
quiet(opts) {
return Object.assign(opts ?? {}, exports.flags.boolean({
description: resolve(opts, 'description', messages.getMessage('flags.quiet.description')),
longDescription: resolve(opts, 'longDescription', messages.getMessage('flags.quiet.description.long')),
}));
},
targetdevhubusername(opts) {
return Object.assign(opts ?? {}, exports.flags.string({
char: 'v',
description: resolve(opts, 'description', messages.getMessage('flags.targetdevhubusername.description')),
longDescription: resolve(opts, 'longDescription', messages.getMessage('flags.targetdevhubusername.description.long')),
}));
},
targetusername(opts) {
return Object.assign(opts ?? {}, exports.flags.string({
char: 'u',
description: resolve(opts, 'description', messages.getMessage('flags.targetusername.description')),
longDescription: resolve(opts, 'longDescription', messages.getMessage('flags.targetusername.description.long')),
}));
},
verbose(opts) {
return Object.assign(opts ?? {}, exports.flags.boolean({
description: resolve(opts, 'description', messages.getMessage('flags.verbose.description')),
longDescription: resolve(opts, 'longDescription', messages.getMessage('flags.verbose.description.long')),
}));
},
};
/**
* Validate the custom flag configuration. This includes:
*
* - The flag name is in all lowercase.
* - A string description is provided.
* - If a char attribute is provided, it is one alphabetical character in length.
* - If a long description is provided, it is a string.
*
* @param {SfdxFlagDefinition} flag The flag configuration.
* @param {string} key The flag name.
* @throws SfError If the criteria is not meet.
*/
function validateCustomFlag(key, flag) {
if (!/^(?!(?:[-]|[0-9]*$))[a-z0-9-]+$/.test(key)) {
throw messages.createError('error.InvalidFlagName', [key]);
}
if (flag.char && (flag.char.length !== 1 || !/[a-zA-Z]/.test(flag.char))) {
throw messages.createError('error.InvalidFlagChar', [key]);
}
if (!flag.description || !(0, ts_types_1.isString)(flag.description)) {
throw messages.createError('error.MissingOrInvalidFlagDescription', [key]);
}
if (flag.longDescription !== undefined && !(0, ts_types_1.isString)(flag.longDescription)) {
throw messages.createError('error.InvalidLongDescriptionFormat', [key]);
}
return flag;
}
// eslint-disable-next-line @typescript-eslint/ban-types
function isBuiltin(flag) {
return (0, ts_types_1.hasString)(flag, 'type') && flag.type === 'builtin';
}
/**
* Builds flags for a command given a configuration object. Supports the following use cases:
* 1. Enabling common SFDX flags. E.g., { verbose: true }
* 2. Defining typed flags. E.g., { myFlag: Flags.array({ char: '-a' }) }
* 3. Defining custom typed flags. E.g., { myFlag: Flags.custom({ parse: (val) => parseInt(val, 10) }) }
*
* @param {FlagsConfig} flagsConfig The configuration object for a flag. @see {@link FlagsConfig}
* @param options Extra configuration options.
* @returns {flags.Output} The flags for the command.
* @ignore
*/
function buildSfdxFlags(flagsConfig, options) {
// Required flag options for all SFDX commands
const output = {
json: exports.requiredBuiltinFlags.json(),
loglevel: exports.requiredBuiltinFlags.loglevel(),
};
if (options.targetdevhubusername)
output.targetdevhubusername = exports.optionalBuiltinFlags.targetdevhubusername();
if (options.targetusername)
output.targetusername = exports.optionalBuiltinFlags.targetusername();
if (options.targetdevhubusername || options.targetusername)
output.apiversion = exports.optionalBuiltinFlags.apiversion();
// Process configuration for custom and builtin flags
(0, ts_types_1.definiteEntriesOf)(flagsConfig).forEach(([key, flag]) => {
if (isBuiltin(flag)) {
if (!(0, ts_types_1.isKeyOf)(exports.optionalBuiltinFlags, key)) {
throw messages.createError('error.UnknownBuiltinFlagType', [key]);
}
// @ts-ignore
output[key] = exports.optionalBuiltinFlags[key](flag);
}
else {
// @ts-ignore
output[key] = validateCustomFlag(key, flag);
}
});
return output;
}
exports.buildSfdxFlags = buildSfdxFlags;
//# sourceMappingURL=sfdxFlags.js.map
;