UNPKG

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
"use strict"; 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