57 Commits

Author SHA1 Message Date
renovate[bot]
932d59bf38 chore(deps): update actions/github-script action to v7 2023-11-13 18:34:19 +00:00
Guz
504deabcb6 Merge pull request #21 from LoredDev/fix-linting-issues
refactor: ♻️ fix eslint errors in @eslegant/cli
2023-09-09 21:46:41 -03:00
Guz013
fa9667ef09 refactor: ♻️ fix eslint errors in @eslegant/cli
This fixes the constant eslint error and warnings in the repository.
It is mostly a temporally solution rather than a actual refactor, so
a lot of eslint-disable comments where used.
2023-09-09 21:40:53 -03:00
Guz013
263e1edb63 feat: browser compatibility checking 2023-09-09 18:30:16 -03:00
Guz013
3aed0f1708 refactor: ♻️ change constants handling 2023-09-09 18:29:28 -03:00
Guz013
4a1f38ff24 feat: new rules inspired by standardjs 2023-09-05 12:22:20 -03:00
Guz013
2e1914c733 feat: security related rules 2023-09-04 20:56:00 -03:00
Guz013
dcce924286 feat: add more NodeJS-specific rules 2023-09-04 18:42:15 -03:00
Guz013
8749e51c13 fix: 🐛 'off' rule level being overridden 2023-09-04 17:26:26 -03:00
Guz013
853b868a24 fix: 🐛 no-extra-parens conflicts 2023-09-04 17:25:24 -03:00
Guz013
f4e52b991c feat: default variant for the configs 2023-09-04 17:12:43 -03:00
Guz
15765c8e73 Merge pull request #18 from LoredDev/13-new-config-files
fix: missing commits
2023-09-04 16:51:05 -03:00
Guz
b41d515e7f chore: merge pull request #17
chore(deps): update actions/checkout action to v4
2023-09-04 16:46:07 -03:00
renovate[bot]
3ccab1d5b2 chore(deps): update actions/checkout action to v4 2023-09-04 19:44:33 +00:00
Guz
47eb42405b chore: merge pull request #16
feat/refactor: new configuration files
2023-09-04 16:44:16 -03:00
Guz013
d8fcf7bfe3 feat: group and sort imports rule 2023-09-04 16:43:14 -03:00
Guz013
71442056ce fix: 🐛 add globals to the core config 2023-09-04 16:41:32 -03:00
Guz013
da3429a868 chore: 🔧 add changeset 2023-09-04 16:38:38 -03:00
Guz013
2ca40e1ef1 fix: 🐛 misc fixes
Fixed problems config not being exported and used in the presets and
some rules not configured properly.
2023-09-02 20:33:13 -03:00
Guz013
3434620ff1 docs: 📚️ add docs to all files
Added documentation to all files, configs and presets. Following the new
strict documentation rules.
2023-09-02 20:31:57 -03:00
Guz013
f874ed622e feat: defineConfig helper function 2023-09-02 20:25:32 -03:00
Guz013
ac7c70e81c feat: rules variations 2023-09-02 08:19:33 -03:00
Guz013
ef6b399bb7 feat: eslint-plugin-perfectionist rules 2023-09-02 06:46:32 -03:00
Guz013
8ec97a8b66 feat: add eslint's max-len rule 2023-09-02 06:42:28 -03:00
Guz013
a2cc881053 feat: jsdoc rules and remove old config 2023-09-02 06:20:45 -03:00
Guz013
10c4155283 fix: 🐛 typescript-eslint missing formatting rules 2023-09-02 05:47:00 -03:00
Guz013
da8d4312a0 feat: eslint-plugin-import rules 2023-09-02 04:41:18 -03:00
Guz013
5dfa998860 feat: typescript-eslint rules 2023-09-02 03:05:33 -03:00
Guz013
8c86b748ad chore: 🔧 add vscode workspaces 2023-09-01 18:22:48 -03:00
Guz013
fc2613f874 chore!: 💥 🔧 rename @eslegant/config to @eslegant/js
Renamed @eslegant/config to @eslegant/js, reflecting the new structure and
purpose of it just having rules related to javascript and
typescript

Moved said package to the new ./configs/ directory. Packages under the
./packages/ directory will be related to ones that aren't related to
ESLint itself and/or its configuration.
2023-09-01 18:08:56 -03:00
Guz013
a64062f3a5 refactor: ♻️ type declarations and exports 2023-09-01 17:40:15 -03:00
Guz013
b6ebbf9eb8 chore: 🔧 commit overrides file (wip config) 2023-09-01 17:28:37 -03:00
Guz013
dd1d0276b0 feat: 🦄 unicorn rules 2023-09-01 17:27:27 -03:00
Guz013
5a6102ff45 fix: 🐛 config merging/extending 2023-09-01 15:10:37 -03:00
Guz013
8f77f4591f docs: 📚️ add small documentation for each config 2023-08-31 17:30:47 -03:00
Guz013
e20c7e6425 feat: strict preset 2023-08-31 17:12:45 -03:00
Guz013
88412b067d feat: "suggestions" and "problems" rules 2023-08-31 17:11:50 -03:00
Guz013
d5e30078d4 feat: new formatting rules 2023-08-31 13:10:21 -03:00
Guz013
b28c7d2c62 refactor!: 💥 ♻️ restructure the config and presets
Now each config is related to a specific language and or purpose,
having different "flavors" or levels of strictness for them

Presets now have the purpose of simply grouping said configs for easier
of them.

BREAKING CHANGE
2023-08-30 15:31:13 -03:00
Guz013
9043156913 chore: 🔧 fix packages names in changesets 2023-08-29 15:52:34 -03:00
Guz013
3f773f5636 chore: 🔧 add changeset 2023-08-29 15:50:25 -03:00
Guz013
aad3b68f65 ci: 👷 update actions with new name 2023-08-29 15:48:22 -03:00
Guz
c3b34735dd chore: merge #15 branch 2023-08-29 15:41:58 -03:00
Guz013
3c23f0b07c chore: 🔧 rename fixtures packages 2023-08-29 15:41:00 -03:00
Guz013
ff44d8b56e chore: 🔧 update links 2023-08-29 15:39:01 -03:00
Guz013
c1aa5d0235 refactor!: 💥 ♻️ rename packages
Renamed all packages and dependencies from "eslit" to "eslegant"

BREAKING CHANGE
2023-08-29 15:31:57 -03:00
Guz
0be127d50a Merge pull request #14 from LoredDev:improve-cli-api
Move configs array to it's own package
2023-08-29 15:14:47 -03:00
Guz013
c061fdc8cd feat!: 💥 export cli api and remove configs array
Removed the configs array from the cli package, as it now is handled by
the eslegant package.
Exported the Cli class so it can be run by other packages

BREAKING CHANGE
2023-08-29 15:08:29 -03:00
Guz013
26f29009d6 feat: create the eslegant package 2023-08-29 15:06:24 -03:00
Guz013
da21030000 feat: add eslint-plugin-i 2023-08-25 17:31:56 -03:00
Guz013
39d323a3ce ci: 👷 add scope to node setup 2023-08-23 12:09:24 -03:00
Guz013
3e866ee562 ci: 👷 fix npm token 2023-08-23 12:03:16 -03:00
Guz013
3bedc6d1ba ci: 👷 fix missing token 2023-08-23 11:55:11 -03:00
Guz013
64a45cd86a style: 🎨 lint project 2023-08-23 11:50:22 -03:00
Guz013
10e543094f chore(deps): 🔗 update dependencies 2023-08-23 11:45:04 -03:00
Guz013
b257ed000f fix(cli): 🐛 small fixes when prompts are canceled 2023-08-23 11:40:16 -03:00
github-actions[bot]
5537a71bf3 ci: 👷 update dev branch 2023-08-23 14:14:41 +00:00
104 changed files with 4380 additions and 1697 deletions

View File

