Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rush] Add support for PNPM version 3.x #1210

Merged
merged 17 commits into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from 15 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
1 change: 1 addition & 0 deletions apps/rush-lib/assets/rush-init/[dot]gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Don't allow people to merge changes to these generated files, because the result
# may be invalid. You need to run "rush update" again.
pnpm-lock.yaml merge=binary
shrinkwrap.yaml merge=binary
npm-shrinkwrap.json merge=binary
yarn.lock merge=binary
Expand Down
16 changes: 14 additions & 2 deletions apps/rush-lib/assets/rush-init/rush.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,19 @@
* The default value is false to avoid legacy compatibility issues.
* It is strongly recommended to set strictPeerDependencies=true.
*/
/*[LINE "DEMO"]*/ "strictPeerDependencies": true
/*[LINE "DEMO"]*/ "strictPeerDependencies": true,


/**
* Configures the strategy used to select versions during installation.
*
* This feature requires PNPM version 3.1 or newer. It corresponds to the "--resolution-strategy" command-line
* option for PNPM. Possible values are "fast" and "fewer-dependencies". PNPM's default is "fast", but this may
* be incompatible with certain packages, for example the "@types" packages from DefinitelyTyped. Rush's default
* is "fewer-dependencies", which causes PNPM to avoid installing a newer version if an already installed version
* can be reused; this is more similar to NPM's algorithm.
*/
/*[LINE "HYPOTHETICAL"]*/ "resolutionStrategy": "fast"
},

/**
Expand All @@ -57,7 +69,7 @@
* Specify a SemVer range to ensure developers use a NodeJS version that is appropriate
* for your repo.
*/
"nodeSupportedVersionRange": ">=8.9.4 <9.0.0",
"nodeSupportedVersionRange": ">=10.13.0 <11.0.0",

/**
* If you would like the version specifiers for your dependencies to be consistent, then
Expand Down
70 changes: 70 additions & 0 deletions apps/rush-lib/src/api/PackageManagerFeatureSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as semver from 'semver';
import { RushConstants } from '../logic/RushConstants';

/**
* This represents the available Package Manager tools as a string
* @public
*/
export type PackageManager = 'pnpm' | 'npm' | 'yarn';
octogonz marked this conversation as resolved.
Show resolved Hide resolved

/**
* Reports the known features of a package manager as detected from its version number.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the "number" at the end of this sentence.

* @beta
*/
export class PackageManagerFeatureSet {
/**
* The package manager.
*/
public readonly packageManager: PackageManager;

/**
* The SemVer version of the package manager.
*/
public readonly version: string;

/**
* The filename of the shrinkwrap file that is used by the package manager.
*
* @remarks
* Example: `npm-shrinkwrap.json` or `pnpm-lock.yaml`
*/
public readonly shrinkwrapFilename: string;

/**
* PNPM only. True if `--resolution-strategy` is supported.
*/
public readonly supportsPnpmResolutionStrategy: boolean;

public constructor(packageManager: PackageManager, version: string) {
this.packageManager = packageManager;
this.version = version;

const parsedVersion: semver.SemVer = new semver.SemVer(version);

this.supportsPnpmResolutionStrategy = false;

switch (this.packageManager) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It kinda feels like this data should be expressed in JSON files. TS for code, JSON for data.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but let's collect a little more "data" before we design a specialized manager for it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - that's fine.

case 'pnpm':
if (parsedVersion.major >= 3) {
this.shrinkwrapFilename = RushConstants.pnpmV3ShrinkwrapFilename;

if (parsedVersion.minor >= 1) {
// Introduced in version 3.1.0-0
this.supportsPnpmResolutionStrategy = true;
}
} else {
this.shrinkwrapFilename = RushConstants.pnpmV1ShrinkwrapFilename;
}
break;
case 'npm':
this.shrinkwrapFilename = RushConstants.npmShrinkwrapFilename;
break;
case 'yarn':
this.shrinkwrapFilename = RushConstants.yarnShrinkwrapFilename;
break;
}
}
}
122 changes: 76 additions & 46 deletions apps/rush-lib/src/api/RushConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { VersionPolicyConfiguration } from './VersionPolicyConfiguration';
import { EnvironmentConfiguration } from './EnvironmentConfiguration';
import { CommonVersionsConfiguration } from './CommonVersionsConfiguration';
import { Utilities } from '../utilities/Utilities';
import { PackageManager, PackageManagerFeatureSet } from './PackageManagerFeatureSet';

