From e1728f81e20a30b8588225f44ad3ad836fdf1964 Mon Sep 17 00:00:00 2001
From: Gil Tayar <gil@tayar.org>
Date: Sun, 29 Sep 2019 07:31:55 +0300
Subject: [PATCH] add support for Node.JS native ES modules

---
 .eslintrc.yml                                 |   9 +-
 docs/index.md                                 |  54 ++++++-
 karma.conf.js                                 |   1 +
 lib/cli/config.js                             |   3 +-
 lib/cli/options.js                            |   2 +-
 lib/cli/run-helpers.js                        |  15 +-
 lib/cli/run.js                                |  11 +-
 lib/esm-utils.js                              |  31 ++++
 lib/mocha.js                                  |  59 +++++++-
 lib/mocharc.json                              |   2 +-
 lib/utils.js                                  |  22 +++
 package-lock.json                             | 139 ++++++++++++------
 package-scripts.js                            |   2 +-
 package.json                                  |   3 +-
 test/integration/config.spec.js               |   2 +
 test/integration/esm.spec.js                  |  53 +++++++
 test/integration/fixtures/config/mocharc.cjs  |   9 ++
 test/integration/fixtures/esm/add.mjs         |   3 +
 .../fixtures/esm/esm-failure.fixture.mjs      |   5 +
 .../fixtures/esm/esm-success.fixture.mjs      |   5 +
 .../integration/fixtures/esm/js-folder/add.js |   3 +
 .../esm/js-folder/esm-in-js.fixture.js        |   5 +
 .../fixtures/esm/js-folder/package.json       |   3 +
 test/integration/helpers.js                   |   2 +-
 test/integration/suite.spec.js                |   8 +-
 25 files changed, 372 insertions(+), 79 deletions(-)
 create mode 100644 lib/esm-utils.js
 create mode 100644 test/integration/esm.spec.js
 create mode 100644 test/integration/fixtures/config/mocharc.cjs
 create mode 100644 test/integration/fixtures/esm/add.mjs
 create mode 100644 test/integration/fixtures/esm/esm-failure.fixture.mjs
 create mode 100644 test/integration/fixtures/esm/esm-success.fixture.mjs
 create mode 100644 test/integration/fixtures/esm/js-folder/add.js
 create mode 100644 test/integration/fixtures/esm/js-folder/esm-in-js.fixture.js
 create mode 100644 test/integration/fixtures/esm/js-folder/package.json

diff --git a/.eslintrc.yml b/.eslintrc.yml
index 300a48ef67..96f28ebb9f 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -31,7 +31,14 @@ overrides:
       ecmaVersion: 2017
     env:
       browser: false
-
+  - files:
+      - esm-utils.js
+    parserOptions:
+      ecmaVersion: 2018
+      sourceType: module
+    parser: babel-eslint
+    env:
+      browser: false
   - files:
       - test/**/*.{js,mjs}
     env:
