chore: merge pull request #8 from LoredDev/3-eslint-flat-config-by-default

refactor/feat: use a more standard ESLint flat configuration object
This commit is contained in:
Guz
2023-07-21 17:38:42 -03:00
committed by GitHub
31 changed files with 306 additions and 384 deletions

View File

@@ -0,0 +1,6 @@
---
"@eslit/config": minor
---
Rewritten most of the package logic, so now it uses a more standard ESLint configuration object structure. All configurations now are separated in scope and presets are created for better convenience when configuring ESLint.
Fixing ESLint flat-config by default [#3](https://github.com/LoredDev/ESLit/issues/3).

View File

@@ -21,5 +21,8 @@
"json",
"jsonc",
"yaml"
],
"cSpell.words": [
"ESLIT"
]
}

View File

@@ -1,7 +1,6 @@
import { defineConfig } from '@eslit/core';
import { configs, defineConfig, presets } from '@eslit/config';
export default defineConfig({
environment: {
node: true,
},
});
export default defineConfig([
...presets.default,
configs.environments.node,
]);

View File

@@ -11,7 +11,7 @@
"license": "MIT",
"type": "module",
"dependencies": {
"@eslit/core": "workspace:*"
"@eslit/config": "workspace:*"
},
"devDependencies": {
"@changesets/cli": "^2.26.2",

56
packages/config/index.d.ts vendored Normal file
View File

@@ -0,0 +1,56 @@
import type { Config, EnvOptions } from './src/types';
import type { Linter } from 'eslint';
/**
* Helper functions for creating/configuring ESLint.
*
* @param config - Array or function returning an array of ESLint's configuration objects array to be used.
* @param environment - An object with environment variables to be declared and used by the configuration.
* @returns The array of ESLint's configuration objects.
*/
export async function defineConfig(config: Config, environment?: EnvOptions): Promise<Linter.FlatConfig[]>;
export const configs: Readonly<{
/**
* **This configuration is necessary to be used before any other one**.
* Common configuration for using ESLit rules overrides.
*/
common: Linter.FlatConfig
/**
* Recommended configuration overrides of ESLit
*/
recommended: Linter.FlatConfig
/**
* Formatting rules/configuration overrides for Javascript and Typescript
*/
formatting: Linter.FlatConfig
/**
* Typescript specific configuration overrides
*/
typescript: Linter.FlatConfig
/**
* Configuration objects for different development environments.
*/
environments: {
/**
* Configuration for Node development environment
*/
node: Linter.FlatConfig
/**
* Configuration for Deno development environment
*/
deno: Linter.FlatConfig
/**
* Configuration for browser development environment
*/
browser: Linter.FlatConfig
}
/**
* JSDoc rules overrides
*/
jsdoc: Linter.FlatConfig
}>;
export const presets: Readonly<{
default: Linter.FlatConfig[]
}>;

View File

@@ -1,5 +1,5 @@
{
"name": "@eslit/core",
"name": "@eslit/config",
"version": "0.1.0",
"description": "",
"main": "index.js",
@@ -22,7 +22,7 @@
"lint": "eslint ."
},
"repository": {
"directory": "packages/core",
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLit"
},

View File

@@ -0,0 +1,36 @@
import tsESLint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import jsdoc from 'eslint-plugin-jsdoc';
/**
* **This configuration is necessary to be used before any other one**.
* Common configuration for using ESLit rules overrides.
*
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
plugins: {
'@typescript-eslint': tsESLint,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'jsdoc': jsdoc,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: process.env.ESLIT_TSCONFIG ?? [
'./{ts,js}config{.eslint,}.json',
'./packages/*/{ts,js}config{.eslint,}.json',
'./apps/*/{ts,js}config{.eslint,}.json',
],
tsconfigRootDir: process.env.ESLIT_ROOT ?? process.cwd(),
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
ecmaVersion: (
/** @type {import('eslint').Linter.ParserOptions['ecmaVersion']} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
() => {return JSON.parse(process.env.ESLIT_ECMASCRIPT ?? '"latest"');}
)(),
},
};
export default config;

View File

@@ -0,0 +1,47 @@
import globals from 'globals';
/**
* Configuration for Node development environment
*
* @type {import('eslint').Linter.FlatConfig}
*/
const node = {
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
languageOptions: {
globals: {
...globals.nodeBuiltin,
},
},
};
/**
* Configuration for Deno development environment
*
* @type {import('eslint').Linter.FlatConfig}
*/
const deno = {
files: ['**/*.js', '**/*.ts'],
languageOptions: {
globals: {
Deno: true,
...globals.browser,
},
},
};
/**
* Configuration for browser development environment
*
* @type {import('eslint').Linter.FlatConfig}
*/
const browser = {
files: ['**/*.js', '**/*.ts'],
languageOptions: {
globals: {
Deno: true,
...globals.browser,
},
},
};
export default { node, deno, browser };

View File

@@ -1,7 +1,7 @@
/**
* Formatting rules/configuration for Javascript and Typescript
* Formatting rules/configuration overrides for Javascript and Typescript
*
* @type {import('../types').ESConfig}
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
@@ -15,7 +15,14 @@ const config = {
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
'indent': 'off',
'@typescript-eslint/indent': ['error', process.env.READABLE_ESLINT_OPTIONS?.indent === 'space' ? 2 : 'tab', {
'@typescript-eslint/indent': ['error', (() => {
/** @type {import('../types').EnvOptions['ESLIT_INDENT']} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const indent = JSON.parse(process.env.ESLINT_INDENT ?? '"tab"');
if (indent === 'space') return 2;
else return indent;
})(), {
SwitchCase: 1,
VariableDeclarator: 1,
outerIIFEBody: 1,
@@ -67,13 +74,13 @@ const config = {
'@typescript-eslint/object-curly-spacing': ['error', 'always'],
'quotes': 'off',
'@typescript-eslint/quotes': ['error', process.env.READABLE_ESLINT_OPTIONS?.quotes ?? 'single'],
'@typescript-eslint/quotes': ['error', process.env.ESLINT_QUOTES ?? 'single'],
'semi': 'off',
'@typescript-eslint/semi': ['error', 'always'],
'space-before-blocks': 'off',
'@typescript-eslint/space-before-blocks': ['error', process.env.READABLE_ESLINT_OPTIONS?.semi ?? 'always'],
'@typescript-eslint/space-before-blocks': ['error', process.env.ESLIT_SEMI ?? 'always'],
'space-before-function-paren': 'off',
'@typescript-eslint/space-before-function-paren': ['error', {

View File

@@ -0,0 +1,8 @@
import formatting from './formatting.js';
import jsdoc from './jsdoc.js';
import typescript from './typescript.js';
import recommended from './recommended.js';
import environments from './environments.js';
import common from './common.js';
export default { formatting, jsdoc, typescript, recommended, environments, common };

View File

@@ -1,11 +1,17 @@
import jsdoc from 'eslint-plugin-jsdoc';
/**
* JSDoc rules overrides
*
* @type {import('../types').ESConfig}
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
rules: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...jsdoc.configs['recommended-typescript-flavor-error'].rules,
'jsdoc/tag-lines': ['error', 'always', {
count: 1,
applyToEndTag: false,

View File

@@ -1,12 +1,19 @@
import tsESlint from '@typescript-eslint/eslint-plugin';
import js from '@eslint/js';
/**
* Common configuration related to language features of Javascript and Typescript
* Recommended configuration overrides of ESLit
*
* @type {import('../types').ESConfig}
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
rules: {
...js.configs.recommended.rules,
...tsESlint.configs.recommended.rules,
...tsESlint.configs['recommended-requiring-type-checking'].rules,
...tsESlint.configs['eslint-recommended'].rules,
...tsESlint.configs.strict.rules,
'@typescript-eslint/ban-ts-comment': ['error', {
'ts-ignore': 'allow-with-description',
}],

View File

@@ -1,9 +1,9 @@
import jsdoc from 'eslint-plugin-jsdoc';
/**
* Typescript specific configuration
* Typescript specific configuration overrides
*
* @type {import('../types').ESConfig}
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.ts', '**/*.cts', '**/*.mts'],
@@ -35,7 +35,9 @@ const config = {
...(
/** @type {() => import('eslint').Linter.RulesRecord} */
() => {
const inferrableTypes = process.env.READABLE_ESLINT_OPTIONS?.inferrableTypes ?? 'never';
/** @type {import('../types').inferrableTypesOptions} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const inferrableTypes = JSON.parse(process.env.ESLIT_INFER_TYPES ?? '"never"');
if (typeof inferrableTypes === 'string') {
return {

View File

@@ -0,0 +1,22 @@
import { eslintrc } from './eslintrc-compact.js';
/**
* @param {import('./types').Config} config
* Array or function returning an array of ESLint's configuration objects array to be used.
*
* @param {import('./types').EnvOptions | undefined} environment
* An object with environment variables to be declared and used by the configuration.
*
* @returns {Promise<import('eslint').Linter.FlatConfig[]>}
* The array of ESLint's configuration objects.
*/
export async function defineConfig(config, environment = {}) {
for (const [key, value] of Object.entries(environment)) {
process.env[key] = JSON.stringify(value);
}
return typeof config === 'function' ? await config(eslintrc) : config;
}
export { default as configs } from './configs/index.js';
export { default as presets } from './presets/index.js';

View File

@@ -0,0 +1,23 @@
import configs from '../configs/index.js';
/**
* @type {Readonly<import('eslint').Linter.FlatConfig[]>}
*/
const preset = [
{
ignores: [
'**/node_modules',
'**/dist',
'**/fixtures',
'**/pnpm-lock.yaml',
'**/yarn.lock',
'**/package-lock.json',
],
},
configs.common,
configs.recommended,
configs.formatting,
configs.jsdoc,
configs.typescript,
];
export default preset;

View File

@@ -0,0 +1,4 @@
import defaultPreset from './default.js';
export default { default: defaultPreset };

56
packages/config/src/types.d.ts vendored Normal file
View File

@@ -0,0 +1,56 @@
import type { FlatCompat } from '@eslint/eslintrc';
import type { Linter } from 'eslint';
type MaybePromise<T> = Promise<T> | T;
export type Config = Linter.FlatConfig[] | ((eslintrc: FlatCompat) => MaybePromise<Linter.FlatConfig[]>);
export interface EnvOptions {
ESLIT_TSCONFIG?: string | string[] | true
ESLIT_ROOT?: string
ESLIT_INDENT?: 'tab' | 'space' | number
ESLIT_ECMASCRIPT?: Linter.ParserOptions['ecmaVersion']
ESLIT_QUOTES?: 'single' | 'double'
ESLIT_SEMI?: 'never' | 'always'
/**
* Typescript's type-checking is able to infer types from parameters.
* So using an explicit `:` type annotation isn't obligatory.
*
* But, **by default in strict mode**, type annotations are always mandated to make
* the code more readable, explicit and robust to changes.
*
* See {@link https://typescript-eslint.io/rules/no-inferrable-types typescript-eslint documentation }
* for more info.
* ---
* **Option: `never`** (default)
* Types are always explicit in Typescript
*
* @example ```ts
// Typescript
const id: number = 10;
const name: string = 'foo';
```
* ---
* **Option: `always`**
* Types are always inferred in Typescript
*
* @example ```ts
// Typescript
const id = 10;
const name = 'foo';
```
*/
ESLIT_INFER_TYPES?: inferrableTypesOptions
[ENV: string]: unknown
}
export type inferrableTypesOptions = [
'never' | 'always',
{
/** @see {@link https://typescript-eslint.io/rules/no-inferrable-types#ignoreparameters} */
parameters?: boolean
/** @see {@link https://typescript-eslint.io/rules/no-inferrable-types#ignoreproperties} */
properties?: boolean
/** @see {@link https://typescript-eslint.io/rules/explicit-function-return-type} */
returnValues?: boolean
},
] | 'never' | 'always';

View File

@@ -1,3 +0,0 @@
import type { Config, ESConfig } from './src/types';
export async function defineConfig(config: Config): Promise<ESConfig[]>;

View File

@@ -1,58 +0,0 @@
import globals from 'globals';
/**
* @param {import('../types').Config['environment']} environment
* Manual configuration of environments, if undefined,
* the function tries to detect the environment automatically
* @returns {import('../types').ESConfig[]}
* ESLint configuration with global variables and environment
*/
export function environments(environment) {
environment ||= {
node:
typeof window === 'undefined' &&
typeof process !== 'undefined' &&
typeof require !== 'function',
deno:
typeof window !== 'undefined' &&
// @ts-expect-error because this package is develop in node
typeof Deno !== 'undefined',
browser:
typeof window !== 'undefined',
};
return [
{
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
languageOptions: {
ecmaVersion: environment.ecmaVersion ?? 'latest',
globals: {
...globals.builtin,
...environment.customGlobals,
},
},
},
{
files: ['**/*.cjs', '**/*.cts'],
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.node,
...globals.commonjs,
},
},
},
{
files: ['**/*.js', '**/*.mjs', '**/*.ts', '**/*.mts'],
languageOptions: {
sourceType: 'module',
globals: {
...(environment.node ? globals.nodeBuiltin : {}),
...(environment.browser || environment.deno ? globals.browser : {}),
...(environment.deno ? { Deno: true } : {}),
},
},
},
];
}