const MINIMUM_SUPPORTED_RUSH_JSON_VERSION: string = '0.0.0';

Expand Down Expand Up @@ -81,6 +82,7 @@ export interface IRushRepositoryJson {
*/
export interface IPnpmOptionsJson {
strictPeerDependencies?: boolean;
resolutionStrategy?: ResolutionStrategy;
}

/**
Expand Down Expand Up @@ -152,6 +154,8 @@ export interface ICurrentVariantJson {
export class PnpmOptionsConfiguration {
/**
* If true, then Rush will add the "--strict-peer-dependencies" option when invoking PNPM.
*
* @remarks
* This causes "rush install" to fail if there are unsatisfied peer dependencies, which is
* an invalid state that can cause build failures or incompatible dependency versions.
* (For historical reasons, JavaScript package managers generally do not treat this invalid state
Expand All @@ -161,9 +165,26 @@ export class PnpmOptionsConfiguration {
*/
public readonly strictPeerDependencies: boolean;

/**
* The resolution strategy that will be used by PNPM.
*
* @remarks
* Configures the strategy used to select versions during installation.
*
* This feature requires PNPM version 3.1 or newer. It corresponds to the `--resolution-strategy` command-line
* option for PNPM. Possible values are `"fast"` and `"fewer-dependencies"`. PNPM's default is `"fast"`, but this
* may be incompatible with certain packages, for example the `@types` packages from DefinitelyTyped. Rush's default
* is `"fewer-dependencies"`, which causes PNPM to avoid installing a newer version if an already installed version
* can be reused; this is more similar to NPM's algorithm.
*
* For more background, see this discussion: {@link https://github.com/pnpm/pnpm/issues/1187}
*/
public readonly resolutionStrategy: ResolutionStrategy;

/** @internal */
public constructor(json: IPnpmOptionsJson) {
this.strictPeerDependencies = !!json.strictPeerDependencies;
this.resolutionStrategy = json.resolutionStrategy || 'fewer-dependencies';
}
}

Expand Down Expand Up @@ -209,10 +230,10 @@ export interface ITryFindRushJsonLocationOptions {
}

/**
* This represents the available Package Manager tools as a string
* This represents the available PNPM resolution strategies as a string
* @public
*/
export type PackageManager = 'pnpm' | 'npm' | 'yarn';
export type ResolutionStrategy = 'fewer-dependencies' | 'fast';

