From 01d1084e1ff757aa03b8ef6f4381528b2d7d5ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 4 Sep 2026 17:51:33 +0200 Subject: [PATCH] :truck: 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). --- .github/workflows/ci.yml | 1 + README.md | 12 +++++- cspell.json | 4 +- package-lock.json | 86 ++++++++++++++++++++++++++++++++++++++++ package.json | 6 ++- project-specs.md | 53 +++++++++++++++++++++++-- 6 files changed, 154 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33c8b81..362fa0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }} diff --git a/README.md b/README.md index 350dcf0..770a97b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cspell.json b/cspell.json index e89aaf9..3b376bc 100644 --- a/cspell.json +++ b/cspell.json @@ -15,7 +15,9 @@ "typescriptteam", "gitmoji", "dbaeumer", - "msvc" + "msvc", + "publint", + "stricter" ], "ignorePaths": ["dist", "node_modules", "coverage", "*.svg", ".gitignore"] } diff --git a/package-lock.json b/package-lock.json index 8d41709..b08a88d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a9ea8e7..faab718 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/project-specs.md b/project-specs.md index 56a34b2..756ece2 100644 --- a/project-specs.md +++ b/project-specs.md @@ -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 `:` script is implicitly aggregated by a +`` 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