Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
20 changes: 20 additions & 0 deletions .changeset/v2-node-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@codama/node-types': major
'@codama/nodes': major
'@codama/visitors-core': major
---

Regenerate the node types, constructors and core visitors from the Codama v2 spec (`@codama/spec@2`).

The node model changes substantially from v1:

- **`name` → `identifier`.** Every named node (`programNode`, `accountNode`, `instructionNode`, links, …) now carries `identifier` instead of `name`, and identifiers preserve their casing — v2 no longer mandates camelCase (`transfer_tokens` and `transferTokens` are both valid; uniqueness is resolved by case-folding).
- **Numeric system rework.** `numberTypeNode`/`numberValueNode` split into `integerTypeNode`/`floatTypeNode` and `integerValueNode`/`floatValueNode` (numeric values are string-encoded to stay lossless through JSON). `amountTypeNode`/`solAmountTypeNode` become `fixedPointTypeNode`; new `durationTypeNode`; `dateTimeTypeNode`/`durationTypeNode` carry `ticksPerSecond`.
- **Flat transforms.** The wrapper type nodes (`fixedSizeTypeNode`, `sizePrefixTypeNode`, `pre/postOffsetTypeNode`, `sentinelTypeNode`, `hiddenPrefix/SuffixTypeNode`) are removed; every type node instead carries an optional `transforms: transformNode[]` applied innermost-first.
- **Instruction data.** `instructionArgumentNode`, `instructionArgumentLinkNode`, `resolverValueNode` and `instructionNode.arguments`/`extraArguments` are removed; instruction arguments live in `instructionNode.data`, reached via path expressions. `argumentValueNode` → `dataValueNode`; `accountFieldValueNode` → `accountDataValueNode`.
- **Enum variants unified.** The three variant nodes collapse into a single `enumVariantTypeNode` with an optional `data`.
- **Text and docs.** `docs` and text-bearing attributes are the union `string | textNode`; a `textNode` carries structured metadata (plugins). New `textNode`.
- **Universal plugins.** Every node gains an optional `plugins: pluginNode[]` base attribute — the extension point for renderer-specific or not-yet-standardised metadata.
- **Other.** `programNode.origin` removed (provenance moves to plugins); `pluginNode.name` → `pluginNode.namespace`; `sentinelCountNode` added; path expressions for field references and discriminators.

Node factory ergonomics: trailing optional parameters now live in the constructor's `options` bag (e.g. `constantNode(identifier, type, value, { docs })`, `accountLinkNode(identifier, { program })`).
15 changes: 7 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ pnpm lint # oxlint && oxfmt --check
pnpm lint:fix
```

Parts of the codebase — everything under a `generated/` directory — are produced by the private `@codama-internal/spec-generators` package from the [`@codama/spec`](https://github.com/codama-idl/spec) meta-model, and must never be edited by hand. CI regenerates everything and fails on any diff, so committed generated code always matches the spec pins in `packages/spec-generators/package.json`:
Most `generated/` directories — those under `@codama/node-types`, `@codama/nodes` and `@codama/visitors-core` — are produced by the private `@codama-internal/spec-generators` package from the [`@codama/spec`](https://github.com/codama-idl/spec) meta-model, and must never be edited by hand. CI regenerates them and fails on any diff, so committed generated code always matches the single living spec pin in `packages/spec-generators/package.json`:

- `@codama/spec` is the living pin: the spec version the current major is generated from.
- `@codama/spec-v1` is an aliased pin to the final release of an older major, used solely to refresh the frozen node types committed in `@codama/upgrade`.
- `@codama/spec` is the living pin: the spec version the current major is generated from. It is exact rather than a range, so generated output only changes through a deliberate, reviewable pin (or generator) change.

Both pins are exact rather than ranges, so generated output only changes through a deliberate, reviewable pin (or generator) change.
The generator is **single-major**: it only renders the spec on its own branch. Node types for **older** majors live as frozen static snapshots under `packages/upgrade/src/vN/generated` (e.g. `src/v1/generated`); these are hand-maintained source, are **not** regenerated by `pnpm generate`, and are excluded from the CI freshness check. See `packages/upgrade/src/v1/README.md`.

## Changesets

Expand All @@ -28,11 +27,11 @@ Any user-facing change needs a changeset: run `npx changeset add --empty` and ed

Branch, dist-tag and lifecycle mechanics (cut / bake / promote) are defined once for the whole ecosystem in the spec repository's [RELEASING.md](https://github.com/codama-idl/spec/blob/HEAD/RELEASING.md). Releasing major N+1 of the [Codama spec](https://github.com/codama-idl/spec) additionally requires the following steps specific to this repository. The design intent is that this list never grows: one new upgrade function per major, everything else mechanical.

1. **Freeze the vN node types.** In `packages/spec-generators/package.json`, repoint the frozen alias at the final N.x spec release (renaming it `@codama/spec-vN`) and move the living `@codama/spec` pin to the (N+1).x release. Running `pnpm generate` then commits the frozen vN snapshot under `packages/upgrade/src/vN/` and restamps `CODAMA_VERSION` — remember to also freeze copies of the hand-written siblings (`brands.ts`, `Docs.ts`, `Version.ts`) next to the generated output, as done for v1.
2. **Write the upgrade step.** Add a single hand-written, pure `upgradeVNToVN+1` function to `@codama/upgrade` — a JSON-tree-in, JSON-tree-out converter in the `nodes-from-anchor` top-down style — and wire it into `upgrade()` as its `if (major <= N)` block. Export the vN types as a type-only namespace (`export type * as vN`) from the package index.
3. **Run the ecosystem lifecycle.** Cut this repository's `N.x` maintenance branch per RELEASING.md, after which `main` hosts the vN+1 work, with one codama-specific detail: the seeded major changeset covers **all** public packages, upholding the same-major invariant. `main` then versions as `(N+1).0.0-rc.n` under the `rc` dist-tag, bakes under `next`, and is promoted to `latest` while `N.x` switches to `release-N.x`. Old majors receive clarifications and documentation fixes only, never semantic changes.
1. **Freeze the vN node types.** Before moving the living `@codama/spec` pin to the (N+1).x release, snapshot the current vN node types into `@codama/upgrade`: copy `@codama/node-types/src/generated` into `packages/upgrade/src/vN/generated`, keeping the layout identical so a later backported vN change can be ported forward by applying the same patch. Also copy the hand-written siblings (`brands.ts`, `Docs.ts`, `Version.ts`) next to it as vN-shaped frozen copies, so the snapshot is self-contained, and export the vN types as a type-only namespace (`export type * as vN`) from the package index. The snapshot is static source — it is never regenerated. Then move the living `@codama/spec` pin to the (N+1).x release and run `pnpm generate`, which rewrites the living `generated/` dirs and restamps `CODAMA_VERSION`.
2. **Write the upgrade step.** Add a single hand-written, pure `upgradeVNToVN+1` function to `@codama/upgrade` — a JSON-tree-in, JSON-tree-out converter in the `nodes-from-anchor` top-down style — and wire it into `upgrade()` as its `if (major <= N)` block.
3. **Run the ecosystem lifecycle.** Cut this repository's `N.x` maintenance branch per RELEASING.md, after which `main` hosts the vN+1 work, with one codama-specific detail: the seeded major changeset covers **all** public packages, upholding the same-major invariant. `main` then versions as `(N+1).0.0-rc.n` under the `rc` dist-tag through the candidacy, and is promoted to `latest` while `N.x` switches to `release-N.x`. Old majors receive clarifications and documentation fixes only, never semantic changes.

Two invariants protect consumers and must never be broken:

- **The upgrade chain is append-only.** The upgrade functions and frozen node types in `@codama/upgrade` are committed source with zero runtime dependencies; they are never removed or rewritten, so every major back to 1.0.0 stays upgradable forever.
- **Frozen types refresh lazily, generators are never forked.** The generators on `main` only ever consume two spec eras: the living pin and the frozen alias. Keep the alias until the meta-model API drifts incompatibly and `spec-generators` stops compiling — a loud tripwire whose response is to delete the freeze block and the alias (two lines), never to fork era-specific generators. If a backported old-major spec patch matters after that point, generate once on the era's maintenance branch and copy the output forward; otherwise, slightly stale docblocks in frozen types are acceptable, since old majors never change shape.
- **The generator is single-major; snapshots are static.** The generator on `main` only ever renders the living spec of the current major — it is never forked or taught to speak an older spec dialect. Older majors' node types are frozen static snapshots under `packages/upgrade/src/vN/generated`, whose layout mirrors that major's `@codama/node-types/src/generated` so a rare backported change is ported forward as a patch. Slightly stale docblocks in a snapshot are acceptable, since old majors never change shape.
2 changes: 2 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND = 9;
export const CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND = 10;
export const CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE = 11;
export const CODAMA_ERROR__UNSUPPORTED_VERSION = 12;
export const CODAMA_ERROR__INVALID_BRANDED_STRING = 13;

// Visitors-related errors.
// Reserve error codes in the range [1200000-1200999].
Expand Down Expand Up @@ -140,6 +141,7 @@ export type CodamaErrorCode =
| typeof CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE
| typeof CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY
| typeof CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND
| typeof CODAMA_ERROR__INVALID_BRANDED_STRING
| typeof CODAMA_ERROR__LINKED_NODE_NOT_FOUND
| typeof CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE
| typeof CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS
Expand Down
5 changes: 5 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE,
CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY,
CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
CODAMA_ERROR__INVALID_BRANDED_STRING,
CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS,
Expand Down Expand Up @@ -204,6 +205,10 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
enumName: CamelCaseString;
variant: CamelCaseString;
};
[CODAMA_ERROR__INVALID_BRANDED_STRING]: {
actual: string;
expected: string;
};
[CODAMA_ERROR__LINKED_NODE_NOT_FOUND]: {
kind: LinkNode['kind'];
linkNode: LinkNode;
Expand Down
2 changes: 2 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE,
CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY,
CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
CODAMA_ERROR__INVALID_BRANDED_STRING,
CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS,
Expand Down Expand Up @@ -114,6 +115,7 @@ export const CodamaErrorMessages: Readonly<{
[CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY]:
'Unsupported optional account strategy [$strategy] for account [$accountName] in [$instructionName].',
[CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND]: 'Enum variant [$variant] not found in enum type [$enumName].',
[CODAMA_ERROR__INVALID_BRANDED_STRING]: 'Expected a valid [$expected], got [$actual].',
[CODAMA_ERROR__LINKED_NODE_NOT_FOUND]: 'Could not find linked node [$name] from [$kind].',
[CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]:
'Node.js filesystem function [$fsFunction] is not available in your environment.',
Expand Down
12 changes: 0 additions & 12 deletions packages/node-types/src/Docs.ts

This file was deleted.

53 changes: 49 additions & 4 deletions packages/node-types/src/brands.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
/**
* Hand-written branded string types used throughout the generated
* node-type surface to mark identifiers that must conform to a specific
* casing convention.
* node-type surface to mark strings that must conform to a specific
* spec constraint (identifiers, namespaces, path expressions,
* string-encoded numbers) or casing convention.
*
* These types live outside `./generated/` because they're static — they
* never change with the spec — so there's nothing to regenerate. The
* generator's symbol map points at this file when emitting `import type
* { CamelCaseString } from '../brands';` lines.
* { IdentifierString } from '../brands';` lines.
*
* The brand is purely a TypeScript marker; runtime parsing and
* validation happen wherever string identifiers cross the package
* validation happen wherever branded strings cross the package
* boundary.
*/

/**
* A string asserted to be a Codama identifier: `[A-Za-z_][A-Za-z0-9_]*`.
* No casing is mandated — `transferTokens`, `transfer_tokens` and
* `TransferTokens` are all valid — but identifiers sharing a scope must
* remain unique after case-folding and stripping underscores.
*/
export type IdentifierString = string & {
readonly ['__string:codama']: 'identifier';
};

/**
* A string asserted to be a namespace: a chain of identifiers separated
* by single dots (`identifier ("." identifier)*`). Used for plugin
* namespaces.
*/
export type NamespaceString = string & {
readonly ['__string:codama']: 'namespace';
};

/**
* A string asserted to be a path expression pointing into nested data
* (`first ( "." identifier | "[" integer "]" )*`).
*/
export type PathString = string & {
readonly ['__string:codama']: 'path';
};

/**
* A string asserted to be a base-10 integer (`0|-?[1-9][0-9]*`). String
* storage keeps the full 64- and 128-bit ranges lossless through JSON.
*/
export type IntegerString = string & {
readonly ['__string:codama']: 'integer';
};

/**
* A string asserted to be a canonical decimal number
* (`-?(0|[1-9][0-9]*)("." [0-9]*[1-9])?`, or `NaN`/`Infinity`/`-Infinity`).
* String storage makes float round-trips deterministic across serialisers.
*/
export type DecimalString = string & {
readonly ['__string:codama']: 'decimal';
};

