-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtree.js
More file actions
243 lines (213 loc) · 7.79 KB
/
tree.js
File metadata and controls
243 lines (213 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/**
* Thanks to Caolan McMahon's work on Jam on which this file is based.
* https://github.com/caolan/jam/
*/
/**
* Manages version trees according to range requirements and available
* version sets
*/
var semver = require('semver'),
events = require('events'),
async = require('async'),
versions = require('./utils/versions'),
_ = require('underscore')._;
/**
* Build a new version tree.
*
* @param {Object} pkg - the values from package.json for the root package
* @param {Array} sources - and array of functions for getting more available
* versions of packages. Source functions accept the package name and a
* callback, and should return an object containing package.json values keyed
* by version number (see the versions property in the returned version tree
* below). Subsequent source functions are only called if the previous
* fails to satisfy the version requirements,
* @param {Function} callback - called when processing is complete, passed an
* optional error as the first argument and a version tree as the second.
*
* Returned version tree format:
* {
* foo: {
* versions: {
* '0.0.1': {config: <package.json>, source: <source>},
* '0.0.2': {config: <package.json>, source: <source>}
* },
* current_version: '0.0.2',
* ranges: {bar: '>= 0.0.2'}
* },
* bar: {
* versions: {
* '0.0.1': {config: <package.json>, source: <source>}
* },
* current_version: '0.0.1',
* ranges: {}
* }
* }
*/
exports.build = function (pkg, sources, callback) {
var packages = {};
if (pkg.source !== 'root' && pkg.config.name) {
packages[pkg.config.name] = exports.createPackage([]);
}
exports.extend(pkg, sources, packages, callback);
};
/**
* Extends a version tree with new information when a new package is added or
* a package version changes.
*
* @param {Object} pkg - package.json values for the updated package
* @param {Array} sources - an array of source functions (see exports.build)
* @param {Object} packages - the existing version tree to extend
* @param {Function} callback
*/
exports.extend = function (pkg, sources, packages, callback) {
debugger;
var name = pkg.config.name;
var version = pkg.config.version;
if (pkg.source !== 'root') {
if (!packages[name]) {
packages[name] = exports.createPackage([]);
}
packages[name].versions[version] = pkg;
packages[name].current_version = version;
}
var cfg = pkg.config;
var depobj = cfg.webDependencies;
if (cfg.browser && cfg.browser.dependencies) {
depobj = cfg.browser.dependencies;
}
var dependencies = Object.keys(depobj || {});
if (!dependencies.length) {
return callback(null, packages);
}
async.forEach(dependencies, function (dep, cb) {
exports.addDependency(
pkg.config.name, // parent name
dep, // dependency name
depobj[dep], // dependency version range
sources, // package sources
packages, // version tree
cb // callback
);
},
function (err) {
callback(err, packages);
});
};
/**
* Add a new dependency to an existing version tree. This will create a new
* package object in the tree with the provided range then use the source
* functions to resolve it.
*
* @param {String} parent - the name of the package which depends on this one
* @param {String} name - the name of the dependency package
* @param {String} range - the acceptable version range for the new package
* @param {Array} sources - an Array of source functions (see exports.build)
* @param {Object} packages - the version tree object to update
* @param {Function} cb - callback function
*/
exports.addDependency = function (parent, name, range, sources, packages, cb) {
debugger;
if (!packages[name]) {
packages[name] = exports.createPackage(sources);
}
var dep = packages[name];
var curr = dep.current_version;
dep.ranges[parent || '_root'] = range;
if (!curr || !versions.satisfiesAll(curr, Object.keys(dep.ranges))) {
var available = Object.keys(dep.versions);
var ranges = _.values(dep.ranges);
var match = versions.maxSatisfying(available, ranges);
if (match) {
dep.current_version = match;
exports.extend(dep.versions[match], sources, packages, cb);
}
else {
return exports.updateDep(name, dep, function (err) {
if (err) {
return cb(err);
}
// re-run iterator with original args now there are
// new versions available
return exports.addDependency(
parent, name, range, sources, packages, cb
);
});
}
}
};
/**
* Updates a package object in the version tree (in-place) adding new versions
* from the next available source. If an update is already in progress, it will
* call the callback once the in-progress one has completed instead of calling
* the next source function.
*
* @param {String} name - the name of the package to find new versions for
* @param {Object} dep - the package object in the version tree to update
* @param {Function} callback
*/
exports.updateDep = function (name, dep, callback) {
debugger;
if (dep.update_in_progress) {
return dep.ev.once('update', callback);
}
else if (dep.sources.length) {
var fn = dep.sources.shift();
if (!dep.ev) {
dep.ev = new events.EventEmitter();
dep.ev.setMaxListeners(10000);
}
dep.update_in_progress = true;
return fn(name, function (err, versions) {
if (err) {
return callback(err);
}
// keep existing versions, only add new ones
dep.versions = _.extend(versions, dep.versions);
dep.update_in_progress = false;
dep.ev.emit('update');
// re-run iterator with original args now there are
// new versions available
return callback();
});
}
else {
return callback(exports.dependencyError(name, dep));
}
};
/**
* Creates a new empty package object for adding to the version tree. The
* sources array is cloned so it can be manipulated to keep track of
* un-checked sources for this package.
*
* @param {Array} sources - the original array of available source functions
* @returns {Object}
*/
exports.createPackage = function (sources) {
return {
versions: {},
ranges: {},
sources: sources.slice()
};
};
/**
* Creates a suitable Error when a dependency cannot be met. The error message
* will list available version numbers and the requirements other packages had
* if appropriate. The Error object is *returned* not thrown.
*
* @param {String} name - the name of the package which failed to resolve
* @param {Object} dep - the package object from the version tree
* @returns {Error}
*/
exports.dependencyError = function (name, dep) {
if (!Object.keys(dep.versions).length) {
return new Error("No package for '" + name + "'");
}
var ranges = '';
for (var r in dep.ranges) {
ranges += '\t' + r + ' requires ' + dep.ranges[r] + '\n';
}
return new Error("No matching version for '" + name + "'\n\n" +
"Available versions:\n\t" +
Object.keys(dep.versions).join(", ") + '\n\n' +
"Requirements:\n" + ranges);
};