cortex
Version:
Cortex is an npm-like package manager for browsers.
443 lines (372 loc) • 10.9 kB
JavaScript
'use strict';
var node_path = require('path');
var node_url = require('url');
var mix = require('mix2');
var cortex_json = require('read-cortex-json');
var async = require('async');
var shrinked = require('shrinked');
var fs = require('fs');
var deps_helper = require('../util/deps');
////////////////////////////////////////////////////////////////////////
// See ./README.md for details
////////////////////////////////////////////////////////////////////////
exports.shorthands = {
c: 'cwd',
r: 'recursive',
g: 'global',
f: 'force'
};
exports.options = {
// Indicates that user want to save something into package.json
// _save: {
// type: Boolean,
// enumerable: false,
// default: false
// },
recursive: {
type: Boolean,
info: 'install all dependencies recursively. If `false`, cortex will only download the current package.',
default: true
},
'stable-only': {
type: Boolean,
info: 'only allow to install dependencies of stable versions, if not found, it will throw.'
},
cwd: {
type: node_path,
info: 'current working directory.',
// `cortex install` could be executed anywhere
default: process.cwd(),
set: function(cwd) {
// Save the origin cwd
this.set('_cwd', cwd);
var done = this.async();
// if not inside a project, returns null
cortex_json.package_root(cwd, function (cwd) {
done(null, cwd);
});
}
},
'global': {
enumerable: false,
type: Boolean,
info: 'install packages globally.'
},
// if --save --save-async, --save-async will be ignored
save: {
type: Boolean,
info: 'package will appear in your "cortex.dependencies" of cortex.json.',
set: save_check_package
},
'save-async': {
type: Boolean,
info: 'package will appear in your "cortex.asyncDependencies" of cortex.json.',
set: save_check_package
},
'save-dev': {
type: Boolean,
info: 'package will appear in your "cortex.devDependencies" of cortex.json.',
set: save_check_package
},
clone: {
type: Boolean,
info: 'try to clone the package repo down to the workspace.'
},
production: {
type: Boolean,
info: 'ignore "devDependencies". Only works when `cortex install` runs on local without arguments.',
set: function (production) {
// When user executes `cortex install --production`,
// cortex will not install devDependencies
var keys = this.get('production')
? deps_helper.PRODUCTION_DEP_KEYS
: deps_helper.NON_PRODUCTION_DEP_KEYS;
this.set('_keys', keys);
return production;
}
},
_desc: {
enumerable: false,
type: String,
info: 'which will display at the beginning',
default: 'installing'
},
_update: {
enumerable: false,
type: Boolean,
info: 'if true, cortex will not read cortex-shrinkwrap.json'
},
_init: {
enumerable: false,
type: Boolean,
info: 'which indicates that it is to install packages from cortex.json'
},
packages: {
enumerable: false,
// command line type: String
// programmatical type: Array.<string>
info: 'packages to install. if not specified, cortex will read them from your package.json. if you install a package without version, cortex will try to install the latest one.',
set: function(packages) {
var done = this.async();
exports._deal_packages(packages, this, done);
}
},
prerelease: {
enumerable: false,
info: 'limit to a certain pre-release.'
},
force: {
type: Boolean,
info: 'force installing existing packages.'
}
};
function save_check_package(save) {
var done = this.async();
// There must be only one save type
if (this.get('_save')) {
return done(null, false);
}
// if --save, package.json must exists
if (save) {
// NS: see ./README.md
var g = this.get('global');
if (g) {
return done({
code: 'INSTALL_SAVE_GLOBAL',
message: '`cortex install --global --save` makes no sense.'
});
}
// CNS: see ./README.md
var cwd = this.get('cwd');
// 'cortex install' might be called inside a subtle directory of the project.
if (cwd === null) {
return done({
code: 'SAVE_CORTEX_JSON_NOT_FOUND',
message: 'cortex.json not found, could not save dependencies'
});
}
// options._save: internal use
// Indicates `cortex install` is going to save dependencies of either type
this.set('_save', true);
}
done(null, save);
}
// @param {Object} options
// - pkg
// - cwd
// - save
// - remain
exports._deal_packages = function(packages, self, callback) {
// Invalid option
// ```
// cortex install --packages
// ```
if (packages === true) {
return callback({
code: 'INSTALL_INVALID_PKGS',
message: 'invalid value of option --packages, which must be specified.',
data: {
option: 'packages'
}
});
}
// 1.
// Install packages which explicitly defined in option --packages
// ```
// # could be executed anywhere
// cortex install --packages ajax,lang
// ```
var parsed_packages = [];
if (packages) {
parsed_packages = packages.split(/\s*,\s*/);
}
// 2.
// Install packages in argv.remain
// ```
// # could be executed anywhere
// cortex install ajax lang
// ```
var remain = self.get('_');
if (!parsed_packages.length && remain.length) {
parsed_packages = remain;
}
var g = self.get('global');
var package_root = self.get('cwd');
var cwd = self.get('_cwd');
var save = self.get('_save');
// NP, see ./README.md
if (!package_root && !parsed_packages.length) {
return callback({
code: 'CAN_NOT_READ_DEPS',
message: 'Can not read dependencies. '
+ 'Cortex can\'t find a "cortex.json" file in "' + cwd + '".',
data: {
cwd: cwd
}
});
}
if (parsed_packages.length) {
// NC(G), see ./README.md
if (g) {
return callback(null, exports._santitize(parsed_packages));
}
// NC(A), see ./README.md
if (!package_root) {
self.set('cwd', cwd);
return callback(null, exports._santitize(parsed_packages));
}
}
// Else, we need to check current packages
//////////////////////////////////////////////////////////////////
cortex_json.read(package_root, function (err, pkg) {
if (err) {
return callback(err);
}
var deps = exports._deps_from_pkg(pkg, self.get('_keys'));
var name = pkg.name;
if (name in deps) {
return callback({
code: 'DEPEND_ON_SELF',
message: 'Invalid dependencies, '
+ '"' + name + '" can {{bold NOT}} depend on itself.',
data: {
name: name
}
});
}
// R, see ./README.md
// We will not check duplication if `cortex install` with no arguments
if (!parsed_packages.length) {
// Ignore --save*
// The `--save` options of `cortex install --save` actually means nothing.
// #418
// And cortex install will not force to install stable versions.
self.set('_save', false);
// #450
// Which means it will install packages from cortex.json
self.set('_init', true);
return exports._get_deps(pkg, deps, self, callback);
}
exports._check_packages(pkg, parsed_packages, save, callback);
});
};
exports._deps_from_pkg = function (pkg, keys) {
var deps = {};
// But cortex will not install
// 'asyncDependencies' and 'devDependencies' recursively which handled by neuropil.
keys.forEach(function(key) {
mix(deps, pkg[key]);
});
return deps;
};
exports._get_deps = function (pkg, deps, self, callback) {
var keys = self.get('_keys');
function deps_from_pkg () {
callback(null, exports._make_deps_array(deps));
}
// `cortex update` with no arguments
// then we should not read packages from cortex-shrinkwrap.json
if (self.get('_update')) {
return deps_from_pkg();
}
var cwd = self.get('cwd');
var shrinkwrap_json = node_path.join(cwd, 'cortex-shrinkwrap.json');
fs.exists(shrinkwrap_json, function (exists) {
if (!exists) {
return deps_from_pkg();
}
self.set('_desc', 'installing from shrinkwrap:');
self.set('recursive', false);
return exports._deps_from_shrinkwrap(shrinkwrap_json, pkg, keys, callback);
});
};
exports._deps_from_shrinkwrap = function (file, pkg, keys, callback) {
shrinked(file, {
// #413
// Actually, shrinked tree does not contains devDependencies for now
dependencyKeys: keys
}, function (err, tree) {
if (err) {
return callback(err);
}
var pkg_name = pkg.name;
var packages = Object.keys(tree).reduce(function (prev, name) {
// Should not install itself
if (name === pkg_name) {
return prev;
}
var versions = Object.keys(tree[name]);
var pkgs = versions.map(function (version) {
return name + '@' + version;
});
return prev.concat(pkgs);
}, []);
var dev_deps = exports._deps_from_pkg(pkg, ['devDependencies']);
packages = packages.concat(exports._make_deps_array(dev_deps));
callback(null, packages);
});
};
exports._make_deps_array = function(map) {
var deps = [];
// -> ['a@0.0.2']
Object.keys(map).forEach(function(name) {
deps.push(name + '@' + map[name]);
});
return deps;
};
exports._santitize = function(packages) {
return packages.map(function(p) {
var splitted = p.split('@');
return splitted[0] + '@' + (splitted[1] || '*');
});
};
exports._check_packages = function(pkg, packages, save, callback) {
var pkg_name = pkg.name;
var error;
var map = {};
function cb (err, packages) {
if (error) {
return;
}
if (err) {
error = true;
}
callback(err, packages);
}
packages = packages.map(function (p) {
if (error) {
return;
}
var splitted = p.split('@');
var name = splitted[0];
// CS, see ./README.md
if (pkg_name === name) {
return ~cb({
code: 'INSTALL_SELF',
message: 'Refusing to install "' + pkg_name + '" as a dependency of itself.',
data: {
name: name
}
});
}
// CV, see ./README.md
if (save && (name in map)) {
return ~cb({
code: 'SAVE_MULTI_VERSIONS',
message: 'With the "--save" option, '
+ 'installing more than one version of the package "' + name + '" is prohibited.',
data: {
name: name
}
});
}
map[name] = true;
return name + '@' + (splitted[1] || '*');
});
cb(null, packages);
};
exports.info = 'Install specified packages or install packages from package.json';
exports.usage = [
'{{name}} install <package>[,<package>[,...]]',
'{{name}} install [options]'
];