3 Commits

Author SHA1 Message Date
Guz
88e90e3533 Merge pull request #11 from LoredDev/changeset-release/main
🦋 Release branch
2023-08-23 11:13:48 -03:00
Guz
db8bcda886 Merge pull request #9 from LoredDev/changeset-release/main
🦋 Release branch
2023-08-23 11:09:20 -03:00
github-actions[bot]
75f08247b2 ci: 👷🦋 version packages 2023-08-23 14:07:04 +00:00
97 changed files with 1710 additions and 4400 deletions

View File

@@ -2,7 +2,7 @@ root = true
[*]
indent_style = tab
indent_size = 4
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

View File

@@ -13,10 +13,6 @@ inputs:
required: false
type: boolean
default: true
scope:
required: false
type: string
default: '@eslegant'
runs:
using: composite
@@ -25,8 +21,6 @@ runs:
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
registry-url: 'https://registry.npmjs.org'
scope: ${{ inputs.scope }}
- name: Install PNPM
uses: pnpm/action-setup@v2

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 2
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 2

View File

@@ -8,8 +8,8 @@ on:
jobs:
mirror:
if: github.repository == 'loreddev/eslegant'
if: github.repository == 'loreddev/eslit'
uses: loreddev/.github/.github/workflows/mirrors.yml@main
secrets: inherit
with:
codeberg-repo: https://codeberg.org/LoredDev/ESLegant
codeberg-repo: https://codeberg.org/LoredDev/ESLit

View File

@@ -10,7 +10,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
update-release:
if: ${{ github.repository == 'loreddev/eslegant' }}
if: ${{ github.repository == 'loreddev/eslit' }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -20,7 +20,7 @@ jobs:
HUSKY: 0
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0

View File

@@ -11,14 +11,14 @@ on:
jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'loreddev/eslegant' }}
if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'loreddev/eslit' }}
name: Release preview
env:
HUSKY: 0
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -27,10 +27,8 @@ jobs:
- name: Update versions
run: pnpm changeset version --snapshot next
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release packages
run: pnpm run release --tag next
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,10 +10,10 @@ on:
jobs:
generate-changeset:
runs-on: ubuntu-latest
if: github.actor == 'renovate[bot]' && github.repository == 'loreddev/eslegant'
if: github.actor == 'renovate[bot]' && github.repository == 'loreddev/eslit'
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Configure Git

View File

@@ -1,53 +0,0 @@
{
"folders": [
{
"name": "configs/js",
"path": "../configs/js"
},
{
"name": "packages/cli",
"path": "../packages/cli"
},
{
"name": "packages/create-eslegant",
"path": "../packages/create-eslegant"
},
{
"name": "packages/eslegant",
"path": "../packages/eslegant"
},
{
"name": "ROOT",
"path": "../"
}
],
"settings": {
"eslint.workingDirectories": ["./"],
"eslint.experimental.useFlatConfig": true,
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": false,
},
// The following is optional.
// It's better to put under project setting `.vscode/settings.json`
// to avoid conflicts with working with different eslint configs
// that does not support all formats.
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
],
"cSpell.words": [
"eslegant"
]
}
}

View File

@@ -23,11 +23,6 @@
"yaml"
],
"cSpell.words": [
"eslegant",
"estree",
"nanospinner",
"picocolors",
"picomatch",
"sisteransi"
"ESLIT"
]
}

View File

@@ -1,45 +0,0 @@
# @eslit/config
## 0.3.0
### Minor Changes
- Added rules for NodeJS environments, using the eslint-plugin-n and eslint-plugin-security. ([`dcce924`](https://github.com/LoredDev/ESLegant/commit/dcce9242867061235c4396cdaced707dec111c16))
The added configs in the `recommended` object helps preventing issues
such as using deprecated or unsupported APIs and warns about security issues.
Building on top of the recommended configs of the plugins.
In the `strict` object they helps making the code more node-explicit, such as importing
global variables (e.g. `process` needs to be imported from `node:process`).
- Added new ESLint rules inspired by StandardJS. ([`4a1f38f`](https://github.com/LoredDev/ESLegant/commit/4a1f38ff2452f9555203e9ff301fc3b90be6854c))
- New rules structure. ([#18](https://github.com/LoredDev/ESLegant/pull/18))
Now all configs have at least `recommended` and `strict` variants, each having `error`, `warn` and `disabled`/`off` rule levels.
They are exported under the `configs` object, and are separated by purpose.
Presets are now exported under the `presets` object, being a easier way of enabling multiple configs at once.
The package has a more defined purpose, and will be used just for rules/configs related to
JavaScript and TypeScript.
- Configs now export a `default` variation, where rule leves aren't overriden. ([`f4e52b9`](https://github.com/LoredDev/ESLegant/commit/f4e52b991c19f8e1f515383c792effd72838ded8))
- New rules related to possible security vulnerabilities in JavaScript. ([`2e1914c`](https://github.com/LoredDev/ESLegant/commit/2e1914c733b16d5f82b39a672c758a63b77ae282))
Provided by `eslint-plugin-security` and `eslint-plugin-no-secrets`
- Renamed all packages from "eslit" to "eslegant" ([`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803))
### Patch Changes
- Updated dependencies ([`10e5430`](https://github.com/LoredDev/ESLegant/commit/10e543094f4e5d3c9f3c0ea91fd24ad42888a9b0))
- Renamed @eslegant/config to @eslegant/js ([#16](https://github.com/LoredDev/ESLegant/pull/16))
## 0.2.0
### Minor Changes
- 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. ([#8](https://github.com/LoredDev/ESLit/pull/8))
(fixes [#3](https://github.com/loreddev/eslit/issues/3)).

View File

@@ -1,48 +0,0 @@
# Duplicated rules
This is a list of rules that implements the same features and/or end
up fixing the same errors.
- **[`@typescript/member-ordering`][ts/member-ordering], [`@typescript/sort-type-constituents`][ts/sort-type-constituents], [`import/order`][im/order]**:
implements the same functions from [`eslint-plugin-perfectionist`][plugin-perfectionist]
- **[`unicorn/no-for-loop`][un/no-for-loop] and [`@typescript/prefer-for-of`][ts/prefer-for-of]**:
`unicorn/no-for-loop` was used as it also reports when `i`/`index` is used.
- **[`unicorn/prefer-includes`][un/prefer-includes] and [`@typescript/prefer-includes`][ts/prefer-includes]**:
`unicorn/prefer-includes` was used as it not needs type information to run.
- **[`@typescript/prefer-regexp-exec`][ts/prefer-regexp-exec] and [`unicorn/prefer-regexp-test`][un/prefer-regexp-test]**:
`unicorn/prefer-regexp-exec` was used, because it reports on `RegExp#exec()` and `String#match()`
- **[`import/extensions`][im/extensions] and [`n/file-extension-in-import`][n/file-extension-in-import]**:
`import/extensions` was used, as it is not a node-specific issue.
- **[`import/no-extraneous-dependencies`][im/no-extraneous-dependencies], [`n/no-extraneous-require`][n/no-extraneous-require] and [`n/no-extraneous-import`][n/no-extraneous-import]**:
`import/no-extraneous-dependencies` was used, as it is not a node-specific issue.
- **[`import/no-unresolved`][im/no-unresolved], [`n/no-missing-require`][n/no-missing-require] and [`n/no-missing-import`][n/no-missing-import]**:
`import/no-unresolved` was used, as it is not a node-specific issue.
[ts/member-ordering]: <https://typescript-eslint.io/rules/member-ordering>
[ts/prefer-for-of]: <https://typescript-eslint.io/rules/prefer-for-of>
[ts/prefer-includes]: <https://typescript-eslint.io/rules/prefer-includes>
[ts/prefer-regexp-exec]: <https://typescript-eslint.io/rules/prefer-regexp-exec>
[ts/sort-type-constituents]: <https://typescript-eslint.io/rules/sort-type-constituents>
[un/no-for-loop]: <https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-for-loop.md>
[un/prefer-includes]: <https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-includes.md>
[un/prefer-regexp-test]: <https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-regexp-test.md>
[im/order]: <https://github.com/import-js/eslint-plugin-import/blob/maib/docs/rules/order.md>
[im/extensions]: <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/extensions.md>
[im/no-extraneous-dependencies]: <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-extraneous-dependencies.md>
[im/no-unresolved]: <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unresolved.md>
[n/file-extension-in-import]: <https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/file-extension-in-import.md>
[n/no-extraneous-import]: <https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-extraneous-import.md>
[n/no-extraneous-require]: <https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-extraneous-require.md>
[n/no-missing-import]: <https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-import.md>
[n/no-missing-require]: <https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-require.md>
[plugin-perfectionist]: <https://eslint-plugin-perfectionist.azat.io/>

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"exclude": ["./node_modules/**", "./dist/**"],
"include": ["src/index.d.ts", "./src/**/*.ts", "./src/**/*.js", "src/index.js"],
}

View File

@@ -1,24 +0,0 @@
/**
* @file
* Type declaration for the `eslint-plugin-compat` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { ESLint } from 'eslint';
/**
* @summary Check the browser compatibility of your code.
*
* ---
* **Note:** Types in this project where overridden to be compatible with
* ESLint new flat config types. ESlint already has backwards compatibility
* for plugins not created in the new flat config.
* @see {@link https://www.npmjs.com/package/eslint-plugin-compat npm package}
*/
declare module 'eslint-plugin-compat' {
declare const plugin: ESLint.Plugin;
export default plugin;
}

View File

@@ -1,35 +0,0 @@
/**
* @file
* Type declaration for the `eslint-plugin-i` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
// eslint-disable-next-line unicorn/prevent-abbreviations
import type { ESLint, Linter } from 'eslint';
/**
* @summary ESLint plugin with rules that help validate proper imports.
*
* ---
* **Note:** Types in this project where overridden to be compatible with
* ESLint new flat config types. ESlint already has backwards compatibility
* for plugins not created in the new flat config.
* @see {@link https://www.npmjs.com/package/eslint-plugin-import npm package}
*/
declare module 'eslint-plugin-i' {
interface importEslintPlugin extends ESLint.Plugin {
configs: {
recommended: {
rules: Linter.RulesRecord,
},
typescript: {
rules: Linter.RulesRecord,
},
},
}
declare const plugin: importEslintPlugin;
export default plugin;
}

View File

@@ -1,34 +0,0 @@
/**
* @file
* Type declaration for the `eslint-plugin-jsdoc` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { ESLint } from 'eslint';
/**
* @summary JSDoc specific linting rules for ESLint.
*
* ---
* **Note:** Types in this project where overridden to be compatible with
* ESLint new flat config types. ESlint already has backwards compatibility
* for plugins not created in the new flat config.
* @see {@link https://www.npmjs.org/package/eslint-plugin-jsdoc npm package}
*/
declare module 'eslint-plugin-jsdoc' {
// eslint-disable-next-line unicorn/prevent-abbreviations
interface jsDocESlintPlugin extends ESLint.Plugin {
configs: ESLint.Plugin['configs'] & {
recommended: ESLint.ConfigData,
'recommended-error': ESLint.ConfigData,
'recommended-typescript': ESLint.ConfigData,
'recommended-typescript-error': ESLint.ConfigData,
'recommended-typescript-flavor': ESLint.ConfigData,
'recommended-typescript-flavor-error': ESLint.ConfigData,
},
}
declare const plugin: jsDocESlintPlugin;
export default plugin;
}

View File

@@ -1,24 +0,0 @@
/**
* @file
* Type declaration for the `eslint-plugin-n` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { ESLint } from 'eslint';
/**
* @summary Additional ESLint's rules for Node.js.
*
* ---
* **Note:** Types in this project where overridden to be compatible with
* ESLint new flat config types. ESlint already has backwards compatibility
* for plugins not created in the new flat config.
* @see {@link https://www.npmjs.com/package/eslint-plugin-n npm package}
*/
declare module 'eslint-plugin-n' {
declare const plugin: ESLint.Plugin;
export default plugin;
}

View File

@@ -1,24 +0,0 @@
/**
* @file
* Type declaration for the `eslint-plugin-no-secrets` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { ESLint } from 'eslint';
/**
* @summary An eslint plugin to find strings that might be secrets/credentials.
*
* ---
* **Note:** Types in this project where overridden to be compatible with
* ESLint new flat config types. ESlint already has backwards compatibility
* for plugins not created in the new flat config.
* @see {@link https://www.npmjs.com/package/eslint-plugin-no-secrets npm package}
*/
declare module 'eslint-plugin-no-secrets' {
declare const plugin: ESLint.Plugin;
export default plugin;
}

View File

@@ -1,24 +0,0 @@
/**
* @file
* Type declaration for the `eslint-plugin-security` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { ESLint } from 'eslint';
/**
* @summary ESLint rules for Node Security.
*
* ---
* **Note:** Types in this project where overridden to be compatible with
* ESLint new flat config types. ESlint already has backwards compatibility
* for plugins not created in the new flat config.
* @see {@link https://www.npmjs.com/package/eslint-plugin-security npm package}
*/
declare module 'eslint-plugin-security' {
declare const plugin: ESLint.Plugin;
export default plugin;
}

View File

@@ -1,18 +0,0 @@
/**
* @file
* Type declaration for the `globals` package in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
declare module 'globals' {
const globals: {
browser: { [rule: string]: boolean, },
builtin: { [rule: string]: boolean, },
commonjs: { [rule: string]: boolean, },
node: { [rule: string]: boolean, },
nodeBuiltin: { [rule: string]: boolean, },
};
export default globals;
}

View File

@@ -1,64 +0,0 @@
/**
* @file
* Configuration object that adds necessary plugins for the other objects.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import process from 'node:process';
import tsESLint from '@typescript-eslint/eslint-plugin';
import securityPlugin from 'eslint-plugin-security';
import unicornPlugin from 'eslint-plugin-unicorn';
// @ts-expect-error because the package doesn't export correct types
import tsParser from '@typescript-eslint/parser';
import jsdocPlugin from 'eslint-plugin-jsdoc';
import importPlugin from 'eslint-plugin-i';
import globals from 'globals';
// eslint-disable-next-line import/no-relative-parent-imports
import { FILES } from '../constants.js';
/** @type {import('eslint').Linter.FlatConfig} */
const config = {
files: FILES,
languageOptions: {
globals: {
...globals.builtin,
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parser: tsParser,
parserOptions: {
project: process.env.ESLEGANT_TSCONFIG ?? [
'./{ts,js}config{.eslint,}.json',
'./*/{ts,js}config{.eslint,}.json',
'./*/*/{ts,js}config{.eslint,}.json',
],
tsconfigRootDir: process.env.ESLEGANT_ROOT ?? process.cwd(),
},
},
plugins: {
'@typescript-eslint': tsESLint,
'import': importPlugin,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'jsdoc': jsdocPlugin,
// @ts-expect-error because eslint-plugin-security doesn't export correct types
'security': securityPlugin,
// @ts-expect-error because eslint-plugin-unicorn doesn't export correct types
'unicorn': unicornPlugin,
},
settings: {
'import/extensions': FILES,
'import/parsers': {
'@typescript-eslint/parser': FILES,
},
'import/resolver': {
node: true,
typescript: true,
},
'jsdoc/mode': 'typescript',
},
};
export default config;

View File

@@ -1,56 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects that helps document your code.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import { createVariations } from '../lib/rule-variations.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: FILES,
rules: {
...{}, // Plugin: eslint-plugin-jsdoc
'jsdoc/match-description': 'error',
'jsdoc/require-description-complete-sentence': 'error',
'jsdoc/require-hyphen-before-param-description': ['error', 'always'],
'jsdoc/require-param-description': 'error',
'jsdoc/require-property-description': 'error',
'jsdoc/require-returns-check': 'error',
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // Plugin: eslint-plugin-jsdoc
'jsdoc/require-description': 'error',
'jsdoc/require-file-overview': ['error', { tags: {
author: {
mustExist: true,
},
copyright: {
initialCommentsOnly: true,
},
file: {
initialCommentsOnly: true,
mustExist: true,
preventDuplicates: true,
},
license: {
initialCommentsOnly: true,
mustExist: true,
preventDuplicates: true,
},
} }],
},
});
const documentation = { recommended, strict };
export default documentation;

View File

@@ -1,51 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects for the browser environment.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import compatPlugin from 'eslint-plugin-compat';
import globals from 'globals';
import { createVariations } from '../../lib/rule-variations.js';
import { FILES } from '../../constants.js';
const recommended = createVariations({
files: FILES,
languageOptions: {
globals: {
...globals.browser,
},
},
plugins: {
compat: compatPlugin,
},
rules: {
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/prefer-add-event-listener': 'error',
'unicorn/prefer-dom-node-append': 'error',
'unicorn/prefer-dom-node-dataset': 'error',
'unicorn/prefer-dom-node-remove': 'error',
'unicorn/prefer-dom-node-text-content': 'error',
'unicorn/prefer-keyboard-event-key': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
'unicorn/prefer-query-selector': 'error',
...{}, // Plugin: eslint-plugin-compat
'compat/compat': 'error',
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
},
});
const browser = { recommended, strict };
export default browser;

View File

@@ -1,12 +0,0 @@
/**
* @file
* Configuration object for the all the environment.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import browser from './browser.js';
import node from './node.js';
const environments = { browser, node };
export default environments;

View File

@@ -1,116 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects for the NodeJS environment.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import nodePlugin from 'eslint-plugin-n';
import globals from 'globals';
import { createVariations } from '../../lib/rule-variations.js';
import { FILES } from '../../constants.js';
const commonjs = createVariations({
files: ['**/*.cts', '**/*.cjs'],
languageOptions: {
globals: {
...globals.nodeBuiltin,
...globals.commonjs,
},
},
plugins: {
// @ts-expect-error because types are not defined in 'eslint-plugin-n'
n: nodePlugin,
},
rules: {
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-var-requires': 'off',
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/prefer-module': 'off',
...{}, // Plugin: eslint-plugin-import
'import/no-commonjs': 'off',
...{}, // Plugin: eslint-plugin-n
'n/global-require': 'error',
'n/no-exports-assign': 'error',
...{}, // Plugin: eslint-plugin-security
'security/detect-non-literal-require': 'error',
},
});
const recommended = createVariations({
files: FILES,
languageOptions: {
globals: {
...globals.nodeBuiltin,
},
},
plugins: {
// @ts-expect-error because types are not defined in 'eslint-plugin-n'
n: nodePlugin,
},
rules: {
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/prefer-node-protocol': 'error',
...{}, // Plugin: eslint-plugin-import
'import/no-dynamic-require': 'error',
...{}, // Plugin: eslint-plugin-n
'n/no-deprecated-api': 'error',
'n/no-process-exit': 'error',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'error',
'n/no-unpublished-require': 'error',
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'error',
'n/process-exit-as-throw': 'error',
'n/shebang': 'error',
...{}, // Plugin: eslint-plugin-security
'security/detect-buffer-noassert': 'warn',
'security/detect-child-process': 'warn',
'security/detect-new-buffer': 'warn',
'security/detect-no-csrf-before-method-override': 'warn',
'security/detect-non-literal-fs-filename': 'warn',
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // Plugin: eslint-plugin-n
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/prefer-global/buffer': ['error', 'never'],
'n/prefer-global/console': ['error', 'always'],
'n/prefer-global/process': ['error', 'never'],
'n/prefer-global/text-decoder': ['error', 'always'],
'n/prefer-global/text-encoder': ['error', 'always'],
'n/prefer-global/url': ['error', 'always'],
'n/prefer-global/url-search-params': ['error', 'always'],
'n/prefer-promises/dns': 'error',
'n/prefer-promises/fs': 'error',
...{}, // Plugin: eslint-plugin-security
'security/detect-buffer-noassert': 'error',
'security/detect-child-process': 'error',
'security/detect-new-buffer': 'error',
'security/detect-no-csrf-before-method-override': 'error',
'security/detect-non-literal-fs-filename': 'warn',
},
});
const node = { commonjs, recommended, strict };
export default node;

View File

@@ -1,192 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects for code formatting and style in JavaScript and TypeScript.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import perfectionistPlugin from 'eslint-plugin-perfectionist';
import { createVariations } from '../lib/rule-variations.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: FILES,
plugins: {
// @ts-expect-error because plugin doesn't export correct type
perfectionist: perfectionistPlugin,
},
rules: {
...{}, // ESLint rules
'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'comma-style': 'error',
'curly': ['error', 'multi-or-nest', 'consistent'],
'dot-location': ['error', 'property'],
'eol-last': 'error',
'generator-star-spacing': ['error', 'before'],
'no-mixed-spaces-and-tabs': 'error',
'no-multi-spaces': 'error',
'no-whitespace-before-property': 'error',
'padded-blocks': ['error', 'never'],
'rest-spread-spacing': 'error',
'semi-spacing': 'error',
'space-in-parens': 'error',
'space-unary-ops': 'error',
'spaced-comment': ['error', 'always', {
block: {
balanced: true,
exceptions: ['*'],
markers: ['!'],
},
line: {
exceptions: ['/', '#'],
markers: ['/'],
},
}],
'template-curly-spacing': ['error', 'never'],
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/block-spacing': ['error', 'always'],
'@typescript-eslint/brace-style': ['error', 'stroustrup', {
allowSingleLine: true,
}],
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/comma-spacing': 'error',
'@typescript-eslint/dot-notation': 'error',
'@typescript-eslint/func-call-spacing': 'error',
'@typescript-eslint/indent': ['error', 'tab', { ArrayExpression: 1,
CallExpression: { arguments: 1 },
FunctionDeclaration: { body: 1,
parameters: 1 },
FunctionExpression: { body: 1,
parameters: 1 },
ImportDeclaration: 1,
MemberExpression: 1,
ObjectExpression: 1,
SwitchCase: 1,
VariableDeclarator: 1,
flatTernaryExpressions: false,
ignoreComments: false, ignoredNodes: [
'TemplateLiteral *',
'JSXElement',
'JSXElement > *',
'JSXAttribute',
'JSXIdentifier',
'JSXNamespacedName',
'JSXMemberExpression',
'JSXSpreadAttribute',
'JSXExpressionContainer',
'JSXOpeningElement',
'Element',
'JSXFragment',
'JSXOpeningFragment',
'JSXClosingFragment',
'JSXText',
'JSXEmptyExpression',
'JSXSpreadChild',
'TSTypeParameterInstantiation',
'FunctionExpression > .params[decorators.length > 0]',
// eslint-disable-next-line max-len
'FunctionExpression > .params > :matches(Decorator, :not(:first-child))',
// eslint-disable-next-line max-len
'ClassBody.body > PropertyDefinition[decorators.length > 0] > .key',
], offsetTernaryExpressions: true, outerIIFEBody: 1,
}],
'@typescript-eslint/key-spacing': ['error', {
afterColon: true,
beforeColon: false,
}],
'@typescript-eslint/keyword-spacing': ['error', {
after: true,
before: true,
}],
'@typescript-eslint/lines-between-class-members': ['error'],
'@typescript-eslint/member-delimiter-style': ['error', {
multiline: { delimiter: 'comma', requireLast: true },
singleline: { delimiter: 'comma', requireLast: true },
}],
'@typescript-eslint/no-extra-parens': ['error', 'functions'],
'@typescript-eslint/object-curly-spacing': ['error', 'always'],
'@typescript-eslint/quotes': ['error', 'single'],
'@typescript-eslint/semi': ['error', 'always'],
'@typescript-eslint/space-before-blocks': ['error', 'always'],
'@typescript-eslint/space-before-function-paren': ['error', {
anonymous: 'always',
asyncArrow: 'always',
named: 'never',
}],
'@typescript-eslint/space-infix-ops': 'error',
'@typescript-eslint/type-annotation-spacing': ['error', {
after: true,
before: false,
overrides: {
arrow: { after: true, before: true },
},
}],
'block-spacing': 'off',
'brace-style': 'off',
'comma-dangle': 'off',
'comma-spacing': 'off',
'dot-notation': 'off',
'func-call-spacing': 'off',
'indent': 'off',
'key-spacing': 'off',
'keyword-spacing': 'off',
'lines-between-class-members': 'off',
'object-curly-spacing': 'off',
'quotes': 'off',
'semi': 'off',
'space-before-blocks': 'off',
'space-before-function-paren': 'off',
'space-infix-ops': 'off',
...{}, // Plugin: eslint-plugin-import
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'import/exports-last': 'error',
'import/first': 'error',
'import/group-exports': 'error',
...{}, // Plugin: eslint-plugin-perfectionist
'perfectionist/sort-array-includes': ['error', { type: 'natural' }],
'perfectionist/sort-classes': ['error', { type: 'natural' }],
'perfectionist/sort-enums': ['error', { type: 'natural' }],
'perfectionist/sort-exports': ['error', { type: 'line-length' }],
'perfectionist/sort-imports': ['error', {
groups: [
'type',
'builtin',
'external',
'internal-type',
'internal',
['parent-type', 'sibling-type', 'index-type'],
['parent', 'sibling', 'index'],
'object',
'unknown',
],
order: 'desc',
type: 'line-length',
}],
'perfectionist/sort-interfaces': ['error', { type: 'natural' }],
'perfectionist/sort-jsx-props': ['error', { type: 'natural' }],
'perfectionist/sort-map-elements': ['error', { type: 'natural' }],
'perfectionist/sort-named-exports': ['error', { type: 'natural' }],
'perfectionist/sort-named-imports': ['error', { type: 'natural' }],
'perfectionist/sort-object-types': ['error', { type: 'natural' }],
'perfectionist/sort-objects': ['error', { type: 'natural' }],
'perfectionist/sort-union-types': ['error', { type: 'natural' }],
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
},
});
const formatting = { recommended, strict };
export default formatting;

View File

@@ -1,322 +0,0 @@
/**
* @file
* Type declarations and documentations for all configs objects.
* All these types are public to the user.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { Linter } from 'eslint';
interface ConfigVariations {
/**
* @summary
* Enable rules with the predefined levels of the package.
* @description
* Most of the rules in ESLegant are on `error` level. This
* was preferred so it is harder to ignore them. But it has
* some small exceptions where rules will be at `warn` level,
* being more as a "reminder" than a actual rule.
*
* If you want to **every** rule in the config to have an
* `error` or `warn` level, you can use the other variants.
*/
default: Linter.FlatConfig,
/**
* @description
* Enable all rules with `error` level.
*/
error: Linter.FlatConfig,
/**
* @description
* Disable all rules in this config.
*/
off: Linter.FlatConfig,
/**
* @description
* Enable all rules with `warn` level.
*/
warn: Linter.FlatConfig,
}
const configs: Readonly<{
/**
* @summary
* **Add this configuration at the start of your array**.
* @description
* This config adds necessary plugins and configuration for ESLint
* to use in the other configs **This should always be in the top
* of the configuration array**.
*/
core: Linter.FlatConfig,
/**
* @description
* This configuration helps you document your code with JSDoc.
*/
documentation: {
/**
* @summary
* Recommended rules from the original plugins.
* @description
* Less strict rules, that nots enforces documentation on
* every aspect/function of your code. Recommended rules for
* projects in prototyping or starting phases, or apps that
* doesn't needs massive documentations.
*/
recommended: ConfigVariations,
/**
* @summary
* More opinionated configuration, created for ESLegant and Lored's projects.
* @borrows Builds on top of the recommended configuration
* @description
* Enforces some documentation of every function and file.
* Better for libraries (in more production-ready phases),
* large codebase's and/or projects with multiple teams/people.
*/
strict: ConfigVariations,
},
/**
* @summary
* Configurations related to specific JavaScript/TypeScript environments,
* such as Node, Deno and the Browser.
* @description
* Adds global variables and rules related to code on said environments.
* **They can and should be combined** depending on the codebase you're
* working with.
*/
environments: {
/**
* @summary
* Browser environment configuration, use this if you are working
* on a pure client-side or mixed codebase environment.
* @description
* Warns about possible incompatible Web APIs on your codebase, you can
* configure the target browsers using {@link https://github.com/browserslist/browserslist browserslist}
* on `package.json`.
*/
browser: {
/**
* @description
* Recommends end enforces the use of newer web APIs
* on your codebase.
*/
recommended: ConfigVariations,
/**
* @borrows Builds on top of the recommended configuration
* @todo
* For now, the strict rules is a alias of the recommended,
* as it not currently adds any new rules.
*/
strict: ConfigVariations,
},
node: {
/**
* @summary
* Configuration overrides for CommonJS files.
* @description
* ESLegant recommends the use of ESModules and newer syntax
* for javascript files. This configuration should mostly be
* used for exceptions, like config files that don't support
* ESM.
*
* This configuration just affects files ending in `*.cjs`
* and `*.cts` extensions. If you want to use in other files
* consider creating a config object.
* @example <caption>Example of using the config for specific CommonJS files</caption>
* import { configs } from '@eslegant/js';
*
* export default [
* {
* ...configs.environments.node.commonjs.error
* files: ['*.config.js'],
* }
* ]
*/
commonjs: ConfigVariations,
/**
* @description
* Recommends newer and best practices on NodeJS projects.
*/
recommended: ConfigVariations,
/**
* @borrows Builds on top of the recommended configuration
* @todo
* For now, the strict rules is a alias of the recommended,
* as it not currently adds any new rules.
*/
strict: ConfigVariations,
},
},
/**
* @summary
* This config relates to code formatting and style in JavaScript and TypeScript.
* @description
* This configuration enforces a specific code formatting and style in JavaScript
* and TypeScript. The purpose of it can sometimes overlap with the `suggestions`
* config, so to separate better, this tries to mostly enforces just rules for
* how the code looks and is organized than coding style/way of handling something.
*/
formatting: {
/**
* @summary
* ESLegant / Lored's code style configuration.
* @description
* This configuration recommends a opinionated code and formatting style, which
* is mostly similar to other styles in the JavaScript environment.
* It is based on the work and config of Anthony's ESLint config (`antfu/eslint-config`),
* with the most notable changes being the use of `tabs` instead of 2 space indentation
* and the use of semicolons.
* @see {@link https://github.com/antfu/eslint-config Anthony's config}
*/
recommended: ConfigVariations,
/**
* @borrows Builds on top of the recommended configuration
* @todo
* For now, the strict rules is a alias of the recommended,
* as it not currently adds any new rules.
*/
strict: ConfigVariations,
},
/**
* @description
* This configuration enforces a specific naming convention for the codebase.
* With the object of making the code more readable and understandable.
*/
naming: {
/**
* @summary
* Prevents bad naming conventions and behavior.
* @description
* This configuration prevents bad names and behaviors such as abbreviations.
* It does not enforces specific names or naming structure/strategies.
* **With the exception of** file names being enforces to be `kebab-case`.
*/
recommended: ConfigVariations,
/**
* @summary
* Enforces specific naming structure/strategies.
* @borrows Builds on top of the recommended configuration
* @description
* This configuration enforces specific names or naming structure/strategies for your
* code. Enforcing things such using verbs and nouns in specif orders and
* when abbreviations are accepted or not.
*/
strict: ConfigVariations,
},
overrides: {
'inferrable-types': ConfigVariations,
performance: ConfigVariations,
},
/**
* @summary
* Prevents possible syntax errors in your code.
* @description
* This configuration object prevents possible syntax and code logic
* errors on your file. Mostly not opinionated.
*/
problems: {
/**
* @description
* Rules which prevents most errors in your code. Based
* mostly on ESLint's recommended configuration.
*/
recommended: ConfigVariations,
/**
* @borrows Builds on top of the recommended configuration
* @description
* Extra-safety rules, reporting possible forgettable errors
* or errors in typing.
*/
strict: ConfigVariations,
},
/**
* @summary
* Prevents possible vulnerabilities.
* @description
* This configuration tries to prevent possible vulnerabilities
* in you code, such as hard-coded secrets, personal information in comments,
* XSS attacks, etc.
*/
security: {
/**
* @description
* Rules which warns you about possible security vulnerabilities.
*/
recommended: ConfigVariations,
/**
* @borrows Builds on top of the recommended configuration
* @description
* Similar to recommended config, but with rules in error-level
* to make possible vulnerabilities harder to ignore.
*/
strict: ConfigVariations,
},
/**
* @summary
* Enforces different ways of coding in JavaScript and TypeScript.
* @description
* This configuration enforces different ways doing things, coding style and/or
* code logic patterns. Preferring over explicit and declarative code than
* implicit.
*/
suggestions: {
/**
* @summary
* Recommended for projects in prototyping/starting phases.
* @description
* This configuration enforces mostly best practices,
* based on the `recommended` options of the plugins.
*/
recommended: ConfigVariations,
/**
* @summary
* Strict rules that takes "guarding rails" for your code.
* @borrows Builds on top of the recommended configuration
* @description
* **This will get in the way of your programming**. This configuration
* tries prevent possible bad code smells and practices that could built
* up when your project grows. Enforcing you to refactor more the code
* and separating and or reorganizing functions and code logic.
* @see {@link https://youtu.be/CFRhGnuXG-4 Example: Why You Shouldn't Nest Your Code - by: CodeAesthetic}
* - The maximum depth allowed is 4. (`max-depth: [error, 4]`)
* @see {@link https://youtu.be/J1f5b4vcxCQ Example: Dependency Injection, The Best Pattern - by: CodeAesthetic}
* - Files should be organized in a tree-like structure, and shouldn't import modules
* in parent directories. This helps you organize your code and suggests using
* dependency injection more.
*/
strict: ConfigVariations,
},
/**
* @summary
* Rules for TypeScript files specifically. **Use this if
* you have Typescript files in your project**.
* Affects `*.ts`, `*.tsx`, `*.mts` and `*.cts` files.
* @description
* Most of TypeScript rules can be applied to type-checked JavaScript
* files also. But some can just be fixed in TypeScript syntax, so they
* where disabled and moved to this specific configuration.
*
* It also disable things that aren't useful when using TypeScript, such
* as types in JSDoc comments.
*
* **This should be placed after the `suggestion` config.**.
*
*/
'suggestions-typescript': {
/**
* @summary
* Rules similar to {@link configs.suggestions.recommended `suggestions#recommended`},
* but for TypeScript.
*/
recommended: ConfigVariations,
/**
* @summary
* Rules similar to {@link configs.suggestions.strict `suggestions#strict`},
* but for TypeScript.
*/
strict: ConfigVariations,
},
}>;
export default configs;
export type { ConfigVariations };

View File

@@ -1,32 +0,0 @@
/* eslint-disable import/max-dependencies */
/**
* @file
* Main export files for all the configs objects, merging then in one `configs` object.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import typescript from './suggestions-typescript.js';
import environments from './environments/index.js';
import documentation from './documentation.js';
import suggestions from './suggestions.js';
import formatting from './formatting.js';
import overrides from './overrides.js';
import security from './security.js';
import problems from './problems.js';
import naming from './naming.js';
import core from './core.js';
const configs = {
core,
documentation,
environments,
formatting,
naming,
overrides,
problems,
security,
suggestions,
'suggestions-typescript': typescript,
};
export default configs;

View File

@@ -1,40 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects which enforces a specific naming convention for the codebase.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import { createVariations } from '../lib/rule-variations.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: FILES,
rules: {
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
/*
* TODO [>=1.0.0]: This will be replaced by a better naming convention.
* 'unicorn/prevent-abbreviations': 'error',
*/
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // Plugin: @typescript-eslint/eslint-plugin
// '@typescript-eslint/naming-convention': 'error',
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/no-keyword-prefix': 'error',
},
});
const suggestions = { recommended, strict };
export default suggestions;

View File

@@ -1,35 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Overrides for specific scenarios or preferences of users. The objects
* are supposed to be placed on the end of the arrays and can
* change configs of multiple other categories.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
* @todo This file is not completed fully.
*/
import { createVariations } from '../lib/rule-variations.js';
import { FILES, TS_FILES } from '../constants.js';
// TODO [>=1.0.0]: Create a separate config for performance related practices
const performance = createVariations({
files: FILES,
rules: {
'prefer-object-spread': 'off',
'prefer-spread': 'off',
},
});
const inferrableTypes = createVariations({
files: TS_FILES,
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/typedef': 'off',
},
});
const overrides = { 'inferrable-types': inferrableTypes, performance };
export default overrides;

View File

@@ -1,109 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects for preventing possible syntax errors.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import { createVariations } from '../lib/rule-variations.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: FILES,
rules: {
...{}, // ESLint rules
'constructor-super': 'error',
'for-direction': 'error',
'getter-return': 'error',
'no-async-promise-executor': 'error',
'no-class-assign': 'error',
'no-compare-neg-zero': 'error',
'no-cond-assign': 'error',
'no-const-assign': 'error',
'no-constant-condition': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-delete-var': 'error',
'no-dupe-args': 'error',
'no-dupe-class-members': 'error',
'no-dupe-else-if': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-empty-character-class': 'error',
'no-empty-pattern': 'error',
'no-ex-assign': 'error',
'no-fallthrough': 'error',
'no-func-assign': 'error',
'no-global-assign': 'error',
'no-import-assign': 'error',
'no-inner-declarations': 'error',
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-misleading-character-class': 'error',
'no-nonoctal-decimal-escape': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-octal-escape': 'error',
'no-prototype-builtins': 'error',
'no-regex-spaces': 'error',
'no-self-assign': 'error',
'no-setter-return': 'error',
'no-shadow-restricted-names': 'error',
'no-sparse-arrays': 'error',
'no-this-before-super': 'error',
'no-undef': 'error',
'no-unexpected-multiline': 'error',
'no-unreachable': 'error',
'no-unsafe-finally': 'error',
'no-unsafe-negation': 'error',
'no-unsafe-optional-chaining': 'error',
'no-unused-labels': 'error',
'no-useless-backreference': 'error',
'use-isnan': 'error',
'valid-typeof': 'error',
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/no-loss-of-precision': 'error',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'no-loss-of-precision': 'off',
'no-redeclare': 'off',
'no-unused-vars': 'off',
...{}, // Plugin: eslint-plugin-import
'import/default': 'error',
'import/export': 'error',
'import/named': 'error',
'import/namespace': 'error',
'import/no-unresolved': 'error',
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // ESLint rules
'no-constant-binary-expression': 'error',
'no-duplicate-imports': 'error',
'no-new-native-nonconstructor': 'error',
'no-promise-executor-return': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-template-curly-in-string': 'error',
'no-unmodified-loop-condition': 'error',
'no-unreachable-loop': 'error',
'no-unused-private-class-members': 'error',
'require-atomic-updates': 'error',
...{}, // Plugin: eslint-plugin-import
'import/no-extraneous-dependencies': 'error',
},
});
const problems = { recommended, strict };
export default problems;

View File

@@ -1,61 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects for preventing possible security vulnerabilities.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import noSecretsPluginRegexes from 'eslint-plugin-no-secrets/regexes.js';
import noSecretsPlugin from 'eslint-plugin-no-secrets';
import { createVariations } from '../lib/rule-variations.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: FILES,
plugins: {
'no-secrets': noSecretsPlugin,
},
rules: {
...{}, // Plugin: eslint-plugin-security
'security/detect-bidi-characters': 'warn',
'security/detect-disable-mustache-escape': 'warn',
'security/detect-eval-with-expression': 'warn',
'security/detect-non-literal-regexp': 'warn',
'security/detect-object-injection': 'warn',
'security/detect-possible-timing-attacks': 'warn',
'security/detect-pseudoRandomBytes': 'warn',
'security/detect-unsafe-regex': 'warn',
...{}, // Plugin: eslint-plugin-no-secrets
'no-secrets/no-secrets': 'warn',
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // Plugin: eslint-plugin-security
'security/detect-bidi-characters': 'error',
'security/detect-disable-mustache-escape': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-non-literal-regexp': 'error',
'security/detect-object-injection': 'warn',
'security/detect-possible-timing-attacks': 'warn',
'security/detect-pseudoRandomBytes': 'error',
'security/detect-unsafe-regex': 'error',
...{}, // Plugin: eslint-plugin-no-secrets
'no-secrets/no-secrets': ['error', {
additionalRegexes: noSecretsPluginRegexes,
}],
},
});
const security = { recommended, strict };
export default security;

View File

@@ -1,40 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects that enforces different ways of coding in TypeScript specifically.
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import { createVariations } from '../lib/rule-variations.js';
import { TS_FILES } from '../constants.js';
const recommended = createVariations({
files: [TS_FILES].flat(),
rules: {
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/explicit-function-return-type': 'error',
...{}, // Plugin: eslint-plugin-jsdoc
'jsdoc/check-tag-names': ['error', { typed: true }],
'jsdoc/no-types': 'error',
'jsdoc/require-param-type': 'off',
'jsdoc/require-property-type': 'off',
'jsdoc/require-returns-type': 'off',
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/explicit-member-accessibility': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
},
});
const typescript = { recommended, strict };
export default typescript;

View File

@@ -1,453 +0,0 @@
/* eslint-disable import/no-relative-parent-imports */
/* eslint-disable unicorn/no-useless-spread */
/**
* @file
* Configuration objects that enforces different ways of coding in JavaScript and TypeScript..
* See more info on the configs type declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import { createVariations } from '../lib/rule-variations.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: FILES,
rules: {
'camelcase': 'error',
'max-len': ['error', {
code: 80,
comments: 100,
ignoreTemplateLiterals: true,
ignoreUrls: true,
}],
'no-case-declarations': 'error',
'no-confusing-arrow': 'error',
'no-console': 'error',
'no-constant-condition': 'error',
'no-delete-var': 'error',
'no-empty': 'error',
'no-lonely-if': 'error',
'no-new-symbol': 'error',
'no-redeclare': 'error',
'no-useless-catch': 'error',
'no-useless-escape': 'error',
'no-var': 'error',
'no-with': 'error',
'object-shorthand': ['error', 'always', {
avoidQuotes: true,
ignoreConstructors: false,
}],
'prefer-exponentiation-operator': 'error',
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'prefer-template': 'error',
'quote-props': ['error', 'consistent-as-needed'],
'require-yield': 'error',
...{}, // Plugin: @typescript-eslint/eslint-plugin
/*
* '@typescript-eslint/adjacent-overload-signatures': 'error',
* Incompatible with perfectionist/sort-interfaces and
* perfectionist/sort-object-types.
*/
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/ban-tslint-comment': 'error',
'@typescript-eslint/ban-types': 'error',
'@typescript-eslint/class-literal-property-style': 'error',
'@typescript-eslint/consistent-generic-constructors': 'error',
// eslint-disable-next-line max-len
'@typescript-eslint/consistent-indexed-object-style': ['error', 'index-signature'],
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-confusing-void-expression': 'error',
'@typescript-eslint/no-duplicate-enum-values': 'error',
'@typescript-eslint/no-duplicate-type-constituents': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extra-non-null-assertion': 'error',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-for-in-array': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-unnecessary-type-arguments': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-declaration-merging': 'error',
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/non-nullable-type-assertion-style': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-literal-enum-member': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/prefer-return-this-type': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
'@typescript-eslint/restrict-template-expressions': 'error',
'@typescript-eslint/triple-slash-reference': 'error',
'@typescript-eslint/unbound-method': 'error',
'@typescript-eslint/unified-signatures': 'error',
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/better-regex': 'error',
'unicorn/catch-error-name': 'error',
'unicorn/consistent-destructuring': 'error',
'unicorn/consistent-function-scoping': 'error',
'unicorn/empty-brace-spaces': 'error',
'unicorn/error-message': 'error',
'unicorn/escape-case': 'error',
'unicorn/expiring-todo-comments': ['error', {
allowWarningComments: false,
}],
'unicorn/explicit-length-check': 'error',
'unicorn/new-for-builtins': 'error',
'unicorn/no-abusive-eslint-disable': 'error',
'unicorn/no-array-callback-reference': 'error',
'unicorn/no-array-for-each': 'error',
'unicorn/no-array-method-this-argument': 'error',
'unicorn/no-array-push-push': 'error',
'unicorn/no-array-reduce': 'error',
'unicorn/no-await-expression-member': 'error',
'unicorn/no-console-spaces': 'error',
'unicorn/no-document-cookie': 'error',
'unicorn/no-empty-file': 'error',
'unicorn/no-for-loop': 'error',
'unicorn/no-hex-escape': 'error',
'unicorn/no-instanceof-array': 'error',
'unicorn/no-invalid-remove-event-listener': 'error',
'unicorn/no-lonely-if': 'error',
'unicorn/no-new-array': 'error',
'unicorn/no-new-buffer': 'error',
'unicorn/no-object-as-default-parameter': 'error',
'unicorn/no-process-exit': 'error',
'unicorn/no-static-only-class': 'error',
'unicorn/no-thenable': 'error',
'unicorn/no-this-assignment': 'error',
'unicorn/no-typeof-undefined': 'error',
'unicorn/no-unnecessary-await': 'error',
'unicorn/no-unreadable-array-destructuring': 'error',
'unicorn/no-unreadable-iife': 'error',
'unicorn/no-useless-fallback-in-spread': 'error',
'unicorn/no-useless-length-check': 'error',
'unicorn/no-useless-promise-resolve-reject': 'error',
'unicorn/no-useless-spread': 'error',
'unicorn/no-useless-switch-case': 'error',
/*
* TODO (@guz013) [>=1.0.0]: 'unicorn/no-useless-undefined' rule
* It could be better to use a invert to this rule instead, as
* explicity code could be better then implicitly.
*/
'unicorn/no-zero-fractions': 'error',
'unicorn/number-literal-case': 'error',
'unicorn/numeric-separators-style': 'error',
'unicorn/prefer-array-find': ['error', { checkFromLast: true }],
'unicorn/prefer-array-flat': 'error',
'unicorn/prefer-array-flat-map': 'error',
'unicorn/prefer-array-index-of': 'error',
'unicorn/prefer-array-some': 'error',
'unicorn/prefer-at': 'error',
'unicorn/prefer-blob-reading-methods': 'error',
'unicorn/prefer-code-point': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-default-parameters': 'error',
'unicorn/prefer-event-target': 'error',
'unicorn/prefer-export-from': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/prefer-logical-operator-over-ternary': 'error',
'unicorn/prefer-math-trunc': 'error',
'unicorn/prefer-modern-math-apis': 'error',
'unicorn/prefer-module': 'error',
'unicorn/prefer-native-coercion-functions': 'error',
'unicorn/prefer-negative-index': 'error',
'unicorn/prefer-number-properties': 'error',
'unicorn/prefer-optional-catch-binding': 'error',
'unicorn/prefer-prototype-methods': 'error',
'unicorn/prefer-reflect-apply': 'error',
'unicorn/prefer-regexp-test': 'error',
'unicorn/prefer-set-has': 'error',
// TODO [>=1.0.0]: Should this be on a "performance" preset?
'unicorn/prefer-set-size': 'error',
// TODO [>=1.0.0]: Should this be disabled on a "performance" preset?
'unicorn/prefer-spread': 'error',
'unicorn/prefer-string-replace-all': 'error',
'unicorn/prefer-string-slice': 'error',
'unicorn/prefer-string-starts-ends-with': 'error',
'unicorn/prefer-string-trim-start-end': 'error',
'unicorn/prefer-switch': ['error', {
emptyDefaultCase: 'do-nothing-comment',
}],
'unicorn/prefer-ternary': 'error',
'unicorn/prefer-top-level-await': 'error',
'unicorn/prefer-type-error': 'error',
'unicorn/relative-url-style': 'error',
'unicorn/require-array-join-separator': 'error',
'unicorn/require-number-to-fixed-digits-argument': 'error',
'unicorn/switch-case-braces': 'error',
'unicorn/text-encoding-identifier-case': 'error',
'unicorn/throw-new-error': 'error',
...{}, // Plugin: eslint-plugin-import
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-deprecated': 'error',
'import/no-duplicates': 'error',
'import/no-empty-named-blocks': 'error',
'import/no-named-as-default': 'error',
'import/no-named-as-default-member': 'error',
'import/no-self-import': 'error',
'import/no-useless-path-segments': 'error',
...{}, // Plugin: eslint-plugin-jsdoc
'jsdoc/check-alignment': 'error',
'jsdoc/check-param-names': 'error',
'jsdoc/check-property-names': 'error',
'jsdoc/check-syntax': 'error',
'jsdoc/check-tag-names': 'error',
'jsdoc/check-types': 'error',
'jsdoc/check-values': 'error',
'jsdoc/empty-tags': 'error',
'jsdoc/implements-on-classes': 'error',
'jsdoc/multiline-blocks': 'error',
'jsdoc/no-multi-asterisks': ['error', { allowWhitespace: true }],
'jsdoc/require-asterisk-prefix': ['error', 'always'],
'jsdoc/require-jsdoc': 'error',
'jsdoc/require-param': 'error',
'jsdoc/require-param-name': 'error',
'jsdoc/require-param-type': 'error',
'jsdoc/require-property': 'error',
'jsdoc/require-property-name': 'error',
'jsdoc/require-property-type': 'error',
'jsdoc/require-returns': 'error',
'jsdoc/require-returns-check': 'error',
'jsdoc/require-returns-type': 'error',
'jsdoc/require-throws': 'error',
'jsdoc/require-yields': 'error',
'jsdoc/require-yields-check': 'error',
'jsdoc/sort-tags': 'error',
// 'jsdoc/valid-types': 'error', This is already handled by Typescript type checking mostly
},
});
const strict = createVariations({
...recommended.error,
rules: {
...recommended.error.rules,
...{}, // ESLint rules
'accessor-pairs': 'error',
'arrow-body-style': ['error', 'as-needed'],
'block-scoped-var': 'error',
'capitalized-comments': 'error',
'complexity': ['error', 10],
'consistent-return': 'error',
'consistent-this': 'error',
'default-case': 'error',
'default-case-last': 'error',
'eqeqeq': 'error',
'func-name-matching': 'error',
'func-names': ['error', 'as-needed'],
'func-style': ['error', 'declaration'],
'grouped-accessor-pairs': ['error', 'setBeforeGet'],
'handle-callback-err': 'error',
'logical-assignment-operators': ['error', 'always', {
enforceForIfStatements: true,
}],
'max-classes-per-file': ['error', 1],
'max-depth': ['error', 4],
'max-lines': ['error', 500],
'max-lines-per-function': ['error', {
max: 60,
skipBlankLines: true,
skipComments: true,
}],
'max-nested-callbacks': ['error', 10],
'max-params': ['error', 4],
'max-statements': ['error', 15],
'multiline-comment-style': ['error', 'starred-block'],
'new-cap': 'error',
'new-parens': 'error',
'no-alert': 'error',
'no-await-in-loop': 'error',
'no-bitwise': 'error',
'no-caller': 'error',
'no-continue': 'error',
'no-div-regex': 'error',
'no-else-return': 'error',
'no-empty-static-block': 'error',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-extra-boolean-cast': 'error',
// TODO [>=1.0.0]: Fix no-extra-parens conflict with the unicorn/no-nested-ternary rule.
'no-extra-parens': ['error', 'functions'],
'no-floating-decimal': 'error',
'no-implicit-coercion': 'error',
'no-implied-eval': 'error',
'no-iterator': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-mixed-operators': 'error',
'no-multi-assign': 'error',
'no-multi-str': 'error',
'no-multiple-empty-lines': 'error',
'no-negated-condition': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-object': 'error',
'no-new-wrappers': 'error',
'no-path-concat': 'error',
'no-proto': 'error',
'no-return-assign': 'error',
'no-script-url': 'error',
'no-sequences': 'error',
'no-undef-init': 'error',
'no-underscore-dangle': 'error',
'no-unneeded-ternary': 'error',
'no-unused-expressions': 'error',
'no-use-before-define': 'error',
'no-useless-call': 'error',
'no-useless-computed-key': 'error',
'no-useless-rename': 'error',
'no-useless-return': 'error',
'one-var': ['error', 'never'],
'operator-assignment': ['error', 'always'],
'operator-linebreak': 'error',
'prefer-arrow-callback': 'error',
'prefer-const': 'error',
'prefer-named-capture-group': 'error',
'prefer-numeric-literals': 'error',
'prefer-object-has-own': 'error',
'prefer-object-spread': 'error',
'prefer-promise-reject-errors': 'error',
'prefer-regex-literals': 'error',
'radix': ['error', 'always'],
'require-unicode-regexp': 'error',
'symbol-description': 'error',
'wrap-iife': 'error',
'yoda': ['error', 'never'],
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/class-methods-use-this': 'error',
'@typescript-eslint/consistent-type-exports': ['error', {
fixMixedExportsWithInlineTypeSpecifier: false,
}],
'@typescript-eslint/consistent-type-imports': ['error', {
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports',
prefer: 'type-imports',
}],
'@typescript-eslint/default-param-last': 'error',
'@typescript-eslint/dot-notation': 'error',
'@typescript-eslint/method-signature-style': ['error', 'method'],
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-extra-semi': 'error',
'@typescript-eslint/no-invalid-this': 'error',
'@typescript-eslint/no-loop-func': 'error',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-throw-literal': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-use-before-define': 'error',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/parameter-properties': 'error',
'@typescript-eslint/prefer-enum-initializers': 'error',
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/typedef': 'error',
// '@typescript-eslint/no-magic-numbers': ['error'],
'class-methods-use-this': 'off',
'default-param-last': 'off',
'no-array-constructor': 'off',
'no-empty-function': 'off',
'no-extra-semi': 'off',
'no-invalid-this': 'off',
'no-loop-func': 'off',
'no-shadow': 'off',
'no-throw-literal': 'off',
'no-use-before-defined': 'off',
'no-useless-constructor': 'off',
'require-await': 'off',
// 'no-magic-numbers': 'off',
...{}, // Plugin: eslint-plugin-unicorn
'no-nested-ternary': 'off',
'unicorn/custom-error-definition': 'error',
'unicorn/no-negated-condition': 'error',
'unicorn/no-nested-ternary': 'error',
/*
* TODO (@guz013) [>=1.0.0]: 'unicorn/no-null' rule
* It could be better to use a invert to this rule instead,
* because the null type could be a better representation to
* "no value" then undefined.
*/
'unicorn/no-unsafe-regex': 'error',
'unicorn/no-unused-properties': 'error',
...{}, // Plugin: eslint-plugin-import
'import/extensions': ['error', 'always', { ignorePackages: true }],
'import/max-dependencies': ['error', {
ignoreTypeImports: true,
max: 10,
}],
'import/no-absolute-path': 'error',
'import/no-anonymous-default-export': 'error',
'import/no-cycle': 'error',
'import/no-import-module-exports': 'error',
'import/no-mutable-exports': 'error',
'import/no-named-default': 'error',
'import/no-relative-packages': 'error',
'import/no-relative-parent-imports': 'error',
'import/no-unassigned-import': ['error', {
allow: ['**/*.css', '**/*.scss', '**/*.less'],
}],
'import/prefer-default-export': 'error',
'import/unambiguous': 'error',
...{}, // Plugin: eslint-plugin-jsdoc
'jsdoc/check-access': 'error',
'jsdoc/check-indentation': 'error',
'jsdoc/informative-docs': 'error',
'jsdoc/multiline-blocks': ['error', { noSingleLineBlocks: true }],
'jsdoc/no-bad-blocks': 'error',
'jsdoc/no-blank-block-descriptions': 'error',
'jsdoc/no-blank-blocks': 'error',
},
});
const suggestions = { recommended, strict };
export default suggestions;

View File

@@ -1,29 +0,0 @@
/**
* @file
* Constant values used around the package.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
const JS_FILES = [
'**/*.js',
'**/*.mjs',
'**/*.cjs',
'**/*.jsx',
];
const TS_FILES = [
'**/*.ts',
'**/*.mts',
'**/*.cts',
'**/*.tsx',
];
const FILES = [
JS_FILES,
TS_FILES,
].flat();
export {
FILES,
JS_FILES,
TS_FILES,
};

View File

@@ -1,22 +0,0 @@
/**
* @file
* Types entrypoint of the package.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { Linter } from 'eslint';
export { default as presets } from './presets/index.d.ts';
export { default as configs } from './configs/index.d.ts';
/**
* Helper function to provide type-checking when defining
* ESLint's configuration.
*
* @param config - The configuration array to be returned.
* @returns The configuration array passed on the first parameter.
*/
export function defineConfig(
config: Linter.FlatConfig[],
): Linter.FlatConfig[];

