Update
This commit is contained in:
50
src/index.ts
50
src/index.ts
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { LocalRuleOptions } from '../index';
|
||||
import type { LocalRuleOptions } from '..';
|
||||
import { error } from '../constants';
|
||||
|
||||
export const localRules: Partial<LocalRuleOptions> = {
|
||||
|
Reference in New Issue
Block a user