@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
397 lines • 16.7 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
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigFile = void 0;
const fs = __importStar(require("node:fs"));
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const kit_1 = require("@salesforce/kit");
const global_1 = require("../global");
const logger_1 = require("../logger/logger");
const sfError_1 = require("../sfError");
const internal_1 = require("../util/internal");
const fileLocking_1 = require("../util/fileLocking");
const configStore_1 = require("./configStore");
const lwwMap_1 = require("./lwwMap");
/**
* 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 {
// whether file contents have been read
hasRead = false;
// Initialized in init
logger;
// Initialized in create
path;
/**
* 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);
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, node_os_1.homedir)() : (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, node_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.debug(`Reading config file: ${this.getPath()} because ${!this.hasRead ? 'hasRead is false' : 'force parameter is true'}`);
await (0, fileLocking_1.pollUntilUnlock)(this.getPath());
const obj = (0, kit_1.parseJsonMap)(await fs.promises.readFile(this.getPath(), 'utf8'), this.getPath());
this.setContentsFromFileContents(obj, (await fs.promises.stat(this.getPath(), { bigint: true })).mtimeNs);
}
// Necessarily set this even when an error happens to avoid infinite re-reading.
// To attempt another read, pass `force=true`.
this.hasRead = true;
return this.getContents();
}
catch (err) {
this.hasRead = true;
if (err.code === 'ENOENT') {
if (!throwOnNotFound) {
this.setContentsFromFileContents({});
return this.getContents();
}
}
// Necessarily set this even when an error happens to avoid infinite re-reading.
// To attempt another read, pass `force=true`.
throw err;
}
}
/**
* 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) {
(0, fileLocking_1.pollUntilUnlockSync)(this.getPath());
this.logger.debug(`Reading config file: ${this.getPath()}`);
const obj = (0, kit_1.parseJsonMap)(fs.readFileSync(this.getPath(), 'utf8'));
this.setContentsFromFileContents(obj, fs.statSync(this.getPath(), { bigint: true }).mtimeNs);
}
// Necessarily set this even when an error happens to avoid infinite re-reading.
// To attempt another read, pass `force=true`.
this.hasRead = true;
return this.getContents();
}
catch (err) {
this.hasRead = true;
if (err.code === 'ENOENT') {
if (!throwOnNotFound) {
this.setContentsFromFileContents({});
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() {
const lockResponse = await (0, fileLocking_1.lockInit)(this.getPath());
// lock the file. Returns an unlock function to call when done.
try {
const fileTimestamp = (await fs.promises.stat(this.getPath(), { bigint: true })).mtimeNs;
const fileContents = (0, kit_1.parseJsonMap)(await fs.promises.readFile(this.getPath(), 'utf8'), this.getPath());
this.logAndMergeContents(fileTimestamp, fileContents);
}
catch (err) {
this.handleWriteError(err);
}
// write the merged LWWMap to file
await lockResponse.writeAndUnlock(JSON.stringify(this.getContents(), 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() {
const lockResponse = (0, fileLocking_1.lockInitSync)(this.getPath());
try {
// get the file modstamp. Do this after the lock acquisition in case the file is being written to.
const fileTimestamp = fs.statSync(this.getPath(), { bigint: true }).mtimeNs;
const fileContents = (0, kit_1.parseJsonMap)(fs.readFileSync(this.getPath(), 'utf8'), this.getPath());
this.logAndMergeContents(fileTimestamp, fileContents);
}
catch (err) {
this.handleWriteError(err);
}
// write the merged LWWMap to file
lockResponse.writeAndUnlock(JSON.stringify(this.getContents(), 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 this.access(node_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(node_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 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');
}
// 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(Boolean(this.options.isGlobal));
if (this.options.isGlobal === true || this.options.isState === true) {
configRootFolder = (0, node_path_1.join)(configRootFolder, this.options.stateFolder ?? global_1.Global.SFDX_STATE_FOLDER);
}
this.path = (0, node_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);
}
// method exists to share code between write() and writeSync()
logAndMergeContents(fileTimestamp, fileContents) {
this.logger.debug(`Existing file contents on filesystem (timestamp: ${fileTimestamp.toString()}`, fileContents);
this.logger.debug('Contents in configFile in-memory', this.getContents());
// read the file contents into a LWWMap using the modstamp
const stateFromFile = (0, lwwMap_1.stateFromContents)(fileContents, fileTimestamp);
// merge the new contents into the in-memory LWWMap
this.contents.merge(stateFromFile);
this.logger.debug('Result from merge', this.getContents());
}
// shared error handling for both write() and writeSync()
handleWriteError(err) {
if (err instanceof Error && err.message.includes('ENOENT')) {
this.logger.debug(`No file found at ${this.getPath()}. Write will create it.`);
}
else {
throw err;
}
}
}
exports.ConfigFile = ConfigFile;
//# sourceMappingURL=configFile.js.map
;