UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

343 lines 13.5 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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigFile = void 0; const fs = require("fs"); const fs_1 = require("fs"); const os_1 = require("os"); const path_1 = require("path"); const ts_types_1 = require("@salesforce/ts-types"); const kit_1 = require("@salesforce/kit"); const global_1 = require("../global"); const logger_1 = require("../logger"); const sfError_1 = require("../sfError"); const internal_1 = require("../util/internal"); const configStore_1 = require("./configStore"); /** * Represents a json config file used to manage settings and state. Global config * files are stored in the home directory hidden state folder (.sfdx) and local config * files are stored in the project path, either in the hidden state folder or wherever * specified. * * ``` * class MyConfig extends ConfigFile { * public static getFileName(): string { * return 'myConfigFilename.json'; * } * } * const myConfig = await MyConfig.create({ * isGlobal: true * }); * myConfig.set('mykey', 'myvalue'); * await myConfig.write(); * ``` */ class ConfigFile extends configStore_1.BaseConfigStore { /** * Create an instance of a config file without reading the file. Call `read` or `readSync` * after creating the ConfigFile OR instantiate with {@link ConfigFile.create} instead. * * @param options The options for the class instance * @ignore */ constructor(options) { super(options); // whether file contents have been read this.hasRead = false; this.logger = logger_1.Logger.childFromRoot(this.constructor.name); const statics = this.constructor; let defaultOptions = {}; try { defaultOptions = statics.getDefaultOptions(); } catch (e) { /* Some implementations don't let you call default options */ } // Merge default and passed in options this.options = Object.assign(defaultOptions, this.options); } /** * Returns the config's filename. */ static getFileName() { // Can not have abstract static methods, so throw a runtime error. throw new sfError_1.SfError('Unknown filename for config file.'); } /** * Returns the default options for the config file. * * @param isGlobal If the file should be stored globally or locally. * @param filename The name of the config file. */ static getDefaultOptions(isGlobal = false, filename) { return { isGlobal, isState: true, filename: filename || this.getFileName(), stateFolder: global_1.Global.SFDX_STATE_FOLDER, }; } /** * Helper used to determine what the local and global folder point to. Returns the file path of the root folder. * * @param isGlobal True if the config should be global. False for local. */ static async resolveRootFolder(isGlobal) { return isGlobal ? (0, os_1.homedir)() : await (0, internal_1.resolveProjectPath)(); } /** * Helper used to determine what the local and global folder point to. Returns the file path of the root folder. * * @param isGlobal True if the config should be global. False for local. */ static resolveRootFolderSync(isGlobal) { return isGlobal ? (0, os_1.homedir)() : (0, internal_1.resolveProjectPathSync)(); } /** * Determines if the config file is read/write accessible. Returns `true` if the user has capabilities specified * by perm. * * @param {number} perm The permission. * * **See** {@link https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_access_path_mode_callback} */ async access(perm) { try { await fs.promises.access(this.getPath(), perm); return true; } catch (err) { return false; } } /** * Determines if the config file is read/write accessible. Returns `true` if the user has capabilities specified * by perm. * * @param {number} perm The permission. * * **See** {@link https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_access_path_mode_callback} */ accessSync(perm) { try { fs.accessSync(this.getPath(), perm); return true; } catch (err) { return false; } } /** * Read the config file and set the config contents. Returns the config contents of the config file. As an * optimization, files are only read once per process and updated in memory and via `write()`. To force * a read from the filesystem pass `force=true`. * **Throws** *{@link SfError}{ name: 'UnexpectedJsonFileFormat' }* There was a problem reading or parsing the file. * * @param [throwOnNotFound = false] Optionally indicate if a throw should occur on file read. * @param [force = false] Optionally force the file to be read from disk even when already read within the process. */ async read(throwOnNotFound = false, force = false) { try { // Only need to read config files once. They are kept up to date // internally and updated persistently via write(). if (!this.hasRead || force) { this.logger.info(`Reading config file: ${this.getPath()}`); const obj = (0, kit_1.parseJsonMap)(await fs.promises.readFile(this.getPath(), 'utf8')); this.setContentsFromObject(obj); } return this.getContents(); } catch (err) { if (err.code === 'ENOENT') { if (!throwOnNotFound) { this.setContents(); return this.getContents(); } } throw err; } finally { // Necessarily set this even when an error happens to avoid infinite re-reading. // To attempt another read, pass `force=true`. this.hasRead = true; } } /** * Read the config file and set the config contents. Returns the config contents of the config file. As an * optimization, files are only read once per process and updated in memory and via `write()`. To force * a read from the filesystem pass `force=true`. * **Throws** *{@link SfError}{ name: 'UnexpectedJsonFileFormat' }* There was a problem reading or parsing the file. * * @param [throwOnNotFound = false] Optionally indicate if a throw should occur on file read. * @param [force = false] Optionally force the file to be read from disk even when already read within the process. */ readSync(throwOnNotFound = false, force = false) { try { // Only need to read config files once. They are kept up to date // internally and updated persistently via write(). if (!this.hasRead || force) { this.logger.info(`Reading config file: ${this.getPath()}`); const obj = (0, kit_1.parseJsonMap)(fs.readFileSync(this.getPath(), 'utf8')); this.setContentsFromObject(obj); } return this.getContents(); } catch (err) { if (err.code === 'ENOENT') { if (!throwOnNotFound) { this.setContents(); return this.getContents(); } } throw err; } finally { // Necessarily set this even when an error happens to avoid infinite re-reading. // To attempt another read, pass `force=true`. this.hasRead = true; } } /** * Write the config file with new contents. If no new contents are provided it will write the existing config * contents that were set from {@link ConfigFile.read}, or an empty file if {@link ConfigFile.read} was not called. * * @param newContents The new contents of the file. */ async write(newContents) { if (newContents) { this.setContents(newContents); } try { await fs.promises.mkdir((0, path_1.dirname)(this.getPath()), { recursive: true }); } catch (err) { throw sfError_1.SfError.wrap(err); } this.logger.info(`Writing to config file: ${this.getPath()}`); await fs.promises.writeFile(this.getPath(), JSON.stringify(this.toObject(), null, 2)); return this.getContents(); } /** * Write the config file with new contents. If no new contents are provided it will write the existing config * contents that were set from {@link ConfigFile.read}, or an empty file if {@link ConfigFile.read} was not called. * * @param newContents The new contents of the file. */ writeSync(newContents) { if ((0, ts_types_1.isPlainObject)(newContents)) { this.setContents(newContents); } try { fs.mkdirSync((0, path_1.dirname)(this.getPath()), { recursive: true }); } catch (err) { throw sfError_1.SfError.wrap(err); } this.logger.info(`Writing to config file: ${this.getPath()}`); fs.writeFileSync(this.getPath(), JSON.stringify(this.toObject(), null, 2)); return this.getContents(); } /** * Check to see if the config file exists. Returns `true` if the config file exists and has access, false otherwise. */ async exists() { return await this.access(fs_1.constants.R_OK); } /** * Check to see if the config file exists. Returns `true` if the config file exists and has access, false otherwise. */ existsSync() { return this.accessSync(fs_1.constants.R_OK); } /** * Get the stats of the file. Returns the stats of the file. * * {@link fs.stat} */ async stat() { return fs.promises.stat(this.getPath()); } /** * Get the stats of the file. Returns the stats of the file. * * {@link fs.stat} */ statSync() { return fs.statSync(this.getPath()); } /** * Delete the config file if it exists. * * **Throws** *`Error`{ name: 'TargetFileNotFound' }* If the {@link ConfigFile.getFilename} file is not found. * {@link fs.unlink} */ async unlink() { const exists = await this.exists(); if (exists) { return await fs.promises.unlink(this.getPath()); } throw new sfError_1.SfError(`Target file doesn't exist. path: ${this.getPath()}`, 'TargetFileNotFound'); } /** * Delete the config file if it exists. * * **Throws** *`Error`{ name: 'TargetFileNotFound' }* If the {@link ConfigFile.getFilename} file is not found. * {@link fs.unlink} */ unlinkSync() { const exists = this.existsSync(); if (exists) { return fs.unlinkSync(this.getPath()); } throw new sfError_1.SfError(`Target file doesn't exist. path: ${this.getPath()}`, 'TargetFileNotFound'); } /** * Returns the absolute path to the config file. * * The first time getPath is called, the path is resolved and becomes immutable. This allows implementers to * override options properties, like filePath, on the init method for async creation. If that is required for * creation, the config files can not be synchronously created. */ getPath() { if (!this.path) { if (!this.options.filename) { throw new sfError_1.SfError('The ConfigOptions filename parameter is invalid.', 'InvalidParameter'); } const _isGlobal = (0, ts_types_1.isBoolean)(this.options.isGlobal) && this.options.isGlobal; const _isState = (0, ts_types_1.isBoolean)(this.options.isState) && this.options.isState; // Don't let users store config files in homedir without being in the state folder. let configRootFolder = this.options.rootFolder ? this.options.rootFolder : ConfigFile.resolveRootFolderSync(!!this.options.isGlobal); if (_isGlobal || _isState) { configRootFolder = (0, path_1.join)(configRootFolder, this.options.stateFolder || global_1.Global.SFDX_STATE_FOLDER); } this.path = (0, path_1.join)(configRootFolder, this.options.filePath ? this.options.filePath : '', this.options.filename); } return this.path; } /** * Returns `true` if this config is using the global path, `false` otherwise. */ isGlobal() { return !!this.options.isGlobal; } /** * Used to initialize asynchronous components. * * **Throws** *`Error`{ code: 'ENOENT' }* If the {@link ConfigFile.getFilename} file is not found when * options.throwOnNotFound is true. */ async init() { await super.init(); // Read the file, which also sets the path and throws any errors around project paths. await this.read(this.options.throwOnNotFound); } } exports.ConfigFile = ConfigFile; //# sourceMappingURL=configFile.js.map