diff --git a/docs/index.md b/docs/index.md
index cf0f226259..1a67af108a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -39,6 +39,7 @@ Mocha is a feature-rich JavaScript test framework running on [Node.js][] and in
 - [mocha.opts file support](#-opts-path)
 - clickable suite titles to filter test execution
 - [node debugger support](#-inspect-inspect-brk-inspect)
+- [node native ES modules support](#nodejs-native-esm-support)
 - [detects multiple calls to `done()`](#detects-multiple-calls-to-done)
 - [use any assertion library you want](#assertions)
 - [extensible reporting, bundled with 9+ reporters](#reporters)
@@ -70,6 +71,7 @@ Mocha is a feature-rich JavaScript test framework running on [Node.js][] and in
 - [Command-Line Usage](#command-line-usage)
 - [Interfaces](#interfaces)
 - [Reporters](#reporters)
+- [Node.JS native ESM support](#nodejs-native-esm-support)
 - [Running Mocha in the Browser](#running-mocha-in-the-browser)
 - [Desktop Notification Support](#desktop-notification-support)
 - [Configuring Mocha (Node.js)](#configuring-mocha-nodejs)
@@ -354,11 +356,11 @@ With its default "BDD"-style interface, Mocha provides the hooks `before()`, `af
 ```js
 describe('hooks', function() {
   before(function() {
-    // runs before all tests in this block
+    // runs once before the first test in this block
   });
 
   after(function() {
-    // runs after all tests in this block
+    // runs once after the last test in this block
   });
 
   beforeEach(function() {
@@ -868,7 +870,8 @@ Configuration
   --package  Path to package.json for config                            [string]
 
 File Handling
-  --extension          File extension(s) to load           [array] [default: js]
+  --extension          File extension(s) to load
+                                           [array] [default: ["js","cjs","mjs"]]
   --file               Specify file(s) to be loaded prior to root suite
                        execution                       [array] [default: (none)]
   --ignore, --exclude  Ignore file(s) or glob pattern(s)
@@ -1538,6 +1541,42 @@ Alias: `HTML`, `html`
 
 **The HTML reporter is not intended for use on the command-line.**
 
+## Node.JS native ESM support
+
+> _New in v7.1.0_
+
+Mocha supports writing your tests as ES modules, and not just using CommonJS. For example:
+
+```js
+// test.mjs
+import {add} from './add.mjs';
+import assert from 'assert';
+
+it('should add to numbers from an es module', () => {
+  assert.equal(add(3, 5), 8);
+});
+```
+
+To enable this you don't need to do anything special. Write your test file as an ES module. In Node.js
+this means either ending the file with a `.mjs` extension, or, if you want to use the regular `.js` extension, by
+adding `"type": "module"` to your `package.json`.
+More information can be found in the [Node.js documentation](https://nodejs.org/api/esm.html).
+
+> Mocha supports ES modules only from Node.js v12.11.0 and above. To enable this in versions smaller than 13.2.0, you need to add `--experimental-modules` when running
+> Mocha. From version 13.2.0 of Node.js, you can use ES modules without any flags.
+
+### Current Limitations
+
+Node.JS native ESM support still has status: **Stability: 1 - Experimental**
+
+- [Watch mode](#-watch-w) does not support ES Module test files
+- [Custom reporters](#third-party-reporters) and [custom interfaces](#interfaces)
+  can only be CommonJS files
+- [Required modules](#-require-module-r-module) can only be CommonJS files
+- [Configuration file](#configuring-mocha-nodejs) can only be a CommonJS file (`mocharc.js` or `mocharc.cjs`)
+- When using module-level mocks via libs like `proxyquire`, `rewiremock` or `rewire`, hold off on using ES modules for your test files
+- Node.JS native ESM support does not work with [esm][npm-esm] module
+
 ## Running Mocha in the Browser
 
 Mocha runs in the browser. Every release of Mocha will have new builds of `./mocha.js` and `./mocha.css` for use in the browser.
@@ -1609,17 +1648,17 @@ mocha.setup({
 
 ### Browser-specific Option(s)
 
-Browser Mocha supports many, but not all [cli options](#command-line-usage).  
+Browser Mocha supports many, but not all [cli options](#command-line-usage).
 To use a [cli option](#command-line-usage) that contains a "-", please convert the option to camel-case, (eg. `check-leaks` to `checkLeaks`).
 
 #### Options that differ slightly from [cli options](#command-line-usage):
 
-`reporter` _{string|constructor}_  
+`reporter` _{string|constructor}_
 You can pass a reporter's name or a custom reporter's constructor. You can find **recommended** reporters for the browser [here](#reporting). It is possible to use [built-in reporters](#reporters) as well. Their employment in browsers is neither recommended nor supported, open the console to see the test results.
 
 #### Options that _only_ function in browser context:
 
-`noHighlighting` _{boolean}_  
+`noHighlighting` _{boolean}_
 If set to `true`, do not attempt to use syntax highlighting on output test code.
 
 ### Reporting
@@ -1701,7 +1740,8 @@ tests as shown below:
 
 In addition to supporting the deprecated [`mocha.opts`](#mochaopts) run-control format, Mocha now supports configuration files, typical of modern command-line tools, in several formats:
 
-- **JavaScript**: Create a `.mocharc.js` in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration.
+- **JavaScript**: Create a `.mocharc.js` (or `mocharc.cjs` when using [`"type"="module"`](#nodejs-native-esm-support) in your `package.json`)
+  in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration.
 - **YAML**: Create a `.mocharc.yaml` (or `.mocharc.yml`) in your project's root directory.
 - **JSON**: Create a `.mocharc.json` (or `.mocharc.jsonc`) in your project's root directory. Comments &mdash; while not valid JSON &mdash; are allowed in this file, and will be ignored by Mocha.
 - **package.json**: Create a `mocha` property in your project's `package.json`.
diff --git a/karma.conf.js b/karma.conf.js
index b0c713856c..ae84d7d830 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -35,6 +35,7 @@ module.exports = config => {
           .ignore('chokidar')
           .ignore('fs')
           .ignore('glob')
+          .ignore('./lib/esm-utils.js')
           .ignore('path')
           .ignore('supports-color')
           .on('bundled', (err, content) => {
diff --git a/lib/cli/config.js b/lib/cli/config.js
index 6fa4e2dbca..24de6cd474 100644
--- a/lib/cli/config.js
+++ b/lib/cli/config.js
@@ -21,6 +21,7 @@ const findUp = require('find-up');
  * @private
  */
 exports.CONFIG_FILES = [
+  '.mocharc.cjs',
   '.mocharc.js',
   '.mocharc.yaml',
   '.mocharc.yml',
@@ -75,7 +76,7 @@ exports.loadConfig = filepath => {
   try {
     if (ext === '.yml' || ext === '.yaml') {
       config = parsers.yaml(filepath);
-    } else if (ext === '.js') {
+    } else if (ext === '.js' || ext === '.cjs') {
       config = parsers.js(filepath);
     } else {
       config = parsers.json(filepath);
diff --git a/lib/cli/options.js b/lib/cli/options.js
index c87552c542..9fbc22ca0b 100644
--- a/lib/cli/options.js
+++ b/lib/cli/options.js
@@ -265,7 +265,7 @@ module.exports.loadPkgRc = loadPkgRc;
  * Priority list:
  *
  * 1. Command-line args
- * 2. RC file (`.mocharc.js`, `.mocharc.ya?ml`, `mocharc.json`)
+ * 2. RC file (`.mocharc.c?js`, `.mocharc.ya?ml`, `mocharc.json`)
  * 3. `mocha` prop of `package.json`
  * 4. `mocha.opts`
  * 5. default configuration (`lib/mocharc.json`)
diff --git a/lib/cli/run-helpers.js b/lib/cli/run-helpers.js
index 6c662a665f..72823c48f6 100644
--- a/lib/cli/run-helpers.js
+++ b/lib/cli/run-helpers.js
@@ -15,8 +15,6 @@ const collectFiles = require('./collect-files');
 
 const cwd = (exports.cwd = process.cwd());
 
-exports.watchRun = watchRun;
-
 /**
  * Exits Mocha when tests + code under test has finished execution (default)
  * @param {number} code - Exit code; typically # of failures
@@ -92,19 +90,21 @@ exports.handleRequires = (requires = []) => {
 };
 
 /**
- * Collect test files and run mocha instance.
+ * Collect and load test files, then run mocha instance.
  * @param {Mocha} mocha - Mocha instance
  * @param {Options} [opts] - Command line options
  * @param {boolean} [opts.exit] - Whether or not to force-exit after tests are complete
  * @param {Object} fileCollectParams - Parameters that control test
  *   file collection. See `lib/cli/collect-files.js`.
- * @returns {Runner}
+ * @returns {Promise<Runner>}
  * @private
  */
-exports.singleRun = (mocha, {exit}, fileCollectParams) => {
+const singleRun = async (mocha, {exit}, fileCollectParams) => {
   const files = collectFiles(fileCollectParams);
   debug('running tests with files', files);
   mocha.files = files;
+
+  await mocha.loadFilesAsync();
   return mocha.run(exit ? exitMocha : exitMochaLater);
 };
 
@@ -113,8 +113,9 @@ exports.singleRun = (mocha, {exit}, fileCollectParams) => {
  * @param {Mocha} mocha - Mocha instance
  * @param {Object} opts - Command line options
  * @private
+ * @returns {Promise}
  */
-exports.runMocha = (mocha, options) => {
+exports.runMocha = async (mocha, options) => {
   const {
     watch = false,
     extension = [],
@@ -140,7 +141,7 @@ exports.runMocha = (mocha, options) => {
   if (watch) {
     watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
   } else {
-    exports.singleRun(mocha, {exit}, fileCollectParams);
+    await singleRun(mocha, {exit}, fileCollectParams);
   }
 };
 
diff --git a/lib/cli/run.js b/lib/cli/run.js
index e4e6ba2791..014227d569 100644
--- a/lib/cli/run.js
+++ b/lib/cli/run.js
@@ -87,7 +87,6 @@ exports.builder = yargs =>
       },
       extension: {
         default: defaults.extension,
-        defaultDescription: 'js',
         description: 'File extension(s) to load',
         group: GROUPS.FILES,
         requiresArg: true,
@@ -299,8 +298,14 @@ exports.builder = yargs =>
     .number(types.number)
     .alias(aliases);
 
-exports.handler = argv => {
+exports.handler = async function(argv) {
   debug('post-yargs config', argv);
   const mocha = new Mocha(argv);
-  runMocha(mocha, argv);
+
+  try {
+    await runMocha(mocha, argv);
+  } catch (err) {
+    console.error('\n' + (err.stack || `Error: ${err.message || err}`));
+    process.exit(1);
+  }
 };
diff --git a/lib/esm-utils.js b/lib/esm-utils.js
new file mode 100644
index 0000000000..df2b5fed0e
--- /dev/null
+++ b/lib/esm-utils.js
@@ -0,0 +1,31 @@
+const url = require('url');
+const path = require('path');
+
+const requireOrImport = async file => {
+  file = path.resolve(file);
+
+  if (path.extname(file) === '.mjs') {
+    return import(url.pathToFileURL(file));
+  }
+  // This is currently the only known way of figuring out whether a file is CJS or ESM.
+  // If Node.js or the community establish a better procedure for that, we can fix this code.
+  // Another option here would be to always use `import()`, as this also supports CJS, but I would be
+  // wary of using it for _all_ existing test files, till ESM is fully stable.
+  try {
+    return require(file);
+  } catch (err) {
+    if (err.code === 'ERR_REQUIRE_ESM') {
+      return import(url.pathToFileURL(file));
+    } else {
+      throw err;
+    }
+  }
+};
+
+exports.loadFilesAsync = async (files, preLoadFunc, postLoadFunc) => {
+  for (const file of files) {
+    preLoadFunc(file);
+    const result = await requireOrImport(file);
+    postLoadFunc(file, result);
+  }
+};
diff --git a/lib/mocha.js b/lib/mocha.js
index 0b43004abc..740e1fd841 100644
--- a/lib/mocha.js
+++ b/lib/mocha.js
@@ -14,6 +14,7 @@ var utils = require('./utils');
 var mocharc = require('./mocharc.json');
 var errors = require('./errors');
 var Suite = require('./suite');
+var esmUtils = utils.supportsEsModules() ? require('./esm-utils') : undefined;
 var createStatsCollector = require('./stats-collector');
 var createInvalidReporterError = errors.createInvalidReporterError;
 var createInvalidInterfaceError = errors.createInvalidInterfaceError;
@@ -290,16 +291,18 @@ Mocha.prototype.ui = function(ui) {
 };
 
 /**
- * Loads `files` prior to execution.
+ * Loads `files` prior to execution. Does not support ES Modules.
  *
  * @description
  * The implementation relies on Node's `require` to execute
  * the test interface functions and will be subject to its cache.
+ * Supports only CommonJS modules. To load ES modules, use Mocha#loadFilesAsync.
  *
  * @private
  * @see {@link Mocha#addFile}
  * @see {@link Mocha#run}
  * @see {@link Mocha#unloadFiles}
+ * @see {@link Mocha#loadFilesAsync}
  * @param {Function} [fn] - Callback invoked upon completion.
  */
 Mocha.prototype.loadFiles = function(fn) {
@@ -314,6 +317,49 @@ Mocha.prototype.loadFiles = function(fn) {
   fn && fn();
 };
 
+/**
+ * Loads `files` prior to execution. Supports Node ES Modules.
+ *
+ * @description
+ * The implementation relies on Node's `require` and `import` to execute
+ * the test interface functions and will be subject to its cache.
+ * Supports both CJS and ESM modules.
+ *
+ * @public
+ * @see {@link Mocha#addFile}
+ * @see {@link Mocha#run}
+ * @see {@link Mocha#unloadFiles}
+ * @returns {Promise}
+ * @example
+ *
+ * // loads ESM (and CJS) test files asynchronously, then runs root suite
+ * mocha.loadFilesAsync()
+ *   .then(() => mocha.run(failures => process.exitCode = failures ? 1 : 0))
+ *   .catch(() => process.exitCode = 1);
+ */
+Mocha.prototype.loadFilesAsync = function() {
+  var self = this;
+  var suite = this.suite;
+  this.loadAsync = true;
+
+  if (!esmUtils) {
+    return new Promise(function(resolve) {
+      self.loadFiles(resolve);
+    });
+  }
+
+  return esmUtils.loadFilesAsync(
+    this.files,
+    function(file) {
+      suite.emit(EVENT_FILE_PRE_REQUIRE, global, file, self);
+    },
+    function(file, resultModule) {
+      suite.emit(EVENT_FILE_REQUIRE, resultModule, file, self);
+      suite.emit(EVENT_FILE_POST_REQUIRE, global, file, self);
+    }
+  );
+};
+
 /**
  * Removes a previously loaded file from Node's `require` cache.
  *
@@ -330,8 +376,9 @@ Mocha.unloadFile = function(file) {
  * Unloads `files` from Node's `require` cache.
  *
  * @description
- * This allows files to be "freshly" reloaded, providing the ability
+ * This allows required files to be "freshly" reloaded, providing the ability
  * to reuse a Mocha instance programmatically.
+ * Note: does not clear ESM module files from the cache
  *
  * <strong>Intended for consumers &mdash; not used internally</strong>
  *
@@ -842,10 +889,14 @@ Object.defineProperty(Mocha.prototype, 'version', {
  * @see {@link Mocha#unloadFiles}
  * @see {@link Runner#run}
  * @param {DoneCB} [fn] - Callback invoked when test execution completed.
- * @return {Runner} runner instance
+ * @returns {Runner} runner instance
+ * @example
+ *
+ * // exit with non-zero status if there were test failures
+ * mocha.run(failures => process.exitCode = failures ? 1 : 0);
  */
 Mocha.prototype.run = function(fn) {
-  if (this.files.length) {
+  if (this.files.length && !this.loadAsync) {
     this.loadFiles();
   }
   var suite = this.suite;
diff --git a/lib/mocharc.json b/lib/mocharc.json
index e6f5d99c5b..1ed9157675 100644
--- a/lib/mocharc.json
+++ b/lib/mocharc.json
@@ -1,6 +1,6 @@
 {
   "diff": true,
-  "extension": ["js"],
+  "extension": ["js", "cjs", "mjs"],
   "opts": "./test/mocha.opts",
   "package": "./package.json",
   "reporter": "spec",
diff --git a/lib/utils.js b/lib/utils.js
index 805d98d463..59b250c20e 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -831,3 +831,25 @@ exports.defineConstants = function(obj) {
   }
   return Object.freeze(exports.createMap(obj));
 };
+
+/**
+ * Whether current version of Node support ES modules
+ *
+ * @description
+ * Versions prior to 10 did not support ES Modules, and version 10 has an old incompatibile version of ESM.
+ * This function returns whether Node.JS has ES Module supports that is compatible with Mocha's needs,
+ * which is version >=12.11.
+ *
+ * @returns {Boolean} whether the current version of Node.JS supports ES Modules in a way that is compatible with Mocha
+ */
+exports.supportsEsModules = function() {
+  if (!process.browser && process.versions && process.versions.node) {
+    var versionFields = process.versions.node.split('.');
+    var major = +versionFields[0];
+    var minor = +versionFields[1];
+
+    if (major >= 13 || (major === 12 && minor >= 11)) {
+      return true;
+    }
+  }
+};
diff --git a/package-lock.json b/package-lock.json
index 05f0471347..86234a92da 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -104,7 +104,7 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
@@ -684,7 +684,7 @@
     "aproba": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
-      "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=",
+      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
       "dev": true
     },
     "arch": {
@@ -1330,7 +1330,7 @@
         },
         "source-map": {
           "version": "0.1.43",
-          "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
           "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
           "dev": true,
           "optional": true,
@@ -1342,7 +1342,7 @@
     },
     "ast-types": {
       "version": "0.7.8",
-      "resolved": "http://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz",
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz",
       "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk=",
       "dev": true
     },
@@ -1470,6 +1470,31 @@
         }
       }
     },
+    "babel-eslint": {
+      "version": "10.0.3",
+      "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz",
+      "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/parser": "^7.0.0",
+        "@babel/traverse": "^7.0.0",
+        "@babel/types": "^7.0.0",
+        "eslint-visitor-keys": "^1.0.0",
+        "resolve": "^1.12.0"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.12.0",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
+          "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
+          "dev": true,
+          "requires": {
+            "path-parse": "^1.0.6"
+          }
+        }
+      }
+    },
     "babel-runtime": {
       "version": "6.26.0",
       "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
@@ -1904,7 +1929,7 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
       "dev": true
     },
     "body-parser": {
@@ -2069,7 +2094,7 @@
     },
     "brfs": {
       "version": "1.6.1",
-      "resolved": "http://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz",
+      "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz",
       "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==",
       "dev": true,
       "requires": {
@@ -2158,7 +2183,7 @@
     "browser-stdout": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
-      "integrity": "sha1-uqVZ7hTO1zRSIputcyZGfGH6vWA="
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
     },
     "browser-sync": {
       "version": "2.26.7",
@@ -2328,7 +2353,7 @@
         },
         "yargs": {
           "version": "6.4.0",
-          "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz",
           "integrity": "sha1-gW4ahm1VmMzzTlWW3c4i2S2kkNQ=",
           "dev": true,
           "requires": {
@@ -2350,7 +2375,7 @@
         },
         "yargs-parser": {
           "version": "4.2.1",
-          "resolved": "http://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
           "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
           "dev": true,
           "requires": {
@@ -3106,7 +3131,7 @@
     "cipher-base": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
-      "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
       "dev": true,
       "requires": {
         "inherits": "^2.0.1",
@@ -3771,7 +3796,7 @@
     "createerror": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/createerror/-/createerror-1.3.0.tgz",
-      "integrity": "sha1-xma9TNa5TjVBU5ZWnUZJ3QzbMxM=",
+      "integrity": "sha512-w9UZUtkaGd8MfS7eMG7Sa0lV5vCJghqQfiOnwNVrPhbZScUp5h0jwYoAF933MKlotlG1JAJOCCT3xU6r+SDKNw==",
       "dev": true
     },
     "cross-env": {
@@ -4615,7 +4640,7 @@
     "diff": {
       "version": "3.5.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
-      "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI="
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA=="
     },
     "diffie-hellman": {
       "version": "5.0.3",
@@ -5744,7 +5769,7 @@
     "evp_bytestokey": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
-      "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
       "dev": true,
       "requires": {
         "md5.js": "^1.3.4",
@@ -6386,13 +6411,13 @@
       "dependencies": {
         "colors": {
           "version": "0.6.2",
-          "resolved": "http://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
           "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=",
           "dev": true
         },
         "commander": {
           "version": "2.1.0",
-          "resolved": "http://registry.npmjs.org/commander/-/commander-2.1.0.tgz",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz",
           "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=",
           "dev": true
         }
@@ -7218,7 +7243,7 @@
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0="
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     "functional-red-black-tree": {
       "version": "1.0.1",
@@ -7287,7 +7312,7 @@
         },
         "strip-ansi": {
           "version": "3.0.1",
-          "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
           "requires": {
@@ -7507,7 +7532,7 @@
     },
     "got": {
       "version": "6.7.1",
-      "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
+      "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
       "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
       "dev": true,
       "requires": {
@@ -7848,7 +7873,7 @@
     "html-encoding-sniffer": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
-      "integrity": "sha1-5w2EuU2lOqN14R/jo1G+ZkLKRvg=",
+      "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
       "dev": true,
       "requires": {
         "whatwg-encoding": "^1.0.1"
@@ -9084,7 +9109,7 @@
     "is-plain-object": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
-      "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
       "dev": true,
       "requires": {
         "isobject": "^3.0.1"
@@ -9853,6 +9878,15 @@
         "strip-ansi": "^4.0.0"
       },
       "dependencies": {
+        "log-symbols": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+          "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.0.1"
+          }
+        },
         "strip-ansi": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
@@ -10074,6 +10108,15 @@
             "is-extglob": "^2.1.1"
           }
         },
+        "log-symbols": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+          "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+          "dev": true,
+          "requires": {
+            "chalk": "^2.0.1"
+          }
+        },
         "p-map": {
           "version": "1.2.0",
           "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
@@ -10237,7 +10280,7 @@
         },
         "strip-ansi": {
           "version": "3.0.1",
-          "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
           "requires": {
@@ -10399,7 +10442,7 @@
         },
         "yargs": {
           "version": "6.6.0",
-          "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
           "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
           "dev": true,
           "requires": {
@@ -10420,7 +10463,7 @@
         },
         "yargs-parser": {
           "version": "4.2.1",
-          "resolved": "http://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz",
           "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=",
           "dev": true,
           "requires": {
@@ -10560,11 +10603,11 @@
       "dev": true
     },
     "log-symbols": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
-      "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
       "requires": {
-        "chalk": "^2.0.1"
+        "chalk": "^2.4.2"
       }
     },
     "log-update": {
@@ -10703,7 +10746,7 @@
     },
     "magic-string": {
       "version": "0.22.5",
-      "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
       "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==",
       "dev": true,
       "requires": {
@@ -10869,7 +10912,7 @@
         },
         "globby": {
           "version": "6.1.0",
-          "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
           "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
           "dev": true,
           "requires": {
@@ -10952,7 +10995,7 @@
     "markdown-toc": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz",
-      "integrity": "sha1-RKFWBoREkDFK/ARESD+eexEiwzk=",
+      "integrity": "sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==",
       "dev": true,
       "requires": {
         "concat-stream": "^1.5.2",
@@ -11262,7 +11305,7 @@
     "miller-rabin": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
-      "integrity": "sha1-8IA1HIZbDcViqEYpZtqlNUPHik0=",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
       "dev": true,
       "requires": {
         "bn.js": "^4.0.0",
@@ -11582,7 +11625,7 @@
     "no-case": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
-      "integrity": "sha1-YLgTOWvjmz8SiKTB7V0efSi0ZKw=",
+      "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
       "dev": true,
       "requires": {
         "lower-case": "^1.1.1"
@@ -11684,7 +11727,7 @@
         },
         "tty-browserify": {
           "version": "0.0.0",
-          "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+          "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
           "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
           "dev": true
         },
@@ -11861,7 +11904,7 @@
     "npmlog": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
-      "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
+      "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
       "dev": true,
       "requires": {
         "are-we-there-yet": "~1.1.2",
@@ -12300,7 +12343,7 @@
         },
         "yargs": {
           "version": "3.32.0",
-          "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
           "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
           "dev": true,
           "requires": {
@@ -12381,7 +12424,7 @@
         },
         "resolve-from": {
           "version": "4.0.0",
-          "resolved": false,
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
           "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
           "dev": true
         },
@@ -12543,7 +12586,7 @@
     },
     "opn": {
       "version": "5.3.0",
-      "resolved": "http://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
       "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==",
       "dev": true,
       "requires": {
@@ -13079,7 +13122,7 @@
         "postcss": {
           "version": "5.2.18",
           "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
-          "integrity": "sha1-ut+hSX1GJE9jkPWLMZgw2RB4U8U=",
+          "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
           "dev": true,
           "requires": {
             "chalk": "^1.1.3",
@@ -13681,7 +13724,7 @@
         "postcss": {
           "version": "5.2.18",
           "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
-          "integrity": "sha1-ut+hSX1GJE9jkPWLMZgw2RB4U8U=",
+          "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
           "dev": true,
           "requires": {
             "chalk": "^1.1.3",
@@ -13907,13 +13950,13 @@
     },
     "pretty-bytes": {
       "version": "4.0.2",
-      "resolved": "http://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
       "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=",
       "dev": true
     },
     "pretty-ms": {
       "version": "0.2.2",
-      "resolved": "http://registry.npmjs.org/pretty-ms/-/pretty-ms-0.2.2.tgz",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-0.2.2.tgz",
       "integrity": "sha1-2oeaaC/zOjcBEEbxPWJ/Z8c7hPY=",
       "dev": true,
       "requires": {
@@ -14115,7 +14158,7 @@
         },
         "yargs": {
           "version": "3.10.0",
-          "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
           "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
           "dev": true,
           "requires": {
@@ -14259,7 +14302,7 @@
       "dependencies": {
         "minimist": {
           "version": "1.2.0",
-          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
@@ -14914,7 +14957,7 @@
     },
     "rgba-regex": {
       "version": "1.0.0",
-      "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
+      "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
       "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
       "dev": true
     },
@@ -15042,7 +15085,7 @@
     "sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-      "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
       "dev": true
     },
     "saxes": {
@@ -17130,7 +17173,7 @@
         },
         "strip-ansi": {
           "version": "0.1.1",
-          "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz",
           "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=",
           "dev": true
         }
@@ -17173,7 +17216,7 @@
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
-      "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
       "dev": true,
       "requires": {
         "os-tmpdir": "~1.0.2"
@@ -18149,7 +18192,7 @@
     "webidl-conversions": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
-      "integrity": "sha1-qFWYCx8LazWbodXZ+zmulB+qY60=",
+      "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
       "dev": true
     },
     "whatwg-encoding": {
diff --git a/package-scripts.js b/package-scripts.js
index 3f9b2d0bd4..a94de98e40 100644
--- a/package-scripts.js
+++ b/package-scripts.js
@@ -23,7 +23,7 @@ function test(testName, mochaParams) {
 module.exports = {
   scripts: {
     build: {
-      script: `browserify -e browser-entry.js --plugin ./scripts/dedefine --ignore './lib/cli/*.js' --ignore 'chokidar' --ignore 'fs' --ignore 'glob' --ignore 'path' --ignore 'supports-color' -o mocha.js`,
+      script: `browserify -e browser-entry.js --plugin ./scripts/dedefine --ignore './lib/cli/*.js' --ignore "./lib/esm-utils.js" --ignore 'chokidar' --ignore 'fs' --ignore 'glob' --ignore 'path' --ignore 'supports-color' -o mocha.js`,
       description: 'Build browser bundle'
     },
     lint: {
diff --git a/package.json b/package.json
index fcebd7f32e..8cad374e94 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
     "growl": "1.10.5",
     "he": "1.2.0",
     "js-yaml": "3.13.1",
-    "log-symbols": "2.2.0",
+    "log-symbols": "3.0.0",
     "minimatch": "3.0.4",
     "mkdirp": "0.5.1",
     "ms": "2.1.1",
@@ -74,6 +74,7 @@
     "acorn": "^7.0.0",
     "assetgraph-builder": "^6.10.1",
     "autoprefixer": "^9.6.0",
+    "babel-eslint": "^10.0.3",
     "browserify": "^16.2.3",
     "browserify-package-json": "^1.0.1",
     "chai": "^4.2.0",
diff --git a/test/integration/config.spec.js b/test/integration/config.spec.js
index 8d81bec9ab..52fa886220 100644
--- a/test/integration/config.spec.js
+++ b/test/integration/config.spec.js
@@ -11,9 +11,11 @@ describe('config', function() {
   it('should return the same values for all supported config types', 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 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(json, 'to equal', yaml);
   });
 
diff --git a/test/integration/esm.spec.js b/test/integration/esm.spec.js
new file mode 100644
index 0000000000..b4cf761f2a
--- /dev/null
+++ b/test/integration/esm.spec.js
@@ -0,0 +1,53 @@
+'use strict';
+var run = require('./helpers').runMochaJSON;
+var utils = require('../../lib/utils');
+var args =
+  +process.versions.node.split('.')[0] >= 13 ? [] : ['--experimental-modules'];
+
+describe('esm', function() {
+  before(function() {
+    if (!utils.supportsEsModules()) this.skip();
+  });
+
+  it('should pass a passing esm test that uses esm', function(done) {
+    var fixture = 'esm/esm-success.fixture.mjs';
+    run(fixture, args, function(err, result) {
+      if (err) {
+        done(err);
+        return;
+      }
+
+      expect(result, 'to have passed test count', 1);
+      done();
+    });
+  });
+
+  it('should fail a failing esm test that uses esm', function(done) {
+    var fixture = 'esm/esm-failure.fixture.mjs';
+    run(fixture, args, function(err, result) {
+      if (err) {
+        done(err);
+        return;
+      }
+
+      expect(result, 'to have failed test count', 1).and(
+        'to have failed test',
+        'should use a function from an esm, and fail'
+      );
+      done();
+    });
+  });
+
+  it('should recognize esm files ending with .js due to package.json type flag', function(done) {
+    var fixture = 'esm/js-folder/esm-in-js.fixture.js';
+    run(fixture, args, function(err, result) {
+      if (err) {
+        done(err);
+        return;
+      }
+
+      expect(result, 'to have passed test count', 1);
+      done();
+    });
+  });
+});
diff --git a/test/integration/fixtures/config/mocharc.cjs b/test/integration/fixtures/config/mocharc.cjs
new file mode 100644
index 0000000000..adec9d68ed
--- /dev/null
+++ b/test/integration/fixtures/config/mocharc.cjs
@@ -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/esm/add.mjs b/test/integration/fixtures/esm/add.mjs
new file mode 100644
index 0000000000..7d658310b0
--- /dev/null
+++ b/test/integration/fixtures/esm/add.mjs
@@ -0,0 +1,3 @@
+export function add(a, b) {
+  return a + b;
+}
diff --git a/test/integration/fixtures/esm/esm-failure.fixture.mjs b/test/integration/fixtures/esm/esm-failure.fixture.mjs
new file mode 100644
index 0000000000..588d8fce7f
--- /dev/null
+++ b/test/integration/fixtures/esm/esm-failure.fixture.mjs
@@ -0,0 +1,5 @@
+import {add} from './add.mjs';
+
+it('should use a function from an esm, and fail', () => {
+  expect(add(3, 5), 'to be', 9);
+});
diff --git a/test/integration/fixtures/esm/esm-success.fixture.mjs b/test/integration/fixtures/esm/esm-success.fixture.mjs
new file mode 100644
index 0000000000..0df926cae7
--- /dev/null
+++ b/test/integration/fixtures/esm/esm-success.fixture.mjs
@@ -0,0 +1,5 @@
+import {add} from './add.mjs';
+
+it('should use a function from an esm', () => {
+  expect(add(3, 5), 'to be', 8);
+});
diff --git a/test/integration/fixtures/esm/js-folder/add.js b/test/integration/fixtures/esm/js-folder/add.js
new file mode 100644
index 0000000000..7d658310b0
--- /dev/null
+++ b/test/integration/fixtures/esm/js-folder/add.js
@@ -0,0 +1,3 @@
+export function add(a, b) {
+  return a + b;
+}
diff --git a/test/integration/fixtures/esm/js-folder/esm-in-js.fixture.js b/test/integration/fixtures/esm/js-folder/esm-in-js.fixture.js
new file mode 100644
index 0000000000..92abf1241e
--- /dev/null
+++ b/test/integration/fixtures/esm/js-folder/esm-in-js.fixture.js
@@ -0,0 +1,5 @@
+import {add} from './add.js';
+
+it('should use a function from an esm module with a js extension', () => {
+  expect(add(3, 5), 'to be', 8);
+});
diff --git a/test/integration/fixtures/esm/js-folder/package.json b/test/integration/fixtures/esm/js-folder/package.json
new file mode 100644
index 0000000000..3dbc1ca591
--- /dev/null
+++ b/test/integration/fixtures/esm/js-folder/package.json
@@ -0,0 +1,3 @@
+{
+  "type": "module"
+}
diff --git a/test/integration/helpers.js b/test/integration/helpers.js
index 0d65e91e6b..6cdf7e93cf 100644
--- a/test/integration/helpers.js
+++ b/test/integration/helpers.js
@@ -299,7 +299,7 @@ function _spawnMochaWithListeners(args, fn, opts) {
 }
 
 function resolveFixturePath(fixture) {
-  if (path.extname(fixture) !== '.js') {
+  if (path.extname(fixture) !== '.js' && path.extname(fixture) !== '.mjs') {
     fixture += '.fixture.js';
   }
   return path.join('test', 'integration', 'fixtures', fixture);
diff --git a/test/integration/suite.spec.js b/test/integration/suite.spec.js
index d3aac74034..ce1171b0a2 100644
--- a/test/integration/suite.spec.js
+++ b/test/integration/suite.spec.js
@@ -12,9 +12,11 @@ describe('suite w/no callback', function() {
         if (err) {
           return done(err);
         }
-        var pattern = new RegExp('TypeError', 'g');
-        var result = res.output.match(pattern) || [];
-        expect(result, 'to have length', 2);
+        expect(
+          res.output,
+          'to match',
+          /no callback was supplied. Supply a callback/
+        );
         done();
       },
       {stdio: 'pipe'}