Skip to content

Commit

Permalink
[New] sync/async: add realpath/realpathSync options
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB authored and ljharb committed Apr 7, 2020
1 parent 8238b8c commit be951d3
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 25 deletions.
28 changes: 17 additions & 11 deletions lib/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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'];
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
34 changes: 20 additions & 14 deletions lib/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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++) {
Expand All @@ -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'];
Expand All @@ -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'));
Expand All @@ -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 + "'");
Expand Down Expand Up @@ -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));
Expand All @@ -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');
Expand Down
22 changes: 22 additions & 0 deletions readme.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
70 changes: 70 additions & 0 deletions test/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
}
Expand Down Expand Up @@ -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);
}
};
}
Expand Down Expand Up @@ -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);
}
};
}
Expand Down Expand Up @@ -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);
}
};
}
Expand All @@ -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);
});
});
Loading

0 comments on commit be951d3

Please sign in to comment.