@gesslar/toolkit 0.2.0 → 0.2.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -56,9 +56,9 @@
56
56
  "devDependencies": {
57
57
  "@stylistic/eslint-plugin": "^5.4.0",
58
58
  "@types/node": "^24.5.2",
59
- "@typescript-eslint/eslint-plugin": "^8.44.0",
60
- "@typescript-eslint/parser": "^8.44.0",
59
+ "@typescript-eslint/eslint-plugin": "^8.44.1",
60
+ "@typescript-eslint/parser": "^8.44.1",
61
61
  "eslint": "^9.36.0",
62
- "eslint-plugin-jsdoc": "^60.1.0"
62
+ "eslint-plugin-jsdoc": "^60.4.1"
63
63
  }
64
64
  }
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ export {default as FS} from "./lib/FS.js"
5
5
 
6
6
  // Utility classes
7
7
  export {default as Cache} from "./lib/Cache.js"
8
+ export {default as Collection} from "./lib/Collection.js"
8
9
  export {default as Data} from "./lib/Data.js"
9
10
  export {default as Glog} from "./lib/Glog.js"
10
11
  export {default as Sass} from "./lib/Sass.js"
@@ -0,0 +1,519 @@
1
+ import Data from "./Data.js"
2
+ import Valid from "./Valid.js"
3
+ import Sass from "./Sass.js"
4
+ import Util from "./Util.js"
5
+
6
+ export default class Collection {
7
+ static evalArray(collection, predicate, forward=true) {
8
+ const req = "Array"
9
+ const type = Data.typeOf(collection)
10
+
11
+ Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`)
12
+ Valid.type(predicate, "Function",
13
+ `Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`)
14
+
15
+ const work = forward
16
+ ? Array.from(collection)
17
+ : Array.from(collection).toReversed()
18
+
19
+ for(let i = 0; i < work.length; i++) {
20
+ const result = predicate(work[i], i, collection) ?? null
21
+
22
+ if(result)
23
+ return result
24
+ }
25
+ }
26
+
27
+ static evalObject(collection, predicate) {
28
+ const req = "Object"
29
+ const type = Data.typeOf(collection)
30
+
31
+ Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`)
32
+ Valid.type(predicate, "Function",
33
+ `Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`)
34
+
35
+ const work = Object.entries(collection)
36
+
37
+ for(let i = 0; i < work.length; i++) {
38
+ const result = predicate(work[i][1], work[i][0], collection)
39
+
40
+ if(result)
41
+ return result
42
+ }
43
+ }
44
+
45
+ static evalSet(collection, predicate) {
46
+ const req = "Set"
47
+ const type = Data.typeOf(collection)
48
+
49
+ Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`)
50
+ Valid.type(predicate, "Function",
51
+ `Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`)
52
+
53
+ const work = Array.from(collection)
54
+
55
+ for(let i = 0; i < work.length; i++) {
56
+ const result = predicate(work[i], collection)
57
+
58
+ if(result)
59
+ return result
60
+ }
61
+ }
62
+
63
+ static evalMap(collection, predicate, forward=true) {
64
+ const req = "Map"
65
+ const type = Data.typeOf(collection)
66
+
67
+ Valid.type(collection, req, `Invalid collection. Expected '${req}, got ${type}`)
68
+ Valid.type(predicate, "Function",
69
+ `Invalid predicate, expected 'Function', got ${Data.typeOf(predicate)}`)
70
+
71
+ const work = forward
72
+ ? Array.from(collection)
73
+ : Array.from(collection).toReversed()
74
+
75
+ for(let i = 0; i < work.length; i++) {
76
+ const result = predicate(work[i][1], work[i][0], collection) ?? null
77
+
78
+ if(result)
79
+ return result
80
+ }
81
+ }
82
+
83
+ static zip(array1, array2) {
84
+ const minLength = Math.min(array1.length, array2.length)
85
+
86
+ return Array.from({length: minLength}, (_, i) => [array1[i], array2[i]])
87
+ }
88
+
89
+ static unzip(array) {
90
+ if(!Array.isArray(array) || array.length === 0) {
91
+ return [] // Handle empty or invalid input
92
+ }
93
+
94
+ // Determine the number of "unzipped" arrays needed
95
+ // This assumes all inner arrays have the same length, or we take the max length
96
+ const numUnzippedArrays = Math.max(...array.map(arr => arr.length))
97
+
98
+ // Initialize an array of empty arrays to hold the unzipped results
99
+ const unzipped = Array.from({length: numUnzippedArrays}, () => [])
100
+
101
+ // Iterate through the zipped array and populate the unzipped arrays
102
+ for(let i = 0; i < array.length; i++) {
103
+ for(let j = 0; j < numUnzippedArrays; j++) {
104
+ unzipped[j].push(array[i][j])
105
+ }
106
+ }
107
+
108
+ return unzipped
109
+ }
110
+
111
+ static async asyncMap(array, asyncFn) {
112
+ const req = "Array"
113
+ const type = Data.typeOf(array)
114
+
115
+ Valid.type(array, req, `Invalid array. Expected '${req}', got '${type}'`)
116
+ Valid.type(asyncFn, "Function",
117
+ `Invalid mapper function, expected 'Function', got '${Data.typeOf(asyncFn)}'`)
118
+
119
+ const results = []
120
+
121
+ for(const item of array) {
122
+ results.push(await asyncFn(item))
123
+ }
124
+
125
+ return results
126
+ }
127
+
128
+ /**
129
+ * Checks if all elements in an array are of a specified type
130
+ *
131
+ * @param {Array} arr - The array to check
132
+ * @param {string} type - The type to check for (optional, defaults to the
133
+ * type of the first element)
134
+ * @returns {boolean} Whether all elements are of the specified type
135
+ */
136
+ static isArrayUniform(arr, type) {
137
+ const req = "Array"
138
+ const arrType = Data.typeOf(arr)
139
+
140
+ Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`)
141
+
142
+ const checkType = type ? Util.capitalize(type) : null
143
+
144
+ return arr.every(
145
+ (item, _index, arr) =>
146
+ Data.typeOf(item) === (checkType || Data.typeOf(arr[0])),
147
+ )
148
+ }
149
+
150
+ /**
151
+ * Checks if an array is unique
152
+ *
153
+ * @param {Array} arr - The array of which to remove duplicates
154
+ * @returns {Array} The unique elements of the array
155
+ */
156
+ static isArrayUnique(arr) {
157
+ const req = "Array"
158
+ const arrType = Data.typeOf(arr)
159
+
160
+ Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`)
161
+
162
+ return arr.filter((item, index, self) => self.indexOf(item) === index)
163
+ }
164
+
165
+ /**
166
+ * Returns the intersection of two arrays.
167
+ *
168
+ * @param {Array} arr1 - The first array.
169
+ * @param {Array} arr2 - The second array.
170
+ * @returns {Array} The intersection of the two arrays.
171
+ */
172
+ static arrayIntersection(arr1, arr2) {
173
+ const req = "Array"
174
+ const arr1Type = Data.typeOf(arr1)
175
+ const arr2Type = Data.typeOf(arr2)
176
+
177
+ Valid.type(arr1, req, `Invalid first array. Expected '${req}', got '${arr1Type}'`)
178
+ Valid.type(arr2, req, `Invalid second array. Expected '${req}', got '${arr2Type}'`)
179
+
180
+ const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length)
181
+
182
+ return short.filter(value => long.includes(value))
183
+ }
184
+
185
+ /**
186
+ * Checks whether two arrays have any elements in common.
187
+ *
188
+ * This function returns `true` if at least one element from `arr1` exists in
189
+ * `arr2`, and `false` otherwise. It optimizes by iterating over the shorter
190
+ * array for efficiency.
191
+ *
192
+ * Example:
193
+ * arrayIntersects([1, 2, 3], [3, 4, 5]) // returns true
194
+ * arrayIntersects(["a", "b"], ["c", "d"]) // returns false
195
+ *
196
+ * @param {Array} arr1 - The first array to check for intersection.
197
+ * @param {Array} arr2 - The second array to check for intersection.
198
+ * @returns {boolean} True if any element is shared between the arrays, false otherwise.
199
+ */
200
+ static arrayIntersects(arr1, arr2) {
201
+ const req = "Array"
202
+ const arr1Type = Data.typeOf(arr1)
203
+ const arr2Type = Data.typeOf(arr2)
204
+
205
+ Valid.type(arr1, req, `Invalid first array. Expected '${req}', got '${arr1Type}'`)
206
+ Valid.type(arr2, req, `Invalid second array. Expected '${req}', got '${arr2Type}'`)
207
+
208
+ const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length)
209
+
210
+ return !!short.find(value => long.includes(value))
211
+ }
212
+
213
+ /**
214
+ * Pads an array to a specified length with a value. This operation
215
+ * occurs in-place.
216
+ *
217
+ * @param {Array} arr - The array to pad.
218
+ * @param {number} length - The length to pad the array to.
219
+ * @param {unknown} value - The value to pad the array with.
220
+ * @param {number} position - The position to pad the array at.
221
+ * @returns {Array} The padded array.
222
+ */
223
+ static arrayPad(arr, length, value, position = 0) {
224
+ const req = "Array"
225
+ const arrType = Data.typeOf(arr)
226
+
227
+ Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`)
228
+ Valid.type(length, "Number", `Invalid length. Expected 'Number', got '${Data.typeOf(length)}'`)
229
+ Valid.type(position, "Number", `Invalid position. Expected 'Number', got '${Data.typeOf(position)}'`)
230
+
231
+ const diff = length - arr.length
232
+
233
+ if(diff <= 0)
234
+ return arr
235
+
236
+ const padding = Array(diff).fill(value)
237
+
238
+ if(position === 0)
239
+ // prepend - default
240
+ return padding.concat(arr)
241
+ else if(position === -1)
242
+ // append
243
+ return arr.concat(padding) // somewhere in the middle - THAT IS ILLEGAL
244
+ else
245
+ throw Sass.new("Invalid position")
246
+ }
247
+
248
+ /**
249
+ * Checks if all elements in an array are strings.
250
+ *
251
+ * @param {Array} arr - The array to check.
252
+ * @returns {boolean} Returns true if all elements are strings, false otherwise.
253
+ * @example
254
+ * uniformStringArray(['a', 'b', 'c']) // returns true
255
+ * uniformStringArray(['a', 1, 'c']) // returns false
256
+ */
257
+ static uniformStringArray(arr) {
258
+ if(!Data.isType(arr, "Array"))
259
+ return false
260
+
261
+ return arr.every(item => typeof item === "string")
262
+ }
263
+
264
+ /**
265
+ * Filters an array asynchronously using a predicate function.
266
+ * Applies the predicate to all items in parallel and returns filtered results.
267
+ *
268
+ * @param {Array} arr - The array to filter
269
+ * @param {function(unknown): Promise<boolean>} predicate - Async predicate function that returns a promise resolving to boolean
270
+ * @returns {Promise<Array>} Promise resolving to the filtered array
271
+ */
272
+ static async asyncFilter(arr, predicate) {
273
+ const req = "Array"
274
+ const arrType = Data.typeOf(arr)
275
+
276
+ Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`)
277
+ Valid.type(predicate, "Function",
278
+ `Invalid predicate function, expected 'Function', got '${Data.typeOf(predicate)}'`)
279
+
280
+ const results = await Promise.all(arr.map(predicate))
281
+
282
+ return arr.filter((_, index) => results[index])
283
+ }
284
+
285
+ /**
286
+ * Clones an object
287
+ *
288
+ * @param {object} obj - The object to clone
289
+ * @param {boolean} freeze - Whether to freeze the cloned object
290
+ * @returns {object} The cloned object
291
+ */
292
+ static cloneObject(obj, freeze = false) {
293
+ const result = {}
294
+
295
+ for(const [key, value] of Object.entries(obj)) {
296
+ if(Data.isType(value, "Array")) {
297
+ // Clone arrays by mapping over them
298
+ result[key] = value.map(item =>
299
+ Data.isType(item, "object") || Data.isType(item, "Array")
300
+ ? Collection.cloneObject(item)
301
+ : item
302
+ )
303
+ } else if(Data.isType(value, "object")) {
304
+ result[key] = Collection.cloneObject(value)
305
+ } else {
306
+ result[key] = value
307
+ }
308
+ }
309
+
310
+ return freeze ? Object.freeze(result) : result
311
+ }
312
+
313
+ /**
314
+ * Checks if an object is empty
315
+ *
316
+ * @param {object} obj - The object to check
317
+ * @returns {boolean} Whether the object is empty
318
+ */
319
+ static isObjectEmpty(obj) {
320
+ const req = "Object"
321
+ const objType = Data.typeOf(obj)
322
+
323
+ Valid.type(obj, req, `Invalid object. Expected '${req}', got '${objType}'`)
324
+
325
+ return Object.keys(obj).length === 0
326
+ }
327
+
328
+ /**
329
+ * Ensures that a nested path of objects exists within the given object.
330
+ * Creates empty objects along the path if they don't exist.
331
+ *
332
+ * @param {object} obj - The object to check/modify
333
+ * @param {Array<string>} keys - Array of keys representing the path to ensure
334
+ * @returns {object} Reference to the deepest nested object in the path
335
+ */
336
+ static assureObjectPath(obj, keys) {
337
+ const req = "Object"
338
+ const objType = Data.typeOf(obj)
339
+ const keysReq = "Array"
340
+ const keysType = Data.typeOf(keys)
341
+
342
+ Valid.type(obj, req, `Invalid object. Expected '${req}', got '${objType}'`)
343
+ Valid.type(keys, keysReq, `Invalid keys array. Expected '${keysReq}', got '${keysType}'`)
344
+
345
+ let current = obj // a moving reference to internal objects within obj
346
+ const len = keys.length
347
+
348
+ for(let i = 0; i < len; i++) {
349
+ const elem = keys[i]
350
+
351
+ // Prevent prototype pollution
352
+ if(elem === "__proto__" || elem === "constructor" || elem === "prototype") {
353
+ throw Sass.new(`Dangerous key "${elem}" not allowed in object path`)
354
+ }
355
+
356
+ if(!current[elem])
357
+ current[elem] = {}
358
+
359
+ current = current[elem]
360
+ }
361
+
362
+ // Return the current pointer
363
+ return current
364
+ }
365
+
366
+ /**
367
+ * Sets a value in a nested object structure using an array of keys; creating
368
+ * the structure if it does not exist.
369
+ *
370
+ * @param {object} obj - The target object to set the value in
371
+ * @param {Array<string>} keys - Array of keys representing the path to the target property
372
+ * @param {unknown} value - The value to set at the target location
373
+ */
374
+ static setNestedValue(obj, keys, value) {
375
+ const req = "Object"
376
+ const objType = Data.typeOf(obj)
377
+ const keysReq = "Array"
378
+ const keysType = Data.typeOf(keys)
379
+
380
+ Valid.type(obj, req, `Invalid object. Expected '${req}', got '${objType}'`)
381
+ Valid.type(keys, keysReq, `Invalid keys array. Expected '${keysReq}', got '${keysType}'`)
382
+
383
+ const nested = Collection.assureObjectPath(obj, keys.slice(0, -1))
384
+ const finalKey = keys[keys.length-1]
385
+
386
+ // Prevent prototype pollution on final key too
387
+ if(finalKey === "__proto__" || finalKey === "constructor" || finalKey === "prototype") {
388
+ throw Sass.new(`Dangerous key "${finalKey}" not allowed in object path`)
389
+ }
390
+
391
+ nested[finalKey] = value
392
+ }
393
+
394
+ /**
395
+ * Deeply merges two or more objects. Arrays are replaced, not merged.
396
+ *
397
+ * @param {...object} sources - Objects to merge (left to right)
398
+ * @returns {object} The merged object
399
+ */
400
+ static mergeObject(...sources) {
401
+ const isObject = obj => typeof obj === "object" && obj !== null && !Array.isArray(obj)
402
+
403
+ return sources.reduce((acc, obj) => {
404
+ if(!isObject(obj))
405
+ return acc
406
+
407
+ Object.keys(obj).forEach(key => {
408
+ const accVal = acc[key]
409
+ const objVal = obj[key]
410
+
411
+ if(isObject(accVal) && isObject(objVal))
412
+ acc[key] = Collection.mergeObject(accVal, objVal)
413
+ else
414
+ acc[key] = objVal
415
+ })
416
+
417
+ return acc
418
+ }, {})
419
+ }
420
+
421
+ /**
422
+ * Freezes an object and all of its properties recursively.
423
+ *
424
+ * @param {object} obj The object to freeze.
425
+ * @returns {object} The frozen object.
426
+ */
427
+ static deepFreezeObject(obj) {
428
+ if(obj === null || typeof obj !== "object")
429
+ return obj // Skip null and non-objects
430
+
431
+ // Retrieve and freeze properties
432
+ const propNames = Object.getOwnPropertyNames(obj)
433
+
434
+ for(const name of propNames) {
435
+ const value = obj[name]
436
+
437
+ // Recursively freeze nested objects
438
+ if(typeof value === "object" && value !== null)
439
+ Collection.deepFreezeObject(value)
440
+ }
441
+
442
+ // Freeze the object itself
443
+ return Object.freeze(obj)
444
+ }
445
+
446
+ /**
447
+ * Maps an object using a transformer function
448
+ *
449
+ * @param {object} original The original object
450
+ * @param {function(unknown): unknown} transformer The transformer function
451
+ * @param {boolean} mutate Whether to mutate the original object
452
+ * @returns {Promise<object>} The mapped object
453
+ */
454
+ static async mapObject(original, transformer, mutate = false) {
455
+ Valid.type(original, "object", {allowEmpty: true})
456
+ Valid.type(transformer, "function")
457
+ Valid.type(mutate, "boolean")
458
+
459
+ const result = mutate ? original : {}
460
+
461
+ for(const [key, value] of Object.entries(original))
462
+ result[key] = Data.isType(value, "object")
463
+ ? await Collection.mapObject(value, transformer, mutate)
464
+ : (result[key] = await transformer(key, value))
465
+
466
+ return result
467
+ }
468
+
469
+ /**
470
+ * Allocates an object from a source array and a spec array or function.
471
+ *
472
+ * @param {unknown} source The source array
473
+ * @param {Array<unknown>|function(Array<unknown>): Promise<Array<unknown>>|Array<unknown>} spec The spec array or function
474
+ * @returns {Promise<object>} The allocated object
475
+ */
476
+ static async allocateObject(source, spec) {
477
+ // Data
478
+ const workSource = [],
479
+ workSpec = [],
480
+ result = {}
481
+
482
+ if(!Data.isType(source, "Array", {allowEmpty: false}))
483
+ throw Sass.new("Source must be an array.")
484
+
485
+ workSource.push(...source)
486
+
487
+ if(
488
+ !Data.isType(spec, "Array", {allowEmpty: false}) &&
489
+ !Data.isType(spec, "function")
490
+ )
491
+ throw Sass.new("Spec must be an array or a function.")
492
+
493
+ if(Data.isType(spec, "Function")) {
494
+ const specResult = await spec(workSource)
495
+
496
+ if(!Data.isType(specResult, "Array"))
497
+ throw Sass.new("Spec resulting from function must be an array.")
498
+
499
+ workSpec.push(...specResult)
500
+ } else if(Data.isType(spec, "Array", {allowEmpty: false})) {
501
+ workSpec.push(...spec)
502
+ }
503
+
504
+ if(workSource.length !== workSpec.length)
505
+ throw Sass.new("Source and spec must have the same number of elements.")
506
+
507
+ // Objects must always be indexed by strings.
508
+ workSource.map((element, index, arr) => (arr[index] = String(element)))
509
+
510
+ // Check that all keys are strings
511
+ if(!Collection.isArrayUniform(workSource, "String"))
512
+ throw Sass.new("Indices of an Object must be of type string.")
513
+
514
+ workSource.forEach((element, index) => (result[element] = workSpec[index]))
515
+
516
+ return result
517
+ }
518
+
519
+ }
package/src/lib/Data.js CHANGED
@@ -6,10 +6,7 @@
6
6
  * structures.
