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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/regenerate-nodes-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@codama/nodes': minor
'@codama/visitors-core': patch
---

Regenerate the `xxxNodeInput` types and `xxxNode()` constructors of `@codama/nodes` from the encoded `@codama/spec` description, via a new `nodes` generator inside `@codama-internal/spec-generators`. The runtime `*_NODE_KINDS` arrays (`STANDALONE_TYPE_NODE_KINDS`, `REGISTERED_VALUE_NODE_KINDS`, `INSTRUCTION_INPUT_VALUE_NODE_KINDS`, …, and the top-level `REGISTERED_NODE_KINDS`) are now generated from the spec's union definitions instead of being maintained by hand. A new top-level `CODAMA_VERSION` constant, typed as `CodamaVersion` and pinned to the spec version at generation time, is the single source of truth for the version `@codama/nodes` was built against — `rootNode()` reads it directly when tagging the document.

The bulk of the surface lives under `packages/nodes/src/generated/` and is produced on every `pnpm generate` run. The previously hand-maintained constructors and kinds-arrays are gone; only hand-written helpers (`isNode`, `assertIsNode`, `getAllPrograms`, `getAllInstructions`, `getAllInstructionsWithSubs`, `isScalarEnum`, `isDataEnum`, `isSignedInteger`, the `NestedTypeNode` resolvers, `parseOptionalAccountStrategy`, the legacy `constantValueNodeFromString` / `constantPdaSeedNodeFromString` flavours, etc.) survive at the top of `packages/nodes/src/`. The package's `index.ts` re-exports the generated tree alongside them.

Two intentional behaviour changes shake out of the rebuild:

- **`docs` is now omitted entirely from the encoded shape when it would be empty.** Constructors that accept a `docs?: DocsInput` parameter previously emitted `docs: []` on the frozen node when the caller said nothing about docs; they now drop the `docs` key altogether. This matches the Rust side, keeps absent documentation out of serialised IDLs, and aligns with the `docs?: Docs` optional field already declared by `@codama/node-types`. `removeDocsVisitor` in `@codama/visitors-core` is updated to delete the `docs` key rather than blank it to `[]`, following the same convention.
- **`rootNode().version` now reflects the spec version `@codama/nodes` was generated against, not the runtime package version.** The constructor previously read the `__VERSION__` build-time global injected from the package's `npm_package_version`; it now reads the generated `CODAMA_VERSION` constant. In practice the two have always tracked the same release cadence so the change is invisible at HEAD, but it makes the architectural intent explicit: the version pinned in the IDL is the spec version, not the package version. The `__VERSION__` build-time global and its `packages/nodes/src/types/global.d.ts` declaration are removed accordingly.

The legacy plural-noun constants (`TYPE_NODES`, `VALUE_NODES`, `CONTEXTUAL_VALUE_NODES`, `INSTRUCTION_INPUT_VALUE_NODES`, `COUNT_NODES`, `DISCRIMINATOR_NODES`, `LINK_NODES`, `PDA_SEED_NODES`, `ENUM_VARIANT_TYPE_NODES`) are preserved as alias re-exports of the new canonical `*_NODE_KINDS` names.

The generator drives almost entirely from the spec, with a minimal per-node configuration table carrying only the conveniences the spec can't express: which spec attributes appear as bare positional parameters (the rest land in a trailing `options` bag), and per-attribute overrides for defaulted values, string-coercion patterns on link targets, and the handful of bespoke body expressions (`instructionByteDeltaNode.withHeader`). The renderer derives signature shapes, generic parameters, return types, the `XxxNodeInput` declarations, the `Partial<>` wrapping decision, the `name: string` relaxation, the `docs?: DocsInput` / drop-if-empty handling, and the conditional-spread of optional attributes from the spec directly. An auto-import scan walks each rendered file and pulls in any spec or hand-written identifier the source references, so the configuration never declares imports. Generic-parameter lifting and ordering rely on the same `narrowableDataAttributes` + `genericParamOrder` tables the `nodeTypes` generator uses, keeping the constructor's generics in lockstep with the interface's.
22 changes: 0 additions & 22 deletions packages/nodes/src/ConstantNode.ts

This file was deleted.

21 changes: 21 additions & 0 deletions packages/nodes/src/ConstantPdaSeedNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { BytesEncoding } from '@codama/node-types';

import { programIdValueNode } from './generated/contextualValueNodes/ProgramIdValueNode';
import { constantPdaSeedNode } from './generated/pdaSeedNodes/ConstantPdaSeedNode';
import { bytesTypeNode } from './generated/typeNodes/BytesTypeNode';
import { publicKeyTypeNode } from './generated/typeNodes/PublicKeyTypeNode';
import { stringTypeNode } from './generated/typeNodes/StringTypeNode';
import { bytesValueNode } from './generated/valueNodes/BytesValueNode';
import { stringValueNode } from './generated/valueNodes/StringValueNode';

export function constantPdaSeedNodeFromProgramId() {
return constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode());
}

export function constantPdaSeedNodeFromString<TEncoding extends BytesEncoding>(encoding: TEncoding, string: string) {
return constantPdaSeedNode(stringTypeNode(encoding), stringValueNode(string));
}

export function constantPdaSeedNodeFromBytes<TEncoding extends BytesEncoding>(encoding: TEncoding, data: string) {
return constantPdaSeedNode(bytesTypeNode(), bytesValueNode(encoding, data));
}
15 changes: 15 additions & 0 deletions packages/nodes/src/ConstantValueNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { BytesEncoding } from '@codama/node-types';

import { bytesTypeNode } from './generated/typeNodes/BytesTypeNode';
import { stringTypeNode } from './generated/typeNodes/StringTypeNode';
import { bytesValueNode } from './generated/valueNodes/BytesValueNode';
import { constantValueNode } from './generated/valueNodes/ConstantValueNode';
import { stringValueNode } from './generated/valueNodes/StringValueNode';

export function constantValueNodeFromString<TEncoding extends BytesEncoding>(encoding: TEncoding, string: string) {
return constantValueNode(stringTypeNode(encoding), stringValueNode(string));
}

export function constantValueNodeFromBytes<TEncoding extends BytesEncoding>(encoding: TEncoding, data: string) {
return constantValueNode(bytesTypeNode(), bytesValueNode(encoding, data));
}
9 changes: 9 additions & 0 deletions packages/nodes/src/EnumTypeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { EnumTypeNode } from '@codama/node-types';

export function isScalarEnum(node: EnumTypeNode): boolean {
return node.variants.every(variant => variant.kind === 'enumEmptyVariantTypeNode');
}

export function isDataEnum(node: EnumTypeNode): boolean {
return !isScalarEnum(node);
}
35 changes: 5 additions & 30 deletions packages/nodes/src/InstructionArgumentNode.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,16 @@
import type { InstructionArgumentNode, InstructionInputValueNode } from '@codama/node-types';
import type { InstructionArgumentNode } from '@codama/node-types';

import { structFieldTypeNode } from './generated/typeNodes/StructFieldTypeNode';
import { structTypeNode } from './generated/typeNodes/StructTypeNode';
import { VALUE_NODE_KINDS } from './generated/valueNodes/ValueNode';
import { isNode } from './Node';
import { camelCase, DocsInput, parseDocs } from './shared';
import { structFieldTypeNode } from './typeNodes/StructFieldTypeNode';
import { structTypeNode } from './typeNodes/StructTypeNode';
import { VALUE_NODES } from './valueNodes';

export type InstructionArgumentNodeInput<
TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined,
> = Omit<InstructionArgumentNode<TDefaultValue>, 'docs' | 'kind' | 'name'> & {
readonly docs?: DocsInput;
readonly name: string;
};

export function instructionArgumentNode<TDefaultValue extends InstructionInputValueNode | undefined = undefined>(
input: InstructionArgumentNodeInput<TDefaultValue>,
): InstructionArgumentNode<TDefaultValue> {
return Object.freeze({
kind: 'instructionArgumentNode',

// Data.
name: camelCase(input.name),
...(input.defaultValueStrategy !== undefined && { defaultValueStrategy: input.defaultValueStrategy }),
docs: parseDocs(input.docs),

// Children.
type: input.type,
...(input.defaultValue !== undefined && { defaultValue: input.defaultValue }),
});
}

export function structTypeNodeFromInstructionArgumentNodes(nodes: InstructionArgumentNode[]) {
return structTypeNode(nodes.map(structFieldTypeNodeFromInstructionArgumentNode));
}

export function structFieldTypeNodeFromInstructionArgumentNode(node: InstructionArgumentNode) {
if (isNode(node.defaultValue, VALUE_NODES)) {
if (isNode(node.defaultValue, VALUE_NODE_KINDS)) {
return structFieldTypeNode({ ...node, defaultValue: node.defaultValue });
}
return structFieldTypeNode({
Expand Down
82 changes: 0 additions & 82 deletions packages/nodes/src/InstructionNode.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,13 @@
import type {
DiscriminatorNode,
InstructionAccountNode,
InstructionArgumentNode,
InstructionByteDeltaNode,
InstructionNode,
InstructionRemainingAccountsNode,
OptionalAccountStrategy,
ProgramNode,
RootNode,
} from '@codama/node-types';

import { isNode } from './Node';
import { getAllInstructions } from './ProgramNode';
import { camelCase, DocsInput, parseDocs } from './shared';

type SubInstructionNode = InstructionNode;

export type InstructionNodeInput<
TAccounts extends InstructionAccountNode[] = InstructionAccountNode[],
TArguments extends InstructionArgumentNode[] = InstructionArgumentNode[],
TExtraArguments extends InstructionArgumentNode[] | undefined = InstructionArgumentNode[] | undefined,
TRemainingAccounts extends InstructionRemainingAccountsNode[] | undefined =
| InstructionRemainingAccountsNode[]
| undefined,
TByteDeltas extends InstructionByteDeltaNode[] | undefined = InstructionByteDeltaNode[] | undefined,
TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined,
TSubInstructions extends SubInstructionNode[] | undefined = SubInstructionNode[] | undefined,
> = Omit<
Partial<
InstructionNode<
TAccounts,
TArguments,
TExtraArguments,
TRemainingAccounts,
TByteDeltas,
TDiscriminators,
TSubInstructions
>
>,
'docs' | 'kind' | 'name'
> & {
readonly docs?: DocsInput;
readonly name: string;
};

export function instructionNode<
const TAccounts extends InstructionAccountNode[] = [],
const TArguments extends InstructionArgumentNode[] = [],
const TExtraArguments extends InstructionArgumentNode[] | undefined = undefined,
const TRemainingAccounts extends InstructionRemainingAccountsNode[] | undefined = undefined,
const TByteDeltas extends InstructionByteDeltaNode[] | undefined = undefined,
const TDiscriminators extends DiscriminatorNode[] | undefined = undefined,
const TSubInstructions extends SubInstructionNode[] | undefined = undefined,
>(
input: InstructionNodeInput<
TAccounts,
TArguments,
TExtraArguments,
TRemainingAccounts,
TByteDeltas,
TDiscriminators,
TSubInstructions
>,
): InstructionNode<
TAccounts,
TArguments,
TExtraArguments,
TRemainingAccounts,
TByteDeltas,
TDiscriminators,
TSubInstructions
> {
return Object.freeze({
kind: 'instructionNode',

// Data.
name: camelCase(input.name),
docs: parseDocs(input.docs),
optionalAccountStrategy: parseOptionalAccountStrategy(input.optionalAccountStrategy),
...(input.status !== undefined && { status: input.status }),

// Children.
accounts: (input.accounts ?? []) as TAccounts,
arguments: (input.arguments ?? []) as TArguments,
extraArguments: input.extraArguments,
remainingAccounts: input.remainingAccounts,
byteDeltas: input.byteDeltas,
discriminators: input.discriminators,
subInstructions: input.subInstructions,
});
}

export function parseOptionalAccountStrategy(
optionalAccountStrategy: OptionalAccountStrategy | undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND, CodamaError } from '@codama/errors';
import type { NestedTypeNode, Node, TypeNode } from '@codama/node-types';

import { isNode } from '../Node';
import { TYPE_NODES } from './TypeNode';
import { TYPE_NODE_KINDS } from './generated/typeNodes/TypeNode';
import { isNode } from './Node';

export function resolveNestedTypeNode<TType extends TypeNode>(typeNode: NestedTypeNode<TType>): TType {
switch (typeNode.kind) {
Expand Down Expand Up @@ -44,7 +44,7 @@ export function isNestedTypeNode<TKind extends TypeNode['kind']>(
node: Node | null | undefined,
kind: TKind | TKind[],
): node is NestedTypeNode<Extract<TypeNode, { kind: TKind }>> {
if (!isNode(node, TYPE_NODES)) return false;
if (!isNode(node, TYPE_NODE_KINDS)) return false;
const kinds = Array.isArray(kind) ? kind : [kind];
const resolved = resolveNestedTypeNode(node);
return !!node && kinds.includes(resolved.kind as TKind);
Expand Down
35 changes: 0 additions & 35 deletions packages/nodes/src/Node.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,6 @@
import { CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError } from '@codama/errors';
import type { GetNodeFromKind, Node, NodeKind } from '@codama/node-types';

import { REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS } from './contextualValueNodes/ContextualValueNode';
import { REGISTERED_COUNT_NODE_KINDS } from './countNodes/CountNode';
import { REGISTERED_DISCRIMINATOR_NODE_KINDS } from './discriminatorNodes/DiscriminatorNode';
import { REGISTERED_LINK_NODE_KINDS } from './linkNodes/LinkNode';
import { REGISTERED_PDA_SEED_NODE_KINDS } from './pdaSeedNodes/PdaSeedNode';
import { REGISTERED_TYPE_NODE_KINDS } from './typeNodes/TypeNode';
import { REGISTERED_VALUE_NODE_KINDS } from './valueNodes/ValueNode';

// Node Registration.
export const REGISTERED_NODE_KINDS = [
...REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS,
...REGISTERED_DISCRIMINATOR_NODE_KINDS,
...REGISTERED_LINK_NODE_KINDS,
...REGISTERED_PDA_SEED_NODE_KINDS,
...REGISTERED_COUNT_NODE_KINDS,
...REGISTERED_TYPE_NODE_KINDS,
...REGISTERED_VALUE_NODE_KINDS,
'rootNode' as const,
'programNode' as const,
'pdaNode' as const,
'accountNode' as const,
'eventNode' as const,
'constantNode' as const,
'instructionAccountNode' as const,
'instructionArgumentNode' as const,
'instructionByteDeltaNode' as const,
'instructionNode' as const,
'instructionRemainingAccountsNode' as const,
'instructionStatusNode' as const,
'errorNode' as const,
'definedTypeNode' as const,
];

// Node Helpers.

export function isNode<TKind extends NodeKind>(
node: Node | null | undefined,
kind: TKind | TKind[],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
import type { NumberFormat, NumberTypeNode } from '@codama/node-types';

export function numberTypeNode<TFormat extends NumberFormat = NumberFormat>(
format: TFormat,
endian: 'be' | 'le' = 'le',
): NumberTypeNode<TFormat> {
return Object.freeze({
kind: 'numberTypeNode',

// Data.
format,
endian,
});
}
import type { NumberTypeNode } from '@codama/node-types';

export function isSignedInteger(node: NumberTypeNode): boolean {
return node.format.startsWith('i');
Expand Down
25 changes: 0 additions & 25 deletions packages/nodes/src/PdaNode.ts

This file was deleted.

51 changes: 0 additions & 51 deletions packages/nodes/src/ProgramNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,6 @@ import type {
RootNode,
} from '@codama/node-types';

import { camelCase, DocsInput, parseDocs } from './shared';

export type ProgramNodeInput<
TPdas extends PdaNode[] = PdaNode[],
TAccounts extends AccountNode[] = AccountNode[],
TInstructions extends InstructionNode[] = InstructionNode[],
TDefinedTypes extends DefinedTypeNode[] = DefinedTypeNode[],
TErrors extends ErrorNode[] = ErrorNode[],
TEvents extends EventNode[] = EventNode[],
TConstants extends ConstantNode[] = ConstantNode[],
> = Omit<
Partial<ProgramNode<TPdas, TAccounts, TInstructions, TDefinedTypes, TErrors, TEvents, TConstants>>,
'docs' | 'kind' | 'name' | 'publicKey'
> & {
readonly docs?: DocsInput;
readonly name: string;
readonly publicKey: ProgramNode['publicKey'];
};

export function programNode<
const TPdas extends PdaNode[] = [],
const TAccounts extends AccountNode[] = [],
const TInstructions extends InstructionNode[] = [],
const TDefinedTypes extends DefinedTypeNode[] = [],
const TErrors extends ErrorNode[] = [],
const TEvents extends EventNode[] = [],
const TConstants extends ConstantNode[] = [],
>(
input: ProgramNodeInput<TPdas, TAccounts, TInstructions, TDefinedTypes, TErrors, TEvents, TConstants>,
): ProgramNode<TPdas, TAccounts, TInstructions, TDefinedTypes, TErrors, TEvents, TConstants> {
return Object.freeze({
kind: 'programNode',

// Data.
name: camelCase(input.name),
publicKey: input.publicKey,
version: input.version ?? '0.0.0',
...(input.origin !== undefined && { origin: input.origin }),
docs: parseDocs(input.docs),

// Children.
accounts: (input.accounts ?? []) as TAccounts,
instructions: (input.instructions ?? []) as TInstructions,
definedTypes: (input.definedTypes ?? []) as TDefinedTypes,
pdas: (input.pdas ?? []) as TPdas,
events: (input.events ?? []) as TEvents,
errors: (input.errors ?? []) as TErrors,
constants: (input.constants ?? []) as TConstants,
});
}

export function getAllPrograms(node: ProgramNode | ProgramNode[] | RootNode): ProgramNode[] {
if (Array.isArray(node)) return node;
if (node.kind === 'programNode') return [node];
Expand Down
Loading
Loading