1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | var path = require('path');
|
18 | var fs = require('fs');
|
19 | var mkdirp = require('mkdirp');
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | module.exports = ConfigFile;
|
26 |
|
27 | function ConfigFile(fileName, ordering) {
|
28 | this.fileName = path.resolve(fileName);
|
29 |
|
30 | this.ordering = ordering;
|
31 |
|
32 |
|
33 | this.style = null;
|
34 |
|
35 |
|
36 | this.timestamp = null;
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | this.properties = [];
|
42 |
|
43 |
|
44 | this.changeEvents = [];
|
45 |
|
46 |
|
47 | this.changed = false;
|
48 |
|
49 | this.read();
|
50 | }
|
51 |
|
52 | function configError(memberArray, msg) {
|
53 | if (arguments.length == 1) {
|
54 | msg = memberArray;
|
55 | memberArray = [];
|
56 | }
|
57 | throw new TypeError('Error reading %' + path.relative(process.cwd(), this.fileName) + '%\n\t' +
|
58 | (memberArray.length ? '%' + memberArray.join('.') + '% ' : 'File ') + msg + '.');
|
59 | }
|
60 |
|
61 | function propertyEquals(propA, propB) {
|
62 | if (propA instanceof Array || propB instanceof Array) {
|
63 | if (!(propA instanceof Array && propB instanceof Array))
|
64 | return false;
|
65 |
|
66 | if (propA.length != propB.length)
|
67 | return false;
|
68 |
|
69 | return !propA.some(function(itemA, index) {
|
70 | var itemB = propB[index];
|
71 | return itemA.key != itemB.key || !propertyEquals(itemA.value, itemB.value);
|
72 | });
|
73 | }
|
74 | else {
|
75 | return propA.value === propB.value;
|
76 | }
|
77 | }
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | function setProperty(properties, key, value, ordering) {
|
85 | var changed = false;
|
86 | if (properties.some(function(prop) {
|
87 | if (prop.key == key) {
|
88 |
|
89 | if (!propertyEquals(prop.value, value))
|
90 | changed = true;
|
91 | prop.value = value;
|
92 | return true;
|
93 | }
|
94 | }))
|
95 | return changed;
|
96 |
|
97 | if (!ordering || !ordering.length) {
|
98 | properties.push({
|
99 | key: key,
|
100 | value: value
|
101 | });
|
102 | return true;
|
103 | }
|
104 |
|
105 |
|
106 | var orderIndex = orderingIndex(ordering, key);
|
107 |
|
108 |
|
109 | var maxOrderIndex = properties.length, minOrderIndex = 0;
|
110 | if (orderIndex != -1)
|
111 | properties.forEach(function(prop, index) {
|
112 |
|
113 | var propOrderIndex = orderingIndex(ordering, prop.key);
|
114 | if (propOrderIndex != -1) {
|
115 | if (propOrderIndex < orderIndex && index + 1 > minOrderIndex && index < maxOrderIndex)
|
116 | minOrderIndex = index + 1;
|
117 | if (propOrderIndex > orderIndex && index < maxOrderIndex && index >= minOrderIndex)
|
118 | maxOrderIndex = index;
|
119 | }
|
120 | });
|
121 |
|
122 |
|
123 | orderIndex = -1;
|
124 | for (var i = minOrderIndex; i < maxOrderIndex; i++)
|
125 | if (properties[i].key > key) {
|
126 | orderIndex = i;
|
127 | break;
|
128 | }
|
129 |
|
130 | if (orderIndex == -1)
|
131 | orderIndex = maxOrderIndex;
|
132 |
|
133 | properties.splice(orderIndex, 0, {
|
134 | key: key,
|
135 | value: value
|
136 | });
|
137 |
|
138 | return true;
|
139 | }
|
140 |
|
141 |
|
142 |
|
143 |
|
144 | function getProperty(properties, key) {
|
145 | var propMatch = {
|
146 | index: -1,
|
147 | property: undefined
|
148 | };
|
149 | properties.some(function(prop, index) {
|
150 | if (prop.key == key) {
|
151 | propMatch = {
|
152 | property: prop,
|
153 | index: index
|
154 | };
|
155 | return true;
|
156 | }
|
157 | });
|
158 | return propMatch;
|
159 | }
|
160 |
|
161 | ConfigFile.prototype.rename = function(newName) {
|
162 | newName = path.resolve(newName);
|
163 | if (this.fileName == newName)
|
164 | return;
|
165 | this.originalName = this.originalName || this.timestamp != -1 && this.fileName;
|
166 | this.fileName = newName;
|
167 | try {
|
168 | this.timestamp = fs.statSync(this.fileName).mtime.getTime();
|
169 | }
|
170 | catch(e) {
|
171 | if (e.code != 'ENOENT')
|
172 | throw e;
|
173 | this.timestamp = -1;
|
174 | }
|
175 | this.changed = true;
|
176 | };
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | ConfigFile.prototype.getValue = function(memberArray, type) {
|
184 | var parentProps = this.getProperties(memberArray.slice(0, memberArray.length - 1));
|
185 |
|
186 | if (!parentProps)
|
187 | return;
|
188 |
|
189 | var prop = getProperty(parentProps, memberArray[memberArray.length - 1]).property;
|
190 |
|
191 | if (prop === undefined)
|
192 | return;
|
193 |
|
194 | if (prop.value instanceof Array)
|
195 | configError.call(this, memberArray, 'must be a value');
|
196 |
|
197 | var value = prop.value.value;
|
198 |
|
199 | if (type == 'array' && !(value instanceof Array) || (type && type != 'array' && typeof value != type))
|
200 | configError.call(this, memberArray, 'must be a' + (type == 'array' ? 'n ' : ' ') + type + ' value');
|
201 |
|
202 | return value;
|
203 | };
|
204 |
|
205 | function orderingIndex(ordering, key) {
|
206 | for (var i = 0; i < ordering.length; i++)
|
207 | if (ordering[i] === key || ordering[i] instanceof Array && ordering[i][0] == key)
|
208 | return i;
|
209 | return -1;
|
210 | }
|
211 |
|
212 | function getOrdering(memberArray, ordering) {
|
213 | memberArray.some(function(member) {
|
214 | var orderIndex = orderingIndex(ordering, member);
|
215 | if (orderIndex != -1 && ordering[orderIndex] instanceof Array) {
|
216 | ordering = ordering[orderIndex][1];
|
217 | }
|
218 | else {
|
219 | ordering = [];
|
220 | return true;
|
221 | }
|
222 | });
|
223 | return ordering;
|
224 | }
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 | ConfigFile.prototype.getProperties = function(memberArray, createIfUndefined) {
|
233 | var properties = this.properties;
|
234 | var ordering = this.ordering;
|
235 | var self = this;
|
236 | memberArray.some(function(member, index) {
|
237 | var prop = getProperty(properties, member).property;
|
238 | if (prop) {
|
239 | properties = prop.value;
|
240 | if (!(properties instanceof Array)) {
|
241 | if (createIfUndefined) {
|
242 | configError.call(self, memberArray.slice(0, index + 1), 'should be an object');
|
243 | }
|
244 | else {
|
245 | properties = undefined;
|
246 | return true;
|
247 | }
|
248 | }
|
249 | }
|
250 | else {
|
251 | if (createIfUndefined) {
|
252 | setProperty(properties, member, properties = [], ordering);
|
253 | self.onChange(memberArray);
|
254 | }
|
255 | else {
|
256 | properties = undefined;
|
257 | return true;
|
258 | }
|
259 | }
|
260 | ordering = getOrdering([member], ordering);
|
261 | });
|
262 | return properties;
|
263 | };
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 | ConfigFile.prototype.getObject = function(memberArray, nested, createIfUndefined) {
|
270 | var properties = this.getProperties(memberArray, createIfUndefined);
|
271 |
|
272 | if (!properties)
|
273 | return;
|
274 |
|
275 | var obj = propertiesToObject(properties);
|
276 |
|
277 | var self = this;
|
278 | if (!nested)
|
279 | Object.keys(obj).forEach(function(key) {
|
280 | if (typeof obj[key] == 'object' && obj[key] !== null && !(obj[key] instanceof Array))
|
281 | configError.call(self, memberArray, 'should not contain a nested object at %' + key + '%');
|
282 | });
|
283 |
|
284 | return obj;
|
285 | };
|
286 | function propertiesToObject(properties) {
|
287 | var obj = {};
|
288 | properties.forEach(function(p) {
|
289 | var prop = p.key;
|
290 | var val = p.value;
|
291 |
|
292 | if (val instanceof Array)
|
293 | obj[prop] = propertiesToObject(val);
|
294 | else
|
295 | obj[prop] = val.value;
|
296 | });
|
297 | return obj;
|
298 | }
|
299 |
|
300 | function objectToProperties(obj) {
|
301 | var properties = [];
|
302 | Object.keys(obj).forEach(function(key) {
|
303 | var value = obj[key];
|
304 | if (typeof value == 'object' && !(value instanceof Array) && value !== null)
|
305 | value = objectToProperties(value);
|
306 | else
|
307 | value = { value: value };
|
308 | properties.push({
|
309 | key: key,
|
310 | value: value
|
311 | });
|
312 | });
|
313 | return properties;
|
314 | }
|
315 |
|
316 | ConfigFile.prototype.has = function(memberArray) {
|
317 | var parentProps = this.getProperties(memberArray.slice(0, memberArray.length - 1));
|
318 |
|
319 | if (!parentProps)
|
320 | return false;
|
321 |
|
322 | return getProperty(parentProps, memberArray[memberArray.length - 1]).property !== undefined;
|
323 | };
|
324 |
|
325 |
|
326 | ConfigFile.prototype.remove = function(memberArray, clearParentsIfMadeEmpty) {
|
327 | var parentProps = this.getProperties(memberArray.slice(0, memberArray.length - 1));
|
328 |
|
329 | if (!parentProps)
|
330 | return false;
|
331 |
|
332 | var self = this;
|
333 | var removed = parentProps.some(function(prop, index) {
|
334 | if (prop.key == memberArray[memberArray.length - 1]) {
|
335 | parentProps.splice(index, 1);
|
336 | self.onChange(memberArray.slice(0, memberArray.length - 1));
|
337 | return true;
|
338 | }
|
339 | });
|
340 |
|
341 | if (clearParentsIfMadeEmpty && removed && parentProps.length == 0 && memberArray.length > 1)
|
342 | this.remove(memberArray.slice(0, memberArray.length - 1), true);
|
343 |
|
344 | return removed;
|
345 | };
|
346 |
|
347 | ConfigFile.prototype.clearIfEmpty = function(memberArray) {
|
348 | var props = this.getProperties(memberArray);
|
349 | if (props && !props.length)
|
350 | this.remove(memberArray);
|
351 | };
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 | ConfigFile.prototype.setValue = function(memberArray, value) {
|
358 | var properties = this.getProperties(memberArray.slice(0, memberArray.length - 1), true);
|
359 |
|
360 | var ordering = getOrdering(memberArray.slice(0, memberArray.length - 1), this.ordering);
|
361 |
|
362 | if (setProperty(properties, memberArray[memberArray.length - 1], { value: value }, ordering))
|
363 | this.onChange(memberArray);
|
364 | };
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 | ConfigFile.prototype.setProperties = function(memberArray, properties, clearIfEmpty, keepOrder, extend) {
|
371 | var targetProperties;
|
372 |
|
373 | if (!properties.length) {
|
374 | targetProperties = this.getProperties(memberArray);
|
375 | if (targetProperties && targetProperties.length) {
|
376 | if (clearIfEmpty)
|
377 | this.remove(memberArray);
|
378 | else
|
379 | targetProperties.splice(0, targetProperties.length);
|
380 | this.onChange(memberArray);
|
381 | }
|
382 | else if (!clearIfEmpty)
|
383 | this.getProperties(memberArray, true);
|
384 | return;
|
385 | }
|
386 |
|
387 | targetProperties = this.getProperties(memberArray, true);
|
388 |
|
389 | var ordering;
|
390 | if (!keepOrder)
|
391 | ordering = getOrdering(memberArray.slice(0, memberArray.length - 1), this.ordering);
|
392 |
|
393 | var self = this;
|
394 |
|
395 | var setKeys = [];
|
396 | properties.forEach(function(prop) {
|
397 | setKeys.push(prop.key);
|
398 | if (setProperty(targetProperties, prop.key, prop.value, ordering))
|
399 | self.onChange(memberArray);
|
400 | });
|
401 |
|
402 | if (extend !== true)
|
403 | for (var i = 0; i < targetProperties.length; i++) {
|
404 | var prop = targetProperties[i];
|
405 | if (setKeys.indexOf(prop.key) == -1) {
|
406 | targetProperties.splice(i--, 1);
|
407 | self.onChange(memberArray);
|
408 | }
|
409 | }
|
410 | };
|
411 |
|
412 |
|
413 |
|
414 | ConfigFile.prototype.orderFirst = function(memberArray) {
|
415 | var properties = this.getProperties(memberArray.slice(0, memberArray.length - 1));
|
416 |
|
417 | if (!properties)
|
418 | return;
|
419 |
|
420 | var propIndex = getProperty(properties, memberArray[memberArray.length - 1]).index;
|
421 |
|
422 | if (propIndex != -1 && propIndex != 0) {
|
423 | properties.unshift(properties.splice(propIndex, 1)[0]);
|
424 | this.onChange(memberArray.slice(0, memberArray.length - 1));
|
425 | }
|
426 | };
|
427 |
|
428 |
|
429 |
|
430 | ConfigFile.prototype.orderLast = function(memberArray) {
|
431 | var properties = this.getProperties(memberArray.slice(0, memberArray.length - 1));
|
432 |
|
433 | if (!properties)
|
434 | return;
|
435 |
|
436 | var propIndex = getProperty(properties, memberArray[memberArray.length - 1]).index;
|
437 |
|
438 | if (propIndex != -1 && propIndex != properties.length - 1) {
|
439 | properties.push(properties.splice(propIndex, 1)[0]);
|
440 | this.onChange(memberArray.slice(0, memberArray.length - 1));
|
441 | }
|
442 | };
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 | ConfigFile.prototype.setObject = function(memberArray, obj, clearIfEmpty, keepOrder) {
|
449 |
|
450 | return this.setProperties(memberArray, objectToProperties(obj), clearIfEmpty, keepOrder, false);
|
451 | };
|
452 | ConfigFile.prototype.extendObject = function(memberArray, obj, keepOrder) {
|
453 | return this.setProperties(memberArray, objectToProperties(obj), false, keepOrder, true);
|
454 | };
|
455 |
|
456 |
|
457 | ConfigFile.prototype.serialize = function(obj) {
|
458 | var jsonString = JSON.stringify(obj, null, this.style.tab);
|
459 |
|
460 | if (this.style.trailingNewline)
|
461 | jsonString += this.style.newline;
|
462 |
|
463 | return jsonString
|
464 | .replace(/([^\\])""/g, '$1' + this.style.quote + this.style.quote)
|
465 | .replace(/([^\\])"/g, '$1' + this.style.quote)
|
466 | .replace(/\n/g, this.style.newline);
|
467 | };
|
468 | ConfigFile.prototype.deserialize = function(source) {
|
469 | return JSON.parse(source);
|
470 | };
|
471 |
|
472 |
|
473 |
|
474 | ConfigFile.prototype.onChange = function(memberArray) {
|
475 |
|
476 | if (!this.changeEvents.reduce(function(stopPropagation, evt) {
|
477 | return stopPropagation || evt(memberArray);
|
478 | }, false))
|
479 | this.changed = true;
|
480 | };
|
481 |
|
482 |
|
483 | ConfigFile.prototype.read = function() {
|
484 | var contents;
|
485 | try {
|
486 | this.timestamp = fs.statSync(this.fileName).mtime.getTime();
|
487 | contents = fs.readFileSync(this.fileName).toString();
|
488 | }
|
489 | catch(e) {
|
490 | if (e.code != 'ENOENT')
|
491 | throw e;
|
492 |
|
493 | this.timestamp = -1;
|
494 | contents = '';
|
495 | }
|
496 |
|
497 | this.style = detectStyle(contents);
|
498 |
|
499 | var deserializedObj;
|
500 |
|
501 | try {
|
502 | deserializedObj = this.deserialize(contents || '{}') || {};
|
503 | }
|
504 | catch(e) {
|
505 | configError.call(this, e.toString());
|
506 | }
|
507 |
|
508 | this.setObject([], deserializedObj, false, true);
|
509 | this.changed = false;
|
510 | };
|
511 | ConfigFile.prototype.write = function() {
|
512 | var timestamp;
|
513 | try {
|
514 | timestamp = fs.statSync(this.fileName).mtime.getTime();
|
515 | }
|
516 | catch(e) {
|
517 | if (e.code != 'ENOENT')
|
518 | throw e;
|
519 |
|
520 | timestamp = -1;
|
521 | }
|
522 |
|
523 | if (timestamp !== this.timestamp)
|
524 | throw new Error('Configuration file ' + path.relative(process.cwd(), this.fileName) + ' has been modified by another process.');
|
525 |
|
526 | if (this.changed || timestamp == -1) {
|
527 |
|
528 | mkdirp.sync(path.dirname(this.fileName));
|
529 | var obj = this.getObject([], true);
|
530 | fs.writeFileSync(this.fileName, this.serialize(obj));
|
531 | this.timestamp = fs.statSync(this.fileName).mtime.getTime();
|
532 |
|
533 | this.changed = false;
|
534 |
|
535 |
|
536 | if (this.originalName) {
|
537 | fs.unlinkSync(this.originalName);
|
538 | this.originalName = null;
|
539 | }
|
540 | }
|
541 | };
|
542 |
|
543 | function detectStyle(string) {
|
544 | var style = {
|
545 | tab: ' ',
|
546 | newline: require('os').EOL,
|
547 | trailingNewline: true,
|
548 | quote: '"'
|
549 | };
|
550 |
|
551 | var newLineMatch = string.match( /\r?\n|\r(?!\n)/);
|
552 | if (newLineMatch)
|
553 | style.newline = newLineMatch[0];
|
554 |
|
555 |
|
556 |
|
557 | var tabSpaces = string.split(style.newline).map(function(line) { return line.match(/^[ \t]*/)[0]; }) || [];
|
558 | var tabDifferenceFreqs = {};
|
559 | var lastLength = 0;
|
560 | tabSpaces.forEach(function(tabSpace) {
|
561 | var diff = Math.abs(tabSpace.length - lastLength);
|
562 | if (diff != 0)
|
563 | tabDifferenceFreqs[diff] = (tabDifferenceFreqs[diff] || 0) + 1;
|
564 | lastLength = tabSpace.length;
|
565 | });
|
566 | var bestTabLength;
|
567 | Object.keys(tabDifferenceFreqs).forEach(function(tabLength) {
|
568 | if (!bestTabLength || tabDifferenceFreqs[tabLength] >= tabDifferenceFreqs[bestTabLength])
|
569 | bestTabLength = tabLength;
|
570 | });
|
571 |
|
572 |
|
573 |
|
574 | var tabSamples = {};
|
575 | tabSpaces.forEach(function(tabSpace) {
|
576 | var sample = tabSpace.substr(tabSpace.length - bestTabLength);
|
577 | tabSamples[sample] = (tabSamples[sample] || 0) + 1;
|
578 | });
|
579 | var bestTabSample;
|
580 | Object.keys(tabSamples).forEach(function(sample) {
|
581 | if (!bestTabSample || tabSamples[sample] > tabSamples[bestTabSample])
|
582 | bestTabSample = sample;
|
583 | });
|
584 |
|
585 | if (bestTabSample)
|
586 | style.tab = bestTabSample;
|
587 |
|
588 | var quoteMatch = string.match(/"|'/);
|
589 | if (quoteMatch)
|
590 | style.quote = quoteMatch[0];
|
591 |
|
592 | if (string && !string.match(new RegExp(style.newline + '$')))
|
593 | style.trailingNewline = false;
|
594 |
|
595 | return style;
|
596 | } |
\ | No newline at end of file |