-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.js
389 lines (334 loc) · 12.9 KB
/
index.js
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
'use strict';
/**!
* canihaz.js: Optional, Async installation of npm packages. No more bloated
* dependencies list that is not used every time.
*
* @copyright (c) 2012 observe.it (observe.it) <opensource@observe.it>
* MIT Licensed
*/
/**
* Native modules.
*/
var queue = new(require('events').EventEmitter)
, exec = require('child_process').exec
, path = require('path')
, fs = require('fs');
/**
* Third party modules.
*/
var mkdirp = require('mkdirp')
, semver = require('semver')
, which = require('which')
, npm;
// Find the location of the npm installation so we can spawn it
try { npm = which.sync('npm'); }
catch (e) { npm = 'npm'; }
// Set the maxListeners to 100 as we might have 10 concurrent installation
// requests.
queue.setMaxListeners(100);
/**
* Simple helper function for debugging, if no process.env.DEBUG is set it will
* degrade to emiting the events over the queue.
*
* @type {Function}
*/
var debug = process.env
&& process.env.DEBUG
&& ~process.env.DEBUG.indexOf('canihaz')
? console.log.bind(console, '[debug::canihaz]')
: function debug(line) {
queue.emit('canihaz debug', line);
};
/**
* Require all the things.
*
* Options:
* - dot: Should we create a special dot folder for storage? This is saved in
* the home directory of the user. Should be a string.
* - home: The location of the home folders, as this is operating system
* specific or you might want to override this if you want to store the dot
* folder in a different location. Should be string.
* - location: The location of the package.json that we need to parse and read out
* the possible dependencies for lazy installation.
* - key: Which property should we scan for the optional dependencies? This
* allows you to also lazy install optionalDependencies for example.
* - resuse: Should expose an interface for reusing node_modules that are
* bundled in dependencies? This allows you to cut down on dependencies even
* more.
*
* @param {Object} config
* @api public
*/
module.exports = function canihazitplxktnxilubai(config) {
config = config || {};
var configuration = {
dot: config.dot || false
, home: config.home || process.env.HOME || process.env.USERPROFILE
, location: config.location || path.resolve(__dirname, '../..')
, key: config.key || 'canihaz'
, reuse: false // will be used later
};
// The installation location, this will be replaced if the dot folder option
// is set.
configuration.installation = config.installation || configuration.location;
// For legacy reasons we accept a string as argument which would be used as
// dot folder.
if (typeof config === 'string') configuration.dot = config;
if (configuration.dot) {
configuration.installation = path.resolve(
configuration.home
, '.'+ configuration.dot
);
}
// Parse out the dependencies from the package.json, so we could expose them
// to our exporting statement.
var dependencies = {};
try {
dependencies = require(
path.join(configuration.location, 'package.json')
)[configuration.key];
debug('found dependencies in the package.json: '+ JSON.stringify(dependencies, null, 2));
} catch (e) {}
/**
* Install all the things.
*
* @param {String} name
* @param {String} version
* @param {Function} callback
*/
function has(name, version, cb) {
var regular = typeof name === 'string'
&& typeof version === 'string'
&& typeof cb === 'function'
&& version === '' || semver.satisfies(version);
if (!regular) {
var args = Array.prototype.slice.call(arguments, 0)
, fetched = {}
, order = []
, error;
cb = args.pop();
debug('doing a multi installation for :'+ JSON.stringify(args));
return args.forEach(function install(lib) {
var name, version, checker;
if (typeof lib === 'object') {
name = lib.name;
version = lib.version;
} else {
name = lib;
version = '';
}
// Add module name to the order array, so we can return the libraries
// back in order to the callback
order.push(name);
// Check if we have a `native` version, so we can actually take
// advantage of our caching system. But if we have a version we have to
// degrade our uncached requiretron3000
if (name in has && !version) {
checker = has[name];
} else {
checker = requiretron3000.bind(undefined, configuration, name, version);
}
checker(function fetching(err, library) {
fetched[name] = library;
// If we have an Error, save it, but don't override it always have the
// same, first error stored
if (err && !error) error = err;
if (Object.keys(fetched).length < args.length) return;
var applies = [error];
// Add the libraries back in the same order as supplied in the
// arguments.
order.forEach(function add(item) {
applies.push(fetched[item]);
});
if (error) debug('failed to process multiple dependencies due to '+ error);
cb.apply(cb, applies);
});
});
}
// Defer the call the requiretron3000 and make it happen
debug('installing '+ name +'@'+ version);
return requiretron3000(configuration, name, version, cb);
}
// Expose the dependencies in a better API format by adding them to the
// returned function. This allows you to do:
//
// canihaz.modulename(function () { .. });
//
// instead of doing:
//
// canihaz('modulename', '0.0.x', function () { .. });
//
// So it has much better API and does automatic version resolution which can
// be managed from one single location, and that is the package.json file of
// the module that uses this module.
Object.keys(dependencies).forEach(function iterate(name) {
var version = dependencies[name]
, cache;
Object.defineProperty(has, name, {
value: function findPackage(callback) {
if (cache) return process.nextTick(function loadCacheAsync() {
debug('cache hit for '+ name +'@'+ version);
callback(undefined, cache);
});
requiretron3000(configuration, name, version, function installed(err, pkg) {
if (pkg && !err) cache = pkg;
callback(err, pkg);
});
}
});
});
return has;
};
/**
* Expose the queue so we can do some testing against it.
*
* @type {EventEmitter}
* @api private
*/
module.exports.queue = queue;
/**
* Proudly introducing the requiretron3000, the brand spanking new require system
* that automatically installs dependencies that are not installed.
*
* @param {Object} config the configuration object of the module
* @param {String} name the name of the module that needs to be installed
* @param {String} version the version of the module
* @param {Function} cb
* @api private
*/
function requiretron3000(config, name, version, cb) {
var pkgversion
, x;
// Try to require the module, to see if it's installed, maybe globally or what
// ever.. somewhere else.. but in order to check if it satisfies the version
// number we need to find the path, parse the package.json and see if it's
// a correct match
try {
x = require.resolve(name);
pkgversion = require(path.join(
// Resolve returns the full path of where the entry point of
// a module is so we need to find the `root` folder of the module
x.slice(0, x.lastIndexOf(name))
, 'node_modules', name, 'package.json'
)).version;
// Make sure it satisfies the semver, if it does, require all the things as
// we have a match, whoop whoop
if (!version || semver.satisfies(pkgversion, version)) {
return cb(undefined, require(name));
}
} catch (e) {}
// Oh, okay, maybe it's not installed there ;( NEXT!
try {
x = path.join(config.installation, 'node_modules', name);
pkgversion = require(path.join(x, 'package.json')).version;
if (!version || semver.satisfies(pkgversion, version)) {
return cb(undefined, require(x));
}
} catch (e) {}
// Well, fuck, not installed there either ;9 so we should install it after we
// have ensured that we have an installation directory available for this
// module
ensure(config.installation, function ensured(err) {
if (err) return cb(err);
install(config.installation, name, version || '', cb);
});
}
/**
* Install the package in the given location
*
* @param {String} cwd current working directory where we spawn the child
* @param {String} name name of the npm module to install
* @param {String} version version number or supply an empty string
* @param {Function} cb callback, all done <3
* @api private
*/
function install(cwd, name, version, cb) {
var noRegistry;
if (version) {
// * installations is basically just an installation without a version, so
// just ignore this version
if (version === '*') version = '';
// If it doesn't get through the `validRange` it's probably not a Semver but
// an url like git@github or a tarball
if (version && semver.validRange(version)) {
// If the version number contains spaces we need to wrap it in double quotes
// as we are most likely dealing with version range installation that contains
// silly shit such as: >=0.1.0 <0.2.0
if (version && /\s/.test(version)) version = '"'+ version +'"';
if (version) version = '@'+ version;
} else if (version) {
// This is probably not a valid version but a tarbal or git url, we cannot
// override the name variable or we cannot require it again so we have to
// use a other variable to change the "name" that we want to install
noRegistry = version;
version = '';
}
}
var installation = (noRegistry || name) + version;
// Check if we already have an installation running for this module, if so, we
// are gonna register a listener for the installation
if (queue.listeners(installation).length) return queue.once(installation, cb);
// No fresh installations for this module, so add a listener so we can flag
// the queue that we have installations waiting for this module.
queue.once(installation, cb);
// Spawn the npm binary and have it do it's magic, this way it's using the
// users configured defaults and we don't have to worry about that. If this
// installation doesn't work, the regular installation wouldn't have worked
// either. Wrapping in qoutes is required for Windows compatibility.
var command = '"'+ npm+'"';
// Please note that we need to add command flags BEFORE add the install
// command, if it's appended behind, it could cause installations to fail.
// I've seen this happen with git based installations
command += ' --parseable'; // Parsable output
command += ' --no-save'; // Prevent npm > @5 to install dependencies.
command += ' install '+ installation.trim();
debug('spawning npm: '+ command + ', in cwd: '+ cwd);
exec(command
, {
cwd: cwd // Where should we spawn the installation
}
, function done(err, stdout, stderr) {
var library;
try { library = require(path.join(cwd, 'node_modules', name)); }
catch(e) {
// only set the error if we don't have an error all ready
if (!err) err = e;
}
if (err) debug('installation of '+ name + ' generated an issue: '+ err);
if (stderr) debug('installation of '+ name + ' generated stderr: '+ stderr);
queue.emit(installation, err, library);
}
);
}
/**
* Ensure that the directory exists, if not create it..
*
* @param {String} dir
* @param {Function} cb
* @api private
*/
function ensure(dir, cb) {
// npm has a really funky way of figuring out where to put the dependencies,
// if the folder doesn't have a `node_modules` folder it tries to figure out
// if there is a node_modules folder in the parent and it will attempt to
// install the module there. This will cause a miss match for us we because we
// are requiring modules from an absolute path, not the npm magical path. To
// combat this behaviour we need to make sure that we also create the
// `node_modules` folder in the given directory. So npm knows that we really,
// REALLY want to install it in the `cwd` that we gave the spawned process
dir = path.join(dir, 'node_modules');
// If the fs.stat returns an error we are pretty sure that it doesn't exist,
// so we should create it
fs.stat(dir, function exists(err, stat) {
if (err) return mkdirp(dir, function mkdir(err) {
if (err) return cb(new Error('Failed to create or locate the path'));
debug('successfully generated the dependencies directory ' + dir);
cb();
});
if (!stat.isDirectory()) {
debug('we cant install dependencies in the supplied path, its not a folder');
return cb(new Error('The given path should be a directory'));
}
cb();
});
}