- Start Date: 2019-10-05
- RFC PR: #43
- Authors: Evan Plaice (@evanplaice)
This Issue
Node now supports ES modules. ESLint configurations that use the CommonJS format (i.e., .eslintrc
, .eslintrc.js
) are not compatible with ES module based packages.
The Impact
This applies to ES module based packages (i.e., packages using "type": "module"
) using a CommonJS configuration (i.e., .eslintrc
, .eslintrc.js
).
The Fix
Node provides an 'escape hatch' via the .cjs
extension. The extension explicitly signals to Node that the file should always evaluate as CommonJS. Support for the .cjs
needs to be added to ESLint, and ESLint users building ES module based packages need to be notified.
ES modules are here. ESLint is ubiquitous in the JS ecosystem. With some minor adjustments, ESLint can be made to be compatible with the new format.
To understand the design outlined here requires some background info into the mechanics by which Node modules are loaded.
- Package - A NPM package (i.e., contains
package.json
) - Module - A JS file
- CJS - A CommonJS module
- ESM - A standard ECMAScript module
- Consumer - A package that uses/depends on this package
- Dependent - Package(s) that this package needs to work
- Boundary - The demarcation between package, consumer, dependent
Historically, NPM packages have come in a variety of different package formats (e.g., IIFE, AMD, UMD, CJS). Prior to an actual spec, NPM settled on CJS as it's de-facto module format. CJS isn't going anywhere, the addition of ESM is additive.
By default, all NPM packages are CJS-based. That means all .js
files will be read as CJS modules. To include an ES module in a CJS package, it must have the .mjs
extension.
If, package.json
contains "type": "module"
then the package is ESM-based. Meaning, all .js
files contained within will be treated as ESM. To include a CJS module in a ESM-based package, it must have the .cjs
extension.
This configuration option does not affect consumers or dependents. Whatever the configuration, it applies to all modules within the package boundary. Assuming packages only ever directly import within their package boundary, there should be no issues with interoperability between CJS/ESM.
A user is creating a new package. They prefer ESM for Browser <-> Node compatibility so they configure their package to be ESM-based.
The user adds eslint
as a devDependency and a typical NPM script to lint the package's contents.
The user defines .eslintrc.js
outlining the rule set they prefer to use within their package.
The user package is ESM-based, all .js
files within are read as ESM.
ESLint is CJS-based, it loads all files within it's package boundary as CJS.
The configuration file is defined as a CJS module (i.e., module.exports
), but has a .js
extension syntax so requiring it throws an error. ESLint, by design reaches across the package boundary to load the user-defined configuration but the user has inadvertently signaled to Node to load it with the wrong module loader.
Add support for the .cjs
file extension
Note: In Node, .cjs
will always load as a CommonJS module, irrespective of package configuration
If a user:
- is building a ESM-based package
- is using a JS-based configuration
They should give the configuration a .cjs
extension.
A quick mention in the FAQ should be suitable to document usage.
None. The change has no negative functionality or performance impacts.
Some developers within the Node ecosystem are strongly opposed to supporting "type": "module"
at all.
tl;dr: Backward compatibility will be unaffected
This change is additive. It associates .cjs
files with JS configurations.
The existing semantics of loading CommonJS configurations for CommonJS-based packages do not change.
In Node, ES module and .cjs
support roll out together.
This change only impacts users who define their package as an ES module.
Existing packages that use the default (i.e., CommonJS module resolution) will be unaffected.
The fencing exists to prevent users from creating dual-mode (i.e., both ESM/CJS) packages as interoperability between the two format can cause issues. In the case of ESLint, loading a simple config file from a user-defined package should not cause any issues or side-effects.
Eating the exception is viable solution.
Instead of using 'import-fresh' to require the config, import it in a cross-format-compatible way using dynamic import()
.
There has been and will continue to be a lot of talk about using this approach as the CJS/ESM interoperability story is fleshed out.
For now it presents too many unknowns.
Why doesn't compatibility w/ ESM-based packages 'just work'?
ESLint reaches across the package boundary to retrieve the user-defined configuration. If the consumer package is ESM-based, the .js
config file will be seen as an ES module. When the ES module loader encounters module.exports
(i.e., the CommonJS module specifier) it will throw an error.
What does this interoperability issue affect?
Only ESM-based packages (i.e., packages with "type": "module"
defined in package.json).
What about 3rd-party linting tools that provide their own built-in rule set for ESLint (ex StandardJS)?
They aren't impacted by this change. 3rd-party modules define both their code and the ESLint rule set used within the same package boundary.
What about support for ES module configurations
The scope of this RFC is limited to only ES module compatibility concerns. If there is a desire to add support for ESM-based configurations, it will need to be addressed in another RFC.