@ls-stack/utils 3.23.0 → 3.24.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.
package/lib/testUtils.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/testUtils.ts
21
21
  var testUtils_exports = {};
22
22
  __export(testUtils_exports, {
23
+ compactSnapshot: () => compactSnapshot,
23
24
  createLoggerStore: () => createLoggerStore,
24
25
  getResultFn: () => getResultFn,
25
26
  waitController: () => waitController
@@ -30,6 +31,17 @@ module.exports = __toCommonJS(testUtils_exports);
30
31
  function isObject(value) {
31
32
  return typeof value === "object" && value !== null && !Array.isArray(value);
32
33
  }
34
+ function isPlainObject(value) {
35
+ if (!value || typeof value !== "object") return false;
36
+ const proto = Object.getPrototypeOf(value);
37
+ if (proto === null) {
38
+ return true;
39
+ }
40
+ const Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor;
41
+ if (Ctor === Object) return true;
42
+ const objectCtorString = Object.prototype.constructor.toString();
43
+ return typeof Ctor == "function" && Function.toString.call(Ctor) === objectCtorString;
44
+ }
33
45
 
34
46
  // src/assertions.ts
35
47
  var isObject2 = isObject;
@@ -153,6 +165,368 @@ function defer() {
153
165
  return { resolve, reject, promise };
154
166
  }
155
167
 
168
+ // src/conversions.ts
169
+ function bytesToHumanReadable(bytes) {
170
+ if (bytes < 1024) {
171
+ return `${bytes} B`;
172
+ }
173
+ const kb = bytes / 1024;
174
+ if (kb < 1024) {
175
+ return `${kb.toFixed(2)} KB`;
176
+ }
177
+ const mb = kb / 1024;
178
+ if (mb < 1024) {
179
+ return `${mb.toFixed(2)} MB`;
180
+ }
181
+ const gb = mb / 1024;
182
+ return `${gb.toFixed(2)} GB`;
183
+ }
184
+
185
+ // src/stringUtils.ts
186
+ function truncateString(str, length, ellipsis = "\u2026") {
187
+ if (str.length <= length) return str;
188
+ return str.slice(0, length - 1) + ellipsis;
189
+ }
190
+
191
+ // src/yamlStringify.ts
192
+ function yamlStringify(obj, {
193
+ maxLineLength = 100,
194
+ showUndefined,
195
+ maxDepth = 50,
196
+ collapseObjects = false,
197
+ addRootObjSpaces = "beforeAndAfter"
198
+ } = {}) {
199
+ if (isObject(obj) || Array.isArray(obj) || typeof obj === "function") {
200
+ return `${stringifyValue(obj, "", maxLineLength, !!showUndefined, maxDepth, 0, collapseObjects, addRootObjSpaces)}
201
+ `;
202
+ }
203
+ return JSON.stringify(obj) || "undefined";
204
+ }
205
+ function stringifyValue(value, indent, maxLineLength, showUndefined, maxDepth, depth, collapseObjects, addObjSpaces) {
206
+ let result = "";
207
+ const childIndent = `${indent} `;
208
+ if (isPlainObject(value)) {
209
+ if (Object.keys(value).length === 0) {
210
+ return "{}";
211
+ }
212
+ if (collapseObjects && depth > 0) {
213
+ const entries = Object.entries(value).filter(
214
+ ([, val]) => val !== void 0 || showUndefined
215
+ );
216
+ const isSimpleObject = entries.every(
217
+ ([, val]) => {
218
+ if (typeof val === "string") {
219
+ return !val.includes("'") && !val.includes('"') && !val.includes("\\");
220
+ }
221
+ return typeof val === "number" || typeof val === "boolean" || val === null || val === void 0;
222
+ }
223
+ );
224
+ if (isSimpleObject && entries.length > 0) {
225
+ let line = "{ ";
226
+ line += entries.map(([key, val]) => {
227
+ let valueStr;
228
+ if (typeof val === "string") {
229
+ if (val.includes("'") && !val.includes('"')) {
230
+ valueStr = `"${val}"`;
231
+ } else if (val.includes('"') && !val.includes("'")) {
232
+ valueStr = `'${val}'`;
233
+ } else if (val.includes("'") && val.includes('"')) {
234
+ valueStr = `"${val.replace(/"/g, '\\"')}"`;
235
+ } else {
236
+ valueStr = `'${val}'`;
237
+ }
238
+ } else {
239
+ valueStr = String(val);
240
+ }
241
+ return `${key}: ${valueStr}`;
242
+ }).join(", ");
243
+ line += " }";
244
+ if (line.length <= maxLineLength) {
245
+ return line;
246
+ }
247
+ }
248
+ }
249
+ let prevValue;
250
+ let afterSpaceWasAdded = false;
251
+ for (let [key, objVal] of Object.entries(value)) {
252
+ if (objVal === void 0 && !showUndefined) {
253
+ continue;
254
+ }
255
+ if (depth > maxDepth) {
256
+ objVal = `{max depth reached}`;
257
+ }
258
+ const normalizedValue2 = normalizeValue(objVal);
259
+ if (normalizedValue2 !== null) {
260
+ objVal = normalizedValue2[1];
261
+ key = `${key}{${normalizedValue2[0]}}`;
262
+ }
263
+ const valueString = stringifyValue(
264
+ objVal,
265
+ childIndent,
266
+ maxLineLength,
267
+ showUndefined,
268
+ maxDepth,
269
+ depth + 1,
270
+ collapseObjects,
271
+ addObjSpaces
272
+ );
273
+ const willBeCollapsed = isObject(objVal) && (Object.keys(objVal).length === 0 || collapseObjects && depth + 1 > 0 && Object.entries(objVal).filter(([, val]) => val !== void 0 || showUndefined).every(([, val]) => {
274
+ if (typeof val === "string") {
275
+ return !val.includes("'") && !val.includes('"') && !val.includes("\\");
276
+ }
277
+ return typeof val === "number" || typeof val === "boolean" || val === null || val === void 0;
278
+ }));
279
+ const prevWasCollapsed = prevValue && isObject(prevValue) && (Object.keys(prevValue).length === 0 || collapseObjects && depth + 1 > 0 && Object.entries(prevValue).filter(([, val]) => val !== void 0 || showUndefined).every(([, val]) => {
280
+ if (typeof val === "string") {
281
+ return !val.includes("'") && !val.includes('"') && !val.includes("\\");
282
+ }
283
+ return typeof val === "number" || typeof val === "boolean" || val === null || val === void 0;
284
+ }));
285
+ if (!afterSpaceWasAdded && indent === "" && isObject(objVal) && !willBeCollapsed && prevValue && !prevWasCollapsed && (addObjSpaces === "before" || addObjSpaces === "beforeAndAfter")) {
286
+ result += "\n";
287
+ }
288
+ if (Array.isArray(objVal)) {
289
+ const arrayIsSingleLine = valueString.split("\n").length === 1;
290
+ if (arrayIsSingleLine && !valueString.trim().startsWith("-")) {
291
+ result += `${indent}${key}: `;
292
+ } else {
293
+ result += `${indent}${key}:
294
+ `;
295
+ }
296
+ } else if (isObject(objVal)) {
297
+ const isCollapsedObject = valueString.startsWith("{") && !valueString.includes("\n");
298
+ if (Object.keys(objVal).length === 0 || isCollapsedObject) {
299
+ result += `${indent}${key}: `;
300
+ } else {
301
+ result += `${indent}${key}:
302
+ `;
303
+ }
304
+ } else {
305
+ result += `${indent}${key}: `;
306
+ }
307
+ result += valueString;
308
+ result += "\n";
309
+ if (indent === "") {
310
+ const isCollapsedObject = valueString.startsWith("{") && !valueString.includes("\n") && valueString.length > 2;
311
+ if (isObject(objVal) && !isCollapsedObject) {
312
+ if (addObjSpaces === "after" || addObjSpaces === "beforeAndAfter") {
313
+ result += "\n";
314
+ afterSpaceWasAdded = true;
315
+ } else {
316
+ afterSpaceWasAdded = false;
317
+ }
318
+ }
319
+ }
320
+ prevValue = objVal;
321
+ }
322
+ return result.trimEnd();
323
+ }
324
+ if (Array.isArray(value)) {
325
+ let arrayWasAdded = false;
326
+ if (value.length === 0 || value.every(
327
+ (item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null || item === void 0
328
+ )) {
329
+ let line = "";
330
+ line += `[`;
331
+ line += value.map((item) => {
332
+ let valueToUse = item;
333
+ if (depth > maxDepth) {
334
+ valueToUse = `{max depth reached}`;
335
+ }
336
+ if (typeof valueToUse === "string" && valueToUse.includes("\n")) {
337
+ valueToUse = valueToUse.replace(/\n/g, "\\n");
338
+ }
339
+ return stringifyValue(
340
+ valueToUse,
341
+ "",
342
+ maxLineLength,
343
+ showUndefined,
344
+ maxDepth,
345
+ depth + 1,
346
+ collapseObjects,
347
+ addObjSpaces
348
+ );
349
+ }).join(", ");
350
+ line += "]";
351
+ if (line.length <= maxLineLength) {
352
+ result += line;
353
+ arrayWasAdded = true;
354
+ }
355
+ }
356
+ if (!arrayWasAdded) {
357
+ for (let item of value) {
358
+ if (depth > maxDepth) {
359
+ item = `{max depth reached}`;
360
+ }
361
+ result += `${indent}- `;
362
+ if (Array.isArray(item) || isObject(item)) {
363
+ let arrayString = stringifyValue(
364
+ item,
365
+ childIndent,
366
+ maxLineLength,
367
+ showUndefined,
368
+ maxDepth,
369
+ depth + 1,
370
+ collapseObjects,
371
+ addObjSpaces
372
+ );
373
+ arrayString = arrayString.trimStart();
374
+ result += arrayString;
375
+ } else {
376
+ result += stringifyValue(
377
+ item,
378
+ childIndent,
379
+ maxLineLength,
380
+ showUndefined,
381
+ maxDepth,
382
+ depth + 1,
383
+ collapseObjects,
384
+ addObjSpaces
385
+ );
386
+ }
387
+ result += "\n";
388
+ }
389
+ }
390
+ return result.trimEnd();
391
+ }
392
+ if (typeof value === "string") {
393
+ if (value.includes("\n")) {
394
+ const lines = value.split("\n");
395
+ for (const [i, line] of lines.entries()) {
396
+ if (i === 0) {
397
+ if (value.endsWith("\n")) {
398
+ result += `|`;
399
+ } else {
400
+ result += `|-`;
401
+ }
402
+ result += `
403
+ ${indent}${line}
404
+ `;
405
+ } else {
406
+ result += `${indent}${line}
407
+ `;
408
+ }
409
+ }
410
+ } else {
411
+ if (value.includes("'") && !value.includes('"')) {
412
+ result += `"${value}"`;
413
+ } else if (value.includes('"') && !value.includes("'")) {
414
+ result += `'${value}'`;
415
+ } else if (value.includes("'") && value.includes('"')) {
416
+ result += `"${value.replace(/"/g, '\\"')}"`;
417
+ } else {
418
+ result += `'${value}'`;
419
+ }
420
+ }
421
+ return result.trimEnd();
422
+ }
423
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) {
424
+ return String(value).trimEnd();
425
+ }
426
+ const normalizedValue = normalizeValue(value);
427
+ if (normalizedValue !== null) {
428
+ return stringifyValue(
429
+ {
430
+ [`${normalizedValue[0]}#`]: normalizedValue[1]
431
+ },
432
+ indent,
433
+ maxLineLength,
434
+ showUndefined,
435
+ maxDepth,
436
+ depth + 1,
437
+ collapseObjects,
438
+ addObjSpaces
439
+ );
440
+ }
441
+ return JSON.stringify(value);
442
+ }
443
+ function normalizeValue(value) {
444
+ if (value === null || isPlainObject(value) || Array.isArray(value)) {
445
+ return null;
446
+ }
447
+ if (value instanceof Map) {
448
+ const mapEntries = Array.from(value.entries());
449
+ let mapValue;
450
+ if (mapEntries.every(([key]) => typeof key === "string")) {
451
+ const mapObjValue = {};
452
+ for (const [key, val] of mapEntries) {
453
+ mapObjValue[key] = val;
454
+ }
455
+ mapValue = mapObjValue;
456
+ } else {
457
+ mapValue = mapEntries.map(([key, val]) => ({
458
+ key,
459
+ value: val
460
+ }));
461
+ }
462
+ return ["Map", mapValue];
463
+ }
464
+ if (value instanceof Set) {
465
+ const setValue = Array.from(value);
466
+ return ["Set", setValue];
467
+ }
468
+ if (value instanceof Date) {
469
+ return ["Date", value.toISOString()];
470
+ }
471
+ if (value instanceof RegExp) {
472
+ return ["RegExp", value.toString()];
473
+ }
474
+ if (value instanceof Error) {
475
+ return [
476
+ "Error",
477
+ {
478
+ message: value.message,
479
+ name: value.name,
480
+ stack: value.stack
481
+ }
482
+ ];
483
+ }
484
+ if (value instanceof File) {
485
+ return [
486
+ "File",
487
+ {
488
+ name: value.name,
489
+ type: value.type,
490
+ lastModified: new Date(value.lastModified).toISOString(),
491
+ size: bytesToHumanReadable(value.size)
492
+ }
493
+ ];
494
+ }
495
+ if (typeof value === "object") {
496
+ if ("toJSON" in value && typeof value.toJSON === "function") {
497
+ return [value.constructor.name, value.toJSON()];
498
+ }
499
+ if ("toString" in value && typeof value.toString === "function") {
500
+ const stringValue = value.toString();
501
+ if (stringValue.toString() !== "[object Object]") {
502
+ return [value.constructor.name, stringValue];
503
+ }
504
+ }
505
+ const objectValue = { ...value };
506
+ const displayValue = {};
507
+ let addedKeys = 0;
508
+ for (const [key, item] of Object.entries(objectValue)) {
509
+ if (addedKeys > 4) {
510
+ displayValue["...and more properties"] = Object.keys(objectValue).length - 4;
511
+ break;
512
+ }
513
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null || item === void 0) {
514
+ displayValue[key] = item;
515
+ addedKeys++;
516
+ }
517
+ }
518
+ return [String(value.constructor.name), displayValue];
519
+ }
520
+ if (typeof value === "function") {
521
+ const functionString = value.toString();
522
+ return [
523
+ `Function`,
524
+ functionString.includes("\n") ? truncateString(functionString.split("\n").join(""), 40) : functionString
525
+ ];
526
+ }
527
+ return null;
528
+ }
529
+
156
530
  // src/testUtils.ts
157
531
  function createLoggerStore({
158
532
  filterKeys: defaultFilterKeys,
@@ -379,8 +753,172 @@ function waitController() {
379
753
  }
380
754
  };
381
755
  }
756
+ function matchesKeyPattern(path, pattern) {
757
+ if (path === pattern) {
758
+ return true;
759
+ }
760
+ if (pattern.includes("*")) {
761
+ const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, "[^.]*");
762
+ const regex = new RegExp(`^${regexPattern}$`);
763
+ return regex.test(path);
764
+ }
765
+ return false;
766
+ }
767
+ function isParentOfPattern(path, pattern) {
768
+ if (pattern.includes("*")) {
769
+ const patternParts = pattern.split(".");
770
+ const pathParts = path.split(".");
771
+ if (pathParts.length >= patternParts.length) {
772
+ return false;
773
+ }
774
+ for (let i = 0; i < pathParts.length; i++) {
775
+ const pathPart = pathParts[i];
776
+ const patternPart = patternParts[i];
777
+ if (patternPart === "*") {
778
+ continue;
779
+ }
780
+ if (pathPart !== patternPart) {
781
+ return false;
782
+ }
783
+ }
784
+ return true;
785
+ } else {
786
+ return pattern.startsWith(`${path}.`);
787
+ }
788
+ }
789
+ function applyKeyFiltering(value, { rejectKeys, filterKeys }, currentPath = "", visited = /* @__PURE__ */ new Set()) {
790
+ if (!isPlainObject(value) && !Array.isArray(value)) {
791
+ return value;
792
+ }
793
+ if (Array.isArray(value)) {
794
+ if (visited.has(value)) {
795
+ throw new Error("Circular reference detected in array during key filtering");
796
+ }
797
+ visited.add(value);
798
+ try {
799
+ return value.map(
800
+ (item, index) => applyKeyFiltering(item, { rejectKeys, filterKeys }, currentPath ? `${currentPath}[${index}]` : `[${index}]`, visited)
801
+ );
802
+ } finally {
803
+ visited.delete(value);
804
+ }
805
+ }
806
+ if (isPlainObject(value)) {
807
+ if (visited.has(value)) {
808
+ throw new Error("Circular reference detected in object during key filtering");
809
+ }
810
+ visited.add(value);
811
+ try {
812
+ const result = {};
813
+ for (const [key, itemValue] of Object.entries(value)) {
814
+ const fullPath = currentPath ? `${currentPath}.${key}` : key;
815
+ if (rejectKeys?.some(
816
+ (rejectPath) => matchesKeyPattern(fullPath, rejectPath) || matchesKeyPattern(key, rejectPath)
817
+ )) {
818
+ continue;
819
+ }
820
+ 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
+ )
827
+ );
828
+ if (!shouldInclude) {
829
+ continue;
830
+ }
831
+ }
832
+ result[key] = applyKeyFiltering(itemValue, { rejectKeys, filterKeys }, fullPath, visited);
833
+ }
834
+ return result;
835
+ } finally {
836
+ visited.delete(value);
837
+ }
838
+ }
839
+ return value;
840
+ }
841
+ function compactSnapshot(value, {
842
+ collapseObjects = true,
843
+ maxLineLength = 100,
844
+ showUndefined = false,
845
+ showBooleansAs = true,
846
+ rejectKeys,
847
+ filterKeys,
848
+ ...options
849
+ } = {}) {
850
+ let processedValue = value;
851
+ if (rejectKeys || filterKeys) {
852
+ processedValue = applyKeyFiltering(processedValue, { rejectKeys, filterKeys });
853
+ }
854
+ processedValue = showBooleansAs ? replaceBooleansWithEmoji(processedValue, showBooleansAs) : processedValue;
855
+ return `
856
+ ${yamlStringify(processedValue, {
857
+ collapseObjects,
858
+ maxLineLength,
859
+ showUndefined,
860
+ ...options
861
+ })}`;
862
+ }
863
+ function replaceBooleansWithEmoji(value, showBooleansAs, visited = /* @__PURE__ */ new Set()) {
864
+ if (showBooleansAs === false) {
865
+ return value;
866
+ }
867
+ const defaultTrueText = "\u2705";
868
+ const defaultFalseText = "\u274C";
869
+ const config = typeof showBooleansAs === "boolean" ? { trueText: defaultTrueText, falseText: defaultFalseText } : {
870
+ trueText: showBooleansAs.trueText ?? defaultTrueText,
871
+ falseText: showBooleansAs.falseText ?? defaultFalseText,
872
+ props: showBooleansAs.props ?? {},
873
+ ignoreProps: showBooleansAs.ignoreProps ?? []
874
+ };
875
+ function processValue(val, propName) {
876
+ if (typeof val === "boolean") {
877
+ if (propName && config.ignoreProps?.includes(propName)) {
878
+ return val;
879
+ }
880
+ if (propName && config.props?.[propName]) {
881
+ const propConfig = config.props[propName];
882
+ if (propConfig === true) {
883
+ return val ? config.trueText : config.falseText;
884
+ }
885
+ return val ? propConfig.trueText ?? config.trueText : propConfig.falseText ?? config.falseText;
886
+ }
887
+ return val ? config.trueText : config.falseText;
888
+ }
889
+ if (Array.isArray(val)) {
890
+ if (visited.has(val)) {
891
+ throw new Error("Circular reference detected in array");
892
+ }
893
+ visited.add(val);
894
+ try {
895
+ return val.map((item) => processValue(item));
896
+ } finally {
897
+ visited.delete(val);
898
+ }
899
+ }
900
+ if (isPlainObject(val)) {
901
+ if (visited.has(val)) {
902
+ throw new Error("Circular reference detected in object");
903
+ }
904
+ visited.add(val);
905
+ try {
906
+ const result = {};
907
+ for (const [key, itemValue] of Object.entries(val)) {
908
+ result[key] = processValue(itemValue, key);
909
+ }
910
+ return result;
911
+ } finally {
912
+ visited.delete(val);
913
+ }
914
+ }
915
+ return val;
916
+ }
917
+ return processValue(value);
918
+ }
382
919
  // Annotate the CommonJS export names for ESM import in node:
