This commit is contained in:
Alex
2024-04-17 00:43:45 -04:00
parent 51455e3c21
commit fb50ede688
12 changed files with 401 additions and 297 deletions

View File

@ -56,11 +56,18 @@ export interface LocalRuleOptions {
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
export interface CustomRule {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}>;
options?: RuleLevel;
}
/**
* ESLint Configuration.
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
*/
type Config = Omit<ESLintConfig, 'rules'> & {
export type InputConfig = Omit<ESLintConfig, 'rules'> & {
/**
* Rules.
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
@ -68,24 +75,13 @@ type Config = Omit<ESLintConfig, 'rules'> & {
rules?: RuleOptions;
/**
* Glob pattern to find paths to custom rule files in JavaScript or TypeScript.
* Note this must be a string literal or an array of string literals since
* this is statically analyzed.
*/
customRules?: {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}>;
options?: RuleLevel;
}[];
customRuleFiles?: string | string[];
};
export function defineCustomRule<Options extends readonly unknown[]>(
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, Options>;
}>,
options?: Options,
) {
return { rule, options };
}
/**
* Returns a ESLint config object.
*
@ -104,15 +100,17 @@ export function defineCustomRule<Options extends readonly unknown[]>(
* Non bundled:
* 1. [`graphql`](https://the-guild.dev/graphql/eslint/rules)
*/
export function extendConfig({
plugins = [],
settings,
rules,
extends: _extends,
overrides,
customRules,
...rest
}: Config = {}): ESLintConfig {
export function extendConfig(of: InputConfig = {}): ESLintConfig {
const {
plugins = [],
settings,
rules,
extends: _extends,
overrides,
customRuleFiles,
...rest
} = of;
const hasReact = plugins.includes('react');
const hasReactRefresh = plugins.includes('react-refresh');
const hasUnicorn = plugins.includes('unicorn');
@ -129,7 +127,7 @@ export function extendConfig({
fs.mkdirSync(ruleDir, { recursive: true });
}
const result: Config = {
const result: InputConfig = {
root: true,
parser: '@typescript-eslint/parser',
plugins: unique('@typescript-eslint', 'import', 'rules', plugins),

View File

@ -1,18 +1,13 @@
import type { ESLint } from 'eslint';
import * as fs from 'node:fs';
import { resolve, basename, extname } from 'node:path';
function tryRequire(candidates: string[]) {
for (const candidate of candidates) {
try {
require(candidate);
return;
} catch {}
}
}
import { glob } from 'fast-glob';
import { parseModule } from 'esprima';
import query from 'esquery';
import type { Node, Property } from 'estree';
// https://github.com/gulpjs/interpret
tryRequire([
const transpilers = [
'esbin/register',
'esbuild-register',
'ts-node/register/transpile-only',
@ -20,20 +15,63 @@ tryRequire([
'sucrase/register',
'@babel/register',
'coffeescript/register',
]);
];
const folders = resolve(process.cwd(), 'eslint-local-rules');
const files = fs.readdirSync(folders);
function tryRequire() {
for (const candidate of transpilers) {
try {
require(candidate);
return;
} catch {}
}
}
const unwrapDefault = <T = any>(module: any): T => module.default ?? module;
const plugin: ESLint.Plugin = {
rules: {},
};
for (const file of files) {
const module = require(resolve(folders, file));
const unwrap = module.default ?? module;
const name = unwrap.name ?? basename(file, extname(file));
plugin.rules![name] = unwrap;
function hydrateESTreeNode(n: Node): any {
switch (n.type) {
case 'Literal':
return n.value;
case 'ArrayExpression':
return n.elements.filter(Boolean).map(hydrateESTreeNode);
default:
throw new Error(`Unsupported node type: ${n.type}`);
}
}
function parseConfigFile(js: string) {
const [node] = query(
parseModule(js),
'CallExpression[callee.name="extendConfig"] > ObjectExpression > Property[key.name="customRuleFiles"]',
);
return hydrateESTreeNode((node as Property).value);
}
function main() {
const rootDir = process.cwd();
const eslintConfigFile = ['.eslintrc.js', '.eslintrc.cjs']
.map(file => resolve(rootDir, file))
.find(file => fs.existsSync(file));
if (!eslintConfigFile) return;
const eslintConfig = fs.readFileSync(eslintConfigFile, 'utf8');
const customRuleFiles = parseConfigFile(eslintConfig);
if (!customRuleFiles?.length) return;
tryRequire();
for (const file of glob.sync(customRuleFiles)) {
const module = unwrapDefault(require(file));
const name = module.name ?? basename(file, extname(file));
plugin.rules![name] = module;
}
}
main();
export = plugin;

View File

@ -1,4 +1,4 @@
import type { LocalRuleOptions } from '../index';
import type { LocalRuleOptions } from '..';
import { error } from '../constants';
export const localRules: Partial<LocalRuleOptions> = {