diff --git a/.vscode/settings.json b/.vscode/settings.json index 14b4db7..ead1e7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,6 +23,11 @@ "yaml" ], "cSpell.words": [ - "eslegant" + "eslegant", + "estree", + "nanospinner", + "picocolors", + "picomatch", + "sisteransi" ] } diff --git a/configs/js/src/configs/formatting.js b/configs/js/src/configs/formatting.js index ebfe16a..946f872 100644 --- a/configs/js/src/configs/formatting.js +++ b/configs/js/src/configs/formatting.js @@ -24,7 +24,7 @@ const recommended = createVariations({ 'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }], 'comma-style': 'error', 'curly': ['error', 'multi-or-nest', 'consistent'], - 'dot-location': 'error', + 'dot-location': ['error', 'property'], 'eol-last': 'error', 'generator-star-spacing': ['error', 'before'], 'no-mixed-spaces-and-tabs': 'error', diff --git a/configs/js/src/configs/naming.js b/configs/js/src/configs/naming.js index 0062f97..b32adf9 100644 --- a/configs/js/src/configs/naming.js +++ b/configs/js/src/configs/naming.js @@ -16,7 +16,10 @@ const recommended = createVariations({ rules: { ...{}, // Plugin: eslint-plugin-unicorn 'unicorn/filename-case': ['error', { case: 'kebabCase' }], - 'unicorn/prevent-abbreviations': 'error', + /* + * TODO [>=1.0.0]: This will be replaced by a better naming convention. + * 'unicorn/prevent-abbreviations': 'error', + */ }, }); diff --git a/configs/js/src/configs/suggestions.js b/configs/js/src/configs/suggestions.js index 3cbe94d..29fd152 100644 --- a/configs/js/src/configs/suggestions.js +++ b/configs/js/src/configs/suggestions.js @@ -15,7 +15,12 @@ const recommended = createVariations({ files: FILES, rules: { 'camelcase': 'error', - 'max-len': ['error', { code: 80, comments: 100, ignoreUrls: true }], + 'max-len': ['error', { + code: 80, + comments: 100, + ignoreTemplateLiterals: true, + ignoreUrls: true, + }], 'no-case-declarations': 'error', 'no-confusing-arrow': 'error', 'no-console': 'error', @@ -287,7 +292,7 @@ const strict = createVariations({ }], 'max-nested-callbacks': ['error', 10], 'max-params': ['error', 4], - 'max-statements': ['error', 10], + 'max-statements': ['error', 15], 'multiline-comment-style': ['error', 'starred-block'], 'new-cap': 'error', 'new-parens': 'error', @@ -303,11 +308,8 @@ const strict = createVariations({ 'no-extend-native': 'error', 'no-extra-bind': 'error', 'no-extra-boolean-cast': 'error', - 'no-extra-parens': ['error', 'all', { - enforceForArrowConditionals: false, - nestedBinaryExpressions: false, - ternaryOperandBinaryExpressions: false, - }], + // TODO [>=1.0.0]: Fix no-extra-parens conflict with the unicorn/no-nested-ternary rule. + 'no-extra-parens': ['error', 'functions'], 'no-floating-decimal': 'error', 'no-implicit-coercion': 'error', 'no-implied-eval': 'error', diff --git a/configs/js/src/index.js b/configs/js/src/index.js index 0c94145..9633b9a 100644 --- a/configs/js/src/index.js +++ b/configs/js/src/index.js @@ -22,6 +22,6 @@ function defineConfig(config) { const eslegant = { configs, presets }; -export { defineConfig, eslegant as default }; +export { defineConfig, eslegant as default }; export { default as configs } from './configs/index.js'; export { default as presets } from './presets/index.js'; diff --git a/configs/js/src/lib/rule-variations.js b/configs/js/src/lib/rule-variations.js index 47822e2..e5e199e 100644 --- a/configs/js/src/lib/rule-variations.js +++ b/configs/js/src/lib/rule-variations.js @@ -27,6 +27,7 @@ function changeLevel(ruleEntry, level) { if (typeof level === 'number') { /** @type {RuleLevel[]} */ const levels = ['error', 'off', 'warn']; + // eslint-disable-next-line security/detect-object-injection level = levels[level]; } @@ -34,7 +35,6 @@ function changeLevel(ruleEntry, level) { return [level, ruleEntry[1]]; return level; - } /** diff --git a/packages/cli/index.d.ts b/packages/cli/index.d.ts index be33e9f..041ee02 100644 --- a/packages/cli/index.d.ts +++ b/packages/cli/index.d.ts @@ -1,17 +1,17 @@ -import type { CliArgs } from './src/types'; +import type { CliArgs } from './src/types'; /** - * Class that handles the creation and running the ESLegant command line interface + * Class that handles the creation and running the ESLegant command line interface. */ export default class Cli { /** - * @param args Arguments to pass to the cli when its runs + * @param args - Arguments to pass to the cli when its runs. */ - constructor(args: CliArgs); + public constructor(args: CliArgs); /** - * Runs the cli with the given arguments + * Runs the cli with the given arguments. */ - async run(): Promise; + public async run(): Promise; } export type { CliArgs, Config } from './src/types.d.ts'; diff --git a/packages/cli/index.js b/packages/cli/index.js index 16c6c35..53e0fc3 100644 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -1 +1 @@ -export { default as default } from './src/cli.js'; +export { default } from './src/cli.js'; diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 57cd6fe..0c4eef8 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -1,31 +1,35 @@ -import { Command } from 'commander'; -import ConfigsProcessor from './configsProcessor.js'; -import Workspace from './workspace.js'; -import c from 'picocolors'; +/* eslint-disable no-console */ +/* eslint-disable import/max-dependencies */ +import process from 'node:process'; import path from 'node:path'; + import { createSpinner } from 'nanospinner'; -import count from './lib/count.js'; -import prompts from 'prompts'; -import ConfigsFile from './configsFile.js'; -import cardinal from 'cardinal'; +import { Command } from 'commander'; import { erase } from 'sisteransi'; -import PackageInstaller from './packageInstaller.js'; -import notNull from './lib/notNull.js'; +import cardinal from 'cardinal'; +import prompts from 'prompts'; +import c from 'picocolors'; + +import PackageInstaller from './package-installer.js'; +import ConfigsProcessor from './configs-processor.js'; +import ConfigsFile from './configs-file.js'; +import notNull from './lib/not-null.js'; +import Workspace from './workspace.js'; +import count from './lib/count.js'; const stdout = process.stdout; export default class Cli { - #program = new Command(); /** @type {import('./types').CliArgs} */ args = { - dir: process.cwd(), configs: [], + dir: process.cwd(), }; /** - * @param {import('./types').CliArgs} [args] Cli arguments object + * @param {import('./types').CliArgs} [args] - Cli arguments object. */ constructor(args) { this.#program @@ -42,118 +46,148 @@ export default class Cli { ...args, }; - this.args.dir = !this.args.dir.startsWith('/') - ? path.join(process.cwd(), this.args.dir) - : this.args.dir; + this.args.dir = this.args.dir.startsWith('/') + ? this.args.dir + : path.join(process.cwd(), this.args.dir); } + // eslint-disable-next-line max-lines-per-function, max-statements, complexity async run() { - process.chdir(this.args.dir); const configs = this.args.configs; const spinner = createSpinner('Detecting workspace configuration'); const processor = new ConfigsProcessor({ configs }); - const workspace = new Workspace(this.args.dir, this.args?.packages); + const workspace = new Workspace(this.args.dir, this.args.packages); - let packages = (await workspace.getPackages()) - .map(pkg => { - spinner.update({ text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}` }); - - pkg.config = processor.detectConfig(pkg); - - return pkg; + let packages = await workspace.getPackages(); + packages = packages.map((pkg) => { + spinner.update({ + text: `Detecting configuration for package ${c.bold(c.blue(pkg.name))}`, }); + pkg.config = processor.detectConfig(pkg); + + return pkg; + }); + spinner.success({ text: - 'Detecting workspace configuration ' + - c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`), + `Detecting workspace configuration ${ + c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`)}`, }); - const merge = this.args.mergeToRoot !== undefined ? this.args.mergeToRoot : packages.length > 1 ? + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const merge = this.args.mergeToRoot ?? (packages.length > 1 /** @type {{merge: boolean}} */ - (await prompts({ - name: 'merge', - message: - `Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}` + - c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages')), + ? (await prompts({ initial: true, + message: + `Would you like to merge all configuration files into one root ${c.blue('eslint.config.js?')}${ + c.italic(c.dim('\nAll configurations will be applied to the entire workspace and packages'))}`, + name: 'merge', type: 'confirm', - })).merge : true; + // eslint-disable-next-line unicorn/no-await-expression-member + })).merge : true); console.log(c.dim('\nPlease select which options you prefer\n')); packages = await processor.questionConfig( - merge ? workspace.mergePackages(packages) : packages, - configs.filter(c => c.manual), + merge ? Workspace.mergePackages(packages) : packages, + configs.filter(config => config.manual), ); - const fileHandler = new ConfigsFile(configs, packages.find(c => c.root)?.path); + const fileHandler = new ConfigsFile( + configs, + packages.find(config => config.root)?.path, + ); for (const pkg of packages) { - pkg.configFile = fileHandler.generateObj(pkg); - pkg.configFile.content = await fileHandler.generate(pkg.configFile); + // eslint-disable-next-line no-await-in-loop + pkg.configFile.content = await ConfigsFile.generate(pkg.configFile); /** @type {boolean} */ const shouldWrite = /** @type {{write: boolean}} */ + // eslint-disable-next-line no-await-in-loop (await prompts({ - type: 'confirm', - name: 'write', + initial: true, message: `Do you want to write this config file for ${pkg.root ? c.blue('the root directory') : c.blue(pkg.name) }?\n\n${cardinal.highlight(pkg.configFile.content)}`, - initial: true, + name: 'write', + type: 'confirm', + // eslint-disable-next-line unicorn/no-await-expression-member })).write; - stdout.write(erase.lines(pkg.configFile.content.split('\n').length + 2)); - - if (shouldWrite) await fileHandler.write(pkg.configFile.path, pkg.configFile.content); + stdout.write( + erase.lines(pkg.configFile.content.split('\n').length + 2), + ); + if (shouldWrite) { + // eslint-disable-next-line no-await-in-loop + await ConfigsFile.write( + pkg.configFile.path, + pkg.configFile.content, + ); + } } - const packagesMap = new Map(packages.map(p => [p.path, [...notNull(p.configFile).imports.keys()]])); - const installer = new PackageInstaller(packagesMap, packages.find(p => p.root === true)?.path ?? this.args.dir); + const packagesMap = new Map(packages.map(p => + [p.path, [...notNull(p.configFile).imports.keys()]], + )); + const installer = new PackageInstaller( + packagesMap, + packages.find(p => p.root === true)?.path ?? this.args.dir, + ); /** @type {boolean | 'changePackage'} */ - let installPkgs = this.args.installPkgs !== undefined ? true : - /** @type {{install: boolean | 'changePackage'}} */ - (await prompts({ - name: 'install', - message: - `Would you like to ESLit to install the npm packages with ${c.green(installer.packageManager.name)}?\n${c.reset(c.dim(` Packages to install: ${[...new Set([...packagesMap.values()])].join(' ')}\n`))}`, - choices: [ - { title: 'Yes, install all packages', value: true, description: installer.packageManager.description }, - { title: 'No, I will install them manually', value: false }, - { title: 'Change package manager', value: 'changePackage' }, - ], - type: 'select', - })).install; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + let installPkgs = this.args.installPkgs ?? (await prompts({ + choices: [ + { + description: installer.packageManager.description, + title: 'Yes, install all packages', + value: true, + }, + { title: 'No, I will install them manually', value: false }, + { title: 'Change package manager', value: 'changePackage' }, + ], + message: + `Would you like to ESLit to install the npm packages with ${c.green(installer.packageManager.name)}?\n${c.reset(c.dim(` Packages to install: ${[...new Set(packagesMap.values())].join(' ')}\n`))}`, + name: 'install', + type: 'select', + // eslint-disable-next-line unicorn/no-await-expression-member + })).install; if (installPkgs === 'changePackage') { /** @type {{manager: import('./types').PackageManagerName}} */ const prompt = await prompts({ - name: 'manager', + choices: Object.values(installer.packageManagers).map(m => ({ + description: m.description, + title: m.name, + value: m.name, + })), message: 'What package manager do you want ESLit to use?', - choices: Object.values(installer.packageManagers).map(m => { - return { title: m.name, description: m.description, value: m.name }; - }), + name: 'manager', type: 'select', }); - installer.packageManager = installer.packageManagers[prompt.manager]; + installer.packageManager = + installer.packageManagers[prompt.manager]; - if (!installer.packageManager) throw console.log(c.red('You must select a package manager')); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!installer.packageManager) + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-throw-literal, @typescript-eslint/no-confusing-void-expression + throw console.log(c.red('You must select a package manager')); installPkgs = true; } if (installPkgs) await installer.install(); - } - } diff --git a/packages/cli/src/configsFile.js b/packages/cli/src/configs-file.js similarity index 50% rename from packages/cli/src/configsFile.js rename to packages/cli/src/configs-file.js index 1d22cf0..0af5ba7 100644 --- a/packages/cli/src/configsFile.js +++ b/packages/cli/src/configs-file.js @@ -1,19 +1,26 @@ -import path from 'node:path'; -import notNull from './lib/notNull.js'; -import { parse, prettyPrint } from 'recast'; -import fs from 'node:fs/promises'; import { existsSync } from 'node:fs'; -import astUtils from './lib/astUtils.js'; +import process from 'node:process'; +import fs from 'node:fs/promises'; +import { join } from 'node:path'; + +import { parse, prettyPrint } from 'recast'; + +import astUtils from './lib/ast-utils.js'; +import notNull from './lib/not-null.js'; /** - * @param {import('./types').ConfigFile['imports']} map1 - The map to has it values merged from map2 - * @param {import('./types').ConfigFile['imports']} map2 - The map to has it values merged to map1 - * @returns {import('./types').ConfigFile['imports']} The resulting map + * @param {import('./types.js').ConfigFile['imports']} map1 - + * The map to has it values merged from map2. + * @param {import('./types.js').ConfigFile['imports']} map2 - + * The map to has it values merged to map1. + * @returns {import('./types.js').ConfigFile['imports']} The resulting map. */ +// eslint-disable-next-line max-statements function mergeImportsMaps(map1, map2) { for (const [key, value] of map2) { if (!map1.has(key)) { map1.set(key, value); + // eslint-disable-next-line no-continue continue; } const imports1 = notNull(map1.get(key)); @@ -23,33 +30,47 @@ function mergeImportsMaps(map1, map2) { * Because arrays and objects are always different independently from having equal values * ([] === [] -> false). It is converted to a string so the comparison can be made. */ - switch ([typeof imports1 === 'string', typeof imports2 === 'string'].join(',')) { - case 'true,true': - if (imports1.toString() === imports2.toString()) + switch ([ + typeof imports1 === 'string', + typeof imports2 === 'string', + ].join(',')) { + case 'true,true': { + if (imports1.toString() === imports2.toString()) { map1.set(key, value); - else - map1.set(key, [['default', imports1.toString()], ['default', imports2.toString()]]); + } + else { + map1.set(key, [ + ['default', imports1.toString()], + ['default', imports2.toString()], + ]); + } break; - case 'true,false': + } + case 'true,false': { map1.set(key, [['default', imports1.toString()], ...imports2]); break; - case 'false,true': + } + case 'false,true': { map1.set(key, [['default', imports2.toString()], ...imports1]); break; - case 'false,false': + } + case 'false,false': { map1.set(key, [...imports1, ...imports2]); break; + } + default: + // No nothing } if (typeof map1.get(key) !== 'string') - map1.set(key, [...new Set(map1.get(key))]); + map1.set(key, [...new Set(map1.get(key))]); } return map1; } /** - * @param {string} path1 The path to traverse from - * @param {string} root The root path - * @returns {string} The path to traverse + * @param {string} path1 - The path to traverse from. + * @param {string} root - The root path to be removed from the path1. + * @returns {string} The path to traverse. */ function getPathDepth(path1, root) { const pathDepth = path1.replace(root, '').split('/').slice(1); @@ -58,13 +79,12 @@ function getPathDepth(path1, root) { } export default class ConfigsWriter { - /** @type {string} */ root = process.cwd(); /** - * @param {import('./types').Config[]} configs The array of configs to construct from - * @param {string} [root] The root directory path + * @param {import('./types.js').Config[]} configs - The array of configs to construct from. + * @param {string} [root] - The root directory path. */ constructor(configs, root) { this.configs = configs; @@ -72,93 +92,13 @@ export default class ConfigsWriter { } /** - * @param {import('./types').Package} pkg The package to generate the config string from - * @returns {import('./types').ConfigFile} The config file object - */ - generateObj(pkg) { - /** @type {import('./types').ConfigFile} */ - const configObj = { - path: path.join(pkg.path, 'eslint.config.js'), - imports: new Map(), - configs: [], - presets: [], - rules: [], - }; - - if (!pkg.root) { - const rootConfig = path.join(getPathDepth(pkg.path, this.root), 'eslint.config.js'); - configObj.imports.set(!rootConfig.startsWith('.') ? `./${rootConfig}` : rootConfig, 'root'); - configObj.presets.push('root'); - } - - for (const [configName, optionsNames] of notNull(pkg.config)) { - - const config = this.configs.find(c => c.name === configName); - if (!config) continue; - - const options = config.options.filter(o => optionsNames.includes(o.name)); - if (!options || options.length === 0) continue; - - const imports = options.reduce((acc, opt) => { - const map1 = new Map(Object.entries(acc.packages ?? {})); - const map2 = new Map(Object.entries(opt.packages ?? {})); - acc.packages = Object.fromEntries(mergeImportsMaps(map1, map2)); - return acc; - }); - - configObj.imports = mergeImportsMaps(configObj.imports, new Map(Object.entries(imports.packages ?? {}))); - - configObj.configs = [...new Set([ - ...configObj.configs, - ...options.map(o => o.configs ?? []).flat(), - ])]; - - configObj.rules = [...new Set([ - ...configObj.rules, - ...options.map(o => o.rules ?? []).flat(), - ])]; - - configObj.presets = [...new Set([ - ...configObj.presets, - ...options.map(o => o.presets ?? []).flat(), - ])]; - - } - - return configObj; - } - - /** - * ! NOTE: - * These functions declared bellow are notably hard to read and have lots of exceptions and - * disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to - * prevent because a lot of the AST typescript types are somewhat wrong or simply hard to work - * with them. - * - * But for somewhat help developing and prevent unwanted errors in the future, the types and eslint - * errors are explicitly disabled and types are explicitly overridden. This is why there are so - * many JSDoc type annotations and comments in general. - * - * Any help to make this code more readable and robust is appreciated - */ - - /** - * @typedef {import('estree').Program} Program - * @typedef {( - * import('./lib/astUtils.js').ExpressionOrIdentifier | - * import('estree').ObjectExpression | - * import('estree').SpreadElement - * )} ConfigArrayElement - */ - - /** - * @param {Program} ast The program ast to be manipulated - * @returns {Promise} The final ast with the recreated default export + * @param {Program} ast - The program ast to be manipulated. + * @returns {Promise} The final ast with the recreated default export. * @private */ - async addDefaultExport(ast) { - + static async addDefaultExport(ast) { /** @type {{program: Program}} */ + // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call const { program: exportTemplateAst } = parse([ '/** @type {import(\'eslint\').Linter.FlatConfig[]} */', @@ -168,78 +108,81 @@ export default class ConfigsWriter { ].join('\n'), { parser: (await import('recast/parsers/babel.js')) }); /** @type {import('estree').ExportDefaultDeclaration} */ // @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded - const exportTemplateNode = exportTemplateAst.body.find(n => n.type === 'ExportDefaultDeclaration'); + const exportTemplateNode = exportTemplateAst.body + .find(n => n.type === 'ExportDefaultDeclaration'); /** @type {import('estree').ExportDefaultDeclaration | undefined} */ // @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded - let astExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); + const astExport = ast.body + .find(n => n.type === 'ExportDefaultDeclaration'); + if (!astExport) { ast.body.push(exportTemplateNode); return ast; } /** @type {import('estree').VariableDeclaration | undefined} */ - const oldExportValue = astExport.declaration.type !== 'ArrayExpression' - // @ts-expect-error astExport.declaration is a expression - ? astUtils.createVariable('oldConfig', 'const', astExport.declaration) - : undefined; + const oldExportValue = astExport.declaration.type === 'ArrayExpression' + ? undefined + : astUtils.createVariable( + 'oldConfig', + // @ts-expect-error astExport.declaration is a expression + astExport.declaration, + 'const', + ); if (!oldExportValue) return ast; // @ts-expect-error declaration is a ArrayExpression + // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call exportTemplateNode.declaration.elements.push({ + argument: { name: 'oldConfig', type: 'Identifier' }, type: 'SpreadElement', - argument: { type: 'Identifier', name: 'oldConfig' }, }); const astExportIdx = ast.body.indexOf(astExport); + // eslint-disable-next-line security/detect-object-injection ast.body[astExportIdx] = exportTemplateNode; ast.body.splice(astExportIdx - 1, 0, oldExportValue); return ast; - } /** - * @param {import('./types').ConfigFile['rules']} rules The rules to be used to create the object - * @returns {import('estree').ObjectExpression} The object containing the spread rules - * @private + * ! NOTE: + * These functions declared bellow are notably hard to read and have lots of exceptions and + * disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to + * prevent because a lot of the AST typescript types are somewhat wrong or simply hard to work + * with them. + * + * But for somewhat help developing and prevent unwanted errors in the future, the types and + * eslint errors are explicitly disabled and types are explicitly overridden. This is why + * there are so many JSDoc type annotations and comments in general. + * + * Any help to make this code more readable and robust is appreciated. */ - createRulesObject(rules) { - /** @type {import('estree').SpreadElement[]} */ - // @ts-expect-error The array is filtered to remove undefined's - const expressions = rules - .map(r => { - const e = astUtils.stringToExpression(r); - if (e) return astUtils.toSpreadElement(e); - else undefined; - }).filter(e => e); - - return { - type: 'ObjectExpression', - properties: [{ - // @ts-expect-error because ObjectProperty doesn't exist in estree types - type: 'ObjectProperty', - key: { type: 'Identifier', name: 'rules' }, - value: { - type: 'ObjectExpression', - properties: expressions, - }, - }], - }; - - } /** - * Adds elements to the default export node, without adding duplicates + * @typedef {import('estree').Program} Program + * @typedef {( + * import('./lib/ast-utils.js').ExpressionOrIdentifier | + * import('estree').ObjectExpression | + * import('estree').SpreadElement + * )} ConfigArrayElement + */ + + /** + * Adds elements to the default export node, without adding duplicates. * @typedef {import('estree').ArrayExpression} ArrayExpression - * @param {Program} ast The program ast to be manipulated - * @param {ConfigArrayElement[]} elements The elements to be added to the array - * @returns {Program} The final ast with the recreated default export + * @param {Program} ast - The program ast to be manipulated. + * @param {ConfigArrayElement[]} elements - The elements to be added to the array. + * @returns {Program} The final ast with the recreated default export. * @private */ - addElementsToExport(ast, elements) { + static addElementsToExport(ast, elements) { /** @type {import('estree').ExportDefaultDeclaration} */ // @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded - const exportNode = ast.body.find(n => n.type === 'ExportDefaultDeclaration'); + const exportNode = ast.body.find(n => + n.type === 'ExportDefaultDeclaration', + ); const exportNodeIdx = ast.body.indexOf(exportNode); /** @type {ArrayExpression} */ @@ -247,45 +190,61 @@ export default class ConfigsWriter { const array = exportNode.declaration; for (const e of elements) { - if (e.type !== 'ObjectExpression' && astUtils.findInArray(array, e)) continue; - array.elements.push(e); + if (!( + e.type !== 'ObjectExpression' && + astUtils.findInArray(array, e) + )) + array.elements.push(e); } exportNode.declaration = array; + // eslint-disable-next-line security/detect-object-injection ast.body[exportNodeIdx] = exportNode; return ast; } /** - * @param {Program} ast The program ast to be manipulated - * @param {import('./types').ConfigFile['imports']} imports The imports map to be used - * @returns {Program} The final ast with the recreated default export + * @param {Program} ast - The program ast to be manipulated. + * @param {import('./types.js').ConfigFile['imports']} imports - The imports map to be used. + * @returns {Program} The final ast with the recreated default export. */ - addPackageImports(ast, imports) { - + static addPackageImports(ast, imports) { /** @type {import('estree').ImportDeclaration[]} */ const importDeclarations = []; for (const [pkgName, specifiers] of imports) { /** @type {import('estree').ImportDeclaration | undefined} */ // @ts-expect-error type error, the specifier has to be ImportDeclaration to be founded - const existingDeclaration = ast.body.find(s => s.type === 'ImportDeclaration' && s.source.value === pkgName); + const existingDeclaration = ast.body.find(s => + s.type === 'ImportDeclaration' && + s.source.value === pkgName, + ); const importDeclaration = astUtils.createImportDeclaration( - pkgName, typeof specifiers === 'string' ? specifiers : undefined, existingDeclaration, + pkgName, + typeof specifiers === 'string' + ? specifiers + : undefined, + existingDeclaration, ); if (typeof specifiers !== 'string') { - specifiers.forEach(s => { - if (typeof s === 'string') return importDeclaration.addSpecifier(s); - else return importDeclaration.addSpecifier(s[0], s[1]); - }); + for (const s of specifiers) { + if (typeof s === 'string') + importDeclaration.addSpecifier(s); + else + importDeclaration.addSpecifier(s[0], s[1]); + } } - if (existingDeclaration) ast.body[ast.body.indexOf(existingDeclaration)] = importDeclaration.body; - else importDeclarations.push(importDeclaration.body); - + if (existingDeclaration) { + ast.body[ast.body.indexOf(existingDeclaration)] = + importDeclaration.body; + } + else { + importDeclarations.push(importDeclaration.body); + } } ast.body.unshift(...importDeclarations); @@ -293,20 +252,55 @@ export default class ConfigsWriter { return ast; } + /** + * @param {import('./types.js').ConfigFile['rules']} rules - + * The rules to be used to create the object. + * @returns {import('estree').ObjectExpression} The object containing the spread rules. + * @private + */ + static createRulesObject(rules) { + /** @type {import('estree').SpreadElement[]} */ + // @ts-expect-error The array is filtered to remove undefined's + const expressions = rules + .map((r) => { + const e = astUtils.stringToExpression(r); + return e ? astUtils.toSpreadElement(e) : undefined; + }).filter(Boolean); + + return { + properties: [{ + key: { name: 'rules', type: 'Identifier' }, + // @ts-expect-error because ObjectProperty doesn't exist in estree types + type: 'ObjectProperty', + value: { + properties: expressions, + type: 'ObjectExpression', + }, + }], + type: 'ObjectExpression', + }; + } /** - * @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file - * @returns {Promise} The generated config file contents + * @param {import('./types.js').ConfigFile} config - + * The config file object to be transformed into a eslint.config.js file. + * @returns {Promise} The generated config file contents. */ - async generate(config) { - - const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : ''; + static async generate(config) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const existingConfig = existsSync(config.path) + // eslint-disable-next-line security/detect-non-literal-fs-filename + ? await fs.readFile(config.path, 'utf8') + : ''; /** @type {{program: Program}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { program: ast } = parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) }); + const { program: ast } = parse( + existingConfig, + { parser: (await import('recast/parsers/babel.js')) }, + ); - await this.addDefaultExport(ast); + await ConfigsWriter.addDefaultExport(ast); /** * @type {ConfigArrayElement[]} @@ -314,31 +308,100 @@ export default class ConfigsWriter { // @ts-expect-error The array is filtered to remove undefined's const elements = [ ...config.configs.map(c => astUtils.stringToExpression(c)), - ...config.presets.map(p => { + ...config.presets.map((p) => { const e = astUtils.stringToExpression(p); - if (e) return astUtils.toSpreadElement(e); - else undefined; + return e ? astUtils.toSpreadElement(e) : undefined; }), config.rules.length > 0 - ? this.createRulesObject(config.rules) + ? ConfigsWriter.createRulesObject(config.rules) : undefined, - ].filter(e => e); + ].filter(Boolean); - this.addElementsToExport(ast, elements); - this.addPackageImports(ast, config.imports); + ConfigsWriter.addElementsToExport(ast, elements); + ConfigsWriter.addPackageImports(ast, config.imports); - const finalCode = prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code; + const finalCode = prettyPrint( + ast, + { parser: (await import('recast/parsers/babel.js')) }, + ).code; return finalCode; + } + + /** + * @param {import('./types.js').Package} pkg - The package to generate the config string from. + * @returns {import('./types.js').ConfigFile} The config file object. + */ + // eslint-disable-next-line max-statements + generateObj(pkg) { + /** @type {import('./types.js').ConfigFile} */ + const configObj = { + configs: [], + imports: new Map(), + path: join(pkg.path, 'eslint.config.js'), + presets: [], + rules: [], + }; + + if (!pkg.root) { + const rootConfig = join( + getPathDepth(pkg.path, this.root), + 'eslint.config.js', + ); + configObj.imports.set(rootConfig.startsWith('.') ? rootConfig : `./${rootConfig}`, 'root'); + configObj.presets.push('root'); + } + + for (const [configName, optionsNames] of notNull(pkg.config)) { + const config = this.configs.find(c => c.name === configName); + // eslint-disable-next-line no-continue + if (!config) continue; + + const options = config.options.filter(o => + optionsNames.includes(o.name), + ); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-continue + if (!options || options.length === 0) continue; + + // eslint-disable-next-line unicorn/no-array-reduce + const imports = options.reduce((acc, opt) => { + const map1 = new Map(Object.entries(acc.packages ?? {})); + const map2 = new Map(Object.entries(opt.packages ?? {})); + acc.packages = Object.fromEntries(mergeImportsMaps(map1, map2)); + return acc; + }); + + configObj.imports = mergeImportsMaps( + configObj.imports, + new Map(Object.entries(imports.packages ?? {})), + ); + + configObj.configs = [...new Set([ + ...configObj.configs, + ...options.flatMap(o => o.configs ?? []), + ])]; + + configObj.rules = [...new Set([ + ...configObj.rules, + ...options.flatMap(o => o.rules ?? []), + ])]; + + configObj.presets = [...new Set([ + ...configObj.presets, + ...options.flatMap(o => o.presets ?? []), + ])]; + } + + return configObj; } /** - * @param {string} path The path to the file to be written - * @param {string} content The content of the file + * @param {string} path - The path to the file to be written. + * @param {string} content - The content of the file. * @returns {Promise} */ - async write(path, content) { - await fs.writeFile(path, content, 'utf-8'); + static async write(path, content) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + await fs.writeFile(path, content, 'utf8'); } - } diff --git a/packages/cli/src/configsProcessor.js b/packages/cli/src/configs-processor.js similarity index 52% rename from packages/cli/src/configsProcessor.js rename to packages/cli/src/configs-processor.js index 6424ed1..d035257 100755 --- a/packages/cli/src/configsProcessor.js +++ b/packages/cli/src/configs-processor.js @@ -1,56 +1,75 @@ -#!node +import process from 'node:process'; import path from 'node:path'; -import glob from 'picomatch'; + import prompts from 'prompts'; +import glob from 'picomatch'; import c from 'picocolors'; + import capitalize from './lib/capitalize.js'; export default class ConfigsProcessor { - /** @type {string} */ - dir = process.cwd(); - /** @type {import('./types.js').Config[]} */ configs; - /** @type {string[] | undefined} */ - #packagesPatterns; + /** @type {string} */ + dir = process.cwd(); /** * @param {{ - * configs: import('./types.js').Config[], - * packages?: string[], - * directory?: string, - * }} options - Cli options + * configs: import('./types.js').Config[], + * packages?: string[], + * directory?: string, + * }} options - Cli options. */ constructor(options) { - this.#packagesPatterns = options.packages; - this.configs = options?.configs; + this.configs = options.configs; this.dir = path.normalize(options.directory ?? this.dir); } /** - * @param {import('./types.js').Package} pkg - Package to detect from - * @param {import('./types.js').Config['options']} options - Options to be passed - * @param {boolean} single - Whether to only detect one option - * @returns {string[]} - The detected options + * @param {import('./types.js').Package} pkg - The package to detect configs. + * @returns {import('./types.js').Package['config']} - Detected configs record. */ - detectOptions(pkg, options, single) { + detectConfig(pkg) { + /** @type {import('./types.js').Package['config']} */ + const pkgConfig = new Map(); + for (const config of this.configs.filter(cfg => !cfg.manual)) { + pkgConfig.set(config.name, ConfigsProcessor.detectOptions( + pkg, + config.options, + config.type !== 'multiple', + )); + } + + return pkgConfig; + } + + /** + * @param {import('./types.js').Package} pkg - Package to detect from. + * @param {import('./types.js').Config['options']} options - Options to be passed. + * @param {boolean} single - Whether to only detect one option. + * @returns {string[]} - The detected options. + */ + static detectOptions(pkg, options, single) { /** @type {string[]} */ const detectedOptions = []; for (const option of options) { - if (option.detect === true) { detectedOptions.push(option.name); + // eslint-disable-next-line no-continue continue; } - else if (!option.detect) continue; + // eslint-disable-next-line no-continue + else if (!option.detect) { continue; } const match = glob(option.detect); - const files = pkg.files.filter(f => match ? match(f) : false); - const directories = pkg.directories.filter(f => match ? match(f) : false); + const files = pkg.files.filter(f => (match ? match(f) : false)); + const directories = pkg.directories.filter(f => + (match ? match(f) : false), + ); if (files.length > 0 || directories.length > 0) { detectedOptions.push(option.name); @@ -62,26 +81,24 @@ export default class ConfigsProcessor { } /** - * @param {import('./types.js').Package[] | import('./types.js').Package} pkg - The packages to questions the configs - * @param {import('./types').Config[]} configs - The configs to be used - * @returns {Promise} - The selected options by the user + * @param {import('./types.js').Package[] | import('./types.js').Package} pkg - + * The packages to questions the configs. + * @param {import('./types.js').Config[]} configs - The configs to be used. + * @returns {Promise} - The selected options by the user. */ + // eslint-disable-next-line max-statements, complexity, max-lines-per-function async questionConfig(pkg, configs) { - const packages = Array.isArray(pkg) ? [...pkg] : [pkg]; const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`); for (const config of configs) { - /** @type {import('prompts').Choice[]} */ - const configChoices = config.options.map(option => {return { title: `${capitalize(option.name)}`, value: option.name };}); + const configChoices = config.options.map(option => ({ title: `${capitalize(option.name)}`, value: option.name })); /** @type {Record} */ + // eslint-disable-next-line no-await-in-loop const selectedOptions = await prompts({ - name: config.name, - type: config.type === 'multiple' ? 'multiselect' : 'select', - message: capitalize(config.name), choices: config.type === 'confirm' ? [ { title: 'Yes', @@ -93,11 +110,19 @@ export default class ConfigsProcessor { }, ] : configChoices, hint: config.description, - instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')), + instructions: instructions + c.dim(c.italic( + // eslint-disable-next-line max-len + '\nSelect none if you don\'t want to use this configuration\n', + )), + message: capitalize(config.name), + name: config.name, + type: config.type === 'multiple' ? 'multiselect' : 'select', }); + // eslint-disable-next-line no-continue, @typescript-eslint/no-unnecessary-condition if (!selectedOptions[config.name]) continue; + // eslint-disable-next-line no-continue if (selectedOptions[config.name].length === 0) continue; if (packages.length <= 1) { @@ -105,64 +130,47 @@ export default class ConfigsProcessor { ...(packages[0].config ?? []), ...Object.entries(selectedOptions), ]); + // eslint-disable-next-line no-continue continue; } - /** @type {{title: string, value: import('./types').Package}[]} */ + /** @type {{title: string, value: import('./types.js').Package}[]} */ const packagesOptions = packages - .map(pkg => { - return !pkg.root - ? { - title: `${pkg.name} ${c.dim(pkg.path.replace(this.dir, '.'))}`, - value: pkg, - } - : { title: 'root', value: pkg }; - }) + .map(p => (p.root + ? { title: 'root', value: p } + : { + title: `${p.name} ${c.dim(p.path.replace(this.dir, '.'))}`, + value: p, + })) .filter(p => p.title !== 'root'); - /** @type {Record<'packages', import('./types').Package[]>} */ + /** @type {Record<'packages', import('./types.js').Package[]>} */ + // eslint-disable-next-line no-await-in-loop const selected = await prompts({ + choices: packagesOptions, + instructions: instructions + c.dim(c.italic( + '\nToggle all to use in the root configuration\n', + )), + message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`, + min: 1, name: 'packages', type: 'autocompleteMultiselect', - message: `What packages would you like to apply ${config.type === 'single' ? 'this choice' : 'these choices'}?`, - choices: packagesOptions, - min: 1, - instructions: instructions + c.dim(c.italic('\nToggle all to use in the root configuration\n')), }); - selected.packages = selected.packages ?? []; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + selected.packages ??= []; - selected.packages.map(pkg => { - pkg.config = new Map([ - ...(pkg.config ?? []), + selected.packages.map((p) => { + p.config = new Map([ + ...(p.config ?? []), ...Object.entries(selectedOptions), - ]); return pkg; + ]); return p; }); - packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg); + packages.map(p => + selected.packages.find(s => s.name === p.name) ?? p, + ); } return packages; - } - - /** - * @param {import('./types').Package} pkg - The package to detect configs - * @returns {import('./types').Package['config']} - Detected configs record - */ - detectConfig(pkg) { - - /** @type {import('./types.js').Package['config']} */ - const pkgConfig = new Map(); - - for (const config of this.configs.filter(c => !c.manual)) { - pkgConfig.set(config.name, this.detectOptions( - pkg, - config.options, - config.type !== 'multiple', - )); - } - - return pkgConfig; - } - } diff --git a/packages/cli/src/lib/astUtils.js b/packages/cli/src/lib/ast-utils.js similarity index 55% rename from packages/cli/src/lib/astUtils.js rename to packages/cli/src/lib/ast-utils.js index be0607c..9e043cc 100644 --- a/packages/cli/src/lib/astUtils.js +++ b/packages/cli/src/lib/ast-utils.js @@ -2,10 +2,10 @@ import { parse, print } from 'recast'; /** * @typedef {( - * import('estree').MemberExpression | - * import('estree').Identifier | - * import('estree').CallExpression | - * import('estree').NewExpression + * import('estree').MemberExpression | + * import('estree').Identifier | + * import('estree').CallExpression | + * import('estree').NewExpression * )} ExpressionOrIdentifier * This type only includes the expressions used in the cli's config type * @typedef {import('estree').VariableDeclaration} VariableDeclaration @@ -18,42 +18,49 @@ import { parse, print } from 'recast'; */ /** - * @param {IdentifierName} identifier Nave of the variable identifier - * @param {VariableKind} [kind] Type of variable declaration - * @param {VariableInit} [init] Initial value of the variable - * @returns {VariableDeclaration} The variable declaration ast node object + * @param {IdentifierName} identifier - Nave of the variable identifier. + * @param {VariableInit} [init] - Initial value of the variable. + * @param {VariableKind} [kind] - Type of variable declaration. + * @returns {VariableDeclaration} The variable declaration ast node object. */ -function createVariable(identifier, kind = 'const', init) { +function createVariable(identifier, init, kind = 'const') { return { - type: 'VariableDeclaration', - kind, declarations: [{ - type: 'VariableDeclarator', - id: { type: 'Identifier', name: identifier }, + id: { name: identifier, type: 'Identifier' }, init, + type: 'VariableDeclarator', }], + kind, + type: 'VariableDeclaration', }; } /** - * @param {string} string The expression in string - * @returns {ExpressionOrIdentifier | undefined} The expression or identifier node of that string (undefined if string is not a expression) + * @param {string} string - The code/expression in string. + * @returns {ExpressionOrIdentifier | undefined} - + * The expression or identifier node of that string (undefined if string is not a expression). */ function stringToExpression(string) { /** @type {ExpressionOrIdentifier} */ + // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const e = parse(string).program.body[0].expression; - if (['MemberExpression', 'Identifier', 'CallExpression', 'NewExpression'].includes(e.type)) return e; - else return undefined; + if ([ + 'CallExpression', + 'Identifier', + 'MemberExpression', + 'NewExpression', + ].includes(e.type)) return e; + return undefined; } /** - * @param {ArrayExpression} array The array node to search trough - * @param {ExpressionOrIdentifier | SpreadElement} element The element to be search - * @returns {ExpressionOrIdentifier | undefined} The element of the array founded, undefined if it isn't found + * @param {ArrayExpression} array - The array node to search trough. + * @param {ExpressionOrIdentifier | SpreadElement} element - The element to be search. + * @returns {ExpressionOrIdentifier | undefined} + * The element of the array founded, undefined if it isn't found. */ function findInArray(array, element) { - /** @type {ExpressionOrIdentifier[]} */ // @ts-expect-error The array should have just tge type above element = element.type === 'SpreadElement' ? element.argument : element; @@ -61,7 +68,7 @@ function findInArray(array, element) { /** @type {ExpressionOrIdentifier[]} */ // @ts-expect-error The array is filtered to have the type above const filteredElements = array.elements - .map(n => { + .map((n) => { if (n?.type === 'SpreadElement') return n.argument; return n; }).filter(n => n && n.type === element.type); @@ -69,56 +76,88 @@ function findInArray(array, element) { const toStringElements = filteredElements.map(n => print(n).code); const toStringElement = print(element).code; - const idx = toStringElements.findIndex(e => e === toStringElement); + const idx = toStringElements.indexOf(toStringElement); + // eslint-disable-next-line security/detect-object-injection return filteredElements[idx]; } /** - * @param {ExpressionOrIdentifier} expression The expression to be spread - * @returns {SpreadElement} The spread element node + * @param {ExpressionOrIdentifier} expression - The expression to be spread. + * @returns {SpreadElement} The spread element node. */ function toSpreadElement(expression) { return { - type: 'SpreadElement', argument: expression, + type: 'SpreadElement', }; } +// eslint-disable-next-line no-secrets/no-secrets /** * @typedef {{ - * body: import('estree').ImportDeclaration - * addSpecifier: (specifier: string, alias?: string) => ThisType - * convertDefaultSpecifier: () => ThisType + * body: import('estree').ImportDeclaration + * addSpecifier: (specifier: string, alias?: string) => ThisType + * convertDefaultSpecifier: () => ThisType * }} ImportDeclarationHelper - * @param {string} source The package name or source path to be imported - * @param {string} [defaultImported] The default specifier imported - * @param {import('estree').ImportDeclaration} [body] The body of the import declaration to start with - * @returns {ImportDeclarationHelper} A helper object for manipulating the import declaration + * @param {string} source - The package name or source path to be imported. + * @param {string} [defaultImported] - The default specifier imported. + * @param {import('estree').ImportDeclaration} [body] - + * The body of the import declaration to start with. + * @returns {ImportDeclarationHelper} A helper object for manipulating the import declaration. */ function createImportDeclaration(source, defaultImported, body) { const helper = { + /** + * @param {string} specifier - The value to be imported from the package. + * @param {string} [alias] - The local alias of the value. + * @returns {ThisType} This helper with the added specifiers. + */ + addSpecifier(specifier, alias) { + this.convertDefaultSpecifier(); + if (this.body.specifiers.some(s => + s.local.name === alias || s.local.name === specifier, + )) + return this; + + this.body.specifiers.push({ + imported: { + name: specifier, + type: 'Identifier', + }, + local: { + name: alias ?? specifier, + type: 'Identifier', + }, + type: 'ImportSpecifier', + }); + + return this; + }, /** @type {import('estree').ImportDeclaration} */ body: body ?? { - type: 'ImportDeclaration', - specifiers: defaultImported ? [{ - type: 'ImportDefaultSpecifier', - local: { type: 'Identifier', name: defaultImported }, - }] : [], source: { type: 'Literal', value: source, }, + specifiers: defaultImported ? [{ + local: { name: defaultImported, type: 'Identifier' }, + type: 'ImportDefaultSpecifier', + }] : [], + type: 'ImportDeclaration', }, /** * Converts a default specifier to a specifier with a alias. + * @returns {ThisType} - + * This helper with the converted default specifier. * @example * import eslit from 'eslit'; * // Is converted to * import { default as eslit } from 'eslit'; - * @returns {ThisType} This helper with the converted default specifier */ convertDefaultSpecifier() { - const specifier = this.body.specifiers.find(s => s.type === 'ImportDefaultSpecifier'); + const specifier = this.body.specifiers.find(s => + s.type === 'ImportDefaultSpecifier', + ); if (!specifier) return this; @@ -128,44 +167,24 @@ function createImportDeclaration(source, defaultImported, body) { ); return this.addSpecifier('default', specifier.local.name); }, - /** - * @param {string} specifier The value to be imported from the package - * @param {string} [alias] The local alias of the value - * @returns {ThisType} This helper with the added specifiers - */ - addSpecifier(specifier, alias) { - this.convertDefaultSpecifier(); - if (this.body.specifiers.find(s => s.local.name === alias || s.local.name === specifier)) - return this; - - this.body.specifiers.push({ - type: 'ImportSpecifier', - imported: { - type: 'Identifier', - name: specifier, - }, - local: { - type: 'Identifier', - name: alias ?? specifier, - }, - }); - - return this; - }, }; - if (defaultImported && body && !body.specifiers.find(s => s.type === 'ImportDefaultSpecifier' && s.local.name === defaultImported)) { + if (defaultImported && + body && + !body.specifiers.some(s => + s.type === 'ImportDefaultSpecifier' && + s.local.name === defaultImported, + )) helper.addSpecifier('default', defaultImported); - } - return helper; + return helper; } const astUtils = { + createImportDeclaration, createVariable, + findInArray, stringToExpression, toSpreadElement, - findInArray, - createImportDeclaration, }; export default astUtils; diff --git a/packages/cli/src/lib/capitalize.js b/packages/cli/src/lib/capitalize.js index 2f0dd9d..4c8a1f7 100644 --- a/packages/cli/src/lib/capitalize.js +++ b/packages/cli/src/lib/capitalize.js @@ -1,7 +1,7 @@ /** - * @param {string} str - The string to capitalize - * @returns {string} The capitalized string + * @param {string} str - The string to capitalize. + * @returns {string} The capitalized string. */ export default function capitalize(str) { return str[0].toUpperCase() + str.slice(1); diff --git a/packages/cli/src/lib/count.js b/packages/cli/src/lib/count.js index c1f6384..7fde05e 100644 --- a/packages/cli/src/lib/count.js +++ b/packages/cli/src/lib/count.js @@ -1,11 +1,12 @@ /** - * @param {import('../types').Package[]} packages - Package list - * @returns {number} Number of packages' configs + * @param {import('../types').Package[]} packages - Package list. + * @returns {number} Number of packages' configs. */ function packagesWithConfigs(packages) { return packages.map(p => - [...p.config?.values() ?? []].filter((options) => options.length > 0).length, + [...p.config?.values() ?? []] + .filter(options => options.length > 0).length, ).reduce((partial, sum) => partial + sum, 0); } diff --git a/packages/cli/src/lib/notNull.js b/packages/cli/src/lib/not-null.js similarity index 61% rename from packages/cli/src/lib/notNull.js rename to packages/cli/src/lib/not-null.js index b58aa98..bea0068 100644 --- a/packages/cli/src/lib/notNull.js +++ b/packages/cli/src/lib/not-null.js @@ -1,15 +1,17 @@ +// eslint-disable-next-line no-secrets/no-secrets /** * JSDoc types lack a non-null assertion. * @template T - * @param {T} value The value which to assert against null or undefined - * @returns {NonNullable} The said value - * @throws {TypeError} If the value is unexpectedly null or undefined - * @author Jimmy Wärting - https://github.com/jimmywarting + * @param {T} value - The value which to assert against null or undefined. + * @returns {NonNullable} The said value. + * @throws {TypeError} If the value is unexpectedly null or undefined. * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031 * @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966 + * @author Jimmy Wärting - https://github.com/jimmywarting */ export default function notNull(value) { // Use `==` to check for both null and undefined - if (value == null) throw new Error('did not expect value to be null or undefined'); + if (value === null || value === undefined) + throw new Error('did not expect value to be null or undefined'); return value; } diff --git a/packages/cli/src/packageInstaller.js b/packages/cli/src/package-installer.js similarity index 57% rename from packages/cli/src/packageInstaller.js rename to packages/cli/src/package-installer.js index a1760a5..88469f4 100644 --- a/packages/cli/src/packageInstaller.js +++ b/packages/cli/src/package-installer.js @@ -1,26 +1,29 @@ +/* eslint-disable no-console */ +/* eslint-disable max-classes-per-file */ +import { readFile, writeFile } from 'node:fs/promises'; import { existsSync, readFileSync } from 'node:fs'; -import { join } from 'node:path'; import { exec } from 'node:child_process'; +import { join } from 'node:path'; + +import { parse, prettyPrint } from 'recast'; import { createSpinner } from 'nanospinner'; import c from 'picocolors'; -import { parse, prettyPrint } from 'recast'; -import { readFile, writeFile } from 'node:fs/promises'; /** * @type {import('./types').PackageManagerHandler} */ class CommandHandler { + /** @type {((path: string, packages: string[]) => string | Promise) | undefined} */ + checker; /** @type {string} */ command; - /** @type {((path: string, packages: string[]) => string | Promise) | undefined} */ - checker; - /** - * @param {string} command What command to use to install - * @param {(path: string, packages: string[]) => string | Promise} [checker] Checks if a argument should be passed + * @param {string} command - What command to use to install. + * @param {(path: string, packages: string[]) => string | Promise} [checker] - + * Checks if a argument should be passed. */ constructor(command, checker) { this.command = command; @@ -28,20 +31,20 @@ class CommandHandler { } /** - * @param {string} path The path to run the command - * @param {string[]} packages The packages to be added on the command + * @param {string} path - The path to run the command. + * @param {string[]} packages - The packages to be added on the command. * @returns {Promise} */ async install(path, packages) { - if (this.checker) this.command += await this.checker(path, packages); return new Promise((res) => { const spinner = createSpinner(`Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}`).start(); try { + // eslint-disable-next-line security/detect-child-process const child = exec(`${this.command} ${packages.join(' ')}`, { cwd: path }); - child.stdout?.on('data', (chunk) => spinner.update({ + child.stdout?.on('data', chunk => spinner.update({ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument text: `Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}\n ${c.dim(chunk)}`, })); @@ -52,7 +55,8 @@ class CommandHandler { }); } catch (error) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-confusing-void-expression res(console.error(`Error while installing the packages with ${this.command} ${c.dim(packages.join(' '))} on ${path}: ${error}`)); } }); @@ -62,53 +66,49 @@ class CommandHandler { /** * @type {import('./types').PackageManagerHandler} */ -class DenoHandler { - +const DenoHandler = { /** - * @param {string} path The path to run the command - * @param {string[]} packages The packages to be added on the command + * @param {string} path - The path to run the command. + * @param {string[]} packages - The packages to be added on the command. * @returns {Promise} */ async install(path, packages) { const configPath = join(path, 'eslint.config.js'); + // eslint-disable-next-line security/detect-non-literal-fs-filename if (!existsSync(configPath)) return; + // eslint-disable-next-line security/detect-non-literal-fs-filename const configFile = await readFile(configPath, 'utf8'); - /** @type {{program: import('estree').Program}}*/ + /** @type {{program: import('estree').Program}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { program: ast } = parse(configFile, { parser: (await import('recast/parsers/babel.js')) }); + const { program: ast } = parse( + configFile, + { parser: (await import('recast/parsers/babel.js')) }, + ); ast.body.map((node) => { if (node.type !== 'ImportDeclaration') return node; - if (packages.includes(node.source.value?.toString() ?? '')) { + if (packages.includes(node.source.value?.toString() ?? '')) node.source.value = `npm:${node.source.value}`; - } + return node; }); - await writeFile(configPath, prettyPrint(ast).code, 'utf-8'); + // eslint-disable-next-line security/detect-non-literal-fs-filename + await writeFile(configPath, prettyPrint(ast).code, 'utf8'); console.log(c.green('Added npm: specifier to dependencies')); - - } - -} + }, +}; export default class PackageInstaller { - - /** - * @typedef {Map} PackagesMap - * @type {PackagesMap} - */ - packagesMap; - /** * @typedef {{ - * name: import('./types').PackageManagerName - * description: string - * handler: import('./types').PackageManagerHandler + * name: import('./types').PackageManagerName + * description: string + * handler: import('./types').PackageManagerHandler * }} PackageManager * @type {PackageManager} */ @@ -118,40 +118,51 @@ export default class PackageInstaller { * @type {Record} */ packageManagers = { - deno: { - name: 'deno', - description: 'Adds npm: specifiers to the eslint.config.js file', - handler: new DenoHandler(), - }, bun: { - name: 'bun', description: 'Uses bun install', handler: new CommandHandler('bun install'), + name: 'bun', }, - pnpm: { - name: 'pnpm', - description: 'Uses pnpm install', - handler: new CommandHandler('pnpm install --save-dev', (path) => { - if (existsSync(join(path, 'pnpm-workspace.yaml')) && existsSync(join(path, 'package.json'))) - return ' -w'; - else return ''; - }), - }, - yarn: { - name: 'yarn', - description: 'Uses yarn add', - handler: new CommandHandler('yarn add --dev'), + deno: { + description: 'Adds npm: specifiers to the eslint.config.js file', + handler: DenoHandler, + name: 'deno', }, npm: { - name: 'npm', description: 'Uses npm install', handler: new CommandHandler('npm install --save-dev'), + name: 'npm', + }, + pnpm: { + description: 'Uses pnpm install', + handler: new CommandHandler('pnpm install --save-dev', (path) => { + if ( + // eslint-disable-next-line security/detect-non-literal-fs-filename + existsSync(join(path, 'pnpm-workspace.yaml')) && + // eslint-disable-next-line security/detect-non-literal-fs-filename + existsSync(join(path, 'package.json')) + ) + return ' -w'; + return ''; + }), + name: 'pnpm', + }, + yarn: { + description: 'Uses yarn add', + handler: new CommandHandler('yarn add --dev'), + name: 'yarn', }, }; /** - * @param {PackagesMap} packagesMap The map of directories and packages to be installed - * @param {string} root Root directory path + * @typedef {Map} PackagesMap + * @type {PackagesMap} + */ + packagesMap; + + /** + * @param {PackagesMap} packagesMap - The map of directories and packages to be installed. + * @param {string} root - Root directory path. */ constructor(packagesMap, root) { this.packagesMap = packagesMap; @@ -159,51 +170,68 @@ export default class PackageInstaller { } /** - * @param {string} root Root directory path - * @returns {PackageManager} The package manager detected; + * @param {string} root - Root directory path. + * @returns {PackageManager} The package manager detected;. * @private */ + // eslint-disable-next-line complexity detectPackageManager(root) { /** @type {(...path: string[]) => boolean} */ - const exists = (...path) => existsSync(join(root, ...path)); + function exists(...path) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + return existsSync(join(root, ...path)); + } switch (true) { case exists('deno.json'): - case exists('deno.jsonc'): + case exists('deno.jsonc'): { return this.packageManagers.deno; + } - case exists('bun.lockb'): + case exists('bun.lockb'): { return this.packageManagers.bun; + } - case exists('pnpm-lock.yaml'): + case exists('pnpm-lock.yaml'): { return this.packageManagers.pnpm; + } - case exists('yarn.lock'): + case exists('yarn.lock'): { return this.packageManagers.yarn; + } - case exists('package-lock.json'): + case exists('package-lock.json'): { return this.packageManagers.npm; + } - case exists('package.json'): + case exists('package.json'): { /** @type {{packageManager?: string}} */ + // eslint-disable-next-line max-len // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-case-declarations - const { packageManager } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); + const { packageManager } = JSON.parse( + // eslint-disable-next-line security/detect-non-literal-fs-filename + readFileSync(join(root, 'package.json'), 'utf8'), + ); if (!packageManager) return this.packageManagers.npm; - if (packageManager.includes('pnpm')) return this.packageManagers.pnpm; - if (packageManager.includes('yarn')) return this.packageManagers.yarn; - if (packageManager.includes('npm')) return this.packageManagers.npm; + if (packageManager.includes('pnpm')) + return this.packageManagers.pnpm; + if (packageManager.includes('yarn')) + return this.packageManagers.yarn; + if (packageManager.includes('npm')) + return this.packageManagers.npm; - else return this.packageManagers.npm; + return this.packageManagers.npm; + } - default: return this.packageManagers.npm; + default: { return this.packageManagers.npm; + } } } async install() { - for (const [path, packages] of this.packagesMap) { + for (const [path, packages] of this.packagesMap) + // eslint-disable-next-line no-await-in-loop await this.packageManager.handler.install(path, packages); - } } - } diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 36a369c..725bb78 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -1,64 +1,72 @@ import type { OptionValues } from 'commander'; -type PackageManagerName = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'; - -type CliArgs = { - packages?: string[] - mergeToRoot?: boolean - installPkgs?: boolean | PackageManagerName - dir: string - configs: Config[] -} & OptionValues; +type PackageManagerName = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn'; interface PackageManagerHandler { - install(path: string, packages: string[]): Promise | void + install(path: string, packages: string[]): Promise | void, } type Config = { - name: string - type: 'single' | 'multiple' - manual?: boolean - description?: string - options: { - name: string - packages?: Record - configs?: string[] - rules?: string[] - presets?: string[] - detect?: string[] | true - }[] -} | { - name: string - type: 'confirm' - manual: true - description?: string + description?: string, + manual: true, + name: string, options: [{ - name: 'yes' - packages?: Record - configs?: string[] - rules?: string[] - presets?: string[] - detect?: undefined - }] + configs?: string[], + detect?: undefined, + name: 'yes', + packages?: { [key: string]: ([string, string] | string)[] | string, }, + presets?: string[], + rules?: string[], + }], + type: 'confirm', +} | { + description?: string, + manual?: boolean, + name: string, + options: { + configs?: string[], + detect?: string[] | true, + name: string, + packages?: { [key: string]: ([string, string] | string)[] | string, }, + presets?: string[], + rules?: string[], + }[], + type: 'multiple' | 'single', }; -interface Package { - root?: boolean - name: string - path: string - files: string[] - directories: string[] - config?: Map - configFile?: ConfigFile -} +type CliArgs = { + configs: Config[], + dir: string, + installPkgs?: PackageManagerName | boolean, + mergeToRoot?: boolean, + packages?: string[], +} & OptionValues; interface ConfigFile { - path: string - imports: Map - configs: string[] - presets: string[] - rules: string[] - content?: string + configs: string[], + content?: string, + imports: Map, + path: string, + presets: string[], + rules: string[], } -export type { PackageManagerName, PackageManagerHandler, CliArgs, Config, Package, ConfigFile }; +interface Package { + config?: Map, + configFile?: ConfigFile, + directories: string[], + files: string[], + name: string, + path: string, + root?: boolean, +} + + +export type { + CliArgs, + Config, + ConfigFile, + Package, + PackageManagerHandler, + PackageManagerName, +}; diff --git a/packages/cli/src/workspace.js b/packages/cli/src/workspace.js index 0c88d1c..607d672 100644 --- a/packages/cli/src/workspace.js +++ b/packages/cli/src/workspace.js @@ -1,35 +1,41 @@ -import fs from 'node:fs/promises'; -import { existsSync } from 'node:fs'; -import YAML from 'yaml'; +/* eslint-disable security/detect-non-literal-fs-filename */ import path, { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import fs from 'node:fs/promises'; + import picomatch from 'picomatch'; +import YAML from 'yaml'; /** * @template T - * @param {Promise} promise - The async function to try running - * @returns {Promise} - Returns the result of the async function, or null if it errors + * @param {Promise} promise - The async function to try running. + * @returns {Promise} - Returns the result of the async function, or null if it errors. */ async function tryRun(promise) { try { return await promise; } - catch (err) { + catch { return null; } } /** - * @param {string} directory - The directory to find .gitignore and .eslintignore - * @returns {Promise} - List of ignore glob patterns + * @param {string} directory - The directory to find .gitignore and .eslintignore. + * @returns {Promise} - List of ignore glob patterns. */ async function getIgnoredFiles(directory) { - const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '') + const gitIgnore = ( + await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? + '') .split('\n') .filter(p => p && !p.startsWith('#')) .map(p => join(directory, '**', p)); - const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '') + const eslintIgnore = ( + await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? + '') .split('\n') .filter(p => p && !p.startsWith('#')) .map(p => join(directory, '**', p)); @@ -46,19 +52,19 @@ async function getPackageName(directory) { const file = await fs.readFile(join(directory, 'package.json'), 'utf8'); /** @type {{name?: string}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const obj = JSON.parse(file); + const object = JSON.parse(file); - if (obj.name) return obj.name; + if (object.name) return object.name; } return path.normalize(directory).split('/').at(-1) ?? directory; } export default class Workspace { - /** - * @param {string} directory - The directory to get the workspace from - * @param {string[] | false} [packagePatterns] - * List of package patterns (`false` to explicitly tell that this workspace is not a monorepo) + * @param {string} directory - + * The directory to get the workspace from. + * @param {string[] | false} [packagePatterns] - + * List of package patterns (`false` to explicitly tell that this workspace is not a monorepo). */ constructor(directory, packagePatterns) { this.dir = directory; @@ -66,125 +72,93 @@ export default class Workspace { } /** - * @param {string} [directory] - The directory to work on - * @param {string[]} [ignores] - Glob patterns to ignore - * @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory - */ - async getPaths(directory = this.dir, ignores = []) { - - ignores.push( - ...[ - '.git', - '.dist', - '.DS_Store', - 'node_modules', - ].map((f) => join(directory, f)), - ...await getIgnoredFiles(directory), - ); - - const paths = (await fs.readdir(directory)) - .map((f) => path.normalize(join(directory, f))) - .filter((p) => !picomatch.isMatch(p, ignores)); - - /** @type {string[]} */ - const files = []; - /** @type {string[]} */ - const directories = []; - - for (const path of paths) { - if ((await fs.lstat(path)).isDirectory()) { - const subPaths = await this.getPaths(path, ignores); - directories.push(path, ...subPaths.directories); - files.push(...subPaths.files); - } - else { - files.push(path); - } - } - - return { - files: files.map(p => path.normalize(p.replace(this.dir, './'))), - directories: directories.map(p => path.normalize(p.replace(this.dir, './'))), - }; - } - - /** - * @returns {Promise} - List of packages on a directory; + * @returns {Promise} - List of packages on a directory;. */ async getPackagePatterns() { - /** @type {string[]} */ - let packagePatterns = []; + const packagePatterns = []; const pnpmWorkspace = existsSync(join(this.dir, 'pnpm-workspace.yaml')) ? 'pnpm-workspace.yaml' - : existsSync(join(this.dir, 'pnpm-workspace.yml')) - ? 'pnpm-workspace.yml' - : null; + : (existsSync(join(this.dir, 'pnpm-workspace.yml')) + ? 'pnpm-workspace.yml' + : null); if (pnpmWorkspace) { - const pnpmWorkspaceYaml = await fs.readFile(join(this.dir, pnpmWorkspace), 'utf8'); + const pnpmWorkspaceYaml = await fs.readFile( + join(this.dir, pnpmWorkspace), + 'utf8', + ); /** @type {{packages?: string[]}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const pnpmWorkspaceObj = YAML.parse(pnpmWorkspaceYaml); + const pnpmWorkspaceObject = YAML.parse(pnpmWorkspaceYaml); - packagePatterns.push(...(pnpmWorkspaceObj?.packages ?? [])); + packagePatterns.push(...pnpmWorkspaceObject.packages ?? []); } else if (existsSync(join(this.dir, 'package.json'))) { - const packageJson = await fs.readFile(join(this.dir, 'package.json'), 'utf8'); + const packageJson = await fs.readFile( + join(this.dir, 'package.json'), + 'utf8', + ); /** @type {{workspaces?: string[]}} */ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const packageJsonObj = JSON.parse(packageJson); + const packageJsonObject = JSON.parse(packageJson); - packagePatterns.push(...(packageJsonObj?.workspaces ?? [])); + packagePatterns.push(...packageJsonObject.workspaces ?? []); } - return packagePatterns.map(p => { + return packagePatterns.map((p) => { p = path.normalize(p); p = p.startsWith('/') ? p.replace('/', '') : p; - p = p.endsWith('/') ? p.slice(0, p.length - 1) : p; + p = p.endsWith('/') ? p.slice(0, -1) : p; return p; }); } /** - * @returns {Promise} - The list of packages that exist in the workspace + * @returns {Promise} - + * The list of packages that exist in the workspace. */ async getPackages() { - const paths = await this.getPaths(); /** @type {import('./types').Package} */ const rootPackage = { - root: true, + directories: paths.directories, + files: paths.files, name: await getPackageName(this.dir), path: this.dir, - files: paths.files, - directories: paths.directories, + root: true, }; if (this.packagePatterns === false) return [rootPackage]; - const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns(); - const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns)); + const packagePatterns = + this.packagePatterns ?? + await this.getPackagePatterns(); + + const packagePaths = paths.directories.filter(d => + picomatch.isMatch(d, packagePatterns), + ); /** @type {import('./types').Package[]} */ const packages = []; for (const packagePath of packagePaths) { packages.push({ - root: false, - path: join(this.dir, packagePath), - name: await getPackageName(join(this.dir, packagePath)), - files: paths.files - .filter(f => picomatch.isMatch(f, `${packagePath}/**/*`)) - .map(f => f.replace(`${packagePath}/`, '')), directories: paths.directories .filter(d => picomatch.isMatch(d, `${packagePath}/**/*`)) .map(d => d.replace(`${packagePath}/`, '')), + files: paths.files + .filter(f => picomatch.isMatch(f, `${packagePath}/**/*`)) + .map(f => f.replace(`${packagePath}/`, '')), + // eslint-disable-next-line no-await-in-loop + name: await getPackageName(join(this.dir, packagePath)), + path: join(this.dir, packagePath), + root: false, }); rootPackage.files = rootPackage.files @@ -195,51 +169,101 @@ export default class Workspace { } return [rootPackage, ...packages]; - } /** - * @param {import('./types').Package[]} packages - Packages to be merged into root - * @returns {[import('./types').Package]} A array containing only the root package + * @param {string} [directory] - The directory to work on. + * @param {string[]} [ignores] - Glob patterns to ignore. + * @returns {Promise<{files: string[], directories: string[]}>} - + * List of all files in the directory. */ - mergePackages(packages) { + async getPaths(directory = this.dir, ignores = []) { + ignores.push( + ...[ + '.git', + '.dist', + '.DS_Store', + 'node_modules', + ].map(f => join(directory, f)), + ...await getIgnoredFiles(directory), + ); + const pathsUnfiltered = await fs.readdir(directory); + const paths = pathsUnfiltered + .map(f => path.normalize(join(directory, f))) + .filter(p => !picomatch.isMatch(p, ignores)); + + /** @type {string[]} */ + const files = []; + /** @type {string[]} */ + const directories = []; + + for (const p of paths) { + // eslint-disable-next-line no-await-in-loop, unicorn/no-await-expression-member + if ((await fs.lstat(p)).isDirectory()) { + // eslint-disable-next-line no-await-in-loop + const subPaths = await this.getPaths(p, ignores); + directories.push(p, ...subPaths.directories); + files.push(...subPaths.files); + } + else { + files.push(p); + } + } + + return { + directories: directories.map(p => + path.normalize(p.replace(this.dir, './')), + ), + files: files.map(p => path.normalize(p.replace(this.dir, './'))), + }; + } + + /** + * @param {import('./types').Package[]} packages - Packages to be merged into root. + * @returns {[import('./types').Package]} A array containing only the root package. + */ + static mergePackages(packages) { const rootPackage = packages.find(p => p.root) ?? packages[0]; - const merged = packages.reduce((accumulated, pkg) => { - + // TODO [>=1.0.0]: Refactor this to remove the use of Array#reduce() + // eslint-disable-next-line unicorn/no-array-reduce + const merged = packages.reduce((accumulated, package_) => { const files = [...new Set([ ...accumulated.files, - ...pkg.files.map(f => join(pkg.path, f)), + ...package_.files.map(f => join(package_.path, f)), ] .map(p => p.replace(`${rootPackage.path}/`, '')), )]; const directories = [...new Set([ ...accumulated.directories, - ...pkg.directories.map(d => join(pkg.path, d)), + ...package_.directories.map(d => join(package_.path, d)), ] .map(p => p.replace(`${rootPackage.path}/`, ''))), ]; const mergedConfig = new Map(); - for (const [config, options] of pkg.config ?? []) { - const accumulatedOptions = accumulated.config?.get(config) ?? []; - mergedConfig.set(config, [...new Set([...options, ...accumulatedOptions])]); + for (const [config, options] of package_.config ?? []) { + const accumulatedOptions = + accumulated.config?.get(config) ?? + []; + mergedConfig.set( + config, + [...new Set([...options, ...accumulatedOptions])], + ); } return { - root: true, - path: rootPackage.path, - name: rootPackage.name, - files, - directories, config: mergedConfig, + directories, + files, + name: rootPackage.name, + path: rootPackage.path, + root: true, }; }, rootPackage); return [merged]; - } - } diff --git a/packages/create-eslegant/bin.js b/packages/create-eslegant/bin.js index b79fa75..327e087 100644 --- a/packages/create-eslegant/bin.js +++ b/packages/create-eslegant/bin.js @@ -1,4 +1,11 @@ +#!/usr/bin/env node +import process from 'node:process'; + import Cli from '@eslegant/cli'; -const cli = new Cli({ configs: (await import('./configs.js')).default, dir: process.cwd() }); +const cli = new Cli({ + // eslint-disable-next-line unicorn/no-await-expression-member + configs: (await import('./configs.js')).default, + dir: process.cwd(), +}); await cli.run(); diff --git a/packages/create-eslegant/configs.js b/packages/create-eslegant/configs.js index f37cf57..7e7daff 100644 --- a/packages/create-eslegant/configs.js +++ b/packages/create-eslegant/configs.js @@ -2,33 +2,36 @@ /** @type {import('@eslegant/cli').Config[]} */ const cliConfig = [ { - name: 'framework', - type: 'multiple', description: 'The UI frameworks being used in the project', + name: 'framework', options: [ { - name: 'svelte', - packages: { 'svelte': 'svelte' }, configs: ['svelte.recommended'], detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'], + name: 'svelte', + packages: { svelte: 'svelte' }, }, { - name: 'vue', - packages: { 'vue': ['vue', ['hello', 'world']], 'svelte': ['hello'] }, configs: ['vue.recommended'], detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'], + name: 'vue', + packages: { + svelte: ['hello'], + vue: ['vue', ['hello', 'world']], + }, }, ], + type: 'multiple', }, { - name: 'strict', - type: 'confirm', manual: true, + name: 'strict', options: [{ - name: 'yes', - packages: { 'eslint': 'config', 'svelte': ['test1'] }, configs: ['config.strict'], + name: 'yes', + packages: { eslint: 'config', svelte: ['test1'] }, }], + type: 'confirm', }, ]; export default cliConfig; diff --git a/packages/create-eslegant/package.json b/packages/create-eslegant/package.json index 0a105b2..a36d0f2 100644 --- a/packages/create-eslegant/package.json +++ b/packages/create-eslegant/package.json @@ -9,8 +9,8 @@ "url": "https://guz.one" }, "files": [ - "./bin.js", - "./configs.js" + "bin.js", + "configs.js" ], "dependencies": { "@eslegant/cli": "workspace:*" diff --git a/packages/eslegant/bin.js b/packages/eslegant/bin.js index b79fa75..327e087 100755 --- a/packages/eslegant/bin.js +++ b/packages/eslegant/bin.js @@ -1,4 +1,11 @@ +#!/usr/bin/env node +import process from 'node:process'; + import Cli from '@eslegant/cli'; -const cli = new Cli({ configs: (await import('./configs.js')).default, dir: process.cwd() }); +const cli = new Cli({ + // eslint-disable-next-line unicorn/no-await-expression-member + configs: (await import('./configs.js')).default, + dir: process.cwd(), +}); await cli.run(); diff --git a/packages/eslegant/configs.js b/packages/eslegant/configs.js index f37cf57..7e7daff 100644 --- a/packages/eslegant/configs.js +++ b/packages/eslegant/configs.js @@ -2,33 +2,36 @@ /** @type {import('@eslegant/cli').Config[]} */ const cliConfig = [ { - name: 'framework', - type: 'multiple', description: 'The UI frameworks being used in the project', + name: 'framework', options: [ { - name: 'svelte', - packages: { 'svelte': 'svelte' }, configs: ['svelte.recommended'], detect: ['**/*.svelte', 'svelte.config.{js,ts,cjs,cts}'], + name: 'svelte', + packages: { svelte: 'svelte' }, }, { - name: 'vue', - packages: { 'vue': ['vue', ['hello', 'world']], 'svelte': ['hello'] }, configs: ['vue.recommended'], detect: ['nuxt.config.{js,ts,cjs,cts}', '**/*.vue'], + name: 'vue', + packages: { + svelte: ['hello'], + vue: ['vue', ['hello', 'world']], + }, }, ], + type: 'multiple', }, { - name: 'strict', - type: 'confirm', manual: true, + name: 'strict', options: [{ - name: 'yes', - packages: { 'eslint': 'config', 'svelte': ['test1'] }, configs: ['config.strict'], + name: 'yes', + packages: { eslint: 'config', svelte: ['test1'] }, }], + type: 'confirm', }, ]; export default cliConfig; diff --git a/packages/eslegant/package.json b/packages/eslegant/package.json index 346743f..720d729 100644 --- a/packages/eslegant/package.json +++ b/packages/eslegant/package.json @@ -9,8 +9,8 @@ "url": "https://guz.one" }, "files": [ - "./bin.js", - "./configs.js" + "bin.js", + "configs.js" ], "dependencies": { "@eslegant/cli": "workspace:*"