Skip to content
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
10 changes: 10 additions & 0 deletions common/changes/@microsoft/rush/fix-5439_2025-12-23-22-15.json
Original file line number Diff line number Diff line change
@@ -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"
}
11 changes: 8 additions & 3 deletions libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
Expand All @@ -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 (
Expand Down
3 changes: 2 additions & 1 deletion libraries/rush-lib/src/logic/RepoStateFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 21 additions & 12 deletions libraries/rush-lib/src/logic/ShrinkwrapFileFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
24 changes: 13 additions & 11 deletions libraries/rush-lib/src/logic/base/BaseInstallManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions libraries/rush-lib/src/logic/pnpm/PnpmLinkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,10 +60,12 @@ export class PnpmLinkManager extends BaseLinkManager {

protected async _linkProjectsAsync(): Promise<void> {
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) {
Expand Down Expand Up @@ -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@[email protected]
// The second parameter is max length of virtual store dir,
Expand All @@ -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@[email protected]
// 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
Expand All @@ -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
Expand Down
43 changes: 31 additions & 12 deletions libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -323,7 +327,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
private readonly _integrities: Map<string, Map<string, string>>;
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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading