🔧 Unify lefthook and package.json scripts via LEFTHOOK_FILES env var

lefthook and package.json had parallel command definitions for the
same tools (oxlint, oxfmt, cspell). Consolidate by making lefthook
call the npm scripts, with staged files passed via the
LEFTHOOK_FILES env var. The scripts use ${LEFTHOOK_FILES:-<default>}
so they default to the full project when invoked manually and to
the staged-files list when invoked from lefthook.

Changes:
- package.json#check:oxlint: `oxlint ${LEFTHOOK_FILES:-src}`
  (lints src/ manually; staged files from lefthook)
- package.json#check:oxfmt: `oxfmt --check ${LEFTHOOK_FILES:-src}`
- package.json#check:cspell: `cspell lint ${LEFTHOOK_FILES:-.}`
  (walks CWD manually; staged files from lefthook)
- package.json#check:tsc, check📦 unchanged (no file args)
- lefthook.yml: file-filtered hooks (oxlint, oxfmt, cspell) now use
  `sh -c 'LEFTHOOK_FILES="$0" npm run check:*' {staged_files}` to
  inject the staged-files list into the env. sort-package-json and
  typecheck call npm scripts directly (no file args).
- project-specs.md: document the unification pattern

Why sh -c + env var instead of the simpler 'npm run ... -- {staged_files}':
  'oxlint src file.ts' lints the whole src/ tree *plus* file.ts
  (oxlint doesn't dedupe paths). Setting LEFTHOOK_FILES as an env
  var (which lefthook's 'env:' config does not template) requires
  the sh -c wrapper, but it gives the right semantics: when the
  var is set, only the explicit files are checked; when unset,
  the default (src/ or .) is used.

Verified:
- 'npm run check:oxlint' (no env) lints all of src/
- 'LEFTHOOK_FILES=src/match.ts npm run check:oxlint' lints only that file
- 'npx lefthook run pre-commit' with a staged TS file: cspell output
  shows '1/1 src/match.ts' (only staged file, not whole project)
- All 5 hooks pass on a real staged change
- 'lefthook validate' reports 'All good'
- 'npm run check' exits 0
This commit is contained in:
tmu committed 2026-09-03 18:44:19 +00:00
1 parent 45b6c141e8
commit e45592ae00
3 files changed
+38 -21

No files matched your search

+5 -5
View File
@@ -5,13 +5,13 @@ pre-commit:
commands:
oxlint:
glob: "*.{ts,tsx,js,jsx,mjs,cjs}"
run: npx oxlint {staged_files}
run: sh -c 'LEFTHOOK_FILES="$0" npm run check:oxlint' {staged_files}
oxfmt:
glob: "*.{ts,tsx,js,jsx,mjs,cjs}"
run: npx oxfmt --check {staged_files}
run: sh -c 'LEFTHOOK_FILES="$0" npm run check:oxfmt' {staged_files}
cspell:
run: npx cspell {staged_files}
run: sh -c 'LEFTHOOK_FILES="$0" npm run check:cspell' {staged_files}
sort-package-json:
run: npx sort-package-json --check
run: npm run check:package
typecheck:
run: npx tsc --noEmit
run: npm run check:tsc
+4 -4
View File
@@ -33,10 +33,10 @@
"scripts": {
"build": "tsc -p tsconfig.build.json",
"check": "npm run check:oxlint && npm run check:oxfmt && npm run check:tsc && npm run check:cspell && npm run check:package && npm run check:outdated",
"check:cspell": "cspell .",
"check:cspell": "cspell lint ${LEFTHOOK_FILES:-.}",
"check:outdated": "check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-darwin-arm64,@oxfmt/binding-darwin-x64,@oxfmt/binding-linux-arm64-gnu,@oxfmt/binding-linux-arm64-musl,@oxfmt/binding-linux-x64-gnu,@oxfmt/binding-linux-x64-musl,@oxfmt/binding-win32-x64-msvc,@oxlint/binding-darwin-arm64,@oxlint/binding-darwin-x64,@oxlint/binding-linux-arm64-gnu,@oxlint/binding-linux-arm64-musl,@oxlint/binding-linux-x64-gnu,@oxlint/binding-linux-x64-musl,@oxlint/binding-win32-x64-msvc",
"check:oxfmt": "oxfmt --check src",
"check:oxlint": "oxlint src",
"check:oxfmt": "oxfmt --check ${LEFTHOOK_FILES:-src}",
"check:oxlint": "oxlint ${LEFTHOOK_FILES:-src}",
"check:package": "sort-package-json --check",
"check:tsc": "tsc --noEmit",
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
@@ -84,4 +84,4 @@
"publishConfig": {
"access": "public"
}
}
}
+29 -12
View File
@@ -38,15 +38,24 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
`oxfmtrc`, `oxlintrc`, `sortpackagerc`, `EDITMSG`, `typescriptteam`,
`gitmoji`, `dbaeumer`, `msvc`) and a few library names (`tslib`,
`typefest`, `lefthook`).
- **Lefthook**: Pre-commit checks (run in parallel, lefthook v2 schema):
- `min_version: 2.0.0`
- `pre-commit.commands.oxlint` — `npx oxlint {staged_files}` on
staged `*.{ts,tsx,js,jsx,mjs,cjs}` files
- `pre-commit.commands.oxfmt` — `npx oxfmt --check {staged_files}`
on staged `*.{ts,tsx,js,jsx,mjs,cjs}` files
- `pre-commit.commands.cspell` — `npx cspell {staged_files}`
- `pre-commit.commands.sort-package-json` — `npx sort-package-json --check`
- `pre-commit.commands.typecheck` — `npx tsc --noEmit`
- **Lefthook**: Pre-commit checks (run in parallel, lefthook v2 schema).
Single source of truth for the underlying commands is in
`package.json#scripts`; the lefthook config only describes *what to
run on which files*. Hooks that should run on staged files
(`oxlint`, `oxfmt`, `cspell`) set the `LEFTHOOK_FILES` env var to
the staged-files list via a `sh -c` wrapper, and the npm script
uses `${LEFTHOOK_FILES:-<default>}` to default to the full project
when invoked manually:
- `pre-commit.commands.oxlint` (glob `*.{ts,tsx,js,jsx,mjs,cjs}`):
`sh -c 'LEFTHOOK_FILES="$0" npm run check:oxlint' {staged_files}`
- `pre-commit.commands.oxfmt` (glob `*.{ts,tsx,js,jsx,mjs,cjs}`):
`sh -c 'LEFTHOOK_FILES="$0" npm run check:oxfmt' {staged_files}`
- `pre-commit.commands.cspell`:
`sh -c 'LEFTHOOK_FILES="$0" npm run check:cspell' {staged_files}`
- `pre-commit.commands.sort-package-json`:
`npm run check:package` (no file args needed)
- `pre-commit.commands.typecheck`:
`npm run check:tsc` (no file args needed)
`outdated` is intentionally NOT a pre-commit check (it can flag
upstream patch releases that aren't actionable locally); it runs in
CI and via the explicit `lefthook run outdated` command.
@@ -163,10 +172,18 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
- `check`: Run all checks in order — `check:oxlint`, `check:oxfmt`,
`check:tsc`, `check:cspell`, `check:package`, `check:outdated`.
- `check:oxlint`: `oxlint src`.
- `check:oxfmt`: `oxfmt --check src`.
- `check:oxlint`: `oxlint ${LEFTHOOK_FILES:-src}` — lints `src/`
by default; when invoked from the lefthook pre-commit hook with
`LEFTHOOK_FILES` set to the staged-files list, lints only those
files. This is the single source of truth for the oxlint command
and is shared between the manual `npm run check` and the pre-commit
hook.
- `check:oxfmt`: `oxfmt --check ${LEFTHOOK_FILES:-src}` — same
pattern as `check:oxlint` (whole `src/` by default, staged files
from lefthook).
- `check:tsc`: `tsc --noEmit`.
- `check:cspell`: `cspell .`.
- `check:cspell`: `cspell lint ${LEFTHOOK_FILES:-.}` — walks the
project root by default; from lefthook, only the staged files.
- `check:package`: `sort-package-json --check`.
- `check:outdated`: `check-outdated --ignore-pre-releases --ignore-packages @oxfmt/binding-*,@oxlint/binding-*`. The oxc native bindings are declared as `optionalDependencies` so the correct one is selected per platform automatically; the `*`-platform bindings show as "not installed" on the current platform and are explicitly ignored here.