Merge pull request #10 from LoredDev/2-eslit-cli-project-setup

feat: Command line interface for setting up eslint/eslit configs
This commit is contained in:
Guz
2023-08-23 11:06:16 -03:00
committed by GitHub
23 changed files with 1760 additions and 53 deletions

View File

@@ -0,0 +1,5 @@
---
"@eslit/cli": minor
---
Now the cli can automatically detect the workspace structure on monorepos and single repositories

View File

@@ -0,0 +1,16 @@
{
"name": "@eslit-fixtures/library",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"scripts": {
"test": "pnpm cli"
},
"dependencies": {
"@eslit/cli": "workspace:*"
},
"keywords": [],
"author": "",
"license": "ISC"
}

36
fixtures/monorepo/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
# vercel
.vercel

View File

@@ -0,0 +1,15 @@
{
"private": true,
"scripts": {
"test:cli": "pnpm cli"
},
"devDependencies": {
"@eslit/cli": "workspace:*"
},
"packageManager": "pnpm@8.6.10",
"name": "monorepo",
"workspaces": [
"apps/*",
"packages/*"
]
}

View File

@@ -7,9 +7,11 @@
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:lint": "eslint ."
"test:lint": "eslint .",
"test:cli": "pnpm cli"
},
"devDependencies": {
"@eslit/cli": "workspace:*",
"@fontsource/fira-mono": "^4.5.10",
"@neoconfetti/svelte": "^1.0.0",
"@sveltejs/adapter-auto": "^2.0.0",

View File

@@ -14,6 +14,7 @@
"@eslit/config": "workspace:*"
},
"devDependencies": {
"@eslit/cli": "workspace:*",
"@changesets/cli": "^2.26.2",
"@commitlint/config-conventional": "^17.6.6",
"@commitlint/types": "^17.4.4",

View File

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

46
packages/cli/package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "@eslit/cli",
"version": "0.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint ."
},
"keywords": [],
"author": {
"email": "contact.guz013@gmail.com",
"name": "Gustavo \"Guz\" L. de Mello",
"url": "https://guz.one"
},
"module": "./src/index.js",
"source": "./src/index.js",
"files": [
"src",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLit",
"type": "module",
"repository": {
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLit"
},
"bin": "./src/index.js",
"license": "MIT",
"dependencies": {
"cardinal": "^2.1.1",
"commander": "^11.0.0",
"nanospinner": "^1.1.0",
"picocolors": "^1.0.0",
"picomatch": "^2.3.1",
"prompts": "^2.4.2",
"recast": "^0.23.3",
"sisteransi": "^1.0.5",
"yaml": "^2.3.1"
},
"devDependencies": {
"@types/estree": "^1.0.1",
"@types/node": "^20.4.2",
"@types/prompts": "^2.4.4"
}
}

155
packages/cli/src/cli.js Normal file
View File

@@ -0,0 +1,155 @@
import { Command } from 'commander';
import ConfigsProcessor from './configsProcessor.js';
import configs from './configs.js';
import Workspace from './workspace.js';
import c from 'picocolors';
import path from 'node:path';
import { createSpinner } from 'nanospinner';
import count from './lib/count.js';
import prompts from 'prompts';
import ConfigsFile from './configsFile.js';
import * as cardinal from 'cardinal';
import ansi from 'sisteransi';
import PackageInstaller from './packageInstaller.js';
import notNull from './lib/notNull.js';
const stdout = process.stdout;
export default class Cli {
#program = new Command();
/** @type {import('./types').CliArgs} */
args = {
dir: process.cwd(),
};
/**
* @param {import('./types').CliArgs} [args] Cli arguments object
*/
constructor(args) {
this.#program
.option('--packages <string...>')
.option('--dir <path>', undefined)
.option('--merge-to-root')
.option('--install-pkgs')
.parse();
this.args = {
...this.args,
...this.#program.opts(),
...args,
};
this.args.dir = !this.args.dir.startsWith('/')
? path.join(process.cwd(), this.args.dir)
: this.args.dir;
}
async run() {
process.chdir(this.args.dir);
const spinner = createSpinner('Detecting workspace configuration');
const processor = new ConfigsProcessor({ configs });
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;
});
spinner.success({
text:
'Detecting workspace configuration ' +
c.dim(`${count.packagesWithConfigs(packages)} configs founded\n`),
});
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')),
initial: true,
type: 'confirm',
})).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),
);
const fileHandler = new ConfigsFile(configs, packages.find(c => c.root)?.path);
for (const pkg of packages) {
pkg.configFile = fileHandler.generateObj(pkg);
pkg.configFile.content = await fileHandler.generate(pkg.configFile);
/** @type {boolean} */
const shouldWrite =
/** @type {{write: boolean}} */
(await prompts({
type: 'confirm',
name: 'write',
message: `Do you want to write this config file for ${pkg.root
? c.blue('the root directory')
: c.blue(pkg.name)
}?\n\n${cardinal.highlight(pkg.configFile.content)}`,
initial: true,
})).write;
stdout.write(ansi.erase.lines(pkg.configFile.content.split('\n').length + 2));
if (shouldWrite) await fileHandler.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);
/** @type {boolean | 'changePackage'} */
let installPkgs = this.args.installPkgs !== undefined ? true :
/** @type {{install: boolean | 'changePackage'}} */
(await prompts({
name: 'install',
message:
`Would you like to ESLit to install the npm packages with ${c.green(installer.packageManager.name)}?`,
choices: [
{ title: 'Yes, install all packages', value: true, description: installer.packageManager.description },
{ title: 'No, I will install them manually', value: false },
{ title: 'Change package manager', value: 'changePackage' },
],
type: 'select',
})).install;
if (installPkgs === 'changePackage') {
/** @type {{manager: import('./types').PackageManagerName}} */
const prompt = await prompts({
name: 'manager',
message: 'What package manager do you want ESLit to use?',
choices: Object.values(installer.packageManagers).map(m => {
return { title: m.name, description: m.description, value: m.name };
}),
type: 'select',
});
installer.packageManager = installer.packageManagers[prompt.manager];
installPkgs = true;
}
if (installPkgs) await installer.install();
}
}

