Skip to content

Commit

Permalink
[rush-lib] fix: update shrinkwrap when globalPackageExtensions has be…
Browse files Browse the repository at this point in the history
…en changed (#4913)

* fix: update shrinkwrap when globalPackageExtensions has been changed

* fix: code review

* fix: code review

* refactor: simplify codes & test cases
  • Loading branch information
kenrick95 authored Sep 12, 2024
1 parent e9140b6 commit c6b5e1d
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Always update shrinkwrap when `globalPackageExtensions` in `common/config/rush/pnpm-config.json` has been changed.",
"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 a `Sort.sortKeys` function for sorting keys in an object",
"type": "minor"
}
],
"packageName": "@rushstack/node-core-library"
}
1 change: 1 addition & 0 deletions common/reviews/api/node-core-library.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,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): 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
56 changes: 56 additions & 0 deletions libraries/node-core-library/src/Sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,60 @@ export class Sort {
set.add(item);
}
}

/**
* Sort the keys deeply given an object or an array.
*
* Doesn't handle cyclic reference.
*
* @param object - The object to be sorted
*
* @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): T {
if (!isPlainObject(object) && !Array.isArray(object)) {
throw new TypeError(`Expected object or array`);
}

return Array.isArray(object) ? (innerSortArray(object) as T) : (innerSortKeys(object) as T);
}
}

function isPlainObject(obj: unknown): obj is object {
return obj !== null && typeof obj === 'object';
}

function innerSortArray(arr: unknown[]): unknown[] {
const result: unknown[] = [];
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<Record<string, unknown>>): Partial<Record<string, unknown>> {
const result: Partial<Record<string, unknown>> = {};
const keys: string[] = Object.keys(obj).sort();
for (const key of keys) {
const value: unknown = obj[key];
if (Array.isArray(value)) {
result[key] = innerSortArray(value);
} else if (isPlainObject(value)) {
result[key] = innerSortKeys(value);
} else {
result[key] = value;
}
}

return result;
}
47 changes: 47 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,50 @@ test('Sort.sortSet', () => {
Sort.sortSet(set);
expect(Array.from(set)).toEqual(['aardvark', 'goose', 'zebra']);
});

describe('Sort.sortKeys', () => {
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);

expect(Object.keys(unsortedObj)).toEqual(['q', 'p', 'r']);
expect(Object.keys(sortedObj)).toEqual(['p', 'q', 'r']);
});
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(sortedArr).not.toBe(unsortedArr);

expect(Object.keys(unsortedArr[0])).toEqual(['b', 'a']);
expect(Object.keys(sortedArr[0])).toEqual(['a', 'b']);

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);

expect(sortedDeepObj).not.toBe(unsortedDeepObj);

expect(Object.keys(unsortedDeepObj)).toEqual(['c', 'b', 'a']);
expect(Object.keys(sortedDeepObj)).toEqual(['a', 'b', 'c']);

expect(Object.keys(unsortedDeepObj.b)).toEqual(['y', 'z', 'x']);
expect(Object.keys(sortedDeepObj.b)).toEqual(['x', 'y', 'z']);

expect(Object.keys(unsortedDeepObj.c)).toEqual(['q', 'r', 'p']);
expect(Object.keys(sortedDeepObj.c)).toEqual(['p', 'q', 'r']);

expect(Object.keys(unsortedDeepObj.c.r)).toEqual(['a']);
expect(Object.keys(sortedDeepObj.c.r)).toEqual(['a']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -388,6 +402,18 @@ export class WorkspaceInstallManager extends BaseInstallManager {
return { shrinkwrapIsUpToDate, shrinkwrapWarnings };
}

private _getPackageExtensionChecksum(
packageExtensions: Record<string, unknown> | 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!);

return packageExtensionsChecksum;
}

protected canSkipInstall(lastModifiedDate: Date, subspace: Subspace): boolean {
if (!super.canSkipInstall(lastModifiedDate, subspace)) {
return false;
Expand Down Expand Up @@ -744,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, unknown>): string {
const s: string = JSON.stringify(Sort.sortKeys(obj));
return createHash('md5').update(s).digest('hex');
}
4 changes: 4 additions & 0 deletions libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ export interface IPnpmShrinkwrapYaml {
specifiers: Record<string, string>;
/** 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 {
Expand Down Expand Up @@ -275,6 +277,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
public readonly specifiers: ReadonlyMap<string, string>;
public readonly packages: ReadonlyMap<string, IPnpmShrinkwrapDependencyYaml>;
public readonly overrides: ReadonlyMap<string, string>;
public readonly packageExtensionsChecksum: undefined | string;

private readonly _shrinkwrapJson: IPnpmShrinkwrapYaml;
private readonly _integrities: Map<string, Map<string, string>>;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit c6b5e1d

Please sign in to comment.