@gesslar/toolkit 0.2.4 → 0.2.6

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.4",
3
+ "version": "0.2.6",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -49,16 +49,16 @@
49
49
  "license": "Unlicense",
50
50
  "homepage": "https://github.com/gesslar/toolkit#readme",
51
51
  "dependencies": {
52
- "globby": "^14.1.0",
52
+ "globby": "^15.0.0",
53
53
  "json5": "^2.2.3",
54
54
  "yaml": "^2.8.1"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@stylistic/eslint-plugin": "^5.4.0",
58
- "@types/node": "^24.5.2",
59
- "@typescript-eslint/eslint-plugin": "^8.44.1",
60
- "@typescript-eslint/parser": "^8.44.1",
58
+ "@types/node": "^24.6.2",
59
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
60
+ "@typescript-eslint/parser": "^8.45.0",
61
61
  "eslint": "^9.36.0",
62
- "eslint-plugin-jsdoc": "^60.4.1"
62
+ "eslint-plugin-jsdoc": "^60.7.1"
63
63
  }
64
64
  }
@@ -139,6 +139,11 @@ export default class Collection {
139
139
 
140
140
  Valid.type(arr, req, `Invalid array. Expected '${req}', got '${arrType}'`)
141
141
 
142
+ // Validate type parameter if provided
143
+ if(type !== undefined) {
144
+ Valid.type(type, "string", `Invalid type parameter. Expected 'string', got '${Data.typeOf(type)}'`)
145
+ }
146
+
142
147
  const checkType = type ? Util.capitalize(type) : null
143
148
 
144
149
  return arr.every(
@@ -169,7 +174,7 @@ export default class Collection {
169
174
  * @param {Array} arr2 - The second array.
170
175
  * @returns {Array} The intersection of the two arrays.
171
176
  */
172
- static arrayIntersection(arr1, arr2) {
177
+ static intersection(arr1, arr2) {
173
178
  const req = "Array"
174
179
  const arr1Type = Data.typeOf(arr1)
175
180
  const arr2Type = Data.typeOf(arr2)
@@ -190,14 +195,14 @@ export default class Collection {
190
195
  * array for efficiency.
191
196
  *
192
197
  * Example:
193
- * arrayIntersects([1, 2, 3], [3, 4, 5]) // returns true
194
- * arrayIntersects(["a", "b"], ["c", "d"]) // returns false
198
+ * Collection.intersects([1, 2, 3], [3, 4, 5]) // returns true
199
+ * Collection.intersects(["a", "b"], ["c", "d"]) // returns false
195
200
  *
196
201
  * @param {Array} arr1 - The first array to check for intersection.
197
202
  * @param {Array} arr2 - The second array to check for intersection.
198
203
  * @returns {boolean} True if any element is shared between the arrays, false otherwise.
199
204
  */
200
- static arrayIntersects(arr1, arr2) {
205
+ static intersects(arr1, arr2) {
201
206
  const req = "Array"
202
207
  const arr1Type = Data.typeOf(arr1)
203
208
  const arr2Type = Data.typeOf(arr2)
@@ -245,22 +250,6 @@ export default class Collection {
245
250
  throw Sass.new("Invalid position")
246
251
  }
247
252
 
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
253
  /**
265
254
  * Filters an array asynchronously using a predicate function.
266
255
  * Applies the predicate to all items in parallel and returns filtered results.
@@ -345,14 +334,11 @@ export default class Collection {
345
334
  let current = obj // a moving reference to internal objects within obj
346
335
  const len = keys.length
347
336
 
337
+ Valid.prototypePollutionProtection(keys)
338
+
348
339
  for(let i = 0; i < len; i++) {
349
340
  const elem = keys[i]
350
341
 
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
342
  if(!current[elem])
357
343
  current[elem] = {}
358
344
 
@@ -383,10 +369,7 @@ export default class Collection {
383
369
  const nested = Collection.assureObjectPath(obj, keys.slice(0, -1))
384
370
  const finalKey = keys[keys.length-1]
385
371
 
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
- }
372
+ Valid.prototypePollutionProtection([finalKey])
390
373
 
391
374
  nested[finalKey] = value
392
375
  }
@@ -474,7 +457,6 @@ export default class Collection {
474
457
  * @returns {Promise<object>} The allocated object
475
458
  */
476
459
  static async allocateObject(source, spec) {
477
- // Data
478
460
  const workSource = [],
479
461
  workSpec = [],
480
462
  result = {}
@@ -516,4 +498,65 @@ export default class Collection {
516
498
  return result
517
499
  }
518
500
 
501
+ static flattenObjectArray(objects) {
502
+ const req = "Array"
503
+ const type = Data.typeOf(objects)
504
+
505
+ Valid.type(objects, req, `Invalid objects array. Expected '${req}', got '${type}'`)
506
+
507
+ return objects.reduce((acc, curr) => {
508
+ const elemType = Data.typeOf(curr)
509
+
510
+ if(!Data.isPlainObject(curr))
511
+ throw Sass.new(`Invalid array element. Expected plain object, got '${elemType}'`)
512
+
513
+ Valid.prototypePollutionProtection(Object.keys(curr))
514
+
515
+ Object.entries(curr).forEach(([key, value]) => {
516
+ if(!acc[key])
517
+ acc[key] = []
518
+
519
+ acc[key].push(value)
520
+ })
521
+
522
+ return acc
523
+ }, {})
524
+ }
525
+
526
+ static trimArray(arr, except=[]) {
527
+ Valid.type(arr, "Array")
528
+ Valid.type(except, "Array")
529
+
530
+ Collection.trimArrayLeft(arr, except)
531
+ Collection.trimArrayRight(arr, except)
532
+
533
+ return arr
534
+ }
535
+
536
+ static trimArrayRight(arr, except=[]) {
537
+ Valid.type(arr, "Array")
538
+ Valid.type(except, "Array")
539
+
540
+ arr.reverse()
541
+ Collection.trimArrayLeft(arr, except)
542
+ arr.reverse()
543
+
544
+ return arr
545
+ }
546
+
547
+ static trimArrayLeft(arr, except=[]) {
548
+ Valid.type(arr, "Array")
549
+ Valid.type(except, "Array")
550
+
551
+ while(arr.length > 0) {
552
+ const value = arr[0]
553
+
554
+ if(value || except.includes(value))
555
+ break
556
+
557
+ arr.shift()
558
+ }
559
+
560
+ return arr
561
+ }
519
562
  }
package/src/lib/Data.js CHANGED
@@ -323,19 +323,6 @@ export default class Data {
323
323
  }, {})
324
324
  }
325
325
 
326
- /**
327
- * Checks if all elements in an array are strings.
328
- *
329
- * @param {Array} arr - The array to check.
330
- * @returns {boolean} Returns true if all elements are strings, false otherwise.
331
- * @example
332
- * uniformStringArray(['a', 'b', 'c']) // returns true
333
- * uniformStringArray(['a', 1, 'c']) // returns false
334
- */
335
- static uniformStringArray(arr) {
336
- return Array.isArray(arr) && arr.every(item => typeof item === "string")
337
- }
338
-
339
326
  /**
340
327
  * Filters an array asynchronously using a predicate function.
341
328
  * Applies the predicate to all items in parallel and returns filtered results.
package/src/lib/FS.js CHANGED
@@ -3,7 +3,6 @@ import path from "node:path"
3
3
  import url from "node:url"
4
4
 
5
5
  import Collection from "./Collection.js"
6
- import Data from "./Data.js"
7
6
  import DirectoryObject from "./DirectoryObject.js"
8
7
  import FileObject from "./FileObject.js"
9
8
  import Sass from "./Sass.js"
@@ -73,7 +72,7 @@ export default class FS {
73
72
  (
74
73
  (typeof glob === "string" && glob.length > 0) ||
75
74
  (
76
- Array.isArray(glob) && Data.uniformStringArray(glob) &&
75
+ Collection.isArrayUniform(glob, "string") &&
77
76
  glob.length > 0
78
77
  )
79
78
  ),
@@ -92,7 +91,7 @@ export default class FS {
92
91
 
93
92
  if(
94
93
  Array.isArray(globbyArray) &&
95
- Data.uniformStringArray(globbyArray) &&
94
+ Collection.isArrayUniform(globbyArray, "string") &&
96
95
  !globbyArray.length
97
96
  )
98
97
  throw Sass.new(
package/src/lib/Util.js CHANGED
@@ -2,6 +2,8 @@ import {createHash} from "node:crypto"
2
2
  import {performance} from "node:perf_hooks"
3
3
  import {EventEmitter} from "node:events"
4
4
  import Sass from "./Sass.js"
5
+ import Valid from "./Valid.js"
6
+ import Collection from "./Collection.js"
5
7
 
6
8
  /**
7
9
  * Utility class providing common helper functions for string manipulation,
@@ -300,9 +302,18 @@ export default class Util {
300
302
  }
301
303
 
302
304
  static regexify(input, trim=true, flags=[]) {
305
+ Valid.type(input, "String")
306
+ Valid.type(trim, "Boolean")
307
+ Valid.type(flags, "Array")
308
+
309
+ Valid.assert(
310
+ flags.length === 0 ||
311
+ (flags.length > 0 && Collection.isArrayUniform(flags, "String")),
312
+ "All flags must be strings")
313
+
303
314
  return new RegExp(
304
315
  input
305
- .split("\n")
316
+ .split(/\r\n|\r|\n/)
306
317
  .map(i => trim ? i.trim() : i)
307
318
  .filter(i => trim ? Boolean(i) : true)
308
319
  .join("")
package/src/lib/Valid.js CHANGED
@@ -2,6 +2,7 @@ import _assert from "node:assert/strict"
2
2
 
3
3
  import Sass from "./Sass.js"
4
4
  import Data from "./Data.js"
5
+ import Collection from "./Collection.js"
5
6
 
6
7
  export default class Valid {
7
8
  /**
@@ -46,4 +47,15 @@ export default class Valid {
46
47
  throw Sass.new(`${message}${arg ? `: ${arg}` : ""}`)
47
48
  }
48
49
 
50
+ static #restrictedProto = ["__proto__", "constructor", "prototype"]
51
+ static prototypePollutionProtection(keys) {
52
+ Valid.type(keys, "String[]")
53
+
54
+ const oopsIDidItAgain = Collection.intersection(this.#restrictedProto, keys)
55
+
56
+ Valid.assert(
57
+ oopsIDidItAgain.length === 0,
58
+ `We don't pee in your pool, don't pollute ours with your ${String(oopsIDidItAgain)}`
59
+ )
60
+ }
49
61
  }
@@ -138,7 +138,7 @@ export default class Collection {
138
138
 
139
139
  /**
140
140
  * Maps an array through an async function, executing operations sequentially.
141
- *
141
+ *
142
142
  * Unlike Promise.all(array.map(fn)), this executes each async operation
143
143
  * one at a time, maintaining order and preventing overwhelming external resources.
144
144
  *
@@ -174,17 +174,14 @@ export default class Collection {
174
174
  static isArrayUnique<T>(arr: Array<T>): Array<T>
175
175
 
176
176
  /** Get the intersection of two arrays */
177
- static arrayIntersection<T>(arr1: Array<T>, arr2: Array<T>): Array<T>
177
+ static intersection<T>(arr1: Array<T>, arr2: Array<T>): Array<T>
178
178
 
179
179
  /** Check if two arrays have any elements in common */
180
- static arrayIntersects<T>(arr1: Array<T>, arr2: Array<T>): boolean
180
+ static intersects<T>(arr1: Array<T>, arr2: Array<T>): boolean
181
181
 
182
182
  /** Pad an array to a specified length */
183
183
  static arrayPad<T>(arr: Array<T>, length: number, value: T, position?: number): Array<T>
184
184
 
185
- /** Check if all elements in an array are strings */
186
- static uniformStringArray(arr: Array<unknown>): arr is Array<string>
187
-
188
185
  /** Filter an array asynchronously */
189
186
  static asyncFilter<T>(arr: Array<T>, predicate: (item: T) => Promise<boolean>): Promise<Array<T>>
190
187
 
@@ -215,4 +212,89 @@ export default class Collection {
215
212
 
216
213
  /** Allocate an object from a source array and spec */
217
214
  static allocateObject(source: Array<unknown>, spec: Array<unknown> | ((source: Array<unknown>) => Promise<Array<unknown>> | Array<unknown>)): Promise<Record<string, unknown>>
215
+
216
+ /**
217
+ * Flattens an array of plain objects into a single object where each key contains
218
+ * an array of all values for that key across all input objects.
219
+ *
220
+ * @param objects - Array of plain objects to flatten
221
+ * @returns Object with keys mapped to arrays of values from all input objects
222
+ *
223
+ * @throws {Sass} If objects is not an Array or if any element is not a plain object
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * import { Collection } from '@gesslar/toolkit'
228
+ *
229
+ * const objects = [
230
+ * { name: 'Alice', age: 25 },
231
+ * { name: 'Bob', age: 30 },
232
+ * { name: 'Charlie', age: 35 }
233
+ * ]
234
+ *
235
+ * const result = Collection.flattenObjectArray(objects)
236
+ * // result: { name: ['Alice', 'Bob', 'Charlie'], age: [25, 30, 35] }
237
+ * ```
238
+ */
239
+ static flattenObjectArray(objects: Array<Record<string, unknown>>): Record<string, Array<unknown>>
240
+
241
+ /**
242
+ * Trims falsy values from both ends of an array.
243
+ *
244
+ * @param arr - The array to trim
245
+ * @param except - Array of values to exclude from trimming (default: [])
246
+ * @returns The trimmed array (modified in place)
247
+ *
248
+ * @throws {Sass} If arr or except is not an Array
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * import { Collection } from '@gesslar/toolkit'
253
+ *
254
+ * const arr = [null, 0, 1, 2, "", undefined]
255
+ * Collection.trimArray(arr)
256
+ * console.log(arr) // [1, 2]
257
+ * ```
258
+ */
259
+ static trimArray<T>(arr: Array<T>, except?: Array<T>): Array<T>
260
+
261
+ /**
262
+ * Trims falsy values from the right end of an array.
263
+ *
264
+ * @param arr - The array to trim
265
+ * @param except - Array of values to exclude from trimming (default: [])
266
+ * @returns The trimmed array (modified in place)
267
+ *
268
+ * @throws {Sass} If arr or except is not an Array
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * import { Collection } from '@gesslar/toolkit'
273
+ *
274
+ * const arr = [1, "", undefined]
275
+ * Collection.trimArrayRight(arr)
276
+ * console.log(arr) // [1]
277
+ * ```
278
+ */
279
+ static trimArrayRight<T>(arr: Array<T>, except?: Array<T>): Array<T>
280
+
281
+ /**
282
+ * Trims falsy values from the left end of an array.
283
+ *
284
+ * @param arr - The array to trim
285
+ * @param except - Array of values to exclude from trimming (default: [])
286
+ * @returns The trimmed array (modified in place)
287
+ *
288
+ * @throws {Sass} If arr or except is not an Array
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * import { Collection } from '@gesslar/toolkit'
293
+ *
294
+ * const arr = [null, undefined, "value"]
295
+ * Collection.trimArrayLeft(arr)
296
+ * console.log(arr) // ["value"]
297
+ * ```
298
+ */
299
+ static trimArrayLeft<T>(arr: Array<T>, except?: Array<T>): Array<T>
218
300
  }
@@ -125,9 +125,6 @@ export default class Data {
125
125
  /** Deeply merge objects */
126
126
  static mergeObject<T extends Record<string, any>>(...sources: Array<T>): T
127
127
 
128
- /** Check if all elements in an array are strings */
129
- static uniformStringArray(arr: Array<unknown>): arr is Array<string>
130
-
131
128
  /** Filter an array asynchronously */
132
129
  static asyncFilter<T>(arr: Array<T>, predicate: (item: T) => Promise<boolean>): Promise<Array<T>>
133
130
 
@@ -224,10 +224,15 @@ declare class Util {
224
224
  * them to be written across multiple lines with proper formatting and indentation.
225
225
  * The resulting regex is functionally identical to writing it as a single line.
226
226
  *
227
- * @param input - Multiline string containing the regex pattern
227
+ * @param input - Multiline string containing the regex pattern (required)
228
228
  * @param trim - Whether to trim whitespace from each line (default: true)
229
229
  * @param flags - Array of regex flags to apply (default: [])
230
230
  * @returns A new RegExp object with the processed pattern
231
+ *
232
+ * @throws Will throw if input is not a string
233
+ * @throws Will throw if trim is not a boolean
234
+ * @throws Will throw if flags is not an array
235
+ * @throws Will throw if flags contains non-string elements
231
236
  *
232
237
  * @example
233
238
  * ```typescript
@@ -7,4 +7,7 @@ export default class Valid {
7
7
 
8
8
  /** Assert a condition */
9
9
  static assert(condition: boolean, message: string, arg?: number | null): void
10
+
11
+ /** Protect against prototype pollution by checking for dangerous keys */
12
+ static prototypePollutionProtection(keys: string[]): void
10
13
  }