capnweb-validate: validateRpc() in static block, not just a decorator - #231
Draft
teamchong wants to merge 4 commits into
Draft
capnweb-validate: validateRpc() in static block, not just a decorator#231teamchong wants to merge 4 commits into
teamchong wants to merge 4 commits into
Conversation
`validateRpc(Api)` and `validateRpc<Surface>()(Api)` now work as
higher-order functions, for builds where decorators aren't available.
Both emit the same validator as the decorator form and share it.
The argument must name a class declared in the same module: the
transform reads the declaration for `@skipRpcValidation` members and
platform method filtering. `@skipRpcValidation()` is a method decorator,
so the wrapper form takes names instead, read statically at build time:
`validateRpc(Api, { skip: ["raw"] })`.
🦋 Changeset detectedLatest commit: 615cca1 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Narrow via `return bad(...)` instead of casting past the never-returning helper, find the class among merged declarations, reject an options object with no skip list, and cover the empty/absent skip cases in tests.
…skip names A bare `validateRpc(Api);` statement only validates the class if it actually runs, so the transform now requires it to be a top-level statement appearing after the class declaration. Nesting it in a block, or placing it before the declaration, is a build error rather than validation that silently never happens. Also reject a method named twice in the skip list, which is always a mistake.
teamchong
force-pushed
the
validate-rpc-hoc
branch
from
August 7, 2026 14:23
8c2e392 to
4acf787
Compare
…calls
A discarded `validateRpc(MyClass)` call can sit anywhere, and only validates
the class if that code runs. Require the discarded form to be a static block in
the class it validates, which runs exactly when the class is created, and let it
name its class implicitly:
class Api extends RpcTarget { static { validateRpc(); } ... }
The static block form also takes the surface as a type argument and the skip
list as its only argument, and takes both together:
static { validateRpc<Surface>({ skip: ["raw"] }); }
The transform already read both from the call. With a surface to name, `skip`
is checked against `keyof Surface` as well as against the resolved surface at
build time, which the form without a type argument has no type to do.
teamchong
force-pushed
the
validate-rpc-hoc
branch
from
August 7, 2026 14:26
4acf787 to
615cca1
Compare
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.
Decorators don't survive Vite 8 / oxc unless
experimentalDecoratorsis on, andturning that on isn't free: it switches TypeScript to legacy decorator semantics,
which breaks anything already using standard decorators
(cloudflare/workers-sdk#12626). So validation shouldn't require a decorator.
Before:
After:
Each form emits the same validator as the decorator it replaces, and a module
using both shares one. A static block runs when the class is created and isn't
inherited, so the validator is per-class, same as the decorator.
Forms
static { validateRpc(); }@validateRpc()static { validateRpc<Surface>(); }@validateRpc<Surface>()static { validateRpc({ skip: ["raw"] }); }@skipRpcValidation()onraw()static { validateRpc<Surface>({ skip: ["raw"] }); }export default validateRpc(Api);@validateRpc(), for a class exported as an expressionBuild errors
staticblock in the class it validatesvalidateRpc()with no class is only valid in a static block of a named class declarationvalidateRpc<Surface>(Api)class { ... }@skipRpcValidation()members and platform method filteringskipmust be an inline object literal with an array of string literalsskipname must exist in the resolved surface, and isn't allowed twice; with a type argument it'skeyof Surfacetoo@skipRpcValidation(); a repeat is always a mistake