Skip to content

Feature: ESM configuration file #5353

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 29 additions & 12 deletions lib/cli/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,45 @@ const utils = require('../utils');
exports.CONFIG_FILES = [
'.mocharc.cjs',
'.mocharc.js',
'.mocharc.mjs',
'.mocharc.yaml',
'.mocharc.yml',
'.mocharc.jsonc',
'.mocharc.json'
];

/**
* Loads a CommonJS or ESM module, resolving its path if the file path is not
* relative
*
* @param {string} filepath - Module file path to load
* @returns {Object} CommonJS or ESM module
*/
const loadModule = filepath => {
let cwdFilepath;
try {
debug('parsers: load cwd-relative path: "%s"', path.resolve(filepath));
cwdFilepath = require.resolve(path.resolve(filepath)); // evtl. throws
return require(cwdFilepath);
} catch (err) {
if (cwdFilepath) throw err;

debug('parsers: retry load as module-relative path: "%s"', filepath);
return require(filepath);
}
};

/**
* 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').load(fs.readFileSync(filepath, 'utf8')),
js: filepath => {
let cwdFilepath;
try {
debug('parsers: load cwd-relative path: "%s"', path.resolve(filepath));
cwdFilepath = require.resolve(path.resolve(filepath)); // evtl. throws
return require(cwdFilepath);
} catch (err) {
if (cwdFilepath) throw err;
js: loadModule,
mjs: filepath => {
const module = loadModule(filepath);

debug('parsers: retry load as module-relative path: "%s"', filepath);
return require(filepath);
}
return module.default ?? module;
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg Jul 2, 2025

Choose a reason for hiding this comment

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

+1 to me on the module.default ?? module change. I think it might make sense to do it for js too. If the package itself is ESM then users will expect to be able to write export default in a mocha.config.js.

Thinking out loud: that might be considered a breaking change. If someone happen to have a module.exports.default = ... in their mocha.config.js then that would change their behavior. This Sourcegraph search for default in Mocha config files shows that nobody in public seems to be doing this. So I think it's fine.

tl;dr, requesting changes: that the .default ?? should always apply.

},
json: filepath =>
JSON.parse(
Expand All @@ -73,7 +88,9 @@ exports.loadConfig = filepath => {
config = parsers.yaml(filepath);
} else if (ext === '.js' || ext === '.cjs') {
config = parsers.js(filepath);
} else {
} else if (ext === '.mjs') {
config = parsers.mjs(filepath);
}else {
config = parsers.json(filepath);
}
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions test/integration/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ describe('config', function () {
var configDir = path.join(__dirname, 'fixtures', 'config');
var js = loadConfig(path.join(configDir, 'mocharc.js'));
var cjs = loadConfig(path.join(configDir, 'mocharc.cjs'));
var mjs = loadConfig(path.join(configDir, 'mocharc.mjs'));
var json = loadConfig(path.join(configDir, 'mocharc.json'));
var yaml = loadConfig(path.join(configDir, 'mocharc.yaml'));
expect(js, 'to equal', json);
expect(js, 'to equal', cjs);
expect(mjs, 'to equal', cjs);
Copy link
Member

Choose a reason for hiding this comment

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

[Bug] Typo?

Suggested change
expect(mjs, 'to equal', cjs);
expect(mjs, 'to equal', mjs);

expect(json, 'to equal', yaml);
});

Expand Down
9 changes: 9 additions & 0 deletions test/integration/fixtures/config/mocharc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

// a comment
export default {
require: ['foo', 'bar'],
bail: true,
reporter: 'dot',
slow: 60
};
12 changes: 12 additions & 0 deletions test/node-unit/cli/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('cli/config', function () {
sinon.stub(parsers, 'yaml').returns(phonyConfigObject);
sinon.stub(parsers, 'json').returns(phonyConfigObject);
sinon.stub(parsers, 'js').returns(phonyConfigObject);
sinon.stub(parsers, 'mjs').returns(phonyConfigObject);
});

describe('when supplied a filepath with ".yaml" extension', function () {
Expand Down Expand Up @@ -74,6 +75,17 @@ describe('cli/config', function () {
});
});

describe('when supplied a filepath with ".mjs" extension', function () {
const filepath = 'foo.mjs';

it('should use the MJS parser', function () {
loadConfig(filepath);
expect(parsers.mjs, 'to have calls satisfying', [
{args: [filepath], returned: phonyConfigObject}
]).and('was called once');
});
});

describe('when supplied a filepath with ".jsonc" extension', function () {
const filepath = 'foo.jsonc';

Expand Down
Loading