7
7
  */
8
8
 
9
- import Sass from "./Sass.js"
10
9
  import TypeSpec from "./TypeSpec.js"
11
- import Util from "./Util.js"
12
- import Valid from "./Valid.js"
13
10
 
14
11
  export default class Data {
15
12
  /**
@@ -101,206 +98,6 @@ export default class Data {
101
98
  return string.startsWith(prepend) ? string : `${prepend}${string}`
102
99
  }
103
100
 
104
- /**
105
- * Checks if all elements in an array are of a specified type
106
- *
107
- * @param {Array} arr - The array to check
108
- * @param {string} type - The type to check for (optional, defaults to the
109
- * type of the first element)
110
- * @returns {boolean} Whether all elements are of the specified type
111
- */
112
- static isArrayUniform(arr, type) {
113
- const checkType = type ? Util.capitalize(type) : null
114
-
115
- return arr.every(
116
- (item, _index, arr) =>
117
- Data.typeOf(item) === (checkType || Data.typeOf(arr[0])),
118
- )
119
- }
120
-
121
- /**
122
- * Checks if an array is unique
123
- *
124
- * @param {Array} arr - The array of which to remove duplicates
125
- * @returns {Array} The unique elements of the array
126
- */
127
- static isArrayUnique(arr) {
128
- return arr.filter((item, index, self) => self.indexOf(item) === index)
129
- }
130
-
131
- /**
132
- * Returns the intersection of two arrays.
133
- *
134
- * @param {Array} arr1 - The first array.
135
- * @param {Array} arr2 - The second array.
136
- * @returns {Array} The intersection of the two arrays.
137
- */
138
- static arrayIntersection(arr1, arr2) {
139
- const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length)
140
-
141
- return short.filter(value => long.includes(value))
142
- }
143
-
144
- /**
145
- * Checks whether two arrays have any elements in common.
146
- *
147
- * This function returns `true` if at least one element from `arr1` exists in
148
- * `arr2`, and `false` otherwise. It optimizes by iterating over the shorter
149
- * array for efficiency.
150
- *
151
- * Example:
152
- * arrayIntersects([1, 2, 3], [3, 4, 5]) // returns true
153
- * arrayIntersects(["a", "b"], ["c", "d"]) // returns false
154
- *
155
- * @param {Array} arr1 - The first array to check for intersection.
156
- * @param {Array} arr2 - The second array to check for intersection.
157
- * @returns {boolean} True if any element is shared between the arrays, false otherwise.
158
- */
159
- static arrayIntersects(arr1, arr2) {
160
- const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length)
161
-
162
- return !!short.find(value => long.includes(value))
163
- }
164
-
165
- /**
166
- * Pads an array to a specified length with a value. This operation
167
- * occurs in-place.
168
- *
169
- * @param {Array} arr - The array to pad.
170
- * @param {number} length - The length to pad the array to.
171
- * @param {unknown} value - The value to pad the array with.
172
- * @param {number} position - The position to pad the array at.
173
- * @returns {Array} The padded array.
174
- */
175
- static arrayPad(arr, length, value, position = 0) {
176
- const diff = length - arr.length
177
-
178
- if(diff <= 0)
179
- return arr
180
-
181
- const padding = Array(diff).fill(value)
182
-
183
- if(position === 0)
184
- // prepend - default
185
- return padding.concat(arr)
186
- else if(position === -1)
187
- // append
188
- return arr.concat(padding) // somewhere in the middle - THAT IS ILLEGAL
189
- else
190
- throw Sass.new("Invalid position")
191
- }
192
-
193
- /**
194
- * Clones an object
195
- *
196
- * @param {object} obj - The object to clone
197
- * @param {boolean} freeze - Whether to freeze the cloned object
198
- * @returns {object} The cloned object
199
- */
200
- static cloneObject(obj, freeze = false) {
201
- const result = {}
202
-
203
- for(const [key, value] of Object.entries(obj)) {
204
- if(Data.isType(value, "Array")) {
205
- // Clone arrays by mapping over them
206
- result[key] = value.map(item =>
207
- Data.isType(item, "object") || Data.isType(item, "Array")
208
- ? Data.cloneObject(item)
209
- : item
210
- )
211
- } else if(Data.isType(value, "object")) {
212
- result[key] = Data.cloneObject(value)
213
- } else {
214
- result[key] = value
215
- }
216
- }
217
-
218
- return freeze ? Object.freeze(result) : result
219
- }
220
-
221
- /**
222
- * Allocates an object from a source array and a spec array or function.
223
- *
224
- * @param {unknown} source The source array
225
- * @param {Array<unknown>|function(Array<unknown>): Promise<Array<unknown>>|Array<unknown>} spec The spec array or function
226
- * @returns {Promise<object>} The allocated object
227
- */
228
- static async allocateObject(source, spec) {
229
- // Data
230
- const workSource = [],
231
- workSpec = [],
232
- result = {}
233
-
234
- if(!Data.isType(source, "Array", {allowEmpty: false}))
235
- throw Sass.new("Source must be an array.")
236
-
237
- workSource.push(...source)
238
-
239
- if(
240
- !Data.isType(spec, "Array", {allowEmpty: false}) &&
241
- !Data.isType(spec, "function")
242
- )
243
- throw Sass.new("Spec must be an array or a function.")
244
-
245
- if(Data.isType(spec, "Function")) {
246
- const specResult = await spec(workSource)
247
-
248
- if(!Data.isType(specResult, "Array"))
249
- throw Sass.new("Spec resulting from function must be an array.")
250
-
251
- workSpec.push(...specResult)
252
- } else if(Data.isType(spec, "Array", {allowEmpty: false})) {
253
- workSpec.push(...spec)
254
- }
255
-
256
- if(workSource.length !== workSpec.length)
257
- throw Sass.new("Source and spec must have the same number of elements.")
258
-
259
- // Objects must always be indexed by strings.
260
- workSource.map((element, index, arr) => (arr[index] = String(element)))
261
-
262
- // Check that all keys are strings
263
- if(!Data.isArrayUniform(workSource, "String"))
264
- throw Sass.new("Indices of an Object must be of type string.")
265
-
266
- workSource.forEach((element, index) => (result[element] = workSpec[index]))
267
-
268
- return result
269
- }
270
-
271
- /**
272
- * Maps an object using a transformer function
273
- *
274
- * @param {object} original The original object
275
- * @param {function(unknown): unknown} transformer The transformer function
276
- * @param {boolean} mutate Whether to mutate the original object
277
- * @returns {Promise<object>} The mapped object
278
- */
279
- static async mapObject(original, transformer, mutate = false) {
280
- Valid.type(original, "object", {allowEmpty: true})
281
- Valid.type(transformer, "function")
282
- Valid.type(mutate, "boolean")
283
-
284
- const result = mutate ? original : {}
285
-
286
- for(const [key, value] of Object.entries(original))
287
- result[key] = Data.isType(value, "object")
288
- ? await Data.mapObject(value, transformer, mutate)
289
- : (result[key] = await transformer(key, value))
290
-
291
- return result
292
- }
293
-
294
- /**
295
- * Checks if an object is empty
296
- *
297
- * @param {object} obj - The object to check
298
- * @returns {boolean} Whether the object is empty
299
- */
300
- static isObjectEmpty(obj) {
301
- return Object.keys(obj).length === 0
302
- }
303
-
304
101
  /**
305
102
  * Creates a type spec from a string. A type spec is an array of objects
306
103
  * defining the type of a value and whether an array is expected.
@@ -381,9 +178,12 @@ export default class Data {
381
178
 
382
179
  const type = typeof value
383
180
 
384
- return type === "object"
385
- ? value.constructor.name
386
- : type.charAt(0).toUpperCase() + type.slice(1)
181
+ if(type === "object")
182
+ return value.constructor.name
183
+
184
+ const [first, ...rest] = Array.from(type)
185
+
186
+ return `${first?.toLocaleUpperCase() ?? ""}${rest.join("")}`
387
187
  }
388
188
 
389
189
  /**
package/src/lib/FS.js CHANGED
@@ -2,6 +2,7 @@ import {globby} from "globby"
2
2
  import path from "node:path"
3
3
  import url from "node:url"
4
4
 
5
+ import Collection from "./Collection.js"
5
6
  import Data from "./Data.js"
6
7
  import DirectoryObject from "./DirectoryObject.js"
7
8
  import FileObject from "./FileObject.js"
@@ -10,7 +11,9 @@ import Valid from "./Valid.js"
10
11
 
11
12
  const fdTypes = Object.freeze(["file", "directory"])
12
13
  const upperFdTypes = Object.freeze(fdTypes.map(type => type.toUpperCase()))
13
- const fdType = Object.freeze(await Data.allocateObject(upperFdTypes, fdTypes))
14
+ const fdType = Object.freeze(
15
+ await Collection.allocateObject(upperFdTypes, fdTypes)
16
+ )
14
17
 
15
18
  export default class FS {
16
19
  static fdTypes = fdTypes
@@ -115,7 +118,11 @@ export default class FS {
115
118
  * @returns {string} The relative path from `from` to `to`, or the absolute path if not reachable
116
119
  */
