`getMatcher` / `getMatcherW` become `getPrimitiveUnionMatcher` / `getPrimitiveUnionMatcherW`; `src/primitive.ts` and its test move to `src/primitive-union.*`. The `Union` suffix mirrors `getTaggedUnionMatcher`.
324 lines
11 KiB
Markdown
324 lines
11 KiB
Markdown
# Tooling
|
|
|
|
Every tool below was chosen and configured deliberately. The commands a
|
|
contributor runs are in [CONTRIBUTING.md](../CONTRIBUTING.md) and the versions
|
|
in [package.json](../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`.
|
|
- **vscode-languageserver-protocol** — LSP client and protocol types for the
|
|
autocomplete test helper (`src/util/__tests__/lsp-completion.ts`).
|
|
|
|
When each runs is in
|
|
[CONTRIBUTING.md § Feedback tiers](../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](../README.md#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 **at one site** is silenced with a
|
|
source-level `oxlint-disable` directive (see `src/primitive-union.ts`). A rule that is
|
|
wrong for a whole **file class** is turned off in a `.oxlintrc.json` `overrides`
|
|
entry instead — e.g. `typescript/no-floating-promises` (synchronous
|
|
`expectTypeOf` reads as an unhandled promise) and `unicorn/no-null` (intentional
|
|
`null` inputs) for `**/*.test.ts`. The same exemption is not repeated as a
|
|
file-level header in every affected file.
|
|
|
|
#### Why
|
|
|
|
- A one-site disable sits next to the code it silences, visible to anyone
|
|
reading the source, and the rule stays on everywhere else.
|
|
- A file-class rule is a property of the file class, not of one line; the
|
|
override states it once, where the rest of the file-class config lives.
|
|
|
|
#### Rejected
|
|
|
|
- A project-wide disable in `.oxlintrc.json` for a one-site 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.
|
|
- A repeated file-level `oxlint-disable` header for a file-class false positive:
|
|
the copies drift and scatter one config decision across the tree.
|
|
|
|
#### Known issue
|
|
|
|
- Both placements are _human_ last resorts. AI agents must neither add a source
|
|
disable nor edit `.oxlintrc.json`; they fix the type at its root (see
|
|
[AGENTS.md § Never do](../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.
|
|
|
|
## Language server tooling
|
|
|
|
### `vscode-languageserver-protocol` backs the autocomplete helper
|
|
|
|
#### Decision (2026-09)
|
|
|
|
The autocomplete helper (`src/util/__tests__/lsp-completion.ts`) drives
|
|
`tsc --lsp --stdio` through `vscode-languageserver-protocol`'s
|
|
`createMessageConnection` and its typed request / notification objects, instead
|
|
of a hand-rolled JSON-RPC client.
|
|
|
|
#### Why
|
|
|
|
- Framing, `Content-Length` parsing, the pending-request map and server-request
|
|
dispatch are protocol plumbing the helper only reimplemented; the official
|
|
client owns them and tolerates the server's `string | number` ids.
|
|
- `InitializeRequest`, `CompletionRequest`, `DidOpenTextDocumentNotification`,
|
|
… carry their parameter and result types, so `CompletionList` / `CompletionItem`
|
|
replace the helper's ad-hoc shape guards.
|
|
- The `./node` entry re-exports `vscode-jsonrpc/node`, so one devDependency
|
|
supplies both the transport and the protocol types. It is test-only and never
|
|
ships (`files` publishes `dist/` only).
|
|
|
|
#### Rejected
|
|
|
|
- `vscode-languageclient`: the editor-side client with a full feature registry
|
|
— far more than a test helper needs.
|
|
- Generic JSON-RPC (`jsonrpc-lite`, `jayson`): still no LSP types, so they
|
|
replace framing only and leave the typed protocol surface unimplemented.
|
|
- Keeping the hand-rolled client: the low-level shape is the maintenance cost
|
|
the helper exists to remove, and it must be re-audited against the server.
|
|
|
|
## Editor and agent tooling
|
|
|
|
### VSCode integration
|
|
|
|
- Recommended extensions are in
|
|
[.vscode/extensions.json](../.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.
|