From beff2d820c07bd8921f6d7e507f9a192865ad99c Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 22 May 2020 15:45:00 -0700 Subject: [PATCH] Support shrinkwrap-deps.json and cleanup --- .../src/logic/WorkspaceInstallManager.ts | 141 +++++++++++------- .../src/logic/base/BaseShrinkwrapFile.ts | 72 +++++---- .../src/logic/npm/NpmShrinkwrapFile.ts | 17 ++- .../src/logic/pnpm/PnpmLinkManager.ts | 132 ++++++++-------- .../pnpm/PnpmProjectDependencyManifest.ts | 32 ++-- .../src/logic/pnpm/PnpmShrinkwrapFile.ts | 85 +++++------ .../src/logic/yarn/YarnShrinkwrapFile.ts | 24 +-- 7 files changed, 282 insertions(+), 221 deletions(-) diff --git a/apps/rush-lib/src/logic/WorkspaceInstallManager.ts b/apps/rush-lib/src/logic/WorkspaceInstallManager.ts index bc13eec02d5..e02834cdfc5 100644 --- a/apps/rush-lib/src/logic/WorkspaceInstallManager.ts +++ b/apps/rush-lib/src/logic/WorkspaceInstallManager.ts @@ -11,14 +11,15 @@ import { JsonFile, IPackageJson, FileSystem, - FileConstants + FileConstants, + InternalError } from '@rushstack/node-core-library'; import { AlreadyReportedError } from '../utilities/AlreadyReportedError'; import { BaseInstallManager, IInstallManagerOptions } from './base/BaseInstallManager'; import { BaseShrinkwrapFile } from '../logic/base/BaseShrinkwrapFile'; import { DependencySpecifier } from './DependencySpecifier'; -import { PackageJsonEditor } from '../api/PackageJsonEditor'; +import { PackageJsonEditor, DependencyType } from '../api/PackageJsonEditor'; import { PnpmWorkspaceFile } from './pnpm/PnpmWorkspaceFile'; import { PurgeManager } from './PurgeManager'; import { RushConfiguration } from '../api/RushConfiguration'; @@ -42,6 +43,15 @@ export class WorkspaceInstallManager extends BaseInstallManager { super(rushConfiguration, rushGlobalFolder, purgeManager, options); } + public static getCommonWorkspaceKey(rushConfiguration: RushConfiguration): string { + switch (rushConfiguration.packageManager) { + case 'pnpm': + return '.' + default: + throw new InternalError('Not implemented'); + } + } + public async doInstall(): Promise { // Workspaces do not support the noLink option, so throw if this is passed if (this.options.noLink) { @@ -72,6 +82,19 @@ export class WorkspaceInstallManager extends BaseInstallManager { if (!shrinkwrapFile) { shrinkwrapIsUpToDate = false; + } else { + if ( + shrinkwrapFile.getWorkspaceKeys().length === 0 && + this.rushConfiguration.projects.length !== 0 && + !this.options.fullUpgrade + ) { + console.log(); + console.log(colors.red( + 'The shrinkwrap file has not been updated to support workspaces. Run "rush update --full" to update ' + + 'the shrinkwrap file.' + )); + throw new AlreadyReportedError(); + } } // dependency name --> version specifier @@ -83,7 +106,14 @@ export class WorkspaceInstallManager extends BaseInstallManager { allExplicitPreferredVersions.forEach((version: string, dependency: string) => { const dependencySpecifier: DependencySpecifier = new DependencySpecifier(dependency, version); - if (!shrinkwrapFile.hasCompatibleTopLevelDependency(dependencySpecifier)) { + // The common package.json is used to ensure common versions are installed, so look for this workspace + // and validate that the requested dependency is specified + if ( + !shrinkwrapFile.hasCompatibleWorkspaceDependency( + dependencySpecifier, + WorkspaceInstallManager.getCommonWorkspaceKey(this.rushConfiguration) + ) + ) { shrinkwrapWarnings.push(`Missing dependency "${dependency}" (${version}) required by the preferred versions from ` + RushConstants.commonVersionsFilename); shrinkwrapIsUpToDate = false; @@ -91,31 +121,12 @@ export class WorkspaceInstallManager extends BaseInstallManager { }); if (this._findOrphanedWorkspaceProjects(shrinkwrapFile)) { - // If there are any orphaned projects, then "npm install" would fail because the shrinkwrap - // contains references such as "resolved": "file:projects\\project1" that refer to nonexistent - // file paths. + // If there are any orphaned projects, then install would fail because the shrinkwrap + // contains references that refer to nonexistent file paths. shrinkwrapIsUpToDate = false; } } - const commonPackageJson: IPackageJson = { - dependencies: {}, - description: 'Temporary file generated by the Rush tool', - name: 'rush-common', - private: true, - version: '0.0.0' - }; - - // dependency name --> version specifier - const allPreferredVersions: Map = - BaseInstallManager.collectPreferredVersions(this.rushConfiguration, this.options.variant); - - // Add any preferred versions to the top of the commonPackageJson - // do this in alphabetical order for simpler debugging - for (const dependency of Array.from(allPreferredVersions.keys()).sort()) { - commonPackageJson.dependencies![dependency] = allPreferredVersions.get(dependency)!; - } - // To generate the workspace file, we will add each project to the file as we loop through and validate const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile( path.join(this.rushConfiguration.commonTempFolder, 'pnpm-workspace.yaml') @@ -131,20 +142,19 @@ export class WorkspaceInstallManager extends BaseInstallManager { const dependencySpecifier: DependencySpecifier = new DependencySpecifier(name, version); // Is there a locally built Rush project that could satisfy this dependency? - const localProject: RushConfigurationProject | undefined = - this.rushConfiguration.getProjectByName(name); + const referencedLocalProject: RushConfigurationProject | undefined = this.rushConfiguration.getProjectByName(name); // Validate that local projects are referenced with workspace notation. If not, and it is not a // cyclic dependency, then it needs to be updated to specify `workspace:*` explicitly. Currently only // supporting versions and version ranges for specifying a local project. if ( (dependencySpecifier.specifierType === 'version' || dependencySpecifier.specifierType === 'range') && - localProject && + referencedLocalProject && !rushProject.cyclicDependencyProjects.has(name) ) { // Make sure that this version is intended to target a local package. If not, then we will fail since it // is not explicitly specified as a cyclic dependency. - if (!semver.satisfies(localProject.packageJsonEditor.version, dependencySpecifier.versionSpecifier)) { + if (!semver.satisfies(referencedLocalProject.packageJsonEditor.version, dependencySpecifier.versionSpecifier)) { console.log(); console.log(colors.red( `"${rushProject.packageName}" depends on package "${name}" (${version}) which exists within the workspace ` @@ -168,25 +178,21 @@ export class WorkspaceInstallManager extends BaseInstallManager { shrinkwrapIsUpToDate = false; continue; } else if (dependencySpecifier.specifierType === 'workspace') { - // Already specified as a local project, let's just validate that the specifier is valid. - if (!semver.satisfies(localProject!.packageJsonEditor.version, dependencySpecifier.versionSpecifier)) { - console.log(); - console.log(colors.red( - `"${rushProject.packageName}" depends on package "${name}" (${version}) which exists within the workspace ` - + 'but cannot be fulfilled with the specified version range. Specify a valid workspace version range.' - )); - throw new AlreadyReportedError(); + // Already specified as a local project. Allow the package manager to validate this + continue; } + + // PNPM does not specify peer dependencies for workspaces in the shrinkwrap, so skip validating these + if (this.rushConfiguration.packageManager === 'pnpm' && dependencyType === DependencyType.Peer) { continue; } // It is not a local dependency, validate that it is compatible if ( shrinkwrapFile && - !shrinkwrapFile.tryEnsureCompatibleWorkspaceDependency( + !shrinkwrapFile.hasCompatibleWorkspaceDependency( dependencySpecifier, - rushProject.packageName, - this.rushConfiguration + shrinkwrapFile.getWorkspaceKeyByPath(this.rushConfiguration.commonTempFolder, rushProject.projectFolder) ) ) { shrinkwrapWarnings.push(`Missing dependency "${name}" (${version}) required by "${rushProject.packageName}"`); @@ -194,8 +200,8 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } - // Save the package.json if we modified the version references - if (rushProject.packageJsonEditor.saveIfModified()) { + // Save the package.json if we modified the version references and warn that the package.json was modified + if (packageJson.saveIfModified()) { console.log(colors.yellow( `"${rushProject.packageName}" depends on one or more workspace packages which did not use "workspace:" ` + 'notation. The package.json has been modified and must be committed to source control.' @@ -203,14 +209,37 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } + // Update the common package.json to contain all preferred versions + const commonPackageJson: IPackageJson = { + dependencies: {}, + description: 'Temporary file generated by the Rush tool', + name: 'rush-common', + private: true, + version: '0.0.0' + }; + + // dependency name --> version specifier + const allPreferredVersions: Map = BaseInstallManager.collectPreferredVersions( + this.rushConfiguration, + this.options.variant + ); + + // Add any preferred versions to the top of the commonPackageJson + // do this in alphabetical order for simpler debugging + for (const dependency of Array.from(allPreferredVersions.keys()).sort()) { + commonPackageJson.dependencies![dependency] = allPreferredVersions.get(dependency)!; + } + // Example: "C:\MyRepo\common\temp\package.json" - const commonPackageJsonFilename: string = path.join(this.rushConfiguration.commonTempFolder, - FileConstants.PackageJson); + const commonPackageJsonFilename: string = path.join( + this.rushConfiguration.commonTempFolder, + FileConstants.PackageJson + ); - // Don't update the file timestamp unless the content has changed, since "rush install" - // will consider this timestamp - JsonFile.save(commonPackageJson, commonPackageJsonFilename, { onlyIfChanged: true }); + // Save the generated files. Don't update the file timestamp unless the content has changed, + // since "rush install" will consider this timestamp workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true }); + JsonFile.save(commonPackageJson, commonPackageJsonFilename, { onlyIfChanged: true }); stopwatch.stop(); console.log(`Finished creating workspace (${stopwatch.toString()})`); @@ -362,13 +391,21 @@ export class WorkspaceInstallManager extends BaseInstallManager { */ private _findOrphanedWorkspaceProjects(shrinkwrapFile: BaseShrinkwrapFile): boolean { - for (const workspacePath of shrinkwrapFile.getWorkspacePaths()) { - const projectPath: string = path.resolve(this.rushConfiguration.commonTempFolder, workspacePath); - if (!this.rushConfiguration.tryGetProjectForPath(projectPath)) { + for (const workspaceKey of shrinkwrapFile.getWorkspaceKeys()) { + + // Look for the RushConfigurationProject using the workspace key + let rushProjectPath: string; + if (this.rushConfiguration.packageManager === 'pnpm') { + // PNPM workspace keys are relative paths from the workspace root, which is the common temp folder + rushProjectPath = path.resolve(this.rushConfiguration.commonTempFolder, workspaceKey); + } else { + throw new InternalError('Orphaned workspaces cannot be checked for the provided package manager'); + } + + if (!this.rushConfiguration.tryGetProjectForPath(rushProjectPath)) { console.log(os.EOL + colors.yellow(Utilities.wrapWords( - `Your ${this.rushConfiguration.shrinkwrapFilePhrase} references a project at "${projectPath}" ` - + 'which no longer exists.')) - + os.EOL); + `Your ${this.rushConfiguration.shrinkwrapFilePhrase} references a project at "${rushProjectPath}" ` + + 'which no longer exists.')) + os.EOL); return true; // found one } } diff --git a/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts b/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts index 60f000513f3..d8d695b7ff0 100644 --- a/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts @@ -8,7 +8,7 @@ import { PackageName, FileSystem } from '@rushstack/node-core-library'; import { RushConstants } from '../../logic/RushConstants'; import { DependencySpecifier } from '../DependencySpecifier'; import { IPolicyValidatorOptions } from '../policy/PolicyValidator'; -import { PackageManagerOptionsConfigurationBase, RushConfiguration } from '../../api/RushConfiguration'; +import { PackageManagerOptionsConfigurationBase } from '../../api/RushConfiguration'; /** * This class is a parser for both npm's npm-shrinkwrap.json and pnpm's pnpm-lock.yaml file formats. @@ -95,20 +95,6 @@ export abstract class BaseShrinkwrapFile { return this._checkDependencyVersion(dependencySpecifier, shrinkwrapDependency); } - public tryEnsureCompatibleWorkspaceDependency( - dependencySpecifier: DependencySpecifier, - projectName: string, - rushConfiguration: RushConfiguration - ): boolean { - const shrinkwrapDependency: DependencySpecifier | undefined = - this.tryEnsureWorkspaceDependencyVersion(dependencySpecifier, projectName, rushConfiguration); - if (!shrinkwrapDependency) { - return false; - } - - return this._checkDependencyVersion(dependencySpecifier, shrinkwrapDependency); - } - /** * Returns the list of temp projects defined in this file. * Example: [ '@rush-temp/project1', '@rush-temp/project2' ] @@ -117,29 +103,61 @@ export abstract class BaseShrinkwrapFile { */ public abstract getTempProjectNames(): ReadonlyArray; + /** @virtual */ + protected abstract tryEnsureDependencyVersion(dependencySpecifier: DependencySpecifier, + tempProjectName: string): DependencySpecifier | undefined; + + /** @virtual */ + protected abstract getTopLevelDependencyVersion(dependencyName: string): DependencySpecifier | undefined; + + /** + * Returns true if the specified workspace in the shrinkwrap file includes a package that would + * satisfy the specified SemVer version range. + * + * Consider this example: + * + * - project-a\ + * - lib-a@1.2.3 + * - lib-b@1.0.0 + * - lib-b@2.0.0 + * + * In this example, hasCompatibleWorkspaceDependency("lib-b", ">= 1.1.0", "workspace-key-for-project-a") + * would fail because it finds lib-b@1.0.0 which does not satisfy the pattern ">= 1.1.0". + * + * @virtual + */ + public hasCompatibleWorkspaceDependency(dependencySpecifier: DependencySpecifier, workspaceKey: string): boolean { + const shrinkwrapDependency: DependencySpecifier | undefined = this.getWorkspaceDependencyVersion( + dependencySpecifier, + workspaceKey + ); + return shrinkwrapDependency + ? this._checkDependencyVersion(dependencySpecifier, shrinkwrapDependency) + : false; + } + /** - * Returns the list of paths to Rush projects relative to the - * install root. + * Returns the list of keys to workspace projects specified in the shrinkwrap. * Example: [ '../../apps/project1', '../../apps/project2' ] * * @virtual */ - public abstract getWorkspacePaths(): ReadonlyArray; + public abstract getWorkspaceKeys(): ReadonlyArray; - /** @virtual */ - protected abstract tryEnsureDependencyVersion(dependencySpecifier: DependencySpecifier, - tempProjectName: string): DependencySpecifier | undefined; + /** + * Returns the key to the project in the workspace specified by the shrinkwrap. + * Example: '../../apps/project1' + * + * @virtual + */ + public abstract getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string /** @virtual */ - protected abstract tryEnsureWorkspaceDependencyVersion( + protected abstract getWorkspaceDependencyVersion( dependencySpecifier: DependencySpecifier, - projectName: string, - rushConfiguration: RushConfiguration + workspaceKey: string ): DependencySpecifier | undefined; - /** @virtual */ - protected abstract getTopLevelDependencyVersion(dependencyName: string): DependencySpecifier | undefined; - /** @virtual */ protected abstract serialize(): string; diff --git a/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts b/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts index 4de2f4379d9..2b6dc3a767d 100644 --- a/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts @@ -10,7 +10,6 @@ import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; import { DependencySpecifier } from '../DependencySpecifier'; -import { RushConfiguration } from '../../api/RushConfiguration'; interface INpmShrinkwrapDependencyJson { version: string; @@ -114,16 +113,20 @@ export class NpmShrinkwrapFile extends BaseShrinkwrapFile { } /** @override */ - protected tryEnsureWorkspaceDependencyVersion( - dependencySpecifier: DependencySpecifier, - projectName: string, - rushConfiguration: RushConfiguration - ): DependencySpecifier | undefined { + public getWorkspaceKeys(): ReadonlyArray { + throw new InternalError('Not implemented'); + } + + /** @override */ + public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string { throw new InternalError('Not implemented'); } /** @override */ - public getWorkspacePaths(): ReadonlyArray { + protected getWorkspaceDependencyVersion( + dependencySpecifier: DependencySpecifier, + workspaceKey: string + ): DependencySpecifier | undefined { throw new InternalError('Not implemented'); } } \ No newline at end of file diff --git a/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts b/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts index c26fafee010..e2560963d7a 100644 --- a/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts +++ b/apps/rush-lib/src/logic/pnpm/PnpmLinkManager.ts @@ -24,9 +24,9 @@ import { BasePackage } from '../base/BasePackage'; import { RushConstants } from '../../logic/RushConstants'; import { IRushLinkJson } from '../../api/RushConfiguration'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import { PnpmShrinkwrapFile, IPnpmShrinkwrapDependencyYaml } from './PnpmShrinkwrapFile'; +import { PnpmShrinkwrapFile, IPnpmShrinkwrapDependencyYaml, IPnpmShrinkwrapImporterYaml } from './PnpmShrinkwrapFile'; import { PnpmProjectDependencyManifest } from './PnpmProjectDependencyManifest'; -import { PackageJsonDependency } from '../../api/PackageJsonEditor'; +import { PackageJsonDependency, DependencyType } from '../../api/PackageJsonEditor'; // special flag for debugging, will print extra diagnostic information, // but comes with performance cost @@ -108,66 +108,70 @@ export class PnpmLinkManager extends BaseLinkManager { } } - // // We won't be using the package to actually create symlinks, this is just to bootstrap - // // the shrinkwrap-deps.json generation logic. - // const localPackage: BasePackage = BasePackage.createLinkedPackage( - // project.packageName, - // project.packageJsonEditor.version, - // project.projectFolder - // ); - - // // Iterate through all the regular dependencies - // const parentShrinkwrapEntry: IPnpmShrinkwrapDependencyYaml | undefined = - // pnpmShrinkwrapFile.getShrinkwrapEntryFromTempProjectDependencyKey(tempProjectDependencyKey); - // if (!parentShrinkwrapEntry) { - // throw new InternalError( - // 'Cannot find shrinkwrap entry using dependency key for temp project: ' + - // `${project.tempProjectName}`); - // } + const importerKey: string = pnpmShrinkwrapFile.getWorkspaceKeyByPath( + this._rushConfiguration.commonTempFolder, + project.projectFolder + ); + const workspaceImporter: IPnpmShrinkwrapImporterYaml | undefined = pnpmShrinkwrapFile.getWorkspaceImporter(importerKey); + if (!workspaceImporter) { + throw new InternalError(`Cannot find shrinkwrap entry using importer key for workspace project: ${importerKey}`); + } - // const pnpmProjectDependencyManifest: PnpmProjectDependencyManifest = new PnpmProjectDependencyManifest({ - // pnpmShrinkwrapFile, - // project - // }); - - // const dependencies: PackageJsonDependency[] = [ - // ...project.packageJsonEditor.dependencyList, - // ...project.packageJsonEditor.devDependencyList - // ].filter(x => !x.version.startsWith('workspace:')); - - // for (const { name, dependencyType } of dependencies) { - - // // read the version number from the shrinkwrap entry - // const isOptional: boolean = dependencyType === DependencyType.Optional; - // const version: string | undefined = isOptional - // ? (parentShrinkwrapEntry.optionalDependencies || {})[name] - // : (parentShrinkwrapEntry.dependencies || {})[name]; - // if (!version) { - // if (!isOptional) { - // throw new InternalError( - // `Cannot find shrinkwrap entry dependency "${name}" for workspace project: ` + - // `${project.packageName}`); - // } - // continue; - // } + const pnpmProjectDependencyManifest: PnpmProjectDependencyManifest = new PnpmProjectDependencyManifest({ + pnpmShrinkwrapFile, + project + }); - // const newLocalFolderPath: string = path.join(localPackage.folderPath, 'node_modules', name); - // const newLocalPackage: BasePackage = BasePackage.createLinkedPackage( - // name, - // version, - // newLocalFolderPath - // ); + // Dev dependen + const dependencies: PackageJsonDependency[] = [ + ...project.packageJsonEditor.dependencyList, + ...project.packageJsonEditor.devDependencyList + ].filter(x => !x.version.startsWith('workspace:')); + + for (const { name, dependencyType } of dependencies) { + // read the version number from the shrinkwrap entry + let version: string | undefined; + switch (dependencyType) { + case DependencyType.Regular: + version = (workspaceImporter.dependencies || {})[name]; + break; + case DependencyType.Dev: + version = (workspaceImporter.devDependencies || {})[name]; + break; + case DependencyType.Optional: + version = (workspaceImporter.optionalDependencies || {})[name]; + break; + case DependencyType.Peer: + // Peer dependencies do not need to be considered + continue; + } - // if (!this._rushConfiguration.experimentsConfiguration.configuration.legacyIncrementalBuildDependencyDetection) { - // pnpmProjectDependencyManifest.addDependency(newLocalPackage, parentShrinkwrapEntry); - // } - // } + if (!version) { + if (dependencyType !== DependencyType.Optional) { + throw new InternalError( + `Cannot find shrinkwrap entry dependency "${name}" for workspace project: ${project.packageName}` + ); + } + continue; + } - // if (!this._rushConfiguration.experimentsConfiguration.configuration.legacyIncrementalBuildDependencyDetection) { - // pnpmProjectDependencyManifest.save(); - // } else { - // pnpmProjectDependencyManifest.deleteIfExists(); - // } + if (!this._rushConfiguration.experimentsConfiguration.configuration.legacyIncrementalBuildDependencyDetection) { + pnpmProjectDependencyManifest.addDependency( + name, + version, + { + ...(workspaceImporter.optionalDependencies || {}), + ...(workspaceImporter.dependencies || {}), + ...(workspaceImporter.devDependencies || {}) + }); + } + } + + if (!this._rushConfiguration.experimentsConfiguration.configuration.legacyIncrementalBuildDependencyDetection) { + pnpmProjectDependencyManifest.save(); + } else { + pnpmProjectDependencyManifest.deleteIfExists(); + } } /** @@ -425,7 +429,8 @@ export class PnpmLinkManager extends BaseLinkManager { throw new InternalError(`Dependency "${dependencyName}" is not a symlink in "${pathToLocalInstallation}`); } - // read the version number from the shrinkwrap entry + // read the version number from the shrinkwrap entry and return if no version is specified + // and the dependency is optional const version: string | undefined = isOptional ? (parentShrinkwrapEntry.optionalDependencies || {})[dependencyName] : (parentShrinkwrapEntry.dependencies || {})[dependencyName]; @@ -450,7 +455,14 @@ export class PnpmLinkManager extends BaseLinkManager { newLocalPackage.symlinkTargetFolderPath = FileSystem.getRealPath(dependencyLocalInstallationSymlink); if (!this._rushConfiguration.experimentsConfiguration.configuration.legacyIncrementalBuildDependencyDetection) { - pnpmProjectDependencyManifest.addDependency(newLocalPackage, parentShrinkwrapEntry); + pnpmProjectDependencyManifest.addDependency( + newLocalPackage.name, + newLocalPackage.version!, + { + ...(parentShrinkwrapEntry.optionalDependencies || {}), + ...(parentShrinkwrapEntry.dependencies || {}) + } + ); } return newLocalPackage; diff --git a/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts b/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts index f3c8d644d89..00d02fe72df 100644 --- a/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts +++ b/apps/rush-lib/src/logic/pnpm/PnpmProjectDependencyManifest.ts @@ -16,7 +16,6 @@ import { } from './PnpmShrinkwrapFile'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { RushConstants } from '../RushConstants'; -import { BasePackage } from '../base/BasePackage'; import { DependencySpecifier } from '../DependencySpecifier'; export interface IPnpmProjectDependencyManifestOptions { @@ -63,12 +62,8 @@ export class PnpmProjectDependencyManifest { ); } - public addDependency(pkg: BasePackage, parentShrinkwrapEntry: IPnpmShrinkwrapDependencyYaml): void { - if (!pkg.version) { - throw new InternalError(`Version missing from dependency ${pkg.name}`); - } - - this._addDependencyInternal(pkg.name, pkg.version, parentShrinkwrapEntry); + public addDependency(name: string, version: string, parentDependencies: { [dependency: string]: string }): void { + this._addDependencyInternal(name, version, parentDependencies); } /** @@ -98,7 +93,7 @@ export class PnpmProjectDependencyManifest { private _addDependencyInternal( name: string, version: string, - parentShrinkwrapEntry: IPnpmShrinkwrapDependencyYaml, + parentDependencies: { [dependency: string]: string }, throwIfShrinkwrapEntryMissing: boolean = true ): void { const shrinkwrapEntry: IPnpmShrinkwrapDependencyYaml | undefined = this._pnpmShrinkwrapFile.getShrinkwrapEntry( @@ -125,11 +120,17 @@ export class PnpmProjectDependencyManifest { // Add the current dependency this._projectDependencyManifestFile.set(specifier, integrity); + // Collect the shrinkwrap dependencies + const shrinkwrapDependencies: { [dependency: string]: string } = { + ...(shrinkwrapEntry.optionalDependencies || {}), + ...(shrinkwrapEntry.dependencies || {}) + } + // Add the dependencies of the dependency for (const dependencyName in shrinkwrapEntry.dependencies) { if (shrinkwrapEntry.dependencies.hasOwnProperty(dependencyName)) { const dependencyVersion: string = shrinkwrapEntry.dependencies[dependencyName]; - this._addDependencyInternal(dependencyName, dependencyVersion, shrinkwrapEntry); + this._addDependencyInternal(dependencyName, dependencyVersion, shrinkwrapDependencies); } } @@ -141,7 +142,7 @@ export class PnpmProjectDependencyManifest { this._addDependencyInternal( optionalDependencyName, dependencyVersion, - shrinkwrapEntry, + shrinkwrapDependencies, throwIfShrinkwrapEntryMissing = false); } } @@ -171,13 +172,10 @@ export class PnpmProjectDependencyManifest { } // If not, check the parent. - if ( - parentShrinkwrapEntry.dependencies && - parentShrinkwrapEntry.dependencies.hasOwnProperty(peerDependencyName) - ) { + if (parentDependencies.hasOwnProperty(peerDependencyName)) { const dependencySpecifier: DependencySpecifier | undefined = parsePnpmDependencyKey( peerDependencyName, - parentShrinkwrapEntry.dependencies[peerDependencyName] + parentDependencies[peerDependencyName] ); if (dependencySpecifier) { if (!semver.valid(dependencySpecifier.versionSpecifier)) { @@ -204,7 +202,7 @@ export class PnpmProjectDependencyManifest { this._addDependencyInternal( peerDependencyName, peerDependencyKeys[peerDependencyName], - shrinkwrapEntry + shrinkwrapDependencies ); continue; } @@ -231,7 +229,7 @@ export class PnpmProjectDependencyManifest { this._addDependencyInternal( peerDependencyName, this._pnpmShrinkwrapFile.getTopLevelDependencyKey(peerDependencyName)!, - shrinkwrapEntry + shrinkwrapDependencies ); } } diff --git a/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 6f5094e353e..0b1de2a115b 100644 --- a/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -8,7 +8,7 @@ import { FileSystem } from '@rushstack/node-core-library'; import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; import { DependencySpecifier } from '../DependencySpecifier'; -import { PackageManagerOptionsConfigurationBase, PnpmOptionsConfiguration, RushConfiguration } from '../../api/RushConfiguration'; +import { PackageManagerOptionsConfigurationBase, PnpmOptionsConfiguration } from '../../api/RushConfiguration'; import { IPolicyValidatorOptions } from '../policy/PolicyValidator'; import { AlreadyReportedError } from '../../utilities/AlreadyReportedError'; @@ -51,7 +51,9 @@ export interface IPnpmShrinkwrapImporterYaml { dependencies: { [dependency: string]: string } /** The list of resolved version numbers for dev dependencies */ devDependencies: { [dependency: string]: string } - /** The list of specifiers used to resolve direct dependency versions */ + /** The list of resolved version numbers for optional dependencies */ + optionalDependencies: { [dependency: string]: string } + /** The list of specifiers used to resolve dependency versions */ specifiers: { [dependency: string]: string } } @@ -86,7 +88,7 @@ export interface IPnpmShrinkwrapImporterYaml { * } * } */ -interface IPnpmShrinkwrapYaml extends IPnpmShrinkwrapImporterYaml { +interface IPnpmShrinkwrapYaml { /** The list of resolved version numbers for direct dependencies */ dependencies: { [dependency: string]: string } /** The list of importers for local workspace projects */ @@ -206,6 +208,9 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { if (!this._shrinkwrapJson.packages) { this._shrinkwrapJson.packages = { }; } + if (!this._shrinkwrapJson.importers) { + this._shrinkwrapJson.importers = { }; + } } public static loadFromFile( @@ -290,19 +295,6 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { return this._getTempProjectNames(this._shrinkwrapJson.dependencies); } - /** @override */ - public getWorkspacePaths(): ReadonlyArray { - const result: string[] = []; - for (const key of Object.keys(this._shrinkwrapJson.importers)) { - // If it starts with @rush-temp, then include it: - if (key !== '.') { - result.push(key); - } - } - result.sort(); // make the result deterministic - return result; - } - /** * Gets the path to the tarball file if the package is a tarball. * Returns undefined if the package entry doesn't exist or the package isn't a tarball. @@ -319,14 +311,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { } public getTopLevelDependencyKey(dependencyName: string): string | undefined { - // For workspaces, top-level dependencies aren't populated, so instead we use the root workspace - // for common package versions and consider this the 'top level' - const dependenciesToCheck: { [dependency: string]: string } = ( - this._shrinkwrapJson.importers && this._shrinkwrapJson.importers.hasOwnProperty('.') - ) ? this._shrinkwrapJson.importers['.'].dependencies - : this._shrinkwrapJson.dependencies; - - return BaseShrinkwrapFile.tryGetValue(dependenciesToCheck, dependencyName); + return BaseShrinkwrapFile.tryGetValue(this._shrinkwrapJson.dependencies, dependencyName); } /** @@ -340,14 +325,7 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { * @override */ public getTopLevelDependencyVersion(dependencyName: string): DependencySpecifier | undefined { - // For workspaces, top-level dependencies aren't populated, so instead we use the root workspace - // for common package versions and consider this the 'top level' - const dependenciesToCheck: { [dependency: string]: string } = ( - this._shrinkwrapJson.importers && this._shrinkwrapJson.importers.hasOwnProperty('.') - ) ? this._shrinkwrapJson.importers['.'].dependencies - : this._shrinkwrapJson.dependencies; - - let value: string | undefined = BaseShrinkwrapFile.tryGetValue(dependenciesToCheck, dependencyName); + let value: string | undefined = BaseShrinkwrapFile.tryGetValue(this._shrinkwrapJson.dependencies, dependencyName); if (value) { // Getting the top level dependency version from a PNPM lockfile version 5.1 @@ -423,13 +401,6 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { return undefined; } - public getImporter(importerPath: string): IPnpmShrinkwrapImporterYaml | undefined { - const importer: IPnpmShrinkwrapImporterYaml | undefined = - BaseShrinkwrapFile.tryGetValue(this._shrinkwrapJson.importers, importerPath); - - return importer && importer.dependencies ? importer : undefined; - } - public getShrinkwrapEntryFromTempProjectDependencyKey( tempProjectDependencyKey: string ): IPnpmShrinkwrapDependencyYaml | undefined { @@ -530,6 +501,28 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { return this._parsePnpmDependencyKey(packageName, dependencyKey); } + /** @override */ + public getWorkspaceKeys(): ReadonlyArray { + const result: string[] = []; + for (const key of Object.keys(this._shrinkwrapJson.importers)) { + // Avoid including the common workspace + if (key !== '.') { + result.push(key); + } + } + result.sort(); // make the result deterministic + return result; + } + + /** @override */ + public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string { + return path.relative(workspaceRoot, projectFolder).replace(new RegExp(`\\${path.sep}`, 'g'), '/'); + } + + public getWorkspaceImporter(importerPath: string): IPnpmShrinkwrapImporterYaml | undefined { + return BaseShrinkwrapFile.tryGetValue(this._shrinkwrapJson.importers, importerPath); + } + /** * Gets the resolved version number of a dependency for a specific temp project. * For PNPM, we can reuse the version that another project is using. @@ -537,10 +530,9 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { * * @override */ - protected tryEnsureWorkspaceDependencyVersion( + protected getWorkspaceDependencyVersion( dependencySpecifier: DependencySpecifier, - projectName: string, - rushConfiguration: RushConfiguration + workspaceKey: string ): DependencySpecifier | undefined { // PNPM doesn't have the same advantage of NPM, where we can skip generate as long as the @@ -552,18 +544,13 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { // linked to. const packageName: string = dependencySpecifier.packageName; - - // Importer paths use forward slashes - const importerPath: string = path.relative( - rushConfiguration.commonTempFolder, - rushConfiguration.getProjectByName(projectName)!.projectFolder - ).replace(new RegExp(`\\${path.sep}`, 'g'), '/'); - const projectImporter: IPnpmShrinkwrapImporterYaml | undefined = this.getImporter(importerPath); + const projectImporter: IPnpmShrinkwrapImporterYaml | undefined = this.getWorkspaceImporter(workspaceKey); if (!projectImporter) { return undefined; } const allDependencies: { [dependency: string]: string } = { + ...(projectImporter.optionalDependencies || {}), ...(projectImporter.dependencies || {}), ...(projectImporter.devDependencies || {}) } diff --git a/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts b/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts index 1716f936cc8..b8be508aad8 100644 --- a/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts +++ b/apps/rush-lib/src/logic/yarn/YarnShrinkwrapFile.ts @@ -1,12 +1,13 @@ import * as os from 'os'; import * as lockfile from '@yarnpkg/lockfile'; + import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile'; import { FileSystem, PackageName, IParsedPackageNameOrError, InternalError } from '@rushstack/node-core-library'; import { RushConstants } from '../RushConstants'; import { DependencySpecifier } from '../DependencySpecifier'; -import { RushConfiguration } from '../../api/RushConfiguration'; +// import { RushConfigurationProject } from '../../api/RushConfigurationProject'; /** * Used with YarnShrinkwrapFile._encodePackageNameAndSemVer() and _decodePackageNameAndSemVer(). @@ -196,11 +197,6 @@ export class YarnShrinkwrapFile extends BaseShrinkwrapFile { return this._tempProjectNames; } - /** @override */ - public getWorkspacePaths(): ReadonlyArray { - throw new InternalError('Not implemented'); - } - /** @override */ public hasCompatibleTopLevelDependency(dependencySpecifier: DependencySpecifier): boolean { // It seems like we should normalize the key somehow, but Yarn apparently does not @@ -236,10 +232,20 @@ export class YarnShrinkwrapFile extends BaseShrinkwrapFile { throw new InternalError('Not implemented'); } - protected tryEnsureWorkspaceDependencyVersion( + /** @override */ + public getWorkspaceKeys(): ReadonlyArray { + throw new InternalError('Not implemented'); + } + + /** @override */ + public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string { + throw new InternalError('Not implemented'); + } + + /** @override */ + protected getWorkspaceDependencyVersion( dependencySpecifier: DependencySpecifier, - projectName: string, - rushConfiguration: RushConfiguration + workspaceKey: string ): DependencySpecifier | undefined { throw new InternalError('Not implemented'); }