Skip to content

chore(dev): add opt-in Prettier pre-commit hook and editor format-on-save#355

Open
pjdoland wants to merge 2 commits into
plmbr:mainfrom
pjdoland:chore/dev-formatting-guardrails
Open

chore(dev): add opt-in Prettier pre-commit hook and editor format-on-save#355
pjdoland wants to merge 2 commits into
plmbr:mainfrom
pjdoland:chore/dev-formatting-guardrails

Conversation

@pjdoland
Copy link
Copy Markdown
Collaborator

@pjdoland pjdoland commented Jun 1, 2026

Summary

Contributor PRs land red on CI fairly often, and the failure is almost always the same shape: a single Markdown, TypeScript, or CSS file that was edited without running jlpm lint first. jlpm lint:check gates Prettier across the whole tree (**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}), so one unformatted file is enough to turn the run red even when the actual change is fine.

This adds two small, independent, opt-in layers that move that formatting fix from CI back to the moment of editing or committing. Neither changes what CI enforces; they just catch the drift earlier. jlpm lint remains the manual catch-all.

The two layers are split into two commits so either can be adopted or dropped on its own:

  • 9efdcfe Editor format-on-save (zero-risk, no dependencies). .editorconfig sets baseline whitespace rules (and mirrors the existing package.json 4-space override). .vscode/settings.json enables format-on-save with Prettier, scoped per language so Python and any formatter a contributor already uses are left untouched. .vscode/extensions.json recommends the matching extensions. Only those two .vscode files are un-ignored in .gitignore; all other per-user .vscode files stay ignored.
  • 17d5256 Pre-commit hook. A postinstall script runs Husky, which registers a pre-commit hook that invokes lint-staged. lint-staged runs prettier --write --ignore-unknown on staged files only and re-stages the result, so the committed version is already formatted.

Solution

The load-bearing decisions:

  • postinstall, not prepare. Husky's documented setup uses prepare, but I confirmed empirically that Yarn Berry (this repo's jlpm) does not run the root workspace's prepare on install, while it does run postinstall. So prepare would silently never install the hook here.
  • husky || exit 0 guard. Husky is a devDependency, so anyone installing the published @plmbr/notebook-intelligence npm package does not receive it; a bare postinstall: husky would fail their install with "command not found." The || exit 0 keeps it a no-op in that case, and also where there is no .git (the sdist to wheel build), so it never affects packaging. exit 0 is portable across the POSIX shell, Yarn Berry's portable shell, and cmd.exe.
  • Prettier-only hook. lint-staged runs only Prettier (safe to auto-apply and idempotent). ESLint and Stylelint rule violations are deliberately left for the contributor and CI to surface, since --fix for those can make non-trivial changes. Because eslint-plugin-prettier and stylelint-prettier both enforce the same Prettier config, formatting fixed by the hook satisfies the Prettier dimension of all three linters.
  • "*": "prettier --write --ignore-unknown". Keyed on * rather than a duplicated extension glob, so it cannot drift from prettier:base. --ignore-unknown skips file types Prettier has no parser for, and Prettier still honors .prettierignore, so staged build output or binaries are left untouched.

Testing

  • Verified the hook end-to-end on real commits: a deliberately misformatted Markdown file is reformatted and re-staged before the commit completes; a binary/unknown file staged alongside it is left byte-for-byte unchanged. Both commits in this PR were themselves made through the hook.
  • Confirmed husky || exit 0 exits 0 when the husky binary is absent (consumer case) and when there is no .git (sdist build case).
  • Confirmed the .gitignore negation tracks exactly .vscode/settings.json and .vscode/extensions.json while keeping other .vscode files ignored (git check-ignore).
  • jlpm lint:check, jlpm tsc --noEmit, and jlpm jest (328 tests) all green. No Python changed, so pytest is not applicable.

Risks / follow-ups

  • Both layers are opt-in by design. The hook can be skipped per commit with git commit --no-verify or disabled entirely with HUSKY=0 jlpm install.
  • The hook formats any Prettier-parseable staged file, which is a superset of the types CI checks. This is harmless (formatting is idempotent and CI only checks the narrower set), but it means a staged .yaml or .scss, for example, would also be formatted.
  • Optional, not included to avoid touching packaging config: the newly tracked dotfiles ride along in the source sdist (not the wheel, and not the npm tarball). They could be added to the [tool.hatch.build.targets.sdist] exclude list, but the sdist already carries other dev files (CONTRIBUTING.md, lint configs), so pruning only these would be inconsistent. Happy to add it if you would prefer a leaner archive.
  • yarn.lock grows by the husky and lint-staged dependency trees; the change is purely additive (generated with jlpm).

pjdoland added 2 commits June 1, 2026 19:22
Contributors who edit docs or styles sometimes forget to run jlpm lint
before committing, so Prettier formatting drift only surfaces in CI. This
adds a zero-risk editor layer that fixes the formatting at the source.

.editorconfig sets the baseline whitespace rules (and mirrors the
package.json 4-space override) so non-VS-Code editors stay consistent.
The .vscode settings enable format-on-save with Prettier, scoped per
language so Python and any formatter a contributor already configures are
left untouched. extensions.json recommends the matching extensions.

The .vscode share is opt-in: settings.json and extensions.json are
un-ignored in .gitignore while all other (per-user) .vscode files stay
ignored.
The recurring red CI on contributor PRs is almost always a single
unformatted Markdown, TypeScript, or CSS file: jlpm lint:check gates
Prettier across the tree, and it is easy to forget jlpm lint before
pushing. This adds an opt-in pre-commit hook that formats staged files so
the fix happens before the commit instead of in CI.

On jlpm install a postinstall script runs husky, which registers a
pre-commit hook that invokes lint-staged. lint-staged runs
`prettier --write --ignore-unknown` on staged files only and re-stages
the result, so the committed version is already formatted. The hook is
Prettier-only: ESLint and Stylelint rule violations are still left for
the contributor and CI to surface.

postinstall (not prepare) is used because Yarn Berry does not run the
root workspace's prepare script on install, but does run postinstall. The
`|| exit 0` guard keeps it a no-op where husky is absent (consumers of the
published npm package, whose install never receives the husky devDep) and
where there is no .git checkout (sdist to wheel builds), so it never
breaks packaging. Contributors can skip a single run with
`git commit --no-verify` or opt out with `HUSKY=0 jlpm install`.

CONTRIBUTING documents both this hook and the editor format-on-save layer.
@pjdoland pjdoland added the enhancement New feature or request label Jun 2, 2026
@pjdoland pjdoland added this to the 5.1.x milestone Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant