Skip to content

Commit

Permalink
New: Implement DotCompat class (#20)
Browse files Browse the repository at this point in the history
* New: DotCompat class

* More test and functionality

* Dot compat cleanup

* Add tests for extends

* Finish up

* Unignore fixture files

* Update README.md

Co-authored-by: Brandon Mills <[email protected]>

* Update lib/dot-compat.js

Co-authored-by: Brandon Mills <[email protected]>

* Rename 'dot config' to 'flat config'

* Allow functions in ignores array

* Fix overrides behavior

* Fix incorrect translation of language options

* Throw errors when parser/plugin is missing

* Combine ignore patterns

* Fix linting errors

* Fix typo

* Fix failing test

* Pass in pluginEnvironments to translateESLintRC()

* Pass in pluginProcessors to translateESLintRC()

* Code cleanup

* Update README

* fix errors

* Cleanup package.json conflict

* Finish tests

* fix lint errors

Co-authored-by: Brandon Mills <[email protected]>
  • Loading branch information
nzakas and btmills authored Feb 25, 2021
1 parent dac76c0 commit d9a527b
Show file tree
Hide file tree
Showing 15 changed files with 1,440 additions and 52 deletions.
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This repository contains the legacy ESLintRC configuration file format for ESLin

**Note:** This package is not intended for use outside of the ESLint ecosystem. It is ESLint-specific and not intended for use in other programs.

## Usage
## Installation

You can install the package as follows:

Expand All @@ -16,6 +16,46 @@ npm install @eslint/eslintrc --save-dev
yarn add @eslint/eslintrc -D
```

## Future Usage

**Note:** This package is not intended for public use at this time. The following is an example of how it will be used in the future.

The primary class in this package is `FlatCompat`, which is a utility to translate ESLintRC-style configs into flat configs. Here's how you use it inside of your `eslint.config.js` file:

```js
import { FlatCompat } from "@eslint/eslintrc";

const compat = new FlatCompat();

export default [

// mimic ESLintRC-style extends
compat.extends("standard", "example"),

// mimic environments
compat.env({
es2020: true,
node: true
}),

// mimic plugins
compat.plugins("airbnb", "react"),

// translate an entire config
compat.config({
plugins: ["airbnb", "react"],
extends: "standard",
env: {
es2020: true,
node: true
},
rules: {
semi: "error"
}
})
];
```

## License

MIT License
12 changes: 12 additions & 0 deletions conf/eslint-all.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @fileoverview Stub eslint:all config
* @author Nicholas C. Zakas
*/

"use strict";

module.exports = {
settings: {
"eslint:all": true
}
};
12 changes: 12 additions & 0 deletions conf/eslint-recommended.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @fileoverview Stub eslint:recommended config
* @author Nicholas C. Zakas
*/

"use strict";

module.exports = {
settings: {
"eslint:recommended": true
}
};
308 changes: 308 additions & 0 deletions lib/flat-compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
/**
* @fileoverview Compatibility class for flat config.
* @author Nicholas C. Zakas
*/

"use strict";

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

const path = require("path");
const environments = require("../conf/environments");
const createDebug = require("debug");
const { ConfigArrayFactory } = require("./config-array-factory");

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/** @typedef {import("../../shared/types").Environment} Environment */
/** @typedef {import("../../shared/types").Processor} Processor */

const debug = createDebug("eslintrc:flat-compat");
const cafactory = Symbol("cafactory");

/**
* Translates an ESLintRC-style config object into a flag-config-style config
* object.
* @param {Object} eslintrcConfig An ESLintRC-style config object.
* @param {Object} options Options to help translate the config.
* @param {string} options.resolveConfigRelativeTo To the directory to resolve
* configs from.
* @param {string} options.resolvePluginsRelativeTo The directory to resolve
* plugins from.
* @param {ReadOnlyMap<string,Environment>} options.pluginEnvironments A map of plugin environment
* names to objects.
* @param {ReadOnlyMap<string,Processor>} options.pluginProcessors A map of plugin processor
* names to objects.
* @returns {Object} A flag-config-style config object.
*/
function translateESLintRC(eslintrcConfig, {
resolveConfigRelativeTo,
resolvePluginsRelativeTo,
pluginEnvironments,
pluginProcessors
}) {

const flatConfig = {};
const configs = [];
const languageOptions = {};
const linterOptions = {};
const keysToCopy = ["settings", "rules", "processor"];
const languageOptionsKeysToCopy = ["globals", "parser", "parserOptions"];
const linterOptionsKeysToCopy = ["noInlineConfig", "reportUnusedDisableDirectives"];

// check for special settings for eslint:all and eslint:recommended:
if (eslintrcConfig.settings) {
if (eslintrcConfig.settings["eslint:all"] === true) {
return ["eslint:all"];
}

if (eslintrcConfig.settings["eslint:recommended"] === true) {
return ["eslint:recommended"];
}
}

// copy over simple translations
for (const key of keysToCopy) {
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
flatConfig[key] = eslintrcConfig[key];
}
}

// copy over languageOptions
for (const key of languageOptionsKeysToCopy) {
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {

// create the languageOptions key in the flat config
flatConfig.languageOptions = languageOptions;

if (key === "parser") {
debug(`Resolving parser '${languageOptions[key]}' relative to ${resolveConfigRelativeTo}`);

if (eslintrcConfig[key].error) {
throw eslintrcConfig[key].error;
}

languageOptions[key] = eslintrcConfig[key].definition;
continue;
}

// clone any object values that are in the eslintrc config
if (eslintrcConfig[key] && typeof eslintrcConfig[key] === "object") {
languageOptions[key] = {
...eslintrcConfig[key]
};
} else {
languageOptions[key] = eslintrcConfig[key];
}
}
}

// copy over linterOptions
for (const key of linterOptionsKeysToCopy) {
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
flatConfig.linterOptions = linterOptions;
linterOptions[key] = eslintrcConfig[key];
}
}

// move ecmaVersion a level up
if (languageOptions.parserOptions) {

if ("ecmaVersion" in languageOptions.parserOptions) {
languageOptions.ecmaVersion = languageOptions.parserOptions.ecmaVersion;
delete languageOptions.parserOptions.ecmaVersion;
}

if ("sourceType" in languageOptions.parserOptions) {
languageOptions.sourceType = languageOptions.parserOptions.sourceType;
delete languageOptions.parserOptions.sourceType;
}

// check to see if we even need parserOptions anymore and remove it if not
if (Object.keys(languageOptions.parserOptions).length === 0) {
delete languageOptions.parserOptions;
}
}

// overrides
if (eslintrcConfig.criteria) {
flatConfig.files = [absoluteFilePath => eslintrcConfig.criteria.test(absoluteFilePath)];
}

// translate plugins
if (eslintrcConfig.plugins && typeof eslintrcConfig.plugins === "object") {
debug(`Translating plugins: ${eslintrcConfig.plugins}`);

flatConfig.plugins = {};

for (const pluginName of Object.keys(eslintrcConfig.plugins)) {

debug(`Translating plugin: ${pluginName}`);
debug(`Resolving plugin '${pluginName} relative to ${resolvePluginsRelativeTo}`);

const { definition: plugin, error } = eslintrcConfig.plugins[pluginName];

if (error) {
throw error;
}

flatConfig.plugins[pluginName] = plugin;

// create a config for any processors
if (plugin.processors) {
for (const processorName of Object.keys(plugin.processors)) {
if (processorName.startsWith(".")) {
debug(`Assigning processor: ${pluginName}/${processorName}`);

configs.unshift({
files: [`**/*${processorName}`],
processor: pluginProcessors.get(`${pluginName}/${processorName}`)
});
}

}
}
}
}

// translate env - must come after plugins
if (eslintrcConfig.env && typeof eslintrcConfig.env === "object") {
for (const envName of Object.keys(eslintrcConfig.env)) {

// only add environments that are true
if (eslintrcConfig.env[envName]) {
debug(`Translating environment: ${envName}`);

if (environments.has(envName)) {

// built-in environments should be defined first
configs.unshift(...translateESLintRC(environments.get(envName), {
resolveConfigRelativeTo,
resolvePluginsRelativeTo
}));
} else if (pluginEnvironments.has(envName)) {

// if the environment comes from a plugin, it should come after the plugin config
configs.push(...translateESLintRC(pluginEnvironments.get(envName), {
resolveConfigRelativeTo,
resolvePluginsRelativeTo
}));
}
}
}
}

// only add if there are actually keys in the config
if (Object.keys(flatConfig).length > 0) {
configs.push(flatConfig);
}

return configs;
}


//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
* A compatibility class for working with configs.
*/
class FlatCompat {

constructor({
baseDirectory = process.cwd(),
resolvePluginsRelativeTo = baseDirectory
} = {}) {
this.baseDirectory = baseDirectory;
this.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
this[cafactory] = new ConfigArrayFactory({
cwd: baseDirectory,
resolvePluginsRelativeTo,
eslintAllPath: path.resolve(__dirname, "../conf/eslint-all.js"),
eslintRecommendedPath: path.resolve(__dirname, "../conf/eslint-recommended.js")
});
}

/**
* Translates an ESLintRC-style config into a flag-config-style config.
* @param {Object} eslintrcConfig The ESLintRC-style config object.
* @returns {Object} A flag-config-style config object.
*/
config(eslintrcConfig) {
const eslintrcArray = this[cafactory].create(eslintrcConfig, {
basePath: this.baseDirectory
});

const flatArray = [];
let hasIgnorePatterns = false;

eslintrcArray.forEach(configData => {
if (configData.type === "config") {
hasIgnorePatterns = hasIgnorePatterns || configData.ignorePattern;
flatArray.push(...translateESLintRC(configData, {
resolveConfigRelativeTo: path.join(this.baseDirectory, "__placeholder.js"),
resolvePluginsRelativeTo: path.join(this.resolvePluginsRelativeTo, "__placeholder.js"),
pluginEnvironments: eslintrcArray.pluginEnvironments,
pluginProcessors: eslintrcArray.pluginProcessors
}));
}
});

// combine ignorePatterns to emulate ESLintRC behavior better
if (hasIgnorePatterns) {
flatArray.unshift({
ignores: [filePath => {

// Compute the final config for this file.
// This filters config array elements by `files`/`excludedFiles` then merges the elements.
const finalConfig = eslintrcArray.extractConfig(filePath);

// Test the `ignorePattern` properties of the final config.
return Boolean(finalConfig.ignores) && finalConfig.ignores(filePath);
}]
});
}

return flatArray;
}

/**
* Translates the `env` section of an ESLintRC-style config.
* @param {Object} envConfig The `env` section of an ESLintRC config.
* @returns {Object} A flag-config object representing the environments.
*/
env(envConfig) {
return this.config({
env: envConfig
});
}

/**
* Translates the `extends` section of an ESLintRC-style config.
* @param {...string} configsToExtend The names of the configs to load.
* @returns {Object} A flag-config object representing the config.
*/
extends(...configsToExtend) {
return this.config({
extends: configsToExtend
});
}

/**
* Translates the `plugins` section of an ESLintRC-style config.
* @param {...string} plugins The names of the plugins to load.
* @returns {Object} A flag-config object representing the plugins.
*/
plugins(...plugins) {
return this.config({
plugins
});
}
}

exports.FlatCompat = FlatCompat;
Loading

0 comments on commit d9a527b

Please sign in to comment.