Files
tiny-pattern-ts/development/tooling.md
T
tmu dc7ce22fc5 📝 Add development/ docs for decisions and known issues
Category files under development/ replace the decision prose that was
scattered through README.md and CONTRIBUTING.md. Each decision is a block with
Decision (YYYY-MM) / Why / Rejected / Known issue; the new library.md records
the public API contract and its limitations. AGENTS.md and backlog.tasks now
point at the new home.
2026-09-15 13:26:59 +00:00

8.2 KiB

Tooling

The choice and configuration of every tool below is the result of a deliberate trade-off, not a default. This file is the record of those trade-offs. The commands a contributor runs are in CONTRIBUTING.md; the tool versions are in package.json.

Tool inventory

  • TypeScript 7 — type checker and build (tsc).
  • node --test + --strip-types — test runner.
  • c8 — code coverage for test:ci.
  • oxlint — Rust-based linter, with type-aware rules powered by oxlint-tsgolint (typescript-go).
  • 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.
  • knip — finds unused dependencies, exports, and files.
  • check-outdated — reports dependencies behind the registry; it exits non-zero whenever any dependency is outdated.
  • publint — validates package.json for ESM publishing correctness.
  • @arethetypeswrong/cli (attw) — validates .d.ts declarations against multiple module-resolution scenarios.
  • lefthook — git hooks.
  • @spences10/pi-lsp — read-only LSP code intelligence for AI coding agents (project-local .pi/settings.json). Talks to this repo's TypeScript 7 via tsc --lsp --stdio.

When each tool 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 extends it to add the emit-only options (declaration, sourceMap, inlineSources, outDir, target: es2024, rewriteRelativeImportExtensions: true) and to exclude test files.

Why

  • It lets 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 can map into src/ without it being shipped.
  • declarationMap is intentionally off: a .d.ts.map cannot embed source and would dangle.

The build starts from an empty dist/

Decision (2026-09)

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

Why

  • tsc does not prune orphaned emit output. Dropping declarationMap, for example, left stale *.d.ts.map files behind, so the build must start from an empty dist/ to be reproducible.
  • prebuild removes only dist; the manual clean still resets dist + coverage, so a local coverage report survives a build.

Source imports use .ts extensions

Decision (2026-09)

Source imports use .ts extensions, never .js.

Why

  • node --strip-types only resolves the .ts form at test time.
  • rewriteRelativeImportExtensions: true in tsconfig.build.json 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 declaratively via options.typeAware: true in .oxlintrc.json (powered by oxlint-tsgolint).

Why

  • The script commands stay clean — no CLI flag.
  • Type-aware mode is a property of the config, not the invocation, so it cannot be forgotten on one call site.

Rejected

  • Passing 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)

Source-level oxlint-disable directives are used for known type-aware false positives (see src/pattern.ts, src/match.ts, src/index.test.ts), rather than rules being disabled in .oxlintrc.json.

Why

  • The disable lives next to the code it silences, so the trade-off is visible to anyone reading the source.

Rejected

  • Silencing a rule project-wide 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 convention. AI coding agents must not add one; they fix the type at its root instead (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 feedback than letting oxlint/oxfmt run and then 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 is a sane fallback for 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 .editorconfig 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 intentionally omits the types category.

Why

  • The types category 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 targets ESM-only

Decision (2026-09)

attw --profile esm-only is used.

Why

  • It is semantically correct: this 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; the project targets ES2024.
  • type-fest was never imported.
  • knip flagged both, which is 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 the LEFTHOOK_FILES env var to the staged-files list, and the affected scripts use ${LEFTHOOK_FILES:-<default>} to default to the whole project when invoked manually.

Why

  • It keeps package.json#scripts as the single source of truth for the underlying commands — lefthook.yml only describes 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: see .vscode/extensions.json (oxc, cspell, TypeScript native-preview, EditorConfig, todo-tasks).
  • TypeScript 7 is used 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 user's local [language] formatter settings 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

  • The package 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 binary — no typescript-language-server dependency is required.
  • Earlier releases (<= 0.0.10) hard-wire to typescript-language-server --stdio and are TS6-only.
  • The tool is intermediate agent feedback (hover, references, definition, symbols, diagnostics). It has no rename / code-action / apply-edit surface, and is never a correctness gate — npm run check / verify remain that.
  • .pi/settings.json is the shared, committed declaration; .pi/npm/ is a gitignored install cache that pi recreates automatically on a trusted startup (it runs npm install for any missing project package), so the cache is deliberately not tracked.