117
120
  static relativeOrAbsolutePath(from, to) {
118
- const relative = path.relative(from.path, to.path)
121
+ const fromBasePath = from.isDirectory
122
+ ? from.path
123
+ : from.directory?.path ?? path.dirname(from.path)
124
+
125
+ const relative = path.relative(fromBasePath, to.path)
119
126
 
120
127
  return relative.startsWith("..")
121
128
  ? to.path
@@ -385,12 +385,13 @@ export default class FileObject extends FS {
385
385
  */
386
386
  async loadData(type="any", encoding="utf8") {
387
387
  const content = await this.read(encoding)
388
+ const normalizedType = type.toLocaleLowerCase()
388
389
  const toTry = {
389
390
  json5: [JSON5],
390
391
  json: [JSON5],
391
392
  yaml: [YAML],
392
393
  any: [JSON5,YAML]
393
- }[type.toLowerCase()]
394
+ }[normalizedType]
394
395
 
395
396
  if(!toTry) {
396
397
  throw Sass.new(`Unsupported data type '${type}'. Supported types: json, json5, yaml, any`)
@@ -4,8 +4,9 @@
4
4
  * including arrays, unions, and options.
5
5
  */
6
6
 
7
- import Sass from "./Sass.js"
7
+ import Collection from "./Collection.js"
8
8
  import Data from "./Data.js"
9
+ import Sass from "./Sass.js"
9
10
  import Util from "./Util.js"
10
11
 
11
12
  /**
@@ -180,7 +181,7 @@ export default class TypeSpec {
180
181
  return allowEmpty
181
182
 
182
183
  // Check if array elements match the required type
183
- return Data.isArrayUniform(value, allowedType)
184
+ return Collection.isArrayUniform(value, allowedType)
184
185
  }
185
186
 
186
187
  return false
package/src/lib/Util.js CHANGED
@@ -15,9 +15,15 @@ export default class Util {
15
15
  * @returns {string} Text with first letter capitalized
16
16
  */
17
17
  static capitalize(text) {
18
- return typeof text === "string"
19
- && `${text.slice(0,1).toUpperCase()}${text.slice(1)}`
20
- || text
18
+ if(typeof text !== "string")
19
+ throw new TypeError("Util.capitalize expects a string")
20
+
21
+ if(text.length === 0)
22
+ return ""
23
+
24
+ const [first, ...rest] = Array.from(text)
25
+
26
+ return `${first.toLocaleUpperCase()}${rest.join("")}`
21
27
  }
22
28
 
23
29
  /**
@@ -0,0 +1,218 @@
1
+ // Implementation: ../lib/Collection.js
2
+ // Type definitions for Collection utilities
3
+
4
+ /**
5
+ * Collection utility functions for evaluating and manipulating arrays, objects, sets, and maps.
6
+ * Provides functional programming patterns for collection processing with consistent error handling.
7
+ */
8
+ export default class Collection {
9
+ /**
10
+ * Evaluates an array with a predicate function, returning the first truthy result.
11
+ *
12
+ * @param collection - The array to evaluate
13
+ * @param predicate - Function called for each element: (element, index, array) => result
14
+ * @param forward - Whether to iterate forward (true) or backward (false). Default: true
15
+ * @returns The first truthy result from the predicate, or undefined if none found
16
+ *
17
+ * @throws {Sass} If collection is not an Array or predicate is not a Function
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { Collection } from '@gesslar/toolkit'
22
+ *
23
+ * const numbers = [1, 2, 3, 4, 5]
24
+ * const result = Collection.evalArray(numbers, (n, i) => n > 3 ? n * 2 : null)
25
+ * console.log(result) // 8 (first element > 3, doubled)
26
+ * ```
27
+ */
28
+ static evalArray<T, R>(
29
+ collection: T[],
30
+ predicate: (element: T, index: number, array: T[]) => R | null | undefined,
31
+ forward?: boolean
32
+ ): R | undefined
33
+
34
+ /**
35
+ * Evaluates an object with a predicate function, returning the first truthy result.
36
+ *
37
+ * @param collection - The object to evaluate
38
+ * @param predicate - Function called for each property: (value, key, object) => result
39
+ * @returns The first truthy result from the predicate, or undefined if none found
40
+ *
41
+ * @throws {Sass} If collection is not an Object or predicate is not a Function
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { Collection } from '@gesslar/toolkit'
46
+ *
47
+ * const obj = {a: 1, b: 2, c: 3}
48
+ * const result = Collection.evalObject(obj, (value, key) => value > 2 ? `${key}:${value}` : null)
49
+ * console.log(result) // "c:3"
50
+ * ```
51
+ */
52
+ static evalObject<T, R>(
53
+ collection: Record<string, T>,
54
+ predicate: (value: T, key: string, object: Record<string, T>) => R | null | undefined
55
+ ): R | undefined
56
+
57
+ /**
58
+ * Evaluates a Set with a predicate function, returning the first truthy result.
59
+ *
60
+ * @param collection - The Set to evaluate
61
+ * @param predicate - Function called for each element: (element, set) => result
62
+ * @returns The first truthy result from the predicate, or undefined if none found
63
+ *
64
+ * @throws {Sass} If collection is not a Set or predicate is not a Function
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * import { Collection } from '@gesslar/toolkit'
69
+ *
70
+ * const set = new Set([1, 2, 3, 4, 5])
71
+ * const result = Collection.evalSet(set, (n, s) => n > 3 ? n * 2 : null)
72
+ * console.log(result) // 8
73
+ * ```
74
+ */
75
+ static evalSet<T, R>(
76
+ collection: Set<T>,
77
+ predicate: (element: T, set: Set<T>) => R | null | undefined
78
+ ): R | undefined
79
+
80
+ /**
81
+ * Evaluates a Map with a predicate function, returning the first truthy result.
82
+ *
83
+ * @param collection - The Map to evaluate
84
+ * @param predicate - Function called for each entry: (value, key, map) => result
85
+ * @param forward - Whether to iterate forward (true) or backward (false). Default: true
86
+ * @returns The first truthy result from the predicate, or undefined if none found
87
+ *
88
+ * @throws {Sass} If collection is not a Map or predicate is not a Function
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * import { Collection } from '@gesslar/toolkit'
93
+ *
94
+ * const map = new Map([['a', 1], ['b', 2], ['c', 3]])
95
+ * const result = Collection.evalMap(map, (value, key) => value > 2 ? `${key}:${value}` : null)
96
+ * console.log(result) // "c:3"
97
+ * ```
98
+ */
99
+ static evalMap<K, V, R>(
100
+ collection: Map<K, V>,
101
+ predicate: (value: V, key: K, map: Map<K, V>) => R | null | undefined,
102
+ forward?: boolean
103
+ ): R | undefined
104
+
105
+ /**
106
+ * Zips two arrays together into an array of pairs.
107
+ *
108
+ * @param array1 - The first array
109
+ * @param array2 - The second array
110
+ * @returns Array of [element1, element2] pairs, length of shorter input array
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * import { Collection } from '@gesslar/toolkit'
115
+ *
116
+ * const result = Collection.zip([1, 2, 3], ['a', 'b', 'c'])
117
+ * console.log(result) // [[1, 'a'], [2, 'b'], [3, 'c']]
118
+ * ```
119
+ */
120
+ static zip<T, U>(array1: T[], array2: U[]): [T, U][]
121
+
122
+ /**
123
+ * Unzips an array of arrays into separate arrays.
124
+ *
125
+ * @param array - Array of arrays to unzip
126
+ * @returns Array of separate arrays, one for each position
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * import { Collection } from '@gesslar/toolkit'
131
+ *
132
+ * const zipped = [[1, 'a'], [2, 'b'], [3, 'c']]
133
+ * const result = Collection.unzip(zipped)
134
+ * console.log(result) // [[1, 2, 3], ['a', 'b', 'c']]
135
+ * ```
136
+ */
137
+ static unzip<T>(array: T[][]): T[][]
138
+
139
+ /**
140
+ * Maps an array through an async function, executing operations sequentially.
141
+ *
142
+ * Unlike Promise.all(array.map(fn)), this executes each async operation
143
+ * one at a time, maintaining order and preventing overwhelming external resources.
144
+ *
145
+ * @param array - The array to map over
146
+ * @param asyncFn - Async function called for each element: (element) => Promise<result>
147
+ * @returns Promise resolving to array of mapped results
148
+ *
149
+ * @throws {Sass} If array is not an Array or asyncFn is not a Function
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * import { Collection } from '@gesslar/toolkit'
154
+ *
155
+ * // Sequential API calls (won't overwhelm server)
156
+ * const urls = ['url1', 'url2', 'url3']
157
+ * const responses = await Collection.asyncMap(urls, async (url) => {
158
+ * return await fetch(url).then(r => r.json())
159
+ * })
160
+ * console.log(responses) // [data1, data2, data3]
161
+ *
162
+ * // Works with sync functions too
163
+ * const numbers = [1, 2, 3]
164
+ * const doubled = await Collection.asyncMap(numbers, async (n) => n * 2)
165
+ * console.log(doubled) // [2, 4, 6]
166
+ * ```
167
+ */
168
+ static asyncMap<T, R>(array: T[], asyncFn: (element: T) => Promise<R> | R): Promise<R[]>
169
+
170
+ /** Check if all elements in an array are of a specified type or all the same type */
171
+ static isArrayUniform(arr: Array<unknown>, type?: string): boolean
172
+
173
+ /** Remove duplicate elements from an array, returning a new array with unique values */
174
+ static isArrayUnique<T>(arr: Array<T>): Array<T>
175
+
176
+ /** Get the intersection of two arrays */
177
+ static arrayIntersection<T>(arr1: Array<T>, arr2: Array<T>): Array<T>
178
+
179
+ /** Check if two arrays have any elements in common */
180
+ static arrayIntersects<T>(arr1: Array<T>, arr2: Array<T>): boolean
181
+
182
+ /** Pad an array to a specified length */
183
+ static arrayPad<T>(arr: Array<T>, length: number, value: T, position?: number): Array<T>
184
+
185
+ /** Check if all elements in an array are strings */
186
+ static uniformStringArray(arr: Array<unknown>): arr is Array<string>
187
+
188
+ /** Filter an array asynchronously */
189
+ static asyncFilter<T>(arr: Array<T>, predicate: (item: T) => Promise<boolean>): Promise<Array<T>>
190
+
191
+ /** Clone an object */
192
+ static cloneObject<T extends Record<string, any>>(obj: T, freeze?: boolean): T
193
+
194
+ /** Check if an object is empty */
195
+ static isObjectEmpty(obj: Record<string, any>): boolean
196
+
197
+ /** Ensure a nested path of objects exists */
198
+ static assureObjectPath(obj: Record<string, any>, keys: Array<string>): Record<string, any>
199
+
200
+ /** Set a value in a nested object structure */
201
+ static setNestedValue(obj: Record<string, any>, keys: Array<string>, value: unknown): void
202
+
203
+ /** Deeply merge objects */
204
+ static mergeObject<T extends Record<string, any>>(...sources: Array<T>): T
205
+
206
+ /** Recursively freeze an object */
207
+ static deepFreezeObject<T>(obj: T): T
208
+
209
+ /** Map an object using a transformer function */
210
+ static mapObject<T extends Record<string, any>, R>(
211
+ original: T,
212
+ transformer: (key: string, value: any) => R | Promise<R>,
213
+ mutate?: boolean
214
+ ): Promise<Record<string, R>>
215
+
216
+ /** Allocate an object from a source array and spec */
217
+ static allocateObject(source: Array<unknown>, spec: Array<unknown> | ((source: Array<unknown>) => Promise<Array<unknown>> | Array<unknown>)): Promise<Record<string, unknown>>
218
+ }
@@ -91,118 +91,6 @@ export default class Data {
91
91
  */
92
92
  static prependString(string: string, prepend: string): string
93
93
 
94
- /**
95
- * Check if all elements in an array are of a specified type or all the same type.
96
- *
97
- * Performs type checking on every element in the array using the toolkit's type
98
- * system. If no type is specified, checks that all elements are of the same type.
99
- * Useful for validating data structures and ensuring type consistency before processing.
100
- *
101
- * @param arr - The array to check for type uniformity. Can be empty (returns true).
102
- * @param type - Optional type name to check against. If not provided, checks that all
103
- * elements have the same type. Must be a valid type from Data.dataTypes.
104
- * @returns True if all elements match the specified type or are all the same type,
105
- * false if there's any type mismatch or if type parameter is invalid
106
- *
107
- * @example
108
- * ```typescript
109
- * import { Data } from '@gesslar/toolkit'
110
- *
111
- * // Check for specific type uniformity
112
- * const numbers = [1, 2, 3, 4, 5]
113
- * const strings = ['a', 'b', 'c']
114
- * const mixed = [1, 'a', true]
115
- *
116
- * console.log(Data.isArrayUniform(numbers, 'number')) // true
117
- * console.log(Data.isArrayUniform(strings, 'string')) // true
118
- * console.log(Data.isArrayUniform(mixed, 'number')) // false
119
- *
120
- * // Check that all elements are the same type (any type)
121
- * console.log(Data.isArrayUniform(numbers)) // true (all numbers)
122
- * console.log(Data.isArrayUniform(strings)) // true (all strings)
123
- * console.log(Data.isArrayUniform(mixed)) // false (mixed types)
124
- * console.log(Data.isArrayUniform([])) // true (empty array)
125
- *
126
- * // Useful for validation before processing
127
- * function processNumbers(data: unknown[]) {
128
- * if (!Data.isArrayUniform(data, 'number')) {
129
- * throw new Error('Array must contain only numbers')
130
- * }
131
- * return data.reduce((sum, num) => sum + num, 0)
132
- * }
133
- * ```
134
- */
135
- static isArrayUniform(arr: Array<unknown>, type?: string): boolean
136
-
137
- /**
138
- * Remove duplicate elements from an array, returning a new array with unique values.
139
- *
140
- * Creates a new array containing only the first occurrence of each unique value,
141
- * preserving the original order of first appearances. Uses strict equality (===)
142
- * for primitive comparisons and shallow comparison for objects.
143
- *
144
- * @param arr - The array to remove duplicates from. Can be empty or contain any types.
145
- * @returns A new array with duplicate elements removed, preserving order of first occurrence
146
- *
147
- * @example
148
- * ```typescript
149
- * import { Data } from '@gesslar/toolkit'
150
- *
151
- * // Basic duplicate removal
152
- * const numbers = [1, 2, 2, 3, 3, 4]
153
- * const uniqueNumbers = Data.isArrayUnique(numbers)
154
- * console.log(uniqueNumbers) // [1, 2, 3, 4]
155
- *
156
- * // Mixed types
157
- * const mixed = ['a', 1, 'a', 2, 1, 'b']
158
- * const uniqueMixed = Data.isArrayUnique(mixed)
159
- * console.log(uniqueMixed) // ['a', 1, 2, 'b']
160
- *
161
- * // Object arrays (shallow comparison)
162
- * const users = [
163
- * { id: 1, name: 'Alice' },
164
- * { id: 2, name: 'Bob' },
165
- * { id: 1, name: 'Alice' } // Different object reference, not filtered
166
- * ]
167
- * const uniqueUsers = Data.isArrayUnique(users)
168
- * console.log(uniqueUsers.length) // 3 (objects compared by reference)
169
- *
170
- * // Empty and single element arrays
171
- * console.log(Data.isArrayUnique([])) // []
172
- * console.log(Data.isArrayUnique(['single'])) // ['single']
173
- *
174
- * // String array deduplication
175
- * const tags = ['javascript', 'node', 'javascript', 'typescript', 'node']
176
- * const uniqueTags = Data.isArrayUnique(tags)
177
- * console.log(uniqueTags) // ['javascript', 'node', 'typescript']
178
- * ```
179
- */
180
- static isArrayUnique<T>(arr: Array<T>): Array<T>
181
-
182
- /** Get the intersection of two arrays */
183
- static arrayIntersection<T>(arr1: Array<T>, arr2: Array<T>): Array<T>
184
-
185
- /** Check if two arrays have any elements in common */
186
- static arrayIntersects<T>(arr1: Array<T>, arr2: Array<T>): boolean
187
-
188
- /** Pad an array to a specified length */
189
- static arrayPad<T>(arr: Array<T>, length: number, value: T, position?: number): Array<T>
190
-
191
- /** Clone an object */
192
- static cloneObject<T extends Record<string, any>>(obj: T, freeze?: boolean): T
193
-
194
- /** Allocate an object from a source array and spec */
195
- static allocateObject(source: Array<unknown>, spec: Array<unknown> | ((source: Array<unknown>) => Promise<Array<unknown>> | Array<unknown>)): Promise<Record<string, unknown>>
196
-
197
- /** Map an object using a transformer function */
198
- static mapObject<T extends Record<string, any>, R>(
199
- original: T,
200
- transformer: (key: string, value: any) => R | Promise<R>,
201
- mutate?: boolean
202
- ): Promise<Record<string, R>>
203
-
204
- /** Check if an object is empty */
205
- static isObjectEmpty(obj: Record<string, any>): boolean
206
94
 
207
95
  /** Create a type spec from a string */
208
96
  static newTypeSpec(string: string, options?: any): Type
@@ -10,6 +10,7 @@ declare class Util {
10
10
  *
11
11
  * @param text - The text to capitalize
12
12
  * @returns Text with first letter capitalized
13
+ * @throws {TypeError} If `text` is not a string
13
14
  *
14
15
  * @example
15
16
  * ```typescript
@@ -6,6 +6,7 @@ export { default as FS } from './FS.js'
6
6
 
7
7
  // Utility classes
8
8
  export { default as Cache } from './Cache.js'
9
+ export { default as Collection } from './Collection.js'
9
10
  export { default as Data } from './Data.js'
10
11
  export { default as Glog } from './Glog.js'
11
12
  export { default as Sass } from './Sass.js'