@abw/badger 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,913 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
+
7
+ var process = _interopDefault(require('node:process'));
8
+ var path = _interopDefault(require('node:path'));
9
+ var promises = require('node:fs/promises');
10
+ var yaml = _interopDefault(require('js-yaml'));
11
+
12
+ /**
13
+ * Determines if a value is a string
14
+ * @param {String} value - value to test
15
+ * @return {Boolean} true if `value` is a string or false if not
16
+ */
17
+ function isString(value) {
18
+ return typeof value === 'string';
19
+ }
20
+
21
+ /**
22
+ * Determines if a value is an array
23
+ * @param {Array} value - value to test
24
+ * @return {Boolean} true if `value` is an Array or false if not
25
+ */
26
+ function isArray(value) {
27
+ return Array.isArray(value);
28
+ }
29
+
30
+ /**
31
+ * Determines if a value is a Function
32
+ * @param {Function} value - value to test
33
+ * @return {Boolean} true if `value` is a Function or false if not
34
+ */
35
+ function isFunction(value) {
36
+ return typeof value === 'function'
37
+ }
38
+
39
+ /**
40
+ * Determines if a value is an Object (but not an Array)
41
+ * @param {Object} value - value to test
42
+ * @return {Boolean} true if `value` is an Object or false if not
43
+ */
44
+ function isObject(value) {
45
+ return typeof value === "object"
46
+ && ! isArray(value)
47
+ && ! isNull(value);
48
+ }
49
+
50
+ /**
51
+ * Determines if a value is `undefined`
52
+ * @param {any} value - value to test
53
+ * @return {Boolean} true if `value` is `undefined` or false if not
54
+ */
55
+ function isUndefined(value) {
56
+ return typeof value === 'undefined';
57
+ }
58
+
59
+ /**
60
+ * Determines if a value is `null`
61
+ * @param {any} value - value to test
62
+ * @return {Boolean} true if `value` is `null` or false if not
63
+ */
64
+ function isNull(value) {
65
+ return value === null;
66
+ }
67
+
68
+ /**
69
+ * Determines if a value is defined and not null
70
+ * @param {any} value - value to test
71
+ * @return {Boolean} true if `value` is not `undefined` or `null`
72
+ */
73
+ function hasValue(value) {
74
+ return ! (isUndefined(value) || isNull(value));
75
+ }
76
+
77
+ /**
78
+ * Determines if all values are defined and not null
79
+ * @param {any[]} values - values to test
80
+ * @return {Boolean} true if all values are not `undefined` or `null`
81
+ */
82
+ function haveValue(...values) {
83
+ return values.every( value => hasValue(value) );
84
+ }
85
+
86
+ /**
87
+ * Determines if a value is undefined or null
88
+ * @param {any} value - value to test
89
+ * @return {Boolean} true if `value` is `undefined` or `null`
90
+ */
91
+ function noValue(value) {
92
+ return ! hasValue(value);
93
+ }
94
+
95
+ /**
96
+ * Throws a new Error object
97
+ * @param {String[]} message - error message string(s)
98
+ * @throws {Error}
99
+ */
100
+ function fail(...message) {
101
+ throw new Error(message.join(''));
102
+ }
103
+
104
+ /**
105
+ * Re-throw an existing Error object
106
+ * @param {Error} error - error object
107
+ * @throws {Error}
108
+ */
109
+ function rethrow(error) {
110
+ throw error;
111
+ }
112
+
113
+ /**
114
+ * Do nothing. Nothing at all.
115
+ */
116
+ function doNothing() {
117
+ // speak again Cordelia
118
+ }
119
+
120
+ const ANSIStart = '\u001B[';
121
+ const ANSIEnd = 'm';
122
+ const ANSIColors = {
123
+ reset: 0,
124
+ bold: 1,
125
+ bright: 1,
126
+ dark: 2,
127
+ black: 0,
128
+ red: 1,
129
+ green: 2,
130
+ yellow: 3,
131
+ blue: 4,
132
+ magenta: 5,
133
+ cyan: 6,
134
+ grey: 7,
135
+ white: 8,
136
+ fg: 30,
137
+ bg: 40,
138
+ };
139
+
140
+ const escapeCode = (str, base=0) => {
141
+ let codes = [ ];
142
+ let pair = str.split(/ /, 2);
143
+ const hue = pair.pop();
144
+ const code = (base ? ANSIColors[base] : 0) + ANSIColors[hue];
145
+ codes.push(code);
146
+ if (pair.length) {
147
+ const shade = pair.length ? pair.shift() : 'dark';
148
+ codes.push(ANSIColors[shade]);
149
+ }
150
+ return ANSIStart + codes.join(';') + ANSIEnd;
151
+ };
152
+
153
+ const escape = (c={}) => {
154
+ // color c can be specified as a string (e.g. 'red') which is shorthand
155
+ // for an object containing 'fg' (e.g. { fg: 'red' }) and/or 'bg' for
156
+ // foreground and background colors respectively
157
+ const col = isObject(c) ? c : { fg: c };
158
+ let escapes = [ ];
159
+ if (col.bg) {
160
+ escapes.push(escapeCode(col.bg, 'bg'));
161
+ }
162
+ if (col.fg) {
163
+ escapes.push(escapeCode(col.fg, 'fg'));
164
+ }
165
+ return escapes.join('');
166
+ };
167
+
168
+ const reset = () => escapeCode('reset');
169
+
170
+ /**
171
+ * Returns a debugging function which is enabled by the first `enabled` argument.
172
+ * If this is `false` then it returns a function which does nothing. If it is
173
+ * true then it returns a function that forwards all arguments to `console.log`.
174
+ * An optional `prefix` be be specified to prefix each debugging line. The
175
+ * optional third argument `color` can be used to specify a color for the prefix.
176
+ * @param {Boolean} enabled - is debugging enabled?
177
+ * @param {String} [prefix] - optional prefix for debugging messages
178
+ * @param {String|Object} [color] - a color name or object (see {@link Badger/Utils/Color})
179
+ * @param {String} [color.fg] - foreground color
180
+ * @param {String} [color.bg] - background color
181
+ * @return {Function} a debugging function
182
+ * @example
183
+ * const debug = Debugger(true)
184
+ * @example
185
+ * const debug = Debugger(true, 'Debug > ')
186
+ * @example
187
+ * const debug = Debugger(true, 'Debug > ', 'blue')
188
+ * @example
189
+ * const debug = Debugger(true, 'Debug > ', { bg: 'blue', fg: 'bright yellow' })
190
+ */
191
+ function Debugger(enabled, prefix='', color) {
192
+ return enabled
193
+ ? prefix
194
+ ? (format, ...args) =>
195
+ console.log(
196
+ '%s' + prefix + '%s' + format,
197
+ color ? escape(color) : '',
198
+ reset(),
199
+ ...args,
200
+ )
201
+ : console.log.bind(console)
202
+ : doNothing;
203
+ }
204
+
205
+ /**
206
+ * Creates a debugging function via {@link Debugger} and attaches it to the object
207
+ * passed as the first argument as the `debug` function.
208
+ * @param {Object} obj - the object to receive the `debug` function
209
+ * @param {Boolean} enabled - is debugging enabled?
210
+ * @param {String} [prefix] - optional prefix for debugging messages
211
+ * @param {String|Object} [color] - a color name or object (see {@link Badger/Utils/Color})
212
+ * @param {String} [color.fg] - foreground color
213
+ * @param {String} [color.bg] - background color
214
+ * @example
215
+ * const debug = addDebug(myObject, true)
216
+ * @example
217
+ * const debug = addDebug(myObject, true, 'Debug > ')
218
+ * @example
219
+ * const debug = addDebug(myObject, true, 'Debug > ', 'blue')
220
+ * @example
221
+ * const debug = addDebug(myObject, true, 'Debug > ', { bg: 'blue', fg: 'bright yellow' })
222
+ */
223
+ function addDebug(obj, enabled, prefix='', color) {
224
+ obj.debug = Debugger(enabled, prefix, color);
225
+ }
226
+
227
+ const defaultOptions = {
228
+ encoding: 'utf8'
229
+ };
230
+
231
+ class Path {
232
+ constructor(path, options={}) {
233
+ // allow path/file/directory to be constructed from an existing object
234
+ if (path instanceof Path) {
235
+ path = path.path();
236
+ }
237
+ this.state = { path, options: { ...defaultOptions, ...options } };
238
+ addDebug(this, options.debug, options.debugPrefix || 'Path', options.debugColor);
239
+ }
240
+ path() {
241
+ return this.state.path;
242
+ }
243
+ relativePath(...parts) {
244
+ return path.join(this.state.path, ...parts);
245
+ }
246
+ options(options={}) {
247
+ return { ...this.state.options, ...options };
248
+ }
249
+ async exists() {
250
+ try {
251
+ await this.stat();
252
+ return true;
253
+ }
254
+ catch (error) {
255
+ return error.code === 'ENOENT'
256
+ ? false
257
+ : rethrow(error);
258
+ }
259
+ }
260
+ async stat() {
261
+ const stats = await promises.stat(this.state.path);
262
+ return this.state.stats = stats;
263
+ }
264
+ unstat() {
265
+ this.state.stats = undefined;
266
+ console.log('XXX unstat: ', this.state.stats);
267
+ return this;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Function to encode JSON
273
+ * @param {Object} data - The data to encode as JSON text
274
+ * @return {String} a JSON encoded string
275
+ * @example
276
+ * encode({ message: 'Hello World' })
277
+ */
278
+ const encode = data => JSON.stringify(data);
279
+
280
+ /**
281
+ * Function to decode JSON
282
+ * @param {String} text - The JSON text to decode
283
+ * @return {Object|Array} the decoded object or array
284
+ * @example
285
+ * decode("{ message: 'Hello World' }")
286
+ */
287
+ const decode = text => JSON.parse(text);
288
+
289
+ /**
290
+ * An object containing the JSON `encode` and `decode` functions
291
+ */
292
+ const codec = { encode, decode };
293
+
294
+ // simple wrapper around JSON load/dump
295
+
296
+ /**
297
+ * Function to encode YAML
298
+ * @param {Object} data - The data to encode as YAML text
299
+ * @return {String} a YAML encoded string
300
+ * @example
301
+ * encode({ message: 'Hello World' })
302
+ */
303
+ const encode$1 = data => yaml.dump(data);
304
+
305
+ /**
306
+ * Function to decode YAML
307
+ * @param {String} text - The YAML text to decode
308
+ * @return {Object|Array} the decoded object or array
309
+ * @example
310
+ * decode("message: Hello World")
311
+ */
312
+ const decode$1 = text => yaml.load(text);
313
+
314
+ /**
315
+ * An object containing the YAML `encode` and `decode` functions
316
+ */
317
+ const codec$1 = { encode: encode$1, decode: decode$1 };
318
+
319
+ /**
320
+ * Codecs provide a consistent encode()/decode() interface for serialising
321
+ * and de-serialising data. This standard naming convention makes it possible
322
+ * for the ../Filesystem/File.js module to support a "codec" option for
323
+ * files. When this option is set the file.read() and file.write() methods
324
+ * automatically handle the translation to and from the serialised format
325
+ * using a codec object returned by the codec() function below. The codec
326
+ * name can be specified in any case, e.g. "Yaml", "YAML", "yaml", "YaML",
327
+ * etc., and it will be converted to lower case.
328
+ */
329
+
330
+ /**
331
+ * Lookup table for codecs
332
+ */
333
+ const codecs = {
334
+ json: codec, yaml: codec$1
335
+ };
336
+
337
+ /**
338
+ * Function to fetch a codec
339
+ * @param {string} name - The title of the code, in any case, e.g. "yaml", "YAML", "Yaml"
340
+ */
341
+ const codec$2 = name => codecs[
342
+ name.toLowerCase()
343
+ ];
344
+
345
+ class File extends Path {
346
+ /**
347
+ * Returns a new {@link Directory} object for the parent directory of the file
348
+ * @param {Object} [options] - directory configuration options
349
+ * @param {Boolean} [options.codec] - codec for encoding/decoding file data
350
+ * @return {Object} a {@link Directory} object for the parent
351
+ */
352
+ directory(options) {
353
+ return dir(path.dirname(this.state.path), options);
354
+ }
355
+
356
+ /**
357
+ * An alias for the {@link directory} method for lazy people
358
+ * @return {Object} the parent {@link Directory} object
359
+ */
360
+ dir(...args) {
361
+ return this.directory(...args);
362
+ }
363
+
364
+ /**
365
+ * Reads the file content. If a `codec` has been specified then the content is decoded.
366
+ * @param {Object} [options] - directory configuration options
367
+ * @param {Boolean} [options.codec] - codec for encoding/decoding file data
368
+ * @return {String|Object} the file content
369
+ * @example
370
+ * const text = file('myfile.txt').read();
371
+ * @example
372
+ * const data = file('myfile.json', { codec: 'json' }).read();
373
+ * @example
374
+ * const data = file('myfile.json').read({ codec: 'json' });
375
+ */
376
+ read(options) {
377
+ const opts = this.options(options);
378
+ const file = promises.readFile(this.state.path, opts);
379
+ return opts.codec
380
+ ? file.then(text => codec$2(opts.codec).decode(text))
381
+ : file;
382
+ }
383
+
384
+ /**
385
+ * Writes the file content. If a `codec` has been specified then the content will be encoded.
386
+ * @param {String|Object} data - directory configuration options
387
+ * @param {Object} [options] - directory configuration options
388
+ * @param {Boolean} [options.codec] - codec for encoding/decoding file data
389
+ * @example
390
+ * file('myfile.txt').write('Hello World');
391
+ * @example
392
+ * file('myfile.json', { codec: 'json' }).write({ message: 'Hello World' });
393
+ * @example
394
+ * file('myfile.json').write({ message: 'Hello World' }, { codec: 'json' });
395
+ */
396
+ write(data, options) {
397
+ const opts = this.options(options);
398
+ const text = opts.codec
399
+ ? codec$2(opts.codec).encode(data)
400
+ : data;
401
+ return promises.writeFile(this.state.path, text, opts).then( () => this );
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Function to create a new {@link File} object for a file
407
+ * @param {String} path - file path
408
+ * @param {Object} [options] - configuration options
409
+ * @param {Boolean} [options.codec] - a codec for encoding/decoding files
410
+ * @return {Object} the {@link File} object
411
+ */
412
+ const file = (path, options) => {
413
+ return new File(path, options);
414
+ };
415
+
416
+ class Directory extends Path {
417
+ /**
418
+ * Fetch a new {@link File} object for a file in the directory.
419
+ * @param {string} path - file path
420
+ * @param {Object} [options] - file configuration options
421
+ * @param {String} [options.codec] - codec for encoding/decoding file data
422
+ * @return {Object} the {@link File} object
423
+ */
424
+ file(path, options) {
425
+ this.debug("file(%s, %o)", path, options);
426
+ return file(this.relativePath(path), this.options(options));
427
+ }
428
+
429
+ /**
430
+ * Fetch a new {@link Directory} object for a sub-directory in the directory.
431
+ * @param {string} path - directory path
432
+ * @param {Object} [options] - directory configuration options
433
+ * @param {String} [options.codec] - codec for encoding/decoding file data
434
+ * @return {Object} the {@link Directory} object
435
+ */
436
+ directory(path, options) {
437
+ this.debug("directory(%s, %o)", path, options);
438
+ return dir(this.relativePath(path), this.options(options));
439
+ }
440
+
441
+ /**
442
+ * An alias for the {@link directory} method for lazy people
443
+ * @return {Object} the {@link Directory} object
444
+ */
445
+ dir(path, options) {
446
+ this.debug("dir(%s, %o)", path, options);
447
+ return this.directory(path, options);
448
+ }
449
+
450
+ /**
451
+ * Returns a new {@link Directory} object for the parent directory
452
+ * @param {Object} [options] - directory configuration options
453
+ * @param {Boolean} [options.codec] - codec for encoding/decoding file data
454
+ * @return {Object} a {@link Directory} object for the parent
455
+ */
456
+ parent(options) {
457
+ this.debug("parent()");
458
+ return this.directory('..', options);
459
+ }
460
+
461
+ /**
462
+ * Returns the names of the files and sub-directories in the directory
463
+ * @return {Promise} fulfills with an array of the file and directory names
464
+ */
465
+ async read() {
466
+ this.debug("read()");
467
+ return await promises.readdir(this.path());
468
+ }
469
+
470
+ /**
471
+ * Determines if the directory is empty.
472
+ * @return {Promise} fulfills with a boolean value true (empty) or false (not empty).
473
+ */
474
+ async isEmpty() {
475
+ this.debug("isEmpty()");
476
+ const entries = await this.read();
477
+ return entries.length === 0;
478
+ }
479
+
480
+ /**
481
+ * Determines if the directory is not empty.
482
+ * @return {Promise} fulfills with a boolean value true (not empty) or false (empty).
483
+ */
484
+ async notEmpty() {
485
+ this.debug("notEmpty()");
486
+ const empty = await this.isEmpty();
487
+ return !empty;
488
+ }
489
+
490
+ /**
491
+ * Empty the directory.
492
+ * @param {Object} [options] - configuration options
493
+ * @param {Boolean} [options.force] - force removal of files and directories
494
+ * @param {Boolean} [options.recursive] - recursively empty and delete sub-directories
495
+ * @return {Promise} fulfills to the {@link Directory} object
496
+ */
497
+ async empty(options={}) {
498
+ this.debug("empty(%o)", options);
499
+ if (await this.exists() && await this.notEmpty()) {
500
+ await promises.rm(this.path(), options);
501
+ }
502
+ return this;
503
+ }
504
+
505
+ /**
506
+ * Make the directory.
507
+ * @param {Object} [options] - configuration options
508
+ * @param {Boolean} [options.recursive] - create intermediate directories
509
+ * @return {Promise} fulfills to the {@link Directory} object
510
+ */
511
+ async mkdir(options={}) {
512
+ this.debug("mkdir(%o)", options);
513
+ const exists = await this.exists();
514
+ if (! exists) {
515
+ await promises.mkdir(this.path(), options);
516
+ }
517
+ return this;
518
+ }
519
+
520
+ /**
521
+ * Remove the directory.
522
+ * @param {Object} [options] - configuration options
523
+ * @param {Boolean} [options.empty] - delete items in directory
524
+ * @param {Boolean} [options.force] - force delete files and directories
525
+ * @param {Boolean} [options.recursive] - recursively delete sub-directories
526
+ * @return {Promise} fulfills to the {@link Directory} object
527
+ */
528
+ async rmdir(options={}) {
529
+ this.debug("rmdir(%o)", options);
530
+ if (options.empty) {
531
+ await this.empty(options);
532
+ }
533
+ if (await this.exists()) {
534
+ await promises.rmdir(this.path());
535
+ }
536
+ return this;
537
+ }
538
+
539
+ /**
540
+ * Create the directory and any intermediate directories.
541
+ * @param {Object} [options] - configuration options
542
+ * @param {Boolean} [options.recursive=true] - recursively create intermediate directories
543
+ * @return {Promise} fulfills to the {@link Directory} object
544
+ */
545
+ create(options={ recursive: true }) {
546
+ this.debug("create(%o)", options);
547
+ return this.mkdir(options);
548
+ }
549
+
550
+ /**
551
+ * Empty and delete the directory.
552
+ * @param {Object} [options] - configuration options
553
+ * @param {Boolean} [options.empty=true] - empty directory of any files and sub-directories
554
+ * @param {Boolean} [options.recursive=true] - recursively delete sub-directories
555
+ * @param {Boolean} [options.force=true] - force deletion of files and sub-directories
556
+ * @return {Promise} fulfills to the {@link Directory} object
557
+ */
558
+ destroy(options={ empty: true, recursive: true, force: true }) {
559
+ this.debug("destroy(%o)", options);
560
+ return this.rmdir(options);
561
+ }
562
+
563
+ /**
564
+ * Assert that a directory exists and optionally create it
565
+ * @param {Object} [options] - configuration options
566
+ * @param {Boolean} [options.create] - create the directory and any intermediate directories if it doesn't exist - equivalent to adding `mkdir` and `recursive` options or calling {@link create}
567
+ * @param {Boolean} [options.mkdir] - create the directory, add the `recursive` option to create intermediate directories - equivalent to calling {@link mkdir}
568
+ * @param {Boolean} [options.recursive] - when used with `mkdir`, creates any intermediate directories
569
+ * @return {Promise} fulfills to the {@link Directory} object
570
+ */
571
+ async mustExist(options={}) {
572
+ this.debug("mustExist(%o)", options);
573
+ if (await this.exists()) {
574
+ return this;
575
+ }
576
+ if (options.mkdir) {
577
+ return this.mkdir(options);
578
+ }
579
+ if (options.create) {
580
+ return this.create();
581
+ }
582
+ fail("Directory does not exist: ", this.path());
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Function to create a new {@link Directory} object
588
+ * @param {string} path - directory path
589
+ * @param {Object} [options] - configuration options
590
+ * @param {Boolean} [options.codec] - a codec for encoding/decoding files
591
+ * @return {Object} the {@link Directory} object
592
+ */
593
+ const dir = (path, options) => {
594
+ return new Directory(path, options);
595
+ };
596
+
597
+ /**
598
+ * Function to create a new {@link Directory} object for the current working directory
599
+ * @param {Object} [options] - configuration options
600
+ * @param {Boolean} [options.codec] - a codec for encoding/decoding files
601
+ * @return {Object} the {@link Directory} object
602
+ */
603
+ const cwd = options => {
604
+ return dir(process.cwd(), options);
605
+ };
606
+
607
+ /**
608
+ * Function to create a new {@link Directory} object for the directory of a JS source file
609
+ * @param {string} url - module url - from `import.meta.url`
610
+ * @param {Object} [options] - configuration options
611
+ * @param {Boolean} [options.codec] - a codec for encoding/decoding files
612
+ * @return {Object} the {@link Directory} object
613
+ */
614
+ const bin = (url, options) => {
615
+ return dir(
616
+ path.dirname(url.replace(/^file:\/\//, '')),
617
+ options
618
+ );
619
+ };
620
+
621
+ /**
622
+ * Split a comma/whitespace delimited string into an Array
623
+ * @param {String} [value] - string to split
624
+ * @return {Array} array of split strings
625
+ * @example
626
+ * const strings = splitList('one two three')
627
+ * @example
628
+ * const strings = splitList('one,two,three')
629
+ * @example
630
+ * const strings = splitList('one, two, three')
631
+ */
632
+ function splitList(value) {
633
+ return isString(value)
634
+ ? value.split(/,\s*|\s+/)
635
+ : isArray(value)
636
+ ? value
637
+ : [value];
638
+ }
639
+
640
+ /**
641
+ * Join an Array into a single string
642
+ * @param {Array} [array] - array to join
643
+ * @param {String} [joint=' '] - delimiter to join strings
644
+ * @param {String} [lastJoint=joint] - delimiter for final item
645
+ * @return {String} joined string
646
+ * @example
647
+ * joinList(['one', 'two', 'three']); // one two three
648
+ * @example
649
+ * joinList(['one', 'two', 'three'], ', '); // one, two, three
650
+ * @example
651
+ * joinList(['one', 'two', 'three'], ', ', ' and '); // one, two and three
652
+ */
653
+ function joinList(array, joint=' ', lastJoint=joint) {
654
+ let copy = [...array];
655
+ const last = copy.pop();
656
+ return copy.length
657
+ ? [copy.join(joint), last].join(lastJoint)
658
+ : last;
659
+ }
660
+
661
+ /**
662
+ * Join an Array into a single string using commas for delimiters and ` and ` for the final item
663
+ * @param {Array} [array] - array to join
664
+ * @param {String} [joint=', '] - delimiter to join strings
665
+ * @param {String} [lastJoint=' and '] - delimiter for final item
666
+ * @return {String} joined string
667
+ * @example
668
+ * joinListAnd(['one', 'two', 'three']); // one, two and three
669
+ */
670
+ function joinListAnd(array, joint=', ', lastJoint=' and ') {
671
+ return joinList(array, joint, lastJoint);
672
+ }
673
+
674
+ /**
675
+ * Join an Array into a single string using commas for delimiters and ` or ` for the final item
676
+ * @param {Array} [array] - array to join
677
+ * @param {String} [joint=', '] - delimiter to join strings
678
+ * @param {String} [lastJoint=' or '] - delimiter for final item
679
+ * @return {String} joined string
680
+ * @example
681
+ * joinListOr(['one', 'two', 'three']); // one, two or three
682
+ */
683
+ function joinListOr(array, joint=', ', lastJoint=' or ') {
684
+ return joinList(array, joint, lastJoint);
685
+ }
686
+
687
+ /**
688
+ * Capitalise a string by converting the first character to upper case and other characters to lower case
689
+ * @param {String} [word] - word to capitalise
690
+ * @return {String} capitalised string
691
+ * @example
692
+ * capitalise('badger'); // Badger
693
+ * @example
694
+ * capitalise('BADGER'); // Badger
695
+ */
696
+ function capitalise(word) {
697
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
698
+ }
699
+
700
+ /**
701
+ * Convert a snake case string to studly caps
702
+ * @param {String} [snake] - word to capitalise
703
+ * @return {String} capitalised string
704
+ * @example
705
+ * snakeToStudly('happy_badger_dance'); // HappyBadgerDance
706
+ * @example
707
+ * snakeToStudly('happy_badger/dance'); // HappyBadger/Dance
708
+ */
709
+ function snakeToStudly(snake) {
710
+ return snake.split('/').map(
711
+ // each segment can be like foo_bar which we convert to FooBar
712
+ segment => segment.split('_').map(capitalise).join('')
713
+ ).join('/');
714
+ }
715
+
716
+ /**
717
+ * Convert a snake case string to camel case
718
+ * @param {String} [snake] - word to capitalise
719
+ * @return {String} capitalised string
720
+ * @example
721
+ * snakeToCamel('happy_badger_dance'); // happyBadgerDance
722
+ * @example
723
+ * snakeToCamel('happy_badger/dance'); // happyBadger/dance
724
+ */
725
+ function snakeToCamel(snake) {
726
+ return snake.split('/').map(
727
+ // each segment can be like foo_bar which we convert to FooBar
728
+ segment => segment.split('_').map((i, n) => n ? capitalise(i) : i).join('')
729
+ ).join('/');
730
+ }
731
+
732
+ /**
733
+ * Assert that a parameter object contains an item with a defined/non-null value
734
+ * @param {Object} params={} - parameters object
735
+ * @param {String} name - parameter that must be included
736
+ * @return {any} the parameter value
737
+ * @throws {Error} if the parameter is not defined or null
738
+ * @example
739
+ * const foo = requiredParam({ foo: 10 }, 'foo');
740
+ */
741
+ function requiredParam(params={}, name) {
742
+ const value = params[name];
743
+ if (hasValue(value)) {
744
+ return value;
745
+ }
746
+ else {
747
+ fail("Missing value for required parameter: ", name);
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Assert that a parameter object contains all specified item with a defined/non-null value
753
+ * @param {Object} params={} - parameters object
754
+ * @param {Array|String} names - parameters that must be included, as an Array or whitespace/comma delimited string (see {@link splitList})
755
+ * @return {Array} the parameter values
756
+ * @throws {Error} if any parameter is not defined or null
757
+ * @example
758
+ * const [foo, bar] = requiredParams({ foo: 10, bar: 20 }, 'foo bar');
759
+ */
760
+ function requiredParams(params={}, names) {
761
+ return splitList(names).map( name => requiredParam(params, name) );
762
+ }
763
+
764
+ /**
765
+ * An alias for {@link requiredParams} for people who don't like typing long names (and for symmetry with {@link anyParams}))
766
+ */
767
+ const allParams=requiredParams;
768
+
769
+ /**
770
+ * Assert that a parameter object contains any of the specified items with a defined/non-null value
771
+ * @param {Object} params={} - parameters object
772
+ * @param {Array|String} names - parameters of which at least one must be included, as an Array or whitespace/comma delimited string (see {@link splitList})
773
+ * @return {Array} the parameter values
774
+ * @throws {Error} if any parameter is not defined or null
775
+ * @example
776
+ * const [foo, bar] = anyParams({ foo: 10, wiz: 99 }, 'foo bar');
777
+ */
778
+ function anyParams(params, names) {
779
+ let found = false;
780
+ const nlist = splitList(names);
781
+ const values = nlist.map(
782
+ name => {
783
+ const value = params[name];
784
+ if (hasValue(value)) {
785
+ found = true;
786
+ }
787
+ return value;
788
+ }
789
+ );
790
+ return found
791
+ ? values
792
+ : fail("Missing value for one of: ", joinListOr(nlist));
793
+ }
794
+
795
+ const defaults = {
796
+ codecs: 'yaml json',
797
+ };
798
+
799
+ class Config {
800
+ constructor(params={}) {
801
+ const options = { ...defaults, ...params };
802
+ const [rootDir] = allParams(options, 'dir');
803
+ const [codec, codecs] = anyParams(options, 'codec codecs');
804
+
805
+ this.state = {
806
+ dir: dir(rootDir),
807
+ codecs: splitList(codecs) || [codec],
808
+ };
809
+
810
+ addDebug(this, options.debug, options.debugPrefix, options.debugColor);
811
+ this.debug('root dir: ', this.state.dir.path());
812
+ this.debug('codecs: ', this.state.codecs);
813
+ }
814
+ async file(uri) {
815
+ for (let codec of this.state.codecs) {
816
+ const path = uri + '.' + codec;
817
+ const file = this.state.dir.file(path, { codec });
818
+ this.debug('looking for config file: ', file.path());
819
+ if (await file.exists()) {
820
+ this.debug('config file exists: ', file.path());
821
+ return file;
822
+ }
823
+ }
824
+ return undefined;
825
+ }
826
+ async config(uri, defaults) {
827
+ const file = await this.file(uri);
828
+ return file
829
+ ? await file.read()
830
+ : (defaults || fail("No configuration file for " + uri))
831
+ }
832
+ }
833
+
834
+ const config = options => new Config(options);
835
+
836
+ const defaults$1 = {
837
+ config: {
838
+ dir: 'config',
839
+ }
840
+ };
841
+ class Workspace {
842
+ constructor(props={}) {
843
+ const rootDir = dir(requiredParam(props, 'dir'));
844
+ const cfgDir = rootDir.dir(props.config?.dir || defaults$1.config.dir);
845
+ const cfgOpts = { ...defaults$1.config, ...(props.config||{}), dir: cfgDir };
846
+ const cfgObj = config(cfgOpts);
847
+
848
+ this.state = {
849
+ rootDir: rootDir,
850
+ configDir: cfgDir,
851
+ config: cfgObj
852
+ };
853
+
854
+ addDebug(this, props.debug, props.debugPrefix, props.debugColor);
855
+ this.debug('root dir: ', rootDir.path());
856
+ this.debug('config dir: ', cfgDir.path());
857
+ }
858
+ dir(path, options) {
859
+ this.debug("dir(%s, %o)", path, options);
860
+ return hasValue(path)
861
+ ? this.state.rootDir(path, options)
862
+ : this.state.rootDir;
863
+ }
864
+ configDir(path, options) {
865
+ this.debug("configDir(%s, %o)", path, options);
866
+ return hasValue(path)
867
+ ? this.state.configDir(path, options)
868
+ : this.state.configDir;
869
+ }
870
+ config(uri, defaults) {
871
+ this.debug("config(%s, %o)", uri, defaults);
872
+ return hasValue(uri)
873
+ ? this.state.config.config(uri, defaults)
874
+ : this.state.config;
875
+ }
876
+ }
877
+
878
+ const workspace = props => new Workspace(props);
879
+
880
+ exports.Config = Config;
881
+ exports.Path = Path;
882
+ exports.Workspace = Workspace;
883
+ exports.allParams = allParams;
884
+ exports.anyParams = anyParams;
885
+ exports.bin = bin;
886
+ exports.capitalise = capitalise;
887
+ exports.codec = codec$2;
888
+ exports.codecs = codecs;
889
+ exports.config = config;
890
+ exports.cwd = cwd;
891
+ exports.dir = dir;
892
+ exports.doNothing = doNothing;
893
+ exports.fail = fail;
894
+ exports.file = file;
895
+ exports.hasValue = hasValue;
896
+ exports.haveValue = haveValue;
897
+ exports.isArray = isArray;
898
+ exports.isFunction = isFunction;
899
+ exports.isNull = isNull;
900
+ exports.isObject = isObject;
901
+ exports.isString = isString;
902
+ exports.isUndefined = isUndefined;
903
+ exports.joinList = joinList;
904
+ exports.joinListAnd = joinListAnd;
905
+ exports.joinListOr = joinListOr;
906
+ exports.noValue = noValue;
907
+ exports.requiredParam = requiredParam;
908
+ exports.requiredParams = requiredParams;
909
+ exports.rethrow = rethrow;
910
+ exports.snakeToCamel = snakeToCamel;
911
+ exports.snakeToStudly = snakeToStudly;
912
+ exports.splitList = splitList;
913
+ exports.workspace = workspace;