From 9b453e47ba5d49f70c512deed91424dbc3d14e7b Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 23 Dec 2025 17:15:40 -0500 Subject: [PATCH 1/3] Fix an erroneous 'The shrinkwrap file has not been updated to support workspaces...' error. --- .../rush/fix-5439_2025-12-23-22-15.json | 10 +++ .../src/logic/ProjectChangeAnalyzer.ts | 11 ++- libraries/rush-lib/src/logic/RepoStateFile.ts | 3 +- .../src/logic/ShrinkwrapFileFactory.ts | 33 ++++--- .../src/logic/base/BaseInstallManager.ts | 24 ++--- .../installManager/WorkspaceInstallManager.ts | 9 +- .../src/logic/pnpm/PnpmLinkManager.ts | 17 +++- .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 43 ++++++--- .../pnpm/test/PnpmShrinkwrapFile.test.ts | 48 ++++++---- .../src/logic/policy/ShrinkwrapFilePolicy.ts | 9 +- .../src/logic/test/ShrinkwrapFile.test.ts | 87 +++++++++++++++---- .../workspace-pnpm-lock-no-projects-v9.yaml | 9 ++ 12 files changed, 221 insertions(+), 82 deletions(-) create mode 100644 common/changes/@microsoft/rush/fix-5439_2025-12-23-22-15.json create mode 100644 libraries/rush-lib/src/logic/test/shrinkwrapFile/workspace-pnpm-lock-no-projects-v9.yaml diff --git a/common/changes/@microsoft/rush/fix-5439_2025-12-23-22-15.json b/common/changes/@microsoft/rush/fix-5439_2025-12-23-22-15.json new file mode 100644 index 00000000000..a4c6e0d9e63 --- /dev/null +++ b/common/changes/@microsoft/rush/fix-5439_2025-12-23-22-15.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix an issue where `rush update` will error complaining that the shrinkwrap file hasn't been updated to support workspaces in a subspace with no projects.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts index 65d79ac0872..fdb021acb2a 100644 --- a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -167,8 +167,11 @@ export class ProjectChangeAnalyzer { } if (rushConfiguration.isPnpm) { - const currentShrinkwrap: PnpmShrinkwrapFile | undefined = - PnpmShrinkwrapFile.loadFromFile(fullShrinkwrapPath); + const subspaceHasNoProjects: boolean = subspace.getProjects().length === 0; + const currentShrinkwrap: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( + fullShrinkwrapPath, + { subspaceHasNoProjects } + ); if (!currentShrinkwrap) { throw new Error(`Unable to obtain current shrinkwrap file.`); @@ -179,7 +182,9 @@ export class ProjectChangeAnalyzer { blobSpec: `${mergeCommit}:${relativeShrinkwrapFilePath}`, repositoryRoot: repoRoot }); - const oldShrinkWrap: PnpmShrinkwrapFile = PnpmShrinkwrapFile.loadFromString(oldShrinkwrapText); + const oldShrinkWrap: PnpmShrinkwrapFile = PnpmShrinkwrapFile.loadFromString(oldShrinkwrapText, { + subspaceHasNoProjects + }); for (const project of subspaceProjects) { if ( diff --git a/libraries/rush-lib/src/logic/RepoStateFile.ts b/libraries/rush-lib/src/logic/RepoStateFile.ts index 0bfd0f2e82e..0b7d907c7fd 100644 --- a/libraries/rush-lib/src/logic/RepoStateFile.ts +++ b/libraries/rush-lib/src/logic/RepoStateFile.ts @@ -181,7 +181,8 @@ export class RepoStateFile { rushConfiguration.pnpmOptions.preventManualShrinkwrapChanges; if (preventShrinkwrapChanges) { const pnpmShrinkwrapFile: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( - subspace.getCommittedShrinkwrapFilePath(variant) + subspace.getCommittedShrinkwrapFilePath(variant), + { subspaceHasNoProjects: subspace.getProjects().length === 0 } ); if (pnpmShrinkwrapFile) { diff --git a/libraries/rush-lib/src/logic/ShrinkwrapFileFactory.ts b/libraries/rush-lib/src/logic/ShrinkwrapFileFactory.ts index 1fdec16fab2..1372d8db6c9 100644 --- a/libraries/rush-lib/src/logic/ShrinkwrapFileFactory.ts +++ b/libraries/rush-lib/src/logic/ShrinkwrapFileFactory.ts @@ -7,32 +7,41 @@ import { NpmShrinkwrapFile } from './npm/NpmShrinkwrapFile'; import { PnpmShrinkwrapFile } from './pnpm/PnpmShrinkwrapFile'; import { YarnShrinkwrapFile } from './yarn/YarnShrinkwrapFile'; +export interface IShrinkwrapFileFactoryOptions { + packageManager: PackageManagerName; + subspaceHasNoProjects: boolean; +} + +export interface IGetShrinkwrapFileOptions extends IShrinkwrapFileFactoryOptions { + shrinkwrapFilePath: string; +} + +export interface IParseShrinkwrapFileOptions extends IShrinkwrapFileFactoryOptions { + shrinkwrapContent: string; +} + export class ShrinkwrapFileFactory { - public static getShrinkwrapFile( - packageManager: PackageManagerName, - shrinkwrapFilename: string - ): BaseShrinkwrapFile | undefined { + public static getShrinkwrapFile(options: IGetShrinkwrapFileOptions): BaseShrinkwrapFile | undefined { + const { packageManager, shrinkwrapFilePath, subspaceHasNoProjects } = options; switch (packageManager) { case 'npm': - return NpmShrinkwrapFile.loadFromFile(shrinkwrapFilename); + return NpmShrinkwrapFile.loadFromFile(shrinkwrapFilePath); case 'pnpm': - return PnpmShrinkwrapFile.loadFromFile(shrinkwrapFilename); + return PnpmShrinkwrapFile.loadFromFile(shrinkwrapFilePath, { subspaceHasNoProjects }); case 'yarn': - return YarnShrinkwrapFile.loadFromFile(shrinkwrapFilename); + return YarnShrinkwrapFile.loadFromFile(shrinkwrapFilePath); default: throw new Error(`Invalid package manager: ${packageManager}`); } } - public static parseShrinkwrapFile( - packageManager: PackageManagerName, - shrinkwrapContent: string - ): BaseShrinkwrapFile | undefined { + public static parseShrinkwrapFile(options: IParseShrinkwrapFileOptions): BaseShrinkwrapFile | undefined { + const { packageManager, shrinkwrapContent, subspaceHasNoProjects } = options; switch (packageManager) { case 'npm': return NpmShrinkwrapFile.loadFromString(shrinkwrapContent); case 'pnpm': - return PnpmShrinkwrapFile.loadFromString(shrinkwrapContent); + return PnpmShrinkwrapFile.loadFromString(shrinkwrapContent, { subspaceHasNoProjects }); case 'yarn': return YarnShrinkwrapFile.loadFromString(shrinkwrapContent); default: diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index bf85934df57..a7016129ae6 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -259,14 +259,15 @@ export abstract class BaseInstallManager { ]); if (this.options.allowShrinkwrapUpdates && !shrinkwrapIsUpToDate) { - const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilePath(variant); - const shrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile( - this.rushConfiguration.packageManager, - committedShrinkwrapFileName - ); + const shrinkwrapFilePath: string = subspace.getCommittedShrinkwrapFilePath(variant); + const shrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: this.rushConfiguration.packageManager, + shrinkwrapFilePath, + subspaceHasNoProjects: subspace.getProjects().length === 0 + }); shrinkwrapFile?.validateShrinkwrapAfterUpdate(this.rushConfiguration, subspace, this._terminal); // Copy (or delete) common\temp\pnpm-lock.yaml --> common\config\rush\pnpm-lock.yaml - Utilities.syncFile(subspace.getTempShrinkwrapFilename(), committedShrinkwrapFileName); + Utilities.syncFile(subspace.getTempShrinkwrapFilename(), shrinkwrapFilePath); } else { // TODO: Validate whether the package manager updated it in a nontrivial way } @@ -470,12 +471,13 @@ export abstract class BaseInstallManager { // (If it's a full update, then we ignore the shrinkwrap from Git since it will be overwritten) if (!this.options.fullUpgrade) { - const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilePath(variant); + const shrinkwrapFilePath: string = subspace.getCommittedShrinkwrapFilePath(variant); try { - shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile( - this.rushConfiguration.packageManager, - committedShrinkwrapFileName - ); + shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: this.rushConfiguration.packageManager, + shrinkwrapFilePath, + subspaceHasNoProjects: subspace.getProjects().length === 0 + }); } catch (ex) { terminal.writeLine(); terminal.writeLine( diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index d9b3b577c90..0c603aef59b 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -690,10 +690,11 @@ export class WorkspaceInstallManager extends BaseInstallManager { // more up-to-date than the checked-in shrinkwrap since filtered installs are not written back. // Note that if there are no projects, or if we're in PNPM workspace mode and there are no // projects with dependencies, a lockfile won't be generated. - const tempShrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile( - this.rushConfiguration.packageManager, - subspace.getTempShrinkwrapFilename() - ); + const tempShrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: this.rushConfiguration.packageManager, + shrinkwrapFilePath: subspace.getTempShrinkwrapFilename(), + subspaceHasNoProjects: subspace.getProjects().length === 0 + }); if (tempShrinkwrapFile) { // Write or delete all project shrinkwraps related to the install diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmLinkManager.ts b/libraries/rush-lib/src/logic/pnpm/PnpmLinkManager.ts index 2fd3425f9a4..60dd7235814 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmLinkManager.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmLinkManager.ts @@ -27,6 +27,7 @@ import { type IPnpmVersionSpecifier, normalizePnpmVersionSpecifier } from './PnpmShrinkwrapFile'; +import type { Subspace } from '../../api/Subspace'; // special flag for debugging, will print extra diagnostic information, // but comes with performance cost @@ -59,10 +60,12 @@ export class PnpmLinkManager extends BaseLinkManager { protected async _linkProjectsAsync(): Promise { if (this._rushConfiguration.projects.length > 0) { + const subspace: Subspace = this._rushConfiguration.defaultSubspace; // Use shrinkwrap from temp as the committed shrinkwrap may not always be up to date // See https://github.com/microsoft/rushstack/issues/1273#issuecomment-492779995 const pnpmShrinkwrapFile: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( - this._rushConfiguration.defaultSubspace.getTempShrinkwrapFilename() + subspace.getTempShrinkwrapFilename(), + { subspaceHasNoProjects: subspace.getProjects().length === 0 } ); if (!pnpmShrinkwrapFile) { @@ -323,7 +326,9 @@ export class PnpmLinkManager extends BaseLinkManager { RushConstants.nodeModulesFolderName ); } else if (this._pnpmVersion.major >= 10) { - const pnpmKitV10: typeof import('@rushstack/rush-pnpm-kit-v10') = await import('@rushstack/rush-pnpm-kit-v10'); + const pnpmKitV10: typeof import('@rushstack/rush-pnpm-kit-v10') = await import( + '@rushstack/rush-pnpm-kit-v10' + ); // project@file+projects+presentation-integration-tests.tgz_jsdom@11.12.0 // The second parameter is max length of virtual store dir, @@ -341,7 +346,9 @@ export class PnpmLinkManager extends BaseLinkManager { RushConstants.nodeModulesFolderName ); } else if (this._pnpmVersion.major >= 9) { - const pnpmKitV9: typeof import('@rushstack/rush-pnpm-kit-v9') = await import('@rushstack/rush-pnpm-kit-v9'); + const pnpmKitV9: typeof import('@rushstack/rush-pnpm-kit-v9') = await import( + '@rushstack/rush-pnpm-kit-v9' + ); // project@file+projects+presentation-integration-tests.tgz_jsdom@11.12.0 // The second parameter is max length of virtual store dir, for v9 default is 120 https://pnpm.io/9.x/npmrc#virtual-store-dir-max-length @@ -355,7 +362,9 @@ export class PnpmLinkManager extends BaseLinkManager { RushConstants.nodeModulesFolderName ); } else if (this._pnpmVersion.major >= 8) { - const pnpmKitV8: typeof import('@rushstack/rush-pnpm-kit-v8') = await import('@rushstack/rush-pnpm-kit-v8'); + const pnpmKitV8: typeof import('@rushstack/rush-pnpm-kit-v8') = await import( + '@rushstack/rush-pnpm-kit-v8' + ); // PNPM 8 changed the local path format again and the hashing algorithm, and // is now using the scoped '@pnpm/dependency-path' package // See https://github.com/pnpm/pnpm/releases/tag/v8.0.0 diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 8ec9a700d18..ede98dae323 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -129,7 +129,11 @@ export interface IPnpmShrinkwrapYaml extends Lockfile { registry?: string; } -export interface ILoadFromFileOptions { +export interface ILoadFromStringOptions { + subspaceHasNoProjects: boolean; +} + +export interface ILoadFromFileOptions extends ILoadFromStringOptions { withCaching?: boolean; } @@ -323,7 +327,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { private readonly _integrities: Map>; private _pnpmfileConfiguration: PnpmfileConfiguration | undefined; - private constructor(shrinkwrapJson: IPnpmShrinkwrapYaml, hash: string) { + private constructor(shrinkwrapJson: IPnpmShrinkwrapYaml, hash: string, subspaceHasNoProjects: boolean) { super(); this.hash = hash; this._shrinkwrapJson = shrinkwrapJson; @@ -351,11 +355,21 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { this.overrides = new Map(Object.entries(shrinkwrapJson.overrides || {})); this.packageExtensionsChecksum = shrinkwrapJson.packageExtensionsChecksum; - // Lockfile v9 always has "." in importers filed. - this.isWorkspaceCompatible = - this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V9 - ? this.importers.size > 1 - : this.importers.size > 0; + let isWorkspaceCompatible: boolean; + const importerCount: number = this.importers.size; + if (this.shrinkwrapFileMajorVersion >= ShrinkwrapFileMajorVersion.V9) { + // Lockfile v9 always has "." in importers filed. + if (subspaceHasNoProjects) { + // If there are no projects in this subspace, the "." importer will be the only importer + isWorkspaceCompatible = importerCount === 1; + } else { + isWorkspaceCompatible = importerCount > 1; + } + } else { + isWorkspaceCompatible = importerCount > 0; + } + + this.isWorkspaceCompatible = isWorkspaceCompatible; this._integrities = new Map(); } @@ -387,11 +401,11 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { public static loadFromFile( shrinkwrapYamlFilePath: string, - options: ILoadFromFileOptions = {} + options: ILoadFromFileOptions ): PnpmShrinkwrapFile | undefined { try { const shrinkwrapContent: string = FileSystem.readFile(shrinkwrapYamlFilePath); - return PnpmShrinkwrapFile.loadFromString(shrinkwrapContent); + return PnpmShrinkwrapFile.loadFromString(shrinkwrapContent, options); } catch (error) { if (FileSystem.isNotExistError(error as Error)) { return undefined; // file does not exist @@ -400,13 +414,17 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { } } - public static loadFromString(shrinkwrapContent: string): PnpmShrinkwrapFile { + public static loadFromString( + shrinkwrapContent: string, + options: ILoadFromStringOptions + ): PnpmShrinkwrapFile { const hash: string = crypto.createHash('sha-256').update(shrinkwrapContent, 'utf8').digest('hex'); const cached: PnpmShrinkwrapFile | undefined = cacheByLockfileHash.get(hash); if (cached) { return cached; } + const { subspaceHasNoProjects } = options; const shrinkwrapJson: IPnpmShrinkwrapYaml = yamlModule.load(shrinkwrapContent) as IPnpmShrinkwrapYaml; if ((shrinkwrapJson as LockfileFileV9).snapshots) { const lockfile: IPnpmShrinkwrapYaml | null = convertLockfileV9ToLockfileObject( @@ -436,10 +454,11 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { lockfile.dependencies[name] = PnpmShrinkwrapFile.getLockfileV9PackageId(name, versionSpecifier); } } - return new PnpmShrinkwrapFile(lockfile, hash); + + return new PnpmShrinkwrapFile(lockfile, hash, subspaceHasNoProjects); } - return new PnpmShrinkwrapFile(shrinkwrapJson, hash); + return new PnpmShrinkwrapFile(shrinkwrapJson, hash, subspaceHasNoProjects); } public getShrinkwrapHash(experimentsConfig?: IExperimentsJson): string { diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts index 2ef3bc58304..16f68c967b6 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts @@ -5,6 +5,7 @@ import { type DependencySpecifier, DependencySpecifierType } from '../../Depende import { PnpmShrinkwrapFile, parsePnpm9DependencyKey, parsePnpmDependencyKey } from '../PnpmShrinkwrapFile'; import { RushConfiguration } from '../../../api/RushConfiguration'; import type { RushConfigurationProject } from '../../../api/RushConfigurationProject'; +import type { Subspace } from '../../../api/Subspace'; const DEPENDENCY_NAME: string = 'dependency_name'; const SCOPED_DEPENDENCY_NAME: string = '@scope/dependency_name'; @@ -282,8 +283,12 @@ snapshots: bar@1.2.0: {} `; - const shrinkwrapFile1 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent1); - const shrinkwrapFile2 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent2); + const shrinkwrapFile1 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent1, { + subspaceHasNoProjects: false + }); + const shrinkwrapFile2 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent2, { + subspaceHasNoProjects: false + }); // Clear cache to ensure fresh computation PnpmShrinkwrapFile.clearCache(); @@ -313,7 +318,8 @@ snapshots: it('can detect not modified', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v5/not-modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v5/not-modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -327,7 +333,8 @@ snapshots: it('can detect modified', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v5/modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v5/modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -341,7 +348,8 @@ snapshots: it('can detect overrides', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v5/overrides-not-modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v5/overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -357,7 +365,8 @@ snapshots: it('can detect not modified', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v6/not-modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v6/not-modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -371,7 +380,8 @@ snapshots: it('can detect modified', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v6/modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v6/modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -385,7 +395,8 @@ snapshots: it('can detect overrides', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v6/overrides-not-modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v6/overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -399,7 +410,8 @@ snapshots: it('can handle the inconsistent version of a package declared in dependencies and devDependencies', async () => { const project = getMockRushProject2(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v6/inconsistent-dep-devDep.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v6/inconsistent-dep-devDep.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -415,7 +427,8 @@ snapshots: it('can detect not modified', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v9/not-modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v9/not-modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -429,7 +442,8 @@ snapshots: it('can detect modified', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v9/modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v9/modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -443,7 +457,8 @@ snapshots: it('can detect overrides', async () => { const project = getMockRushProject(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v9/overrides-not-modified.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v9/overrides-not-modified.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -457,7 +472,8 @@ snapshots: it('can handle the inconsistent version of a package declared in dependencies and devDependencies', async () => { const project = getMockRushProject2(); const pnpmShrinkwrapFile = getPnpmShrinkwrapFileFromFile( - `${__dirname}/yamlFiles/pnpm-lock-v9/inconsistent-dep-devDep.yaml` + `${__dirname}/yamlFiles/pnpm-lock-v9/inconsistent-dep-devDep.yaml`, + project.rushConfiguration.defaultSubspace ); await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( @@ -471,8 +487,10 @@ snapshots: }); }); -function getPnpmShrinkwrapFileFromFile(filepath: string): PnpmShrinkwrapFile { - const pnpmShrinkwrapFile = PnpmShrinkwrapFile.loadFromFile(filepath); +function getPnpmShrinkwrapFileFromFile(filepath: string, subspace: Subspace): PnpmShrinkwrapFile { + const pnpmShrinkwrapFile = PnpmShrinkwrapFile.loadFromFile(filepath, { + subspaceHasNoProjects: subspace.getProjects().length === 0 + }); if (!pnpmShrinkwrapFile) { throw new Error(`Get PnpmShrinkwrapFileFromFile failed from ${filepath}`); } diff --git a/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts b/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts index dd2f4e3c9f3..224cffe679a 100644 --- a/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts +++ b/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts @@ -23,10 +23,11 @@ export function validate( ): void { // eslint-disable-next-line no-console console.log('Validating package manager shrinkwrap file.\n'); - const shrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile( - rushConfiguration.packageManager, - subspace.getCommittedShrinkwrapFilePath(variant) - ); + const shrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: rushConfiguration.packageManager, + shrinkwrapFilePath: subspace.getCommittedShrinkwrapFilePath(variant), + subspaceHasNoProjects: subspace.getProjects().length === 0 + }); if (!shrinkwrapFile) { // eslint-disable-next-line no-console diff --git a/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts index 7211e1af79f..f9fe1b4e87e 100644 --- a/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts @@ -16,8 +16,12 @@ import { NpmShrinkwrapFile } from '../npm/NpmShrinkwrapFile'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; describe(NpmShrinkwrapFile.name, () => { - const filename: string = `${__dirname}/shrinkwrapFile/npm-shrinkwrap.json`; - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('npm', filename)!; + const shrinkwrapFilePath: string = `${__dirname}/shrinkwrapFile/npm-shrinkwrap.json`; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'npm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; it('verifies root-level dependency', () => { expect(shrinkwrapFile.hasCompatibleTopLevelDependency(new DependencySpecifier('q', '~1.5.0'))).toEqual( @@ -113,42 +117,62 @@ describe(PnpmShrinkwrapFile.name, () => { } describe('V5.0 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/non-workspace-pnpm-lock-v5.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateNonWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(false); }); describe('V5.3 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/non-workspace-pnpm-lock-v5.3.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateNonWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(false); }); describe('V6.1 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/non-workspace-pnpm-lock-v6.1.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateNonWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(false); }); describe('V9 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/non-workspace-pnpm-lock-v9.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateNonWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(false); }); }); @@ -192,34 +216,65 @@ describe(PnpmShrinkwrapFile.name, () => { } describe('V5.3 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/workspace-pnpm-lock-v5.3.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(true); }); describe('V6.1 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/workspace-pnpm-lock-v5.3.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(true); }); describe('V9 lockfile', () => { - const filename: string = path.resolve( + const shrinkwrapFilePath: string = path.resolve( __dirname, '../../../src/logic/test/shrinkwrapFile/workspace-pnpm-lock-v9.yaml' ); - const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile('pnpm', filename)!; + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: false + })!; validateWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(true); + }); + + describe('V9 lockfile with no projects', () => { + const shrinkwrapFilePath: string = path.resolve( + __dirname, + '../../../src/logic/test/shrinkwrapFile/workspace-pnpm-lock-no-projects-v9.yaml' + ); + + const shrinkwrapFile: BaseShrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({ + packageManager: 'pnpm', + shrinkwrapFilePath, + subspaceHasNoProjects: true + })!; + + validateWorkspaceLockfile(shrinkwrapFile); + expect(shrinkwrapFile.isWorkspaceCompatible).toBe(true); }); }); }); diff --git a/libraries/rush-lib/src/logic/test/shrinkwrapFile/workspace-pnpm-lock-no-projects-v9.yaml b/libraries/rush-lib/src/logic/test/shrinkwrapFile/workspace-pnpm-lock-no-projects-v9.yaml new file mode 100644 index 00000000000..490c2e4fa8d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/shrinkwrapFile/workspace-pnpm-lock-no-projects-v9.yaml @@ -0,0 +1,9 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +importers: + + .: {} From e697d50844c84a21215e785705b6e42685ac7c2d Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 23 Dec 2025 17:56:40 -0500 Subject: [PATCH 2/3] fixup! Fix an erroneous 'The shrinkwrap file has not been updated to support workspaces...' error. --- .../logic/test/ProjectChangeAnalyzer.test.ts | 19 +++++++++++++------ .../src/logic/test/ShrinkwrapFile.test.ts | 1 - 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index bacc3dafa41..df3d771a17b 100644 --- a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -88,19 +88,22 @@ jest.mock('../Git', () => { }; }); -const OriginalPnpmShrinkwrapFile = jest.requireActual('../pnpm/PnpmShrinkwrapFile').PnpmShrinkwrapFile; +const OriginalPnpmShrinkwrapFile: typeof PnpmShrinkwrapFile = jest.requireActual( + '../pnpm/PnpmShrinkwrapFile' +).PnpmShrinkwrapFile; jest.mock('../pnpm/PnpmShrinkwrapFile', () => { return { PnpmShrinkwrapFile: { - loadFromFile: (fullShrinkwrapPath: string): PnpmShrinkwrapFile => { - return OriginalPnpmShrinkwrapFile.loadFromString(_getMockedPnpmShrinkwrapFile()); + loadFromFile: (fullShrinkwrapPath: string, options: ILoadFromFileOptions): PnpmShrinkwrapFile => { + return OriginalPnpmShrinkwrapFile.loadFromString(_getMockedPnpmShrinkwrapFile(), options); }, - loadFromString: (text: string): PnpmShrinkwrapFile => { + loadFromString: (text: string, options: ILoadFromStringOptions): PnpmShrinkwrapFile => { return OriginalPnpmShrinkwrapFile.loadFromString( _getMockedPnpmShrinkwrapFile() // Change dependencies version .replace(/1\.0\.1/g, '1.0.0') - .replace(/foo_1_0_1/g, 'foo_1_0_0') + .replace(/foo_1_0_1/g, 'foo_1_0_0'), + options ); } } @@ -127,7 +130,11 @@ import type { GetInputsSnapshotAsyncFn, IInputsSnapshotParameters } from '../incremental/InputsSnapshot'; -import type { PnpmShrinkwrapFile } from '../pnpm/PnpmShrinkwrapFile'; +import type { + ILoadFromFileOptions, + ILoadFromStringOptions, + PnpmShrinkwrapFile +} from '../pnpm/PnpmShrinkwrapFile'; describe(ProjectChangeAnalyzer.name, () => { beforeEach(() => { diff --git a/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts index f9fe1b4e87e..88ddd366b1e 100644 --- a/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/test/ShrinkwrapFile.test.ts @@ -273,7 +273,6 @@ describe(PnpmShrinkwrapFile.name, () => { subspaceHasNoProjects: true })!; - validateWorkspaceLockfile(shrinkwrapFile); expect(shrinkwrapFile.isWorkspaceCompatible).toBe(true); }); }); From 555da9de985b73e06a6ba1c4db6c58393cc1be42 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 23 Dec 2025 18:17:37 -0800 Subject: [PATCH 3/3] fixup! Fix an erroneous 'The shrinkwrap file has not been updated to support workspaces...' error. --- .../rush-resolver-cache-plugin/src/afterInstallAsync.ts | 7 +++++-- .../src/test/computeResolverCacheFromLockfileAsync.test.ts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index 47977f1ee7c..9c675ca96b8 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -88,7 +88,8 @@ export async function afterInstallAsync( const cacheFilePath: string = `${workspaceRoot}/resolver-cache.json`; const lockFile: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile(lockFilePath, { - withCaching: true + withCaching: true, + subspaceHasNoProjects: subspace.getProjects().length === 0 }); if (!lockFile) { throw new Error(`Failed to load shrinkwrap file: ${lockFilePath}`); @@ -192,7 +193,9 @@ export async function afterInstallAsync( } catch (error) { if (!context.optional) { throw new Error( - `Error reading index file for: "${context.descriptionFileRoot}" (${descriptionFileHash}): ${error.toString()}` + `Error reading index file for: "${ + context.descriptionFileRoot + }" (${descriptionFileHash}): ${error.toString()}` ); } return false; diff --git a/rush-plugins/rush-resolver-cache-plugin/src/test/computeResolverCacheFromLockfileAsync.test.ts b/rush-plugins/rush-resolver-cache-plugin/src/test/computeResolverCacheFromLockfileAsync.test.ts index 123354f33e7..458dc1682f2 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/test/computeResolverCacheFromLockfileAsync.test.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/test/computeResolverCacheFromLockfileAsync.test.ts @@ -97,7 +97,8 @@ describe(computeResolverCacheFromLockfileAsync.name, () => { const { workspaceRoot, commonPrefixToTrim, lockfileName, afterExternalPackagesAsync } = testCase; const lockfile: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( - `${collateralFolder}/${lockfileName}` + `${collateralFolder}/${lockfileName}`, + { subspaceHasNoProjects: false } ); if (lockfile === undefined) { throw new Error(`Failed to load lockfile: ${lockfileName}`);