Files
tiny-pattern-ts/development/tooling.md
T
tmu 74c39e1346 ♻️ Tighten the development/ prose
Same decisions, rationale, rejected alternatives and known issues, said with
less padding: ~5,530 -> ~4,730 words (-15%). Every fact from the first draft is
kept; only the wording, duplicated lead-ins and restated context are cut.
2026-09-15 13:56:49 +00:00

7.5 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)

Known type-aware false positives are silenced with source-level oxlint-disable directives (see src/pattern.ts, src/match.ts, src/index.test.ts), not with rules disabled in .oxlintrc.json.

Why

  • The disable sits next to the code it silences, visible to anyone reading the source.

Rejected

  • A project-wide disable in .oxlintrc.json: it hides the suppression from the reader of the affected code.

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).

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.

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 and type-fest are deliberately not used

Decision (2026-09)

Neither tslib nor type-fest is a dependency.

Why

  • tslib is a runtime helper for old ES3/ES5 targets; this project targets ES2024.
  • type-fest was never imported.
  • knip flagged both, the same signal that keeps the list honest.

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.