Skip to content

fix(deps): take fumadocs 16.15.15, which fixes bold text in a built portal - #388

Merged
sohail2721 merged 6 commits into
devfrom
fix/bump-fumadocs
Sep 28, 2026
Merged

sohail2721 merged 6 commits into
devfrom
fix/bump-fumadocs

Conversation

@sohail2721

@sohail2721 sohail2721 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes apimatic/apimatic-io#2260. Supersedes #384, which pinned the transitive dependency instead; close that one if this lands.

The bug

apimatic portal generate failed on generated/sdks/python.mdx with RangeError: Maximum call stack size exceeded, while quickstart on the same project was fine — quickstart ends at portal serve, a dev server, and never runs the production build.

Only Python failed because python.md is the only SDK doc carrying a **bold** (**identical method names**, in the async-client paragraph). csharp.md and typescript.md contain no ** at all. Nothing is Python-specific in principle: any bold in included SDK docs, or in user content, breaks the build.

Cause

mdast-util-to-markdown@2.1.3 (published 2026-09-27 09:31 UTC; the issue was filed at 15:01) moved emphasis handling off the strong handler onto a property of it:

strong.attention = attention                              // strong.js:8

phrasing checks for that property and, finding it, emits the markers and recurses into the node's children, which terminates. The handler is now only a fallback, and it does this:

export function strong(node, _, state, info) {
  return state.containerPhrasing({type: 'root', children: [node]}, info)   // strong.js:29
}                                                          //          ^^^^ the same node

fumadocs-core replaced every handler with a bare closure, copying no properties. .attention disappeared, the terminating branch was never taken, and the fallback handed the same node back forever:

phrasing → handle.attention? NO → state.handle(child) → fumadocs wrapper
  → strong → containerPhrasing({root, children:[THE SAME NODE]}) → phrasing → …

fumadocs-core@16.15.15 keeps the properties on the wrapper:

return Object.assign(wrapped, handler);

Reported upstream as fuma-nama/fumadocs#3604 — filed 15:03, closed 15:15, released 15:17.

What moves

fumadocs-ui peer-depends on fumadocs-core at exactly the same version, so both go 16.15.8 → 16.15.15. fumadocs-openapi@11.4.1 peers ^16.15.0 and fumadocs-mdx@15.4.0 peers ^16.15.3; neither needs to move.

Breaking changes, in patch releases

llms(source).index() became asynchronous, and that is why the second commit exists:

16.15.8 16.15.15
index() => string => Promise<string>
indexNode() => string => Promise<string>

Without the fix, llms.txt was written as the literal text [object Promise]. renderIndex is now async and the route awaits it. Nothing here calls indexNode.

The export surface moved too:

package change
fumadocs-core ./mcp added
fumadocs-ui ./components/toc/block added
fumadocs-ui ./components/ui/scroll-area removed

We import none of those, and the removed one is now an internal @radix-ui/react-scroll-area import, so nothing dangles. Underneath it is not a small bump: 24 of 91 core modules and 55 of 126 ui modules changed.

Release-age quarantine

pnpm-workspace.yaml sets minimumReleaseAge: 10080 (7 days), which would block 16.15.15 until about 2026-10-04. This adds the three packages to minimumReleaseAgeExclude rather than lowering the gate, so the policy still stands for everything else. The exemption should come out once those dates pass.

Worth saying plainly: that quarantine is the reason our lockfile never took 2.1.3 and our builds stayed green while user installs broke. It protects us and not them, because npm i -g has no such rule. That gap is worth addressing separately.

Verified

Built with the exact combination that was fatal — fumadocs 16.15.15 with mdast-util-to-markdown at 2.1.3:

  • APIMATIC_E2E=1 npm test: 1468 passing, 0 failing, eslint clean
  • apimatic portal generate succeeds: 7 pages, search index and llms-full.txt written
  • a global install of the packed tarball resolves 16.15.15 + 2.1.3 with the fix present

Rendered output diffed against a 16.15.8 build of the same project, comparing the visible text of every page:

  • same 9 pages, llms-full.txt byte-identical
  • zero words removed on any page
  • the 1856 added words are not content. 16.15.15 moves the heading and code-block copy-button labels out of aria-label into an sr-only span, so Copy, Anchor, Link and Text now live in the text layer. The buttons already existed, nothing became visible, and a heading's textContent now reads InstallationCopy Anchor Link. The search index, the TOC and the Markdown twins come from the MDX AST and carry none of it — measured as 0 occurrences in api/search.json, llms.txt, llms-full.txt and sdks/*.md

What is not covered

The render diff is one project (three SDK languages, one spec, a plugin page). A portal with richer content — versioned docs, deeper navigation, more MDX components — has not been compared. Given that a patch release silently changed a return type to a promise, a second look before merge is worth the time.

