From 7e9fff41339087b493c8cc1cc3e74a7f29b764de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Thu, 9 Jul 2026 19:07:53 +0200 Subject: [PATCH] Add order-insensitive deep object comparison to cli-kit Adds `deepCompareWithOrderInsensitiveArrays` (plus private helpers) to `@shopify/cli-kit/common/object`. Deeply compares two values while treating arrays as unordered, which the deploy/release config breakdown will use to compare webhook subscriptions regardless of ordering. Purely additive: no existing export is modified. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cli-kit/src/public/common/object.test.ts | 35 +++++++++++++++++++ packages/cli-kit/src/public/common/object.ts | 31 ++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/packages/cli-kit/src/public/common/object.test.ts b/packages/cli-kit/src/public/common/object.test.ts index 93a116639c0..00dc883cbbf 100644 --- a/packages/cli-kit/src/public/common/object.test.ts +++ b/packages/cli-kit/src/public/common/object.test.ts @@ -1,6 +1,7 @@ import { compact, deepCompare, + deepCompareWithOrderInsensitiveArrays, deepDifference, deepMergeObjects, getPathValue, @@ -116,6 +117,40 @@ describe('deepCompare', () => { }) }) +describe('deepCompareWithOrderInsensitiveArrays', () => { + test('returns true when arrays contain the same values in a different order', () => { + const first = { + webhooks: { + subscriptions: [ + {topic: 'orders/delete', uri: 'https://example.com/orders/delete'}, + {topic: 'products/update', uri: 'https://example.com/products/update'}, + ], + }, + } + const second = { + webhooks: { + subscriptions: [ + {uri: 'https://example.com/products/update', topic: 'products/update'}, + {uri: 'https://example.com/orders/delete', topic: 'orders/delete'}, + ], + }, + } + + const result = deepCompareWithOrderInsensitiveArrays(first, second) + + expect(result).toBeTruthy() + }) + + test('returns false when arrays contain different values', () => { + const first = {items: [{id: 1}, {id: 2}]} + const second = {items: [{id: 1}, {id: 3}]} + + const result = deepCompareWithOrderInsensitiveArrays(first, second) + + expect(result).toBeFalsy() + }) +}) + describe('deepDifference', () => { test('returns the difference between two objects', () => { // Given diff --git a/packages/cli-kit/src/public/common/object.ts b/packages/cli-kit/src/public/common/object.ts index 23e6e122df8..0d6a02f2a8f 100644 --- a/packages/cli-kit/src/public/common/object.ts +++ b/packages/cli-kit/src/public/common/object.ts @@ -68,6 +68,37 @@ export function deepCompare(one: object, two: object): boolean { return lodashIsEqual(one, two) } +/** + * Deeply compares two values and treats arrays as order-insensitive. + * + * @param one - The first value to be compared. + * @param two - The second value to be compared. + * @returns True if the normalized values are equal, false otherwise. + */ +export function deepCompareWithOrderInsensitiveArrays(one: unknown, two: unknown): boolean { + return lodashIsEqual(normalizeOrderInsensitiveArrays(one), normalizeOrderInsensitiveArrays(two)) +} + +function normalizeOrderInsensitiveArrays(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(normalizeOrderInsensitiveArrays).sort(compareNormalizedValues) + } + + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value) + .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)) + .map(([key, nestedValue]) => [key, normalizeOrderInsensitiveArrays(nestedValue)]), + ) + } + + return value ?? {} +} + +function compareNormalizedValues(left: unknown, right: unknown) { + return JSON.stringify(left).localeCompare(JSON.stringify(right)) +} + /** * Return the difference between two nested objects. *