Files
tiny-pattern-ts/development/tooling.md
T
tmu a3eb6183af 🔥 Remove the match and pattern example modules
They were the template's demo API, not the library's real surface. Drop
them, their README walkthrough, and the tooling note that cited their
disable directives, and point index.ts at the primitive matchers that
remain.
2026-09-16 21:57:55 +00:00

8.8 KiB

Tooling

Every tool below was chosen and configured deliberately. The commands a contributor runs are in CONTRIBUTING.md and the versions in package.json.

Tool inventory

  • TypeScript 7 — type checker and build (tsc).
  • node --test + --strip-types — test runner.
  • c8 — coverage for test:ci.
  • oxlint — Rust linter, type-aware via oxlint-tsgolint (typescript-go).
  • oxfmt — Rust formatter (Prettier-compatible) for JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; its package.json key sorting replaces sort-package-json.
  • cspell — spell checking.
  • knip — unused dependencies, exports, and files.
  • check-outdated — dependencies behind the registry; exits non-zero when any is outdated.
  • publint — validates package.json for ESM publishing.
  • @arethetypeswrong/cli (attw) — validates .d.ts against module-resolution scenarios.
  • lefthook — git hooks.
  • @spences10/pi-lsp — read-only LSP code intelligence for AI agents (project-local .pi/settings.json); talks to this repo's TypeScript 7 via tsc --lsp --stdio.

When each runs is in CONTRIBUTING.md § Feedback tiers.

TypeScript and build

One type-check config, one emit config

Decision (2026-09)

tsconfig.json extends @tsconfig/strictest + @tsconfig/node26. tsconfig.build.json adds the emit-only options (declaration, sourceMap, inlineSources, outDir, target: es2024, rewriteRelativeImportExtensions: true) and excludes test files.

