UNPKG

13.5 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2020, salesforce.com, inc.
4 * All rights reserved.
5 * Licensed under the BSD 3-Clause license.
6 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9exports.ConfigFile = void 0;
10const fs = require("fs");
11const fs_1 = require("fs");
12const os_1 = require("os");
13const path_1 = require("path");
14const ts_types_1 = require("@salesforce/ts-types");
15const kit_1 = require("@salesforce/kit");
16const global_1 = require("../global");
17const logger_1 = require("../logger");
18const sfError_1 = require("../sfError");
19const internal_1 = require("../util/internal");
20const configStore_1 = require("./configStore");
21/**
22 * Represents a json config file used to manage settings and state. Global config
23 * files are stored in the home directory hidden state folder (.sfdx) and local config
24 * files are stored in the project path, either in the hidden state folder or wherever
25 * specified.
26 *
27 * ```
28 * class MyConfig extends ConfigFile {
29 * public static getFileName(): string {
30 * return 'myConfigFilename.json';
31 * }
32 * }
33 * const myConfig = await MyConfig.create({
34 * isGlobal: true
35 * });
36 * myConfig.set('mykey', 'myvalue');
37 * await myConfig.write();
38 * ```
39 */
40class ConfigFile extends configStore_1.BaseConfigStore {
41 /**
42 * Create an instance of a config file without reading the file. Call `read` or `readSync`
43 * after creating the ConfigFile OR instantiate with {@link ConfigFile.create} instead.
44 *
45 * @param options The options for the class instance
46 * @ignore
47 */
48 constructor(options) {
49 super(options);
50 // whether file contents have been read
51 this.hasRead = false;
52 this.logger = logger_1.Logger.childFromRoot(this.constructor.name);
53 const statics = this.constructor;
54 let defaultOptions = {};
55 try {
56 defaultOptions = statics.getDefaultOptions();
57 }
58 catch (e) {
59 /* Some implementations don't let you call default options */
60 }
61 // Merge default and passed in options
62 this.options = Object.assign(defaultOptions, this.options);
63 }
64 /**
65 * Returns the config's filename.
66 */
67 static getFileName() {
68 // Can not have abstract static methods, so throw a runtime error.
69 throw new sfError_1.SfError('Unknown filename for config file.');
70 }
71 /**
72 * Returns the default options for the config file.
73 *
74 * @param isGlobal If the file should be stored globally or locally.
75 * @param filename The name of the config file.
76 */
77 static getDefaultOptions(isGlobal = false, filename) {
78 return {
79 isGlobal,
80 isState: true,
81 filename: filename || this.getFileName(),
82 stateFolder: global_1.Global.SFDX_STATE_FOLDER,
83 };
84 }
85 /**
86 * Helper used to determine what the local and global folder point to. Returns the file path of the root folder.
87 *
88 * @param isGlobal True if the config should be global. False for local.
89 */
90 static async resolveRootFolder(isGlobal) {
91 return isGlobal ? (0, os_1.homedir)() : await (0, internal_1.resolveProjectPath)();
92 }
93 /**
94 * Helper used to determine what the local and global folder point to. Returns the file path of the root folder.
95 *
96 * @param isGlobal True if the config should be global. False for local.
97 */
98 static resolveRootFolderSync(isGlobal) {
99 return isGlobal ? (0, os_1.homedir)() : (0, internal_1.resolveProjectPathSync)();
100 }
101 /**
102 * Determines if the config file is read/write accessible. Returns `true` if the user has capabilities specified
103 * by perm.
104 *
105 * @param {number} perm The permission.
106 *
107 * **See** {@link https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_access_path_mode_callback}
108 */
109 async access(perm) {
110 try {
111 await fs.promises.access(this.getPath(), perm);
112 return true;
113 }
114 catch (err) {
115 return false;
116 }
117 }
118 /**
119 * Determines if the config file is read/write accessible. Returns `true` if the user has capabilities specified
120 * by perm.
121 *
122 * @param {number} perm The permission.
123 *
124 * **See** {@link https://nodejs.org/dist/latest/docs/api/fs.html#fs_fs_access_path_mode_callback}
125 */
126 accessSync(perm) {
127 try {
128 fs.accessSync(this.getPath(), perm);
129 return true;
130 }
131 catch (err) {
132 return false;
133 }
134 }
135 /**
136 * Read the config file and set the config contents. Returns the config contents of the config file. As an
137 * optimization, files are only read once per process and updated in memory and via `write()`. To force
138 * a read from the filesystem pass `force=true`.
139 * **Throws** *{@link SfError}{ name: 'UnexpectedJsonFileFormat' }* There was a problem reading or parsing the file.
140 *
141 * @param [throwOnNotFound = false] Optionally indicate if a throw should occur on file read.
142 * @param [force = false] Optionally force the file to be read from disk even when already read within the process.
143 */
144 async read(throwOnNotFound = false, force = false) {
145 try {
146 // Only need to read config files once. They are kept up to date
147 // internally and updated persistently via write().
148 if (!this.hasRead || force) {
149 this.logger.info(`Reading config file: ${this.getPath()}`);
150 const obj = (0, kit_1.parseJsonMap)(await fs.promises.readFile(this.getPath(), 'utf8'));
151 this.setContentsFromObject(obj);
152 }
153 return this.getContents();
154 }
155 catch (err) {
156 if (err.code === 'ENOENT') {
157 if (!throwOnNotFound) {
158 this.setContents();
159 return this.getContents();
160 }
161 }
162 throw err;
163 }
164 finally {
165 // Necessarily set this even when an error happens to avoid infinite re-reading.
166 // To attempt another read, pass `force=true`.
167 this.hasRead = true;
168 }
169 }
170 /**
171 * Read the config file and set the config contents. Returns the config contents of the config file. As an
172 * optimization, files are only read once per process and updated in memory and via `write()`. To force
173 * a read from the filesystem pass `force=true`.
174 * **Throws** *{@link SfError}{ name: 'UnexpectedJsonFileFormat' }* There was a problem reading or parsing the file.
175 *
176 * @param [throwOnNotFound = false] Optionally indicate if a throw should occur on file read.
177 * @param [force = false] Optionally force the file to be read from disk even when already read within the process.
178 */
179 readSync(throwOnNotFound = false, force = false) {
180 try {
181 // Only need to read config files once. They are kept up to date
182 // internally and updated persistently via write().
183 if (!this.hasRead || force) {
184 this.logger.info(`Reading config file: ${this.getPath()}`);
185 const obj = (0, kit_1.parseJsonMap)(fs.readFileSync(this.getPath(), 'utf8'));
186 this.setContentsFromObject(obj);
187 }
188 return this.getContents();
189 }
190 catch (err) {
191 if (err.code === 'ENOENT') {
192 if (!throwOnNotFound) {
193 this.setContents();
194 return this.getContents();
195 }
196 }
197 throw err;
198 }
199 finally {
200 // Necessarily set this even when an error happens to avoid infinite re-reading.
201 // To attempt another read, pass `force=true`.
202 this.hasRead = true;
203 }
204 }
205 /**
206 * Write the config file with new contents. If no new contents are provided it will write the existing config
207 * contents that were set from {@link ConfigFile.read}, or an empty file if {@link ConfigFile.read} was not called.
208 *
209 * @param newContents The new contents of the file.
210 */
211 async write(newContents) {
212 if (newContents) {
213 this.setContents(newContents);
214 }
215 try {
216 await fs.promises.mkdir((0, path_1.dirname)(this.getPath()), { recursive: true });
217 }
218 catch (err) {
219 throw sfError_1.SfError.wrap(err);
220 }
221 this.logger.info(`Writing to config file: ${this.getPath()}`);
222 await fs.promises.writeFile(this.getPath(), JSON.stringify(this.toObject(), null, 2));
223 return this.getContents();
224 }
225 /**
226 * Write the config file with new contents. If no new contents are provided it will write the existing config
227 * contents that were set from {@link ConfigFile.read}, or an empty file if {@link ConfigFile.read} was not called.
228 *
229 * @param newContents The new contents of the file.
230 */
231 writeSync(newContents) {
232 if ((0, ts_types_1.isPlainObject)(newContents)) {
233 this.setContents(newContents);
234 }
235 try {
236 fs.mkdirSync((0, path_1.dirname)(this.getPath()), { recursive: true });
237 }
238 catch (err) {
239 throw sfError_1.SfError.wrap(err);
240 }
241 this.logger.info(`Writing to config file: ${this.getPath()}`);
242 fs.writeFileSync(this.getPath(), JSON.stringify(this.toObject(), null, 2));
243 return this.getContents();
244 }
245 /**
246 * Check to see if the config file exists. Returns `true` if the config file exists and has access, false otherwise.
247 */
248 async exists() {
249 return await this.access(fs_1.constants.R_OK);
250 }
251 /**
252 * Check to see if the config file exists. Returns `true` if the config file exists and has access, false otherwise.
253 */
254 existsSync() {
255 return this.accessSync(fs_1.constants.R_OK);
256 }
257 /**
258 * Get the stats of the file. Returns the stats of the file.
259 *
260 * {@link fs.stat}
261 */
262 async stat() {
263 return fs.promises.stat(this.getPath());
264 }
265 /**
266 * Get the stats of the file. Returns the stats of the file.
267 *
268 * {@link fs.stat}
269 */
270 statSync() {
271 return fs.statSync(this.getPath());
272 }
273 /**
274 * Delete the config file if it exists.
275 *
276 * **Throws** *`Error`{ name: 'TargetFileNotFound' }* If the {@link ConfigFile.getFilename} file is not found.
277 * {@link fs.unlink}
278 */
279 async unlink() {
280 const exists = await this.exists();
281 if (exists) {
282 return await fs.promises.unlink(this.getPath());
283 }
284 throw new sfError_1.SfError(`Target file doesn't exist. path: ${this.getPath()}`, 'TargetFileNotFound');
285 }
286 /**
287 * Delete the config file if it exists.
288 *
289 * **Throws** *`Error`{ name: 'TargetFileNotFound' }* If the {@link ConfigFile.getFilename} file is not found.
290 * {@link fs.unlink}
291 */
292 unlinkSync() {
293 const exists = this.existsSync();
294 if (exists) {
295 return fs.unlinkSync(this.getPath());
296 }
297 throw new sfError_1.SfError(`Target file doesn't exist. path: ${this.getPath()}`, 'TargetFileNotFound');
298 }
299 /**
300 * Returns the absolute path to the config file.
301 *
302 * The first time getPath is called, the path is resolved and becomes immutable. This allows implementers to
303 * override options properties, like filePath, on the init method for async creation. If that is required for
304 * creation, the config files can not be synchronously created.
305 */
306 getPath() {
307 if (!this.path) {
308 if (!this.options.filename) {
309 throw new sfError_1.SfError('The ConfigOptions filename parameter is invalid.', 'InvalidParameter');
310 }
311 const _isGlobal = (0, ts_types_1.isBoolean)(this.options.isGlobal) && this.options.isGlobal;
312 const _isState = (0, ts_types_1.isBoolean)(this.options.isState) && this.options.isState;
313 // Don't let users store config files in homedir without being in the state folder.
314 let configRootFolder = this.options.rootFolder
315 ? this.options.rootFolder
316 : ConfigFile.resolveRootFolderSync(!!this.options.isGlobal);
317 if (_isGlobal || _isState) {
318 configRootFolder = (0, path_1.join)(configRootFolder, this.options.stateFolder || global_1.Global.SFDX_STATE_FOLDER);
319 }
320 this.path = (0, path_1.join)(configRootFolder, this.options.filePath ? this.options.filePath : '', this.options.filename);
321 }
322 return this.path;
323 }
324 /**
325 * Returns `true` if this config is using the global path, `false` otherwise.
326 */
327 isGlobal() {
328 return !!this.options.isGlobal;
329 }
330 /**
331 * Used to initialize asynchronous components.
332 *
333 * **Throws** *`Error`{ code: 'ENOENT' }* If the {@link ConfigFile.getFilename} file is not found when
334 * options.throwOnNotFound is true.
335 */
336 async init() {
337 await super.init();
338 // Read the file, which also sets the path and throws any errors around project paths.
339 await this.read(this.options.throwOnNotFound);
340 }
341}
342exports.ConfigFile = ConfigFile;
343//# sourceMappingURL=configFile.js.map
\No newline at end of file