169 lines
5.0 KiB
TypeScript
169 lines
5.0 KiB
TypeScript
import { Logger, colors as cliColors } from '@poppinss/cliui';
|
|
import { pascalCase, kebabCase } from 'change-case';
|
|
import type { Rule } from 'eslint';
|
|
import type { JSONSchema4 } from 'json-schema';
|
|
import { execSync } from 'node:child_process';
|
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { format } from 'prettier';
|
|
import { generateTypeFromSchema } from './json-schema-to-ts';
|
|
import { buildJSDoc, prettierConfig, type Plugin } from './utils';
|
|
|
|
const logger = new Logger();
|
|
const colors = cliColors.ansi();
|
|
|
|
/**
|
|
* Build the rule description to append to the JSDoc.
|
|
*/
|
|
function buildDescription(description = ''): string {
|
|
description = description.charAt(0).toUpperCase() + description.slice(1);
|
|
if (description.length > 0 && !description.endsWith('.')) {
|
|
description += '.';
|
|
}
|
|
|
|
return description.replace('*/', '');
|
|
}
|
|
|
|
export async function RuleFile(
|
|
plugin: Plugin,
|
|
pluginDirectory: string,
|
|
ruleName: string,
|
|
rule: Rule.RuleModule,
|
|
) {
|
|
let content = '';
|
|
|
|
const rulePath = resolve(pluginDirectory, `${ruleName}.d.ts`);
|
|
const ruleNamePascalCase = pascalCase(ruleName);
|
|
const schema: JSONSchema4 | JSONSchema4[] | undefined = rule.meta?.schema;
|
|
const isSchemaArray = Array.isArray(schema);
|
|
|
|
const mainSchema = isSchemaArray ? schema[0] : schema;
|
|
const sideSchema = isSchemaArray && schema.length > 1 ? schema[1] : undefined;
|
|
const thirdSchema = isSchemaArray && schema.length > 2 ? schema[2] : undefined;
|
|
|
|
/**
|
|
* Generate a JSDoc with the rule description and `@see` url.
|
|
*/
|
|
function generateTypeJsDoc(): string {
|
|
const { meta } = rule;
|
|
const seeDocLink: string = meta?.docs?.url
|
|
? `@see [${ruleName}](${meta.docs.url})`
|
|
: '';
|
|
|
|
return buildJSDoc([
|
|
buildDescription(rule.meta?.docs?.description),
|
|
' ',
|
|
rule.meta?.deprecated ? '@deprecated' : '',
|
|
rule.meta?.deprecated ? ' ' : '',
|
|
seeDocLink,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Generate a type from a JSON schema and append it to the file content.
|
|
*/
|
|
async function appendJsonSchemaType(
|
|
schema: JSONSchema4,
|
|
comment: string,
|
|
): Promise<void> {
|
|
const type = await generateTypeFromSchema(schema, ruleNamePascalCase + comment);
|
|
const jsdoc = buildJSDoc([`${comment}.`]);
|
|
content += `\n${jsdoc}`;
|
|
content += `\n${type}\n`;
|
|
}
|
|
|
|
/**
|
|
* Scoped rule name ESLint config uses.
|
|
*/
|
|
function prefixedRuleName(): string {
|
|
const { prefix, name } = plugin;
|
|
const rulePrefix = (prefix ?? kebabCase(name)) + '/';
|
|
return name === 'ESLint' ? ruleName : `${rulePrefix}${ruleName}`;
|
|
}
|
|
|
|
/**
|
|
* Append the `import type { RuleConfig } from '../rule-config'` at the top of the file.
|
|
*/
|
|
const nestedDepth = ruleName.split('/').length;
|
|
content += `import type { RuleConfig } from '${'../'.repeat(nestedDepth)}rule-config'\n\n`;
|
|
|
|
/**
|
|
* Generate and append types for the rule schemas.
|
|
*/
|
|
if (thirdSchema) {
|
|
await appendJsonSchemaType(thirdSchema, 'Setting');
|
|
}
|
|
if (sideSchema) {
|
|
await appendJsonSchemaType(sideSchema, 'Config');
|
|
}
|
|
if (mainSchema) {
|
|
await appendJsonSchemaType(mainSchema, 'Option');
|
|
}
|
|
|
|
if (mainSchema) {
|
|
/**
|
|
* Append the rule type options to the file content.
|
|
*/
|
|
let type: string = '';
|
|
if (!isSchemaArray) {
|
|
type = `${ruleNamePascalCase}Option`;
|
|
} else if (thirdSchema) {
|
|
type = `[${ruleNamePascalCase}Option?, ${ruleNamePascalCase}Config?, ${ruleNamePascalCase}Setting?]`;
|
|
} else if (sideSchema) {
|
|
type = `[${ruleNamePascalCase}Option?, ${ruleNamePascalCase}Config?]`;
|
|
} else if (mainSchema) {
|
|
type = `[${ruleNamePascalCase}Option?]`;
|
|
}
|
|
|
|
content += buildJSDoc(['Options.']) + '\n';
|
|
content += `export type ${ruleNamePascalCase}Options = ${type}\n\n`;
|
|
}
|
|
|
|
/**
|
|
* Append the rule config type to the file content.
|
|
*/
|
|
content += generateTypeJsDoc() + '\n';
|
|
content += `export type ${ruleNamePascalCase}RuleConfig = RuleConfig<${mainSchema ? `${ruleNamePascalCase}Options` : '[]'}>;\n\n`;
|
|
|
|
/**
|
|
* Append the final rule interface to the file content.
|
|
*/
|
|
content += generateTypeJsDoc() + '\n';
|
|
content += `export interface ${ruleNamePascalCase}Rule {`;
|
|
content += `${generateTypeJsDoc()}\n`;
|
|
content += `'${prefixedRuleName()}': ${ruleNamePascalCase}RuleConfig;`;
|
|
content += '}';
|
|
|
|
content = await format(content, prettierConfig);
|
|
|
|
/**
|
|
* Create the directory of the rule file if it doesn't exist.
|
|
*/
|
|
const subPath = dirname(rulePath);
|
|
if (!existsSync(subPath)) {
|
|
mkdirSync(subPath, { recursive: true });
|
|
}
|
|
|
|
writeFileSync(rulePath, content);
|
|
|
|
/**
|
|
* Apply a patch to the generated content if a diff file exists for it.
|
|
*
|
|
* Must be called after `generate()`.
|
|
*/
|
|
const pathParts = rulePath.split('/');
|
|
const diffFile = resolve(
|
|
__dirname,
|
|
'./diffs/rules',
|
|
pathParts.at(-2) ?? '',
|
|
`${pathParts.at(-1) ?? ''}.diff`,
|
|
);
|
|
|
|
if (existsSync(diffFile)) {
|
|
logger.logUpdate(colors.yellow(` 🧹 Adjusting ${prefixedRuleName()}`));
|
|
logger.logUpdatePersist();
|
|
|
|
execSync(`git apply ${diffFile}`);
|
|
}
|
|
}
|