Repository navigation
build!: move to Node 24 and TypeScript 7 - #347
Conversation
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>
saeedjamshaid
left a comment
There was a problem hiding this comment.
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
check_build.ymlbumped to Node 26 but still bootstraps pnpm withcorepack enable— Corepack is no longer distributed with Node 25+.release.ymlhas the same corepack bootstrap, plus it pulls pnpm from the registry at release time.- The comment justifying the removal of
npm install -g npm@latestis 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
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: 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:
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: Risks1. 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:
The five 2. The 3. Nothing publishes on merge — there is room to sequence. The base is 4. The inverted names are protected only by documentation. 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. 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. 9. Scope. Nine commits spanning engine floor, compiler swap, a latent runtime bug fix, CI matrix, VS Code config, Bottom lineJustified, 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.
🤖 Review by Claude Code |
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>
|
saeedjamshaid
left a comment
There was a problem hiding this comment.
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.satisfiescloses 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-enginespaths for real.semveris independencies, notdevDependencies— 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.nodeis asemver.subsetof every template dependency's declared range keeps the fact on record and checks it. I resolved all sixteenTEMPLATE_DEPENDENCIESagainst 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'andcache-dependency-pathretained. - Formatting is clean. No line over 120 characters in any changed file.
test/mocha.optsand.vscode/launch.jsonare both gone,.gitignorere-ignores the latter, and.ai/instructions.mdnow 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' }, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
#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>
# [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>
|
🎉 This PR is included in version 2.0.0-beta.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |



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:
Why TypeScript 7, and why TypeScript 6 is still here
Measured on this repo, cold, Windows, Node 24.12.0:
build(full emit)pretest(type-check suite)The two type-checking gates drop from ~9.5 s to ~2.3 s. To be precise about the claim:
pnpm testruns mocha throughtsx/esbuild and never touches the compiler, so the mocha run itself is not faster — the win isbuildandpretest.@typescript-eslintcannot run on TypeScript 7. Every published version, canary included, declarestypescript >=4.8.4 <6.1.0and throws at module init, which would take outlint,posttestand the pre-commit hook (#10940). So the two compilers sit side by side in Microsoft's documented arrangement:tscis TypeScript 7 and builds; baretypescriptis 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 survivespnpm update --latest. This is temporary —.ai/instructions.mdrecords 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
1be71d9).SdkQuickstartActionbuilt itsValidationServicefromthis.configDirin a property initialiser, butconfigDiris 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.configDiris undefined there. Worth reviewing on its own.targetmoves es2017 → es2024, which also selects native class-field semantics. That is a behavioural change beyond syntax; the suite (480 passing) is the evidence.useDefineForClassFields: falseis the one-line fallback if you would rather split it out.enginesis only advisory, so the floor is enforced at run time by aninithook, with a test asserting the constant matchesengines.node.npm install -g npm@lateststep — 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/nodeis pinned to 24.13.4, not 24.13.6 — newer releases are inside the repo's own 7-dayminimumReleaseAgewindow, 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.ymlis 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-stepsetup-nodedance. 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:posttestiseslint ., which also sweeps a stale.claude/worktreescopy in my working tree. CI checkouts do not have that, so CI is unaffected — but it does meanpnpm testfails locally for anyone with a worktree. Not fixed here; say the word and I will add.claudeto the eslint ignores.BREAKING CHANGE:
engines.nodeis now>=24.0.0. Node 22 is no longer supported.🤖 Generated with Claude Code