index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. "use strict";
  2. var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
  3. if (!privateMap.has(receiver)) {
  4. throw new TypeError("attempted to set private field on non-instance");
  5. }
  6. privateMap.set(receiver, value);
  7. return value;
  8. };
  9. var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
  10. if (!privateMap.has(receiver)) {
  11. throw new TypeError("attempted to get private field on non-instance");
  12. }
  13. return privateMap.get(receiver);
  14. };
  15. var _a, _b;
  16. var _validator, _encryptionKey, _options, _defaultValues;
  17. Object.defineProperty(exports, "__esModule", { value: true });
  18. const fs = require("fs");
  19. const path = require("path");
  20. const crypto = require("crypto");
  21. const assert = require("assert");
  22. const events_1 = require("events");
  23. const dotProp = require("dot-prop");
  24. const makeDir = require("make-dir");
  25. const pkgUp = require("pkg-up");
  26. const envPaths = require("env-paths");
  27. const atomically = require("atomically");
  28. const ajv_1 = require("ajv");
  29. const ajv_formats_1 = require("ajv-formats");
  30. const debounceFn = require("debounce-fn");
  31. const semver = require("semver");
  32. const onetime = require("onetime");
  33. const encryptionAlgorithm = 'aes-256-cbc';
  34. const createPlainObject = () => {
  35. return Object.create(null);
  36. };
  37. const isExist = (data) => {
  38. return data !== undefined && data !== null;
  39. };
  40. // Prevent caching of this module so module.parent is always accurate
  41. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  42. delete require.cache[__filename];
  43. const parentDir = path.dirname((_b = (_a = module.parent) === null || _a === void 0 ? void 0 : _a.filename) !== null && _b !== void 0 ? _b : '.');
  44. const checkValueType = (key, value) => {
  45. const nonJsonTypes = new Set([
  46. 'undefined',
  47. 'symbol',
  48. 'function'
  49. ]);
  50. const type = typeof value;
  51. if (nonJsonTypes.has(type)) {
  52. throw new TypeError(`Setting a value of type \`${type}\` for key \`${key}\` is not allowed as it's not supported by JSON`);
  53. }
  54. };
  55. const INTERNAL_KEY = '__internal__';
  56. const MIGRATION_KEY = `${INTERNAL_KEY}.migrations.version`;
  57. class Conf {
  58. constructor(partialOptions = {}) {
  59. var _a;
  60. _validator.set(this, void 0);
  61. _encryptionKey.set(this, void 0);
  62. _options.set(this, void 0);
  63. _defaultValues.set(this, {});
  64. this._deserialize = value => JSON.parse(value);
  65. this._serialize = value => JSON.stringify(value, null, '\t');
  66. const options = {
  67. configName: 'config',
  68. fileExtension: 'json',
  69. projectSuffix: 'nodejs',
  70. clearInvalidConfig: false,
  71. accessPropertiesByDotNotation: true,
  72. ...partialOptions
  73. };
  74. const getPackageData = onetime(() => {
  75. const packagePath = pkgUp.sync({ cwd: parentDir });
  76. // Can't use `require` because of Webpack being annoying:
  77. // https://github.com/webpack/webpack/issues/196
  78. const packageData = packagePath && JSON.parse(fs.readFileSync(packagePath, 'utf8'));
  79. return packageData !== null && packageData !== void 0 ? packageData : {};
  80. });
  81. if (!options.cwd) {
  82. if (!options.projectName) {
  83. options.projectName = getPackageData().name;
  84. }
  85. if (!options.projectName) {
  86. throw new Error('Project name could not be inferred. Please specify the `projectName` option.');
  87. }
  88. options.cwd = envPaths(options.projectName, { suffix: options.projectSuffix }).config;
  89. }
  90. __classPrivateFieldSet(this, _options, options);
  91. if (options.schema) {
  92. if (typeof options.schema !== 'object') {
  93. throw new TypeError('The `schema` option must be an object.');
  94. }
  95. const ajv = new ajv_1.default({
  96. allErrors: true,
  97. useDefaults: true
  98. });
  99. ajv_formats_1.default(ajv);
  100. const schema = {
  101. type: 'object',
  102. properties: options.schema
  103. };
  104. __classPrivateFieldSet(this, _validator, ajv.compile(schema));
  105. for (const [key, value] of Object.entries(options.schema)) {
  106. if (value === null || value === void 0 ? void 0 : value.default) {
  107. __classPrivateFieldGet(this, _defaultValues)[key] = value.default;
  108. }
  109. }
  110. }
  111. if (options.defaults) {
  112. __classPrivateFieldSet(this, _defaultValues, {
  113. ...__classPrivateFieldGet(this, _defaultValues),
  114. ...options.defaults
  115. });
  116. }
  117. if (options.serialize) {
  118. this._serialize = options.serialize;
  119. }
  120. if (options.deserialize) {
  121. this._deserialize = options.deserialize;
  122. }
  123. this.events = new events_1.EventEmitter();
  124. __classPrivateFieldSet(this, _encryptionKey, options.encryptionKey);
  125. const fileExtension = options.fileExtension ? `.${options.fileExtension}` : '';
  126. this.path = path.resolve(options.cwd, `${(_a = options.configName) !== null && _a !== void 0 ? _a : 'config'}${fileExtension}`);
  127. const fileStore = this.store;
  128. const store = Object.assign(createPlainObject(), options.defaults, fileStore);
  129. this._validate(store);
  130. try {
  131. assert.deepEqual(fileStore, store);
  132. }
  133. catch (_b) {
  134. this.store = store;
  135. }
  136. if (options.watch) {
  137. this._watch();
  138. }
  139. if (options.migrations) {
  140. if (!options.projectVersion) {
  141. options.projectVersion = getPackageData().version;
  142. }
  143. if (!options.projectVersion) {
  144. throw new Error('Project version could not be inferred. Please specify the `projectVersion` option.');
  145. }
  146. this._migrate(options.migrations, options.projectVersion);
  147. }
  148. }
  149. get(key, defaultValue) {
  150. if (__classPrivateFieldGet(this, _options).accessPropertiesByDotNotation) {
  151. return this._get(key, defaultValue);
  152. }
  153. return key in this.store ? this.store[key] : defaultValue;
  154. }
  155. set(key, value) {
  156. if (typeof key !== 'string' && typeof key !== 'object') {
  157. throw new TypeError(`Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`);
  158. }
  159. if (typeof key !== 'object' && value === undefined) {
  160. throw new TypeError('Use `delete()` to clear values');
  161. }
  162. if (this._containsReservedKey(key)) {
  163. throw new TypeError(`Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`);
  164. }
  165. const { store } = this;
  166. const set = (key, value) => {
  167. checkValueType(key, value);
  168. if (__classPrivateFieldGet(this, _options).accessPropertiesByDotNotation) {
  169. dotProp.set(store, key, value);
  170. }
  171. else {
  172. store[key] = value;
  173. }
  174. };
  175. if (typeof key === 'object') {
  176. const object = key;
  177. for (const [key, value] of Object.entries(object)) {
  178. set(key, value);
  179. }
  180. }
  181. else {
  182. set(key, value);
  183. }
  184. this.store = store;
  185. }
  186. /**
  187. Check if an item exists.
  188. @param key - The key of the item to check.
  189. */
  190. has(key) {
  191. if (__classPrivateFieldGet(this, _options).accessPropertiesByDotNotation) {
  192. return dotProp.has(this.store, key);
  193. }
  194. return key in this.store;
  195. }
  196. /**
  197. Reset items to their default values, as defined by the `defaults` or `schema` option.
  198. @see `clear()` to reset all items.
  199. @param keys - The keys of the items to reset.
  200. */
  201. reset(...keys) {
  202. for (const key of keys) {
  203. if (isExist(__classPrivateFieldGet(this, _defaultValues)[key])) {
  204. this.set(key, __classPrivateFieldGet(this, _defaultValues)[key]);
  205. }
  206. }
  207. }
  208. /**
  209. Delete an item.
  210. @param key - The key of the item to delete.
  211. */
  212. delete(key) {
  213. const { store } = this;
  214. if (__classPrivateFieldGet(this, _options).accessPropertiesByDotNotation) {
  215. dotProp.delete(store, key);
  216. }
  217. else {
  218. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  219. delete store[key];
  220. }
  221. this.store = store;
  222. }
  223. /**
  224. Delete all items.
  225. This resets known items to their default values, if defined by the `defaults` or `schema` option.
  226. */
  227. clear() {
  228. this.store = createPlainObject();
  229. for (const key of Object.keys(__classPrivateFieldGet(this, _defaultValues))) {
  230. this.reset(key);
  231. }
  232. }
  233. /**
  234. Watches the given `key`, calling `callback` on any changes.
  235. @param key - The key wo watch.
  236. @param callback - A callback function that is called on any changes. When a `key` is first set `oldValue` will be `undefined`, and when a key is deleted `newValue` will be `undefined`.
  237. @returns A function, that when called, will unsubscribe.
  238. */
  239. onDidChange(key, callback) {
  240. if (typeof key !== 'string') {
  241. throw new TypeError(`Expected \`key\` to be of type \`string\`, got ${typeof key}`);
  242. }
  243. if (typeof callback !== 'function') {
  244. throw new TypeError(`Expected \`callback\` to be of type \`function\`, got ${typeof callback}`);
  245. }
  246. return this._handleChange(() => this.get(key), callback);
  247. }
  248. /**
  249. Watches the whole config object, calling `callback` on any changes.
  250. @param callback - A callback function that is called on any changes. When a `key` is first set `oldValue` will be `undefined`, and when a key is deleted `newValue` will be `undefined`.
  251. @returns A function, that when called, will unsubscribe.
  252. */
  253. onDidAnyChange(callback) {
  254. if (typeof callback !== 'function') {
  255. throw new TypeError(`Expected \`callback\` to be of type \`function\`, got ${typeof callback}`);
  256. }
  257. return this._handleChange(() => this.store, callback);
  258. }
  259. get size() {
  260. return Object.keys(this.store).length;
  261. }
  262. get store() {
  263. try {
  264. const data = fs.readFileSync(this.path, __classPrivateFieldGet(this, _encryptionKey) ? null : 'utf8');
  265. const dataString = this._encryptData(data);
  266. const deserializedData = this._deserialize(dataString);
  267. this._validate(deserializedData);
  268. return Object.assign(createPlainObject(), deserializedData);
  269. }
  270. catch (error) {
  271. if (error.code === 'ENOENT') {
  272. this._ensureDirectory();
  273. return createPlainObject();
  274. }
  275. if (__classPrivateFieldGet(this, _options).clearInvalidConfig && error.name === 'SyntaxError') {
  276. return createPlainObject();
  277. }
  278. throw error;
  279. }
  280. }
  281. set store(value) {
  282. this._ensureDirectory();
  283. this._validate(value);
  284. this._write(value);
  285. this.events.emit('change');
  286. }
  287. *[(_validator = new WeakMap(), _encryptionKey = new WeakMap(), _options = new WeakMap(), _defaultValues = new WeakMap(), Symbol.iterator)]() {
  288. for (const [key, value] of Object.entries(this.store)) {
  289. yield [key, value];
  290. }
  291. }
  292. _encryptData(data) {
  293. if (!__classPrivateFieldGet(this, _encryptionKey)) {
  294. return data.toString();
  295. }
  296. try {
  297. // Check if an initialization vector has been used to encrypt the data
  298. if (__classPrivateFieldGet(this, _encryptionKey)) {
  299. try {
  300. if (data.slice(16, 17).toString() === ':') {
  301. const initializationVector = data.slice(0, 16);
  302. const password = crypto.pbkdf2Sync(__classPrivateFieldGet(this, _encryptionKey), initializationVector.toString(), 10000, 32, 'sha512');
  303. const decipher = crypto.createDecipheriv(encryptionAlgorithm, password, initializationVector);
  304. data = Buffer.concat([decipher.update(Buffer.from(data.slice(17))), decipher.final()]).toString('utf8');
  305. }
  306. else {
  307. const decipher = crypto.createDecipher(encryptionAlgorithm, __classPrivateFieldGet(this, _encryptionKey));
  308. data = Buffer.concat([decipher.update(Buffer.from(data)), decipher.final()]).toString('utf8');
  309. }
  310. }
  311. catch (_a) { }
  312. }
  313. }
  314. catch (_b) { }
  315. return data.toString();
  316. }
  317. _handleChange(getter, callback) {
  318. let currentValue = getter();
  319. const onChange = () => {
  320. const oldValue = currentValue;
  321. const newValue = getter();
  322. try {
  323. // TODO: Use `util.isDeepStrictEqual` when targeting Node.js 10
  324. assert.deepEqual(newValue, oldValue);
  325. }
  326. catch (_a) {
  327. currentValue = newValue;
  328. callback.call(this, newValue, oldValue);
  329. }
  330. };
  331. this.events.on('change', onChange);
  332. return () => this.events.removeListener('change', onChange);
  333. }
  334. _validate(data) {
  335. if (!__classPrivateFieldGet(this, _validator)) {
  336. return;
  337. }
  338. const valid = __classPrivateFieldGet(this, _validator).call(this, data);
  339. if (valid || !__classPrivateFieldGet(this, _validator).errors) {
  340. return;
  341. }
  342. const errors = __classPrivateFieldGet(this, _validator).errors
  343. .map(({ dataPath, message = '' }) => `\`${dataPath.slice(1)}\` ${message}`);
  344. throw new Error('Config schema violation: ' + errors.join('; '));
  345. }
  346. _ensureDirectory() {
  347. // TODO: Use `fs.mkdirSync` `recursive` option when targeting Node.js 12.
  348. // Ensure the directory exists as it could have been deleted in the meantime.
  349. makeDir.sync(path.dirname(this.path));
  350. }
  351. _write(value) {
  352. let data = this._serialize(value);
  353. if (__classPrivateFieldGet(this, _encryptionKey)) {
  354. const initializationVector = crypto.randomBytes(16);
  355. const password = crypto.pbkdf2Sync(__classPrivateFieldGet(this, _encryptionKey), initializationVector.toString(), 10000, 32, 'sha512');
  356. const cipher = crypto.createCipheriv(encryptionAlgorithm, password, initializationVector);
  357. data = Buffer.concat([initializationVector, Buffer.from(':'), cipher.update(Buffer.from(data)), cipher.final()]);
  358. }
  359. // Temporary workaround for Conf being packaged in a Ubuntu Snap app.
  360. // See https://github.com/sindresorhus/conf/pull/82
  361. if (process.env.SNAP) {
  362. fs.writeFileSync(this.path, data);
  363. }
  364. else {
  365. try {
  366. atomically.writeFileSync(this.path, data);
  367. }
  368. catch (error) {
  369. // Fix for https://github.com/sindresorhus/electron-store/issues/106
  370. // Sometimes on Windows, we will get an EXDEV error when atomic writing
  371. // (even though to the same directory), so we fall back to non atomic write
  372. if (error.code === 'EXDEV') {
  373. fs.writeFileSync(this.path, data);
  374. return;
  375. }
  376. throw error;
  377. }
  378. }
  379. }
  380. _watch() {
  381. this._ensureDirectory();
  382. if (!fs.existsSync(this.path)) {
  383. this._write(createPlainObject());
  384. }
  385. fs.watch(this.path, { persistent: false }, debounceFn(() => {
  386. // On Linux and Windows, writing to the config file emits a `rename` event, so we skip checking the event type.
  387. this.events.emit('change');
  388. }, { wait: 100 }));
  389. }
  390. _migrate(migrations, versionToMigrate) {
  391. let previousMigratedVersion = this._get(MIGRATION_KEY, '0.0.0');
  392. const newerVersions = Object.keys(migrations)
  393. .filter(candidateVersion => this._shouldPerformMigration(candidateVersion, previousMigratedVersion, versionToMigrate));
  394. let storeBackup = { ...this.store };
  395. for (const version of newerVersions) {
  396. try {
  397. const migration = migrations[version];
  398. migration(this);
  399. this._set(MIGRATION_KEY, version);
  400. previousMigratedVersion = version;
  401. storeBackup = { ...this.store };
  402. }
  403. catch (error) {
  404. this.store = storeBackup;
  405. throw new Error(`Something went wrong during the migration! Changes applied to the store until this failed migration will be restored. ${error}`);
  406. }
  407. }
  408. if (this._isVersionInRangeFormat(previousMigratedVersion) || !semver.eq(previousMigratedVersion, versionToMigrate)) {
  409. this._set(MIGRATION_KEY, versionToMigrate);
  410. }
  411. }
  412. _containsReservedKey(key) {
  413. if (typeof key === 'object') {
  414. const firsKey = Object.keys(key)[0];
  415. if (firsKey === INTERNAL_KEY) {
  416. return true;
  417. }
  418. }
  419. if (typeof key !== 'string') {
  420. return false;
  421. }
  422. if (__classPrivateFieldGet(this, _options).accessPropertiesByDotNotation) {
  423. if (key.startsWith(`${INTERNAL_KEY}.`)) {
  424. return true;
  425. }
  426. return false;
  427. }
  428. return false;
  429. }
  430. _isVersionInRangeFormat(version) {
  431. return semver.clean(version) === null;
  432. }
  433. _shouldPerformMigration(candidateVersion, previousMigratedVersion, versionToMigrate) {
  434. if (this._isVersionInRangeFormat(candidateVersion)) {
  435. if (previousMigratedVersion !== '0.0.0' && semver.satisfies(previousMigratedVersion, candidateVersion)) {
  436. return false;
  437. }
  438. return semver.satisfies(versionToMigrate, candidateVersion);
  439. }
  440. if (semver.lte(candidateVersion, previousMigratedVersion)) {
  441. return false;
  442. }
  443. if (semver.gt(candidateVersion, versionToMigrate)) {
  444. return false;
  445. }
  446. return true;
  447. }
  448. _get(key, defaultValue) {
  449. return dotProp.get(this.store, key, defaultValue);
  450. }
  451. _set(key, value) {
  452. const { store } = this;
  453. dotProp.set(store, key, value);
  454. this.store = store;
  455. }
  456. }
  457. exports.default = Conf;
  458. // For CommonJS default export support
  459. module.exports = Conf;
  460. module.exports.default = Conf;