Skip to content

Commit 6b7088c

Browse files
committed
Merge pull request #26 from 201-created/add-shrinkwrap
Add support for npm-shrinkwrap.json
2 parents f7ee071 + 9d6dac7 commit 6b7088c

File tree

31 files changed

+720
-49
lines changed

31 files changed

+720
-49
lines changed

README.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
ember-cli-dependency-checker [![Build Status](https://travis-ci.org/quaertym/ember-cli-dependency-checker.svg?branch=master)](https://travis-ci.org/quaertym/ember-cli-dependency-checker)
22
============================
33

4-
Ember CLI addon that checks for missing npm and bower dependencies before running ember commands
4+
An Ember CLI addon that checks for missing npm and bower dependencies before running ember commands.
55

66
### Installation
77

@@ -11,6 +11,77 @@ In your Ember CLI app (>= v1.2.0) run the following:
1111
npm install --save-dev ember-cli-dependency-checker
1212
```
1313

14+
### Usage
15+
16+
Upon being included in a project (when running `ember build` for example), the dependency checker
17+
will confirm versions according to several signals of intent:
18+
19+
* `bower.json` will be compared to the contents of `bower_components` (or your Ember-CLI
20+
configured bower directory)
21+
* `package.json` will be compared to the contents of `node_modules`. This check only
22+
takes the top-level of dependencies into account. Nested dependencies are not confirmed.
23+
* `npm-shrinkwrap.json`, if present, will be compared to the contents of `node_modules`. This
24+
is done only if a `package.json` check does not find any unsatisfied dependencies. Nested
25+
dependencies are confirmed.
26+
27+
### Shrinkwrap Workflow
28+
29+
**This workflow presumes npm v2.7.6 - v3.0.0**, though it may work well for earlier versions.
30+
31+
When installing dependencies, it is important that `npm shrinkwrap --dev` is run and the resulting
32+
`npm-shrinkwrap.json` file committed. For example, to install the [Torii](https://github.com/Vestorly/torii)
33+
library:
34+
35+
```
36+
npm install --save-dev torii
37+
npm shrinkwrap --dev
38+
git add package.json npm-shrinkwrap.json
39+
git commit -m"Install Torii"
40+
```
41+
42+
**If the npm-shrinkwrap.json file is not committed, nested dependencies cannot be confirmed**.
43+
Remembering to execute `npm shrinkwrap --dev` and commit `npm-shrinkwrap.json` is akin to committing
44+
the `Gemfile.lock` file when using Ruby's Bundler library.
45+
46+
If `ember` is run and the contents of `node_modules/` differs from the contents of `package.json`
47+
and `npm-shrinkwrap.json` an error will be raised. To resolve a difference in dependencies,
48+
you must destroy the `node_modules/` directory and re-run `npm install`. With a blank
49+
directory, `npm install` will respect the versions pinned in `npm-shrinkwrap.json`.
50+
51+
In some rare cases there may be un-resolvable conflicts between installable versions of
52+
dependencies and those pinned. Upgrading packages after deleting the `npm-shrinkwrap.json`
53+
file or changing the version of a dependency requested in `package.json` may be the only
54+
way to resolve theses cases.
55+
56+
### Deployment with Shrinkwrap
57+
58+
Ember-CLI projects may be built on Travis or another dedicated build tool like Jenkins. To
59+
ensure that versions of dependencies (including of nested dependencies) are the same during
60+
builds as they are on the authoring developer's computer, it is recommended
61+
that you confirm dependencies before a build. Do this by running `ember version` to
62+
begin a dependency check, then if needed clearing the `node_modules/` and `bower_components/` folder
63+
and installing dependencies. For example:
64+
65+
```
66+
ember version || (rm -rf node_modules/ bower_components/ && npm install && bower install)
67+
ember build -e production
68+
```
69+
70+
### Caveats
71+
72+
Due to the limited information available in configuration files and packages, git
73+
dependencies may fall out of sync. Using shrinkwrap will confirm that they are correct
74+
upon installation, but they cannot be confirmed at runtime until improvements are
75+
made to the `npm-shrinkwrap.json` file.
76+
77+
Pinning solely to versioned releases should be preferred.
78+
79+
### Tests
80+
81+
To run tests:
82+
83+
`npm test`
84+
1485
### LICENSE
1586

1687
MIT

index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
'use strict';
22

3-
module.exports = require('./lib/dependency-checker');
3+
var Reporter = require('./lib/reporter');
4+
var DependencyChecker = require('./lib/dependency-checker');
5+
6+
module.exports = function emberCliDependencyCheckerAddon(project) {
7+
var reporter = new Reporter();
8+
var dependencyChecker = new DependencyChecker(project, reporter);
9+
dependencyChecker.checkDependencies();
10+
11+
return {
12+
name: 'ember-cli-dependency-checker'
13+
};
14+
};

lib/dependency-checker.js

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,47 @@ function isUnsatisfied(pkg) {
1313
return !!pkg.needsUpdate;
1414
}
1515

16-
function EmberCLIDependencyChecker(project) {
17-
this.name = 'ember-cli-dependency-checker';
16+
function EmberCLIDependencyChecker(project, reporter) {
1817
this.project = project;
19-
this.checkDependencies();
18+
this.reporter = reporter;
2019
}
2120

2221
EmberCLIDependencyChecker.prototype.checkDependencies = function() {
23-
24-
if(alreadyChecked || process.env.SKIP_DEPENDENCY_CHECKER) {
22+
if (alreadyChecked || process.env.SKIP_DEPENDENCY_CHECKER) {
2523
return;
2624
}
2725

2826
var bowerDeps = this.readBowerDependencies();
29-
var unsatisfiedBowerDeps = bowerDeps.filter(isUnsatisfied);
27+
this.reporter.unsatisifedPackages('bower', bowerDeps.filter(isUnsatisfied));
3028

3129
var npmDeps = this.readNPMDependencies();
32-
var unsatisfiedNPMDeps = npmDeps.filter(isUnsatisfied);
30+
var unsatisfiedDeps = npmDeps.filter(isUnsatisfied);
31+
this.reporter.unsatisifedPackages('npm', unsatisfiedDeps);
32+
33+
if (unsatisfiedDeps.length === 0) {
34+
var shrinkWrapDeps = this.readShrinkwrapDeps();
35+
this.reporter.unsatisifedPackages('npm-shrinkwrap', shrinkWrapDeps.filter(isUnsatisfied));
36+
}
3337

34-
var message = '';
35-
message += this.reportUnsatisfiedPackages('npm', unsatisfiedNPMDeps);
36-
message += this.reportUnsatisfiedPackages('bower', unsatisfiedBowerDeps);
3738
EmberCLIDependencyChecker.setAlreadyChecked(true);
3839

39-
if (message !== '') {
40-
var DependencyError = require('./dependency-error');
41-
throw new DependencyError(message);
40+
this.reporter.report();
41+
};
42+
43+
EmberCLIDependencyChecker.prototype.readShrinkwrapDeps = function() {
44+
var filePath = path.join(this.project.root, 'npm-shrinkwrap.json');
45+
if (fileExists(filePath)) {
46+
var ShrinkWrapChecker = require('./shrinkwrap-checker');
47+
var shrinkWrapBody = readFile(filePath);
48+
var shrinkWrapJSON = {};
49+
try {
50+
shrinkWrapJSON = JSON.parse(shrinkWrapBody);
51+
} catch(e) {
52+
// JSON parse error
53+
}
54+
return ShrinkWrapChecker.checkDependencies(this.project.root, shrinkWrapJSON);
55+
} else {
56+
return [];
4257
}
4358
};
4459

@@ -96,27 +111,6 @@ EmberCLIDependencyChecker.prototype.readNPMDependencies = function() {
96111
}, this);
97112
};
98113

99-
EmberCLIDependencyChecker.prototype.reportUnsatisfiedPackages = function(type, packages) {
100-
this.chalk = this.chalk || require('chalk');
101-
this.EOL = this.EOL || require('os').EOL;
102-
103-
var chalk = this.chalk;
104-
var EOL = this.EOL;
105-
var message = '';
106-
if (packages.length > 0) {
107-
message += EOL + chalk.red('Missing ' + type + ' packages: ') + EOL;
108-
109-
packages.map(function(pkg) {
110-
message += chalk.reset('Package: ') + chalk.cyan(pkg.name) + EOL;
111-
message += chalk.grey(' * Specified: ') + chalk.reset(pkg.versionSpecified) + EOL;
112-
message += chalk.grey(' * Installed: ') + chalk.reset(pkg.versionInstalled || '(not installed)') + EOL + EOL;
113-
});
114-
115-
message += chalk.red('Run `'+ type +' install` to install missing dependencies.') + EOL;
116-
}
117-
return message;
118-
};
119-
120114
EmberCLIDependencyChecker.setAlreadyChecked = function(value) {
121115
alreadyChecked = value;
122116
};

lib/package.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
'use strict';
22

3-
function Package(name, versionSpecified, versionInstalled) {
4-
this.name = name;
5-
this.versionSpecified = versionSpecified;
6-
this.versionInstalled = versionInstalled;
7-
this.needsUpdate = this.updateRequired();
3+
function Package() {
4+
this.init.apply(this, arguments);
85
}
96

107
Package.prototype = Object.create({});
118
Package.prototype.constructor = Package;
129

10+
Package.prototype.init = function(name, versionSpecified, versionInstalled) {
11+
this.name = name;
12+
this.versionSpecified = versionSpecified;
13+
this.versionInstalled = versionInstalled;
14+
this.needsUpdate = this.updateRequired();
15+
};
16+
1317
Package.prototype.updateRequired = function() {
1418
if (!this.versionInstalled) {
1519
return true;

lib/reporter.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
function installCommand(type) {
4+
switch (type) {
5+
case 'npm':
6+
return 'npm install';
7+
case 'npm-shrinkwrap':
8+
return 'rm -rf node_modules/ && npm install';
9+
case 'bower':
10+
return 'bower install';
11+
}
12+
}
13+
14+
var Reporter = function() {
15+
this.messages = [];
16+
};
17+
18+
Reporter.prototype.unsatisifedPackages = function(type, packages) {
19+
this.chalk = this.chalk || require('chalk');
20+
this.EOL = this.EOL || require('os').EOL;
21+
22+
var chalk = this.chalk;
23+
var EOL = this.EOL;
24+
25+
if (packages.length > 0) {
26+
var message = '';
27+
message += EOL + chalk.red('Missing ' + type + ' packages: ') + EOL;
28+
29+
packages.forEach(function(pkg) {
30+
message += chalk.reset('Package: ') + chalk.cyan(pkg.name) + EOL;
31+
if (pkg.parents) {
32+
message += chalk.reset('Required by: ') + chalk.cyan(pkg.parents.join(' / ')) + EOL;
33+
}
34+
message += chalk.grey(' * Specified: ') + chalk.reset(pkg.versionSpecified) + EOL;
35+
message += chalk.grey(' * Installed: ') + chalk.reset(pkg.versionInstalled || '(not installed)') + EOL + EOL;
36+
});
37+
38+
message += chalk.red('Run `'+ installCommand(type) +'` to install missing dependencies.') + EOL;
39+
this.messages.push(message);
40+
}
41+
};
42+
43+
Reporter.prototype.report = function() {
44+
if (this.messages.length) {
45+
var DependencyError = require('./dependency-error');
46+
throw new DependencyError(this.messages.join(''));
47+
}
48+
};
49+
50+
module.exports = Reporter;

lib/shrinkwrap-checker.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
var readPackageJSON = require('./utils/read-package-json');
4+
var ShrinkwrapPackage = require('./shrinkwrap-package');
5+
var path = require('path');
6+
7+
var ShrinkwrapChecker = function(root, name, versionSpecified, parents){
8+
this.root = root;
9+
this.name = name;
10+
this.versionSpecified = versionSpecified;
11+
this.parents = parents;
12+
};
13+
14+
ShrinkwrapChecker.prototype.check = function() {
15+
var packageJSON = readPackageJSON(this.root) || {};
16+
var versionInstalled = packageJSON.version;
17+
18+
return new ShrinkwrapPackage(
19+
this.name, this.versionSpecified, versionInstalled, this.parents);
20+
};
21+
22+
ShrinkwrapChecker.checkDependencies = function(root, shrinkWrapJSON) {
23+
var resolvedDependencies = [];
24+
var currentNode;
25+
26+
var nodesToCheck = [{
27+
root: root,
28+
parents: [],
29+
childDependencies: shrinkWrapJSON.dependencies,
30+
name: shrinkWrapJSON.name,
31+
version: shrinkWrapJSON.version
32+
}];
33+
34+
var checker, resolved;
35+
36+
while (currentNode = nodesToCheck.pop()) {
37+
checker = new ShrinkwrapChecker(
38+
currentNode.root, currentNode.name, currentNode.version, currentNode.parents);
39+
40+
resolved = checker.check();
41+
resolvedDependencies.push(resolved);
42+
43+
if (!resolved.needsUpdate && currentNode.childDependencies) {
44+
/* jshint loopfunc:true*/
45+
var parents = currentNode.parents.concat(currentNode.name);
46+
Object.keys(currentNode.childDependencies).forEach(function(childDepName){
47+
var childDep = currentNode.childDependencies[childDepName];
48+
49+
nodesToCheck.push({
50+
root: path.join(currentNode.root, 'node_modules', childDepName),
51+
parents: parents,
52+
name: childDepName,
53+
childDependencies: childDep.dependencies,
54+
version: childDep.version
55+
});
56+
});
57+
}
58+
}
59+
60+
return resolvedDependencies;
61+
};
62+
63+
module.exports = ShrinkwrapChecker;

lib/shrinkwrap-package.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
var Package = require('./package');
4+
5+
function ShrinkwrapPackage(name, versionSpecified, versionInstalled, parents) {
6+
this._super$init.call(this, name, versionSpecified, versionInstalled);
7+
this.init(name, versionSpecified, versionInstalled, parents);
8+
}
9+
10+
ShrinkwrapPackage.prototype = Object.create(Package.prototype);
11+
ShrinkwrapPackage.prototype.constructor = ShrinkwrapPackage;
12+
13+
ShrinkwrapPackage.prototype._super$init = Package.prototype.init;
14+
ShrinkwrapPackage.prototype.init = function(name, versionSpecified, versionInstalled, parents) {
15+
this.parents = parents;
16+
};
17+
18+
ShrinkwrapPackage.prototype.updateRequired = function() {
19+
return this.versionSpecified !== this.versionInstalled;
20+
};
21+
22+
module.exports = ShrinkwrapPackage;

lib/utils/read-package-json.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
var path = require('path');
4+
var fs = require('fs');
5+
function readFile(path){
6+
if (fs.existsSync(path)) {
7+
return fs.readFileSync(path);
8+
}
9+
}
10+
11+
module.exports = function readPackageJSON(projectRoot) {
12+
var filePath = path.join(projectRoot, 'package.json');
13+
try {
14+
return JSON.parse(readFile(filePath));
15+
} catch (e) {
16+
return null;
17+
}
18+
};

tests/fixtures/project-shrinkwrap-check/node_modules/minimist/package.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/fixtures/project-shrinkwrap-check/npm-shrinkwrap.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)