@@ -0,0 +1,12 @@
---
"@eslegant/js": minor
---
Added rules for NodeJS environments, using the eslint-plugin-n and eslint-plugin-security.
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`).

View File

@@ -0,0 +1,5 @@
---
"@eslegant/js": minor
---
Added new ESLint rules inspired by StandardJS.

View File

@@ -0,0 +1,12 @@
---
"@eslegant/js": minor
---
New rules structure.
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.

View File

@@ -0,0 +1,6 @@
---
"@eslegant/cli": minor
---
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.
With this, the new command line interface that handles the actual configs of this repo is the "eslegant" package.

View File

@@ -0,0 +1,6 @@
---
"@eslegant/js": patch
"@eslegant/cli": patch
---
Updated dependencies

View File

@@ -0,0 +1,5 @@
---
"eslegant": patch
---
Created the ESLegant package, being now the actual command that runs the CLI with the ESLegant's configs

View File

@@ -0,0 +1,5 @@
---
"@eslegant/js": minor
---
Configs now export a `default` variation, where rule leves aren't overriden.

View File

@@ -0,0 +1,5 @@
---
"@eslegant/cli": patch
---
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.

View File

@@ -0,0 +1,5 @@
---
"create-eslegant": patch
---
Created the "create-eslegant" package, as a _alias_ to the eslegant package, so it is compatible with `npm init` or `npm create` commands

View File

@@ -0,0 +1,6 @@
---
"@eslegant/js": minor
---
New rules related to possible security vulnerabilities in JavaScript.
Provided by `eslint-plugin-security` and `eslint-plugin-no-secrets`

View File

@@ -0,0 +1,8 @@
---
"create-eslegant": minor
"eslegant": minor
"@eslegant/js": minor
"@eslegant/cli": minor
---
Renamed all packages from "eslit" to "eslegant"

View File

@@ -0,0 +1,5 @@
---
"@eslegant/js": patch
---
Renamed @eslegant/config to @eslegant/js

View File

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

View File

@@ -13,6 +13,10 @@ inputs:
required: false
type: boolean
default: true
scope:
required: false
type: string
default: '@eslegant'
runs:
using: composite
@@ -21,6 +25,8 @@ 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@v3
uses: actions/checkout@v4
with:
fetch-depth: 2
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 2

View File

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

View File

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

View File

@@ -11,14 +11,14 @@ on:
jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'loreddev/eslit' }}
if: ${{ github.event.workflow_run.conclusion == 'success' && github.repository == 'loreddev/eslegant' }}
name: Release preview
env:
HUSKY: 0
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -27,8 +27,10 @@ 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:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_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/eslit'
if: github.actor == 'renovate[bot]' && github.repository == 'loreddev/eslegant'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Configure Git
@@ -21,7 +21,7 @@ jobs:
git config --global user.email action@github.io
git config --global user.name 'Github changeset workflow'
- name: Generate changeset
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
const { promises: fs } = require("fs");

53
.vscode/project.code-workspace vendored Normal file
View File

@@ -0,0 +1,53 @@
{
"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,6 +23,11 @@
"yaml"
],
"cSpell.words": [
"ESLIT"
"eslegant",
"estree",
"nanospinner",
"picocolors",
"picomatch",
"sisteransi"
]
}

View File

@@ -0,0 +1,48 @@
# 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/>

5
configs/js/jsconfig.json Normal file
View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "@eslit/config",
"name": "@eslegant/js",
"version": "0.2.0",
"description": "",
"main": "index.js",
@@ -9,11 +9,11 @@
"src",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLit",
"homepage": "https://github.com/LoredDev/ESLegant",
"exports": {
"default": "./src/index.js",
"import": "./src/index.js",
"types": "./index.d.ts"
"types": "./src/index.d.ts"
},
"type": "module",
"types": "./src/index.js",
@@ -22,9 +22,9 @@
"lint": "eslint ."
},
"repository": {
"directory": "packages/config",
"directory": "configs/js",
"type": "git",
"url": "https://github.com/LoredDev/ESLit"
"url": "https://github.com/LoredDev/ESLegant"
},
"author": {
"email": "contact.guz013@gmail.com",
@@ -34,17 +34,25 @@
"license": "MIT",
"devDependencies": {
"@types/eslint__js": "^8.42.0",
"@types/node": "^20.4.2",
"eslint": "^8.45.0",
"@types/node": "^20.5.3",
"eslint": "^8.47.0",
"typescript": "^5.1.6"
},
"dependencies": {
"@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"
"@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"
},
"peerDependencies": {
"eslint": "^8.45.0",

View File

@@ -0,0 +1,24 @@
/**
* @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

@@ -0,0 +1,35 @@
/**
* @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

@@ -0,0 +1,34 @@
/**
* @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

@@ -0,0 +1,24 @@
/**
* @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

@@ -0,0 +1,24 @@
/**
* @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

@@ -0,0 +1,24 @@
/**
* @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;
}

18
configs/js/src/@types/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
/**
* @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,46 +1,53 @@
/**
* @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.
* **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}
*/
declare module '@typescript-eslint/eslint-plugin' {
interface typescriptEslintPlugin extends ESLint.Plugin {
configs: {
recommended: {
rules: Linter.RulesRecord
}
'recommended-requiring-type-checking': {
rules: Linter.RulesRecord
}
'eslint-recommended': {
rules: Linter.RulesRecord
}
rules: Linter.RulesRecord,
},
recommended: {
rules: Linter.RulesRecord,
},
'recommended-requiring-type-checking': {
rules: Linter.RulesRecord,
},
strict: {
rules: Linter.RulesRecord
}
}
rules: Linter.RulesRecord,
},
},
}
declare const plugin: typescriptEslintPlugin;
export default plugin;
}
/**
* @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.
* @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.
* **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}
*/
declare module '@typescript-eslint/parser' {
declare const parser: Linter.ParserModule;

View File

@@ -0,0 +1,64 @@
/**
* @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

@@ -0,0 +1,56 @@
/* 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

@@ -0,0 +1,51 @@
/* 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

@@ -0,0 +1,12 @@
/**
* @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

@@ -0,0 +1,116 @@
/* 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

@@ -0,0 +1,192 @@
/* 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;

322
configs/js/src/configs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,322 @@
/**
* @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

@@ -0,0 +1,32 @@
/* 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

@@ -0,0 +1,40 @@
/* 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

@@ -0,0 +1,35 @@
/* 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

@@ -0,0 +1,109 @@
/* 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

@@ -0,0 +1,61 @@
/* 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

@@ -0,0 +1,40 @@
/* 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

@@ -0,0 +1,453 @@
/* 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

@@ -0,0 +1,29 @@
/**
* @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,
};

22
configs/js/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
/**
* @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[];

27
configs/js/src/index.js Normal file
View File

@@ -0,0 +1,27 @@
/**
* @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

@@ -0,0 +1,104 @@
/**
* @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 };

51
configs/js/src/presets/index.d.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
/**
* @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

@@ -0,0 +1,12 @@
/**
* @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

@@ -0,0 +1,23 @@
/**
* @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

@@ -0,0 +1,23 @@
/**
* @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,6 +1,16 @@
import { configs, defineConfig, presets } from '@eslit/config';
import { configs, defineConfig, presets } from '@eslegant/js';
export default defineConfig([
...presets.default,
configs.environments.node,
...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'] }],
},
},
]);

View File

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

View File

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

View File

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

17
packages/cli/index.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
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';

1
packages/cli/index.js Normal file
View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "@eslit/cli",
"name": "@eslegant/cli",
"version": "0.1.0",
"description": "",
"scripts": {
@@ -16,16 +16,16 @@
"source": "./src/index.js",
"files": [
"src",
"index.js",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLit",
"homepage": "https://github.com/LoredDev/ESLegant",
"type": "module",
"repository": {
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLit"
"url": "https://github.com/LoredDev/ESLegant"
},
"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.3",
"recast": "^0.23.4",
"sisteransi": "^1.0.5",
"yaml": "^2.3.1"
},
"devDependencies": {
"@types/estree": "^1.0.1",
"@types/node": "^20.4.2",
"@types/node": "^20.5.3",
"@types/prompts": "^2.4.4"
},
"publishConfig": {

View File

@@ -1,34 +1,39 @@
import { Command } from 'commander';
import ConfigsProcessor from './configsProcessor.js';
import configs from './configs.js';
import Workspace from './workspace.js';
import c from 'picocolors';
/* eslint-disable no-console */
/* eslint-disable import/max-dependencies */
import process from 'node:process';
import path from 'node:path';
import { createSpinner } from 'nanospinner';
import count from './lib/count.js';
import { Command } from 'commander';
import { erase } from 'sisteransi';
import cardinal from 'cardinal';
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';
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 Workspace from './workspace.js';
import count from './lib/count.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')
@@ -41,115 +46,148 @@ export default class Cli {
...args,
};
this.args.dir = !this.args.dir.startsWith('/')
? path.join(process.cwd(), this.args.dir)
: this.args.dir;
this.args.dir = this.args.dir.startsWith('/')
? this.args.dir
: path.join(process.cwd(), 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())
.map(pkg => {
spinner.update({ text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}` });
pkg.config = processor.detectConfig(pkg);
return pkg;
let packages = await workspace.getPackages();
packages = packages.map((pkg) => {
spinner.update({
text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}`,
});
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`)}`,
});
const merge = this.args.mergeToRoot ?? packages.length > 1 ?
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const merge = this.args.mergeToRoot ?? (packages.length > 1
/** @type {{merge: boolean}} */
(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')),
? (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'))}`,
name: 'merge',
type: 'confirm',
})).merge : true;
// eslint-disable-next-line unicorn/no-await-expression-member
})).merge : true);
console.log(c.dim('\nPlease select which options you prefer\n'));
packages = await processor.questionConfig(
merge ? workspace.mergePackages(packages) : packages,
configs.filter(c => c.manual),
merge ? Workspace.mergePackages(packages) : packages,
configs.filter(config => config.manual),
);
const fileHandler = new ConfigsFile(configs, packages.find(c => c.root)?.path);
const fileHandler = new ConfigsFile(
configs,
packages.find(config => config.root)?.path,
);
for (const pkg of packages) {
pkg.configFile = fileHandler.generateObj(pkg);
pkg.configFile.content = await fileHandler.generate(pkg.configFile);
// eslint-disable-next-line no-await-in-loop
pkg.configFile.content = await ConfigsFile.generate(pkg.configFile);
/** @type {boolean} */
const shouldWrite =
/** @type {{write: boolean}} */
// eslint-disable-next-line no-await-in-loop
(await prompts({
type: 'confirm',
name: 'write',
initial: true,
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)}`,
initial: true,
name: 'write',
type: 'confirm',
// eslint-disable-next-line unicorn/no-await-expression-member
})).write;
stdout.write(ansi.erase.lines(pkg.configFile.content.split('\n').length + 2));
if (shouldWrite) await fileHandler.write(pkg.configFile.path, pkg.configFile.content);
stdout.write(
erase.lines(pkg.configFile.content.split('\n').length + 2),
);
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'} */
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;
// 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;
if (installPkgs === 'changePackage') {
/** @type {{manager: import('./types').PackageManagerName}} */
const prompt = await prompts({
name: 'manager',
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?',
choices: Object.values(installer.packageManagers).map(m => {
return { title: m.name, description: m.description, value: m.name };
}),
name: 'manager',
type: 'select',
});
installer.packageManager = installer.packageManagers[prompt.manager];
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'));
installPkgs = true;
}
if (installPkgs) await installer.install();
}
}

View File

@@ -1,19 +1,26 @@
import path from 'node:path';
import notNull from './lib/notNull.js';
import * as recast from 'recast';
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import astUtils from './lib/astUtils.js';
import process from 'node:process';
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';
/**
* @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
* @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.
*/
// 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));
@@ -23,33 +30,47 @@ 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
* @returns {string} The path to traverse
* @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.
*/
function getPathDepth(path1, root) {
const pathDepth = path1.replace(root, '').split('/').slice(1);
@@ -58,13 +79,12 @@ function getPathDepth(path1, root) {
}
export default class ConfigsWriter {
/** @type {string} */
root = process.cwd();
/**
* @param {import('./types').Config[]} configs The array of configs to construct from
* @param {string} [root] The root directory path
* @param {import('./types.js').Config[]} configs - The array of configs to construct from.
* @param {string} [root] - The root directory path.
*/
constructor(configs, root) {
this.configs = configs;
@@ -72,60 +92,58 @@ export default class ConfigsWriter {
}
/**
* @param {import('./types').Package} pkg The package to generate the config string from
* @returns {import('./types').ConfigFile} The config file object
* @param {Program} ast - The program ast to be manipulated.
* @returns {Promise<Program>} The final ast with the recreated default export.
* @private
*/
generateObj(pkg) {
/** @type {import('./types').ConfigFile} */
const configObj = {
path: path.join(pkg.path, 'eslint.config.js'),
imports: new Map(),
configs: [],
presets: [],
rules: [],
};
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');
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');
}
/** @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');
for (const [configName, optionsNames] of notNull(pkg.config)) {
if (!astExport) { ast.body.push(exportTemplateNode); return ast; }
const config = this.configs.find(c => c.name === configName);
if (!config) continue;
/** @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 options = config.options.filter(o => optionsNames.includes(o.name));
if (!options || options.length === 0) continue;
if (!oldExportValue) return ast;
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;
});
// @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',
});
configObj.imports = mergeImportsMaps(configObj.imports, new Map(Object.entries(imports.packages ?? {})));
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.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;
return ast;
}
/**
@@ -135,111 +153,36 @@ 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/astUtils.js').ExpressionOrIdentifier |
* import('estree').ObjectExpression |
* import('estree').SpreadElement
* import('./lib/ast-utils.js').ExpressionOrIdentifier |
* import('estree').ObjectExpression |
* import('estree').SpreadElement
* )} ConfigArrayElement
*/
/**
* @param {Program} ast The program ast to be manipulated
* @returns {Promise<Program>} The final ast with the recreated default export
* @private
*/
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 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
* 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.
* @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) {
static 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 exportNode = ast.body.find(n =>
n.type === 'ExportDefaultDeclaration',
);
const exportNodeIdx = ast.body.indexOf(exportNode);
/** @type {ArrayExpression} */
@@ -247,45 +190,61 @@ export default class ConfigsWriter {
const array = exportNode.declaration;
for (const e of elements) {
if (e.type !== 'ObjectExpression' && astUtils.findInArray(array, e)) continue;
array.elements.push(e);
if (!(
e.type !== 'ObjectExpression' &&
astUtils.findInArray(array, e)
))
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').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.js').ConfigFile['imports']} imports - The imports map to be used.
* @returns {Program} The final ast with the recreated default export.
*/
addPackageImports(ast, imports) {
static 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') {
specifiers.forEach(s => {
if (typeof s === 'string') return importDeclaration.addSpecifier(s);
else return importDeclaration.addSpecifier(s[0], s[1]);
});
for (const s of specifiers) {
if (typeof s === 'string')
importDeclaration.addSpecifier(s);
else
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);
@@ -293,20 +252,55 @@ 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').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.js').ConfigFile} config -
* The config file object to be transformed into a eslint.config.js file.
* @returns {Promise<string>} The generated config file contents.
*/
async generate(config) {
const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : '';
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')
: '';
/** @type {{program: Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) });
const { program: ast } = parse(
existingConfig,
{ parser: (await import('recast/parsers/babel.js')) },
);
await this.addDefaultExport(ast);
await ConfigsWriter.addDefaultExport(ast);
/**
* @type {ConfigArrayElement[]}
@@ -314,31 +308,100 @@ 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);
if (e) return astUtils.toSpreadElement(e);
else undefined;
return e ? astUtils.toSpreadElement(e) : undefined;
}),
config.rules.length > 0
? this.createRulesObject(config.rules)
? ConfigsWriter.createRulesObject(config.rules)
: undefined,
].filter(e => e);
].filter(Boolean);
this.addElementsToExport(ast, elements);
this.addPackageImports(ast, config.imports);
ConfigsWriter.addElementsToExport(ast, elements);
ConfigsWriter.addPackageImports(ast, config.imports);
const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code;
const finalCode = 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>}
*/
async write(path, content) {
await fs.writeFile(path, content, 'utf-8');
static async write(path, content) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
await fs.writeFile(path, content, 'utf8');
}
}

View File

@@ -1,56 +1,75 @@
#!node
import process from 'node:process';
import path from 'node:path';
import glob from 'picomatch';
import prompts from 'prompts';
import glob from 'picomatch';
import c from 'picocolors';
import str from './lib/str.js';
import capitalize from './lib/capitalize.js';
export default class ConfigsProcessor {
/** @type {string} */
dir = process.cwd();
/** @type {import('./types.js').Config[]} */
configs;
/** @type {string[] | undefined} */
#packagesPatterns;
/** @type {string} */
dir = process.cwd();
/**
* @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.#packagesPatterns = options.packages;
this.configs = options?.configs;
this.configs = options.configs;
this.dir = path.normalize(options.directory ?? this.dir);
}
/**
* @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
* @param {import('./types.js').Package} pkg - The package to detect configs.
* @returns {import('./types.js').Package['config']} - Detected configs record.
*/
detectOptions(pkg, options, single) {
detectConfig(pkg) {
/** @type {import('./types.js').Package['config']} */
const pkgConfig = new Map();
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;
}
else if (!option.detect) continue;
// eslint-disable-next-line no-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);
@@ -62,26 +81,24 @@ export default class ConfigsProcessor {
}
/**
* @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
* @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.
*/
// 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 => {return { title: `${str.capitalize(option.name)}`, value: option.name };});
const configChoices = config.options.map(option => ({ title: `${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',
@@ -93,11 +110,19 @@ export default class ConfigsProcessor {
},
] : configChoices,
hint: config.description,
instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')),
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',
});
if (selectedOptions[config.name] === null) continue;
// eslint-disable-next-line no-continue, @typescript-eslint/no-unnecessary-condition
if (!selectedOptions[config.name]) continue;
// eslint-disable-next-line no-continue
if (selectedOptions[config.name].length === 0) continue;
if (packages.length <= 1) {
@@ -105,64 +130,47 @@ export default class ConfigsProcessor {
...(packages[0].config ?? []),
...Object.entries(selectedOptions),
]);
// eslint-disable-next-line no-continue
continue;
}
/** @type {{title: string, value: import('./types').Package}[]} */
/** @type {{title: string, value: import('./types.js').Package}[]} */
const packagesOptions = packages
.map(pkg => {
return !pkg.root
? {
title: `${pkg.name} ${c.dim(pkg.path.replace(this.dir, '.'))}`,
value: pkg,
}
: { title: 'root', value: pkg };
})
.map(p => (p.root
? { title: 'root', value: p }
: {
title: `${p.name} ${c.dim(p.path.replace(this.dir, '.'))}`,
value: p,
}))
.filter(p => p.title !== 'root');
/** @type {Record<'packages', import('./types').Package[]>} */
/** @type {Record<'packages', import('./types.js').Package[]>} */
// eslint-disable-next-line no-await-in-loop
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')),
});
selected.packages = selected.packages ?? [];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
selected.packages ??= [];
selected.packages.map(pkg => {
pkg.config = new Map([
...(pkg.config ?? []),
selected.packages.map((p) => {
p.config = new Map([
...(p.config ?? []),
...Object.entries(selectedOptions),
]); return pkg;
]); return p;
});
packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg);
packages.map(p =>
selected.packages.find(s => s.name === p.name) ?? p,
);
}
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;
}
}

