Repository navigation
Add PloneClient.extend() and clientEndpoints utility for custom endpoints - #221
Merged
Merged
Conversation
…ints - @plone/client: PloneClient.extend(), polymorphic initialize(), augmentable PloneClientExtensions interface, export apiRequest, getBackendURL and request types; drop nonexistent tsup entry. - Aurora: clientEndpoints utility type composed by getPloneClientClass() in the middleware. - Docs: how-to guide and client README section. Co-Authored-By: Claude <noreply@anthropic.com>
The constructor loop reassigned the bound function to the callback's
local variable, so nothing was ever bound. Detached methods such as
`const { getContent } = cli` lost `this`. Extension methods are now
bound as well.
Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Oct 4, 2026
pnicolli
approved these changes
Oct 6, 2026
sneridagh
added a commit
that referenced
this pull request
Oct 8, 2026
* origin/main: (190 commits) Make control panels saveable (#220) Public UI: render a single .content-area root (#230) (#234) Remove tsconfig test/spec/story excludes that never matched any file (#222) Add PloneClient.extend() and clientEndpoints utility for custom endpoints (#221) Store the default block width of every top-level block (#190) Releasing @plone/aurora 1.0.0-alpha.19 Release @plone/cmsui 1.0.0-alpha.12 Release @plone/agave 1.0.0-alpha.9 Release @plone/theming 1.0.0-alpha.9 Release @plone/layout 1.0.0-alpha.14 Release @plone/blocks 1.0.0-alpha.18 Release @plone/plate 1.0.0-alpha.25 Release @plone/registry 4.0.0-alpha.5 Release @plone/quanta 1.0.0-alpha.2 Content CSS Phase 11: theming guide for blocks and cleanup (#219) Rename the Plone blocks' classnames to the content contract (#218) Share the block spacing between the Public UI and the editor (#217) Content CSS Phase 8: structural blocks and content root to styles/content.css (#216) Remove the Plate media nodes from Aurora's presets (#215) Move the table styles to styles/content.css (#214) ... # Conflicts: # packages/cmsui/components/BooleanWidget/BooleanWidget.stories.tsx # packages/cmsui/components/BooleanWidget/BooleanWidget.test.tsx # packages/cmsui/components/BooleanWidget/BooleanWidget.tsx # packages/cmsui/news/+boolean-widget-adapter.bugfix
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.
Summary
Add-ons had no clean way to add methods for their own REST API endpoints to the Plone client. This PR adds one, in two layers:
@plone/client(framework-agnostic)PloneClient.extend(methods)returns a subclass with the given methods. It can be chained, leaves the original class unchanged, and gives the added methods their types without augmentation.getContent.initialize()now returns an instance of the class it's called on, so subclasses and extended classes work.PloneClientExtensionsinterface, merged intoPloneClient. Add-ons augment it so that every value typedPloneClient(for exampleploneClientContext) knows about their methods.apiRequest,getBackendURL, and the typesApiRequestParams,PloneClientConfig,RequestResponse,RequestError,PloneClientMethodsandExtendedPloneClient. Custom endpoints are written the same way as core ones.src/bla.tsentry from the tsup config.Aurora
clientEndpointsutility type. The newgetPloneClientClass()in the middleware combines theploneClientutility with every registeredclientEndpointsset. Add-ons register their own endpoints declaratively, with no dependency on load order and without overwriting each other.Docs: new how-to guide
docs/how-to-guides/add-client-endpoints.md, plus a section in the client README.Tests
packages/client/src/client.test.ts: unit tests with axios mocked, no backend needed. They cover polymorphicinitialize,thisbinding, chaining, overrides, the original class staying unchanged, and method binding (see below).apps/aurora/app/middleware.server.test.ts: tests forgetPloneClientClass(), including extending a customploneClientclass.apps/auroraagainst the built package. Augmented methods show up onLoaderUtilityArgs['cli'], and calling an unknown method is a type error.Fix: endpoint methods were never bound to the instance
The bug
The
PloneClientconstructor was meant to bind every endpoint method to its instance:Function.prototype.bind()does not change the function it's called on. It returns a new function. Here that new function was assigned topropertyValue, which is only the callback's parameter, and it was thrown away when the callback returned. Nothing was written back to the instance, so the loop ran once per endpoint and had no effect.Impact
Endpoints get the client as
thisonly when called ascli.method(). Every endpoint starts by readingthis.config, so any call that detaches the method from the client fails:In a detached call
thisisundefined, because ES modules run in strict mode.Why it went unnoticed
Every endpoint is declared with
this: PloneClient, so TypeScript rejects detached calls at compile time ("The 'this' context of type 'void' is not assignable to method's 'this' of type 'PloneClient'"). All code in the monorepo callscli.x(). The bug affects plain JavaScript consumers, code that casts withas any, and libraries that take callbacks.Interaction with
extend()extend()assigns its methods in the subclass constructor, which runs after the base constructor. Fixing only the base loop would bind the core endpoints but leave every extension method unbound.The fix
A small
bindMethods(target, methods)helper assigns each function to the instance, bound to it. Both places use it:bindMethods(this, this). The core endpoints are class fields, so they're already on the instance when the constructor body runs.extend()subclass constructor callsbindMethods(this, extensions)instead ofObject.assign.Each instance gets its own bound copies. A method taken from one client always uses that client's
config, including its token, which matters because Aurora creates one client per request.Trade-off: a bound method can no longer be pointed at a different
thiswith.call()or.apply(). Nothing relies on that.Not changed: types
The public types are unchanged. TypeScript still types endpoints with
this: PloneClientand still flags detached calls, even though they now work at runtime. Allowing them in the types would mean re-declaring every core field asOmitThisParameter<…>, which can be a follow-up if we want it. The new tests use an explicitOmitThisParametercast to call methods detached.Tests
New
PloneClient method bindingtests inclient.test.tscover a detached core method, a detached extension method, and per-instance binding (two instances with differentapiPath). On the old constructor all three fail; with the fix all pass.Side note:
packages/client/tsconfig.jsonexcludessrc/**/*.test.{ts,tsx}, buttscdoesn't support brace patterns, so test files are in fact type-checked bycheck:ts.