From aaf2b7249a1675fee105c37d1e679145bee7f50c Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Sun, 7 Apr 2019 06:42:21 -0500 Subject: [PATCH] Use cwd-relative pathname to load config file (#3829) --- lib/cli/config.js | 35 ++++++-- test/integration/config.spec.js | 86 ++++++++++++++++++- .../fixtures/config/mocha-config/index.js | 9 ++ .../fixtures/config/mocha-config/package.json | 14 +++ test/node-unit/cli/config.spec.js | 4 +- 5 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 test/integration/fixtures/config/mocha-config/index.js create mode 100644 test/integration/fixtures/config/mocha-config/package.json diff --git a/lib/cli/config.js b/lib/cli/config.js index 3691f0a079..6fa4e2dbca 100644 --- a/lib/cli/config.js +++ b/lib/cli/config.js @@ -9,9 +9,9 @@ */ const fs = require('fs'); -const findUp = require('find-up'); const path = require('path'); const debug = require('debug')('mocha:cli:config'); +const findUp = require('find-up'); /** * These are the valid config files, in order of precedence; @@ -28,14 +28,31 @@ exports.CONFIG_FILES = [ '.mocharc.json' ]; +const isModuleNotFoundError = err => + err.code !== 'MODULE_NOT_FOUND' || + err.message.indexOf('Cannot find module') !== -1; + /** - * Parsers for various config filetypes. Each accepts a filepath and + * Parsers for various config filetypes. Each accepts a filepath and * returns an object (but could throw) */ const parsers = (exports.parsers = { yaml: filepath => require('js-yaml').safeLoad(fs.readFileSync(filepath, 'utf8')), - js: filepath => require(filepath), + js: filepath => { + const cwdFilepath = path.resolve(filepath); + try { + debug(`parsers: load using cwd-relative path: "${cwdFilepath}"`); + return require(cwdFilepath); + } catch (err) { + if (isModuleNotFoundError(err)) { + debug(`parsers: retry load as module-relative path: "${filepath}"`); + return require(filepath); + } else { + throw err; // rethrow + } + } + }, json: filepath => JSON.parse( require('strip-json-comments')(fs.readFileSync(filepath, 'utf8')) @@ -45,15 +62,18 @@ const parsers = (exports.parsers = { /** * Loads and parses, based on file extension, a config file. * "JSON" files may have comments. + * + * @private * @param {string} filepath - Config file path to load * @returns {Object} Parsed config object - * @private */ exports.loadConfig = filepath => { let config = {}; + debug(`loadConfig: "${filepath}"`); + const ext = path.extname(filepath); try { - if (/\.ya?ml/.test(ext)) { + if (ext === '.yml' || ext === '.yaml') { config = parsers.yaml(filepath); } else if (ext === '.js') { config = parsers.js(filepath); @@ -61,20 +81,21 @@ exports.loadConfig = filepath => { config = parsers.json(filepath); } } catch (err) { - throw new Error(`failed to parse ${filepath}: ${err}`); + throw new Error(`failed to parse config "${filepath}": ${err}`); } return config; }; /** * Find ("find up") config file starting at `cwd` + * * @param {string} [cwd] - Current working directory * @returns {string|null} Filepath to config, if found */ exports.findConfig = (cwd = process.cwd()) => { const filepath = findUp.sync(exports.CONFIG_FILES, {cwd}); if (filepath) { - debug(`found config at ${filepath}`); + debug(`findConfig: found "${filepath}"`); } return filepath; }; diff --git a/test/integration/config.spec.js b/test/integration/config.spec.js index 76a4a718d4..8d81bec9ab 100644 --- a/test/integration/config.spec.js +++ b/test/integration/config.spec.js @@ -1,10 +1,11 @@ 'use strict'; -// this is not a "functional" test; we aren't invoking the mocha executable. -// instead we just avoid test doubles. +// This is not a "functional" test; we aren't invoking the mocha executable. +// Instead we just avoid test doubles. -var loadConfig = require('../../lib/cli/config').loadConfig; +var fs = require('fs'); var path = require('path'); +var loadConfig = require('../../lib/cli/config').loadConfig; describe('config', function() { it('should return the same values for all supported config types', function() { @@ -15,4 +16,83 @@ describe('config', function() { expect(js, 'to equal', json); expect(json, 'to equal', yaml); }); + + describe('when configuring Mocha via a ".js" file', function() { + var projRootDir = path.join(__dirname, '..', '..'); + var configDir = path.join(__dirname, 'fixtures', 'config'); + var json = loadConfig(path.join(configDir, 'mocharc.json')); + + it('should load configuration given absolute path', function() { + var js; + + function _loadConfig() { + js = loadConfig(path.join(configDir, 'mocharc.js')); + } + + expect(_loadConfig, 'not to throw'); + expect(js, 'to equal', json); + }); + + it('should load configuration given cwd-relative path', function() { + var relConfigDir = configDir.substring(projRootDir.length + 1); + var js; + + function _loadConfig() { + js = loadConfig(path.join('.', relConfigDir, 'mocharc.js')); + } + + expect(_loadConfig, 'not to throw'); + expect(js, 'to equal', json); + }); + + // In other words, path does not begin with '/', './', or '../' + describe('when path is neither absolute or relative', function() { + var nodeModulesDir = path.join(projRootDir, 'node_modules'); + var pkgName = 'mocha-config'; + var installedLocally = false; + var symlinkedPkg = false; + + before(function() { + try { + var srcPath = path.join(configDir, pkgName); + var targetPath = path.join(nodeModulesDir, pkgName); + fs.symlinkSync(srcPath, targetPath, 'dir'); + symlinkedPkg = true; + installedLocally = true; + } catch (err) { + if (err.code === 'EEXIST') { + console.log('setup:', 'package already exists in "node_modules"'); + installedLocally = true; + } else { + console.error('setup failed:', err); + } + } + }); + + it('should load configuration given module-relative path', function() { + var js; + + if (!installedLocally) { + return this.skip(); + } + + function _loadConfig() { + js = loadConfig(path.join(pkgName, 'index.js')); + } + + expect(_loadConfig, 'not to throw'); + expect(js, 'to equal', json); + }); + + after(function() { + if (symlinkedPkg) { + try { + fs.unlinkSync(path.join(nodeModulesDir, pkgName)); + } catch (err) { + console.error('teardown failed:', err); + } + } + }); + }); + }); }); diff --git a/test/integration/fixtures/config/mocha-config/index.js b/test/integration/fixtures/config/mocha-config/index.js new file mode 100644 index 0000000000..6bf4e58d03 --- /dev/null +++ b/test/integration/fixtures/config/mocha-config/index.js @@ -0,0 +1,9 @@ +'use strict'; + +// a comment +module.exports = { + require: ['foo', 'bar'], + bail: true, + reporter: 'dot', + slow: 60 +}; diff --git a/test/integration/fixtures/config/mocha-config/package.json b/test/integration/fixtures/config/mocha-config/package.json new file mode 100644 index 0000000000..c6fe4df0f6 --- /dev/null +++ b/test/integration/fixtures/config/mocha-config/package.json @@ -0,0 +1,14 @@ +{ + "name": "mocha-config", + "version": "1.0.0", + "description": "Configure Mocha via package", + "main": "index.js", + "peerDependencies": { + "mocha": "^7.0.0" + }, + "keywords": [ + "mocha", + "config" + ], + "license": "CC0" +} diff --git a/test/node-unit/cli/config.spec.js b/test/node-unit/cli/config.spec.js index eeb4bb2b82..2823cdcd24 100644 --- a/test/node-unit/cli/config.spec.js +++ b/test/node-unit/cli/config.spec.js @@ -82,12 +82,10 @@ describe('cli/config', function() { describe('when supplied a filepath with unsupported extension', function() { beforeEach(function() { - sandbox.stub(parsers, 'yaml').returns(config); sandbox.stub(parsers, 'json').returns(config); - sandbox.stub(parsers, 'js').returns(config); }); - it('should assume JSON', function() { + it('should use the JSON parser', function() { loadConfig('foo.bar'); expect(parsers.json, 'was called'); });