UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

492 lines (490 loc) 18.4 kB
"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 */ /* -------------------------------------------------------------------------------------------------------------------- * WARNING: This file has been deprecated and should now be considered locked against further changes. Its contents * have been partially or wholely superceded by functionality included in the @salesforce/core npm package, and exists * now to service prior uses in this repository only until they can be ported to use the new @salesforce/core library. * * If you need or want help deciding where to add new functionality or how to migrate to the new library, please * contact the CLI team at alm-cli@salesforce.com. * ----------------------------------------------------------------------------------------------------------------- */ // Node const os = require("os"); const util = require("util"); const path = require("path"); const crypto = require("crypto"); const mkdirp = require("mkdirp"); const archiver = require("archiver"); // Thirdparty const BBPromise = require("bluebird"); const kit_1 = require("@salesforce/kit"); const _ = require("lodash"); const fs = BBPromise.promisifyAll(require('fs-extra')); // Local const Messages = require("../messages"); const errors = require("./errors"); const consts = require("./constants"); const messages = Messages(); const logger = require("./logApi"); // The hidden folder that we keep all SFDX's state in. const STATE_FOLDER = '.sfdx'; const SFDX_CONFIG_FILE_NAME = 'sfdx-config.json'; // For 208 this needs to be 'sfdx toolbelt' const SfdxCLIClientId = 'sfdx toolbelt'; const SFDX_HTTP_HEADERS = { 'content-type': 'application/json', 'user-agent': SfdxCLIClientId, }; let zipDirPath; const _getHomeDir = function () { return os.homedir(); }; const _getGlobalHiddenFolder = function () { return path.join(_getHomeDir(), STATE_FOLDER); }; const _toLowerCase = (val, key) => key.toLowerCase(); const _checkEmptyContent = function (data, jsonPath, throwOnEmpty = true) { // REVIEWME: why throw? shouldn't the caller handle? if (!data.length) { if (throwOnEmpty) { throw new Error(messages.getMessage('JsonParseError', [jsonPath, 1, 'FILE HAS NO CONTENT'])); } else { data = {}; } } return data; }; /** * * @param data JSON data as a string to parse * @param jsonPath path to the json file used for error reporting * @param throwOnEmpty throw a JsonParseError when the content is empty */ function parseJSON(data, jsonPath, throwOnEmpty = true) { const _data = _checkEmptyContent(data, jsonPath, throwOnEmpty); return kit_1.parseJson(_data, jsonPath, throwOnEmpty); } const self = { /** * Read a file and convert it to JSON * * @param {string} jsonPath The path of the file * @return promise The contents of the file as a JSON object */ async readJSON(jsonPath, throwOnEmpty = true) { const data = await fs.readFile(jsonPath, 'utf8'); return parseJSON(data, jsonPath, throwOnEmpty); }, parseJSON, /** * Helper for handling errors resulting from reading and then parsing a JSON file * * @param e - the error * @param filePath - the filePath to the JSON file being read */ processReadAndParseJsonFileError(e, filePath) { if (e.name === 'SyntaxError') { e.message = messages.getMessage('InvalidJson', filePath); } return e; }, /** * simple helper for creating an error with a name. * * @param message - the message for the error * @param name - the name of the error. preferably containing no spaces, starting with a capital letter, and camel-case. * @returns {Error} */ getError(message, name) { let error = new Error(message); error['name'] = name; if (util.isNullOrUndefined(message) || util.isNullOrUndefined(name)) { error = new Error('Both name and message are required for sf toolbelt errors.'); error['name'] = 'NameAndMessageRequired'; } return error; }, /** * function that normalizes cli args between yargs and heroku toolbelt * * @param context - the cli context * @returns {object} */ fixCliContext(context) { // This can be called from heroku or appconfig. let fixedContext = context; if (!util.isNullOrUndefined(context.flags)) { fixedContext = context.flags; } return fixedContext; }, /** * Simple helper method to determine that the path is a file (all SFDX files have an extension) * * @param localPath * @returns {boolean} */ containsFileExt(localPath) { const typeExtension = path.extname(localPath); return typeExtension && typeExtension !== ''; }, /** * Simple helper method to determine if a fs path exists. * * @param localPath The path to check. Either a file or directory. * @returns {boolean} true if the path exists false otherwise. */ pathExistsSync(localPath) { try { return fs.statSync(localPath); } catch (err) { return false; } }, /** * Ensure that a directory exists, creating as necessary * * @param localPath The path to the directory */ ensureDirectoryExistsSync(localPath) { if (!self.pathExistsSync(localPath)) { mkdirp.sync(localPath); } }, /** * If a file exists, delete it * * @param localPath - Path of the file to delete. */ deleteIfExistsSync(localPath) { if (self.pathExistsSync(localPath)) { const stats = fs.statSync(localPath); if (stats.isDirectory()) { fs.rmdirSync(localPath); } else { fs.unlinkSync(localPath); } } }, /** * If a directory exists, force remove it and anything inside * * @param localPath - Path of the directory to delete. */ deleteDirIfExistsSync(localPath) { fs.removeSync(localPath); }, /** * If a directory exists, return all the items inside of it * * @param localPath - Path of the directory * @param deep{boolean} - Whether to include files in all subdirectories recursively * @param excludeDirs{boolean} - Whether to exclude directories in the returned list * @returns {Array} - files in directory */ getDirectoryItems(localPath, deep, excludeDirs) { let dirItems = []; if (self.pathExistsSync(localPath)) { fs.readdirSync(localPath).forEach((file) => { const curPath = path.join(localPath, file); const isDir = fs.statSync(curPath).isDirectory(); if (!isDir || (isDir && !excludeDirs)) { dirItems.push(curPath); } if (deep && isDir) { dirItems = [...dirItems, ...this.getDirectoryItems(curPath, true, excludeDirs)]; } }); } return dirItems; }, /** * Helper method for getting config file data from $HOME/.sfdx. * * @param {string} jsonConfigFileName The name of the config file stored in .sfdx. * @param {object} defaultIfNotExist A value returned if the files doesn't exist. It not set, an error would be thrown. * @returns {BBPromise<object>} The resolved content as a json object. */ getGlobalConfig(jsonConfigFileName, defaultIfNotExist) { if (util.isNullOrUndefined(jsonConfigFileName)) { throw new errors.MissingRequiredParameter('jsonConfigFileName'); } const configFilePath = path.join(_getGlobalHiddenFolder(), jsonConfigFileName); return this.readJSON(configFilePath).catch((err) => { if (err.code === 'ENOENT' && _.isObject(defaultIfNotExist)) { return BBPromise.resolve(defaultIfNotExist); } return BBPromise.reject(err); }); }, /** * Synchronous version of getAppConfig. * * @deprecated */ getGlobalConfigSync(jsonConfigFileName) { if (util.isNullOrUndefined(jsonConfigFileName)) { throw new errors.MissingRequiredParameter('jsonConfigFileName'); } const configPath = path.join(_getGlobalHiddenFolder(), jsonConfigFileName); try { return JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (e) { throw this.processReadAndParseJsonFileError(e, configPath); } }, /** * Helper method for saving config files to .sfdx. * * @param config The config.json configuration object. * @param jsonConfigFileName The name for the config file to store in .sfdx. * @param jsonConfigObject The json object to store in .sfdx/[jsonConfigFileName] * @returns BBPromise */ saveGlobalConfig(jsonConfigFileName, jsonConfigObject) { if (util.isNullOrUndefined(jsonConfigFileName)) { throw new errors.MissingRequiredParameter('jsonConfigFileName'); } if (util.isNullOrUndefined(jsonConfigObject)) { throw new errors.MissingRequiredParameter('jsonConfigObject'); } return (fs .mkdirAsync(path.join(_getGlobalHiddenFolder()), consts.DEFAULT_USER_DIR_MODE) .error((err) => { // This directory already existing is a normal and expected thing. if (err.code !== 'EEXIST') { throw err; } }) // Handle the login result and persist the access token. .then(() => { const configFilePath = path.join(_getGlobalHiddenFolder(), jsonConfigFileName); return fs.writeFileAsync(configFilePath, JSON.stringify(jsonConfigObject, undefined, 4), { encoding: 'utf8', flag: 'w+', mode: consts.DEFAULT_USER_FILE_MODE, }); })); }, /** * Get the name of the directory containing workspace state * * @returns {string} */ getWorkspaceStateFolderName() { return STATE_FOLDER; }, getConfigFileName() { return SFDX_CONFIG_FILE_NAME; }, /** * Get the full path to the file storing the workspace org config information * * @param wsPath - The root path of the workspace * @returns {*} */ getWorkspaceOrgConfigPath(wsPath) { return path.join(wsPath, STATE_FOLDER, this.getConfigFileName()); }, /** * Helper function that returns true if a value is an integer. * * @param value the value to compare * @returns {boolean} true if value is an integer. this is not a mathematical definition. that is -0 returns true. * this is in intended to be followed up with parseInt. */ isInt(value) { return (!isNaN(value) && (function (x) { return (x | 0) === x; })(parseFloat(value))); }, /** * Execute each function in the array sequentially. * * @param promiseFactories An array of functions to be executed that return BBPromises. * @returns {BBPromise.<T>} */ sequentialExecute(promiseFactories) { let result = BBPromise.resolve(); promiseFactories.forEach((promiseFactory) => { result = result.then(promiseFactory); }); return result; }, /** * Given a request object or string url a request object is returned with the additional http headers needed by force.com * * @param {(string|object)} request - A string url or javascript object. * @param options - {object} that may contain headers to add to request * @returns {object} a request object containing {method, url, headers} */ setSfdxRequestHeaders(request, options = {}) { if (!request) { return undefined; } // if request is simple string, regard it as url in GET method const _request = _.isString(request) ? { method: 'GET', url: request } : request; // normalize header keys const reqHeaders = _.mapKeys(request.headers, _toLowerCase); const optHeaders = _.mapKeys(options.headers, _toLowerCase); // set headers, overriding as appropriate _request.headers = Object.assign({}, this.getSfdxRequestHeaders(), reqHeaders, optHeaders); return _request; }, getSfdxRequestHeaders() { return SFDX_HTTP_HEADERS; }, getSfdxCLIClientId() { if (process.env.SFDX_SET_CLIENT_IDS) { return `${SfdxCLIClientId}:${process.env.SFDX_SET_CLIENT_IDS}`; } return SfdxCLIClientId; }, isVerbose() { return process.argv.indexOf('--verbose') > 0; }, /** * Zips directory to given zipfile. * * https://github.com/archiverjs/node-archiver * * @param dir to zip * @param zipfile * @param options */ zipDir(dir, zipfile, options = {}) { const file = path.parse(dir); const outFile = zipfile || path.join(os.tmpdir() || '.', `${file.base}.zip`); const output = fs.createWriteStream(outFile); this.setZipDirPath(outFile); const timer = process.hrtime(); return new BBPromise((resolve, reject) => { const archive = archiver('zip', options); archive.on('finish', () => { logger.debug(`${archive.pointer()} bytes written to ${outFile} using ${this.getElapsedTime(timer)}ms`); // zip file returned once stream is closed, see 'close' listener below }); archive.on('error', (err) => { reject(err); }); output.on('close', () => { resolve(outFile); }); archive.pipe(output); archive.directory(dir, ''); archive.finalize(); }); }, setZipDirPath(path) { zipDirPath = path; }, getZipDirPath() { return zipDirPath; }, getElapsedTime(timer) { const elapsed = process.hrtime(timer); return (elapsed[0] * 1000 + elapsed[1] / 1000000).toFixed(3); }, /** * Uses Lodash _.mapKeys to convert object keys to another format using the specified conversion function. * * E.g., to deep convert all object keys to camelCase: mapKeys(myObj, _.camelCase, true) * to shallow convert object keys to lower case: mapKeys(myObj, _.toLower) * * NOTE: This mutates the object passed in for conversion. * * @param obj - {Object} The object to convert the keys * @param converterFn - {Function} The function that converts the object key * @param deep - {boolean} Whether to do a deep object key conversion * @return {Object} - the object with the converted keys */ mapKeys(obj, converterFn, deep) { return _.mapKeys(obj, (val, key, o) => { const _key = converterFn.call(null, key); if (deep) { let _val = val; if (_.isArray(val)) { _.forEach(val, (v1) => { if (_.isPlainObject(v1)) { _val = this.mapKeys(v1, converterFn, deep); } }); } else if (_.isPlainObject(val)) { _val = this.mapKeys(val, converterFn, deep); } o[_key] = _val; if (key !== _key) { delete o[key]; } } return _key; }); }, // A very common usecase of mapKeys. toLowerCaseKeys(obj, deep) { return this.mapKeys(obj, _.toLower, deep); }, /** * Helper to make a nodejs base64 encoded string compatible with rfc4648 alternative encoding for urls. * * @param {string} base64Encoded - a nodejs base64 encoded string * @returns {string} returns the string escaped. */ base64UrlEscape(base64Encoded) { // builtin node js base 64 encoding is not 64 url compatible. // See - https://toolsn.ietf.org/html/rfc4648#section-5 return _.replace(base64Encoded, /\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }, /** * Helper that will un-escape a base64 url encoded string. * * @param {string} base64EncodedAndEscaped - the based 64 escaped and encoded string. * @returns {string} returns the string un-escaped. */ base64UrlUnEscape(base64EncodedAndEscaped) { // builtin node js base 64 encoding is not 64 url compatible. // See - https://toolsn.ietf.org/html/rfc4648#section-5 const _unescaped = _.replace(base64EncodedAndEscaped, /-/g, '+').replace(/_/g, '/'); return _unescaped + '==='.slice((_unescaped.length + 3) % 4); }, getContentHash(contents) { return crypto.createHash('sha1').update(contents).digest('hex'); }, /** * Logs the collection of unsupported mime types to the server * * @param unsupportedMimeTypes * @param _logger * @param force */ async logUnsupportedMimeTypeError(unsupportedMimeTypes, _logger, force) { if (unsupportedMimeTypes.length > 0) { const errName = 'UnsupportedMimeTypes'; const unsupportedMimeTypeError = new Error(); unsupportedMimeTypeError.name = errName; unsupportedMimeTypeError.message = messages.getMessage(errName, [...new Set(unsupportedMimeTypes)]); unsupportedMimeTypeError.stack = ''; _.set(unsupportedMimeTypeError, 'errAllowlist', errName); try { // TODO Use new telemetry exception throwing. } catch (err) { // Ignore; Don't fail source commands if logServerError fails } } return BBPromise.resolve(); }, }; module.exports = self; //# sourceMappingURL=srcDevUtil.js.map