Skip to content
dryan-orgPublic

About

Opinionated linting and formatting configurations for TypeScript, JavaScript, CSS, HTML, and Python projects

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Pedantry

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 pedantry CLI (Python, run with uvx) writes the files no tool can share: .editorconfig, lefthook.yml, a .prettierignore baseline, the ruff/ty/vulture/pytest block in pyproject.toml, and the shared agent rules.
  • The @dryan/pedantry npm package carries the configs that tools import natively: ESLint, TypeScript, Prettier, Stylelint, and helpers for Vitest, Rollup, @web/test-runner and 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.

What it enforces

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

Set up a repo

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 uses

Then enable the hooks and commit:

lefthook install

For 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 Django

The CLI

pedantry 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

sync

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.

check

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 check

migrate

Moves a repo that uses the old .pedantry git submodule and copied files. It never commits.

  1. Removes the submodule (.gitmodules, the index entry, the checkout), and replaces symlinks into it.
  2. With a package.json that uses JS or CSS tooling, or already lists @dryan/pedantry, runs npm install --save-dev github:dryan-org/pedantry#vX.Y.Z (skip with --skip-npm). The same call raises eslint, @eslint/js and stylelint that the repo lists below the required major (10, 10 and 17), and the report lists what was raised.
  3. Rewrites the eslint.config.* import to @dryan/pedantry/eslint, points tsconfig.json at @dryan/pedantry/tsconfig (dropping options identical to the base), drops .pedantry from ignores and excludes, and moves an empty .prettierrc.json5 to "prettier": "@dryan/pedantry/prettier".
  4. Writes the pin and runs sync. It also creates CLAUDE.md (@AGENTS.md) when missing, deletes an empty .prettierrc.json5 in a repo with no package.json, and keeps the repo's .editorconfig header comments above the managed block. Tables it drops from pyproject.toml take their attached comments with them, and the managed block goes where the first dropped table was, so the rest keep their order.
  5. Stages what it wrote (the gitlink and .gitmodules removal first, then the new files, except paths your .gitignore excludes), so git add -A is not needed. Hooks for tools the repo lacks (no ESLint config, no vulture dependency) are left out of lefthook.yml and listed in the report.
  6. 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.
  7. 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.mjs from package.json scripts (Stylelint finds stylelint.config.* itself) and writes a one-line stylelint.config.mjs when the repo has none.
  • Removes a .pedantry/lefthook.yml entry from an extends: list, and a trailing comment after a .pedantry ignore entry. An ESLint ignores array that fits on one line after the removal is collapsed, as Prettier would.
  • Leaves package.json without 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, noUncheckedIndexedAccess or noPropertyAccessFromIndexSignature gets that option set to false (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.

Repo-specific changes

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.

Versioning

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 --upgrade

and bump the npm dependency (npm install --save-dev github:dryan-org/pedantry#v2.1.0).

The npm package

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;
// tsconfig.json: only compilerOptions are shared. paths, include, exclude and
// outDir stay in the repo, because TypeScript resolves them relative to the file they are in.
{
  "extends": "@dryan/pedantry/tsconfig",
  "compilerOptions": { "outDir": "dist" },
}
// 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 7

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.

Vendoring (repos without GitHub access)

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 --vendor

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

Development

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 pin

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

License

ISC

About

Opinionated linting and formatting configurations for TypeScript, JavaScript, CSS, HTML, and Python projects

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages