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 all 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` 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 @@ -831,6 +831,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
*
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
* @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 {
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
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(
kenrick95 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading