From 77eba1032720ead7185ec0187ac2709f7eaeb922 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sat, 30 Jan 2021 11:50:09 +0100 Subject: [PATCH] [New] add `readPackage` and `readPackageSync` (#236) Co-authored-by: Simen Bekkhus Co-authored-by: Jordan Harband --- lib/async.js | 33 ++++++++++++++++---- lib/sync.js | 21 ++++++++----- readme.markdown | 31 ++++++++++++++++++- test/mock.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++ test/mock_sync.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 14 deletions(-) diff --git a/lib/async.js b/lib/async.js index 29285079..02e80ac8 100644 --- a/lib/async.js +++ b/lib/async.js @@ -42,6 +42,20 @@ var maybeRealpath = function maybeRealpath(realpath, x, opts, cb) { } }; +var defaultReadPackage = function defaultReadPackage(readFile, pkgfile, cb) { + readFile(pkgfile, function (readFileErr, body) { + if (readFileErr) cb(readFileErr); + else { + try { + var pkg = JSON.parse(body); + cb(null, pkg); + } catch (jsonErr) { + cb(null); + } + } + }); +}; + var getPackageCandidates = function getPackageCandidates(x, start, opts) { var dirs = nodeModulesPaths(start, opts, x); for (var i = 0; i < dirs.length; i++) { @@ -70,6 +84,13 @@ module.exports = function resolve(x, options, callback) { var isDirectory = opts.isDirectory || defaultIsDir; var readFile = opts.readFile || fs.readFile; var realpath = opts.realpath || defaultRealpath; + var readPackage = opts.readPackage || defaultReadPackage; + if (opts.readFile && opts.readPackage) { + var conflictErr = new TypeError('`readFile` and `readPackage` are mutually exclusive.'); + return process.nextTick(function () { + cb(conflictErr); + }); + } var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; @@ -197,9 +218,10 @@ module.exports = function resolve(x, options, callback) { // on err, ex is false if (!ex) return loadpkg(path.dirname(dir), cb); - readFile(pkgfile, function (err, body) { + readPackage(readFile, pkgfile, function (err, pkgParam) { if (err) cb(err); - try { var pkg = JSON.parse(body); } catch (jsonErr) {} + + var pkg = pkgParam; if (pkg && opts.packageFilter) { pkg = opts.packageFilter(pkg, pkgfile); @@ -225,11 +247,10 @@ module.exports = function resolve(x, options, callback) { if (err) return cb(err); if (!ex) return loadAsFile(path.join(x, 'index'), fpkg, cb); - readFile(pkgfile, function (err, body) { + readPackage(readFile, pkgfile, function (err, pkgParam) { if (err) return cb(err); - try { - var pkg = JSON.parse(body); - } catch (jsonErr) {} + + var pkg = pkgParam; if (pkg && opts.packageFilter) { pkg = opts.packageFilter(pkg, pkgfile); diff --git a/lib/sync.js b/lib/sync.js index d5308c92..ef9bd803 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -45,6 +45,14 @@ var maybeRealpathSync = function maybeRealpathSync(realpathSync, x, opts) { return x; }; +var defaultReadPackageSync = function defaultReadPackageSync(readFileSync, pkgfile) { + var body = readFileSync(pkgfile); + try { + var pkg = JSON.parse(body); + return pkg; + } catch (jsonErr) {} +}; + var getPackageCandidates = function getPackageCandidates(x, start, opts) { var dirs = nodeModulesPaths(start, opts, x); for (var i = 0; i < dirs.length; i++) { @@ -63,6 +71,10 @@ module.exports = function resolveSync(x, options) { var readFileSync = opts.readFileSync || fs.readFileSync; var isDirectory = opts.isDirectory || defaultIsDir; var realpathSync = opts.realpathSync || defaultRealpathSync; + var readPackageSync = opts.readPackageSync || defaultReadPackageSync; + if (opts.readFileSync && opts.readPackageSync) { + throw new TypeError('`readFileSync` and `readPackageSync` are mutually exclusive.'); + } var packageIterator = opts.packageIterator; var extensions = opts.extensions || ['.js']; @@ -127,11 +139,7 @@ module.exports = function resolveSync(x, options) { return loadpkg(path.dirname(dir)); } - var body = readFileSync(pkgfile); - - try { - var pkg = JSON.parse(body); - } catch (jsonErr) {} + var pkg = readPackageSync(readFileSync, pkgfile); if (pkg && opts.packageFilter) { // v2 will pass pkgfile @@ -145,8 +153,7 @@ module.exports = function resolveSync(x, options) { var pkgfile = path.join(maybeRealpathSync(realpathSync, x, opts), '/package.json'); if (isFile(pkgfile)) { try { - var body = readFileSync(pkgfile, 'UTF8'); - var pkg = JSON.parse(body); + var pkg = readPackageSync(readFileSync, pkgfile); } catch (e) {} if (pkg && opts.packageFilter) { diff --git a/readme.markdown b/readme.markdown index f742c38d..7c9f2dcd 100644 --- a/readme.markdown +++ b/readme.markdown @@ -71,6 +71,11 @@ options are: * opts.realpath - function to asynchronously resolve a potential symlink to its real path +* `opts.readPackage(readFile, pkgfile, cb)` - function to asynchronously read and parse a package.json file + * readFile - the passed `opts.readFile` or `fs.readFile` if not specified + * pkgfile - path to package.json + * cb - callback + * `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 @@ -137,6 +142,19 @@ default `opts` values: else cb(null, realPathErr ? file : realPath); }); }, + readPackage: function defaultReadPackage(readFile, pkgfile, cb) { + readFile(pkgfile, function (readFileErr, body) { + if (readFileErr) cb(readFileErr); + else { + try { + var pkg = JSON.parse(body); + cb(null, pkg); + } catch (jsonErr) { + cb(null); + } + } + }); + }, moduleDirectory: 'node_modules', preserveSymlinks: true } @@ -155,7 +173,7 @@ options are: * opts.includeCoreModules - set to `false` to exclude node core modules (e.g. `fs`) from the search -* opts.readFile - how to read files synchronously +* opts.readFileSync - how to read files synchronously * opts.isFile - function to synchronously test whether a file exists @@ -163,6 +181,10 @@ options are: * opts.realpathSync - function to synchronously resolve a potential symlink to its real path +* `opts.readPackageSync(readFileSync, pkgfile)` - function to synchronously read and parse a package.json file + * readFileSync - the passed `opts.readFileSync` or `fs.readFileSync` if not specified + * pkgfile - path to package.json + * `opts.packageFilter(pkg, dir)` - transform the parsed package.json contents before looking at the "main" field * pkg - package data * dir - directory for package.json (Note: the second argument will change to "pkgfile" in v2) @@ -232,6 +254,13 @@ default `opts` values: } return file; }, + readPackageSync: function defaultReadPackageSync(readFileSync, pkgfile) { + var body = readFileSync(pkgfile); + try { + var pkg = JSON.parse(body); + return pkg; + } catch (jsonErr) {} + }, moduleDirectory: 'node_modules', preserveSymlinks: true } diff --git a/test/mock.js b/test/mock.js index b9f17fe2..61162754 100644 --- a/test/mock.js +++ b/test/mock.js @@ -237,3 +237,79 @@ test('symlinked', function (t) { t.equal(pkg, undefined); }); }); + +test('readPackage', function (t) { + t.plan(3); + + var files = {}; + files[path.resolve('/foo/node_modules/bar/something-else.js')] = 'beep'; + files[path.resolve('/foo/node_modules/bar/package.json')] = JSON.stringify({ + main: './baz.js' + }); + files[path.resolve('/foo/node_modules/bar/baz.js')] = 'boop'; + + var dirs = {}; + dirs[path.resolve('/foo')] = true; + dirs[path.resolve('/foo/node_modules')] = 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)]); + }, + realpath: function (file, cb) { + cb(null, file); + } + }; + } + + t.test('with readFile', function (st) { + st.plan(3); + + resolve('bar', opts('/foo'), function (err, res, pkg) { + st.error(err); + st.equal(res, path.resolve('/foo/node_modules/bar/baz.js')); + st.equal(pkg && pkg.main, './baz.js'); + }); + }); + + var readPackage = function (readFile, file, cb) { + var barPackage = path.join('bar', 'package.json'); + if (file.slice(-barPackage.length) === barPackage) { + cb(null, { main: './something-else.js' }); + } else { + cb(null, JSON.parse(files[path.resolve(file)])); + } + }; + + t.test('with readPackage', function (st) { + st.plan(3); + + var options = opts('/foo'); + delete options.readFile; + options.readPackage = readPackage; + resolve('bar', options, function (err, res, pkg) { + st.error(err); + st.equal(res, path.resolve('/foo/node_modules/bar/something-else.js')); + st.equal(pkg && pkg.main, './something-else.js'); + }); + }); + + t.test('with readFile and readPackage', function (st) { + st.plan(1); + + var options = opts('/foo'); + options.readPackage = readPackage; + resolve('bar', options, function (err) { + st.throws(function () { throw err; }, TypeError, 'errors when both readFile and readPackage are provided'); + }); + }); +}); diff --git a/test/mock_sync.js b/test/mock_sync.js index fcf81144..0e3a60d4 100644 --- a/test/mock_sync.js +++ b/test/mock_sync.js @@ -139,3 +139,78 @@ test('symlinked', function (t) { path.resolve('/foo/bar/symlinked/baz.js') ); }); + +test('readPackageSync', function (t) { + t.plan(3); + + var files = {}; + files[path.resolve('/foo/node_modules/bar/something-else.js')] = 'beep'; + files[path.resolve('/foo/node_modules/bar/package.json')] = JSON.stringify({ + main: './baz.js' + }); + files[path.resolve('/foo/node_modules/bar/baz.js')] = 'boop'; + + var dirs = {}; + dirs[path.resolve('/foo')] = true; + dirs[path.resolve('/foo/node_modules')] = true; + + function opts(basedir, useReadPackage) { + return { + 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: useReadPackage ? null : function (file) { + return files[path.resolve(file)]; + }, + realpathSync: function (file) { + return file; + } + }; + } + t.test('with readFile', function (st) { + st.plan(1); + + st.equal( + resolve.sync('bar', opts('/foo')), + path.resolve('/foo/node_modules/bar/baz.js') + ); + }); + + var readPackageSync = function (readFileSync, file) { + if (file.indexOf(path.join('bar', 'package.json')) >= 0) { + return { main: './something-else.js' }; + } else { + return JSON.parse(files[path.resolve(file)]); + } + }; + + t.test('with readPackage', function (st) { + st.plan(1); + + var options = opts('/foo'); + delete options.readFileSync; + options.readPackageSync = readPackageSync; + + st.equal( + resolve.sync('bar', options), + path.resolve('/foo/node_modules/bar/something-else.js') + ); + }); + + t.test('with readFile and readPackage', function (st) { + st.plan(1); + + var options = opts('/foo'); + options.readPackageSync = readPackageSync; + st.throws( + function () { resolve.sync('bar', options); }, + TypeError, + 'errors when both readFile and readPackage are provided' + ); + }); +}); +