#!/usr/bin/env bun import { Logger, colors as cliColors } from '@poppinss/cliui'; import { pascalCase } from 'change-case'; import type { Rule } from 'eslint'; import { existsSync, promises as fs } from 'node:fs'; import { join, resolve } from 'node:path'; import { format } from 'prettier'; import { buildJSDoc, prettierConfig, type Plugin } from './utils'; import { PLUGIN_REGISTRY, loadPlugin } from './plugins-map'; import { RuleFile } from './rule-file'; interface FailedRule { ruleName: string; err: unknown; } const logger = new Logger(); const colors = cliColors.ansi(); const getRuleName = (name: string) => name === 'ESLint' ? 'ESLintRule' : `${pascalCase(name)}Rule`; const index: [string, string][] = []; /** * Generate the `index.d.ts` file for the plugin's rules that will re-export all rules. */ async function generateRuleIndexFile( pluginDirectory: string, { rules, name, id }: Plugin, failedRules: FailedRule[], ): Promise { const generatedRules = Object.keys(rules!).filter( ruleName => !failedRules.some(failedRule => failedRule.ruleName === ruleName), ); /** * Build all the import statements for the rules. */ const rulesImports = generatedRules .map(name => `import type { ${getRuleName(name)} } from './${name}';`) .join('\n'); /** * Build the exported type that is an intersection of all the rules. */ const rulesFinalIntersection = generatedRules .map(name => `${getRuleName(name)}`) .sort() .join(' & '); const pluginRulesType = ` ${buildJSDoc([`All ${name} rules.`])} export type ${name}Rules = ${rulesFinalIntersection}; `; /** * Write the final `index.d.ts` file. */ const fileContent = ` ${rulesImports} ${pluginRulesType} `; const indexPath = join(pluginDirectory, 'index.d.ts'); await fs.writeFile(indexPath, await format(fileContent, prettierConfig)); index.push([`${name}Rules`, `./${id}/index`]); } /** * Print a report after having generated rules files for a plugin. */ function printGenerationReport( rules: Array<[string, Rule.RuleModule]>, failedRules: FailedRule[], ): void { const msg: string = ` ✅ Generated ${rules.length - failedRules.length} rules`; logger.logUpdate(colors.green(msg)); logger.logUpdatePersist(); if (failedRules.length) { logger.log(colors.red(` ❌ Failed ${failedRules.length} rules`)); failedRules.forEach(({ ruleName, err }) => { logger.log(colors.red(` - ${ruleName}: ${String(err)}`)); }); } logger.log(''); } /** * Generate a `.d.ts` file for each rule in the given plugin. */ async function generateRulesFiles( plugin: Plugin, pluginDirectory: string, ): Promise<{ failedRules: FailedRule[] }> { const failedRules: FailedRule[] = []; const pluginRules = plugin.rules; if (!pluginRules) { throw new Error( `Plugin ${plugin.name} doesn't have any rules. Did you forget to load them?`, ); } const rules = Object.entries(pluginRules); for (const [ruleName, rule] of rules) { logger.logUpdate(colors.yellow(` Generating > ${ruleName}`)); try { await RuleFile(plugin, pluginDirectory, ruleName, rule); } catch (err) { failedRules.push({ ruleName, err }); } } printGenerationReport(rules, failedRules); return { failedRules }; } const rulesDirectory = resolve(__dirname, '../../../src/types/rules'); /** * If it doesn't exist, create the directory that will contain the plugin's rule files. */ async function createPluginDirectory(pluginName: string): Promise { const pluginDirectory = join(rulesDirectory, pluginName); if (existsSync(pluginDirectory)) { await fs.rm(pluginDirectory, { recursive: true, force: true }); } await fs.mkdir(pluginDirectory, { mode: 0o755, recursive: true }); return pluginDirectory; } export interface RunOptions { plugins?: string[]; targetDirectory?: string; } for (const plugin of PLUGIN_REGISTRY) { logger.info(`Generating ${plugin.name} rules.`); logger.logUpdate(colors.yellow(` Loading plugin > ${plugin.name}`)); const loadedPlugin = await loadPlugin(plugin); const pluginDir = await createPluginDirectory(plugin.id); const { failedRules } = await generateRulesFiles(loadedPlugin, pluginDir); await generateRuleIndexFile(pluginDir, loadedPlugin, failedRules); } await fs.writeFile( resolve(rulesDirectory, 'index.d.ts'), await format( [ 'import type { RuleConfig } from "./rule-config";', ...index.map(([name, path]) => `import { ${name} } from ${JSON.stringify(path)};`), '', '/**', ' * Rules.', ' *', ' * @see [Rules](https://eslint.org/docs/user-guide/configuring/rules)', ' */', '', 'export type Rules = Partial<', ...index.map(([name]) => ` ${name} &`), ' Record', '>;', ].join('\n'), prettierConfig, ), );