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

Add ProjectWideDependencyChecker check API #206

Merged
merged 1 commit into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ module.exports = {

### allAddons

An iterator which gives acccess to all addon instances
An iterator which gives access to all addon instances

```js
const VersionChecker = require('ember-cli-version-checker');
Expand All @@ -249,7 +249,67 @@ module.exports = {
let checker = VersionChecker.forProject(this.project);

for (let { name, root } = checker.allAddons()) {
// access to the add-on, in this case root + name
// access to the addon, in this case name and root
}
}
};
```

### check
stefanpenner marked this conversation as resolved.
Show resolved Hide resolved

A utility to verify that addons are installed at appropriate versions. `npm`
and `yarn` resolve conflicting transitive dependency requirements by installing
multiple versions. They do not include a mechanism for packages to declare
that a dependency must be unique. This is, however, a practical constraint
when building Ember applications (for example, we would not want to build an
application that shipped two versions of Ember Data). [Related discussion on npm](https://github.com/npm/rfcs/pull/23)

Every addon in the ember ecosystem implicitly depends on `ember-source`, and
most likely a specific version range. If that dependency is specified as a
`package.json` dependency, a mismatch between application and addon would
result in duplicating `ember-source`. Instead of failing the build, we would
build an application with an unknown version of `ember-source`, subverting the
point of specifying dependency version ranges in the first place! The `check`
API provides a mechanism to avoid this and fail fast in the build step, instead
of building an invalid application with harder to debug runtime errors.

For example, as of today `ember-data` supports `ember-source` `>= 3.4.8`, if it
where to use this addon, it could specify this constraint and provide good
error messages to users.

```javascript
const VersionChecker = require('ember-cli-version-checker');

module.exports = {
name: 'awesome-addon',
included() {
this._super.included.apply(this, arguments);

const checker = VersionChecker.forProject(this.project);
const check = checker.check({
'ember-source': '>= 3.4.8'
});

// if it would like to simply assert
check.assert('[awesome-addon] dependency check failed');
// will throw an error message similar to the following if the check was not satisfied:

// [awesome-addon] dependency check failed:
// - 'ember-source' expected version [>= 3.4.8] but got version: [2.0.0]

// if the requirements are more advanced, we can inspect the resulting check.

if (!check.isSatisfied) {
const altCheck = checker.check({
'magical-polyfil': '>= 1.0.0',
'ember-source': '>= 3.0.0'
})

check.assert('[awesome-addon] dependency check failed:');
// will throw error message similar to the following if the check was not satisfied:
// [awesome-addon] dependency check failed:
// - 'magical-polyfil' expected version [>= 1.0.0] but got version: [0.0.1]
// - 'ember-source' expected version [>= 3.0.0] but got version: [2.0.-]
}
}
};
Expand Down
90 changes: 89 additions & 1 deletion src/project-wide-dependency-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const {
hasSingleImplementation,
allAddons,
} = require('./utils/single-implementation');
const semver = require('semver');
const SilentError = require('silent-error');

const { EOL } = require('os');
/* global Set */

module.exports = class ProjectWideDependencyChecker {
Expand Down Expand Up @@ -48,6 +49,22 @@ module.exports = class ProjectWideDependencyChecker {
return addons;
}

filterAddonsByNames(names) {
stefanpenner marked this conversation as resolved.
Show resolved Hide resolved
const result = Object.create(null);
for (let name of names) {
result[name] = [];
}

for (let addon of this.allAddons()) {
const addonResult = result[addon.name];
if (addonResult !== undefined) {
addonResult.push(addon);
}
}

return result;
}

assertSingleImplementation(name, customMessage) {
const uniqueImplementations = new Set();

Expand Down Expand Up @@ -77,4 +94,75 @@ module.exports = class ProjectWideDependencyChecker {

throw new SilentError(message);
}

check(constraints) {
const names = Object.keys(constraints);
const addons = this.filterAddonsByNames(names);
const node_modules = Object.create(null);

for (let name in addons) {
const found = addons[name];
const versions = found.map(addon => addon.pkg.version);

const constraint = constraints[name];
const missing = versions.length === 0;
const isSatisfied =
!missing &&
versions.every(version => semver.satisfies(version, constraint));

let message;
if (isSatisfied) {
message = '';
} else if (missing) {
message = `'${name}' was not found, expected version: [${constraint}]`;
} else {
message = `'${name}' expected version: [${constraint}] but got version${
versions.length > 1 ? 's' : ''
}: [${versions.join(', ')}]`;
}

node_modules[name] = {
stefanpenner marked this conversation as resolved.
Show resolved Hide resolved
versions,
isSatisfied,
message,
};
}

return new Check(node_modules);
}
};

class Check {
constructor(node_modules) {
this.node_modules = node_modules;
Object.freeze(this);
}

get isSatisfied() {
return Object.values(this.node_modules).every(
node_module => node_module.isSatisfied
);
}

get message() {
let result = '';

for (const name in this.node_modules) {
const { message } = this.node_modules[name];
if (message !== '') {
result += ` - ${message}${EOL}`;
}
}

return result;
}

assert(description = 'Checker Assertion Failed') {
if (this.isSatisfied) {
return;
}
throw new Error(
`[Ember-cli-version-checker] ${description}\n${this.message}`
);
}
}
Loading