UNPKG

@salesforce/core

Version:

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

397 lines 16.7 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 */ 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