179 lines
4.8 KiB
TypeScript
Executable File
179 lines
4.8 KiB
TypeScript
Executable File
#!/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<void> {
|
|
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<string> {
|
|
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<string, RuleConfig>',
|
|
'>;',
|
|
].join('\n'),
|
|
prettierConfig,
|
|
),
|
|
);
|