View File

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

View File

@@ -0,0 +1,344 @@
import path from 'node:path';
import notNull from './lib/notNull.js';
import * as recast from 'recast';
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import astUtils from './lib/astUtils.js';
/**
* @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
*/
function mergeImportsMaps(map1, map2) {
for (const [key, value] of map2) {
if (!map1.has(key)) {
map1.set(key, value);
continue;
}
const imports1 = notNull(map1.get(key));
const imports2 = notNull(map2.get(key));
/**
* 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())
map1.set(key, value);
else
map1.set(key, [['default', imports1.toString()], ['default', imports2.toString()]]);
break;
case 'true,false':
map1.set(key, [['default', imports1.toString()], ...imports2]);
break;
case 'false,true':
map1.set(key, [['default', imports2.toString()], ...imports1]);
break;
case 'false,false':
map1.set(key, [...imports1, ...imports2]);
break;
}
if (typeof map1.get(key) !== 'string')
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
*/
function getPathDepth(path1, root) {
const pathDepth = path1.replace(root, '').split('/').slice(1);
if (pathDepth.length <= 1) return pathDepth.map(() => '.').join('/');
return pathDepth.map(() => '..').join('/');
}
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
*/
constructor(configs, root) {
this.configs = configs;
this.root = root ?? this.root;
}
/**
* @param {import('./types').Package} pkg The package to generate the config string from
* @returns {import('./types').ConfigFile} The config file object
*/
generateObj(pkg) {
/** @type {import('./types').ConfigFile} */
const configObj = {
path: path.join(pkg.path, 'eslint.config.js'),
imports: new Map(),
configs: [],
presets: [],
rules: [],
};
if (!pkg.root) {
const rootConfig = path.join(getPathDepth(pkg.path, this.root), 'eslint.config.js');
configObj.imports.set(!rootConfig.startsWith('.') ? `./${rootConfig}` : rootConfig, 'root');
configObj.presets.push('root');
}
for (const [configName, optionsNames] of notNull(pkg.config)) {
const config = this.configs.find(c => c.name === configName);
if (!config) continue;
const options = config.options.filter(o => optionsNames.includes(o.name));
if (!options || options.length === 0) continue;
const imports = options.reduce((acc, opt) => {
const map1 = new Map(Object.entries(acc.packages ?? {}));
const map2 = new Map(Object.entries(opt.packages ?? {}));
acc.packages = Object.fromEntries(mergeImportsMaps(map1, map2));
return acc;
});
configObj.imports = mergeImportsMaps(configObj.imports, new Map(Object.entries(imports.packages ?? {})));
configObj.configs = [...new Set([
...configObj.configs,
...options.map(o => o.configs ?? []).flat(),
])];
configObj.rules = [...new Set([
...configObj.rules,
...options.map(o => o.rules ?? []).flat(),
])];
configObj.presets = [...new Set([
...configObj.presets,
...options.map(o => o.presets ?? []).flat(),
])];
}
return configObj;
}
/**
* ! NOTE:
* These functions declared bellow are notably hard to read and have lots of exceptions and
* disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to
* prevent because a lot of the AST typescript types are somewhat wrong or simply hard to work
* with them.
*
* But for somewhat help developing and prevent unwanted errors in the future, the types and eslint
* errors are explicitly disabled and types are explicitly overridden. This is why there are so
* many JSDoc type annotations and comments in general.
*
* Any help to make this code more readable and robust is appreciated
*/
/**
* @typedef {import('estree').Program} Program
* @typedef {(
* import('./lib/astUtils.js').ExpressionOrIdentifier |
* import('estree').ObjectExpression |
* import('estree').SpreadElement
* )} ConfigArrayElement
*/
/**
* @param {Program} ast The program ast to be manipulated
* @returns {Promise<Program>} The final ast with the recreated default export
* @private
*/
async addDefaultExport(ast) {
/** @type {{program: Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const { program: exportTemplateAst } = recast.parse([
'/** @type {import(\'eslint\').Linter.FlatConfig[]} */',
'export default [',
'',
'];',
].join('\n'), { parser: (await import('recast/parsers/babel.js')) });
/** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
const exportTemplateNode = exportTemplateAst.body.find(n => n.type === 'ExportDefaultDeclaration');
/** @type {import('estree').ExportDefaultDeclaration | undefined} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
let astExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration');
if (!astExport) { ast.body.push(exportTemplateNode); return ast; }
/** @type {import('estree').VariableDeclaration | undefined} */
const oldExportValue = astExport.declaration.type !== 'ArrayExpression'
// @ts-expect-error astExport.declaration is a expression
? astUtils.createVariable('oldConfig', 'const', astExport.declaration)
: undefined;
if (!oldExportValue) return ast;
// @ts-expect-error declaration is a ArrayExpression
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
exportTemplateNode.declaration.elements.push({
type: 'SpreadElement',
argument: { type: 'Identifier', name: 'oldConfig' },
});
const astExportIdx = ast.body.indexOf(astExport);
ast.body[astExportIdx] = exportTemplateNode;
ast.body.splice(astExportIdx - 1, 0, oldExportValue);
return ast;
}
/**
* @param {import('./types').ConfigFile['rules']} rules The rules to be used to create the object
* @returns {import('estree').ObjectExpression} The object containing the spread rules
* @private
*/
createRulesObject(rules) {
/** @type {import('estree').SpreadElement[]} */
// @ts-expect-error The array is filtered to remove undefined's
const expressions = rules
.map(r => {
const e = astUtils.stringToExpression(r);
if (e) return astUtils.toSpreadElement(e);
else undefined;
}).filter(e => e);
return {
type: 'ObjectExpression',
properties: [{
// @ts-expect-error because ObjectProperty doesn't exist in estree types
type: 'ObjectProperty',
key: { type: 'Identifier', name: 'rules' },
value: {
type: 'ObjectExpression',
properties: expressions,
},
}],
};
}
/**
* Adds elements to the default export node, without adding duplicates
* @typedef {import('estree').ArrayExpression} ArrayExpression
* @param {Program} ast The program ast to be manipulated
* @param {ConfigArrayElement[]} elements The elements to be added to the array
* @returns {Program} The final ast with the recreated default export
* @private
*/
addElementsToExport(ast, elements) {
/** @type {import('estree').ExportDefaultDeclaration} */
// @ts-expect-error Node type needs to be ExportDefaultDeclaration to be founded
const exportNode = ast.body.find(n => n.type === 'ExportDefaultDeclaration');
const exportNodeIdx = ast.body.indexOf(exportNode);
/** @type {ArrayExpression} */
// @ts-expect-error declaration is a ArrayExpression
const array = exportNode.declaration;
for (const e of elements) {
if (e.type !== 'ObjectExpression' && astUtils.findInArray(array, e)) continue;
array.elements.push(e);
}
exportNode.declaration = array;
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
*/
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 importDeclaration = astUtils.createImportDeclaration(
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]);
});
}
if (existingDeclaration) ast.body[ast.body.indexOf(existingDeclaration)] = importDeclaration.body;
else importDeclarations.push(importDeclaration.body);
}
ast.body.unshift(...importDeclarations);
return ast;
}
/**
* @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file
* @returns {Promise<string>} The generated config file contents
*/
async generate(config) {
const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : '';
/** @type {{program: Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) });
await this.addDefaultExport(ast);
/**
* @type {ConfigArrayElement[]}
*/
// @ts-expect-error The array is filtered to remove undefined's
const elements = [
...config.configs.map(c => astUtils.stringToExpression(c)),
...config.presets.map(p => {
const e = astUtils.stringToExpression(p);
if (e) return astUtils.toSpreadElement(e);
else undefined;
}),
config.rules.length > 0
? this.createRulesObject(config.rules)
: undefined,
].filter(e => e);
this.addElementsToExport(ast, elements);
this.addPackageImports(ast, config.imports);
const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code;
return finalCode;
}
/**
* @param {string} path The path to the file to be written
* @param {string} content The content of the file
* @returns {Promise<void>}
*/
async write(path, content) {
await fs.writeFile(path, content, 'utf-8');
}
}