/**
* This represents the Rush configuration for a repository, based on the "rush.json"
Expand All @@ -230,10 +251,12 @@ export class RushConfiguration {
private _commonScriptsFolder: string;
private _commonRushConfigFolder: string;
private _packageManager: PackageManager;
private _packageManagerFeatureSet: PackageManagerFeatureSet;
private _npmCacheFolder: string;
private _npmTmpFolder: string;
private _pnpmStoreFolder: string;
private _yarnCacheFolder: string;
private _shrinkwrapFilename: string;
private _tempShrinkwrapFilename: string;
private _tempShrinkwrapPreinstallFilename: string;
private _rushLinkJsonFilename: string;
Expand Down Expand Up @@ -405,7 +428,11 @@ export class RushConfiguration {
* _validateCommonRushConfigFolder() function makes sure that this folder only contains
* recognized config files.
*/
private static _validateCommonRushConfigFolder(commonRushConfigFolder: string, packageManager: PackageManager): void {
private static _validateCommonRushConfigFolder(
commonRushConfigFolder: string,
packageManager: PackageManager,
shrinkwrapFilename: string
): void {
if (!FileSystem.exists(commonRushConfigFolder)) {
console.log(`Creating folder: ${commonRushConfigFolder}`);
FileSystem.ensureFolder(commonRushConfigFolder);
Expand All @@ -427,17 +454,13 @@ export class RushConfiguration {
}

const knownSet: Set<string> = new Set<string>(knownRushConfigFilenames.map(x => x.toUpperCase()));
switch (packageManager) {
case 'npm':
knownSet.add(RushConstants.npmShrinkwrapFilename.toUpperCase());
break;
case 'pnpm':
knownSet.add(RushConstants.pnpmShrinkwrapFilename.toUpperCase());
knownSet.add(RushConstants.pnpmfileFilename.toUpperCase());
break;
case 'yarn':
knownSet.add(RushConstants.yarnShrinkwrapFilename.toUpperCase());
break;

// Add the shrinkwrap filename for the package manager to the known set.
knownSet.add(shrinkwrapFilename.toUpperCase());

// If the package manager is pnpm, then also add the pnpm file to the known set.
if (packageManager === 'pnpm') {
knownSet.add(RushConstants.pnpmfileFilename.toUpperCase());
octogonz marked this conversation as resolved.
Show resolved Hide resolved
}

// Is the filename something we know? If not, report an error.
Expand All @@ -463,6 +486,14 @@ export class RushConfiguration {
return this._packageManager;
}

/**
* {@inheritdoc PackageManagerFeatureSet}
* @beta
*/
public get packageManagerFeatureSet(): PackageManagerFeatureSet {
return this._packageManagerFeatureSet;
}

/**
* The absolute path to the "rush.json" configuration file that was loaded to construct this object.
*/
Expand Down Expand Up @@ -569,7 +600,7 @@ export class RushConfiguration {
* command uses a temporary copy, whose path is tempShrinkwrapFilename.)
* @remarks
* This property merely reports the filename; the file itself may not actually exist.
* Example: `C:\MyRepo\common\npm-shrinkwrap.json` or `C:\MyRepo\common\shrinkwrap.yaml`
* Example: `C:\MyRepo\common\npm-shrinkwrap.json` or `C:\MyRepo\common\pnpm-lock.yaml`
*
* @deprecated Use `getCommittedShrinkwrapFilename` instead, which gets the correct common
* shrinkwrap file name for a given active variant.
Expand All @@ -578,12 +609,22 @@ export class RushConfiguration {
return this.getCommittedShrinkwrapFilename();
}

/**
* The filename (without any path) of the shrinkwrap file that is used by the package manager.
* @remarks
* This property merely reports the filename; the file itself may not actually exist.
* Example: `npm-shrinkwrap.json` or `pnpm-lock.yaml`
*/
public get shrinkwrapFilename(): string {
return this._shrinkwrapFilename;
}

/**
* The full path of the temporary shrinkwrap file that is used during "rush install".
* This file may get rewritten by the package manager during installation.
* @remarks
* This property merely reports the filename; the file itself may not actually exist.
* Example: `C:\MyRepo\common\temp\npm-shrinkwrap.json` or `C:\MyRepo\common\temp\shrinkwrap.yaml`
* Example: `C:\MyRepo\common\temp\npm-shrinkwrap.json` or `C:\MyRepo\common\temp\pnpm-lock.yaml`
*/
public get tempShrinkwrapFilename(): string {
return this._tempShrinkwrapFilename;
Expand All @@ -596,7 +637,7 @@ export class RushConfiguration {
* @remarks
* This property merely reports the filename; the file itself may not actually exist.
* Example: `C:\MyRepo\common\temp\npm-shrinkwrap-preinstall.json`
* or `C:\MyRepo\common\temp\shrinkwrap-preinstall.yaml`
* or `C:\MyRepo\common\temp\pnpm-lock-preinstall.yaml`
*/
public get tempShrinkwrapPreinstallFilename(): string {
return this._tempShrinkwrapPreinstallFilename;
Expand Down Expand Up @@ -831,21 +872,7 @@ export class RushConfiguration {

const variantConfigFolderPath: string = this._getVariantConfigFolderPath(variant);

if (this.packageManager === 'pnpm') {
return path.join(
variantConfigFolderPath,
RushConstants.pnpmShrinkwrapFilename);
} else if (this.packageManager === 'npm') {
return path.join(
variantConfigFolderPath,
RushConstants.npmShrinkwrapFilename);
} else if (this.packageManager === 'yarn') {
return path.join(
variantConfigFolderPath,
RushConstants.yarnShrinkwrapFilename);
} else {
throw new Error('Invalid package manager.');
}
return path.join(variantConfigFolderPath, this._shrinkwrapFilename);
}

/**
Expand Down Expand Up @@ -1003,31 +1030,34 @@ export class RushConfiguration {
}

if (this._packageManager === 'npm') {
this._tempShrinkwrapFilename = path.join(this._commonTempFolder, RushConstants.npmShrinkwrapFilename);

this._packageManagerToolVersion = rushConfigurationJson.npmVersion!;
this._packageManagerToolFilename = path.resolve(path.join(this._commonTempFolder,
'npm-local', 'node_modules', '.bin', 'npm'));
} else if (this._packageManager === 'pnpm') {
this._tempShrinkwrapFilename = path.join(this._commonTempFolder, RushConstants.pnpmShrinkwrapFilename);

this._packageManagerToolVersion = rushConfigurationJson.pnpmVersion!;
this._packageManagerToolFilename = path.resolve(path.join(this._commonTempFolder,
'pnpm-local', 'node_modules', '.bin', 'pnpm'));
} else {
this._tempShrinkwrapFilename = path.join(this._commonTempFolder, RushConstants.yarnShrinkwrapFilename);

this._packageManagerToolVersion = rushConfigurationJson.yarnVersion!;
this._packageManagerToolFilename = path.resolve(path.join(this._commonTempFolder,
'yarn-local', 'node_modules', '.bin', 'yarn'));
}

/// From "C:\repo\common\temp\shrinkwrap.yaml" --> "C:\repo\common\temp\shrinkwrap-preinstall.yaml"
this._packageManagerFeatureSet = new PackageManagerFeatureSet(this._packageManager,
this._packageManagerToolVersion);
this._shrinkwrapFilename = this._packageManagerFeatureSet.shrinkwrapFilename;

this._tempShrinkwrapFilename = path.join(
this._commonTempFolder, this._shrinkwrapFilename
);
this._packageManagerToolFilename = path.resolve(path.join(
this._commonTempFolder, `${this.packageManager}-local`, 'node_modules', '.bin', `${this.packageManager}`
));

/// From "C:\repo\common\temp\pnpm-lock.yaml" --> "C:\repo\common\temp\pnpm-lock-preinstall.yaml"
const parsedPath: path.ParsedPath = path.parse(this._tempShrinkwrapFilename);
this._tempShrinkwrapPreinstallFilename = path.join(parsedPath.dir,
parsedPath.name + '-preinstall' + parsedPath.ext);

RushConfiguration._validateCommonRushConfigFolder(this._commonRushConfigFolder, this.packageManager);
RushConfiguration._validateCommonRushConfigFolder(
this._commonRushConfigFolder,
this.packageManager,
this._shrinkwrapFilename
);

this._projectFolderMinDepth = rushConfigurationJson.projectFolderMinDepth !== undefined
? rushConfigurationJson.projectFolderMinDepth : 1;
Expand Down
24 changes: 23 additions & 1 deletion apps/rush-lib/src/api/test/RushConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('RushConfiguration', () => {

expect(rushConfiguration.packageManager).toEqual('pnpm');
assertPathProperty('committedShrinkwrapFilename',
rushConfiguration.committedShrinkwrapFilename, './repo/common/config/rush/shrinkwrap.yaml');
rushConfiguration.committedShrinkwrapFilename, './repo/common/config/rush/pnpm-lock.yaml');
assertPathProperty('commonFolder',
rushConfiguration.commonFolder, './repo/common');
assertPathProperty('commonRushConfigFolder',
Expand Down Expand Up @@ -168,6 +168,28 @@ describe('RushConfiguration', () => {
done();
});

it('can load repo/rush-pnpm-2.json', (done: jest.DoneCallback) => {
const rushFilename: string = path.resolve(__dirname, 'repo', 'rush-pnpm-2.json');
const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushFilename);

expect(rushConfiguration.packageManager).toEqual('pnpm');
expect(rushConfiguration.packageManagerToolVersion).toEqual('2.0.0');
expect(rushConfiguration.shrinkwrapFilename).toEqual('shrinkwrap.yaml');

done();
});

it('can load repo/rush-pnpm-3.json', (done: jest.DoneCallback) => {
const rushFilename: string = path.resolve(__dirname, 'repo', 'rush-pnpm-3.json');
const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushFilename);

expect(rushConfiguration.packageManager).toEqual('pnpm');
expect(rushConfiguration.packageManagerToolVersion).toEqual('3.0.0');
expect(rushConfiguration.shrinkwrapFilename).toEqual('pnpm-lock.yaml');

done();
});

it('allows the temp directory to be set via environment variable', () => {
const expectedValue: string = path.resolve('/var/temp');
process.env['RUSH_TEMP_FOLDER'] = expectedValue; // tslint:disable-line:no-string-literal
Expand Down
Loading