eslint-plugin-repo-lint
v0.1.0
Published
Drop-in ESLint plugin that loads TypeScript rule files from a conventional .lints/ directory without a build step.
Maintainers
Readme
eslint-plugin-repo-lint
Add project-specific ESLint rules to a .lints/ folder. No build step, no plugin scaffolding. Great for cases ast-grep can't express.
Install
pnpm install --save-dev eslint-plugin-repo-lintIf you're writing rules in TypeScript, also install @typescript-eslint/utils:
pnpm install --save-dev @typescript-eslint/utilsUse
Flat config (ESLint 9+):
// eslint.config.js
const repoLint = require("eslint-plugin-repo-lint");
module.exports = [repoLint.configs["flat/all"]];Or eslint.config.mjs:
import repoLint from "eslint-plugin-repo-lint";
export default [repoLint.configs["flat/all"]];Opt in per rule instead of flat/all:
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):
# .eslintrc.yml
extends: ["plugin:repo-lint/all"]Write a rule
Drop a TypeScript file in .lints/. The filename becomes the rule name.
// .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.
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { globals: true },
});// .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:
pnpx vitest run .lintsNotes
- 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.
- Rules written in TypeScript are not type-checked. Run
tsc --noEmitagainst your.lints/folder separately if you want to check types. - The rule loader anchors on the first
eslint.config.*(flat config) or.eslintrc.*withroot: trueit finds walking up from ESLint's working directory. The walk-up logic stops when it encounters a.gitdirectory to try prevent breaking out of a repo boundary.
