Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rush-lib] fix: update shrinkwrap when globalPackageExtensions has been changed #4913

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Always update shrinkwrap when globalPackageExtensions has been changed",
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/node-core-library",
"comment": "Add Sort.sortKeys for sorting keys in an object",
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
"type": "minor"
}
],
"packageName": "@rushstack/node-core-library"
}
7 changes: 7 additions & 0 deletions common/reviews/api/node-core-library.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,12 @@ export interface IRunWithRetriesOptions<TResult> {
retryDelayMs?: number;
}

// @public
export interface ISortKeysOptions {
compare?: (x: string, y: string) => number;
deep?: boolean;
}

// @public
export interface IStringBuilder {
append(text: string): void;
Expand Down Expand Up @@ -831,6 +837,7 @@ export class Sort {
static isSorted<T>(collection: Iterable<T>, comparer?: (x: any, y: any) => number): boolean;
static isSortedBy<T>(collection: Iterable<T>, keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): boolean;
static sortBy<T>(array: T[], keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): void;
static sortKeys<T extends Partial<Record<string, unknown>> | unknown[]>(object: T, options?: ISortKeysOptions): T;
static sortMapKeys<K, V>(map: Map<K, V>, keyComparer?: (x: K, y: K) => number): void;
static sortSet<T>(set: Set<T>, comparer?: (x: T, y: T) => number): void;
static sortSetBy<T>(set: Set<T>, keySelector: (element: T) => any, keyComparer?: (x: T, y: T) => number): void;
Expand Down
128 changes: 128 additions & 0 deletions libraries/node-core-library/src/Sort.ts
Original file line number Diff line number Diff line change
@@ -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;
/**
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
* 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<Record<string, unknown>> | unknown[],
Partial<Record<string, unknown>> | unknown[]
>;

/**
* Operations for sorting collections.
*
Expand Down Expand Up @@ -231,4 +263,100 @@ export class Sort {
set.add(item);
}
}

/**
* Sort the keys given in an object
*
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
* @param object - The object to be sorted
* @param options - The options for sort keys
*
* @example
*
* ```ts
* console.log(Sort.sortKeys({ c: 3, b: 2, a: 1 })); // { a: 1, b: 2, c: 3}
* ```
*/
public static sortKeys<T extends Partial<Record<string, unknown>> | unknown[]>(
object: T,
options: ISortKeysOptions = {
deep: false,
compare: Sort.compareByValue
}
): T {
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
function isPlainObject(obj: unknown): obj is object {
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
return obj !== null && typeof obj === 'object';
}

function innerSortArray(arr: unknown[], context: ISortKeysContext): unknown[] {
const resultFromCache: undefined | Partial<Record<string, unknown>> | 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) => {
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
if (Array.isArray(entry)) {
return innerSortArray(entry, context);
} else if (isPlainObject(entry)) {
return innerSortKeys(entry, context);
}
return entry;
})
);
} else {
result.push(...arr);
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
}

return result;
}
function innerSortKeys(
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
obj: Partial<Record<string, unknown>>,
context: ISortKeysContext
): Partial<Record<string, unknown>> {
const resultFromCache: undefined | Partial<Record<string, unknown>> | unknown[] = context.cache.get(obj);
if (resultFromCache !== undefined) {
return resultFromCache as Partial<Record<string, unknown>>;
}
const result: Partial<Record<string, unknown>> = {};
const keys: string[] = Object.keys(obj).sort(context.options.compare);

context.cache.set(obj, result);

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;
}
} else {
newValue = value;
}
Object.defineProperty(result, key, {
...Object.getOwnPropertyDescriptor(obj, key),
value: newValue
});
}

return result;
}
2 changes: 1 addition & 1 deletion libraries/node-core-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
189 changes: 189 additions & 0 deletions libraries/node-core-library/src/test/Sort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,192 @@ 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
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
actual: Partial<Record<string, any>>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expected: Partial<Record<string, any>>
): void {
expect(actual).toEqual(expected);

const seen = new Set();

function assertSameKeysInOrder(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object1: Partial<Record<string, any>>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object2: Partial<Record<string, any>>
): 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', () => {
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', () => {
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<Record<string, any>> = { a: 0 };
object.circular = object;
Sort.sortKeys(object, { deep: true });
}).not.toThrow();
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object: Partial<Record<string, any>> = { z: 0 };
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']);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object1: Partial<Record<string, any>> = { b: 0 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object2: Partial<Record<string, any>> = { d: 0 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object3: Partial<Record<string, any>> = { a: [{ b: 0 }] };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object4: Partial<Record<string, any>> = { 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<Record<string, any>> = {
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<any> = [
{ 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<Record<string, any>> = {
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<Record<string, any>> = {};
Object.defineProperties(object, descriptors);

const sorted = Sort.sortKeys(object);

deepEqualInOrder(sorted, { a: 2, b: 1 });
expect(Object.getOwnPropertyDescriptors(sorted)).toEqual(descriptors);
});
});
Loading
Loading