🤖 Generated with Claude Code

…ortal

`fumadocs-core` wrapped every markdown stringifier handler in a bare closure,
dropping the properties the handler carried. That cost nothing until
`mdast-util-to-markdown@2.1.3` moved emphasis onto one of them: with
`.attention` gone the `strong` handler kept handing the same node back to
`containerPhrasing`, and any page holding a `**bold**` exhausted the stack.
`portal generate` failed on every Python SDK page, whose docs always carry one.

16.15.15 keeps the properties on the wrapper, so 2.1.3 serializes as it should.
`fumadocs-ui` peer-depends on `fumadocs-core` exactly, so both move together.

The three packages are excused the release-age quarantine to take it now rather
than on 2026-10-04. The quarantine is why our lockfile never saw 2.1.3 and our
builds stayed green while installs broke; it is worth keeping for everything
else, so the exemption names only what this fix needs and should come out once
the dates pass.

Fixes apimatic/apimatic-io#2260. Upstream: fuma-nama/fumadocs#3604.

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

`llms(source).index()` answered with a string in 16.15.8 and answers with a
promise in 16.15.15, so the route wrote `[object Promise]` into `llms.txt`.
`indexNode` changed the same way; nothing here calls it.

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

Copy link
Copy Markdown
Contributor Author

Second verification, on a much richer portal

The earlier render diff was a 7-page project. Repeated it on a 123-page one — a Twilio Conversations portal: an 856 KB OpenAPI document, 14 hand-written Markdown pages across guides/, recipes/ and sample-apps/, 176 bold markers (the construct that caused the bug), 64 headings, 76 code fences, 7 tables, blockquotes, images and 37 links, plus SDK pages for three languages and a plugin page.

Built the same project twice — dev at fumadocs 16.15.8, this branch at 16.15.15 — and compared the visible text of every page:

result
pages 125, identical set
pages changed 122
words removed 0
words added 1856
distinct words added Copy (698), Anchor (460), Link (460), Text (238)
llms.txt byte-identical, 20,672 bytes
llms-full.txt 2 lines differ

Every addition is a copy-to-clipboard label on a heading or code block. Nothing was lost on any page.

The two llms-full.txt lines are an improvement, not a regression:

- ::: </Accordion>
+ :::
+ </Accordion>

A ::: directive terminator belongs on its own line; emitting it followed by a JSX close tag on the same line was malformed.

llms.txt being byte-identical also confirms the async fix in the second commit works against the real 16.15.15 — it is the file that was otherwise written as the literal [object Promise].

APIMATIC_E2E=1 npm test against genuine 16.15.15: 1468 passing, eslint clean.

Correction to the earlier numbers

My first run of this comparison was invalid and I want that on the record: after switching back to this branch I had pushed and packed without re-running pnpm install, so node_modules still held the previous branch's 16.15.8 while package.json declared 16.15.15. I was reading the declared version, not the installed one, and comparing 16.15.8 against itself. The numbers above are from builds where the installed version was verified directly. The local test figure quoted earlier in this PR had the same flaw; the run above does not.

@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.

Please correct the description, since it is the record of what this bump changed:

  • The API-change table misses the one change that needed code: llms(source).index() now returns a promise, which 6e16ecc handles in llms.server.ts and llms[.]txt.ts.
  • "No content changed" and "new copy-to-clipboard buttons" aren't accurate. The heading and code-block buttons already existed. 16.15.15 moves their label from aria-label into an sr-only span inside the element, so every heading's text now ends in "Copy Anchor Link" (for example, <h2 id="installation"> reads InstallationCopy Anchor Link), and code blocks gain "Copy Text".

Comment thread package.json
Comment thread package.json
"fumadocs-mdx": "15.4.0",
"fumadocs-openapi": "11.4.1",
"fumadocs-ui": "16.15.8",
"fumadocs-ui": "16.15.15",

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.

In 16.15.15, ViewOptionsPopover builds the "Read {url}" prompt from window.location.href unless a pageUrl prop is passed. 16.15.8 used new URL(pathname, window.location.origin). We don't pass pageUrl (portal-template/src/routes/$.tsx:145), so a reader's query string and hash, such as tracking or preview parameters, now go to ChatGPT, Claude and the other vendors. Please pass a pageUrl built from origin and path so only those are sent, as before.

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.

Still open: #382 moved the popover to portal-template/src/components/page-header.tsx:18, and it still renders <ViewOptionsPopover markdownUrl={markdownUrl} /> with no pageUrl, so the prompt still carries the reader's full URL, query string and hash included.

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.

@sohail2721 this is the last open point from my review, and it isn't in your "Still open" list, so flagging it again. On 5ab53eb, portal-template/src/components/page-header.tsx:18 still renders <ViewOptionsPopover markdownUrl={markdownUrl} /> without pageUrl, so the prompt sent to the AI vendors carries the reader's query string and hash.