View File

@@ -0,0 +1,168 @@
#!node
import path from 'node:path';
import glob from 'picomatch';
import prompts from 'prompts';
import c from 'picocolors';
import str from './lib/str.js';
export default class ConfigsProcessor {
/** @type {string} */
dir = process.cwd();
/** @type {import('./types.js').Config[]} */
configs;
/** @type {string[] | undefined} */
#packagesPatterns;
/**
* @param {{
* configs: import('./types.js').Config[],
* packages?: string[],
* directory?: string,
* }} options - Cli options
*/
constructor(options) {
this.#packagesPatterns = options.packages;
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
*/
detectOptions(pkg, options, single) {
/** @type {string[]} */
const detectedOptions = [];
for (const option of options) {
if (option.detect === true) {
detectedOptions.push(option.name);
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);
if (files.length > 0 || directories.length > 0) {
detectedOptions.push(option.name);
if (single) break;
}
}
return detectedOptions;
}
/**
* @param {import('./types.js').Package[] | import('./types.js').Package} pkg - The packages to questions the configs
* @param {import('./types').Config[]} configs - The configs to be used
* @returns {Promise<import('./types.js').Package[]>} - The selected options by the user
*/
async questionConfig(pkg, configs) {
const packages = Array.isArray(pkg) ? [...pkg] : [pkg];
const instructions = c.dim(`\n${c.bold('A: Toggle all')} - ↑/↓: Highlight option - ←/→/[space]: Toggle selection - enter/return: Complete answer`);
for (const config of configs) {
/** @type {import('prompts').Choice[]} */
const configChoices = config.options.map(option => {return { title: `${str.capitalize(option.name)}`, value: option.name };});
/** @type {Record<string, string[]>} */
const selectedOptions = await prompts({
name: config.name,
type: config.type === 'multiple' ? 'multiselect' : 'select',
message: str.capitalize(config.name),
choices: config.type === 'confirm' ? [
{
title: 'Yes',
value: ['yes'],
},
{
title: 'No',
value: null,
},
] : configChoices,
hint: config.description,
instructions: instructions + c.dim(c.italic('\nSelect none if you don\'t want to use this configuration\n')),
});
if (selectedOptions[config.name] === null) continue;
if (selectedOptions[config.name].length === 0) continue;
if (packages.length <= 1) {
packages[0].config = new Map([
...(packages[0].config ?? []),
...Object.entries(selectedOptions),
]);
continue;
}
/** @type {{title: string, value: import('./types').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 };
})
.filter(p => p.title !== 'root');
/** @type {Record<'packages', import('./types').Package[]>} */
const selected = await prompts({
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 ?? [];
selected.packages.map(pkg => {
pkg.config = new Map([
...(pkg.config ?? []),
...Object.entries(selectedOptions),
]); return pkg;
});
packages.map(pkg => selected.packages.find(s => s.name === pkg.name) ?? pkg);
}
return packages;
}
/**
* @param {import('./types').Package} pkg - The package to detect configs
* @returns {import('./types').Package['config']} - Detected configs record
*/
detectConfig(pkg) {
/** @type {import('./types.js').Package['config']} */
const pkgConfig = new Map();
for (const config of this.configs.filter(c => !c.manual)) {
pkgConfig.set(config.name, this.detectOptions(
pkg,
config.options,
config.type !== 'multiple',
));
}
return pkgConfig;
}
}

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

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

View File

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

View File

@@ -0,0 +1,12 @@
/**
* @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,
).reduce((partial, sum) => partial + sum, 0);
}
export default { packagesWithConfigs };

View File

@@ -0,0 +1,15 @@
/**
* JSDoc types lack a non-null assertion.
* @template T
* @param {T} value The value which to assert against null or undefined
* @returns {NonNullable<T>} The said value
* @throws {TypeError} If the value is unexpectedly null or undefined
* @author Jimmy Wärting - https://github.com/jimmywarting
* @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-873331031
* @see https://github.com/Microsoft/TypeScript/issues/23405#issuecomment-1249287966
*/
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');
return value;
}

View File

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

View File

@@ -0,0 +1,210 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { exec } from 'node:child_process';
import { createSpinner } from 'nanospinner';
import c from 'picocolors';
import * as recast from 'recast';
import { readFile, writeFile } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
/**
* @type {import('./types').PackageManagerHandler}
*/
class CommandHandler {
/** @type {string} */
command;
/** @type {((path: string, packages: string[]) => string | Promise<string>) | undefined} */
checker;
/**
* @param {string} command What command to use to install
* @param {(path: string, packages: string[]) => string | Promise<string>} [checker] Checks if a argument should be passed
*/
constructor(command, checker) {
this.command = command;
this.checker = checker;
}
/**
* @param {string} path The path to run the command
* @param {string[]} packages The packages to be added on the command
* @returns {Promise<void>}
*/
async install(path, packages) {
if (this.checker)
this.command += await this.checker(path, packages);
return new Promise((res) => {
const spinner = createSpinner(`Installing packages with ${c.green(this.command)} ${c.dim(packages.join(' '))}`).start();
try {
const child = exec(`${this.command} ${packages.join(' ')}`, { cwd: path });
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)}`,
}));
child.stdout?.on('close', () => {
spinner.success({
text: `Installed packages with ${c.green(this.command)}`,
}); res();
});
}
catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
res(console.error(`Error while installing the packages with ${this.command} ${c.dim(packages.join(' '))} on ${path}: ${error}`));
}
});
}
}
/**
* @type {import('./types').PackageManagerHandler}
*/
class DenoHandler {
/**
* @param {string} path The path to run the command
* @param {string[]} packages The packages to be added on the command
* @returns {Promise<void>}
*/
async install(path, packages) {
const configPath = join(path, 'eslint.config.js');
if (!existsSync(configPath)) return;
const configFile = await readFile(configPath, 'utf8');
/** @type {{program: import('estree').Program}}*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { program: ast } = recast.parse(configFile, { parser: (await import('recast/parsers/babel.js')) });
ast.body.map((node) => {
if (node.type !== 'ImportDeclaration') return node;
if (packages.includes(node.source.value?.toString() ?? '')) {
node.source.value = `npm:${node.source.value}`;
}
return node;
});
await writeFile(configPath, recast.prettyPrint(ast).code, 'utf-8');
console.log(c.green('Added npm: specifier to dependencies'));
}
}
export default class PackageInstaller {
/**
* @typedef {Map<string, string[]>} PackagesMap
* @type {PackagesMap}
*/
packagesMap;
/**
* @typedef {{
* name: import('./types').PackageManagerName
* description: string
* handler: import('./types').PackageManagerHandler
* }} PackageManager
* @type {PackageManager}
*/
packageManager;
/**
* @type {Record<import('./types').PackageManagerName, PackageManager>}
*/
packageManagers = {
deno: {
name: 'deno',
description: 'Adds npm: specifiers to the eslint.config.js file',
handler: new DenoHandler(),
},
bun: {
name: 'bun',
description: 'Uses bun install',
handler: new CommandHandler('bun install'),
},
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'),
},
npm: {
name: 'npm',
description: 'Uses npm install',
handler: new CommandHandler('npm install --save-dev'),
},
};
/**
* @param {PackagesMap} packagesMap The map of directories and packages to be installed
* @param {string} root Root directory path
*/
constructor(packagesMap, root) {
this.packagesMap = packagesMap;
this.packageManager = this.detectPackageManager(root);
}
/**
* @param {string} root Root directory path
* @returns {PackageManager} The package manager detected;
* @private
*/
detectPackageManager(root) {
/** @type {(...path: string[]) => boolean} */
const exists = (...path) => existsSync(join(root, ...path));
switch (true) {
case exists('deno.json'):
case exists('deno.jsonc'):
return this.packageManagers.deno;
case exists('bun.lockb'):
return this.packageManagers.bun;
case exists('pnpm-lock.yaml'):
return this.packageManagers.pnpm;
case exists('yarn.lock'):
return this.packageManagers.yarn;
case exists('package-lock.json'):
return this.packageManagers.npm;
case exists('package.json'):
/** @type {{packageManager?: string}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, no-case-declarations
const { packageManager } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
if (!packageManager) return this.packageManagers.npm;
if (packageManager.includes('pnpm')) return this.packageManagers.pnpm;
if (packageManager.includes('yarn')) return this.packageManagers.yarn;
if (packageManager.includes('npm')) return this.packageManagers.npm;
else return this.packageManagers.npm;
default: return this.packageManagers.npm;
}
}
async install() {
for (const [path, packages] of this.packagesMap) {
await this.packageManager.handler.install(path, packages);
}
}
}

61
packages/cli/src/types.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,246 @@
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import YAML from 'yaml';
import path, { join } from 'node:path';
import glob from 'picomatch';
import picomatch from 'picomatch';
/**
* @template T
* @param {Promise<T>} promise - The async function to try running
* @returns {Promise<T | null>} - Returns the result of the async function, or null if it errors
*/
async function tryRun(promise) {
try {
return await promise;
}
catch (err) {
return null;
}
}
/**
* @param {string} directory - The directory to find .gitignore and .eslintignore
* @returns {Promise<string[]>} - List of ignore glob patterns
*/
async function getIgnoredFiles(directory) {
const gitIgnore = (await tryRun(fs.readFile(join(directory, '.gitignore'), 'utf8')) ?? '')
.split('\n')
.filter(p => p && !p.startsWith('#'))
.map(p => join(directory, '**', p));
const eslintIgnore = (await tryRun(fs.readFile(join(directory, '.eslintignore'), 'utf8')) ?? '')
.split('\n')
.filter(p => p && !p.startsWith('#'))
.map(p => join(directory, '**', p));
return [...eslintIgnore, ...gitIgnore];
}
/**
* @param {string} directory - The directory to work in.
* @returns {Promise<string>} - The package name founded.
*/
async function getPackageName(directory) {
if (existsSync(join(directory, 'package.json'))) {
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);
if (obj.name) return obj.name;
}
return path.normalize(directory).split('/').at(-1) ?? directory;
}
export default class Workspace {
/**
* @param {string} directory - The directory to get the workspace from
* @param {string[] | false} [packagePatterns]
* List of package patterns (`false` to explicitly tell that this workspace is not a monorepo)
*/
constructor(directory, packagePatterns) {
this.dir = directory;
this.packagePatterns = packagePatterns;
}
/**
* @param {string} [directory] - The directory to work on
* @param {string[]} [ignores] - Glob patterns to ignore
* @returns {Promise<{files: string[], directories: string[]}>} - List of all files in the directory
*/
async getPaths(directory = this.dir, ignores = []) {
ignores.push(
...[
'.git',
'.dist',
'.DS_Store',
'node_modules',
].map((f) => join(directory, f)),
...await getIgnoredFiles(directory),
);
const paths = (await fs.readdir(directory))
.map((f) => path.normalize(join(directory, f)))
.filter((p) => !glob.isMatch(p, ignores));
/** @type {string[]} */
const files = [];
/** @type {string[]} */
const directories = [];
for (const path of paths) {
if ((await fs.lstat(path)).isDirectory()) {
const subPaths = await this.getPaths(path, ignores);
directories.push(path, ...subPaths.directories);
files.push(...subPaths.files);
}
else {
files.push(path);
}
}
return {
files: files.map(p => path.normalize(p.replace(this.dir, './'))),
directories: directories.map(p => path.normalize(p.replace(this.dir, './'))),
};
}
/**
* @returns {Promise<string[]>} - List of packages on a directory;
*/
async getPackagePatterns() {
/** @type {string[]} */
let packagePatterns = [];
const pnpmWorkspace =
existsSync(join(this.dir, 'pnpm-workspace.yaml'))
? 'pnpm-workspace.yaml'
: existsSync(join(this.dir, 'pnpm-workspace.yml'))
? 'pnpm-workspace.yml'
: null;
if (pnpmWorkspace) {
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);
packagePatterns.push(...(pnpmWorkspaceObj?.packages ?? []));
}
else if (existsSync(join(this.dir, 'package.json'))) {
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);
packagePatterns.push(...(packageJsonObj?.workspaces ?? []));
}
return packagePatterns.map(p => {
p = path.normalize(p);
p = p.startsWith('/') ? p.replace('/', '') : p;
p = p.endsWith('/') ? p.slice(0, p.length - 1) : p;
return p;
});
}
/**
* @returns {Promise<import('./types').Package[]>} - The list of packages that exist in the workspace
*/
async getPackages() {
const paths = await this.getPaths();
/** @type {import('./types').Package} */
const rootPackage = {
root: true,
name: await getPackageName(this.dir),
path: this.dir,
files: paths.files,
directories: paths.directories,
};
if (this.packagePatterns === false) return [rootPackage];
const packagePatterns = this.packagePatterns ?? await this.getPackagePatterns();
const packagePaths = paths.directories.filter(d => picomatch.isMatch(d, packagePatterns));
/** @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}/`, '')),
});
rootPackage.files = rootPackage.files
.filter(f => picomatch.isMatch(f, `!${packagePath}/**/*`));
rootPackage.directories = rootPackage.directories
.filter(d => picomatch.isMatch(d, `!${packagePath}/**/*`));
}
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
*/
mergePackages(packages) {
const rootPackage = packages.find(p => p.root) ?? packages[0];
const merged = packages.reduce((accumulated, pkg) => {
const files = [...new Set([
...accumulated.files,
...pkg.files.map(f => join(pkg.path, f)),
]
.map(p => p.replace(`${rootPackage.path}/`, '')),
)];
const directories = [...new Set([
...accumulated.directories,
...pkg.directories.map(d => join(pkg.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])]);
}
return {
root: true,
path: rootPackage.path,
name: rootPackage.name,
files,
directories,
config: mergedConfig,
};
}, rootPackage);
return [merged];
}
}

