diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e1c0297 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "0.12" + - "0.10" +notifications: + email: + on_success: never \ No newline at end of file diff --git a/README.md b/README.md index 28c4235..8e03c12 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## slinker +[![Build Status](https://travis-ci.org/lewisdawson/slinker.svg)](https://travis-ci.org/lewisdawson/slinker) + A simple package used to symlink [Browserify](http://browserify.org/) dependencies as well as node.js submodule dependencies. At a high level, slinker takes a list of local node.js submodules (directories) and adds a symlink for each submodule to the `node_modules` (or equivalent) folder. ## Why?? @@ -74,6 +76,38 @@ Slinker contains a number of configuration parameters that can be used to custom An `Array` of submodule names that can be found within the `modulesBasePath`. +###### Relative Paths + +A submodule name can also be a path to a subdirectory, relative to the `modulesBasePath` directory. The inner-most subdirectory name is used for the name of the symlink. For example: + +```javascript +slinker.link({ + modules: ['path/to/models'], + modulesBasePath: __dirname, + symlinkPrefix: '@', + nodeModulesPath: path.join(__dirname, 'node_modules'), + // other configs below +}); + ``` + +This will result in a symlink named `@models`, linked to `__dirname/path/to/models`, under the `__dirname/node_modules` directory. + +###### module Definition Object + +A submodule name can also be aliased if you prefer that the symlink is a name other than the actual submodule name. To utilize the aliasing, an Object that contains the `module` and the `alias` properties. The `module` property is the name/path of the submodule while the `alias` property is the alias name of the the symlink to be used. For example: + +```javascript +slinker.link({ + modules: [{ module: 'models', alias: 'awesome_models'}], + modulesBasePath: __dirname, + symlinkPrefix: '@', + nodeModulesPath: path.join(__dirname, 'node_modules'), + // other configs below +}); + ``` + +This will create a symlink in the `nodeModulesPath` directory named `@awesome_models` that points to the `__dirname/models` directory. The `module` property must still be a valid module name or relative path that is relative to the `modulesBasePath` directory. + #### modulesBasePath The `String` path under which all submodules will be searched for. By default, the directory under which slinker is being invoked will be used. If you want the base path to be the current directory of Slinker's invocation, you can use `__dirname`. diff --git a/index.js b/index.js index 55ed710..1f89b9e 100644 --- a/index.js +++ b/index.js @@ -4,59 +4,59 @@ 'use strict'; var _ = require('underscore'), - path = require('path'), - fs = require('fs'), - symlinksCreated = [], - slinkerDefaults; + path = require('path'), + fs = require('fs'), + symlinksCreated = [], + slinkerDefaults; slinkerDefaults = { - modules: [], - symlinkPrefix: '@', - nodeModulesPath: './node_modules' + modules: [], + symlinkPrefix: '@', + nodeModulesPath: './node_modules' }; /** * @param {Object} slinkerOptions - * The options Object that was passed to the Slinker invocation + * The options Object that was passed to the Slinker invocation * @param {Object} slinkerOptions - * The options Object used to configure the symlink behavior + * The options Object used to configure the symlink behavior * @param {String} symlinkConfig.module - * The name of the module to create a symlink for + * The name of the module to create a symlink for * @param {String} symlinkConfig.modulePath - * The path to the physical module that's used for symlink creation (includes the module name) + * The path to the physical module that's used for symlink creation (includes the module name) * @param {String} symlinkConfig.symlinkNodeModulesPath - * The path to the location where the symlink will reside (includes the module name) + * The path to the location where the symlink will reside (includes the module name) * @param {Boolean} exists - * A Boolean value that indicates if the symlink already exists + * A Boolean value that indicates if the symlink already exists */ function createSymlinkIfNotExists(slinkerOptions, symlinkConfig, exists) { - if(!exists) { - fs.symlink(symlinkConfig.modulePath, symlinkConfig.symlinkNodeModulesPath, 'file', function(err) { - if(err) { - console.log("Error creating symlink for module '" + symlinkConfig.module + "'! " + err); - invokeOnError(slinkerOptions, err); - } else { - console.log("Symlink for module '" + symlinkConfig.module + "' created."); - onSymlinkCreated(slinkerOptions, symlinkConfig.module); - } - }); - } else { - console.log("Symlink for module '" + symlinkConfig.module + "' already exists. No op."); - onSymlinkCreated(slinkerOptions, symlinkConfig.module); - } + if (!exists) { + fs.symlink(symlinkConfig.modulePath, symlinkConfig.symlinkNodeModulesPath, 'file', function(err) { + if (err) { + console.log("Error creating symlink for module '" + symlinkConfig.moduleAlias + "'! " + err); + invokeOnError(slinkerOptions, err); + } else { + console.log("Symlink for module '" + symlinkConfig.moduleAlias + "' created."); + onSymlinkCreated(slinkerOptions, symlinkConfig.module); + } + }); + } else { + console.log("Symlink for module '" + symlinkConfig.moduleAlias + "' already exists. No op."); + onSymlinkCreated(slinkerOptions, symlinkConfig.moduleAlias); + } } /** * Invoked each time a symlink is created. * * @param {Object} slinkerOptions - * The options passed to slinker - * @param {String} module - * The name of the module that was created + * The options passed to slinker + * @param {String} moduleAlias + * The name of the module that was created */ -function onSymlinkCreated(slinkerOptions, module) { - symlinksCreated.push(module); - invokeOnComplete(slinkerOptions); +function onSymlinkCreated(slinkerOptions, moduleAlias) { + symlinksCreated.push(moduleAlias); + invokeOnComplete(slinkerOptions); } /** @@ -64,12 +64,12 @@ function onSymlinkCreated(slinkerOptions, module) { * been specified, it is invoked. * * @param {Object} slinkerOptions - * The options passed to slinker + * The options passed to slinker */ function invokeOnComplete(slinkerOptions) { - if(typeof slinkerOptions.onComplete === 'function' && slinkerOptions.modules.length === symlinksCreated.length) { - slinkerOptions.onComplete(); - } + if (typeof slinkerOptions.onComplete === 'function' && slinkerOptions.modules.length === symlinksCreated.length) { + slinkerOptions.onComplete(); + } } /** @@ -77,14 +77,14 @@ function invokeOnComplete(slinkerOptions) { * been specified, it is invoked. * * @param {Object} slinkerOptions - * The options passed to slinker + * The options passed to slinker * @param {String} error - * The error that occurred + * The error that occurred */ function invokeOnError(slinkerOptions, error) { - if(typeof slinkerOptions.onError === 'function') { - slinkerOptions.onError(error); - } + if (typeof slinkerOptions.onError === 'function') { + slinkerOptions.onError(error); + } } /** @@ -92,68 +92,154 @@ function invokeOnError(slinkerOptions, error) { * met and slinker should be invoked. * * options {Object} options - * The options passed to slinker + * The options passed to slinker */ function checkPreconditions(options) { - if(!options) { - throw Error("'options' must be specified!"); - } - - if(!(options.modules instanceof Array)) { - throw Error("'options.modules' must be an array!"); - } - - // If the modules array is empty, immediately call the onComplete() if it exists - if(!options.modules.length) { - if(options.onComplete) { - invokeOnComplete(options); - } - - return false; - } - - return true; + if (!options) { + throw Error("'options' must be specified!"); + } + + if (!(options.modules instanceof Array)) { + throw Error("'options.modules' must be an array!"); + } + + // If the modules array is empty, immediately call the onComplete() if it exists + if (!options.modules.length) { + if (options.onComplete) { + invokeOnComplete(options); + } + + return false; + } + + return true; +} + +/** + * @param module + * The module parameter to determine the type of + * @returns True if the parameter module is an Object, other false. + */ +function isModuleObject(module) { + return (module && typeof(module) === 'object' && !Array.isArray(module)); +} + +/** + * @param module + * The module string or object + * @returns The module's path for the parameter module. If the parameter module is a JS object, then the `module` + * property for the module is returned. + */ +function getModulePathProperty(module) { + var path = module; + + if (isModuleObject(module)) { + path = module.module; + + if (!path) { + throw 'A module Object must contain the \'module\' definition!'; + } + } + + return path; +} + +/** + * @param module + * The module definition to retrieve the alias from + * @returns The alias for the module if it's a JS object. Otherwise, return null. + */ +function getModuleAliasProperty(module) { + var name = null; + + if (isModuleObject(module) && module.alias) { + name = module.alias; + } + + return name; +} + +/** + * Determines from the module path, what the name of the symlink should be. The deepest name in the path is used. For + * example, if the module is `my/module/path/is/cool`, then the symlink name would be `cool`. If the module is an + * object and the object contains the `alias` property, then the `alias` property is used for the symlink name. + * + * @param module + * The module path or object used to determine the name of the symlink + * @return The name of the symlink, derived from the deepest path of the module or the module `alias` + */ +function getSymlinkNameFromModule(module) { + var name = null, + splitModule; + + name = getModuleAliasProperty(module); + // If the module is not an Object or the alias wasn't specified, split the module path (if necessary) + if (name == null) { + module = getModulePathProperty(module); + splitModule = module.split(path.sep); + + if (splitModule.length === 1) { + return module; + } + + return splitModule[splitModule.length - 1]; + } + + return name; } module.exports = { - /** - * - * @param {Object} options - * An Object that contains the symlinkConfigurations used for symlinking - * @param {Array} options.modules - * An array that contains the name of each module (directory) to symlink - * @param {String} options.modulesBasePath - * The base path under which all modules that are to be symlinked reside - * @param {String} options.symlinkPrefix - * The prefix to use when creating a symlink - * @param {String} options.nodeModulesPath - * The path to the node_modules directory where all symlinks will be created - * @param {Function} options.onComplete - * A callback that is invoked once all symlinks have been created - * @param {Function} options.onError - * A callback that is invoked if an error occurred while attempting to create a symlink - */ - link: function(options) { - options = _.defaults(options, slinkerDefaults); - - if(checkPreconditions(options)) { - _.each(options.modules, function(module) { - var modulePath, - symlinkNodeModulesPath; - - // The actual path to the file - modulePath = path.join(options.modulesBasePath, module); - // The path to the symlink under the node_modules directory - symlinkNodeModulesPath = path.join(options.nodeModulesPath, options.symlinkPrefix + module); - - fs.exists(symlinkNodeModulesPath, _.bind(createSymlinkIfNotExists, this, options, { - module: module, - modulePath: modulePath, - symlinkNodeModulesPath: symlinkNodeModulesPath - })); - }); - } - } + /** + * Resets the array of modules that was created. + */ + reset: function() { + symlinksCreated = []; + }, + + /** + * + * @param {Object} options + * An Object that contains the symlinkConfigurations used for symlinking + * @param {Array} options.modules + * An array that contains the name of each module (directory) to symlink + * @param {String} options.modulesBasePath + * The base path under which all modules that are to be symlinked reside + * @param {String} options.symlinkPrefix + * The prefix to use when creating a symlink + * @param {String} options.nodeModulesPath + * The path to the node_modules directory where all symlinks will be created + * @param {Function} options.onComplete + * A callback that is invoked once all symlinks have been created + * @param {Function} options.onError + * A callback that is invoked if an error occurred while attempting to create a symlink + */ + link: function(options) { + options = _.defaults(options, slinkerDefaults); + + if (checkPreconditions(options)) { + _.each(options.modules, function(module) { + var modulePath, + moduleAlias, + symlinkNodeModulesPath; + + modulePath = getModulePathProperty(module); + // The actual path to the file + modulePath = path.join(options.modulesBasePath, modulePath); + + // Get the alias namve for the module + moduleAlias = getSymlinkNameFromModule(module); + // The path to the symlink under the node_modules directory + symlinkNodeModulesPath = + path.join(options.nodeModulesPath, options.symlinkPrefix + moduleAlias); + + fs.exists(symlinkNodeModulesPath, _.bind(createSymlinkIfNotExists, this, options, { + moduleAlias: moduleAlias, + modulePath: modulePath, + symlinkNodeModulesPath: symlinkNodeModulesPath + })); + }); + } + } }; \ No newline at end of file diff --git a/package.json b/package.json index a619e76..387d6f9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A simple module used to symlink node module dependencies together.", "main": "index.js", "scripts": { - "test": "mocha test/indexTest.js" + "test": "./node_modules/mocha/bin/mocha test/indexTest.js" }, "repository": { "type": "git", diff --git a/test/indexTest.js b/test/indexTest.js index 7363cb3..a2ca0b0 100644 --- a/test/indexTest.js +++ b/test/indexTest.js @@ -4,146 +4,201 @@ 'use strict'; var should = require('chai').should(), - expect = require('chai').expect, - slinker = require('../index'), - fs = require('fs'), - path = require('path'), - glob = require('glob'), - _ = require('underscore'); + expect = require('chai').expect, + slinker = require('../index'), + fs = require('fs'), + path = require('path'), + glob = require('glob'), + _ = require('underscore'); describe('indexTest', function() { - var symlinkModules, - noSymlinkModules, - nodeModulesPath, - symlinkPrefix, - modulesBasePath, - defaultSlinkerConfig; - - modulesBasePath = __dirname; - symlinkModules = ['module_one', 'module_three']; - noSymlinkModules = ['module_two']; - nodeModulesPath = path.join(modulesBasePath, 'mock_node_modules'); - symlinkPrefix = '@'; - - defaultSlinkerConfig = { - modules: symlinkModules, - modulesBasePath: modulesBasePath, - symlinkPrefix: symlinkPrefix, - nodeModulesPath: nodeModulesPath - }; - - function constructNodeModuleSymlinkPath(module) { - return path.join(nodeModulesPath, symlinkPrefix + module); - } - - /** - * @parameter path - * The path on the file system of the symlink - * @return true if the parameter symlink path exists, otherwise false - */ - function doesSymlinkExist(path) { - try { - fs.lstatSync(path); - return true; - } catch(err) { - // The symlink doesn't exist - return false; - } - } - - /** - * Asserts that all parameter (symlink) modules exitences are equal to the parameter - * exists. - */ - function assertSymlinksEqual(symlinkPaths, exists) { - var path, - i; - - _.each(symlinkPaths, function(symlinkPath) { - symlinkPath = constructNodeModuleSymlinkPath(symlinkPath); - expect(doesSymlinkExist(symlinkPath)).to.equal(exists, 'Expected symlink "' + symlinkPath + '" to ' + (exists ? 'exist, but it doesn\'t' : 'not exist, but it does')); - }); - } - - /** - * Assert that all parameter symlinkPaths exist. - * - * @param symlinkPaths - * The array of symlink paths to assert - */ - function assertSymlinksExist(symlinkPaths) { - assertSymlinksEqual(symlinkPaths, true); - } - - /** - * Assert that all parameter symlinkPaths do not exist. - * - * @param symlinkPaths - * The array of symlink paths to assert - */ - function assertSymlinksNotExist(symlinkPaths) { - assertSymlinksEqual(symlinkPaths, false); - } - - /** - * Removes all existing symlinks from the test directory. - */ - function removeExistingSymlinks(onComplete) { - var globPath = path.join(nodeModulesPath, symlinkPrefix + '**'); - - glob(globPath, function(err, files) { - _.each(files, function(file) { - fs.unlinkSync(file); - }); - - onComplete(); - }); - } - - /** - * Executed before each test. - */ - beforeEach(function(done) { - var allModules = symlinkModules.concat(noSymlinkModules); - - removeExistingSymlinks(function() { - assertSymlinksNotExist(allModules); - - done(); - }); - }); - - it('#link(): should add no symlinks if no directories (modules) are specified in options.modules', function(done) { - var allModules = symlinkModules.concat(noSymlinkModules), - slinkerConfig = _.defaults({ - modules: [], - onComplete: function() { - assertSymlinksNotExist(allModules); - - done(); - }, - onError: function(err) { - throw Error('Unexpected error occurred while creating symlinks! ' + err); - } - }, defaultSlinkerConfig); - - slinker.link(slinkerConfig); - }); - - it('#link(): should add a symlink for each directory (module) name specified in options.modules', function(done) { - var slinkerConfig = _.defaults({ - onComplete: function() { - assertSymlinksExist(symlinkModules); - - done(); - }, - onError: function(err) { - throw Error('Unexpected error occurred while creating symlinks! ' + err); - } - }, defaultSlinkerConfig); - - slinker.link(slinkerConfig); - }); + var symlinkModules, + noSymlinkModules, + relativePathSymlinkModules, + aliasedSymlinkModules, + nodeModulesPath, + symlinkPrefix, + modulesBasePath, + defaultSlinkerConfig; + + modulesBasePath = __dirname; + symlinkModules = ['module_one', 'module_three']; + relativePathSymlinkModules = ['module_four/a/nested']; + aliasedSymlinkModules = [{ + module: 'module_one', + alias: 'module_one_alias' + }]; + noSymlinkModules = ['module_two']; + nodeModulesPath = path.join(modulesBasePath, 'mock_node_modules'); + symlinkPrefix = '@'; + + defaultSlinkerConfig = { + modules: symlinkModules, + modulesBasePath: modulesBasePath, + symlinkPrefix: symlinkPrefix, + nodeModulesPath: nodeModulesPath + }; + + function isModuleObject(module) { + return (module && typeof(module) === 'object' && !Array.isArray(module)); + } + + function constructNodeModuleSymlinkPath(module) { + var splitModule; + + if(isModuleObject(module)) { + module = module.alias; + } else { + splitModule = module.split(path.sep); + module = (splitModule.length === 1 ? module : splitModule[splitModule.length - 1]); + } + + return path.join(nodeModulesPath, symlinkPrefix + module); + } + + /** + * @parameter path + * The path on the file system of the symlink + * @return true if the parameter symlink path exists, otherwise false + */ + function doesSymlinkExist(path) { + try { + fs.lstatSync(path); + return true; + } catch (err) { + // The symlink doesn't exist + return false; + } + } + + /** + * Asserts that all parameter (symlink) modules exitences are equal to the parameter + * exists. + */ + function assertSymlinksEqual(symlinkPaths, exists) { + _.each(symlinkPaths, function(symlinkPath) { + symlinkPath = constructNodeModuleSymlinkPath(symlinkPath); + expect(doesSymlinkExist(symlinkPath)).to.equal(exists, 'Expected symlink "' + symlinkPath + '" to ' + + (exists ? 'exist, but it doesn\'t' : + 'not exist, but it does')); + }); + } + + /** + * Assert that all parameter symlinkPaths exist. + * + * @param symlinkPaths + * The array of symlink paths to assert + */ + function assertSymlinksExist(symlinkPaths) { + assertSymlinksEqual(symlinkPaths, true); + } + + /** + * Assert that all parameter symlinkPaths do not exist. + * + * @param symlinkPaths + * The array of symlink paths to assert + */ + function assertSymlinksNotExist(symlinkPaths) { + assertSymlinksEqual(symlinkPaths, false); + } + + /** + * Removes all existing symlinks from the test directory. + */ + function removeExistingSymlinks(onComplete) { + var globPath = path.join(nodeModulesPath, symlinkPrefix + '**'); + + glob(globPath, function(err, files) { + _.each(files, function(file) { + fs.unlinkSync(file); + }); + + onComplete(); + }); + } + + /** + * Executed before each test. + */ + beforeEach(function(done) { + var allModules = symlinkModules.concat(noSymlinkModules); + + slinker.reset(); + + removeExistingSymlinks(function() { + assertSymlinksNotExist(allModules); + + done(); + }); + }); + + it('#link(): should add no symlinks if no directories (modules) are specified in options.modules', function(done) { + var allModules = symlinkModules.concat(noSymlinkModules), + slinkerConfig = _.defaults({ + modules: [], + onComplete: function() { + assertSymlinksNotExist(allModules); + + done(); + }, + onError: function(err) { + throw Error('Unexpected error occurred while creating symlinks! ' + err); + } + }, defaultSlinkerConfig); + + slinker.link(slinkerConfig); + }); + + it('#link(): should add a symlink for each directory (module) name specified in options.modules', function(done) { + var slinkerConfig = _.defaults({ + onComplete: function() { + assertSymlinksExist(symlinkModules); + + done(); + }, + onError: function(err) { + throw Error('Unexpected error occurred while creating symlinks! ' + err); + } + }, defaultSlinkerConfig); + + slinker.link(slinkerConfig); + }); + + it('#link(): should add a symlink for a relative path directory, and the symlink should have the name of the deepest directory of the path', + function(done) { + var slinkerConfig = _.defaults({ + modules: relativePathSymlinkModules, + onComplete: function() { + assertSymlinksExist(relativePathSymlinkModules); + + done(); + }, + onError: function(err) { + throw Error('Unexpected error occurred while creating symlinks! ' + err); + } + }, defaultSlinkerConfig); + + slinker.link(slinkerConfig); + }); + + it('#link(): should add a symlink for a directory (module), where the symlink name is the alias specified in the module object definition', + function(done) { + var slinkerConfig = _.defaults({ + modules: aliasedSymlinkModules, + onComplete: function() { + assertSymlinksExist(aliasedSymlinkModules); + + done(); + }, + onError: function(err) { + throw Error('Unexpected error occurred while creating symlinks! ' + err); + } + }, defaultSlinkerConfig); + + slinker.link(slinkerConfig); + }); }); \ No newline at end of file diff --git a/test/module_four/.gitignore b/test/module_four/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/module_four/a/.gitignore b/test/module_four/a/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/module_four/a/nested/.gitignore b/test/module_four/a/nested/.gitignore new file mode 100644 index 0000000..e69de29