From f06806033a06d8a031f776040cc084ee5531d278 Mon Sep 17 00:00:00 2001 From: Kenrick Date: Fri, 6 Sep 2024 14:09:19 +0800 Subject: [PATCH 1/4] fix: update shrinkwrap when globalPackageExtensions has been changed --- ...ap-packageExtensions_2024-09-06-03-55.json | 10 + ...ap-packageExtensions_2024-09-06-03-55.json | 10 + common/reviews/api/node-core-library.api.md | 4 + libraries/node-core-library/src/Sort.ts | 88 +++++++++ .../node-core-library/src/test/Sort.test.ts | 179 ++++++++++++++++++ .../installManager/WorkspaceInstallManager.ts | 32 +++- .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 4 + 7 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json create mode 100644 common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json diff --git a/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json b/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json new file mode 100644 index 00000000000..deaa6a64bf8 --- /dev/null +++ b/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Always update shrinkwrap when globalPackageExtensions has been changed", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json b/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json new file mode 100644 index 00000000000..32953c4e722 --- /dev/null +++ b/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add Sort.sortKeys for sorting keys in an object", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 4c7786d3668..9ff8037e54a 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -831,6 +831,10 @@ export class Sort { static isSorted(collection: Iterable, comparer?: (x: any, y: any) => number): boolean; static isSortedBy(collection: Iterable, keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): boolean; static sortBy(array: T[], keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): void; + static sortKeys> | unknown[]>(object: T, { deep, compare }?: { + deep?: boolean; + compare?: (x: string, y: string) => number; + }): T; static sortMapKeys(map: Map, keyComparer?: (x: K, y: K) => number): void; static sortSet(set: Set, comparer?: (x: T, y: T) => number): void; static sortSetBy(set: Set, keySelector: (element: T) => any, keyComparer?: (x: T, y: T) => number): void; diff --git a/libraries/node-core-library/src/Sort.ts b/libraries/node-core-library/src/Sort.ts index 77ff843c67a..3643b379fd1 100644 --- a/libraries/node-core-library/src/Sort.ts +++ b/libraries/node-core-library/src/Sort.ts @@ -231,4 +231,92 @@ export class Sort { set.add(item); } } + + /** + * Sort the keys given in an object + * + * @example + * + * ```ts + * console.log(Sort.sortKeys({ c: 3, b: 2, a: 1 })); // { a: 1, b: 2, c: 3} + * ``` + */ + public static sortKeys> | unknown[]>( + object: T, + { deep, compare }: { deep?: boolean; compare?: (x: string, y: string) => number } = { + deep: false, + compare: Sort.compareByValue + } + ): T { + function isPlainObject(obj: unknown): obj is object { + return obj !== null && typeof obj === 'object'; + } + if (!isPlainObject(object) && !Array.isArray(object)) { + throw new TypeError(`Expected object or array`); + } + + const cache: WeakMap< + Partial> | unknown[], + Partial> | unknown[] + > = new WeakMap(); + + function innerSortArray(arr: unknown[]): unknown[] { + const resultFromCache: undefined | Partial> | unknown[] = cache.get(arr); + if (resultFromCache !== undefined) { + return resultFromCache as unknown[]; + } + const result: unknown[] = []; + cache.set(arr, result); + if (deep) { + result.push( + ...arr.map((entry) => { + if (Array.isArray(entry)) { + return innerSortArray(entry); + } else if (isPlainObject(entry)) { + return innerSortKeys(entry); + } + return entry; + }) + ); + } else { + result.push(...arr); + } + + return result; + } + function innerSortKeys(obj: Partial>): Partial> { + const resultFromCache: undefined | Partial> | unknown[] = cache.get(obj); + if (resultFromCache !== undefined) { + return resultFromCache as Partial>; + } + const result: Partial> = {}; + const keys: string[] = Object.keys(obj).sort(compare); + + cache.set(obj, result); + + for (const key of keys) { + const value: unknown = obj[key]; + let newValue: unknown; + if (deep) { + if (Array.isArray(value)) { + newValue = innerSortArray(value); + } else if (isPlainObject(value)) { + newValue = innerSortKeys(value); + } else { + newValue = value; + } + } else { + newValue = value; + } + Object.defineProperty(result, key, { + ...Object.getOwnPropertyDescriptor(obj, key), + value: newValue + }); + } + + return result; + } + + return Array.isArray(object) ? (innerSortArray(object) as T) : (innerSortKeys(object) as T); + } } diff --git a/libraries/node-core-library/src/test/Sort.test.ts b/libraries/node-core-library/src/test/Sort.test.ts index 997e5f344c1..17cb8124c0e 100644 --- a/libraries/node-core-library/src/test/Sort.test.ts +++ b/libraries/node-core-library/src/test/Sort.test.ts @@ -69,3 +69,182 @@ test('Sort.sortSet', () => { Sort.sortSet(set); expect(Array.from(set)).toEqual(['aardvark', 'goose', 'zebra']); }); + +describe('Sort.sortKeys', () => { + // Test cases from https://github.com/sindresorhus/sort-keys/blob/v4.2.0/test.js + function deepEqualInOrder( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + actual: Partial>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expected: Partial> + ): void { + expect(actual).toEqual(expected); + + const seen = new Set(); + + function assertSameKeysInOrder( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + object1: Partial>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + object2: Partial> + ): void { + // This function assumes the objects given are already deep equal. + + if (seen.has(object1) && seen.has(object2)) { + return; + } + + seen.add(object1); + seen.add(object2); + + if (Array.isArray(object1)) { + for (const index of object1.keys()) { + assertSameKeysInOrder(object1[index], object2[index]); + } + } else if (typeof object1 === 'object') { + const keys1 = Object.keys(object1); + const keys2 = Object.keys(object2); + expect(keys1).toEqual(keys2); + for (const index of keys1.keys()) { + assertSameKeysInOrder(object1[keys1[index]], object2[keys2[index]]); + } + } + } + + assertSameKeysInOrder(actual, expected); + } + + test('sort the keys of an object', () => { + deepEqualInOrder(Sort.sortKeys({ c: 0, a: 0, b: 0 }), { a: 0, b: 0, c: 0 }); + }); + + test('custom compare function', () => { + const compare: (a: string, b: string) => number = (a: string, b: string) => b.localeCompare(a); + deepEqualInOrder(Sort.sortKeys({ c: 0, a: 0, b: 0 }, { compare }), { c: 0, b: 0, a: 0 }); + }); + + test('deep option', () => { + deepEqualInOrder(Sort.sortKeys({ c: { c: 0, a: 0, b: 0 }, a: 0, b: 0 }, { deep: true }), { + a: 0, + b: 0, + c: { a: 0, b: 0, c: 0 } + }); + + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object: Partial> = { a: 0 }; + object.circular = object; + Sort.sortKeys(object, { deep: true }); + }).not.toThrow(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object: Partial> = { z: 0 }; + object.circular = object; + const sortedObject = Sort.sortKeys(object, { deep: true }); + + expect(sortedObject).toBe(sortedObject.circular); + expect(Object.keys(sortedObject)).toEqual(['circular', 'z']); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object1: Partial> = { b: 0 }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object2: Partial> = { d: 0 }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object3: Partial> = { a: [{ b: 0 }] }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object4: Partial> = { a: [{ d: 0 }] }; + + object1.a = object2; + object2.c = object1; + object3.a[0].a = object4.a[0]; + object4.a[0].c = object3.a[0]; + + expect(() => { + Sort.sortKeys(object1, { deep: true }); + Sort.sortKeys(object2, { deep: true }); + Sort.sortKeys(object3, { deep: true }); + Sort.sortKeys(object4, { deep: true }); + }).not.toThrow(); + + const sorted = Sort.sortKeys(object1, { deep: true }); + const deepSorted = Sort.sortKeys(object3, { deep: true }); + + expect(sorted).toBe(sorted.a.c); + deepEqualInOrder(deepSorted.a[0], deepSorted.a[0].a.c); + expect(Object.keys(sorted)).toStrictEqual(['a', 'b']); + expect(Object.keys(deepSorted.a[0])).toStrictEqual(['a', 'b']); + deepEqualInOrder( + Sort.sortKeys({ c: { c: 0, a: 0, b: 0 }, a: 0, b: 0, z: [9, 8, 7, 6, 5] }, { deep: true }), + { a: 0, b: 0, c: { a: 0, b: 0, c: 0 }, z: [9, 8, 7, 6, 5] } + ); + expect(Object.keys(Sort.sortKeys({ a: [{ b: 0, a: 0 }] }, { deep: true }).a[0])).toEqual(['a', 'b']); + }); + + test('deep arrays', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object: Partial> = { + b: 0, + a: [{ b: 0, a: 0 }, [{ b: 0, a: 0 }]] + }; + object.a.push(object); + object.a[1].push(object.a[1]); + + expect(() => { + Sort.sortKeys(object, { deep: true }); + }).not.toThrow(); + + const sorted = Sort.sortKeys(object, { deep: true }); + // Cannot use .toBe() as Jest will encounter https://github.com/jestjs/jest/issues/10577 + expect(sorted.a[2] === sorted).toBeTruthy(); + expect(sorted.a[1][1] === sorted.a[1]).toBeTruthy(); + expect(Object.keys(sorted)).toEqual(['a', 'b']); + expect(Object.keys(sorted.a[0])).toEqual(['a', 'b']); + expect(Object.keys(sorted.a[1][0])).toEqual(['a', 'b']); + }); + + test('top-level array', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const array: Array = [ + { b: 0, a: 0 }, + { c: 0, d: 0 } + ]; + const sorted = Sort.sortKeys(array); + expect(sorted).not.toBe(array); + expect(sorted[0]).toBe(array[0]); + expect(sorted[1]).toBe(array[1]); + + const deepSorted = Sort.sortKeys(array, { deep: true }); + expect(deepSorted).not.toBe(array); + expect(deepSorted[0]).not.toBe(array[0]); + expect(deepSorted[1]).not.toBe(array[1]); + expect(Object.keys(deepSorted[0])).toEqual(['a', 'b']); + expect(Object.keys(deepSorted[1])).toEqual(['c', 'd']); + }); + + test('keeps property descriptors intact', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const descriptors: Partial> = { + b: { + value: 1, + configurable: true, + enumerable: true, + writable: false + }, + a: { + value: 2, + configurable: false, + enumerable: true, + writable: true + } + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object: Partial> = {}; + Object.defineProperties(object, descriptors); + + const sorted = Sort.sortKeys(object); + + deepEqualInOrder(sorted, { a: 2, b: 1 }); + expect(Object.getOwnPropertyDescriptors(sorted)).toEqual(descriptors); + }); +}); diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index bdcb2615f4d..f582da32357 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -10,8 +10,10 @@ import { AlreadyReportedError, Async, type IDependenciesMetaTable, - Path + Path, + Sort } from '@rushstack/node-core-library'; +import { createHash } from 'crypto'; import { BaseInstallManager } from '../base/BaseInstallManager'; import type { IInstallManagerOptions } from '../base/BaseInstallManagerTypes'; @@ -378,6 +380,18 @@ export class WorkspaceInstallManager extends BaseInstallManager { shrinkwrapIsUpToDate = false; } + // Check if packageExtensionsChecksum matches globalPackageExtension's hash + const packageExtensionsChecksum: string | undefined = this._getPackageExtensionChecksum( + this.rushConfiguration.pnpmOptions.globalPackageExtensions + ); + const packageExtensionsChecksumAreEqual: boolean = + packageExtensionsChecksum === shrinkwrapFile?.packageExtensionsChecksum; + + if (!packageExtensionsChecksumAreEqual) { + shrinkwrapWarnings.push("The package extension hash doesn't match the current shrinkwrap."); + shrinkwrapIsUpToDate = false; + } + // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration, subspace, undefined); @@ -388,6 +402,22 @@ export class WorkspaceInstallManager extends BaseInstallManager { return { shrinkwrapIsUpToDate, shrinkwrapWarnings }; } + private _getPackageExtensionChecksum( + packageExtensions: Record | undefined + ): string | undefined { + const packageExtensionsChecksum: string | undefined = + Object.keys(packageExtensions ?? {}).length === 0 + ? undefined + : createObjectChecksum(packageExtensions!); + + function createObjectChecksum(obj: Record): string { + const s: string = JSON.stringify(Sort.sortKeys(obj, { deep: true })); + return createHash('md5').update(s).digest('hex'); + } + + return packageExtensionsChecksum; + } + protected canSkipInstall(lastModifiedDate: Date, subspace: Subspace): boolean { if (!super.canSkipInstall(lastModifiedDate, subspace)) { return false; diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 52fbd234f81..3860200b737 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -148,6 +148,8 @@ export interface IPnpmShrinkwrapYaml { specifiers: Record; /** The list of override version number for dependencies */ overrides?: { [dependency: string]: string }; + /** The checksum of package extensions fields for extending dependencies */ + packageExtensionsChecksum?: string; } export interface ILoadFromFileOptions { @@ -275,6 +277,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { public readonly specifiers: ReadonlyMap; public readonly packages: ReadonlyMap; public readonly overrides: ReadonlyMap; + public readonly packageExtensionsChecksum: undefined | string; private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml; private readonly _integrities: Map>; @@ -304,6 +307,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { this.specifiers = new Map(Object.entries(shrinkwrapJson.specifiers || {})); this.packages = new Map(Object.entries(shrinkwrapJson.packages || {})); this.overrides = new Map(Object.entries(shrinkwrapJson.overrides || {})); + this.packageExtensionsChecksum = shrinkwrapJson.packageExtensionsChecksum; // Importers only exist in workspaces this.isWorkspaceCompatible = this.importers.size > 0; From 343bd55e7f0f3292c305ace6477e231859ba83a3 Mon Sep 17 00:00:00 2001 From: Kenrick Date: Mon, 9 Sep 2024 11:02:42 +0800 Subject: [PATCH 2/4] fix: code review --- common/reviews/api/node-core-library.api.md | 11 +- libraries/node-core-library/src/Sort.ts | 162 +++++++++++------- libraries/node-core-library/src/index.ts | 2 +- .../node-core-library/src/test/Sort.test.ts | 12 +- 4 files changed, 120 insertions(+), 67 deletions(-) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 9ff8037e54a..502e330a150 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -608,6 +608,12 @@ export interface IRunWithRetriesOptions { retryDelayMs?: number; } +// @public +export interface ISortKeysOptions { + compare?: (x: string, y: string) => number; + deep?: boolean; +} + // @public export interface IStringBuilder { append(text: string): void; @@ -831,10 +837,7 @@ export class Sort { static isSorted(collection: Iterable, comparer?: (x: any, y: any) => number): boolean; static isSortedBy(collection: Iterable, keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): boolean; static sortBy(array: T[], keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): void; - static sortKeys> | unknown[]>(object: T, { deep, compare }?: { - deep?: boolean; - compare?: (x: string, y: string) => number; - }): T; + static sortKeys> | unknown[]>(object: T, options?: ISortKeysOptions): T; static sortMapKeys(map: Map, keyComparer?: (x: K, y: K) => number): void; static sortSet(set: Set, comparer?: (x: T, y: T) => number): void; static sortSetBy(set: Set, keySelector: (element: T) => any, keyComparer?: (x: T, y: T) => number): void; diff --git a/libraries/node-core-library/src/Sort.ts b/libraries/node-core-library/src/Sort.ts index 3643b379fd1..fd02c9ec899 100644 --- a/libraries/node-core-library/src/Sort.ts +++ b/libraries/node-core-library/src/Sort.ts @@ -1,6 +1,38 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +/** + * Options of {@link Sort.sortKeys} + * + * @public + */ +export interface ISortKeysOptions { + /** + * Whether or not to recursively sort keys, both in objects and arrays + * @defaultValue false + */ + deep?: boolean; + /** + * Custom compare function when sorting the keys + * + * @defaultValue Sort.compareByValue + * @param x - Key name + * @param y - Key name + * @returns -1 if `x` is smaller than `y`, 1 if `x` is greater than `y`, or 0 if the values are equal. + */ + compare?: (x: string, y: string) => number; +} + +interface ISortKeysContext { + cache: SortKeysCache; + options: ISortKeysOptions; +} + +type SortKeysCache = WeakMap< + Partial> | unknown[], + Partial> | unknown[] +>; + /** * Operations for sorting collections. * @@ -235,6 +267,9 @@ export class Sort { /** * Sort the keys given in an object * + * @param object - The object to be sorted + * @param options - The options for sort keys + * * @example * * ```ts @@ -243,80 +278,85 @@ export class Sort { */ public static sortKeys> | unknown[]>( object: T, - { deep, compare }: { deep?: boolean; compare?: (x: string, y: string) => number } = { + options: ISortKeysOptions = { deep: false, compare: Sort.compareByValue } ): T { - function isPlainObject(obj: unknown): obj is object { - return obj !== null && typeof obj === 'object'; - } if (!isPlainObject(object) && !Array.isArray(object)) { throw new TypeError(`Expected object or array`); } + const cache: SortKeysCache = new WeakMap(); + const context: ISortKeysContext = { + cache, + options + }; - const cache: WeakMap< - Partial> | unknown[], - Partial> | unknown[] - > = new WeakMap(); + return Array.isArray(object) + ? (innerSortArray(object, context) as T) + : (innerSortKeys(object, context) as T); + } +} +function isPlainObject(obj: unknown): obj is object { + return obj !== null && typeof obj === 'object'; +} - function innerSortArray(arr: unknown[]): unknown[] { - const resultFromCache: undefined | Partial> | unknown[] = cache.get(arr); - if (resultFromCache !== undefined) { - return resultFromCache as unknown[]; - } - const result: unknown[] = []; - cache.set(arr, result); - if (deep) { - result.push( - ...arr.map((entry) => { - if (Array.isArray(entry)) { - return innerSortArray(entry); - } else if (isPlainObject(entry)) { - return innerSortKeys(entry); - } - return entry; - }) - ); - } else { - result.push(...arr); - } +function innerSortArray(arr: unknown[], context: ISortKeysContext): unknown[] { + const resultFromCache: undefined | Partial> | unknown[] = context.cache.get(arr); + if (resultFromCache !== undefined) { + return resultFromCache as unknown[]; + } + const result: unknown[] = []; + context.cache.set(arr, result); + if (context.options.deep) { + result.push( + ...arr.map((entry) => { + if (Array.isArray(entry)) { + return innerSortArray(entry, context); + } else if (isPlainObject(entry)) { + return innerSortKeys(entry, context); + } + return entry; + }) + ); + } else { + result.push(...arr); + } - return result; - } - function innerSortKeys(obj: Partial>): Partial> { - const resultFromCache: undefined | Partial> | unknown[] = cache.get(obj); - if (resultFromCache !== undefined) { - return resultFromCache as Partial>; - } - const result: Partial> = {}; - const keys: string[] = Object.keys(obj).sort(compare); + return result; +} +function innerSortKeys( + obj: Partial>, + context: ISortKeysContext +): Partial> { + const resultFromCache: undefined | Partial> | unknown[] = context.cache.get(obj); + if (resultFromCache !== undefined) { + return resultFromCache as Partial>; + } + const result: Partial> = {}; + const keys: string[] = Object.keys(obj).sort(context.options.compare); - cache.set(obj, result); + context.cache.set(obj, result); - for (const key of keys) { - const value: unknown = obj[key]; - let newValue: unknown; - if (deep) { - if (Array.isArray(value)) { - newValue = innerSortArray(value); - } else if (isPlainObject(value)) { - newValue = innerSortKeys(value); - } else { - newValue = value; - } - } else { - newValue = value; - } - Object.defineProperty(result, key, { - ...Object.getOwnPropertyDescriptor(obj, key), - value: newValue - }); + for (const key of keys) { + const value: unknown = obj[key]; + let newValue: unknown; + if (context.options.deep) { + if (Array.isArray(value)) { + newValue = innerSortArray(value, context); + } else if (isPlainObject(value)) { + newValue = innerSortKeys(value, context); + } else { + newValue = value; } - - return result; + } else { + newValue = value; } - - return Array.isArray(object) ? (innerSortArray(object) as T) : (innerSortKeys(object) as T); + Object.defineProperty(result, key, { + ...Object.getOwnPropertyDescriptor(obj, key), + value: newValue + }); } + + return result; } diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 70396912fbc..19b7ebd8057 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -93,7 +93,7 @@ export { type IPathFormatConciselyOptions } from './Path'; export { Encoding, Text, NewlineKind, type IReadLinesFromIterableOptions } from './Text'; -export { Sort } from './Sort'; +export { Sort, type ISortKeysOptions } from './Sort'; export { AlreadyExistsBehavior, FileSystem, diff --git a/libraries/node-core-library/src/test/Sort.test.ts b/libraries/node-core-library/src/test/Sort.test.ts index 17cb8124c0e..9b8be981a5c 100644 --- a/libraries/node-core-library/src/test/Sort.test.ts +++ b/libraries/node-core-library/src/test/Sort.test.ts @@ -115,7 +115,12 @@ describe('Sort.sortKeys', () => { } test('sort the keys of an object', () => { - deepEqualInOrder(Sort.sortKeys({ c: 0, a: 0, b: 0 }), { a: 0, b: 0, c: 0 }); + const unsortedObj = { c: 0, a: 0, b: 0 }; + const sortedObj = Sort.sortKeys(unsortedObj); + // Assert that it's not sorted in-place + expect(sortedObj).not.toBe(unsortedObj); + deepEqualInOrder(unsortedObj, { c: 0, a: 0, b: 0 }); + deepEqualInOrder(sortedObj, { a: 0, b: 0, c: 0 }); }); test('custom compare function', () => { @@ -142,6 +147,11 @@ describe('Sort.sortKeys', () => { object.circular = object; const sortedObject = Sort.sortKeys(object, { deep: true }); + // Assert that it's not sorted in-place + expect(sortedObject).not.toBe(object); + expect(Object.keys(object)).toEqual(['z', 'circular']); + + // Assert that circular value references the same thing expect(sortedObject).toBe(sortedObject.circular); expect(Object.keys(sortedObject)).toEqual(['circular', 'z']); From ed9dc1d695162ec9a4e2beaf0033963588f8e1b4 Mon Sep 17 00:00:00 2001 From: Kenrick Date: Tue, 10 Sep 2024 11:17:14 +0800 Subject: [PATCH 3/4] fix: code review --- ...ap-packageExtensions_2024-09-06-03-55.json | 2 +- ...ap-packageExtensions_2024-09-06-03-55.json | 2 +- libraries/node-core-library/src/Sort.ts | 26 ++++--- .../node-core-library/src/test/Sort.test.ts | 68 +++++++------------ .../installManager/WorkspaceInstallManager.ts | 16 +++-- 5 files changed, 52 insertions(+), 62 deletions(-) diff --git a/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json b/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json index deaa6a64bf8..562bec8e668 100644 --- a/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json +++ b/common/changes/@microsoft/rush/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Always update shrinkwrap when globalPackageExtensions has been changed", + "comment": "Always update shrinkwrap when `globalPackageExtensions` in `common/config/rush/pnpm-config.json` has been changed.", "type": "none" } ], diff --git a/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json b/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json index 32953c4e722..c63a92f4177 100644 --- a/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json +++ b/common/changes/@rushstack/node-core-library/kenrick-fix-update-shrinkwrap-packageExtensions_2024-09-06-03-55.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/node-core-library", - "comment": "Add Sort.sortKeys for sorting keys in an object", + "comment": "Add a `Sort.sortKeys` function for sorting keys in an object", "type": "minor" } ], diff --git a/libraries/node-core-library/src/Sort.ts b/libraries/node-core-library/src/Sort.ts index fd02c9ec899..1d340836241 100644 --- a/libraries/node-core-library/src/Sort.ts +++ b/libraries/node-core-library/src/Sort.ts @@ -12,6 +12,7 @@ export interface ISortKeysOptions { * @defaultValue false */ deep?: boolean; + /** * Custom compare function when sorting the keys * @@ -297,6 +298,7 @@ export class Sort { : (innerSortKeys(object, context) as T); } } + function isPlainObject(obj: unknown): obj is object { return obj !== null && typeof obj === 'object'; } @@ -309,22 +311,24 @@ function innerSortArray(arr: unknown[], context: ISortKeysContext): unknown[] { const result: unknown[] = []; context.cache.set(arr, result); if (context.options.deep) { - result.push( - ...arr.map((entry) => { - if (Array.isArray(entry)) { - return innerSortArray(entry, context); - } else if (isPlainObject(entry)) { - return innerSortKeys(entry, context); - } - return entry; - }) - ); + for (const entry of arr) { + if (Array.isArray(entry)) { + result.push(innerSortArray(entry, context)); + } else if (isPlainObject(entry)) { + result.push(innerSortKeys(entry, context)); + } else { + result.push(entry); + } + } } else { - result.push(...arr); + for (const entry of arr) { + result.push(entry); + } } return result; } + function innerSortKeys( obj: Partial>, context: ISortKeysContext diff --git a/libraries/node-core-library/src/test/Sort.test.ts b/libraries/node-core-library/src/test/Sort.test.ts index 9b8be981a5c..c252b23b299 100644 --- a/libraries/node-core-library/src/test/Sort.test.ts +++ b/libraries/node-core-library/src/test/Sort.test.ts @@ -73,21 +73,14 @@ test('Sort.sortSet', () => { describe('Sort.sortKeys', () => { // Test cases from https://github.com/sindresorhus/sort-keys/blob/v4.2.0/test.js function deepEqualInOrder( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actual: Partial>, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expected: Partial> + actual: Partial>, + expected: Partial> ): void { expect(actual).toEqual(expected); - const seen = new Set(); + const seen: Set = new Set(); - function assertSameKeysInOrder( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - object1: Partial>, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - object2: Partial> - ): void { + function assertSameKeysInOrder(object1: unknown, object2: unknown): void { // This function assumes the objects given are already deep equal. if (seen.has(object1) && seen.has(object2)) { @@ -97,16 +90,24 @@ describe('Sort.sortKeys', () => { seen.add(object1); seen.add(object2); - if (Array.isArray(object1)) { + if (Array.isArray(object1) && Array.isArray(object2)) { for (const index of object1.keys()) { assertSameKeysInOrder(object1[index], object2[index]); } - } else if (typeof object1 === 'object') { + } else if ( + typeof object1 === 'object' && + typeof object2 === 'object' && + object1 != null && + object2 != null + ) { const keys1 = Object.keys(object1); const keys2 = Object.keys(object2); expect(keys1).toEqual(keys2); for (const index of keys1.keys()) { - assertSameKeysInOrder(object1[keys1[index]], object2[keys2[index]]); + assertSameKeysInOrder( + (object1 as Partial>)[keys1[index]], + (object2 as Partial>)[keys2[index]] + ); } } } @@ -135,13 +136,6 @@ describe('Sort.sortKeys', () => { c: { a: 0, b: 0, c: 0 } }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object: Partial> = { a: 0 }; - object.circular = object; - Sort.sortKeys(object, { deep: true }); - }).not.toThrow(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const object: Partial> = { z: 0 }; object.circular = object; @@ -169,20 +163,13 @@ describe('Sort.sortKeys', () => { object3.a[0].a = object4.a[0]; object4.a[0].c = object3.a[0]; - expect(() => { - Sort.sortKeys(object1, { deep: true }); - Sort.sortKeys(object2, { deep: true }); - Sort.sortKeys(object3, { deep: true }); - Sort.sortKeys(object4, { deep: true }); - }).not.toThrow(); - - const sorted = Sort.sortKeys(object1, { deep: true }); - const deepSorted = Sort.sortKeys(object3, { deep: true }); + const sortedObject1 = Sort.sortKeys(object1, { deep: true }); + const sortedObject3 = Sort.sortKeys(object3, { deep: true }); - expect(sorted).toBe(sorted.a.c); - deepEqualInOrder(deepSorted.a[0], deepSorted.a[0].a.c); - expect(Object.keys(sorted)).toStrictEqual(['a', 'b']); - expect(Object.keys(deepSorted.a[0])).toStrictEqual(['a', 'b']); + expect(sortedObject1).toBe(sortedObject1.a.c); + deepEqualInOrder(sortedObject3.a[0], sortedObject3.a[0].a.c); + expect(Object.keys(sortedObject1)).toStrictEqual(['a', 'b']); + expect(Object.keys(sortedObject3.a[0])).toStrictEqual(['a', 'b']); deepEqualInOrder( Sort.sortKeys({ c: { c: 0, a: 0, b: 0 }, a: 0, b: 0, z: [9, 8, 7, 6, 5] }, { deep: true }), { a: 0, b: 0, c: { a: 0, b: 0, c: 0 }, z: [9, 8, 7, 6, 5] } @@ -199,10 +186,6 @@ describe('Sort.sortKeys', () => { object.a.push(object); object.a[1].push(object.a[1]); - expect(() => { - Sort.sortKeys(object, { deep: true }); - }).not.toThrow(); - const sorted = Sort.sortKeys(object, { deep: true }); // Cannot use .toBe() as Jest will encounter https://github.com/jestjs/jest/issues/10577 expect(sorted.a[2] === sorted).toBeTruthy(); @@ -213,8 +196,7 @@ describe('Sort.sortKeys', () => { }); test('top-level array', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const array: Array = [ + const array: Array>> = [ { b: 0, a: 0 }, { c: 0, d: 0 } ]; @@ -232,8 +214,7 @@ describe('Sort.sortKeys', () => { }); test('keeps property descriptors intact', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const descriptors: Partial> = { + const descriptors: PropertyDescriptorMap = { b: { value: 1, configurable: true, @@ -248,8 +229,7 @@ describe('Sort.sortKeys', () => { } }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object: Partial> = {}; + const object: Partial> = {}; Object.defineProperties(object, descriptors); const sorted = Sort.sortKeys(object); diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index f582da32357..117dd9bbfa9 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -405,16 +405,12 @@ export class WorkspaceInstallManager extends BaseInstallManager { private _getPackageExtensionChecksum( packageExtensions: Record | undefined ): string | undefined { + // https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L331 const packageExtensionsChecksum: string | undefined = Object.keys(packageExtensions ?? {}).length === 0 ? undefined : createObjectChecksum(packageExtensions!); - function createObjectChecksum(obj: Record): string { - const s: string = JSON.stringify(Sort.sortKeys(obj, { deep: true })); - return createHash('md5').update(s).digest('hex'); - } - return packageExtensionsChecksum; } @@ -774,3 +770,13 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } } + +/** + * Source: https://github.com/pnpm/pnpm/blob/ba9409ffcef0c36dc1b167d770a023c87444822d/pkg-manager/core/src/install/index.ts#L821-L824 + * @param obj + * @returns + */ +function createObjectChecksum(obj: Record): string { + const s: string = JSON.stringify(Sort.sortKeys(obj, { deep: true })); + return createHash('md5').update(s).digest('hex'); +} From 83b2867c0aa5694a43f69ca69b5bdc7507cd1334 Mon Sep 17 00:00:00 2001 From: Kenrick Date: Tue, 10 Sep 2024 14:15:52 +0800 Subject: [PATCH 4/4] refactor: simplify codes & test cases --- common/reviews/api/node-core-library.api.md | 8 +- libraries/node-core-library/src/Sort.ts | 114 ++--------- libraries/node-core-library/src/index.ts | 2 +- .../node-core-library/src/test/Sort.test.ts | 180 +++--------------- .../installManager/WorkspaceInstallManager.ts | 2 +- 5 files changed, 51 insertions(+), 255 deletions(-) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 502e330a150..b1283bb5536 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -608,12 +608,6 @@ export interface IRunWithRetriesOptions { retryDelayMs?: number; } -// @public -export interface ISortKeysOptions { - compare?: (x: string, y: string) => number; - deep?: boolean; -} - // @public export interface IStringBuilder { append(text: string): void; @@ -837,7 +831,7 @@ export class Sort { static isSorted(collection: Iterable, comparer?: (x: any, y: any) => number): boolean; static isSortedBy(collection: Iterable, keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): boolean; static sortBy(array: T[], keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): void; - static sortKeys> | unknown[]>(object: T, options?: ISortKeysOptions): T; + static sortKeys> | unknown[]>(object: T): T; static sortMapKeys(map: Map, keyComparer?: (x: K, y: K) => number): void; static sortSet(set: Set, comparer?: (x: T, y: T) => number): void; static sortSetBy(set: Set, keySelector: (element: T) => any, keyComparer?: (x: T, y: T) => number): void; diff --git a/libraries/node-core-library/src/Sort.ts b/libraries/node-core-library/src/Sort.ts index 1d340836241..8994db1fb39 100644 --- a/libraries/node-core-library/src/Sort.ts +++ b/libraries/node-core-library/src/Sort.ts @@ -1,39 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -/** - * Options of {@link Sort.sortKeys} - * - * @public - */ -export interface ISortKeysOptions { - /** - * Whether or not to recursively sort keys, both in objects and arrays - * @defaultValue false - */ - deep?: boolean; - - /** - * Custom compare function when sorting the keys - * - * @defaultValue Sort.compareByValue - * @param x - Key name - * @param y - Key name - * @returns -1 if `x` is smaller than `y`, 1 if `x` is greater than `y`, or 0 if the values are equal. - */ - compare?: (x: string, y: string) => number; -} - -interface ISortKeysContext { - cache: SortKeysCache; - options: ISortKeysOptions; -} - -type SortKeysCache = WeakMap< - Partial> | unknown[], - Partial> | unknown[] ->; - /** * Operations for sorting collections. * @@ -266,10 +233,11 @@ export class Sort { } /** - * Sort the keys given in an object + * Sort the keys deeply given an object or an array. + * + * Doesn't handle cyclic reference. * * @param object - The object to be sorted - * @param options - The options for sort keys * * @example * @@ -277,25 +245,12 @@ export class Sort { * console.log(Sort.sortKeys({ c: 3, b: 2, a: 1 })); // { a: 1, b: 2, c: 3} * ``` */ - public static sortKeys> | unknown[]>( - object: T, - options: ISortKeysOptions = { - deep: false, - compare: Sort.compareByValue - } - ): T { + public static sortKeys> | unknown[]>(object: T): T { if (!isPlainObject(object) && !Array.isArray(object)) { throw new TypeError(`Expected object or array`); } - const cache: SortKeysCache = new WeakMap(); - const context: ISortKeysContext = { - cache, - options - }; - return Array.isArray(object) - ? (innerSortArray(object, context) as T) - : (innerSortKeys(object, context) as T); + return Array.isArray(object) ? (innerSortArray(object) as T) : (innerSortKeys(object) as T); } } @@ -303,63 +258,32 @@ function isPlainObject(obj: unknown): obj is object { return obj !== null && typeof obj === 'object'; } -function innerSortArray(arr: unknown[], context: ISortKeysContext): unknown[] { - const resultFromCache: undefined | Partial> | unknown[] = context.cache.get(arr); - if (resultFromCache !== undefined) { - return resultFromCache as unknown[]; - } +function innerSortArray(arr: unknown[]): unknown[] { const result: unknown[] = []; - context.cache.set(arr, result); - if (context.options.deep) { - for (const entry of arr) { - if (Array.isArray(entry)) { - result.push(innerSortArray(entry, context)); - } else if (isPlainObject(entry)) { - result.push(innerSortKeys(entry, context)); - } else { - result.push(entry); - } - } - } else { - for (const entry of arr) { + for (const entry of arr) { + if (Array.isArray(entry)) { + result.push(innerSortArray(entry)); + } else if (isPlainObject(entry)) { + result.push(innerSortKeys(entry)); + } else { result.push(entry); } } - return result; } -function innerSortKeys( - obj: Partial>, - context: ISortKeysContext -): Partial> { - const resultFromCache: undefined | Partial> | unknown[] = context.cache.get(obj); - if (resultFromCache !== undefined) { - return resultFromCache as Partial>; - } +function innerSortKeys(obj: Partial>): Partial> { const result: Partial> = {}; - const keys: string[] = Object.keys(obj).sort(context.options.compare); - - context.cache.set(obj, result); - + const keys: string[] = Object.keys(obj).sort(); for (const key of keys) { const value: unknown = obj[key]; - let newValue: unknown; - if (context.options.deep) { - if (Array.isArray(value)) { - newValue = innerSortArray(value, context); - } else if (isPlainObject(value)) { - newValue = innerSortKeys(value, context); - } else { - newValue = value; - } + if (Array.isArray(value)) { + result[key] = innerSortArray(value); + } else if (isPlainObject(value)) { + result[key] = innerSortKeys(value); } else { - newValue = value; + result[key] = value; } - Object.defineProperty(result, key, { - ...Object.getOwnPropertyDescriptor(obj, key), - value: newValue - }); } return result; diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 19b7ebd8057..70396912fbc 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -93,7 +93,7 @@ export { type IPathFormatConciselyOptions } from './Path'; export { Encoding, Text, NewlineKind, type IReadLinesFromIterableOptions } from './Text'; -export { Sort, type ISortKeysOptions } from './Sort'; +export { Sort } from './Sort'; export { AlreadyExistsBehavior, FileSystem, diff --git a/libraries/node-core-library/src/test/Sort.test.ts b/libraries/node-core-library/src/test/Sort.test.ts index c252b23b299..b87eef16a37 100644 --- a/libraries/node-core-library/src/test/Sort.test.ts +++ b/libraries/node-core-library/src/test/Sort.test.ts @@ -71,170 +71,48 @@ test('Sort.sortSet', () => { }); describe('Sort.sortKeys', () => { - // Test cases from https://github.com/sindresorhus/sort-keys/blob/v4.2.0/test.js - function deepEqualInOrder( - actual: Partial>, - expected: Partial> - ): void { - expect(actual).toEqual(expected); - - const seen: Set = new Set(); - - function assertSameKeysInOrder(object1: unknown, object2: unknown): void { - // This function assumes the objects given are already deep equal. - - if (seen.has(object1) && seen.has(object2)) { - return; - } - - seen.add(object1); - seen.add(object2); - - if (Array.isArray(object1) && Array.isArray(object2)) { - for (const index of object1.keys()) { - assertSameKeysInOrder(object1[index], object2[index]); - } - } else if ( - typeof object1 === 'object' && - typeof object2 === 'object' && - object1 != null && - object2 != null - ) { - const keys1 = Object.keys(object1); - const keys2 = Object.keys(object2); - expect(keys1).toEqual(keys2); - for (const index of keys1.keys()) { - assertSameKeysInOrder( - (object1 as Partial>)[keys1[index]], - (object2 as Partial>)[keys2[index]] - ); - } - } - } - - assertSameKeysInOrder(actual, expected); - } - - test('sort the keys of an object', () => { - const unsortedObj = { c: 0, a: 0, b: 0 }; + test('Simple object', () => { + const unsortedObj = { q: 0, p: 0, r: 0 }; const sortedObj = Sort.sortKeys(unsortedObj); + // Assert that it's not sorted in-place expect(sortedObj).not.toBe(unsortedObj); - deepEqualInOrder(unsortedObj, { c: 0, a: 0, b: 0 }); - deepEqualInOrder(sortedObj, { a: 0, b: 0, c: 0 }); - }); - test('custom compare function', () => { - const compare: (a: string, b: string) => number = (a: string, b: string) => b.localeCompare(a); - deepEqualInOrder(Sort.sortKeys({ c: 0, a: 0, b: 0 }, { compare }), { c: 0, b: 0, a: 0 }); + expect(Object.keys(unsortedObj)).toEqual(['q', 'p', 'r']); + expect(Object.keys(sortedObj)).toEqual(['p', 'q', 'r']); }); - - test('deep option', () => { - deepEqualInOrder(Sort.sortKeys({ c: { c: 0, a: 0, b: 0 }, a: 0, b: 0 }, { deep: true }), { - a: 0, - b: 0, - c: { a: 0, b: 0, c: 0 } - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object: Partial> = { z: 0 }; - object.circular = object; - const sortedObject = Sort.sortKeys(object, { deep: true }); + test('Simple array with objects', () => { + const unsortedArr = [ + { b: 1, a: 0 }, + { y: 0, z: 1, x: 2 } + ]; + const sortedArr = Sort.sortKeys(unsortedArr); // Assert that it's not sorted in-place - expect(sortedObject).not.toBe(object); - expect(Object.keys(object)).toEqual(['z', 'circular']); - - // Assert that circular value references the same thing - expect(sortedObject).toBe(sortedObject.circular); - expect(Object.keys(sortedObject)).toEqual(['circular', 'z']); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object1: Partial> = { b: 0 }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object2: Partial> = { d: 0 }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object3: Partial> = { a: [{ b: 0 }] }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object4: Partial> = { a: [{ d: 0 }] }; - - object1.a = object2; - object2.c = object1; - object3.a[0].a = object4.a[0]; - object4.a[0].c = object3.a[0]; - - const sortedObject1 = Sort.sortKeys(object1, { deep: true }); - const sortedObject3 = Sort.sortKeys(object3, { deep: true }); - - expect(sortedObject1).toBe(sortedObject1.a.c); - deepEqualInOrder(sortedObject3.a[0], sortedObject3.a[0].a.c); - expect(Object.keys(sortedObject1)).toStrictEqual(['a', 'b']); - expect(Object.keys(sortedObject3.a[0])).toStrictEqual(['a', 'b']); - deepEqualInOrder( - Sort.sortKeys({ c: { c: 0, a: 0, b: 0 }, a: 0, b: 0, z: [9, 8, 7, 6, 5] }, { deep: true }), - { a: 0, b: 0, c: { a: 0, b: 0, c: 0 }, z: [9, 8, 7, 6, 5] } - ); - expect(Object.keys(Sort.sortKeys({ a: [{ b: 0, a: 0 }] }, { deep: true }).a[0])).toEqual(['a', 'b']); - }); + expect(sortedArr).not.toBe(unsortedArr); - test('deep arrays', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object: Partial> = { - b: 0, - a: [{ b: 0, a: 0 }, [{ b: 0, a: 0 }]] - }; - object.a.push(object); - object.a[1].push(object.a[1]); - - const sorted = Sort.sortKeys(object, { deep: true }); - // Cannot use .toBe() as Jest will encounter https://github.com/jestjs/jest/issues/10577 - expect(sorted.a[2] === sorted).toBeTruthy(); - expect(sorted.a[1][1] === sorted.a[1]).toBeTruthy(); - expect(Object.keys(sorted)).toEqual(['a', 'b']); - expect(Object.keys(sorted.a[0])).toEqual(['a', 'b']); - expect(Object.keys(sorted.a[1][0])).toEqual(['a', 'b']); - }); + expect(Object.keys(unsortedArr[0])).toEqual(['b', 'a']); + expect(Object.keys(sortedArr[0])).toEqual(['a', 'b']); - test('top-level array', () => { - const array: Array>> = [ - { b: 0, a: 0 }, - { c: 0, d: 0 } - ]; - const sorted = Sort.sortKeys(array); - expect(sorted).not.toBe(array); - expect(sorted[0]).toBe(array[0]); - expect(sorted[1]).toBe(array[1]); - - const deepSorted = Sort.sortKeys(array, { deep: true }); - expect(deepSorted).not.toBe(array); - expect(deepSorted[0]).not.toBe(array[0]); - expect(deepSorted[1]).not.toBe(array[1]); - expect(Object.keys(deepSorted[0])).toEqual(['a', 'b']); - expect(Object.keys(deepSorted[1])).toEqual(['c', 'd']); + expect(Object.keys(unsortedArr[1])).toEqual(['y', 'z', 'x']); + expect(Object.keys(sortedArr[1])).toEqual(['x', 'y', 'z']); }); + test('Nested objects', () => { + const unsortedDeepObj = { c: { q: 0, r: { a: 42 }, p: 2 }, b: { y: 0, z: 1, x: 2 }, a: 2 }; + const sortedDeepObj = Sort.sortKeys(unsortedDeepObj); - test('keeps property descriptors intact', () => { - const descriptors: PropertyDescriptorMap = { - b: { - value: 1, - configurable: true, - enumerable: true, - writable: false - }, - a: { - value: 2, - configurable: false, - enumerable: true, - writable: true - } - }; + expect(sortedDeepObj).not.toBe(unsortedDeepObj); + + expect(Object.keys(unsortedDeepObj)).toEqual(['c', 'b', 'a']); + expect(Object.keys(sortedDeepObj)).toEqual(['a', 'b', 'c']); - const object: Partial> = {}; - Object.defineProperties(object, descriptors); + expect(Object.keys(unsortedDeepObj.b)).toEqual(['y', 'z', 'x']); + expect(Object.keys(sortedDeepObj.b)).toEqual(['x', 'y', 'z']); - const sorted = Sort.sortKeys(object); + expect(Object.keys(unsortedDeepObj.c)).toEqual(['q', 'r', 'p']); + expect(Object.keys(sortedDeepObj.c)).toEqual(['p', 'q', 'r']); - deepEqualInOrder(sorted, { a: 2, b: 1 }); - expect(Object.getOwnPropertyDescriptors(sorted)).toEqual(descriptors); + expect(Object.keys(unsortedDeepObj.c.r)).toEqual(['a']); + expect(Object.keys(sortedDeepObj.c.r)).toEqual(['a']); }); }); diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 117dd9bbfa9..3ce4aa5f65c 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -777,6 +777,6 @@ export class WorkspaceInstallManager extends BaseInstallManager { * @returns */ function createObjectChecksum(obj: Record): string { - const s: string = JSON.stringify(Sort.sortKeys(obj, { deep: true })); + const s: string = JSON.stringify(Sort.sortKeys(obj)); return createHash('md5').update(s).digest('hex'); }