View File

@@ -1,5 +0,0 @@
export { default as common } from './common.js';
export { default as formatting } from './formatting.js';
export { default as jsdoc } from './jsdoc.js';
export { default as typescript } from './typescript.js';
export * from './environments.js';

View File

@@ -1,12 +0,0 @@
import type { Config } from './types';
declare global {
namespace NodeJS {
interface ProcessEnv {
READABLE_ESLINT_STRICT: Config['strict']
READABLE_ESLINT_OPTIONS: Config['options']
}
}
}
export {};

View File

@@ -1,83 +0,0 @@
import { eslintrc } from './eslintrc-compact.js';
import tsESlint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import jsdoc from 'eslint-plugin-jsdoc';
import js from '@eslint/js';
import * as configs from './configs/index.js';
import { getTsConfigs } from './tsconfigs.js';
/**
* @param {import('./types').Config} userConfig
* User configuration
* @returns {Promise<import('./types').ESConfig[]>}
* The complete list of configs for ESLint
*/
export async function defineConfig(userConfig) {
userConfig.strict ??= true;
userConfig.rootDir ??= process.cwd();
userConfig.tsconfig ??= await getTsConfigs(userConfig.rootDir);
process.env.READABLE_ESLINT_STRICT = userConfig.strict;
process.env.READABLE_ESLINT_OPTIONS = {
inferrableTypes: userConfig.strict ? 'always' : 'never',
...userConfig.options,
};
const userOverrides = (typeof userConfig.overrides !== 'function'
? userConfig.overrides
: await userConfig.overrides(eslintrc)) ?? [];
return [
{
ignores: [
'**/node_modules',
'**/dist',
'**/fixtures',
'**/pnpm-lock.yaml',
'**/yarn.lock',
'**/package-lock.json',
],
},
js.configs.recommended,
{
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
plugins: {
'@typescript-eslint': tsESlint,
/**
* @todo
* Fix eslint-plugin-jsdoc type definitions.
* _Typescript should have detected [eslint-plugin-jsdoc.d.ts](./@types/eslint-plugin-jsdoc.d.ts)._
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'jsdoc': jsdoc,
},
languageOptions: {
sourceType: 'module',
parser: tsParser,
parserOptions: {
project: userConfig.tsconfig,
tsconfigRootDir: userConfig.rootDir,
},
},
// See plugins['jsdoc'] for more info on this error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
rules: {
...tsESlint.configs.recommended.rules,
...tsESlint.configs['recommended-requiring-type-checking'].rules,
...tsESlint.configs['eslint-recommended'].rules,
...(userConfig.strict ? tsESlint.configs.strict.rules : null),
// See plugins['jsdoc'] for more info on this error
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...jsdoc.configs['recommended-typescript-flavor-error'].rules,
},
},
configs.common,
configs.formatting,
configs.jsdoc,
configs.typescript,
...configs.environments(userConfig.environment),
...userOverrides,
];
}

View File

@@ -1,87 +0,0 @@
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { join, normalize } from 'node:path';
/** @type {(...path: string[]) => string} */
function toPath(...path) {
return normalize(join(...path));
}
/** @type {(...path: string[]) => boolean} */
function exists(...path) {
return existsSync(toPath(...path));
}
/**
* @param {string} directory what the root directory to detect an workspace/monorepo configuration file
* @returns {Promise<string[]>} list of possible paths of packages' tsconfig.json and jsconfig.json files
*/
async function getMonorepoConfigs(directory) {
/** @type {string[]} */
const paths = [];
if (exists(directory, 'pnpm-workspace.yaml') || exists(directory, 'pnpm-workspace.yml')) {
const YAML = await import('yaml');
const yamlFilePath = exists(directory, 'pnpm-workspace.yaml')
? join(directory, 'pnpm-workspace.yaml')
: join(directory, 'pnpm-workspace.yml');
/** @type {{packages?: string[], [properties: string]: unknown}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const pnpmWorkspaces = YAML.parse(await readFile(yamlFilePath, 'utf-8'));
const files = pnpmWorkspaces.packages?.map(w => [
toPath(directory, w, 'tsconfig.json'),
toPath(directory, w, 'jsconfig.json'),
]).flat() ?? [];
paths.push(...files);
}
else if (exists(directory, 'package.json')) {
/** @type {{workspaces?: string[], [properties: string]: unknown}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const packageJson = JSON.parse(await readFile(join(directory, 'package.json'), 'utf-8'));
const files = packageJson.workspaces?.map(w => [
toPath(directory, w, 'tsconfig.json'),
toPath(directory, w, 'jsconfig.json'),
]).flat() ?? [];
paths.push(...files);
}
return paths;
}
/**
* @param {string} directory what the root directory to work on
* @returns {Promise<string[]>} list of tsconfig.json and jsconfig.json file paths
*/
export async function getTsConfigs(directory) {
const rootTSConfig = exists(directory, 'tsconfig.eslint.json')
? toPath(directory, 'tsconfig.eslint.json')
: exists(directory, 'tsconfig.json')
? toPath(directory, 'tsconfig.json')
: undefined;
const rootJSConfig = exists(directory, 'jsconfig.eslint.json')
? toPath(directory, 'jsconfig.eslint.json')
: exists(directory, 'jsconfig.json')
? toPath(directory, 'jsconfig.json')
: undefined;
const monorepoConfigs = await getMonorepoConfigs(directory);
const paths = /** @type {string[]} */
([rootTSConfig, rootJSConfig, ...monorepoConfigs]).filter(p => p);
return paths;
}

View File

@@ -1,112 +0,0 @@
import type { FlatCompat } from '@eslint/eslintrc';
import type { Linter } from 'eslint';
export type ESConfig = Readonly<Linter.FlatConfig>;
export interface Config {
tsconfig?: string | string[] | true
strict?: boolean
rootDir?: string
/**
* @summary
* Environment and language settings
*
* If no globals/environments are defined, the configuration tries to detect the
* environment using `typeof`. See each option for more explanation
*/
environment?: {
/**
* @summary
* Enables NodeJS environment globals.
*
* **Note:** this does not enables CommonJS globals, if you are using
* CommonJS, use a file ending in `.cjs` or `.cts`
*
* @example // Detects if
* typeof window === 'undefined' &&
* typeof process !== 'undefined' &&
* typeof require !== 'undefined'
*/
node?: boolean
/**
* @summary
* Enables the global `Deno` namespace and browser/web standards globals
*
* @example // Detects if
* typeof window !== 'undefined' &&
* typeof Deno !== 'undefined'
*/
deno?: boolean
/**
* @summary
* Enables browser/web standards globals
*
* @example // Detects if
* typeof window !== 'undefined'
*/
browser?: boolean
/**
* @summary
* What JavaScript (ECMAScript) that will be evaluated
*
* **Defaults to `latest`**
*/
ecmaVersion?: Linter.ParserOptions['ecmaVersion']
/**
* @summary
* User defined globals for edge-cases or if available aren't enough
*
* **Does not overrides previous enabled ones**
*/
customGlobals?: Record<string, boolean>
}
options?: {
indent?: 'tab' | 'space'
quotes?: 'single' | 'double'
semi?: 'never' | 'always'
/**
* Typescript's type-checking is able to infer types from parameters.
* So using an explicit `:` type annotation isn't obligatory.
*
* But, **by default in strict mode**, type annotations are always mandated to make
* the code more readable, explicit and robust to changes.
*
* See {@link https://typescript-eslint.io/rules/no-inferrable-types typescript-eslint documentation }
* for more info.
* ---
* **Option: `never`** (default)
* Types are always explicit in Typescript
*
* @example ```ts
// Typescript
const id: number = 10;
const name: string = 'foo';
```
* ---
* **Option: `always`**
* Types are always inferred in Typescript
*
* @example ```ts
// Typescript
const id = 10;
const name = 'foo';
```
*/
inferrableTypes?: inferrableTypesOptions
}
overrides?:
| Linter.FlatConfig[]
| ((eslintrc: FlatCompat) => Linter.FlatConfig[] | Promise<Linter.FlatConfig[]>)
}
export type inferrableTypesOptions = [
'never' | 'always',
{
/** @see {@link https://typescript-eslint.io/rules/no-inferrable-types#ignoreparameters} */
parameters?: boolean
/** @see {@link https://typescript-eslint.io/rules/no-inferrable-types#ignoreproperties} */
properties?: boolean
/** @see {@link https://typescript-eslint.io/rules/explicit-function-return-type} */
returnValues?: boolean
},
] | 'never' | 'always';

6
pnpm-lock.yaml generated
View File

@@ -8,9 +8,9 @@ importers:
.:
dependencies:
'@eslit/core':
'@eslit/config':
specifier: workspace:*
version: link:packages/core
version: link:packages/config
devDependencies:
'@changesets/cli':
specifier: ^2.26.2
@@ -76,7 +76,7 @@ importers:
specifier: ^4.4.2
version: 4.4.2
packages/core:
packages/config:
dependencies:
'@eslint/eslintrc':
specifier: ^2.1.0