Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use cwd-relative pathname to load config file #3829

Merged
merged 18 commits into from
Apr 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7d7154e
fix(lib/cli/config.js): Add cwd-relative fallback to load config file
plroebuck Mar 12, 2019
49c55cc
test(integration/config.spec.js): Add test for loading from cwd-based…
plroebuck Mar 12, 2019
bcbcb6d
Merge branch 'master' into plroebuck/config-relpath
plroebuck Mar 14, 2019
cf62435
feat(cli/config.js): Change config file load scheme to be cwd-relative
plroebuck Mar 16, 2019
e578813
build(package.json): Add "validate-npm-package-name" package
plroebuck Mar 16, 2019
5dcb8e3
test(cli/config.spec.js): Update unit test
plroebuck Mar 16, 2019
e6a61d1
test(cli/config.spec.js): Changed test title
plroebuck Mar 16, 2019
8bc0df6
fix(cli/config.js): Forgot to commit fix for parsing JSONC
plroebuck Mar 16, 2019
e344846
test(integration/config.spec.js): Add tests for configuring with a pa…
plroebuck Mar 17, 2019
a3b0489
test(integration/config.spec.js): Debug why this doesn't work in CI
plroebuck Mar 18, 2019
00450dd
test(fixtures/config/mocha-config): Add fixture for Mocha configurati…
plroebuck Mar 19, 2019
0af6421
test(integration/config.spec.js): Remove debug and code cleanup
plroebuck Mar 19, 2019
24795ee
refactor(mocha-config/package.json): Update for later
plroebuck Apr 5, 2019
55ce117
test(integration/config.spec.js): Updated integration test
plroebuck Apr 5, 2019
9d46628
test(cli/config.spec.js): Updated CLI test
plroebuck Apr 5, 2019
7251c18
refactor(cli/config.js): Updates per PR review
plroebuck Apr 5, 2019
b51af9d
build(package.json): Revert addition of "validate-npm-package-name" p…
plroebuck Apr 5, 2019
4853993
Revert "build(package.json): Add "validate-npm-package-name" package"
plroebuck Apr 5, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions lib/cli/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 => {
boneskull marked this conversation as resolved.
Show resolved Hide resolved
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'))
Expand All @@ -45,36 +62,40 @@ 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, does this cover both JSON and JS?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, as written, this is a breaking change, because the default becomes JS instead of JSON. require() will choke on a jsonc file.

Copy link
Contributor Author

@plroebuck plroebuck Apr 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require will load both JS and JSON. Our documentation states JS is top priority. It unfortunately also states confusingly JSON is default; why?

It can be argued that the latter refers to standard JSON, not the Mocha config file, so not breaking -- JSONC != JSON where this is concerned. This would only require documentation clarification.

For a real breaking change, Mocha should only allow comments in ".mocharc.jsonc". We should deprecate use of comments in ".mocharc.json" and follow the standard.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our documentation states JS is top priority. It unfortunately also states confusingly JSON is default; why?

"Priority" refers to the order in which configuration files are merged, not the order in which they are searched for. Sounds like that needs clarification.

Mocha should only allow comments in ".mocharc.jsonc". We should deprecate use of comments in ".mocharc.json" and follow the standard.

IMO, this is unnecessarily hostile to the user; we don't lose anything by parsing .json files as JSONC, and we can't do that with require(). Would appreciate other input on this. @craigtaub @cspotcode @juerba etc?

So then my requested change is to revert the order so that the else block calls the json parser, and the previous else if block calls the js parser.

Copy link
Contributor Author

@plroebuck plroebuck Apr 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocha should only allow comments in ".mocharc.jsonc". We should deprecate use of comments in ".mocharc.json" and follow the standard.

IMO, this is unnecessarily hostile to the user; we don't lose anything by parsing .json files as JSONC, and we can't do that with require().

I objected to allowing comments to JSON when the yargs/config PR was first introduced, though I realized the potential need for comments in a config file. Now we have that via JSONC. But I fail to find any value in adding app-specific behavior to a standard file format that renders it unusable anywhere else. Perfect example right here -- we're potentially unable to load a JSON file via require because of our comment feature.

And I wouldn't call it user hostile either. I think it would match user expectation for a JSON file to be able to pass JSONLint and to be requireable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I fail to find any value in adding app-specific behavior to a standard file format [...]

I agree, we should follow standards whenever possible, which is user- and maintainer-friendly. Expectations are clearly defined for both sides.

So yes:

We should deprecate use of comments in ".mocharc.json" and follow the standard.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means we deprecate it instead of break it unless we want to cut v7.

Anybody who has a .mocharc (or specific config file lacking a file extension) containing JSON with comments will encounter an exception.

If this is how we want to go, then I'll live with it, but I can't get behind releasing this as-is in a minor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see the following config-related changes in v7.0:

  1. Remove comment stripping processing for ".mocharc.json".
  2. Change the default for config pathname without known extension from JSON to just be require-able.
  3. Deprecate module-relative file processing fallback (unnecessary).
  4. Allow for configuration by package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means we deprecate it instead of break it unless we want to cut v7.
...but I can't get behind releasing this as-is in a minor.

If "breaking change" was previously unclear, I meant semver-major.
Deprecation sometime between now and then...

Simple change in meantime would be to remove the comments from "example/config/.mocharc.json", which can be done semver-patch.

} else {
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;
};
86 changes: 83 additions & 3 deletions test/integration/config.spec.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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);
}
}
});
});
});
});
9 changes: 9 additions & 0 deletions test/integration/fixtures/config/mocha-config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

// a comment
module.exports = {
require: ['foo', 'bar'],
bail: true,
reporter: 'dot',
slow: 60
};
14 changes: 14 additions & 0 deletions test/integration/fixtures/config/mocha-config/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
4 changes: 1 addition & 3 deletions test/node-unit/cli/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down