View File

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

View File

@@ -0,0 +1,190 @@
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

@@ -1,170 +0,0 @@
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

@@ -0,0 +1,8 @@
/**
* @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,12 +1,14 @@
/**
* @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);
}
export default { packagesWithConfigs };
const count = { packagesWithConfigs };
export default count;

View File

@@ -1,15 +1,17 @@
// 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
* @author Jimmy Wärting - https://github.com/jimmywarting
* @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.
* @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) throw new Error('did not expect value to be null or undefined');
if (value === null || value === undefined)
throw new Error('did not expect value to be null or undefined');
return value;
}

View File

@@ -1,10 +0,0 @@
/**
* @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,27 +1,29 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
/* 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 { join } from 'node:path';
import { parse, prettyPrint } from 'recast';
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;
@@ -29,20 +31,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)}`,
}));
@@ -53,7 +55,8 @@ class CommandHandler {
});
}
catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-confusing-void-expression
res(console.error(`Error while installing the packages with ${this.command} ${c.dim(packages.join(' '))} on ${path}: ${error}`));
}
});
@@ -63,53 +66,49 @@ class CommandHandler {
/**
* @type {import('./types').PackageManagerHandler}
*/
class DenoHandler {
const 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 } = recast.parse(configFile, { parser: (await import('recast/parsers/babel.js')) });
const { program: ast } = 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;
});
await writeFile(configPath, recast.prettyPrint(ast).code, 'utf-8');
// eslint-disable-next-line security/detect-non-literal-fs-filename
await writeFile(configPath, prettyPrint(ast).code, 'utf8');
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}
*/
@@ -119,40 +118,51 @@ 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',
},
pnpm: {
name: 'pnpm',
description: 'Uses pnpm install',
handler: new CommandHandler('pnpm install --save-dev', (path) => {
if (existsSync(join(path, 'pnpm-workspace.yaml')) && existsSync(join(path, 'package.json')))
return ' -w';
else return '';
}),
},
yarn: {
name: 'yarn',
description: 'Uses yarn add',
handler: new CommandHandler('yarn add --dev'),
deno: {
description: 'Adds npm: specifiers to the eslint.config.js file',
handler: DenoHandler,
name: 'deno',
},
npm: {
name: 'npm',
description: 'Uses npm install',
handler: new CommandHandler('npm install --save-dev'),
name: 'npm',
},
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'))
)
return ' -w';
return '';
}),
name: 'pnpm',
},
yarn: {
description: 'Uses yarn add',
handler: new CommandHandler('yarn add --dev'),
name: 'yarn',
},
};
/**
* @param {PackagesMap} packagesMap The map of directories and packages to be installed
* @param {string} root Root directory path
* @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.
*/
constructor(packagesMap, root) {
this.packagesMap = packagesMap;
@@ -160,51 +170,68 @@ 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} */
const exists = (...path) => existsSync(join(root, ...path));
function exists(...path) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
return 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(readFileSync(join(root, 'package.json'), 'utf8'));
const { packageManager } = JSON.parse(
// eslint-disable-next-line security/detect-non-literal-fs-filename
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;
else return this.packageManagers.npm;
return this.packageManagers.npm;
}
default: return this.packageManagers.npm;
default: { return this.packageManagers.npm;
}
}
}
async install() {
for (const [path, packages] of this.packagesMap) {
for (const [path, packages] of this.packagesMap)
// eslint-disable-next-line no-await-in-loop
await this.packageManager.handler.install(path, packages);
}
}
}

View File

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

View File

@@ -1,36 +1,41 @@
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import YAML from 'yaml';
/* eslint-disable security/detect-non-literal-fs-filename */
import path, { join } from 'node:path';
import glob from 'picomatch';
import { existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import picomatch from 'picomatch';
import YAML from 'yaml';
/**
* @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 (err) {
catch {
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));
@@ -47,19 +52,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 obj = JSON.parse(file);
const object = JSON.parse(file);
if (obj.name) return obj.name;
if (object.name) return object.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;
@@ -67,125 +72,93 @@ export default class Workspace {
}
/**
* @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;
* @returns {Promise<string[]>} - List of packages on a directory;.
*/
async getPackagePatterns() {
/** @type {string[]} */
let packagePatterns = [];
const 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 pnpmWorkspaceObj = YAML.parse(pnpmWorkspaceYaml);
const pnpmWorkspaceObject = YAML.parse(pnpmWorkspaceYaml);
packagePatterns.push(...(pnpmWorkspaceObj?.packages ?? []));
packagePatterns.push(...pnpmWorkspaceObject.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 packageJsonObj = JSON.parse(packageJson);
const packageJsonObject = JSON.parse(packageJson);
packagePatterns.push(...(packageJsonObj?.workspaces ?? []));
packagePatterns.push(...packageJsonObject.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, p.length - 1) : p;
p = p.endsWith('/') ? p.slice(0, -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 = {
root: true,
directories: paths.directories,
files: paths.files,
name: await getPackageName(this.dir),
path: this.dir,
files: paths.files,
directories: paths.directories,
root: true,
};
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({
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}/`, '')),
directories: paths.directories
.filter(d => picomatch.isMatch(d, `${packagePath}/**/*`))
.map(d => d.replace(`${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,
});
rootPackage.files = rootPackage.files
@@ -196,51 +169,101 @@ export default class Workspace {
}
return [rootPackage, ...packages];
}
/**
* @param {import('./types').Package[]} packages - Packages to be merged into root
* @returns {[import('./types').Package]} A array containing only the root package
* @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.
*/
mergePackages(packages) {
async getPaths(directory = this.dir, ignores = []) {
ignores.push(
...[
'.git',
'.dist',
'.DS_Store',
'node_modules',
].map(f => join(directory, f)),
...await getIgnoredFiles(directory),
);
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];
const merged = packages.reduce((accumulated, pkg) => {
// 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 files = [...new Set([
...accumulated.files,
...pkg.files.map(f => join(pkg.path, f)),
...package_.files.map(f => join(package_.path, f)),
]
.map(p => p.replace(`${rootPackage.path}/`, '')),
)];
const directories = [...new Set([
...accumulated.directories,
...pkg.directories.map(d => join(pkg.path, d)),
...package_.directories.map(d => join(package_.path, d)),
]
.map(p => p.replace(`${rootPackage.path}/`, ''))),
];
const mergedConfig = new Map();
for (const [config, options] of pkg.config ?? []) {
const accumulatedOptions = accumulated.config?.get(config) ?? [];
mergedConfig.set(config, [...new Set([...options, ...accumulatedOptions])]);
for (const [config, options] of package_.config ?? []) {
const accumulatedOptions =
accumulated.config?.get(config) ??
[];
mergedConfig.set(
config,
[...new Set([...options, ...accumulatedOptions])],
);
}
return {
root: true,
path: rootPackage.path,
name: rootPackage.name,
files,
directories,
config: mergedConfig,
directories,
files,
name: rootPackage.name,
path: rootPackage.path,
root: true,
};
}, rootPackage);
return [merged];
}
}