Why

  • The editor and CI type-check from one config while the build emits from the other, so a test file cannot leak into dist/.
  • inlineSources embeds the original TypeScript in dist/*.js.map, so debuggers map into src/ without it being shipped.
  • declarationMap stays off: a .d.ts.map cannot embed source and would dangle.

The build starts from an empty dist/

Decision (2026-09)

npm run build runs a prebuild hook that empties dist/.

Why

  • tsc does not prune orphaned emit output — dropping declarationMap left stale *.d.ts.map files — so reproducibility needs an empty dist/.
  • prebuild removes only dist; the manual clean resets dist + coverage, so a local coverage report survives a build.

Source imports use .ts extensions

Decision (2026-09)

Source imports use .ts, never .js.

Why

  • node --strip-types resolves the .ts form at test time.
  • rewriteRelativeImportExtensions rewrites them to .js in the emitted JavaScript.
  • The emitted .d.ts keep the .ts specifier, which TypeScript >= 5.0 resolves (see README § Requirements), so no post-processing step is needed.

Rejected

  • "Pre-fixing" an import to .js: it breaks the inner node --strip-types loop.

Linting and formatting

Type-aware oxlint is a config property

Decision (2026-09)

Type-aware oxlint is enabled via options.typeAware: true in .oxlintrc.json (powered by oxlint-tsgolint).

Why

  • The script commands stay clean — no CLI flag.
  • A config property cannot be forgotten on one call site.

Rejected

  • A CLI flag in the check:oxlint / fix:oxlint scripts: it puts the mode in two places and invites them to drift.

oxlint-disable directives live next to the code

Decision (2026-09)

A type-aware rule that false-positives is silenced with a source-level oxlint-disable directive (see src/primitive.ts, src/primitive.test.ts), not by turning the rule off in .oxlintrc.json.

Why

  • The disable sits next to the code it silences, visible to anyone reading the source.
  • The rule stays on everywhere else, so only the mis-firing line is exempted.

Rejected

  • A project-wide disable in .oxlintrc.json for a false positive: it hides the exemption from the reader of the affected code and switches the rule off repo-wide for a one-site problem.

Known issue

  • A source-level disable is a human last resort. AI agents must not add one; they fix the type at its root (see AGENTS.md § Never do).

Unwanted stylistic rules are turned off in the config

Decision (2026-09)

A stylistic rule the project rejects is "off" in the .oxlintrc.json rules map, not silenced at a use site. Current entries: eslint/capitalized-comments (comments may start lowercase) and eslint/no-ternary (ternaries are allowed), joining the oxfmt-superseded rules already off.

Why

  • The rule is wrong for the whole project, not mis-firing at one site, so there is no line to annotate.
  • Keeping the two mechanisms separate keeps a source-level oxlint-disable meaningful: it marks a lone exception.

Rejected

  • A source-level oxlint-disable per use: the same exemption repeated at every site, and oxfmt can move the site.

check:tsc runs first

Decision (2026-09)

check:tsc runs first in the npm run check chain.

Why

  • A type error short-circuits the rest, which is faster than running oxlint/oxfmt and failing on tsc at the end.

.editorconfig is a fallback, not a gate

Decision (2026-09)

.editorconfig exists for editor compatibility; where both apply, .oxfmtrc.json is authoritative.

Why

  • .editorconfig covers the files oxfmt does not format: shell scripts, dotfiles, LICENSE, the commit-message template, and git's COMMIT_EDITMSG buffer.
  • oxfmt is the formatter; the overlapping keys only keep non-oxfmt editors close to the formatted result, so they cannot disagree with the checker.

Static analysis and packaging

knip omits the types category

Decision (2026-09)

knip --include dependencies,exports,files omits the types category.

Why

  • types produces systematic false positives for libraries whose exported types are part of the public API.
  • The narrower scope keeps the signal high without config-file boilerplate.

maintain:outdated ignores @types/node

Decision (2026-09)

Pass --ignore-packages @types/node.

Why

  • DT pins @types/node's latest dist-tag to LTS (22.x); current-line types ride other tags. Scan sees latest < installed — permanent "reverted", exit 1, zero signal. --ignore-pre-releases no help: 22.20.3 is stable.

Rejected

  • --types major,minor,patch: hides real reverted reports elsewhere.
  • @types/node@26.*: tag stays wrong across majors; un-pin per bump = ritual.

Known issue

  • A genuinely behind @types/node goes unreported; match it to .node-version by hand.

attw targets ESM-only

Decision (2026-09)

attw --profile esm-only is used.

Why

  • The package is intentionally ESM-only (no CommonJS shim), so CJS resolution scenarios are out of scope by design, not a bug.

tslib is deliberately not used

Decision (2026-09)

tslib is not a dependency.

Why

  • tslib is a runtime helper for old ES3/ES5 targets; this project targets ES2024.

Git hooks and script wiring

LEFTHOOK_FILES scopes commands to staged files

Decision (2026-09)

The pre-commit hook sets LEFTHOOK_FILES to the staged-files list, and the affected scripts use ${LEFTHOOK_FILES:-<default>} to default to the whole project.

Why

  • It keeps package.json#scripts the single source of truth; lefthook.yml only says what to run on which files.
  • The same script works by hand (whole project) and staged (scoped), so there is no second command to maintain.

Editor and agent tooling

VSCode integration

  • Recommended extensions are in .vscode/extensions.json (oxc, cspell, TypeScript native-preview, EditorConfig, todo-tasks).
  • TypeScript 7 runs via the typescriptteam.native-preview extension.
  • The oxc extension provides oxlint squiggles and oxfmt format-on-save; .vscode/settings.json pins it per language so a local [language] formatter setting cannot override the project's choice.

@spences10/pi-lsp is pinned and read-only

Decision (2026-09)

@spences10/pi-lsp is pinned to 0.0.46 and used read-only.

Why

  • It inspects node_modules/typescript, sees major >= 7 with no lib/tsserver.js (true of the typescript-go / tsgo port), and spawns the repo's own tsc --lsp --stdio — no typescript-language-server dependency is needed.
  • Earlier releases (<= 0.0.10) hard-wire to typescript-language-server --stdio and are TS6-only.
  • It is intermediate agent feedback (hover, references, definition, symbols, diagnostics), with no rename / code-action / apply-edit surface, and is never a gate — npm run check / verify are.
  • .pi/settings.json is the committed declaration; .pi/npm/ is a gitignored install cache that pi recreates on a trusted startup (running npm install for any missing project package), so it is deliberately not tracked.