Repository: ember-cli/ember-try (js)

Issue:
`pnpm` or generic package manager support
Currently `ember-try` only supports two package managers for installing `node_modules`:
- `npm`: the default
- `yarn` (`v1.x`): if the user opts-in by setting `useYarn: true`
- (`bower`: technically supported in parallel, however it's deprecated since #402, basically dead 🧟, and doesn't actually manage `node_modules`.)

`yarn` `v1.x` is discontinued in favor of `v3.x` (dubbed `berry`). Unfortunately, the `berry` CLI and the new PnP package format is largely incompatible with `ember-cli` and `ember-try`. Related issues:
- #741
- #214

I doubt, that adopting `berry` will be easy or possible at all for the Ember ecosystem.

However by now, there are further actively maintained alternatives to choose from. For instance, there's:
- [`pnpm`](https://pnpm.io/): an alternative package manager, with a feature set similar to `yarn`, but better performance and disk efficiency.
- [`rush`](https://rushjs.io/): a monorepo (build) manager, similar to `lerna`, that wraps around the underlying package manager, currently supporting `npm`, `pnpm` & `yarn`.

I would _love_ to start using `pnpm` for various reasons.

The main issue isn't the resulting `node_modules` structure, which luckily appears to be compatible with or eaasy to adapt to for `ember-cli` projects, at least in my limited experience thus far.
The deal breaker currently is the implicit ecosystem-wide assumption that either `npm` or `yarn` are being used.

There is an effort to fix that for `ember install` (https://github.com/ember-cli/ember-cli/pull/9563), but `npm` / `yarn` are used explicitly in the wider ecosystem as well, like here in `ember-try`.

I got nerd-sniped and am happy to report that I was able to monkey-patch `ember-try` with the following snippet to make it use `pnpm`:

```js
const NPMDependencyManagerAdapter = require('ember-try/lib/dependency-manager-adapters/npm');

// Maps arguments for `yarn` to their `pnpm` equivalents. A falsy value, like
// `undefined`, means that the argument is not supported by `pnpm` and will be
// dropped.
// https://github.com/ember-cli/ember-try/blob/v1.4.0/lib/dependency-manager-adapters/npm.js#L116-L123
const argsMap = {
  // > Don't read or generate a `yarn.lock` lockfile.
  // https://classic.yarnpkg.com/en/docs/cli/install/#toc-yarn-install-no-lockfile
  //
  // Normally we would enable `--frozen-lockfile`, however:
  // > If `true`, pnpm doesn't generate a lockfile and fails to install if the
  // > lockfile is out of sync with the manifest / an update is needed or no
  // > lockfile is present.
  // https://pnpm.io/cli/install#--frozen-lockfile
  //
  // Instead we use:
  // > When set to `false`, pnpm won't read or generate a `pnpm-lock.yaml` file.
  // https://pnpm.io/npmrc#lockfile
  '--no-lockfile': '--config.lockfile=false',

  // > Ignore engines check.
  // https://classic.yarnpkg.com/en/docs/cli/install/#toc-yarn-install-ignore-engines
  //
  // > During local development, pnpm will always fail with an error message, if
  // > its version does not match the one specified in the engines field.
  // >
  // > Unless the user has set the `engine-strict` config flag (see `.npmrc`),
  // > this field is advisory only and will only produce warnings when your
  // > package is installed as a dependency.
  // https://pnpm.io/package_json#engines
  //
  // ! This probably doesn't work as expected, because of the "during local
  // development" limitation.
  '--ignore-engines': '--config.engine-strict=false',
};

// Override the `init` method (= "`constructor`") of the `ember-try` dependency
// manager adapter for `npm`, so that `pnpm` support can be optionally enabled
// by setting `npmOptions: { manager: 'pnpm' }` in the `config/ember-try.js`.
//
// https://github.com/ember-cli/ember-try/blob/v1.4.0/lib/dependency-manager-adapters/npm.js#L13-L16
const originalInit = NPMDependencyManagerAdapter.prototype.init;
NPMDependencyManagerAdapter.prototype.init = function (...args) {
  originalInit.apply(this, args);

  // If `npmOptions.manager` is set to `"pnpm"`, then enable `pnpm` mode.
  //
  // `npmOptions` is passed in as `managerOptions`.
  // https://github.com/ember-cli/ember-try/blob/v1.4.0/lib/utils/dependency-manager-adapter-factory.js#L39
  if (this.managerOptions?.manager === 'pnpm') {
    // If `npmOptions` is an object with an `args` property, then let `args`
    // take the place of `managerOptions`, so that the upstream code can process
    // it.
    if (this.managerOptions.args instanceof Array)
      this.managerOptions = this.managerOptions.args;
    // If `npmOptions` itself already is an array (with an extra `manager`
    // property), then keep it as-is, so that the upstream code can process it.
    // If it isn't an array, and `args` is also not set, then unset
    // `managerOptions`, as `npmOptions` was only used to opt-in to `pnpm` mode.
    else if (!(this.managerOptions instanceof Array))
      this.managerOptions = undefined;

    // Enable the `yarn` code path, which is close to what we need for `pnpm`.
    this.useYarnCommand = true;

    // Substitute `yarn.lock` with `pnpm-lock.yaml` accordingly.
    this.yarnLock = 'pnpm-lock.yaml';

    // Note: the upstream convention is to append `.ember-try` _after_ the file
    // extension, however this breaks syntax highlighting, so I've chosen to
    // insert it right before the file extension.
    this.yarnLockBackupFileName = 'pnpm-lock.ember-try.yaml';

    // Patch the `run` method to replace `yarn` with `pnpm` and translate &
    // filter the arguments accordingly.
    const originalRun = this.run;
    this.run = (_cmd, args, opts) =>
      originalRun.call(
        this,
        'pnpm',
        args
          .map((arg) => (arg in argsMap ? argsMap[arg] : arg))
          .filter(Boolean),
        opts
      );
  }
};
```

I keep this file in an extra directory that is `.npmignore`'d and import it in `config/ember-try.js` like so:

```js
const getChannelURL = require('ember-source-channel-url');
const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup');

try {
  require('../patches/ember-try');
} catch {
  console.warn('Failed to patch `ember-try` for `pnpm` support.');
}

module.exports = async function () {
  return {
    npmOptions: {
      manager: 'pnpm',
      args: ['--prefer-offline'],
    },

    scenarios: [ /* ... */ ]
  }
};
```

It's not perfect, but it works. 🥳

I would be _more than happy_ to PR this, so that `ember-try` can officially / experimentally support `pnpm`. The necessary underlying code changes aren't too drastic, but I'm not entirely sure regarding:
- What the official user-facing API should look like; `useYarn` + `usePnpm` doesn't really make sense and means that we'll keep having this issue, when the next package manager / wrapper comes along.
- Whether this might be a good opportunity to refactor the internal API, so that it becomes more flexible and might even allow users to provide their own adapters.

What are your thoughts on this?

Pull request description:
Implement `pnpm` adapter
As discussed via Discord I've created a new dependency manager adapter for `pnpm`, largely based on the existing `npm` adapter.

The main differences to the `npm` adapter are:

- We do not backup the `node_modules` folder and instead run `pnpm install` once more in the cleanup phase
- We do not use `--no-lockfile` so that we only have to resolve the dependencies that have actually changed, resulting in a much smaller amount of necessary dependency downloads

I've implemented a `usePnpm` option, similar to the existing `useYarn` option. I'm aware that this might not be ideal, but it works well enough for now and can still be refactored later.

Resolves #744



Patch:
diff --git a/lib/dependency-manager-adapters/pnpm.js b/lib/dependency-manager-adapters/pnpm.js
new file mode 100644
index 0000000..959ffa7
--- /dev/null
+++ b/lib/dependency-manager-adapters/pnpm.js
@@ -0,0 +1,170 @@
+'use strict';
+
+const CoreObject = require('core-object');
+const fs = require('fs-extra');
+const path = require('path');
+const debug = require('debug')('ember-try:dependency-manager-adapter:pnpm');
+
+const PACKAGE_JSON = 'package.json';
+const PACKAGE_JSON_BACKUP = 'package.json.ember-try';
+const PNPM_LOCKFILE = 'pnpm-lock.yaml';
+
+// Note: the upstream convention is to append `.ember-try` _after_ the file
+// extension, however this breaks syntax highlighting, so I've chosen to
+// insert it right before the file extension.
+const PNPM_LOCKFILE_BACKUP = 'pnpm-lock.ember-try.yaml';
+
+module.exports = CoreObject.extend({
+  // This still needs to be `npm` because we're still reading the dependencies
+  // from the `npm` key of the ember-try config.
+  configKey: 'npm',
+
+  init() {
+    this._super.apply(this, arguments);
+    this.run = this.run || require('../utils/run');
+  },
+
+  async setup() {
+    let pkg = path.join(this.cwd, PACKAGE_JSON);
+    let pkgBackup = path.join(this.cwd, PACKAGE_JSON_BACKUP);
+    debug(`Copying ${PACKAGE_JSON}`);
+    await fs.copy(pkg, pkgBackup);
+
+    let lockFile = path.join(this.cwd, PNPM_LOCKFILE);
+    let lockFileBackup = path.join(this.cwd, PNPM_LOCKFILE_BACKUP);
+    if (fs.existsSync(lockFile)) {
+      debug(`Copying ${PNPM_LOCKFILE}`);
+      await fs.copy(lockFile, lockFileBackup);
+    }
+  },
+
+  async changeToDependencySet(depSet) {
+    await this.applyDependencySet(depSet);
+
+    await this._install(depSet);
+
+    let deps = Object.assign({}, depSet.dependencies, depSet.devDependencies);
+    let currentDeps = Object.keys(deps).map((dep) => {
+      return {
+        name: dep,
+        versionExpected: deps[dep],
+        versionSeen: this._findCurrentVersionOf(dep),
+        packageManager: 'pnpm',
+      };
+    });
+
+    debug('Switched to dependencies: \n', currentDeps);
+
+    return currentDeps;
+  },
+
+  async cleanup() {
+    try {
+      debug(`Restoring original ${PACKAGE_JSON}`);
+      let pkg = path.join(this.cwd, PACKAGE_JSON);
+      let pkgBackup = path.join(this.cwd, PACKAGE_JSON_BACKUP);
+      await fs.copy(pkgBackup, pkg);
+      await fs.remove(pkgBackup);
+
+      debug(`Restoring original ${PNPM_LOCKFILE}`);
+      let lockFile = path.join(this.cwd, PNPM_LOCKFILE);
+      let lockFileBackup = path.join(this.cwd, PNPM_LOCKFILE_BACKUP);
+      await fs.copy(lockFileBackup, lockFile);
+      await fs.remove(lockFileBackup);
+
+      await this._install();
+    } catch (e) {
+      console.log('Error cleaning up scenario:', e); // eslint-disable-line no-console
+    }
+  },
+
+  _findCurrentVersionOf(packageName) {
+    let filename = path.join(this.cwd, 'node_modules', packageName, PACKAGE_JSON);
+    if (fs.existsSync(filename)) {
+      return JSON.parse(fs.readFileSync(filename)).version;
+    } else {
+      return null;
+    }
+  },
+
+  async _install(depSet) {
+    let mgrOptions = this.managerOptions || [];
+
+    // buildManagerOptions overrides all default
+    if (typeof this.buildManagerOptions === 'function') {
+      mgrOptions = this.buildManagerOptions(depSet);
+
+      if (!Array.isArray(mgrOptions)) {
+        throw new Error('buildManagerOptions must return an array of options');
+      }
+    } else if (!mgrOptions.includes('--frozen-lockfile=false')) {
+      mgrOptions.push('--frozen-lockfile=false');
+    }
+
+    // Note: We are explicitly *not* using `--no-lockfile` here, so that we
+    // only have to resolve the dependencies that have actually changed.
+
+    debug('Run pnpm install with options %s', mgrOptions);
+
+    await this.run('pnpm', [].concat(['install'], mgrOptions), { cwd: this.cwd });
+  },
+
+  async applyDependencySet(depSet) {
+    debug('Changing to dependency set: %s', JSON.stringify(depSet));
+
+    if (!depSet) {
+      return;
+    }
+
+    let backupPackageJSON = path.join(this.cwd, PACKAGE_JSON_BACKUP);
+    let packageJSONFile = path.join(this.cwd, PACKAGE_JSON);
+    let packageJSON = JSON.parse(fs.readFileSync(backupPackageJSON));
+    let newPackageJSON = this._packageJSONForDependencySet(packageJSON, depSet);
+
+    debug('Write package.json with: \n', JSON.stringify(newPackageJSON));
+    fs.writeFileSync(packageJSONFile, JSON.stringify(newPackageJSON, null, 2));
+
+    // We restore the original lockfile here, so that we always create a minimal
+    // diff compared to the original locked dependency set.
+
+    let lockFile = path.join(this.cwd, PNPM_LOCKFILE);
+    let lockFileBackup = path.join(this.cwd, PNPM_LOCKFILE_BACKUP);
+    if (fs.existsSync(lockFileBackup)) {
+      debug(`Restoring original ${PNPM_LOCKFILE}`);
+      await fs.copy(lockFileBackup, lockFile);
+    }
+  },
+
+  _packageJSONForDependencySet(packageJSON, depSet) {
+    this._overridePackageJSONDependencies(packageJSON, depSet, 'dependencies');
+    this._overridePackageJSONDependencies(packageJSON, depSet, 'devDependencies');
+    this._overridePackageJSONDependencies(packageJSON, depSet, 'peerDependencies');
+    this._overridePackageJSONDependencies(packageJSON, depSet, 'ember');
+
+    // see https://pnpm.io/package_json#pnpmoverrides
+    this._overridePackageJSONDependencies(packageJSON, depSet, 'overrides');
+
+    return packageJSON;
+  },
+
+  _overridePackageJSONDependencies(packageJSON, depSet, kindOfDependency) {
+    if (!depSet[kindOfDependency]) {
+      return;
+    }
+
+    let packageNames = Object.keys(depSet[kindOfDependency]);
+
+    for (let packageName of packageNames) {
+      if (!packageJSON[kindOfDependency]) {
+        packageJSON[kindOfDependency] = {};
+      }
+
+      let version = depSet[kindOfDependency][packageName];
+      if (version === null) {
+        delete packageJSON[kindOfDependency][packageName];
+      } else {
+        packageJSON[kindOfDependency][packageName] = version;
+      }
+    }
+  },
+});
diff --git a/lib/utils/dependency-manager-adapter-factory.js b/lib/utils/dependency-manager-adapter-factory.js
index 8687d21..e1c29a6 100644
--- a/lib/utils/dependency-manager-adapter-factory.js
+++ b/lib/utils/dependency-manager-adapter-factory.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const NpmAdapter = require('../dependency-manager-adapters/npm');
+const PnpmAdapter = require('../dependency-manager-adapters/pnpm');
 const WorkspaceAdapter = require('../dependency-manager-adapters/workspace');
 
 module.exports = {
@@ -34,6 +35,18 @@ module.exports = {
           buildManagerOptions: config.buildManagerOptions,
         })
       );
+    } else if (config.usePnpm) {
+      console.warn(
+        'pnpm support is experimental for now. if you notice any problems please open an issue.'
+      );
+
+      adapters.push(
+        new PnpmAdapter({
+          cwd: root,
+          managerOptions: config.npmOptions,
+          buildManagerOptions: config.buildManagerOptions,
+        })
+      );
     } else if (hasNpm) {
       adapters.push(
         new NpmAdapter({


Test patch:
diff --git a/test/dependency-manager-adapters/pnpm-adapter-test.js b/test/dependency-manager-adapters/pnpm-adapter-test.js
new file mode 100644
index 0000000..655a088
--- /dev/null
+++ b/test/dependency-manager-adapters/pnpm-adapter-test.js
@@ -0,0 +1,335 @@
+'use strict';
+
+let expect = require('chai').expect;
+let fs = require('fs-extra');
+let path = require('path');
+let tmp = require('tmp-sync');
+let PnpmAdapter = require('../../lib/dependency-manager-adapters/pnpm');
+let generateMockRun = require('../helpers/generate-mock-run');
+
+let root = process.cwd();
+let tmproot = path.join(root, 'tmp');
+let tmpdir;
+
+describe('pnpm Adapter', () => {
+  beforeEach(() => {
+    tmpdir = tmp.in(tmproot);
+    process.chdir(tmpdir);
+  });
+
+  afterEach(async () => {
+    process.chdir(root);
+    await fs.remove(tmproot);
+  });
+
+  describe('#setup', () => {
+    it('backs up the `package.json` and `pnpm-lock.yaml` files', async () => {
+      await fs.outputJson('package.json', { originalPackageJSON: true });
+      await fs.outputFile('pnpm-lock.yaml', 'originalYAML: true\n');
+      await fs.outputJson('node_modules/prove-it.json', { originalNodeModules: true });
+
+      let adapter = new PnpmAdapter({ cwd: tmpdir });
+      await adapter.setup();
+
+      expect(await fs.readJson('package.json.ember-try')).to.deep.equal({
+        originalPackageJSON: true,
+      });
+      expect(await fs.readFile('pnpm-lock.ember-try.yaml', 'utf-8')).to.equal(
+        'originalYAML: true\n'
+      );
+      expect(fs.existsSync('.node_modules.ember-try')).to.be.false;
+    });
+
+    it('ignores missing `pnpm-lock.yaml` files', async () => {
+      await fs.outputJson('package.json', { originalPackageJSON: true });
+      await fs.outputJson('node_modules/prove-it.json', { originalNodeModules: true });
+
+      let adapter = new PnpmAdapter({ cwd: tmpdir });
+      await adapter.setup();
+
+      expect(await fs.readJson('package.json.ember-try')).to.deep.equal({
+        originalPackageJSON: true,
+      });
+      expect(fs.existsSync('pnpm-lock.ember-try.yaml')).to.be.false;
+      expect(fs.existsSync('.node_modules.ember-try')).to.be.false;
+    });
+  });
+
+  describe('#changeToDependencySet', () => {
+    it('updates the `package.json` and runs `pnpm install`', async () => {
+      await fs.outputJson('package.json', {
+        devDependencies: {
+          'ember-try-test-suite-helper': '0.1.0',
+        },
+      });
+
+      let runCount = 0;
+      let stubbedRun = generateMockRun(
+        [
+          {
+            command: 'pnpm install --frozen-lockfile=false',
+            async callback(command, args, opts) {
+              runCount++;
+              expect(opts).to.have.property('cwd', tmpdir);
+            },
+          },
+        ],
+        { allowPassthrough: false }
+      );
+
+      let adapter = new PnpmAdapter({
+        cwd: tmpdir,
+        run: stubbedRun,
+      });
+
+      await adapter.setup();
+      let result = await adapter.changeToDependencySet({
+        devDependencies: {
+          'ember-try-test-suite-helper': '1.0.0',
+        },
+      });
+
+      expect(result).to.deep.equal([
+        {
+          name: 'ember-try-test-suite-helper',
+          packageManager: 'pnpm',
+          versionExpected: '1.0.0',
+          versionSeen: null,
+        },
+      ]);
+
+      expect(await fs.readJson('package.json')).to.deep.equal({
+        devDependencies: {
+          'ember-try-test-suite-helper': '1.0.0',
+        },
+      });
+
+      expect(await fs.readJson('package.json.ember-try')).to.deep.equal({
+        devDependencies: {
+          'ember-try-test-suite-helper': '0.1.0',
+        },
+      });
+
+      expect(runCount).to.equal(1);
+    });
+  });
+
+  describe('#cleanup', () => {
+    it('restores the `package.json` and `yarn-lock.yaml` files, and then runs `pnpm 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 runCount = 0;
+      let stubbedRun = generateMockRun(
+        [
+          {
+            command: 'pnpm install --frozen-lockfile=false',
+            async callback(command, args, opts) {
+              runCount++;
+              expect(opts).to.have.property('cwd', tmpdir);
+            },
+          },
+        ],
+        { allowPassthrough: false }
+      );
+
+      let adapter = new PnpmAdapter({
+        cwd: tmpdir,
+        run: stubbedRun,
+      });
+      await adapter.cleanup();
+
+      expect(await fs.readJson('package.json')).to.deep.equal({ originalPackageJSON: true });
+      expect(await fs.readFile('pnpm-lock.yaml', 'utf-8')).to.equal('originalYAML: true\n');
+      expect(fs.existsSync('package.json.ember-try')).to.be.false;
+      expect(fs.existsSync('pnpm-lock.ember-try.yaml')).to.be.false;
+      expect(fs.existsSync('.node_modules.ember-try')).to.be.false;
+
+      expect(runCount).to.equal(1);
+    });
+  });
+
+  describe('#_packageJSONForDependencySet', () => {
+    it('changes specified dependency versions', () => {
+      let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
+      let packageJSON = {
+        devDependencies: { 'ember-feature-flags': '1.0.0' },
+        dependencies: { 'ember-cli-babel': '5.0.0' },
+      };
+      let depSet = { dependencies: { 'ember-cli-babel': '6.0.0' } };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.dependencies['ember-cli-babel']).to.equal('6.0.0');
+    });
+
+    describe('ember property', () => {
+      it('adds the ember property to project package.json', () => {
+        let npmAdapter = new PnpmAdapter({
+          cwd: tmpdir,
+          useYarnCommand: true,
+        });
+        let packageJSON = {};
+        let depSet = {
+          ember: { edition: 'octane' },
+        };
+
+        let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+        expect(resultJSON).to.deep.equal({ ember: { edition: 'octane' } });
+      });
+
+      it('merges the ember property to project package.json', () => {
+        let npmAdapter = new PnpmAdapter({
+          cwd: tmpdir,
+          useYarnCommand: true,
+        });
+        let packageJSON = { ember: { foo: 'bar' } };
+        let depSet = {
+          ember: { edition: 'octane' },
+        };
+
+        let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+        expect(resultJSON).to.deep.equal({ ember: { foo: 'bar', edition: 'octane' } });
+      });
+
+      it('overrides existing fields inside the ember property to project package.json', () => {
+        let npmAdapter = new PnpmAdapter({
+          cwd: tmpdir,
+          useYarnCommand: true,
+        });
+        let packageJSON = { ember: { edition: 'classic' } };
+        let depSet = {
+          ember: { edition: 'octane' },
+        };
+
+        let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+        expect(resultJSON).to.deep.equal({ ember: { edition: 'octane' } });
+      });
+
+      it('removes any items with a null value', () => {
+        let npmAdapter = new PnpmAdapter({
+          cwd: tmpdir,
+          useYarnCommand: true,
+        });
+        let packageJSON = { ember: { edition: 'octane' } };
+        let depSet = {
+          ember: { edition: null },
+        };
+
+        let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+        expect(resultJSON).to.deep.equal({ ember: {} });
+      });
+    });
+
+    it('adds an override for the specified dependency version', () => {
+      let npmAdapter = new PnpmAdapter({
+        cwd: tmpdir,
+        useYarnCommand: true,
+      });
+      let packageJSON = {
+        dependencies: { 'ember-cli-babel': '5.0.0' },
+      };
+      let depSet = {
+        dependencies: { 'ember-cli-babel': '6.0.0' },
+        overrides: { 'ember-cli-babel': '6.0.0' },
+      };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.overrides['ember-cli-babel']).to.equal('6.0.0');
+    });
+
+    it('removes a dependency from overrides if its version is null', () => {
+      let npmAdapter = new PnpmAdapter({
+        cwd: tmpdir,
+        useYarnCommand: true,
+      });
+      let packageJSON = {
+        dependencies: { 'ember-cli-babel': '5.0.0' },
+        overrides: { 'ember-cli-babel': '5.0.0' },
+      };
+      let depSet = {
+        dependencies: { 'ember-cli-babel': '6.0.0' },
+        overrides: { 'ember-cli-babel': null },
+      };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.overrides['ember-cli-babel']).to.be.undefined;
+    });
+
+    it('doesnt add resolutions if there are none specified', () => {
+      let npmAdapter = new PnpmAdapter({
+        cwd: tmpdir,
+        useYarnCommand: true,
+      });
+      let packageJSON = {
+        dependencies: { 'ember-cli-babel': '5.0.0' },
+      };
+      let depSet = {
+        dependencies: { 'ember-cli-babel': '6.0.0' },
+      };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.resolutions).to.be.undefined;
+    });
+
+    it('doesnt add resolutions when not using yarn', () => {
+      let npmAdapter = new PnpmAdapter({
+        cwd: tmpdir,
+        useYarnCommand: false,
+      });
+      let packageJSON = {
+        dependencies: { 'ember-cli-babel': '5.0.0' },
+      };
+      let depSet = {
+        dependencies: { 'ember-cli-babel': '6.0.0' },
+        resolutions: { 'ember-cli-babel': '6.0.0' },
+      };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.resolutions).to.be.undefined;
+    });
+
+    it('changes specified npm dev dependency versions', () => {
+      let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
+      let packageJSON = {
+        devDependencies: { 'ember-feature-flags': '1.0.0' },
+        dependencies: { 'ember-cli-babel': '5.0.0' },
+      };
+      let depSet = { devDependencies: { 'ember-feature-flags': '2.0.1' } };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.devDependencies['ember-feature-flags']).to.equal('2.0.1');
+    });
+
+    it('changes specified npm peer dependency versions', () => {
+      let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
+      let packageJSON = { peerDependencies: { 'ember-cli-babel': '5.0.0' } };
+      let depSet = { peerDependencies: { 'ember-cli-babel': '4.0.0' } };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.peerDependencies['ember-cli-babel']).to.equal('4.0.0');
+    });
+
+    it('can remove a package', () => {
+      let npmAdapter = new PnpmAdapter({ cwd: tmpdir });
+      let packageJSON = { devDependencies: { 'ember-feature-flags': '1.0.0' } };
+      let depSet = { devDependencies: { 'ember-feature-flags': null } };
+
+      let resultJSON = npmAdapter._packageJSONForDependencySet(packageJSON, depSet);
+
+      expect(resultJSON.devDependencies).to.not.have.property('ember-feature-flags');
+    });
+  });
+});