require-analyzer.js | |
---|---|
/*
* require-analyzer.js: Determine dependencies for a given node.js file, directory tree, or module.
*
* (C) 2010, Nodejitsu Inc.
*
*/
var util = require('util'),
path = require('path'),
fs = require('fs'),
path = require('path'),
events = require('events'),
spawn = require('child_process').spawn,
npm = require('npm'),
npmout = require('npm/lib/utils/output'),
npmls = require('npm/lib/utils/read-installed'),
semver = require('semver'),
findit = require('findit');
var analyzer = exports,
_write = npmout.write; | |
Create the list of | var core = {};
Object.keys(process.binding('natives')).forEach(function (mod) {
core[mod] = true;
}); |
function analyze (options, callback)@options {Object} Options to analyze against@callback {function} Continuation to respond to when complete.Calls the appropriate | analyzer.analyze = function (options, callback) {
var emitter = new events.EventEmitter();
if (!options || !options.target) { |
If there are no | callback(new Error('options and options.target are required'));
return emitter;
}
|
Stat the directory and call the appropriate method
on | fs.stat(options.target, function (err, stats) {
if (err) {
return callback(err);
}
var analyzeFn, rootDir;
if (stats.isDirectory()) {
analyzeFn = analyzer.dir;
}
else if (stats.isFile()) {
analyzeFn = analyzer.file;
}
else {
return callback(new Error(target + ' is not a file or a directory.'));
}
analyzeFn.call(null, options, function (err, deps) {
if (err) {
emitter.emit('childError', err);
}
|
Emit the | emitter.emit('dependencies', deps);
if (options.npm === false) {
return callback(null, deps);
}
var npmEmitter = analyzer.npmAnalyze(deps, options, function (nerr, reduced, suspect) {
return callback(err || nerr, reduced, suspect);
});
|
Re-emit the | ['search', 'reduce'].forEach(function (ev) {
npmEmitter.on(ev, function () {
var args = Array.prototype.slice.call(arguments);
args.unshift(ev);
emitter.emit.apply(emitter, args);
});
});
});
});
return emitter;
}; |
function npmAnalyze (deps, options, callback)@deps {Array} List of dependencies to analyze.@options {Object} Set of options to analyze with.@callback {function} Continuation to respond to when complete.Analyzes the list of dependencies using | analyzer.npmAnalyze = function (deps, options, callback) {
var emitter = new events.EventEmitter(),
pkgs = {};
analyzer.findModulesDir(options.target, function (err, root) { |
Setup npm options | options.npm = {
prefix: root,
exit: false
}; |
Monkey patch | npmout.write = function () {
var args = Array.prototype.slice.call(arguments),
callback;
args.forEach(function (arg) {
if (typeof arg === 'function') {
callback = arg;
}
});
callback();
};
npm.load(options.npm, function (err, npm) {
if (err) {
return callback(err);
} |
Analyze dependencies by searching for all installed locally via npm.
Then see if it depends on any other dependencies that are in the
list so those dependencies may be removed (only if | npmls(root, function (err, result) {
if (err) {
return callback(err);
}
else if (!result || !result.dependencies) {
return callback(null);
}
Object.keys(result.dependencies).forEach(function (pkg) {
if (!deps || deps.indexOf(pkg) !== -1) {
pkgs[pkg] = result.dependencies[pkg];
}
});
emitter.emit('search', pkgs);
if (!options.reduce) {
npmout.write = _write;
return callback(null, pkgs);
}
var reduced = analyzer.merge({}, pkgs),
suspect = {};
deps.forEach(function (dep) {
if (pkgs[dep] && pkgs[dep].dependencies) {
Object.keys(pkgs[dep].dependencies).forEach(function (cdep) {
if (reduced[cdep]) {
suspect[cdep] = pkgs[cdep];
delete reduced[cdep];
}
});
}
});
emitter.emit('reduce', reduced, suspect);
npmout.write = _write;
callback(null, reduced, suspect);
});
});
});
return emitter;
}; |
function package (dir, callback)@dir {string} Parent directory to analyze@callback {function} Continuation to respond to when complete.Checks for the existance of a package.json in the specified | analyzer.dir = function (options, callback) { |
Read the target directory | fs.readdir(options.target, function (err, files) {
if (err) {
return callback(err);
}
|
If there is a package.json in the directory
then analyze the require(s) based on | if (files.indexOf('package.json') !== -1) {
return analyzer.package(options, callback);
}
|
Otherwise find all files in the directory tree
and attempt to run | var files = [],
done = [],
packages = {},
traversed = false,
finder = findit.find(options.target);
function onRequired () { |
Respond to the | if (traversed && files.length === done.length) {
callback(null, Object.keys(packages));
}
}
finder.on('file', function (file) { |
If the file is not | var ext = path.extname(file),
clone = analyzer.merge({}, options);
if (ext !== '.js' && ext !== '.coffee') {
return;
}
files.push(file);
clone.target = file;
analyzer.file(clone, function (err, deps) {
deps.forEach(function (dep) {
packages[dep] = true;
});
done.push(file);
onRequired();
});
});
finder.on('end', function () {
traversed = true;
onRequired();
});
});
}; |
function package (dir, callback)@dir {string} Parent path of the package.json to analyze@callback {function} Continuation to respond to when complete.Attempts to read the package.json in the specified | analyzer.package = function (options, callback) { |
Attempt to read the package.json in the current directory | fs.readFile(path.join(options.target, 'package.json'), function (err, pkg) {
if (err) {
return callback(err);
}
try { |
Attempt to read the package.json data. | pkg = JSON.parse(pkg.toString());
|
TODO (indexzero): Support more than | if (!pkg.main) {
return callback(new Error('package.json must have a `main` property.'));
}
|
Analyze the require(s) based on the | options = analyzer.clone(options);
options.target = path.join(options.target, path.normalize(pkg.main));
analyzer.file(options, function (err, deps) {
deps = deps.filter(function (d) { return d !== pkg.name });
callback(err, deps);
});
}
catch (ex) {
return callback(ex);
}
});
}; |
function file (file, callback)@file {string} Path of the node script to analyze@callback {callback} Continuation to respond to when complete.Attempts to find the packages required by the node script located at
| analyzer.file = function (options, callback) { |
Spawn the | var packages = {},
merged = {},
errs = ['Errors received when analyzing ' + options.target],
deps = spawn('node', [path.join(__dirname, '..', 'bin', 'find-dependencies'), options.target]);
function parseLines(data, prefix, fn) {
data = data.toString();
if (data !== '') {
data.toString().split('\n').filter(function (line) {
return line !== '';
}).forEach(function (line) {
if (line.indexOf(prefix) !== -1) {
line = line.replace(prefix, '');
fn(line);
}
});
}
}
deps.stdout.on('data', function (data) { |
For each line of data output from the child process remove empty lines and then add the specified packages to list of known packages. | parseLines(data, '__!load::', function (dep) {
packages[dep] = true;
});
});
deps.stderr.on('data', function (data) {
parseLines(data, '__!err::', function (line) {
errs.push(line);
});
}); |
Set the default timeout to | options.timeout = options.timeout || 5000; |
If a timeout has been set then exit the process after the specified timespan | var timeoutId = setTimeout(function () {
deps.kill();
}, options.timeout);
deps.on('exit', function () { |
Remove the timeout now that we have exited. | clearTimeout(timeoutId);
|
When the process is complete remove any Include any packages which may be of the form | packages = Object.keys(packages);
(options.raw ? packages : packages.filter(function (pkg) {
return pkg[0] !== '.' && pkg[0] !== '/' && !core[pkg];
}).map(function (pkg) {
return pkg.split('/')[0];
})).forEach(function (pkg) {
merged[pkg] = true;
});
return errs.length > 1
? callback(new Error(errs.join('\n')), Object.keys(merged))
: callback(null, Object.keys(merged));
});
}; |
function findModulesDir (target)@target {string} The directory (or file) to search up fromSearches up from the specified | analyzer.findModulesDir = function (target, callback) {
fs.stat(target, function (err, stats) {
if (err) {
return callback(err);
}
if (stats.isDirectory()) {
return fs.readdir(target, function (err, files) {
if (err) {
return callback(err);
}
if (files.indexOf('node_modules') !== -1) {
return callback(null, target);
}
});
}
else if (stats.isFile()) {
return analyzer.findModulesDir(path.dirname(target), callback);
}
});
}; |
function (target [arg1, arg2, ...])@target {Object} Object to merge intoMerges all properties in | analyzer.merge = function (target) {
var objs = Array.prototype.slice.call(arguments, 1);
objs.forEach(function(o) {
Object.keys(o).forEach(function (attr) {
if (! o.__lookupGetter__(attr)) {
target[attr] = o[attr];
}
});
});
return target;
}; |
function clone (object)@object {Object} Object to clone.Shallow clones the target | analyzer.clone = function (object) {
return Object.keys(object).reduce(function (obj, k) {
obj[k] = object[k];
return obj;
}, {});
}; |
function extractVersions (dependencies)@dependencies {Object} Set of dependencies to transformTransforms the | analyzer.extractVersions = function (dependencies) {
var all = {};
Object.keys(dependencies).forEach(function (pkg) {
var version = dependencies[pkg].version.trim().split('.'),
build = version[2].match(/^\d+(\-?[\w|\-]+)/);
version[2] = build ? version[2] : 'x';
all[pkg] = build ? '>= ' + dependencies[pkg].version : version.join('.');
});
return all;
} |
function updates (current, updated)@current {Object} Current dependencies@updated {Object} Updated dependenciesCompares the | analyzer.updates = function (current, updated) {
var updates = {
added: {},
updated: {}
};
if (!current) {
updates.updated = updated || {};
return updates;
}
else if (!updated) {
return updates;
}
|
Get the list of all added dependencies | Object.keys(updated).filter(function (key) {
return !current[key];
}).forEach(function (key) {
updates.added[key] = updated[key];
});
|
Get the list of all dependencies that have been updated | Object.keys(updated).filter(function (key) {
if (!current[key]) {
return false;
}
var left = updated[key].replace(/\<|\>|\=|\s/ig, ''),
right = current[key].replace(/\<|\>|\=|\s/ig, '');
return semver.gt(left, right);
}).forEach(function (key) {
updates.updated[key] = updated[key];
})
return updates;
};
|