View File

@@ -5,11 +5,11 @@
"main": "index.js",
"module": "./src/index.js",
"source": "./src/index.js",
"files": [
"src",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLit",
"files": [
"src",
"index.d.ts"
],
"homepage": "https://github.com/LoredDev/ESLit",
"exports": {
"default": "./src/index.js",
"import": "./src/index.js",
@@ -21,16 +21,16 @@
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint ."
},
"repository": {
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLit"
},
"repository": {
"directory": "packages/config",
"type": "git",
"url": "https://github.com/LoredDev/ESLit"
},
"author": {
"email": "contact.guz013@gmail.com",
"name": "Gustavo \"Guz\" L. de Mello",
"url": "https://guz.one"
},
"email": "contact.guz013@gmail.com",
"name": "Gustavo \"Guz\" L. de Mello",
"url": "https://guz.one"
},
"license": "MIT",
"devDependencies": {
"@types/eslint__js": "^8.42.0",
@@ -44,14 +44,13 @@
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint-plugin-jsdoc": "^46.4.4",
"globals": "^13.20.0",
"yaml": "^2.3.1"
"globals": "^13.20.0"
},
"peerDependencies": {
"eslint": "^8.45.0",
"typescript": "^5.1.6"
},
"publishConfig": {
"access": "public"
}
"publishConfig": {
"access": "public"
}
}

