UNPKG

3.88 kBJavaScriptView Raw
1'use strict';
2
3const fs = require('fs');
4const path = require('path');
5
6const CONFIG_FILE_NAME = Symbol('configuration file name');
7const LOCATIONS = Symbol('configuration file location');
8const SERIALIZERS = Symbol('configuration file serializers');
9
10function suffixPath(configLocation, configFileName) {
11 const suffix = path.sep + configFileName;
12 return configLocation.substr(-suffix.length) !== suffix
13 ? configLocation + suffix
14 : configLocation;
15}
16
17function readJsonOrReturnObject(jsonPath) {
18 try {
19 return JSON.parse(fs.readFileSync(jsonPath));
20 } catch (_error) {
21 return {};
22 }
23}
24
25/**
26 * Manages a unified configuration file for all modules that register their config.
27 */
28class ConfigManager {
29 /**
30 * @param {Array} possibleLocations A list of possible config file locations, from high to low priority.
31 * @param {string} configFileName The name of the config file.
32 *
33 * @constructor
34 */
35 constructor(possibleLocations, configFileName) {
36 this[CONFIG_FILE_NAME] = configFileName;
37 this[LOCATIONS] = possibleLocations.filter(possibleLocation => !!possibleLocation);
38 this[SERIALIZERS] = {};
39
40 if (!this[LOCATIONS].length) {
41 throw new Error('No config location');
42 }
43
44 this.read();
45 }
46
47 /**
48 * @param {string} configName A key of the configuration object that you can share with other modules, or not.
49 * @param {*} defaultValue If no configuration is found, use this.
50 * @param {*|function(config): ?*} [serialize]
51 *
52 * @return {*}
53 */
54 registerConfig(configName, defaultValue, serialize) {
55 if (!this[configName]) {
56 this[configName] = defaultValue || null;
57 }
58
59 Object.assign(this[SERIALIZERS], {
60 [configName]: serialize
61 });
62
63 return this[configName];
64 }
65
66 /**
67 * Checks the path & exist status for all possible configuration file locations.
68 *
69 * @return {Array}
70 */
71 getStatus() {
72 return this[LOCATIONS].filter(configLocation => !!configLocation)
73 .map(configLocation => suffixPath(configLocation, this[CONFIG_FILE_NAME]))
74 .map(configLocation => ({
75 path: configLocation,
76 exists: fs.existsSync(configLocation)
77 }));
78 }
79
80 /**
81 * Read all possible configuration files and assign them to one new object.
82 */
83 read() {
84 Object.assign(
85 this,
86 this.getStatus().reduce(
87 (config, file) =>
88 file.exists ? Object.assign(readJsonOrReturnObject(file.path), config) : config,
89 {}
90 )
91 );
92 }
93
94 /**
95 * Save a stringified version of the configuration object to a given location.
96 *
97 * @param {string} configLocation (Preferably absolute) path to the dot-rc file, or its intended directory.
98 *
99 * @return {Promise} Resolves to the successful configuration location.
100 */
101 save(configLocation) {
102 if (!configLocation) {
103 const configStatus = this.getStatus();
104 configLocation = (
105 configStatus.find(loc => loc.exists) || configStatus[configStatus.length - 1]
106 ).path;
107 }
108
109 const suffix = path.sep + this[CONFIG_FILE_NAME];
110 if (configLocation.substr(-suffix.length) !== suffix) {
111 configLocation += suffix;
112 }
113
114 return new Promise((resolve, reject) =>
115 fs.writeFile(configLocation, this.toString(), error => {
116 return error ? reject(error) : resolve(configLocation);
117 })
118 );
119 }
120
121 /**
122 * Prepare a stringified version of the configuration object - one that can be deserialized too.
123 *
124 * @return {string}
125 */
126 toString() {
127 const serializedConfig = Object.keys(this[SERIALIZERS]).reduce((config, serializerName) => {
128 const serializerFn = this[SERIALIZERS][serializerName];
129 const serialized =
130 typeof serializerFn === 'function'
131 ? serializerFn(this[serializerName])
132 : this[serializerName];
133
134 return serialized !== null && serialized !== undefined
135 ? Object.assign(config, {
136 [serializerName]: serialized
137 })
138 : config;
139 }, {});
140
141 return JSON.stringify(serializedConfig, null, '\t');
142 }
143}
144
145module.exports = ConfigManager;