Repository navigation
feat(portal): give the portal block its v2 shape, with branding and tabs - #355
Merged
Merged
Conversation
saeedjamshaid
added a commit
to apimatic/sample-docs-as-code-portal
that referenced
this pull request
Sep 24, 2026
apimatic/apimatic-cli#355 trims the portal block to the settings a portal needs for its name, address and brand, and refuses the rest as unknown keys. src/apimatic.json loses the colour preset, the fonts, the layout, the home page's call to action, the api options and the advanced tokens; every one held its default except the call to action, and the home page already links to the authentication guide. The README no longer lists fonts and layout among what the file sets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The plan was written against portal.json and plugin-config.json, before apimatic.json (#348), the nav.json work (#346) and the publishing level of language entries (#350) landed. Rewritten in place rather than amended again: - The block is the `portal` block of src/apimatic.json, nested into site, brand, navigation, home, api, ai and advanced. The flat dev keys are plain unknown keys, with no near-miss hints. - The portal requires the shared top-level `languages` block with at least one entry, written by hand for now; quickstart stops after the scaffold instead of serving a project that cannot validate. - Tabs come from the root nav.json rather than a sections list: apimatic:api and apimatic:sdks (renamed from apimatic:pages) place the API and SDK tabs, a top-level folder opts in with "root": true, and the Home, Guides and SDKs labels are fixed. - apimatic.schema.json covers the whole file, strict only for `portal`. - Delivered as one PR in seven reviewed steps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The `portal` block of src/apimatic.json becomes site, brand, navigation, home, api, ai and advanced, each parsed by its own value object under src/types/portal/config/. Every problem is reported at once by its dotted path; the flat keys the block had on dev are plain unknown keys, with no hint. The portal now requires the top-level `languages` block with at least one entry, keyed by an SDK language, and reads that block's findings along with its own. An entry with no publishing record counts. A malformed plugin block still never fails a build. The site's name and description default to what the only specification says about itself, the description stopping at its first blank line; with several specifications the name is required, so the specifications are now read before the block is judged. The logo, a dark logo and the favicon are all checked for on disk, and every missing one is reported with the setting that names it. Color parses the primary's accepted forms and picks a contrasting foreground, and the font table carries each family's Google Fonts weight axis, checked against the service. The template is untouched: the browser identity keeps its fields until the template reads the new keys. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
apimatic.schema.json covers the whole file: the root and the plugin and languages blocks stay lenient, as the CLI reads them, while the portal block refuses unknown keys at every level. It ships in the package and is addressed by APIMATIC_SCHEMA_URL. A conformance test runs the schema and the parsers over the same fixtures, with the deliberate gaps (colour channel ranges, the several-specs name rule, the one-language minimum) pinned by their own cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CLI now writes two files into the build project instead of
substituting the identity into portal.ts: portal.identity.json, which the
browser imports whole and so holds nothing that addresses the build
machine, and src/styles/theme.css, which PortalStylesheet renders from the
block: the colour preset, the glass styles, the two font families, and
each mode's primary colour with a contrast-picked foreground followed by
the raw token overrides. portal.config.json keeps only what the build
set-up reads, and gains the API options for a later step.
The template applies the rest: a layout module that picks docs,
notebook, notebook with the tabs in the navbar, or glass, together with
that layout's page components; both logos; the favicon with its type; the
Google Fonts stylesheet; header links; a colour mode that hides the switch
and the hotkey when forced; the home page's call to action; and the page
actions setting under its new name.
A token value is written into the stylesheet as it stands, so the parser
and the schema now refuse one holding ';', '{', '}' or a comment.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A second page-tree transformer regroups the root, once nav.json has ordered it, into one root folder per tab: Home for the index page, the API reference, SDKs for the generated pages, a tab of its own for each top-level folder whose nav.json sets "root": true, and Guides for everything else. Tabs are ordered by where each one's first node sits, with Home leading unless the file places the index page itself. A tab's index page moves to the top of its sidebar, and a project without an index page gets a node for the fallback home page so it shows a tab bar too. The layouts are given an explicit tab list, since Fumadocs' own skips a tab that holds only folders, as the reference always does. The CLI accepts 'root' only on a folder directly under content/, other than content/api/, and refuses an 'index' entry in a tab's nav.json. The walk now tells a top-level folder from a nested one. The injected-pages token is renamed apimatic:sdks; the old name is an unknown token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
openApiSection takes the API options. The document bundleSpecification makes is filtered before Fumadocs sees it: the operations the block leaves out (deprecated ones unless showDeprecated, those marked x-internal unless showInternal) are removed from paths and webhooks, and a path item left with none is dropped. Filtering the document rather than the pages keeps a hidden operation out of everything built from it: its page, its sidebar row, an emptied tag folder, and every other page's payload. groupBy passes through to Fumadocs in place of the fixed 'tag', and the prerender list goes through the same function, so the URLs it emits follow both settings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A colour or font tweak used to need the preview restarted. portal serve now watches src/apimatic.json and, on each save, re-runs the config half of the source resolution: the portal and languages rules, the existence of every file the block names, and the site name and description taken from the specifications read at startup. An edit is therefore accepted exactly when a build would accept it. An accepted edit rewrites portal.identity.json and theme.css, and the dev server reloads them. A refused one is reported as portal generate would report it, and the preview keeps what it last accepted. Each file is written only when its contents change, so a plugin command rewriting its own block changes nothing and prints nothing. A change to portal.api says a restart is needed, since the reference pages and the prerender list are made once. The watch is on the directory, so an editor that saves by renaming a new file over the old one keeps being seen. A save's events are gathered before it is reported, one save is handled at a time, and closing waits for a save still being handled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The quickstart scaffold writes $schema first, then schemaVersion and the portal block with every default spelled out, so the file shows what can be set and an editor completes and checks the rest. Nothing in the wizard asks for the project's SDK languages yet, and a portal is not built without one, so the wizard no longer starts a preview that would refuse the project it just wrote. It keeps its Node-version and entitlement checks, then ends with next steps: the languages entry to add, what it also means for the plugin commands, and the portal serve command to run. The serve action loses the hook only quickstart used. The README's upgrade notes name the nested block, the languages requirement and the tabs, and the serve description is updated. A second end-to-end build brands a portal in every way the block allows: glass, forced dark, a preset with a primary, fonts, links, a call to action on the fallback home page, and hidden deprecated and internal operations. The default build now also checks the tabs in the header, the default theme, and that the browser gets the identity and nothing from the build-only config. The plans this one changes carry dated amendments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Validation let through values that break the portal after it builds: - A token value with an unclosed parenthesis, bracket or quote, or a trailing backslash, ran past its declaration and failed the build or dropped both colour-mode blocks. A value is now made of the characters every CSS colour is written with, and its parentheses must balance. - static//logo.png found the file and was served as //logo.png, on another host. Every name on a static path must be there, and each is escaped in the URL. - A file named in another case passed on Windows and macOS and 404s on the host. It is now reported, with its spelling on disk. - A link path is resolved as a browser resolves it, so /\host is refused, and a full address must carry its //. - The primary colour follows CSS's own rules for the comma and space forms, and takes any hue unit. The schema follows each rule it can express. The API filter follows a $ref to the end of its chain, reads the keys beside it, and drops a tag only hidden operations carried, since every page's payload carries the tags. Grouped by route, the operations on / reach the sidebar, and a path and a webhook sharing a page are refused. A tab no longer opens on the page with the same path in the tab being left, and Home leads when index names a folder. Token overrides reach the sidebar under every preset. portal serve reads the file again once it watches it, coalesces saves, says when the watch fails or a new static directory needs a restart, and stops watching before it says it stops. A prerelease scaffold names its own version in $schema, which jsDelivr serves, where @2 resolves to nothing until 2.0.0 ships. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…PI filter pathItemShown and tagsUsed each walked the fixed methods and additionalOperations by hand, which put both over SonarCloud's cognitive complexity limit (22 and 27 against 15). operationsOf now lists them for both, and the copy without hidden operations is built by withoutHidden. Behaviour is unchanged; a test covers an emptied additionalOperations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
firstPageUrl in tabs.ts chose the next URL with a nested ternary, which SonarCloud flags (S3358); firstPageIn in navigation.ts had the same shape. Both now return early. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The missing-static-file check compared two paths through toString(), which the value-object conventions rule out and SonarCloud flags for the null check beside it (S6582). FilePath.isEqual compares the directory and the name exactly, so a name that differs only in case is still reported. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ternary SonarCloud flags the nested ternary in balanced() (S3358). It now branches on each parenthesis. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SonarCloud prefers includes() to a some() that only checks for one value (S7765). Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three messages quoted their allowed values with a template literal nested in the message's own, which SonarCloud flags (S4624) in two of them. quotedList in config/fields.ts now does it for all three. The messages are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…viewConfig watchConfig kept the last accepted config and whether the last save was refused in closures, and decided in the action whether an edit needs a restart, names static files the preview does not serve, or should be announced as applied. PreviewConfig in src/types/portal now holds that state and answers those questions, with unit tests of its own; the action is left reading the file, applying it and prompting. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The case-mismatch test expected a quote right after "which is spelt ", but picocolors colours output on Windows and in CI whatever the terminal, so a colour code sits there and the test failed wherever colour was on. The report is now read with its escape codes stripped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The quickstart prompts built the path of src/apimatic.json themselves to print it in the next steps. PortalSourceContext.scaffold now answers with the file it wrote, as a context's write operations do, and the action passes it on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ping it The file watch drops whatever its handler throws, so a fault that no Result carried, such as a directory turning unreadable while the static files are checked, left the preview stale without a word. The serve action now catches it and reports it as a change it could not apply; the watch keeps running. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The template's Portal interface and ApiOptions were held to what the CLI writes only by reading the template source with regexes for top-level keys and union members, so a field renamed inside logo, favicon, a link or api went unnoticed until an e2e build. The identity types now sit in portal-types.ts, which imports no generated file, and a test asserts both types are identical to the CLI's, nested fields and union members included, where pretest's tsc checks it. BuildIdentity is now picked from Portal rather than declared again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The one caller that set it read reportSourceProblem(problem, directory, false), which said nothing about what false turned off. The flag is now an option named at the call site, still defaulting to true. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ings do Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fonts.googleFontsUrl returned a string while SiteConfig.origin returns the other absolute address the block derives as a UrlPath. Both are now rich types, unwrapped where the identity is written out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
isWebAddress requires the address to start with http:// or https://, so testing that it does not start with '/' first changed nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The missing-languages error and quickstart's next steps each spelt out the same example entry. LANGUAGES_EXAMPLE in portal-languages.ts now serves both, so they cannot drift apart. The text is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nnot say
The project's rule is to comment only a reason the code cannot show. This
branch's comments often restated the code (a CSS font-family accessor
documented as the CSS font-family value, a namespace class headed by its
settings path), or told the history of a fix ("was not enough", "without
this it shipped"). Those are gone or cut down to the reason, across
src/ and portal-template/. Every constraint and workaround stays: the
jsDelivr prerelease rule, the .invalid origin, the #nd-sidebar specificity,
Fumadocs' $ref projection, and the like. Only comment lines change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One SDKs tab from a generated sdks/ folder: an overview, a page per
configured language in the order written, and a context plugin page when
apimatic.json has a plugin block. Placeholder templates in a shipped
portal-pages/ directory, rendered by {{key}} substitution before MDX ever
sees them, compiled as a second Fumadocs collection over a relative
'generated' literal so no build path is published, positioned by
apimatic:sdks, prerendered, and regenerated under portal serve.
Implementation waits for #355 to merge.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The plan put the context plugin page at /sdks/context-plugin, inside the SDKs tab. It now has its own folder and tab at /context-plugin, positioned with a new apimatic:plugin token, and the synthetic SDKs tab is dropped because the CLI writes only folders. Each token claims its own section's folder, unnamed sections keep a fixed order before the API reference, and /context-plugin is reserved like /sdks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6 tasks done
MuHamza30
reviewed
Sep 24, 2026
MuHamza30
reviewed
Sep 24, 2026
MuHamza30
reviewed
Sep 24, 2026
MuHamza30
reviewed
Sep 24, 2026
MuHamza30
reviewed
Sep 24, 2026
MuHamza30
reviewed
Sep 24, 2026
MuHamza30
requested changes
Sep 24, 2026
…site A link such as /.//evil.com stays on the portal when resolved, but its dot segment drops away and leaves //evil.com as the path the header writes out, which a browser opens as another host. Refuse a resolved path that starts with two slashes. The schema's pattern cannot follow dot segments, so it refuses an empty segment anywhere in the path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A group tag carries no operations of its own, so it was never counted as emptied: with every tag under it hidden, its name and description still rode along in every page's payload. Drop a group no remaining operation carries once every tag under it has gone, up the chain, and from x-tagGroups too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ches applyConfig rewrote portal.identity.json and theme.css in place, which truncates each before writing it, so the watching dev server could read one empty or half-written. Write each beside its target and rename it over, as replaceContents does. The build-time write stays as it was: it runs before the dev server starts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The file was committed without the pre-commit hook, so the hook would fold this reformat into the next change to it. No test changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
apimatic.json's document, the portal block's fields and the OpenAPI document each tested for a JSON object on their own. They now share isJsonObject from utils/json-utils. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ayload The payload slimmer listed the operation methods without 3.2's query, and it keeps any key it does not know as a method, so a query operation and everything under additionalOperations rode along, with the schemas they reach, in the payload of every other page at that path. It now takes the filter's list from one module, and narrows additionalOperations to what the page renders. The template's three JSON object checks become one as well. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortalSourceContext split a file's display path into strings to walk the directories, then rebuilt a FileName from the name it matched, which the value-object conventions rule out. The walk now lives in FileService.spelledOnDisk, which carries the matched DirectoryPath and FileName through rather than rebuilding them, and keeps its string comparisons at the file system boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PortalLanguages worked out whether each language is published, and re-checked the shapes the document had already validated to do it, but nothing asks. PortalConfig's siteDescription, siteOrigin, navigationSettings and aiSettings were read only by tests: their values reach the portal through identity(), which the tests now read instead. PortalLanguages keeps all(), which the generated pages read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…does The parser refused a resolved path starting with two slashes while the schema refused any empty segment, so /docs//page built but was marked invalid in an editor. The parser now refuses the empty segment itself, read as a browser reads it, which also covers every dot-segment route to //host; the two rules agree on every path the fuzz tried. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A tag that might go stayed while any tag not yet gone named it as its parent, itself included, so two emptied groups naming each other, or one naming itself, kept their names and descriptions in every payload. The tags that stay are now grown upward from those that stay regardless. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fumadocs builds no page for an operation under additionalOperations, so keeping the one a page renders was a branch nothing could reach. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…JsonObject Two object checks written out by hand were left behind by the shared one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
As .ai/instructions.md asks: one line of why, and none that restates the code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
MuHamza30
approved these changes
Sep 24, 2026
sohail2721
pushed a commit
that referenced
this pull request
Sep 24, 2026
The upgrade notes collided: #355 rewrote the `portal` block for its v2 shape while this branch still described the v1 one. Dev's text wins, because that is what the block is now, and the one thing only this branch knows is folded into it -- `plugin generate` writes the `languages` block too, not just `sdk publish`. Co-Authored-By: Claude Opus 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>
|
🎉 This PR is included in version 2.0.0-beta.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



The
portalblock ofsrc/apimatic.jsontakes the shape it keeps for 2.0:site,brand,navigationandai, nine settings in all. A portal carries its name, address and description, a logo per colour mode, a favicon, a primary colour, an optional forced colour mode and header links. The top level of the site renders as tabs taken fromnav.json. The spec is.ai/plans/portal-config.md; its section 15 lists what was cut for the first release and why.The block
site.namespec/holds severalsite.urlrobots.txtsite.descriptionbrand.logosrc/static/brand.faviconsrc/static/brand.colors.primary#rgbor#rrggbb, once or per mode; the text on it is picked for contrastbrand.colorModelight,darkorboth; a forced mode hides the switchbothnavigation.links{ label, url }; another site opens in a new tabai.pageActionsFixed rather than configurable in this release: Fumadocs' notebook layout with the tabs in the header, the neutral theme, Geist and Geist Mono, and an API reference grouped by tag that documents deprecated operations and leaves out those marked
x-internal: true. Each is what its setting's default was, so any of them can come back as a setting without changing a portal that leaves it out.What changes for a user
dev(title,siteUrl,aiPageActions, …) are reported as unknown settings, with no hint; that block never shipped. Every problem is reported at once, each by its dotted path.languagesentry. At least one is required, for example"languages": { "typescript": {} }. Nothing writes it for a portal project yet, so quickstart stops after the scaffold and shows the entry to add rather than starting a preview that would refuse the project.nav.jsonsets"root": truegets a tab of its own.apimatic:pagesis renamedapimatic:sdks.$schema, so editors complete and check the file fromapimatic.schema.json, which ships in the package and covers the whole file.portal servere-applies edits to theportalblock. An edit a build would refuse is reported, and the preview keeps what it last accepted. A newly createdsrc/staticis reported as needing a restart.llms.txt. Two operations sharing an operationId, or one listing a tag twice, would put two pages at one path, and are refused rather than one page being dropped.Commits
apimatic.json.Color,PortalLanguages, and the name and description taken from the spec.apimatic.schema.json, with a test that runs the schema and the parser over the same fixtures.portal.identity.jsonandtheme.cssreplace the literal substituted intoportal.ts, and the template applies the block.rootpage-tree transformer builds one root folder per tab, an explicit tab list is passed to the layout, and the CLI validatesroot.bundleSpecification(fix(portal): render split OpenAPI specifications correctly #354) returns, before Fumadocs sees it.FileWatchServiceand the serve action.4cdbefe): input validation, the filter's$refhandling and emptied tags, a tab opening on the wrong page, and the serve watcher.PreviewConfigandFilePath.isEqualextracted, the CLI and template types held equal by the compiler, comments cut to what the code cannot say, and a test that failed wherever colour output is on.apioptions and raw colour tokens removed, each with its schema, tests and plan entry.app.cssto importing the theme before the generated stylesheet; and the shared-page refusal names a repeated tag as well as a repeated operationId.dev:apimatic.json's directory by its real path. On Windows, libuv aborts the whole process at the first event under a directory given by its 8.3 short name, such as the runners'C:\Users\RUNNER~1\..., andportal servewould crash the same way. A Windows-only test watches through a short name.How it fits #354
This branch was built on #354, and is rebased onto
devnow that #354 is squash-merged there.openApiSectionhands FumadocswithoutInternalOperations(await bundleSpecification(file))through the function form ofinput, so there is one server. With #354's inlining, operations behind a path-item$refget pages whatever the filter leaves out. #354's own test callsopenApiSectionwith the signature this PR gives it.Verification
pnpm test, after the rebase ontodev: 959 passing, 28 pending. The pending tests are the e2e cases, which needAPIMATIC_E2E=1.APIMATIC_E2E=1, after the rebase: 27 passing, 1 skipped as before. There are two real builds:theme.cssagainst Tailwind: a unit test compiles it after the neutral theme and checks each mode's primary comes after the theme's own rules.portal servepicking up a rewritten identity and theme.v2branch is moved to this layout, trimmed block included, in Make src/ on v2 the starting point for the CLI 2 quickstart sample-docs-as-code-portal#12.🤖 Generated with Claude Code