@j0nathan-ll0yd/eslint-config
The shared ESLint flat config for Mantle projects, plus the nine Mantle-specific rules it registers.
Conventions that a linter can check belong in a linter, not in a prose document. This package is that tier: it ships the base rule set every Mantle repo runs and the custom rules that encode framework-specific invariants.
createMantleEslintConfig(options)
Build the flat config array. Call it from the project's eslint.config.mjs and export the result.
// eslint.config.mjs
import { createMantleEslintConfig } from "@j0nathan-ll0yd/eslint-config";
export default createMantleEslintConfig({
tsconfigRootDir: import.meta.dirname,
localRules: {
"local-rules/enforce-powertools": "error",
"local-rules/strict-env-vars": "error",
},
});MantleEslintConfigOptions
| Option | Type | Purpose |
|---|---|---|
tsconfigRootDir | string | Absolute path to the directory holding tsconfig.json. Required. |
ignores | string[] | Glob patterns to ignore on top of the defaults. |
localRulesPlugin | unknown | A project-owned rules plugin, merged over the shared rules in the same namespace. |
localRules | Record<string, unknown> | Rule entries to enable. A registered rule stays off until it appears here. |
overrides | unknown[] | Extra flat-config entries appended after every built-in entry. |
Default ignores cover node_modules, dist, build, .turbo, coverage, eslint.config.mjs, and eslint-local-rules/**.
The config composes @eslint/js, typescript-eslint, eslint-plugin-unicorn, eslint-plugin-security, eslint-plugin-drizzle, eslint-plugin-tsdoc, and eslint-plugin-jsdoc. The unicorn rules are cherry-picked rather than taken as unicorn/recommended; each active rule carries a comment and a ratchet-ledger entry in packages/eslint-config/src/index.ts.
Subpath: @j0nathan-ll0yd/eslint-config/local-rules
The rule plugin on its own, for a project that wants to compose its config by hand.
import mantlePlugin from "@j0nathan-ll0yd/eslint-config/local-rules";
export default [mantlePlugin.configs.recommended];Also exported from this subpath: PLUGIN_NAMESPACE (the string local-rules, the prefix every rule id carries), and the import-order helpers categorizeImport, frameworkImportPattern, getCategoryIndex, and DEFAULT_FRAMEWORK_PACKAGES.
Rules
| Rule id | Recommended | What it enforces |
|---|---|---|
local-rules/artifact-validator-sync | error | An artifact codec's validate or judge judges synchronously, in its own terms. |
local-rules/cascade-delete-order | warn | Cascade deletes must not run through Promise.all -- ordering matters. |
local-rules/enforce-powertools | error | Lambda handlers go through the observability wrapper. |
local-rules/env-validation | error | No direct process.env; use getRequiredEnv() from @j0nathan-ll0yd/env. |
local-rules/import-order | warn | Handler imports follow the framework's category order. |
local-rules/migrations-safety | error | Schema changes live in migration and schema files only. |
local-rules/no-uppercase-header-access | error | No uppercase bracket access on event.headers -- HTTP/2 lowercases keys (C90). |
local-rules/response-helpers | warn | Handlers return through the response helpers, not raw response objects. |
local-rules/spacing-conventions | warn | Logical spacing inside function bodies. |
local-rules/strict-env-vars | error | Environment access is centralized and statically analyzable. |
Registration alone changes nothing: a flat-config rule is inert until a localRules entry turns it on. The recommended config is the one place the severities above are applied.
local-rules/artifact-validator-sync is the one exception -- createMantleEslintConfig() turns it on at error for every consumer, and a project localRules entry can still override it. It is on by default because it closes the one publication-boundary hole that cannot be closed at runtime: void check(body).catch(logError) inside a validator returns undefined, exactly as a conformant validator does, so prepareArtifact cannot separate the two. It is also lint-clean under @typescript-eslint/no-floating-promises, because void and .catch() are how C96 prescribes marking a promise as deliberately unawaited -- which is right everywhere except inside a validator, where deliberately unawaited means deliberately unjudged. A rule a project has to remember to enable would leave that open by default in every new repo.
It fires only inside a codec validator body -- jsonCodec(fn), textCodec(fn), or a validate property on an object literal that also carries encode -- and never inside a nested function, so a predicate handed to every/some is untouched. It is off in test files, where constructing a bad validator is the evidence. A project with no artifacts sees nothing.
Related
mantle check-- the AST rules that run outside ESLint.- Style Guide -- the conventions behind these rules.