Skip to content

Commit

Permalink
[Breaking] add isDirectory; error early if provided basedir is no…
Browse files Browse the repository at this point in the history
…t a directory

Fixes #154
  • Loading branch information
ljharb committed Jun 16, 2018
1 parent bab0475 commit 698a3e1
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 32 deletions.
56 changes: 40 additions & 16 deletions lib/async.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ var defaultIsFile = function isFile(file, cb) {
});
};

var defaultIsDir = function isDirectory(dir, cb) {
fs.stat(dir, function (err, stat) {
if (!err) {
return cb(null, stat.isDirectory());
}
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
return cb(err);
});
};

module.exports = function resolve(x, options, callback) {
var cb = callback;
var opts = options || {};
Expand All @@ -29,6 +39,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 extensions = opts.extensions || ['.js'];
Expand All @@ -37,22 +48,35 @@ module.exports = function resolve(x, options, callback) {

opts.paths = opts.paths || [];

if (/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/.test(x)) {
var res = path.resolve(parent, x);
if (x === '..' || x.slice(-1) === '/') res += '/';
if (/\/$/.test(x) && res === parent) {
loadAsDirectory(res, opts.package, onfile);
} else loadAsFile(res, opts.package, onfile);
} else loadNodeModules(x, parent, function (err, n, pkg) {
if (err) cb(err);
else if (core[x]) return cb(null, x);
else if (n) return cb(null, n, pkg);
else {
var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'");
moduleError.code = 'MODULE_NOT_FOUND';
cb(moduleError);
}
});
if (opts.basedir) {
var basedirError = new TypeError('Provided basedir "' + opts.basedir + '" is not a directory');
isDirectory(opts.basedir, function (err, result) {
if (err) return cb(err);
if (!result) { return cb(basedirError); }
validBasedir();
});
} else {
validBasedir();
}
var res;
function validBasedir() {
if (/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/.test(x)) {
res = path.resolve(parent, x);
if (x === '..' || x.slice(-1) === '/') res += '/';
if (/\/$/.test(x) && res === parent) {
loadAsDirectory(res, opts.package, onfile);
} else loadAsFile(res, opts.package, onfile);
} else loadNodeModules(x, parent, function (err, n, pkg) {
if (err) cb(err);
else if (core[x]) return cb(null, x);
else if (n) return cb(null, n, pkg);
else {
var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'");
moduleError.code = 'MODULE_NOT_FOUND';
cb(moduleError);
}
});
}

function onfile(err, m, pkg) {
if (err) cb(err);
Expand Down
15 changes: 15 additions & 0 deletions lib/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,23 @@ var defaultIsFile = function isFile(file) {
return stat.isFile() || stat.isFIFO();
};

var defaultIsDir = function isDirectory(dir) {
try {
var stat = fs.statSync(dir);
} catch (e) {
if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) return false;
throw e;
}
return stat.isDirectory();
};

module.exports = function (x, options) {
if (typeof x !== 'string') {
throw new TypeError('Path must be a string.');
}
var opts = options || {};
var isFile = opts.isFile || defaultIsFile;
var isDirectory = opts.isDirectory || defaultIsDir;
var readFileSync = opts.readFileSync || fs.readFileSync;

var extensions = opts.extensions || ['.js'];
Expand All @@ -28,6 +39,10 @@ module.exports = function (x, options) {

opts.paths = opts.paths || [];

if (opts.basedir && !isDirectory(opts.basedir)) {
throw new TypeError('Provided basedir "' + opts.basedir + '" is not a directory');
}

if (/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/.test(x)) {
var res = path.resolve(parent, x);
if (x === '..' || x.slice(-1) === '/') res += '/';
Expand Down
22 changes: 22 additions & 0 deletions readme.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ options are:

* opts.isFile - function to asynchronously test whether a file exists

* opts.isDirectory - function to asynchronously test whether a file exists and is a directory

* `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 @@ -94,6 +96,15 @@ default `opts` values:
return cb(err);
});
},
isDirectory: function isDirectory(dir, cb) {
fs.stat(dir, function (err, stat) {
if (!err) {
return cb(null, stat.isDirectory());
}
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
return cb(err);
});
},
moduleDirectory: 'node_modules',
preserveSymlinks: false
}
Expand All @@ -114,6 +125,8 @@ options are:

* opts.isFile - function to synchronously test whether a file exists

* opts.isDirectory - function to synchronously test whether a file exists and is a directory

* `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 @@ -149,6 +162,15 @@ default `opts` values:
}
return stat.isFile() || stat.isFIFO();
},
isDirectory: function isDirectory(dir) {
try {
var stat = fs.statSync(dir);
} catch (e) {
if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) return false;
throw e;
}
return stat.isDirectory();
},
moduleDirectory: 'node_modules',
preserveSymlinks: false
}
Expand Down
24 changes: 24 additions & 0 deletions test/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ test('mock', function (t) {
var files = {};
files[path.resolve('/foo/bar/baz.js')] = 'beep';

var dirs = {};
dirs[path.resolve('/foo/bar')] = true;

function opts(basedir) {
return {
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)]);
}
Expand Down Expand Up @@ -49,12 +55,18 @@ test('mock from package', function (t) {
var files = {};
files[path.resolve('/foo/bar/baz.js')] = 'beep';

var dirs = {};
dirs[path.resolve('/foo/bar')] = true;

function opts(basedir) {
return {
basedir: path.resolve(basedir),
isFile: function (file, cb) {
cb(null, Object.prototype.hasOwnProperty.call(files, file));
},
isDirectory: function (dir, cb) {
cb(null, !!dirs[path.resolve(dir)]);
},
'package': { main: 'bar' },
readFile: function (file, cb) {
cb(null, files[file]);
Expand Down Expand Up @@ -94,12 +106,18 @@ test('mock package', function (t) {
main: './baz.js'
});

var dirs = {};
dirs[path.resolve('/foo')] = true;

function opts(basedir) {
return {
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)]);
}
Expand All @@ -122,12 +140,18 @@ test('mock package from package', function (t) {
main: './baz.js'
});

var dirs = {};
dirs[path.resolve('/foo')] = true;

function opts(basedir) {
return {
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)]);
},
'package': { main: 'bar' },
readFile: function (file, cb) {
cb(null, files[path.resolve(file)]);
Expand Down
20 changes: 16 additions & 4 deletions test/mock_sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ test('mock', function (t) {
var files = {};
files[path.resolve('/foo/bar/baz.js')] = 'beep';

var dirs = {};
dirs[path.resolve('/foo/bar')] = true;

function opts(basedir) {
return {
basedir: path.resolve(basedir),
isFile: function (file) {
return Object.prototype.hasOwnProperty.call(files, file);
return Object.prototype.hasOwnProperty.call(files, path.resolve(file));
},
isDirectory: function (dir) {
return !!dirs[path.resolve(dir)];
},
readFileSync: function (file) {
return files[file];
return files[path.resolve(file)];
}
};
}
Expand Down Expand Up @@ -48,14 +54,20 @@ test('mock package', function (t) {
main: './baz.js'
});

var dirs = {};
dirs[path.resolve('/foo')] = true;

function opts(basedir) {
return {
basedir: path.resolve(basedir),
isFile: function (file) {
return Object.prototype.hasOwnProperty.call(files, file);
return Object.prototype.hasOwnProperty.call(files, path.resolve(file));
},
isDirectory: function (dir) {
return !!dirs[path.resolve(dir)];
},
readFileSync: function (file) {
return files[file];
return files[path.resolve(file)];
}
};
}
Expand Down
37 changes: 27 additions & 10 deletions test/node_path.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,66 @@
var fs = require('fs');
var path = require('path');
var test = require('tape');
var resolve = require('../');

test('$NODE_PATH', function (t) {
t.plan(4);

var isDir = function (dir, cb) {
if (dir === '/node_path' || dir === 'node_path/x') {
return cb(null, true);
}
fs.stat(dir, function (err, stat) {
if (!err) {
return cb(null, stat.isDirectory());
}
if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
return cb(err);
});
};

resolve('aaa', {
paths: [
path.join(__dirname, '/node_path/x'),
path.join(__dirname, '/node_path/y')
],
basedir: __dirname
basedir: __dirname,
isDirectory: isDir
}, function (err, res) {
t.equal(res, path.join(__dirname, '/node_path/x/aaa/index.js'));
t.equal(res, path.join(__dirname, '/node_path/x/aaa/index.js'), 'aaa resolves');
});

resolve('bbb', {
paths: [
path.join(__dirname, '/node_path/x'),
path.join(__dirname, '/node_path/y')
],
basedir: __dirname
basedir: __dirname,
isDirectory: isDir
}, function (err, res) {
t.equal(res, path.join(__dirname, '/node_path/y/bbb/index.js'));
t.equal(res, path.join(__dirname, '/node_path/y/bbb/index.js'), 'bbb resolves');
});

resolve('ccc', {
paths: [
path.join(__dirname, '/node_path/x'),
path.join(__dirname, '/node_path/y')
],
basedir: __dirname
basedir: __dirname,
isDirectory: isDir
}, function (err, res) {
t.equal(res, path.join(__dirname, '/node_path/x/ccc/index.js'));
t.equal(res, path.join(__dirname, '/node_path/x/ccc/index.js'), 'ccc resolves');
});

// ensure that relative paths still resolve against the
// regular `node_modules` correctly
// ensure that relative paths still resolve against the regular `node_modules` correctly
resolve('tap', {
paths: [
'node_path'
],
basedir: 'node_path/x'
basedir: 'node_path/x',
isDirectory: isDir
}, function (err, res) {
var root = require('tap/package.json').main;
t.equal(res, path.resolve(__dirname, '..', 'node_modules/tap', root));
t.equal(res, path.resolve(__dirname, '..', 'node_modules/tap', root), 'tap resolves');
});
});
2 changes: 1 addition & 1 deletion test/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ test('not a directory', function (t) {
t.equal(res, undefined);
t.equal(pkg, undefined);

t.equal(err && err.message, 'Cannot find module \'' + path + "' from '" + __filename + "'");
t.equal(err && err.message, 'Provided basedir "' + __filename + '" is not a directory');
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/resolver_sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ test('not a directory', function (t) {
t.fail();
} catch (err) {
t.ok(err, 'a non-directory errors');
t.equal(err && err.message, 'Cannot find module \'' + path + "' from '" + __filename + "'");
t.equal(err && err.message, 'Provided basedir "' + __filename + '" is not a directory');
}
t.end();
});
Expand Down

0 comments on commit 698a3e1

Please sign in to comment.