👷 Type-check the TS floor in CI
Add a `compat` job that type-checks the whole suite and a consumer fixture against the minimum supported TypeScript (5.9), reusing the `dist/` artifact `build` produced and gating `publish`. The compiler is resolved by npx, so it never enters `devDependencies` or the local `check`/`verify` loop. The fixture imports the package by name, resolving the emitted declarations through the `exports` map; `expectTypeOf` / `.not.toBeAny()` make it reject an `any`-typed declaration, which a bare compile would accept. Correct the README consumer floor from >= 5.0 to >= 5.9 (set by `type-fest`) and drop the `node10` resolution claim, which the exports-only entry never satisfied.
This commit is contained in:
1 parent
45df45df4b
commit
75807c4bd8
10 files changed
+218
-8
No files matched your search
@@ -6,6 +6,9 @@ the job graph; this file records why it is shaped the way it is.
|
||||
## Pipeline
|
||||
|
||||
- **`build`** (push to `main` / tag) — build + correctness + packaging.
|
||||
- **`compat`** (push to `main` / tag) — type-check the suite and a consumer
|
||||
fixture against the minimum supported TypeScript; consumes `build`'s `dist/`
|
||||
and gates `publish`. See [§ TypeScript compatibility](#typescript-compatibility).
|
||||
- **`maintain`** (push to `main`, non-blocking) — `npm run maintain`; reports,
|
||||
never fails the build.
|
||||
- **`publish`** (tag) — packaging checks + `publish:publint` / `publish:attw`,
|
||||
@@ -130,6 +133,72 @@ the `build` job; `npm run verify` stays coverage-free.
|
||||
lists it under `--all`. It carries a file-level `/* c8 ignore start */` with
|
||||
the reason. Adding runtime code there means removing that directive.
|
||||
|
||||
## TypeScript compatibility
|
||||
|
||||
#### Decision (2026-09)
|
||||
|
||||
A dedicated `compat` job runs `npm run test:compat` — the `npx`-pinned
|
||||
TypeScript 5.9 compiler (`typescript@5.9.2`) over `compat/tsconfig.json` —
|
||||
against the `dist/` artifact `build` produced, and `publish` requires it. The
|
||||
floor is TypeScript 5.9, pinned in the `test:compat` script itself (the single
|
||||
source of truth) and documented in [README § Requirements](../README.md#requirements).
|
||||
|
||||
#### Why
|
||||
|
||||
- The compiler is a **different major** from the repo's TypeScript 7, so it is
|
||||
resolved by `npx` at run time. It must never appear in `devDependencies`: that
|
||||
would install it for every local `npm ci` and drift the lockfile, which would
|
||||
put a second compiler in the local `check` / `verify` loop and every editor.
|
||||
- **CI-only is the point.** `npx` fetches over the network — like `maintain`'s
|
||||
scans, a network-bound check is never a local feedback tier (see
|
||||
[workflow.md § Feedback tiers](./workflow.md#feedback-tiers)). Locally the
|
||||
same gate is reproducible with `npm run build && npm run test:compat`.
|
||||
- **`compat` consumes `build`'s artifact** rather than rebuilding, so it judges
|
||||
the exact bytes `check`, `test:ci` and `publint` saw.
|
||||
- **`compat` gates `publish`** because the types are the feature: an artifact
|
||||
that is not consumable at the advertised floor must not ship.
|
||||
- **The fixture is a consumer, not a unit test.** `compat/fixture.ts` imports
|
||||
the package by name (`tiny-pattern-ts`), so it resolves through the `exports`
|
||||
map to `dist/index.d.ts` and exercises the emitted declarations' relative
|
||||
`.ts` specifiers — not the source. `expectTypeOf` / `.not.toBeAny()` are
|
||||
load-bearing: a bare compile would also pass if a declaration collapsed to
|
||||
`any`; the exact-equality assertions reject that.
|
||||
|
||||
#### Rejected
|
||||
|
||||
- **A `devDependencies` alias** (`npm:typescript@5.9`): installs the legacy
|
||||
compiler locally, defeating "CI only".
|
||||
- **A second lockfile / sub-project** (`compat/` with its own `npm ci`): a
|
||||
pinned, reproducible matrix, but a whole extra lockfile to maintain for one
|
||||
compiler. `npx -p` is enough.
|
||||
- **A `paths` / `moduleSuffixes` redirect** to typecheck the _existing_ suite
|
||||
against `dist/` without touching it: `paths` cannot remap the relative
|
||||
`./index.ts` imports the tests use; `moduleSuffixes` only lets _missing_
|
||||
source resolve to suffixed copies, so it would need generated `.compat.ts`
|
||||
declarations staged into `src/` (plus excludes). Both spend more than the
|
||||
fixture buys. See the [handover](../backlog.tasks) discussion.
|
||||
- **Writing the fixture against source** (relative import): it would prove the
|
||||
source compiles under 5.9, not that the _published_ declarations do, which is
|
||||
the promise consumers rely on.
|
||||
- **Replacing `attw`**: `attw` owns the full resolution matrix
|
||||
(`node10`/`node16`/`nodenext`/`bundler`); `compat` answers only "does the
|
||||
documented floor compile the artifact".
|
||||
|
||||
#### Known issue
|
||||
|
||||
- `@tsconfig/node26` cannot be extended: its `lib: ["es2025", ...]` and
|
||||
`target: es2025` are rejected by 5.9 (`TS6046`). `compat/tsconfig.json`
|
||||
extends only `@tsconfig/strictest` and sets `lib` / `target: es2024`, the
|
||||
ceiling 5.9 accepts.
|
||||
- `skipLibCheck: false` is deliberate — it is what makes the floor honest
|
||||
(`type-fest` pins it at 5.9), rather than hiding a broken dependency d.ts
|
||||
behind `true`.
|
||||
- The version appears in both the `test:compat` script and the README; a floor
|
||||
bump is a two-file change. The script is authoritative.
|
||||
- `src/doc-test` is excluded from `compat/tsconfig.json`. The generated examples
|
||||
are checked against the source by their own project; `test:compat` covers the
|
||||
suite plus the fixture.
|
||||
|
||||
## Coverage serving
|
||||
|
||||
#### Decision (2026-09)
|
||||
|
||||
@@ -107,8 +107,8 @@ Each factory is two overloads whose order is load-bearing:
|
||||
- **Variance / `const` type parameters / `NoInfer` / `unique symbol` brands /
|
||||
defaulted type-param guards.** None change inference or evaluation order;
|
||||
`in`/`out` on the handler map broke contextual typing outright. `NoInfer`
|
||||
specifically leaks into the emitted `.d.ts`, raising the consumer floor to
|
||||
TypeScript 5.4 (README promises `>= 5.0`).
|
||||
specifically leaks into the emitted `.d.ts`, which would raise the consumer
|
||||
floor above the documented one (see [README § Requirements](../README.md#requirements)).
|
||||
- **Union merge**, **overload merge with only the exhaustive arm last**,
|
||||
**inferred universe**, **conditional `RequireKeys`**, **cases-first curried** —
|
||||
decided against while the API was single-object; their reasons (reported
|
||||
|
||||
Reference in new issue
Block a user