Copying the template into .git/COMMIT_EDITMSG has no lasting effect: git pre-fills that file from the commit.template config, so the script registered nothing and the template never appeared. Use git's own mechanism, which is what git reads for every interactive commit. Name, prefix and call site are unchanged, so README's setup bullet stays correct; only the sentence describing the old copy in CONTRIBUTING needed rewording. Note the config holds a path relative to the working directory, so it applies to commits made from the repo root.
tiny-pattern-ts
Pattern matching for TypeScript/ESM environments (F#-style, not regex).
Development
- Build:
npm run build - Test:
npm run test,npm run test:ci - Watch:
npm run watch(re-runs tests on file save; the earliest feedback tier) - Checks:
npm run check,npm run fix - Verify (definition of done):
npm run verify—npm run check+ the unit suite in one shot; the whole-project correctness gate (excludes advisorymaintain) - Maintenance (advisory):
npm run maintain—knip+check-outdated; run on a maintenance / update-deps branch, not part of the feature loop - Individual fixes:
npm run fix:oxfmt,npm run fix:oxlint
Tooling
- TypeScript 7 — type checker and build (
tsc). - node --test +
--experimental-strip-types— test runner (Node 22.6+, flag dropped on Node 24). - c8 — code coverage for
test:ci. - oxlint — Rust-based linter. Type-aware rules are enabled via
options.typeAware: truein.oxlintrc.json(powered byoxlint-tsgolint). - oxlint-tsgolint — type-aware linting via typescript-go (declarative via
.oxlintrc.json, no CLI flag). Catches unsafe type assertions, unnecessary type parameters, and other type-system issues that regular oxlint can't see. Source-leveloxlint-disabledirectives are used to silence known false positives (e.g.,expectTypeOf()in test files). - oxfmt — Rust-based formatter (Prettier-compatible). Formats JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; built-in
package.jsonkey sorting replacessort-package-json. - cspell — spell checking.
- knip — finds unused dependencies, exports, and files. Scoped via
--include dependencies,exports,filesto skip the noisytypescategory (which produces false positives for libraries whose exported types are part of the public API). - publint — validates
package.jsonfor ESM publishing correctness. Runs on publish only (in CI), not as part ofnpm run check. - @arethetypeswrong/cli (
attw) — validates.d.tsdeclarations against multiple module-resolution scenarios. Runs on publish only with--profile esm-only(the package is intentionally ESM-only). - lefthook — git pre-commit hooks.
Tooling decisions
The choice and configuration of each tool above is the result of deliberate trade-offs, not defaults. The non-obvious ones:
tsconfig.jsonextends@tsconfig/strictest+@tsconfig/node26;tsconfig.build.jsonextends it to add the emit-only options (declaration,sourceMap,outDir,target: es2024,rewriteRelativeImportExtensions: true) and to exclude test files. This separation lets the editor and CI type-check from one config while the build emits from the other.- Source imports use
.tsextensions sonode --strip-typesresolves them at test time.rewriteRelativeImportExtensions: trueintsconfig.build.jsonrewrites them to.jsin the emitteddist/*output, so consumers see conventional ESM imports. - Type-aware oxlint is enabled declaratively via
options.typeAware: truein.oxlintrc.json(powered byoxlint-tsgolint). The script commands stay clean — no CLI flag — and type-aware mode is a property of the config, not the invocation. - Source-level
oxlint-disabledirectives are used for known type-aware false positives (seesrc/pattern.ts,src/match.ts,src/index.test.ts). The disable lives next to the code it silences, not in.oxlintrc.json, so the trade-off is visible to anyone reading the source. knip --include dependencies,exports,filesintentionally omits thetypescategory, which produces systematic false positives for libraries whose exported types are part of the public API. The targeted scope keeps the signal high without config-file boilerplate.attw --profile esm-onlyis semantically correct: this package is intentionally ESM-only (no CommonJS shim), so CJS resolution scenarios are out of scope by design, not a bug.- The
publish:prefix has no local aggregator.publintandattwvalidate the publishable artifact (dist/), not the source, and require a fresh build. They run only in the CIpublishjob immediately beforenpm publish— there is intentionally nonpm run publish. check:tscruns first in thenpm run checkchain so a type error short-circuits the rest (faster feedback than letting oxlint/oxfmt run and then failing on tsc at the end).verifyis the one-shot definition of done.npm run verifycomposesnpm run checkwith the unit suite (test:unit) into a single whole-project correctness gate, so a human or an agent reaches for one command instead of re-deriving the sequence. It deliberately usestest:unit(nottest) becausecheckalready runscheck:tsc— so tsc runs exactly once. It excludesmaintain(advisory) by design. CI is not switched to it: the build job runscheck+test:cito also collect coverage.- The
check:/maintain:split is correctness gates vs. advisory scans.npm run checkis the fast, offline, whole-project correctness ladder (tsc → oxlint → oxfmt → cspell) and can run anywhere, including the agent loop.knip(~4s, whole project) andcheck-outdated(~6.5s, queries the npm registry) are advisory, not correctness — a stale dependency or an unused export must not fail a feature PR — so they moved tonpm run maintain, kept out of pre-commit, and run in CI as a non-blocking job (see.github/workflows/ci.yml). Notecheck-outdatedexits non-zero whenever any dep is outdated, which is exactly why it must not gate merges. - The pre-commit hook sets the
LEFTHOOK_FILESenv var to the staged-files list, and the affected scripts use${LEFTHOOK_FILES:-<default>}to default to the whole project when invoked manually. This keepspackage.json#scriptsas the single source of truth for the underlying commands —lefthook.ymlonly describes what to run on which files. tslibandtype-festare deliberately not used.tslibis a runtime helper for old ES3/ES5 targets (the project targets ES2024);type-festwas never imported. knip caught both.
Requirements
- Node.js >= 26 (engines field; pinned via
.node-version).
VSCode integration
- Recommended extensions: see
.vscode/extensions.json(oxc, cspell). - TypeScript 7 is used via the
typescriptteam.native-previewextension. - oxc extension provides oxlint squiggles and oxfmt format-on-save.
Workflows
- Version updates via
npm version. - Publishing via GitHub Actions on tagged commits (see
.github/workflows/ci.yml); thepublishjob runspublish:publintandpublish:attwbeforenpm publish.
Script prefix convention
Script names follow a prefix convention that signals when they run:
check:*— read-only verification. Aggregated bynpm run check. Used in pre-commit hooks and CI's build job.fix:*— mutating counterpart ofcheck:*. Aggregated bynpm run fix. Use afternpm run checkto auto-resolve issues.test:*— test scripts.npm run testruns the full suite;test:unit/test:ciare scope-specific variants.maintain:*— advisory repo-maintenance scans (dead code, dependency freshness). Aggregated bynpm run maintain. Whole-project and/or network-bound, so not correctness gates: run on a maintenance branch, and in CI as a non-blocking job that reports without failing.publish:*— runs only at publish time, in the CIpublishjob (immediately beforenpm publish). There is no localnpm run publishscript — publishing is CI-only by policy.
Bare, prefix-free top-level commands are the entry points: build, clean, check, fix, test, watch, maintain, and verify. verify (check + test:unit) is the one-shot "whole-project correctness" gate; maintain is the advisory counterpart that never gates a merge.
Contributing
For maintainer and contributor docs — the script prefix convention, the feedback-tier system, the rules the tools don't enforce, and the publishing workflow — see CONTRIBUTING.md. AI coding agents: your entry point is AGENTS.md, which points back to CONTRIBUTING.md.
- Commit signing (GPG).
- Set up commit message template:
npm run use:git-commit-message. - See
commit-message-template. - Type-only tests use
expect-type'sexpectTypeOf(...)insidenode --testcases.