diff --git a/lib/async.js b/lib/async.js index 86983dd6..ad4b36b7 100644 --- a/lib/async.js +++ b/lib/async.js @@ -5,7 +5,7 @@ var nodeModulesPaths = require('./node-modules-paths.js'); var normalizeOptions = require('./normalize-options.js'); var isCore = require('./is-core'); -var realpath = fs.realpath && typeof fs.realpath.native === 'function' ? fs.realpath.native : fs.realpath; +var realpathFS = fs.realpath && typeof fs.realpath.native === 'function' ? fs.realpath.native : fs.realpath; var defaultIsFile = function isFile(file, cb) { fs.stat(file, function (err, stat) { @@ -27,12 +27,16 @@ var defaultIsDir = function isDirectory(dir, cb) { }); }; -var maybeUnwrapSymlink = function maybeUnwrapSymlink(x, opts, cb) { +var defaultRealpath = function realpath(x, cb) { + realpathFS(x, function (realpathErr, realPath) { + if (realpathErr && realpathErr.code !== 'ENOENT') cb(realpathErr); + else cb(null, realpathErr ? x : realPath); + }); +}; + +var maybeRealpath = function maybeRealpath(realpath, x, opts, cb) { if (!opts || !opts.preserveSymlinks) { - realpath(x, function (realPathErr, realPath) { - if (realPathErr && realPathErr.code !== 'ENOENT') cb(realPathErr); - else cb(null, realPathErr ? x : realPath); - }); + realpath(x, cb); } else { cb(null, x); } @@ -65,6 +69,7 @@ module.exports = function resolve(x, options, callback) { var isFile = opts.isFile || defaultIsFile; var isDirectory = opts.isDirectory || defaultIsDir; var readFile = opts.readFile || fs.readFile; + var realpath = opts.realpath || defaultRealpath; var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; @@ -76,7 +81,8 @@ module.exports = function resolve(x, options, callback) { // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory var absoluteStart = path.resolve(basedir); - maybeUnwrapSymlink( + maybeRealpath( + realpath, absoluteStart, opts, function (err, realStart) { @@ -112,7 +118,7 @@ module.exports = function resolve(x, options, callback) { } else loadNodeModules(x, basedir, function (err, n, pkg) { if (err) cb(err); else if (n) { - return maybeUnwrapSymlink(n, opts, function (err, realN) { + return maybeRealpath(realpath, n, opts, function (err, realN) { if (err) { cb(err); } else { @@ -133,7 +139,7 @@ module.exports = function resolve(x, options, callback) { else loadAsDirectory(res, function (err, d, pkg) { if (err) cb(err); else if (d) { - maybeUnwrapSymlink(d, opts, function (err, realD) { + maybeRealpath(realpath, d, opts, function (err, realD) { if (err) { cb(err); } else { @@ -197,7 +203,7 @@ module.exports = function resolve(x, options, callback) { } if ((/[/\\]node_modules[/\\]*$/).test(dir)) return cb(null); - maybeUnwrapSymlink(dir, opts, function (unwrapErr, pkgdir) { + maybeRealpath(realpath, dir, opts, function (unwrapErr, pkgdir) { if (unwrapErr) return loadpkg(path.dirname(dir), cb); var pkgfile = path.join(pkgdir, 'package.json'); isFile(pkgfile, function (err, ex) { @@ -225,7 +231,7 @@ module.exports = function resolve(x, options, callback) { fpkg = opts.package; } - maybeUnwrapSymlink(x, opts, function (unwrapErr, pkgdir) { + maybeRealpath(realpath, x, opts, function (unwrapErr, pkgdir) { if (unwrapErr) return loadAsDirectory(path.dirname(x), fpkg, cb); var pkgfile = path.join(pkgdir, 'package.json'); isFile(pkgfile, function (err, ex) { diff --git a/lib/sync.js b/lib/sync.js index 932c1ee6..08378087 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -5,7 +5,7 @@ var caller = require('./caller.js'); var nodeModulesPaths = require('./node-modules-paths.js'); var normalizeOptions = require('./normalize-options.js'); -var realpath = fs.realpathSync && typeof fs.realpathSync.native === 'function' ? fs.realpathSync.native : fs.realpathSync; +var realpathFS = fs.realpathSync && typeof fs.realpathSync.native === 'function' ? fs.realpathSync.native : fs.realpathSync; var defaultIsFile = function isFile(file) { try { @@ -27,19 +27,24 @@ var defaultIsDir = function isDirectory(dir) { return stat.isDirectory(); }; -var maybeUnwrapSymlink = function maybeUnwrapSymlink(x, opts) { - if (!opts || !opts.preserveSymlinks) { - try { - return realpath(x); - } catch (realPathErr) { - if (realPathErr.code !== 'ENOENT') { - throw realPathErr; - } +var defaultRealpathSync = function realpathSync(x) { + try { + return realpathFS(x); + } catch (realpathErr) { + if (realpathErr.code !== 'ENOENT') { + throw realpathErr; } } return x; }; +var maybeRealpathSync = function maybeRealpathSync(realpathSync, x, opts) { + if (!opts || !opts.preserveSymlinks) { + return realpathSync(x); + } + return x; +}; + var getPackageCandidates = function getPackageCandidates(x, start, opts) { var dirs = nodeModulesPaths(start, opts, x); for (var i = 0; i < dirs.length; i++) { @@ -57,6 +62,7 @@ module.exports = function resolveSync(x, options) { var isFile = opts.isFile || defaultIsFile; var isDirectory = opts.isDirectory || defaultIsDir; var readFileSync = opts.readFileSync || fs.readFileSync; + var realpathSync = opts.realpathSync || defaultRealpathSync; var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; @@ -66,7 +72,7 @@ module.exports = function resolveSync(x, options) { opts.paths = opts.paths || []; // ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory - var absoluteStart = maybeUnwrapSymlink(path.resolve(basedir), opts); + var absoluteStart = maybeRealpathSync(realpathSync, path.resolve(basedir), opts); if (opts.basedir && !isDirectory(absoluteStart)) { var dirError = new TypeError('Provided basedir "' + opts.basedir + '" is not a directory' + (opts.preserveSymlinks ? '' : ', or a symlink to a directory')); @@ -78,12 +84,12 @@ module.exports = function resolveSync(x, options) { var res = path.resolve(absoluteStart, x); if (x === '.' || x === '..' || x.slice(-1) === '/') res += '/'; var m = loadAsFileSync(res) || loadAsDirectorySync(res); - if (m) return maybeUnwrapSymlink(m, opts); + if (m) return maybeRealpathSync(realpathSync, m, opts); } else if (isCore(x)) { return x; } else { var n = loadNodeModulesSync(x, absoluteStart); - if (n) return maybeUnwrapSymlink(n, opts); + if (n) return maybeRealpathSync(realpathSync, n, opts); } var err = new Error("Cannot find module '" + x + "' from '" + parent + "'"); @@ -120,7 +126,7 @@ module.exports = function resolveSync(x, options) { } if ((/[/\\]node_modules[/\\]*$/).test(dir)) return; - var pkgfile = path.join(isDirectory(dir) ? maybeUnwrapSymlink(dir, opts) : dir, 'package.json'); + var pkgfile = path.join(isDirectory(dir) ? maybeRealpathSync(realpathSync, dir, opts) : dir, 'package.json'); if (!isFile(pkgfile)) { return loadpkg(path.dirname(dir)); @@ -140,7 +146,7 @@ module.exports = function resolveSync(x, options) { } function loadAsDirectorySync(x) { - var pkgfile = path.join(isDirectory(x) ? maybeUnwrapSymlink(x, opts) : x, '/package.json'); + var pkgfile = path.join(isDirectory(x) ? maybeRealpathSync(realpathSync, x, opts) : x, '/package.json'); if (isFile(pkgfile)) { try { var body = readFileSync(pkgfile, 'UTF8'); diff --git a/readme.markdown b/readme.markdown index cdea6778..8d5f5248 100644 --- a/readme.markdown +++ b/readme.markdown @@ -61,6 +61,8 @@ options are: * opts.isDirectory - function to asynchronously test whether a file exists and is a directory +* opts.realpath - function to asynchronously resolve a potential symlink to its real path + * `opts.packageFilter(pkg, pkgfile, dir)` - transform the parsed package.json contents before looking at the "main" field * pkg - package data * pkgfile - path to package.json @@ -117,6 +119,13 @@ default `opts` values: return cb(err); }); }, + realpath: function realpath(file, cb) { + var realpath = typeof fs.realpath.native === 'function' ? fs.realpath.native : fs.realpath; + realpath(file, function (realPathErr, realPath) { + if (realPathErr && realPathErr.code !== 'ENOENT') cb(realPathErr); + else cb(null, realPathErr ? file : realPath); + }); + }, moduleDirectory: 'node_modules', preserveSymlinks: false } @@ -139,6 +148,8 @@ options are: * opts.isDirectory - function to synchronously test whether a file exists and is a directory +* opts.realpathSync - function to synchronously resolve a potential symlink to its real path + * `opts.packageFilter(pkg, pkgfile, dir)` - transform the parsed package.json contents before looking at the "main" field * pkg - package data * pkgfile - path to package.json @@ -195,6 +206,17 @@ default `opts` values: } return stat.isDirectory(); }, + realpathSync: function realpathSync(file) { + try { + var realpath = typeof fs.realpathSync.native === 'function' ? fs.realpathSync.native : fs.realpathSync; + return realpath(file); + } catch (realPathErr) { + if (realPathErr.code !== 'ENOENT') { + throw realPathErr; + } + } + return file; + }, moduleDirectory: 'node_modules', preserveSymlinks: false } diff --git a/test/mock.js b/test/mock.js index d4f57a31..b9f17fe2 100644 --- a/test/mock.js +++ b/test/mock.js @@ -22,6 +22,9 @@ test('mock', function (t) { }, readFile: function (file, cb) { cb(null, files[path.resolve(file)]); + }, + realpath: function (file, cb) { + cb(null, file); } }; } @@ -70,6 +73,9 @@ test('mock from package', function (t) { 'package': { main: 'bar' }, readFile: function (file, cb) { cb(null, files[file]); + }, + realpath: function (file, cb) { + cb(null, file); } }; } @@ -121,6 +127,9 @@ test('mock package', function (t) { }, readFile: function (file, cb) { cb(null, files[path.resolve(file)]); + }, + realpath: function (file, cb) { + cb(null, file); } }; } @@ -157,6 +166,9 @@ test('mock package from package', function (t) { 'package': { main: 'bar' }, readFile: function (file, cb) { cb(null, files[path.resolve(file)]); + }, + realpath: function (file, cb) { + cb(null, file); } }; } @@ -167,3 +179,61 @@ test('mock package from package', function (t) { t.equal(pkg && pkg.main, './baz.js'); }); }); + +test('symlinked', function (t) { + t.plan(4); + + var files = {}; + files[path.resolve('/foo/bar/baz.js')] = 'beep'; + files[path.resolve('/foo/bar/symlinked/baz.js')] = 'beep'; + + var dirs = {}; + dirs[path.resolve('/foo/bar')] = true; + dirs[path.resolve('/foo/bar/symlinked')] = true; + + function opts(basedir) { + return { + preserveSymlinks: false, + basedir: path.resolve(basedir), + isFile: function (file, cb) { + cb(null, Object.prototype.hasOwnProperty.call(files, path.resolve(file))); + }, + isDirectory: function (dir, cb) { + cb(null, !!dirs[path.resolve(dir)]); + }, + readFile: function (file, cb) { + cb(null, files[path.resolve(file)]); + }, + realpath: function (file, cb) { + var resolved = path.resolve(file); + + if (resolved.indexOf('symlinked') >= 0) { + cb(null, resolved); + return; + } + + var ext = path.extname(resolved); + + if (ext) { + var dir = path.dirname(resolved); + var base = path.basename(resolved); + cb(null, path.join(dir, 'symlinked', base)); + } else { + cb(null, path.join(resolved, 'symlinked')); + } + } + }; + } + + resolve('./baz', opts('/foo/bar'), function (err, res, pkg) { + if (err) return t.fail(err); + t.equal(res, path.resolve('/foo/bar/symlinked/baz.js')); + t.equal(pkg, undefined); + }); + + resolve('./baz.js', opts('/foo/bar'), function (err, res, pkg) { + if (err) return t.fail(err); + t.equal(res, path.resolve('/foo/bar/symlinked/baz.js')); + t.equal(pkg, undefined); + }); +}); diff --git a/test/mock_sync.js b/test/mock_sync.js index 50812968..ef2bc6d3 100644 --- a/test/mock_sync.js +++ b/test/mock_sync.js @@ -23,6 +23,9 @@ test('mock', function (t) { }, readFileSync: function (file) { return files[path.resolve(file)]; + }, + realpathSync: function (file) { + return file; } }; } @@ -70,6 +73,9 @@ test('mock package', function (t) { }, readFileSync: function (file) { return files[path.resolve(file)]; + }, + realpathSync: function (file) { + return file; } }; } @@ -79,3 +85,58 @@ test('mock package', function (t) { path.resolve('/foo/node_modules/bar/baz.js') ); }); + +test('symlinked', function (t) { + t.plan(2); + + var files = {}; + files[path.resolve('/foo/bar/baz.js')] = 'beep'; + files[path.resolve('/foo/bar/symlinked/baz.js')] = 'beep'; + + var dirs = {}; + dirs[path.resolve('/foo/bar')] = true; + dirs[path.resolve('/foo/bar/symlinked')] = true; + + function opts(basedir) { + return { + preserveSymlinks: false, + basedir: path.resolve(basedir), + isFile: function (file) { + return Object.prototype.hasOwnProperty.call(files, path.resolve(file)); + }, + isDirectory: function (dir) { + return !!dirs[path.resolve(dir)]; + }, + readFileSync: function (file) { + return files[path.resolve(file)]; + }, + realpathSync: function (file) { + var resolved = path.resolve(file); + + if (resolved.indexOf('symlinked') >= 0) { + return resolved; + } + + var ext = path.extname(resolved); + + if (ext) { + var dir = path.dirname(resolved); + var base = path.basename(resolved); + return path.join(dir, 'symlinked', base); + } else { + return path.join(resolved, 'symlinked'); + } + } + }; + } + + t.equal( + resolve.sync('./baz', opts('/foo/bar')), + path.resolve('/foo/bar/symlinked/baz.js') + ); + + t.equal( + resolve.sync('./baz.js', opts('/foo/bar')), + path.resolve('/foo/bar/symlinked/baz.js') + ); +});