Skip to content

Commit

Permalink
Support shrinkwrap-deps.json and cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
D4N14L committed May 22, 2020
1 parent ca31390 commit beff2d8
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 221 deletions.
141 changes: 89 additions & 52 deletions apps/rush-lib/src/logic/WorkspaceInstallManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<void> {
// Workspaces do not support the noLink option, so throw if this is passed
if (this.options.noLink) {
Expand Down Expand Up @@ -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
Expand All @@ -83,39 +106,27 @@ 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;
}
});

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<string, string> =
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')
Expand All @@ -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 `
Expand All @@ -168,49 +178,68 @@ 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}"`);
shrinkwrapIsUpToDate = false;
}
}

// 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.'
));
}
}

// 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<string, string> = 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()})`);
Expand Down Expand Up @@ -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
}
}
Expand Down
72 changes: 45 additions & 27 deletions apps/rush-lib/src/logic/base/BaseShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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' ]
Expand All @@ -117,29 +103,61 @@ export abstract class BaseShrinkwrapFile {
*/
public abstract getTempProjectNames(): ReadonlyArray<string>;

/** @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\
* - [email protected]
* - [email protected]
* - [email protected]
*
* In this example, hasCompatibleWorkspaceDependency("lib-b", ">= 1.1.0", "workspace-key-for-project-a")
* would fail because it finds [email protected] 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<string>;
public abstract getWorkspaceKeys(): ReadonlyArray<string>;

/** @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;

Expand Down
17 changes: 10 additions & 7 deletions apps/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
BaseShrinkwrapFile
} from '../base/BaseShrinkwrapFile';
import { DependencySpecifier } from '../DependencySpecifier';
import { RushConfiguration } from '../../api/RushConfiguration';

interface INpmShrinkwrapDependencyJson {
version: string;
Expand Down Expand Up @@ -114,16 +113,20 @@ export class NpmShrinkwrapFile extends BaseShrinkwrapFile {
}

/** @override */
protected tryEnsureWorkspaceDependencyVersion(
dependencySpecifier: DependencySpecifier,
projectName: string,
rushConfiguration: RushConfiguration
): DependencySpecifier | undefined {
public getWorkspaceKeys(): ReadonlyArray<string> {
throw new InternalError('Not implemented');
}

/** @override */
public getWorkspaceKeyByPath(workspaceRoot: string, projectFolder: string): string {
throw new InternalError('Not implemented');
}

/** @override */
public getWorkspacePaths(): ReadonlyArray<string> {
protected getWorkspaceDependencyVersion(
dependencySpecifier: DependencySpecifier,
workspaceKey: string
): DependencySpecifier | undefined {
throw new InternalError('Not implemented');
}
}
Loading

0 comments on commit beff2d8

Please sign in to comment.