/** A string asserted to be in camelCase form. */
export type CamelCaseString = string & {
readonly ['__stringCase:codama']: 'camelCase';
Expand Down
31 changes: 19 additions & 12 deletions packages/node-types/src/generated/AccountNode.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import type { CamelCaseString } from '../brands';
import type { Docs } from '../Docs';
import type { IdentifierString } from '../brands';
import type { DiscriminatorNode } from './discriminatorNodes/DiscriminatorNode';
import type { PdaLinkNode } from './linkNodes/PdaLinkNode';
import type { NestedTypeNode } from './typeNodes/NestedTypeNode';
import type { StructTypeNode } from './typeNodes/StructTypeNode';
import type { PluginNode } from './PluginNode';
import type { TextNode } from './TextNode';
import type { TypeNode } from './typeNodes/TypeNode';

/**
* An on-chain account: its name, data structure, optional fixed size, optional PDA, and optional discriminators.
* An on-chain account: its identifier, data type, optional fixed size, optional PDA, and optional discriminators.
*
* ![Diagram](https://github.com/codama-idl/codama/assets/3642397/77974dad-212e-49b1-8e41-5d466c273a02)
*/
export interface AccountNode<
TData extends NestedTypeNode<StructTypeNode> = NestedTypeNode<StructTypeNode>,
TDocs extends string | TextNode | undefined = string | TextNode | undefined,
TData extends TypeNode = TypeNode,
TPda extends PdaLinkNode | undefined = PdaLinkNode | undefined,
TDiscriminators extends Array<DiscriminatorNode> | undefined = Array<DiscriminatorNode> | undefined,
TPlugins extends Array<PluginNode> | undefined = Array<PluginNode> | undefined,
> {
readonly kind: 'accountNode';

// Data.
/** The name of the account. */
readonly name: CamelCaseString;
/** The identifier of the account. */
readonly identifier: IdentifierString;
/** The size of the account in bytes, when the data length is fixed. */
readonly size?: number;
/** Markdown documentation for the account. */
readonly docs?: Docs;

// Children.
/** Markdown documentation for the account. */
readonly docs?: TDocs;
/**
* The struct describing the account data.
* It must be a struct so its fields can be referenced by other nodes — e.g. `accountFieldValueNode`.
* The type describing the account data — any type node, including a `definedTypeLinkNode` to share or reuse a defined type.
* Nodes that reference account fields by name — e.g. `accountDataValueNode` or `fieldDiscriminatorNode` — are only valid when this type resolves to a struct (following links).
*/
readonly data: TData;
/** A link to the PDA the account is derived from, if applicable. */
Expand All @@ -38,4 +40,9 @@ export interface AccountNode<
* When multiple are listed, they are combined with a logical AND.
*/
readonly discriminators?: TDiscriminators;
/**
* Namespaced plugins with custom structured data.
* The universal extension point for renderer-specific or not-yet-standardised metadata.
*/
readonly plugins?: TPlugins;
}
25 changes: 18 additions & 7 deletions packages/node-types/src/generated/ConstantNode.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import type { CamelCaseString } from '../brands';
import type { Docs } from '../Docs';
import type { IdentifierString } from '../brands';
import type { PluginNode } from './PluginNode';
import type { TextNode } from './TextNode';
import type { TypeNode } from './typeNodes/TypeNode';
import type { ValueNode } from './valueNodes/ValueNode';

/** A named constant exposed by the program: a typed value associated with a name. */
export interface ConstantNode<TType extends TypeNode = TypeNode, TValue extends ValueNode = ValueNode> {
export interface ConstantNode<
TDocs extends string | TextNode | undefined = string | TextNode | undefined,
TType extends TypeNode = TypeNode,
TValue extends ValueNode = ValueNode,
TPlugins extends Array<PluginNode> | undefined = Array<PluginNode> | undefined,
> {
readonly kind: 'constantNode';

// Data.
/** The name of the constant. */
readonly name: CamelCaseString;
/** Markdown documentation for the constant. */
readonly docs?: Docs;
/** The identifier of the constant. */
readonly identifier: IdentifierString;

// Children.
/** Markdown documentation for the constant. */
readonly docs?: TDocs;
/** The type of the constant. */
readonly type: TType;
/** The concrete value of the constant. */
readonly value: TValue;
/**
* Namespaced plugins with custom structured data.
* The universal extension point for renderer-specific or not-yet-standardised metadata.
*/
readonly plugins?: TPlugins;
}
Loading
Loading