@ls-stack/utils 3.24.0 → 3.24.1

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/docs/testUtils.md CHANGED
@@ -14,7 +14,7 @@
14
14
  function compactSnapshot(value, __namedParameters): string;
15
15
  ```
16
16
 
17
- Defined in: [packages/utils/src/testUtils.ts:439](https://github.com/lucasols/utils/blob/main/packages/utils/src/testUtils.ts#L439)
17
+ Defined in: [packages/utils/src/testUtils.ts:487](https://github.com/lucasols/utils/blob/main/packages/utils/src/testUtils.ts#L487)
18
18
 
19
19
  #### Parameters
20
20
 
package/lib/testUtils.cjs CHANGED
@@ -753,14 +753,20 @@ function waitController() {
753
753
  }
754
754
  };
755
755
  }
756
- function matchesKeyPattern(path, pattern) {
757
- if (path === pattern) {
756
+ function matchesKeyPattern(fullPath, key, pattern, currentPath) {
757
+ if (fullPath === pattern) {
758
758
  return true;
759
759
  }
760
- if (pattern.includes("*")) {
761
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, "[^.]*");
762
- const regex = new RegExp(`^${regexPattern}$`);
763
- return regex.test(path);
760
+ if (pattern.startsWith("*.")) {
761
+ const propName = pattern.slice(2);
762
+ return currentPath !== "" && key === propName;
763
+ }
764
+ if (pattern.startsWith("*") && !pattern.startsWith("*.")) {
765
+ const propName = pattern.slice(1);
766
+ return key === propName;
767
+ }
768
+ if (!pattern.includes("*")) {
769
+ return currentPath === "" && key === pattern;
764
770
  }
765
771
  return false;
766
772
  }
@@ -792,12 +798,19 @@ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "",
792
798
  }
793
799
  if (Array.isArray(value)) {
794
800
  if (visited.has(value)) {
795
- throw new Error("Circular reference detected in array during key filtering");
801
+ throw new Error(
802
+ "Circular reference detected in array during key filtering"
803
+ );
796
804
  }
797
805
  visited.add(value);
798
806
  try {
799
807
  return value.map(
800
- (item, index) => applyKeyFiltering(item, { rejectKeys, filterKeys }, currentPath ? `${currentPath}[${index}]` : `[${index}]`, visited)
808
+ (item, index) => applyKeyFiltering(
809
+ item,
810
+ { rejectKeys, filterKeys },
811
+ currentPath ? `${currentPath}[${index}]` : `[${index}]`,
812
+ visited
813
+ )
801
814
  );
802
815
  } finally {
803
816
  visited.delete(value);
@@ -805,7 +818,9 @@ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "",
805
818
  }
806
819
  if (isPlainObject(value)) {
807
820
  if (visited.has(value)) {
808
- throw new Error("Circular reference detected in object during key filtering");
821
+ throw new Error(
822
+ "Circular reference detected in object during key filtering"
823
+ );
809
824
  }
810
825
  visited.add(value);
811
826
  try {
@@ -813,23 +828,43 @@ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "",
813
828
  for (const [key, itemValue] of Object.entries(value)) {
814
829
  const fullPath = currentPath ? `${currentPath}.${key}` : key;
815
830
  if (rejectKeys?.some(
816
- (rejectPath) => matchesKeyPattern(fullPath, rejectPath) || matchesKeyPattern(key, rejectPath)
831
+ (rejectPath) => matchesKeyPattern(fullPath, key, rejectPath, currentPath)
817
832
  )) {
818
833
  continue;
819
834
  }
820
835
  if (filterKeys) {
821
- const shouldInclude = filterKeys.some(
822
- (filterPath) => (
823
- // Exact match
824
- matchesKeyPattern(fullPath, filterPath) || matchesKeyPattern(key, filterPath) || // This path is a parent of a filter pattern (so we include it to allow children)
825
- isParentOfPattern(fullPath, filterPath)
826
- )
836
+ const exactMatch = filterKeys.some(
837
+ (filterPath) => matchesKeyPattern(fullPath, key, filterPath, currentPath)
838
+ );
839
+ const isParent = filterKeys.some(
840
+ (filterPath) => isParentOfPattern(fullPath, filterPath)
827
841
  );
828
- if (!shouldInclude) {
842
+ if (!exactMatch && !isParent) {
829
843
  continue;
830
844
  }
845
+ if (exactMatch) {
846
+ result[key] = applyKeyFiltering(
847
+ itemValue,
848
+ { rejectKeys },
849
+ fullPath,
850
+ visited
851
+ );
852
+ } else {
853
+ result[key] = applyKeyFiltering(
854
+ itemValue,
855
+ { rejectKeys, filterKeys },
856
+ fullPath,
857
+ visited
858
+ );
859
+ }
860
+ } else {
861
+ result[key] = applyKeyFiltering(
862
+ itemValue,
863
+ { rejectKeys, filterKeys },
864
+ fullPath,
865
+ visited
866
+ );
831
867
  }
832
- result[key] = applyKeyFiltering(itemValue, { rejectKeys, filterKeys }, fullPath, visited);
833
868
  }
834
869
  return result;
835
870
  } finally {
@@ -849,7 +884,10 @@ function compactSnapshot(value, {
849
884
  } = {}) {
850
885
  let processedValue = value;
851
886
  if (rejectKeys || filterKeys) {
852
- processedValue = applyKeyFiltering(processedValue, { rejectKeys, filterKeys });
887
+ processedValue = applyKeyFiltering(processedValue, {
888
+ rejectKeys: Array.isArray(rejectKeys) ? rejectKeys : rejectKeys ? [rejectKeys] : void 0,
889
+ filterKeys: Array.isArray(filterKeys) ? filterKeys : filterKeys ? [filterKeys] : void 0
890
+ });
853
891
  }
854
892
  processedValue = showBooleansAs ? replaceBooleansWithEmoji(processedValue, showBooleansAs) : processedValue;
855
893
  return `
