Skip to content

Commit

Permalink
Enforce resolution-mode=highest for pnpm versions 8.0.0 to 8.6.*
Browse files Browse the repository at this point in the history
  • Loading branch information
lolmaus committed Sep 14, 2023
1 parent 2438dca commit 2090108
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 2 deletions.
91 changes: 91 additions & 0 deletions lib/dependency-manager-adapters/pnpm.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ 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';
const NPMRC_CONFIG_BACKUP = '.npmrc.ember-try';

module.exports = CoreObject.extend({
// This still needs to be `npm` because we're still reading the dependencies
Expand All @@ -27,6 +33,7 @@ module.exports = CoreObject.extend({
async changeToDependencySet(depSet) {
await this.applyDependencySet(depSet);

await this._updateNpmRc();
await this._install(depSet);

let deps = Object.assign({}, depSet.dependencies, depSet.devDependencies);
Expand All @@ -49,6 +56,7 @@ 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
}
Expand All @@ -63,6 +71,89 @@ 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.
*
* 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)) {
// Backup
let npmrcBackupPath = path.join(this.cwd, NPMRC_CONFIG_BACKUP);
debug(`Copying ${NPMRC_CONFIG}`);
await fs.copy(npmrcPath, npmrcBackupPath);

await fs.appendFileSync(npmrcPath, '\nresolution-mode = highest\n');
} else {
fs.writeFileSync(npmrcPath, 'resolution-mode = highest\n');
}
},

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

if (fs.existsSync(NPMRC_CONFIG_BACKUP)) {
fs.renameSync(NPMRC_CONFIG_BACKUP, NPMRC_CONFIG);
} else if (fs.existsSync(NPMRC_CONFIG)) {
fs.removeSync(NPMRC_CONFIG);
}
},

async _getPnpmVersion(command = 'pnpm --version') {
try {
let result = await exec(command);
return result.stdout.split('\n')[0];
} catch (error) {
return null;
}
},

_doesPnpmRequireResolutionModeFix(versionStr) {
return versionStr ? semverGte(versionStr, '8.0.0') && semverLt(versionStr, '8.7.0') : false;
},

async _install(depSet) {
let mgrOptions = this.managerOptions || [];

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"release-it": "^14.11.6",
"release-it-lerna-changelog": "^3.1.0",
"rsvp": "^4.7.0",
"sinon": "^15.2.0",
"tmp-sync": "^1.1.0"
},
"engines": {
Expand Down
199 changes: 199 additions & 0 deletions test/dependency-manager-adapters/pnpm-adapter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ let expect = require('chai').expect;
let fs = require('fs-extra');
let path = require('path');
let tmp = require('tmp-sync');
const sinon = require('sinon');
let PnpmAdapter = require('../../lib/dependency-manager-adapters/pnpm');
let generateMockRun = require('../helpers/generate-mock-run');

Expand All @@ -18,6 +19,7 @@ describe('pnpm Adapter', () => {
});

afterEach(async () => {
sinon.restore();
process.chdir(root);
await fs.remove(tmproot);
});
Expand Down Expand Up @@ -108,6 +110,39 @@ describe('pnpm Adapter', () => {

expect(runCount).to.equal(1);
});

it('runs _updateNpmRc before _install', async () => {
await fs.outputJson('package.json', {
devDependencies: {
'ember-try-test-suite-helper': '0.1.0',
},
});

let adapter = new PnpmAdapter({
cwd: tmpdir,
});

const updateStub = sinon.replace(
adapter,
'_updateNpmRc',
sinon.fake(() => {})
);

const installStub = sinon.replace(
adapter,
'_install',
sinon.fake(() => {})
);

await adapter.setup();
await adapter.changeToDependencySet({
devDependencies: {
'ember-try-test-suite-helper': '1.0.0',
},
});

expect(updateStub.calledBefore(installStub)).to.be.true;
});
});

describe('#cleanup', () => {
Expand Down Expand Up @@ -147,6 +182,33 @@ describe('pnpm Adapter', () => {

expect(runCount).to.equal(1);
});

it('runs _revertNpmRc after _install', async () => {
await fs.outputJson('package.json', { modifiedPackageJSON: true });
await fs.outputJson('package.json.ember-try', { originalPackageJSON: true });
await fs.outputFile('pnpm-lock.yaml', 'modifiedYAML: true\n');
await fs.outputFile('pnpm-lock.ember-try.yaml', 'originalYAML: true\n');

let adapter = new PnpmAdapter({
cwd: tmpdir,
});

const revertStub = sinon.replace(
adapter,
'_revertNpmRc',
sinon.fake(() => {})
);

const installStub = sinon.replace(
adapter,
'_install',
sinon.fake(() => {})
);

await adapter.cleanup();

expect(revertStub.calledAfter(installStub)).to.be.true;
});
});

describe('#_packageJSONForDependencySet', () => {
Expand Down Expand Up @@ -330,4 +392,141 @@ describe('pnpm Adapter', () => {
expect(resultJSON.devDependencies).to.not.have.property('ember-feature-flags');
});
});

describe('#_doesPnpmRequireResolutionModeFix', () => {
[
{ given: null, expected: false },
{ given: '1.0.0', expected: false },
{ given: '7.9.9999', expected: false },
{ given: '8.0.0', expected: true },
{ given: '8.1.2', expected: true },
{ given: '8.6.9999', expected: true },
{ given: '8.7.0', expected: false },
{ given: '8.7.1', expected: false },
{ given: '9.0.0', expected: false },
].forEach(({ given, expected }) => {
it(`works with given version "${given}"`, () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let result = npmAdapter._doesPnpmRequireResolutionModeFix(given);
expect(result).equal(expected);
});
});
});

describe('#_getPnpmVersion', () => {
// prettier-ignore
[
{ version: '1.0.0' },
{ version: '8.6.2' },
{ version: 'how the turntables' },
].forEach(({ version }) => {
it(`works with given version "${version}"`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let result = await npmAdapter._getPnpmVersion(`echo ${version}`);
expect(result).equal(version);
});
});
});

describe('#_updateNpmRc', () => {
describe('when pnpm version requires the resolution-mode fix', () => {
it(`should create a new .npmrc file when none exists`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let npmrcPath = path.join(tmpdir, '.npmrc');
let npmrcBackupPath = path.join(tmpdir, '.npmrc.ember-try');

await npmAdapter._updateNpmRc('8.6.0');

let actualFileContent = fs.readFileSync(npmrcPath, 'utf8');
let expectedFileContent = 'resolution-mode = highest\n';

expect(actualFileContent, '.npmrc content').to.equal(expectedFileContent);
expect(fs.existsSync(npmrcBackupPath), '.npmrc.ember-try does not exist').to.be.false;
});

it(`should update an npmrc file when it already exists`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let npmrcPath = path.join(tmpdir, '.npmrc');
let npmrcBackupPath = path.join(tmpdir, '.npmrc.ember-try');

fs.writeFileSync(npmrcPath, 'foo = bar\n');

await npmAdapter._updateNpmRc('8.6.0');

let actualFileContent = fs.readFileSync(npmrcPath, 'utf8');
let expectedFileContent = 'foo = bar\n\nresolution-mode = highest\n';
expect(actualFileContent, '.npmrc content').to.equal(expectedFileContent);

let actualBackupFileContent = fs.readFileSync(npmrcBackupPath, 'utf8');
let expectedBackupFileContent = 'foo = bar\n';
expect(actualBackupFileContent, '.npmrc-backup content').to.equal(
expectedBackupFileContent
);
});
});

describe('when pnpm version does not the resolution-mode fix', () => {
it(`should not create a new .npmrc file`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let npmrcPath = path.join(tmpdir, '.npmrc');
let npmrcBackupPath = path.join(tmpdir, '.npmrc.ember-try');

await npmAdapter._updateNpmRc('7.6.0');

expect(fs.existsSync(npmrcPath), '.npmrc does not exist').to.be.false;
expect(fs.existsSync(npmrcBackupPath), '.npmrc.ember-try does not exist').to.be.false;
});
});
});

describe('#_revertNpmRc', () => {
describe('when pnpm version requires the resolution-mode fix', () => {
it(`when backup does not exist, it should delete the .npmrc file`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let npmrcPath = path.join(tmpdir, '.npmrc');
let npmrcBackupPath = path.join(tmpdir, '.npmrc.ember-try');

fs.writeFileSync(npmrcPath, 'resolution-mode = highest\n');

await npmAdapter._revertNpmRc('8.6.0');

expect(fs.existsSync(npmrcPath), '.npmrc.ember-try does not exist').to.be.false;
expect(fs.existsSync(npmrcBackupPath), '.npmrc.ember-try does not exist').to.be.false;
});

it(`when backup exists, it should replace the original file with backup and delete the backup`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let npmrcPath = path.join(tmpdir, '.npmrc');
let npmrcBackupPath = path.join(tmpdir, '.npmrc.ember-try');

fs.writeFileSync(npmrcPath, 'foo = bar\n\nresolution-mode = highest\n');
fs.writeFileSync(npmrcBackupPath, 'foo = bar\n');

await npmAdapter._revertNpmRc('8.6.0');

let actualFileContent = fs.readFileSync(npmrcPath, 'utf8');
let expectedFileContent = 'foo = bar\n';

expect(actualFileContent, '.npmrc content').to.equal(expectedFileContent);
expect(fs.existsSync(npmrcBackupPath), '.npmrc.ember-try existence').to.be.false;
});
});

describe('when pnpm version does not the resolution-mode fix', () => {
it(`should not touch the existing .npmrc file`, async () => {
let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
let npmrcPath = path.join(tmpdir, '.npmrc');
let npmrcBackupPath = path.join(tmpdir, '.npmrc.ember-try');

fs.writeFileSync(npmrcPath, 'foo = bar\n');

await npmAdapter._revertNpmRc('7.6.0');

let actualFileContent = fs.readFileSync(npmrcPath, 'utf8');

expect(actualFileContent, '.npmrc content').to.equal('foo = bar\n');
expect(fs.existsSync(npmrcBackupPath), '.npmrc.ember-try does not exist').to.be.false;
});
});
});
});
Loading

0 comments on commit 2090108

Please sign in to comment.