View File

@@ -1,56 +0,0 @@
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,26 +0,0 @@
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;
}

View File

@@ -1,10 +0,0 @@
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,36 +0,0 @@
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

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

View File

@@ -1,97 +0,0 @@
/**
* 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

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

View File

@@ -1,15 +0,0 @@
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

@@ -1,42 +0,0 @@
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

@@ -1,65 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -1,23 +0,0 @@
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

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

View File

@@ -1,56 +0,0 @@
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

@@ -0,0 +1,11 @@
#!/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 +1,37 @@
/** @type {import('./types').Config[]} */
export default [
/** @type {import('@eslegant/cli').Config[]} */
const cliConfig = [
{
name: 'framework',
type: 'multiple',
description: 'The UI frameworks being used in the project',
name: 'framework',
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',
},
{
name: 'strict',
type: 'confirm',
manual: true,
name: 'strict',
options: [{
name: 'yes',
packages: { 'eslint': 'config', 'svelte': ['test1'] },
configs: ['config.strict'],
name: 'yes',
packages: { eslint: 'config', svelte: ['test1'] },
}],
type: 'confirm',
},
];
export default cliConfig;

View File

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

View File

@@ -0,0 +1,33 @@
{
"name": "create-eslegant",
"version": "0.1.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"
}
}

11
packages/eslegant/bin.js Executable file
View File

@@ -0,0 +1,11 @@
#!/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

@@ -0,0 +1,37 @@
/** @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

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

Some files were not shown because too many files have changed in this diff Show More