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

30
dist/index.d.ts vendored
View File

@ -1,4 +1,4 @@
// Generated by dts-bundle-generator v9.3.1
// Generated by dts-bundle-generator v9.4.0
import { ESLintUtils } from '@typescript-eslint/utils';
import { Rule } from 'eslint';
@ -26,32 +26,28 @@ export interface LocalRuleOptions {
"rules/no-empty-object-literal": RuleEntry<unknown>;
}
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/)
*/
export type Config = Omit<ESLintConfig, "rules"> & {
export type InputConfig = Omit<ESLintConfig, "rules"> & {
/**
* Rules.
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/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;
}[];
};
export declare function defineCustomRule<Options extends readonly unknown[]>(rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, Options>;
}>, options?: Options): {
rule: () => Promise<{
default: Rule.RuleModule | ESLintUtils.RuleModule<string, Options>;
}>;
options: Options | undefined;
customRuleFiles?: string | string[];
};
/**
* Returns a ESLint config object.
@ -71,6 +67,6 @@ export declare function defineCustomRule<Options extends readonly unknown[]>(rul
* Non bundled:
* 1. [`graphql`](https://the-guild.dev/graphql/eslint/rules)
*/
export declare function extendConfig({ plugins, settings, rules, extends: _extends, overrides, customRules, ...rest }?: Config): ESLintConfig;
export declare function extendConfig(of?: InputConfig): ESLintConfig;
export {};

4
dist/package.json vendored
View File

@ -1,6 +1,6 @@
{
"name": "@aet/eslint-rules",
"version": "0.0.23",
"version": "0.0.24-beta.1",
"license": "UNLICENSED",
"peerDependencies": {
"eslint": "^8.57.0",
@ -28,6 +28,8 @@
"eslint-plugin-es-x": "^7.6.0",
"eslint-plugin-jsdoc": "^48.2.3",
"eslint-plugin-unicorn": "^52.0.0",
"esprima": "^4.0.1",
"esquery": "^1.5.0",
"estraverse": "^5.3.0",
"fast-glob": "^3.3.2",
"get-tsconfig": "^4.7.3",

2
dist/prettier.d.ts vendored
View File

@ -1,4 +1,4 @@
// Generated by dts-bundle-generator v9.3.1
// Generated by dts-bundle-generator v9.4.0
import { Config } from 'prettier';

18
dist/types.d.ts vendored
View File

@ -1,22 +1,16 @@
// Generated by dts-bundle-generator v9.3.1
// Generated by dts-bundle-generator v9.4.0
import { ESLintUtils } from '@typescript-eslint/utils';
import { Rule } from 'eslint';
export declare function defineRules(rules: {
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>;
}): {
[ruleName: string]:
| Rule.RuleModule
| ESLintUtils.RuleModule<string, unknown[], ESLintUtils.RuleListener>;
[ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[], ESLintUtils.RuleListener>;
};
export declare function defineRule({
name,
create,
...meta
}: Rule.RuleMetaData & {
name?: string;
create: (context: Rule.RuleContext) => Rule.RuleListener;
export declare function defineRule({ name, create, ...meta }: Rule.RuleMetaData & {
name?: string;
create: (context: Rule.RuleContext) => Rule.RuleListener;
}): Rule.RuleModule;
export {};

View File

@ -12,6 +12,8 @@
"@types/babel-plugin-macros": "^3.1.3",
"@types/babel__core": "^7.20.5",
"@types/eslint": "^8.56.9",
"@types/esprima": "^4.0.6",
"@types/esquery": "^1.5.3",
"@types/estree": "^1.0.5",
"@types/estree-jsx": "^1.0.5",
"@types/lodash": "^4.17.0",
@ -29,6 +31,8 @@
"eslint": "8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^1.24.1",
"esprima": "^4.0.1",
"esquery": "^1.5.0",
"fast-glob": "^3.3.2",
"find-cache-dir": "^5.0.0",
"json-schema-to-ts": "^3.0.1",

30
pnpm-lock.yaml generated
View File

@ -34,6 +34,12 @@ devDependencies:
'@types/eslint':
specifier: ^8.56.9
version: 8.56.9
'@types/esprima':
specifier: ^4.0.6
version: 4.0.6
'@types/esquery':
specifier: ^1.5.3
version: 1.5.3
'@types/estree':
specifier: ^1.0.5
version: 1.0.5
@ -85,6 +91,12 @@ devDependencies:
eslint-define-config:
specifier: ^1.24.1
version: 1.24.1
esprima:
specifier: ^4.0.1
version: 4.0.1
esquery:
specifier: ^1.5.0
version: 1.5.0
fast-glob:
specifier: ^3.3.2
version: 3.3.2
@ -1788,6 +1800,18 @@ packages:
'@types/json-schema': 7.0.15
dev: true
/@types/esprima@4.0.6:
resolution: {integrity: sha512-lIk+kSt9lGv5hxK6aZNjiUEGZqKmOTpmg0tKiJQI+Ow98fLillxsiZNik5+RcP7mXL929KiTH/D9jGtpDlMbVw==}
dependencies:
'@types/estree': 1.0.5
dev: true
/@types/esquery@1.5.3:
resolution: {integrity: sha512-c55hQOcoPkWDfuEN9EdP1YyNH4D909U40gUEpY0nB5PWHExWHEPxcx3sx0fJ1Gzf4j1OpWktmIgciIlpgHtfDg==}
dependencies:
'@types/estree': 1.0.5
dev: true
/@types/estree-jsx@1.0.5:
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
dependencies:
@ -2446,6 +2470,12 @@ packages:
eslint-visitor-keys: 3.4.3
dev: true
/esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
dev: true
/esquery@1.5.0:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'}

View File

@ -1,218 +1,19 @@
#!/usr/bin/env tsx
import assert from 'node:assert';
import { readFileSync, promises as fs } from 'node:fs';
import { resolve, extname, relative } from 'node:path';
import { promises as fs } from 'node:fs';
import { resolve, relative } from 'node:path';
import { isBuiltin } from 'node:module';
import esbuild from 'esbuild';
import type { Loader, Plugin } from 'esbuild';
import * as babel from '@babel/core';
import type { Plugin } from 'esbuild';
import { memoize } from 'lodash';
import { gray, green } from 'picocolors';
import type { types as t, types } from '@babel/core';
import { dependencies } from '../dist/package.json';
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
import * as polyfill from '../src/polyfill';
import { buildLocalRules } from '../src/build-local-rules';
import { execSync } from 'node:child_process';
const polyfills = Object.keys(polyfill);
import { dts } from './dts';
import { babelPlugin } from './modifier';
const ENV = (process.env.NODE_ENV ??= 'production');
const PROD = ENV === 'production';
class HandlerMap {
map = new Map<string, MacroHandler>();
set(names: string | string[], handler: MacroHandler) {
names = Array.isArray(names) ? names : [names];
const macro = createMacro(handler);
for (const name of names) {
this.map.set(name, macro);
}
return this;
}
get keys() {
return Array.from(this.map.keys());
}
resolvePath = (module: string) => module;
require = (module: string) => this.map.get(module);
isMacrosName = (module: string) => this.map.has(module);
}
const map = new HandlerMap()
.set(
'object.assign',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
)
.set(
['object-values', 'object.values'],
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
)
.set(
'object.fromentries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
)
.set(
'object.entries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
)
.set(
'hasown',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'has',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'array-includes',
proto(t => t.identifier('includes')),
)
.set(
'array.prototype.flatmap',
proto(t => t.identifier('flatMap')),
)
.set(
'array.prototype.flat',
proto(t => t.identifier('flat')),
)
.set(
'array.prototype.findlastindex',
proto(t => t.identifier('findLastIndex')),
)
.set(
'array.prototype.tosorted',
proto(t => t.identifier('toSorted')),
)
.set(
'array.prototype.toreversed',
proto(t => t.identifier('toReversed')),
)
.set(
'array.prototype.findlast',
proto(t => t.identifier('findLast')),
)
.set(
'string.prototype.matchall',
proto(t => t.identifier('matchAll')),
)
.set(
'string.prototype.includes',
proto(t => t.identifier('includes')),
)
.set(
'object.groupby',
replace(t =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash')]),
t.identifier('groupBy'),
),
),
);
// es-iterator-helpers/Iterator.prototype.*
const polyfillPath = resolve(__dirname, '../src/polyfill.ts');
const requirePolyfill = (t: typeof types, name: string) =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]),
t.identifier(name),
);
map.set(
`es-iterator-helpers/Iterator.from`,
replace(t => requirePolyfill(t, 'from')),
);
for (const name of polyfills) {
map.set(
`es-iterator-helpers/Iterator.prototype.${name}`,
replace(t => requirePolyfill(t, name)),
);
}
map.set(
'safe-regex-test',
replace(t => requirePolyfill(t, 'safeRegexTest')),
);
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
referencePath.replaceWith(getReplacement(t));
});
};
}
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
const { parent, parentPath } = referencePath;
assert(t.isCallExpression(parent));
const [callee, ...rest] = parent.arguments;
parentPath!.replaceWith(
t.callExpression(
t.memberExpression(callee as t.Expression, getProperty(t)),
rest,
),
);
});
};
}
export const babelPlugin: Plugin = {
name: 'babel',
setup(build) {
const { keys, ...macroOptions } = map;
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
const { path } = args;
if (path.includes('node_modules/')) {
return null;
}
let source = readFileSync(path, 'utf-8')
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries')
.replaceAll(
"Object.keys(require('prop-types'))",
JSON.stringify(Object.keys(require('prop-types'))),
);
if (
path.includes('packages/eslint-plugin-import/src/rules/') ||
path.includes('packages/eslint-plugin-import/config/')
) {
source = source.replace('\nmodule.exports = {', '\nexport default {');
}
const isFlow = source.includes('@flow');
const loader = extname(path).slice(1) as Loader;
if (!isFlow && !keys.some(key => source.includes(key))) {
return { contents: source, loader };
}
const res = babel.transformSync(source, {
filename: path,
babelrc: false,
configFile: false,
parserOpts: {
plugins: [isFlow ? 'flow' : 'typescript'],
},
plugins: [
isFlow && '@babel/plugin-transform-flow-strip-types',
['babel-plugin-macros', macroOptions],
].filter(Boolean),
})!;
return {
contents: res.code!,
loader,
};
});
},
};
declare global {
interface Array<T> {
filter(
@ -264,14 +65,14 @@ if (process.env.DEBUG) {
});
}
async function bundle(
function bundle(
entry: string,
outfile = entry
.replace('./packages/', './dist/')
.replace('src/', '')
.replace('.ts', '.js'),
) {
await esbuild.build({
return esbuild.build({
entryPoints: [entry],
outfile,
bundle: true,
@ -281,9 +82,7 @@ async function bundle(
sourcemap: 'linked',
plugins,
define: {},
alias: {
// esm modules
},
alias: {},
external: ['find-cache-dir'],
banner: {
js: '/* eslint-disable */',
@ -317,18 +116,11 @@ async function useText(path: string) {
}
function bundleType(source: string, output: string) {
execSync(
[
'npx',
'dts-bundle-generator',
JSON.stringify(source),
'-o',
JSON.stringify(output),
'--project',
'"./tsconfig.build.json"',
'--no-check',
].join(' '),
);
return dts({
source,
dist: output,
project: './tsconfig.build.json',
});
}
async function main() {

36
scripts/dts.ts Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env node
import * as ts from 'typescript';
import {
generateDtsBundle,
type EntryPointConfig,
} from 'dts-bundle-generator/dist/bundle-generator';
export function dts({
source,
dist,
project,
}: {
source: string;
dist: string;
project: string;
}): void {
const entry: EntryPointConfig = {
filePath: source,
failOnClass: false,
output: {
inlineDeclareExternals: false,
inlineDeclareGlobals: false,
sortNodes: false,
noBanner: false,
respectPreserveConstEnum: false,
exportReferencedTypes: true,
},
};
const generatedDts = generateDtsBundle([entry], {
preferredConfigPath: project,
followSymlinks: true,
});
ts.sys.writeFile(dist, generatedDts[0]);
}

214
scripts/modifier.ts Normal file
View File

@ -0,0 +1,214 @@
#!/usr/bin/env tsx
import assert from 'node:assert';
import { readFileSync, promises as fs } from 'node:fs';
import { resolve, extname, relative } from 'node:path';
import { isBuiltin } from 'node:module';
import esbuild from 'esbuild';
import type { Loader, Plugin } from 'esbuild';
import * as babel from '@babel/core';
import { memoize } from 'lodash';
import { gray, green } from 'picocolors';
import type { types as t, types } from '@babel/core';
import { dependencies } from '../dist/package.json';
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
import * as polyfill from '../src/polyfill';
import { buildLocalRules } from '../src/build-local-rules';
import { dts } from './dts';
const polyfills = Object.keys(polyfill);
const ENV = (process.env.NODE_ENV ??= 'production');
const PROD = ENV === 'production';
class HandlerMap {
map = new Map<string, MacroHandler>();
set(names: string | string[], handler: MacroHandler) {
names = Array.isArray(names) ? names : [names];
const macro = createMacro(handler);
for (const name of names) {
this.map.set(name, macro);
}
return this;
}
get keys() {
return Array.from(this.map.keys());
}
resolvePath = (module: string) => module;
require = (module: string) => this.map.get(module);
isMacrosName = (module: string) => this.map.has(module);
}
const map = new HandlerMap()
.set(
'object.assign',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
)
.set(
['object-values', 'object.values'],
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
)
.set(
'object.fromentries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
)
.set(
'object.entries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
)
.set(
'hasown',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'has',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'array-includes',
proto(t => t.identifier('includes')),
)
.set(
'array.prototype.flatmap',
proto(t => t.identifier('flatMap')),
)
.set(
'array.prototype.flat',
proto(t => t.identifier('flat')),
)
.set(
'array.prototype.findlastindex',
proto(t => t.identifier('findLastIndex')),
)
.set(
'array.prototype.tosorted',
proto(t => t.identifier('toSorted')),
)
.set(
'array.prototype.toreversed',
proto(t => t.identifier('toReversed')),
)
.set(
'array.prototype.findlast',
proto(t => t.identifier('findLast')),
)
.set(
'string.prototype.matchall',
proto(t => t.identifier('matchAll')),
)
.set(
'string.prototype.includes',
proto(t => t.identifier('includes')),
)
.set(
'object.groupby',
replace(t =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash')]),
t.identifier('groupBy'),
),
),
);
// es-iterator-helpers/Iterator.prototype.*
const polyfillPath = resolve(__dirname, '../src/polyfill.ts');
const requirePolyfill = (t: typeof types, name: string) =>
t.memberExpression(
t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]),
t.identifier(name),
);
map.set(
`es-iterator-helpers/Iterator.from`,
replace(t => requirePolyfill(t, 'from')),
);
for (const name of polyfills) {
map.set(
`es-iterator-helpers/Iterator.prototype.${name}`,
replace(t => requirePolyfill(t, name)),
);
}
map.set(
'safe-regex-test',
replace(t => requirePolyfill(t, 'safeRegexTest')),
);
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
referencePath.replaceWith(getReplacement(t));
});
};
}
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
const { parent, parentPath } = referencePath;
assert(t.isCallExpression(parent));
const [callee, ...rest] = parent.arguments;
parentPath!.replaceWith(
t.callExpression(
t.memberExpression(callee as t.Expression, getProperty(t)),
rest,
),
);
});
};
}
export const babelPlugin: Plugin = {
name: 'babel',
setup(build) {
const { keys, ...macroOptions } = map;
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
const { path } = args;
if (path.includes('node_modules/')) {
return null;
}
let source = readFileSync(path, 'utf-8')
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries')
.replaceAll(
"Object.keys(require('prop-types'))",
JSON.stringify(Object.keys(require('prop-types'))),
);
if (
path.includes('packages/eslint-plugin-import/src/rules/') ||
path.includes('packages/eslint-plugin-import/config/')
) {
source = source.replace('\nmodule.exports = {', '\nexport default {');
}
const isFlow = source.includes('@flow');
const loader = extname(path).slice(1) as Loader;
if (!isFlow && !keys.some(key => source.includes(key))) {
return { contents: source, loader };
}
const res = babel.transformSync(source, {
filename: path,
babelrc: false,
configFile: false,
parserOpts: {
plugins: [isFlow ? 'flow' : 'typescript'],
},
plugins: [
isFlow && '@babel/plugin-transform-flow-strip-types',
['babel-plugin-macros', macroOptions],
].filter(Boolean),
})!;
return {
contents: res.code!,
loader,
};
});
},
};

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> = {