[DWS] Refresh toolchain, move to TypeScript 6, raise minimum Node.js to 22 - #16
Merged
Conversation
nickwinder
marked this pull request as ready for review
August 9, 2026 21:42
HungKNguyen
approved these changes
Aug 10, 2026
HungKNguyen
left a comment
Collaborator
There was a problem hiding this comment.
LGTM a followup either in this PR or another PR, we should follow the PSPDFKit org rule and use SHA-pin the GitHub Actions instead of version numbers (context)
nickwinder
force-pushed
the
nick/dws/dependency-refresh
branch
from
August 10, 2026 21:39
00f999d to
07a0b36
Compare
… to 22
Node.js 18 reached end-of-life in April 2025 and Node.js 20 in April 2026.
CI was still testing both and engines.node still claimed >=18, so the
supported matrix consisted entirely of runtimes that no longer receive
security patches. engines.node moves to >=22.0.0 and CI now runs 22.x and
24.x. This is breaking, but needs no code change from consumers: there is no
API difference.
Dependency updates. ESLint 9.39.2 -> 10.8.1 (with @eslint/js to 10.0.1),
globals 16 -> 17, @types/node 24 -> 26, typescript-eslint 8.53.0 -> 8.66.0,
jest 30.2.0 -> 30.4.2, ts-jest, prettier, openapi-typescript, tsx, dotenv
and rimraf to their latest in-range releases. ESLint 10 is what required the
Node floor to move first; it needs ^20.19 || ^22.13 || >=24.
TypeScript 5.9.3 -> 6.0.3. TypeScript 7 is not reachable yet -- ts-jest
peers on <7 and typescript-eslint on <6.1.0 -- but 6.0 is the supported
bridge toward it, and staying on 5.9 meant sitting on compiler options that
7.0 removes outright. Three things fell out of it:
- moduleResolution moves from node (node10) to bundler. TypeScript 6
deprecates node10 and 7 drops it. bundler describes how this package is
actually built: tsup/esbuild bundles src/ and imports are extensionless.
node16/nodenext would have meant rewriting every relative import to
carry a .js extension.
- TypeScript 6 no longer implicitly pulls in every node_modules/@types
package, so the test project names jest and node explicitly. Without
that, type-aware linting degrades silently into thousands of
"type could not be resolved" errors rather than failing outright.
- openapi-typescript declares a typescript ^5.x peer, which makes npm ci
fail with ERESOLVE on TypeScript 6. A scoped overrides entry relaxes
that one peer. The tool itself is unaffected: it regenerates both files
under src/generated/ byte-for-byte identically on 6.0.3, and CI's drift
job re-checks that on every run.
tsconfig.test.json is replaced by src/__tests__/tsconfig.json. It existed
only to hand type-aware linting a project containing the tests, and
typescript-eslint's project service finds a config adjacent to the files it
covers without being told where to look. eslint.config.mjs moves from the
legacy parserOptions.project array to projectService accordingly.
npm run typecheck now also covers the test suite. Tests were excluded from
the only project tsc --noEmit ran against, so nothing typechecked them
outside of ts-jest at test time.
ESLint 10 removes the --ext flag, so the lint scripts drop `--ext .ts`. The
flag was already redundant under flat config -- `eslint src` and
`eslint src --ext .ts` resolve the same 31 files. Verified after the upgrade
that the count is still 31, because the failure mode here is a lint that
silently matches nothing and exits 0.
The newer typescript-eslint flags nine type assertions as unnecessary. All
nine were genuinely redundant and are removed; one `!config || !config.type`
guard collapses to `!config?.type`. Semantics are unchanged.
Codecov is removed. CI still runs the unit tests with --coverage, so the
thresholds in jest.config.mjs continue to gate the build; coverage is simply
no longer shipped to a third-party service.
Remaining GitHub Actions: checkout, setup-node and upload-artifact to v7,
github-script to v9, gitleaks-action v2 -> v3. The gitleaks bump is
time-sensitive rather than cosmetic -- v2 runs on the Node 20 Actions
runtime, which GitHub removes from hosted runners on 2026-09-16.
Prettier 3.9 reformats three test files. format:check was already failing on
main (inputs.test.ts had eight lines over the 100-column limit), so this
brings the tree to clean rather than merely preserving it.
Changelog and migration entries land under 3.0.0, which is prepared in-repo
but not yet published to npm -- so this does not need a new major.
Verified on TypeScript 6.0.3: typecheck (src + tests), lint (31 files),
format:check, test:unit (315/315), build (esm + cjs + d.ts), generated-types
drift check, npm ci against the regenerated lockfile, and a downstream
typecheck of the emitted .d.ts from a consumer pinned to @types/node 22.
`npm audit --omit=dev` reports 0 vulnerabilities.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nickwinder
force-pushed
the
nick/dws/dependency-refresh
branch
from
August 11, 2026 07:44
07a0b36 to
c0791a4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Two problems, both invisible until something breaks:
engines.nodeclaimed>=18.0.0and CI tested 18.x/20.x. Node 18 went EOL in April 2025 and Node 20 in April 2026 — the project was testing against, and promising support for, runtimes that receive no security patches.gitleaks-action@v2stops working on 2026-09-16. It runs on the Node 20 Actions runtime, which GitHub removes from hosted runners on that date. Secret scanning would have started failing with no code change on our side.The dependency staleness (16 packages) is the routine part; those two are the reasons this shouldn't sit.
What changed
engines.node>=22.0.0, CI matrices22.x/24.x. Breaking, but needs no consumer code change.5.9.3→6.0.3— see the section below; this was the substantive part.9.39.2→10.8.1,globals16 → 17,@types/node24 → 26, typescript-eslint8.53.0→8.66.0,jest30.2.0→30.4.2.jest.config.mjsthresholds still gate the build; it is just no longer uploaded to a third-party service.checkout/setup-node/upload-artifact→ v7,github-script→ v9,gitleaks-actionv2 → v3 (the time-sensitive one).npm run typechecknow covers the test suite, which nothing typechecked before outside ofts-jestat test time.The main thing to review: TypeScript 6
TypeScript 7 is not reachable —
ts-jestpeers ontypescript <7and typescript-eslint on<6.1.0. But 6.0 is the supported bridge toward it, and staying on 5.9 meant sitting on compiler options that 7.0 removes outright. Four things had to be solved, and each is a place to check my reasoning:moduleResolution: node(node10) →bundler. Deprecated in 6, removed in 7.bundlerdescribes how this package is actually built — tsup/esbuild bundlessrc/, imports are extensionless.node16/nodenextwould have required adding a.jsextension to every relative import. I deliberately did not reach forignoreDeprecations, which only defers the failure into TS 7.node_modules/@typespackage, sosrc/__tests__/tsconfig.jsonnamesjestandnodeexplicitly. This one is worth understanding: without it, type-aware linting doesn't fail loudly — it degrades into ~3,500no-unsafe-*"type could not be resolved" errors, because jest's globals silently becomeany.openapi-typescriptdeclares atypescript: ^5.xpeer, which makesnpm cifail withERESOLVEon TypeScript 6 — that would have broken every CI job. A scopedoverridesentry relaxes that one peer. This is evidence-backed rather than hopeful: the tool regenerates both files undersrc/generated/byte-for-byte identically on 6.0.3, and CI'sgenerated-types-driftjob re-checks that on every run.tsconfig.test.json→src/__tests__/tsconfig.json, andeslint.config.mjsmoves from the legacyparserOptions.projectarray toprojectService. The old file existed only to hand type-aware linting a project containing the tests; the project service discovers a config adjacent to the files it covers without being told where.Design notes worth a look
--ext, so the lint scripts drop--ext .ts. The flag was already redundant under flat config — I confirmedeslint srcandeslint src --ext .tsresolve the same 31 files before changing anything. The failure mode here is a lint that silently matches nothing and exits 0, so the file count is asserted explicitly in Verification below.!config || !config.typeguard collapses to!config?.type. All semantics-preserving.format:checkwas already failing onmain—inputs.test.tshad eight lines over the 100-column limit — so this brings the tree to clean rather than merely preserving it.3.0.0is prepared in-repo but unpublished (npmlatestis2.1.0), so the breaking Node bump folds into it.docs/MIGRATION.mdgains it as item 5.@types/node26 sits above the Node 22 floor. I checked this rather than assuming: the emitted.d.tsreferences onlyBuffer,Uint8Array,FileandNodeJS.ReadableStream, and a consumer pinned to@types/node@22typechecks against it cleanly (evidence below).Verification
All commands run on the branch head, on TypeScript 6.0.3.
Definition of done (
AGENTS.md):Lint really is linting — guards the ESLint 10 silent-zero-files trap. 31 files, identical to the pre-upgrade baseline:
Generated types did not drift — the exact check CI runs:
npm ciresolves from the regenerated lockfile. This is the check that caught theopenapi-typescriptpeer conflict — without the scoped override it fails withERESOLVE, which would have broken every CI job:Build and artifacts:
Downstream consumer typecheck — a throwaway project pinned to
@types/node@22+typescript@5.9.3, importingNutrientClient,FileInput,AccountUsageandProductNamefrom the emitteddist/index.d.ts, compiles clean understrict. Confirms the@types/node26 bump does not leak into the public surface.Security posture:
One dev-only low remains:
esbuild(viatsup) arbitrary file read when running its dev server on Windows. It affectsesbuild serve, which this repo never invokes. Clearing it needs anoverridesedit that was deliberately left out of scope; flagging rather than silently absorbing it.Integration tests were not run — they require a live API key.