Skip to content

Automate external plugin adapter code generation with Kinobi renderer - #297

Open
nhanphan wants to merge 2 commits into
mainfrom
claude/js-sdk-codegen-transformers-pkycnc
Open

Automate external plugin adapter code generation with Kinobi renderer#297
nhanphan wants to merge 2 commits into
mainfrom
claude/js-sdk-codegen-transformers-pkycnc

Conversation

@nhanphan

Copy link
Copy Markdown
Contributor

Summary

This PR introduces a custom Kinobi renderer that automatically generates the ergonomic external plugin adapter wrapper layer, eliminating the need for hand-written per-adapter files. Previously, adding a new external plugin adapter required coordinated edits across ~6 files (~90 lines each). Now, regular adapters require no manual changes—just add the Rust type and re-run pnpm generate.

Key Changes

  • New Kinobi Renderer (configs/kinobiExternalPluginAdapters.cjs): A 730-line custom renderer that:

    • Derives the complete adapter set from the externalPluginAdapter enum in the IDL
    • Inspects each base* defined type to determine per-field transforms
    • Uses an OVERRIDES table to capture IDL-inexpressible details (inline data fields, redundant plugin-key fields, DataSection dataAuthority derivation, non-updatable adapters)
    • Generates ergonomic types, plugin types, init/update args, and transform functions for each adapter
  • Generated Plugin Files (moved to clients/js/src/generated/plugins/):

    • base.ts: Base types and interfaces for all adapters
    • registry.ts: Central registry with adapter manifests and conversion functions
    • Per-adapter files: lifecycleHook.ts, oracle.ts, appData.ts, linkedLifecycleHook.ts, linkedAppData.ts, dataSection.ts, agentIdentity.ts
    • Each file contains ergonomic types, plugin types, init/update args, and *ToBase/*FromBase transform functions
  • Simplified Hand-Written Files (clients/js/src/plugins/):

    • lifecycleHook.ts, oracle.ts, appData.ts, linkedLifecycleHook.ts, linkedAppData.ts, dataSection.ts, agentIdentity.ts: Now contain only runtime logic (e.g., Oracle validation serialization, data parsing)
    • externalPluginAdapters.ts: Drastically simplified to import from generated registry and provide high-level utilities
  • Kinobi Configuration (configs/kinobi.cjs): Added custom renderer invocation to generate the adapter layer during the standard pnpm generate workflow

Implementation Details

  • Field Classification: The renderer classifies struct fields into categories (lifecycle checks, substitutable types, schemas, passthroughs) and applies appropriate transforms
  • Import Management: Automatic import accumulation and ordering ensures clean, organized generated code
  • Overrides System: Adapter-specific behavior (data fields, extra fields, non-updatable status) is centralized in a single configuration object
  • Type Safety: Generated types maintain full TypeScript type safety with proper discriminated unions for init/update args
  • Backward Compatibility: The generated code maintains the same public API as the hand-written version

Benefits

  • Reduced Maintenance: Adding a new regular external plugin adapter now requires zero changes to the JavaScript client
  • Consistency: All adapters follow the same pattern, reducing bugs and improving code quality
  • Scalability: The system scales to any number of adapters without proportional code growth
  • Single Source of Truth: Adapter definitions come from the Rust IDL, eliminating duplication

https://claude.ai/code/session_01Uxe3mDBDA2GiQdWvXpvnQE

The ergonomic external-plugin-adapter layer (per-adapter `type`-discriminated
types, option-unwrapping `fromBase`/`initToBase`/`updateToBase`, manifests, and
the registry/dispatch) used to be hand-written — roughly one ~90-line file per
adapter plus coordinated edits across ~6 files for every new adapter, which is
how drift like the DataSection init-args mislabel and the irregular
`agentIdentities` plural crept in.

This adds a custom Kinobi renderer (configs/kinobiExternalPluginAdapters.cjs)
that derives the whole layer from the IDL node tree:
  - the adapter set comes from the `externalPluginAdapter` enum,
  - per-field transforms come from inspecting each `base*` defined type
    (option-wrapping, substitutable leaf links, lifecycle-check tuples),
  - a small OVERRIDES table captures only what the IDL cannot express
    (which adapters carry inline `data`, the DataSection `dataAuthority`
    derivation, redundant plugin-key fields, DataSection being non-updatable).

The renderer runs from configs/kinobi.cjs after the JS render and emits into
clients/js/src/generated/plugins. The hand-written src/plugins adapter files
become thin re-export shims that keep only genuinely custom logic
(findOracleAccount, deserializeOracleValidation, findExtraAccounts). Adding a
regular external plugin adapter now needs no JS changes at all — add the Rust
type and run `pnpm generate`.

Verification: whole-package typecheck, build and eslint pass; a runtime
old-vs-new diff over a fixture matrix shows every transformer and the registry
dispatch are byte-identical, and re-running the pipeline reproduces the output
exactly.

Behavioral note: the generator uses nullish coalescing (`?? null`) uniformly
for optional `schema`. The old LinkedLifecycleHook.initToBase used a truthy
check (`l.schema ? l.schema : null`) that silently dropped
`schema: Binary` (enum value 0) to null; the generated code now preserves it.
This is the only behavioral difference from the previous implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uxe3mDBDA2GiQdWvXpvnQE
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mpl-core-js-docs Ready Ready Preview, Comment Jul 17, 2026 12:11am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added consistent JavaScript SDK support for external plugin adapters, including initialization, updates, conversion, and registry handling across supported plugin types.
    • Improved adapter data handling, lifecycle checks, field transformations, and manifest integration.
  • Refactor
    • Consolidated plugin adapter interfaces and behavior to improve consistency and reduce discrepancies between supported adapters.
    • Existing plugin exports remain available through the updated generated SDK surface.

Walkthrough

The PR adds a Kinobi renderer for generated external plugin adapter modules, integrates it into JavaScript generation, and changes handwritten plugin files to re-export generated types, converters, manifests, and registry functions.

Changes

External plugin adapter generation

Layer / File(s) Summary
Adapter discovery and transformation rules
configs/kinobiExternalPluginAdapters.cjs
Defines adapter-specific overrides, field classification, import generation, and IDL-based adapter descriptors.
Per-adapter module generation
configs/kinobiExternalPluginAdapters.cjs
Generates shared base types and adapter modules containing ergonomic types, conversion functions, reverse mapping, and manifests.
Registry and generated output pipeline
configs/kinobiExternalPluginAdapters.cjs
Generates the registry and index, formats and writes generated files, and exposes the generation entrypoints.
Plugin module re-exports
clients/js/src/plugins/*.ts, configs/kinobi.cjs
Routes plugin exports through generated modules and invokes external adapter generation after the JavaScript render step.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KinobiConfig
  participant AdapterGenerator
  participant GeneratedPlugins
  participant PluginEntryPoints
  KinobiConfig->>AdapterGenerator: pass IDL root and JavaScript output settings
  AdapterGenerator->>GeneratedPlugins: generate and format adapter modules
  GeneratedPlugins-->>KinobiConfig: return generated plugin paths
  PluginEntryPoints->>GeneratedPlugins: re-export generated adapter APIs
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: generating external plugin adapter code with a Kinobi renderer.
Description check ✅ Passed The description directly describes the renderer, generated plugin files, and config changes in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/js-sdk-codegen-transformers-pkycnc

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: a925972 Previous: 2181404 Ratio
CU: create a new, empty asset 8200 Compute Units 8200 Compute Units 1
Space: create a new, empty asset 91 Bytes 91 Bytes 1
CU: create a new, empty asset with empty collection 17084 Compute Units 17084 Compute Units 1
Space: create a new, empty asset with empty collection 91 Bytes 91 Bytes 1
CU: create a new asset with plugins 26913 Compute Units 26913 Compute Units 1
Space: create a new asset with plugins 194 Bytes 194 Bytes 1
CU: create a new asset with plugins and empty collection 32366 Compute Units 32366 Compute Units 1
Space: create a new asset with plugins and empty collection 194 Bytes 194 Bytes 1
CU: list an asset 20269 Compute Units 20269 Compute Units 1
CU: sell an asset 25146 Compute Units 25146 Compute Units 1
CU: list an asset with empty collection 25177 Compute Units 25177 Compute Units 1
CU: sell an asset with empty collection 33191 Compute Units 33191 Compute Units 1
CU: list an asset with collection royalties 24294 Compute Units 24294 Compute Units 1
CU: sell an asset with collection royalties 35962 Compute Units 35962 Compute Units 1
CU: transfer an empty asset 3958 Compute Units 3958 Compute Units 1
CU: transfer an empty asset with empty collection 5684 Compute Units 5684 Compute Units 1
CU: transfer an asset with plugins 7271 Compute Units 7271 Compute Units 1
CU: transfer an asset with plugins and empty collection 8997 Compute Units 8997 Compute Units 1

This comment was automatically generated by workflow using github-action-benchmark.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@configs/kinobiExternalPluginAdapters.cjs`:
- Line 562: Update the generated isExternalPluginAdapterType check to test
whether plugin.type is an own property of externalPluginAdapterManifests, rather
than using the prototype-inclusive in operator. Preserve true results for
supported manifest keys while returning false for inherited names such as
constructor, toString, and __proto__.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d9cabb36-5b58-418f-8848-cccf4863d0d9

📥 Commits

Reviewing files that changed from the base of the PR and between 2181404 and a925972.

⛔ Files ignored due to path filters (10)
  • clients/js/src/generated/plugins/agentIdentity.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/appData.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/base.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/dataSection.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/index.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/lifecycleHook.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/linkedAppData.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/linkedLifecycleHook.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/oracle.ts is excluded by !**/generated/**
  • clients/js/src/generated/plugins/registry.ts is excluded by !**/generated/**
📒 Files selected for processing (10)
  • clients/js/src/plugins/agentIdentity.ts
  • clients/js/src/plugins/appData.ts
  • clients/js/src/plugins/dataSection.ts
  • clients/js/src/plugins/externalPluginAdapters.ts
  • clients/js/src/plugins/lifecycleHook.ts
  • clients/js/src/plugins/linkedAppData.ts
  • clients/js/src/plugins/linkedLifecycleHook.ts
  • clients/js/src/plugins/oracle.ts
  • configs/kinobi.cjs
  • configs/kinobiExternalPluginAdapters.cjs

)
.join('\n')}\n};`;

const isType = `export const isExternalPluginAdapterType = (plugin: { type: string }) =>\n plugin.type in externalPluginAdapterManifests;`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an own-property check for adapter detection.

in accepts inherited names such as constructor, toString, and __proto__, so callers can receive true for unsupported adapter types. (tc39.es)

Proposed fix
-  const isType = `export const isExternalPluginAdapterType = (plugin: { type: string }) =>\n  plugin.type in externalPluginAdapterManifests;`;
+  const isType = `export const isExternalPluginAdapterType = (plugin: { type: string }) =>\n  Object.prototype.hasOwnProperty.call(\n    externalPluginAdapterManifests,\n    plugin.type\n  );`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isType = `export const isExternalPluginAdapterType = (plugin: { type: string }) =>\n plugin.type in externalPluginAdapterManifests;`;
const isType = `export const isExternalPluginAdapterType = (plugin: { type: string }) =>\n Object.prototype.hasOwnProperty.call(
externalPluginAdapterManifests,
plugin.type
);`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@configs/kinobiExternalPluginAdapters.cjs` at line 562, Update the generated
isExternalPluginAdapterType check to test whether plugin.type is an own property
of externalPluginAdapterManifests, rather than using the prototype-inclusive in
operator. Preserve true results for supported manifest keys while returning
false for inherited names such as constructor, toString, and __proto__.

…manifest

The internal-plugin section of clients/js/src/plugins/types.ts (the `*Plugin`
aliases, the owner/authority/create-only V2 argument unions, and the
`*PluginsList` types) was hand-maintained: adding an internal plugin meant
editing it in ~6 places, which is how ordering drift crept in.

This generates that layer:
  - The plugin set and whether each carries inline data come from the IDL
    `Plugin` enum.
  - The owner-managed / authority-managed / create-only split is derived from
    the program itself. A new Rust example (`dump_plugin_manifest`) emits
    configs/plugin-manifest.json from `PluginType::manager()` and the
    `PERMANENT_DELEGATES` set, so the JS unions cannot drift from on-chain
    creation rules. `pnpm generate:plugin-manifest` regenerates it and it is
    wired into `pnpm generate`.
  - The asset/collection/common list scope stays a small documented table in
    the renderer, because the SDK's fetch-side lists are intentionally more
    permissive than the program's create-time rules (e.g. owner-managed plugins
    are listed as common).

types.ts becomes a thin shim that re-exports the generated `internal.ts` and
keeps only the deprecated v1 `CreatePluginArgs` and the foundational
`BasePlugin`. lib.ts is left as-is: its `createPluginV2`/`mapPlugin` default
branches already handle new plugins without edits, so it does not grow per
plugin.

Verified: the generated unions/lists are mutually assignable with the previous
hand-written types (temporary `Eq<>` assertion), and typecheck, build and
eslint pass across the package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uxe3mDBDA2GiQdWvXpvnQE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants