@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
343 lines • 13.5 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.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
;