@@ -51,8 +51,59 @@ declare function compactSnapshot(value: unknown, { collapseObjects, maxLineLengt
51
51
  trueText?: string;
52
52
  falseText?: string;
53
53
  };
54
- rejectKeys?: string[];
55
- filterKeys?: string[];
54
+ /**
55
+ * Reject (exclude) keys from the snapshot using pattern matching.
56
+ *
57
+ * **Pattern Syntax:**
58
+ * - `'prop'` - Only root-level properties named 'prop'
59
+ * - `'prop.nested'` - Exact nested property paths like `obj.prop.nested`
60
+ * - `'*prop'` - Any property named exactly 'prop' at any level (root or nested)
61
+ * - `'*.prop'` - Any nested property named 'prop' (excludes root-level matches)
62
+ *
63
+ * **Examples:**
64
+ * ```typescript
65
+ * // Reject root-level 'secret' only
66
+ * rejectKeys: ['secret']
67
+ *
68
+ * // Reject nested 'password' properties only
69
+ * rejectKeys: ['*.password']
70
+ *
71
+ * // Reject any 'apiKey' property at any level
72
+ * rejectKeys: ['*apiKey']
73
+ *
74
+ * // Reject specific nested path
75
+ * rejectKeys: ['user.settings.theme']
76
+ * ```
77
+ */
78
+ rejectKeys?: string[] | string;
79
+ /**
80
+ * Filter (include only) keys that match the specified patterns.
81
+ *
82
+ * **Pattern Syntax:** (same as rejectKeys)
83
+ * - `'prop'` - Only root-level properties named 'prop'
84
+ * - `'prop.nested'` - Exact nested property paths like `obj.prop.nested`
85
+ * - `'*prop'` - Any property named exactly 'prop' at any level (root or nested)
86
+ * - `'*.prop'` - Any nested property named 'prop' (excludes root-level matches)
87
+ *
88
+ * **Examples:**
89
+ * ```typescript
90
+ * // Include only root-level 'user'
91
+ * filterKeys: ['user']
92
+ *
93
+ * // Include all 'name' properties at any level
94
+ * filterKeys: ['*name']
95
+ *
96
+ * // Include only nested 'id' properties
97
+ * filterKeys: ['*.id']
98
+ *
99
+ * // Include specific nested paths
100
+ * filterKeys: ['user.profile.email', 'settings.theme']
101
+ * ```
102
+ *
103
+ * **Note:** When filtering, parent paths are automatically included if needed
104
+ * to preserve the structure for nested matches.
105
+ */
106
+ filterKeys?: string[] | string;
56
107
  }): string;
