index.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import { existsSync, readFileSync, writeFileSync } from 'node:fs';
  2. import { resolve } from 'node:path';
  3. import { homedir } from 'node:os';
  4. import destr from 'destr';
  5. import { defu } from 'defu';
  6. function isBuffer (obj) {
  7. return obj &&
  8. obj.constructor &&
  9. (typeof obj.constructor.isBuffer === 'function') &&
  10. obj.constructor.isBuffer(obj)
  11. }
  12. function keyIdentity (key) {
  13. return key
  14. }
  15. function flatten (target, opts) {
  16. opts = opts || {};
  17. const delimiter = opts.delimiter || '.';
  18. const maxDepth = opts.maxDepth;
  19. const transformKey = opts.transformKey || keyIdentity;
  20. const output = {};
  21. function step (object, prev, currentDepth) {
  22. currentDepth = currentDepth || 1;
  23. Object.keys(object).forEach(function (key) {
  24. const value = object[key];
  25. const isarray = opts.safe && Array.isArray(value);
  26. const type = Object.prototype.toString.call(value);
  27. const isbuffer = isBuffer(value);
  28. const isobject = (
  29. type === '[object Object]' ||
  30. type === '[object Array]'
  31. );
  32. const newKey = prev
  33. ? prev + delimiter + transformKey(key)
  34. : transformKey(key);
  35. if (!isarray && !isbuffer && isobject && Object.keys(value).length &&
  36. (!opts.maxDepth || currentDepth < maxDepth)) {
  37. return step(value, newKey, currentDepth + 1)
  38. }
  39. output[newKey] = value;
  40. });
  41. }
  42. step(target);
  43. return output
  44. }
  45. function unflatten (target, opts) {
  46. opts = opts || {};
  47. const delimiter = opts.delimiter || '.';
  48. const overwrite = opts.overwrite || false;
  49. const transformKey = opts.transformKey || keyIdentity;
  50. const result = {};
  51. const isbuffer = isBuffer(target);
  52. if (isbuffer || Object.prototype.toString.call(target) !== '[object Object]') {
  53. return target
  54. }
  55. // safely ensure that the key is
  56. // an integer.
  57. function getkey (key) {
  58. const parsedKey = Number(key);
  59. return (
  60. isNaN(parsedKey) ||
  61. key.indexOf('.') !== -1 ||
  62. opts.object
  63. )
  64. ? key
  65. : parsedKey
  66. }
  67. function addKeys (keyPrefix, recipient, target) {
  68. return Object.keys(target).reduce(function (result, key) {
  69. result[keyPrefix + delimiter + key] = target[key];
  70. return result
  71. }, recipient)
  72. }
  73. function isEmpty (val) {
  74. const type = Object.prototype.toString.call(val);
  75. const isArray = type === '[object Array]';
  76. const isObject = type === '[object Object]';
  77. if (!val) {
  78. return true
  79. } else if (isArray) {
  80. return !val.length
  81. } else if (isObject) {
  82. return !Object.keys(val).length
  83. }
  84. }
  85. target = Object.keys(target).reduce(function (result, key) {
  86. const type = Object.prototype.toString.call(target[key]);
  87. const isObject = (type === '[object Object]' || type === '[object Array]');
  88. if (!isObject || isEmpty(target[key])) {
  89. result[key] = target[key];
  90. return result
  91. } else {
  92. return addKeys(
  93. key,
  94. result,
  95. flatten(target[key], opts)
  96. )
  97. }
  98. }, {});
  99. Object.keys(target).forEach(function (key) {
  100. const split = key.split(delimiter).map(transformKey);
  101. let key1 = getkey(split.shift());
  102. let key2 = getkey(split[0]);
  103. let recipient = result;
  104. while (key2 !== undefined) {
  105. if (key1 === '__proto__') {
  106. return
  107. }
  108. const type = Object.prototype.toString.call(recipient[key1]);
  109. const isobject = (
  110. type === '[object Object]' ||
  111. type === '[object Array]'
  112. );
  113. // do not write over falsey, non-undefined values if overwrite is false
  114. if (!overwrite && !isobject && typeof recipient[key1] !== 'undefined') {
  115. return
  116. }
  117. if ((overwrite && !isobject) || (!overwrite && recipient[key1] == null)) {
  118. recipient[key1] = (
  119. typeof key2 === 'number' &&
  120. !opts.object
  121. ? []
  122. : {}
  123. );
  124. }
  125. recipient = recipient[key1];
  126. if (split.length > 0) {
  127. key1 = getkey(split.shift());
  128. key2 = getkey(split[0]);
  129. }
  130. }
  131. // unflatten again for 'messy objects'
  132. recipient[key1] = unflatten(target[key], opts);
  133. });
  134. return result
  135. }
  136. const RE_KEY_VAL = /^\s*([^\s=]+)\s*=\s*(.*)?\s*$/;
  137. const RE_LINES = /\n|\r|\r\n/;
  138. const defaults = {
  139. name: ".conf",
  140. dir: process.cwd(),
  141. flat: false
  142. };
  143. function withDefaults(options) {
  144. if (typeof options === "string") {
  145. options = { name: options };
  146. }
  147. return { ...defaults, ...options };
  148. }
  149. function parse(contents, options = {}) {
  150. const config = {};
  151. const lines = contents.split(RE_LINES);
  152. for (const line of lines) {
  153. const match = line.match(RE_KEY_VAL);
  154. if (!match) {
  155. continue;
  156. }
  157. const key = match[1];
  158. if (!key || key === "__proto__" || key === "constructor") {
  159. continue;
  160. }
  161. const value = destr(
  162. (match[2] || "").trim()
  163. /* val */
  164. );
  165. if (key.endsWith("[]")) {
  166. const nkey = key.slice(0, Math.max(0, key.length - 2));
  167. config[nkey] = (config[nkey] || []).concat(value);
  168. continue;
  169. }
  170. config[key] = value;
  171. }
  172. return options.flat ? config : unflatten(config, { overwrite: true });
  173. }
  174. function parseFile(path, options) {
  175. if (!existsSync(path)) {
  176. return {};
  177. }
  178. return parse(readFileSync(path, "utf8"), options);
  179. }
  180. function read(options) {
  181. options = withDefaults(options);
  182. return parseFile(resolve(options.dir, options.name), options);
  183. }
  184. function readUser(options) {
  185. options = withDefaults(options);
  186. options.dir = process.env.XDG_CONFIG_HOME || homedir();
  187. return read(options);
  188. }
  189. function serialize(config) {
  190. return Object.entries(flatten(config)).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join("\n");
  191. }
  192. function write(config, options) {
  193. options = withDefaults(options);
  194. writeFileSync(resolve(options.dir, options.name), serialize(config), {
  195. encoding: "utf8"
  196. });
  197. }
  198. function writeUser(config, options) {
  199. options = withDefaults(options);
  200. options.dir = process.env.XDG_CONFIG_HOME || homedir();
  201. write(config, options);
  202. }
  203. function update(config, options) {
  204. options = withDefaults(options);
  205. if (!options.flat) {
  206. config = unflatten(config, { overwrite: true });
  207. }
  208. const newConfig = defu(config, read(options));
  209. write(newConfig, options);
  210. return newConfig;
  211. }
  212. function updateUser(config, options) {
  213. options = withDefaults(options);
  214. options.dir = process.env.XDG_CONFIG_HOME || homedir();
  215. return update(config, options);
  216. }
  217. export { defaults, parse, parseFile, read, readUser, serialize, update, updateUser, write, writeUser };