@ezez/utils 1.9.0 → 2.1.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.
Files changed (163) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +4 -0
  3. package/dist/deserialize.d.ts +13 -0
  4. package/dist/deserialize.d.ts.map +1 -0
  5. package/dist/deserialize.js +42 -0
  6. package/dist/deserialize.js.map +1 -0
  7. package/dist/index.d.ts +4 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/omit.js +3 -1
  12. package/dist/omit.js.map +1 -1
  13. package/dist/replace.d.ts.map +1 -1
  14. package/dist/replace.js.map +1 -1
  15. package/dist/replaceDeep.d.ts +3 -0
  16. package/dist/replaceDeep.d.ts.map +1 -0
  17. package/dist/replaceDeep.js +26 -0
  18. package/dist/replaceDeep.js.map +1 -0
  19. package/dist/round.d.ts +3 -0
  20. package/dist/round.d.ts.map +1 -0
  21. package/dist/round.js +9 -0
  22. package/dist/round.js.map +1 -0
  23. package/dist/safe.d.ts.map +1 -1
  24. package/dist/safe.js +1 -1
  25. package/dist/safe.js.map +1 -1
  26. package/dist/serialize.d.ts +16 -0
  27. package/dist/serialize.d.ts.map +1 -0
  28. package/dist/serialize.js +61 -0
  29. package/dist/serialize.js.map +1 -0
  30. package/dist/setImmutable.js +1 -1
  31. package/dist/setImmutable.js.map +1 -1
  32. package/dist/sortBy.js +1 -2
  33. package/dist/sortBy.js.map +1 -1
  34. package/dist/sortProps.d.ts.map +1 -1
  35. package/dist/sortProps.js.map +1 -1
  36. package/dist/throttle.js +5 -7
  37. package/dist/throttle.js.map +1 -1
  38. package/docs/assets/search.js +1 -1
  39. package/docs/functions/cap.html +12 -6
  40. package/docs/functions/capitalize.html +12 -6
  41. package/docs/functions/coalesce.html +12 -6
  42. package/docs/functions/compareArrays.html +12 -6
  43. package/docs/functions/compareProps.html +12 -6
  44. package/docs/functions/deserialize.html +149 -0
  45. package/docs/functions/ensureArray.html +12 -6
  46. package/docs/functions/ensureDate.html +12 -6
  47. package/docs/functions/ensureError.html +12 -6
  48. package/docs/functions/ensurePrefix.html +12 -6
  49. package/docs/functions/ensureSuffix.html +12 -6
  50. package/docs/functions/ensureTimestamp.html +12 -6
  51. package/docs/functions/escapeRegExp.html +12 -6
  52. package/docs/functions/formatDate.html +12 -6
  53. package/docs/functions/get.html +12 -6
  54. package/docs/functions/getMultiple.html +12 -6
  55. package/docs/functions/insertSeparator.html +12 -6
  56. package/docs/functions/isEmpty.html +12 -6
  57. package/docs/functions/isNumericString.html +12 -6
  58. package/docs/functions/isPlainObject.html +12 -6
  59. package/docs/functions/last.html +12 -6
  60. package/docs/functions/later-1.html +12 -6
  61. package/docs/functions/mapAsync.html +12 -6
  62. package/docs/functions/mapValues.html +12 -6
  63. package/docs/functions/match.html +12 -6
  64. package/docs/functions/merge.html +20 -14
  65. package/docs/functions/mostFrequent.html +12 -6
  66. package/docs/functions/noop.html +12 -6
  67. package/docs/functions/occurrences.html +12 -6
  68. package/docs/functions/omit.html +12 -6
  69. package/docs/functions/pick.html +12 -6
  70. package/docs/functions/pull.html +12 -6
  71. package/docs/functions/remove.html +12 -6
  72. package/docs/functions/removeCommonProperties.html +12 -6
  73. package/docs/functions/replace.html +12 -6
  74. package/docs/functions/replaceDeep.html +154 -0
  75. package/docs/functions/rethrow.html +12 -6
  76. package/docs/functions/round.html +144 -0
  77. package/docs/functions/safe.html +13 -7
  78. package/docs/functions/scale.html +12 -6
  79. package/docs/functions/seq.html +12 -6
  80. package/docs/functions/seqEarlyBreak.html +12 -6
  81. package/docs/functions/serialize.html +154 -0
  82. package/docs/functions/set.html +12 -6
  83. package/docs/functions/setImmutable.html +12 -6
  84. package/docs/functions/sortBy.html +12 -6
  85. package/docs/functions/sortProps.html +15 -7
  86. package/docs/functions/stripPrefix.html +12 -6
  87. package/docs/functions/stripSuffix.html +12 -6
  88. package/docs/functions/throttle.html +12 -6
  89. package/docs/functions/truthy.html +12 -6
  90. package/docs/functions/unique.html +12 -6
  91. package/docs/functions/wait.html +12 -6
  92. package/docs/functions/waitFor.html +12 -6
  93. package/docs/functions/waitSync.html +12 -6
  94. package/docs/index.html +15 -5
  95. package/docs/interfaces/ComparePropsOptions.html +6 -6
  96. package/docs/interfaces/GetMultipleSource.html +12 -6
  97. package/docs/interfaces/GetSource.html +12 -6
  98. package/docs/interfaces/IsNumericStringOptions.html +9 -9
  99. package/docs/interfaces/OccurencesOptions.html +6 -6
  100. package/docs/interfaces/SetImmutableSource.html +12 -6
  101. package/docs/interfaces/SetSource.html +12 -6
  102. package/docs/interfaces/ThrottleOptions.html +7 -7
  103. package/docs/interfaces/ThrottledFunctionExtras.html +7 -7
  104. package/docs/modules.html +17 -5
  105. package/docs/pages/CHANGELOG.html +77 -39
  106. package/docs/pages/Introduction.html +11 -5
  107. package/docs/types/CustomDeserializers.html +152 -0
  108. package/docs/types/CustomSerializers.html +152 -0
  109. package/docs/types/Later.html +12 -6
  110. package/docs/types/MapValuesFn.html +12 -6
  111. package/docs/types/MatchCallback.html +12 -6
  112. package/docs/types/SeqEarlyBreaker.html +12 -6
  113. package/docs/types/SeqFn.html +12 -6
  114. package/docs/types/SeqFunctions.html +12 -6
  115. package/docs/types/SetImmutablePath.html +12 -6
  116. package/docs/types/ThrottledFunction.html +12 -6
  117. package/docs/variables/mapValuesUNSET.html +12 -6
  118. package/docs/variables/mergeUNSET.html +12 -6
  119. package/esm/deserialize.d.ts +13 -0
  120. package/esm/deserialize.d.ts.map +1 -0
  121. package/esm/deserialize.js +39 -0
  122. package/esm/deserialize.js.map +1 -0
  123. package/esm/index.d.ts +4 -0
  124. package/esm/index.d.ts.map +1 -1
  125. package/esm/index.js +4 -0
  126. package/esm/index.js.map +1 -1
  127. package/esm/replace.d.ts.map +1 -1
  128. package/esm/replace.js.map +1 -1
  129. package/esm/replaceDeep.d.ts +3 -0
  130. package/esm/replaceDeep.d.ts.map +1 -0
  131. package/esm/replaceDeep.js +23 -0
  132. package/esm/replaceDeep.js.map +1 -0
  133. package/esm/round.d.ts +3 -0
  134. package/esm/round.d.ts.map +1 -0
  135. package/esm/round.js +6 -0
  136. package/esm/round.js.map +1 -0
  137. package/esm/safe.d.ts.map +1 -1
  138. package/esm/safe.js.map +1 -1
  139. package/esm/serialize.d.ts +16 -0
  140. package/esm/serialize.d.ts.map +1 -0
  141. package/esm/serialize.js +58 -0
  142. package/esm/serialize.js.map +1 -0
  143. package/esm/sortBy.js +1 -2
  144. package/esm/sortBy.js.map +1 -1
  145. package/esm/sortProps.d.ts.map +1 -1
  146. package/esm/sortProps.js.map +1 -1
  147. package/esm/throttle.js +5 -7
  148. package/esm/throttle.js.map +1 -1
  149. package/package.json +26 -25
  150. package/pnpm-lock.yaml +5223 -0
  151. package/src/deserialize.spec.ts +69 -0
  152. package/src/deserialize.ts +74 -0
  153. package/src/index.ts +4 -0
  154. package/src/replace.ts +0 -1
  155. package/src/replaceDeep.spec.ts +71 -0
  156. package/src/replaceDeep.ts +44 -0
  157. package/src/round.spec.ts +27 -0
  158. package/src/round.ts +21 -0
  159. package/src/safe.ts +0 -1
  160. package/src/serialize.spec.ts +83 -0
  161. package/src/serialize.ts +102 -0
  162. package/src/sortProps.ts +4 -1
  163. package/babel.config.cjs +0 -6
@@ -0,0 +1,69 @@
1
+ import type { CustomDeserializers } from "./deserialize";
2
+
3
+ import { deserialize } from "./deserialize";
4
+
5
+ class Person {
6
+ public name: string;
7
+
8
+ public constructor(name: string) {
9
+ this.name = name;
10
+ }
11
+ }
12
+
13
+ describe("deserialize", () => {
14
+ it("can deserialize primitives", async () => {
15
+ must(deserialize(`"s:test"`)).equal("test");
16
+ must(deserialize(`"n:123"`)).equal(123);
17
+ must(deserialize(`"i:123"`)).equal(123n);
18
+ must(deserialize(`"u:"`)).equal(undefined);
19
+ must(deserialize(`"b:1"`)).equal(true);
20
+ must(deserialize(`"b:"`)).equal(false);
21
+ must(deserialize(`"l:"`)).equal(null);
22
+ });
23
+
24
+ it("can deserialize arrays", () => {
25
+ must(deserialize(`["s:a","s:b","s:c"]`)).eql(["a", "b", "c"]);
26
+ must(deserialize(`["n:1","i:2","u:","b:1","b:","l:"]`)).eql([1, 2n, undefined, true, false, null]);
27
+
28
+ // deep arrays:
29
+ must(deserialize(`["n:1",["n:2",["n:3",["n:4",["n:5"]]]]]`)).eql([1, [2, [3, [4, [5]]]]]);
30
+ });
31
+
32
+ it("can deserialize objects", () => {
33
+ must(deserialize(`{"a":"s:a","b":"s:b","c":"s:c"}`)).eql({ a: "a", b: "b", c: "c" });
34
+ });
35
+
36
+ it("supports custom deserializers", () => {
37
+ const customDeserializers: CustomDeserializers = {
38
+ p: (value) => {
39
+ return new Person(value);
40
+ },
41
+ };
42
+
43
+ const p1 = deserialize<Person>(`"p:John"`, customDeserializers);
44
+ must(p1).be.instanceOf(Person);
45
+ must(p1.name).equal("John");
46
+
47
+ const res = deserialize<{ a: Person }>(`{"a":"p:John"}`, customDeserializers);
48
+ const p2 = res.a;
49
+ must(p2).be.instanceOf(Person);
50
+ must(p2.name).equal("John");
51
+
52
+ must(res).eql({ a: p2 });
53
+ });
54
+
55
+ it("allows custom deserializers to support primitives", () => {
56
+ const customSerializers: CustomDeserializers = {
57
+ sym: (value) => {
58
+ return Symbol.for(value);
59
+ },
60
+ };
61
+
62
+ // must(serialize(Symbol("x"), customSerializers)).equal(`"sym:x"`);
63
+ must(deserialize(`"sym:x"`, customSerializers)).equal(Symbol.for("x"));
64
+ });
65
+
66
+ it("throws on unknown data type", () => {
67
+ must(() => deserialize(`"v:test"`)).throw("Unsupported data type: v");
68
+ });
69
+ });
@@ -0,0 +1,74 @@
1
+ import { replaceDeep } from "./replaceDeep.js";
2
+
3
+ // if JSON.parse, even via replacer will return `undefined` the value will be eaten out, all other values
4
+ // are fine, so we can use this symbol to simulate `undefined` when parsing, then replace it in the result if needed
5
+ const UNDEFINED = Symbol("undefined");
6
+
7
+ type CustomDeserializers = {
8
+ [key: string]: (data: string) => unknown;
9
+ s?: never;
10
+ n?: never;
11
+ u?: never;
12
+ l?: never;
13
+ b?: never;
14
+ i?: never;
15
+ };
16
+
17
+ /**
18
+ * Deserializes a string serialized with `serialize` into a value.
19
+ *
20
+ * You will need to specify deserializers if custom data types are serialized. See `serialize` for more information.
21
+ *
22
+ * @see {@link serialize}.
23
+ *
24
+ * @param serializedString - the serialized string
25
+ * @param customDeserializers - an object with custom deserializers
26
+ */
27
+ const deserialize = <T>(serializedString: string, customDeserializers?: CustomDeserializers): T => {
28
+ let hasUndefined = false;
29
+ const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
30
+ if (typeof value === "string") {
31
+ if (value.startsWith("s:")) {
32
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
33
+ return value.slice(2);
34
+ }
35
+ if (value.startsWith("n:")) {
36
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
37
+ return Number(value.slice(2));
38
+ }
39
+ if (value.startsWith("i:")) {
40
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
41
+ return BigInt(value.slice(2));
42
+ }
43
+ if (value === "u:") {
44
+ hasUndefined = true;
45
+ return UNDEFINED;
46
+ }
47
+ if (value === "l:") {
48
+ return null;
49
+ }
50
+ if (value.startsWith("b:")) {
51
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
52
+ return value.slice(2) === "1";
53
+ }
54
+
55
+ const semiColonIndex = value.indexOf(":");
56
+ const type = value.slice(0, semiColonIndex);
57
+ if (customDeserializers && type in customDeserializers) {
58
+ return customDeserializers[type]!(value.slice(semiColonIndex + 1));
59
+ }
60
+
61
+ throw new Error(`Unsupported data type: ${type}`);
62
+ }
63
+ return value;
64
+ };
65
+
66
+ const parsed = JSON.parse(serializedString, replacer) as T;
67
+
68
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
69
+ return hasUndefined ? replaceDeep(parsed, UNDEFINED, undefined) : parsed;
70
+ };
71
+
72
+ export { deserialize };
73
+
74
+ export type { CustomDeserializers };
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./capitalize.js";
6
6
  export * from "./coalesce.js";
