Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Throw on incorrect pnpm resolution-mode
Browse files Browse the repository at this point in the history
lolmaus committed Sep 27, 2023
1 parent 188b123 commit d861acf
Showing 5 changed files with 273 additions and 211 deletions.
109 changes: 34 additions & 75 deletions lib/dependency-manager-adapters/pnpm.js
Original file line number Diff line number Diff line change
@@ -5,14 +5,11 @@ const fs = require('fs-extra');
const path = require('path');
const debug = require('debug')('ember-try:dependency-manager-adapter:pnpm');
const Backup = require('../utils/backup');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const semverLt = require('semver/functions/lt');
const semverGte = require('semver/functions/gte');

const PACKAGE_JSON = 'package.json';
const PNPM_LOCKFILE = 'pnpm-lock.yaml';
const NPMRC_CONFIG = '.npmrc';

module.exports = CoreObject.extend({
// This still needs to be `npm` because we're still reading the dependencies
@@ -26,8 +23,8 @@ module.exports = CoreObject.extend({
},

async setup() {
await this._throwOnResolutionMode();
await this.backup.addFiles([PACKAGE_JSON, PNPM_LOCKFILE]);
await this._updateNpmRc();
},

async changeToDependencySet(depSet) {
@@ -54,7 +51,6 @@ module.exports = CoreObject.extend({
await this.backup.restoreFiles([PACKAGE_JSON, PNPM_LOCKFILE]);
await this.backup.cleanUp();
await this._install();
await this._revertNpmRc();
} catch (e) {
console.log('Error cleaning up scenario:', e); // eslint-disable-line no-console
}
@@ -71,86 +67,49 @@ module.exports = CoreObject.extend({

/**
* pnpm versions 8.0.0 through 8.6.* have the `resolution-mode` setting inverted to
* `lowest-direct`, which breaks ember-try. This method conditionally adds the necessary config to
* .npmrc to fix this.
* `lowest-direct`, which breaks ember-try. This method throws a helpful error in the following
* cases:
* - `pnpm config get resolution-mode` reports a non-empty value that is not highest;
* - pnpm version is within dangerous range and `pnpm config get resolution-mode` reports an
* empty value.
*
* It covers the following cases:
* - pnpm version is out of dangerious range or cannot be retrieved — do not do anything
* - pnpm version is within dangerous range and .npmrc does not exist — create .npmrc with
* `resolution-mode = highest`
* - pnpm version is within dangerous range and .npmrc exists — backup the .npmrc file and
* append` resolution-mode = highest` to .npmrc
*
* @param {undefined | string} version — this is only used in testing. Call this fucntion without
* arguments
* @returns Promise<void>
*/
async _updateNpmRc(version) {
if (!version) {
version = await this._getPnpmVersion();
}

if (!this._doesPnpmRequireResolutionModeFix(version)) {
return;
}

let npmrcPath = path.join(this.cwd, NPMRC_CONFIG);

if (fs.existsSync(npmrcPath)) {
await this.backup.addFile(NPMRC_CONFIG);

await fs.appendFileSync(npmrcPath, '\nresolution-mode = highest\n');
} else {
fs.writeFileSync(npmrcPath, 'resolution-mode = highest\n');
async _throwOnResolutionMode() {
let version = await this._getPnpmVersion();
let resolutionMode = await this._getResolutionMode();

if (this._isResolutionModeWrong(version, resolutionMode)) {
throw new Error(
'You are using an old version of pnpm that uses wrong resolution mode that violates ember-try expectations. Please either upgrade pnpm or set `resolution-mode` to `highest` in `.npmrc`.'
);
}
},

/**
* pnpm versions 8.0.0 through 8.6.* have the `resolution-mode` setting inverted to
* `lowest-direct`, which breaks ember-try. This method conditionally adds the necessary config to
* .npmrc to fix this.
*
* It covers the following cases:
* - pnpm version is out of dangerious range or cannot be retrieved — do not do anything
* - pnpm version is within dangerous range and the backup does not exist — delete the .npmrc
* - pnpm version is within dangerous range and the backup exists — rename the backup to .npmrc,
* overwriting the current .npmrc
*
* @param {undefined | string} version — this is only used in testing. Call this fucntion without
* arguments
* @returns Promise<void>
*/
async _revertNpmRc(version) {
if (!version) {
version = await this._getPnpmVersion();
}

if (!this._doesPnpmRequireResolutionModeFix(version)) {
return;
}

let npmrcPath = path.join(this.cwd, NPMRC_CONFIG);

if (this.backup.hasFile(NPMRC_CONFIG)) {
await this.backup.restoreFile(NPMRC_CONFIG);
} else {
if (fs.existsSync(npmrcPath)) {
fs.removeSync(npmrcPath);
}
}
async _getPnpmVersion() {
let result = await this.run('pnpm', ['--version'], { cwd: this.cwd, stdio: 'pipe' });
return result.stdout.split('\n')[0];
},

async _getPnpmVersion(command = 'pnpm --version') {
try {
let result = await exec(command);
return result.stdout.split('\n')[0];
} catch (error) {
return null;
}
async _getResolutionMode() {
let result = await this.run('pnpm', ['config', 'get', 'resolution-mode'], {
cwd: this.cwd,
stdio: 'pipe',
});

return result.stdout.split('\n')[0];
},

_doesPnpmRequireResolutionModeFix(versionStr) {
return versionStr ? semverGte(versionStr, '8.0.0') && semverLt(versionStr, '8.7.0') : false;
_isResolutionModeWrong(versionStr, resolutionMode) {
// prettier-ignore
return (
resolutionMode.length
&& resolutionMode !== 'highest'
) || (
!resolutionMode.length
&& semverGte(versionStr, '8.0.0')
&& semverLt(versionStr, '8.7.0')
);
},

async _install(depSet) {
15 changes: 1 addition & 14 deletions lib/utils/backup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const debug = require('debug')('ember-try:backup');
const { copy, existsSync, mkdirSync } = require('fs-extra');
const { copy, existsSync } = require('fs-extra');
const { createHash } = require('node:crypto');
const { join } = require('node:path');
const { promisify } = require('node:util');
@@ -12,7 +12,6 @@ module.exports = class Backup {
constructor({ cwd }) {
this.cwd = cwd;
this.dir = join(tempDir, 'ember-try', createHash('sha256').update(cwd).digest('hex'));
mkdirSync(this.dir, { recursive: true });

debug(`Created backup directory ${this.dir}`);
}
@@ -93,16 +92,4 @@ module.exports = class Backup {
restoreFiles(filenames) {
return Promise.all(filenames.map((filename) => this.restoreFile(filename)));
}

/**
* Checks if a file exists in the backup directory.
*
* @param {String} filename Filename relative to the current working directory.
* @returns {boolean}
*/
hasFile(filename) {
let backupFile = this.pathForFile(filename);

return existsSync(backupFile);
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@
},
"devDependencies": {
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"codecov": "^3.8.3",
"ember-cli": "~3.22.0",
"eslint": "^7.31.0",
Loading

0 comments on commit d861acf

Please sign in to comment.