@uxf/core 11.80.4 → 11.87.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/README.md CHANGED
@@ -125,6 +125,63 @@ useIsomorphicLayoutEffect(() => {
125
125
 
126
126
  > **Note**: Requires valid `line-height` and `font-size` styles for accurate sizing.
127
127
 
128
+ ### assertNever
129
+
130
+ Checks that value is always type "never".
131
+ ```ts
132
+ switch(value) {
133
+ case "a":
134
+ return "A";
135
+ case "b":
136
+ return "B";
137
+ default:
138
+ return assertNever(value);
139
+ }
140
+ ```
141
+
142
+ ## assertNotNil
143
+
144
+ ```tsx
145
+ import { assertNotNil } from "@uxf/core/utils/assert-not-nil";
146
+
147
+ const testObject: { value: number | null } = { value: 10 };
148
+
149
+ assertNotNil(testObject.value);
150
+
151
+ // is the same as
152
+
153
+ if (isNil(testObject.value)) {
154
+ throw new Error("Value is null");
155
+ }
156
+ ```
157
+
158
+ ## camelCaseToDash
159
+
160
+ ```tsx
161
+ import { camelCaseToDash } from "@uxf/core/utils/camelCaseToDash";
162
+
163
+ const example = camelCaseToDash("fooBar"); /* returns "foo-bar" */
164
+ ```
165
+
166
+ ## capitalize
167
+
168
+ ```tsx
169
+ import { capitalize } from "@uxf/core/utils/capitalize";
170
+
171
+ const example = capitalize("hello world"); /* returns "Hello world" */
172
+ ```
173
+
174
+ ## composeRefs
175
+
176
+ ```tsx
177
+ import { composeRefs } from "@uxf/core/utils/composeRefs";
178
+
179
+ const firstRef = useRef<HTMLDivElement>(null);
180
+ const secondRef = useRef<HTMLDivElement>(null);
181
+
182
+ const example = <div ref={composeRefs(firstRef, secondRef)} />;
183
+ ```
184
+
128
185
  ### cx, cxa
129
186
  It is our fork of `clsx` library https://github.com/lukeed/clsx
130
187
 
@@ -175,9 +232,30 @@ cxa("foo", [1 && "bar", { baz:false, bat:null }, ["hello", ["world"]]], "cya");
175
232
  //=> "foo bar hello world cya"
176
233
  ```
177
234
 
235
+ ### deepEqualIgnoringKeyOrder
236
+
237
+ deepEqualIgnoringKeyOrder compares two values for deep equality while ignoring the order of object keys. It serializes values in a stable way, ensuring that objects with the same data but different key orders are treated as equal. Arrays remain order-sensitive, circular references are handled safely, and primitives are compared by value.
238
+
239
+ Use this helper for equality checks in tests, memoization, caching, or change detection scenarios where object key order shouldn’t matter. It’s especially useful when comparing payloads, configs, or API responses that may have non-deterministic key ordering.
240
+
241
+ ```typescript
242
+ import { deepEqualIgnoringKeyOrder } from "./deep-equal-ingoring-key-order";
243
+
244
+ const a = { b: 2, a: 1, nested: { y: 2, x: 1 } };
245
+ const b = { nested: { x: 1, y: 2 }, a: 1, b: 2 };
246
+ console.log(deepEqualIgnoringKeyOrder(a, b)); // true
247
+
248
+ const arr1 = [{ a: 1, b: 2 }, { c: 3, d: 4 }];
249
+ const arr2 = [{ b: 2, a: 1 }, { d: 4, c: 3 }];
250
+ console.log(deepEqualIgnoringKeyOrder(arr1, arr2)); // true (objects equal, same array order)
251
+
252
+ const arr3 = [{ d: 4, c: 3 }, { b: 2, a: 1 }];
253
+ console.log(deepEqualIgnoringKeyOrder(arr1, arr3)); // false (array order differs)
254
+ ```
255
+
178
256
  ### downloadFile
179
257
 
180
- Intended as only way to programmatically download file if there is no option to use native anchor with `download` html attribute (eg. in form submit events).
258
+ Intended as only way to programmatically download file if there is no option to use native anchor with `download` html attribute (eg. in form submit events).
181
259
 
182
260
  ```ts
183
261
  import { downloadFile } from "@uxf/core/utils/download-file";
@@ -199,6 +277,50 @@ escapeQuotes('The "quick" fox');
199
277
  // Output: The \"quick\" fox
200
278
  ```
201
279
 
280
+ ## filterNullish
281
+
282
+ ```tsx
283
+ import { filterNullish } from "@uxf/core/utils/filter-nullish";
284
+
285
+ filterNullish([0, "text", null, undefined, [], {}]); /* returns [0, "text", [], {}] */
286
+ ```
287
+
288
+ ## filterAriaAndDataAttrs
289
+
290
+ Filters an object to return only properties that start with `aria-` or `data-` prefixes. This utility is useful when you need to pass accessibility and data attributes to HTML elements while excluding other props like event handlers or component-specific props.
291
+
292
+ ```tsx
293
+ import { filterAriaAndDataAttrs } from "@uxf/core/utils/filter-aria-and-data-attrs";
294
+
295
+ // Filter accessibility and data attributes from component props
296
+ const props = {
297
+ "aria-label": "Close button",
298
+ "data-testid": "close-btn",
299
+ className: "button",
300
+ onClick: handleClick,
301
+ };
302
+
303
+ const htmlAttrs = filterAriaAndDataAttrs(props);
304
+ // Result: { "aria-label": "Close button", "data-testid": "close-btn" }
305
+
306
+ <button {...htmlAttrs}>Close</button>
307
+ ```
308
+
309
+ ```tsx
310
+ // Use case: Passing only safe attributes to a native element
311
+ function CustomInput({ label, onChange, ...restProps }) {
312
+ const accessibilityAttrs = filterAriaAndDataAttrs(restProps);
313
+
314
+ return <input {...accessibilityAttrs} onChange={onChange} />;
315
+ }
316
+
317
+ <CustomInput
318
+ aria-describedby="helper-text"
319
+ data-analytics="email-input"
320
+ customProp="ignored"
321
+ />
322
+ ```
323
+
202
324
  ### formatBytes
203
325
 
204
326
  Appends suitable unit to the byte value of data size.
@@ -207,18 +329,102 @@ formatBytes(17.5 * 1024);
207
329
  //=> "17.5 kB"
208
330
  ```
209
331
 
210
- ### assertNever
332
+ ## isEmpty
211
333
 
212
- Checks that value is always type "never".
213
- ```ts
214
- switch(value) {
215
- case "a":
216
- return "A";
217
- case "b":
218
- return "B";
219
- default:
220
- return assertNever(value);
221
- }
334
+ ```tsx
335
+ import { isEmpty } from "@uxf/core/utils/is-empty";
336
+
337
+ isEmpty("not-empty"); /* returns false */
338
+ isEmpty(""); /* returns true */
339
+ isEmpty(["1"]); /* returns false */
340
+ isEmpty([]); /* returns true */
341
+ ```
342
+
343
+ ## isBrowser / isServer
344
+
345
+ ```tsx
346
+ import { isBrowser } from "@uxf/core/utils/isBrowser";
347
+ import { isServer } from "@uxf/core/utils/isServer";
348
+
349
+ const browserExample = isBrowser; /* returns true if DOM is available */
350
+ const serverExample = isServer; /* returns true if DOM is NOT available */
351
+ ```
352
+
353
+ ## isNil
354
+
355
+ ```tsx
356
+ import { isNil } from "@uxf/core/utils/is-nil";
357
+
358
+ isNil(null); /* returns true */
359
+ isNil(undefined); /* returns true */
360
+ isNil(true); /* returns false */
361
+ isNil(1); /* returns false */
362
+ isNil(0); /* returns false */
363
+ isNil([]); /* returns false */
364
+ isNil("string"); /* returns false */
365
+ ```
366
+
367
+ ## isNotNil
368
+
369
+ ```tsx
370
+ import { isNotNil } from "@uxf/core/utils/is-not-nil";
371
+
372
+ isNotNil(null); /* returns false */
373
+ isNotNil(undefined); /* returns false */
374
+ isNotNil(true); /* returns true */
375
+ isNotNil(1); /* returns true */
376
+ isNotNil(0); /* returns true */
377
+ isNotNil([]); /* returns true */
378
+ isNotNil("string"); /* returns true */
379
+ ```
380
+
381
+ ```tsx
382
+ import { last } from "@uxf/core/utils/last";
383
+
384
+ last([1, 2]); /* returns 2 */
385
+ last([]); /* returns undefined */
386
+ ```
387
+
388
+ ## slugify
389
+
390
+ ```tsx
391
+ import { slugify } from "@uxf/core/utils/slugify";
392
+
393
+ const example = slugify("Jak se dnes máte?"); /* returns "jak-se-dnes-mate" */
394
+ ```
395
+
396
+ ### stableStringify
397
+
398
+ Deterministically converts any JavaScript value into a JSON string by recursively sorting object keys while preserving array order. It safely handles circular references by replacing them with the string "[Circular]" and never mutates the input. This makes it ideal for creating stable cache keys, hashing inputs, logging, or equality checks that should ignore object key order.
399
+
400
+ ```typescript
401
+ // Example usage
402
+ import { stableStringify } from "./stable-stringify";
403
+
404
+ // Key order does not affect the output
405
+ const a = { b: 2, a: 1, nested: { y: 2, x: 1 } };
406
+ const b = { nested: { x: 1, y: 2 }, a: 1, b: 2 };
407
+
408
+ console.log(stableStringify(a));
409
+ // -> {"a":1,"b":2,"nested":{"x":1,"y":2}}
410
+
411
+ console.log(stableStringify(b));
412
+ // -> {"a":1,"b":2,"nested":{"x":1,"y":2}} // same as above
413
+
414
+ // Circular references are handled
415
+ const obj: any = { name: "root" };
416
+ obj.self = obj;
417
+
418
+ console.log(stableStringify(obj));
419
+ // -> {"name":"root","self":"[Circular]"}
420
+ ```
421
+
422
+ ## trimTrailingZeros
423
+
424
+ ```tsx
425
+ import { trimTrailingZeros } from "@uxf/core/utils/trimTrailingZeros";
426
+
427
+ const example = trimTrailingZeros("120,450"); /* returns "120,45" */
222
428
  ```
223
429
 
224
430
  ## Validators
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/core",
3
- "version": "11.80.4",
3
+ "version": "11.87.0",
4
4
  "description": "UXF Core",
5
5
  "author": "Petr Vejvoda <vejvoda@uxf.cz>",
6
6
  "homepage": "https://gitlab.com/uxf-npm/core#readme",
@@ -0,0 +1 @@
1
+ export declare function deepEqualIgnoringKeyOrder(a: unknown, b: unknown): boolean;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deepEqualIgnoringKeyOrder = deepEqualIgnoringKeyOrder;
4
+ const stable_stringify_1 = require("./stable-stringify");
5
+ function deepEqualIgnoringKeyOrder(a, b) {
6
+ return (0, stable_stringify_1.stableStringify)(a) === (0, stable_stringify_1.stableStringify)(b);
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // packages/core/utils/deep-equal-ingoring-key-order.test.ts
4
+ const deep_equal_ingoring_key_order_1 = require("./deep-equal-ingoring-key-order");
5
+ describe("deepEqualIgnoringKeyOrder", () => {
6
+ it("returns true for objects with same keys in different orders", () => {
7
+ const a = { b: 2, a: 1, c: 3 };
8
+ const b = { c: 3, b: 2, a: 1 };
9
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(true);
10
+ });
11
+ it("returns true for nested objects with different key orders", () => {
12
+ const a = { z: 0, nested: { b: 2, a: 1 }, another: { y: 2, x: 1 } };
13
+ const b = { another: { x: 1, y: 2 }, nested: { a: 1, b: 2 }, z: 0 };
14
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(true);
15
+ });
16
+ it("returns true for arrays where items are objects with different key orders", () => {
17
+ const a = [
18
+ { b: 2, a: 1 },
19
+ { d: 4, c: 3 },
20
+ ];
21
+ const b = [
22
+ { a: 1, b: 2 },
23
+ { c: 3, d: 4 },
24
+ ];
25
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(true);
26
+ });
27
+ it("returns false when array order differs", () => {
28
+ const a = [
29
+ { a: 1, b: 2 },
30
+ { c: 3, d: 4 },
31
+ ];
32
+ const b = [
33
+ { c: 3, d: 4 },
34
+ { a: 1, b: 2 },
35
+ ];
36
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(false);
37
+ });
38
+ it("returns false when values differ", () => {
39
+ const a = { a: 1, b: 2 };
40
+ const b = { a: 1, b: 3 };
41
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(false);
42
+ });
43
+ it("returns false for different types", () => {
44
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(1, "1")).toBe(false);
45
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(true, "true")).toBe(false);
46
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(null, {})).toBe(false);
47
+ });
48
+ it("returns true for identical primitives", () => {
49
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(null, null)).toBe(true);
50
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(42, 42)).toBe(true);
51
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(true, true)).toBe(true);
52
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)("hello", "hello")).toBe(true);
53
+ });
54
+ it("handles equal circular object references as equal", () => {
55
+ const a = { name: "root" };
56
+ a.self = a;
57
+ const b = { name: "root" };
58
+ b.self = b;
59
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(true);
60
+ });
61
+ it("distinguishes different circular shapes", () => {
62
+ const x = { a: 1 };
63
+ x.self = x; // self points to parent
64
+ const y = { a: 1, self: {} };
65
+ y.self.self = y.self; // self points to itself (nested circular)
66
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(x, y)).toBe(false);
67
+ });
68
+ it("works with nested circular structures", () => {
69
+ const a = { id: 1, child: { id: 2 } };
70
+ a.child.parent = a;
71
+ const b = { child: { id: 2 }, id: 1 };
72
+ b.child.parent = b;
73
+ expect((0, deep_equal_ingoring_key_order_1.deepEqualIgnoringKeyOrder)(a, b)).toBe(true);
74
+ });
75
+ });
@@ -0,0 +1,3 @@
1
+ export declare function filterAriaAndDataAttrs<P extends Record<string, any>>(props: P): {
2
+ [k: string]: any;
3
+ };
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filterAriaAndDataAttrs = filterAriaAndDataAttrs;
4
+ function filterAriaAndDataAttrs(props) {
5
+ return Object.fromEntries(Object.entries(props).filter((p) => { var _a, _b; return Boolean(((_a = p.at(0)) === null || _a === void 0 ? void 0 : _a.startsWith("aria-")) || ((_b = p.at(0)) === null || _b === void 0 ? void 0 : _b.startsWith("data-"))); }));
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const filter_aria_and_data_attrs_1 = require("./filter-aria-and-data-attrs");
4
+ describe("filterAriaAndDataAttrs", () => {
5
+ it("should filter only aria- attributes", () => {
6
+ const props = {
7
+ "aria-label": "test label",
8
+ "aria-hidden": true,
9
+ className: "test-class",
10
+ id: "test-id",
11
+ };
12
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
13
+ expect(result).toEqual({
14
+ "aria-label": "test label",
15
+ "aria-hidden": true,
16
+ });
17
+ });
18
+ it("should filter only data- attributes", () => {
19
+ const props = {
20
+ "data-testid": "test",
21
+ "data-value": 123,
22
+ className: "test-class",
23
+ onClick: jest.fn(),
24
+ };
25
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
26
+ expect(result).toEqual({
27
+ "data-testid": "test",
28
+ "data-value": 123,
29
+ });
30
+ });
31
+ it("should filter both aria- and data- attributes", () => {
32
+ const props = {
33
+ "aria-label": "label",
34
+ "data-testid": "test",
35
+ "aria-describedby": "desc",
36
+ "data-custom": "value",
37
+ className: "class",
38
+ style: { color: "red" },
39
+ };
40
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
41
+ expect(result).toEqual({
42
+ "aria-label": "label",
43
+ "data-testid": "test",
44
+ "aria-describedby": "desc",
45
+ "data-custom": "value",
46
+ });
47
+ });
48
+ it("should return empty object when no aria- or data- attributes present", () => {
49
+ const props = {
50
+ className: "test",
51
+ id: "id",
52
+ onClick: jest.fn(),
53
+ };
54
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
55
+ expect(result).toEqual({});
56
+ });
57
+ it("should return empty object for empty input", () => {
58
+ const props = {};
59
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
60
+ expect(result).toEqual({});
61
+ });
62
+ it("should handle attributes with various value types", () => {
63
+ const props = {
64
+ "aria-expanded": false,
65
+ "aria-level": 2,
66
+ "data-object": { nested: "value" },
67
+ "data-array": [1, 2, 3],
68
+ "data-null": null,
69
+ "data-undefined": undefined,
70
+ regularProp: "value",
71
+ };
72
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
73
+ expect(result).toEqual({
74
+ "aria-expanded": false,
75
+ "aria-level": 2,
76
+ "data-object": { nested: "value" },
77
+ "data-array": [1, 2, 3],
78
+ "data-null": null,
79
+ "data-undefined": undefined,
80
+ });
81
+ });
82
+ it("should not filter attributes that contain but don't start with aria- or data-", () => {
83
+ const props = {
84
+ "aria-label": "keep",
85
+ "data-id": "keep",
86
+ "my-aria-label": "remove",
87
+ "my-data-id": "remove",
88
+ };
89
+ const result = (0, filter_aria_and_data_attrs_1.filterAriaAndDataAttrs)(props);
90
+ expect(result).toEqual({
91
+ "aria-label": "keep",
92
+ "data-id": "keep",
93
+ });
94
+ });
95
+ });
package/utils/resizer.js CHANGED
@@ -35,7 +35,11 @@ const BASE_PATH = (_b = process.env.NEXT_PUBLIC_BASE_PATH) !== null && _b !== vo
35
35
  const resizerStaticImageUrl = (src, width = "auto", height = "auto", props = {}, version = 1) => {
36
36
  var _a;
37
37
  const { fit = "cover", position = "center", toFormat = undefined, trim = "not-trim", background = "FFF" } = props;
38
- const filepath = typeof src === "string" ? src : src.src;
38
+ let filepath = typeof src === "string" ? src : src.src;
39
+ // fix absolute assetPrefix
40
+ if (!filepath.startsWith("/")) {
41
+ filepath = new URL(filepath).pathname;
42
+ }
39
43
  const dotIndex = filepath.lastIndexOf(".");
40
44
  const filename = filepath.slice(0, dotIndex);
41
45
  const extension = filepath.slice(dotIndex + 1);
@@ -16,7 +16,7 @@ describe("resizerGetDefaultConfig", () => {
16
16
  toFormat: "avif",
17
17
  quality: 1,
18
18
  });
19
- expect(url).toBe("/generated/static/x_x_cv_b_t_nt_1/1http://example.com/image.jpg.avif");
19
+ expect(url).toBe("/generated/static/x_x_cv_b_t_nt_1/1/image.jpg.avif");
20
20
  });
21
21
  it("should return a valid URL for an ImageResponse source", () => {
22
22
  const imageResponse = {
@@ -29,7 +29,7 @@ describe("resizerGetDefaultConfig", () => {
29
29
  });
30
30
  it("should return a valid URL for a string source", () => {
31
31
  const url = (0, resizer_1.resizerImageUrl)("http://example.com/image.png", 250, 250, { toFormat: "webp" });
32
- expect(url).toBe("/generated/static/250_250_cv_c_FFF_nt_x/1http://example.com/image.png.webp");
32
+ expect(url).toBe("/generated/static/250_250_cv_c_FFF_nt_x/1/image.png.webp");
33
33
  });
34
34
  it("should return undefined for null or undefined source", () => {
35
35
  expect((0, resizer_1.resizerImageUrl)(null)).toBeUndefined();
@@ -0,0 +1 @@
1
+ export declare function stableStringify(value: unknown): string;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stableStringify = stableStringify;
4
+ function stableStringify(value) {
5
+ const seen = new WeakSet();
6
+ function normalize(val) {
7
+ if (val && typeof val === "object") {
8
+ if (seen.has(val)) {
9
+ return "[Circular]";
10
+ }
11
+ seen.add(val);
12
+ if (Array.isArray(val)) {
13
+ return val.map(normalize);
14
+ }
15
+ // plain object: sort keys
16
+ const out = {};
17
+ for (const key of Object.keys(val).sort()) {
18
+ out[key] = normalize(val[key]);
19
+ }
20
+ return out;
21
+ }
22
+ return val;
23
+ }
24
+ return JSON.stringify(normalize(value));
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const stable_stringify_1 = require("./stable-stringify");
4
+ describe("stableStringify", () => {
5
+ it("produces identical strings for objects with the same keys in different orders", () => {
6
+ const a = { b: 2, a: 1, c: 3 };
7
+ const b = { c: 3, b: 2, a: 1 };
8
+ expect((0, stable_stringify_1.stableStringify)(a)).toBe((0, stable_stringify_1.stableStringify)(b));
9
+ expect((0, stable_stringify_1.stableStringify)(a)).toBe('{"a":1,"b":2,"c":3}');
10
+ });
11
+ it("sorts keys recursively in nested objects", () => {
12
+ const a = { z: 0, nested: { b: 2, a: 1 }, another: { y: 2, x: 1 } };
13
+ const b = { another: { x: 1, y: 2 }, nested: { a: 1, b: 2 }, z: 0 };
14
+ const sa = (0, stable_stringify_1.stableStringify)(a);
15
+ const sb = (0, stable_stringify_1.stableStringify)(b);
16
+ expect(sa).toBe(sb);
17
+ expect(sa).toBe('{"another":{"x":1,"y":2},"nested":{"a":1,"b":2},"z":0}');
18
+ });
19
+ it("normalizes arrays while preserving array order", () => {
20
+ const a = [
21
+ { b: 2, a: 1 },
22
+ { d: 4, c: 3 },
23
+ ];
24
+ const b = [
25
+ { a: 1, b: 2 },
26
+ { c: 3, d: 4 },
27
+ ];
28
+ expect((0, stable_stringify_1.stableStringify)(a)).toBe((0, stable_stringify_1.stableStringify)(b));
29
+ expect((0, stable_stringify_1.stableStringify)(a)).toBe('[{"a":1,"b":2},{"c":3,"d":4}]');
30
+ });
31
+ it("handles circular references in objects", () => {
32
+ const obj = { name: "root" };
33
+ obj.self = obj;
34
+ expect((0, stable_stringify_1.stableStringify)(obj)).toBe('{"name":"root","self":"[Circular]"}');
35
+ });
36
+ it("handles circular references in arrays", () => {
37
+ const arr = [];
38
+ arr.push(arr);
39
+ expect((0, stable_stringify_1.stableStringify)(arr)).toBe('["[Circular]"]');
40
+ });
41
+ it("handles mixed nested circular structures", () => {
42
+ const a = { id: 1, child: { id: 2 } };
43
+ a.child.parent = a;
44
+ const str = (0, stable_stringify_1.stableStringify)(a);
45
+ expect(str).toBe('{"child":{"id":2,"parent":"[Circular]"},"id":1}');
46
+ });
47
+ it("handles primitives", () => {
48
+ expect((0, stable_stringify_1.stableStringify)(null)).toBe("null");
49
+ expect((0, stable_stringify_1.stableStringify)(42)).toBe("42");
50
+ expect((0, stable_stringify_1.stableStringify)(true)).toBe("true");
51
+ expect((0, stable_stringify_1.stableStringify)("hello")).toBe('"hello"');
52
+ });
53
+ it("does not mutate the input object", () => {
54
+ const input = { b: 2, a: 1, nested: { y: 2, x: 1 } };
55
+ const snapshot = JSON.parse(JSON.stringify(input));
56
+ void (0, stable_stringify_1.stableStringify)(input);
57
+ expect(input).toEqual(snapshot);
58
+ });
59
+ });