Comment thread pnpm-lock.yaml Outdated
Muhammad Sohail and others added 2 commits September 28, 2026 13:52
fumadocs-ui 16.15.15 asks for `^1.47.0` where 16.15.8 asked for `^1.39.0`, so
the pin at 1.43.0 no longer answered it and a second copy was resolved: the
template and fumadocs-core drew their icons from one, fumadocs-ui from the
other, and the portal bundled both. 1.47.0 answers every range at once.

`pnpm dedupe` then collapsed what the duplicate had split, which also settles
a second copy of `yaml` on 2.9.1, within the range already declared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing built in the suite carried a bold or italic word, so a serializer that
mangled the markers and later looped over one until the stack ran out both went
unnoticed here. The fixture now carries emphasis, and the page, its Markdown
twin and llms-full.txt are read back for it.

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

Copy link
Copy Markdown
Contributor Author

Both addressed — thank you, the second one in particular found something neither the pin nor this bump was accounting for.

lucide-react duplicate

Confirmed, and it is this branch that introduced it:

wants our pin 1.43.0
fumadocs-ui@16.15.8 ^1.39.0 satisfies — one copy
fumadocs-ui@16.15.15 ^1.47.0 does not — two copies

Pinned to 1.47.0 rather than 1.48.0: 1.48.0 is 4 days old and would need a third minimumReleaseAgeExclude entry, while 1.47.0 is 11 days old and satisfies ^1.47.0 on its own.

That alone left fumadocs-openapi and @fumadocs/api-docs still resolving 1.43.0, so pnpm dedupe followed. The lockfile now holds exactly one lucide-react, 1.47.0, with no edge pointing anywhere else. Dedupe also collapsed a duplicate yaml, settling it on 2.9.1 — inside the ^2.8.0 already declared, and disclosed here since it is not a fumadocs change.

Emphasis coverage

Added your line to SDK_DOCS and a test reading it back from sdks/typescript.md, llms-full.txt and the rendered page.

It behaves exactly as you said:

  • this branch — passes
  • dev's dependencies, same test — fails: expected '# TypeScript SDK…' to include 'Use **identical method names**…'

And your *&#x2A; is right. Isolating the serializer at each version:

wrapper drops .peek (16.15.8)   "Use **identical method names*&#x2A;, &#x2A;*(identical)*&#x2A; and &#x2A;`code`* here."
wrapper keeps .peek (16.15.15)  "Use **identical method names**, **(identical)** and *`code`* here."

