From fb50ede688f76f54876aaa272eeb38a9f3101d9d Mon Sep 17 00:00:00 2001 From: Alex <8125011+alex-kinokon@users.noreply.github.com> Date: Wed, 17 Apr 2024 00:43:45 -0400 Subject: [PATCH] Update --- dist/index.d.ts | 30 +++--- dist/package.json | 4 +- dist/prettier.d.ts | 2 +- dist/types.d.ts | 18 ++-- package.json | 4 + pnpm-lock.yaml | 30 ++++++ scripts/build.ts | 234 +++---------------------------------------- scripts/dts.ts | 36 +++++++ scripts/modifier.ts | 214 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 50 +++++---- src/local/index.ts | 74 ++++++++++---- src/presets/local.ts | 2 +- 12 files changed, 401 insertions(+), 297 deletions(-) create mode 100644 scripts/dts.ts create mode 100644 scripts/modifier.ts diff --git a/dist/index.d.ts b/dist/index.d.ts index be69361..2b43fe1 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -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; } export type RuleOptions = Rules & Partial; +export interface CustomRule { + rule: () => Promise<{ + default: Rule.RuleModule | ESLintUtils.RuleModule; + }>; + options?: RuleLevel; +} /** * ESLint Configuration. * @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/) */ -export type Config = Omit & { +export type InputConfig = Omit & { /** * 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; - }>; - options?: RuleLevel; - }[]; -}; -export declare function defineCustomRule(rule: () => Promise<{ - default: Rule.RuleModule | ESLintUtils.RuleModule; -}>, options?: Options): { - rule: () => Promise<{ - default: Rule.RuleModule | ESLintUtils.RuleModule; - }>; - options: Options | undefined; + customRuleFiles?: string | string[]; }; /** * Returns a ESLint config object. @@ -71,6 +67,6 @@ export declare function defineCustomRule(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 {}; diff --git a/dist/package.json b/dist/package.json index b166ac0..df3c0b8 100644 --- a/dist/package.json +++ b/dist/package.json @@ -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", diff --git a/dist/prettier.d.ts b/dist/prettier.d.ts index d8fe1af..5300625 100644 --- a/dist/prettier.d.ts +++ b/dist/prettier.d.ts @@ -1,4 +1,4 @@ -// Generated by dts-bundle-generator v9.3.1 +// Generated by dts-bundle-generator v9.4.0 import { Config } from 'prettier'; diff --git a/dist/types.d.ts b/dist/types.d.ts index a3eb703..39fbbc2 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -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; + [ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule; }): { - [ruleName: string]: - | Rule.RuleModule - | ESLintUtils.RuleModule; + [ruleName: string]: Rule.RuleModule | ESLintUtils.RuleModule; }; -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 {}; diff --git a/package.json b/package.json index c8e67e0..bea5276 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25c4cf3..a252a9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/scripts/build.ts b/scripts/build.ts index 94562f8..b227deb 100755 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -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(); - - 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 { 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() { diff --git a/scripts/dts.ts b/scripts/dts.ts new file mode 100644 index 0000000..ffaece7 --- /dev/null +++ b/scripts/dts.ts @@ -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]); +} diff --git a/scripts/modifier.ts b/scripts/modifier.ts new file mode 100644 index 0000000..3848fd0 --- /dev/null +++ b/scripts/modifier.ts @@ -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(); + + 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, + }; + }); + }, +}; diff --git a/src/index.ts b/src/index.ts index 79803be..319723e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,11 +56,18 @@ export interface LocalRuleOptions { export type RuleOptions = Rules & Partial; +export interface CustomRule { + rule: () => Promise<{ + default: Rule.RuleModule | ESLintUtils.RuleModule; + }>; + options?: RuleLevel; +} + /** * ESLint Configuration. * @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/) */ -type Config = Omit & { +export type InputConfig = Omit & { /** * Rules. * @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules) @@ -68,24 +75,13 @@ type Config = Omit & { 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; - }>; - options?: RuleLevel; - }[]; + customRuleFiles?: string | string[]; }; -export function defineCustomRule( - rule: () => Promise<{ - default: Rule.RuleModule | ESLintUtils.RuleModule; - }>, - options?: Options, -) { - return { rule, options }; -} - /** * Returns a ESLint config object. * @@ -104,15 +100,17 @@ export function defineCustomRule( * 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), diff --git a/src/local/index.ts b/src/local/index.ts index 4feb198..c887d1b 100644 --- a/src/local/index.ts +++ b/src/local/index.ts @@ -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 = (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; diff --git a/src/presets/local.ts b/src/presets/local.ts index 34efdf0..93a72b0 100644 --- a/src/presets/local.ts +++ b/src/presets/local.ts @@ -1,4 +1,4 @@ -import type { LocalRuleOptions } from '../index'; +import type { LocalRuleOptions } from '..'; import { error } from '../constants'; export const localRules: Partial = {