diff --git a/doc/api/modules.markdown b/doc/api/modules.markdown index 7b7ebefcb38..eb3a4905a35 100644 --- a/doc/api/modules.markdown +++ b/doc/api/modules.markdown @@ -82,11 +82,32 @@ then `require('./foo/bar')` would load the file at entry point to their module, while structuring their package how it suits them. +Any folders named `"node_modules"` that exist in the current module path +will also be appended to the effective require path. This allows for +bundling libraries and other dependencies in a 'node_modules' folder at +the root of a program. + +To avoid overly long lookup paths in the case of nested packages, +the following 2 optimizations are made: + +1. If the module calling `require()` is already within a `node_modules` + folder, then the lookup will not go above the top-most `node_modules` + directory. +2. Node will not append `node_modules` to a path already ending in + `node_modules`. + +So, for example, if the file at +`/usr/lib/node_modules/foo/node_modules/bar.js` were to do +`require('baz')`, then the following places would be searched for a +`baz` module, in this order: + +* 1: `/usr/lib/node_modules/foo/node_modules` +* 2: `/usr/lib/node_modules` + `require.paths` can be modified at runtime by simply unshifting new paths onto it, or at startup with the `NODE_PATH` environmental variable (which should be a list of paths, colon separated). - The second time `require('foo')` is called, it is not loaded again from disk. It looks in the `require.cache` object to see if it has been loaded before. diff --git a/lib/module.js b/lib/module.js index b5724ffaf6e..f94da1d047c 100644 --- a/lib/module.js +++ b/lib/module.js @@ -163,6 +163,35 @@ Module._findPath = function(request, paths) { return false; }; +// 'from' is the __dirname of the module. +Module._nodeModulePaths = function(from) { + // guarantee that 'from' is absolute. + from = path.resolve(from); + + // note: this approach *only* works when the path is guaranteed + // to be absolute. Doing a fully-edge-case-correct path.split + // that works on both Windows and Posix is non-trivial. + var splitRe = process.platform === 'win32' ? /[\/\\]/ : /\//; + // yes, '/' works on both, but let's be a little canonical. + var joiner = process.platform === 'win32' ? '\\' : '/'; + var paths = []; + var parts = from.split(splitRe); + + var root = parts.indexOf('node_modules') - 1; + if (root < 0) root = 0; + + var tip = parts.length - 1; + + for (var tip = parts.length - 1; tip >= root; tip --) { + // don't search in .../node_modules/node_modules + if (parts[tip] === 'node_modules') continue; + var dir = parts.slice(0, tip + 1).concat('node_modules').join(joiner); + paths.push(dir); + } + + return paths; +} + Module._resolveLookupPaths = function(request, parent) { if (NativeModule.exists(request)) { @@ -171,14 +200,18 @@ Module._resolveLookupPaths = function(request, parent) { var start = request.substring(0, 2); if (start !== './' && start !== '..') { - return [request, Module._paths]; + var paths = Module._paths; + if (parent) paths = paths.concat(parent.paths); + return [request, paths]; } // with --eval, parent.id is not set and parent.filename is null if (!parent || !parent.id || !parent.filename) { // make require('./path/to/foo') work - normally the path is taken // from realpath(__filename) but with eval there is no filename - return [request, ['.'].concat(Module._paths)]; + var mainPaths = ['.'].concat(Module._paths); + mainPaths = mainPaths.concat(Module._nodeModulePaths('.')); + return [request, mainPaths]; } // Is the parent an index module? @@ -268,6 +301,7 @@ Module.prototype.load = function(filename) { assert(!this.loaded); this.filename = filename; + this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; diff --git a/test/fixtures/node_modules/asdf.js b/test/fixtures/node_modules/asdf.js new file mode 100644 index 00000000000..67e2ea21ed4 --- /dev/null +++ b/test/fixtures/node_modules/asdf.js @@ -0,0 +1,3 @@ +console.error(__filename); +console.error(module.paths.join('\n')+'\n'); +throw new Error('Should not ever get here.'); diff --git a/test/fixtures/node_modules/bar.js b/test/fixtures/node_modules/bar.js new file mode 100644 index 00000000000..0a9ce329c56 --- /dev/null +++ b/test/fixtures/node_modules/bar.js @@ -0,0 +1,2 @@ +console.error(__filename); +console.error(module.paths.join('\n')+'\n'); diff --git a/test/fixtures/node_modules/baz/index.js b/test/fixtures/node_modules/baz/index.js new file mode 100644 index 00000000000..be18439e616 --- /dev/null +++ b/test/fixtures/node_modules/baz/index.js @@ -0,0 +1,13 @@ +console.error(__filename); +console.error(module.paths.join('\n')+'\n'); +// this should work, and get the one that doesn't throw +require('bar'); + +// since this is inside a node_modules folder, +// it should be impossible to ever see /node_modules in the +// lookup paths, since it's rooted on the uppermost node_modules +// directory. +require('assert').equal(-1, module.paths.indexOf('/node_modules')); + +// this should work, and get the one in ./node_modules/asdf.js +require('asdf'); diff --git a/test/fixtures/node_modules/baz/node_modules/asdf.js b/test/fixtures/node_modules/baz/node_modules/asdf.js new file mode 100644 index 00000000000..0a9ce329c56 --- /dev/null +++ b/test/fixtures/node_modules/baz/node_modules/asdf.js @@ -0,0 +1,2 @@ +console.error(__filename); +console.error(module.paths.join('\n')+'\n'); diff --git a/test/fixtures/node_modules/foo.js b/test/fixtures/node_modules/foo.js new file mode 100644 index 00000000000..90ab0f3609a --- /dev/null +++ b/test/fixtures/node_modules/foo.js @@ -0,0 +1,3 @@ +console.error(__filename); +console.error(module.paths.join('\n')+'\n'); +require('baz'); diff --git a/test/fixtures/node_modules/node_modules/bar.js b/test/fixtures/node_modules/node_modules/bar.js new file mode 100644 index 00000000000..67e2ea21ed4 --- /dev/null +++ b/test/fixtures/node_modules/node_modules/bar.js @@ -0,0 +1,3 @@ +console.error(__filename); +console.error(module.paths.join('\n')+'\n'); +throw new Error('Should not ever get here.'); diff --git a/test/simple/test-module-loading.js b/test/simple/test-module-loading.js index fc1e63b8d2c..12f685ecd23 100644 --- a/test/simple/test-module-loading.js +++ b/test/simple/test-module-loading.js @@ -77,6 +77,11 @@ var root = require('../fixtures/cycles/root'), assert.equal(root.foo, foo); assert.equal(root.sayHello(), root.hello); +common.debug('test node_modules folders'); +// asserts are in the fixtures files themselves, +// since they depend on the folder structure. +require('../fixtures/node_modules/foo'); + common.debug('test name clashes'); // this one exists and should import the local module var my_path = require('./path');