eslint-plugin-repo-lint
Version:
Drop-in ESLint plugin that loads TypeScript rule files from a conventional .lints/ directory without a build step.
221 lines • 7.29 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
const fs = __importStar(require("node:fs"));
const node_module_1 = require("node:module");
const path = __importStar(require("node:path"));
const sucrase_1 = require("sucrase");
const pkg = require("../package.json");
const LINTS_DIR_NAME = process.env.REPO_LINT_DIR || ".lints";
const RULE_FILE_RE = /\.(ts|js)$/;
const TEST_FILE_RE = /\.(test|spec)\.(ts|js)$/;
const INDEX_FILE_RE = /^index\.(ts|js)$/;
const FLAT_CONFIG_FILES = [
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.mts",
"eslint.config.cts",
];
const ESLINTRC_FILES = [
".eslintrc.json",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.js",
".eslintrc.cjs",
".eslintrc.mjs",
".eslintrc",
];
const ESLINTRC_ROOT_RE = /\broot\s*:\s*true\b/;
function isDirectory(p) {
return fs.statSync(p, { throwIfNoEntry: false })?.isDirectory() ?? false;
}
function exists(p) {
return !!fs.statSync(p, { throwIfNoEntry: false });
}
function hasFlatConfig(dir) {
for (const name of FLAT_CONFIG_FILES) {
if (exists(path.join(dir, name))) {
return true;
}
}
return false;
}
// Best-effort detection of `root: true` in legacy eslintrc files. JSON gets a
// real parse; YAML/JS/no-extension fall back to a substring check — good
// enough for the common case (a literal `root: true` line). A computed
// `root: someBool` in JS won't be detected, which is documented in the README.
function eslintrcFileHasRoot(filePath) {
let source;
try {
source = fs.readFileSync(filePath, "utf8");
}
catch {
return false;
}
if (filePath.endsWith(".json")) {
try {
const parsed = JSON.parse(source);
return parsed?.root === true;
}
catch {
return false;
}
}
return ESLINTRC_ROOT_RE.test(source);
}
function hasRootEslintrc(dir) {
for (const name of ESLINTRC_FILES) {
const p = path.join(dir, name);
if (exists(p) && eslintrcFileHasRoot(p)) {
return true;
}
}
return false;
}
/**
* Walk up from cwd looking for an ESLint config anchor.
*
* If the anchor's sibling `.lints/` exists, use it. Otherwise, we fall back to
* the outermost `.lints/` we saw on the way up, so a nested `monorepo/typescript/.lints/`
* still gets discovered when the anchor doesn't have an accompanying `.lints/` directory.
*/
function findLintsDir(start) {
let dir = start;
let outermostLints = null;
while (true) {
const candidate = path.join(dir, LINTS_DIR_NAME);
if (isDirectory(candidate)) {
outermostLints = candidate;
}
if (hasFlatConfig(dir) || hasRootEslintrc(dir)) {
return isDirectory(candidate) ? candidate : outermostLints;
}
if (exists(path.join(dir, ".git"))) {
break;
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
return outermostLints;
}
function isRuleModule(value) {
return (typeof value === "object" &&
value !== null &&
typeof value.create === "function");
}
/**
* Load a rule file without touching `require.extensions` or installing a global
* `.ts` hook. JS files go through plain `require()`; TS files are transformed
* in-memory by sucrase and evaluated in a fresh `Module` instance.
*/
function requireRuleFile(filename) {
if (filename.endsWith(".js")) {
return require(filename);
}
const source = fs.readFileSync(filename, "utf8");
const { code } = (0, sucrase_1.transform)(source, {
transforms: ["typescript", "imports"],
filePath: filename,
});
const m = new node_module_1.Module(filename, module);
m.filename = filename;
m.paths = node_module_1.Module._nodeModulePaths(path.dirname(filename));
m._compile(code, filename);
return m.exports;
}
function loadRules(dir) {
const rules = {};
const seen = {};
for (const file of fs.readdirSync(dir).toSorted()) {
if (!RULE_FILE_RE.test(file)) {
continue;
}
if (TEST_FILE_RE.test(file)) {
continue;
}
if (INDEX_FILE_RE.test(file)) {
continue;
}
if (file.startsWith("_")) {
continue;
}
if (file.startsWith(".")) {
continue;
}
const full = path.join(dir, file);
if (!fs.statSync(full).isFile()) {
continue;
}
const name = file.replace(RULE_FILE_RE, "");
if (seen[name]) {
throw new Error(`eslint-plugin-repo-lint: rule name '${name}' is defined by both ${seen[name]} and ${file}. ` +
`Pick one extension or rename one of the files.`);
}
seen[name] = file;
const mod = requireRuleFile(full);
const candidate = mod && typeof mod === "object" && "default" in mod
? mod.default
: mod;
if (!isRuleModule(candidate)) {
throw new Error(`eslint-plugin-repo-lint: ${full} must default-export a RuleModule (got ${typeof candidate}). ` +
`Expected an object with a 'create' function.`);
}
rules[name] = candidate;
}
return rules;
}
const lintsDir = findLintsDir(process.cwd());
const rules = lintsDir ? loadRules(lintsDir) : {};
const allRulesEnabled = Object.fromEntries(Object.keys(rules).map((r) => [`repo-lint/${r}`, "error"]));
const plugin = {
meta: { name: "repo-lint", version: pkg.version },
rules,
configs: {},
};
plugin.configs.all = {
plugins: ["repo-lint"],
rules: allRulesEnabled,
};
plugin.configs["flat/all"] = {
name: "repo-lint/all",
plugins: { "repo-lint": plugin },
rules: allRulesEnabled,
};
module.exports = plugin;
//# sourceMappingURL=index.js.map