7
7
  export * from "./compareArrays.js";
8
8
  export * from "./compareProps.js";
9
+ export * from "./deserialize.js";
9
10
  export * from "./ensureArray.js";
10
11
  export * from "./ensureDate.js";
11
12
  export * from "./ensureError.js";
@@ -33,10 +34,13 @@ export * from "./pull.js";
33
34
  export * from "./remove.js";
34
35
  export * from "./removeCommonProperties.js";
35
36
  export * from "./replace.js";
37
+ export * from "./replaceDeep.js";
36
38
  export * from "./rethrow.js";
39
+ export * from "./round.js";
37
40
  export * from "./safe.js";
38
41
  export * from "./scale.js";
39
42
  export * from "./seq.js";
43
+ export * from "./serialize.js";
40
44
  export * from "./set.js";
41
45
  export * from "./setImmutable.js";
42
46
  export * from "./sortBy.js";
package/src/replace.ts CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable max-len */
2
1
  import { escapeRegExp } from "./escapeRegExp.js";
3
2
 
4
3
  /**
@@ -0,0 +1,71 @@
1
+ import { replaceDeep } from "./replaceDeep";
2
+
3
+ describe("replaceDeep", () => {
4
+ it("should replace given value in a deep object", () => {
5
+ const source = [
6
+ 99,
7
+ 100,
8
+ {
9
+ favouriteBook: {
10
+ title: "The Ring of The Lord",
11
+ price: 100,
12
+ },
13
+ otherBooks: [
14
+ {
15
+ title: "Parry Hotter",
16
+ price: 50,
17
+ tag: "100",
18
+ },
19
+ {
20
+ title: "The Hobbyte 100",
21
+ price: [100],
22
+ },
23
+ ],
24
+ },
25
+ ];
26
+
27
+ must(replaceDeep(source, 100, 200)).eql([
28
+ 99,
29
+ 200,
30
+ {
31
+ favouriteBook: {
32
+ title: "The Ring of The Lord",
33
+ price: 200,
34
+ },
35
+ otherBooks: [
36
+ {
37
+ title: "Parry Hotter",
38
+ price: 50,
39
+ tag: "100",
40
+ },
41
+ {
42
+ title: "The Hobbyte 100",
43
+ price: [200],
44
+ },
45
+ ],
46
+ },
47
+ ]);
48
+ });
49
+
50
+ it("should leave primitives as-is unless they equal to the search value", () => {
51
+ must(replaceDeep(100, 200, 300)).equal(100);
52
+ must(replaceDeep(200, 200, 300)).equal(300);
53
+ must(replaceDeep("100", 200, 300)).equal("100");
54
+ // ESLINT BUG:
55
+ // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
56
+ must(replaceDeep(undefined, 200, 300)).equal(undefined);
57
+ must(replaceDeep(null, 200, 300)).equal(null);
58
+ must(replaceDeep(true, 200, 300)).equal(true);
59
+ must(replaceDeep(666n, 200, 300)).equal(666n);
60
+ });
61
+
62
+ it("should work with nans", async () => {
63
+ must(replaceDeep({
64
+ a: NaN,
65
+ b: 123,
66
+ }, NaN, 300)).eql({
67
+ a: 300,
68
+ b: 123,
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Replaces all occurrences of `search` with `value` in `source` object/array. Comparison is done with `Object.is`.
3
+ * If `source` is exactly the `search` a `value` will be returned. It does not do a substring replacements.
4
+ *
5
+ * It mutates the `source` object/array!
6
+ *
7
+ * TypeScript users: This is way too dynamic to type properly, therefore, typing assumes the most basic form of
8
+ * replacement where search and value are of the same type. If that's not the case for you - you'll have to typecast.
9
+ *
10
+ * @param source - source object/array/value
11
+ * @param search - value to search for
12
+ * @param value - value to replace with
13
+ */
14
+ const replaceDeep = <T>(source: T, search: unknown, value: unknown): T => {
15
+ if (Object.is(source, search)) {
16
+ return value as T;
17
+ }
18
+
19
+ if (source == null) {
20
+ return source;
21
+ }
22
+
23
+ if (typeof source === "object") {
24
+ if (Array.isArray(source)) {
25
+ for (let i = 0; i < source.length; i++) {
26
+ // eslint-disable-next-line no-param-reassign,@typescript-eslint/no-unsafe-assignment
27
+ source[i] = replaceDeep(source[i], search, value);
28
+ }
29
+ return source;
30
+ }
31
+
32
+ return Object.keys(source).reduce<Record<string, unknown>>((acc, key) => {
33
+ // eslint-disable-next-line no-param-reassign
34
+ acc[key] = replaceDeep((source as Record<string, unknown>)[key], search, value);
35
+ return acc;
36
+ }, {}) as T;
37
+ }
38
+
39
+ return source;
40
+ };
41
+
42
+ export {
43
+ replaceDeep,
44
+ };
@@ -0,0 +1,27 @@
1
+ import { round } from "./round";
2
+
3
+ describe("round", () => {
4
+ it("rounds the values without precision", async () => {
5
+ round(1.23).must.equal(1);
6
+ round(1.55).must.equal(2);
7
+ round(1.5).must.equal(2);
8
+ round(1.49999).must.equal(1);
9
+ });
10
+
11
+ it("rounds the values with precision", async () => {
12
+ round(1.23, 1).must.equal(1.2);
13
+ round(1.55, 1).must.equal(1.6);
14
+ round(1.5, 1).must.equal(1.5);
15
+ round(1.49999, 1).must.equal(1.5);
16
+
17
+ round(1.5, 2).must.equal(1.5);
18
+ });
19
+
20
+ it("rounds the tricky values", async () => {
21
+ round(0.1 + 0.2, 1).must.equal(0.3);
22
+ });
23
+
24
+ it("rounds with higher precision", async () => {
25
+ round(1.2, 6).must.equal(1.2);
26
+ });
27
+ });
package/src/round.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Rounds a number to a given precision
3
+ *
4
+ * @example
5
+ * round(1.23) // 1
6
+ * round(1.55) // 2
7
+ * round(1.333, 2) // 1.33
8
+ * round(1.2345, 3) // 1.234
9
+ *
10
+ * @param value - value to round
11
+ * @param precision - precision to round to
12
+ */
13
+ const round = (value: number, precision?: number) => {
14
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
15
+ const multiplier = Math.pow(10, precision || 0);
16
+ return Math.round(value * multiplier) / multiplier;
17
+ };
18
+
19
+ export {
20
+ round,
21
+ };
package/src/safe.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  function safe<T>(fn: () => T): T | undefined;
2
2
  function safe<T, Y>(fn: () => T, def: Y): T | Y;
3
3
 
4
- /* eslint-disable max-len */
5
4
  /**
6
5
  * Safely execute a function, return its return value or default value if the function throws.
7
6
  * @param fn - function to run
@@ -0,0 +1,83 @@
1
+ import type { CustomSerializers } from "./serialize";
2
+
3
+ import { serialize } from "./serialize";
4
+
5
+ interface Test {
6
+ a: string;
7
+ }
8
+
9
+ class Person {
10
+ public name: string;
11
+
12
+ public constructor(name: string) {
13
+ this.name = name;
14
+ }
15
+ }
16
+
17
+ describe("serialize", () => {
18
+ it("can serialize primitives", async () => {
19
+ must(serialize("test")).equal(`"s:test"`);
20
+ must(serialize(123)).equal(`"n:123"`);
21
+ must(serialize(123n)).equal(`"i:123"`);
22
+ must(serialize(undefined)).equal(`"u:"`);
23
+ must(serialize(true)).equal(`"b:1"`);
24
+ must(serialize(false)).equal(`"b:"`);
25
+ must(serialize(null)).equal(`"l:"`);
26
+ });
27
+
28
+ it("can serialize arrays", async () => {
29
+ must(serialize(["a", "b", "c"])).equal(`["s:a","s:b","s:c"]`);
30
+ must(serialize([1, 2n, undefined, true, false, null])).equal(`["n:1","i:2","u:","b:1","b:","l:"]`);
31
+
32
+ // deep arrays:
33
+ must(serialize([1, [2, [3, [4, [5]]]]])).equal(`["n:1",["n:2",["n:3",["n:4",["n:5"]]]]]`);
34
+ });
35
+
36
+ it("can serialize objects", async () => {
37
+ must(serialize({ a: "a", b: "b", c: "c" })).equal(`{"a":"s:a","b":"s:b","c":"s:c"}`);
38
+
39
+ const a: Test = { a: "a" };
40
+ must(serialize(a)).equal(`{"a":"s:a"}`);
41
+ });
42
+
43
+ it("supports custom serializers", async () => {
44
+ const customSerializers: CustomSerializers = {
45
+ p: (value) => {
46
+ if (value instanceof Person) {
47
+ return value.name;
48
+ }
49
+ return null;
50
+ },
51
+ };
52
+
53
+ must(serialize(new Person("John"), customSerializers)).equal(`"p:John"`);
54
+ must(serialize({ a: new Person("John") }, customSerializers)).equal(`{"a":"p:John"}`);
55
+ });
56
+
57
+ it("throws on unknown data type", () => {
58
+ const x = Symbol("x");
59
+
60
+ must(() => serialize(x)).throw("Unsupported data type: symbol");
61
+ });
62
+
63
+ it("allows custom serializers to support primitives", () => {
64
+ const customSerializers: CustomSerializers = {
65
+ sym: (value) => {
66
+ if (typeof value === "symbol") {
67
+ return String(value);
68
+ }
69
+ return null;
70
+ },
71
+ };
72
+
73
+ must(serialize(Symbol("x"), customSerializers)).equal(`"sym:Symbol(x)"`);
74
+ must(serialize({ a: Symbol("x") }, customSerializers)).equal(`{"a":"sym:Symbol(x)"}`);
75
+ });
76
+
77
+ it("returns the same for any order of props", () => {
78
+ const a = { a: 1, b: 2 };
79
+ const b = { b: 2, a: 1 };
80
+
81
+ must(serialize(a)).equal(serialize(b));
82
+ });
83
+ });
@@ -0,0 +1,102 @@
1
+ import { sortProps } from "./sortProps.js";
2
+
3
+ type CustomSerializers = {
4
+ [key: string]: (data: unknown) => (string | null);
5
+ s?: never;
6
+ n?: never;
7
+ u?: never;
8
+ l?: never;
9
+ b?: never;
10
+ i?: never;
11
+ };
12
+
13
+ type Options = {
14
+ sortProps?: boolean;
15
+ };
16
+
17
+ /**
18
+ * Serializes the data into a string. Think of it as a JSON.stringify on steroids.
19
+ * In opposite to JSON.stringify it supports serializing undefined.
20
+ *
21
+ * It also supports custom serializers, which can be used to serialize custom data types. Each value has a prefix which
22
+ * specifies the type of the value, custom serializers is a map of these prefixes to functions which can serialize the
23
+ * value into a string. IMPORTANT: Because this is using JSON.serialize under the hood if a value to serialize includes
24
+ * `toJSON` it won't trigger custom serializer but will be serialized as string. `Date` class defines `toJSON` method!
25
+ *
26
+ * The extra aim of this function is to produce the same output for "the same" data, regardless of the order of the keys
27
+ * (which is not guaranteed by JS spec, but in practice it is guaranteed by current implementations of all JS engines),
28
+ * so it sorts the keys and when serializing data by default, you can opt-out of this behavior by passing
29
+ * `{ sortProps: false }` as the third argument.
30
+ *
31
+ * @param data - the data to serialize
32
+ * @param customSerializers - an object with custom serializers
33
+ * @param options - options
34
+ */
35
+ const serialize = (data: unknown, customSerializers?: CustomSerializers, options?: Options) => { // eslint-disable-line max-lines-per-function,max-len
36
+ const replacer = (_key: string, value: unknown) => { // eslint-disable-line max-statements
37
+ if (typeof value === "string") {
38
+ return `s:${value}`;
39
+ }
40
+ if (typeof value === "number") {
41
+ return `n:${value}`;
42
+ }
43
+ if (typeof value === "bigint") {
44
+ return `i:${value}`;
45
+ }
46
+ if (typeof value === "undefined") {
47
+ return "u:";
48
+ }
49
+ if (typeof value === "boolean") {
50
+ return "b:" + (value ? "1" : "");
51
+ }
52
+ if (value === null) {
53
+ return "l:";
54
+ }
55
+ const serializerKeys = Object.keys(customSerializers ?? {});
56
+ for (const key of serializerKeys) {
57
+ const serialized = customSerializers![key]!(value);
58
+ if (typeof serialized === "string") {
59
+ return `${key}:${serialized}`;
60
+ }
61
+ }
62
+ if (typeof value === "object") {
63
+ return value;
64
+ }
65
+
66
+ throw new Error(`Unsupported data type: ${typeof value}`);
67
+ };
68
+
69
+ if (
70
+ data == null
71
+ || typeof data === "string"
72
+ || typeof data === "number"
73
+ || typeof data === "bigint"
74
+ || typeof data === "undefined"
75
+ || typeof data === "boolean"
76
+ || Array.isArray(data)
77
+ ) {
78
+ return JSON.stringify(data, replacer);
79
+ }
80
+
81
+ const serializerKeysGlobal = Object.keys(customSerializers ?? {});
82
+ for (const key of serializerKeysGlobal) {
83
+ const serialized = customSerializers![key]!(data);
84
+ if (typeof serialized === "string") {
85
+ return `"${key}:${serialized}"`;
86
+ }
87
+ }
88
+
89
+ if (typeof data === "object") {
90
+ return JSON.stringify(
91
+ options?.sortProps === false
92
+ ? data
93
+ : sortProps(data as Record<string, unknown>),
94
+ replacer,
95
+ );
96
+ }
97
+
98
+ throw new Error(`Unsupported data type: ${typeof data}`);
99
+ };
100
+
101
+ export type { CustomSerializers };
102
+ export { serialize };
package/src/sortProps.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Sorts the properties of an object alphabetically, ascending or descending.
3
- * REMEMBER: In theory JS engines do not guarantee the order of object properties. In practice most popular engines do.
3
+ * It does not mutate the original object.
4
+ *
5
+ * REMEMBER: In theory, JS engines do not guarantee the order of object properties.
6
+ * In practice most of the popular engines do.
4
7
  * @param object - source object
5
8
  * @param asc - sort ascending?
6
9
  * @example sortProps({ b: 2, a: 1, z: 26 }) // { a: 1, b: 2, z: 26 }
package/babel.config.cjs DELETED
@@ -1,6 +0,0 @@
1
- module.exports = {
2
- presets: [
3
- ['@babel/preset-env', { targets: { node: 'current' } }],
4
- '@babel/preset-typescript',
5
- ],
6
- };