`ts-morph` 18.0.0 to ^28.0.0, which swaps `@ts-morph/common` 0.19 for 0.29
and with it the vendored compiler that parses every contract: TypeScript
5.0.2 to 6.0.2.
Generated output does not change. Every generator and language combination
for the contract fixture and each generator's spec-example contracts — 318
artifacts across 40 contracts — is byte-identical to ts-morph 18, and the
same 50 combinations decline to parse as before.
Getting there needed three changes to the contract compiler options, none
of which the type checker or the suite would have reported on their own.
TypeScript 6 refuses to run against two deprecated options rather than
warning, so every command failed outright: `baseUrl` and
`moduleResolution: node10`. `baseUrl` is deleted, not silenced — the single
`paths` entry is already absolute, so it was doing nothing, and contracts
still resolve `@airtasker/spot` without it. `node10` is kept behind
`ignoreDeprecations: "6.0"`, because moving off it changes how a contract's
own imports resolve. That is published behaviour and belongs in COMPASS-31,
which also has to land before TypeScript 7 removes the option.
TypeScript 6 also stopped pulling in `node_modules/@types` by itself, which
made a contract importing a node builtin stop type-checking. Confirmed
against both versions: the same contract is valid on ts-morph 18 and fails
on 28 with "Cannot find name 'path'". Naming `types: ["node"]` restores
that case, and `typeRoots` has to point at Spot's own copy, because the
default walks up from the working directory and the image's is a mounted
workspace with no `node_modules`. `@types/node` moves to `dependencies` so
it reaches an image built with `--prod`.
`types` and `typeRoots` together are the whole of what a contract can rely
on ambiently now, where TypeScript 5 gave it every resolvable `@types`
package. No option restores that, and under pnpm the type root holds only
`node`, so the narrowing is structural rather than a policy setting. The
options object says so and points at COMPASS-32, where whether to give the
reach back is decided.
Both halves of the fix are covered rather than asserted. Dropping `types`
fails the new `ts-project` spec; dropping `typeRoots` fails the parity job's
self-containment case, which runs the image against a directory with no
`node_modules` on any parent path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ticket
COMPASS-28 — Bump
spot's NodeJS minimum from 18 to 22What
ts-morph18.0.0→^28.0.0. That swaps@ts-morph/common0.19 for 0.29, and with it the vendored compiler that parses every user contract: TypeScript 5.0.2 → 6.0.2. Supersedes Dependabot #2591.Generated output does not change
The headline risk was a silent shift in what
spotemits. It did not happen — verified with a wider net than the snapshots:Every generator × language combination for
test-fixtures/contract/api.tsand each generator's__spec-examples__contracts — 318 artifacts across ~40 contracts — is byte-identical, and the same 50 combinations decline to parse as before, so nothing changed about which contracts are accepted either.Three things TypeScript 6 broke, none of which the build or the suite reported
pnpm buildpassed andpnpm testlooked normal at first. Every actual command failed.1. TS 6 errors on deprecated options instead of warning.
baseUrlis deleted, not silenced — the singlepathsentry is already an absolute path, so it was doing nothing; confirmed contracts still resolve@airtasker/spotwithout it.node10is kept behindignoreDeprecations: "6.0", because the replacements (Bundler,Node16) change how a contract's own imports resolve. That is published behaviour, so it is COMPASS-31 — which also has to land before TS 7 removes the option outright.2. TS 6 stopped including
node_modules/@typesautomatically, so a contract importing a node builtin stopped type-checking. Confirmed against both versions with the same contract:Contract is validError: Cannot find name 'path'types: ["node"]restores it. There is no way back to the old automatic behaviour —typeRootsalone does not re-enable it — so a contract relying on some other ambient@typespackage is a narrowing this does not cover. Flagging that explicitly rather than describing this as a clean restoration.3.
typeRootsthen has to point at Spot's own copy. The default walks up from the working directory, and the image's working directory is a mounted workspace with nonode_modulesat all. So@types/nodemoves todependenciesto reach an image built with--prod— alongsidetypescriptandeslint, which are already runtime dependencies for the same kind of reason.That one had a false summit: with
@types/nodein the image buttypeRootsunset, the image still failed on a clean workspace while passing when I happened to mount the repo, which hasnode_modules.Both halves are covered, not asserted
types: ["node"]lib/src/ts-project.spec.ts— fails withCannot find name 'path'typeRootsdocker-parityself-containment case —FAIL: the image could not process a contract outside a Node projectThe second is the useful one:
scripts/check-image-parityalready runs the image against amktemp -dit verifies has nonode_moduleson any parent path, so CI guards the image path without anything new. Confirmed by building a mutant image and watching parity fail on exactly that case.Also confirms a claim from #2719
#2719 worked around
@types/node@22failing under ts-morph 18's vendored TS 5.0.2 (CompressionStreamcircularity) and said the skew would go away when ts-morph moved. It has: under TS 6.0.2 the same probe reports 0 diagnostics even withoutskipLibCheck.skipLibCheckstays for its own documented reason — a contract author cannot fix errors inside a declaration file.How this was verified
pnpm buildpnpm testpnpm lint:checkpnpm build-docsdocker build+check-image-parityContract is valid,Generated .../api.yml--prodinstall +bin/run validate