View File

@@ -2,7 +2,6 @@ import jsdoc from 'eslint-plugin-jsdoc';
/**
* JSDoc rules overrides
*
* @type {Readonly<import('eslint').Linter.FlatConfig>}
*/
const config = {
@@ -11,16 +10,6 @@ const config = {
rules: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...jsdoc.configs['recommended-typescript-flavor-error'].rules,
'jsdoc/tag-lines': ['error', 'always', {
count: 1,
applyToEndTag: false,
startLines: 1,
endLines: 0,
tags: {
param: { lines: 'never' },
},
}],
},
};
export default config;

209
pnpm-lock.yaml generated
View File

@@ -21,6 +21,9 @@ importers:
'@commitlint/types':
specifier: ^17.4.4
version: 17.4.4
'@eslit/cli':
specifier: workspace:*
version: link:packages/cli
'@svitejs/changesets-changelog-github-compact':
specifier: ^1.1.0
version: 1.1.0
@@ -34,8 +37,23 @@ importers:
specifier: ^1.10.9
version: 1.10.9
fixtures/library:
dependencies:
'@eslit/cli':
specifier: workspace:*
version: link:../../packages/cli
fixtures/monorepo:
devDependencies:
'@eslit/cli':
specifier: workspace:*
version: link:../../packages/cli
fixtures/svelte:
devDependencies:
'@eslit/cli':
specifier: workspace:*
version: link:../../packages/cli
'@fontsource/fira-mono':
specifier: ^4.5.10
version: 4.5.10
@@ -79,6 +97,46 @@ importers:
specifier: ^4.4.2
version: 4.4.2
packages/cli:
dependencies:
cardinal:
specifier: ^2.1.1
version: 2.1.1
commander:
specifier: ^11.0.0
version: 11.0.0
nanospinner:
specifier: ^1.1.0
version: 1.1.0
picocolors:
specifier: ^1.0.0
version: 1.0.0
picomatch:
specifier: ^2.3.1
version: 2.3.1
prompts:
specifier: ^2.4.2
version: 2.4.2
recast:
specifier: ^0.23.3
version: 0.23.3
sisteransi:
specifier: ^1.0.5
version: 1.0.5
yaml:
specifier: ^2.3.1
version: 2.3.1
devDependencies:
'@types/estree':
specifier: ^1.0.1
version: 1.0.1
'@types/node':
specifier: ^20.4.2
version: 20.4.2
'@types/prompts':
specifier: ^2.4.4
version: 2.4.4
packages/config:
dependencies:
'@eslint/eslintrc':
@@ -99,9 +157,6 @@ importers:
globals:
specifier: ^13.20.0
version: 13.20.0
yaml:
specifier: ^2.3.1
version: 2.3.1
devDependencies:
'@types/eslint__js':
specifier: ^8.42.0
@@ -849,6 +904,13 @@ packages:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
dev: true
/@types/prompts@2.4.4:
resolution: {integrity: sha512-p5N9uoTH76lLvSAaYSZtBCdEXzpOOufsRjnhjVSrZGXikVGHX9+cc9ERtHRV4hvBKHyZb1bg4K+56Bd2TqUn4A==}
dependencies:
'@types/node': 20.4.2
kleur: 3.0.3
dev: true
/@types/pug@2.0.6:
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
dev: true
@@ -1159,6 +1221,10 @@ packages:
dependencies:
color-convert: 2.0.1
/ansicolors@0.3.2:
resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==}
dev: false
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
@@ -1217,10 +1283,25 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/assert@2.0.0:
resolution: {integrity: sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==}
dependencies:
es6-object-assign: 1.1.0
is-nan: 1.3.2
object-is: 1.1.5
util: 0.12.5
dev: false
/ast-types@0.16.1:
resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
engines: {node: '>=4'}
dependencies:
tslib: 2.4.1
dev: false
/available-typed-arrays@1.0.5:
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
engines: {node: '>= 0.4'}
dev: true
/axobject-query@3.2.1:
resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==}
@@ -1282,7 +1363,6 @@ packages:
dependencies:
function-bind: 1.1.1
get-intrinsic: 1.2.1
dev: true
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
@@ -1302,6 +1382,14 @@ packages:
engines: {node: '>=6'}
dev: true
/cardinal@2.1.1:
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
hasBin: true
dependencies:
ansicolors: 0.3.2
redeyed: 2.1.1
dev: false
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -1393,6 +1481,11 @@ packages:
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
/commander@11.0.0:
resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
engines: {node: '>=16'}
dev: false
/comment-parser@1.3.1:
resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==}
engines: {node: '>= 12.0.0'}
@@ -1516,7 +1609,6 @@ packages:
dependencies:
has-property-descriptors: 1.0.0
object-keys: 1.1.1
dev: true
/dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
@@ -1637,6 +1729,10 @@ packages:
is-symbol: 1.0.4
dev: true
/es6-object-assign@1.1.0:
resolution: {integrity: sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==}
dev: false
/es6-promise@3.3.1:
resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
dev: true
@@ -1874,7 +1970,6 @@ packages:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
dev: true
/esquery@1.5.0:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
@@ -1992,7 +2087,6 @@ packages:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
is-callable: 1.2.7
dev: true
/fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
@@ -2025,7 +2119,6 @@ packages:
/function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/function.prototype.name@1.1.5:
resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
@@ -2053,7 +2146,6 @@ packages:
has: 1.0.3
has-proto: 1.0.1
has-symbols: 1.0.3
dev: true
/get-symbol-description@1.0.0:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
@@ -2113,7 +2205,6 @@ packages:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
get-intrinsic: 1.2.1
dev: true
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -2148,31 +2239,26 @@ packages:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
dependencies:
get-intrinsic: 1.2.1
dev: true
/has-proto@1.0.1:
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
engines: {node: '>= 0.4'}
dev: true
/has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: true
/has-tostringtag@1.0.0:
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
engines: {node: '>= 0.4'}
dependencies:
has-symbols: 1.0.3
dev: true
/has@1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: true
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@@ -2237,6 +2323,14 @@ packages:
side-channel: 1.0.4
dev: true
/is-arguments@1.1.1:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
has-tostringtag: 1.0.0
dev: false
/is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies:
@@ -2280,7 +2374,6 @@ packages:
/is-callable@1.2.7:
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
engines: {node: '>= 0.4'}
dev: true
/is-ci@3.0.1:
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
@@ -2311,12 +2404,27 @@ packages:
engines: {node: '>=8'}
dev: true
/is-generator-function@1.0.10:
resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: false
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
/is-nan@1.3.2:
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.2.0
dev: false
/is-negative-zero@2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'}
@@ -2397,7 +2505,6 @@ packages:
for-each: 0.3.3
gopd: 1.0.1
has-tostringtag: 1.0.0
dev: true
/is-weakref@1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
@@ -2457,6 +2564,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
/kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
@@ -2649,6 +2760,12 @@ packages:
hasBin: true
dev: true
/nanospinner@1.1.0:
resolution: {integrity: sha512-yFvNYMig4AthKYfHFl1sLj7B2nkHL4lzdig4osvl9/LdGbXwrdFRoqBS98gsEsOakr0yH+r5NZ/1Y9gdVB8trA==}
dependencies:
picocolors: 1.0.0
dev: false
/natural-compare-lite@1.4.0:
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
@@ -2685,10 +2802,17 @@ packages:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
dev: true
/object-is@1.1.5:
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.2.0
dev: false
/object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
dev: true
/object.assign@4.1.4:
resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
@@ -2814,7 +2938,6 @@ packages:
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
@@ -2887,6 +3010,14 @@ packages:
hasBin: true
dev: true
/prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
dev: false
/pseudomap@1.0.2:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
dev: true
@@ -2944,6 +3075,17 @@ packages:
picomatch: 2.3.1
dev: true
/recast@0.23.3:
resolution: {integrity: sha512-HbCVFh2ANP6a09nzD4lx7XthsxMOJWKX5pIcUwtLrmeEIl3I0DwjCoVXDE0Aobk+7k/mS3H50FK4iuYArpcT6Q==}
engines: {node: '>= 4'}
dependencies:
assert: 2.0.0
ast-types: 0.16.1
esprima: 4.0.1
source-map: 0.6.1
tslib: 2.4.1
dev: false
/redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
@@ -2952,6 +3094,12 @@ packages:
strip-indent: 3.0.0
dev: true
/redeyed@2.1.1:
resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==}
dependencies:
esprima: 4.0.1
dev: false
/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: true
@@ -3118,6 +3266,10 @@ packages:
totalist: 3.0.1
dev: true
/sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: false
/slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@@ -3150,6 +3302,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: false
/spawndamnit@2.0.0:
resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==}
dependencies:
@@ -3433,7 +3590,6 @@ packages:
/tslib@2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: true
/tsutils@3.21.0(typescript@5.0.2):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@@ -3590,6 +3746,16 @@ packages:
dependencies:
punycode: 2.3.0
/util@0.12.5:
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
dependencies:
inherits: 2.0.4
is-arguments: 1.1.1
is-generator-function: 1.0.10
is-typed-array: 1.1.10
which-typed-array: 1.1.10
dev: false
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
@@ -3692,7 +3858,6 @@ packages:
gopd: 1.0.1
has-tostringtag: 1.0.0
is-typed-array: 1.1.10
dev: true
/which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}