upd8-util.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. // This is used by upd8.js! It's part of the 8ackend. Read the notes there if
  2. // you're curious.
  3. //
  4. // Friendly(!) disclaimer: these utility functions haven't 8een tested all that
  5. // much. Do not assume it will do exactly what you want it to do in all cases.
  6. // It will likely only do exactly what I want it to, and only in the cases I
  7. // decided were relevant enough to 8other handling.
  8. 'use strict';
  9. // Apparently JavaScript doesn't come with a function to split an array into
  10. // chunks! Weird. Anyway, this is an awesome place to use a generator, even
  11. // though we don't really make use of the 8enefits of generators any time we
  12. // actually use this. 8ut it's still awesome, 8ecause I say so.
  13. module.exports.splitArray = function*(array, fn) {
  14. let lastIndex = 0;
  15. while (lastIndex < array.length) {
  16. let nextIndex = array.findIndex((item, index) => index >= lastIndex && fn(item));
  17. if (nextIndex === -1) {
  18. nextIndex = array.length;
  19. }
  20. yield array.slice(lastIndex, nextIndex);
  21. // Plus one because we don't want to include the dividing line in the
  22. // next array we yield.
  23. lastIndex = nextIndex + 1;
  24. }
  25. };
  26. // This function's name is a joke. Jokes! Hahahahahahahaha. Funny.
  27. module.exports.joinNoOxford = function(array, plural = 'and') {
  28. array = array.filter(Boolean);
  29. if (array.length === 0) {
  30. // ????????
  31. return '';
  32. }
  33. if (array.length === 1) {
  34. return array[0];
  35. }
  36. if (array.length === 2) {
  37. return `${array[0]} ${plural} ${array[1]}`;
  38. }
  39. return `${array.slice(0, -1).join(', ')} ${plural} ${array[array.length - 1]}`;
  40. };
  41. module.exports.progressPromiseAll = function (msgOrMsgFn, array) {
  42. if (!array.length) {
  43. return Promise.resolve([]);
  44. }
  45. const msgFn = (typeof msgOrMsgFn === 'function'
  46. ? msgOrMsgFn
  47. : () => msgOrMsgFn);
  48. let done = 0, total = array.length;
  49. process.stdout.write(`\r${msgFn()} [0/${total}]`);
  50. const start = Date.now();
  51. return Promise.all(array.map(promise => promise.then(val => {
  52. done++;
  53. // const pc = `${done}/${total}`;
  54. const pc = (Math.round(done / total * 1000) / 10 + '%').padEnd('99.9%'.length, ' ');
  55. if (done === total) {
  56. const time = Date.now() - start;
  57. process.stdout.write(`\r\x1b[2m${msgFn()} [${pc}] \x1b[0;32mDone! \x1b[0;2m(${time} ms) \x1b[0m\n`)
  58. } else {
  59. process.stdout.write(`\r${msgFn()} [${pc}] `);
  60. }
  61. return val;
  62. })));
  63. };
  64. module.exports.queue = function (array, max = 50) {
  65. if (max === 0) {
  66. return array.map(fn => fn());
  67. }
  68. const begin = [];
  69. let current = 0;
  70. const ret = array.map(fn => new Promise((resolve, reject) => {
  71. begin.push(() => {
  72. current++;
  73. Promise.resolve(fn()).then(value => {
  74. current--;
  75. if (current < max && begin.length) {
  76. begin.shift()();
  77. }
  78. resolve(value);
  79. }, reject);
  80. });
  81. }));
  82. for (let i = 0; i < max && begin.length; i++) {
  83. begin.shift()();
  84. }
  85. return ret;
  86. };
  87. module.exports.delay = ms => new Promise(res => setTimeout(res, ms));
  88. module.exports.th = function (n) {
  89. if (n % 10 === 1 && n !== 11) {
  90. return n + 'st';
  91. } else if (n % 10 === 2 && n !== 12) {
  92. return n + 'nd';
  93. } else if (n % 10 === 3 && n !== 13) {
  94. return n + 'rd';
  95. } else {
  96. return n + 'th';
  97. }
  98. };
  99. // My function names just keep getting 8etter.
  100. module.exports.s = function (n, word) {
  101. return `${n} ${word}` + (n === 1 ? '' : 's');
  102. };
  103. // Hey, did you know I apparently put a space 8efore the parameters in function
  104. // names? 8ut only in function expressions, not declar8tions? I mean, I guess
  105. // you did. You're pro8a8ly more familiar with my code than I am 8y this
  106. // point. I haven't messed with any of this code in ages. Yay!!!!!!!!
  107. //
  108. // This function only does anything on o8jects you're going to 8e reusing.
  109. // Argua8ly I could use a WeakMap here, 8ut since the o8ject needs to 8e
  110. // reused to 8e useful anyway, I just store the result with a symbol.
  111. // Sorry if it's 8een frozen I guess??
  112. module.exports.cacheOneArg = function (fn) {
  113. const symbol = Symbol('Cache');
  114. return arg => {
  115. if (!arg[symbol]) {
  116. arg[symbol] = fn(arg);
  117. }
  118. return arg[symbol];
  119. };
  120. };
  121. const decorateTime = function (functionToBeWrapped) {
  122. const fn = function(...args) {
  123. const start = Date.now();
  124. const ret = functionToBeWrapped(...args);
  125. const end = Date.now();
  126. fn.timeSpent += end - start;
  127. fn.timesCalled++;
  128. return ret;
  129. };
  130. fn.wrappedName = functionToBeWrapped.name;
  131. fn.timeSpent = 0;
  132. fn.timesCalled = 0;
  133. fn.displayTime = function() {
  134. const averageTime = fn.timeSpent / fn.timesCalled;
  135. console.log(`\x1b[1m${fn.wrappedName}(...):\x1b[0m ${fn.timeSpent} ms / ${fn.timesCalled} calls \x1b[2m(avg: ${averageTime} ms)\x1b[0m`);
  136. };
  137. decorateTime.decoratedFunctions.push(fn);
  138. return fn;
  139. };
  140. decorateTime.decoratedFunctions = [];
  141. decorateTime.displayTime = function() {
  142. if (decorateTime.decoratedFunctions.length) {
  143. console.log(`\x1b[1mdecorateTime results: ` + '-'.repeat(40) + '\x1b[0m');
  144. for (const fn of decorateTime.decoratedFunctions) {
  145. fn.displayTime();
  146. }
  147. }
  148. };
  149. module.exports.decorateTime = decorateTime;
  150. // Stolen as #@CK from mtui!
  151. const parseOptions = async function(options, optionDescriptorMap) {
  152. // This function is sorely lacking in comments, but the basic usage is
  153. // as such:
  154. //
  155. // options is the array of options you want to process;
  156. // optionDescriptorMap is a mapping of option names to objects that describe
  157. // the expected value for their corresponding options.
  158. // Returned is a mapping of any specified option names to their values, or
  159. // a process.exit(1) and error message if there were any issues.
  160. //
  161. // Here are examples of optionDescriptorMap to cover all the things you can
  162. // do with it:
  163. //
  164. // optionDescriptorMap: {
  165. // 'telnet-server': {type: 'flag'},
  166. // 't': {alias: 'telnet-server'}
  167. // }
  168. //
  169. // options: ['t'] -> result: {'telnet-server': true}
  170. //
  171. // optionDescriptorMap: {
  172. // 'directory': {
  173. // type: 'value',
  174. // validate(name) {
  175. // // const whitelistedDirectories = ['apple', 'banana']
  176. // if (whitelistedDirectories.includes(name)) {
  177. // return true
  178. // } else {
  179. // return 'a whitelisted directory'
  180. // }
  181. // }
  182. // },
  183. // 'files': {type: 'series'}
  184. // }
  185. //
  186. // ['--directory', 'apple'] -> {'directory': 'apple'}
  187. // ['--directory', 'artichoke'] -> (error)
  188. // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']}
  189. //
  190. // TODO: Be able to validate the values in a series option.
  191. const handleDashless = optionDescriptorMap[parseOptions.handleDashless];
  192. const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown];
  193. const result = Object.create(null);
  194. for (let i = 0; i < options.length; i++) {
  195. const option = options[i];
  196. if (option.startsWith('--')) {
  197. // --x can be a flag or expect a value or series of values
  198. let name = option.slice(2).split('=')[0]; // '--x'.split('=') = ['--x']
  199. let descriptor = optionDescriptorMap[name];
  200. if (!descriptor) {
  201. if (handleUnknown) {
  202. handleUnknown(option);
  203. } else {
  204. console.error(`Unknown option name: ${name}`);
  205. process.exit(1);
  206. }
  207. continue;
  208. }
  209. if (descriptor.alias) {
  210. name = descriptor.alias;
  211. descriptor = optionDescriptorMap[name];
  212. }
  213. if (descriptor.type === 'flag') {
  214. result[name] = true;
  215. } else if (descriptor.type === 'value') {
  216. let value = option.slice(2).split('=')[1];
  217. if (!value) {
  218. value = options[++i];
  219. if (!value || value.startsWith('-')) {
  220. value = null;
  221. }
  222. }
  223. if (!value) {
  224. console.error(`Expected a value for --${name}`);
  225. process.exit(1);
  226. }
  227. result[name] = value;
  228. } else if (descriptor.type === 'series') {
  229. if (!options.slice(i).includes(';')) {
  230. console.error(`Expected a series of values concluding with ; (\\;) for --${name}`);
  231. process.exit(1);
  232. }
  233. const endIndex = i + options.slice(i).indexOf(';');
  234. result[name] = options.slice(i + 1, endIndex);
  235. i = endIndex;
  236. }
  237. if (descriptor.validate) {
  238. const validation = await descriptor.validate(result[name]);
  239. if (validation !== true) {
  240. console.error(`Expected ${validation} for --${name}`);
  241. process.exit(1);
  242. }
  243. }
  244. } else if (option.startsWith('-')) {
  245. // mtui doesn't use any -x=y or -x y format optionuments
  246. // -x will always just be a flag
  247. let name = option.slice(1);
  248. let descriptor = optionDescriptorMap[name];
  249. if (!descriptor) {
  250. if (handleUnknown) {
  251. handleUnknown(option);
  252. } else {
  253. console.error(`Unknown option name: ${name}`);
  254. process.exit(1);
  255. }
  256. continue;
  257. }
  258. if (descriptor.alias) {
  259. name = descriptor.alias;
  260. descriptor = optionDescriptorMap[name];
  261. }
  262. if (descriptor.type === 'flag') {
  263. result[name] = true;
  264. } else {
  265. console.error(`Use --${name} (value) to specify ${name}`);
  266. process.exit(1);
  267. }
  268. } else if (handleDashless) {
  269. handleDashless(option);
  270. }
  271. }
  272. return result;
  273. }
  274. parseOptions.handleDashless = Symbol();
  275. parseOptions.handleUnknown = Symbol();
  276. module.exports.parseOptions = parseOptions;
  277. // Cheap FP for a cheap dyke!
  278. // I have no idea if this is what curry actually means.
  279. module.exports.curry = f => x => (...args) => f(x, ...args);
  280. module.exports.mapInPlace = (array, fn) => array.splice(0, array.length, ...array.map(fn));
  281. module.exports.filterEmptyLines = string => string.split('\n').filter(line => line.trim()).join('\n');
  282. module.exports.unique = arr => Array.from(new Set(arr));
  283. const logColor = color => (literals, ...values) => {
  284. const w = s => process.stdout.write(s);
  285. w(`\x1b[${color}m`);
  286. for (let i = 0; i < literals.length; i++) {
  287. w(literals[i]);
  288. if (values[i] !== undefined) {
  289. w(`\x1b[1m`);
  290. w(String(values[i]));
  291. w(`\x1b[0;${color}m`);
  292. }
  293. }
  294. w(`\x1b[0m\n`);
  295. };
  296. module.exports.logInfo = logColor(2);
  297. module.exports.logWarn = logColor(33);
  298. module.exports.logError = logColor(31);
  299. module.exports.sortByName = (a, b) => {
  300. let an = a.name.toLowerCase();
  301. let bn = b.name.toLowerCase();
  302. if (an.startsWith('the ')) an = an.slice(4);
  303. if (bn.startsWith('the ')) bn = bn.slice(4);
  304. return an < bn ? -1 : an > bn ? 1 : 0;
  305. };
  306. module.exports.chunkByConditions = function(array, conditions) {
  307. if (array.length === 0) {
  308. return [];
  309. } else if (conditions.length === 0) {
  310. return [array];
  311. }
  312. const out = [];
  313. let cur = [array[0]];
  314. for (let i = 1; i < array.length; i++) {
  315. const item = array[i];
  316. const prev = array[i - 1];
  317. let chunk = false;
  318. for (const condition of conditions) {
  319. if (condition(item, prev)) {
  320. chunk = true;
  321. break;
  322. }
  323. }
  324. if (chunk) {
  325. out.push(cur);
  326. cur = [item];
  327. } else {
  328. cur.push(item);
  329. }
  330. }
  331. out.push(cur);
  332. return out;
  333. };
  334. module.exports.chunkByProperties = function(array, properties) {
  335. return module.exports.chunkByConditions(array, properties.map(p => (a, b) => {
  336. if (a[p] instanceof Date && b[p] instanceof Date)
  337. return +a[p] !== +b[p];
  338. if (a[p] !== b[p]) return true;
  339. // Not sure if this line is still necessary with the specific check for
  340. // d8tes a8ove, 8ut, uh, keeping it anyway, just in case....?
  341. if (a[p] != b[p]) return true;
  342. return false;
  343. }))
  344. .map(chunk => ({
  345. ...Object.fromEntries(properties.map(p => [p, chunk[0][p]])),
  346. chunk
  347. }));
  348. };
  349. // Very cool function origin8ting in... http-music pro8a8ly!
  350. // Sorry if we happen to 8e violating past-us's copyright, lmao.
  351. module.exports.promisifyProcess = function(proc, showLogging = true) {
  352. // Takes a process (from the child_process module) and returns a promise
  353. // that resolves when the process exits (or rejects, if the exit code is
  354. // non-zero).
  355. //
  356. // Ayy look, no alpha8etical second letter! Couldn't tell this was written
  357. // like three years ago 8efore I was me. 8888)
  358. return new Promise((resolve, reject) => {
  359. if (showLogging) {
  360. proc.stdout.pipe(process.stdout);
  361. proc.stderr.pipe(process.stderr);
  362. }
  363. proc.on('exit', code => {
  364. if (code === 0) {
  365. resolve();
  366. } else {
  367. reject(code);
  368. }
  369. })
  370. })
  371. };
  372. // Stolen from jq! Which pro8a8ly stole the concept from other places. Nice.
  373. module.exports.withEntries = (obj, fn) => Object.fromEntries(fn(Object.entries(obj)));
  374. // Stolen from here: https://stackoverflow.com/a/53925033
  375. // We changed the # to // though.
  376. module.exports.makeExtendedRegExp = (inputPatternStr, flags) => {
  377. // Remove everything between the first unescaped `//` and the end of a line
  378. // and then remove all unescaped whitespace
  379. const cleanedPatternStr = inputPatternStr
  380. .replace(/(^|[^\\])\/\/.*/g, '$1')
  381. .replace(/(^|[^\\])\s+/g, '$1');
  382. return new RegExp(cleanedPatternStr, flags);
  383. };
  384. // Stolen from here: https://stackoverflow.com/a/3561711
  385. //
  386. // There's a proposal for a native JS function like this, 8ut it's not even
  387. // past stage 1 yet: https://github.com/tc39/proposal-regex-escaping
  388. module.exports.escapeRegex = string =>
  389. string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');