Worth drawing out, because it changes what this PR is: dev corrupts emphasis today. Any bold or italic in SDK docs already reaches sdks/*.md and llms-full.txt with &#x2A; in place of the markers — silently, in the files the AI page actions and llms.txt consumers read. The stack overflow in #2260 was the same dropped-properties defect becoming fatal once 2.1.3 made .attention load-bearing; .peek had been dropped all along and was merely disfiguring the output.

So this is not only a fix for a build that fails — it is a fix for output that was already wrong.

APIMATIC_E2E=1 npm test: 1469 passing, eslint clean.

@sohail2721

Copy link
Copy Markdown
Contributor Author

Merged dev in (5ab53eb) and re-verified. The branch was three commits behind, and one of them mattered.

Why it needed re-verifying

dev had moved on since this branched from d71d23f:

#383 stop printing an operation's raw description under its title
#386 render a request body in a media type Fumadocs has no adapter for
#382 offer the page actions on API reference pages

All three touch portal-template/, and #386 is a workaround for a Fumadocs limitation — exactly the kind of code a Fumadocs bump can invalidate, if 16.15.15 now ships the adapter it compensates for or changes the shape it hooks into. Everything verified earlier in this PR was measured against a base that no longer existed.

The merge was clean; test/e2e/portal-build.test.ts auto-merged despite both sides editing it.

Re-verified on the merge

APIMATIC_E2E=1 npm test: 1475 passing (up from 1469 — dev's new cases), eslint clean, build clean.

#386's workaround specifically, on 16.15.15:

mediaAdapters
  ✔ sends text and a file as the reader entered them
  ✔ sends an object the playground built from the schema as JSON
  ✔ names each type the way Fumadocs looks it up

Plus the page actions from #382, and the emphasis test, all passing together.

Render diff, redone against the new dev

Same 123-page Twilio portal, built from this merge and from dev at 22f4873:

pages 125, identical set
changed 122
words removed 0
words added 1856 — Copy (698), Anchor (460), Link (460), Text (238)
llms.txt byte-identical, 20,672 bytes
llms-full.txt the same 2 lines, ::: moved onto its own line

Unchanged from the earlier measurement, now against a base that exists. The additions are still only copy-button labels.

Still open

  • CI is running on the merge; ubuntu passed before the merge, the rest are in flight.
  • This needs an approving review — reviewDecision is REVIEW_REQUIRED.
  • The minimumReleaseAgeExclude entries should come out after 2026-10-04.
  • fix(deps): pin mdast-util-to-markdown, which a published install lets drift #384 is redundant if this lands.
  • Not covered by any of the above: the icons. lucide-react moved 1.43.0 → 1.47.0, and the template draws ChevronRight, Code, Download and Package from it. The builds prove those still exist; whether any was redrawn is a visual question a text diff cannot answer, and worth a glance at a built portal before merging.

@saeedjamshaid

Copy link
Copy Markdown
Collaborator

Following up on the description point from my review: the new llms().index() section and dropping "no content changed" cover most of it. One part is left. The Verified section still calls Copy, Anchor, Link and Text labels on new copy-to-clipboard buttons. Those buttons were already there: 16.15.15 moves their label from aria-label into an sr-only span, so every heading's text now ends in "Copy Anchor Link" (<h2 id="installation"> reads InstallationCopy Anchor Link), and code blocks gain "Copy Text".

@sohail2721

Copy link
Copy Markdown
Contributor Author

You are right, and the correction is bigger than the wording — thank you for not letting it stand.

I verified the component. 16.15.8 put the label on the button:

jsx("button", { "aria-label": t("Copy Anchor Link", …), children: <LinkIcon/> })

16.15.15 moves it inside as real markup, and adds aria-live:

jsxs("button", { "aria-live": "polite", children: [<LinkIcon/>,
  jsx("span", { className: "sr-only", children: isChecked ? t("Copied Anchor Link") : t("Copy Anchor Link") })] })

So the button was always there, and what changed is where its label lives. The rendered heading is now:

<h2 id="installation" class="group/heading …">
  <a data-card="" href="#installation">Installation</a>
  <button aria-live="polite" …>[icon]<span class="sr-only">Copy Anchor Link</span></button>
</h2>

Its textContent is InstallationCopy Anchor Link, exactly as you said. My diff read extracted text, so it counted those spans and I reported them as new buttons. Wrong on both counts — nothing was added, and nothing is visible.

The upstream reason looks deliberate rather than accidental: an aria-label never announces its own change, so the Copy → Copied transition was silent. An aria-live region with real text announces it. The cost is that the heading's accessible name now absorbs the button's label.

Where it does and does not reach

Since "every heading's text now ends in Copy Anchor Link" could plausibly leak into what we index and export, I measured rather than assumed, on a built portal:

api/search.json 0 occurrences
llms.txt, llms-full.txt, sdks/*.md 0
TOC entry for #installation "Installation"
the heading's own <a> "Installation"
sr-only spans on the page 5, one per heading

Everything built from the MDX AST — the search index, the TOC, the Markdown twins — is untouched. The change is confined to the rendered DOM's accessible text. Visually nothing moves, because sr-only is visually hidden.

So the honest summary of that 1856-word delta: not one word of content was added or removed. It is the same labels, relocated from an attribute into the text layer, and my diffing method could not tell those apart. I have corrected the Verified section to say so.

Two consequences worth keeping in view, neither blocking:

  • A screen reader reading the heading now hears the button's label as part of it.
  • Anything asserting on a heading's textContent will see the suffix. Nothing in our suite does today, but it is a trap for whoever writes such a test next.

@saeedjamshaid

Copy link
Copy Markdown
Collaborator

On the icons question in your last "Still open" list: I compared every icon between lucide-react 1.43.0 and 1.47.0. 17 changed shape, and none of them is among the icons imported by the template (ChevronRight, Code, Download, Package), fumadocs-ui 16.15.15, fumadocs-openapi or @fumadocs/api-docs. Aliases such as Edit → square-pen were followed to their real files. No icon the portal draws was redrawn, so that one needs no visual check.

…hole URL

16.15.8 built the "Read {url}" prompt from `new URL(pathname, location.origin)`.
16.15.15 falls back to `window.location.href` unless a `pageUrl` prop is given,
so with this bump a reader's query string and hash — tracking and preview
parameters among them — began reaching ChatGPT, Claude and the other vendors the
popover links out to.

The page header now names `pageUrl`, built from the origin and path as before,
and keeps the component's own fallback for a render with no `window`.

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

Copy link
Copy Markdown

@sohail2721
sohail2721 merged commit fa178ab into dev Sep 28, 2026
10 checks passed
MuHamza30 added a commit that referenced this pull request Sep 28, 2026
…397)

This reverts commit 22f4873.

#388 later gave the popover the page's own address in page-header.tsx,
which this deletes, so that moves back to the docs page's action row
and its check reads routes/$.tsx.

Refs apimatic/apimatic-io#2247

Co-authored-by: Claude Opus 5.5 (1M context) <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