├── test ├── unit │ ├── fixtures │ │ ├── invalid_json │ │ └── valid_json │ ├── test-lib-couchdb.js │ ├── test-lib-versions.js │ ├── test-lib-utils.js │ └── test-lib-commands-install.js ├── unit.bat ├── all.bat ├── integration.bat ├── integration │ ├── fixtures │ │ ├── package-one │ │ │ ├── main.js │ │ │ └── package.json │ │ ├── package-two-v2 │ │ │ ├── two.js │ │ │ └── package.json │ │ ├── package-two │ │ │ ├── two.js │ │ │ └── package.json │ │ ├── package-one-v2 │ │ │ ├── main.js │ │ │ └── package.json │ │ ├── package-one-v3 │ │ │ ├── main.js │ │ │ └── package.json │ │ ├── package-three-invalid-extjs │ │ │ ├── main.js │ │ │ └── package.json │ │ ├── package-three-invalid-characters │ │ │ ├── main.js │ │ │ └── package.json │ │ ├── project-empty │ │ │ └── README │ │ ├── project-rangedeps │ │ │ └── package.json │ │ ├── project-packagejson │ │ │ └── package.json │ │ └── project-custompaths │ │ │ └── package.json │ ├── test-publish-unpublish.js │ ├── test-emptyproject-install-compile.js │ ├── test-link.js │ ├── test-publish.js │ ├── test-emptyproject-install-rm-rebuild.js │ ├── test-emptyproject-publish-install-upgrade.js │ ├── test-packagejson-publish-install-ls-remove.js │ ├── test-emptyproject-publish-install-ls-remove.js │ ├── test-custompaths-publish-install-ls-remove.js │ └── test-rangedeps-publish-install-upgrade.js ├── unit.sh ├── all.sh ├── integration.sh └── utils.js ├── .gitignore ├── .npmrc ├── lib ├── tmpls │ └── require.config.js ├── schema │ ├── package.json │ └── package-full.json ├── commands │ ├── index.js │ ├── help.js │ ├── clear-cache.js │ ├── pack.js │ ├── unpublish.js │ ├── publish.js │ ├── link_old.js │ ├── rebuild.js │ ├── ls.js │ ├── search.js │ ├── remove.js │ ├── clean.js │ ├── compile.js │ └── upgrade.js ├── env.js ├── settings.js ├── args.js ├── github.js ├── cache.js ├── logger.js ├── tar.js ├── jamrc.js ├── versions.js ├── packages.js ├── fstream-jam.js └── project.js ├── LICENSE ├── package.json ├── bin └── jam.js ├── index.js └── README.md /test/unit/fixtures/invalid_json: -------------------------------------------------------------------------------- 1 | asdf 123 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | jam.iml 3 | .idea 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/valid_json: -------------------------------------------------------------------------------- 1 | { 2 | "one": 1, 3 | "two": 2 4 | } 5 | -------------------------------------------------------------------------------- /test/unit.bat: -------------------------------------------------------------------------------- 1 | pushd %~dp0 2 | node ..\node_modules\nodeunit\bin\nodeunit unit 3 | popd 4 | -------------------------------------------------------------------------------- /test/all.bat: -------------------------------------------------------------------------------- 1 | pushd %~dp0 2 | node ..\node_modules\nodeunit\bin\nodeunit unit integration 3 | popd 4 | -------------------------------------------------------------------------------- /test/integration.bat: -------------------------------------------------------------------------------- 1 | pushd %~dp0 2 | node ..\node_modules\nodeunit\bin\nodeunit integration 3 | popd 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-one/main.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package One'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-two-v2/two.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package Two'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-two/two.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package Two'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-one-v2/main.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package One'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-one-v3/main.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package One'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-three-invalid-extjs/main.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package Three'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-one", 3 | "version": "0.0.1", 4 | "description": "Test package one" 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-three-invalid-characters/main.js: -------------------------------------------------------------------------------- 1 | deifne(['exports'], function (exports) { 2 | exports.name = 'Package Three'; 3 | }); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-one-v2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-one", 3 | "version": "0.0.2", 4 | "description": "Test package one" 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-one-v3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-one", 3 | "version": "0.0.3", 4 | "description": "Test package one" 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/fixtures/project-empty/README: -------------------------------------------------------------------------------- 1 | This is used to test jam commands inside a fresh project without an 2 | existing package.json (either for NPM or for Jam). 3 | -------------------------------------------------------------------------------- /test/integration/fixtures/project-rangedeps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "jam": { 3 | "dependencies": { 4 | "package-two": "<=0.0.2" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-three-invalid-characters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-@", 3 | "version": "0.0.1", 4 | "description": "Test package three" 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-three-invalid-extjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-three.js", 3 | "version": "0.0.1", 4 | "description": "Test package three" 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; 'npm config ls -l' to show all defaults. 2 | ; python = "python2.7" 3 | 4 | ; @see http://dragonballs.dev.mail.ru:4874/ 5 | registry = "http://dragonballs.dev.mail.ru:5984/registry/_design/app/_rewrite" 6 | -------------------------------------------------------------------------------- /test/integration/fixtures/project-packagejson/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "jam": { 3 | "dependencies": { 4 | "package-one": null, 5 | "package-two": null 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-two", 3 | "version": "0.0.1", 4 | "description": "Test package two", 5 | "dependencies": { 6 | "package-one": null 7 | }, 8 | "main": "two.js" 9 | } 10 | -------------------------------------------------------------------------------- /test/integration/fixtures/package-two-v2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-two", 3 | "version": "0.0.2", 4 | "description": "Test package two", 5 | "dependencies": { 6 | "package-one": "0.0.2" 7 | }, 8 | "main": "two.js" 9 | } 10 | -------------------------------------------------------------------------------- /lib/tmpls/require.config.js: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | var packages = "{data}"; 3 | 4 | if (typeof module === 'object' && module.exports) { 5 | module.exports = packages; 6 | } else if (typeof require === 'function' && require.config) { 7 | require.config("{data}"); 8 | } else if (typeof global === 'object') { 9 | global.require = packages; 10 | } 11 | })(this); 12 | 13 | -------------------------------------------------------------------------------- /test/unit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCE="${BASH_SOURCE[0]}" 4 | DIR="$( dirname "$SOURCE" )" 5 | while [ -h "$SOURCE" ] 6 | do 7 | SOURCE="$(readlink "$SOURCE")" 8 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" 9 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 10 | done 11 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 12 | NODEUNIT="node $DIR/../node_modules/nodeunit/bin/nodeunit" 13 | 14 | cd $DIR 15 | $NODEUNIT unit 16 | -------------------------------------------------------------------------------- /test/integration/fixtures/project-custompaths/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-custompaths", 3 | "version": "1.2.3", 4 | "description": "Example project using custom package path and base URL", 5 | "jam": { 6 | "packageDir": "public/js/vendor", 7 | "baseUrl": "public", 8 | "dependencies": { 9 | "package-one": null, 10 | "package-two": null 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCE="${BASH_SOURCE[0]}" 4 | DIR="$( dirname "$SOURCE" )" 5 | while [ -h "$SOURCE" ] 6 | do 7 | SOURCE="$(readlink "$SOURCE")" 8 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" 9 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 10 | done 11 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 12 | NODEUNIT="node $DIR/../node_modules/nodeunit/bin/nodeunit" 13 | 14 | cd $DIR 15 | $NODEUNIT unit integration 16 | -------------------------------------------------------------------------------- /test/integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCE="${BASH_SOURCE[0]}" 4 | DIR="$( dirname "$SOURCE" )" 5 | while [ -h "$SOURCE" ] 6 | do 7 | SOURCE="$(readlink "$SOURCE")" 8 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" 9 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 10 | done 11 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" 12 | NODEUNIT="node $DIR/../node_modules/nodeunit/bin/nodeunit" 13 | 14 | cd $DIR 15 | $NODEUNIT integration 16 | -------------------------------------------------------------------------------- /test/unit/test-lib-couchdb.js: -------------------------------------------------------------------------------- 1 | var couchdb = require('../../lib/couchdb'), 2 | logger = require('../../lib/logger'); 3 | 4 | logger.clean_exit = true; 5 | 6 | 7 | exports['default ports if none specified'] = function (test) { 8 | var db = new couchdb.CouchDB('http://hostname/dbname'); 9 | test.equal(db.instance.port, 80); 10 | var db2 = new couchdb.CouchDB('https://hostname/dbname2'); 11 | test.equal(db2.instance.port, 443); 12 | test.done(); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/schema/package.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "title": "JSON schema for NPM package.json files", 4 | "$schema": "http://json-schema.org/draft-04/schema#", 5 | 6 | "type": "object", 7 | "required": [ "name", "version" ], 8 | 9 | "properties": { 10 | "name": { 11 | "description": "The name of the package.", 12 | "type": "string" 13 | }, 14 | "version": { 15 | "description": "Version must be parseable by node-semver, which is bundled with npm as a dependency.", 16 | "type": "string" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/commands/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | help: require('./help'), 3 | search: require('./search'), 4 | install: require('./install'), 5 | upgrade: require('./upgrade'), 6 | remove: require('./remove'), 7 | compile: require('./compile'), 8 | clean: require('./clean'), 9 | ls: require('./ls'), 10 | 'clear-cache': require('./clear-cache'), 11 | publish: require('./publish'), 12 | unpublish: require('./unpublish'), 13 | link: require('./link'), 14 | pack: require('./pack'), 15 | rebuild: require('./rebuild') 16 | }; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Caolan McMahon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/commands/help.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'), 2 | logger = require('../logger'); 3 | 4 | 5 | exports.summary = 'Show help specific to a command'; 6 | 7 | exports.usage = '' + 8 | 'jam help [COMMAND]\n' + 9 | '\n' + 10 | 'Parameters:\n' + 11 | ' COMMAND The jam command to show help on\n' + 12 | '\n' + 13 | 'Available commands:\n'; 14 | 15 | 16 | exports.run = function (settings, args, commands) { 17 | // add summary of commands to exports.usage 18 | var len = utils.longest(Object.keys(commands)); 19 | 20 | for (var k in commands) { 21 | exports.usage += ' ' + utils.padRight(k, len); 22 | exports.usage += ' ' + commands[k].summary + '\n'; 23 | } 24 | 25 | if (!args.length) { 26 | console.log('Usage: ' + exports.usage); 27 | logger.clean_exit = true; 28 | } 29 | else { 30 | args.forEach(function (a) { 31 | var cmd = commands[a]; 32 | if (cmd) { 33 | console.log(cmd.summary); 34 | console.log('Usage: ' + cmd.usage); 35 | } 36 | }); 37 | logger.clean_exit = true; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /test/unit/test-lib-versions.js: -------------------------------------------------------------------------------- 1 | var versions = require('../../lib/versions'), 2 | logger = require("../../lib/logger"), 3 | Range = require('../../lib/tree').Range; 4 | 5 | //logger.level = "verbose"; 6 | 7 | exports['maxSatisfying - should return version without build meta'] = function (test) { 8 | var max, expect; 9 | 10 | max = versions.maxSatisfying(['0.10.0', '0.10.0+build.1', '0.10.0+build.2'], [ new Range((expect = "0.10.0")) ]); 11 | 12 | test.equal(max, expect, "Not equal. Actual: " + max + " Expected: " + expect); 13 | test.done(); 14 | }; 15 | 16 | exports['maxSatisfying - should return version with highest meta'] = function (test) { 17 | var max, expect; 18 | 19 | max = versions.maxSatisfying(['0.10.0', '0.10.0+build.1', (expect = '0.10.0+build.2')], [ new Range("0.10.0+build.3") ]); 20 | 21 | test.equal(max, expect, "Not equal. Actual: " + max + " Expected: " + expect); 22 | test.done(); 23 | }; 24 | 25 | exports['equalButNotMeta'] = function (test) { 26 | var warn, expect; 27 | 28 | warn = versions.equalButNotMeta('0.10.0+build.1', [ new Range((expect = "0.10.0")) ]); 29 | 30 | test.equal(warn[0], expect); 31 | test.done(); 32 | }; -------------------------------------------------------------------------------- /lib/commands/clear-cache.js: -------------------------------------------------------------------------------- 1 | var logger = require('../logger'), 2 | cache = require('../cache'); 3 | 4 | 5 | exports.summary = 'Removes packages from the local cache'; 6 | 7 | exports.usage = '' + 8 | 'jam clear-cache [PACKAGE[@VERSION]]\n' + 9 | '\n' + 10 | '* If no package is specified, all packages are cleared from the cache.\n' + 11 | '* If a package is specified without a version, all versions of that\n' + 12 | ' package are cleared.\n' + 13 | '* If a package and a version are specified, only that specific version\n' + 14 | ' is cleared.\n' + 15 | '\n' + 16 | 'Parameters:\n' + 17 | ' PACKAGE Package name to clear\n' + 18 | ' VERSION Package version to clear'; 19 | 20 | 21 | exports.run = function (settings, args, done) { 22 | var version; 23 | var name = args[0]; 24 | 25 | if (name && name.indexOf('@') !== -1) { 26 | var parts = name.split('@'); 27 | name = parts[0]; 28 | version = parts.slice(1).join('@'); 29 | } 30 | 31 | cache.clear(name, version, function (err) { 32 | if (err) { 33 | return logger.error(err); 34 | } 35 | logger.end(); 36 | 37 | if (typeof done === 'function') { 38 | done(); 39 | } 40 | 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /test/unit/test-lib-utils.js: -------------------------------------------------------------------------------- 1 | var utils = require('../../lib/utils'), 2 | path = require('path'), 3 | fs = require('fs'), 4 | child_process = require('child_process'), 5 | logger = require('../../lib/logger'); 6 | 7 | logger.clean_exit = true; 8 | 9 | exports['readJSON - valid'] = function (test) { 10 | test.expect(2); 11 | var p = __dirname + '/fixtures/valid_json'; 12 | utils.readJSON(p, function (err, settings) { 13 | test.ok(!err); 14 | test.same(settings, {one:1,two:2}); 15 | test.done(); 16 | }); 17 | }; 18 | 19 | exports['readJSON - invalid'] = function (test) { 20 | test.expect(1); 21 | var p = __dirname + '/fixtures/invalid_json'; 22 | utils.readJSON(p, function (err, settings) { 23 | test.ok(err, 'return JSON parsing errors'); 24 | test.done(); 25 | }); 26 | }; 27 | 28 | exports['padRight'] = function (test) { 29 | // pad strings below min length 30 | test.equals(utils.padRight('test', 20), 'test '); 31 | // don't pad strings equals to min length 32 | test.equals(utils.padRight('1234567890', 10), '1234567890'); 33 | // don't shorten strings above min length 34 | test.equals(utils.padRight('123456789012345', 10), '123456789012345'); 35 | test.done(); 36 | }; 37 | -------------------------------------------------------------------------------- /lib/commands/pack.js: -------------------------------------------------------------------------------- 1 | var logger = require('../logger'), 2 | settings = require('../settings'), 3 | tar = require('../tar'), 4 | path = require('path'); 5 | 6 | 7 | exports.summary = 'Create a tar.gz package'; 8 | 9 | exports.usage = '' + 10 | 'jam pack SOURCE [TARGET]\n' + 11 | '\n' + 12 | 'Parameters:\n' + 13 | ' SOURCE The directory to pack\n' + 14 | ' TARGET The .tar.gz file to create'; 15 | 16 | 17 | exports.run = function (_settings, args, commands) { 18 | if (args.length < 1) { 19 | console.log(exports.usage); 20 | logger.clean_exit = true; 21 | return; 22 | } 23 | var source = args[0]; 24 | 25 | // TODO: add package.json validation 26 | settings.load(source, function (err, cfg) { 27 | if (err) { 28 | return logger.error(err); 29 | } 30 | 31 | var target = args[1] || cfg.name + '-' + cfg.version + '.tar.gz'; 32 | 33 | // TODO: add directory to cache as name/version/package, then pack 34 | // package directory and store as name/version/package.tar.gz and 35 | // cp file to TARGET 36 | 37 | tar.create(cfg, source, target, function (err) { 38 | if (err) { 39 | return logger.error(err); 40 | } 41 | logger.end(target); 42 | }); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jamjs", 3 | "version": "0.10.0", 4 | "description": "", 5 | "maintainers": [ 6 | { 7 | "name": "Caolan McMahon", 8 | "web": "https://github.com/caolan" 9 | }, 10 | { 11 | "name": "Sergey Kamardin", 12 | "web": "https://github.com/gobwas" 13 | } 14 | ], 15 | "dependencies": { 16 | "almond": "0.2.5", 17 | "async": "~0.1.21", 18 | "chance": "^0.7.3", 19 | "fstream": "~1.0.10", 20 | "fstream-ignore": "~1.0.5", 21 | "inherits": "~1.0.0", 22 | "inherits-js": "^0.1.0", 23 | "is-my-json-valid": "^2.12.0", 24 | "json-honey": "^0.4.1", 25 | "mime": "~1.2.4", 26 | "minimatch": "~0.2.5", 27 | "mkdirp": "~0.3.2", 28 | "ncp": "~0.2.6", 29 | "prompt": "0.2.1", 30 | "request": "~2.9.202", 31 | "requirejs": "2.1.4", 32 | "rimraf": "^2.5.4", 33 | "schinquirer": "^0.1.1", 34 | "semver": "~4.3.3", 35 | "tar": "~2.2.1", 36 | "underscore": "~1.8.3" 37 | }, 38 | "devDependencies": { 39 | "cuculus": "^0.3.3", 40 | "nodeunit": "0.9.1", 41 | "sinon": "^1.14.1" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "http://github.com/caolan/jam.git" 46 | }, 47 | "engines": { 48 | "node": ">= 0.12.2" 49 | }, 50 | "bugs": { 51 | "url": "http://github.com/caolan/jam/issues" 52 | }, 53 | "bin": { 54 | "jam": "./bin/jam.js" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/env.js: -------------------------------------------------------------------------------- 1 | var isWindows = exports.isWindows = process.platform === 'win32'; 2 | exports.temp = process.env.TMPDIR || process.env.TMP || process.env.TEMP || ( isWindows ? "c:\\windows\\temp" : "/tmp" ); 3 | exports.home = ( isWindows ? process.env.USERPROFILE : process.env.HOME ); 4 | if (exports.home) { 5 | process.env.HOME = exports.home; 6 | } else { 7 | exports.home = exports.temp; 8 | } 9 | 10 | if ( isWindows ) { 11 | exports.osSep = '\\'; 12 | exports.nullDevice = 'NUL'; 13 | 14 | // Regex to split a windows path into three parts: [*, device, slash, tail] windows-only 15 | var splitDeviceRe = /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?([\\\/])?([\s\S]*?)$/; 16 | 17 | /** 18 | * Returns true if the path given is absolute. 19 | * @param {String} p1 20 | * @return {Boolean} 21 | * @api public 22 | */ 23 | exports.isAbsolute = function(p) { 24 | var result = splitDeviceRe.exec(p), 25 | device = result[1] || '', 26 | isUnc = device && device.charAt(1) !== ':'; 27 | return !!result[2] || isUnc; // UNC paths are always absolute 28 | }; 29 | 30 | } else { 31 | exports.osSep = '/'; 32 | exports.nullDevice = '/dev/null'; 33 | 34 | /** 35 | * Returns true if the path given is absolute. 36 | * @param {String} p1 37 | * @return {Boolean} 38 | * @api public 39 | */ 40 | exports.isAbsolute = function(p) { 41 | return p[0] === '/'; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /lib/settings.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'), 2 | path = require('path'), 3 | semver = require('semver'), 4 | logger = require('./logger'), 5 | schema = require('./schema/package.json'), 6 | validator = require('is-my-json-valid'), 7 | async = require('async'), 8 | validate; 9 | 10 | validate = validator(schema); 11 | 12 | exports.load = async.memoize(function (dir, callback) { 13 | var settings_file = path.resolve(dir, 'package.json'); 14 | utils.readJSON(settings_file, function (err, settings) { 15 | if (err) { 16 | callback(err); 17 | return; 18 | } 19 | try { 20 | // if there is a jam.name we must override the 21 | // package name early. 22 | if (settings.jam && settings.jam.name) { 23 | settings.name = settings.jam.name; 24 | } 25 | exports.validate(settings, settings_file); 26 | } 27 | catch (e) { 28 | return callback(e); 29 | } 30 | callback(null, settings); 31 | }); 32 | }); 33 | 34 | exports.validate = function (settings, filename) { 35 | var error; 36 | 37 | if (!validate(settings)) { 38 | error = validate.errors[0]; 39 | throw new Error("Validation error: " + error.field + " " + error.message + " at " + filename); 40 | } 41 | 42 | if (!semver.valid(settings.version)) { 43 | throw new Error( 44 | 'Invalid version number in ' + filename + '\n' + 45 | 'Version numbers should follow the format described at ' + 46 | 'http://semver.org (eg, 1.2.3 or 4.5.6-jam.1)' 47 | ); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /lib/commands/unpublish.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'), 2 | logger = require('../logger'), 3 | repository = require('../repository'), 4 | argParse = require('../args').parse, 5 | url = require('url'), 6 | urlParse = url.parse, 7 | urlFormat = url.format; 8 | 9 | 10 | exports.summary = 'Remove a published package from a repository'; 11 | 12 | exports.usage = '' + 13 | 'jam unpublish PACKAGE[@VERSION]\n' + 14 | '\n' + 15 | 'Parameters:\n' + 16 | ' PACKAGE Package name to unpublish\n' + 17 | ' VERSION Package version to unpublish, if no version is specified\n' + 18 | ' all versions of the package are removed\n' + 19 | '\n' + 20 | 'Options:\n' + 21 | ' -r, --repository Target repository URL (defaults to first value in jamrc)'; 22 | 23 | 24 | exports.run = function (settings, args) { 25 | var a = argParse(args, { 26 | 'repo': {match: ['-r', '--repository'], value: true} 27 | }); 28 | var repo = a.options.repo || settings.repositories[0]; 29 | if (process.env.JAM_TEST) { 30 | repo = process.env.JAM_TEST_DB; 31 | if (!repo) { 32 | throw 'JAM_TEST environment variable set, but no JAM_TEST_DB set'; 33 | } 34 | } 35 | 36 | var name = a.positional[0]; 37 | var version; 38 | 39 | if (!name) { 40 | return logger.error('No package name specified'); 41 | } 42 | if (name.indexOf('@') !== -1) { 43 | var parts = name.split('@'); 44 | name = parts[0]; 45 | version = parts.slice(1).join('@'); 46 | } 47 | 48 | utils.completeAuth(repo, true, function (err, repo) { 49 | if (err) { 50 | return logger.error(err); 51 | } 52 | utils.catchAuthError( 53 | repository.unpublish, repo, [name, version, a.options], 54 | function (err) { 55 | if (err) { 56 | return logger.error(err); 57 | } 58 | logger.end(); 59 | } 60 | ); 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var fork = require('child_process').fork, 2 | rimraf = require('rimraf'), 3 | path = require('path'), 4 | _ = require('underscore'); 5 | 6 | 7 | exports.runJam = function (args, /*optional*/opts, callback) { 8 | if (!callback) { 9 | callback = opts; 10 | opts = {}; 11 | } 12 | opts.silent = true; 13 | 14 | var bin = path.resolve(__dirname, '..', 'bin', 'jam.js'); 15 | var jam = fork(bin, args, opts); 16 | var stdout = '', stderr = ''; 17 | 18 | jam.stdout.on('data', function (data) { 19 | stdout += data.toString(); 20 | }); 21 | jam.stderr.on('data', function (data) { 22 | stderr += data.toString(); 23 | }); 24 | jam.on('exit', function (code) { 25 | if (code !== 0 && !opts.expect_error) { 26 | console.log(['Jam command failed', args]); 27 | console.log(stdout); 28 | console.log(stderr); 29 | return callback( 30 | new Error('Returned status code: ' + code), 31 | stdout, 32 | stderr 33 | ); 34 | } 35 | callback(null, stdout, stderr); 36 | }); 37 | jam.disconnect(); 38 | }; 39 | 40 | 41 | // Invalidates cached module and re-requires it. Used to load require.config.js. 42 | exports.freshRequire = function (p) { 43 | var cached = Object.keys(require.cache); 44 | var resolved = require.resolve(p); 45 | if (_.indexOf(cached, resolved) !== -1) { 46 | delete require.cache[resolved]; 47 | } 48 | return require(p); 49 | }; 50 | 51 | exports.myrimraf = function (p, callback, tries) { 52 | tries = tries || 50; 53 | rimraf(p, function (err) { 54 | if (err && err.code === 'EMBUSY') { 55 | if (tries) { 56 | setTimeout(function () { 57 | exports.myrimraf(p, callback, tries - 1); 58 | }, 100); 59 | return; 60 | } 61 | } 62 | return callback.apply(this, arguments); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /bin/jam.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'), 4 | utils = require('../lib/utils'), 5 | jamrc = require('../lib/jamrc'), 6 | logger = require('../lib/logger'), 7 | commands = require('../lib/commands'); 8 | 9 | 10 | var args = process.argv.slice(2); 11 | 12 | for (var i = 0; i < args.length; i += 1) { 13 | if (args[i] === '--debug') { 14 | args.splice(i, 1); 15 | logger.level = 'debug'; 16 | } 17 | 18 | if (args[i] === '--verbose') { 19 | args.splice(i, 1); 20 | logger.level = 'verbose'; 21 | } 22 | } 23 | 24 | jamrc.load(function (err, settings) { 25 | 26 | function usage() { 27 | console.log('jam COMMAND [ARGS]'); 28 | console.log(''); 29 | console.log('Available commands:'); 30 | var len = utils.longest(Object.keys(commands)); 31 | for (var k in commands) { 32 | if (!commands[k].hidden) { 33 | console.log( 34 | ' ' + utils.padRight(k, len) + ' ' + commands[k].summary 35 | ); 36 | } 37 | } 38 | logger.clean_exit = true; 39 | } 40 | 41 | if (!args.length) { 42 | usage(); 43 | } 44 | else { 45 | var cmd = args.shift(); 46 | if (cmd === '-h' || cmd === '--help') { 47 | var concrete = args.shift(); 48 | 49 | console.log('Usage:\n'); 50 | 51 | if (concrete && commands[concrete]) { 52 | console.log(commands[concrete].usage); 53 | } else { 54 | usage(); 55 | } 56 | 57 | console.log('\n'); 58 | logger.clean_exit = true; 59 | } 60 | else if (cmd === '-v' || cmd === '--version') { 61 | utils.getJamVersion(function (err, ver) { 62 | if (err) { 63 | return logger.error(err); 64 | } 65 | logger.clean_exit = true; 66 | console.log(ver); 67 | }); 68 | } 69 | else if (cmd in commands) { 70 | commands[cmd].run(settings, args, commands); 71 | } 72 | else { 73 | logger.error('No such command: ' + cmd); 74 | usage(); 75 | } 76 | } 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /lib/args.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | /** 4 | * Extracts an option from command line args array, removing the opt from the 5 | * array and returning the value. 6 | */ 7 | 8 | exports.getOpt = function (args, opt) { 9 | if (!(opt.match instanceof Array)) { 10 | opt.match = [opt.match]; 11 | } 12 | for (var i = 0; i < args.length; i++) { 13 | for (var j = 0; j < opt.match.length; j++) { 14 | if (args[i].split('=')[0] === opt.match[j]) { 15 | if (opt.value) { 16 | var val; 17 | if (args[i].indexOf('=') !== -1) { 18 | val = args[i].split('=').slice(1).join('='); 19 | args.splice(i, 1); 20 | return val; 21 | } 22 | else { 23 | val = args[i + 1]; 24 | args.splice(i, 2); 25 | return val; 26 | } 27 | } 28 | else { 29 | args.splice(i, 1); 30 | return true; 31 | } 32 | } 33 | } 34 | } 35 | if (opt.value) { 36 | return undefined; 37 | } 38 | return false; 39 | }; 40 | 41 | exports.parse = function (args, opts) { 42 | var result = {positional: args.slice(), options: {}}; 43 | for (var k in opts) { 44 | if (opts.hasOwnProperty(k)) { 45 | var val, arr = []; 46 | do { 47 | val = exports.getOpt(result.positional, opts[k]); 48 | arr.push(val); 49 | } 50 | while (val !== undefined && val !== false); 51 | 52 | arr = _.compact(arr); 53 | 54 | if (arr.length > 1) { 55 | if (opts[k].multiple) { 56 | result.options[k] = arr; 57 | } 58 | else { 59 | throw new Error('Multiple values not allowed for ' + k); 60 | } 61 | } 62 | else if (arr.length === 1) { 63 | result.options[k] = opts[k].multiple ? arr: arr[0]; 64 | } 65 | else { 66 | result.options[k] = opts[k].multiple ? []: undefined; 67 | } 68 | } 69 | } 70 | return result; 71 | }; 72 | -------------------------------------------------------------------------------- /lib/commands/publish.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'), 2 | logger = require('../logger'), 3 | couchdb = require('../couchdb'), 4 | repository = require('../repository'), 5 | argParse = require('../args').parse, 6 | url = require('url'), 7 | urlParse = url.parse, 8 | urlFormat = url.format; 9 | 10 | 11 | exports.summary = 'Publish a package to a repository'; 12 | 13 | exports.usage = '' + 14 | 'jam publish [PACKAGE_PATH]\n' + 15 | '\n' + 16 | 'Parameters:\n' + 17 | ' PACKAGE_PATH Path to package directory to publish (defaults to ".")\n' + 18 | '\n' + 19 | 'Options:\n' + 20 | ' -r, --repository Target repository URL (defaults to first value in .jamrc)'; 21 | 22 | 23 | exports.run = function (settings, args) { 24 | var a = argParse(args, { 25 | 'repo': {match: ['-r', '--repository'], value: true} 26 | }); 27 | var dir = a.positional[0] || '.'; 28 | var repo = a.options.repo || settings.repositories[0]; 29 | if (process.env.JAM_TEST) { 30 | repo = process.env.JAM_TEST_DB; 31 | if (!repo) { 32 | throw 'JAM_TEST environment variable set, but no JAM_TEST_DB set'; 33 | } 34 | } 35 | exports.publish('package', repo, dir, a.options, _.noop); 36 | }; 37 | 38 | 39 | // called by both publish and publish-task commands 40 | exports.publish = function (type, repo, dir, options, callback) { 41 | utils.completeAuth(repo, true, function (err, repo) { 42 | if (err) { 43 | return callback(err); 44 | } 45 | utils.catchAuthError(exports.doPublish, repo, [type, dir, options], 46 | function (err) { 47 | if (err) { 48 | logger.error(err); 49 | return callback(err); 50 | } 51 | 52 | logger.end(); 53 | callback(); 54 | } 55 | ); 56 | }); 57 | }; 58 | 59 | 60 | exports.doPublish = function (repo, type, dir, options, callback) { 61 | var root = couchdb(repo); 62 | root.instance.pathname = ''; 63 | root.session(function (err, info, resp) { 64 | if (err) { 65 | return callback(err); 66 | } 67 | options.user = info.userCtx.name; 68 | options.server_time = new Date(resp.headers.date); 69 | repository.publish(dir, repo, options, callback); 70 | }); 71 | }; 72 | 73 | -------------------------------------------------------------------------------- /lib/github.js: -------------------------------------------------------------------------------- 1 | var url = require('url'), 2 | request = require('request'), 3 | logger = require('./logger'), 4 | _ = require('underscore'); 5 | 6 | 7 | var uc = encodeURIComponent; 8 | 9 | 10 | exports.GITHUB_URL = 'https://api.github.com'; 11 | exports.GITHUB_RAW_URL = 'https://raw.github.com'; 12 | 13 | 14 | exports.getRaw = function (user, repo, ref, path, callback) { 15 | var req = { 16 | json: true, 17 | url: url.resolve( 18 | exports.GITHUB_RAW_URL, 19 | _([user, repo, ref, path]).compact().map(uc).join('/') 20 | ) 21 | }; 22 | request.get(req, function (err, res, data) { 23 | if (data.error) { 24 | return callback(data.error); 25 | } 26 | callback(err, data); 27 | }); 28 | }; 29 | 30 | 31 | exports.repos = {}; 32 | 33 | exports.repos.getArchiveLink = function (user, repo, format, ref, callback) { 34 | var req = { 35 | json: true, 36 | followRedirect: false, 37 | headers: { 38 | 'Accept': 'application/vnd.github.beta+json' 39 | }, 40 | url: url.resolve( 41 | exports.GITHUB_URL, 42 | _.map(['repos', user, repo, format, ref], uc).join('/') 43 | ) 44 | }; 45 | logger.debug('getting github archive link', req.url); 46 | request.get(req, function (err, res, data) { 47 | if (err) { 48 | return callback(err); 49 | } 50 | if (data && data.error) { 51 | return callback(res.error); 52 | } 53 | if (res.statusCode === 404) { 54 | return callback('GitHub repository or tag not found'); 55 | } 56 | if (!res.headers || !res.headers.location) { 57 | return callback('Failed to get archive link'); 58 | } 59 | callback(null, res.headers.location); 60 | }); 61 | }; 62 | 63 | exports.repos.search = function (q, callback) { 64 | var req = { 65 | json: true, 66 | headers: { 67 | 'Accept': 'application/vnd.github.beta+json' 68 | }, 69 | url: url.resolve( 70 | exports.GITHUB_URL, 71 | ['legacy', 'repos', 'search', uc(q)].join('/') 72 | ) 73 | }; 74 | logger.debug('searching github repositories', req.url); 75 | request.get(req, function (err, res, data) { 76 | if (err) { 77 | return callback(err); 78 | } 79 | if (data && data.error) { 80 | return callback(res.error); 81 | } 82 | callback(null, data); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | var tar = require('./tar'), 2 | env = require('./env'), 3 | jamrc = require('./jamrc'), 4 | rimraf = require('rimraf'), 5 | mkdirp = require('mkdirp'), 6 | async = require('async'), 7 | path = require('path'), 8 | fs = require('fs'); 9 | 10 | var pathExists = fs.exists || path.exists; 11 | 12 | exports.add = function (cfg, filepath, callback) { 13 | var filename = cfg.name + '-' + cfg.version + '.tar.gz'; 14 | var dir = exports.dir(cfg.name, cfg.version); 15 | var tarfile = path.resolve(dir, filename); 16 | var cachedir = path.resolve(dir, 'package'); 17 | 18 | mkdirp(dir, function (err) { 19 | if (err) { 20 | return callback(err); 21 | } 22 | async.series([ 23 | async.apply(tar.create, cfg, filepath, tarfile), 24 | async.apply(tar.extract, tarfile, cachedir) 25 | ], 26 | function (err) { 27 | callback(err, tarfile, cachedir); 28 | }); 29 | }); 30 | }; 31 | 32 | 33 | exports.get = function (name, version, callback) { 34 | var filename = name + '-' + version + '.tar.gz'; 35 | var dir = exports.dir(name, version); 36 | var tarfile = path.resolve(dir, filename); 37 | var cachedir = path.resolve(dir, 'package'); 38 | 39 | pathExists(cachedir, function (exists) { 40 | if (exists) { 41 | return callback(null, tarfile, cachedir); 42 | } 43 | else { 44 | // Package not found in cache, return null 45 | return callback(null, null, null); 46 | } 47 | }); 48 | }; 49 | 50 | 51 | exports.moveTar = function (name, version, filepath, callback) { 52 | var filename = name + '-' + version + '.tar.gz'; 53 | var dir = exports.dir(name, version); 54 | var tarfile = path.resolve(dir,filename); 55 | var cachedir = path.resolve(dir,'package'); 56 | 57 | async.series([ 58 | async.apply(mkdirp, dir), 59 | async.apply(rimraf, cachedir), 60 | async.apply(fs.rename, filepath, tarfile), 61 | async.apply(tar.extract, tarfile, cachedir) 62 | ], 63 | function (err) { 64 | if (err) { 65 | return callback(err); 66 | } 67 | callback(null, tarfile, cachedir); 68 | }); 69 | }; 70 | 71 | 72 | exports.dir = function (name, version) { 73 | var args = Array.prototype.slice.call(arguments); 74 | return path.resolve.apply(path, [jamrc.getCacheDir()].concat(args)); 75 | }; 76 | 77 | 78 | exports.clear = function (name, version, callback) { 79 | if (!callback) { 80 | callback = version; 81 | version = null; 82 | } 83 | if (!callback) { 84 | callback = name; 85 | name = null; 86 | } 87 | var dir; 88 | if (!name) { 89 | dir = exports.dir(); 90 | } 91 | else if (!version) { 92 | dir = exports.dir(name); 93 | } 94 | else { 95 | dir = exports.dir(name, version); 96 | } 97 | rimraf(dir, callback); 98 | }; 99 | 100 | 101 | exports.update = function (cfg, filepath, callback) { 102 | exports.clear(cfg.name, cfg.version, function (err) { 103 | if (err) { 104 | return callback(err); 105 | } 106 | exports.add(cfg, filepath, callback); 107 | }); 108 | }; 109 | -------------------------------------------------------------------------------- /lib/commands/link_old.js: -------------------------------------------------------------------------------- 1 | var env = require('../env'), 2 | utils = require('../utils'), 3 | logger = require('../logger'), 4 | settings = require('../settings'), 5 | project = require('../project'), 6 | install = require('./install'), 7 | mkdirp = require('mkdirp'), 8 | rimraf = require('rimraf'), 9 | path = require('path'), 10 | fs = require('fs'); 11 | 12 | 13 | var pathExists = fs.exists || path.exists; 14 | 15 | 16 | exports.summary = 'Creates a link to a development package'; 17 | 18 | exports.usage = '' + 19 | 'jam link PATH\n' + 20 | '\n' + 21 | 'Parameters:\n' + 22 | ' PATH The path to the package directory\n' + 23 | '\n' + 24 | 'Options:\n' + 25 | ' -r, --repository Source repository URL (otherwise uses values in jamrc)\n' + 26 | ' -d, --package-dir Jam package directory (defaults to "./jam")'; 27 | 28 | 29 | exports.run = function (_settings, args, commands) { 30 | var a = argParse(args, { 31 | 'repository': {match: ['-r', '--repository'], value: true}, 32 | 'target_dir': {match: ['-d', '--package-dir'], value: true} 33 | }); 34 | 35 | var opt = a.options; 36 | 37 | opt.repositories = _settings.repositories; 38 | if (a.options.repository) { 39 | opt.repositories = [a.options.repository]; 40 | // don't allow package dir .jamrc file to overwrite repositories 41 | opt.fixed_repositories = true; 42 | } 43 | 44 | if (a.positional.length < 1) { 45 | console.log(exports.usage); 46 | logger.clean_exit = true; 47 | return; 48 | } 49 | var pkg = a.positional[0]; 50 | var cwd = process.cwd(); 51 | 52 | install.initDir(_settings, cwd, opt, function (err, opt, cfg, proj_dir) { 53 | if (err) { 54 | return logger.error(err); 55 | } 56 | 57 | opt = install.extendOptions(proj_dir, _settings, cfg, opt); 58 | 59 | // load info on package about to be linked 60 | settings.load(pkg, function (err, newpkg) { 61 | if (err) { 62 | return logger.error(err); 63 | } 64 | 65 | project.addJamDependency(cfg, newpkg.name, 'linked'); 66 | var newpath = path.resolve(opt.target_dir, newpkg.name || ''); 67 | 68 | mkdirp(path.dirname(newpath), function (err) { 69 | if (err) { 70 | return logger.error(err); 71 | } 72 | utils.createLink(path.resolve(pkg), newpath, function (err) { 73 | if (err) { 74 | return logger.error(err); 75 | } 76 | install.reinstallPackages(cfg, opt, function (err) { 77 | if (err) { 78 | return logger.error(err); 79 | } 80 | project.updateRequireConfig( 81 | opt.target_dir, 82 | opt.baseurl, 83 | function (err) { 84 | if (err) { 85 | return logger.error(err); 86 | } 87 | logger.end(); 88 | } 89 | ); 90 | }); 91 | }); 92 | }); 93 | 94 | }); 95 | 96 | }); 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /lib/commands/rebuild.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var path = require('path'), 6 | tree = require('../tree'), 7 | utils = require('../utils'), 8 | install = require('./install'), 9 | project = require('../project'), 10 | logger = require('../logger'), 11 | argParse = require('../args').parse; 12 | 13 | 14 | /** 15 | * Usage information and docs 16 | */ 17 | 18 | exports.summary = 'Recreates require.js file with the latest config options'; 19 | 20 | 21 | exports.usage = '' + 22 | 'jam rebuild\n' + 23 | '\n' + 24 | 'Options:\n' + 25 | ' -d, --package-dir Package directory (defaults to "./jam")', 26 | ' -c, --config Additional require.config.js properties to be included'; 27 | 28 | 29 | /** 30 | * Run function called when "jam rebuild" command is used 31 | * 32 | * @param {Object} settings - the values from .jamrc files 33 | * @param {Array} args - command-line arguments 34 | */ 35 | 36 | exports.run = function (settings, args) { 37 | var a = argParse(args, { 38 | 'target_dir': {match: ['-d', '--package-dir'], value: true}, 39 | 'baseurl': {match: ['-b', '--baseurl'], value: true}, 40 | 'config': {match: ['-c', '--config'], value: true}, 41 | }); 42 | 43 | var opt = a.options; 44 | var cwd = process.cwd(); 45 | 46 | install.initDir(settings, cwd, opt, function (err, opt, cfg, proj_dir) { 47 | if (err) { 48 | return logger.error(err); 49 | } 50 | 51 | if (!opt.target_dir) { 52 | if (cfg.jam && cfg.jam.packageDir) { 53 | opt.target_dir = path.resolve(proj_dir, cfg.jam.packageDir); 54 | } 55 | else { 56 | opt.target_dir = path.resolve(proj_dir, settings.package_dir || ''); 57 | } 58 | } 59 | if (!opt.baseurl) { 60 | if (cfg.jam && cfg.jam.baseUrl) { 61 | opt.baseurl = path.resolve(proj_dir, cfg.jam.baseUrl); 62 | } 63 | else { 64 | opt.baseurl = path.resolve(proj_dir, settings.baseUrl || ''); 65 | } 66 | } 67 | if (!opt.config) { 68 | if (cfg.jam && cfg.jam.config) { 69 | opt.config = cfg.jam.config; 70 | } else { 71 | opt.conifg = {}; 72 | } 73 | } 74 | exports.rebuild(settings, cfg, opt, function (err) { 75 | if (err) { 76 | return logger.error(err); 77 | } 78 | logger.end(); 79 | }); 80 | }); 81 | }; 82 | 83 | 84 | exports.rebuild = function (settings, cfg, opt, callback) { 85 | exports.readPackages(settings, cfg, opt, function (err, packages) { 86 | if (err) { 87 | return logger.error(err); 88 | } 89 | // TODO: write package.json if --save option provided 90 | project.updateRequireConfig(opt.target_dir, opt.baseurl, opt.config, callback); 91 | }); 92 | }; 93 | 94 | 95 | exports.readPackages = function (settings, cfg, opt, callback) { 96 | var local_sources = [ 97 | install.dirSource(opt.target_dir), 98 | install.repoSource(settings.repositories, cfg) 99 | ]; 100 | var newcfg = utils.convertToRootCfg(cfg); 101 | var pkg = { 102 | config: newcfg, 103 | source: 'root' 104 | }; 105 | logger.info('Building local version tree...'); 106 | tree.build(pkg, local_sources, callback); 107 | }; 108 | -------------------------------------------------------------------------------- /test/integration/test-publish-unpublish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * - jam publish package-one @ 0.0.1 6 | * - jam publish package-one @ 0.0.2 7 | * - jam publish package-one @ 0.0.3 8 | * - jam unpublish package-one @ 0.0.3, check 0.0.1 and 0.0.2 still there 9 | * - jam unpublish package-one, check all version removed 10 | */ 11 | 12 | 13 | var couchdb = require('../../lib/couchdb'), 14 | logger = require('../../lib/logger'), 15 | env = require('../../lib/env'), 16 | utils = require('../utils'), 17 | async = require('async'), 18 | http = require('http'), 19 | path = require('path'), 20 | ncp = require('ncp').ncp, 21 | fs = require('fs'), 22 | _ = require('underscore'); 23 | 24 | 25 | var pathExists = fs.exists || path.exists; 26 | 27 | 28 | logger.clean_exit = true; 29 | 30 | // CouchDB database URL to use for testing 31 | var TESTDB = process.env['JAM_TEST_DB'], 32 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 33 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 34 | 35 | if (!TESTDB) { 36 | throw 'JAM_TEST_DB environment variable not set'; 37 | } 38 | 39 | // remove trailing-slash from TESTDB URL 40 | TESTDB = TESTDB.replace(/\/$/, ''); 41 | 42 | 43 | exports.setUp = function (callback) { 44 | // change to integration test directory before running test 45 | this._cwd = process.cwd(); 46 | process.chdir(__dirname); 47 | 48 | // recreate any existing test db 49 | couchdb(TESTDB).deleteDB(function (err) { 50 | if (err && err.error !== 'not_found') { 51 | return callback(err); 52 | } 53 | // create test db 54 | couchdb(TESTDB).createDB(callback); 55 | }); 56 | }; 57 | 58 | exports.tearDown = function (callback) { 59 | // change back to original working directory after running test 60 | process.chdir(this._cwd); 61 | // delete test db 62 | couchdb(TESTDB).deleteDB(callback); 63 | }; 64 | 65 | 66 | exports['publish, unpublish'] = function (test) { 67 | test.expect(4); 68 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 69 | pkgonev2 = path.resolve(__dirname, 'fixtures', 'package-one-v2'), 70 | pkgonev3 = path.resolve(__dirname, 'fixtures', 'package-one-v3'); 71 | 72 | async.series([ 73 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 74 | async.apply(utils.runJam, ['publish', pkgonev2], {env: ENV}), 75 | async.apply(utils.runJam, ['publish', pkgonev3], {env: ENV}), 76 | async.apply( 77 | utils.runJam, ['unpublish', 'package-one@0.0.3'], {env: ENV} 78 | ), 79 | function (cb) { 80 | couchdb(TESTDB).get('package-one', function (err, doc) { 81 | if (err) { 82 | return cb(err); 83 | } 84 | test.same(Object.keys(doc.versions).sort(), [ 85 | '0.0.1', 86 | '0.0.2' 87 | ]); 88 | test.equal(doc.tags.latest, '0.0.2'); 89 | test.equal(doc.name, 'package-one'); 90 | cb(); 91 | }); 92 | }, 93 | async.apply( 94 | utils.runJam, ['unpublish', 'package-one'], {env: ENV} 95 | ), 96 | function (cb) { 97 | couchdb(TESTDB).get('package-one', function (err, doc) { 98 | test.equal(err.error, 'not_found'); 99 | cb(); 100 | }); 101 | } 102 | ], 103 | test.done); 104 | }; 105 | -------------------------------------------------------------------------------- /lib/commands/ls.js: -------------------------------------------------------------------------------- 1 | var logger = require('../logger'), 2 | settings = require('../settings'), 3 | project = require('../project'), 4 | install = require('./install'), 5 | utils = require('../utils'), 6 | async = require('async'), 7 | path = require('path'), 8 | fs = require('fs'); 9 | 10 | 11 | var pathExists = fs.exists || path.exists; 12 | 13 | 14 | exports.summary = 'List installed packages'; 15 | 16 | exports.usage = '' + 17 | 'jam ls [PACKAGE_DIR]\n' + 18 | '\n' + 19 | 'Packages listed with a leading * are directly installed, others\n' + 20 | 'will be marked as unused once their directly installed dependant\n' + 21 | 'is removed or no longer depends on them.\n' + 22 | '\n' + 23 | 'Parameters:\n' + 24 | ' PACKAGE_DIR The Jam package directory to list packages for\n' + 25 | ' (defaults to "./jam")'; 26 | 27 | 28 | exports.run = function (_settings, args, commands) { 29 | var opt = {}; 30 | var cwd = process.cwd(); 31 | install.initDir(_settings, cwd, opt, function (err, opt, cfg, proj_dir) { 32 | if (err) { 33 | return logger.error(err); 34 | } 35 | var package_dir; 36 | if (!args[0]) { 37 | if (cfg.jam && cfg.jam.packageDir) { 38 | package_dir = path.resolve(proj_dir, cfg.jam.packageDir); 39 | } 40 | else { 41 | package_dir = path.resolve(proj_dir, _settings.package_dir || ''); 42 | } 43 | } 44 | else { 45 | package_dir = args[0]; 46 | } 47 | exports.ls(_settings, cfg, package_dir, function (err, output, pkgs) { 48 | if (err) { 49 | return logger.error(); 50 | } 51 | console.log(output); 52 | logger.clean_exit = true; 53 | }); 54 | }); 55 | }; 56 | 57 | 58 | exports.ls = function (_settings, cfg, package_dir, callback) { 59 | var deps = project.getJamDependencies(cfg); 60 | utils.listDirs(package_dir, function (err, dirs) { 61 | if (err) { 62 | return callback(err); 63 | } 64 | 65 | var packages = []; 66 | var lines = []; 67 | 68 | async.forEachLimit(dirs, 5, function (dir, cb) { 69 | settings.load(dir, function (err, pkg) { 70 | if (err) { 71 | return cb(err); 72 | } 73 | var line = ''; 74 | if (deps.hasOwnProperty(pkg.name)) { 75 | // directly installed 76 | line += '* '; 77 | } 78 | else { 79 | line += ' ' 80 | } 81 | line += pkg.name + ' ' + logger.yellow(pkg.version); 82 | if (deps[pkg.name] === 'linked') { 83 | var p = path.resolve(package_dir, pkg.name || ''); 84 | var realpath = fs.readlinkSync(p); 85 | line += logger.cyan(' => ' + realpath); 86 | } 87 | else if (deps[pkg.name]) { 88 | // locked to a specific version/range 89 | line += logger.red( 90 | ' [locked ' + deps[pkg.name] + ']' 91 | ); 92 | } 93 | packages.push(pkg); 94 | lines.push(line); 95 | cb(); 96 | }); 97 | }, 98 | function (err) { 99 | return callback(err, lines.join('\n'), packages); 100 | }); 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /test/integration/test-emptyproject-install-compile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Starting with an empty project (no package.json) 6 | * - jam publish package-one @ 0.0.1 7 | * - jam publish package-two @ 0.0.1 8 | * - jam install package-two 9 | * - jam compile output.js, test both modules included in output.js 10 | */ 11 | 12 | 13 | var couchdb = require('../../lib/couchdb'), 14 | logger = require('../../lib/logger'), 15 | env = require('../../lib/env'), 16 | utils = require('../utils'), 17 | async = require('async'), 18 | http = require('http'), 19 | path = require('path'), 20 | ncp = require('ncp').ncp, 21 | fs = require('fs'), 22 | _ = require('underscore'); 23 | 24 | 25 | var pathExists = fs.exists || path.exists; 26 | 27 | 28 | logger.clean_exit = true; 29 | 30 | // CouchDB database URL to use for testing 31 | var TESTDB = process.env['JAM_TEST_DB'], 32 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 33 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 34 | 35 | if (!TESTDB) { 36 | throw 'JAM_TEST_DB environment variable not set'; 37 | } 38 | 39 | // remove trailing-slash from TESTDB URL 40 | TESTDB = TESTDB.replace(/\/$/, ''); 41 | 42 | 43 | exports.setUp = function (callback) { 44 | // change to integration test directory before running test 45 | this._cwd = process.cwd(); 46 | process.chdir(__dirname); 47 | 48 | // recreate any existing test db 49 | couchdb(TESTDB).deleteDB(function (err) { 50 | if (err && err.error !== 'not_found') { 51 | return callback(err); 52 | } 53 | // create test db 54 | couchdb(TESTDB).createDB(callback); 55 | }); 56 | }; 57 | 58 | exports.tearDown = function (callback) { 59 | // change back to original working directory after running test 60 | process.chdir(this._cwd); 61 | // delete test db 62 | couchdb(TESTDB).deleteDB(callback); 63 | }; 64 | 65 | 66 | exports['empty project'] = { 67 | 68 | setUp: function (callback) { 69 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 70 | // set current project to empty directory 71 | ncp('./fixtures/project-empty', this.project_dir, callback); 72 | }, 73 | 74 | /* 75 | tearDown: function (callback) { 76 | var that = this; 77 | // timeout to try and wait until dir is no-longer busy on windows 78 | //utils.myrimraf(that.project_dir, callback); 79 | }, 80 | */ 81 | 82 | 'publish, install, upgrade': function (test) { 83 | test.expect(5); 84 | var that = this; 85 | process.chdir(that.project_dir); 86 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 87 | pkgtwo = path.resolve(__dirname, 'fixtures', 'package-two'); 88 | 89 | async.series([ 90 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 91 | async.apply(utils.runJam, ['publish', pkgtwo], {env: ENV}), 92 | async.apply( 93 | utils.runJam, ['install', 'package-two'], {env: ENV} 94 | ), 95 | async.apply( 96 | utils.runJam, ['compile', 'output.js'], {env: ENV} 97 | ), 98 | function (cb) { 99 | var content = fs.readFileSync('output.js').toString(); 100 | test.ok(/Package One/.test(content)); 101 | test.ok(/Package Two/.test(content)); 102 | test.ok(/requirejs/.test(content)); 103 | test.ok(/package-one\/main/.test(content)); 104 | test.ok(/package-two\/two/.test(content)); 105 | cb(); 106 | } 107 | ], 108 | test.done); 109 | } 110 | 111 | }; 112 | -------------------------------------------------------------------------------- /test/integration/test-link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * This test is skipped on Windows because it doesn't support link command 6 | * 7 | * Starting with an empty project (no package.json) 8 | * - jam link ./fixtures/package-one 9 | * - test symlink installed to ./jam/package-one 10 | * - test require.config.js is updated 11 | */ 12 | 13 | 14 | var couchdb = require('../../lib/couchdb'), 15 | logger = require('../../lib/logger'), 16 | env = require('../../lib/env'), 17 | utils = require('../utils'), 18 | async = require('async'), 19 | http = require('http'), 20 | path = require('path'), 21 | ncp = require('ncp').ncp, 22 | fs = require('fs'), 23 | _ = require('underscore'); 24 | 25 | 26 | var pathExists = fs.exists || path.exists; 27 | 28 | 29 | if (env.isWindows) { 30 | // skip this test 31 | return; 32 | } 33 | 34 | 35 | logger.clean_exit = true; 36 | 37 | // CouchDB database URL to use for testing 38 | var TESTDB = process.env['JAM_TEST_DB'], 39 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 40 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 41 | 42 | if (!TESTDB) { 43 | throw 'JAM_TEST_DB environment variable not set'; 44 | } 45 | 46 | // remove trailing-slash from TESTDB URL 47 | TESTDB = TESTDB.replace(/\/$/, ''); 48 | 49 | 50 | exports.setUp = function (callback) { 51 | // change to integration test directory before running test 52 | this._cwd = process.cwd(); 53 | process.chdir(__dirname); 54 | 55 | // recreate any existing test db 56 | couchdb(TESTDB).deleteDB(function (err) { 57 | if (err && err.error !== 'not_found') { 58 | return callback(err); 59 | } 60 | // create test db 61 | couchdb(TESTDB).createDB(callback); 62 | }); 63 | }; 64 | 65 | exports.tearDown = function (callback) { 66 | // change back to original working directory after running test 67 | process.chdir(this._cwd); 68 | // delete test db 69 | couchdb(TESTDB).deleteDB(callback); 70 | }; 71 | 72 | 73 | exports['empty project'] = { 74 | 75 | setUp: function (callback) { 76 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 77 | // set current project to empty directory 78 | ncp('./fixtures/project-empty', this.project_dir, callback); 79 | }, 80 | 81 | /* 82 | tearDown: function (callback) { 83 | var that = this; 84 | // timeout to try and wait until dir is no-longer busy on windows 85 | //utils.myrimraf(that.project_dir, callback); 86 | }, 87 | */ 88 | 89 | 'link': function (test) { 90 | test.expect(2); 91 | var that = this; 92 | process.chdir(that.project_dir); 93 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'); 94 | 95 | async.series([ 96 | async.apply(utils.runJam, ['link', pkgone], {env: ENV}), 97 | function (cb) { 98 | var p = path.resolve(that.project_dir, 'jam', 'package-one'); 99 | fs.lstat(p, function (err, stats) { 100 | if (err) { 101 | return cb(err); 102 | } 103 | test.ok(stats.isSymbolicLink(), 'package-one is symlink'); 104 | var cfg = utils.freshRequire( 105 | path.resolve(that.project_dir, 'jam', 'require.config') 106 | ); 107 | test.same(cfg.packages, [ 108 | { 109 | name: 'package-one', 110 | location: 'jam/package-one' 111 | } 112 | ]); 113 | cb(); 114 | }); 115 | } 116 | ], 117 | test.done); 118 | } 119 | 120 | }; 121 | -------------------------------------------------------------------------------- /test/integration/test-publish.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | couchdb = require('../../lib/couchdb'), 3 | logger = require('../../lib/logger'), 4 | utils = require('../utils'), 5 | path = require('path'); 6 | 7 | 8 | logger.clean_exit = true; 9 | 10 | // CouchDB database URL to use for testing 11 | var TESTDB = process.env['JAM_TEST_DB'], 12 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 13 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 14 | 15 | if (!TESTDB) { 16 | throw 'JAM_TEST_DB environment variable not set'; 17 | } 18 | 19 | // remove trailing-slash from TESTDB URL 20 | TESTDB = TESTDB.replace(/\/$/, ''); 21 | 22 | 23 | exports.setUp = function (callback) { 24 | // change to integration test directory before running test 25 | this._cwd = process.cwd(); 26 | process.chdir(__dirname); 27 | 28 | // recreate any existing test db 29 | couchdb(TESTDB).deleteDB(function (err) { 30 | if (err && err.error !== 'not_found') { 31 | return callback(err); 32 | } 33 | // create test db 34 | couchdb(TESTDB).createDB(callback); 35 | }); 36 | }; 37 | 38 | exports.tearDown = function (callback) { 39 | // change back to original working directory after running test 40 | process.chdir(this._cwd); 41 | // delete test db 42 | couchdb(TESTDB).deleteDB(callback); 43 | }; 44 | 45 | 46 | exports['publish within package directory'] = function (test) { 47 | test.expect(1); 48 | process.chdir('./fixtures/package-one'); 49 | utils.runJam(['publish'], {env: ENV}, function (err, stdout, stderr) { 50 | if (err) { 51 | return test.done(err); 52 | } 53 | couchdb(TESTDB).get('package-one', function (err, doc) { 54 | test.equal(doc.name, 'package-one'); 55 | test.done(err); 56 | }); 57 | }); 58 | }; 59 | 60 | exports['publish path outside package directory'] = function (test) { 61 | test.expect(1); 62 | var args = ['publish', path.resolve('fixtures', 'package-one')]; 63 | utils.runJam(args, {env: ENV}, function (err, stdout, stderr) { 64 | if (err) { 65 | return test.done(err); 66 | } 67 | couchdb(TESTDB).get('package-one', function (err, doc) { 68 | test.equal(doc.name, 'package-one'); 69 | test.done(err); 70 | }); 71 | }); 72 | }; 73 | 74 | exports['publish to command-line repo'] = function (test) { 75 | test.expect(1); 76 | var args = [ 77 | 'publish', 78 | path.resolve('fixtures', 'package-one'), 79 | '--repository=' + TESTDB 80 | ]; 81 | utils.runJam(args, function (err, stdout, stderr) { 82 | if (err) { 83 | return test.done(err); 84 | } 85 | couchdb(TESTDB).get('package-one', function (err, doc) { 86 | test.equal(doc.name, 'package-one'); 87 | test.done(err); 88 | }); 89 | }); 90 | }; 91 | 92 | exports['publish with invalid .js package name'] = function (test) { 93 | test.expect(1); 94 | process.chdir('./fixtures/package-three-invalid-extjs'); 95 | utils.runJam(['publish'], {env: ENV, expect_error: true}, 96 | function (err, stdout, stderr) { 97 | if (err) { 98 | return test.done(err); 99 | } 100 | test.ok(/Invalid name property/.test(stderr)); 101 | test.done(); 102 | } 103 | ); 104 | }; 105 | 106 | exports['publish with invalid package name characters'] = function (test) { 107 | test.expect(1); 108 | process.chdir('./fixtures/package-three-invalid-characters'); 109 | utils.runJam(['publish'], {env: ENV, expect_error: true}, 110 | function (err, stdout, stderr) { 111 | if (err) { 112 | return test.done(err); 113 | } 114 | test.ok(/Invalid name property/.test(stderr)); 115 | test.done(); 116 | } 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /lib/commands/search.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils'), 2 | logger = require('../logger'), 3 | repository = require('../repository'), 4 | github = require('../github'), 5 | async = require('async'); 6 | 7 | 8 | exports.summary = 'Search available package sources'; 9 | 10 | exports.usage = '' + 11 | 'jam search QUERY\n' + 12 | '\n' + 13 | 'Parameters:\n' + 14 | ' QUERY The package name to search for\n' + 15 | '\n' + 16 | 'Options:\n' + 17 | ' -r, --repository Source repository URL (otherwise uses values in jamrc)\n' + 18 | ' -g, --github Search GitHub for matching repository name\n' + 19 | ' (does not search package repositories when used)\n' + 20 | ' -l, --limit Maximum number of results to return (defaults to 10).\n' + 21 | ' When searching multiple repositories the limit is\n' + 22 | ' applied to each repository, not the total'; 23 | 24 | 25 | exports.run = function (settings, args, commands) { 26 | var a = argParse(args, { 27 | 'repository': {match: ['-r', '--repository'], value: true}, 28 | 'github': {match: ['-g', '--github']}, 29 | 'limit': {match: ['-l', '--limit'], value: true} 30 | }); 31 | 32 | var opt = a.options; 33 | var repos = opt.repository ? [opt.repository]: settings.repositories; 34 | var limit = opt.limit || 10; 35 | var q = a.positional[0]; 36 | 37 | if (!q) { 38 | logger.error('No query parameter'); 39 | return; 40 | } 41 | 42 | if (opt.github) { 43 | exports.searchGitHub(q, limit); 44 | } 45 | else { 46 | exports.searchRepositories(repos, q, limit); 47 | } 48 | }; 49 | 50 | 51 | exports.searchRepositories = function (repos, q, limit) { 52 | var total = 0; 53 | async.forEachLimit(repos, 4, function (repo, cb) { 54 | if (typeof repo === 'object' && repo.search === false) { 55 | cb(); 56 | return; 57 | } 58 | 59 | repository.search(repo, q, limit, function (err, data) { 60 | if (repos.length > 1) { 61 | console.log( 62 | logger.bold('\n' + logger.magenta( 63 | 'Results for ' + utils.noAuthURL(repo) 64 | )) 65 | ); 66 | } 67 | if (err) { 68 | logger.error(err); 69 | return cb(err); 70 | } 71 | total += data.rows.length; 72 | data.rows.forEach(function (r) { 73 | var desc = utils.truncate(r.doc.description.split('\n')[0], 76); 74 | console.log( 75 | logger.bold(r.doc.name) + 76 | logger.yellow(' ' + r.doc.tags.latest + '\n') + 77 | ' ' + desc 78 | ); 79 | }); 80 | if (repos.length > 1) { 81 | console.log(logger.cyan( 82 | data.rows.length + ' results (limit: ' + limit + ')\n' 83 | )); 84 | } 85 | cb(); 86 | }); 87 | }, 88 | function (err) { 89 | if (!err) { 90 | return logger.end( 91 | total + ' total results' + 92 | (repos.length > 1 ? '': ' (limit: ' + limit + ')') 93 | ); 94 | } 95 | }); 96 | }; 97 | 98 | 99 | exports.searchGitHub = function (q, limit) { 100 | github.repos.search(q, function (err, data) { 101 | if (err) { 102 | return console.error(err); 103 | } 104 | var repos = data.repositories.slice(0, limit); 105 | repos.forEach(function (r) { 106 | var desc = utils.truncate(r.description.split('\n')[0], 76); 107 | console.log( 108 | logger.bold('gh:' + r.owner + '/' + r.name) + '\n' + 109 | //logger.yellow(' ' + r.version + '\n') + //TODO get latest tag? 110 | ' ' + desc 111 | ); 112 | }); 113 | return logger.end(repos.length + ' total results (limit: ' + limit + ')'); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var util = require('util'), 6 | _ = require('underscore'); 7 | 8 | /** 9 | * The level to log at, change this to alter the global logging level. 10 | * Possible options are: error, warning, info, debug. Default level is info. 11 | */ 12 | exports.level = 'info'; 13 | 14 | 15 | /** 16 | * Wraps some ANSI codes around some text. 17 | */ 18 | var wrap = function (code, reset) { 19 | return function (str) { 20 | return "\x1B[" + code + "m" + str + "\x1B[" + reset + "m"; 21 | }; 22 | }; 23 | 24 | /** 25 | * ANSI colors and styles used by the logger module. 26 | */ 27 | var bold = exports.bold = wrap(1, 22); 28 | var red = exports.red = wrap(31, 39); 29 | var green = exports.green = wrap(32, 39); 30 | var cyan = exports.cyan = wrap(36, 39); 31 | var yellow = exports.yellow = wrap(33, 39); 32 | var magenta = exports.magenta = wrap(35, 39); 33 | 34 | /** 35 | * Executes a function only if the current log level is in the levels list 36 | * 37 | * @param {Array} levels 38 | * @param {Function} fn 39 | */ 40 | 41 | var forLevels = function (levels, fn) { 42 | return function () { 43 | for (var i = 0; i < levels.length; i++) { 44 | if (levels[i] === exports.level) { 45 | return fn.apply(exports, arguments); 46 | } 47 | } 48 | }; 49 | }; 50 | 51 | /** 52 | * Logs verbose messages, using util.inspect to show the properties of objects 53 | * (logged for 'verbose' level only) 54 | */ 55 | 56 | exports.verbose = forLevels(['verbose'], function (label) { 57 | var args, withArgs; 58 | 59 | args = Array.prototype.slice.call(arguments, 0).map(function(arg) { 60 | return typeof arg == "string" ? arg : util.inspect(arg); 61 | }); 62 | 63 | withArgs = args.length > 1; 64 | 65 | if (!withArgs) { 66 | label = null; 67 | } 68 | 69 | if (label && withArgs) { 70 | console.log(yellow(label + ' ') + args.slice(1).join("\n")); 71 | } else { 72 | console.log(args.join('\n')); 73 | } 74 | }); 75 | 76 | /** 77 | * Logs debug messages, using util.inspect to show the properties of objects 78 | * (logged for 'debug', 'verbose' levels) 79 | */ 80 | 81 | exports.debug = forLevels(['debug', 'verbose'], function (label, val) { 82 | if (val === undefined) { 83 | val = label; 84 | label = null; 85 | } 86 | if (typeof val !== 'string') { 87 | val = util.inspect(val); 88 | } 89 | if (label && val) { 90 | console.log(magenta(label + ' ') + val); 91 | } 92 | else { 93 | console.log(label); 94 | } 95 | }); 96 | 97 | /** 98 | * Logs info messages (logged for 'info' and 'debug' levels) 99 | */ 100 | 101 | exports.info = forLevels(['info', 'debug', 'verbose'], function (label, val) { 102 | if (val === undefined) { 103 | val = label; 104 | label = null; 105 | } 106 | if (typeof val !== 'string') { 107 | val = util.inspect(val); 108 | } 109 | if (label) { 110 | console.log(cyan(label + ' ') + val); 111 | } 112 | else { 113 | console.log(val); 114 | } 115 | }); 116 | 117 | /** 118 | * Logs warnings messages (logged for 'warning', 'info' and 'debug' levels) 119 | */ 120 | 121 | exports.warning = forLevels(['warning', 'info', 'debug', 'verbose'], function (msg) { 122 | console.log(yellow(bold('Warning: ') + msg)); 123 | }); 124 | 125 | /** 126 | * Logs error messages (always logged) 127 | */ 128 | 129 | exports.error = function (err) { 130 | var msg = err.message || err.error || err; 131 | if (err.stack) { 132 | msg = err.stack.replace(/^Error: /, ''); 133 | } 134 | console.error(red(bold('Error: ') + msg)); 135 | }; 136 | 137 | 138 | /** 139 | * Display a failure message if exit is unexpected. 140 | */ 141 | 142 | exports.clean_exit = false; 143 | exports.end = function (msg) { 144 | exports.clean_exit = true; 145 | exports.success(msg); 146 | }; 147 | exports.success = function (msg) { 148 | console.log(green(bold('OK') + (msg ? bold(': ') + msg: ''))); 149 | }; 150 | var _onExit = function () { 151 | if (!exports.clean_exit) { 152 | console.log(red(bold('Failed'))); 153 | process.removeListener('exit', _onExit); 154 | process.exit(1); 155 | } 156 | }; 157 | process.on('exit', _onExit); 158 | 159 | /** 160 | * Log uncaught exceptions in the same style as normal errors. 161 | */ 162 | 163 | process.on('uncaughtException', function (err) { 164 | exports.error(err.stack || err); 165 | }); 166 | -------------------------------------------------------------------------------- /lib/commands/remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var path = require('path'), 6 | async = require('async'), 7 | rimraf = require('rimraf'), 8 | install = require('./install'), 9 | tree = require('../tree'), 10 | utils = require('../utils'), 11 | project = require('../project'), 12 | logger = require('../logger'), 13 | clean = require('./clean'), 14 | argParse = require('../args').parse; 15 | 16 | 17 | /** 18 | * Usage information and docs 19 | */ 20 | 21 | exports.summary = 'Removes a package from the package directory'; 22 | 23 | 24 | exports.usage = '' + 25 | 'jam remove PACKAGE [MORE...]\n' + 26 | '\n' + 27 | 'Parameters:\n' + 28 | ' PACKAGE The package to remove\n' + 29 | '\n' + 30 | 'Options:\n' + 31 | ' -d, --package-dir Package directory (defaults to "./jam")'; 32 | 33 | /* TODO 34 | ' --clean Runs clean command after removing the package, to remove\n' + 35 | ' any unused dependencies'; 36 | */ 37 | 38 | 39 | /** 40 | * Run function called when "jam remove" command is used 41 | * 42 | * @param {Object} settings - the values from .jamrc files 43 | * @param {Array} args - command-line arguments 44 | */ 45 | 46 | exports.run = function (settings, args) { 47 | var a = argParse(args, { 48 | 'target_dir': {match: ['-d', '--package-dir'], value: true} 49 | }); 50 | 51 | if (a.positional.length < 1) { 52 | console.log(exports.usage); 53 | logger.clean_exit = true; 54 | return; 55 | } 56 | 57 | var opt = a.options; 58 | var cwd = process.cwd(); 59 | 60 | install.initDir(settings, cwd, opt, function (err, opt, cfg, proj_dir) { 61 | if (err) { 62 | return logger.error(err); 63 | } 64 | 65 | opt = install.extendOptions(proj_dir, settings, cfg, opt); 66 | 67 | var names = a.positional; 68 | exports.remove(settings, cfg, opt, names, function (err) { 69 | if (err) { 70 | return logger.error(err); 71 | } 72 | logger.end(); 73 | }); 74 | }); 75 | }; 76 | 77 | 78 | exports.remove = function (settings, cfg, opt, names, callback) { 79 | exports.checkDependants(settings, cfg, opt, names, function (err) { 80 | if (err) { 81 | return callback(err); 82 | } 83 | async.series([ 84 | async.apply( 85 | async.forEach, names, 86 | async.apply(exports.removePackage, cfg, opt) 87 | ), 88 | async.apply(exports.buildLocalTree, settings, cfg, opt), 89 | async.apply( 90 | project.updateRequireConfig, opt.target_dir, opt.baseurl 91 | ) 92 | ], 93 | callback); 94 | }); 95 | }; 96 | 97 | 98 | exports.checkDependants = function (settings, cfg, opt, names, callback) { 99 | exports.buildLocalTree(settings, cfg, opt, function (err, packages) { 100 | if (err) { 101 | return callback(err); 102 | } 103 | var has_dependants = false; 104 | for (var i = 0; i < names.length; i++) { 105 | var name = names[i]; 106 | var pkg = packages[name]; 107 | if (pkg) { 108 | var ranges = packages[name].ranges; 109 | var dependants = Object.keys(ranges).filter(function (d) { 110 | return d !== '_root' && names.indexOf(d) === -1; 111 | }); 112 | if (dependants.length) { 113 | for (var j = 0; j < dependants.length; j++) { 114 | var d = dependants[j]; 115 | logger.error( 116 | d + ' depends on ' + name + ' ' + ranges[d] 117 | ); 118 | }; 119 | has_dependants = true; 120 | } 121 | } 122 | }; 123 | if (has_dependants) { 124 | return callback('Cannot remove package with dependants'); 125 | } 126 | return callback(); 127 | }); 128 | }; 129 | 130 | 131 | exports.removePackage = function (cfg, opt, name, callback) { 132 | logger.info('removing', name); 133 | cfg = project.removeJamDependency(cfg, name); 134 | rimraf(path.resolve(opt.target_dir, name), callback); 135 | }; 136 | 137 | 138 | exports.buildLocalTree = function (settings, cfg, opt, callback) { 139 | var local_sources = [ 140 | install.dirSource(opt.target_dir), 141 | install.repoSource(settings.repositories, cfg) 142 | ]; 143 | var newcfg = utils.convertToRootCfg(cfg); 144 | var pkg = { 145 | config: newcfg, 146 | source: 'root' 147 | }; 148 | logger.info('Building local version tree...'); 149 | tree.build(pkg, local_sources, callback); 150 | }; 151 | -------------------------------------------------------------------------------- /test/integration/test-emptyproject-install-rm-rebuild.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Starting with an empty project (no package.json) 6 | * - jam publish package-one @ 0.0.1 7 | * - jam publish package-two @ 0.0.1 8 | * - jam install package-two, test both modules in require.config.js 9 | * - rm -rf package-two 10 | * - jam rebuild, test only package-one in require.config.js 11 | */ 12 | 13 | 14 | var couchdb = require('../../lib/couchdb'), 15 | logger = require('../../lib/logger'), 16 | env = require('../../lib/env'), 17 | utils = require('../utils'), 18 | async = require('async'), 19 | http = require('http'), 20 | path = require('path'), 21 | ncp = require('ncp').ncp, 22 | fs = require('fs'), 23 | _ = require('underscore'); 24 | 25 | 26 | var pathExists = fs.exists || path.exists; 27 | 28 | 29 | logger.clean_exit = true; 30 | 31 | // CouchDB database URL to use for testing 32 | var TESTDB = process.env['JAM_TEST_DB'], 33 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 34 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 35 | 36 | if (!TESTDB) { 37 | throw 'JAM_TEST_DB environment variable not set'; 38 | } 39 | 40 | // remove trailing-slash from TESTDB URL 41 | TESTDB = TESTDB.replace(/\/$/, ''); 42 | 43 | 44 | exports.setUp = function (callback) { 45 | // change to integration test directory before running test 46 | this._cwd = process.cwd(); 47 | process.chdir(__dirname); 48 | 49 | // recreate any existing test db 50 | couchdb(TESTDB).deleteDB(function (err) { 51 | if (err && err.error !== 'not_found') { 52 | return callback(err); 53 | } 54 | // create test db 55 | couchdb(TESTDB).createDB(callback); 56 | }); 57 | }; 58 | 59 | exports.tearDown = function (callback) { 60 | // change back to original working directory after running test 61 | process.chdir(this._cwd); 62 | // delete test db 63 | couchdb(TESTDB).deleteDB(callback); 64 | }; 65 | 66 | 67 | exports['empty project'] = { 68 | 69 | setUp: function (callback) { 70 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 71 | // set current project to empty directory 72 | ncp('./fixtures/project-empty', this.project_dir, callback); 73 | }, 74 | 75 | /* 76 | tearDown: function (callback) { 77 | var that = this; 78 | // timeout to try and wait until dir is no-longer busy on windows 79 | //utils.myrimraf(that.project_dir, callback); 80 | }, 81 | */ 82 | 83 | 'publish, install, upgrade': function (test) { 84 | test.expect(2); 85 | var that = this; 86 | process.chdir(that.project_dir); 87 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 88 | pkgtwo = path.resolve(__dirname, 'fixtures', 'package-two'); 89 | 90 | async.series([ 91 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 92 | async.apply(utils.runJam, ['publish', pkgtwo], {env: ENV}), 93 | async.apply( 94 | utils.runJam, ['install', 'package-two'], {env: ENV} 95 | ), 96 | function (cb) { 97 | var cfg = utils.freshRequire( 98 | path.resolve(that.project_dir, 'jam', 'require.config') 99 | ); 100 | var packages= _.sortBy(cfg.packages, function (p) { 101 | return p.name; 102 | }); 103 | test.same(packages, [ 104 | { 105 | name: 'package-one', 106 | location: 'jam/package-one' 107 | }, 108 | { 109 | name: 'package-two', 110 | location: 'jam/package-two', 111 | main: 'two.js' 112 | } 113 | ]); 114 | cb(); 115 | }, 116 | async.apply( 117 | utils.myrimraf, 118 | path.resolve(that.project_dir, 'jam', 'package-two') 119 | ), 120 | async.apply(utils.runJam, ['rebuild'], {env: ENV}), 121 | function (cb) { 122 | var cfg = utils.freshRequire( 123 | path.resolve(that.project_dir, 'jam', 'require.config') 124 | ); 125 | var packages= _.sortBy(cfg.packages, function (p) { 126 | return p.name; 127 | }); 128 | test.same(packages, [ 129 | { 130 | name: 'package-one', 131 | location: 'jam/package-one' 132 | } 133 | ]); 134 | cb(); 135 | } 136 | ], 137 | test.done); 138 | } 139 | 140 | }; 141 | -------------------------------------------------------------------------------- /test/integration/test-emptyproject-publish-install-upgrade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Starting with an empty project (no package.json) 6 | * - jam publish package-one @ 0.0.1 7 | * - jam publish package-one @ 0.0.2 8 | * - jam install package-one @ 0.0.1, test installation succeeded 9 | * - jam upgrade, test package-one is now at 0.0.2 10 | */ 11 | 12 | 13 | var couchdb = require('../../lib/couchdb'), 14 | logger = require('../../lib/logger'), 15 | env = require('../../lib/env'), 16 | utils = require('../utils'), 17 | async = require('async'), 18 | http = require('http'), 19 | path = require('path'), 20 | ncp = require('ncp').ncp, 21 | fs = require('fs'), 22 | _ = require('underscore'); 23 | 24 | 25 | var pathExists = fs.exists || path.exists; 26 | 27 | 28 | logger.clean_exit = true; 29 | 30 | // CouchDB database URL to use for testing 31 | var TESTDB = process.env['JAM_TEST_DB'], 32 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 33 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 34 | 35 | if (!TESTDB) { 36 | throw 'JAM_TEST_DB environment variable not set'; 37 | } 38 | 39 | // remove trailing-slash from TESTDB URL 40 | TESTDB = TESTDB.replace(/\/$/, ''); 41 | 42 | 43 | exports.setUp = function (callback) { 44 | // change to integration test directory before running test 45 | this._cwd = process.cwd(); 46 | process.chdir(__dirname); 47 | 48 | // recreate any existing test db 49 | couchdb(TESTDB).deleteDB(function (err) { 50 | if (err && err.error !== 'not_found') { 51 | return callback(err); 52 | } 53 | // create test db 54 | couchdb(TESTDB).createDB(callback); 55 | }); 56 | }; 57 | 58 | exports.tearDown = function (callback) { 59 | // change back to original working directory after running test 60 | process.chdir(this._cwd); 61 | // delete test db 62 | couchdb(TESTDB).deleteDB(callback); 63 | }; 64 | 65 | 66 | exports['empty project'] = { 67 | 68 | setUp: function (callback) { 69 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 70 | // set current project to empty directory 71 | ncp('./fixtures/project-empty', this.project_dir, callback); 72 | }, 73 | 74 | /* 75 | tearDown: function (callback) { 76 | var that = this; 77 | // timeout to try and wait until dir is no-longer busy on windows 78 | //utils.myrimraf(that.project_dir, callback); 79 | }, 80 | */ 81 | 82 | 'publish, install, upgrade': function (test) { 83 | test.expect(4); 84 | var that = this; 85 | process.chdir(that.project_dir); 86 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 87 | pkgonev2 = path.resolve(__dirname, 'fixtures', 'package-one-v2'); 88 | 89 | async.series([ 90 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 91 | async.apply(utils.runJam, ['publish', pkgonev2], {env: ENV}), 92 | async.apply( 93 | utils.runJam, ['install', 'package-one@0.0.1'], {env: ENV} 94 | ), 95 | function (cb) { 96 | // test that main.js was installed from package 97 | var a = fs.readFileSync(path.resolve(pkgone, 'main.js')); 98 | var b = fs.readFileSync( 99 | path.resolve(that.project_dir, 'jam/package-one/main.js') 100 | ); 101 | test.equal(a.toString(), b.toString()); 102 | 103 | // make sure the requirejs config includes the new package 104 | var cfg = utils.freshRequire( 105 | path.resolve(that.project_dir, 'jam', 'require.config') 106 | ); 107 | test.same(cfg.packages, [ 108 | { 109 | name: 'package-one', 110 | location: 'jam/package-one' 111 | } 112 | ]); 113 | cb(); 114 | }, 115 | function (cb) { 116 | var args = ['upgrade']; 117 | utils.runJam(args, {env: ENV}, function (err, stdout, stderr) { 118 | if (err) { 119 | return cb(err); 120 | } 121 | var cfg = utils.freshRequire( 122 | path.resolve(that.project_dir, 'jam', 'require.config') 123 | ); 124 | test.same(cfg.packages, [ 125 | { 126 | name: 'package-one', 127 | location: 'jam/package-one' 128 | } 129 | ]); 130 | var p = path.resolve( 131 | that.project_dir, 132 | 'jam/package-one/package.json' 133 | ); 134 | var content = fs.readFileSync(p); 135 | var pkg = JSON.parse(content.toString()); 136 | test.equal(pkg.version, '0.0.2'); 137 | cb(); 138 | }); 139 | } 140 | ], 141 | test.done); 142 | } 143 | 144 | }; 145 | -------------------------------------------------------------------------------- /lib/tar.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger'), 2 | env = require('./env'), 3 | Packer = require('./fstream-jam'), 4 | fstream = require('fstream'), 5 | tar = require('tar'), 6 | zlib = require('zlib'), 7 | path = require('path'), 8 | fs = require('fs'); 9 | 10 | 11 | var umask = parseInt(022, 8); 12 | var modes = { 13 | exec: 0777 & (~umask), 14 | file: 0666 & (~umask), 15 | umask: umask 16 | }; 17 | 18 | var myUid = process.getuid && process.getuid(), 19 | myGid = process.getgid && process.getgid(); 20 | 21 | if (process.env.SUDO_UID && myUid === 0) { 22 | if (!isNaN(process.env.SUDO_UID)) { 23 | myUid = +process.env.SUDO_UID; 24 | } 25 | if (!isNaN(process.env.SUDO_GID)) { 26 | myGid = +process.env.SUDO_GID; 27 | } 28 | } 29 | 30 | /** 31 | */ 32 | 33 | exports.create = function(cfg, source, target, callback) { 34 | 35 | logger.info('creating', target); 36 | 37 | function returnError(err) { 38 | // don't call the callback multiple times, just return the first error 39 | var _callback = callback; 40 | callback = function () {}; 41 | return _callback(err); 42 | } 43 | 44 | var fwriter = fstream.Writer({ type: 'File', path: target }); 45 | fwriter.on('error', function (err) { 46 | logger.error('error writing ' + target); 47 | //logger.error(err); 48 | return returnError(err); 49 | }); 50 | fwriter.on('close', function () { 51 | callback(null, target); 52 | }); 53 | 54 | var istream = Packer({ 55 | packageInfo: cfg, 56 | path: source, 57 | type: "Directory", 58 | isDirectory: true 59 | }); 60 | istream.on('error', function (err) { 61 | logger.error('error reading ' + source); 62 | //logger.error(err); 63 | return returnError(err); 64 | }); 65 | istream.on("child", function (c) { 66 | //var root = path.resolve(c.root.path, '../package'); 67 | //logger.info('adding', c.path.substr(root.length + 1)); 68 | }); 69 | 70 | var packer = tar.Pack({ noProprietary: true }); 71 | packer.on('error', function (err) { 72 | logger.error('tar creation error ' + target); 73 | //logger.error(err); 74 | return returnError(err); 75 | }); 76 | 77 | var zipper = zlib.Gzip(); 78 | zipper.on('error', function (err) { 79 | logger.error('gzip error ' + target); 80 | //logger.error(err); 81 | return returnError(err); 82 | }); 83 | 84 | istream.pipe(packer).pipe(zipper).pipe(fwriter); 85 | }; 86 | 87 | 88 | /** 89 | */ 90 | 91 | exports.extract = function (source, target, callback) { 92 | 93 | logger.info('extracting', source); 94 | 95 | var umask = modes.umask; 96 | var dmode = modes.dmode; 97 | var fmode = modes.fmode; 98 | 99 | function returnError(err) { 100 | // don't call the callback multiple times, just return the first error 101 | var _callback = callback; 102 | callback = function () {}; 103 | return _callback(err); 104 | } 105 | 106 | var freader = fs.createReadStream(source); 107 | freader.on('error', function (err) { 108 | logger.error('error reading ' + source); 109 | //logger.error(err); 110 | return returnError(err); 111 | }); 112 | 113 | var extract_opts = { 114 | type: 'Directory', 115 | path: target, 116 | strip: 1, 117 | filter: function () { 118 | // symbolic links are not allowed in packages 119 | if (this.type.match(/^.*Link$/)) { 120 | logger.warning( 121 | 'excluding symbolic link', 122 | this.path.substr(target.length + 1) + ' -> ' + this.linkpath 123 | ); 124 | return false; 125 | } 126 | return true; 127 | } 128 | }; 129 | if (!env.isWindows && typeof myUid === "number" && typeof myGid === "number") { 130 | extract_opts.uid = myUid; 131 | extract_opts.gid = myGid; 132 | } 133 | var extractor = tar.Extract(extract_opts); 134 | extractor.on('error', function (err) { 135 | logger.error('untar error ' + source); 136 | //logger.error(err); 137 | return returnError(err); 138 | }); 139 | extractor.on('entry', function (entry) { 140 | //logger.info('extracting', entry.path); 141 | entry.mode = entry.mode || entry.props.mode; 142 | var original_mode = entry.mode; 143 | entry.mode = entry.mode | (entry.type === "Directory" ? dmode: fmode); 144 | entry.mode = entry.mode & (~umask); 145 | if (original_mode !== entry.mode) { 146 | logger.info( 147 | 'modified mode', 148 | original_mode + ' => ' + entry.mode + ' ' + entry.path 149 | ); 150 | } 151 | 152 | if (process.platform !== "win32" && 153 | typeof myUid === "number" && typeof myGid === "number") { 154 | entry.props.uid = entry.uid = myUid; 155 | entry.props.gid = entry.gid = myGid; 156 | } 157 | }); 158 | extractor.on('end', function () { 159 | return callback(null, target); 160 | }); 161 | 162 | var unzipper = zlib.Unzip(); 163 | unzipper.on('error', function (err) { 164 | logger.error('unzip error ' + source); 165 | //logger.error(err); 166 | return returnError(err); 167 | }); 168 | 169 | freader.pipe(unzipper).pipe(extractor); 170 | }; 171 | -------------------------------------------------------------------------------- /lib/jamrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module handles the loading of the jamrc files used to configure the 3 | * behaviour of the command-line tool. 4 | * 5 | * @module 6 | */ 7 | 8 | var utils = require('./utils'), 9 | async = require('async'), 10 | _ = require('underscore'), 11 | path = require('path'), 12 | fs = require('fs'), 13 | env = require('./env'); 14 | 15 | 16 | var pathExists = fs.exists || path.exists; 17 | 18 | 19 | /** 20 | * Default paths to lookup when constructing values for jamrc. 21 | * Paths are checked in order, with later paths overriding the values obtained 22 | * from earlier ones. 23 | */ 24 | 25 | exports.PATHS = [ 26 | //'/etc/jamrc', 27 | //'/usr/local/etc/jamrc', 28 | env.home + '/.jamrc' 29 | ]; 30 | 31 | 32 | /** 33 | * The defaults jamrc settings 34 | */ 35 | 36 | exports.DEFAULTS = { 37 | repositories: [ "http://jamjs.org/repository" ], 38 | package_dir: 'jam', 39 | link_dir: env.home + '/.jam/link', 40 | system_dir: env.home 41 | }; 42 | 43 | 44 | /** 45 | * Loads base jamrc settings from PATHS, and merges them along with the DEFAULT 46 | * values, returning the result. 47 | * 48 | * @param {Function} callback(err, settings) 49 | */ 50 | 51 | exports.load = function (callback) { 52 | async.map(exports.PATHS, exports.loadFile, function (err, results) { 53 | if (err) { 54 | return callback(err); 55 | } 56 | 57 | var defaults = _.clone(exports.DEFAULTS); 58 | var settings = results.reduce(function (merged, r) { 59 | return exports.merge(merged, r); 60 | }, defaults); 61 | 62 | exports.jamrc = settings || {}; 63 | callback(null, settings); 64 | }); 65 | }; 66 | 67 | 68 | /** 69 | * Looks for a project-specific .jamrc file to override base settings with. 70 | * Walks up the directory tree until it finds a .jamrc file or hits the root. 71 | * Does not throw when no .jamrc is found, just returns null. 72 | * 73 | * @param {String} p - The starting path to search upwards from 74 | * @param {Function} callback(err, path) 75 | */ 76 | 77 | exports.findProjectRC = function (p, callback) { 78 | var filename = path.resolve(p, '.jamrc'); 79 | pathExists(filename, function (exists) { 80 | if (exists) { 81 | return callback(null, filename); 82 | } 83 | var newpath = path.dirname(p); 84 | if (newpath === p) { // root directory 85 | return callback(null, null); 86 | } 87 | else { 88 | return exports.findProjectRC(newpath, callback); 89 | } 90 | }); 91 | }; 92 | 93 | 94 | /** 95 | * Searches for a project-level .jamrc and extends the provided settings 96 | * object if one is found. If no project-level .jamrc is found, returns the 97 | * original settings unmodified. 98 | * 99 | * @param {Object} settings - the base settings to extend 100 | * @param {String} cwd - the path to search upwards from 101 | * @param {Function} callback(err, settings) 102 | */ 103 | 104 | exports.loadProjectRC = function (settings, cwd, callback) { 105 | exports.findProjectRC(cwd, function (err, p) { 106 | if (err) { 107 | return callback(err); 108 | } 109 | if (!p) { 110 | // no project-level .jamrc, return original settings 111 | return callback(null, settings); 112 | } 113 | else { 114 | exports.extend(settings, p, callback); 115 | } 116 | }); 117 | }; 118 | 119 | 120 | /** 121 | * Deep merge for JSON objects, overwrites conflicting properties 122 | * 123 | * @param {Object} a 124 | * @param {Object} b 125 | * @returns {Object} 126 | */ 127 | 128 | exports.merge = function (a, b) { 129 | if (!b) { 130 | return a; 131 | } 132 | for (var k in b) { 133 | if (Array.isArray(b[k])) { 134 | a[k] = b[k]; 135 | } 136 | else if (typeof b[k] === 'object') { 137 | if (typeof a[k] === 'object') { 138 | exports.merge(a[k], b[k]); 139 | } 140 | else if (b.hasOwnProperty(k)) { 141 | a[k] = b[k]; 142 | } 143 | } 144 | else if (b.hasOwnProperty(k)) { 145 | a[k] = b[k] 146 | } 147 | } 148 | return a; 149 | }; 150 | 151 | /** 152 | * Checks a jamrc file exists and loads it if available. If the file does not 153 | * exist the function will respond with an empty object. 154 | * 155 | * @param {String} p - the path of the jamrc file to load 156 | * @param {Function} callback(err, settings) 157 | */ 158 | 159 | exports.loadFile = function (p, callback) { 160 | pathExists(p, function (exists) { 161 | if (exists) { 162 | try { 163 | var mod = require(path.resolve(p)); 164 | } 165 | catch (e) { 166 | return callback(e); 167 | } 168 | callback(null, mod); 169 | } 170 | else { 171 | callback(null, {}); 172 | } 173 | }); 174 | }; 175 | 176 | 177 | /** 178 | * Extend currently loaded settings with another .jamrc file. Used by commands 179 | * specific to a project directory that would like to load project-specific 180 | * settings. 181 | * 182 | * @param {Object} settings - the base settings to extend 183 | * @param {String} path - the path to a .jamrc file to extend settings with 184 | * @param {Function} callback(err, settings) 185 | */ 186 | 187 | exports.extend = function (settings, path, callback) { 188 | exports.loadFile(path, function (err, s) { 189 | if (err) { 190 | return callback(err); 191 | } 192 | exports.merge(settings, s); 193 | callback(null, settings); 194 | }); 195 | }; 196 | 197 | ['Tmp', 'Git', 'Cache'].forEach(function (name) { 198 | 199 | exports['get' + name + 'Dir'] = function () { 200 | var root = env.home; 201 | 202 | if (exports.jamrc && exports.jamrc['system_dir']) { 203 | root = exports.jamrc['system_dir']; 204 | } 205 | 206 | root = root.replace('~', env.home); 207 | return path.resolve(root, '.jam', name.toLowerCase()); 208 | }; 209 | 210 | }); 211 | -------------------------------------------------------------------------------- /test/integration/test-packagejson-publish-install-ls-remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Starting with a project using package.json with jam deps defined 6 | * - jam publish package-one 7 | * - jam publish package-two (depends on package-one) 8 | * - jam install, test installation succeeded 9 | * - jam ls, test packages are listed 10 | * - jam remove package-two, test it's removed 11 | */ 12 | 13 | 14 | var couchdb = require('../../lib/couchdb'), 15 | logger = require('../../lib/logger'), 16 | env = require('../../lib/env'), 17 | utils = require('../utils'), 18 | async = require('async'), 19 | http = require('http'), 20 | path = require('path'), 21 | ncp = require('ncp').ncp, 22 | fs = require('fs'), 23 | _ = require('underscore'); 24 | 25 | 26 | var pathExists = fs.exists || path.exists; 27 | 28 | 29 | logger.clean_exit = true; 30 | 31 | // CouchDB database URL to use for testing 32 | var TESTDB = process.env['JAM_TEST_DB'], 33 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 34 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 35 | 36 | if (!TESTDB) { 37 | throw 'JAM_TEST_DB environment variable not set'; 38 | } 39 | 40 | // remove trailing-slash from TESTDB URL 41 | TESTDB = TESTDB.replace(/\/$/, ''); 42 | 43 | 44 | exports.setUp = function (callback) { 45 | // change to integration test directory before running test 46 | this._cwd = process.cwd(); 47 | process.chdir(__dirname); 48 | 49 | // recreate any existing test db 50 | couchdb(TESTDB).deleteDB(function (err) { 51 | if (err && err.error !== 'not_found') { 52 | return callback(err); 53 | } 54 | // create test db 55 | couchdb(TESTDB).createDB(callback); 56 | }); 57 | }; 58 | 59 | exports.tearDown = function (callback) { 60 | // change back to original working directory after running test 61 | process.chdir(this._cwd); 62 | // delete test db 63 | couchdb(TESTDB).deleteDB(callback); 64 | }; 65 | 66 | 67 | exports['project with package.json'] = { 68 | 69 | setUp: function (callback) { 70 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 71 | // set current project to empty directory 72 | ncp('./fixtures/project-packagejson', this.project_dir, callback); 73 | }, 74 | 75 | /* 76 | tearDown: function (callback) { 77 | var that = this; 78 | // clear current project 79 | //utils.myrimraf(that.project_dir, callback); 80 | }, 81 | */ 82 | 83 | 'publish, install, ls, remove': function (test) { 84 | test.expect(6); 85 | var that = this; 86 | process.chdir(that.project_dir); 87 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 88 | pkgtwo = path.resolve(__dirname, 'fixtures', 'package-two'); 89 | 90 | async.series([ 91 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 92 | async.apply(utils.runJam, ['publish', pkgtwo], {env: ENV}), 93 | async.apply(utils.runJam, ['install'], {env: ENV}), 94 | function (cb) { 95 | // test that main.js was installed from package 96 | var a = fs.readFileSync(path.resolve(pkgone, 'main.js')); 97 | var b = fs.readFileSync( 98 | path.resolve(that.project_dir, 'jam/package-one/main.js') 99 | ); 100 | test.equal(a.toString(), b.toString()); 101 | var c = fs.readFileSync(path.resolve(pkgtwo, 'two.js')); 102 | var d = fs.readFileSync( 103 | path.resolve(that.project_dir, 'jam/package-two/two.js') 104 | ); 105 | test.equal(c.toString(), d.toString()); 106 | 107 | // make sure the requirejs config includes the new package 108 | var cfg = utils.freshRequire( 109 | path.resolve(that.project_dir, 'jam', 'require.config') 110 | ); 111 | var packages= _.sortBy(cfg.packages, function (p) { 112 | return p.name; 113 | }); 114 | test.same(packages, [ 115 | { 116 | name: 'package-one', 117 | location: 'jam/package-one' 118 | }, 119 | { 120 | name: 'package-two', 121 | location: 'jam/package-two', 122 | main: 'two.js' 123 | } 124 | ]); 125 | cb(); 126 | }, 127 | function (cb) { 128 | utils.runJam(['ls'], function (err, stdout, stderr) { 129 | if (err) { 130 | return cb(err); 131 | } 132 | var lines = stdout.replace(/\n$/, '').split('\n'); 133 | test.same(lines.sort(), [ 134 | '* package-one \u001b[33m0.0.1\u001b[39m', 135 | '* package-two \u001b[33m0.0.1\u001b[39m' 136 | ]); 137 | cb(); 138 | }); 139 | }, 140 | function (cb) { 141 | var args = ['remove', 'package-two']; 142 | utils.runJam(args, function (err, stdout, stderr) { 143 | if (err) { 144 | return cb(err); 145 | } 146 | var cfg = utils.freshRequire( 147 | path.resolve(that.project_dir, 'jam', 'require.config') 148 | ); 149 | test.same(cfg.packages, [ 150 | { 151 | name: 'package-one', 152 | location: 'jam/package-one' 153 | } 154 | ]); 155 | var p = path.resolve(that.project_dir, 'jam/package-two'); 156 | pathExists(p, function (exists) { 157 | test.ok(!exists, 'package-two directory removed'); 158 | cb(); 159 | }); 160 | }); 161 | } 162 | ], 163 | test.done); 164 | } 165 | 166 | }; 167 | -------------------------------------------------------------------------------- /test/integration/test-emptyproject-publish-install-ls-remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Starting with an empty project (no package.json) 6 | * - jam publish package-one 7 | * - jam publish package-two (depends on package-one) 8 | * - jam install package-two, test installation succeeded 9 | * - jam ls, test packages are listed 10 | * - jam remove package-two, test it's removed 11 | */ 12 | 13 | 14 | var couchdb = require('../../lib/couchdb'), 15 | logger = require('../../lib/logger'), 16 | env = require('../../lib/env'), 17 | utils = require('../utils'), 18 | async = require('async'), 19 | http = require('http'), 20 | path = require('path'), 21 | ncp = require('ncp').ncp, 22 | fs = require('fs'), 23 | _ = require('underscore'); 24 | 25 | 26 | var pathExists = fs.exists || path.exists; 27 | 28 | 29 | logger.clean_exit = true; 30 | 31 | // CouchDB database URL to use for testing 32 | var TESTDB = process.env['JAM_TEST_DB'], 33 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 34 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 35 | 36 | if (!TESTDB) { 37 | throw 'JAM_TEST_DB environment variable not set'; 38 | } 39 | 40 | // remove trailing-slash from TESTDB URL 41 | TESTDB = TESTDB.replace(/\/$/, ''); 42 | 43 | 44 | exports.setUp = function (callback) { 45 | // change to integration test directory before running test 46 | this._cwd = process.cwd(); 47 | process.chdir(__dirname); 48 | 49 | // recreate any existing test db 50 | couchdb(TESTDB).deleteDB(function (err) { 51 | if (err && err.error !== 'not_found') { 52 | return callback(err); 53 | } 54 | // create test db 55 | couchdb(TESTDB).createDB(callback); 56 | }); 57 | }; 58 | 59 | exports.tearDown = function (callback) { 60 | // change back to original working directory after running test 61 | process.chdir(this._cwd); 62 | // delete test db 63 | couchdb(TESTDB).deleteDB(callback); 64 | }; 65 | 66 | 67 | exports['empty project'] = { 68 | 69 | setUp: function (callback) { 70 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 71 | // set current project to empty directory 72 | ncp('./fixtures/project-empty', this.project_dir, callback); 73 | }, 74 | 75 | /* 76 | tearDown: function (callback) { 77 | var that = this; 78 | // timeout to try and wait until dir is no-longer busy on windows 79 | //utils.myrimraf(that.project_dir, callback); 80 | }, 81 | */ 82 | 83 | 'publish, install, ls, remove': function (test) { 84 | test.expect(6); 85 | var that = this; 86 | process.chdir(that.project_dir); 87 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 88 | pkgtwo = path.resolve(__dirname, 'fixtures', 'package-two'); 89 | 90 | async.series([ 91 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 92 | async.apply(utils.runJam, ['publish', pkgtwo], {env: ENV}), 93 | async.apply(utils.runJam, ['install', 'package-two'], {env: ENV}), 94 | function (cb) { 95 | // test that main.js was installed from package 96 | var a = fs.readFileSync(path.resolve(pkgone, 'main.js')); 97 | var b = fs.readFileSync( 98 | path.resolve(that.project_dir, 'jam/package-one/main.js') 99 | ); 100 | test.equal(a.toString(), b.toString()); 101 | var c = fs.readFileSync(path.resolve(pkgtwo, 'two.js')); 102 | var d = fs.readFileSync( 103 | path.resolve(that.project_dir, 'jam/package-two/two.js') 104 | ); 105 | test.equal(c.toString(), d.toString()); 106 | 107 | // make sure the requirejs config includes the new package 108 | var cfg = utils.freshRequire( 109 | path.resolve(that.project_dir, 'jam', 'require.config') 110 | ); 111 | var packages= _.sortBy(cfg.packages, function (p) { 112 | return p.name; 113 | }); 114 | test.same(packages, [ 115 | { 116 | name: 'package-one', 117 | location: 'jam/package-one' 118 | }, 119 | { 120 | name: 'package-two', 121 | location: 'jam/package-two', 122 | main: 'two.js' 123 | } 124 | ]); 125 | cb(); 126 | }, 127 | function (cb) { 128 | utils.runJam(['ls'], function (err, stdout, stderr) { 129 | if (err) { 130 | return cb(err); 131 | } 132 | var lines = stdout.replace(/\n$/, '').split('\n'); 133 | test.same(lines.sort(), [ 134 | ' package-one \u001b[33m0.0.1\u001b[39m', 135 | ' package-two \u001b[33m0.0.1\u001b[39m' 136 | ]); 137 | cb(); 138 | }); 139 | }, 140 | function (cb) { 141 | var args = ['remove', 'package-two']; 142 | utils.runJam(args, function (err, stdout, stderr) { 143 | if (err) { 144 | return cb(err); 145 | } 146 | var cfg = utils.freshRequire( 147 | path.resolve(that.project_dir, 'jam', 'require.config') 148 | ); 149 | test.same(cfg.packages.sort(), [ 150 | { 151 | name: 'package-one', 152 | location: 'jam/package-one' 153 | } 154 | ]); 155 | var p = path.resolve(that.project_dir, 'jam/package-two'); 156 | pathExists(p, function (exists) { 157 | test.ok(!exists, 'package-two directory removed'); 158 | cb(); 159 | }); 160 | }); 161 | } 162 | ], 163 | test.done); 164 | } 165 | 166 | }; 167 | -------------------------------------------------------------------------------- /test/integration/test-custompaths-publish-install-ls-remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Starting with a project using package.json with jam deps defined and 6 | * custom package install path and base URL 7 | * - jam publish package-one 8 | * - jam publish package-two (depends on package-one) 9 | * - jam install, test installation succeeded 10 | * - jam ls, test packages are listed 11 | * - jam remove package-two, test it's removed 12 | */ 13 | 14 | 15 | var couchdb = require('../../lib/couchdb'), 16 | logger = require('../../lib/logger'), 17 | env = require('../../lib/env'), 18 | utils = require('../utils'), 19 | async = require('async'), 20 | http = require('http'), 21 | path = require('path'), 22 | ncp = require('ncp').ncp, 23 | fs = require('fs'), 24 | _ = require('underscore'); 25 | 26 | 27 | var pathExists = fs.exists || path.exists; 28 | 29 | 30 | logger.clean_exit = true; 31 | 32 | // CouchDB database URL to use for testing 33 | var TESTDB = process.env['JAM_TEST_DB'], 34 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 35 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 36 | 37 | if (!TESTDB) { 38 | throw 'JAM_TEST_DB environment variable not set'; 39 | } 40 | 41 | // remove trailing-slash from TESTDB URL 42 | TESTDB = TESTDB.replace(/\/$/, ''); 43 | 44 | 45 | exports.setUp = function (callback) { 46 | // change to integration test directory before running test 47 | this._cwd = process.cwd(); 48 | process.chdir(__dirname); 49 | 50 | // recreate any existing test db 51 | couchdb(TESTDB).deleteDB(function (err) { 52 | if (err && err.error !== 'not_found') { 53 | return callback(err); 54 | } 55 | // create test db 56 | couchdb(TESTDB).createDB(callback); 57 | }); 58 | }; 59 | 60 | exports.tearDown = function (callback) { 61 | // change back to original working directory after running test 62 | process.chdir(this._cwd); 63 | // delete test db 64 | couchdb(TESTDB).deleteDB(callback); 65 | }; 66 | 67 | 68 | exports['project with package.json'] = { 69 | 70 | setUp: function (callback) { 71 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 72 | // set current project to empty directory 73 | ncp('./fixtures/project-custompaths', this.project_dir, callback); 74 | }, 75 | 76 | /* 77 | tearDown: function (callback) { 78 | var that = this; 79 | // timeout to try and wait until dir is no-longer busy on windows 80 | //utils.myrimraf(that.project_dir, callback); 81 | }, 82 | */ 83 | 84 | 'publish, install, ls, remove': function (test) { 85 | test.expect(6); 86 | var that = this; 87 | process.chdir(that.project_dir); 88 | var pkgone = path.resolve(__dirname, 'fixtures', 'package-one'), 89 | pkgtwo = path.resolve(__dirname, 'fixtures', 'package-two'); 90 | 91 | async.series([ 92 | async.apply(utils.runJam, ['publish', pkgone], {env: ENV}), 93 | async.apply(utils.runJam, ['publish', pkgtwo], {env: ENV}), 94 | async.apply(utils.runJam, ['install'], {env: ENV}), 95 | function (cb) { 96 | // test that main.js was installed from package 97 | var a = fs.readFileSync(path.resolve(pkgone, 'main.js')); 98 | var b = fs.readFileSync(path.resolve( 99 | that.project_dir, 100 | 'public/js/vendor/package-one/main.js' 101 | )); 102 | test.equal(a.toString(), b.toString()); 103 | var c = fs.readFileSync(path.resolve(pkgtwo, 'two.js')); 104 | var d = fs.readFileSync(path.resolve( 105 | that.project_dir, 106 | 'public/js/vendor/package-two/two.js' 107 | )); 108 | test.equal(c.toString(), d.toString()); 109 | 110 | // make sure the requirejs config includes the new package 111 | var cfg = utils.freshRequire(path.resolve( 112 | that.project_dir, 'public/js/vendor/require.config' 113 | )); 114 | var packages= _.sortBy(cfg.packages, function (p) { 115 | return p.name; 116 | }); 117 | test.same(packages, [ 118 | { 119 | name: 'package-one', 120 | location: 'js/vendor/package-one' 121 | }, 122 | { 123 | name: 'package-two', 124 | location: 'js/vendor/package-two', 125 | main: 'two.js' 126 | } 127 | ]); 128 | cb(); 129 | }, 130 | function (cb) { 131 | utils.runJam(['ls'], function (err, stdout, stderr) { 132 | if (err) { 133 | return cb(err); 134 | } 135 | var lines = stdout.replace(/\n$/, '').split('\n'); 136 | test.same(lines.sort(), [ 137 | '* package-one \u001b[33m0.0.1\u001b[39m', 138 | '* package-two \u001b[33m0.0.1\u001b[39m' 139 | ]); 140 | cb(); 141 | }); 142 | }, 143 | function (cb) { 144 | var args = ['remove', 'package-two']; 145 | utils.runJam(args, function (err, stdout, stderr) { 146 | if (err) { 147 | return cb(err); 148 | } 149 | var cfg = utils.freshRequire(path.resolve( 150 | that.project_dir, 'public/js/vendor/require.config' 151 | )); 152 | test.same(cfg.packages.sort(), [ 153 | { 154 | name: 'package-one', 155 | location: 'js/vendor/package-one' 156 | } 157 | ]); 158 | var p = path.resolve( 159 | that.project_dir, 160 | 'public/js/vendor/package-two' 161 | ); 162 | pathExists(p, function (exists) { 163 | test.ok(!exists, 'package-two directory removed'); 164 | cb(); 165 | }); 166 | }); 167 | } 168 | ], 169 | test.done); 170 | } 171 | 172 | }; 173 | -------------------------------------------------------------------------------- /lib/commands/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var path = require('path'), 6 | fs = require('fs'), 7 | async = require('async'), 8 | install = require('./install'), 9 | utils = require('../utils'), 10 | rimraf = require('rimraf'), 11 | settings = require('../settings'), 12 | project = require('../project'), 13 | tree = require('../tree'); 14 | logger = require('../logger'), 15 | argParse = require('../args').parse, 16 | _ = require('underscore'); 17 | 18 | 19 | /** 20 | * Usage information and docs 21 | */ 22 | 23 | exports.summary = 'Removes unused packages from the package directory'; 24 | 25 | 26 | exports.usage = '' + 27 | 'jam clean\n' + 28 | '\n' + 29 | 'Options:\n' + 30 | ' -d, --package-dir Package directory (defaults to "PATH/jam")\n' + 31 | ' -f, --force Do not confirm package removal'; 32 | 33 | 34 | /** 35 | * Run function called when "jam clean" command is used 36 | * 37 | * @param {Object} settings - the values from .jamrc files 38 | * @param {Array} args - command-line arguments 39 | */ 40 | 41 | exports.run = function (settings, args) { 42 | var a = argParse(args, { 43 | 'target_dir': {match: ['-d', '--package-dir'], value: true}, 44 | 'baseurl': {match: ['-b', '--baseurl'], value: true}, 45 | 'force': {match: ['-f', '--force']} 46 | }); 47 | var opt = a.options; 48 | var cwd = process.cwd(); 49 | 50 | install.initDir(settings, cwd, opt, function (err, opt, cfg, proj_dir) { 51 | if (err) { 52 | return logger.error(err); 53 | } 54 | 55 | if (!opt.target_dir) { 56 | if (cfg.jam && cfg.jam.packageDir) { 57 | opt.target_dir = path.resolve(proj_dir, cfg.jam.packageDir); 58 | } 59 | else { 60 | opt.target_dir = path.resolve(proj_dir, settings.package_dir || ''); 61 | } 62 | } 63 | if (!opt.baseurl) { 64 | if (cfg.jam && cfg.jam.baseUrl) { 65 | opt.baseurl = path.resolve(proj_dir, cfg.jam.baseUrl); 66 | } 67 | else { 68 | opt.baseurl = path.resolve(proj_dir, settings.baseUrl || ''); 69 | } 70 | } 71 | 72 | exports.clean(cwd, opt, function (err) { 73 | if (err) { 74 | return logger.error(err); 75 | } 76 | logger.end(); 77 | }); 78 | }); 79 | }; 80 | 81 | 82 | /** 83 | * Clean the project directory's dependencies. 84 | * 85 | * @param {Object} opt - the options object 86 | * @param {Function} callback 87 | */ 88 | 89 | exports.clean = function (cwd, opt, callback) { 90 | exports.unusedDirs(cwd, opt, function (err, dirs) { 91 | if (err) { 92 | return callback(err); 93 | } 94 | if (!dirs.length) { 95 | // nothing to remove 96 | return logger.end(); 97 | } 98 | var reldirs = dirs.map(function (d) { 99 | return path.relative(opt.taget_dir, d); 100 | }); 101 | 102 | if (opt.force) { 103 | exports.deleteDirs(dirs, callback); 104 | } 105 | else { 106 | console.log( 107 | '\n' + 108 | 'The following directories are not required by packages in\n' + 109 | 'package.json and will be REMOVED:\n\n' + 110 | ' ' + reldirs.join('\n ') + 111 | '\n' 112 | ); 113 | utils.getConfirmation('Continue', function (err, ok) { 114 | if (err) { 115 | return callback(err); 116 | } 117 | if (ok) { 118 | exports.deleteDirs(dirs, function (err) { 119 | if (err) { 120 | return callback(err); 121 | } 122 | project.updateRequireConfig( 123 | opt.target_dir, 124 | opt.baseurl, 125 | callback 126 | ); 127 | }); 128 | } 129 | else { 130 | logger.clean_exit = true; 131 | } 132 | }); 133 | } 134 | }); 135 | }; 136 | 137 | 138 | /** 139 | * Delete multiple directory paths. 140 | * 141 | * @param {Array} dirs 142 | * @param {Function} callback 143 | */ 144 | 145 | exports.deleteDirs = function (dirs, callback) { 146 | async.forEach(dirs, function (d, cb) { 147 | logger.info('removing', path.basename(d)); 148 | rimraf(d, cb); 149 | }, 150 | callback); 151 | }; 152 | 153 | 154 | /** 155 | * Discover package directories that do not form part of the current 156 | * package version tree. 157 | * 158 | * @param {Object} opt - the options object 159 | * @param {Function} callback 160 | */ 161 | 162 | exports.unusedDirs = function (cwd, opt, callback) { 163 | project.loadPackageJSON(cwd, function (err, cfg) { 164 | if (err) { 165 | return callback(err); 166 | } 167 | 168 | if (!cfg) { 169 | cfg = project.DEFAULT; 170 | } 171 | 172 | var sources = [ 173 | install.dirSource(opt.target_dir) 174 | ]; 175 | var newcfg = utils.convertToRootCfg(cfg); 176 | var pkg = { 177 | config: newcfg, 178 | source: 'root' 179 | }; 180 | logger.info('Building version tree...'); 181 | tree.build(pkg, sources, function (err, packages) { 182 | if (err) { 183 | return callback(err); 184 | } 185 | return exports.unusedDirsTree(packages, opt, callback); 186 | }); 187 | }); 188 | }; 189 | 190 | 191 | /** 192 | * Lists packages in the package dir and compares against the provided 193 | * version tree, returning the packages not in the tree. 194 | * 195 | * @param {Object} packages - version tree 196 | * @param {Object} opt - options object 197 | * @param {Function} callback 198 | */ 199 | 200 | exports.unusedDirsTree = function (packages, opt, callback) { 201 | utils.listDirs(opt.target_dir, function (err, dirs) { 202 | if (err) { 203 | return callback(err); 204 | } 205 | var unused = _.difference(dirs, Object.keys(packages)); 206 | var unused = []; 207 | var names = Object.keys(packages); 208 | dirs.forEach(function (d) { 209 | if (!_.contains(names, path.basename(d))) { 210 | unused.push(d); 211 | } 212 | }); 213 | return callback(null, unused); 214 | }); 215 | }; 216 | -------------------------------------------------------------------------------- /test/integration/test-rangedeps-publish-install-upgrade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test description 3 | * ================ 4 | * 5 | * Tests that range requirements for dependencies in package.json are 6 | * respected. 7 | * 8 | * Starting with project with *ranged* deps in package.json 9 | * - jam publish package-one @ 0.0.1 10 | * - jam publish package-two @ 0.0.1 11 | * - jam install, test installation succeeded 12 | * - jam publish package-one @ 0.0.2 13 | * - jam publish package-two @ 0.0.2 14 | * - jam publish package-one @ 0.0.3 // this should not get installed 15 | * - jam upgrade, test package versions (both @ 0.0.2) 16 | */ 17 | 18 | 19 | var couchdb = require('../../lib/couchdb'), 20 | logger = require('../../lib/logger'), 21 | env = require('../../lib/env'), 22 | utils = require('../utils'), 23 | async = require('async'), 24 | http = require('http'), 25 | path = require('path'), 26 | ncp = require('ncp').ncp, 27 | fs = require('fs'), 28 | _ = require('underscore'); 29 | 30 | 31 | var pathExists = fs.exists || path.exists; 32 | 33 | 34 | logger.clean_exit = true; 35 | 36 | // CouchDB database URL to use for testing 37 | var TESTDB = process.env['JAM_TEST_DB'], 38 | BIN = path.resolve(__dirname, '../../bin/jam.js'), 39 | ENV = {JAM_TEST: 'true', JAM_TEST_DB: TESTDB}; 40 | 41 | if (!TESTDB) { 42 | throw 'JAM_TEST_DB environment variable not set'; 43 | } 44 | 45 | // remove trailing-slash from TESTDB URL 46 | TESTDB = TESTDB.replace(/\/$/, ''); 47 | 48 | 49 | exports.setUp = function (callback) { 50 | // change to integration test directory before running test 51 | this._cwd = process.cwd(); 52 | process.chdir(__dirname); 53 | 54 | // recreate any existing test db 55 | couchdb(TESTDB).deleteDB(function (err) { 56 | if (err && err.error !== 'not_found') { 57 | return callback(err); 58 | } 59 | // create test db 60 | couchdb(TESTDB).createDB(callback); 61 | }); 62 | }; 63 | 64 | exports.tearDown = function (callback) { 65 | // change back to original working directory after running test 66 | process.chdir(this._cwd); 67 | // delete test db 68 | couchdb(TESTDB).deleteDB(callback); 69 | }; 70 | 71 | 72 | exports['project with ranged dependencies in package.json'] = { 73 | 74 | setUp: function (callback) { 75 | this.project_dir = path.resolve(env.temp, 'jamtest-' + Math.random()); 76 | // set current project to empty directory 77 | ncp('./fixtures/project-rangedeps', this.project_dir, callback); 78 | }, 79 | 80 | /* 81 | tearDown: function (callback) { 82 | var that = this; 83 | // clear current project 84 | //utils.myrimraf(that.project_dir, callback); 85 | }, 86 | */ 87 | 88 | 'publish, install, ls, remove': function (test) { 89 | test.expect(6); 90 | var that = this; 91 | process.chdir(that.project_dir); 92 | 93 | async.series([ 94 | async.apply( 95 | utils.runJam, 96 | ['publish', path.resolve(__dirname, 'fixtures', 'package-one')], 97 | {env: ENV} 98 | ), 99 | async.apply( 100 | utils.runJam, 101 | ['publish', path.resolve(__dirname, 'fixtures', 'package-two')], 102 | {env: ENV} 103 | ), 104 | async.apply(utils.runJam, ['install'], {env: ENV}), 105 | function (cb) { 106 | var cfg = utils.freshRequire( 107 | path.resolve(that.project_dir, 'jam', 'require.config') 108 | ); 109 | var packages = _.sortBy(cfg.packages, function (p) { 110 | return p.name; 111 | }); 112 | test.same(packages, [ 113 | { 114 | name: 'package-one', 115 | location: 'jam/package-one' 116 | }, 117 | { 118 | name: 'package-two', 119 | location: 'jam/package-two', 120 | main: 'two.js' 121 | } 122 | ]); 123 | var p1 = path.resolve( 124 | that.project_dir, 125 | 'jam/package-one/package.json' 126 | ); 127 | var p1pkg = JSON.parse(fs.readFileSync(p1).toString()); 128 | test.equal(p1pkg.version, '0.0.1'); 129 | var p2 = path.resolve( 130 | that.project_dir, 131 | 'jam/package-two/package.json' 132 | ); 133 | var p2pkg = JSON.parse(fs.readFileSync(p2).toString()); 134 | test.equal(p2pkg.version, '0.0.1'); 135 | cb(); 136 | }, 137 | async.apply( 138 | utils.runJam, 139 | [ 140 | 'publish', 141 | path.resolve(__dirname, 'fixtures', 'package-one-v2') 142 | ], 143 | {env: ENV} 144 | ), 145 | async.apply( 146 | utils.runJam, 147 | [ 148 | 'publish', 149 | path.resolve(__dirname, 'fixtures', 'package-two-v2') 150 | ], 151 | {env: ENV} 152 | ), 153 | async.apply( 154 | utils.runJam, 155 | [ 156 | 'publish', 157 | path.resolve(__dirname, 'fixtures', 'package-one-v3') 158 | ], 159 | {env: ENV} 160 | ), 161 | function (cb) { 162 | var args = ['upgrade']; 163 | utils.runJam(args, {env: ENV}, function (err, stdout, stderr) { 164 | if (err) { 165 | return cb(err); 166 | } 167 | var cfg = utils.freshRequire( 168 | path.resolve(that.project_dir, 'jam', 'require.config') 169 | ); 170 | var packages= _.sortBy(cfg.packages, function (p) { 171 | return p.name; 172 | }); 173 | test.same(cfg.packages, [ 174 | { 175 | name: 'package-one', 176 | location: 'jam/package-one' 177 | }, 178 | { 179 | name: 'package-two', 180 | location: 'jam/package-two', 181 | main: 'two.js' 182 | } 183 | ]); 184 | var p1 = path.resolve( 185 | that.project_dir, 186 | 'jam/package-one/package.json' 187 | ); 188 | var p1pkg = JSON.parse(fs.readFileSync(p1).toString()); 189 | test.equal(p1pkg.version, '0.0.2'); 190 | var p2 = path.resolve( 191 | that.project_dir, 192 | 'jam/package-two/package.json' 193 | ); 194 | var p2pkg = JSON.parse(fs.readFileSync(p2).toString()); 195 | test.equal(p2pkg.version, '0.0.2'); 196 | cb(); 197 | }); 198 | } 199 | ], 200 | test.done); 201 | } 202 | 203 | }; 204 | -------------------------------------------------------------------------------- /lib/versions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities for dealing with package versions 3 | */ 4 | 5 | var semver = require('semver'), 6 | logger = require('./logger'), 7 | _ = require('underscore'); 8 | 9 | 10 | /** 11 | * Sorts an array of version numbers in descending semver order (highest 12 | * version number first). This is an alternative to semver.rcompare since it 13 | * doesn't appear to work as expected. 14 | * 15 | * @param {Array} versions - an array of version number strings 16 | * @returns {Array} 17 | */ 18 | 19 | exports.sortDescending = function (versions) { 20 | // for some reason semver.rcompare doesn't work 21 | return versions.slice().sort(semver.compare).reverse(); 22 | }; 23 | 24 | 25 | /** 26 | * Returns the highest version number in an array. 27 | * 28 | * @param {Array} versions - an array of version number strings 29 | * @returns {String} 30 | */ 31 | 32 | exports.max = function (versions) { 33 | return exports.sortDescending(versions)[0]; 34 | }; 35 | 36 | exports.satisfying = function(versions, ranges) { 37 | return versions.filter(function (v) { 38 | return exports.satisfiesAll(v, ranges); 39 | }); 40 | }; 41 | 42 | /** 43 | * Checks an array of range requirements against an array of available versions, 44 | * returning the highest version number that satisfies all ranges or null if 45 | * all ranges can't be satisfied. 46 | * 47 | * @param {Array} versions - an array of version strings 48 | * @param {Array} ranges - an array of range strings 49 | * @returns {String|null} 50 | */ 51 | 52 | exports.maxSatisfying = function (versions, ranges) { 53 | return _.chain(exports.satisfying(versions, ranges)) 54 | .map(function(version) { 55 | var equality, inequal; 56 | 57 | equality = 0; 58 | inequal = []; 59 | 60 | _.forEach(ranges, function(r) { 61 | var index, length, equal, versionParsed, versionBuild, versionHunk, rangeHunk, rangeBuild, rangeParsed, range; 62 | 63 | range = r.valueOf(); 64 | 65 | versionParsed = semver.parse(version); 66 | rangeParsed = semver.parse(range); 67 | 68 | // try to calc equality of same versions 69 | if ( versionParsed && rangeParsed && semver.eq(version, range) ) { 70 | 71 | versionBuild = versionParsed.build; 72 | rangeBuild = rangeParsed.build; 73 | 74 | if (_.isEqual(versionBuild, rangeBuild)) { 75 | equality+= 1; 76 | } else { 77 | length = rangeBuild.length; 78 | equal = 0; 79 | 80 | if (length > 0) { 81 | for (index = 0; index < length; index++) { 82 | versionHunk = versionBuild[index]; 83 | rangeHunk = rangeBuild[index]; 84 | 85 | if (versionHunk == rangeHunk) { 86 | equal++; 87 | } else { 88 | inequal.push(versionHunk); 89 | break; 90 | } 91 | } 92 | 93 | equality+= equal / length; 94 | } 95 | } 96 | } 97 | }); 98 | 99 | return { 100 | criteria: [ equality].concat(inequal), 101 | data: version, 102 | compare: function(other) { 103 | return semver.compare(this.data, other.data); 104 | } 105 | }; 106 | }) 107 | .sort(function(a, b) { 108 | var aCriteria, bCriteria, aValue, bValue, compared, 109 | index, length, result; 110 | 111 | compared = a.compare(b); 112 | 113 | if (compared != 0) { 114 | result = compared; 115 | } else { 116 | aCriteria = a.criteria; 117 | bCriteria = b.criteria; 118 | 119 | length = aCriteria.length; 120 | index = 0; 121 | 122 | for (; index < length; index++) { 123 | aValue = aCriteria[index]; 124 | bValue = bCriteria[index]; 125 | 126 | if (aValue == bValue) { 127 | continue; 128 | } 129 | 130 | if (aValue < bValue) { 131 | result = -1; 132 | } else { 133 | result = 1; 134 | } 135 | 136 | break; 137 | } 138 | } 139 | 140 | if (_.isUndefined(result)) { 141 | result = 0; 142 | } 143 | 144 | return result; 145 | }) 146 | .pluck('data') 147 | .last() 148 | .value(); 149 | }; 150 | 151 | 152 | /** 153 | * Checks if a version number satisfies an array of range requirements. 154 | * 155 | * @param {String} version - a semver version string 156 | * @param {Array} ranges - an array of range strings 157 | * @returns {Boolean} 158 | */ 159 | 160 | exports.satisfiesAll = function (version, ranges) { 161 | return ranges.every(function(range) { 162 | var satisfies; 163 | 164 | if (!semver.valid(version)) { 165 | logger.warning('Could not compare version "' + version + '" cause it is not a valid semver'); 166 | return false; 167 | } 168 | 169 | // if range is null, linked, installed from URL, or from GitHub, or from Git, 170 | // then any version satisfies that requirement 171 | satisfies = false; 172 | satisfies = satisfies || !range; 173 | 174 | // todo is it good? 175 | satisfies = satisfies || range === 'linked'; 176 | 177 | satisfies = satisfies || semver.satisfies(version, range.valueOf()); 178 | 179 | return satisfies; 180 | }); 181 | }; 182 | 183 | 184 | /** 185 | * Experimental method. 186 | * 187 | * @deprecated 188 | * @param version 189 | * @returns {*} 190 | */ 191 | exports.validify = function(version) { 192 | var match, preName, preVersion, result; 193 | 194 | if (semver.valid(version)) { 195 | return version; 196 | } 197 | 198 | // match as 199 | // #1 version 200 | // #2 prerelease name delimiter 201 | // #3 prerelease name 202 | // #4 prerelease version delimiter 203 | // #5 prerelease version 204 | if (match = /(\d+(?:\.\d+(?:\.\d+)?)?)(-)?([a-z]{0,})?(\.)?(\d+)?/i.exec(version)) { 205 | result = match[1]; 206 | 207 | if (preName = match[3]) { 208 | result += "-" + preName; 209 | 210 | if (preVersion = match[5]) { 211 | result += "." + preVersion; 212 | } 213 | } 214 | 215 | return result; 216 | } 217 | 218 | return null; 219 | }; 220 | 221 | exports.equalButNotMeta = function(version, ranges) { 222 | var parsed; 223 | 224 | parsed = semver.parse(version); 225 | 226 | return _.chain(ranges) 227 | .map(function(r) { 228 | var strictVersion, range; 229 | 230 | range = r.valueOf(); 231 | 232 | if ( strictVersion = semver.parse(range) ) { 233 | if ( semver.eq(parsed, strictVersion) && !_.isEqual(parsed.build, strictVersion.build) ) { 234 | return range; 235 | } 236 | } 237 | }) 238 | .filter() 239 | .value(); 240 | }; -------------------------------------------------------------------------------- /test/unit/test-lib-commands-install.js: -------------------------------------------------------------------------------- 1 | var install = require('../../lib/commands/install'); 2 | var logger = require("../../lib/logger"); 3 | var sinon = require("sinon"); 4 | var cuculus = require("cuculus"); 5 | var path = require("path"); 6 | 7 | //logger.clean_exit = true; 8 | 9 | 10 | exports.setUp = function(cb) { 11 | cuculus.modify(path.resolve(__dirname, '../../lib/jamrc'), function(jamrc, onRestore){ 12 | var stub; 13 | 14 | stub = sinon.stub(jamrc, "load", function(cb) { 15 | cb(null, { 16 | repositories: [ 17 | "http://jamjs.org/A", 18 | "http://jamjs.org/B", 19 | "http://jamjs.org/C" 20 | ] 21 | }); 22 | }); 23 | 24 | onRestore(stub.restore.bind(stub)); 25 | }); 26 | 27 | cb(); 28 | }; 29 | 30 | exports.tearDown = function(cb) { 31 | // cleanup 32 | cuculus.restore(path.resolve(__dirname, '../../lib/jamrc')); 33 | cb(); 34 | }; 35 | 36 | exports['extractValidVersion - multiple git'] = function (test) { 37 | var pkg; 38 | 39 | // before 40 | pkg = { 41 | "current_version": "1.0.5", 42 | "requirements": [ 43 | { 44 | "enabled": true, 45 | "path": { 46 | "enabled": true, 47 | "name": "toolkit", 48 | "parents": [ 49 | "root" 50 | ] 51 | }, 52 | "range": { 53 | "range": "1.0.5", 54 | "source": "git://path.to/repo.git#2222" 55 | } 56 | }, 57 | { 58 | "enabled": true, 59 | "path": { 60 | "enabled": true, 61 | "name": "toolkit", 62 | "parents": [ 63 | "root", 64 | "viewer" 65 | ] 66 | }, 67 | "range": { 68 | "range": "1.0.5", 69 | "source": "git://path.to/repo.git#2222" 70 | } 71 | } 72 | ], 73 | "versions": [ 74 | { 75 | "priority": 0, 76 | "source": "git", 77 | "version": "1.0.5", 78 | git: { 79 | path: "git://path.to/repo.git", 80 | commitish: "2222", 81 | uri: "git://path.to/repo.git#2222" 82 | }, 83 | "config": { 84 | "name": "toolkit", 85 | "version": "1.0.5", 86 | "jam": {} 87 | } 88 | }, 89 | { 90 | "priority": 0, 91 | "source": "git", 92 | "version": "1.0.5", 93 | git: { 94 | path: "git://path.to/repo.git", 95 | commitish: "2222", 96 | uri: "git://path.to/repo.git#2222" 97 | }, 98 | "config": { 99 | "name": "toolkit", 100 | "version": "1.0.5", 101 | "jam": {} 102 | } 103 | } 104 | ] 105 | }; 106 | 107 | install.extractValidVersion(pkg, "test", function(err) { 108 | if (err) { 109 | logger.error(err); 110 | } 111 | test.done(err); 112 | }); 113 | }; 114 | 115 | exports['extractValidVersion - should get less prioritized, if it is repository source'] = function (test) { 116 | var pkg; 117 | 118 | // before 119 | pkg = { 120 | "current_version": "1.0.5", 121 | "requirements": [ 122 | { 123 | "enabled": true, 124 | "path": { 125 | "enabled": true, 126 | "name": "toolkit", 127 | "parents": [ 128 | "root" 129 | ] 130 | }, 131 | "range": { 132 | "range": "1.0.5", 133 | "source": "1.0.5" 134 | } 135 | } 136 | ], 137 | "versions": [ 138 | { 139 | "priority": 0, 140 | "source": "repository", 141 | "version": "1.0.5", 142 | "repository": "http://jamjs.org/B", 143 | "config": { 144 | "name": "toolkit", 145 | "version": "1.0.5", 146 | "jam": {} 147 | } 148 | }, 149 | { 150 | "priority": 0, 151 | "source": "repository", 152 | "version": "1.0.5", 153 | "repository": "http://jamjs.org/A", 154 | "config": { 155 | "name": "toolkit", 156 | "version": "1.0.5", 157 | "jam": {} 158 | } 159 | }, 160 | { 161 | "priority": 0, 162 | "source": "repository", 163 | "version": "1.0.5", 164 | "repository": "http://jamjs.org/C", 165 | "config": { 166 | "name": "toolkit", 167 | "version": "1.0.5", 168 | "jam": {} 169 | } 170 | } 171 | ] 172 | }; 173 | 174 | install.extractValidVersion(pkg, "test", function(err, version) { 175 | if (err) { 176 | logger.error(err); 177 | } 178 | if (version.repository != "http://jamjs.org/A") { 179 | logger.error(err = new Error("Not prioritized repo!")) 180 | } 181 | test.done(err); 182 | }); 183 | }; 184 | 185 | exports['extractValidVersion - should throw'] = function (test) { 186 | var pkg; 187 | 188 | // before 189 | pkg = { 190 | "current_version": "1.0.5", 191 | "requirements": [ 192 | { 193 | "enabled": true, 194 | "path": { 195 | "enabled": true, 196 | "name": "toolkit", 197 | "parents": [ 198 | "root" 199 | ] 200 | }, 201 | "range": { 202 | "range": "1.0.5", 203 | "source": "1.0.5" 204 | } 205 | } 206 | ], 207 | "versions": [ 208 | { 209 | "priority": 0, 210 | "source": "repository", 211 | "version": "1.0.5", 212 | "repository": "http://testrepo.org", 213 | "config": { 214 | "name": "toolkit", 215 | "version": "1.0.5", 216 | "jam": {} 217 | } 218 | }, 219 | { 220 | "priority": 0, 221 | "source": "git", 222 | "version": "1.0.5", 223 | git: { 224 | path: "git://path.to/repo.git", 225 | commitish: "2222", 226 | uri: "git://path.to/repo.git#2222" 227 | }, 228 | "config": { 229 | "name": "toolkit", 230 | "version": "1.0.5", 231 | "jam": {} 232 | } 233 | } 234 | ] 235 | }; 236 | 237 | install.extractValidVersion(pkg, "test", function(err) { 238 | var msg; 239 | 240 | msg = 'Multiple sources are found for the package "test@1.0.5":\n' + 241 | '\thttp://testrepo.org\n' + 242 | '\tgit://path.to/repo.git#2222\n'; 243 | 244 | test.ok(err == msg, "Should throw an Error"); 245 | test.done(); 246 | }); 247 | }; 248 | -------------------------------------------------------------------------------- /lib/packages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Functions related to the finding, loading and manipulation of Jam packages 3 | * 4 | * @module 5 | */ 6 | 7 | var settings = require('./settings'), 8 | versions = require('./versions'), 9 | async = require('async'), 10 | logger = require('./logger'), 11 | utils = require('./utils'), 12 | path = require('path'), 13 | fs = require('fs'), 14 | env = require('./env'), 15 | semver = require('semver'), 16 | events = require('events'); 17 | 18 | 19 | var pathExists = fs.exists || path.exists; 20 | 21 | 22 | /** 23 | * Resolve the target package and its dependencies, reading the package.json files 24 | * and adding them to cache object. 25 | * 26 | * @param {Object} cache - an object to add packages metadata and path info to 27 | * @param {String} name - name of package to load 28 | * @param {String} range - acceptable version range for target package 29 | * @param {Array} paths - lookup paths for finding packages 30 | * @param {String} source - the original location for resolving relative paths 31 | * @param {String} parent - name of the parent package (if any) 32 | * @param {Function} cb - callback function 33 | */ 34 | 35 | exports.readMeta = function (cache, name, range, paths, source, parent, cb) { 36 | var cached = cache[name] = { 37 | ready: false, 38 | ranges: [range], 39 | parent: parent, 40 | ev: new events.EventEmitter() 41 | }; 42 | cached.ev.setMaxListeners(10000); 43 | exports.resolve(name, range, paths, source, function (err, v, doc, p) { 44 | if (err) { 45 | return cb(err); 46 | } 47 | cached.path = p; 48 | settings.load(p, function (err, cfg) { 49 | cached.cfg = cfg; 50 | cached.ready = true; 51 | cached.ev.emit('ready'); 52 | paths = paths.concat([p + '/jam']); 53 | exports.readMetaDependencies(cache, cache[name], paths, source, cb); 54 | }); 55 | }); 56 | }; 57 | 58 | 59 | /** 60 | * Read dependencies of a cached package loaded by the readMeta function. 61 | * 62 | * @param {Object} cache - an object to add packages metadata and path info to 63 | * @param {Object} pkg - the cached package object 64 | * @param {Array} paths - lookup paths for finding packages 65 | * @param {String} source - the original location for resolving relative paths 66 | * @param {Function} callback - the callback function 67 | */ 68 | 69 | exports.readMetaDependencies = function (cache, pkg, paths, source, callback) { 70 | var deps = Object.keys(pkg.cfg.dependencies || {}); 71 | 72 | async.forEach(deps, function (dep, cb) { 73 | var range = pkg.cfg.dependencies[dep]; 74 | 75 | function testVersion(cached, range) { 76 | if (!semver.satisfies(cached.cfg.version, range)) { 77 | return callback(new Error( 78 | 'Conflicting version requirements for ' + 79 | cached.cfg.name + ':\n' + 80 | 'Version ' + cached.cfg.version + ' loaded by "' + 81 | cached.parent + '" but "' + pkg.cfg.name + 82 | '" requires ' + range 83 | )); 84 | } 85 | } 86 | if (cache[dep]) { 87 | var cached = cache[dep]; 88 | cached.ranges.push(range); 89 | if (cached.ready) { 90 | testVersion(cached, range); 91 | // return loaded copy 92 | return cb(null, cached); 93 | } 94 | else { 95 | // wait for existing request to load 96 | cached.ev.on('ready', function () { 97 | testVersion(cached, range); 98 | return cb(null, cached); 99 | }); 100 | return; 101 | } 102 | } 103 | else { 104 | exports.readMeta( 105 | cache, dep, range, paths, source, pkg.cfg.name, cb 106 | ); 107 | } 108 | }, callback); 109 | }; 110 | 111 | 112 | /** 113 | * Generates an array of possible paths from the package name, 114 | * source package path and array of package lookup paths (from .jamrc) 115 | * 116 | * @param {String} name - the name / path of the package to lookup 117 | * @param {String} source - the current package that paths are relative to 118 | * @param {Array} paths - an array of package lookup paths 119 | * @returns {Array} 120 | */ 121 | 122 | exports.resolveCandidates = function (name, source, paths) { 123 | var candidates = []; 124 | if ( env.isAbsolute(name) ){ 125 | // absolute path to a specific package directory 126 | candidates.push(name); 127 | } 128 | else if (name[0] === '.') { 129 | // relative path to a specific package directory 130 | candidates.push(path.normalize(path.join(source, name))); 131 | } 132 | else { 133 | // just a package name, use lookup paths 134 | candidates = candidates.concat(paths.map(function (dir) { 135 | return path.join(dir, name); 136 | })); 137 | } 138 | return candidates; 139 | }; 140 | 141 | 142 | /** 143 | * Returns an object keyed by version number, containing the path and cfg 144 | * for each version, giving priority to paths earlier in the candidates list. 145 | * 146 | * eg, with candidates = [pathA, pathB], if both paths contained v1 of the 147 | * package, pathA and the package.json values from that path will be used for 148 | * that version, because it comes before pathB in the candidates array. 149 | * 150 | * @param {Array} candidates - an array of possible package paths 151 | * @param {Function} callback 152 | */ 153 | 154 | exports.availableVersions = function (candidates, callback) { 155 | var versions = []; 156 | async.forEach(candidates, function (c, cb) { 157 | pathExists(path.join(c, 'package.json'), function (exists) { 158 | if (exists) { 159 | settings.load(c, function (err, doc) { 160 | if (err) { 161 | return cb(err); 162 | } 163 | //if (!versions[doc.version]) { 164 | versions.push({ 165 | source: 'local', 166 | path: c, 167 | config: doc, 168 | version: doc.version 169 | }); 170 | //} 171 | cb(); 172 | }); 173 | } 174 | else { 175 | cb(); 176 | } 177 | }); 178 | }, 179 | function (err) { 180 | callback(err, versions); 181 | }); 182 | }; 183 | 184 | 185 | /** 186 | * Looks up the path to a specified package, returning an error if not found. 187 | * 188 | * @param {String} name - the name / path of the package to lookup 189 | * @param {String} range - a version or range of versions to match against 190 | * @param {Array} paths - an array of package lookup paths 191 | * @param {String} source - the current package that paths are relative to 192 | * @param {Function} callback 193 | */ 194 | 195 | exports.resolve = async.memoize(function (name, ranges, paths, source, callback) { 196 | if (!Array.isArray(ranges)) { 197 | ranges = [ranges]; 198 | } 199 | source = source || process.cwd(); 200 | 201 | var candidates = exports.resolveCandidates(name, source, paths); 202 | 203 | exports.availableVersions(candidates, function (err, matches) { 204 | var e; 205 | if (err) { 206 | return callback(err); 207 | } 208 | var vers = Object.keys(matches); 209 | var highest = versions.maxSatisfying(vers, ranges); 210 | if (highest) { 211 | var m = matches[highest]; 212 | return callback(null, highest, m.config, m.path); 213 | } 214 | if (vers.length) { 215 | e = new Error( 216 | "Cannot find package '" + name + "' matching " + 217 | ranges.join(' && ') + "\n" + 218 | "Available versions: " + vers.join(', ') 219 | ); 220 | e.missing = true; 221 | return callback(e); 222 | } 223 | else { 224 | e = new Error("Cannot find package '" + name + "'"); 225 | e.missing = true; 226 | return callback(e); 227 | } 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /lib/schema/package-full.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "JSON schema for NPM package.json files", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | 5 | "type": "object", 6 | "required": [ "name", "version" ], 7 | 8 | "definitions": { 9 | "person": { 10 | "description": "A person who has been involved in creating or maintaining this package", 11 | "type": [ "object", "string" ], 12 | "required": [ "name" ], 13 | "properties": { 14 | "name": { 15 | "type": "string" 16 | }, 17 | "url": { 18 | "type": "string", 19 | "format": "uri" 20 | }, 21 | "email": { 22 | "type": "string", 23 | "format": "email" 24 | } 25 | } 26 | }, 27 | "dependency": { 28 | "description": "Dependencies are specified with a simple hash of package name to version range. The version range is a string which has one or more space-separated descriptors. Dependencies can also be identified with a tarball or git URL.", 29 | "type": "object", 30 | "additionalProperties": { 31 | "type": "string" 32 | } 33 | } 34 | }, 35 | 36 | "patternProperties": { 37 | "^_": { 38 | "description": "Any property starting with _ is valid.", 39 | "additionalProperties": true, 40 | "additionalItems": true 41 | } 42 | }, 43 | 44 | "properties": { 45 | "name": { 46 | "description": "The name of the package.", 47 | "type": "string" 48 | }, 49 | "version": { 50 | "description": "Version must be parseable by node-semver, which is bundled with npm as a dependency.", 51 | "type": "string" 52 | }, 53 | "description": { 54 | "description": "This helps people discover your package, as it's listed in 'npm search'.", 55 | "type": "string" 56 | }, 57 | "keywords": { 58 | "description": "This helps people discover your package as it's listed in 'npm search'.", 59 | "type": "array" 60 | }, 61 | "homepage": { 62 | "description": "The url to the project homepage.", 63 | "type": "string" 64 | }, 65 | "bugs": { 66 | "description": "The url to your project's issue tracker and / or the email address to which issues should be reported. These are helpful for people who encounter issues with your package.", 67 | "type": [ "object", "string" ], 68 | "properties": { 69 | "url": { 70 | "type": "string", 71 | "description": "The url to your project's issue tracker.", 72 | "format": "uri" 73 | }, 74 | "email": { 75 | "type": "string", 76 | "description": "The email address to which issues should be reported." 77 | } 78 | } 79 | }, 80 | "license": { 81 | "type": "string", 82 | "description": "You should specify a license for your package so that people know how they are permitted to use it, and any restrictions you're placing on it." 83 | }, 84 | "licenses": { 85 | "description": "You should specify a license for your package so that people know how they are permitted to use it, and any restrictions you're placing on it.", 86 | "type": "array", 87 | "items": { 88 | "type": "object", 89 | "properties": { 90 | "type": { 91 | "type": "string" 92 | }, 93 | "url": { 94 | "type": "string", 95 | "format": "uri" 96 | } 97 | } 98 | } 99 | }, 100 | "author": { 101 | "$ref": "#/definitions/person" 102 | }, 103 | "contributors": { 104 | "description": "A list of people who contributed to this package.", 105 | "type": "array", 106 | "items": { 107 | "$ref": "#/definitions/person" 108 | } 109 | }, 110 | "maintainers": { 111 | "description": "A list of people who maintains this package.", 112 | "type": "array", 113 | "items": { 114 | "$ref": "#/definitions/person" 115 | } 116 | }, 117 | "files": { 118 | "description": "The 'files' field is an array of files to include in your project. If you name a folder in the array, then it will also include the files inside that folder.", 119 | "type": "array", 120 | "items": { 121 | "type": "string" 122 | } 123 | }, 124 | "main": { 125 | "description": "The main field is a module ID that is the primary entry point to your program.", 126 | "type": "string" 127 | }, 128 | "bin": { 129 | "type": [ "string", "object" ], 130 | "additionalProperties": { 131 | "type": "string" 132 | } 133 | }, 134 | "man": { 135 | "type": [ "array", "string" ], 136 | "description": "Specify either a single file or an array of filenames to put in place for the man program to find.", 137 | "items": { 138 | "type": "string" 139 | } 140 | }, 141 | "directories": { 142 | "type": "object", 143 | "properties": { 144 | "bin": { 145 | "description": "If you specify a 'bin' directory, then all the files in that folder will be used as the 'bin' hash.", 146 | "type": "string" 147 | }, 148 | "doc": { 149 | "description": "Put markdown files in here. Eventually, these will be displayed nicely, maybe, someday.", 150 | "type": "string" 151 | }, 152 | "example": { 153 | "description": "Put example scripts in here. Someday, it might be exposed in some clever way.", 154 | "type": "string" 155 | }, 156 | "lib": { 157 | "description": "Tell people where the bulk of your library is. Nothing special is done with the lib folder in any way, but it's useful meta info.", 158 | "type": "string" 159 | }, 160 | "man": { 161 | "description": "A folder that is full of man pages. Sugar to generate a 'man' array by walking the folder.", 162 | "type": "string" 163 | }, 164 | "test": { 165 | "type": "string" 166 | } 167 | } 168 | }, 169 | "repository": { 170 | "description": "Specify the place where your code lives. This is helpful for people who want to contribute.", 171 | "type": "object", 172 | "properties": { 173 | "type": { 174 | "type": "string" 175 | }, 176 | "url": { 177 | "type": "string" 178 | } 179 | } 180 | }, 181 | "scripts": { 182 | "description": "The 'scripts' member is an object hash of script commands that are run at various times in the lifecycle of your package. The key is the lifecycle event, and the value is the command to run at that point.", 183 | "type": "object", 184 | "additionalProperties": { 185 | "type": "string" 186 | } 187 | }, 188 | "config": { 189 | "description": "A 'config' hash can be used to set configuration parameters used in package scripts that persist across upgrades.", 190 | "type": "object", 191 | "additionalProperties": true 192 | }, 193 | "dependencies": { 194 | "$ref": "#/definitions/dependency" 195 | }, 196 | "devDependencies": { 197 | "$ref": "#/definitions/dependency" 198 | }, 199 | "bundleDependencies": { 200 | "type": "array", 201 | "description": "Array of package names that will be bundled when publishing the package.", 202 | "items": { 203 | "type": "string" 204 | } 205 | }, 206 | "bundledDependencies": { 207 | "type": "array", 208 | "description": "Array of package names that will be bundled when publishing the package.", 209 | "items": { 210 | "type": "string" 211 | } 212 | }, 213 | "optionalDependencies": { 214 | "$ref": "#/definitions/dependency" 215 | }, 216 | "peerDependencies": { 217 | "$ref": "#/definitions/dependency" 218 | }, 219 | "engines": { 220 | "type": "object", 221 | "additionalProperties": { 222 | "type": "string" 223 | } 224 | }, 225 | "engineStrict": { 226 | "type": "boolean" 227 | }, 228 | "os": { 229 | "type": "array", 230 | "items": { 231 | "type": "string" 232 | } 233 | }, 234 | "cpu": { 235 | "type": "array", 236 | "items": { 237 | "type": "string" 238 | } 239 | }, 240 | "preferGlobal": { 241 | "type": "boolean", 242 | "description": "If your package is primarily a command-line application that should be installed globally, then set this value to true to provide a warning if it is installed locally." 243 | }, 244 | "private": { 245 | "type": "boolean", 246 | "description": "If set to true, then npm will refuse to publish it." 247 | }, 248 | "publishConfig": { 249 | "type": "object", 250 | "additionalProperties": true 251 | }, 252 | "dist": { 253 | "type": "object", 254 | "properties": { 255 | "shasum": { 256 | "type": "string" 257 | }, 258 | "tarball": { 259 | "type": "string" 260 | } 261 | } 262 | }, 263 | "readme": { 264 | "type": "string" 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Public API to Jam features 3 | */ 4 | 5 | var ls = require('./lib/commands/ls'), 6 | install = require('./lib/commands/install'), 7 | upgrade = require('./lib/commands/upgrade'), 8 | rebuild = require('./lib/commands/rebuild'), 9 | remove = require('./lib/commands/remove'), 10 | publish = require('./lib/commands/publish'), 11 | repository= require('./lib/repository'), 12 | logger = require('./lib/logger'), 13 | jamrc = require('./lib/jamrc'), 14 | async = require('async'), 15 | path = require('path'); 16 | 17 | 18 | // silence logger module by default 19 | logger.level = 'error'; 20 | logger.clean_exit = true; 21 | 22 | 23 | /** 24 | * Set log-level, by default only errors not passed back to api callbacks are 25 | * logged. If you'd like more console output, call this function beforehand. 26 | * 27 | * Levels: 28 | * 29 | * error 30 | * warning 31 | * info 32 | * debug 33 | * verbose 34 | * 35 | * @param {String} level 36 | */ 37 | 38 | exports.logLevel = function (level) { 39 | logger.level = level; 40 | }; 41 | 42 | 43 | /** 44 | * Compiles packages into a single file for production use. 45 | * 46 | * Options: 47 | * cwd String working directory (defaults to process.cwd()) 48 | * settings Object values from jamrc (optional) 49 | * includes [String] array of require paths to include in build 50 | * shallowExcludes [String] exclude these modules (not their dependencies) 51 | * deepExcludes [String] exclude these modules AND their dependencies 52 | * output String filename to save output to 53 | * pkgdir String location of jam packages 54 | * baseurl String base url value to pass to requirejs optimizer 55 | * wrap Bool wraps output in anonymous function 56 | * almond Bool uses almond shim 57 | * verbose Bool more verbose output from r.js 58 | * nominify Bool don't minify with uglifyjs 59 | * nolicense Bool strip license comments 60 | * 61 | * @param {Object} options 62 | * @param {Function} callback(err, build_response) 63 | */ 64 | 65 | exports.compile = require('./lib/commands/compile').compile; 66 | 67 | 68 | /** 69 | * Install a package using the appropriate settings for the project directory. 70 | * Reads values from .jamrc and package.json to install to the correct 71 | * directory. 72 | * 73 | * @param {String} pdir - the project directory (where package.json is) 74 | * @param {String|Array} names - the package(s) to install 75 | * @param {Function} callback(err) 76 | */ 77 | 78 | exports.install = function (pdir, names, callback) { 79 | if (!Array.isArray(names)) { 80 | names = [names]; 81 | } 82 | jamrc.load(function (err, settings) { 83 | var opt = {repositories: settings.repositories}; 84 | 85 | install.initDir(settings, pdir, opt, function (err, opt, cfg) { 86 | opt = install.extendOptions(pdir, settings, cfg, opt); 87 | install.installPackages(cfg, names, opt, callback); 88 | }); 89 | }); 90 | }; 91 | 92 | 93 | /** 94 | * Upgrades all or specified packages for the provided project. Reads values 95 | * from .jamrc and package.json to find the package directory and repositories. 96 | * 97 | * @param {String} pdir - the project directory (where package.json is) 98 | * @param {String|Array} names - specific package(s) to upgrade (optional) 99 | * @param {Function} callback(err) 100 | */ 101 | 102 | exports.upgrade = function (pdir, /*optional*/names, callback) { 103 | if (!callback) { 104 | callback = names; 105 | names = null; 106 | } 107 | if (names && !Array.isArray(names)) { 108 | names = [names]; 109 | } 110 | jamrc.load(function (err, settings) { 111 | var opt = {repositories: settings.repositories}; 112 | 113 | install.initDir(settings, pdir, opt, function (err, opt, cfg) { 114 | opt = install.extendOptions(pdir, settings, cfg, opt); 115 | upgrade.upgrade(settings, names, opt, cfg, callback); 116 | }); 117 | }); 118 | }; 119 | 120 | 121 | /** 122 | * Removes specified packages from the project's package directory. Reads values 123 | * from .jamrc and package.json to find the package directory. 124 | * 125 | * @param {String} pdir - the project directory (where package.json is) 126 | * @param {String|Array} names - the package(s) to remove 127 | * @param {Function} callback(err) 128 | */ 129 | 130 | exports.remove = function (pdir, names, callback) { 131 | if (!Array.isArray(names)) { 132 | names = [names]; 133 | } 134 | jamrc.load(function (err, settings) { 135 | var opt = {}; 136 | install.initDir(settings, pdir, opt, function (err, opt, cfg) { 137 | opt = install.extendOptions(pdir, settings, cfg, opt); 138 | remove.remove(settings, cfg, opt, names, callback); 139 | }); 140 | }); 141 | }; 142 | 143 | 144 | /** 145 | * Lists installed packages for the given project. The callback gets the 146 | * output that would normally be printed to the terminal and an array of 147 | * package objects (representing the values from each package's package.json 148 | * file). 149 | * 150 | * @param {String} pdir - the project directory (where package.json is) 151 | * @param {Function} callback(err, output, packages) 152 | */ 153 | 154 | exports.ls = function (pdir, callback) { 155 | jamrc.load(function (err, settings) { 156 | var opt = {}; 157 | install.initDir(settings, pdir, opt, function (err, opt, cfg) { 158 | opt = install.extendOptions(pdir, settings, cfg, opt); 159 | ls.ls(settings, cfg, opt.target_dir, callback); 160 | }); 161 | }); 162 | }; 163 | 164 | 165 | /** 166 | * Searches repositories for a package. 167 | * 168 | * @param {String} pdir - the project directory (where package.json is) 169 | * @param {String|Array} q - the search terms 170 | * @param {Number} limit - limit the number of results per-repository (optional) 171 | * @param {Function} callback(err, results) 172 | */ 173 | 174 | exports.search = function (pdir, q, /*optional*/limit, callback) { 175 | if (!callback) { 176 | callback = limit; 177 | limit = 10; 178 | } 179 | jamrc.load(function (err, settings) { 180 | var opt = {repositories: settings.repositories}; 181 | 182 | install.initDir(settings, pdir, opt, function (err, opt, cfg) { 183 | opt = install.extendOptions(pdir, settings, cfg, opt); 184 | 185 | async.concat(opt.repositories, function (repo, cb) { 186 | repository.search(repo, q, limit, function (err, data) { 187 | if (err) { 188 | return cb(err); 189 | } 190 | cb(null, data.rows.map(function (r) { 191 | return r.doc; 192 | })); 193 | }); 194 | }, 195 | callback); 196 | }); 197 | }); 198 | }; 199 | 200 | 201 | /** 202 | * Rebuild require.config.js and require.js according to the packages 203 | * available inside the package directory. 204 | * 205 | * @param {String} pdir - the project directory (where package.json is) 206 | * @param {Function} callback(err) 207 | */ 208 | 209 | exports.rebuild = function (pdir, callback) { 210 | jamrc.load(function (err, settings) { 211 | var opt = {}; 212 | install.initDir(settings, pdir, opt, function (err, opt, cfg) { 213 | opt = install.extendOptions(pdir, settings, cfg, opt); 214 | rebuild.rebuild(settings, cfg, opt, callback); 215 | }); 216 | }); 217 | }; 218 | 219 | /** 220 | * Publish package 221 | * available inside the package directory. 222 | * 223 | * @param {object} config - the project directory (where package.json is) 224 | * @param {string} [config.dir] 225 | * @param {string} [config.repo] 226 | * @param {string} [config.level] 227 | * @param {object} [config.options] 228 | * @param {Function} callback(err) 229 | */ 230 | 231 | exports.publish = function(config, callback) { 232 | var repo, flow, params; 233 | 234 | if (_.isFunction(config)) { 235 | callback = config; 236 | config = {}; 237 | } 238 | 239 | if (_.isString(config.level)) { 240 | logger.level = config.level; 241 | } 242 | 243 | flow = {}; 244 | params = {}; 245 | 246 | params.dir = config.dir || "."; 247 | 248 | repo = config.repo; 249 | if (!repo && process.env.JAM_TEST && !(repo = process.env.JAM_TEST_DB)) { 250 | return callback(new Error('JAM_TEST environment variable set, but no JAM_TEST_DB set')); 251 | } 252 | 253 | if (!repo) { 254 | flow.repo = function(next) { 255 | jamrc.load(function(err, settings) { 256 | if (err) { 257 | return next(err); 258 | } 259 | 260 | next(null, settings.repositories[0]); 261 | }); 262 | }; 263 | } else { 264 | params.repo = repo; 265 | } 266 | 267 | async.parallel(flow, function(err, results) { 268 | if (err) { 269 | return callback(err); 270 | } 271 | 272 | _.extend(params, results); 273 | 274 | publish.publish('package', params.repo, params.dir, config.options || {}, callback); 275 | }); 276 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jam 2 | 3 | For **front-end** developers who crave maintainable assets, 4 | **Jam** is a **package manager** for JavaScript. 5 | Unlike other repositories, we put the **browser** first. 6 | 7 | 8 | * **Manage dependencies** - Using a stack of script tags isn't the most maintainable way of managing dependencies, with Jam packages and loaders like RequireJS you get automatic dependency resolution. 9 | 10 | * **Fast and modular** - Achieve faster load times with asynchronous loading and the ability to optimize downloads. JavaScript modules and packages provide properly namespaced and more modular code. 11 | 12 | * **Use with existing stack** - Jam manages only your front-end assets, the rest of your app can be written in your favourite language or framework. Node.js tools can use the repository directly with the Jam API. 13 | 14 | * **Custom builds** - No more configuring custom builds of popular libraries. Now, every build can be optimized automatically depending on the parts you use, and additional components can always be loaded later. 15 | 16 | * **Focus on size** - Installing multiple versions works great on the server, but client-side we don't want five versions of jQuery! Jam can use powerful dependency resolution to find a working set of packages using only a single version of each. 17 | 18 | * **100% browser** - Every package you see here will work in the browser and play nicely with module loaders like RequireJS. We're not hijacking an existing repository, we're creating a 100% browser-focused community! 19 | 20 | 21 | [Visit the Jam website](http://jamjs.org) 22 | 23 | 24 | ## Example usage 25 | 26 | $ jam install jquery --save 27 | 28 | 29 | ```html 30 | 31 | 32 | 37 | ``` 38 | 39 | [Learn more...](http://jamjs.org) 40 | 41 | 42 | ## Browser packages in package.json 43 | 44 | You can also define your browser dependencies in a project-level package.json 45 | file. If you use Node.js, this format will already familiar to you, and the 46 | Jam dependencies can live alongside your NPM dependencies. It's also possible 47 | to define custom install paths and baseUrls, as well as hand in any requirejs 48 | configuration here: 49 | 50 | ```javascript 51 | { 52 | "name": "my-project", 53 | "version": "0.0.1", 54 | "description": "My example project", 55 | "jam": { 56 | "baseUrl": "public", 57 | "packageDir": "public/vendor", 58 | "dependencies": { 59 | "jquery": "1.7.x", 60 | "underscore": null 61 | }, 62 | "config": { 63 | "paths": { 64 | "templates": "public/templates" 65 | } 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | ### Git URLs as Dependencies 72 | 73 | Git urls can be of the form: 74 | 75 | + git://github.com/user/project.git#commit-ish 76 | + git+ssh://user@hostname/project.git#commit-ish 77 | + git+http://user@hostname/project/blah.git#commit-ish 78 | + git+https://user@hostname/project/blah.git#commit-ish 79 | 80 | The commit-ish can be any tag, sha, or branch which can be supplied as an argument to git checkout. The default is master. 81 | 82 | ```javascript 83 | { 84 | "name": "my-project", 85 | "version": "0.0.1", 86 | "description": "My example project", 87 | "jam": { 88 | "baseUrl": "public", 89 | "packageDir": "public/vendor", 90 | "dependencies": { 91 | "feature": "git://github.com/user/project.git#0.2.3" 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | 98 | ## Installation 99 | 100 | # npm install -g jamjs 101 | 102 | Requires [node.js](http://nodejs.org) 103 | 104 | 105 | ## Settings 106 | 107 | You can customize Jam by creating a `.jamrc` file in your home directory. 108 | 109 | ### .jamrc 110 | 111 | #### repositories 112 | 113 | An array with Jam repositiories. Jam uses `http://jamjs.org/repository` by 114 | default, but it's possible to create a local, e.g. corporate, repository. 115 | 116 | ```javascript 117 | exports.repositories = [ 118 | "http://mycorporation.com:5984/repository/", 119 | "http://jamjs.org/repository" 120 | ]; 121 | ``` 122 | 123 | Repositories are in preference-order, so packages from repositories earlier 124 | in the list will be preferred over packages in repositories later in the 125 | list. However, when no package version is specified, the highest version 126 | number will be installed (even if that's not from the earliest repository). 127 | 128 | You can add custom search URLs to repositories too: 129 | 130 | ```javascript 131 | exports.repositories = [ 132 | { 133 | url: "http://mycorporation.com:5984/repository/", 134 | search: "http://db.com:5984/_fti/key/_design/search/something" 135 | }, 136 | "http://jamjs.org/repository" 137 | ]; 138 | ``` 139 | 140 | If your local repository doesn't implement full text search (e.g. you don't want 141 | to install couchdb lucene), you can disable searching functionality for that repository, otherwise 142 | `jam search` would report an error: 143 | 144 | ```javascript 145 | exports.repositories = [ 146 | { 147 | url: "http://mycorporation.com:5984/repository/", 148 | search: false 149 | }, 150 | "http://jamjs.org/repository" 151 | ]; 152 | ``` 153 | 154 | See the section below on running your own repository. 155 | 156 | 157 | #### package_dir 158 | 159 | Sets the default package installation directory (normally uses `./jam`). This 160 | is best customized in your project-level package.json file, to ensure other 161 | developers also install to the correct location. 162 | 163 | ```javascript 164 | exports.package_dir = 'libs'; 165 | ``` 166 | 167 | #### strict 168 | 169 | Puts jam into strict mode. In this mode, during installation, subpackages versions checked to be strict. 170 | If not - the must be hoisted to the root package with strict version declaration. 171 | 172 | #### production 173 | 174 | Puts jam into production mode. In this mode, during installation, dependencies sources are restricted to be repository. 175 | 176 | ## Running the tests 177 | 178 | Jam includes two test suites, unit tests (in `test/unit`) and integration 179 | tests (in `test/integration`). The unit tests are easy to run by running the 180 | `test/unit.sh` script, or `test\unit.bat` on Windows. The integration tests 181 | first require you to set up a CouchDB instance to test against (you can get 182 | a free account at [IrisCouch](http://www.iriscouch.com/) if you don't want to install 183 | CouchDB). You then need to set the JAM\_TEST\_DB environment variable to 184 | point to a CouchDB database URL for testing: 185 | 186 | #### Linux 187 | ``` 188 | export JAM_TEST_DB=http://user:password@localhost:5984/jamtest 189 | ``` 190 | 191 | #### Windows 192 | ``` 193 | set JAM_TEST_DB=http://user:password@localhost:5984/jamtest 194 | ``` 195 | 196 | **Warning:** All data in the test database will be deleted! 197 | 198 | You can then run the integration tests using `test/integration.sh` or 199 | `test\integration.bat`. To run BOTH the unit and integration tests use 200 | `test/all.sh` or `test\all.bat`. 201 | 202 | 203 | ## Running your own private repository or mirror 204 | 1. Install couchdb 205 | 206 | #### Mac OS X: 207 | 208 | 1. Install [Homebrew](http://mxcl.github.com/homebrew/). 209 | 2. 210 | 211 | ``` 212 | brew install couchdb 213 | ``` 214 | 215 | #### Ubuntu: 216 | 217 | ``` 218 | apt-get install couchdb 219 | ``` 220 | 2. Configure your database 221 | 222 | ``` 223 | curl -X POST http://127.0.0.1:5984/_replicate -d '{ 224 | "source":"http://jamjs.org/repository", 225 | "target":"http://localhost:5984/repository", 226 | "continuous":true, 227 | "doc_ids":["_design/jam-packages"] 228 | }' -H "Content-Type: application/json" 229 | ``` 230 | 231 | #### To create a mirror: 232 | 233 | ``` 234 | curl -X POST http://127.0.0.1:5984/_replicate -d '{ 235 | "source":"http://jamjs.org/repository", 236 | "target":"repository", 237 | "continuous":true, 238 | "create_target":true 239 | }' -H "Content-Type: application/json" 240 | ``` 241 | 242 | #### To create an empty, private repository: 243 | 244 | ``` 245 | curl -X PUT http://127.0.0.1:5984/repository 246 | ``` 247 | 248 | 3. Edit your ```.jamrc``` file to use your new repository: 249 | 250 | ``` 251 | exports.repositories = [ 252 | { 253 | url: "http://localhost:5984/repository", 254 | search: false 255 | }, 256 | "http://jamjs.org/repository" 257 | ]; 258 | ``` 259 | 260 | ### Adding search 261 | 262 | 1. [Install couchdb-lucene](https://github.com/rnewson/couchdb-lucene#build-and-run-couchdb-lucene) 263 | 2. Restart couchdb. 264 | 3. Edit your ```.jamrc``` file to allow searching on your repository: 265 | 266 | ``` 267 | exports.repositories = [ 268 | { 269 | url: "http://localhost:5984/repository", 270 | search: "http://localhost:5984/_fti/local/repository/_design/jam-packages/packages/" 271 | }, 272 | "http://jamjs.org/repository" 273 | ]; 274 | ``` 275 | 276 | ### Publishing packages to your private repository 277 | 278 | ``` 279 | jam publish --repository http://localhost:5984/repository 280 | ``` 281 | 282 | ## More documentation 283 | 284 | To learn how to create and publish packages etc, and for more info on using 285 | packages, consult the [Jam documentation website](http://jamjs.org/docs). 286 | 287 | 288 | ## Links 289 | 290 | * [Homepage](http://jamjs.org) 291 | * [Packages](http://jamjs.org/packages/) 292 | * [Docs](http://jamjs.org/doc) 293 | -------------------------------------------------------------------------------- /lib/commands/compile.js: -------------------------------------------------------------------------------- 1 | // TODO 2 | // https://github.com/jrburke/almond 3 | // 4 | // 5 | // jam compile -i backbone -i d3 -o built.js 6 | // 7 | // 8 | // - set up package paths for requirejs optimizer 9 | // - run equivalent of: 10 | // node r.js -o baseUrl=. name=path/to/almond.js include=main 11 | // out=main-built.js wrap=true 12 | // 13 | // (wrap is optional) 14 | // 15 | // 16 | // OR should this just be an extension of the optimize command? 17 | 18 | var logger = require('../logger'), 19 | settings = require('../settings'), 20 | jamrc = require('../jamrc'), 21 | install = require('./install'), 22 | args = require('../args'), 23 | tar = require('../tar'), 24 | path = require('path'), 25 | async = require('async'), 26 | fs = require('fs'), 27 | requirejs = require('requirejs'), 28 | project = require('../project'); 29 | 30 | 31 | exports.summary = 'Combines modules and requirejs into a single file'; 32 | 33 | exports.usage = '\n' + 34 | 'jam compile TARGET\n' + 35 | 'jam compile -i MODULE ... -o TARGET\n' + 36 | '\n' + 37 | 'When called without -i parameters, all installed packages are included\n' + 38 | 'in the compiled output. With -i parameters, only the specified\n' + 39 | 'packages are compiled, this can also include modules from your own\n' + 40 | 'project directory.\n' + 41 | '\n' + 42 | 'Parameters:\n' + 43 | ' TARGET The filename to save compiled output to\n' + 44 | '\n' + 45 | 'Options:\n' + 46 | ' -i, --include Specific modules to optimize, combining them and\n' + 47 | ' their dependencies into a single file.\n' + 48 | ' -e, --exclude Shallow excludes a module from the build (it\'s\n' + 49 | ' dependencies will still be included).\n' + 50 | ' -E, --deep-exclude Deep excludes a module and it\'s dependencies\n' + 51 | ' from the build.\n' + 52 | ' -o, --out Output file for the compiled code\n' + 53 | ' -d, --package-dir Jam directory to use (defaults to "./jam")\n' + 54 | ' -w, --wrap Wraps the output in an anonymous function to avoid\n' + 55 | ' require and define functions being added to the\n' + 56 | ' global scope, this often makes sense when using the\n' + 57 | ' almond option.\n' + 58 | ' -a, --almond Use the lightweight almond shim instead of RequireJS,\n' + 59 | ' smaller filesize but can only load bundled resources\n' + 60 | ' and cannot request additional modules.\n' + 61 | ' -v, --verbose Increase log level to report all compiled modules\n' + 62 | ' --no-minify Do not minify concatenated file with UglifyJS.\n' + 63 | ' --no-license Do not include license comments.'; 64 | 65 | 66 | exports.run = function (settings, _args) { 67 | var logger = require('../logger'); 68 | 69 | var a = args.parse(_args, { 70 | includes: {match: ['-i', '--include'], multiple: true, value: true}, 71 | shallowExcludes: { 72 | match: ['-e', '--exclude'], 73 | multiple: true, 74 | value: true 75 | }, 76 | deepExcludes: { 77 | match: ['-E', '--deep-exclude'], 78 | multiple: true, 79 | value: true 80 | }, 81 | output: {match: ['-o', '--out'], value: true}, 82 | pkgdir: {match: ['-d','--package-dir'], value: true}, 83 | baseurl: {match: ['--baseurl'], value: true}, 84 | wrap: {match: ['-w','--wrap']}, 85 | almond: {match: ['-a','--almond']}, 86 | verbose: {match: ['-v','--verbose']}, 87 | nominify: {match: ['--no-minify']}, 88 | nolicense: {match: ['--no-license']} 89 | }); 90 | 91 | var opt = a.options; 92 | opt.output = opt.output || a.positional[0]; 93 | 94 | if (!opt.output) { 95 | logger.error('You must specify an output file'); 96 | console.log(exports.usage); 97 | logger.clean_exit = true; 98 | return; 99 | } 100 | 101 | var start_time = new Date().getTime(); 102 | opt.cwd = process.cwd(); 103 | opt.settings = settings; 104 | 105 | exports.compile(opt, function (err) { 106 | if (err) { 107 | return logger.error(err); 108 | } 109 | var duration = new Date().getTime() - start_time; 110 | logger.end(opt.output + ' (' + duration + 'ms)'); 111 | }); 112 | }; 113 | 114 | exports.filterPackages = function (cfgpackages, pkgdir, names, callback) { 115 | async.filter(names, function (n, cb) { 116 | var pkg = _.detect(cfgpackages, function (pkg) { 117 | return pkg.name === n; 118 | }); 119 | if (pkg && pkg.main) { 120 | // main property defined 121 | return cb(true); 122 | } 123 | else { 124 | // check main.js exists, otherwise optimizer fails 125 | fs.exists(path.join(pkgdir, n, 'main.js'), cb); 126 | } 127 | }, callback); 128 | }; 129 | 130 | 131 | // DONT forget to update docs in index.js file when changing args! 132 | exports.compile = function (opt, callback) { 133 | if (!opt.settings) { 134 | opt.settings = jamrc.DEFAULTS; 135 | } 136 | if (!opt.cwd) { 137 | opt.cwd = process.cwd(); 138 | } 139 | install.initDir(opt.settings, opt.cwd, opt, function (err, opt, cfg, pdir) { 140 | if (err) { 141 | return logger.error(err); 142 | } 143 | 144 | if (!opt.pkgdir) { 145 | if (cfg.jam && cfg.jam.packageDir) { 146 | opt.pkgdir = path.resolve(pdir, cfg.jam.packageDir); 147 | } 148 | else { 149 | opt.pkgdir = path.resolve(pdir, opt.settings.package_dir || ''); 150 | } 151 | } 152 | if (!opt.baseurl) { 153 | if (cfg.jam && cfg.jam.baseUrl) { 154 | opt.baseurl = path.resolve(pdir, cfg.jam.baseUrl); 155 | } 156 | else { 157 | opt.baseurl = path.resolve(pdir, opt.settings.baseUrl || ''); 158 | } 159 | } 160 | 161 | var configfile = path.resolve(opt.pkgdir, 'require.config'); 162 | 163 | var packages = require(configfile).packages; 164 | var pkgnames = packages.map(function (p) { 165 | return p.name; 166 | }); 167 | exports.filterPackages( 168 | packages, opt.pkgdir, pkgnames, function (valid_names) { 169 | if (!opt.includes.length) { 170 | // compile all installed modules by default 171 | opt.includes = valid_names; 172 | } 173 | if (!opt.output) { 174 | return callback('You must specify an output file'); 175 | } 176 | 177 | logger.info('compiling', opt.output); 178 | if (opt.includes.length) { 179 | logger.info('include', opt.includes.join(', ')); 180 | } 181 | 182 | var impl; 183 | if (opt.almond) { 184 | logger.info('using almond.js'); 185 | impl = path.relative( 186 | path.resolve(opt.baseurl), 187 | require.resolve('almond') 188 | ); 189 | } 190 | else { 191 | var require_path = require.resolve('requirejs'); 192 | 193 | impl = path.relative( 194 | path.resolve(opt.baseurl), 195 | path.join(require_path, '../../require.js') 196 | ); 197 | } 198 | 199 | var includes; 200 | if (opt.almond) { 201 | includes = opt.includes; 202 | } 203 | else { 204 | includes = [ 205 | path.relative(opt.baseurl, configfile) 206 | ].concat(opt.includes); 207 | } 208 | 209 | var config = { 210 | baseUrl: opt.baseurl, 211 | packages: packages, 212 | name: 'requireLib', 213 | wrap: opt.wrap, 214 | optimize: 'uglify', 215 | include: includes, 216 | out: opt.output, 217 | paths: {requireLib: impl} 218 | }; 219 | if (opt.verbose) { 220 | config.logLevel = 0; 221 | } 222 | if (opt.nominify) { 223 | config.optimize = 'none'; 224 | } 225 | if (opt.nolicense) { 226 | config.preserveLicenseComments = false; 227 | } 228 | if (opt.shallowExcludes && opt.shallowExcludes.length) { 229 | config.excludeShallow = opt.shallowExcludes; 230 | } 231 | if (opt.deepExcludes && opt.deepExcludes.length) { 232 | config.exclude = opt.deepExcludes; 233 | } 234 | try { 235 | requirejs.optimize(config, function (build_response) { 236 | if (/^Error: Error:/.test(build_response)) { 237 | // TODO: try to get better error handling added 238 | // upstream 239 | return callback(build_response); 240 | } 241 | callback(null, build_response); 242 | }); 243 | } 244 | catch (e) { 245 | return callback(e); 246 | } 247 | } 248 | ); 249 | }); 250 | }; 251 | -------------------------------------------------------------------------------- /lib/commands/upgrade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var path = require('path'), 6 | async = require('async'), 7 | semver = require('semver'), 8 | install = require('./install'), 9 | project = require('../project'), 10 | tree = require('../tree'); 11 | utils = require('../utils'), 12 | logger = require('../logger'), 13 | argParse = require('../args').parse, 14 | _ = require('underscore'); 15 | 16 | 17 | /** 18 | * Usage information and docs 19 | */ 20 | 21 | exports.summary = 'Upgrades packages to the latest compatible version'; 22 | 23 | 24 | exports.usage = '' + 25 | 'jam upgrade [PACKAGES ...]\n' + 26 | '\n' + 27 | 'Parameters:\n' + 28 | ' PACKAGES Names of specific packages to upgrade\n' + 29 | '\n' + 30 | 'Options:\n' + 31 | ' -r, --repository Source repository URL (otherwise uses values in jamrc)\n' + 32 | ' -d, --package-dir Package directory (defaults to "./jam")'; 33 | 34 | 35 | /** 36 | * Run function called when "jam upgrade" command is used 37 | * 38 | * @param {Object} settings - the values from .jamrc files 39 | * @param {Array} args - command-line arguments 40 | */ 41 | 42 | exports.run = function (settings, args) { 43 | var a = argParse(args, { 44 | 'repository': {match: ['-r', '--repository'], value: true}, 45 | 'target_dir': {match: ['-d', '--package-dir'], value: true}, 46 | 'baseurl': {match: ['-b', '--baseurl'], value: true} 47 | }); 48 | 49 | var opt = a.options; 50 | var deps = a.positional; 51 | 52 | opt.repositories = settings.repositories; 53 | if (a.options.repository) { 54 | opt.repositories = [a.options.repository]; 55 | // don't allow package dir .jamrc file to overwrite repositories 56 | opt.fixed_repositories = true; 57 | } 58 | if (process.env.JAM_TEST) { 59 | if (!process.env.JAM_TEST_DB) { 60 | throw 'JAM_TEST environment variable set, but no JAM_TEST_DB set'; 61 | } 62 | opt.repositories = [process.env.JAM_TEST_DB]; 63 | opt.fixed_repositories = true; 64 | } 65 | 66 | var cwd = process.cwd(); 67 | install.initDir(settings, cwd, opt, function (err, opt, cfg, proj_dir) { 68 | if (err) { 69 | return logger.error(err); 70 | } 71 | opt = install.extendOptions(proj_dir, settings, cfg, opt); 72 | exports.upgrade(settings, deps, opt, cfg, function (err) { 73 | if (err) { 74 | return logger.error(err); 75 | } 76 | logger.end(); 77 | }); 78 | }); 79 | }; 80 | 81 | 82 | /** 83 | * Upgrade the current project directory's dependencies. 84 | * 85 | * @param {Array} deps - an optional sub-set of package names to upgrade 86 | * @param {Object} opt - the options object 87 | * @param {Function} callback 88 | */ 89 | 90 | exports.upgrade = function (settings, deps, opt, cfg, callback) { 91 | exports.getOutdated(deps, cfg, opt, function (e, changed, local, updated) { 92 | if (e) { 93 | return callback(e); 94 | } 95 | exports.installChanges(changed, opt, function (err) { 96 | if (err) { 97 | return callback(err); 98 | } 99 | project.updateRequireConfig( 100 | opt.target_dir, 101 | opt.baseurl, 102 | function (err) { 103 | if (err) { 104 | return callback(err); 105 | } 106 | //install.checkUnused(updated, opt, callback); 107 | callback(); 108 | } 109 | ); 110 | }); 111 | }); 112 | }; 113 | 114 | 115 | /** 116 | * Builds a remote and a local copy of the version tree. This is used to compare 117 | * the installed packages against those that are available in the repositories. 118 | * 119 | * @param {Array|null} deps - an optional subset of packages to upgrade 120 | * @param {Object} cfg - values from package.json for the root package 121 | * @param {Object} opt - the options object 122 | * @param {Function} callback 123 | */ 124 | 125 | exports.buildTrees = function (deps, cfg, opt, callback) { 126 | var local_sources = [ 127 | install.dirSource(opt.target_dir), 128 | install.repoSource(opt.repositories, cfg) 129 | ]; 130 | var newcfg = utils.convertToRootCfg(cfg); 131 | 132 | utils.listDirs(opt.target_dir, function (err, dirs) { 133 | if (err) { 134 | return callback(err); 135 | } 136 | // add packages not referenced by package.json, but still inside 137 | // package dir, to make sure they get upgraded too 138 | dirs.forEach(function (d) { 139 | var name = path.basename(d); 140 | if (!newcfg.dependencies.hasOwnProperty(name)) { 141 | newcfg.dependencies[name] = null; 142 | } 143 | }); 144 | 145 | var pkg = { 146 | config: newcfg, 147 | source: 'root' 148 | }; 149 | logger.info('Building local version tree...'); 150 | tree.build(pkg, local_sources, function (err, local) { 151 | if (err) { 152 | return callback(err); 153 | } 154 | var update_sources = [ 155 | // check remote source first to make sure we get highest version 156 | install.repoSource(opt.repositories, cfg), 157 | install.dirSource(opt.target_dir) 158 | ]; 159 | var dependency_sources = [ 160 | // check local source first to keep local version if possible 161 | install.dirSource(opt.target_dir), 162 | install.repoSource(opt.repositories, cfg) 163 | ]; 164 | if (!deps || !deps.length) { 165 | // update all packages if none specified 166 | deps = Object.keys(local); 167 | } 168 | 169 | var packages = {}; 170 | // add root package 171 | packages[pkg.config.name] = tree.createPackage([]); 172 | deps.forEach(function (name) { 173 | // prep specified dependencies with the update_sources 174 | packages[name] = tree.createPackage(update_sources); 175 | }); 176 | 177 | logger.info('Building remote version tree...'); 178 | tree.extend( 179 | pkg, dependency_sources, packages, function (err, updated) { 180 | callback(err, local, updated); 181 | } 182 | ); 183 | }); 184 | }); 185 | }; 186 | 187 | 188 | /** 189 | * Gets the remote and local version trees, compares the version numbers for 190 | * each package, and returns a list of packages which have changed. 191 | * 192 | * Each objects in the returned list of changed packages have the following 193 | * properties: 194 | * 195 | * - name - the name of the package 196 | * - version - the new version to be installed 197 | * - old - the old version to be installed (null if it doesn't currently exist) 198 | * 199 | * @param {Object} cfg - the values from package.json for the root package 200 | * @param {Object} opt - the options object 201 | * @param {Function} callback 202 | */ 203 | 204 | exports.getOutdated = function (deps, cfg, opt, callback) { 205 | var _ = require('underscore'); 206 | exports.buildTrees(deps, cfg, opt, function (err, local, updated) { 207 | if (err) { 208 | return callback(err); 209 | } 210 | var all_names = _.uniq(_.keys(local).concat(_.keys(updated))); 211 | 212 | var changed = all_names.map(function (name) { 213 | var lversion = local[name] ? local[name].current_version: null; 214 | var uversion = updated[name] ? updated[name].current_version: null; 215 | 216 | if (lversion) { 217 | var lpkg = _.findWhere(local[name].versions, { version: lversion }); 218 | if (lpkg.source === 'repository') { 219 | // cannot even satisfy requirements with current package, 220 | // this needs re-installing from repositories 221 | return { 222 | name: name, 223 | version: uversion, 224 | old: 'not satisfiable' 225 | }; 226 | } 227 | } 228 | if (!local[name] && updated[name] || lversion !== uversion) { 229 | return {name: name, version: uversion, old: lversion}; 230 | } 231 | }); 232 | callback(null, _.compact(changed), local, updated); 233 | }); 234 | }; 235 | 236 | 237 | /** 238 | * Accepts an array of changed packages and reports the change to the console 239 | * then installs from the repositories. 240 | * 241 | * @param {Array} packages - array of changed packages 242 | * @param {Object} opt - the options object 243 | * @param {Function} callback 244 | */ 245 | 246 | exports.installChanges = function (packages, opt, callback) { 247 | async.forEachLimit(packages, 5, function (dep, cb) { 248 | if (dep.name === '_root') { 249 | return cb(); 250 | } 251 | if (!dep.old) { 252 | logger.info('new package', dep.name + '@' + dep.version); 253 | } 254 | else if (semver.lt(dep.old, dep.version)) { 255 | logger.info( 256 | 'upgrade package', 257 | dep.name + '@' + dep.old + ' => ' + dep.name + '@' + dep.version 258 | ); 259 | } 260 | else if (semver.gt(dep.old, dep.version)) { 261 | logger.info( 262 | 'downgrade package', 263 | dep.name + '@' + dep.old + ' => ' + dep.name + '@' + dep.version 264 | ); 265 | } 266 | install.installRepo(dep.name, dep.version, opt, cb); 267 | }, callback); 268 | }; 269 | -------------------------------------------------------------------------------- /lib/fstream-jam.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Thanks to Isaac Schlueter's work on fstream-npm on which this file is 3 | * based. https://github.com/isaacs/fstream-npm 4 | */ 5 | 6 | 7 | var Ignore = require("fstream-ignore"), 8 | minimatch = require('minimatch'), 9 | inherits = require("inherits"), 10 | utils = require('./utils'), 11 | path = require("path"), 12 | fs = require("fs"); 13 | 14 | 15 | module.exports = Packer 16 | 17 | inherits(Packer, Ignore) 18 | 19 | function Packer (props) { 20 | if (!(this instanceof Packer)) { 21 | return new Packer(props) 22 | } 23 | 24 | if (typeof props === "string") { 25 | props = { path: props } 26 | } 27 | 28 | props.ignoreFiles = [ ".jamignore", 29 | ".gitignore", 30 | "package.json" ] 31 | 32 | Ignore.call(this, props) 33 | 34 | this.bundled = props.bundled 35 | this.bundleLinks = props.bundleLinks 36 | this.package = props.package 37 | if (props.packageInfo && props.packageInfo.browser) { 38 | this.browserInclude = props.packageInfo.browser.include; 39 | } 40 | if (props.packageInfo && props.packageInfo.jam) { 41 | this.browserInclude = props.packageInfo.jam.include; 42 | } 43 | 44 | // in a node_modules folder, resolve symbolic links to 45 | // bundled dependencies when creating the package. 46 | props.follow = this.follow = this.basename === "node_modules" 47 | // console.error("follow?", this.path, props.follow) 48 | 49 | if (this === this.root || 50 | this.parent && 51 | this.parent.basename === "node_modules" && 52 | this.basename.charAt(0) !== ".") { 53 | this.readBundledLinks() 54 | } 55 | 56 | 57 | this.on("entryStat", function (entry, props) { 58 | // files should *always* get into tarballs 59 | // in a user-writable state, even if they're 60 | // being installed from some wackey vm-mounted 61 | // read-only filesystem. 62 | entry.mode = props.mode = props.mode | 0200 63 | }) 64 | } 65 | 66 | Packer.prototype.readBundledLinks = function () { 67 | if (this._paused) { 68 | this.once("resume", this.addIgnoreFiles) 69 | return 70 | } 71 | 72 | this.pause() 73 | fs.readdir(this.path + "/node_modules", function (er, list) { 74 | // no harm if there's no bundle 75 | var l = list && list.length 76 | if (er || l === 0) return this.resume() 77 | 78 | var errState = null 79 | , then = function then (er) { 80 | if (errState) return 81 | if (er) return errState = er, this.resume() 82 | if (-- l === 0) return this.resume() 83 | }.bind(this) 84 | 85 | list.forEach(function (pkg) { 86 | if (pkg.charAt(0) === ".") return then() 87 | var pd = this.path + "/node_modules/" + pkg 88 | fs.realpath(pd, function (er, rp) { 89 | if (er) return then() 90 | this.bundleLinks = this.bundleLinks || {} 91 | this.bundleLinks[pkg] = rp 92 | then() 93 | }.bind(this)) 94 | }, this) 95 | }.bind(this)) 96 | } 97 | 98 | Packer.prototype.applyIgnores = function (entry, partial, entryObj) { 99 | // package.json files can never be ignored. 100 | if (entry === "package.json") return true 101 | 102 | // if the package.json file has a jam.include property, *only* include 103 | // package.json and the files whitelisted in that property 104 | if (this.browserInclude) { 105 | for (var i = 0; i < this.browserInclude.length; i++) { 106 | if (minimatch(entry, this.browserInclude[i])) { 107 | return true; 108 | } 109 | // test for subpaths, eg jam.include = ['foo'], entry = 'foo/bar.js' 110 | if (utils.isSubPath(this.browserInclude[i], entry)) { 111 | return true; 112 | } 113 | } 114 | return !!(partial); 115 | } 116 | 117 | // special rules. see below. 118 | if (entry === "node_modules") return true 119 | 120 | // some files are *never* allowed under any circumstances 121 | if (entry === ".git" || 122 | entry === ".lock-wscript" || 123 | entry.match(/^\.wafpickle-[0-9]+$/) || 124 | entry === "CVS" || 125 | entry === ".svn" || 126 | entry === ".hg" || 127 | entry.match(/^\..*\.swp$/) || 128 | entry.match(/^.*~$/) || 129 | entry === ".DS_Store" || 130 | entry.match(/^\._/) || 131 | entry === "npm-debug.log" 132 | ) { 133 | return false 134 | } 135 | 136 | // in a node_modules folder, we only include bundled dependencies 137 | // also, prevent packages in node_modules from being affected 138 | // by rules set in the containing package, so that 139 | // bundles don't get busted. 140 | // Also, once in a bundle, everything is installed as-is 141 | // To prevent infinite cycles in the case of cyclic deps that are 142 | // linked with npm link, even in a bundle, deps are only bundled 143 | // if they're not already present at a higher level. 144 | if (this.basename === "node_modules") { 145 | // bubbling up. stop here and allow anything the bundled pkg allows 146 | if (entry.indexOf("/") !== -1) return true 147 | 148 | // never include the .bin. It's typically full of platform-specific 149 | // stuff like symlinks and .cmd files anyway. 150 | if (entry === ".bin") return false 151 | 152 | var shouldBundle = false 153 | // the package root. 154 | var p = this.parent 155 | // the package before this one. 156 | var pp = p && p.parent 157 | 158 | // if this entry has already been bundled, and is a symlink, 159 | // and it is the *same* symlink as this one, then exclude it. 160 | if (pp && pp.bundleLinks && this.bundleLinks && 161 | pp.bundleLinks[entry] === this.bundleLinks[entry]) { 162 | return false 163 | } 164 | 165 | // since it's *not* a symbolic link, if we're *already* in a bundle, 166 | // then we should include everything. 167 | if (pp && pp.package) { 168 | return true 169 | } 170 | 171 | // only include it at this point if it's a bundleDependency 172 | var bd = this.package && this.package.bundleDependencies 173 | var shouldBundle = bd && bd.indexOf(entry) !== -1 174 | // if we're not going to bundle it, then it doesn't count as a bundleLink 175 | // if (this.bundleLinks && !shouldBundle) delete this.bundleLinks[entry] 176 | return shouldBundle 177 | } 178 | // if (this.bundled) return true 179 | 180 | return Ignore.prototype.applyIgnores.call(this, entry, partial, entryObj) 181 | } 182 | 183 | Packer.prototype.addIgnoreFiles = function () { 184 | var entries = this.entries 185 | // if there's a .jamignore, then we do *not* want to 186 | // read the .gitignore. 187 | if (-1 !== entries.indexOf(".jamignore")) { 188 | var i = entries.indexOf(".gitignore") 189 | if (i !== -1) { 190 | entries.splice(i, 1) 191 | } 192 | } 193 | 194 | this.entries = entries 195 | 196 | Ignore.prototype.addIgnoreFiles.call(this) 197 | } 198 | 199 | 200 | Packer.prototype.readRules = function (buf, e) { 201 | if (e !== "package.json") { 202 | return Ignore.prototype.readRules.call(this, buf, e) 203 | } 204 | 205 | buf = buf.toString().trim() 206 | 207 | if (buf.length === 0) return [] 208 | 209 | try { 210 | var p = this.package = JSON.parse(buf) 211 | } catch (er) { 212 | er.file = path.resolve(this.path, e) 213 | this.error(er) 214 | return 215 | } 216 | 217 | if (this === this.root) { 218 | this.bundleLinks = this.bundleLinks || {} 219 | this.bundleLinks[p.name] = this._path 220 | } 221 | 222 | this.packageRoot = true 223 | this.emit("package", p) 224 | 225 | // make bundle deps predictable 226 | if (p.bundledDependencies && !p.bundleDependencies) { 227 | p.bundleDependencies = p.bundledDependencies 228 | delete p.bundledDependencies 229 | } 230 | 231 | if (!p.files || !Array.isArray(p.files)) return [] 232 | 233 | // ignore everything except what's in the files array. 234 | return ["*"].concat(p.files.map(function (f) { 235 | return "!" + f 236 | })).concat(p.files.map(function (f) { 237 | return "!" + f.replace(/\/+$/, "") + "/**" 238 | })) 239 | } 240 | 241 | Packer.prototype.getChildProps = function (stat) { 242 | var props = Ignore.prototype.getChildProps.call(this, stat) 243 | 244 | props.package = this.package 245 | 246 | props.bundled = this.bundled && this.bundled.slice(0) 247 | props.bundleLinks = this.bundleLinks && 248 | Object.create(this.bundleLinks) 249 | 250 | // Directories have to be read as Packers 251 | // otherwise fstream.Reader will create a DirReader instead. 252 | if (stat.isDirectory()) { 253 | props.type = this.constructor 254 | } 255 | 256 | // only follow symbolic links directly in the node_modules folder. 257 | props.follow = false 258 | return props 259 | } 260 | 261 | 262 | var order = 263 | [ "package.json" 264 | , ".jamignore" 265 | , ".gitignore" 266 | , /^README(\.md)?$/ 267 | , "LICENCE" 268 | , "LICENSE" 269 | , /\.js$/ ] 270 | 271 | Packer.prototype.sort = function (a, b) { 272 | for (var i = 0, l = order.length; i < l; i ++) { 273 | var o = order[i] 274 | if (typeof o === "string") { 275 | if (a === o) return -1 276 | if (b === o) return 1 277 | } else { 278 | if (a.match(o)) return -1 279 | if (b.match(o)) return 1 280 | } 281 | } 282 | 283 | // deps go in the back 284 | if (a === "node_modules") return 1 285 | if (b === "node_modules") return -1 286 | 287 | return Ignore.prototype.sort.call(this, a, b) 288 | } 289 | 290 | 291 | 292 | Packer.prototype.emitEntry = function (entry) { 293 | if (this._paused) { 294 | this.once("resume", this.emitEntry.bind(this, entry)) 295 | return 296 | } 297 | 298 | // if there is a .gitignore, then we're going to 299 | // rename it to .jammignore in the output. 300 | if (entry.basename === ".gitignore") { 301 | entry.basename = ".jamignore" 302 | entry.path = path.resolve(entry.dirname, entry.basename) 303 | } 304 | 305 | // all *.gyp files are renamed to binding.gyp for node-gyp 306 | // but only when they are in the same folder as a package.json file. 307 | if (entry.basename.match(/\.gyp$/) && 308 | this.entries.indexOf("package.json") !== -1) { 309 | entry.basename = "binding.gyp" 310 | entry.path = path.resolve(entry.dirname, entry.basename) 311 | } 312 | 313 | // skip over symbolic links 314 | if (entry.type === "SymbolicLink") { 315 | entry.abort() 316 | return 317 | } 318 | 319 | if (entry.type !== "Directory") { 320 | // make it so that the folder in the tarball is named "package" 321 | var h = path.dirname((entry.root || entry).path) 322 | , t = entry.path.substr(h.length + 1).replace(/^[^\/\\]+/, "package") 323 | , p = h + "/" + t 324 | 325 | entry.path = p 326 | entry.dirname = path.dirname(p) 327 | return Ignore.prototype.emitEntry.call(this, entry) 328 | } 329 | 330 | // we don't want empty directories to show up in package 331 | // tarballs. 332 | // don't emit entry events for dirs, but still walk through 333 | // and read them. This means that we need to proxy up their 334 | // entry events so that those entries won't be missed, since 335 | // .pipe() doesn't do anythign special with "child" events, on 336 | // with "entry" events. 337 | var me = this 338 | entry.on("entry", function (e) { 339 | if (e.parent === entry) { 340 | e.parent = me 341 | me.emit("entry", e) 342 | } 343 | }) 344 | entry.on("package", this.emit.bind(this, "package")) 345 | } 346 | -------------------------------------------------------------------------------- /lib/project.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'), 2 | path = require('path'), 3 | semver = require('semver'), 4 | async = require('async'), 5 | mkdirp = require('mkdirp'), 6 | ncp = require('ncp').ncp, 7 | fs = require('fs'), 8 | honey = require('json-honey'), 9 | _ = require('underscore'); 10 | 11 | 12 | var pathExists = fs.exists || path.exists; 13 | 14 | 15 | exports.readPackageJSON = function (p, callback) { 16 | utils.readJSON(p, function (err, pkg) { 17 | if (err) { 18 | return callback(err, null, p); 19 | } 20 | try { 21 | exports.validate(pkg, p); 22 | } 23 | catch (e) { 24 | return callback(e); 25 | } 26 | return callback(null, pkg, p); 27 | }); 28 | }; 29 | 30 | exports.writePackageJSON = function (p, pkg, callback) { 31 | var str = honey(pkg); 32 | utils.writeJSON(p, str, callback); 33 | }; 34 | 35 | 36 | /** 37 | * Looks for a project's package.json file. Walks up the directory tree until 38 | * it finds a package.json file or hits the root. Does not throw when no 39 | * packages.json is found, just returns null. 40 | * 41 | * @param {String} p - The starting path to search upwards from 42 | * @param {boolean} [r] - Recursive search 43 | * @param {Function} callback - callback(err, path) 44 | */ 45 | 46 | exports.findPackageJSON = function (p, r, callback) { 47 | var filename; 48 | 49 | if (_.isFunction(r)) { 50 | callback = r; 51 | r = false; 52 | } 53 | 54 | filename = path.resolve(p, 'package.json'); 55 | 56 | pathExists(filename, function (exists) { 57 | var newpath; 58 | 59 | if (exists) { 60 | return callback(null, filename); 61 | } 62 | 63 | if (!r) { 64 | return callback(null, null); 65 | } 66 | 67 | newpath = path.dirname(p); 68 | 69 | if (newpath === p) { // root directory 70 | return callback(null, null); 71 | } 72 | 73 | exports.findPackageJSON(newpath, r, callback); 74 | }); 75 | }; 76 | 77 | /** 78 | * Searches for package.json and returns an object with it's contents, 79 | * returns a null if no file found. Final 80 | * argument of the callback is the matching file path for package.json, 81 | * or null if none were found. 82 | * 83 | * @param {String} cwd - directory to search upwards from for package.json 84 | * @param {boolean} [r] - Recursive search 85 | * @param {Function} callback(err, package_obj, path) 86 | */ 87 | 88 | exports.loadPackageJSON = async.memoize(function (cwd, r, callback) { 89 | if (_.isFunction(r)) { 90 | callback = r; 91 | r = false; 92 | } 93 | 94 | exports.findPackageJSON(cwd, r, function (err, p) { 95 | if (err) { 96 | return callback(err, null, p); 97 | } 98 | 99 | if (!p) { 100 | return callback(null, null, p); 101 | } 102 | 103 | exports.readPackageJSON(p, callback); 104 | }); 105 | }); 106 | 107 | 108 | exports.updatePackageJSON = function(cwd, r, dependencies, callback) { 109 | if (_.isObject(r)) { 110 | callback = dependencies; 111 | dependencies = r; 112 | r = false; 113 | } 114 | 115 | exports.findPackageJSON(cwd, r, function (err, p) { 116 | if (err) { 117 | return callback(err); 118 | } 119 | 120 | if (!p) { 121 | return callback("could not find package.json in '" + cwd + "'"); 122 | } 123 | 124 | logger.info('updating', p); 125 | 126 | exports.readPackageJSON(p, function(err, pkg, path) { 127 | if (err) { 128 | return callback(err); 129 | } 130 | 131 | pkg.jam = pkg.jam || { dependencies: {} }; 132 | 133 | _.extend(pkg.jam.dependencies, dependencies); 134 | 135 | exports.writePackageJSON(p, pkg, callback); 136 | }); 137 | }); 138 | }; 139 | 140 | exports.validate = function (settings, filename) { 141 | // nothing to validate yet 142 | }; 143 | 144 | exports.DEFAULT = { 145 | jam: { 146 | dependencies: {} 147 | } 148 | }; 149 | 150 | /* 151 | exports.createMeta = function (callback) { 152 | utils.getJamVersion(function (err, version) { 153 | if (err) { 154 | return callback(err); 155 | } 156 | callback(null, { 157 | jam_version: version, 158 | dependencies: {} 159 | }); 160 | }); 161 | }; 162 | 163 | exports.writeMeta = function (package_dir, data, callback) { 164 | // TODO: add _rev field to meta file and check if changed since last read 165 | // before writing 166 | var filename = path.resolve(package_dir, 'jam.json'); 167 | try { 168 | var str = JSON.stringify(data, null, 4); 169 | } 170 | catch (e) { 171 | return callback(e); 172 | } 173 | mkdirp(package_dir, function (err) { 174 | if (err) { 175 | return callback(err); 176 | } 177 | logger.info('updating', path.relative(process.cwd(), filename)); 178 | fs.writeFile(filename, str, function (err) { 179 | // TODO: after adding _rev field, return updated _rev value in data here 180 | return callback(err, data); 181 | }); 182 | }); 183 | }; 184 | */ 185 | 186 | // adds RequireJS to project directory 187 | exports.makeRequireJS = function (package_dir, config, callback) { 188 | var require_path = require.resolve('requirejs'); 189 | var source = path.join(require_path, '../../require.js') 190 | var dest = path.resolve(package_dir, 'require.js'); 191 | 192 | logger.info('updating', path.relative(process.cwd(), dest)); 193 | 194 | fs.readFile(source, function (err, content) { 195 | if (err) { 196 | return callback(err); 197 | } 198 | var src = content.toString() + '\n' + config; 199 | fs.writeFile(dest, src, callback); 200 | }); 201 | }; 202 | 203 | exports.getAllPackages = function (dir, callback) { 204 | utils.listDirs(dir, function (err, dirs) { 205 | if (err) { 206 | return callback(err); 207 | } 208 | async.map(dirs, function (d, cb) { 209 | var filename = path.resolve(d, 'package.json'); 210 | exports.readPackageJSON(filename, function (err, cfg, p) { 211 | cb(err, err ? null: {cfg: cfg, dir: path.relative(dir, d)}); 212 | }); 213 | }, callback); 214 | }); 215 | }; 216 | 217 | exports.updateRequireConfig = function (package_dir, baseurl, /*opt*/rcfg, callback) { 218 | rcfg = rcfg || {}; 219 | 220 | if (!callback) { 221 | callback = rcfg; 222 | rcfg = {}; 223 | } 224 | 225 | var packages = []; 226 | var shims = {}; 227 | 228 | var basedir = baseurl ? path.relative(baseurl, package_dir): package_dir; 229 | var dir = basedir.split(path.sep).map(encodeURIComponent).join('/'); 230 | 231 | exports.getAllPackages(package_dir, function (err, pkgs) { 232 | if (err) { 233 | return callback(err); 234 | } 235 | 236 | pkgs.forEach(function (pkg) { 237 | var cfg = pkg.cfg || {}, 238 | val; 239 | 240 | cfg.jam = cfg.jam || {}; 241 | 242 | val = { 243 | name: cfg.name, 244 | location: path.join(dir, encodeURIComponent(pkg.dir), cfg.jam.baseUrl || '').replace(/\/+$/, '') 245 | }; 246 | var main = cfg.main; 247 | if (cfg.browser && cfg.browser.main) { 248 | main = cfg.browser.main; 249 | } 250 | if (cfg.jam && cfg.jam.main) { 251 | main = cfg.jam.main; 252 | } 253 | if (main) { 254 | val.main = main; 255 | } 256 | if (cfg.jam && cfg.jam.name) { 257 | val.name = cfg.jam.name; 258 | } 259 | packages.push(val); 260 | if (cfg.shim) { 261 | shims[cfg.name] = cfg.shim; 262 | } 263 | if (cfg.browser && cfg.browser.shim) { 264 | shims[cfg.name] = cfg.browser.shim; 265 | } 266 | if (cfg.jam && cfg.jam.shim) { 267 | shims[cfg.name] = cfg.jam.shim; 268 | } 269 | if (cfg.jam && cfg.jam.extra_packages) { 270 | var extra_packages = Object.keys(cfg.jam.extra_packages); 271 | extra_packages.forEach(function(extra_pkg){ 272 | var extra_val = { 273 | name: extra_pkg, 274 | location: dir + '/' + encodeURIComponent(pkg.dir), 275 | main: cfg.jam.extra_packages[extra_pkg].main, 276 | local: true 277 | } 278 | packages.push(extra_val); 279 | if (extra_pkg.shim) { 280 | shims[extra_pkg.name] = extra_pkg.name; 281 | } 282 | }); 283 | } 284 | }); 285 | 286 | utils.getJamVersion(function (err, version) { 287 | if (err) { 288 | return callback(err); 289 | } 290 | 291 | var data = { 292 | // TODO: useful option for cache-busting 293 | //urlArgs: '_jam_build=' + (new Date().getTime()), 294 | packages: packages, 295 | version: version, 296 | shim: shims 297 | }; 298 | 299 | // now bring in other require.config.js options to make available 300 | // earlier versions had variable substitution that breaks on r.js compilation 301 | // now there is duplication - however, the original jam has been left untouched. 302 | var cfg = _.clone(rcfg); 303 | cfg.packages = _.union(rcfg.packages || [], packages); 304 | cfg.shim = _.extend({}, rcfg.shim || {}, shims); 305 | var configStr = JSON.stringify(cfg, null, 4); 306 | var dataStr = JSON.stringify(data, null, 4); 307 | var src = fs.readFileSync(path.resolve(__dirname, './tmpls/require.config.js')).toString(); 308 | 309 | src = src.replace(/"\{data\}"/g, dataStr); 310 | 311 | var filename = path.resolve(package_dir, 'require.config.js'); 312 | mkdirp(package_dir, function (err) { 313 | if (err) { 314 | return callback(err); 315 | } 316 | logger.info('updating', path.relative(process.cwd(), filename)); 317 | async.parallel([ 318 | async.apply(fs.writeFile, filename, src), 319 | async.apply(exports.makeRequireJS, package_dir, src) 320 | ], callback); 321 | }); 322 | }); 323 | }); 324 | }; 325 | 326 | exports.getJamDependencies = function (cfg, name) { 327 | var deps; 328 | 329 | if (cfg.jam && cfg.jam.dependencies) { 330 | deps = cfg.jam.dependencies; 331 | } else { 332 | deps = {}; 333 | } 334 | 335 | if (name) { 336 | return deps[name] || null; 337 | } 338 | 339 | return deps; 340 | }; 341 | 342 | exports.setJamDependencies = function (cfg, deps) { 343 | if (!cfg.jam) { 344 | cfg.jam = {}; 345 | } 346 | cfg.jam.dependencies = deps; 347 | return cfg; 348 | }; 349 | 350 | exports.addJamDependency = function (cfg, name, range) { 351 | var deps = exports.getJamDependencies(cfg); 352 | deps[name] = range; 353 | return exports.setJamDependencies(cfg, deps); 354 | }; 355 | 356 | exports.removeJamDependency = function (cfg, name) { 357 | var deps = exports.getJamDependencies(cfg); 358 | delete deps[name]; 359 | return exports.setJamDependencies(cfg, deps); 360 | }; 361 | --------------------------------------------------------------------------------