383
920
  0 && (module.exports = {
921
+ compactSnapshot,
384
922
  createLoggerStore,
385
923
  getResultFn,
386
924
  waitController
@@ -1,3 +1,5 @@
1
+ import { YamlStringifyOptions } from './yamlStringify.cjs';
2
+
1
3
  declare function createLoggerStore({ filterKeys: defaultFilterKeys, rejectKeys: defaultRejectKeys, splitLongLines: defaultSplitLongLines, maxLineLengthBeforeSplit: defaultMaxLineLengthBeforeSplit, fromLastSnapshot: defaultFromLastSnapshot, arrays: defaultArrays, changesOnly: defaultChangesOnly, useEmojiForBooleans: defaultUseEmojiForBooleans, }?: {
2
4
  filterKeys?: string[];
3
5
  rejectKeys?: string[];
@@ -39,5 +41,18 @@ declare function waitController(): {
39
41
  stopWaiting: () => void;
40
42
  stopWaitingAfter: (ms: number) => void;
41
43
  };
44
+ declare function compactSnapshot(value: unknown, { collapseObjects, maxLineLength, showUndefined, showBooleansAs, rejectKeys, filterKeys, ...options }?: YamlStringifyOptions & {
45
+ showBooleansAs?: boolean | {
46
+ props?: Record<string, {
47
+ trueText?: string;
48
+ falseText?: string;
49
+ } | true>;
50
+ ignoreProps?: string[];
51
+ trueText?: string;
52
+ falseText?: string;
53
+ };
54
+ rejectKeys?: string[];
55
+ filterKeys?: string[];
56
+ }): string;
42
57
 
43
- export { createLoggerStore, getResultFn, waitController };
58
+ export { compactSnapshot, createLoggerStore, getResultFn, waitController };
@@ -1,3 +1,5 @@
1
+ import { YamlStringifyOptions } from './yamlStringify.js';
2
+
1
3
  declare function createLoggerStore({ filterKeys: defaultFilterKeys, rejectKeys: defaultRejectKeys, splitLongLines: defaultSplitLongLines, maxLineLengthBeforeSplit: defaultMaxLineLengthBeforeSplit, fromLastSnapshot: defaultFromLastSnapshot, arrays: defaultArrays, changesOnly: defaultChangesOnly, useEmojiForBooleans: defaultUseEmojiForBooleans, }?: {
2
4
  filterKeys?: string[];
3
5
  rejectKeys?: string[];
@@ -39,5 +41,18 @@ declare function waitController(): {
39
41
  stopWaiting: () => void;
40
42
  stopWaitingAfter: (ms: number) => void;
41
43
  };
44
+ declare function compactSnapshot(value: unknown, { collapseObjects, maxLineLength, showUndefined, showBooleansAs, rejectKeys, filterKeys, ...options }?: YamlStringifyOptions & {
45
+ showBooleansAs?: boolean | {
46
+ props?: Record<string, {
47
+ trueText?: string;
48
+ falseText?: string;
49
+ } | true>;
50
+ ignoreProps?: string[];
51
+ trueText?: string;
52
+ falseText?: string;
53
+ };
54
+ rejectKeys?: string[];
55
+ filterKeys?: string[];
56
+ }): string;
42
57
 
43
- export { createLoggerStore, getResultFn, waitController };
58
+ export { compactSnapshot, createLoggerStore, getResultFn, waitController };