UNPKG

eslint-plugin-repo-lint

Version:

Drop-in ESLint plugin that loads TypeScript rule files from a conventional .lints/ directory without a build step.

132 lines (97 loc) 3.22 kB
# eslint-plugin-repo-lint Add project-specific ESLint rules to a .lints/ folder. No build step, no plugin scaffolding. Great for cases [ast-grep](https://github.com/ast-grep/ast-grep) can't express. ## Install ```sh pnpm install --save-dev eslint-plugin-repo-lint ``` If you're writing rules in TypeScript, also install `@typescript-eslint/utils`: ```sh pnpm install --save-dev @typescript-eslint/utils ``` ## Use Flat config (ESLint 9+): ```js // eslint.config.js const repoLint = require("eslint-plugin-repo-lint"); module.exports = [repoLint.configs["flat/all"]]; ``` Or `eslint.config.mjs`: ```js import repoLint from "eslint-plugin-repo-lint"; export default [repoLint.configs["flat/all"]]; ``` Opt in per rule instead of `flat/all`: ```js module.exports = [ { plugins: { "repo-lint": require("eslint-plugin-repo-lint") }, rules: { // requirement: you have a `.lints/sentry-tags-snake-case.{js,ts}` file "repo-lint/sentry-tags-snake-case": "error", }, }, ]; ``` Legacy eslintrc (ESLint 8): ```yaml # .eslintrc.yml extends: ["plugin:repo-lint/all"] ``` ## Write a rule Drop a TypeScript file in `.lints/`. The filename becomes the rule name. ```ts // .lints/sentry-tags-snake-case.ts import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; const rule: TSESLint.RuleModule<"snakeCase", []> = { meta: { type: "problem", fixable: "code", schema: [], messages: { snakeCase: "Tag '{{key}}' should be snake_case (suggested: '{{fixed}}').", }, }, defaultOptions: [], create(context) { return { CallExpression(node: TSESTree.CallExpression) { // ...rule logic... }, }; }, }; export default rule; ``` `.lints/sentry-tags-snake-case.ts` → `repo-lint/sentry-tags-snake-case`. Plain JS files (`.js`) work too — they skip the transpile step. ## Test a rule Co-locate the test as `<rule>.test.ts` next to the rule. The loader ignores `.test.` and `.spec.` files, so it won't try to register the test as a rule. ```ts // vitest.config.ts import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true }, }); ``` ```ts // .lints/sentry-tags-snake-case.test.ts import { RuleTester } from "@typescript-eslint/rule-tester"; import rule from "./sentry-tags-snake-case"; new RuleTester().run("sentry-tags-snake-case", rule, { valid: ["Sentry.setTag('user_id', '42');"], invalid: [ { code: "Sentry.setTag('userId', '42');", errors: [{ messageId: "snakeCase" }], }, ], }); ``` Run: ```sh pnpx vitest run .lints ``` ## Notes 1. ESLint editor extensions typically cache loaded plugin modules. After adding or editing a rule, you might need to restart the ESLint server for your updated rule to get picked up by your editor. 2. Rules written in TypeScript are not type-checked. Run `tsc --noEmit` against your `.lints/` folder separately if you want to check types. 3. The rule loader anchors on the first `eslint.config.*` (flat config) or `.eslintrc.*` with `root: true` it finds walking up from ESLint's working directory. The walk-up logic stops when it encounters a `.git` directory to try prevent breaking out of a repo boundary.