🚚 Rename publint script to publish:publint and document prefix convention

publint validates the publishable artifact (dist/ vs package.json),
not the source. It should run only at publish time, in the CI publish
job, not on every commit.

- Rename check:publint -> publish:publint
- Remove from 'check' chain
- Add 'publish:publint' as a step in the CI publish job, right
  before 'npm publish'
- Introduce a 'publish:' script prefix for publish-time-only scripts
- Document the prefix convention (check:, fix:, test:, publish:)
  in project-specs.md (as a top-level subsection under Scripts)
  and in the README
- Add 'publint' and 'stricter' to cspell word list

A new script should pick the prefix that matches its lifecycle, not
invent a new one. The 'publish:' prefix has no 'npm run publish'
aggregator by design (publishing is CI-only).
This commit is contained in:
tmu committed 2026-09-04 18:01:01 +02:00
1 parent 24a3880aad
commit 01d1084e1f
6 files changed
+154 -8

No files matched your search

+1
View File
@@ -38,6 +38,7 @@ jobs:
registry-url: "https://registry.npmjs.org/"
- run: npm ci
- run: npm run build
- run: npm run publish:publint
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+11 -1
View File
@@ -17,6 +17,7 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex).
- **oxlint** — Rust-based linter.
- **oxfmt** — Rust-based formatter (Prettier-compatible). Formats JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; built-in `package.json` key sorting replaces `sort-package-json`.
- **cspell** — spell checking.
- **publint** — validates `package.json` for ESM publishing correctness. Runs on publish only (in CI), not as part of `npm run check`.
- **lefthook** — git pre-commit hooks.
### Requirements
@@ -32,7 +33,16 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex).
## Workflows
- Version updates via `npm version`.
- Publishing via GitHub Actions on tagged commits (see `.github/workflows/ci.yml`).
- Publishing via GitHub Actions on tagged commits (see `.github/workflows/ci.yml`); the `publish` job runs `publish:publint` before `npm publish`.
## Script prefix convention
Script names follow a prefix convention that signals _when_ they run:
- `check:*` — read-only verification. Aggregated by `npm run check`. Used in pre-commit hooks and CI's build job.
- `fix:*` — mutating counterpart of `check:*`. Run individually (no `npm run fix` aggregator by design — fixes should be intentional, not batched).
- `test:*` — test scripts. `npm run test` runs the full suite; `test:unit` / `test:ci` are scope-specific variants.
- `publish:*` — runs only at publish time, in the CI `publish` job (immediately before `npm publish`). There is no local `npm run publish` script — publishing is CI-only by policy.
## Contribution guidelines
+3 -1
View File
@@ -15,7 +15,9 @@
"typescriptteam",
"gitmoji",
"dbaeumer",
"msvc"
"msvc",
"publint",
"stricter"
],
"ignorePaths": ["dist", "node_modules", "coverage", "*.svg", ".gitignore"]
}
+86
View File
@@ -19,6 +19,7 @@
"lefthook": "^2.1.12",
"oxfmt": "^0.66.0",
"oxlint": "^1.81.0",
"publint": "^0.3.24",
"tslib": "^2.8.1",
"type-fest": "^5.9.0",
"typescript": "^7.0.2"
@@ -1400,6 +1401,22 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@publint/pack": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.7.tgz",
"integrity": "sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyexec": "^1.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://bjornlu.com/sponsor"
}
},
"node_modules/@tsconfig/node26": {
"version": "26.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node26/-/node26-26.0.1.tgz",
@@ -2720,6 +2737,16 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/oxfmt": {
"version": "0.66.0",
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.66.0.tgz",
@@ -2853,6 +2880,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-manager-detector": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz",
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
"dev": true,
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2890,6 +2924,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
@@ -2903,6 +2944,28 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/publint": {
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/publint/-/publint-0.3.24.tgz",
"integrity": "sha512-9zS56KrKBoqi5Qt8h92uMP8TTM9AYZSgnmCo4u2priMqkOZvQnTsziZ2p5LJ2ywbYkAjoCDp2jda9u4cgFefIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@publint/pack": "^0.1.7",
"package-manager-detector": "^1.8.0",
"picocolors": "^1.1.1",
"sade": "^1.8.1"
},
"bin": {
"publint": "src/cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://bjornlu.com/sponsor"
}
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -2913,6 +2976,19 @@
"node": ">=8"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
@@ -3049,6 +3125,16 @@
"node": "20 || >=22"
}
},
"node_modules/tinyexec": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+4 -2
View File
@@ -13,7 +13,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tmueller/tiny-pattern-ts.git"
"url": "git+https://github.com/tmueller/tiny-pattern-ts.git"
},
"files": [
"dist",
@@ -35,7 +35,7 @@
},
"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:outdated",
"check": "npm run check:tsc && npm run check:oxlint && npm run check:oxfmt && npm run check:cspell && npm run check:outdated",
"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 ${LEFTHOOK_FILES:-.}",
@@ -47,6 +47,7 @@
"test": "npm run check:tsc && node --test --strip-types \"src/**/*.test.ts\"",
"test:ci": "c8 --reporter=text --reporter=lcov --reporter=html node --test --strip-types \"src/**/*.test.ts\"",
"test:unit": "node --test --strip-types \"src/**/*.test.ts\"",
"publish:publint": "publint",
"use:git-commit-message": "cp commit-message-template .git/COMMIT_EDITMSG || true"
},
"devDependencies": {
@@ -60,6 +61,7 @@
"lefthook": "^2.1.12",
"oxfmt": "^0.66.0",
"oxlint": "^1.81.0",
"publint": "^0.3.24",
"tslib": "^2.8.1",
"type-fest": "^5.9.0",
"typescript": "^7.0.2"
+49 -4
View File
@@ -157,14 +157,47 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
## 5. Scripts
### PREFIX CONVENTION
Script names use a prefix that signals _when_ the script is intended
to run. A `<prefix>:<name>` script is implicitly aggregated by a
`<prefix>` script (if one exists) and run by the corresponding
lefthook hook or CI step. Picking the right prefix documents the
script's intended lifecycle:
- `check:*` — read-only verification. Aggregated by `npm run check`
(which runs all `check:*` scripts in order). Called from the
lefthook pre-commit hook on staged files, and from the CI build
job on the full project. Read-only; never modifies files.
- `fix:*` — mutating counterpart of a `check:*` script. There is
**no** `npm run fix` aggregator by design: fixes should be
intentional, not batched. Run individually.
- `test:*` — test scripts. `npm run test` is the canonical entry
point (`check:tsc` + unit tests); `test:unit` skips the typecheck
for fast local iteration; `test:ci` adds c8 coverage and is the
CI variant.
- `publish:*` — runs only at publish time, in the CI `publish` job
(immediately before `npm publish`). There is **no** local
`npm run publish` script — publishing is CI-only by policy (see
§7). The `publish:` prefix still documents intent: this script
validates the _publishable artifact_ (e.g., `dist/`) rather than
the source.
A new script should pick the prefix that matches its lifecycle, not
invent a new one. If no existing prefix fits, that is a signal the
script does not belong in the standard pipeline.
### SETUP
- `use:git-commit-message`: Set up commit message template (if needed).
### TEST
- `test`: Run `tsc --noEmit` then `node --test --strip-types src/`.
- `test:unit`: Run unit tests with `node --test --strip-types src/`
- `test`: Run `check:tsc` then `node --test --strip-types "src/**/*.test.ts"`.
The glob is required because Node 26 does not auto-discover test
files in a bare directory argument (`node --test src/` is
interpreted as a module path on Node 26+).
- `test:unit`: Run unit tests with `node --test --strip-types "src/**/*.test.ts"`
(no preceding typecheck).
- `test:ci`: Run tests in CI mode with c8 coverage (text + lcov + html
reporters), uploading `coverage/` as an artifact.
@@ -216,6 +249,16 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
(There is no `fix` aggregator in the scripts; run the `fix:*` scripts
individually.)
### PUBLISH
- `publish:publint`: `publint` — runs the pack-and-lint flow against
the current project (uses `npm pack` to validate the actual
publishable artifact against `package.json`'s `files`, `exports`,
`main`, etc.). Requires a fresh `build` to have populated `dist/`.
Called by the CI `publish` job immediately before `npm publish`
(see §6). Not part of `npm run check` and not run on commit:
it validates publishing correctness, not source correctness.
### HOOKS
- Lefthook runs the relevant `check:*` scripts on staged files in
@@ -235,8 +278,10 @@ typescript-lib-starter-tiny => https://github.com/tmueller/typescript-lib-starte
- `publish` job: only on `refs/tags/*`, depends on `build`. Steps:
`actions/checkout@v4`, `actions/setup-node@v4` (with
`node-version-file: .node-version` and `registry-url`),
`npm ci`, `npm run build`, `npm publish --access public`
with `NODE_AUTH_TOKEN` from secrets.
`npm ci`, `npm run build`, `npm run publish:publint` (see
§5 for why this is `publish:` and not `check:`), and
`npm publish --access public` with `NODE_AUTH_TOKEN` from
secrets.
## 7. Versioning & Publishing