57
108
 
58
109
  export { compactSnapshot, createLoggerStore, getResultFn, waitController };
@@ -51,8 +51,59 @@ declare function compactSnapshot(value: unknown, { collapseObjects, maxLineLengt
51
51
  trueText?: string;
52
52
  falseText?: string;
53
53
  };
54
- rejectKeys?: string[];
55
- filterKeys?: string[];
54
+ /**
55
+ * Reject (exclude) keys from the snapshot using pattern matching.
56
+ *
57
+ * **Pattern Syntax:**
58
+ * - `'prop'` - Only root-level properties named 'prop'
59
+ * - `'prop.nested'` - Exact nested property paths like `obj.prop.nested`
60
+ * - `'*prop'` - Any property named exactly 'prop' at any level (root or nested)
61
+ * - `'*.prop'` - Any nested property named 'prop' (excludes root-level matches)
62
+ *
63
+ * **Examples:**
64
+ * ```typescript
65
+ * // Reject root-level 'secret' only
66
+ * rejectKeys: ['secret']
67
+ *
68
+ * // Reject nested 'password' properties only
69
+ * rejectKeys: ['*.password']
70
+ *
71
+ * // Reject any 'apiKey' property at any level
72
+ * rejectKeys: ['*apiKey']
73
+ *
74
+ * // Reject specific nested path
75
+ * rejectKeys: ['user.settings.theme']
76
+ * ```
77
+ */
78
+ rejectKeys?: string[] | string;
79
+ /**
80
+ * Filter (include only) keys that match the specified patterns.
81
+ *
82
+ * **Pattern Syntax:** (same as rejectKeys)
83
+ * - `'prop'` - Only root-level properties named 'prop'
84
+ * - `'prop.nested'` - Exact nested property paths like `obj.prop.nested`
85
+ * - `'*prop'` - Any property named exactly 'prop' at any level (root or nested)
86
+ * - `'*.prop'` - Any nested property named 'prop' (excludes root-level matches)
87
+ *
88
+ * **Examples:**
89
+ * ```typescript
90
+ * // Include only root-level 'user'
91
+ * filterKeys: ['user']
92
+ *
93
+ * // Include all 'name' properties at any level
94
+ * filterKeys: ['*name']
95
+ *
96
+ * // Include only nested 'id' properties
97
+ * filterKeys: ['*.id']
98
+ *
99
+ * // Include specific nested paths
100
+ * filterKeys: ['user.profile.email', 'settings.theme']
101
+ * ```
102
+ *
103
+ * **Note:** When filtering, parent paths are automatically included if needed
104
+ * to preserve the structure for nested matches.
105
+ */
106
+ filterKeys?: string[] | string;
56
107
  }): string;
57
108
 
58
109
  export { compactSnapshot, createLoggerStore, getResultFn, waitController };
package/lib/testUtils.js CHANGED
@@ -253,14 +253,20 @@ function waitController() {
253
253
  }
254
254
  };
255
255
  }
256
- function matchesKeyPattern(path, pattern) {
257
- if (path === pattern) {
256
+ function matchesKeyPattern(fullPath, key, pattern, currentPath) {
257
+ if (fullPath === pattern) {
258
258
  return true;
259
259
  }
260
- if (pattern.includes("*")) {
261
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, "[^.]*");
262
- const regex = new RegExp(`^${regexPattern}$`);
263
- return regex.test(path);
260
+ if (pattern.startsWith("*.")) {
261
+ const propName = pattern.slice(2);
262
+ return currentPath !== "" && key === propName;
263
+ }
264
+ if (pattern.startsWith("*") && !pattern.startsWith("*.")) {
265
+ const propName = pattern.slice(1);
266
+ return key === propName;
267
+ }
268
+ if (!pattern.includes("*")) {
269
+ return currentPath === "" && key === pattern;
264
270
  }
265
271
  return false;
266
272
  }
@@ -292,12 +298,19 @@ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "",
292
298
  }
293
299
  if (Array.isArray(value)) {
294
300
  if (visited.has(value)) {
295
- throw new Error("Circular reference detected in array during key filtering");
301
+ throw new Error(
302
+ "Circular reference detected in array during key filtering"
303
+ );
296
304
  }
297
305
  visited.add(value);
298
306
  try {
299
307
  return value.map(
300
- (item, index) => applyKeyFiltering(item, { rejectKeys, filterKeys }, currentPath ? `${currentPath}[${index}]` : `[${index}]`, visited)
308
+ (item, index) => applyKeyFiltering(
309
+ item,
310
+ { rejectKeys, filterKeys },
311
+ currentPath ? `${currentPath}[${index}]` : `[${index}]`,
312
+ visited
313
+ )
301
314
  );
302
315
  } finally {
303
316
  visited.delete(value);
@@ -305,7 +318,9 @@ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "",
305
318
  }
306
319
  if (isPlainObject(value)) {
307
320
  if (visited.has(value)) {
308
- throw new Error("Circular reference detected in object during key filtering");
321
+ throw new Error(
322
+ "Circular reference detected in object during key filtering"
323
+ );
309
324
  }
310
325
  visited.add(value);
311
326
  try {
@@ -313,23 +328,43 @@ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "",
313
328
  for (const [key, itemValue] of Object.entries(value)) {
314
329
  const fullPath = currentPath ? `${currentPath}.${key}` : key;
315
330
  if (rejectKeys?.some(
316
- (rejectPath) => matchesKeyPattern(fullPath, rejectPath) || matchesKeyPattern(key, rejectPath)
331
+ (rejectPath) => matchesKeyPattern(fullPath, key, rejectPath, currentPath)
317
332
  )) {
318
333
  continue;
319
334
  }
320
335
  if (filterKeys) {
321
- const shouldInclude = filterKeys.some(
322
- (filterPath) => (
323
- // Exact match
324
- matchesKeyPattern(fullPath, filterPath) || matchesKeyPattern(key, filterPath) || // This path is a parent of a filter pattern (so we include it to allow children)
325
- isParentOfPattern(fullPath, filterPath)
326
- )
336
+ const exactMatch = filterKeys.some(
337
+ (filterPath) => matchesKeyPattern(fullPath, key, filterPath, currentPath)
338
+ );
339
+ const isParent = filterKeys.some(
340
+ (filterPath) => isParentOfPattern(fullPath, filterPath)
327
341
  );
328
- if (!shouldInclude) {
342
+ if (!exactMatch && !isParent) {
329
343
  continue;
330
344
  }
345
+ if (exactMatch) {
346
+ result[key] = applyKeyFiltering(
347
+ itemValue,
348
+ { rejectKeys },
349
+ fullPath,
350
+ visited
351
+ );
352
+ } else {
353
+ result[key] = applyKeyFiltering(
354
+ itemValue,
355
+ { rejectKeys, filterKeys },
356
+ fullPath,
357
+ visited
358
+ );
359
+ }
360
+ } else {
361
+ result[key] = applyKeyFiltering(
362
+ itemValue,
363
+ { rejectKeys, filterKeys },
364
+ fullPath,
365
+ visited
366
+ );
331
367
  }
332
- result[key] = applyKeyFiltering(itemValue, { rejectKeys, filterKeys }, fullPath, visited);
333
368
  }
334
369
  return result;
335
370
  } finally {
@@ -349,7 +384,10 @@ function compactSnapshot(value, {
349
384
  } = {}) {
350
385
  let processedValue = value;
351
386
  if (rejectKeys || filterKeys) {
352
- processedValue = applyKeyFiltering(processedValue, { rejectKeys, filterKeys });
387
+ processedValue = applyKeyFiltering(processedValue, {
388
+ rejectKeys: Array.isArray(rejectKeys) ? rejectKeys : rejectKeys ? [rejectKeys] : void 0,
389
+ filterKeys: Array.isArray(filterKeys) ? filterKeys : filterKeys ? [filterKeys] : void 0
390
+ });
353
391
  }
354
392
  processedValue = showBooleansAs ? replaceBooleansWithEmoji(processedValue, showBooleansAs) : processedValue;
355
393
  return `
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ls-stack/utils",
3
3
  "description": "Universal TypeScript utilities for browser and Node.js",
4
- "version": "3.24.0",
4
+ "version": "3.24.1",
5
5
  "license": "MIT",
6
6
  "files": [
7
7
  "lib",