Skip to content

@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.

javascript
// 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 ​

OptionTypePurpose
tsconfigRootDirstringAbsolute path to the directory holding tsconfig.json. Required.
ignoresstring[]Glob patterns to ignore on top of the defaults.
localRulesPluginunknownA project-owned rules plugin, merged over the shared rules in the same namespace.
localRulesRecord<string, unknown>Rule entries to enable. A registered rule stays off until it appears here.
overridesunknown[]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.

javascript
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 idRecommendedWhat it enforces
local-rules/artifact-validator-syncerrorAn artifact codec's validate or judge judges synchronously, in its own terms.
local-rules/cascade-delete-orderwarnCascade deletes must not run through Promise.all -- ordering matters.
local-rules/enforce-powertoolserrorLambda handlers go through the observability wrapper.
local-rules/env-validationerrorNo direct process.env; use getRequiredEnv() from @j0nathan-ll0yd/env.
local-rules/import-orderwarnHandler imports follow the framework's category order.
local-rules/migrations-safetyerrorSchema changes live in migration and schema files only.
local-rules/no-uppercase-header-accesserrorNo uppercase bracket access on event.headers -- HTTP/2 lowercases keys (C90).
local-rules/response-helperswarnHandlers return through the response helpers, not raw response objects.
local-rules/spacing-conventionswarnLogical spacing inside function bodies.
local-rules/strict-env-varserrorEnvironment 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.