View File

@@ -1,27 +0,0 @@
/**
* @file
* Main file entrypoint of the package.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import presets from './presets/index.js';
import configs from './configs/index.js';
/**
* Helper function to provide type-checking when defining
* ESLint's configuration.
*
* @param {import('eslint').Linter.FlatConfig[]} config
* - The configuration array to be returned.
* @returns {import('eslint').Linter.FlatConfig[]}
*/
function defineConfig(config) {
return config;
}
const eslegant = { configs, presets };
export { defineConfig, eslegant as default };
export { default as configs } from './configs/index.js';
export { default as presets } from './presets/index.js';

View File

@@ -1,104 +0,0 @@
/**
* @file Utility functions used in the package to manipulate rules records.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
/**
* @typedef {import('../configs/index').ConfigVariations} ConfigVariations
* @typedef {import('eslint').Linter.RuleEntry} RuleEntry
* @typedef {import('eslint').Linter.RuleLevel} RuleLevel
* @typedef {import('eslint').Linter.FlatConfig} FlatConfig
*/
/**
* Changes the level of a rule entry. Checking if it
* is a Array or a simple RuleLevel entry.
*
* Useful in conjunction with {@link iterateRules `iterateRules()`} function.
*
* @param {Readonly<RuleEntry>} ruleEntry
* - The rule entry to be modified.
* @param {RuleLevel} level
* - The new level to be passed to the rule.
* @returns {RuleEntry}
*/
function changeLevel(ruleEntry, level) {
if (typeof level === 'number') {
/** @type {RuleLevel[]} */
const levels = ['error', 'off', 'warn'];
// eslint-disable-next-line security/detect-object-injection
level = levels[level];
}
if (Array.isArray(ruleEntry))
return [level, ruleEntry[1]];
return level;
}
/**
* Iterates through a rule entry record, using the handler
* on each entry and returns the resulting object.
*
* Useful for applying plugin prefixes or changing the rule
* level of the entries.
*
* @param {Readonly<{[name: string]: RuleEntry}>} rules
* - The object to be iterated through.
* @param {([name, entry]: [string, RuleEntry]) => [string, RuleEntry]} handler
* - Function to run on every rule entry.
* @returns {{[name: string]: RuleEntry}} - The resulting object.
*/
function iterateRules(rules, handler) {
const entries = Object.entries(rules);
entries.map(entry => handler(entry));
return Object.fromEntries(entries);
}
/**
* Creates {@link ConfigVariations variations} for the given configuration object.
* With `error`, `warn` and `off` rule levels.
*
* Used in the configuration objects of this package.
*
* @param {Readonly<FlatConfig>} config
* - The configuration object to create `error`, `warn`, and `off` variations.
* @returns {ConfigVariations}
*/
function createVariations(config) {
const configError = {
...config,
rules: iterateRules(config.rules ?? {}, ([key, entry]) =>
(entry === 'off' || (Array.isArray(entry) && entry[0] === 'off')
? [key, entry]
: [key, changeLevel(entry, 'error')]),
),
};
const configWarning = {
...config,
rules: iterateRules(config.rules ?? {}, ([key, entry]) =>
(entry === 'off' || (Array.isArray(entry) && entry[0] === 'off')
? [key, entry]
: [key, changeLevel(entry, 'warn')]),
),
};
const configDisabled = {
...config,
rules: iterateRules(
config.rules ?? {},
([key, entry]) => [key, changeLevel(entry, 'off')],
),
};
return {
default: config,
error: configError,
off: configDisabled,
warn: configWarning,
};
}
export { createVariations, iterateRules };

View File

@@ -1,51 +0,0 @@
/**
* @file
* Types declarations and documentation for the presets object.
* All these types are public to the user.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { Linter } from 'eslint';
const presets: Readonly<{
/**
* @summary
* Preset recommended for projects in a prototyping or starting phase.
* @description
* This preset is mostly recommended for projects in the start of
* their development, being more flexible and less opinionated.
* Useful for preventing errors.
*
* Configs used:
* - `problems#recommended`;
* - `suggestions#recommended`;
* - `suggestions-typescript#recommended`;
* - `formatting#recommended`;
* - `naming#recommended`;
* - `documentation#recommended`;
*
* All configs are set on level `error`.
*/
recommended: Linter.FlatConfig[],
/**
* @summary
* Preset recommended for projects in a production or large scale.
* @description
* This preset is more strict and opinionated, focusing on making
* your code follow a specific structure and pattern.
*
* Configs used:
* - `problems#strict`;
* - `suggestions#strict`;
* - `suggestions-typescript#strict`;
* - `formatting#strict`;
* - `naming#strict`;
* - `documentation#recommended`;
*
* All configs are set on level `error`.
*/
strict: Linter.FlatConfig[],
}>;
export default presets;

View File

@@ -1,12 +0,0 @@
/**
* @file
* Presets object export.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import recommended from './recommended.js';
import strict from './strict.js';
const presets = { recommended, strict };
export default presets;

View File

@@ -1,23 +0,0 @@
/**
* @file
* Recommended preset object. More info and docs on the type
* declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
// eslint-disable-next-line import/no-relative-parent-imports
import configs from '../configs/index.js';
/** @type {import('eslint').Linter.FlatConfig[]} */
const recommended = [
configs.core,
configs.problems.recommended.default,
configs.suggestions.recommended.default,
configs['suggestions-typescript'].recommended.default,
configs.formatting.recommended.default,
configs.naming.recommended.default,
configs.documentation.recommended.default,
configs.security.recommended.default,
];
export default recommended;

View File

@@ -1,23 +0,0 @@
/**
* @file
* Strict preset object. More info and docs on the type
* declaration file.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
// eslint-disable-next-line import/no-relative-parent-imports
import configs from '../configs/index.js';
/** @type {import('eslint').Linter.FlatConfig[]} */
const strict = [
configs.core,
configs.problems.strict.default,
configs.suggestions.strict.default,
configs['suggestions-typescript'].strict.default,
configs.formatting.strict.default,
configs.naming.strict.default,
configs.documentation.recommended.default,
configs.security.strict.default,
];
export default strict;

View File

@@ -1,16 +1,6 @@
import { configs, defineConfig, presets } from '@eslegant/js';
import { configs, defineConfig, presets } from '@eslit/config';
export default defineConfig([
...presets.strict,
configs.environments.node.strict.default,
{
...configs.documentation.strict.error,
files: ['configs/**/*.js', 'configs/**/*.ts'],
},
{
files: ['**/*.{js,ts,cjs,tjs,mjs,mts,jsx,tsx}'],
rules: {
'jsdoc/check-values': ['error', { allowedLicenses: ['MIT'] }],
},
},
...presets.default,
configs.environments.node,
]);

View File

@@ -1,12 +1,5 @@
# @eslit-fixtures/library
## 1.0.2
### Patch Changes
- Updated dependencies [[`c061fdc`](https://github.com/LoredDev/ESLegant/commit/c061fdc8cd78e130e3e8f56b5633d0601fcb9b5e), [`10e5430`](https://github.com/LoredDev/ESLegant/commit/10e543094f4e5d3c9f3c0ea91fd24ad42888a9b0), [`b257ed0`](https://github.com/LoredDev/ESLegant/commit/b257ed000fad0a06c1152c7d246e3e46216154d4), [`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803)]:
- @eslegant/cli@0.2.0
## 1.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "fixtures/library",
"version": "1.0.2",
"name": "@eslit-fixtures/library",
"version": "1.0.1",
"description": "",
"main": "index.js",
"private": true,
@@ -8,7 +8,7 @@
"test": "pnpm cli"
},
"dependencies": {
"@eslegant/cli": "workspace:*"
"@eslit/cli": "workspace:*"
},
"keywords": [],
"author": "",

View File

@@ -4,10 +4,10 @@
"test:cli": "pnpm cli"
},
"devDependencies": {
"@eslegant/cli": "workspace:*"
"@eslit/cli": "workspace:*"
},
"packageManager": "pnpm@8.6.10",
"name": "fixtures/monorepo",
"name": "monorepo",
"workspaces": [
"apps/*",
"packages/*"

View File

@@ -1,5 +1,5 @@
{
"name": "fixtures/svelte",
"name": "@eslit-fixtures/svelte",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -11,7 +11,7 @@
"test:cli": "pnpm cli"
},
"devDependencies": {
"@eslegant/cli": "workspace:*",
"@eslit/cli": "workspace:*",
"@fontsource/fira-mono": "^4.5.10",
"@neoconfetti/svelte": "^1.0.0",
"@sveltejs/adapter-auto": "^2.0.0",

View File

@@ -1,5 +1,5 @@
{
"name": "eslegant-monorepo",
"name": "eslit-monorepo",
"private": true,
"scripts": {
"lint": "turbo run lint",
@@ -11,16 +11,16 @@
"license": "MIT",
"type": "module",
"dependencies": {
"@eslegant/js": "workspace:*"
"@eslit/config": "workspace:*"
},
"devDependencies": {
"@eslit/cli": "workspace:*",
"@changesets/cli": "^2.26.2",
"@commitlint/config-conventional": "^17.7.0",
"@commitlint/config-conventional": "^17.6.6",
"@commitlint/types": "^17.4.4",
"eslegant": "workspace:*",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"eslint": "^8.47.0",
"husky": "^8.0.3",
"turbo": "^1.10.12"
"eslint": "^8.44.0",
"husky": "^8.0.0",
"turbo": "^1.10.9"
}
}

View File

@@ -1,20 +1,5 @@
# @eslit/cli
## 0.2.0
### Minor Changes
- Now the cli exports a API that runs the application and the configs object needs to be passed to the Cli class, this way any other package can run and have their configs array. ([#14](https://github.com/LoredDev/ESLegant/pull/14))
With this, the new command line interface that handles the actual configs of this repo is the "eslegant" package.
- Renamed all packages from "eslit" to "eslegant" ([`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803))
### Patch Changes
- Updated dependencies ([`10e5430`](https://github.com/LoredDev/ESLegant/commit/10e543094f4e5d3c9f3c0ea91fd24ad42888a9b0))
- Fixed some small errors that could be thrown when prompts are canceled. Also fixed --merge-to-root cli argument not working and added list of packages that are installed on confirmation prompt. ([`b257ed0`](https://github.com/LoredDev/ESLegant/commit/b257ed000fad0a06c1152c7d246e3e46216154d4))
## 0.1.0
### Minor Changes

View File

@@ -1,17 +0,0 @@
import type { CliArgs } from './src/types';
/**
* Class that handles the creation and running the ESLegant command line interface.
*/
export default class Cli {
/**
* @param args - Arguments to pass to the cli when its runs.
*/
public constructor(args: CliArgs);
/**
* Runs the cli with the given arguments.
*/
public async run(): Promise<void>;
}
export type { CliArgs, Config } from './src/types.d.ts';

View File

@@ -1 +0,0 @@
export { default } from './src/cli.js';

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"exclude": ["./node_modules/**", "./dist/**"],
"include": ["**/*.ts", "**/*.js"],
"include": ["./index.d.ts", "./src/**/*.ts", "./src/**/*.js"],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@eslegant/cli",
"version": "0.2.0",
"name": "@eslit/cli",
"version": "0.1.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@@ -16,16 +16,16 @@
"source": "./src/index.js",
"files": [
"src",
"index.js",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLegant",
"homepage": "https://github.com/LoredDev/ESLit",
"type": "module",
"repository": {
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLegant"
"url": "https://github.com/LoredDev/ESLit"
},
"bin": "./src/index.js",
"license": "MIT",
"dependencies": {
"cardinal": "^2.1.1",
@@ -34,13 +34,13 @@
"picocolors": "^1.0.0",
"picomatch": "^2.3.1",
"prompts": "^2.4.2",
"recast": "^0.23.4",
"recast": "^0.23.3",
"sisteransi": "^1.0.5",
"yaml": "^2.3.1"
},
"devDependencies": {
"@types/estree": "^1.0.1",
"@types/node": "^20.5.3",
"@types/node": "^20.4.2",
"@types/prompts": "^2.4.4"
},
"publishConfig": {

View File

@@ -1,39 +1,34 @@
/* eslint-disable no-console */
/* eslint-disable import/max-dependencies */
import process from 'node:process';
import path from 'node:path';
import { createSpinner } from 'nanospinner';
import { Command } from 'commander';
import { erase } from 'sisteransi';
import cardinal from 'cardinal';
import prompts from 'prompts';
import c from 'picocolors';
import PackageInstaller from './package-installer.js';
import ConfigsProcessor from './configs-processor.js';
import ConfigsFile from './configs-file.js';
import notNull from './lib/not-null.js';
import ConfigsProcessor from './configsProcessor.js';
import configs from './configs.js';
import Workspace from './workspace.js';
import c from 'picocolors';
import path from 'node:path';
import { createSpinner } from 'nanospinner';
import count from './lib/count.js';
import prompts from 'prompts';
import ConfigsFile from './configsFile.js';
import * as cardinal from 'cardinal';
import ansi from 'sisteransi';
import PackageInstaller from './packageInstaller.js';
import notNull from './lib/notNull.js';
const stdout = process.stdout;
export default class Cli {
#program = new Command();
/** @type {import('./types').CliArgs} */
args = {
configs: [],
dir: process.cwd(),
};
/**
* @param {import('./types').CliArgs} [args] - Cli arguments object.
* @param {import('./types').CliArgs} [args] Cli arguments object
*/
constructor(args) {
this.#program
.argument('[url-to-config]')
.option('--packages <string...>')
.option('--dir <path>', undefined)
.option('--merge-to-root')
@@ -46,148 +41,115 @@ export default class Cli {
...args,
};
this.args.dir = this.args.dir.startsWith('/')
? this.args.dir
: path.join(process.cwd(), this.args.dir);
this.args.dir = !this.args.dir.startsWith('/')
? path.join(process.cwd(), this.args.dir)
: this.args.dir;
}
// eslint-disable-next-line max-lines-per-function, max-statements, complexity
async run() {
process.chdir(this.args.dir);
const configs = this.args.configs;
const spinner = createSpinner('Detecting workspace configuration');
const processor = new ConfigsProcessor({ configs });
const workspace = new Workspace(this.args.dir, this.args.packages);
const workspace = new Workspace(this.args.dir, this.args?.packages);
let packages = await workspace.getPackages();
packages = packages.map((pkg) => {
spinner.update({
text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}`,
let packages = (await workspace.getPackages())
.map(pkg => {
spinner.update({ text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}` });
pkg.config = processor.detectConfig(pkg);
return pkg;
});
pkg.config = processor.detectConfig(pkg);
return pkg;
});
spinner.success({
text:
`Detecting workspace configuration ${
c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`)}`,
'Detecting workspace configuration ' +
c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`),
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const merge = this.args.mergeToRoot ?? (packages.length > 1
const merge = this.args.mergeToRoot ?? packages.length > 1 ?
/** @type {{merge: boolean}} */
? (await prompts({
initial: true,
message:
`Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}${
c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages'))}`,
(await prompts({
name: 'merge',
message:
`Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}` +
c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages')),
initial: true,
type: 'confirm',
// eslint-disable-next-line unicorn/no-await-expression-member
})).merge : true);
})).merge : true;
console.log(c.dim('\nPlease select which options you prefer\n'));
packages = await processor.questionConfig(
merge ? Workspace.mergePackages(packages) : packages,
configs.filter(config => config.manual),
merge ? workspace.mergePackages(packages) : packages,
configs.filter(c => c.manual),
);
const fileHandler = new ConfigsFile(
configs,
packages.find(config => config.root)?.path,
);
const fileHandler = new ConfigsFile(configs, packages.find(c => c.root)?.path);
for (const pkg of packages) {
pkg.configFile = fileHandler.generateObj(pkg);
// eslint-disable-next-line no-await-in-loop
pkg.configFile.content = await ConfigsFile.generate(pkg.configFile);
pkg.configFile.content = await fileHandler.generate(pkg.configFile);
/** @type {boolean} */
const shouldWrite =
/** @type {{write: boolean}} */
// eslint-disable-next-line no-await-in-loop
(await prompts({
initial: true,
type: 'confirm',
name: 'write',
message: `Do you want to write this config file for ${pkg.root
? c.blue('the root directory')
: c.blue(pkg.name)
}?\n\n${cardinal.highlight(pkg.configFile.content)}`,
name: 'write',
type: 'confirm',
// eslint-disable-next-line unicorn/no-await-expression-member
initial: true,
})).write;
stdout.write(
erase.lines(pkg.configFile.content.split('\n').length + 2),
);
stdout.write(ansi.erase.lines(pkg.configFile.content.split('\n').length + 2));
if (shouldWrite) await fileHandler.write(pkg.configFile.path, pkg.configFile.content);
if (shouldWrite) {
// eslint-disable-next-line no-await-in-loop
await ConfigsFile.write(
pkg.configFile.path,
pkg.configFile.content,
);
}
}
const packagesMap = new Map(packages.map(p =>
[p.path, [...notNull(p.configFile).imports.keys()]],
));
const installer = new PackageInstaller(
packagesMap,
packages.find(p => p.root === true)?.path ?? this.args.dir,
);
const packagesMap = new Map(packages.map(p => [p.path, [...notNull(p.configFile).imports.keys()]]));
const installer = new PackageInstaller(packagesMap, packages.find(p => p.root === true)?.path ?? this.args.dir);
/** @type {boolean | 'changePackage'} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let installPkgs = this.args.installPkgs ?? (await prompts({
choices: [
{
description: installer.packageManager.description,
title: 'Yes, install all packages',
value: true,
},
{ title: 'No, I will install them manually', value: false },
{ title: 'Change package manager', value: 'changePackage' },
],
message:
`Would you like to ESLit to install the npm packages with ${c.green(installer.packageManager.name)}?\n${c.reset(c.dim(` Packages to install: ${[...new Set(packagesMap.values())].join(' ')}\n`))}`,
name: 'install',
type: 'select',
// eslint-disable-next-line unicorn/no-await-expression-member
})).install;
let installPkgs = this.args.installPkgs !== undefined ? true :
/** @type {{install: boolean | 'changePackage'}} */
(await prompts({
name: 'install',
message:
`Would you like to ESLit to install the npm packages with ${c.green(installer.packageManager.name)}?`,
choices: [
{ title: 'Yes, install all packages', value: true, description: installer.packageManager.description },
{ title: 'No, I will install them manually', value: false },
{ title: 'Change package manager', value: 'changePackage' },
],
type: 'select',
})).install;
if (installPkgs === 'changePackage') {
/** @type {{manager: import('./types').PackageManagerName}} */
const prompt = await prompts({
choices: Object.values(installer.packageManagers).map(m => ({
description: m.description,
title: m.name,
value: m.name,
})),
message: 'What package manager do you want ESLit to use?',
name: 'manager',
message: 'What package manager do you want ESLit to use?',
choices: Object.values(installer.packageManagers).map(m => {
return { title: m.name, description: m.description, value: m.name };
}),
type: 'select',
});
installer.packageManager =
installer.packageManagers[prompt.manager];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!installer.packageManager)
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-throw-literal, @typescript-eslint/no-confusing-void-expression
throw console.log(c.red('You must select a package manager'));
installer.packageManager = installer.packageManagers[prompt.manager];
installPkgs = true;
}
if (installPkgs) await installer.install();
}
}

View File

@@ -1,37 +1,33 @@
/** @type {import('@eslegant/cli').Config[]} */
const cliConfig = [
/** @type {import('./types').Config[]} */
export default [
{
description: 'The UI frameworks being used in the project',
name: 'framework',
type: 'multiple',
description: 'The UI frameworks being used in the project',
options: [
{
name: 'svelte',
packages: { 'svelte': 'svelte' },
configs: ['svelte.recommended'],
detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'],
name: 'svelte',
packages: { svelte: 'svelte' },
},
{
name: 'vue',
packages: { 'vue': ['vue', ['hello', 'world']], 'svelte': ['hello'] },
configs: ['vue.recommended'],
detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'],
name: 'vue',
packages: {
svelte: ['hello'],
vue: ['vue', ['hello', 'world']],
},
},
],
type: 'multiple',
},
{
manual: true,
name: 'strict',
options: [{
configs: ['config.strict'],
name: 'yes',
packages: { eslint: 'config', svelte: ['test1'] },
}],
type: 'confirm',
manual: true,
options: [{
name: 'yes',
packages: { 'eslint': 'config', 'svelte': ['test1'] },
configs: ['config.strict'],
}],
},
];
export default cliConfig;

View File

@@ -1,26 +1,19 @@
import { existsSync } from 'node:fs';
import process from 'node:process';
import path from 'node:path';
import notNull from './lib/notNull.js';
import * as recast from 'recast';
import fs from 'node:fs/promises';
import { join } from 'node:path';
import { parse, prettyPrint } from 'recast';
import astUtils from './lib/ast-utils.js';
import notNull from './lib/not-null.js';
import { existsSync } from 'node:fs';
import astUtils from './lib/astUtils.js';
/**
* @param {import('./types.js').ConfigFile['imports']} map1 -
* The map to has it values merged from map2.
* @param {import('./types.js').ConfigFile['imports']} map2 -
* The map to has it values merged to map1.
* @returns {import('./types.js').ConfigFile['imports']} The resulting map.
* @param {import('./types').ConfigFile['imports']} map1 - The map to has it values merged from map2
* @param {import('./types').ConfigFile['imports']} map2 - The map to has it values merged to map1
* @returns {import('./types').ConfigFile['imports']} The resulting map
*/
// eslint-disable-next-line max-statements
function mergeImportsMaps(map1, map2) {
for (const [key, value] of map2) {
if (!map1.has(key)) {
map1.set(key, value);
// eslint-disable-next-line no-continue
continue;
}
const imports1 = notNull(map1.get(key));
@@ -30,47 +23,33 @@ function mergeImportsMaps(map1, map2) {
* Because arrays and objects are always different independently from having equal values
* ([] === [] -> false). It is converted to a string so the comparison can be made.
*/
switch ([
typeof imports1 === 'string',
typeof imports2 === 'string',
].join(',')) {
case 'true,true': {
if (imports1.toString() === imports2.toString()) {
switch ([typeof imports1 === 'string', typeof imports2 === 'string'].join(',')) {
case 'true,true':
if (imports1.toString() === imports2.toString())
map1.set(key, value);
}
else {
map1.set(key, [
['default', imports1.toString()],
['default', imports2.toString()],
]);
}
else
map1.set(key, [['default', imports1.toString()], ['default', imports2.toString()]]);
break;
}
case 'true,false': {
case 'true,false':
map1.set(key, [['default', imports1.toString()], ...imports2]);
break;
}
case 'false,true': {
case 'false,true':
map1.set(key, [['default', imports2.toString()], ...imports1]);
break;
}
case 'false,false': {
case 'false,false':
map1.set(key, [...imports1, ...imports2]);
break;
}
default:
// No nothing
}
if (typeof map1.get(key) !== 'string')
map1.set(key, [...new Set(map1.get(key))]);
map1.set(key, [...new Set(map1.get(key))]);
}
return map1;
}
/**
* @param {string} path1 - The path to traverse from.
* @param {string} root - The root path to be removed from the path1.
* @returns {string} The path to traverse.
* @param {string} path1 The path to traverse from
* @param {string} root The root path
* @returns {string} The path to traverse
*/
function getPathDepth(path1, root) {
const pathDepth = path1.replace(root, '').split('/').slice(1);
@@ -79,12 +58,13 @@ function getPathDepth(path1, root) {
}
export default class ConfigsWriter {
/** @type {string} */
root = process.cwd();
/**
* @param {import('./types.js').Config[]} configs - The array of configs to construct from.
* @param {string} [root] - The root directory path.
* @param {import('./types').Config[]} configs The array of configs to construct from
* @param {string} [root] The root directory path
*/
constructor(configs, root) {
this.configs = configs;
@@ -92,58 +72,60 @@ export default class ConfigsWriter {
}
/**
* @param {Program} ast - The program ast to be manipulated.
* @returns {Promise<Program>} The final ast with the recreated default export.
* @private
* @param {import('./types').Package} pkg The package to generate the config string from
* @returns {import('./types').ConfigFile} The config file object
*/
static async addDefaultExport(ast) {
/** @type {{program: Program}} */
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const { program: exportTemplateAst } = parse([
'/** @type {import(\'eslint\').Linter.FlatConfig[]} */',
'export default [',
'',
'];',
].join('\n'), { parser: (await import('recast/parsers/babel.js')) });
/** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
const exportTemplateNode = exportTemplateAst.body
.find(n => n.type === 'ExportDefaultDeclaration');
generateObj(pkg) {
/** @type {import('./types').ConfigFile} */
const configObj = {
path: path.join(pkg.path, 'eslint.config.js'),
imports: new Map(),
configs: [],
presets: [],
rules: [],
};
/** @type {import('estree').ExportDefaultDeclaration | undefined} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
const astExport = ast.body
.find(n => n.type === 'ExportDefaultDeclaration');
if (!pkg.root) {
const rootConfig = path.join(getPathDepth(pkg.path, this.root), 'eslint.config.js');
configObj.imports.set(!rootConfig.startsWith('.') ? `./${rootConfig}` : rootConfig, 'root');
configObj.presets.push('root');
}
if (!astExport) { ast.body.push(exportTemplateNode); return ast; }
for (const [configName, optionsNames] of notNull(pkg.config)) {
/** @type {import('estree').VariableDeclaration | undefined} */
const oldExportValue = astExport.declaration.type === 'ArrayExpression'
? undefined
: astUtils.createVariable(
'oldConfig',
// @ts-expect-error astExport.declaration is a expression
astExport.declaration,
'const',
);
const config = this.configs.find(c => c.name === configName);
if (!config) continue;
if (!oldExportValue) return ast;
const options = config.options.filter(o => optionsNames.includes(o.name));
if (!options || options.length === 0) continue;
// @ts-expect-error declaration is a ArrayExpression
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
exportTemplateNode.declaration.elements.push({
argument: { name: 'oldConfig', type: 'Identifier' },
type: 'SpreadElement',
});
const imports = options.reduce((acc, opt) => {
const map1 = new Map(Object.entries(acc.packages ?? {}));
const map2 = new Map(Object.entries(opt.packages ?? {}));
acc.packages = Object.fromEntries(mergeImportsMaps(map1, map2));
return acc;
});
const astExportIdx = ast.body.indexOf(astExport);
// eslint-disable-next-line security/detect-object-injection
ast.body[astExportIdx] = exportTemplateNode;
ast.body.splice(astExportIdx - 1, 0, oldExportValue);
configObj.imports = mergeImportsMaps(configObj.imports, new Map(Object.entries(imports.packages ?? {})));
return ast;
configObj.configs = [...new Set([
...configObj.configs,
...options.map(o => o.configs ?? []).flat(),
])];
configObj.rules = [...new Set([
...configObj.rules,
...options.map(o => o.rules ?? []).flat(),
])];
configObj.presets = [...new Set([
...configObj.presets,
...options.map(o => o.presets ?? []).flat(),
])];
}
return configObj;
}
/**
@@ -153,36 +135,111 @@ export default class ConfigsWriter {
* prevent because a lot of the AST typescript types are somewhat wrong or simply hard to work
* with them.
*
* But for somewhat help developing and prevent unwanted errors in the future, the types and
* eslint errors are explicitly disabled and types are explicitly overridden. This is why
* there are so many JSDoc type annotations and comments in general.
* But for somewhat help developing and prevent unwanted errors in the future, the types and eslint
* errors are explicitly disabled and types are explicitly overridden. This is why there are so
* many JSDoc type annotations and comments in general.
*
* Any help to make this code more readable and robust is appreciated.
* Any help to make this code more readable and robust is appreciated
*/
/**
* @typedef {import('estree').Program} Program
* @typedef {(
* import('./lib/ast-utils.js').ExpressionOrIdentifier |
* import('estree').ObjectExpression |
* import('estree').SpreadElement
* import('./lib/astUtils.js').ExpressionOrIdentifier |
* import('estree').ObjectExpression |
* import('estree').SpreadElement
* )} ConfigArrayElement
*/
/**
* Adds elements to the default export node, without adding duplicates.
* @typedef {import('estree').ArrayExpression} ArrayExpression
* @param {Program} ast - The program ast to be manipulated.
* @param {ConfigArrayElement[]} elements - The elements to be added to the array.
* @returns {Program} The final ast with the recreated default export.
* @param {Program} ast The program ast to be manipulated
* @returns {Promise<Program>} The final ast with the recreated default export
* @private
*/
static addElementsToExport(ast, elements) {
async addDefaultExport(ast) {
/** @type {{program: Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const { program: exportTemplateAst } = recast.parse([
'/** @type {import(\'eslint\').Linter.FlatConfig[]} */',
'export default [',
'',
'];',
].join('\n'), { parser: (await import('recast/parsers/babel.js')) });
/** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
const exportNode = ast.body.find(n =>
n.type === 'ExportDefaultDeclaration',
);
const exportTemplateNode = exportTemplateAst.body.find(n => n.type === 'ExportDefaultDeclaration');
/** @type {import('estree').ExportDefaultDeclaration | undefined} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
let astExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration');
if (!astExport) { ast.body.push(exportTemplateNode); return ast; }
/** @type {import('estree').VariableDeclaration | undefined} */
const oldExportValue = astExport.declaration.type !== 'ArrayExpression'
// @ts-expect-error astExport.declaration is a expression
? astUtils.createVariable('oldConfig', 'const', astExport.declaration)
: undefined;
if (!oldExportValue) return ast;
// @ts-expect-error declaration is a ArrayExpression
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
exportTemplateNode.declaration.elements.push({
type: 'SpreadElement',
argument: { type: 'Identifier', name: 'oldConfig' },
});
const astExportIdx = ast.body.indexOf(astExport);
ast.body[astExportIdx] = exportTemplateNode;
ast.body.splice(astExportIdx - 1, 0, oldExportValue);
return ast;
}
/**
* @param {import('./types').ConfigFile['rules']} rules The rules to be used to create the object
* @returns {import('estree').ObjectExpression} The object containing the spread rules
* @private
*/
createRulesObject(rules) {
/** @type {import('estree').SpreadElement[]} */
// @ts-expect-error The array is filtered to remove undefined's
const expressions = rules
.map(r => {
const e = astUtils.stringToExpression(r);
if (e) return astUtils.toSpreadElement(e);
else undefined;
}).filter(e => e);
return {
type: 'ObjectExpression',
properties: [{
// @ts-expect-error because ObjectProperty doesn't exist in estree types
type: 'ObjectProperty',
key: { type: 'Identifier', name: 'rules' },
value: {
type: 'ObjectExpression',
properties: expressions,
},
}],
};
}
/**
* Adds elements to the default export node, without adding duplicates
* @typedef {import('estree').ArrayExpression} ArrayExpression
* @param {Program} ast The program ast to be manipulated
* @param {ConfigArrayElement[]} elements The elements to be added to the array
* @returns {Program} The final ast with the recreated default export
* @private
*/
addElementsToExport(ast, elements) {
/** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
const exportNode = ast.body.find(n => n.type === 'ExportDefaultDeclaration');
const exportNodeIdx = ast.body.indexOf(exportNode);
/** @type {ArrayExpression} */
@@ -190,61 +247,45 @@ export default class ConfigsWriter {
const array = exportNode.declaration;
for (const e of elements) {
if (!(
e.type !== 'ObjectExpression' &&
astUtils.findInArray(array, e)
))
array.elements.push(e);
if (e.type !== 'ObjectExpression' && astUtils.findInArray(array, e)) continue;
array.elements.push(e);
}
exportNode.declaration = array;
// eslint-disable-next-line security/detect-object-injection
ast.body[exportNodeIdx] = exportNode;
return ast;
}
/**
* @param {Program} ast - The program ast to be manipulated.
* @param {import('./types.js').ConfigFile['imports']} imports - The imports map to be used.
* @returns {Program} The final ast with the recreated default export.
* @param {Program} ast The program ast to be manipulated
* @param {import('./types').ConfigFile['imports']} imports The imports map to be used
* @returns {Program} The final ast with the recreated default export
*/
static addPackageImports(ast, imports) {
addPackageImports(ast, imports) {
/** @type {import('estree').ImportDeclaration[]} */
const importDeclarations = [];
for (const [pkgName, specifiers] of imports) {
/** @type {import('estree').ImportDeclaration | undefined} */
// @ts-expect-error type error, the specifier has to be ImportDeclaration to be founded
const existingDeclaration = ast.body.find(s =>
s.type === 'ImportDeclaration' &&
s.source.value === pkgName,
);
const existingDeclaration = ast.body.find(s => s.type === 'ImportDeclaration' && s.source.value === pkgName);
const importDeclaration = astUtils.createImportDeclaration(
pkgName,
typeof specifiers === 'string'
? specifiers
: undefined,
existingDeclaration,
pkgName, typeof specifiers === 'string' ? specifiers : undefined, existingDeclaration,
);
if (typeof specifiers !== 'string') {
for (const s of specifiers) {
if (typeof s === 'string')
importDeclaration.addSpecifier(s);
else
importDeclaration.addSpecifier(s[0], s[1]);
}
specifiers.forEach(s => {
if (typeof s === 'string') return importDeclaration.addSpecifier(s);
else return importDeclaration.addSpecifier(s[0], s[1]);
});
}
if (existingDeclaration) {
ast.body[ast.body.indexOf(existingDeclaration)] =
importDeclaration.body;
}
else {
importDeclarations.push(importDeclaration.body);
}
if (existingDeclaration) ast.body[ast.body.indexOf(existingDeclaration)] = importDeclaration.body;
else importDeclarations.push(importDeclaration.body);
}
ast.body.unshift(...importDeclarations);
@@ -252,55 +293,20 @@ export default class ConfigsWriter {
return ast;
}
/**
* @param {import('./types.js').ConfigFile['rules']} rules -
* The rules to be used to create the object.
* @returns {import('estree').ObjectExpression} The object containing the spread rules.
* @private
*/
static createRulesObject(rules) {
/** @type {import('estree').SpreadElement[]} */
// @ts-expect-error The array is filtered to remove undefined's
const expressions = rules
.map((r) => {
const e = astUtils.stringToExpression(r);
return e ? astUtils.toSpreadElement(e) : undefined;
}).filter(Boolean);
return {
properties: [{
key: { name: 'rules', type: 'Identifier' },
// @ts-expect-error because ObjectProperty doesn't exist in estree types
type: 'ObjectProperty',
value: {
properties: expressions,
type: 'ObjectExpression',
},
}],
type: 'ObjectExpression',
};
}
/**
* @param {import('./types.js').ConfigFile} config -
* The config file object to be transformed into a eslint.config.js file.
* @returns {Promise<string>} The generated config file contents.
* @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file
* @returns {Promise<string>} The generated config file contents
*/
static async generate(config) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const existingConfig = existsSync(config.path)
// eslint-disable-next-line security/detect-non-literal-fs-filename
? await fs.readFile(config.path, 'utf8')
: '';
async generate(config) {
const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : '';
/** @type {{program: Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { program: ast } = parse(
existingConfig,
{ parser: (await import('recast/parsers/babel.js')) },
);
const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) });
await ConfigsWriter.addDefaultExport(ast);
await this.addDefaultExport(ast);
/**
* @type {ConfigArrayElement[]}
@@ -308,100 +314,31 @@ export default class ConfigsWriter {
// @ts-expect-error The array is filtered to remove undefined's
const elements = [
...config.configs.map(c => astUtils.stringToExpression(c)),
...config.presets.map((p) => {
...config.presets.map(p => {
const e = astUtils.stringToExpression(p);
return e ? astUtils.toSpreadElement(e) : undefined;
if (e) return astUtils.toSpreadElement(e);
else undefined;
}),
config.rules.length > 0
? ConfigsWriter.createRulesObject(config.rules)
? this.createRulesObject(config.rules)
: undefined,
].filter(Boolean);
].filter(e => e);
ConfigsWriter.addElementsToExport(ast, elements);
ConfigsWriter.addPackageImports(ast, config.imports);
this.addElementsToExport(ast, elements);
this.addPackageImports(ast, config.imports);
const finalCode = prettyPrint(
ast,
{ parser: (await import('recast/parsers/babel.js')) },
).code;
const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code;
return finalCode;
}
/**
* @param {import('./types.js').Package} pkg - The package to generate the config string from.
* @returns {import('./types.js').ConfigFile} The config file object.
*/
// eslint-disable-next-line max-statements
generateObj(pkg) {
/** @type {import('./types.js').ConfigFile} */
const configObj = {
configs: [],
imports: new Map(),
path: join(pkg.path, 'eslint.config.js'),
presets: [],
rules: [],
};
if (!pkg.root) {
const rootConfig = join(
getPathDepth(pkg.path, this.root),
'eslint.config.js',
);
configObj.imports.set(rootConfig.startsWith('.') ? rootConfig : `./${rootConfig}`, 'root');
configObj.presets.push('root');
}
for (const [configName, optionsNames] of notNull(pkg.config)) {
const config = this.configs.find(c => c.name === configName);
// eslint-disable-next-line no-continue
if (!config) continue;
const options = config.options.filter(o =>
optionsNames.includes(o.name),
);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-continue
if (!options || options.length === 0) continue;
// eslint-disable-next-line unicorn/no-array-reduce
const imports = options.reduce((acc, opt) => {
const map1 = new Map(Object.entries(acc.packages ?? {}));
const map2 = new Map(Object.entries(opt.packages ?? {}));
acc.packages = Object.fromEntries(mergeImportsMaps(map1, map2));
return acc;
});
configObj.imports = mergeImportsMaps(
configObj.imports,
new Map(Object.entries(imports.packages ?? {})),
);
configObj.configs = [...new Set([
...configObj.configs,
...options.flatMap(o => o.configs ?? []),
])];
configObj.rules = [...new Set([
...configObj.rules,
...options.flatMap(o => o.rules ?? []),
])];
configObj.presets = [...new Set([
...configObj.presets,
...options.flatMap(o => o.presets ?? []),
])];
}
return configObj;
}
/**
* @param {string} path - The path to the file to be written.
* @param {string} content - The content of the file.
* @param {string} path The path to the file to be written
* @param {string} content The content of the file
* @returns {Promise<void>}
*/
static async write(path, content) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
await fs.writeFile(path, content, 'utf8');
async write(path, content) {
await fs.writeFile(path, content, 'utf-8');
}
}

View File

@@ -1,75 +1,56 @@
import process from 'node:process';
#!node
import path from 'node:path';
import prompts from 'prompts';
import glob from 'picomatch';
import prompts from 'prompts';
import c from 'picocolors';
import capitalize from './lib/capitalize.js';
import str from './lib/str.js';
export default class ConfigsProcessor {
/** @type {import('./types.js').Config[]} */
configs;
/** @type {string} */
dir = process.cwd();
/** @type {import('./types.js').Config[]} */
configs;
/** @type {string[] | undefined} */
#packagesPatterns;
/**
* @param {{
* configs: import('./types.js').Config[],
* packages?: string[],
* directory?: string,
* }} options - Cli options.
* configs: import('./types.js').Config[],
* packages?: string[],
* directory?: string,
* }} options - Cli options
*/
constructor(options) {
this.configs = options.configs;
this.#packagesPatterns = options.packages;
this.configs = options?.configs;
this.dir = path.normalize(options.directory ?? this.dir);
}
/**
* @param {import('./types.js').Package} pkg - The package to detect configs.
* @returns {import('./types.js').Package['config']} - Detected configs record.
* @param {import('./types.js').Package} pkg - Package to detect from
* @param {import('./types.js').Config['options']} options - Options to be passed
* @param {boolean} single - Whether to only detect one option
* @returns {string[]} - The detected options
*/
detectConfig(pkg) {
/** @type {import('./types.js').Package['config']} */
const pkgConfig = new Map();
detectOptions(pkg, options, single) {
for (const config of this.configs.filter(cfg => !cfg.manual)) {
pkgConfig.set(config.name, ConfigsProcessor.detectOptions(
pkg,
config.options,
config.type !== 'multiple',
));
}
return pkgConfig;
}
/**
* @param {import('./types.js').Package} pkg - Package to detect from.
* @param {import('./types.js').Config['options']} options - Options to be passed.
* @param {boolean} single - Whether to only detect one option.
* @returns {string[]} - The detected options.
*/
static detectOptions(pkg, options, single) {
/** @type {string[]} */
const detectedOptions = [];
for (const option of options) {
if (option.detect === true) {
detectedOptions.push(option.name);
// eslint-disable-next-line no-continue
continue;
}
// eslint-disable-next-line no-continue
else if (!option.detect) { continue; }
else if (!option.detect) continue;
const match = glob(option.detect);
const files = pkg.files.filter(f => (match ? match(f) : false));
const directories = pkg.directories.filter(f =>
(match ? match(f) : false),
);
const files = pkg.files.filter(f => match ? match(f) : false);
const directories = pkg.directories.filter(f => match ? match(f) : false);
if (files.length > 0 || directories.length > 0) {
detectedOptions.push(option.name);
@@ -81,24 +62,26 @@ export default class ConfigsProcessor {
}
/**
* @param {import('./types.js').Package[] | import('./types.js').Package} pkg -
* The packages to questions the configs.
* @param {import('./types.js').Config[]} configs - The configs to be used.
* @returns {Promise<import('./types.js').Package[]>} - The selected options by the user.
* @param {import('./types.js').Package[] | import('./types.js').Package} pkg - The packages to questions the configs
* @param {import('./types').Config[]} configs - The configs to be used
* @returns {Promise<import('./types.js').Package[]>} - The selected options by the user
*/
// eslint-disable-next-line max-statements, complexity, max-lines-per-function
async questionConfig(pkg, configs) {
const packages = Array.isArray(pkg) ? [...pkg] : [pkg];
const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`);
for (const config of configs) {
/** @type {import('prompts').Choice[]} */
const configChoices = config.options.map(option => ({ title: `${capitalize(option.name)}`, value: option.name }));
const configChoices = config.options.map(option => {return { title: `${str.capitalize(option.name)}`, value: option.name };});
/** @type {Record<string, string[]>} */
// eslint-disable-next-line no-await-in-loop
const selectedOptions = await prompts({
name: config.name,
type: config.type === 'multiple' ? 'multiselect' : 'select',
message: str.capitalize(config.name),
choices: config.type === 'confirm' ? [
{
title: 'Yes',
@@ -110,19 +93,11 @@ export default class ConfigsProcessor {
},
] : configChoices,
hint: config.description,
instructions: instructions + c.dim(c.italic(
// eslint-disable-next-line max-len
'\nSelect none if you don\'t want to use this configuration\n',
)),
message: capitalize(config.name),
name: config.name,
type: config.type === 'multiple' ? 'multiselect' : 'select',
instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')),
});
// eslint-disable-next-line no-continue, @typescript-eslint/no-unnecessary-condition
if (!selectedOptions[config.name]) continue;
if (selectedOptions[config.name] === null) continue;
// eslint-disable-next-line no-continue
if (selectedOptions[config.name].length === 0) continue;
if (packages.length <= 1) {
@@ -130,47 +105,64 @@ export default class ConfigsProcessor {
...(packages[0].config ?? []),
...Object.entries(selectedOptions),
]);
// eslint-disable-next-line no-continue
continue;
}
/** @type {{title: string, value: import('./types.js').Package}[]} */
/** @type {{title: string, value: import('./types').Package}[]} */
const packagesOptions = packages
.map(p => (p.root
? { title: 'root', value: p }
: {
title: `${p.name} ${c.dim(p.path.replace(this.dir, '.'))}`,
value: p,
}))
.map(pkg => {
return !pkg.root
? {
title: `${pkg.name} ${c.dim(pkg.path.replace(this.dir, '.'))}`,
value: pkg,
}
: { title: 'root', value: pkg };
})
.filter(p => p.title !== 'root');
/** @type {Record<'packages', import('./types.js').Package[]>} */
// eslint-disable-next-line no-await-in-loop
/** @type {Record<'packages', import('./types').Package[]>} */
const selected = await prompts({
choices: packagesOptions,
instructions: instructions + c.dim(c.italic(
'\nToggle all to use in the root configuration\n',
)),
message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`,
min: 1,
name: 'packages',
type: 'autocompleteMultiselect',
message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`,
choices: packagesOptions,
min: 1,
instructions: instructions + c.dim(c.italic('\nToggle all to use in the root configuration\n')),
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
selected.packages ??= [];
selected.packages = selected.packages ?? [];
selected.packages.map((p) => {
p.config = new Map([
...(p.config ?? []),
selected.packages.map(pkg => {
pkg.config = new Map([
...(pkg.config ?? []),
...Object.entries(selectedOptions),
]); return p;
]); return pkg;
});
packages.map(p =>
selected.packages.find(s => s.name === p.name) ?? p,
);
packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg);
}
return packages;
}
/**
* @param {import('./types').Package} pkg - The package to detect configs
* @returns {import('./types').Package['config']} - Detected configs record
*/
detectConfig(pkg) {
/** @type {import('./types.js').Package['config']} */
const pkgConfig = new Map();
for (const config of this.configs.filter(c => !c.manual)) {
pkgConfig.set(config.name, this.detectOptions(
pkg,
config.options,
config.type !== 'multiple',
));
}
return pkgConfig;
}
}

4
packages/cli/src/index.js Executable file
View File

@@ -0,0 +1,4 @@
import Cli from './cli.js';
const cli = new Cli();
await cli.run();

View File

@@ -1,190 +0,0 @@
import { parse, print } from 'recast';
/**
* @typedef {(
* import('estree').MemberExpression |
* import('estree').Identifier |
* import('estree').CallExpression |
* import('estree').NewExpression
* )} ExpressionOrIdentifier
* This type only includes the expressions used in the cli's config type
* @typedef {import('estree').VariableDeclaration} VariableDeclaration
* @typedef {import('estree').Identifier['name']} IdentifierName
* @typedef {VariableDeclaration['kind']} VariableKind
* @typedef {import('estree').VariableDeclarator['init']} VariableInit
* @typedef {import('estree').SpreadElement} SpreadElement
* @typedef {import('estree').Expression} Expression
* @typedef {import('estree').ArrayExpression} ArrayExpression
*/
/**
* @param {IdentifierName} identifier - Nave of the variable identifier.
* @param {VariableInit} [init] - Initial value of the variable.
* @param {VariableKind} [kind] - Type of variable declaration.
* @returns {VariableDeclaration} The variable declaration ast node object.
*/
function createVariable(identifier, init, kind = 'const') {
return {
declarations: [{
id: { name: identifier, type: 'Identifier' },
init,
type: 'VariableDeclarator',
}],
kind,
type: 'VariableDeclaration',
};
}
/**
* @param {string} string - The code/expression in string.
* @returns {ExpressionOrIdentifier | undefined} -
* The expression or identifier node of that string (undefined if string is not a expression).
*/
function stringToExpression(string) {
/** @type {ExpressionOrIdentifier} */
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const e = parse(string).program.body[0].expression;
if ([
'CallExpression',
'Identifier',
'MemberExpression',
'NewExpression',
].includes(e.type)) return e;
return undefined;
}
/**
* @param {ArrayExpression} array - The array node to search trough.
* @param {ExpressionOrIdentifier | SpreadElement} element - The element to be search.
* @returns {ExpressionOrIdentifier | undefined}
* The element of the array founded, undefined if it isn't found.
*/
function findInArray(array, element) {
/** @type {ExpressionOrIdentifier[]} */
// @ts-expect-error The array should have just tge type above
element = element.type === 'SpreadElement' ? element.argument : element;
/** @type {ExpressionOrIdentifier[]} */
// @ts-expect-error The array is filtered to have the type above
const filteredElements = array.elements
.map((n) => {
if (n?.type === 'SpreadElement') return n.argument;
return n;
}).filter(n => n && n.type === element.type);
const toStringElements = filteredElements.map(n => print(n).code);
const toStringElement = print(element).code;
const idx = toStringElements.indexOf(toStringElement);
// eslint-disable-next-line security/detect-object-injection
return filteredElements[idx];
}
/**
* @param {ExpressionOrIdentifier} expression - The expression to be spread.
* @returns {SpreadElement} The spread element node.
*/
function toSpreadElement(expression) {
return {
argument: expression,
type: 'SpreadElement',
};
}
// eslint-disable-next-line no-secrets/no-secrets
/**
* @typedef {{
* body: import('estree').ImportDeclaration
* addSpecifier: (specifier: string, alias?: string) => ThisType<ImportDeclarationHelper>
* convertDefaultSpecifier: () => ThisType<ImportDeclarationHelper>
* }} ImportDeclarationHelper
* @param {string} source - The package name or source path to be imported.
* @param {string} [defaultImported] - The default specifier imported.
* @param {import('estree').ImportDeclaration} [body] -
* The body of the import declaration to start with.
* @returns {ImportDeclarationHelper} A helper object for manipulating the import declaration.
*/
function createImportDeclaration(source, defaultImported, body) {
const helper = {
/**
* @param {string} specifier - The value to be imported from the package.
* @param {string} [alias] - The local alias of the value.
* @returns {ThisType<ImportDeclarationHelper>} This helper with the added specifiers.
*/
addSpecifier(specifier, alias) {
this.convertDefaultSpecifier();
if (this.body.specifiers.some(s =>
s.local.name === alias || s.local.name === specifier,
))
return this;
this.body.specifiers.push({
imported: {
name: specifier,
type: 'Identifier',
},
local: {
name: alias ?? specifier,
type: 'Identifier',
},
type: 'ImportSpecifier',
});
return this;
},
/** @type {import('estree').ImportDeclaration} */
body: body ?? {
source: {
type: 'Literal',
value: source,
},
specifiers: defaultImported ? [{
local: { name: defaultImported, type: 'Identifier' },
type: 'ImportDefaultSpecifier',
}] : [],
type: 'ImportDeclaration',
},
/**
* Converts a default specifier to a specifier with a alias.
* @returns {ThisType<ImportDeclarationHelper>} -
* This helper with the converted default specifier.
* @example
* import eslit from 'eslit';
* // Is converted to
* import { default as eslit } from 'eslit';
*/
convertDefaultSpecifier() {
const specifier = this.body.specifiers.find(s =>
s.type === 'ImportDefaultSpecifier',
);
if (!specifier)
return this;
this.body.specifiers.splice(
this.body.specifiers.indexOf(specifier),
1,
);
return this.addSpecifier('default', specifier.local.name);
},
};
if (defaultImported &&
body &&
!body.specifiers.some(s =>
s.type === 'ImportDefaultSpecifier' &&
s.local.name === defaultImported,
))
helper.addSpecifier('default', defaultImported);
return helper;
}
const astUtils = {
createImportDeclaration,
createVariable,
findInArray,
stringToExpression,
toSpreadElement,
};
export default astUtils;

View File

@@ -0,0 +1,170 @@
import * as recast from 'recast';
/**
* @typedef {(
* import('estree').MemberExpression |
* import('estree').Identifier |
* import('estree').CallExpression |
* import('estree').NewExpression
* )} ExpressionOrIdentifier
* This type only includes the expressions used in the cli's config type
* @typedef {import('estree').VariableDeclaration} VariableDeclaration
* @typedef {import('estree').Identifier['name']} IdentifierName
* @typedef {VariableDeclaration['kind']} VariableKind
* @typedef {import('estree').VariableDeclarator['init']} VariableInit
* @typedef {import('estree').SpreadElement} SpreadElement
* @typedef {import('estree').Expression} Expression
* @typedef {import('estree').ArrayExpression} ArrayExpression
*/
/**
* @param {IdentifierName} identifier Nave of the variable identifier
* @param {VariableKind} [kind] Type of variable declaration
* @param {VariableInit} [init] Initial value of the variable
* @returns {VariableDeclaration} The variable declaration ast node object
*/
export function createVariable(identifier, kind = 'const', init) {
return {
type: 'VariableDeclaration',
kind,
declarations: [{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: identifier },
init,
}],
};
}
/**
* @param {string} string The expression in string
* @returns {ExpressionOrIdentifier | undefined} The expression or identifier node of that string (undefined if string is not a expression)
*/
export function stringToExpression(string) {
/** @type {ExpressionOrIdentifier} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const e = recast.parse(string).program.body[0].expression;
if (['MemberExpression', 'Identifier', 'CallExpression', 'NewExpression'].includes(e.type)) return e;
else return undefined;
}
/**
* @param {ArrayExpression} array The array node to search trough
* @param {ExpressionOrIdentifier | SpreadElement} element The element to be search
* @returns {ExpressionOrIdentifier | undefined} The element of the array founded, undefined if it isn't found
*/
export function findInArray(array, element) {
/** @type {ExpressionOrIdentifier[]} */
// @ts-expect-error The array should have just tge type above
element = element.type === 'SpreadElement' ? element.argument : element;
/** @type {ExpressionOrIdentifier[]} */
// @ts-expect-error The array is filtered to have the type above
const filteredElements = array.elements
.map(n => {
if (n?.type === 'SpreadElement') return n.argument;
return n;
}).filter(n => n && n.type === element.type);
const toStringElements = filteredElements.map(n => recast.print(n).code);
const toStringElement = recast.print(element).code;
const idx = toStringElements.findIndex(e => e === toStringElement);
return filteredElements[idx];
}
/**
* @param {ExpressionOrIdentifier} expression The expression to be spread
* @returns {SpreadElement} The spread element node
*/
export function toSpreadElement(expression) {
return {
type: 'SpreadElement',
argument: expression,
};
}
/**
* @typedef {{
* body: import('estree').ImportDeclaration
* addSpecifier: (specifier: string, alias?: string) => ThisType<ImportDeclarationHelper>
* convertDefaultSpecifier: () => ThisType<ImportDeclarationHelper>
* }} ImportDeclarationHelper
* @param {string} source The package name or source path to be imported
* @param {string} [defaultImported] The default specifier imported
* @param {import('estree').ImportDeclaration} [body] The body of the import declaration to start with
* @returns {ImportDeclarationHelper} A helper object for manipulating the import declaration
*/
export function createImportDeclaration(source, defaultImported, body) {
const helper = {
/** @type {import('estree').ImportDeclaration} */
body: body ?? {
type: 'ImportDeclaration',
specifiers: defaultImported ? [{
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: defaultImported },
}] : [],
source: {
type: 'Literal',
value: source,
},
},
/**
* Converts a default specifier to a specifier with a alias.
* @example
* import eslit from 'eslit';
* // Is converted to
* import { default as eslit } from 'eslit';
* @returns {ThisType<ImportDeclarationHelper>} This helper with the converted default specifier
*/
convertDefaultSpecifier() {
const specifier = this.body.specifiers.find(s => s.type === 'ImportDefaultSpecifier');
if (!specifier)
return this;
this.body.specifiers.splice(
this.body.specifiers.indexOf(specifier),
1,
);
return this.addSpecifier('default', specifier.local.name);
},
/**
* @param {string} specifier The value to be imported from the package
* @param {string} [alias] The local alias of the value
* @returns {ThisType<ImportDeclarationHelper>} This helper with the added specifiers
*/
addSpecifier(specifier, alias) {
this.convertDefaultSpecifier();
if (this.body.specifiers.find(s => s.local.name === alias || s.local.name === specifier))
return this;
this.body.specifiers.push({
type: 'ImportSpecifier',
imported: {
type: 'Identifier',
name: specifier,
},
local: {
type: 'Identifier',
name: alias ?? specifier,
},
});
return this;
},
};
if (defaultImported && body && !body.specifiers.find(s => s.type === 'ImportDefaultSpecifier' && s.local.name === defaultImported)) {
helper.addSpecifier('default', defaultImported);
}
return helper;
}
export default {
createVariable,
stringToExpression,
toSpreadElement,
findInArray,
createImportDeclaration,
};

View File

@@ -1,8 +0,0 @@
/**
* @param {string} str - The string to capitalize.
* @returns {string} The capitalized string.
*/
export default function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}

View File

@@ -1,14 +1,12 @@
/**
* @param {import('../types').Package[]} packages - Package list.
* @returns {number} Number of packages' configs.
* @param {import('../types').Package[]} packages - Package list
* @returns {number} Number of packages' configs
*/
function packagesWithConfigs(packages) {
return packages.map(p =>
[...p.config?.values() ?? []]
.filter(options => options.length > 0).length,
[...p.config?.values() ?? []].filter((options) => options.length > 0).length,
).reduce((partial, sum) => partial + sum, 0);
}
const count = { packagesWithConfigs };
export default count;
export default { packagesWithConfigs };

View File

@@ -1,17 +1,15 @@
// eslint-disable-next-line no-secrets/no-secrets
/**
* JSDoc types lack a non-null assertion.
* @template T
* @param {T} value - The value which to assert against null or undefined.
* @returns {NonNullable<T>} The said value.
* @throws {TypeError} If the value is unexpectedly null or undefined.
* @param {T} value The value which to assert against null or undefined
* @returns {NonNullable<T>} The said value
* @throws {TypeError} If the value is unexpectedly null or undefined
* @author Jimmy Wärting - https://github.com/jimmywarting
* @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031
* @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966
* @author Jimmy Wärting - https://github.com/jimmywarting
*/
export default function notNull(value) {
// Use `==` to check for both null and undefined
if (value === null || value === undefined)
throw new Error('did not expect value to be null or undefined');
if (value == null) throw new Error('did not expect value to be null or undefined');
return value;
}

View File

@@ -0,0 +1,10 @@
/**
* @param {string} str - The string to capitalize
* @returns {string} The capitalized string
*/
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
export default { capitalize };

View File

@@ -1,29 +1,27 @@
/* eslint-disable no-console */
/* eslint-disable max-classes-per-file */
import { readFile, writeFile } from 'node:fs/promises';
import { existsSync, readFileSync } from 'node:fs';
import { exec } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { parse, prettyPrint } from 'recast';
import { exec } from 'node:child_process';
import { createSpinner } from 'nanospinner';
import c from 'picocolors';
import * as recast from 'recast';
import { readFile, writeFile } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
/**
* @type {import('./types').PackageManagerHandler}
*/
class CommandHandler {
/** @type {((path: string, packages: string[]) => string | Promise<string>) | undefined} */
checker;
/** @type {string} */
command;
/** @type {((path: string, packages: string[]) => string | Promise<string>) | undefined} */
checker;
/**
* @param {string} command - What command to use to install.
* @param {(path: string, packages: string[]) => string | Promise<string>} [checker] -
* Checks if a argument should be passed.
* @param {string} command What command to use to install
* @param {(path: string, packages: string[]) => string | Promise<string>} [checker] Checks if a argument should be passed
*/
constructor(command, checker) {
this.command = command;
@@ -31,20 +29,20 @@ class CommandHandler {
}
/**
* @param {string} path - The path to run the command.
* @param {string[]} packages - The packages to be added on the command.
* @param {string} path The path to run the command
* @param {string[]} packages The packages to be added on the command
* @returns {Promise<void>}
*/
async install(path, packages) {
if (this.checker)
this.command += await this.checker(path, packages);
return new Promise((res) => {
const spinner = createSpinner(`Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}`).start();
try {
// eslint-disable-next-line security/detect-child-process
const child = exec(`${this.command} ${packages.join(' ')}`, { cwd: path });
child.stdout?.on('data', chunk => spinner.update({
child.stdout?.on('data', (chunk) => spinner.update({
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
text: `Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}\n ${c.dim(chunk)}`,
}));
@@ -55,8 +53,7 @@ class CommandHandler {
});
}
catch (error) {
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-confusing-void-expression
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
res(console.error(`Error while installing the packages with ${this.command} ${c.dim(packages.join(' '))} on ${path}: ${error}`));
}
});
@@ -66,49 +63,53 @@ class CommandHandler {
/**
* @type {import('./types').PackageManagerHandler}
*/
const DenoHandler = {
class DenoHandler {
/**
* @param {string} path - The path to run the command.
* @param {string[]} packages - The packages to be added on the command.
* @param {string} path The path to run the command
* @param {string[]} packages The packages to be added on the command
* @returns {Promise<void>}
*/
async install(path, packages) {
const configPath = join(path, 'eslint.config.js');
// eslint-disable-next-line security/detect-non-literal-fs-filename
if (!existsSync(configPath)) return;
// eslint-disable-next-line security/detect-non-literal-fs-filename
const configFile = await readFile(configPath, 'utf8');
/** @type {{program: import('estree').Program}} */
/** @type {{program: import('estree').Program}}*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { program: ast } = parse(
configFile,
{ parser: (await import('recast/parsers/babel.js')) },
);
const { program: ast } = recast.parse(configFile, { parser: (await import('recast/parsers/babel.js')) });
ast.body.map((node) => {
if (node.type !== 'ImportDeclaration') return node;
if (packages.includes(node.source.value?.toString() ?? ''))
if (packages.includes(node.source.value?.toString() ?? '')) {
node.source.value = `npm:${node.source.value}`;
}
return node;
});
// eslint-disable-next-line security/detect-non-literal-fs-filename
await writeFile(configPath, prettyPrint(ast).code, 'utf8');
await writeFile(configPath, recast.prettyPrint(ast).code, 'utf-8');
console.log(c.green('Added npm: specifier to dependencies'));
},
};
}
}
export default class PackageInstaller {
/**
* @typedef {Map<string, string[]>} PackagesMap
* @type {PackagesMap}
*/
packagesMap;
/**
* @typedef {{
* name: import('./types').PackageManagerName
* description: string
* handler: import('./types').PackageManagerHandler
* name: import('./types').PackageManagerName
* description: string
* handler: import('./types').PackageManagerHandler
* }} PackageManager
* @type {PackageManager}
*/
@@ -118,51 +119,40 @@ export default class PackageInstaller {
* @type {Record<import('./types').PackageManagerName, PackageManager>}
*/
packageManagers = {
deno: {
name: 'deno',
description: 'Adds npm: specifiers to the eslint.config.js file',
handler: new DenoHandler(),
},
bun: {
name: 'bun',
description: 'Uses bun install',
handler: new CommandHandler('bun install'),
name: 'bun',
},
deno: {
description: 'Adds npm: specifiers to the eslint.config.js file',
handler: DenoHandler,
name: 'deno',
},
npm: {
description: 'Uses npm install',
handler: new CommandHandler('npm install --save-dev'),
name: 'npm',
},
pnpm: {
name: 'pnpm',
description: 'Uses pnpm install',
handler: new CommandHandler('pnpm install --save-dev', (path) => {
if (
// eslint-disable-next-line security/detect-non-literal-fs-filename
existsSync(join(path, 'pnpm-workspace.yaml')) &&
// eslint-disable-next-line security/detect-non-literal-fs-filename
existsSync(join(path, 'package.json'))
)
if (existsSync(join(path, 'pnpm-workspace.yaml')) && existsSync(join(path, 'package.json')))
return ' -w';
return '';
else return '';
}),
name: 'pnpm',
},
yarn: {
name: 'yarn',
description: 'Uses yarn add',
handler: new CommandHandler('yarn add --dev'),
name: 'yarn',
},
npm: {
name: 'npm',
description: 'Uses npm install',
handler: new CommandHandler('npm install --save-dev'),
},
};
/**
* @typedef {Map<string, string[]>} PackagesMap
* @type {PackagesMap}
*/
packagesMap;
/**
* @param {PackagesMap} packagesMap - The map of directories and packages to be installed.
* @param {string} root - Root directory path.
* @param {PackagesMap} packagesMap The map of directories and packages to be installed
* @param {string} root Root directory path
*/
constructor(packagesMap, root) {
this.packagesMap = packagesMap;
@@ -170,68 +160,51 @@ export default class PackageInstaller {
}
/**
* @param {string} root - Root directory path.
* @returns {PackageManager} The package manager detected;.
* @param {string} root Root directory path
* @returns {PackageManager} The package manager detected;
* @private
*/
// eslint-disable-next-line complexity
detectPackageManager(root) {
/** @type {(...path: string[]) => boolean} */
function exists(...path) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
return existsSync(join(root, ...path));
}
const exists = (...path) => existsSync(join(root, ...path));
switch (true) {
case exists('deno.json'):
case exists('deno.jsonc'): {
case exists('deno.jsonc'):
return this.packageManagers.deno;
}
case exists('bun.lockb'): {
case exists('bun.lockb'):
return this.packageManagers.bun;
}
case exists('pnpm-lock.yaml'): {
case exists('pnpm-lock.yaml'):
return this.packageManagers.pnpm;
}
case exists('yarn.lock'): {
case exists('yarn.lock'):
return this.packageManagers.yarn;
}
case exists('package-lock.json'): {
case exists('package-lock.json'):
return this.packageManagers.npm;
}
case exists('package.json'): {
case exists('package.json'):
/** @type {{packageManager?: string}} */
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-case-declarations
const { packageManager } = JSON.parse(
// eslint-disable-next-line security/detect-non-literal-fs-filename
readFileSync(join(root, 'package.json'), 'utf8'),
);
const { packageManager } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
if (!packageManager) return this.packageManagers.npm;
if (packageManager.includes('pnpm'))
return this.packageManagers.pnpm;
if (packageManager.includes('yarn'))
return this.packageManagers.yarn;
if (packageManager.includes('npm'))
return this.packageManagers.npm;
if (packageManager.includes('pnpm')) return this.packageManagers.pnpm;
if (packageManager.includes('yarn')) return this.packageManagers.yarn;
if (packageManager.includes('npm')) return this.packageManagers.npm;
return this.packageManagers.npm;
}
else return this.packageManagers.npm;
default: { return this.packageManagers.npm;
}
default: return this.packageManagers.npm;
}
}
async install() {
for (const [path, packages] of this.packagesMap)
// eslint-disable-next-line no-await-in-loop
for (const [path, packages] of this.packagesMap) {
await this.packageManager.handler.install(path, packages);
}
}
}

View File

@@ -1,72 +1,61 @@
import type { OptionValues } from 'commander';
type PackageManagerName = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn';
export type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno';
interface PackageManagerHandler {
install(path: string, packages: string[]): Promise<void> | void,
}
type Config = {
description?: string,
manual: true,
name: string,
options: [{
configs?: string[],
detect?: undefined,
name: 'yes',
packages?: { [key: string]: ([string, string] | string)[] | string, },
presets?: string[],
rules?: string[],
}],
type: 'confirm',
} | {
description?: string,
manual?: boolean,
name: string,
options: {
configs?: string[],
detect?: string[] | true,
name: string,
packages?: { [key: string]: ([string, string] | string)[] | string, },
presets?: string[],
rules?: string[],
}[],
type: 'multiple' | 'single',
};
type CliArgs = {
configs: Config[],
dir: string,
installPkgs?: PackageManagerName | boolean,
mergeToRoot?: boolean,
packages?: string[],
export type CliArgs = {
packages?: string[]
mergeToRoot?: boolean
installPkgs?: boolean | PackageManagerName
dir: string
} & OptionValues;
interface ConfigFile {
configs: string[],
content?: string,
imports: Map<string, ([string, string] | string)[] | string>,
path: string,
presets: string[],
rules: string[],
}
interface Package {
config?: Map<string, string[]>,
configFile?: ConfigFile,
directories: string[],
files: string[],
name: string,
path: string,
root?: boolean,
}
export type {
CliArgs,
Config,
ConfigFile,
Package,
PackageManagerHandler,
PackageManagerName,
export type Config = {
name: string
type: 'single' | 'multiple'
manual?: boolean
description?: string
options: {
name: string
packages?: Record<string, string | (string | [string, string])[]>
configs?: string[]
rules?: string[]
presets?: string[]
detect?: string[] | true
}[]
} | {
name: string
type: 'confirm'
manual: true
description?: string
options: [{
name: 'yes'
packages?: Record<string, string | (string | [string, string])[]>
configs?: string[]
rules?: string[]
presets?: string[]
detect?: undefined
}]
};
export interface Package {
root?: boolean
name: string
path: string
files: string[]
directories: string[]
config?: Map<string, string[]>
configFile?: ConfigFile
}
export interface ConfigFile {
path: string
imports: Map<string, string | (string | [string, string])[]>
configs: string[]
presets: string[]
rules: string[]
content?: string
}
export interface PackageManagerHandler {
install(path: string, packages: string[]): Promise<void> | void
}

View File

@@ -1,41 +1,36 @@
/* eslint-disable security/detect-non-literal-fs-filename */
import path, { join } from 'node:path';
import { existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import picomatch from 'picomatch';
import { existsSync } from 'node:fs';
import YAML from 'yaml';
import path, { join } from 'node:path';
import glob from 'picomatch';
import picomatch from 'picomatch';
/**
* @template T
* @param {Promise<T>} promise - The async function to try running.
* @returns {Promise<T | null>} - Returns the result of the async function, or null if it errors.
* @param {Promise<T>} promise - The async function to try running
* @returns {Promise<T | null>} - Returns the result of the async function, or null if it errors
*/
async function tryRun(promise) {
try {
return await promise;
}
catch {
catch (err) {
return null;
}
}
/**
* @param {string} directory - The directory to find .gitignore and .eslintignore.
* @returns {Promise<string[]>} - List of ignore glob patterns.
* @param {string} directory - The directory to find .gitignore and .eslintignore
* @returns {Promise<string[]>} - List of ignore glob patterns
*/
async function getIgnoredFiles(directory) {
const gitIgnore = (
await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ??
'')
const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '')
.split('\n')
.filter(p => p && !p.startsWith('#'))
.map(p => join(directory, '**', p));
const eslintIgnore = (
await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ??
'')
const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '')
.split('\n')
.filter(p => p && !p.startsWith('#'))
.map(p => join(directory, '**', p));
@@ -52,19 +47,19 @@ async function getPackageName(directory) {
const file = await fs.readFile(join(directory, 'package.json'), 'utf8');
/** @type {{name?: string}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const object = JSON.parse(file);
const obj = JSON.parse(file);
if (object.name) return object.name;
if (obj.name) return obj.name;
}
return path.normalize(directory).split('/').at(-1) ?? directory;
}
export default class Workspace {
/**
* @param {string} directory -
* The directory to get the workspace from.
* @param {string[] | false} [packagePatterns] -
* List of package patterns (`false` to explicitly tell that this workspace is not a monorepo).
* @param {string} directory - The directory to get the workspace from
* @param {string[] | false} [packagePatterns]
* List of package patterns (`false` to explicitly tell that this workspace is not a monorepo)
*/
constructor(directory, packagePatterns) {
this.dir = directory;
@@ -72,93 +67,125 @@ export default class Workspace {
}
/**
* @returns {Promise<string[]>} - List of packages on a directory;.
* @param {string} [directory] - The directory to work on
* @param {string[]} [ignores] - Glob patterns to ignore
* @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory
*/
async getPaths(directory = this.dir, ignores = []) {
ignores.push(
...[
'.git',
'.dist',
'.DS_Store',
'node_modules',
].map((f) => join(directory, f)),
...await getIgnoredFiles(directory),
);
const paths = (await fs.readdir(directory))
.map((f) => path.normalize(join(directory, f)))
.filter((p) => !glob.isMatch(p, ignores));
/** @type {string[]} */
const files = [];
/** @type {string[]} */
const directories = [];
for (const path of paths) {
if ((await fs.lstat(path)).isDirectory()) {
const subPaths = await this.getPaths(path, ignores);
directories.push(path, ...subPaths.directories);
files.push(...subPaths.files);
}
else {
files.push(path);
}
}
return {
files: files.map(p => path.normalize(p.replace(this.dir, './'))),
directories: directories.map(p => path.normalize(p.replace(this.dir, './'))),
};
}
/**
* @returns {Promise<string[]>} - List of packages on a directory;
*/
async getPackagePatterns() {
/** @type {string[]} */
const packagePatterns = [];
let packagePatterns = [];
const pnpmWorkspace =
existsSync(join(this.dir, 'pnpm-workspace.yaml'))
? 'pnpm-workspace.yaml'
: (existsSync(join(this.dir, 'pnpm-workspace.yml'))
? 'pnpm-workspace.yml'
: null);
: existsSync(join(this.dir, 'pnpm-workspace.yml'))
? 'pnpm-workspace.yml'
: null;
if (pnpmWorkspace) {
const pnpmWorkspaceYaml = await fs.readFile(
join(this.dir, pnpmWorkspace),
'utf8',
);
const pnpmWorkspaceYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8');
/** @type {{packages?: string[]}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const pnpmWorkspaceObject = YAML.parse(pnpmWorkspaceYaml);
const pnpmWorkspaceObj = YAML.parse(pnpmWorkspaceYaml);
packagePatterns.push(...pnpmWorkspaceObject.packages ?? []);
packagePatterns.push(...(pnpmWorkspaceObj?.packages ?? []));
}
else if (existsSync(join(this.dir, 'package.json'))) {
const packageJson = await fs.readFile(
join(this.dir, 'package.json'),
'utf8',
);
const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8');
/** @type {{workspaces?: string[]}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const packageJsonObject = JSON.parse(packageJson);
const packageJsonObj = JSON.parse(packageJson);
packagePatterns.push(...packageJsonObject.workspaces ?? []);
packagePatterns.push(...(packageJsonObj?.workspaces ?? []));
}
return packagePatterns.map((p) => {
return packagePatterns.map(p => {
p = path.normalize(p);
p = p.startsWith('/') ? p.replace('/', '') : p;
p = p.endsWith('/') ? p.slice(0, -1) : p;
p = p.endsWith('/') ? p.slice(0, p.length - 1) : p;
return p;
});
}
/**
* @returns {Promise<import('./types').Package[]>} -
* The list of packages that exist in the workspace.
* @returns {Promise<import('./types').Package[]>} - The list of packages that exist in the workspace
*/
async getPackages() {
const paths = await this.getPaths();
/** @type {import('./types').Package} */
const rootPackage = {
directories: paths.directories,
files: paths.files,
root: true,
name: await getPackageName(this.dir),
path: this.dir,
root: true,
files: paths.files,
directories: paths.directories,
};
if (this.packagePatterns === false) return [rootPackage];
const packagePatterns =
this.packagePatterns ??
await this.getPackagePatterns();
const packagePaths = paths.directories.filter(d =>
picomatch.isMatch(d, packagePatterns),
);
const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns();
const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns));
/** @type {import('./types').Package[]} */
const packages = [];
for (const packagePath of packagePaths) {
packages.push({
directories: paths.directories
.filter(d => picomatch.isMatch(d, `${packagePath}/**/*`))
.map(d => d.replace(`${packagePath}/`, '')),
root: false,
path: join(this.dir, packagePath),
name: await getPackageName(join(this.dir, packagePath)),
files: paths.files
.filter(f => picomatch.isMatch(f, `${packagePath}/**/*`))
.map(f => f.replace(`${packagePath}/`, '')),
// eslint-disable-next-line no-await-in-loop
name: await getPackageName(join(this.dir, packagePath)),
path: join(this.dir, packagePath),
root: false,
directories: paths.directories
.filter(d => picomatch.isMatch(d, `${packagePath}/**/*`))
.map(d => d.replace(`${packagePath}/`, '')),
});
rootPackage.files = rootPackage.files
@@ -169,101 +196,51 @@ export default class Workspace {
}
return [rootPackage, ...packages];
}
/**
* @param {string} [directory] - The directory to work on.
* @param {string[]} [ignores] - Glob patterns to ignore.
* @returns {Promise<{files: string[], directories: string[]}>} -
* List of all files in the directory.
* @param {import('./types').Package[]} packages - Packages to be merged into root
* @returns {[import('./types').Package]} A array containing only the root package
*/
async getPaths(directory = this.dir, ignores = []) {
ignores.push(
...[
'.git',
'.dist',
'.DS_Store',
'node_modules',
].map(f => join(directory, f)),
...await getIgnoredFiles(directory),
);
mergePackages(packages) {
const pathsUnfiltered = await fs.readdir(directory);
const paths = pathsUnfiltered
.map(f => path.normalize(join(directory, f)))
.filter(p => !picomatch.isMatch(p, ignores));
/** @type {string[]} */
const files = [];
/** @type {string[]} */
const directories = [];
for (const p of paths) {
// eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member
if ((await fs.lstat(p)).isDirectory()) {
// eslint-disable-next-line no-await-in-loop
const subPaths = await this.getPaths(p, ignores);
directories.push(p, ...subPaths.directories);
files.push(...subPaths.files);
}
else {
files.push(p);
}
}
return {
directories: directories.map(p =>
path.normalize(p.replace(this.dir, './')),
),
files: files.map(p => path.normalize(p.replace(this.dir, './'))),
};
}
/**
* @param {import('./types').Package[]} packages - Packages to be merged into root.
* @returns {[import('./types').Package]} A array containing only the root package.
*/
static mergePackages(packages) {
const rootPackage = packages.find(p => p.root) ?? packages[0];
// TODO [>=1.0.0]: Refactor this to remove the use of Array#reduce()
// eslint-disable-next-line unicorn/no-array-reduce
const merged = packages.reduce((accumulated, package_) => {
const merged = packages.reduce((accumulated, pkg) => {
const files = [...new Set([
...accumulated.files,
...package_.files.map(f => join(package_.path, f)),
...pkg.files.map(f => join(pkg.path, f)),
]
.map(p => p.replace(`${rootPackage.path}/`, '')),
)];
const directories = [...new Set([
...accumulated.directories,
...package_.directories.map(d => join(package_.path, d)),
...pkg.directories.map(d => join(pkg.path, d)),
]
.map(p => p.replace(`${rootPackage.path}/`, ''))),
];
const mergedConfig = new Map();
for (const [config, options] of package_.config ?? []) {
const accumulatedOptions =
accumulated.config?.get(config) ??
[];
mergedConfig.set(
config,
[...new Set([...options, ...accumulatedOptions])],
);
for (const [config, options] of pkg.config ?? []) {
const accumulatedOptions = accumulated.config?.get(config) ?? [];
mergedConfig.set(config, [...new Set([...options, ...accumulatedOptions])]);
}
return {
config: mergedConfig,
directories,
files,
name: rootPackage.name,
path: rootPackage.path,
root: true,
path: rootPackage.path,
name: rootPackage.name,
files,
directories,
config: mergedConfig,
};
}, rootPackage);
return [merged];
}
}

View File

@@ -0,0 +1,8 @@
# @eslit/config
## 0.2.0
### Minor Changes
- 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. ([#8](https://github.com/LoredDev/ESLit/pull/8))
(fixes [#3](https://github.com/loreddev/eslit/issues/3)).

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 @@
{
"extends": "../../tsconfig.json",
"exclude": ["./node_modules/**", "./dist/**"],
"include": ["**/*.ts", "**/*.js"],
"include": ["./index.d.ts", "./src/**/*.ts", "./src/**/*.js"],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@eslegant/js",
"version": "0.3.0",
"name": "@eslit/config",
"version": "0.2.0",
"description": "",
"main": "index.js",
"module": "./src/index.js",
@@ -9,11 +9,11 @@
"src",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLegant",
"homepage": "https://github.com/LoredDev/ESLit",
"exports": {
"default": "./src/index.js",
"import": "./src/index.js",
"types": "./src/index.d.ts"
"types": "./index.d.ts"
},
"type": "module",
"types": "./src/index.js",
@@ -22,9 +22,9 @@
"lint": "eslint ."
},
"repository": {
"directory": "configs/js",
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLegant"
"url": "https://github.com/LoredDev/ESLit"
},
"author": {
"email": "contact.guz013@gmail.com",
@@ -34,25 +34,17 @@
"license": "MIT",
"devDependencies": {
"@types/eslint__js": "^8.42.0",
"@types/node": "^20.5.3",
"eslint": "^8.47.0",
"@types/node": "^20.4.2",
"eslint": "^8.45.0",
"typescript": "^5.1.6"
},
"dependencies": {
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "^8.47.0",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-i": "2.28.0-2",
"eslint-plugin-jsdoc": "^46.5.0",
"eslint-plugin-n": "^16.0.2",
"eslint-plugin-no-secrets": "^0.8.9",
"eslint-plugin-perfectionist": "^1.5.1",
"eslint-plugin-security": "^1.7.1",
"eslint-plugin-unicorn": "^48.0.1",
"globals": "^13.21.0"
"@eslint/eslintrc": "^2.1.0",
"@eslint/js": "^8.45.0",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint-plugin-jsdoc": "^46.4.4",
"globals": "^13.20.0"
},
"peerDependencies": {
"eslint": "^8.45.0",

View File

@@ -0,0 +1,26 @@
import type { ESLint } from 'eslint';
/**
* @see {@link https://www.npmjs.org/package/eslint-plugin-jsdoc npm package}
*
* @summary JSDoc specific linting rules for ESLint.
*
* ---
* **Note:** Types in this project where overridden to be compatible with ESLint new flat
* config types. ESlint already has backwards compatibility for plugins not created in the
* new flat config.
*/
declare module 'eslint-plugin-jsdoc' {
interface jsDocESlintPlugin extends ESLint.Plugin {
configs: ESLint.Plugin['configs'] & {
recommended: ESLint.ConfigData
'recommended-error': ESLint.ConfigData
'recommended-typescript': ESLint.ConfigData
'recommended-typescript-error': ESLint.ConfigData
'recommended-typescript-flavor': ESLint.ConfigData
'recommended-typescript-flavor-error': ESLint.ConfigData
}
}
declare const plugin: jsDocESlintPlugin;
export default plugin;
}

10
packages/config/src/@types/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
declare module 'globals' {
const globals: {
builtin: Record<string, boolean>
browser: Record<string, boolean>
node: Record<string, boolean>
nodeBuiltin: Record<string, boolean>
commonjs: Record<string, boolean>
};
export default globals;
}

View File

@@ -1,53 +1,46 @@
/**
* @file
* Type declaration for the `@typescript-eslint/eslint-plugin` and
* `@typescript-eslint/parser` packages in a attempt to make it
* compatible with the new flat config.
* @license MIT
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
import type { ESLint, Linter } from 'eslint';
/**
* @see {@link https://www.npmjs.com/package/@typescript-eslint/eslint-plugin npm package}
*
* @summary An ESLint plugin which provides lint rules for TypeScript codebases.
*
* ---
* **Note:** Types in this project where overridden to be compatible with ESLint
* new flat config types. ESlint already has backwards compatibility for plugins
* not created in the new flat config.
* @see {@link https://www.npmjs.com/package/@typescript-eslint/eslint-plugin npm package}
* **Note:** Types in this project where overridden to be compatible with ESLint new flat
* config types. ESlint already has backwards compatibility for plugins not created in the
* new flat config.
*/
declare module '@typescript-eslint/eslint-plugin' {
interface typescriptEslintPlugin extends ESLint.Plugin {
configs: {
'eslint-recommended': {
rules: Linter.RulesRecord,
},
recommended: {
rules: Linter.RulesRecord,
},
rules: Linter.RulesRecord
}
'recommended-requiring-type-checking': {
rules: Linter.RulesRecord,
},
rules: Linter.RulesRecord
}
'eslint-recommended': {
rules: Linter.RulesRecord
}
strict: {
rules: Linter.RulesRecord,
},
},
rules: Linter.RulesRecord
}
}
}
declare const plugin: typescriptEslintPlugin;
export default plugin;
}
/**
* @summary An ESLint parser which leverages TypeScript ESTree to
* allow for ESLint to lint TypeScript source code.
* @see {@link https://www.npmjs.com/package/@typescript-eslint/parser npm package}
*
* @summary An ESLint parser which leverages TypeScript ESTree to allow for ESLint
* to lint TypeScript source code.
*
* ---
* **Note:** Types in this project where overridden to be compatible
* with ESLint new flat config types. ESlint already has backwards
* compatibility for parsers not created in the new flat config.
* @see {@link https://www.npmjs.com/package/@typescript-eslint/parser npm package}
* **Note:** Types in this project where overridden to be compatible with ESLint new flat
* config types. ESlint already has backwards compatibility for parsers not created in the
* new flat config.
*/
declare module '@typescript-eslint/parser' {
declare const parser: Linter.ParserModule;

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

@@ -0,0 +1,97 @@
/**
* Formatting rules/configuration overrides for Javascript and Typescript
*
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
rules: {
// Formatting rules
'brace-style': 'off',
'@typescript-eslint/brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
'indent': 'off',
'@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,
MemberExpression: 1,
FunctionDeclaration: { parameters: 1, body: 1 },
FunctionExpression: { parameters: 1, body: 1 },
CallExpression: { arguments: 1 },
ArrayExpression: 1,
ObjectExpression: 1,
ImportDeclaration: 1,
flatTernaryExpressions: false,
offsetTernaryExpressions: true,
ignoreComments: false,
ignoredNodes: [
'TemplateLiteral *',
'JSXElement',
'JSXElement > *',
'JSXAttribute',
'JSXIdentifier',
'JSXNamespacedName',
'JSXMemberExpression',
'JSXSpreadAttribute',
'JSXExpressionContainer',
'JSXOpeningElement',
'JSXClosingElement',
'JSXFragment',
'JSXOpeningFragment',
'JSXClosingFragment',
'JSXText',
'JSXEmptyExpression',
'JSXSpreadChild',
'TSTypeParameterInstantiation',
'FunctionExpression > .params[decorators.length > 0]',
'FunctionExpression > .params > :matches(Decorator, :not(:first-child))',
'ClassBody.body > PropertyDefinition[decorators.length > 0] > .key',
],
}],
'keyword-spacing': 'off',
'@typescript-eslint/keyword-spacing': ['error', { before: true, after: true }],
'lines-between-class-members': 'off',
'@typescript-eslint/lines-between-class-members': ['error'],
'no-extra-parens': 'off',
'@typescript-eslint/no-extra-parens': ['error', 'functions'],
'object-curly-spacing': 'off',
'@typescript-eslint/object-curly-spacing': ['error', 'always'],
'quotes': 'off',
'@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.ESLIT_SEMI ?? 'always'],
'space-before-function-paren': 'off',
'@typescript-eslint/space-before-function-paren': ['error', {
anonymous: 'always',
named: 'never',
asyncArrow: 'always',
}],
'space-infix-ops': 'off',
'@typescript-eslint/space-infix-ops': 'error',
},
};
export default config;

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

@@ -0,0 +1,15 @@
import jsdoc from 'eslint-plugin-jsdoc';
/**
* JSDoc rules overrides
* @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,
},
};
export default config;

View File

@@ -0,0 +1,42 @@
import tsESlint from '@typescript-eslint/eslint-plugin';
import js from '@eslint/js';
/**
* Recommended configuration overrides of ESLit
*
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
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',
}],
'@typescript-eslint/ban-tslint-comment': 'error',
'@typescript-eslint/no-require-imports': 'error',
// Extension rules
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': 'error',
'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'error',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
},
};
export default config;

View File

@@ -0,0 +1,65 @@
import jsdoc from 'eslint-plugin-jsdoc';
/**
* Typescript specific configuration overrides
*
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
files: ['**/*.ts', '**/*.cts', '**/*.mts'],
// See plugins['jsdoc'] on index.js for more info on this error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
rules: {
// See plugins['jsdoc'] on index.js for more info on this error
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...jsdoc.configs['recommended-typescript-error'].rules,
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/class-literal-property-style': 'error',
'@typescript-eslint/consistent-generic-constructors': 'error',
'@typescript-eslint/consistent-indexed-object-style': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-exports': ['error'],
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', disallowTypeAnnotations: true }],
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/member-delimiter-style': ['error', { multiline: { delimiter: 'none' } }],
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
...(
/** @type {() => import('eslint').Linter.RulesRecord} */
() => {
/** @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 {
'@typescript-eslint/explicit-function-return-type': inferrableTypes === 'always' ? 'off' : 'error',
'@typescript-eslint/no-inferrable-types': inferrableTypes === 'always' ? 'off' : 'error',
};
}
else {
return {
'@typescript-eslint/explicit-function-return-type': inferrableTypes[1].returnValues ? 'off' : 'error',
'@typescript-eslint/no-inferrable-types': [
inferrableTypes[0] === 'always' ? 'off' : 'error',
{
ignoreParameters: inferrableTypes[1].parameters ?? false,
ignoreProperties: inferrableTypes[1].properties ?? false,
},
],
};
}
}
)(),
},
};
export default config;

View File

@@ -0,0 +1,15 @@
import { FlatCompat } from '@eslint/eslintrc';
import javascript from '@eslint/js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// mimic CommonJS variables
export const __filename = fileURLToPath(import.meta.url);
export const __dirname = path.dirname(__filename);
export const eslintrc = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: javascript.configs.recommended,
allConfig: javascript.configs.all,
});

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,14 +0,0 @@
# create-eslegant
## 0.2.0
### Minor Changes
- Renamed all packages from "eslit" to "eslegant" ([`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803))
### Patch Changes
- Created the "create-eslegant" package, as a _alias_ to the eslegant package, so it is compatible with `npm init` or `npm create` commands ([#14](https://github.com/LoredDev/ESLegant/pull/14))
- Updated dependencies [[`c061fdc`](https://github.com/LoredDev/ESLegant/commit/c061fdc8cd78e130e3e8f56b5633d0601fcb9b5e), [`10e5430`](https://github.com/LoredDev/ESLegant/commit/10e543094f4e5d3c9f3c0ea91fd24ad42888a9b0), [`b257ed0`](https://github.com/LoredDev/ESLegant/commit/b257ed000fad0a06c1152c7d246e3e46216154d4), [`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803)]:
- @eslegant/cli@0.2.0

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env node
import process from 'node:process';
import Cli from '@eslegant/cli';
const cli = new Cli({
// eslint-disable-next-line unicorn/no-await-expression-member
configs: (await import('./configs.js')).default,
dir: process.cwd(),
});
await cli.run();

View File

@@ -1,33 +0,0 @@
{
"name": "create-eslegant",
"version": "0.2.0",
"description": "",
"keywords": [],
"author": {
"email": "contact.guz013@gmail.com",
"name": "Gustavo \"Guz\" L. de Mello",
"url": "https://guz.one"
},
"files": [
"bin.js",
"configs.js"
],
"dependencies": {
"@eslegant/cli": "workspace:*"
},
"homepage": "https://github.com/LoredDev/ESLegant",
"type": "module",
"repository": {
"directory": "packages/create-eslegant",
"type": "git",
"url": "https://github.com/LoredDev/ESLegant"
},
"bin": "./bin.js",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^20.5.3"
}
}

View File

@@ -1,14 +0,0 @@
# eslegant
## 0.2.0
### Minor Changes
- Renamed all packages from "eslit" to "eslegant" ([`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803))
### Patch Changes
- Created the ESLegant package, being now the actual command that runs the CLI with the ESLegant's configs ([#14](https://github.com/LoredDev/ESLegant/pull/14))
- Updated dependencies [[`c061fdc`](https://github.com/LoredDev/ESLegant/commit/c061fdc8cd78e130e3e8f56b5633d0601fcb9b5e), [`10e5430`](https://github.com/LoredDev/ESLegant/commit/10e543094f4e5d3c9f3c0ea91fd24ad42888a9b0), [`b257ed0`](https://github.com/LoredDev/ESLegant/commit/b257ed000fad0a06c1152c7d246e3e46216154d4), [`3f773f5`](https://github.com/LoredDev/ESLegant/commit/3f773f56363de943dc55b358f6f1767398c2b803)]:
- @eslegant/cli@0.2.0

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env node
import process from 'node:process';
import Cli from '@eslegant/cli';
const cli = new Cli({
// eslint-disable-next-line unicorn/no-await-expression-member
configs: (await import('./configs.js')).default,
dir: process.cwd(),
});
await cli.run();

View File

@@ -1,37 +0,0 @@
/** @type {import('@eslegant/cli').Config[]} */
const cliConfig = [
{
description: 'The UI frameworks being used in the project',
name: 'framework',
options: [
{
configs: ['svelte.recommended'],
detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'],
name: 'svelte',
packages: { svelte: 'svelte' },
},
{
configs: ['vue.recommended'],
detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'],
name: 'vue',
packages: {
svelte: ['hello'],
vue: ['vue', ['hello', 'world']],
},
},
],
type: 'multiple',
},
{
manual: true,
name: 'strict',
options: [{
configs: ['config.strict'],
name: 'yes',
packages: { eslint: 'config', svelte: ['test1'] },
}],
type: 'confirm',
},
];
export default cliConfig;

View File

@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"exclude": ["./node_modules/**", "./dist/**"],
"include": ["**/*.ts", "**/*.js"],
}

View File

@@ -1,33 +0,0 @@
{
"name": "eslegant",
"version": "0.2.0",
"description": "",
"keywords": [],
"author": {
"email": "contact.guz013@gmail.com",
"name": "Gustavo \"Guz\" L. de Mello",
"url": "https://guz.one"
},
"files": [
"bin.js",
"configs.js"
],
"dependencies": {
"@eslegant/cli": "workspace:*"
},
"homepage": "https://github.com/LoredDev/ESLegant",
"type": "module",
"repository": {
"directory": "packages/eslegant",
"type": "git",
"url": "https://github.com/LoredDev/ESLegant"
},
"bin": "./bin.js",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^20.5.3"
}
}

1229
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
packages:
- 'packages/*'
- 'fixtures/*'
- 'configs/*'

View File

@@ -13,6 +13,6 @@
"alwaysStrict": true,
"outDir": "./dir"
},
"include": ["./eslint.config.js", "./commitlint.config.cjs"],
"include": ["eslint.config.js", "commitlint.config.cjs"],
"exclude": ["./node_modules/**", "./dist/**"]
}