diff --git a/common/changes/@microsoft/rush/chao-support-injected-settings_2023-12-13-18-48.json b/common/changes/@microsoft/rush/chao-support-injected-settings_2023-12-13-18-48.json new file mode 100644 index 00000000000..fc4aa1ada01 --- /dev/null +++ b/common/changes/@microsoft/rush/chao-support-injected-settings_2023-12-13-18-48.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix an issue where Rush does not detect changes to the `dependenciesMeta` field in project's `package.json` files, so may incorrectly skip updating/installation.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/chao-support-injected-settings_2023-12-13-18-48.json b/common/changes/@rushstack/node-core-library/chao-support-injected-settings_2023-12-13-18-48.json new file mode 100644 index 00000000000..bf7349ae78d --- /dev/null +++ b/common/changes/@rushstack/node-core-library/chao-support-injected-settings_2023-12-13-18-48.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add the `dependenciesMeta` property to the `INodePackageJson` interface.", + "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 a7f79e8ff1f..be999445c6b 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -347,6 +347,14 @@ export interface IConsoleTerminalProviderOptions { verboseEnabled: boolean; } +// @public +export interface IDependenciesMetaTable { + // (undocumented) + [dependencyName: string]: { + injected?: boolean; + }; +} + // @beta export interface IDynamicPrefixProxyTerminalProviderOptions extends IPrefixProxyTerminalProviderOptionsBase { getPrefix: () => string; @@ -552,6 +560,7 @@ export class Import { export interface INodePackageJson { bin?: string; dependencies?: IPackageJsonDependencyTable; + dependenciesMeta?: IDependenciesMetaTable; description?: string; devDependencies?: IPackageJsonDependencyTable; homepage?: string; diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 787661927d8..7e4e65cb00c 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -903,6 +903,15 @@ export class PackageJsonDependency { get version(): string; } +// @public (undocumented) +export class PackageJsonDependencyMeta { + constructor(name: string, injected: boolean, onChange: () => void); + // (undocumented) + get injected(): boolean; + // (undocumented) + readonly name: string; +} + // @public (undocumented) export class PackageJsonEditor { // @internal @@ -910,6 +919,7 @@ export class PackageJsonEditor { // (undocumented) addOrUpdateDependency(packageName: string, newVersion: string, dependencyType: DependencyType): void; get dependencyList(): ReadonlyArray; + get dependencyMetaList(): ReadonlyArray; get devDependencyList(): ReadonlyArray; // (undocumented) readonly filePath: string; diff --git a/libraries/node-core-library/src/IPackageJson.ts b/libraries/node-core-library/src/IPackageJson.ts index ab33d9a6f6f..b941bab1db5 100644 --- a/libraries/node-core-library/src/IPackageJson.ts +++ b/libraries/node-core-library/src/IPackageJson.ts @@ -60,6 +60,17 @@ export interface IPeerDependenciesMetaTable { }; } +/** + * This interface is part of the {@link IPackageJson} file format. It is used for the + * "dependenciesMeta" field. + * @public + */ +export interface IDependenciesMetaTable { + [dependencyName: string]: { + injected?: boolean; + }; +} + /** * An interface for accessing common fields from a package.json file whose version field may be missing. * @@ -166,6 +177,12 @@ export interface INodePackageJson { */ peerDependencies?: IPackageJsonDependencyTable; + /** + * An array of metadata for dependencies declared inside dependencies, optionalDependencies, and devDependencies. + * https://pnpm.io/package_json#dependenciesmeta + */ + dependenciesMeta?: IDependenciesMetaTable; + /** * An array of metadata about peer dependencies. */ diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 2da14b56ddb..66589e27de1 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -34,7 +34,8 @@ export { IPackageJsonDependencyTable, IPackageJsonScriptTable, IPackageJsonRepository, - IPeerDependenciesMetaTable + IPeerDependenciesMetaTable, + IDependenciesMetaTable } from './IPackageJson'; export { Import, diff --git a/libraries/rush-lib/src/api/PackageJsonEditor.ts b/libraries/rush-lib/src/api/PackageJsonEditor.ts index 4e2651d3d82..f09c2a1e0fe 100644 --- a/libraries/rush-lib/src/api/PackageJsonEditor.ts +++ b/libraries/rush-lib/src/api/PackageJsonEditor.ts @@ -46,6 +46,26 @@ export class PackageJsonDependency { } } +/** + * @public + */ +export class PackageJsonDependencyMeta { + private _injected: boolean; + private _onChange: () => void; + + public readonly name: string; + + public constructor(name: string, injected: boolean, onChange: () => void) { + this.name = name; + this._injected = injected; + this._onChange = onChange; + } + + public get injected(): boolean { + return this._injected; + } +} + /** * @public */ @@ -57,6 +77,8 @@ export class PackageJsonEditor { // and "peerDependencies" are mutually exclusive, but "devDependencies" is not. private readonly _devDependencies: Map; + private readonly _dependenciesMeta: Map; + // NOTE: The "resolutions" field is a yarn specific feature that controls package // resolution override within yarn. private readonly _resolutions: Map; @@ -76,6 +98,7 @@ export class PackageJsonEditor { this._dependencies = new Map(); this._devDependencies = new Map(); this._resolutions = new Map(); + this._dependenciesMeta = new Map(); const dependencies: { [key: string]: string } = data.dependencies || {}; const optionalDependencies: { [key: string]: string } = data.optionalDependencies || {}; @@ -84,6 +107,8 @@ export class PackageJsonEditor { const devDependencies: { [key: string]: string } = data.devDependencies || {}; const resolutions: { [key: string]: string } = data.resolutions || {}; + const dependenciesMeta: { [key: string]: { [key: string]: boolean } } = data.dependenciesMeta || {}; + const _onChange: () => void = this._onChange.bind(this); try { @@ -155,6 +180,13 @@ export class PackageJsonEditor { ); }); + Object.keys(dependenciesMeta || {}).forEach((packageName: string) => { + this._dependenciesMeta.set( + packageName, + new PackageJsonDependencyMeta(packageName, dependenciesMeta[packageName].injected, _onChange) + ); + }); + // (Do not sort this._resolutions because order may be significant; the RFC is unclear about that.) Sort.sortMapKeys(this._dependencies); Sort.sortMapKeys(this._devDependencies); @@ -193,6 +225,13 @@ export class PackageJsonEditor { return [...this._devDependencies.values()]; } + /** + * The list of dependenciesMeta in package.json. + */ + public get dependencyMetaList(): ReadonlyArray { + return [...this._dependenciesMeta.values()]; + } + /** * This field is a Yarn-specific feature that allows overriding of package resolution. * diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index 9e4b00aeb1b..6f7415cde29 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -73,7 +73,12 @@ export { ApprovedPackagesItem, ApprovedPackagesConfiguration } from './api/Appro export { CommonVersionsConfiguration } from './api/CommonVersionsConfiguration'; -export { PackageJsonEditor, PackageJsonDependency, DependencyType } from './api/PackageJsonEditor'; +export { + PackageJsonEditor, + PackageJsonDependency, + DependencyType, + PackageJsonDependencyMeta +} from './api/PackageJsonEditor'; export { RepoStateFile } from './logic/RepoStateFile'; diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 18908a54d45..eaf9f3dcae2 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -4,13 +4,23 @@ import colors from 'colors/safe'; import * as path from 'path'; import * as semver from 'semver'; -import { FileSystem, FileConstants, AlreadyReportedError, Async } from '@rushstack/node-core-library'; +import { + FileSystem, + FileConstants, + AlreadyReportedError, + Async, + type IDependenciesMetaTable +} from '@rushstack/node-core-library'; import { BaseInstallManager } from '../base/BaseInstallManager'; import type { IInstallManagerOptions } from '../base/BaseInstallManagerTypes'; import type { BaseShrinkwrapFile } from '../../logic/base/BaseShrinkwrapFile'; import { DependencySpecifier, DependencySpecifierType } from '../DependencySpecifier'; -import { type PackageJsonEditor, DependencyType } from '../../api/PackageJsonEditor'; +import { + type PackageJsonEditor, + DependencyType, + type PackageJsonDependencyMeta +} from '../../api/PackageJsonEditor'; import { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { RushConstants } from '../../logic/RushConstants'; @@ -23,6 +33,8 @@ import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory'; import { BaseProjectShrinkwrapFile } from '../base/BaseProjectShrinkwrapFile'; import { type CustomTipId, type ICustomTipInfo, PNPM_CUSTOM_TIPS } from '../../api/CustomTipsConfiguration'; +import type { PnpmShrinkwrapFile } from '../pnpm/PnpmShrinkwrapFile'; +import { objectsAreDeepEqual } from '../../utilities/objectUtilities'; /** * This class implements common logic between "rush install" and "rush update". @@ -56,7 +68,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { * @override */ protected async prepareCommonTempAsync( - shrinkwrapFile: BaseShrinkwrapFile | undefined + shrinkwrapFile: (PnpmShrinkwrapFile & BaseShrinkwrapFile) | undefined ): Promise<{ shrinkwrapIsUpToDate: boolean; shrinkwrapWarnings: string[] }> { // Block use of the RUSH_TEMP_FOLDER environment variable if (EnvironmentConfiguration.rushTempFolderOverride !== undefined) { @@ -235,6 +247,56 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } + // For pnpm pacakge manager, we need to handle dependenciesMeta changes in package.json. See more: https://pnpm.io/package_json#dependenciesmeta + // If dependenciesMeta settings is different between package.json and pnpm-lock.yaml, then shrinkwrapIsUpToDate return false. + if (this.rushConfiguration.packageManager === 'pnpm') { + // First, build a object for dependenciesMeta settings in package.json + // key is the package path, value is the dependenciesMeta info for that package + const packagePathToDependenciesMetaInPackageJson: { [key: string]: IDependenciesMetaTable } = {}; + const commonTempFolder: string = this.rushConfiguration.commonTempFolder; + const rushJsonFolder: string = this.rushConfiguration.rushJsonFolder; + + // get the relative path from common temp folder to repo root folder + const relativeFromTempFolderToRootFolder: string = path.relative(commonTempFolder, rushJsonFolder); + for (const rushProject of this.rushConfiguration.projects) { + const packageJson: PackageJsonEditor = rushProject.packageJsonEditor; + const projectRelativeFolder: string = rushProject.projectRelativeFolder; + + // get the relative path from common temp folder to package folder, to align with the value in pnpm-lock.yaml + const relativePathFromTempFolderToPackageFolder: string = + relativeFromTempFolderToRootFolder + '/' + projectRelativeFolder; + const dependencyMetaList: ReadonlyArray = packageJson.dependencyMetaList; + + if (dependencyMetaList.length !== 0) { + const dependenciesMeta: IDependenciesMetaTable = {}; + for (const dependencyMeta of dependencyMetaList) { + dependenciesMeta[dependencyMeta.name] = { + injected: dependencyMeta.injected + }; + } + packagePathToDependenciesMetaInPackageJson[relativePathFromTempFolderToPackageFolder] = + dependenciesMeta; + } + } + + // Second, build a object for dependenciesMeta settings in pnpm-lock.yaml + // key is the package path, value is the dependenciesMeta info for that package + const packagePathToDependenciesMetaInShrinkwrapFile: { [key: string]: IDependenciesMetaTable } = {}; + if (shrinkwrapFile?.importers !== undefined) { + for (const [key, value] of shrinkwrapFile?.importers) { + if (value.dependenciesMeta !== undefined) { + packagePathToDependenciesMetaInShrinkwrapFile[key] = value.dependenciesMeta; + } + } + } + + // Now, we compare these two objects to see if they are equal or not + shrinkwrapIsUpToDate = objectsAreDeepEqual( + packagePathToDependenciesMetaInPackageJson, + packagePathToDependenciesMetaInShrinkwrapFile + ); + } + // Write the common package.json InstallHelpers.generateCommonPackageJson(this.rushConfiguration); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index eae53c03c8c..b470a423faf 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -33,6 +33,9 @@ const yamlModule: typeof import('js-yaml') = Import.lazy('js-yaml', require); export interface IPeerDependenciesMetaYaml { optional?: boolean; } +export interface IDependenciesMetaYaml { + injected?: boolean; +} export type IPnpmV7VersionSpecifier = string; export interface IPnpmV8VersionSpecifier { @@ -69,6 +72,8 @@ export interface IPnpmShrinkwrapImporterYaml { devDependencies?: Record; /** The list of resolved version numbers for optional dependencies */ optionalDependencies?: Record; + /** The list of metadata for dependencies declared inside dependencies, optionalDependencies, and devDependencies. */ + dependenciesMeta?: Record; /** * The list of specifiers used to resolve dependency versions * diff --git a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap index e97ba5ad872..27342158e6b 100644 --- a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap +++ b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap @@ -33,6 +33,7 @@ Loaded @microsoft/rush-lib from process.env._RUSH_LIB_PATH 'Operation', 'OperationStatus', 'PackageJsonDependency', + 'PackageJsonDependencyMeta', 'PackageJsonEditor', 'PackageManager', 'PackageManagerOptionsConfigurationBase',