зеркало из https://github.com/microsoft/rushstack.git
[rush-lib] fix: update shrinkwrap when globalPackageExtensions has been changed (#4913)
* fix: update shrinkwrap when globalPackageExtensions has been changed * fix: code review * fix: code review * refactor: simplify codes & test cases
This commit is contained in:
Родитель
e9140b6202
Коммит
c6b5e1d5d6
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,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;
|
||||
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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>>;
|
||||
|
@ -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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче