Skip to content

Latest commit

 

History

History
454 lines (311 loc) · 17.2 KB

File metadata and controls

454 lines (311 loc) · 17.2 KB

@codama/renderers-js

What this project is

@codama/renderers-js is a code generator that turns Codama IDL ASTs into TypeScript/JavaScript Solana client packages. The generated clients target @solana/kit APIs and are usually written into src/generated inside a consumer package.

The package itself is the generator, not a generated client.

Primary entrypoints

  • src/index.ts: public exports; default export is renderVisitor.
  • src/visitors/renderVisitor.ts: top-level orchestration for generation.
  • src/visitors/getRenderMapVisitor.ts: builds the full file map for a Codama root/program tree.
  • src/visitors/getTypeManifestVisitor.ts: lowers Codama type/value nodes into reusable TS/codec/value fragments.

Mental model

Think of the repo as a 3-layer generator:

  1. getTypeManifestVisitor: decide how a node should be represented in TS/codecs/values.
  2. fragments/*: assemble those representations into account/instruction/type/program pages.
  3. getRenderMapVisitor + renderVisitor: decide which files exist, then format, sync dependencies, and write them.

End-to-end render flow

  1. Caller invokes renderVisitor(packageFolder, options).
  2. Output folder defaults to packageFolder/src/generated.
  3. Existing generated folder is deleted unless deleteFolderBeforeRendering === false.
  4. getRenderMapVisitor(options) walks the Codama tree and returns a RenderMap<Fragment>.
  5. Each file is wrapped with the autogenerated header and final import block via getPageFragment.
  6. Code is optionally formatted with Prettier.
  7. package.json is optionally created/updated based on actual imports used by generated files.
  8. Files are written to disk.

Repository layout

  • src/visitors/: traversal/orchestration layer.
  • src/fragments/: page and helper fragment builders.
  • src/utils/: shared rendering primitives and policies.
  • src/types/: ambient/global TS declarations.
  • test/: unit tests for output generation and utility behavior.
  • test/e2e/: real generated packages validated end-to-end against Solana flows.
  • .changeset/: release notes and versioning metadata.
  • .github/workflows/main.yml: CI and release automation.

Core abstractions

Fragment

Defined in src/utils/fragment.ts.

A Fragment is the fundamental render unit:

  • content: code string
  • imports: deferred import map
  • features: extra generation flags

Fragments are immutable and composable. Most codegen helpers return fragments instead of raw strings.

ImportMap

Defined in src/utils/importMap.ts.

Imports are accumulated symbolically during generation and only materialized when building the final page. This enables deduping, type/non-type import collapsing, aliasing, and late resolution of module paths.

TypeManifest

Produced by getTypeManifestVisitor.

Canonical representation of a type/value lowering. A manifest may contain:

  • encoder
  • decoder
  • strictType
  • looseType
  • value
  • isEnum

Fragments that need type semantics should ask the type manifest visitor for a manifest instead of reimplementing encoding/decoding logic.

RenderScope

Defined in src/utils/options.ts.

Shared context passed to fragments. Includes naming rules, dependency maps, custom-data config, import strategy, link resolution, async resolver info, and the type manifest visitor.

RenderMap

Comes from @codama/renderers-core.

Maps relative output file paths to fragments/content before final writing.

Source architecture details

src/visitors

  • renderVisitor.ts: orchestration with side effects.
  • getRenderMapVisitor.ts: generates output files for root/program/account/instruction/pda/type nodes.
  • getTypeManifestVisitor.ts: semantic lowering engine for Codama types and values.
  • index.ts: visitor exports.

src/fragments

Most files are small pure helpers that generate part of one page.

Main page builders:

  • accounts: accountPage, accountType, accountFetchHelpers, accountPdaHelpers, accountSizeHelpers
  • instructions: instructionPage, instructionType, instructionFunction, instructionInputType, instructionInputResolved, instructionData, instructionParseFunction, instructionRemainingAccounts, instructionByteDelta, instructionExtraArgs
  • types: typePage, type, typeCodec, typeDecoder, typeEncoder, typeWithCodec, typeDiscriminatedUnionHelpers
  • programs: programPage, programConstant, programAccounts, programInstructions, programPlugin
  • misc: errorPage, pdaPage, indexPage, rootIndexPage, discriminator helpers

Page builders mostly compose subfragments rather than embedding all logic inline.

src/utils

  • fragment.ts: fragment creation/merging and final page wrapper with the autogenerated warning header.
  • importMap.ts: symbolic import collection and module resolution.
  • nameTransformers.ts: naming policy and override support.
  • options.ts: public render options and shared scope types.
  • packageJson.ts: derive dependency requirements from actual imports and sync/create package.json.
  • customData.ts: support extracting or importing account/instruction data types from elsewhere.
  • linkOverrides.ts: remap generated/internal import targets.
  • async.ts: decide whether instructions need async variants and compute resolver dependencies.
  • formatCode.ts: Prettier integration.
  • typeManifest.ts: helpers for building and merging type manifests.
  • codecs.ts: codec-related helpers.

Output structure

Generated trees typically contain:

  • accounts/
  • errors/
  • instructions/
  • pdas/
  • programs/
  • types/
  • index.ts

Root index generation is centralized in src/fragments/rootIndexPage.ts. internalNodes can suppress exports while still rendering files.

Naming conventions

Naming is centralized in src/utils/nameTransformers.ts. Do not hardcode names in new code if a nameApi function already exists.

Important defaults:

  • files: camelCase(name).ts
  • data types: PascalCaseName
  • loose/input data types: PascalCaseNameArgs
  • encoder: getPascalCaseNameEncoder
  • decoder: getPascalCaseNameDecoder
  • codec: getPascalCaseNameCodec
  • sync instruction builder: getPascalCaseNameInstruction
  • async instruction builder: getPascalCaseNameInstructionAsync
  • instruction input types: PascalCaseNameInput / PascalCaseNameAsyncInput
  • PDA finder: findPascalCaseNamePda
  • program address constant: PROGRAM_NAME_PROGRAM_ADDRESS
  • default discriminated union discriminator: __kind

These are overrideable via the nameTransformers option.

Import and dependency model

Codegen uses symbolic module names such as:

  • generatedAccounts
  • generatedInstructions
  • generatedPrograms
  • generatedTypes
  • solanaAddresses
  • solanaCodecsCore

These are resolved late by importMap.ts according to:

  1. built-in internal/external defaults
  2. kitImportStrategy
  3. user dependencyMap

kitImportStrategy values:

  • granular: prefer standalone @solana/* packages
  • preferRoot: prefer @solana/kit root exports, fallback to granular/subpaths as needed
  • rootOnly: only use @solana/kit and its subpaths

Dependency syncing in src/utils/packageJson.ts inspects final imports and updates package dependencies accordingly. @solana/kit is added to peerDependencies by default when introduced automatically.

Type lowering behavior

src/visitors/getTypeManifestVisitor.ts is the semantic core of the generator. It handles:

  • arrays, sets, maps, tuples, structs
  • enums and discriminated unions
  • options, zeroable options, remainder options
  • bytes, strings, booleans, numbers, public keys, lamports
  • size prefixes, fixed sizes, offsets, sentinels, hidden prefixes/suffixes
  • constant/default value lowering
  • links to previously defined types

When changing type behavior, start here first.

Important invariant: page/fragment generators should generally consume manifests, not re-derive encoding rules themselves.

Instruction rendering behavior

Instruction generation is one of the more complex parts of the codebase. Depending on the instruction, output may include:

  • instruction input types
  • resolved input helpers
  • instruction type aliases
  • data encoders
  • account metadata builders
  • default value handling
  • remaining account handling
  • byte delta helpers
  • sync and optionally async builders
  • parse helpers

Async generation is controlled by src/utils/async.ts.

Async instruction variants are emitted when needed, especially for:

  • async resolvers
  • PDA-derived defaults
  • async remaining accounts
  • async byte deltas

Program rendering behavior

Program pages aggregate program-level helpers, including:

  • program address constants
  • account identification helpers
  • instruction identification and parse helpers
  • plugin types/helpers
  • error constants and error message helpers

Look at src/fragments/programPage.ts and its subfragments for program-level composition.

Customization hooks

customAccountData / customInstructionData

Defined via src/utils/customData.ts.

These let callers:

  • import data definitions from an external module
  • optionally extract generated account/instruction data into dedicated defined types

Default import source for custom data is hooked unless overridden.

linkOverrides

Defined in src/utils/linkOverrides.ts.

Lets callers reroute imports for linked accounts, defined types, instructions, PDAs, programs, and resolvers.

dependencyMap / dependencyVersions

Used to control how symbolic modules resolve and what version ranges get written to generated package manifests.

internalNodes

Render nodes but omit them from barrel exports.

renderParentInstructions

Controls whether nested parent instructions are rendered or whether only leaf instructions are emitted.

Testing strategy

Unit tests

Unit tests live in test/ and mainly validate generated output strings/imports.

Notable groups:

  • page-level tests: accountsPage.test.ts, instructionsPage.test.ts, programsPage.test.ts, pdasPage.test.ts
  • type behavior: test/types/*.test.ts
  • links: test/links/*.test.ts
  • fragment/import/package-json utilities: test/utils/*.test.ts

Common pattern:

  • construct Codama nodes inline using @codama/nodes
  • run visit(..., getRenderMapVisitor(...))
  • assert against generated content or imports

test/_setup.ts contains helpers that normalize code with standalone Prettier before asserting.

Vitest config

vitest.config.mts runs three projects:

  • browser using happy-dom
  • node
  • react-native

E2E tests are excluded from Vitest.

E2E tests

test/e2e/ contains small generated-client packages such as anchor, system, memo, token, and dummy.

test/e2e/test.sh:

  • starts solana-test-validator on port 8899 if needed
  • regenerates a project using test/e2e/generate.cjs
  • runs pnpm install && pnpm build && pnpm test inside each project

test/e2e/generate.cjs uses the built root package from dist/index.node.cjs, so root builds must succeed before E2E generation works.

E2E projects use AVA, not Vitest.

Build, lint, and release

Commands

  • pnpm install
  • pnpm build
  • pnpm lint
  • pnpm lint:fix
  • pnpm test
  • pnpm test:types
  • pnpm test:unit
  • pnpm test:e2e
  • pnpm test:exports
  • pnpm test:treeshakability
  • pnpm dev

Build outputs

tsup.config.ts builds multiple platform/format bundles:

  • node CJS/ESM
  • browser CJS/ESM
  • react-native ESM

Declarations are emitted separately into dist/types.

Release process

  • Changesets drives versioning and changelog generation.
  • Pending release notes live in .changeset/*.md.
  • pnpm publish-package runs build then changeset publish.
  • GitHub Actions handles release PR/publish on pushes to main.

CI invariants

From .github/workflows/main.yml:

  • CI runs lint and the full test suite.
  • CI requires the working tree to stay clean after build and tests.

That means if a code change affects generated outputs, fixtures, or checked-in artifacts, update them so git status --porcelain stays empty after validation.

TypeScript and style conventions

  • TypeScript is strict.
  • Important compiler settings include isolatedModules, isolatedDeclarations, noUnusedLocals, noUnusedParameters, and noPropertyAccessFromIndexSignature.
  • Root uses moduleResolution: "node".
  • E2E packages often use moduleResolution: "bundler" because @solana/kit subpath exports may require it.
  • ESLint uses @solana/eslint-config-solana.
  • Prettier uses @solana/prettier-config-solana.

High-signal files for common tasks

If you need to...

  • change overall generation flow: src/visitors/renderVisitor.ts
  • add/remove emitted files: src/visitors/getRenderMapVisitor.ts
  • change TS type or codec semantics: src/visitors/getTypeManifestVisitor.ts
  • change import resolution: src/utils/importMap.ts
  • change generated naming: src/utils/nameTransformers.ts
  • change package dependency syncing: src/utils/packageJson.ts
  • change page wrappers/header/import rendering: src/utils/fragment.ts
  • change account output: src/fragments/account*.ts
  • change instruction output: src/fragments/instruction*.ts
  • change program output: src/fragments/program*.ts
  • change type page output: src/fragments/type*.ts
  • understand expected behavior: test/ and test/e2e/

Guidance for LLMs making changes

  1. Prefer extending existing fragment/visitor patterns over introducing one-off string builders.
  2. Reuse nameApi and import helpers instead of hardcoding names/imports.
  3. Keep type semantics centralized in getTypeManifestVisitor when possible.
  4. Preserve fragment immutability and compositional style.
  5. When changing generated output, update or add tests that assert the new behavior.
  6. Be mindful that E2E generation consumes the built package from dist, not src directly.
  7. Respect CI’s clean-working-tree invariant.

Practical repo facts

  • Package manager: pnpm@10.15.1
  • Required Node: >=20.18.0
  • Package type: commonjs
  • Published files: built entrypoints and dist/types
  • Default generated folder in consumer packages: src/generated
  • Generated files always include an autogenerated warning header

Skill Instructions

The following instructions come from installed skills (autogenerated, do not edit manually) and should always be followed.

Changesets

  • Any PR that should trigger a package release MUST include a changeset.
  • Identify affected packages by mapping changed files to their nearest package.json.
  • Choose the right bump: patch for fixes, minor for features, major for breaking changes.
  • While a project is pre-1.0, minor bumps may be treated as breaking.
  • ALWAYS use npx changeset add --empty to generate a new changeset file with a random name. NEVER create changeset files manually.
  • No changeset needed for: docs-only changes, CI config, dev dependency updates, test-only changes.

Shipping (Git)

  • NEVER commit, push, create branches, or create PRs without explicit user approval.
  • Before any git operation that creates or modifies a commit, present a review block containing: changeset entry (if applicable), commit title, and commit/PR description. ALWAYS wait for approval.
  • Use standard git add and git commit workflows. Concise title on the first line, blank line, then description body.
  • Use gh pr create for pull requests.
  • Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph.

Shipping (Graphite)

  • Check if Graphite is installed (which gt). Prefer Graphite when available; fall back to the Git shipping workflow otherwise.
  • NEVER commit, push, create branches, or create PRs without explicit user approval.
  • Before any git operation that creates or modifies a commit, present a review block containing: changeset entry (if applicable), commit title, and commit/PR description. ALWAYS wait for approval.
  • Use gt create -am "Title" -m "Description body" for new PRs. The first -m sets the commit title; the second sets the PR description.
  • Use gt modify -a to amend the current branch with follow-up changes (NEVER create additional commits on the same branch).
  • ALWAYS escape backticks in commit messages with backslashes for shell compatibility (e.g. "Update \my-package` config"`).
  • Do NOT run gt submit after committing. Only run it when the user explicitly asks to submit or push.
  • Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph.

TypeScript Docblocks

  • All exported functions, types, interfaces, and constants MUST have JSDoc docblocks.
  • Start with /**, use * prefix for each line, end with */ — each on its own line.
  • Begin with a clear one-to-two line summary. Add a blank line before tags.
  • Include @param, @typeParam, @return, @throws, and at least one @example when helpful.
  • Use {@link ...} to reference related items. Add @see tags at the end for related APIs.

TypeScript READMEs

  • When adding a new public API, add or update the package's README.
  • Structure: brief intro, installation, usage (with code snippet), deep-dive sections.
  • Code snippets must be realistic, concise, and use TypeScript syntax.
  • Focus on the quickest path to success. Developers should feel excited, not overwhelmed.