Skip to content

Commit

Permalink
[feat] Adds support for Yarn/NPM workspaces
Browse files Browse the repository at this point in the history
This PR adds support for workspaces to allow ember-try to run scenarios
in monorepo based repositories quickly and efficiently. Workspaces are
currently only supported by Yarn, but NPM has indicated that they
[want to add support in the future](https://blog.npmjs.org/post/173239798780/beyond-npm6-the-future-of-the-npm-cli),
and other tools for managing monorepos such as Lerna integrate nicely
with workspaces, so it feels like the best way to add this type of
support.

To enable workspaces, users need to install `ember-cli` and `ember-try`
in the top level monorepo package, add a `config/ember-try.js` file, and
set `useWorkspaces` and `useYarn` to true in that config. This means
that the adapter is limited to using one try config for _all_ of the
packages in the repo. For most monorepo use cases this should be
sufficient, and users can still add ember-try to individual packages if
needed for unique per-package scenarios.

The workspace adapter reuses the NPM adapter for each individual
package. To do this, a new method needed to be exposed to allow depSets
to be applied without running the install, since that needs to be done
_once_ at the top level. An alternative would be to expose this as a
task directly so it could be run as a command in each package, but that
would be significantly more complicated.
  • Loading branch information
pzuraq committed Sep 18, 2018
1 parent c5885e2 commit e9cd69e
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 15 deletions.
27 changes: 16 additions & 11 deletions lib/dependency-manager-adapters/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,9 @@ module.exports = CoreObject.extend({
},
changeToDependencySet(depSet) {
let adapter = this;
depSet = depSet[adapter.configKey];

debug('Changing to dependency set: %s', JSON.stringify(depSet));
adapter.applyDependencySet(depSet);

if (!depSet) { return RSVP.resolve([]); }
let backupPackageJSON = path.join(adapter.cwd, adapter.packageJSONBackupFileName);
let packageJSONFile = path.join(adapter.cwd, adapter.packageJSON);
let packageJSON = JSON.parse(fs.readFileSync(backupPackageJSON));
let newPackageJSON = adapter._packageJSONForDependencySet(packageJSON, depSet);

debug('Write package.json with: \n', JSON.stringify(newPackageJSON));

fs.writeFileSync(packageJSONFile, JSON.stringify(newPackageJSON, null, 2));
return adapter._install().then(() => {
let deps = extend({}, depSet.dependencies || {}, depSet.devDependencies || {});
let currentDeps = Object.keys(deps).map((dep) => {
Expand Down Expand Up @@ -118,6 +108,21 @@ module.exports = CoreObject.extend({
}
});
},
applyDependencySet(depSet) {
depSet = depSet[this.configKey];

debug('Changing to dependency set: %s', JSON.stringify(depSet));

if (!depSet) { return RSVP.resolve([]); }
let backupPackageJSON = path.join(this.cwd, this.packageJSONBackupFileName);
let packageJSONFile = path.join(this.cwd, this.packageJSON);
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));
},
_packageJSONForDependencySet(packageJSON, depSet) {

this._overridePackageJSONDependencies(packageJSON, depSet, 'dependencies');
Expand Down
102 changes: 102 additions & 0 deletions lib/dependency-manager-adapters/workspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const CoreObject = require('core-object');
const fs = require('fs-extra');
const RSVP = require('rsvp');
const path = require('path');
const extend = require('extend');
const debug = require('debug')('ember-try:dependency-manager-adapter:workspaces');
const walkSync = require('walk-sync');

const NpmAdapter = require('./npm');

module.exports = CoreObject.extend({
init() {
this._super.apply(this, arguments);
this.run = this.run || require('../utils/run');

if (!this.useYarnCommand) {
throw new Error('workspaces are currently only supported by Yarn, you must set `useYarn` to true');
}
},

packageJSON: 'package.json',

setup(options) {
if (!options) {
options = {};
}

let packageJSON = JSON.parse(fs.readFileSync(this.packageJSON));
let workspaceGlobs = packageJSON.workspaces;

if (!workspaceGlobs || !workspaceGlobs.length) {
throw new Error('you must define the `workspaces` property in package.json with at least one workspace to use workspaces with ember-try');
}

// workspaces is a list of globs, loop over the list and find
// all paths that contain a `package.json` file
let workspacePaths = walkSync('.', { globs: workspaceGlobs }).filter(workspacePath => {
let packageJSONPath = path.join(this.cwd, workspacePath, 'package.json');
return fs.existsSync(packageJSONPath);
});

this._packageAdapters = workspacePaths.map(workspacePath => {
return new NpmAdapter({
cwd: workspacePath,
run: this.run,
managerOptions: this.managerOptions,
useYarnCommand: true,
});
});

return RSVP.all(this._packageAdapters.map(adapter => adapter.setup(options)));
},

changeToDependencySet(depSet) {
this._packageAdapters.forEach(adapter => {
adapter.applyDependencySet(depSet);
});

return this._install().then(() => {
let deps = extend({}, depSet.dependencies || {}, depSet.devDependencies || {});
let currentDeps = Object.keys(deps).map((dep) => {
return {
name: dep,
versionExpected: deps[dep],
versionSeen: this._findCurrentVersionOf(dep),
packageManager: 'yarn',
};
});

debug('Switched to dependencies: \n', currentDeps);

return RSVP.Promise.resolve(currentDeps);
});
},

cleanup() {
return RSVP.all(this._packageAdapters.map(adapter => adapter.cleanup()));
},

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

debug('Run yarn install with options %s', mgrOptions);

if (mgrOptions.indexOf('--no-lockfile') === -1) {
mgrOptions = mgrOptions.concat(['--no-lockfile']);
}
// npm warns on incompatible engines
// yarn errors, not a good experience
if (mgrOptions.indexOf('--ignore-engines') === -1) {
mgrOptions = mgrOptions.concat(['--ignore-engines']);
}

return this.run('yarn', [].concat(['install'], mgrOptions), { cwd: this.cwd });
},

_findCurrentVersionOf(dep) {
return this._packageAdapters[0]._findCurrentVersionOf(dep);
},
});
3 changes: 2 additions & 1 deletion lib/tasks/try-each.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ module.exports = CoreObject.extend({
let task = this;
let dependencyManagerAdapters = task.dependencyManagerAdapters || DependencyManagerAdapterFactory.generateFromConfig(task.config, task.project.root);
debug('DependencyManagerAdapters: %s', dependencyManagerAdapters.map((item) => { return item.configKey; }));
task.ScenarioManager = new ScenarioManager({ ui: task.ui,
task.ScenarioManager = new ScenarioManager({
ui: task.ui,
dependencyManagerAdapters,
});

Expand Down
17 changes: 16 additions & 1 deletion lib/utils/dependency-manager-adapter-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const BowerAdapter = require('../dependency-manager-adapters/bower');
const NpmAdapter = require('../dependency-manager-adapters/npm');
const WorkspaceAdapter = require('../dependency-manager-adapters/workspace');

module.exports = {
generateFromConfig(config, root) {
Expand All @@ -20,10 +21,24 @@ module.exports = {
hasBower = true;
}
});
if (hasNpm || hasBower) {

if (config.useWorkspaces) {
if (hasBower) {
throw new Error('bower is not supported when using workspaces');
}

adapters.push(
new WorkspaceAdapter({
cwd: root,
managerOptions: config.npmOptions,
useYarnCommand: config.useYarn
})
);
} else if (hasNpm || hasBower) {
adapters.push(new NpmAdapter({ cwd: root, managerOptions: config.npmOptions, useYarnCommand: config.useYarn }));
adapters.push(new BowerAdapter({ cwd: root, managerOptions: config.bowerOptions }));
}

return adapters;
},
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
"promise-map-series": "^0.2.1",
"resolve": "^1.1.6",
"rimraf": "^2.3.2",
"rsvp": "^4.7.0"
"rsvp": "^4.7.0",
"walk-sync": "^0.3.3"
},
"ember-addon": {
"configPath": "tests/dummy/config"
Expand Down
Loading

0 comments on commit e9cd69e

Please sign in to comment.