1 | ;
|
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 | */
|
8 | Object.defineProperty(exports, "__esModule", { value: true });
|
9 | exports.ConfigFile = void 0;
|
10 | const fs = require("fs");
|
11 | const fs_1 = require("fs");
|
12 | const os_1 = require("os");
|
13 | const path_1 = require("path");
|
14 | const ts_types_1 = require("@salesforce/ts-types");
|
15 | const kit_1 = require("@salesforce/kit");
|
16 | const global_1 = require("../global");
|
17 | const logger_1 = require("../logger");
|
18 | const sfError_1 = require("../sfError");
|
19 | const internal_1 = require("../util/internal");
|
20 | const 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 | */
|
40 | class 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 | }
|
342 | exports.ConfigFile = ConfigFile;
|
343 | //# sourceMappingURL=configFile.js.map |
\ | No newline at end of file |