5 Commits

Author SHA1 Message Date
github-actions[bot]
550dd09e89 ci: 👷🦋 version packages 2023-09-10 00:47:14 +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
58 changed files with 1238 additions and 882 deletions

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@eslegant/js",
"version": "0.2.0",
"version": "0.3.0",
"description": "",
"main": "index.js",
"module": "./src/index.js",
@@ -44,11 +44,12 @@
"@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": "^2.0.0",
"eslint-plugin-perfectionist": "^1.5.1",
"eslint-plugin-security": "^1.7.1",
"eslint-plugin-unicorn": "^48.0.1",
"globals": "^13.21.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

@@ -18,12 +18,12 @@ import importPlugin from 'eslint-plugin-i';
import globals from 'globals';
// eslint-disable-next-line import/no-relative-parent-imports
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
/** @type {import('eslint').Linter.FlatConfig} */
const config = {
files: [...tsFiles, ...jsFiles],
files: FILES,
languageOptions: {
globals: {
...globals.builtin,
@@ -50,9 +50,9 @@ const config = {
'unicorn': unicornPlugin,
},
settings: {
'import/extensions': [...tsFiles, ...jsFiles],
'import/extensions': FILES,
'import/parsers': {
'@typescript-eslint/parser': [...tsFiles, ...jsFiles ],
'@typescript-eslint/parser': FILES,
},
'import/resolver': {
node: true,

View File

@@ -9,10 +9,10 @@
*/
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
rules: {
...{}, // Plugin: eslint-plugin-jsdoc
'jsdoc/match-description': 'error',

View File

@@ -8,11 +8,22 @@
* @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 { jsFiles, tsFiles } from '../../constants.js';
import { FILES } from '../../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
languageOptions: {
globals: {
...globals.browser,
},
},
plugins: {
compat: compatPlugin,
},
rules: {
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/prefer-add-event-listener': 'error',
@@ -23,6 +34,9 @@ const recommended = createVariations({
'unicorn/prefer-keyboard-event-key': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
'unicorn/prefer-query-selector': 'error',
...{}, // Plugin: eslint-plugin-compat
'compat/compat': 'error',
},
});
@@ -33,5 +47,5 @@ const strict = createVariations({
},
});
const node = { recommended, strict };
export default node;
const browser = { recommended, strict };
export default browser;

View File

@@ -12,7 +12,7 @@ import nodePlugin from 'eslint-plugin-n';
import globals from 'globals';
import { createVariations } from '../../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../../constants.js';
import { FILES } from '../../constants.js';
const commonjs = createVariations({
files: ['**/*.cts', '**/*.cjs'],
@@ -47,7 +47,7 @@ const commonjs = createVariations({
});
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
languageOptions: {
globals: {
...globals.nodeBuiltin,

View File

@@ -11,10 +11,10 @@
import perfectionistPlugin from 'eslint-plugin-perfectionist';
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
plugins: {
// @ts-expect-error because plugin doesn't export correct type
perfectionist: perfectionistPlugin,
@@ -24,13 +24,13 @@ const recommended = createVariations({
'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'comma-style': 'error',
'curly': ['error', 'multi-or-nest', 'consistent'],
'dot-location': 'error',
'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',
'padded-blocks': ['error', 'never'],
'rest-spread-spacing': 'error',
'semi-spacing': 'error',
'space-in-parens': 'error',

View File

@@ -86,9 +86,13 @@ const configs: Readonly<{
*/
environments: {
/**
* @description
* @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: {
/**

View File

@@ -1,3 +1,4 @@
/* eslint-disable import/max-dependencies */
/**
* @file
* Main export files for all the configs objects, merging then in one `configs` object.

View File

@@ -9,14 +9,17 @@
*/
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
rules: {
...{}, // Plugin: eslint-plugin-unicorn
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
'unicorn/prevent-abbreviations': 'error',
/*
* TODO [>=1.0.0]: This will be replaced by a better naming convention.
* 'unicorn/prevent-abbreviations': 'error',
*/
},
});

View File

@@ -11,11 +11,11 @@
*/
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES, TS_FILES } from '../constants.js';
// TODO [>=1.0.0]: Create a separate config for performance related practices
const performance = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
rules: {
'prefer-object-spread': 'off',
'prefer-spread': 'off',
@@ -23,7 +23,7 @@ const performance = createVariations({
});
const inferrableTypes = createVariations({
files: [...tsFiles],
files: TS_FILES,
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-inferrable-types': 'error',

View File

@@ -9,10 +9,10 @@
*/
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
rules: {
...{}, // ESLint rules
'constructor-super': 'error',

View File

@@ -12,10 +12,10 @@ import noSecretsPluginRegexes from 'eslint-plugin-no-secrets/regexes.js';
import noSecretsPlugin from 'eslint-plugin-no-secrets';
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
plugins: {
'no-secrets': noSecretsPlugin,
},

View File

@@ -9,10 +9,10 @@
*/
import { createVariations } from '../lib/rule-variations.js';
import { tsFiles } from '../constants.js';
import { TS_FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles],
files: [TS_FILES].flat(),
rules: {
...{}, // Plugin: @typescript-eslint/eslint-plugin
'@typescript-eslint/explicit-function-return-type': 'error',

View File

@@ -9,13 +9,18 @@
*/
import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js';
import { FILES } from '../constants.js';
const recommended = createVariations({
files: [...tsFiles, ...jsFiles],
files: FILES,
rules: {
'camelcase': 'error',
'max-len': ['error', { code: 80, comments: 100, ignoreUrls: true }],
'max-len': ['error', {
code: 80,
comments: 100,
ignoreTemplateLiterals: true,
ignoreUrls: true,
}],
'no-case-declarations': 'error',
'no-confusing-arrow': 'error',
'no-console': 'error',
@@ -287,7 +292,7 @@ const strict = createVariations({
}],
'max-nested-callbacks': ['error', 10],
'max-params': ['error', 4],
'max-statements': ['error', 10],
'max-statements': ['error', 15],
'multiline-comment-style': ['error', 'starred-block'],
'new-cap': 'error',
'new-parens': 'error',
@@ -303,11 +308,8 @@ const strict = createVariations({
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-extra-boolean-cast': 'error',
'no-extra-parens': ['error', 'all', {
enforceForArrowConditionals: false,
nestedBinaryExpressions: false,
ternaryOperandBinaryExpressions: false,
}],
// 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',

View File

@@ -5,7 +5,25 @@
* @author Guz013 <contact.guz013@gmail.com> (https://guz.one)
*/
const jsFiles = ['**/*.js', '**/*.mjs', '**/*.cjs', '**/*.jsx'];
const tsFiles = ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.tsx'];
const JS_FILES = [
'**/*.js',
'**/*.mjs',
'**/*.cjs',
'**/*.jsx',
];
const TS_FILES = [
'**/*.ts',
'**/*.mts',
'**/*.cts',
'**/*.tsx',
];
const FILES = [
JS_FILES,
TS_FILES,
].flat();
export { jsFiles, tsFiles };
export {
FILES,
JS_FILES,
TS_FILES,
};

View File

@@ -22,6 +22,6 @@ function defineConfig(config) {
const eslegant = { configs, presets };
export { defineConfig, eslegant as default };
export { defineConfig, eslegant as default };
export { default as configs } from './configs/index.js';
export { default as presets } from './presets/index.js';

View File

@@ -27,6 +27,7 @@ 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];
}
@@ -34,7 +35,6 @@ function changeLevel(ruleEntry, level) {
return [level, ruleEntry[1]];
return level;
}
/**

View File

@@ -2,7 +2,7 @@ import { configs, defineConfig, presets } from '@eslegant/js';
export default defineConfig([
...presets.strict,
configs.environments.node.strict.error,
configs.environments.node.strict.default,
{
...configs.documentation.strict.error,
files: ['configs/**/*.js', 'configs/**/*.ts'],

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "fixtures/library",
"version": "1.0.1",
"version": "1.0.2",
"description": "",
"main": "index.js",
"private": true,

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@eslegant/cli",
"version": "0.1.0",
"version": "0.2.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",

View File

@@ -1,31 +1,35 @@
import { Command } from 'commander';
import ConfigsProcessor from './configsProcessor.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 prompts from 'prompts';
import ConfigsFile from './configsFile.js';
import cardinal from 'cardinal';
import { Command } from 'commander';
import { erase } from 'sisteransi';
import PackageInstaller from './packageInstaller.js';
import notNull from './lib/notNull.js';
import cardinal from 'cardinal';
import prompts from 'prompts';
import c from 'picocolors';
import PackageInstaller from './package-installer.js';
import ConfigsProcessor from './configs-processor.js';
import ConfigsFile from './configs-file.js';
import notNull from './lib/not-null.js';
import 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 = {
dir: process.cwd(),
configs: [],
dir: process.cwd(),
};
/**
* @param {import('./types').CliArgs} [args] Cli arguments object
* @param {import('./types').CliArgs} [args] - Cli arguments object.
*/
constructor(args) {
this.#program
@@ -42,118 +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 !== undefined ? 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(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)}?\n${c.reset(c.dim(` Packages to install: ${[...new Set([...packagesMap.values()])].join(' ')}\n`))}`,
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];
if (!installer.packageManager) throw console.log(c.red('You must select a package 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 { parse, prettyPrint } 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,93 +92,13 @@ 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
*/
generateObj(pkg) {
/** @type {import('./types').ConfigFile} */
const configObj = {
path: path.join(pkg.path, 'eslint.config.js'),
imports: new Map(),
configs: [],
presets: [],
rules: [],
};
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');
}
for (const [configName, optionsNames] of notNull(pkg.config)) {
const config = this.configs.find(c => c.name === configName);
if (!config) continue;
const options = config.options.filter(o => optionsNames.includes(o.name));
if (!options || options.length === 0) continue;
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.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;
}
/**
* ! NOTE:
* These functions declared bellow are notably hard to read and have lots of exceptions and
* disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to
* 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.
*
* 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
* )} ConfigArrayElement
*/
/**
* @param {Program} ast The program ast to be manipulated
* @returns {Promise<Program>} The final ast with the recreated default export
* @param {Program} ast - The program ast to be manipulated.
* @returns {Promise<Program>} The final ast with the recreated default export.
* @private
*/
async addDefaultExport(ast) {
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[]} */',
@@ -168,78 +108,81 @@ export default class ConfigsWriter {
].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');
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');
const 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;
const oldExportValue = astExport.declaration.type === 'ArrayExpression'
? undefined
: astUtils.createVariable(
'oldConfig',
// @ts-expect-error astExport.declaration is a expression
astExport.declaration,
'const',
);
if (!oldExportValue) return ast;
// @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',
argument: { type: 'Identifier', name: 'oldConfig' },
});
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);
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
* ! NOTE:
* These functions declared bellow are notably hard to read and have lots of exceptions and
* disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to
* 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.
*
* Any help to make this code more readable and robust is appreciated.
*/
createRulesObject(rules) {
/** @type {import('estree').SpreadElement[]} */
// @ts-expect-error The array is filtered to remove undefined's
const expressions = rules
.map(r => {
const e = astUtils.stringToExpression(r);
if (e) return astUtils.toSpreadElement(e);
else undefined;
}).filter(e => e);
return {
type: 'ObjectExpression',
properties: [{
// @ts-expect-error because ObjectProperty doesn't exist in estree types
type: 'ObjectProperty',
key: { type: 'Identifier', name: 'rules' },
value: {
type: 'ObjectExpression',
properties: expressions,
},
}],
};
}
/**
* Adds elements to the default export node, without adding duplicates
* @typedef {import('estree').Program} Program
* @typedef {(
* import('./lib/ast-utils.js').ExpressionOrIdentifier |
* import('estree').ObjectExpression |
* import('estree').SpreadElement
* )} ConfigArrayElement
*/
/**
* Adds elements to the default export node, without adding duplicates.
* @typedef {import('estree').ArrayExpression} ArrayExpression
* @param {Program} ast The program ast to be manipulated
* @param {ConfigArrayElement[]} elements The elements to be added to the array
* @returns {Program} The final ast with the recreated default export
* @param {Program} ast - The program ast to be manipulated.
* @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 } = 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 = 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 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: `${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: 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',
});
// 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

@@ -2,10 +2,10 @@ import { parse, print } from 'recast';
/**
* @typedef {(
* import('estree').MemberExpression |
* import('estree').Identifier |
* import('estree').CallExpression |
* import('estree').NewExpression
* 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
@@ -18,42 +18,49 @@ import { parse, print } from 'recast';
*/
/**
* @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
* @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, kind = 'const', init) {
function createVariable(identifier, init, kind = 'const') {
return {
type: 'VariableDeclaration',
kind,
declarations: [{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: identifier },
id: { name: identifier, type: 'Identifier' },
init,
type: 'VariableDeclarator',
}],
kind,
type: 'VariableDeclaration',
};
}
/**
* @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)
* @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 (['MemberExpression', 'Identifier', 'CallExpression', 'NewExpression'].includes(e.type)) return e;
else return undefined;
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
* @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;
@@ -61,7 +68,7 @@ function findInArray(array, element) {
/** @type {ExpressionOrIdentifier[]} */
// @ts-expect-error The array is filtered to have the type above
const filteredElements = array.elements
.map(n => {
.map((n) => {
if (n?.type === 'SpreadElement') return n.argument;
return n;
}).filter(n => n && n.type === element.type);
@@ -69,56 +76,88 @@ function findInArray(array, element) {
const toStringElements = filteredElements.map(n => print(n).code);
const toStringElement = print(element).code;
const idx = toStringElements.findIndex(e => e === toStringElement);
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
* @param {ExpressionOrIdentifier} expression - The expression to be spread.
* @returns {SpreadElement} The spread element node.
*/
function toSpreadElement(expression) {
return {
type: 'SpreadElement',
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>
* 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
* @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 ?? {
type: 'ImportDeclaration',
specifiers: defaultImported ? [{
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: defaultImported },
}] : [],
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';
* @returns {ThisType<ImportDeclarationHelper>} This helper with the converted default specifier
*/
convertDefaultSpecifier() {
const specifier = this.body.specifiers.find(s => s.type === 'ImportDefaultSpecifier');
const specifier = this.body.specifiers.find(s =>
s.type === 'ImportDefaultSpecifier',
);
if (!specifier)
return this;
@@ -128,44 +167,24 @@ function createImportDeclaration(source, defaultImported, body) {
);
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)) {
if (defaultImported &&
body &&
!body.specifiers.some(s =>
s.type === 'ImportDefaultSpecifier' &&
s.local.name === defaultImported,
))
helper.addSpecifier('default', defaultImported);
}
return helper;
return helper;
}
const astUtils = {
createImportDeclaration,
createVariable,
findInArray,
stringToExpression,
toSpreadElement,
findInArray,
createImportDeclaration,
};
export default astUtils;

View File

@@ -1,7 +1,7 @@
/**
* @param {string} str - The string to capitalize
* @returns {string} The capitalized string
* @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,11 +1,12 @@
/**
* @param {import('../types').Package[]} packages - Package list
* @returns {number} Number of packages' configs
* @param {import('../types').Package[]} packages - Package list.
* @returns {number} Number of packages' configs.
*/
function packagesWithConfigs(packages) {
return packages.map(p =>
[...p.config?.values() ?? []].filter((options) => options.length > 0).length,
[...p.config?.values() ?? []]
.filter(options => options.length > 0).length,
).reduce((partial, sum) => partial + sum, 0);
}

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,26 +1,29 @@
/* eslint-disable no-console */
/* eslint-disable max-classes-per-file */
import { readFile, writeFile } from 'node:fs/promises';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
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 { parse, prettyPrint } from 'recast';
import { readFile, writeFile } from 'node:fs/promises';
/**
* @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;
@@ -28,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)}`,
}));
@@ -52,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}`));
}
});
@@ -62,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 } = 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, 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}
*/
@@ -118,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;
@@ -159,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,64 +1,72 @@
import type { OptionValues } from 'commander';
type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno';
type CliArgs = {
packages?: string[]
mergeToRoot?: boolean
installPkgs?: boolean | PackageManagerName
dir: string
configs: Config[]
} & OptionValues;
type PackageManagerName = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn';
interface PackageManagerHandler {
install(path: string, packages: string[]): Promise<void> | void
install(path: string, packages: string[]): Promise<void> | void,
}
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
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',
};
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 {
path: string
imports: Map<string, string | (string | [string, string])[]>
configs: string[]
presets: string[]
rules: string[]
content?: string
configs: string[],
content?: string,
imports: Map<string, ([string, string] | string)[] | string>,
path: string,
presets: string[],
rules: string[],
}
export type { PackageManagerName, PackageManagerHandler, CliArgs, Config, Package, ConfigFile };
interface Package {
config?: Map<string, string[]>,
configFile?: ConfigFile,
directories: string[],
files: string[],
name: string,
path: string,
root?: boolean,
}
export type {
CliArgs,
Config,
ConfigFile,
Package,
PackageManagerHandler,
PackageManagerName,
};

View File

@@ -1,35 +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 { 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));
@@ -46,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;
@@ -66,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) => !picomatch.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
@@ -195,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

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

View File

@@ -1,4 +1,11 @@
#!/usr/bin/env node
import process from 'node:process';
import Cli from '@eslegant/cli';
const cli = new Cli({ configs: (await import('./configs.js')).default, dir: process.cwd() });
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

@@ -2,33 +2,36 @@
/** @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,6 +1,6 @@
{
"name": "create-eslegant",
"version": "0.1.0",
"version": "0.2.0",
"description": "",
"keywords": [],
"author": {
@@ -9,8 +9,8 @@
"url": "https://guz.one"
},
"files": [
"./bin.js",
"./configs.js"
"bin.js",
"configs.js"
],
"dependencies": {
"@eslegant/cli": "workspace:*"

View File

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

View File

@@ -1,4 +1,11 @@
#!/usr/bin/env node
import process from 'node:process';
import Cli from '@eslegant/cli';
const cli = new Cli({ configs: (await import('./configs.js')).default, dir: process.cwd() });
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

@@ -2,33 +2,36 @@
/** @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,6 +1,6 @@
{
"name": "eslegant",
"version": "0.1.0",
"version": "0.2.0",
"description": "",
"keywords": [],
"author": {
@@ -9,8 +9,8 @@
"url": "https://guz.one"
},
"files": [
"./bin.js",
"./configs.js"
"bin.js",
"configs.js"
],
"dependencies": {
"@eslegant/cli": "workspace:*"

236
pnpm-lock.yaml generated
View File

@@ -54,6 +54,9 @@ importers:
eslint-import-resolver-typescript:
specifier: ^3.6.0
version: 3.6.0(@typescript-eslint/parser@6.4.1)(eslint-plugin-import@2.28.1)(eslint@8.47.0)
eslint-plugin-compat:
specifier: ^4.2.0
version: 4.2.0(eslint@8.47.0)
eslint-plugin-i:
specifier: 2.28.0-2
version: 2.28.0-2(@typescript-eslint/parser@6.4.1)(eslint-import-resolver-typescript@3.6.0)(eslint@8.47.0)
@@ -67,8 +70,8 @@ importers:
specifier: ^0.8.9
version: 0.8.9(eslint@8.47.0)
eslint-plugin-perfectionist:
specifier: ^2.0.0
version: 2.0.0(eslint@8.47.0)(typescript@5.1.6)
specifier: ^1.5.1
version: 1.5.1(eslint@8.47.0)(typescript@5.1.6)
eslint-plugin-security:
specifier: ^1.7.1
version: 1.7.1
@@ -794,6 +797,10 @@ packages:
read-yaml-file: 1.1.0
dev: true
/@mdn/browser-compat-data@5.3.15:
resolution: {integrity: sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==}
dev: false
/@neoconfetti/svelte@1.0.0:
resolution: {integrity: sha512-SmksyaJAdSlMa9cTidVSIqYo1qti+WTsviNDwgjNVm+KQ3DRP2Df9umDIzC4vCcpEYY+chQe0i2IKnLw03AT8Q==}
dev: true
@@ -1070,6 +1077,14 @@ packages:
'@typescript-eslint/visitor-keys': 5.45.0
dev: true
/@typescript-eslint/scope-manager@5.62.0:
resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
dev: false
/@typescript-eslint/scope-manager@6.4.1:
resolution: {integrity: sha512-p/OavqOQfm4/Hdrr7kvacOSFjwQ2rrDVJRPxt/o0TOWdFnjJptnjnZ+sYDR7fi4OimvIuKp+2LCkc+rt9fIW+A==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -1078,14 +1093,6 @@ packages:
'@typescript-eslint/visitor-keys': 6.4.1
dev: false
/@typescript-eslint/scope-manager@6.6.0:
resolution: {integrity: sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 6.6.0
'@typescript-eslint/visitor-keys': 6.6.0
dev: false
/@typescript-eslint/type-utils@5.45.0(eslint@8.44.0)(typescript@5.0.2):
resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1131,13 +1138,13 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@typescript-eslint/types@6.4.1:
resolution: {integrity: sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==}
engines: {node: ^16.0.0 || >=18.0.0}
/@typescript-eslint/types@5.62.0:
resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: false
/@typescript-eslint/types@6.6.0:
resolution: {integrity: sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==}
/@typescript-eslint/types@6.4.1:
resolution: {integrity: sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==}
engines: {node: ^16.0.0 || >=18.0.0}
dev: false
@@ -1162,6 +1169,27 @@ packages:
- supports-color
dev: true
/@typescript-eslint/typescript-estree@5.62.0(typescript@5.1.6):
resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
tsutils: 3.21.0(typescript@5.1.6)
typescript: 5.1.6
transitivePeerDependencies:
- supports-color
dev: false
/@typescript-eslint/typescript-estree@6.4.1(typescript@5.1.6):
resolution: {integrity: sha512-xF6Y7SatVE/OyV93h1xGgfOkHr2iXuo8ip0gbfzaKeGGuKiAnzS+HtVhSPx8Www243bwlW8IF7X0/B62SzFftg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -1183,27 +1211,6 @@ packages:
- supports-color
dev: false
/@typescript-eslint/typescript-estree@6.6.0(typescript@5.1.6):
resolution: {integrity: sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 6.6.0
'@typescript-eslint/visitor-keys': 6.6.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
ts-api-utils: 1.0.2(typescript@5.1.6)
typescript: 5.1.6
transitivePeerDependencies:
- supports-color
dev: false
/@typescript-eslint/utils@5.45.0(eslint@8.44.0)(typescript@5.0.2):
resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1224,6 +1231,26 @@ packages:
- typescript
dev: true
/@typescript-eslint/utils@5.62.0(eslint@8.47.0)(typescript@5.1.6):
resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0)
'@types/json-schema': 7.0.12
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6)
eslint: 8.47.0
eslint-scope: 5.1.1
semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
dev: false
/@typescript-eslint/utils@6.4.1(eslint@8.47.0)(typescript@5.1.6):
resolution: {integrity: sha512-F/6r2RieNeorU0zhqZNv89s9bDZSovv3bZQpUNOmmQK1L80/cV4KEu95YUJWi75u5PhboFoKUJBnZ4FQcoqhDw==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -1243,25 +1270,6 @@ packages:
- typescript
dev: false
/@typescript-eslint/utils@6.6.0(eslint@8.47.0)(typescript@5.1.6):
resolution: {integrity: sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.47.0)
'@types/json-schema': 7.0.12
'@types/semver': 7.5.0
'@typescript-eslint/scope-manager': 6.6.0
'@typescript-eslint/types': 6.6.0
'@typescript-eslint/typescript-estree': 6.6.0(typescript@5.1.6)
eslint: 8.47.0
semver: 7.5.4
transitivePeerDependencies:
- supports-color
- typescript
dev: false
/@typescript-eslint/visitor-keys@5.45.0:
resolution: {integrity: sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1270,6 +1278,14 @@ packages:
eslint-visitor-keys: 3.4.3
dev: true
/@typescript-eslint/visitor-keys@5.62.0:
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
'@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3
dev: false
/@typescript-eslint/visitor-keys@6.4.1:
resolution: {integrity: sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -1278,14 +1294,6 @@ packages:
eslint-visitor-keys: 3.4.3
dev: false
/@typescript-eslint/visitor-keys@6.6.0:
resolution: {integrity: sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 6.6.0
eslint-visitor-keys: 3.4.3
dev: false
/acorn-jsx@5.3.2(acorn@8.10.0):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1439,6 +1447,12 @@ packages:
util: 0.12.5
dev: false
/ast-metadata-inferer@0.8.0:
resolution: {integrity: sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA==}
dependencies:
'@mdn/browser-compat-data': 5.3.15
dev: false
/ast-types@0.16.1:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
@@ -1495,6 +1509,17 @@ packages:
wcwidth: 1.0.1
dev: true
/browserslist@4.21.10:
resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001532
electron-to-chromium: 1.4.513
node-releases: 2.0.13
update-browserslist-db: 1.0.11(browserslist@4.21.10)
dev: false
/buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: true
@@ -1541,6 +1566,10 @@ packages:
engines: {node: '>=6'}
dev: true
/caniuse-lite@1.0.30001532:
resolution: {integrity: sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==}
dev: false
/cardinal@2.1.1:
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
hasBin: true
@@ -1826,6 +1855,10 @@ packages:
engines: {node: '>=12'}
dev: true
/electron-to-chromium@1.4.513:
resolution: {integrity: sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==}
dev: false
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: true
@@ -1957,7 +1990,6 @@ packages:
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
dev: true
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
@@ -2030,6 +2062,22 @@ packages:
- supports-color
dev: false
/eslint-plugin-compat@4.2.0(eslint@8.47.0):
resolution: {integrity: sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w==}
engines: {node: '>=14.x'}
peerDependencies:
eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@mdn/browser-compat-data': 5.3.15
ast-metadata-inferer: 0.8.0
browserslist: 4.21.10
caniuse-lite: 1.0.30001532
eslint: 8.47.0
find-up: 5.0.0
lodash.memoize: 4.1.2
semver: 7.5.4
dev: false
/eslint-plugin-es-x@7.2.0(eslint@8.47.0):
resolution: {integrity: sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -2145,26 +2193,16 @@ packages:
eslint: 8.47.0
dev: false
/eslint-plugin-perfectionist@2.0.0(eslint@8.47.0)(typescript@5.1.6):
resolution: {integrity: sha512-VqUk5WR7Dj8L0gNPqn7bl7NTHFYB8l5um4wo7hkMp0Dl+k8RHDAsOef4pPrty6G8vjnzvb3xIZNNshmDJI8SdA==}
/eslint-plugin-perfectionist@1.5.1(eslint@8.47.0)(typescript@5.1.6):
resolution: {integrity: sha512-PiUrAfGDc/l6MKKUP8qt5RXueC7FZC6F/0j8ijXYU8o3x8o2qUi6zEEYBkId/IiKloIXM5KTD4jrH9833kDNzA==}
peerDependencies:
astro-eslint-parser: ^0.14.0
eslint: '>=8.0.0'
svelte: '>=3.0.0'
svelte-eslint-parser: ^0.32.0
vue-eslint-parser: '>=9.0.0'
peerDependenciesMeta:
astro-eslint-parser:
optional: true
svelte:
optional: true
svelte-eslint-parser:
optional: true
vue-eslint-parser:
optional: true
dependencies:
'@typescript-eslint/utils': 6.6.0(eslint@8.47.0)(typescript@5.1.6)
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/utils': 5.62.0(eslint@8.47.0)(typescript@5.1.6)
eslint: 8.47.0
is-core-module: 2.13.0
json5: 2.2.3
minimatch: 9.0.3
natural-compare-lite: 1.4.0
transitivePeerDependencies:
@@ -2234,7 +2272,6 @@ packages:
dependencies:
esrecurse: 4.3.0
estraverse: 4.3.0
dev: true
/eslint-scope@7.2.0:
resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==}
@@ -2409,7 +2446,6 @@ packages:
/estraverse@4.3.0:
resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==}
engines: {node: '>=4.0'}
dev: true
/estraverse@5.3.0:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
@@ -2984,6 +3020,12 @@ packages:
minimist: 1.2.8
dev: false
/json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
dev: false
/jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
optionalDependencies:
@@ -3049,6 +3091,10 @@ packages:
dependencies:
p-locate: 5.0.0
/lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3218,6 +3264,10 @@ packages:
whatwg-url: 5.0.0
dev: true
/node-releases@2.0.13:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: false
/normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
@@ -4074,7 +4124,6 @@ packages:
/tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
/tslib@2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
@@ -4094,6 +4143,16 @@ packages:
typescript: 5.0.2
dev: true
/tsutils@3.21.0(typescript@5.1.6):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
dependencies:
tslib: 1.14.1
typescript: 5.1.6
dev: false
/tty-table@4.2.1:
resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==}
engines: {node: '>=8.0.0'}
@@ -4257,6 +4316,17 @@ packages:
engines: {node: '>= 4.0.0'}
dev: true
/update-browserslist-db@1.0.11(browserslist@4.21.10):
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
browserslist: 4.21.10
escalade: 3.1.1
picocolors: 1.0.0
dev: false
/uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies: