Skip to content

build!: move to Node 24 and TypeScript 7 - #347

Merged
mrafnadeem-apimatic merged 14 commits into
devfrom
build/update-stack
Sep 23, 2026
Merged

mrafnadeem-apimatic merged 14 commits into
devfrom
build/update-stack

Conversation

@mrafnadeem-apimatic

Copy link
Copy Markdown
Contributor

Raises the engine floor to Node 24 and moves compilation to TypeScript 7, ahead of the next major.

Why Node 24

Not because Node 24 is newest — because we should not ship against a runtime that reaches end of life inside this major's supported life. Node 22 is EOL 2027-04-30; Node 24 is patched until 2028-04-30. That is the claim that survives a security questionnaire, and it is the reason worth quoting.

Two things to keep in view:

  • Node 22 is still supported upstream for another seven months, so this does lock out anyone standardised on it. That is deliberate — a major is where a floor moves — but it is a real cost, not a free one.
  • Node 24 enters maintenance on 2026-10-20 and Node 26 becomes LTS on 2026-10-28, both about five weeks out. The floor is the minimum we support, not what we recommend; 26 is what most developers will be running, which is why CI now tests both.

Why TypeScript 7, and why TypeScript 6 is still here

Measured on this repo, cold, Windows, Node 24.12.0:

Gate TS 5.9.3 TS 7.0.2
build (full emit) 4939 / 4749 / 4534 ms 1288 / 1258 / 1308 ms ~3.7×
pretest (type-check suite) 4734 / 4835 ms 944 / 1078 ms ~4.7×

The two type-checking gates drop from ~9.5 s to ~2.3 s. To be precise about the claim: pnpm test runs mocha through tsx/esbuild and never touches the compiler, so the mocha run itself is not faster — the win is build and pretest.

@typescript-eslint cannot run on TypeScript 7. Every published version, canary included, declares typescript >=4.8.4 <6.1.0 and throws at module init, which would take out lint, posttest and the pre-commit hook (#10940). So the two compilers sit side by side in Microsoft's documented arrangement: tsc is TypeScript 7 and builds; bare typescript is the TypeScript 6 shim that only the linter loads. Naming them this way, rather than aliasing TypeScript 7, keeps the compiler each script gets written down and survives pnpm update --latest. This is temporary — .ai/instructions.md records the removal steps and the two traps while it stands.

Emitted output was diffed between compilers: 432 files, 81 differ, all cosmetic — sourcemaps, temp-variable naming, declaration ordering, one quote-style change. No semantic change to shipped runtime behaviour.

Notable

  • A latent bug surfaced and is fixed (1be71d9). SdkQuickstartAction built its ValidationService from this.configDir in a property initialiser, but configDir is a constructor parameter property. It only worked because downlevel emit assigns parameter properties first; under the native class-field semantics an ES2024 target selects, this.configDir is undefined there. Worth reviewing on its own.
  • target moves es2017 → es2024, which also selects native class-field semantics. That is a behavioural change beyond syntax; the suite (480 passing) is the evidence. useDefineForClassFields: false is the one-line fallback if you would rather split it out.
  • The portal-only Node gate is gone. It existed for a TanStack Start build requirement, not a CLI-wide one; two gates holding the same number drift. engines is only advisory, so the floor is enforced at run time by an init hook, with a test asserting the constant matches engines.node.
  • Releasing on Node 24 removed the npm install -g npm@latest step — Node 22 bundles npm 10.9.8, below the 11.5.1 OIDC publishing needs; Node 24 bundles 11.19.0. The constraint is now a comment on the matrix, because pinning that job back would break publishing silently.
  • @types/node is pinned to 24.13.4, not 24.13.6 — newer releases are inside the repo's own 7-day minimumReleaseAge window, and bypassing a supply-chain defence for a types bump is not a trade worth making.

Deviating from what was agreed

The standalone pnpm in test.yml is kept, not reverted to corepack. Its original reason (pnpm 11 needs Node 22.13, the floor was 22.12) is gone, but it is a SHA-pinned executable needing no Node of its own, where corepack resolves pnpm from the registry at run time and needs the three-step setup-node dance. Reverting would have cost supply-chain hygiene in the most security-relevant workflow to remove one step. The comment now states the reason that actually holds. Happy to do the full revert if you disagree.

Verification

pnpm install --frozen-lockfile, pnpm build, pretest, 480 passing / 14 pending / 0 failing, lint clean, built CLI runs, and the engine guard was exercised on both paths.

Lint was run scoped rather than via pnpm test: posttest is eslint ., which also sweeps a stale .claude/worktrees copy in my working tree. CI checkouts do not have that, so CI is unaffected — but it does mean pnpm test fails locally for anyone with a worktree. Not fixed here; say the word and I will add .claude to the eslint ignores.

BREAKING CHANGE: engines.node is now >=24.0.0. Node 22 is no longer supported.

🤖 Generated with Claude Code

mrafnadeem-apimatic and others added 9 commits September 22, 2026 10:49
Node 22 reaches end of life on 2027-04-30, inside the supported life of the
next major, so the CLI no longer ships against it. Node 24 is patched until
2028-04-30.

`engines` alone only warns at install time, so the floor is also enforced at
run time by an `init` hook that names the version it found. That replaces the
portal-only Node gate, which existed for a TanStack Start build requirement
rather than a CLI-wide one; two gates holding the same number would drift.
The portal service keeps its template-dependency check.

`@types/node` moves to 24 to match the runtime it describes, pinned to 24.13.4
because newer releases are inside the repo's 7-day `minimumReleaseAge` window.

BREAKING CHANGE: engines.node is now >=24.0.0. Node 22 is no longer supported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`validationService` was initialised from `this.configDir` in a property
initialiser, but `configDir` is a constructor parameter property. The code only
worked because TypeScript's downlevel emit assigns parameter properties before
property initialisers; under the native class-field semantics that a modern
target selects, `this.configDir` is still undefined at that point and the
service would be constructed with no config directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TypeScript 7 is the native compiler. On this repo it builds `src` in ~1.3s
against ~4.7s, and type-checks the suite in ~1.0s against ~4.8s. Emitted
output is unchanged apart from sourcemaps, temp-variable naming and
declaration ordering.

typescript-eslint cannot run on it: every published version, canary included,
declares `typescript >=4.8.4 <6.1.0` and throws at module init on TS 7, which
would take out `lint`, `posttest` and the pre-commit hook. So the two sit side
by side in Microsoft's documented arrangement — `tsc` is TypeScript 7 and
builds, bare `typescript` is the TypeScript 6 shim that only the linter loads.
Naming them this way rather than aliasing TypeScript 7 keeps the compiler each
script gets written down, and survives `pnpm update --latest`.

TypeScript 6 stopped sweeping node_modules/@types, so the test project names
the globals the suite needs; the source project resolves its types through
`node:` imports and needs no list.

`target` moves to es2024 now that the engine floor is Node 24, which also
selects native class-field semantics. `ts-node` goes: it drives the classic
compiler API, which TypeScript 7 no longer ships, and the suite runs on tsx.
`@types/mocha` was six years behind mocha and had to be named explicitly.

The `typescript.tsdk` pin is dropped. It is deprecated in favour of
`js/ts.tsdk.path`, inert unless a developer opts in by hand, and would now
point at a TypeScript 6 build within a patch of what VS Code already bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The engine floor is Node 24, so the 22 legs go. The second leg is 26 rather
than the floor alone: 26 becomes LTS on 2026-10-28, inside this major's life,
so it is what most developers will be running. The job count is unchanged.

Releasing on Node 24 drops the `npm install -g npm@latest` step, whose only
purpose was that Node 22 bundles npm 10.9.8 while OIDC trusted publishing
needs 11.5.1. Node 24 bundles 11.19.0. The constraint moves to a comment on
the matrix, since pinning that job back to 22 would silently break publishing.

`npm-tag-latest.yml` carried a `node: [ '14' ]` matrix that no step consumed;
the job never sets up Node at all.

The standalone pnpm in test.yml is kept rather than reverted to corepack. Its
original reason is gone, but it is a SHA-pinned executable that needs no Node
of its own, where corepack would resolve pnpm from the registry at run time and
need the three-step setup-node dance the other workflows use. The comment now
states the reason that actually holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`preLaunchTask` named a task the TypeScript extension provides, and that
provider is not registered when the extension stands down for TypeScript 7, so
the configuration would fail to start. The npm task provider supplies `build`
regardless, and still rebuilds before launching.

`program` pointed at `bin/run`, deleted in the ESM migration; it only launched
because Node appends `.js`. The forty commented-out argument lines carried
another machine's absolute paths.

`.gitignore` re-ignored `.vscode/launch.json` in its custom section, cancelling
the allowlist above it. Inert while the file is tracked, but it would have
refused a re-add.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `typescript` dependency no longer being the compiler is surprising enough
to look like a mistake, so state why it is there, which command gets which
compiler, and the condition for deleting the arrangement.

Also corrects the build commands, which documented `tsc -b` although the
project has no references and the scripts call plain `tsc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mrafnadeem-apimatic
mrafnadeem-apimatic marked this pull request as ready for review September 22, 2026 12:00

@saeedjamshaid saeedjamshaid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Node 24 + TypeScript 7 migration

13 findings, 12 of them left inline. The CI ones are the real blockers — the Node 26 leg of check_build cannot work as written, and release.yml is a ticking version of the same bug.

Blocking

  1. check_build.yml bumped to Node 26 but still bootstraps pnpm with corepack enable — Corepack is no longer distributed with Node 25+.
  2. release.yml has the same corepack bootstrap, plus it pulls pnpm from the registry at release time.
  3. The comment justifying the removal of npm install -g npm@latest is factually wrong: the OIDC floor is Node 24.5, not 24.0.

Worth checking before merge
4. Renaming the matrix values renames every reported status check (ubuntu-latest / Node 22.12, Node 22 sample). If branch protection on dev/main requires those names, subsequent PRs will hang on checks that can no longer report — and the fix is in repo settings, not this diff.

One finding that has no line in the diff:

test/mocha.opts survived the ts-node removal. Its first line is --require ts-node/register, naming a package that no longer installs, and its --timeout 5000 contradicts the real --timeout 99999. The file is already dead (mocha dropped mocha.opts support in v8; this repo is on mocha 11 and passes flags on the command line), but it should have been deleted in the same commit that dropped ts-node and the "ts-node": { "esm": true } block from tsconfig.json.

The rest are inline, labelled Blocking / Should fix / Nit.


🤖 Review by Claude Code

Comment thread .github/workflows/check_build.yml
Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/test.yml
Comment thread src/hooks/node-version.ts Outdated
Comment thread src/infrastructure/portal-project-service.ts
Comment thread .ai/instructions.md Outdated
Comment thread .vscode/launch.json Outdated
Comment thread .gitignore
Comment thread test/actions/portal/generate.test.ts Outdated
@saeedjamshaid

Copy link
Copy Markdown
Collaborator

Second pass: is the change justified, and what are the risks?

Following up on the line-level review with a look at the premise rather than the diff. I verified the load-bearing claims rather than taking the description at its word; where I could not, I say so.

Is it justified?

The Node 24 floor: yes, clearly. The reasoning holds up — Node 22 is EOL 2027-04-30, inside the supported life of the major being cut. And this is not a major invented to carry the change: release.config.cjs:12-14 already lists a 1.x branch "once 2.0.0 prereleases exist", so a v2 was planned independently. A floor bump is exactly what a major is for.

TypeScript 7: justified, but it is the weaker half and it carries all the complexity. Two things worth putting on the record, both checked against the registry today:

  • TS 7.0.2 is the latest dist-tag, published 2026-07-08 — GA, roughly 2.5 months old, not a preview. That is a much better footing than "native compiler rewrite" suggests on its face.
  • @typescript-eslint/parser@8.70.1, the current latest, still declares typescript >=4.8.4 <6.1.0. So the dual-compiler arrangement is genuinely forced, not a stylistic preference. The PR's account of #10940 is accurate.

The framing I would add: these two changes have very different profiles. The floor is high-justification, low-risk and time-sensitive. TypeScript 7 buys roughly 7 s per build/type-check cycle — real developer experience, but no security or user-facing benefit — at the cost of carrying an inverted-name two-compiler setup for an unbounded period, gated on a third party with no ship date. Bundled, the TypeScript 7 decision inherits the floor's urgency.

The counterargument is decent: es2017 → es2024 is itself a semantics change that belongs in a major, and doing it once beats doing it twice. My read is keep it, but decide deliberately — d6ff5d8c is cleanly separable if the team would rather not carry the arrangement yet.

Risks

1. Class-field semantics — verified clean. This is the scariest part of the PR, so rather than trust the suite I walked the AST of both branches looking for eager field initialisers that read a constructor parameter property:

  • dev: exactly one hit — src/actions/sdk/quickstart.ts:29 — precisely the bug adb9a3cd fixes.
  • this branch: zero.

The five execute arrow-function fields are lazy (the body evaluates at call time, long after the constructor), so they are safe. The three language fields in the event subclasses are constructor-assigned with no base-class accessor to shadow. The claim checks out, and there are no other instances of the pattern. Scope of that check: src/ only, that pattern plus declared-but-unassigned subclass fields. The repo uses no decorators, so the third useDefineForClassFields hazard does not apply.

2. The 1.x maintenance branch does not exist yet. git ls-remote --heads origin returns only beta, dev and main. Published latest is 1.5.0. release.config.cjs and release.yml both already reference 1.x, but once main advances to 2.0.0 there is no correct commit left to branch v1 maintenance from without surgery. main is still on v1 today, so cut 1.x from main before this progresses toward a release branch. This is the most actionable risk outside the CI blockers.

3. Nothing publishes on merge — there is room to sequence. The base is dev, which is not in release.yml's trigger list (alpha, beta, main, 1.x). The 2.0.0 cut happens later, when dev reaches a release branch. That is what makes point 2 sequenceable rather than urgent.

4. The inverted names are protected only by documentation. typescript resolves to TypeScript 6; the tsc binary is TypeScript 7. Anyone reasoning "the typescript dependency is the compiler" is wrong, and pnpm outdated lists both under the real name. Worth a CI assertion — tsc --version starts with 7, the typescript package resolves to 6 — so that a bad pnpm update fails loudly instead of silently swapping compilers. Documentation is the right place to explain the arrangement, but the wrong place to enforce it.

5. The emit diff is the load-bearing claim, and it is not reproducible from the PR. "432 files, 81 differ, all cosmetic" is the entire basis for no semantic change to shipped runtime behaviour, and it rests on one local run recorded in prose. I could not reproduce it. If that claim matters — and for a compiler swap that emits the published artefact, it should — it belongs in CI or attached as an artefact rather than as a sentence in the description.

6. Declaration emit reaches consumers. tsconfig.json sets declaration: true and the package ships types: lib/index.d.ts. Almost certainly vestigial oclif scaffolding for a bin-only package — but if anything imports @apimatic/cli as a library, TypeScript 7's declaration emit is in the blast radius. Worth confirming whether types should simply be dropped.

7. The floor lands on merge, not at release. The new init hook hard-exits on anything below Node 24, so any contributor still on Node 22 or 23 is blocked from running the built CLI from the day this merges — well before 2.0.0 ships. Intended behaviour, but it is worth an explicit heads-up to the team rather than letting people discover it as a crash.

8. .claude is missing from the eslint ignores — confirmed. eslint.config.js:52-62 ignores lib, node_modules, portal-template, tmp and the test-* directories, but not .claude. Claude Code creates worktrees under .claude/worktrees by default, so posttest (eslint .) will sweep them and pnpm test will fail locally for anyone who uses one. The PR offers the one-line fix — worth taking.

9. Scope. Nine commits spanning engine floor, compiler swap, a latent runtime bug fix, CI matrix, VS Code config, .gitignore, mock-fs removal and docs. The mock-fs removal is a genuine prerequisite (Node 26.8's fs rewrite breaks its binding interception, and CI now tests 26), not creep. But adb9a3cd is a real runtime bug fix that deserves its own review attention — exactly the kind of change that gets waved through inside a large build PR. The description flags it, which is the right instinct.

Bottom line

Justified, and the engineering is better than the average migration PR. The description is unusually self-critical, and the one claim verifiable end-to-end — the class-field hazard — held up exactly. The blockers are the three CI findings in the line-level review, not the premise.

  • Before merge: fix the corepack and OIDC-floor issues, cut 1.x from main, add .claude to the eslint ignores.
  • Before release: get the emit-diff check into CI, or accept it explicitly as a one-time attestation.

🤖 Review by Claude Code

mrafnadeem-apimatic and others added 5 commits September 23, 2026 10:32
Corepack is not distributed with Node 25 and newer, so the Node 26 leg of
check_build could not bootstrap pnpm. Also correct the release floor: npm
11.5.1, which trusted publishing needs, first ships in Node 24.5.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Read engines.node from the manifest oclif has already parsed and compare
with semver, so package.json is the single source of the floor and a
minor-version floor is honoured. Test the hook's behaviour on both sides
of the range, and hold the CLI's range to the Node floors the portal
template's dependencies declare.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
launch.json goes back to being a personal, ignored file. mocha.opts named
ts-node and has been unused since mocha 8. ESLint now skips Claude Code
worktrees under .claude.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The prerender pass fetched pages from a preview server on `localhost`.
On macOS it listened on `::1` only; a stalled connect under load fell
back to 127.0.0.1 and was refused, failing the portal build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@saeedjamshaid saeedjamshaid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at a404e3f

All thirteen findings from the first pass are addressed, and all ten checks are green including the full six-cell matrix. Notes on what I verified rather than took on trust:

  • The engine guard now reads the manifest. this.config.pjson.engines?.node + semver.satisfies closes both the mirrored-constant and major-only-comparison findings in one change, and the three new tests exercise the outside-range, inside-range and no-engines paths for real. semver is in dependencies, not devDependencies — correct, since the init hook imports it at runtime in the published package.
  • The template-floor test is a better answer than the constant I suggested. Asserting engines.node is a semver.subset of every template dependency's declared range keeps the fact on record and checks it. I resolved all sixteen TEMPLATE_DEPENDENCIES against the installed tree: every range is open-ended upward (>=20.19, >=22.12.0, ^20.19.0 || >=22.12.0, >=20, >=0.10.0), so the assertion holds across the board today.
  • Both workflows are correct. Standalone pnpm ordered before setup-node, single node step, cache: 'pnpm' and cache-dependency-path retained.
  • Formatting is clean. No line over 120 characters in any changed file.
  • test/mocha.opts and .vscode/launch.json are both gone, .gitignore re-ignores the latter, and .ai/instructions.md now names the e2e call site.

Four new points on the five new commits, none of them blocking. The only substantive one is the preview-server bind.

Still outstanding outside this PR, for whoever has admin: cut 1.x from main while it is still on v1, and preserve the BREAKING CHANGE: footer when squashing — the repo's squash_merge_commit_message is COMMIT_MESSAGES so it is there by default, but trimming it turns a build type into no release at all rather than 2.0.0.


🤖 Review by Claude Code

// The prerender pass fetches every page from a preview server on `localhost`. Where that
// listens on `::1` alone, a connect that stalls under load falls back to 127.0.0.1 and is
// refused, failing the build; binding IPv4 leaves the fetch a single address to reach.
preview: { host: '127.0.0.1' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a second look — this pins the bind to IPv4, but the fetch target is still localhost, resolved inside TanStack Start. The two ends can still disagree.

On a host where localhost resolves only to ::1 — IPv6-only containers being the realistic case — the prerender fetch can now never reach the server, which is the exact inverse of the failure this fixes. What saves the common case is autoSelectFamily (default true on Node 20+) falling back to IPv4, so in practice this is very likely net-positive. But the mismatch is still there; it is just biased the other way now.

If TanStack Start exposes the prerender origin, pinning both ends to 127.0.0.1 would make it deterministic rather than dependent on Happy Eyeballs. If it does not, worth saying so in the comment — "binding IPv4 leaves the fetch a single address to reach" reads as though the fetch is constrained too, and it is not.

Separately: the trigger described ("a connect that stalls under load") is a flake, and there is no link to the run that caught it. The e2e does have real coverage here — test.yml:51 sets APIMATIC_E2E: '1' across all six cells — so a run URL in the commit body would let the next person tell whether it recurred or whether this fixed it.

for (const dependency of TEMPLATE_DEPENDENCIES) {
const required = readManifest(path.join('node_modules', dependency)).engines?.node;
if (required !== undefined) {
expect(semver.subset(supported, required), `${dependency} needs Node ${required}`).to.be.true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — worth a note in the test. semver.subset against an open-ended supported range has a sharp edge: the moment any template dependency adopts a capped range (^20 || ^22 || ^24 is a common shape), this flips to false and CI goes red.

That is arguably the test doing its job — CI tests Node 26, so a capped dependency genuinely is news. The trap is that the obvious way to make it green again is capping engines.node to match, which is usually the wrong call. A line saying what to do when it fires (check whether the cap is real, and prefer waiting for a dependency release over narrowing the CLI's own range) would save the next person that decision.

For the record, I resolved all sixteen entries against the installed tree and every declared range is open-ended upward today, so nothing is close to tripping it.

it('supports only Node versions every template dependency supports', () => {
const readManifest = (directory: string) =>
JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
const supported = readManifest('.').engines.node;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — readManifest('.') and path.join('node_modules', dependency) below are resolved against the current working directory, while every other test in this file builds absolute paths from os.tmpdir(). It works because mocha runs from the repo root, but it is the one test here that would break if that ever stopped being true.

There is also a subtler mismatch: this reads the top-level node_modules/<dep> entry, whereas runtimeProblem() resolves the same dependency through createRequire from the installed CLI. Under this pnpm layout they land on the same package, but they are different resolution mechanisms, so the test can in principle assert against a different copy than the code under test uses.


- name: configure pnpm cache
- name: setup node
uses: actions/setup-node@v4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — the pnpm action just above is SHA-pinned, but actions/checkout@v4 and actions/setup-node@v4 in this same job are still floating tags. Given the stated rationale for standalone pnpm was supply-chain hygiene specifically in the publishing workflow, the inconsistency stands out.

These are GitHub-owned actions and a v4 tag is a much smaller exposure than a registry fetch, so this is genuinely minor — but if the pinning is worth doing here, it is worth doing to all three.

@mrafnadeem-apimatic
mrafnadeem-apimatic merged commit 2528d6e into dev Sep 23, 2026
10 checks passed
@mrafnadeem-apimatic
mrafnadeem-apimatic deleted the build/update-stack branch September 23, 2026 08:10
saeedjamshaid added a commit that referenced this pull request Sep 23, 2026
#347 landed on dev with `build!:` for Node 24, so the next release from dev is
2.0.0 whatever this series does. The guard against merging dev into main
mid-series stands, but for the notes and the half-moved portal rather than the
version number; the three places that said otherwise now say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Sep 28, 2026
# [2.0.0-beta.1](v1.5.0...v2.0.0-beta.1) (2026-09-28)

* build!: move to Node 24 and TypeScript 7 ([#347](#347)) ([2528d6e](2528d6e))
* feat(quickstart)!: one funnel, and the language question ([#363](#363)) ([c270135](c270135)), closes [#361](#361)
* feat(sdk)!: retire v3 generation and let a publish record itself ([#359](#359)) ([c48f91b](c48f91b)), closes [#358](#358) [#358](#358) [apimatic-io#2233](https://github.com/apimatic-io/issues/2233)

### Bug Fixes

* **config:** keep the package configuration a publish did not write ([#392](#392)) ([bc52735](bc52735))
* **config:** nest publishing under the language ([#350](#350)) ([fbacf74](fbacf74))
* **deps:** take fumadocs 16.15.15, which fixes bold text in a built portal ([#388](#388)) ([fa178ab](fa178ab))
* **plugin:** ask only for the plugin's identity when the languages are recorded ([#389](#389)) ([d721819](d721819)), closes [apimatic/apimatic-io#2258](https://github.com/apimatic/apimatic-io/issues/2258)
* **portal:** bundle the Geist fonts instead of loading them from Google Fonts ([#366](#366)) ([95f2f7e](95f2f7e))
* **portal:** drop the TypeScript definitions panel from operation pages ([#353](#353)) ([042eb89](042eb89))
* **portal:** give a run the temp directory the filesystem names, not an alias ([#377](#377)) ([d71d23f](d71d23f))
* **portal:** keep the encoding a page slug carries ([#396](#396)) ([0bec5f4](0bec5f4)), closes [apimatic/apimatic-io#2270](https://github.com/apimatic/apimatic-io/issues/2270)
* **portal:** keep the header's links on one line ([#398](#398)) ([40dc472](40dc472))
* **portal:** keep the portal name on one line in the header ([#395](#395)) ([f3d352c](f3d352c))
* **portal:** keep the preview starting until its first page answers ([#399](#399)) ([5359442](5359442))
* **portal:** name the operations a shared page comes from, in words a spec's author knows ([#387](#387)) ([01197a7](01197a7))
* **portal:** offer the page actions on API reference pages ([#382](#382)) ([22f4873](22f4873)), closes [apimatic/apimatic-io#2247](https://github.com/apimatic/apimatic-io/issues/2247) [#383](#383)
* **portal:** refuse tag and operationId names a URL or a Windows folder cannot hold ([#400](#400)) ([0ea01b2](0ea01b2)), closes [apimatic/apimatic-io#2262](https://github.com/apimatic/apimatic-io/issues/2262) [#396](#396)
* **portal:** render a request body in a media type Fumadocs has no adapter for ([#386](#386)) ([9f89ad3](9f89ad3))
* **portal:** render an operation whose response example has no value ([#373](#373)) ([1dc84ad](1dc84ad))
* **portal:** render split OpenAPI specifications correctly ([#354](#354)) ([d97cda6](d97cda6))
* **portal:** stop printing an operation's raw description under its title ([#383](#383)) ([25b9f6e](25b9f6e)), closes [apimatic/apimatic-io#2242](https://github.com/apimatic/apimatic-io/issues/2242)
* **quickstart:** download the sample spec from its new openapi.json path ([#401](#401)) ([ebc6743](ebc6743))
* the release test's high and medium findings ([#375](#375)) ([7fe41e1](7fe41e1))

### Features

* code samples support ([#352](#352)) ([7b82568](7b82568))
* **config:** replace portal.json and plugin-config.json with apimatic.json ([#348](#348)) ([506f67f](506f67f))
* local plugin generation ([#358](#358)) ([5ba70de](5ba70de)), closes [#351](#351) [#346](#346) [#348](#348) [#318](#318)
* **portal:** build the documentation portal locally with Fumadocs ([#343](#343)) ([073d09a](073d09a))
* **portal:** build the SDK and Context Plugin pages from real templates and the portal artifacts ([#369](#369)) ([0efd7b3](0efd7b3))
* **portal:** fetch the artifacts from /portal-artifacts ([#361](#361)) ([0e823f3](0e823f3))
* **portal:** generate the SDK and context plugin pages ([#360](#360)) ([338b14a](338b14a))
* **portal:** give the portal block its v2 shape, with branding and tabs ([#355](#355)) ([d0fb189](d0fb189))
* **portal:** lead each SDK card with its install command ([#376](#376)) ([119e9d6](119e9d6)), closes [#375](#375)
* **portal:** let the root nav.json decide the tabs, and fix the nav.json QA findings ([#371](#371)) ([ecf7666](ecf7666))
* **portal:** order the sidebar from nav.json ([#346](#346)) ([306e4f9](306e4f9))
* **portal:** point to api transform when src/spec has no OpenAPI 3.x document ([#364](#364)) ([eefc917](eefc917))
* **portal:** the SDK Overview design, on the SDKs, language and plugin pages ([#391](#391)) ([fd3fe34](fd3fe34))
* **quickstart:** suggest validating with an AI agent when a spec fails ([#385](#385)) ([36fd2bb](36fd2bb))

### Reverts

* **portal:** offer the page actions on API reference pages ([#382](#382)) ([#397](#397)) ([c087ef9](c087ef9)), closes [#388](#388) [apimatic/apimatic-io#2247](https://github.com/apimatic/apimatic-io/issues/2247)

### BREAKING CHANGES

* `apimatic sdk save-changes` is removed, along with
`--codegen-version`, `--stability`, `--track-changes` and `--skip-changes`
on `sdk generate` and `--codegen-version`, `--stability` and
`--update-plugin-config` on `sdk publish`. Java, Ruby, Go and PHP cannot be
generated until they reach v4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): keep --stability, which outlived the version it came with

Retiring v3 took `--stability` with it on the grounds that there was one
answer left. There is one answer *today*: v4 renders every language at beta
and they reach stable one at a time, so the level is a choice that survives
the version being fixed.

The flag is back on both commands, defaulting to stable as before, and the
level a language offers is a table rather than a constant — `stableLevels`
of one is answered without asking, and the interactive flow puts the
question back the moment a language has two. `sdk publish` names the level
in its summary only when the user chose it, read from oclif's parse
metadata, because `--stability stable` and the default are the same string
and the doc's block has no such row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): keep --codegen-version, with one version in it

Retiring v3 removed the flag along with the version, on the grounds that a
choice of one is not a choice. But the flag is how a caller says which
generator they want, and there will be a v5: removing it makes that move a
release that changes what people get, rather than a value they pass.

`CodeGenerationVersion` now holds `V4` alone, and stays an enum so the next
one is a line in it. The flag is back on both commands, accepting `v4`,
defaulting to `v4`, and refusing `v3` by name rather than generating
something else quietly. `sdk publish` names it in its telemetry again.

It is not written down. `codegenVersion` left `apimatic.json` when the
service took over deciding the generator, so the flag says what to generate
with and nothing records what was used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(config): drop codegenVersion from the published schema
* `apimatic quickstart` no longer offers a choice between a
portal and an SDK. It always builds a portal, and `apimatic sdk generate`
is the command for an SDK alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(quickstart): ask which languages the portal covers

The one question quickstart asks besides the spec. Every language a plugin
can carry comes up checked, because a first portal covering everything it
can is what a single Enter should give, and the four it cannot are named in
the prompt rather than left out in silence.

The answer is written to `apimatic.json` through `recordLanguages` before
anything is built, because that file is what says which SDKs a portal
documents — every command after this one reads it rather than the answer
that produced it.

`languageLabel` moves to `types/sdk/generate.ts`, where both prompts that
name a language can reach it, and TypeScript is spelled the way its own
project spells it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(quickstart): name the plugin after the folder, without asking

The `plugin` block is what asks the service for a context plugin, so
quickstart has to write one — and the journey allows it no question about
plugins. The identity comes from the directory the user is standing in.

A folder name is not a plugin id. `Acme Payments` and `acme_payments` are
ordinary names and neither is the kebab-case `pluginId` has to be, so the
id is made from the name while the name itself is kept as typed — the id
goes to `gh repo create`, the name is what a reader sees. A directory whose
name survives none of that falls back, because writing an id the config
would then refuse is worse than not deriving one.

`0.1.0` for the version: every plugin starts somewhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(quickstart): keep generated files out of the repository

Every command that builds a portal writes `src/static/sdk/`,
`src/static/plugin.zip` and an expanded `./plugin` into the project. None
of it belongs in a repository, and `portal serve` writes it too — so
without this a preview leaves a clean checkout dirty.

`/plugin/` is the one that matters. `plugin publish` runs `git init` inside
that directory and pushes it as its own repository; a parent tracking it
would nest one repository inside another.

Quickstart writes the entries because quickstart is what makes the project.
It appends what is missing and rewrites nothing, so a `.gitignore` the user
wrote survives and re-running in an adopted directory stacks no duplicates.

The paths are ignored here before anything on this branch produces them:
the placement lands with the `/portal-artifacts` call, and a user whose
first build dirties their tree has already been failed by then.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(quickstart): end in the preview again, and say the coming languages under the answer

Two things, both about where a line belongs.

The rebase onto dev quietly took dev's ending for the wizard -- the one that
stops at the scaffold because "the wizard does not ask for the project's SDK
languages yet". It does, three lines above that comment. The handoff to
`portal serve`, and the `onAfterServe` the serve action took for it, had both
gone; nothing failed, because no test described the funnel's own ending. One
does now.

`Next Steps` loses its first two items with them. It told the user to name
their SDK languages and then start a preview; the wizard has just done both.
What is left is what to change now that the portal is on screen.

The coming-soon languages move out of the multiselect's message and under the
answer, in grey. Inside the message they rendered above the languages the user
had picked, in the green a submitted answer is drawn in, reading like a fourth
option rather than a footnote to the three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(portal): give quickstart the api transform hint, with the format it needs

Quickstart checks the spec itself before it writes the project, so the
transform hint portal generate and portal serve now give never reached it.
A Postman collection or a RAML file was told only that it was not an
OpenAPI document, with nothing to do about it.

Both now end on one sentence, which names --format=openapi3yaml: the
transform refuses to run without a format, so the command as printed
before would have failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(quickstart): ignore only what is still generated into the project
* engines.node is now >=24.0.0. Node 22 is no longer supported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): build the validation service once configDir is assigned

`validationService` was initialised from `this.configDir` in a property
initialiser, but `configDir` is a constructor parameter property. The code only
worked because TypeScript's downlevel emit assigns parameter properties before
property initialisers; under the native class-field semantics that a modern
target selects, `this.configDir` is still undefined at that point and the
service would be constructed with no config directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build: compile with TypeScript 7, keeping TypeScript 6 for linting

TypeScript 7 is the native compiler. On this repo it builds `src` in ~1.3s
against ~4.7s, and type-checks the suite in ~1.0s against ~4.8s. Emitted
output is unchanged apart from sourcemaps, temp-variable naming and
declaration ordering.

typescript-eslint cannot run on it: every published version, canary included,
declares `typescript >=4.8.4 <6.1.0` and throws at module init on TS 7, which
would take out `lint`, `posttest` and the pre-commit hook. So the two sit side
by side in Microsoft's documented arrangement — `tsc` is TypeScript 7 and
builds, bare `typescript` is the TypeScript 6 shim that only the linter loads.
Naming them this way rather than aliasing TypeScript 7 keeps the compiler each
script gets written down, and survives `pnpm update --latest`.

TypeScript 6 stopped sweeping node_modules/@types, so the test project names
the globals the suite needs; the source project resolves its types through
`node:` imports and needs no list.

`target` moves to es2024 now that the engine floor is Node 24, which also
selects native class-field semantics. `ts-node` goes: it drives the classic
compiler API, which TypeScript 7 no longer ships, and the suite runs on tsx.
`@types/mocha` was six years behind mocha and had to be named explicitly.

The `typescript.tsdk` pin is dropped. It is deprecated in favour of
`js/ts.tsdk.path`, inert unless a developer opts in by hand, and would now
point at a TypeScript 6 build within a patch of what VS Code already bundles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci: test on Node 24 and 26, and release on 24

The engine floor is Node 24, so the 22 legs go. The second leg is 26 rather
than the floor alone: 26 becomes LTS on 2026-10-28, inside this major's life,
so it is what most developers will be running. The job count is unchanged.

Releasing on Node 24 drops the `npm install -g npm@latest` step, whose only
purpose was that Node 22 bundles npm 10.9.8 while OIDC trusted publishing
needs 11.5.1. Node 24 bundles 11.19.0. The constraint moves to a comment on
the matrix, since pinning that job back to 22 would silently break publishing.

`npm-tag-latest.yml` carried a `node: [ '14' ]` matrix that no step consumed;
the job never sets up Node at all.

The standalone pnpm in test.yml is kept rather than reverted to corepack. Its
original reason is gone, but it is a SHA-pinned executable that needs no Node
of its own, where corepack would resolve pnpm from the registry at run time and
need the three-step setup-node dance the other workflows use. The comment now
states the reason that actually holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(vscode): repair the CLI debug configuration

`preLaunchTask` named a task the TypeScript extension provides, and that
provider is not registered when the extension stands down for TypeScript 7, so
the configuration would fail to start. The npm task provider supplies `build`
regardless, and still rebuilds before launching.

`program` pointed at `bin/run`, deleted in the ESM migration; it only launched
because Node appends `.js`. The forty commented-out argument lines carried
another machine's absolute paths.

`.gitignore` re-ignored `.vscode/launch.json` in its custom section, cancelling
the allowlist above it. Inert while the file is tracked, but it would have
refused a re-add.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record the two-compiler toolchain and how to remove it

The `typescript` dependency no longer being the compiler is surprising enough
to look like a mistake, so state why it is there, which command gets which
compiler, and the condition for deleting the arrangement.

Also corrects the build commands, which documented `tsc -b` although the
project has no references and the scripts call plain `tsc`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci: remove unused action

* fix: broken test

* fix: mock fs broken in node 26

* ci: install pnpm standalone in the build and release workflows

Corepack is not distributed with Node 25 and newer, so the Node 26 leg of
check_build could not bootstrap pnpm. Also correct the release floor: npm
11.5.1, which trusted publishing needs, first ships in Node 24.5.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(hooks): check the running Node against the declared engine range

Read engines.node from the manifest oclif has already parsed and compare
with semver, so package.json is the single source of the floor and a
minor-version floor is honoured. Test the hook's behaviour on both sides
of the range, and hold the CLI's range to the Node floors the portal
template's dependencies declare.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: name the e2e test that depends on the two-compiler arrangement

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: untrack launch.json and drop leftover config

launch.json goes back to being a personal, ignored file. mocha.opts named
ts-node and has been unused since mocha 8. ESLint now skips Claude Code
worktrees under .claude.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(portal): bind the prerender preview server to 127.0.0.1

The prerender pass fetched pages from a preview server on `localhost`.
On macOS it listened on `::1` only; a stalled connect under load fell
back to 127.0.0.1 and was refused, failing the portal build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0-beta.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants