5 Commits

Author SHA1 Message Date
renovate[bot]
8e0b831592 chore(deps): update dependency husky to v9 2024-02-08 03:40:29 +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
40 changed files with 1065 additions and 720 deletions

View File

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

View File

@@ -44,6 +44,7 @@
"@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1", "@typescript-eslint/parser": "^6.4.1",
"eslint-import-resolver-typescript": "^3.6.0", "eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-i": "2.28.0-2", "eslint-plugin-i": "2.28.0-2",
"eslint-plugin-jsdoc": "^46.5.0", "eslint-plugin-jsdoc": "^46.5.0",
"eslint-plugin-n": "^16.0.2", "eslint-plugin-n": "^16.0.2",

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'; import globals from 'globals';
// eslint-disable-next-line import/no-relative-parent-imports // 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} */ /** @type {import('eslint').Linter.FlatConfig} */
const config = { const config = {
files: [...tsFiles, ...jsFiles], files: FILES,
languageOptions: { languageOptions: {
globals: { globals: {
...globals.builtin, ...globals.builtin,
@@ -50,9 +50,9 @@ const config = {
'unicorn': unicornPlugin, 'unicorn': unicornPlugin,
}, },
settings: { settings: {
'import/extensions': [...tsFiles, ...jsFiles], 'import/extensions': FILES,
'import/parsers': { 'import/parsers': {
'@typescript-eslint/parser': [...tsFiles, ...jsFiles ], '@typescript-eslint/parser': FILES,
}, },
'import/resolver': { 'import/resolver': {
node: true, node: true,

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,10 @@
import perfectionistPlugin from 'eslint-plugin-perfectionist'; import perfectionistPlugin from 'eslint-plugin-perfectionist';
import { createVariations } from '../lib/rule-variations.js'; import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js'; import { FILES } from '../constants.js';
const recommended = createVariations({ const recommended = createVariations({
files: [...tsFiles, ...jsFiles], files: FILES,
plugins: { plugins: {
// @ts-expect-error because plugin doesn't export correct type // @ts-expect-error because plugin doesn't export correct type
perfectionist: perfectionistPlugin, perfectionist: perfectionistPlugin,
@@ -24,13 +24,13 @@ const recommended = createVariations({
'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }], 'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
'comma-style': 'error', 'comma-style': 'error',
'curly': ['error', 'multi-or-nest', 'consistent'], 'curly': ['error', 'multi-or-nest', 'consistent'],
'dot-location': 'error', 'dot-location': ['error', 'property'],
'eol-last': 'error', 'eol-last': 'error',
'generator-star-spacing': ['error', 'before'], 'generator-star-spacing': ['error', 'before'],
'no-mixed-spaces-and-tabs': 'error', 'no-mixed-spaces-and-tabs': 'error',
'no-multi-spaces': 'error', 'no-multi-spaces': 'error',
'no-whitespace-before-property': 'error', 'no-whitespace-before-property': 'error',
'padded-blocks': 'error', 'padded-blocks': ['error', 'never'],
'rest-spread-spacing': 'error', 'rest-spread-spacing': 'error',
'semi-spacing': 'error', 'semi-spacing': 'error',
'space-in-parens': 'error', 'space-in-parens': 'error',

View File

@@ -86,9 +86,13 @@ const configs: Readonly<{
*/ */
environments: { environments: {
/** /**
* @description * @summary
* Browser environment configuration, use this if you are working * Browser environment configuration, use this if you are working
* on a pure client-side or mixed codebase environment. * 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: { browser: {
/** /**

View File

@@ -1,3 +1,4 @@
/* eslint-disable import/max-dependencies */
/** /**
* @file * @file
* Main export files for all the configs objects, merging then in one `configs` object. * 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 { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js'; import { FILES } from '../constants.js';
const recommended = createVariations({ const recommended = createVariations({
files: [...tsFiles, ...jsFiles], files: FILES,
rules: { rules: {
...{}, // Plugin: eslint-plugin-unicorn ...{}, // Plugin: eslint-plugin-unicorn
'unicorn/filename-case': ['error', { case: 'kebabCase' }], '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 { 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 // TODO [>=1.0.0]: Create a separate config for performance related practices
const performance = createVariations({ const performance = createVariations({
files: [...tsFiles, ...jsFiles], files: FILES,
rules: { rules: {
'prefer-object-spread': 'off', 'prefer-object-spread': 'off',
'prefer-spread': 'off', 'prefer-spread': 'off',
@@ -23,7 +23,7 @@ const performance = createVariations({
}); });
const inferrableTypes = createVariations({ const inferrableTypes = createVariations({
files: [...tsFiles], files: TS_FILES,
rules: { rules: {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-inferrable-types': 'error', '@typescript-eslint/no-inferrable-types': 'error',

View File

@@ -9,10 +9,10 @@
*/ */
import { createVariations } from '../lib/rule-variations.js'; import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js'; import { FILES } from '../constants.js';
const recommended = createVariations({ const recommended = createVariations({
files: [...tsFiles, ...jsFiles], files: FILES,
rules: { rules: {
...{}, // ESLint rules ...{}, // ESLint rules
'constructor-super': 'error', '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 noSecretsPlugin from 'eslint-plugin-no-secrets';
import { createVariations } from '../lib/rule-variations.js'; import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js'; import { FILES } from '../constants.js';
const recommended = createVariations({ const recommended = createVariations({
files: [...tsFiles, ...jsFiles], files: FILES,
plugins: { plugins: {
'no-secrets': noSecretsPlugin, 'no-secrets': noSecretsPlugin,
}, },

View File

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

View File

@@ -9,13 +9,18 @@
*/ */
import { createVariations } from '../lib/rule-variations.js'; import { createVariations } from '../lib/rule-variations.js';
import { jsFiles, tsFiles } from '../constants.js'; import { FILES } from '../constants.js';
const recommended = createVariations({ const recommended = createVariations({
files: [...tsFiles, ...jsFiles], files: FILES,
rules: { rules: {
'camelcase': 'error', '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-case-declarations': 'error',
'no-confusing-arrow': 'error', 'no-confusing-arrow': 'error',
'no-console': 'error', 'no-console': 'error',
@@ -287,7 +292,7 @@ const strict = createVariations({
}], }],
'max-nested-callbacks': ['error', 10], 'max-nested-callbacks': ['error', 10],
'max-params': ['error', 4], 'max-params': ['error', 4],
'max-statements': ['error', 10], 'max-statements': ['error', 15],
'multiline-comment-style': ['error', 'starred-block'], 'multiline-comment-style': ['error', 'starred-block'],
'new-cap': 'error', 'new-cap': 'error',
'new-parens': 'error', 'new-parens': 'error',
@@ -303,11 +308,8 @@ const strict = createVariations({
'no-extend-native': 'error', 'no-extend-native': 'error',
'no-extra-bind': 'error', 'no-extra-bind': 'error',
'no-extra-boolean-cast': 'error', 'no-extra-boolean-cast': 'error',
'no-extra-parens': ['error', 'all', { // TODO [>=1.0.0]: Fix no-extra-parens conflict with the unicorn/no-nested-ternary rule.
enforceForArrowConditionals: false, 'no-extra-parens': ['error', 'functions'],
nestedBinaryExpressions: false,
ternaryOperandBinaryExpressions: false,
}],
'no-floating-decimal': 'error', 'no-floating-decimal': 'error',
'no-implicit-coercion': 'error', 'no-implicit-coercion': 'error',
'no-implied-eval': 'error', 'no-implied-eval': 'error',

View File

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

@@ -27,6 +27,7 @@ function changeLevel(ruleEntry, level) {
if (typeof level === 'number') { if (typeof level === 'number') {
/** @type {RuleLevel[]} */ /** @type {RuleLevel[]} */
const levels = ['error', 'off', 'warn']; const levels = ['error', 'off', 'warn'];
// eslint-disable-next-line security/detect-object-injection
level = levels[level]; level = levels[level];
} }
@@ -34,7 +35,6 @@ function changeLevel(ruleEntry, level) {
return [level, ruleEntry[1]]; return [level, ruleEntry[1]];
return level; return level;
} }
/** /**

View File

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

View File

@@ -20,7 +20,7 @@
"eslegant": "workspace:*", "eslegant": "workspace:*",
"@svitejs/changesets-changelog-github-compact": "^1.1.0", "@svitejs/changesets-changelog-github-compact": "^1.1.0",
"eslint": "^8.47.0", "eslint": "^8.47.0",
"husky": "^8.0.3", "husky": "^9.0.0",
"turbo": "^1.10.12" "turbo": "^1.10.12"
} }
} }

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 { 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'; 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,31 +1,35 @@
import { Command } from 'commander'; /* eslint-disable no-console */
import ConfigsProcessor from './configsProcessor.js'; /* eslint-disable import/max-dependencies */
import Workspace from './workspace.js'; import process from 'node:process';
import c from 'picocolors';
import path from 'node:path'; import path from 'node:path';
import { createSpinner } from 'nanospinner'; import { createSpinner } from 'nanospinner';
import count from './lib/count.js'; import { Command } from 'commander';
import prompts from 'prompts';
import ConfigsFile from './configsFile.js';
import cardinal from 'cardinal';
import { erase } from 'sisteransi'; import { erase } from 'sisteransi';
import PackageInstaller from './packageInstaller.js'; import cardinal from 'cardinal';
import notNull from './lib/notNull.js'; 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; const stdout = process.stdout;
export default class Cli { export default class Cli {
#program = new Command(); #program = new Command();
/** @type {import('./types').CliArgs} */ /** @type {import('./types').CliArgs} */
args = { args = {
dir: process.cwd(),
configs: [], configs: [],
dir: process.cwd(),
}; };
/** /**
* @param {import('./types').CliArgs} [args] Cli arguments object * @param {import('./types').CliArgs} [args] - Cli arguments object.
*/ */
constructor(args) { constructor(args) {
this.#program this.#program
@@ -42,24 +46,26 @@ export default class Cli {
...args, ...args,
}; };
this.args.dir = !this.args.dir.startsWith('/') this.args.dir = this.args.dir.startsWith('/')
? path.join(process.cwd(), this.args.dir) ? this.args.dir
: this.args.dir; : path.join(process.cwd(), this.args.dir);
} }
// eslint-disable-next-line max-lines-per-function, max-statements, complexity
async run() { async run() {
process.chdir(this.args.dir); process.chdir(this.args.dir);
const configs = this.args.configs; const configs = this.args.configs;
const spinner = createSpinner('Detecting workspace configuration'); const spinner = createSpinner('Detecting workspace configuration');
const processor = new ConfigsProcessor({ configs }); 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()) let packages = await workspace.getPackages();
.map(pkg => { packages = packages.map((pkg) => {
spinner.update({ text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}` }); spinner.update({
text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}`,
});
pkg.config = processor.detectConfig(pkg); pkg.config = processor.detectConfig(pkg);
@@ -68,92 +74,120 @@ export default class Cli {
spinner.success({ spinner.success({
text: text:
'Detecting workspace configuration ' + `Detecting workspace configuration ${
c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`), 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}} */ /** @type {{merge: boolean}} */
(await prompts({ ? (await prompts({
name: 'merge',
message:
`Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}` +
c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages')),
initial: true, 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', 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')); console.log(c.dim('\nPlease select which options you prefer\n'));
packages = await processor.questionConfig( packages = await processor.questionConfig(
merge ? workspace.mergePackages(packages) : packages, merge ? Workspace.mergePackages(packages) : packages,
configs.filter(c => c.manual), 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) { for (const pkg of packages) {
pkg.configFile = fileHandler.generateObj(pkg); 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} */ /** @type {boolean} */
const shouldWrite = const shouldWrite =
/** @type {{write: boolean}} */ /** @type {{write: boolean}} */
// eslint-disable-next-line no-await-in-loop
(await prompts({ (await prompts({
type: 'confirm', initial: true,
name: 'write',
message: `Do you want to write this config file for ${pkg.root message: `Do you want to write this config file for ${pkg.root
? c.blue('the root directory') ? c.blue('the root directory')
: c.blue(pkg.name) : c.blue(pkg.name)
}?\n\n${cardinal.highlight(pkg.configFile.content)}`, }?\n\n${cardinal.highlight(pkg.configFile.content)}`,
initial: true, name: 'write',
type: 'confirm',
// eslint-disable-next-line unicorn/no-await-expression-member
})).write; })).write;
stdout.write(erase.lines(pkg.configFile.content.split('\n').length + 2)); stdout.write(
erase.lines(pkg.configFile.content.split('\n').length + 2),
if (shouldWrite) await fileHandler.write(pkg.configFile.path, pkg.configFile.content); );
if (shouldWrite) {
// eslint-disable-next-line no-await-in-loop
await ConfigsFile.write(
pkg.configFile.path,
pkg.configFile.content,
);
}
} }
const packagesMap = new Map(packages.map(p => [p.path, [...notNull(p.configFile).imports.keys()]])); const packagesMap = new Map(packages.map(p =>
const installer = new PackageInstaller(packagesMap, packages.find(p => p.root === true)?.path ?? this.args.dir); [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'} */ /** @type {boolean | 'changePackage'} */
let installPkgs = this.args.installPkgs !== undefined ? true : // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
/** @type {{install: boolean | 'changePackage'}} */ let installPkgs = this.args.installPkgs ?? (await prompts({
(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: [ choices: [
{ title: 'Yes, install all packages', value: true, description: installer.packageManager.description }, {
description: installer.packageManager.description,
title: 'Yes, install all packages',
value: true,
},
{ title: 'No, I will install them manually', value: false }, { title: 'No, I will install them manually', value: false },
{ title: 'Change package manager', value: 'changePackage' }, { 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', type: 'select',
// eslint-disable-next-line unicorn/no-await-expression-member
})).install; })).install;
if (installPkgs === 'changePackage') { if (installPkgs === 'changePackage') {
/** @type {{manager: import('./types').PackageManagerName}} */ /** @type {{manager: import('./types').PackageManagerName}} */
const prompt = await prompts({ 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?', message: 'What package manager do you want ESLit to use?',
choices: Object.values(installer.packageManagers).map(m => { name: 'manager',
return { title: m.name, description: m.description, value: m.name };
}),
type: 'select', 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; installPkgs = true;
} }
if (installPkgs) await installer.install(); 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 { 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.js').ConfigFile['imports']} map1 -
* @param {import('./types').ConfigFile['imports']} map2 - The map to has it values merged to map1 * The map to has it values merged from map2.
* @returns {import('./types').ConfigFile['imports']} The resulting map * @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) { function mergeImportsMaps(map1, map2) {
for (const [key, value] of map2) { for (const [key, value] of map2) {
if (!map1.has(key)) { if (!map1.has(key)) {
map1.set(key, value); map1.set(key, value);
// eslint-disable-next-line no-continue
continue; continue;
} }
const imports1 = notNull(map1.get(key)); const imports1 = notNull(map1.get(key));
@@ -23,23 +30,37 @@ function mergeImportsMaps(map1, map2) {
* Because arrays and objects are always different independently from having equal values * 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. * ([] === [] -> false). It is converted to a string so the comparison can be made.
*/ */
switch ([typeof imports1 === 'string', typeof imports2 === 'string'].join(',')) { switch ([
case 'true,true': typeof imports1 === 'string',
if (imports1.toString() === imports2.toString()) typeof imports2 === 'string',
].join(',')) {
case 'true,true': {
if (imports1.toString() === imports2.toString()) {
map1.set(key, value); 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; break;
case 'true,false': }
case 'true,false': {
map1.set(key, [['default', imports1.toString()], ...imports2]); map1.set(key, [['default', imports1.toString()], ...imports2]);
break; break;
case 'false,true': }
case 'false,true': {
map1.set(key, [['default', imports2.toString()], ...imports1]); map1.set(key, [['default', imports2.toString()], ...imports1]);
break; break;
case 'false,false': }
case 'false,false': {
map1.set(key, [...imports1, ...imports2]); map1.set(key, [...imports1, ...imports2]);
break; break;
} }
default:
// No nothing
}
if (typeof map1.get(key) !== 'string') if (typeof map1.get(key) !== 'string')
map1.set(key, [...new Set(map1.get(key))]); map1.set(key, [...new Set(map1.get(key))]);
} }
@@ -47,9 +68,9 @@ function mergeImportsMaps(map1, map2) {
} }
/** /**
* @param {string} path1 The path to traverse from * @param {string} path1 - The path to traverse from.
* @param {string} root The root path * @param {string} root - The root path to be removed from the path1.
* @returns {string} The path to traverse * @returns {string} The path to traverse.
*/ */
function getPathDepth(path1, root) { function getPathDepth(path1, root) {
const pathDepth = path1.replace(root, '').split('/').slice(1); const pathDepth = path1.replace(root, '').split('/').slice(1);
@@ -58,13 +79,12 @@ function getPathDepth(path1, root) {
} }
export default class ConfigsWriter { export default class ConfigsWriter {
/** @type {string} */ /** @type {string} */
root = process.cwd(); root = process.cwd();
/** /**
* @param {import('./types').Config[]} configs The array of configs to construct from * @param {import('./types.js').Config[]} configs - The array of configs to construct from.
* @param {string} [root] The root directory path * @param {string} [root] - The root directory path.
*/ */
constructor(configs, root) { constructor(configs, root) {
this.configs = configs; this.configs = configs;
@@ -72,93 +92,13 @@ export default class ConfigsWriter {
} }
/** /**
* @param {import('./types').Package} pkg The package to generate the config string from * @param {Program} ast - The program ast to be manipulated.
* @returns {import('./types').ConfigFile} The config file object * @returns {Promise<Program>} The final ast with the recreated default export.
*/
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
* @private * @private
*/ */
async addDefaultExport(ast) { static async addDefaultExport(ast) {
/** @type {{program: Program}} */ /** @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 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const { program: exportTemplateAst } = parse([ const { program: exportTemplateAst } = parse([
'/** @type {import(\'eslint\').Linter.FlatConfig[]} */', '/** @type {import(\'eslint\').Linter.FlatConfig[]} */',
@@ -168,78 +108,81 @@ export default class ConfigsWriter {
].join('\n'), { parser: (await import('recast/parsers/babel.js')) }); ].join('\n'), { parser: (await import('recast/parsers/babel.js')) });
/** @type {import('estree').ExportDefaultDeclaration} */ /** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded // @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} */ /** @type {import('estree').ExportDefaultDeclaration | undefined} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded // @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; } if (!astExport) { ast.body.push(exportTemplateNode); return ast; }
/** @type {import('estree').VariableDeclaration | undefined} */ /** @type {import('estree').VariableDeclaration | undefined} */
const oldExportValue = astExport.declaration.type !== 'ArrayExpression' const oldExportValue = astExport.declaration.type === 'ArrayExpression'
? undefined
: astUtils.createVariable(
'oldConfig',
// @ts-expect-error astExport.declaration is a expression // @ts-expect-error astExport.declaration is a expression
? astUtils.createVariable('oldConfig', 'const', astExport.declaration) astExport.declaration,
: undefined; 'const',
);
if (!oldExportValue) return ast; if (!oldExportValue) return ast;
// @ts-expect-error declaration is a ArrayExpression // @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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
exportTemplateNode.declaration.elements.push({ exportTemplateNode.declaration.elements.push({
argument: { name: 'oldConfig', type: 'Identifier' },
type: 'SpreadElement', type: 'SpreadElement',
argument: { type: 'Identifier', name: 'oldConfig' },
}); });
const astExportIdx = ast.body.indexOf(astExport); const astExportIdx = ast.body.indexOf(astExport);
// eslint-disable-next-line security/detect-object-injection
ast.body[astExportIdx] = exportTemplateNode; ast.body[astExportIdx] = exportTemplateNode;
ast.body.splice(astExportIdx - 1, 0, oldExportValue); ast.body.splice(astExportIdx - 1, 0, oldExportValue);
return ast; return ast;
} }
/** /**
* @param {import('./types').ConfigFile['rules']} rules The rules to be used to create the object * ! NOTE:
* @returns {import('estree').ObjectExpression} The object containing the spread rules * These functions declared bellow are notably hard to read and have lots of exceptions and
* @private * 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 * @typedef {import('estree').ArrayExpression} ArrayExpression
* @param {Program} ast The program ast to be manipulated * @param {Program} ast - The program ast to be manipulated.
* @param {ConfigArrayElement[]} elements The elements to be added to the array * @param {ConfigArrayElement[]} elements - The elements to be added to the array.
* @returns {Program} The final ast with the recreated default export * @returns {Program} The final ast with the recreated default export.
* @private * @private
*/ */
addElementsToExport(ast, elements) { static addElementsToExport(ast, elements) {
/** @type {import('estree').ExportDefaultDeclaration} */ /** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded // @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); const exportNodeIdx = ast.body.indexOf(exportNode);
/** @type {ArrayExpression} */ /** @type {ArrayExpression} */
@@ -247,45 +190,61 @@ export default class ConfigsWriter {
const array = exportNode.declaration; const array = exportNode.declaration;
for (const e of elements) { for (const e of elements) {
if (e.type !== 'ObjectExpression' && astUtils.findInArray(array, e)) continue; if (!(
e.type !== 'ObjectExpression' &&
astUtils.findInArray(array, e)
))
array.elements.push(e); array.elements.push(e);
} }
exportNode.declaration = array; exportNode.declaration = array;
// eslint-disable-next-line security/detect-object-injection
ast.body[exportNodeIdx] = exportNode; ast.body[exportNodeIdx] = exportNode;
return ast; return ast;
} }
/** /**
* @param {Program} ast The program ast to be manipulated * @param {Program} ast - The program ast to be manipulated.
* @param {import('./types').ConfigFile['imports']} imports The imports map to be used * @param {import('./types.js').ConfigFile['imports']} imports - The imports map to be used.
* @returns {Program} The final ast with the recreated default export * @returns {Program} The final ast with the recreated default export.
*/ */
addPackageImports(ast, imports) { static addPackageImports(ast, imports) {
/** @type {import('estree').ImportDeclaration[]} */ /** @type {import('estree').ImportDeclaration[]} */
const importDeclarations = []; const importDeclarations = [];
for (const [pkgName, specifiers] of imports) { for (const [pkgName, specifiers] of imports) {
/** @type {import('estree').ImportDeclaration | undefined} */ /** @type {import('estree').ImportDeclaration | undefined} */
// @ts-expect-error type error, the specifier has to be ImportDeclaration to be founded // @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( const importDeclaration = astUtils.createImportDeclaration(
pkgName, typeof specifiers === 'string' ? specifiers : undefined, existingDeclaration, pkgName,
typeof specifiers === 'string'
? specifiers
: undefined,
existingDeclaration,
); );
if (typeof specifiers !== 'string') { if (typeof specifiers !== 'string') {
specifiers.forEach(s => { for (const s of specifiers) {
if (typeof s === 'string') return importDeclaration.addSpecifier(s); if (typeof s === 'string')
else return importDeclaration.addSpecifier(s[0], s[1]); importDeclaration.addSpecifier(s);
}); else
importDeclaration.addSpecifier(s[0], s[1]);
}
} }
if (existingDeclaration) ast.body[ast.body.indexOf(existingDeclaration)] = importDeclaration.body; if (existingDeclaration) {
else importDeclarations.push(importDeclaration.body); ast.body[ast.body.indexOf(existingDeclaration)] =
importDeclaration.body;
}
else {
importDeclarations.push(importDeclaration.body);
}
} }
ast.body.unshift(...importDeclarations); ast.body.unshift(...importDeclarations);
@@ -293,20 +252,55 @@ export default class ConfigsWriter {
return ast; 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 * @param {import('./types.js').ConfigFile} config -
* @returns {Promise<string>} The generated config file contents * The config file object to be transformed into a eslint.config.js file.
* @returns {Promise<string>} The generated config file contents.
*/ */
async generate(config) { static async generate(config) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : ''; const existingConfig = existsSync(config.path)
// eslint-disable-next-line security/detect-non-literal-fs-filename
? await fs.readFile(config.path, 'utf8')
: '';
/** @type {{program: Program}} */ /** @type {{program: Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // 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[]} * @type {ConfigArrayElement[]}
@@ -314,31 +308,100 @@ export default class ConfigsWriter {
// @ts-expect-error The array is filtered to remove undefined's // @ts-expect-error The array is filtered to remove undefined's
const elements = [ const elements = [
...config.configs.map(c => astUtils.stringToExpression(c)), ...config.configs.map(c => astUtils.stringToExpression(c)),
...config.presets.map(p => { ...config.presets.map((p) => {
const e = astUtils.stringToExpression(p); const e = astUtils.stringToExpression(p);
if (e) return astUtils.toSpreadElement(e); return e ? astUtils.toSpreadElement(e) : undefined;
else undefined;
}), }),
config.rules.length > 0 config.rules.length > 0
? this.createRulesObject(config.rules) ? ConfigsWriter.createRulesObject(config.rules)
: undefined, : undefined,
].filter(e => e); ].filter(Boolean);
this.addElementsToExport(ast, elements); ConfigsWriter.addElementsToExport(ast, elements);
this.addPackageImports(ast, config.imports); 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; 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} path - The path to the file to be written.
* @param {string} content The content of the file * @param {string} content - The content of the file.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async write(path, content) { static async write(path, content) {
await fs.writeFile(path, content, 'utf-8'); // 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 path from 'node:path';
import glob from 'picomatch';
import prompts from 'prompts'; import prompts from 'prompts';
import glob from 'picomatch';
import c from 'picocolors'; import c from 'picocolors';
import capitalize from './lib/capitalize.js'; import capitalize from './lib/capitalize.js';
export default class ConfigsProcessor { export default class ConfigsProcessor {
/** @type {string} */
dir = process.cwd();
/** @type {import('./types.js').Config[]} */ /** @type {import('./types.js').Config[]} */
configs; configs;
/** @type {string[] | undefined} */ /** @type {string} */
#packagesPatterns; dir = process.cwd();
/** /**
* @param {{ * @param {{
* configs: import('./types.js').Config[], * configs: import('./types.js').Config[],
* packages?: string[], * packages?: string[],
* directory?: string, * directory?: string,
* }} options - Cli options * }} options - Cli options.
*/ */
constructor(options) { constructor(options) {
this.#packagesPatterns = options.packages; this.configs = options.configs;
this.configs = options?.configs;
this.dir = path.normalize(options.directory ?? this.dir); this.dir = path.normalize(options.directory ?? this.dir);
} }
/** /**
* @param {import('./types.js').Package} pkg - Package to detect from * @param {import('./types.js').Package} pkg - The package to detect configs.
* @param {import('./types.js').Config['options']} options - Options to be passed * @returns {import('./types.js').Package['config']} - Detected configs record.
* @param {boolean} single - Whether to only detect one option
* @returns {string[]} - The detected options
*/ */
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[]} */ /** @type {string[]} */
const detectedOptions = []; const detectedOptions = [];
for (const option of options) { for (const option of options) {
if (option.detect === true) { if (option.detect === true) {
detectedOptions.push(option.name); detectedOptions.push(option.name);
// eslint-disable-next-line no-continue
continue; continue;
} }
else if (!option.detect) continue; // eslint-disable-next-line no-continue
else if (!option.detect) { continue; }
const match = glob(option.detect); const match = glob(option.detect);
const files = pkg.files.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); const directories = pkg.directories.filter(f =>
(match ? match(f) : false),
);
if (files.length > 0 || directories.length > 0) { if (files.length > 0 || directories.length > 0) {
detectedOptions.push(option.name); 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.js').Package[] | import('./types.js').Package} pkg -
* @param {import('./types').Config[]} configs - The configs to be used * The packages to questions the configs.
* @returns {Promise<import('./types.js').Package[]>} - The selected options by the user * @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) { async questionConfig(pkg, configs) {
const packages = Array.isArray(pkg) ? [...pkg] : [pkg]; 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`); const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`);
for (const config of configs) { for (const config of configs) {
/** @type {import('prompts').Choice[]} */ /** @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[]>} */ /** @type {Record<string, string[]>} */
// eslint-disable-next-line no-await-in-loop
const selectedOptions = await prompts({ const selectedOptions = await prompts({
name: config.name,
type: config.type === 'multiple' ? 'multiselect' : 'select',
message: capitalize(config.name),
choices: config.type === 'confirm' ? [ choices: config.type === 'confirm' ? [
{ {
title: 'Yes', title: 'Yes',
@@ -93,11 +110,19 @@ export default class ConfigsProcessor {
}, },
] : configChoices, ] : configChoices,
hint: config.description, 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; if (!selectedOptions[config.name]) continue;
// eslint-disable-next-line no-continue
if (selectedOptions[config.name].length === 0) continue; if (selectedOptions[config.name].length === 0) continue;
if (packages.length <= 1) { if (packages.length <= 1) {
@@ -105,64 +130,47 @@ export default class ConfigsProcessor {
...(packages[0].config ?? []), ...(packages[0].config ?? []),
...Object.entries(selectedOptions), ...Object.entries(selectedOptions),
]); ]);
// eslint-disable-next-line no-continue
continue; continue;
} }
/** @type {{title: string, value: import('./types').Package}[]} */ /** @type {{title: string, value: import('./types.js').Package}[]} */
const packagesOptions = packages const packagesOptions = packages
.map(pkg => { .map(p => (p.root
return !pkg.root ? { title: 'root', value: p }
? { : {
title: `${pkg.name} ${c.dim(pkg.path.replace(this.dir, '.'))}`, title: `${p.name} ${c.dim(p.path.replace(this.dir, '.'))}`,
value: pkg, value: p,
} }))
: { title: 'root', value: pkg };
})
.filter(p => p.title !== 'root'); .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({ 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', name: 'packages',
type: 'autocompleteMultiselect', 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 => { selected.packages.map((p) => {
pkg.config = new Map([ p.config = new Map([
...(pkg.config ?? []), ...(p.config ?? []),
...Object.entries(selectedOptions), ...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; 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

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

View File

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

View File

@@ -1,11 +1,12 @@
/** /**
* @param {import('../types').Package[]} packages - Package list * @param {import('../types').Package[]} packages - Package list.
* @returns {number} Number of packages' configs * @returns {number} Number of packages' configs.
*/ */
function packagesWithConfigs(packages) { function packagesWithConfigs(packages) {
return packages.map(p => 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); ).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. * JSDoc types lack a non-null assertion.
* @template T * @template T
* @param {T} value The value which to assert against null or undefined * @param {T} value - The value which to assert against null or undefined.
* @returns {NonNullable<T>} The said value * @returns {NonNullable<T>} The said value.
* @throws {TypeError} If the value is unexpectedly null or undefined * @throws {TypeError} If the value is unexpectedly null or undefined.
* @author Jimmy Wärting - https://github.com/jimmywarting
* @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031 * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031
* @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966 * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966
* @author Jimmy Wärting - https://github.com/jimmywarting
*/ */
export default function notNull(value) { export default function notNull(value) {
// Use `==` to check for both null and undefined // 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; 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 { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { exec } from 'node:child_process'; import { exec } from 'node:child_process';
import { join } from 'node:path';
import { parse, prettyPrint } from 'recast';
import { createSpinner } from 'nanospinner'; import { createSpinner } from 'nanospinner';
import c from 'picocolors'; import c from 'picocolors';
import { parse, prettyPrint } from 'recast';
import { readFile, writeFile } from 'node:fs/promises';
/** /**
* @type {import('./types').PackageManagerHandler} * @type {import('./types').PackageManagerHandler}
*/ */
class CommandHandler { class CommandHandler {
/** @type {((path: string, packages: string[]) => string | Promise<string>) | undefined} */
checker;
/** @type {string} */ /** @type {string} */
command; command;
/** @type {((path: string, packages: string[]) => string | Promise<string>) | undefined} */
checker;
/** /**
* @param {string} command What command to use to install * @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 {(path: string, packages: string[]) => string | Promise<string>} [checker] -
* Checks if a argument should be passed.
*/ */
constructor(command, checker) { constructor(command, checker) {
this.command = command; this.command = command;
@@ -28,20 +31,20 @@ class CommandHandler {
} }
/** /**
* @param {string} path The path to run the command * @param {string} path - The path to run the command.
* @param {string[]} packages The packages to be added on the command * @param {string[]} packages - The packages to be added on the command.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async install(path, packages) { async install(path, packages) {
if (this.checker) if (this.checker)
this.command += await this.checker(path, packages); this.command += await this.checker(path, packages);
return new Promise((res) => { return new Promise((res) => {
const spinner = createSpinner(`Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}`).start(); const spinner = createSpinner(`Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}`).start();
try { try {
// eslint-disable-next-line security/detect-child-process
const child = exec(`${this.command} ${packages.join(' ')}`, { cwd: path }); 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 // 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)}`, text: `Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}\n ${c.dim(chunk)}`,
})); }));
@@ -52,7 +55,8 @@ class CommandHandler {
}); });
} }
catch (error) { 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}`)); res(console.error(`Error while installing the packages with ${this.command} ${c.dim(packages.join(' '))} on ${path}: ${error}`));
} }
}); });
@@ -62,48 +66,44 @@ class CommandHandler {
/** /**
* @type {import('./types').PackageManagerHandler} * @type {import('./types').PackageManagerHandler}
*/ */
class DenoHandler { const DenoHandler = {
/** /**
* @param {string} path The path to run the command * @param {string} path - The path to run the command.
* @param {string[]} packages The packages to be added on the command * @param {string[]} packages - The packages to be added on the command.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async install(path, packages) { async install(path, packages) {
const configPath = join(path, 'eslint.config.js'); const configPath = join(path, 'eslint.config.js');
// eslint-disable-next-line security/detect-non-literal-fs-filename
if (!existsSync(configPath)) return; if (!existsSync(configPath)) return;
// eslint-disable-next-line security/detect-non-literal-fs-filename
const configFile = await readFile(configPath, 'utf8'); 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 // 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) => { ast.body.map((node) => {
if (node.type !== 'ImportDeclaration') return 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}`; node.source.value = `npm:${node.source.value}`;
}
return node; 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')); console.log(c.green('Added npm: specifier to dependencies'));
},
} };
}
export default class PackageInstaller { export default class PackageInstaller {
/**
* @typedef {Map<string, string[]>} PackagesMap
* @type {PackagesMap}
*/
packagesMap;
/** /**
* @typedef {{ * @typedef {{
* name: import('./types').PackageManagerName * name: import('./types').PackageManagerName
@@ -118,40 +118,51 @@ export default class PackageInstaller {
* @type {Record<import('./types').PackageManagerName, PackageManager>} * @type {Record<import('./types').PackageManagerName, PackageManager>}
*/ */
packageManagers = { packageManagers = {
deno: {
name: 'deno',
description: 'Adds npm: specifiers to the eslint.config.js file',
handler: new DenoHandler(),
},
bun: { bun: {
name: 'bun',
description: 'Uses bun install', description: 'Uses bun install',
handler: new CommandHandler('bun install'), handler: new CommandHandler('bun install'),
name: 'bun',
}, },
pnpm: { deno: {
name: 'pnpm', description: 'Adds npm: specifiers to the eslint.config.js file',
description: 'Uses pnpm install', handler: DenoHandler,
handler: new CommandHandler('pnpm install --save-dev', (path) => { name: 'deno',
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'),
}, },
npm: { npm: {
name: 'npm',
description: 'Uses npm install', description: 'Uses npm install',
handler: new CommandHandler('npm install --save-dev'), 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 * @typedef {Map<string, string[]>} PackagesMap
* @param {string} root Root directory path * @type {PackagesMap}
*/
packagesMap;
/**
* @param {PackagesMap} packagesMap - The map of directories and packages to be installed.
* @param {string} root - Root directory path.
*/ */
constructor(packagesMap, root) { constructor(packagesMap, root) {
this.packagesMap = packagesMap; this.packagesMap = packagesMap;
@@ -159,51 +170,68 @@ export default class PackageInstaller {
} }
/** /**
* @param {string} root Root directory path * @param {string} root - Root directory path.
* @returns {PackageManager} The package manager detected; * @returns {PackageManager} The package manager detected;.
* @private * @private
*/ */
// eslint-disable-next-line complexity
detectPackageManager(root) { detectPackageManager(root) {
/** @type {(...path: string[]) => boolean} */ /** @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) { switch (true) {
case exists('deno.json'): case exists('deno.json'):
case exists('deno.jsonc'): case exists('deno.jsonc'): {
return this.packageManagers.deno; return this.packageManagers.deno;
}
case exists('bun.lockb'): case exists('bun.lockb'): {
return this.packageManagers.bun; return this.packageManagers.bun;
}
case exists('pnpm-lock.yaml'): case exists('pnpm-lock.yaml'): {
return this.packageManagers.pnpm; return this.packageManagers.pnpm;
}
case exists('yarn.lock'): case exists('yarn.lock'): {
return this.packageManagers.yarn; return this.packageManagers.yarn;
}
case exists('package-lock.json'): case exists('package-lock.json'): {
return this.packageManagers.npm; return this.packageManagers.npm;
}
case exists('package.json'): case exists('package.json'): {
/** @type {{packageManager?: string}} */ /** @type {{packageManager?: string}} */
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-case-declarations // 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) return this.packageManagers.npm;
if (packageManager.includes('pnpm')) return this.packageManagers.pnpm; if (packageManager.includes('pnpm'))
if (packageManager.includes('yarn')) return this.packageManagers.yarn; return this.packageManagers.pnpm;
if (packageManager.includes('npm')) return this.packageManagers.npm; 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() { 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); await this.packageManager.handler.install(path, packages);
} }
} }
}

View File

@@ -1,64 +1,72 @@
import type { OptionValues } from 'commander'; import type { OptionValues } from 'commander';
type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'; type PackageManagerName = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn';
type CliArgs = {
packages?: string[]
mergeToRoot?: boolean
installPkgs?: boolean | PackageManagerName
dir: string
configs: Config[]
} & OptionValues;
interface PackageManagerHandler { interface PackageManagerHandler {
install(path: string, packages: string[]): Promise<void> | void install(path: string, packages: string[]): Promise<void> | void,
} }
type Config = { type Config = {
name: string description?: string,
type: 'single' | 'multiple' manual: true,
manual?: boolean name: string,
description?: string
options: {
name: string
packages?: Record<string, string | (string | [string, string])[]>
configs?: string[]
rules?: string[]
presets?: string[]
detect?: string[] | true
}[]
} | {
name: string
type: 'confirm'
manual: true
description?: string
options: [{ options: [{
name: 'yes' configs?: string[],
packages?: Record<string, string | (string | [string, string])[]> detect?: undefined,
configs?: string[] name: 'yes',
rules?: string[] packages?: { [key: string]: ([string, string] | string)[] | string, },
presets?: string[] presets?: string[],
detect?: undefined 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 { type CliArgs = {
root?: boolean configs: Config[],
name: string dir: string,
path: string installPkgs?: PackageManagerName | boolean,
files: string[] mergeToRoot?: boolean,
directories: string[] packages?: string[],
config?: Map<string, string[]> } & OptionValues;
configFile?: ConfigFile
}
interface ConfigFile { interface ConfigFile {
path: string configs: string[],
imports: Map<string, string | (string | [string, string])[]> content?: string,
configs: string[] imports: Map<string, ([string, string] | string)[] | string>,
presets: string[] path: string,
rules: string[] presets: string[],
content?: 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'; /* eslint-disable security/detect-non-literal-fs-filename */
import { existsSync } from 'node:fs';
import YAML from 'yaml';
import path, { join } from 'node:path'; import path, { join } from 'node:path';
import { existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import picomatch from 'picomatch'; import picomatch from 'picomatch';
import YAML from 'yaml';
/** /**
* @template T * @template T
* @param {Promise<T>} promise - The async function to try running * @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 * @returns {Promise<T | null>} - Returns the result of the async function, or null if it errors.
*/ */
async function tryRun(promise) { async function tryRun(promise) {
try { try {
return await promise; return await promise;
} }
catch (err) { catch {
return null; return null;
} }
} }
/** /**
* @param {string} directory - The directory to find .gitignore and .eslintignore * @param {string} directory - The directory to find .gitignore and .eslintignore.
* @returns {Promise<string[]>} - List of ignore glob patterns * @returns {Promise<string[]>} - List of ignore glob patterns.
*/ */
async function getIgnoredFiles(directory) { 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') .split('\n')
.filter(p => p && !p.startsWith('#')) .filter(p => p && !p.startsWith('#'))
.map(p => join(directory, '**', p)); .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') .split('\n')
.filter(p => p && !p.startsWith('#')) .filter(p => p && !p.startsWith('#'))
.map(p => join(directory, '**', p)); .map(p => join(directory, '**', p));
@@ -46,19 +52,19 @@ async function getPackageName(directory) {
const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); const file = await fs.readFile(join(directory, 'package.json'), 'utf8');
/** @type {{name?: string}} */ /** @type {{name?: string}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // 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; return path.normalize(directory).split('/').at(-1) ?? directory;
} }
export default class Workspace { export default class Workspace {
/** /**
* @param {string} directory - The directory to get the workspace from * @param {string} directory -
* @param {string[] | false} [packagePatterns] * The directory to get the workspace from.
* List of package patterns (`false` to explicitly tell that this workspace is not a monorepo) * @param {string[] | false} [packagePatterns] -
* List of package patterns (`false` to explicitly tell that this workspace is not a monorepo).
*/ */
constructor(directory, packagePatterns) { constructor(directory, packagePatterns) {
this.dir = directory; this.dir = directory;
@@ -66,125 +72,93 @@ export default class Workspace {
} }
/** /**
* @param {string} [directory] - The directory to work on * @returns {Promise<string[]>} - List of packages on a directory;.
* @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;
*/ */
async getPackagePatterns() { async getPackagePatterns() {
/** @type {string[]} */ /** @type {string[]} */
let packagePatterns = []; const packagePatterns = [];
const pnpmWorkspace = const pnpmWorkspace =
existsSync(join(this.dir, 'pnpm-workspace.yaml')) existsSync(join(this.dir, 'pnpm-workspace.yaml'))
? 'pnpm-workspace.yaml' ? 'pnpm-workspace.yaml'
: existsSync(join(this.dir, 'pnpm-workspace.yml')) : (existsSync(join(this.dir, 'pnpm-workspace.yml'))
? 'pnpm-workspace.yml' ? 'pnpm-workspace.yml'
: null; : null);
if (pnpmWorkspace) { 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[]}} */ /** @type {{packages?: string[]}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // 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'))) { 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[]}} */ /** @type {{workspaces?: string[]}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // 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 = path.normalize(p);
p = p.startsWith('/') ? p.replace('/', '') : 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; 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() { async getPackages() {
const paths = await this.getPaths(); const paths = await this.getPaths();
/** @type {import('./types').Package} */ /** @type {import('./types').Package} */
const rootPackage = { const rootPackage = {
root: true, directories: paths.directories,
files: paths.files,
name: await getPackageName(this.dir), name: await getPackageName(this.dir),
path: this.dir, path: this.dir,
files: paths.files, root: true,
directories: paths.directories,
}; };
if (this.packagePatterns === false) return [rootPackage]; if (this.packagePatterns === false) return [rootPackage];
const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns(); const packagePatterns =
const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns)); this.packagePatterns ??
await this.getPackagePatterns();
const packagePaths = paths.directories.filter(d =>
picomatch.isMatch(d, packagePatterns),
);
/** @type {import('./types').Package[]} */ /** @type {import('./types').Package[]} */
const packages = []; const packages = [];
for (const packagePath of packagePaths) { for (const packagePath of packagePaths) {
packages.push({ 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 directories: paths.directories
.filter(d => picomatch.isMatch(d, `${packagePath}/**/*`)) .filter(d => picomatch.isMatch(d, `${packagePath}/**/*`))
.map(d => d.replace(`${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 rootPackage.files = rootPackage.files
@@ -195,51 +169,101 @@ export default class Workspace {
} }
return [rootPackage, ...packages]; return [rootPackage, ...packages];
} }
/** /**
* @param {import('./types').Package[]} packages - Packages to be merged into root * @param {string} [directory] - The directory to work on.
* @returns {[import('./types').Package]} A array containing only the root package * @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 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([ const files = [...new Set([
...accumulated.files, ...accumulated.files,
...pkg.files.map(f => join(pkg.path, f)), ...package_.files.map(f => join(package_.path, f)),
] ]
.map(p => p.replace(`${rootPackage.path}/`, '')), .map(p => p.replace(`${rootPackage.path}/`, '')),
)]; )];
const directories = [...new Set([ const directories = [...new Set([
...accumulated.directories, ...accumulated.directories,
...pkg.directories.map(d => join(pkg.path, d)), ...package_.directories.map(d => join(package_.path, d)),
] ]
.map(p => p.replace(`${rootPackage.path}/`, ''))), .map(p => p.replace(`${rootPackage.path}/`, ''))),
]; ];
const mergedConfig = new Map(); const mergedConfig = new Map();
for (const [config, options] of pkg.config ?? []) { for (const [config, options] of package_.config ?? []) {
const accumulatedOptions = accumulated.config?.get(config) ?? []; const accumulatedOptions =
mergedConfig.set(config, [...new Set([...options, ...accumulatedOptions])]); accumulated.config?.get(config) ??
[];
mergedConfig.set(
config,
[...new Set([...options, ...accumulatedOptions])],
);
} }
return { return {
root: true,
path: rootPackage.path,
name: rootPackage.name,
files,
directories,
config: mergedConfig, config: mergedConfig,
directories,
files,
name: rootPackage.name,
path: rootPackage.path,
root: true,
}; };
}, rootPackage); }, rootPackage);
return [merged]; return [merged];
} }
} }

View File

@@ -1,4 +1,11 @@
#!/usr/bin/env node
import process from 'node:process';
import Cli from '@eslegant/cli'; 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(); await cli.run();

View File

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

View File

@@ -9,8 +9,8 @@
"url": "https://guz.one" "url": "https://guz.one"
}, },
"files": [ "files": [
"./bin.js", "bin.js",
"./configs.js" "configs.js"
], ],
"dependencies": { "dependencies": {
"@eslegant/cli": "workspace:*" "@eslegant/cli": "workspace:*"

View File

@@ -1,4 +1,11 @@
#!/usr/bin/env node
import process from 'node:process';
import Cli from '@eslegant/cli'; 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(); await cli.run();

View File

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

View File

@@ -9,8 +9,8 @@
"url": "https://guz.one" "url": "https://guz.one"
}, },
"files": [ "files": [
"./bin.js", "bin.js",
"./configs.js" "configs.js"
], ],
"dependencies": { "dependencies": {
"@eslegant/cli": "workspace:*" "@eslegant/cli": "workspace:*"

78
pnpm-lock.yaml generated
View File

@@ -31,8 +31,8 @@ importers:
specifier: ^8.47.0 specifier: ^8.47.0
version: 8.47.0 version: 8.47.0
husky: husky:
specifier: ^8.0.3 specifier: ^9.0.0
version: 8.0.3 version: 9.0.10
turbo: turbo:
specifier: ^1.10.12 specifier: ^1.10.12
version: 1.10.12 version: 1.10.12
@@ -54,6 +54,9 @@ importers:
eslint-import-resolver-typescript: eslint-import-resolver-typescript:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0(@typescript-eslint/parser@6.4.1)(eslint-plugin-import@2.28.1)(eslint@8.47.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: eslint-plugin-i:
specifier: 2.28.0-2 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) version: 2.28.0-2(@typescript-eslint/parser@6.4.1)(eslint-import-resolver-typescript@3.6.0)(eslint@8.47.0)
@@ -794,6 +797,10 @@ packages:
read-yaml-file: 1.1.0 read-yaml-file: 1.1.0
dev: true 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: /@neoconfetti/svelte@1.0.0:
resolution: {integrity: sha512-SmksyaJAdSlMa9cTidVSIqYo1qti+WTsviNDwgjNVm+KQ3DRP2Df9umDIzC4vCcpEYY+chQe0i2IKnLw03AT8Q==} resolution: {integrity: sha512-SmksyaJAdSlMa9cTidVSIqYo1qti+WTsviNDwgjNVm+KQ3DRP2Df9umDIzC4vCcpEYY+chQe0i2IKnLw03AT8Q==}
dev: true dev: true
@@ -1440,6 +1447,12 @@ packages:
util: 0.12.5 util: 0.12.5
dev: false 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: /ast-types@0.16.1:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -1496,6 +1509,17 @@ packages:
wcwidth: 1.0.1 wcwidth: 1.0.1
dev: true 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: /buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
dev: true dev: true
@@ -1542,6 +1566,10 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/caniuse-lite@1.0.30001532:
resolution: {integrity: sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==}
dev: false
/cardinal@2.1.1: /cardinal@2.1.1:
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
hasBin: true hasBin: true
@@ -1827,6 +1855,10 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/electron-to-chromium@1.4.513:
resolution: {integrity: sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==}
dev: false
/emoji-regex@8.0.0: /emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: true dev: true
@@ -1958,7 +1990,6 @@ packages:
/escalade@3.1.1: /escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true
/escape-string-regexp@1.0.5: /escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
@@ -2031,6 +2062,22 @@ packages:
- supports-color - supports-color
dev: false 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): /eslint-plugin-es-x@7.2.0(eslint@8.47.0):
resolution: {integrity: sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==} resolution: {integrity: sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@@ -2688,9 +2735,9 @@ packages:
resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==}
dev: true dev: true
/husky@8.0.3: /husky@9.0.10:
resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} resolution: {integrity: sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA==}
engines: {node: '>=14'} engines: {node: '>=18'}
hasBin: true hasBin: true
dev: true dev: true
@@ -3044,6 +3091,10 @@ packages:
dependencies: dependencies:
p-locate: 5.0.0 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: /lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -3213,6 +3264,10 @@ packages:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
dev: true dev: true
/node-releases@2.0.13:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: false
/normalize-package-data@2.5.0: /normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies: dependencies:
@@ -4261,6 +4316,17 @@ packages:
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
dev: true 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: /uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies: dependencies: