Opinionated linting and formatting configuration for TypeScript, JavaScript, CSS, and Python projects, tuned for Web Components (vanilla and Lit) and hybrid Python/JavaScript repos.
Pedantry is one repo with one version, delivered two ways:
- The
pedantryCLI (Python, run withuvx) writes the files no tool can share:.editorconfig,lefthook.yml, a.prettierignorebaseline, the ruff/ty/vulture/pytest block inpyproject.toml, and the shared agent rules. - The
@dryan/pedantrynpm package carries the configs that tools import natively: ESLint, TypeScript, Prettier, Stylelint, and helpers for Vitest, Rollup,@web/test-runnerand the Custom Elements Manifest.
Every repo pins a pedantry version. Upgrading is one edit plus pedantry sync, and pedantry check fails CI when a repo drifts.
| Area | Tools |
|---|---|
| JavaScript/TypeScript | ESLint (flat config, typescript-eslint, JSON/JSONC), Prettier (defaults only), strict tsconfig |
| CSS | Stylelint (standard + recess order), Shadow DOM aware (:host, ::slotted, ::part) |
| Python | Ruff (lint and format), ty, Vulture, pytest, django-upgrade (Django only) |
| Git hooks | Lefthook, running the tools above on staged files |
| Editor | EditorConfig |
| Agents | Shared rules in .pedantry/AGENTS.md |
You need uv (every Mac and CI runner here has it). Run the CLI straight from GitHub at a tag:
uvx --from git+https://github.com/dryan-org/pedantry@v2.0.3 pedantry sync --type python --type typescript--type is one or more of python, typescript, javascript, css, django (Django implies Python, JavaScript and CSS). The first sync also records the pin, so later runs need no options.
For a repo with a package.json, add the npm package (it is not published to npmjs.org; it installs from GitHub like any github: dependency):
npm install --save-dev github:dryan-org/pedantry#v2.0.3
npm install --save-dev eslint@^10 prettier typescript # whichever tools the repo usesThen enable the hooks and commit:
lefthook installFor Python tools the hooks call uv run, so the repo needs them as dev dependencies:
uv add --dev ruff vulture ty pytest # plus django-upgrade for Djangopedantry sync [--type T ...] [--vendor] [--upgrade] [--dry-run] [--force] [--json] [--path DIR]
pedantry check [--diff] [--json] [--path DIR]
pedantry migrate [--type T ...] [--vendor] [--skip-npm] [--dry-run] [--json] [--path DIR]
pedantry version
Writes the managed files for the pinned version and types. Run it again and nothing changes.
| File | How it is managed |
|---|---|
lefthook.yml |
Whole file, stamped. Python tools via uv run; prettier, eslint and stylelint via npx. |
.pedantry/AGENTS.md |
Whole file, stamped. The shared agent rules. |
.editorconfig |
Block between # >>> pedantry and # <<< pedantry. Anything outside the markers is yours. |
.prettierignore |
Same marked block. |
pyproject.toml |
Same marked block with ruff, ty, vulture, pytest (and django-upgrade). Python and Django only. |
CLAUDE.md |
Created as @AGENTS.md when the repo has an AGENTS.md and no CLAUDE.md; never touched after. |
pedantry.toml (no pyproject) |
The pin only. |
.pedantry/* with --vendor |
Local copies of the JS configs, see below. |
The Python block fits the repo instead of assuming a layout: ruff target-version and ty python-version come from the lower bound of [project] requires-python (3.13 when it says nothing), ty include is src if that exists, else the top-level package directories, else ., and pytest testpaths is tests or test if one exists (omitted otherwise). Rerun sync after adding such a directory.
Stamped files start with pedantry X.Y.Z: managed by pedantry sync, do not edit.
The eslint, stylelint and vulture jobs are written only when the repo has the tool: an ESLint or Stylelint config file (or eslintConfig / stylelint key in package.json), and for vulture a dependency (dependency-groups, optional dependencies or [project] dependencies) or a [tool.vulture] table outside the managed block. check applies the same rule, so adding a config later makes check ask for a sync.
sync --upgrade also moves the @dryan/pedantry npm pin in package.json to the new tag (npm install --save-dev github:dryan-org/pedantry#vX.Y.Z) when it lists the package; without npm it prints the command instead. In the same call it raises eslint, @eslint/js and stylelint when package.json lists them below the major pedantry's configs need (10, 10 and 17), so the lockfile also installs under npm 10; the bump is reported in the output (npm.bumped with --json).
With TypeScript 7 or later in package.json, the lefthook eslint job covers *.{js,json} only and says why; see "TypeScript 7" below.
Safeguards: sync refuses to overwrite a lefthook.yml (or other whole-file target) that has no stamp, and it refuses to write a pyproject.toml that would define a managed table twice. In both cases it points you to pedantry migrate. --force overwrites anyway. --dry-run prints diffs and writes nothing.
Exits 1 and lists the files that differ from what sync would write for the pinned version (--diff shows how). Edits outside the markers never count as drift. Run it in CI:
- run: uvx --from git+https://github.com/dryan-org/pedantry@v2.0.3 pedantry checkMoves a repo that uses the old .pedantry git submodule and copied files. It never commits.
- Removes the submodule (
.gitmodules, the index entry, the checkout), and replaces symlinks into it. - With a
package.jsonthat uses JS or CSS tooling, or already lists@dryan/pedantry, runsnpm install --save-dev github:dryan-org/pedantry#vX.Y.Z(skip with--skip-npm). The same call raiseseslint,@eslint/jsandstylelintthat the repo lists below the required major (10, 10 and 17), and the report lists what was raised. - Rewrites the
eslint.config.*import to@dryan/pedantry/eslint, pointstsconfig.jsonat@dryan/pedantry/tsconfig(dropping options identical to the base), drops.pedantryfrom ignores and excludes, and moves an empty.prettierrc.json5to"prettier": "@dryan/pedantry/prettier". - Writes the pin and runs sync. It also creates
CLAUDE.md(@AGENTS.md) when missing, deletes an empty.prettierrc.json5in a repo with nopackage.json, and keeps the repo's.editorconfigheader comments above the managed block. Tables it drops frompyproject.tomltake their attached comments with them, and the managed block goes where the first dropped table was, so the rest keep their order. - Stages what it wrote (the gitlink and
.gitmodulesremoval first, then the new files, except paths your.gitignoreexcludes), sogit add -Ais not needed. Hooks for tools the repo lacks (no ESLint config, no vulture dependency) are left out oflefthook.ymland listed in the report. - Initializes the old submodule first (
git submodule update --init .pedantry) if it is not checked out, so the result does not depend on checkout state. Without network it says so and carries on. - Prints a report: what it replaced (identical to pedantry, or a stale copy of an older pedantry), what it preserved (per-repo edits), and what needs a human.
If npm install fails (typically because the vX.Y.Z tag does not exist yet), migrate and sync --upgrade say so, show npm's last lines, and carry on: every file is still written, and the report tells you the command to run again. A repo with no Python, TypeScript, JavaScript or CSS tooling files (an Xcode project, say) is pinned with types = [], so only the editor, Prettier, hook and agent files are managed.
Other things migrate does so the result stays working:
- Drops
--config .pedantry/stylelint.config.mjsfrompackage.jsonscripts (Stylelint findsstylelint.config.*itself) and writes a one-linestylelint.config.mjswhen the repo has none. - Removes a
.pedantry/lefthook.ymlentry from anextends:list, and a trailing comment after a.pedantryignore entry. An ESLintignoresarray that fits on one line after the removal is collapsed, as Prettier would. - Leaves
package.jsonwithout a"prettier"key when the repo has its own Prettier config file (.prettierrc.json,prettier.config.js, ...): Prettier reads that key first, so adding it would replace the repo's options. - Keeps tsc as strict as before: an old copy of the tsconfig that lacked
strict,noUnusedLocals,noUnusedParameters,noImplicitReturns,noFallthroughCasesInSwitch,noUncheckedIndexedAccessornoPropertyAccessFromIndexSignaturegets that option set tofalse(reported under preserved; delete the line to adopt pedantry's setting). - Lists every other file that still names a
.pedantry/<file>that no longer exists (a dynamic ESLint import, docs, scripts) with the replacement to use.
The report also flags: a repo that never had [tool.ruff] (the managed ruleset is new; run ruff check --statistics), AGENTS.md lines that still say "submodule", dangling symlinks into the old submodule (removed, nothing to restore), TypeScript 7, and symlinks that became regular files. Git sees that last change as a typechange, and infra gh commit before infra-core 1.2.2 keeps mode 120000 for it, so commit in two steps (delete the symlinks, then add the files); 1.2.2 and later handles it in one.
What "preserved" means: hooks that differ from pedantry's move to lefthook-local.yml; editor and ignore rules go after the managed block; a pyproject.toml table you changed stays where it is and is listed under keep in [tool.pedantry]; tsconfig options that differ stay in the repo's tsconfig.json. Use --dry-run to see the plan first.
| To change | Edit |
|---|---|
| Hooks | lefthook-local.yml (lefthook merges it in) |
| Editor or prettier ignore rules | Below # <<< pedantry |
| ruff, ty, vulture, pytest | Outside the marked block. To own a whole managed table, add it to keep in [tool.pedantry]. |
| ESLint, TypeScript, Stylelint | The repo's own config, which imports or extends @dryan/pedantry/... |
| Agent rules | The repo's own AGENTS.md, which keeps the line @.pedantry/AGENTS.md |
Never edit a managed file by hand. pedantry check will fail, and the next sync overwrites it.
Tags are vX.Y.Z and cover both the npm package and the CLI. A repo's pin lives in one place:
# pyproject.toml, or pedantry.toml (without the table header) in repos with no pyproject
[tool.pedantry]
version = "2.0.3"
types = ["python", "typescript"]sync and check refuse to run when the running pedantry is not the pinned version, and print the exact uvx --from ...@vX.Y.Z command to use. To upgrade, run the new version with --upgrade:
uvx --from git+https://github.com/dryan-org/pedantry@v2.1.0 pedantry sync --upgradeand bump the npm dependency (npm install --save-dev github:dryan-org/pedantry#v2.1.0).
Peer dependencies: eslint ^10 (required: @eslint/js 10 needs it), plus optional stylelint ^17 (what stylelint-config-standard needs). Keep the repo's own eslint and @eslint/js at ^10, or its lockfile will not install under npm 10.
// eslint.config.ts
import type { Linter } from "eslint";
import pedantry from "@dryan/pedantry/eslint";
const config: Linter.Config[] = [
...pedantry,
// repo overrides
];
export default config;// package.json
{ "prettier": "@dryan/pedantry/prettier" }The Prettier export works for ESM and CommonJS repos alike (require("@dryan/pedantry/prettier") gets a plain config object).
// stylelint.config.mjs
export default { extends: ["@dryan/pedantry/stylelint"] };Helpers take overrides (plain objects merge into the defaults, arrays replace them):
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { vitestConfig } from "@dryan/pedantry/vitest";
export default defineConfig(vitestConfig({ test: { environment: "jsdom" } }));// rollup.config.js
import { rollupConfig } from "@dryan/pedantry/rollup";
export default rollupConfig({ input: "src/my-element.ts" });
// web-test-runner.config.js
import { webTestRunnerConfig } from "@dryan/pedantry/web-test-runner";
export default await webTestRunnerConfig({ files: ["test/**/*.test.js"] });
// custom-elements-manifest.config.js
import { customElementsManifestConfig } from "@dryan/pedantry/custom-elements-manifest";
export default customElementsManifestConfig();The ESLint, Prettier and Stylelint configs and their plugins come with the package. The tools themselves (eslint, prettier, stylelint, typescript, vitest, rollup and its plugins, @web/test-runner) are the repo's own dev dependencies.
typescript-eslint reads the TypeScript JS API, which TypeScript 7 (the native port) does not export, and no typescript-eslint release supports it yet (its peer range stops below 6.1). When @dryan/pedantry/eslint finds TypeScript 7 or later installed, it does not load typescript-eslint, ignores .ts, .tsx, .mts and .cts files (nothing can parse them), still lints JavaScript and JSON, and prints a one-line warning. Type-check with tsc instead. Once typescript-eslint supports TypeScript 7 the guard goes away.
For repos that cannot install from GitHub (for example Azure DevOps), sync writes everything locally:
uvx --from git+https://github.com/dryan-org/pedantry@v2.0.3 pedantry sync --type typescript --vendorThis adds .pedantry/eslint.config.ts, .pedantry/tsconfig.base.json, .pedantry/stylelint.config.mjs and .pedantry/helpers/*.js, and records vendor = true in the pin so check covers them. The repo's own configs import them (import pedantry from "./.pedantry/eslint.config", "extends": "./.pedantry/tsconfig.base.json"), and sync prints the npm install --save-dev line for the packages they import. Updates are manual: run sync again with a newer version.
uv sync
uv run pytest # CLI tests: sync, check, migrate on fixture repos
npm ci --ignore-scripts
npm test # packs the npm package, installs it into a fixture, runs eslint/tsc/prettier/stylelint
uv run pedantry check # this repo follows its own pinThe shared agent rules are src/pedantry/data/AGENTS.md; the npm files are in src/pedantry/data/js/. See AGENTS.md for the release steps.
ISC