diff --git a/.changeset/proud-queens-pull.md b/.changeset/proud-queens-pull.md new file mode 100644 index 000000000..a4be3e555 --- /dev/null +++ b/.changeset/proud-queens-pull.md @@ -0,0 +1,24 @@ +--- +'@codama/node-types': minor +'@codama/nodes-from-anchor': patch +--- + +Regenerate the entire `@codama/node-types` source surface from the encoded `@codama/spec` description, via the new private `@codama-internal/spec-generators` package. + +The bulk of the surface lives under `src/generated/` and is produced by the `gen-ts-node-types` generator from the spec. The previously hand-maintained interfaces are gone; every node, union, enumeration, and per-spec shared type is rebuilt from the spec on every `pnpm generate` run. + +A small set of static helpers — the brand types, the `Docs` alias, and the `Version` template-literal type — live as hand-written sibling files at the top of `packages/node-types/src/`, alongside the existing `ProgramVersion` deprecated alias. They are imported by the generated surface but never regenerated, since their content doesn't depend on the spec. The generator wipes only `src/generated/` on every run; hand-written content at the top level survives. The per-spec `CodamaVersion` literal stays generated, in its own `src/generated/shared/codamaVersion.ts` file pinned to the spec version at generation time. + +Most of the rebuild is structural: imports now point at per-file paths (`./linkNodes/PdaLinkNode`) instead of subdirectory barrels, every interface and field carries a JSDoc block sourced from the spec, and array fields are emitted as `Array` rather than `T[]` so an inline-union element type doesn't need extra parentheses to preserve precedence. A handful of named API differences also shake out from this: + +- `accountNode.size` is now typed as `number | undefined` (the previous `| null` arm had no consumer and is dropped). +- `programNode.origin` is now typed as the named `ProgramOrigin` union (`'anchor' | 'shank'`) instead of an inline literal union. +- `instructionAccountNode.isSigner` and `instructionRemainingAccountsNode.isSigner` now read `boolean | 'either'` instead of `true | false | 'either'` (a TypeScript-only readability normalisation; the encoded spec keeps the explicit `true | false` form so other codegen targets can still emit a multi-variant enum). +- `numberTypeNode.format` and `stringTypeNode.encoding` are emitted as named `NumberFormat` / `BytesEncoding` aliases imported from `./shared/`, with the same generic-narrowing behaviour preserved. +- `programNode.version` is now typed as the unified `Version` template-literal alias (`` `${number}.${number}.${number}` ``) — a tighter shape than the previous plain string, so non-conforming literal strings will now surface as TypeScript errors at the call site. The historical `ProgramVersion` name is preserved as a hand-written `@deprecated` re-export so existing consumers continue to compile; `@codama/nodes-from-anchor` is updated to import `Version` directly. +- `docs?` fields use a `Docs = Array` alias mirroring the `'docs'` `TypeExpr` kind in `@codama/spec`. The alias is hand-written and lives at `packages/node-types/src/Docs.ts`. +- Documentation strings that ship as multiple paragraphs in the spec now render as multi-paragraph JSDoc blocks. Affected fields and types include `accountNode.discriminators`, `instructionNode.discriminators`, `instructionAccountNode.isSigner`, `instructionRemainingAccountsNode.isSigner`, `rootNode`, the `ConditionalValueNode` interface and its `condition`, `InstructionInputValueNode`, `ResolverValueNode`, `AmountTypeNode` and its `unit`, `MapTypeNode.size`, `NestedTypeNode`, `StringTypeNode.size`, `EnumValueNode.value`, and `NumberValueNode.number`. + +Alongside the per-node interfaces, the package now exports seven `RegisteredNode` category-registry unions (`RegisteredContextualValueNode`, `RegisteredCountNode`, `RegisteredDiscriminatorNode`, `RegisteredLinkNode`, `RegisteredPdaSeedNode`, `RegisteredTypeNode`, `RegisteredValueNode`) corresponding one-to-one with `@codama/spec`'s category registries, plus a `GetNodeFromKind` helper that resolves to the concrete interface for a given kind. The registry unions are the recommended extension point for downstream packages that need to introduce custom node kinds. + +The generator consumes `@codama/spec@1.6.0-rc.4`, which reshapes the spec into per-category groups (`spec.categories[]`) and renames the `nestedTypeNode` `TypeExpr` kind to `nestedUnion` (with an explicit `alias` field). All `docs?` fields throughout the spec are arrays of paragraph strings rather than single newline-separated strings — the renderer accepts the array shape directly. Internally, the generator's renderers are layout-agnostic: they emit `use(...)` calls keyed by symbolic module strings (e.g. `'node:numberTypeNode'`, `'enumeration:Endianness'`, `'brand:CamelCaseString'`), and a single per-spec `RenderScope` resolves those symbolic keys to concrete file locations at write time. Adding a new file kind to the generator means extending the `RenderScope` symbol map; renderers themselves stay free of file-layout knowledge. diff --git a/package.json b/package.json index a98a2b587..86ff37b52 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "build": "turbo run build --log-order grouped", + "generate": "pnpm --filter @codama-internal/spec-generators generate", "lint": "turbo run lint --log-order grouped", "lint:fix": "turbo lint:fix --log-order grouped && pnpm prettier --ignore-unknown --write '{.,!packages}/*'", "test": "turbo run test --log-order grouped", diff --git a/packages/node-types/src/AccountNode.ts b/packages/node-types/src/AccountNode.ts deleted file mode 100644 index 9decd19b8..000000000 --- a/packages/node-types/src/AccountNode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { DiscriminatorNode } from './discriminatorNodes'; -import type { PdaLinkNode } from './linkNodes'; -import type { CamelCaseString, Docs } from './shared'; -import type { NestedTypeNode, StructTypeNode } from './typeNodes'; - -export interface AccountNode< - TData extends NestedTypeNode = NestedTypeNode, - TPda extends PdaLinkNode | undefined = PdaLinkNode | undefined, - TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, -> { - readonly kind: 'accountNode'; - - // Data. - readonly name: CamelCaseString; - readonly size?: number | null; - readonly docs?: Docs; - - // Children. - readonly data: TData; - readonly pda?: TPda; - readonly discriminators?: TDiscriminators; -} diff --git a/packages/node-types/src/DefinedTypeNode.ts b/packages/node-types/src/DefinedTypeNode.ts deleted file mode 100644 index e779239e8..000000000 --- a/packages/node-types/src/DefinedTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { CamelCaseString, Docs } from './shared'; -import type { TypeNode } from './typeNodes/TypeNode'; - -export interface DefinedTypeNode { - readonly kind: 'definedTypeNode'; - - // Data. - readonly name: CamelCaseString; - readonly docs?: Docs; - - // Children. - readonly type: TType; -} diff --git a/packages/node-types/src/Docs.ts b/packages/node-types/src/Docs.ts new file mode 100644 index 000000000..6cdbb0f42 --- /dev/null +++ b/packages/node-types/src/Docs.ts @@ -0,0 +1,12 @@ +/** + * Hand-written `Docs` alias used by every `docs?` field in the generated + * node-type surface. + * + * Lives outside `./generated/` because it's a static type alias — there's + * nothing the spec contributes beyond the array shape itself. The + * generator's symbol map points at this file when emitting `import type + * { Docs } from '../Docs';` lines. + */ + +/** Markdown documentation for a node — one paragraph per array entry. */ +export type Docs = Array; diff --git a/packages/node-types/src/ErrorNode.ts b/packages/node-types/src/ErrorNode.ts deleted file mode 100644 index 80627a1df..000000000 --- a/packages/node-types/src/ErrorNode.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CamelCaseString, Docs } from './shared'; - -export interface ErrorNode { - readonly kind: 'errorNode'; - - // Data. - readonly name: CamelCaseString; - readonly code: number; - readonly message: string; - readonly docs?: Docs; -} diff --git a/packages/node-types/src/EventNode.ts b/packages/node-types/src/EventNode.ts deleted file mode 100644 index 40794ffc0..000000000 --- a/packages/node-types/src/EventNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DiscriminatorNode } from './discriminatorNodes'; -import type { CamelCaseString, Docs } from './shared'; -import type { TypeNode } from './typeNodes'; - -export interface EventNode< - TData extends TypeNode = TypeNode, - TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, -> { - readonly kind: 'eventNode'; - - // Data. - readonly name: CamelCaseString; - readonly docs?: Docs; - - // Children. - readonly data: TData; - readonly discriminators?: TDiscriminators; -} diff --git a/packages/node-types/src/InstructionAccountNode.ts b/packages/node-types/src/InstructionAccountNode.ts deleted file mode 100644 index 40dce62e5..000000000 --- a/packages/node-types/src/InstructionAccountNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { InstructionInputValueNode } from './contextualValueNodes'; -import type { CamelCaseString, Docs } from './shared'; - -export interface InstructionAccountNode< - TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, -> { - readonly kind: 'instructionAccountNode'; - - // Data. - readonly name: CamelCaseString; - readonly isWritable: boolean; - readonly isSigner: boolean | 'either'; - readonly isOptional?: boolean; - readonly docs?: Docs; - - // Children. - readonly defaultValue?: TDefaultValue; -} diff --git a/packages/node-types/src/InstructionArgumentNode.ts b/packages/node-types/src/InstructionArgumentNode.ts deleted file mode 100644 index 939a5b345..000000000 --- a/packages/node-types/src/InstructionArgumentNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { InstructionInputValueNode } from './contextualValueNodes'; -import type { CamelCaseString, Docs } from './shared'; -import type { TypeNode } from './typeNodes'; - -export interface InstructionArgumentNode< - TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, -> { - readonly kind: 'instructionArgumentNode'; - - // Data. - readonly name: CamelCaseString; - readonly defaultValueStrategy?: 'omitted' | 'optional'; - readonly docs?: Docs; - - // Children. - readonly type: TypeNode; - readonly defaultValue?: TDefaultValue; -} diff --git a/packages/node-types/src/InstructionByteDeltaNode.ts b/packages/node-types/src/InstructionByteDeltaNode.ts deleted file mode 100644 index afaecaca4..000000000 --- a/packages/node-types/src/InstructionByteDeltaNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ArgumentValueNode, ResolverValueNode } from './contextualValueNodes'; -import type { AccountLinkNode } from './linkNodes'; -import type { NumberValueNode } from './valueNodes'; - -type InstructionByteDeltaNodeValue = AccountLinkNode | ArgumentValueNode | NumberValueNode | ResolverValueNode; - -export interface InstructionByteDeltaNode< - TValue extends InstructionByteDeltaNodeValue = InstructionByteDeltaNodeValue, -> { - readonly kind: 'instructionByteDeltaNode'; - - // Data. - readonly withHeader: boolean; - readonly subtract?: boolean; - - // Children. - readonly value: TValue; -} diff --git a/packages/node-types/src/InstructionNode.ts b/packages/node-types/src/InstructionNode.ts deleted file mode 100644 index be6479dd1..000000000 --- a/packages/node-types/src/InstructionNode.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { DiscriminatorNode } from './discriminatorNodes'; -import type { InstructionAccountNode } from './InstructionAccountNode'; -import type { InstructionArgumentNode } from './InstructionArgumentNode'; -import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode'; -import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode'; -import type { InstructionStatusNode } from './InstructionStatusNode'; -import type { CamelCaseString, Docs } from './shared'; - -type SubInstructionNode = InstructionNode; - -export type OptionalAccountStrategy = 'omitted' | 'programId'; - -export interface InstructionNode< - 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, -> { - readonly kind: 'instructionNode'; - - // Data. - readonly name: CamelCaseString; - readonly docs?: Docs; - readonly optionalAccountStrategy?: OptionalAccountStrategy; - - // Children. - readonly accounts: TAccounts; - readonly arguments: TArguments; - readonly extraArguments?: TExtraArguments; - readonly remainingAccounts?: TRemainingAccounts; - readonly byteDeltas?: TByteDeltas; - readonly discriminators?: TDiscriminators; - readonly status?: InstructionStatusNode; - readonly subInstructions?: TSubInstructions; -} diff --git a/packages/node-types/src/InstructionRemainingAccountsNode.ts b/packages/node-types/src/InstructionRemainingAccountsNode.ts deleted file mode 100644 index d35270422..000000000 --- a/packages/node-types/src/InstructionRemainingAccountsNode.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ArgumentValueNode, ResolverValueNode } from './contextualValueNodes'; -import type { Docs } from './shared'; - -export interface InstructionRemainingAccountsNode< - TValue extends ArgumentValueNode | ResolverValueNode = ArgumentValueNode | ResolverValueNode, -> { - readonly kind: 'instructionRemainingAccountsNode'; - - // Data. - readonly isOptional?: boolean; - readonly isSigner?: boolean | 'either'; - readonly isWritable?: boolean; - readonly docs?: Docs; - - // Children. - readonly value: TValue; -} diff --git a/packages/node-types/src/InstructionStatusNode.ts b/packages/node-types/src/InstructionStatusNode.ts deleted file mode 100644 index 3a707c4da..000000000 --- a/packages/node-types/src/InstructionStatusNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { InstructionLifecycle } from './shared'; - -export interface InstructionStatusNode { - readonly kind: 'instructionStatusNode'; - - // Data. - readonly lifecycle: InstructionLifecycle; - readonly message?: string; -} diff --git a/packages/node-types/src/PdaNode.ts b/packages/node-types/src/PdaNode.ts deleted file mode 100644 index 632d3501b..000000000 --- a/packages/node-types/src/PdaNode.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { PdaSeedNode } from './pdaSeedNodes'; -import type { CamelCaseString, Docs } from './shared'; - -export interface PdaNode { - readonly kind: 'pdaNode'; - - // Data. - readonly name: CamelCaseString; - readonly docs?: Docs; - readonly programId?: string; - - // Children. - readonly seeds: TSeeds; -} diff --git a/packages/node-types/src/ProgramNode.ts b/packages/node-types/src/ProgramNode.ts deleted file mode 100644 index 0eb816dbf..000000000 --- a/packages/node-types/src/ProgramNode.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { AccountNode } from './AccountNode'; -import type { ConstantNode } from './ConstantNode'; -import type { DefinedTypeNode } from './DefinedTypeNode'; -import type { ErrorNode } from './ErrorNode'; -import type { EventNode } from './EventNode'; -import type { InstructionNode } from './InstructionNode'; -import type { PdaNode } from './PdaNode'; -import type { CamelCaseString, Docs, ProgramVersion } from './shared'; - -export interface ProgramNode< - 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[], -> { - readonly kind: 'programNode'; - - // Data. - readonly name: CamelCaseString; - readonly publicKey: string; - readonly version: ProgramVersion; - readonly origin?: 'anchor' | 'shank'; - readonly docs?: Docs; - - // Children. - readonly accounts: TAccounts; - readonly instructions: TInstructions; - readonly definedTypes: TDefinedTypes; - readonly pdas: TPdas; - readonly events: TEvents; - readonly errors: TErrors; - readonly constants: TConstants; -} diff --git a/packages/node-types/src/ProgramVersion.ts b/packages/node-types/src/ProgramVersion.ts new file mode 100644 index 000000000..9e0cdb00b --- /dev/null +++ b/packages/node-types/src/ProgramVersion.ts @@ -0,0 +1,10 @@ +import type { Version } from './Version'; + +/** + * Hand-written compatibility alias for the program version string. + * + * @deprecated Use `Version` instead. This alias is kept so existing + * imports continue to compile and will be removed in a future major + * release. + */ +export type ProgramVersion = Version; diff --git a/packages/node-types/src/RootNode.ts b/packages/node-types/src/RootNode.ts deleted file mode 100644 index 5c1633b99..000000000 --- a/packages/node-types/src/RootNode.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ProgramNode } from './ProgramNode'; -import type { CodamaVersion } from './shared'; - -export interface RootNode< - TProgram extends ProgramNode = ProgramNode, - TAdditionalPrograms extends ProgramNode[] = ProgramNode[], -> { - readonly kind: 'rootNode'; - - // Data. - readonly standard: 'codama'; - readonly version: CodamaVersion; - - // Children. - readonly program: TProgram; - readonly additionalPrograms: TAdditionalPrograms; -} diff --git a/packages/node-types/src/Version.ts b/packages/node-types/src/Version.ts new file mode 100644 index 000000000..e8c185213 --- /dev/null +++ b/packages/node-types/src/Version.ts @@ -0,0 +1,12 @@ +/** + * Hand-written `Version` alias — a semver-shaped template-literal type + * used wherever the generated surface needs to validate that a string + * looks like a version number. + * + * Lives outside `./generated/` because it's static. The per-spec + * `CodamaVersion` literal — which pins to the spec version at generation + * time — stays under `./generated/shared/`. + */ + +/** A semver-shaped version string (e.g. "1.6.0"). */ +export type Version = `${number}.${number}.${number}`; diff --git a/packages/node-types/src/brands.ts b/packages/node-types/src/brands.ts new file mode 100644 index 000000000..3498427e1 --- /dev/null +++ b/packages/node-types/src/brands.ts @@ -0,0 +1,39 @@ +/** + * Hand-written branded string types used throughout the generated + * node-type surface to mark identifiers that must conform to a specific + * 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. + * + * The brand is purely a TypeScript marker; runtime parsing and + * validation happen wherever string identifiers cross the package + * boundary. + */ + +/** A string asserted to be in camelCase form. */ +export type CamelCaseString = string & { + readonly ['__stringCase:codama']: 'camelCase'; +}; + +/** A string asserted to be in kebabCase form. */ +export type KebabCaseString = string & { + readonly ['__stringCase:codama']: 'kebabCase'; +}; + +/** A string asserted to be in pascalCase form. */ +export type PascalCaseString = string & { + readonly ['__stringCase:codama']: 'pascalCase'; +}; + +/** A string asserted to be in snakeCase form. */ +export type SnakeCaseString = string & { + readonly ['__stringCase:codama']: 'snakeCase'; +}; + +/** A string asserted to be in titleCase form. */ +export type TitleCaseString = string & { + readonly ['__stringCase:codama']: 'titleCase'; +}; diff --git a/packages/node-types/src/contextualValueNodes/AccountBumpValueNode.ts b/packages/node-types/src/contextualValueNodes/AccountBumpValueNode.ts deleted file mode 100644 index ee90bbbfa..000000000 --- a/packages/node-types/src/contextualValueNodes/AccountBumpValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface AccountBumpValueNode { - readonly kind: 'accountBumpValueNode'; - - // Data. - readonly name: CamelCaseString; -} diff --git a/packages/node-types/src/contextualValueNodes/AccountValueNode.ts b/packages/node-types/src/contextualValueNodes/AccountValueNode.ts deleted file mode 100644 index 2e7ba3393..000000000 --- a/packages/node-types/src/contextualValueNodes/AccountValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface AccountValueNode { - readonly kind: 'accountValueNode'; - - // Data. - readonly name: CamelCaseString; -} diff --git a/packages/node-types/src/contextualValueNodes/ArgumentValueNode.ts b/packages/node-types/src/contextualValueNodes/ArgumentValueNode.ts deleted file mode 100644 index b840094ea..000000000 --- a/packages/node-types/src/contextualValueNodes/ArgumentValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface ArgumentValueNode { - readonly kind: 'argumentValueNode'; - - // Data. - readonly name: CamelCaseString; -} diff --git a/packages/node-types/src/contextualValueNodes/ConditionalValueNode.ts b/packages/node-types/src/contextualValueNodes/ConditionalValueNode.ts deleted file mode 100644 index 2fc5f2c78..000000000 --- a/packages/node-types/src/contextualValueNodes/ConditionalValueNode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ValueNode } from '../valueNodes'; -import type { AccountValueNode } from './AccountValueNode'; -import type { ArgumentValueNode } from './ArgumentValueNode'; -import type { InstructionInputValueNode } from './ContextualValueNode'; -import type { ResolverValueNode } from './ResolverValueNode'; - -type ConditionNode = AccountValueNode | ArgumentValueNode | ResolverValueNode; - -export interface ConditionalValueNode< - TCondition extends ConditionNode = ConditionNode, - TValue extends ValueNode | undefined = ValueNode | undefined, - TIfTrue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, - TIfFalse extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, -> { - readonly kind: 'conditionalValueNode'; - - // Children. - readonly condition: TCondition; - readonly value?: TValue; - readonly ifTrue?: TIfTrue; - readonly ifFalse?: TIfFalse; -} diff --git a/packages/node-types/src/contextualValueNodes/PdaSeedValueNode.ts b/packages/node-types/src/contextualValueNodes/PdaSeedValueNode.ts deleted file mode 100644 index 01013e88e..000000000 --- a/packages/node-types/src/contextualValueNodes/PdaSeedValueNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { CamelCaseString } from '../shared'; -import type { ValueNode } from '../valueNodes'; -import type { AccountValueNode } from './AccountValueNode'; -import type { ArgumentValueNode } from './ArgumentValueNode'; - -export interface PdaSeedValueNode< - TValue extends AccountValueNode | ArgumentValueNode | ValueNode = AccountValueNode | ArgumentValueNode | ValueNode, -> { - readonly kind: 'pdaSeedValueNode'; - - // Data. - readonly name: CamelCaseString; - - // Children. - readonly value: TValue; -} diff --git a/packages/node-types/src/contextualValueNodes/PdaValueNode.ts b/packages/node-types/src/contextualValueNodes/PdaValueNode.ts deleted file mode 100644 index 2997824d1..000000000 --- a/packages/node-types/src/contextualValueNodes/PdaValueNode.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PdaLinkNode } from '../linkNodes'; -import type { PdaNode } from '../PdaNode'; -import type { AccountValueNode } from './AccountValueNode'; -import type { ArgumentValueNode } from './ArgumentValueNode'; -import type { PdaSeedValueNode } from './PdaSeedValueNode'; - -export interface PdaValueNode< - TSeeds extends PdaSeedValueNode[] = PdaSeedValueNode[], - TProgram extends AccountValueNode | ArgumentValueNode | undefined = - | AccountValueNode - | ArgumentValueNode - | undefined, -> { - readonly kind: 'pdaValueNode'; - - // Children. - readonly pda: PdaLinkNode | PdaNode; - readonly seeds: TSeeds; - readonly programId?: TProgram; -} diff --git a/packages/node-types/src/contextualValueNodes/ResolverValueNode.ts b/packages/node-types/src/contextualValueNodes/ResolverValueNode.ts deleted file mode 100644 index 1181f69dd..000000000 --- a/packages/node-types/src/contextualValueNodes/ResolverValueNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { CamelCaseString, Docs } from '../shared'; -import type { AccountValueNode } from './AccountValueNode'; -import type { ArgumentValueNode } from './ArgumentValueNode'; - -export interface ResolverValueNode< - TDependsOn extends (AccountValueNode | ArgumentValueNode)[] = (AccountValueNode | ArgumentValueNode)[], -> { - readonly kind: 'resolverValueNode'; - - // Data. - readonly name: CamelCaseString; - readonly docs?: Docs; - - // Children. - readonly dependsOn?: TDependsOn; -} diff --git a/packages/node-types/src/countNodes/PrefixedCountNode.ts b/packages/node-types/src/countNodes/PrefixedCountNode.ts deleted file mode 100644 index 280845b65..000000000 --- a/packages/node-types/src/countNodes/PrefixedCountNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NestedTypeNode, NumberTypeNode } from '../typeNodes'; - -export interface PrefixedCountNode = NestedTypeNode> { - readonly kind: 'prefixedCountNode'; - - // Children. - readonly prefix: TPrefix; -} diff --git a/packages/node-types/src/discriminatorNodes/ConstantDiscriminatorNode.ts b/packages/node-types/src/discriminatorNodes/ConstantDiscriminatorNode.ts deleted file mode 100644 index 93c0a7144..000000000 --- a/packages/node-types/src/discriminatorNodes/ConstantDiscriminatorNode.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ConstantValueNode } from '../valueNodes'; - -export interface ConstantDiscriminatorNode { - readonly kind: 'constantDiscriminatorNode'; - - // Data. - readonly offset: number; - - // Children. - readonly constant: TConstant; -} diff --git a/packages/node-types/src/discriminatorNodes/FieldDiscriminatorNode.ts b/packages/node-types/src/discriminatorNodes/FieldDiscriminatorNode.ts deleted file mode 100644 index a29db7a87..000000000 --- a/packages/node-types/src/discriminatorNodes/FieldDiscriminatorNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface FieldDiscriminatorNode { - readonly kind: 'fieldDiscriminatorNode'; - - // Data. - readonly name: CamelCaseString; - readonly offset: number; -} diff --git a/packages/node-types/src/generated/AccountNode.ts b/packages/node-types/src/generated/AccountNode.ts new file mode 100644 index 000000000..2d2041b7e --- /dev/null +++ b/packages/node-types/src/generated/AccountNode.ts @@ -0,0 +1,34 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { DiscriminatorNode } from './discriminatorNodes/DiscriminatorNode'; +import type { PdaLinkNode } from './linkNodes/PdaLinkNode'; +import type { NestedTypeNode } from './typeNodes/NestedTypeNode'; +import type { StructTypeNode } from './typeNodes/StructTypeNode'; + +/** An on-chain account: its name, data structure, optional fixed size, optional PDA, and optional discriminators. */ +export interface AccountNode< + TData extends NestedTypeNode = NestedTypeNode, + TPda extends PdaLinkNode | undefined = PdaLinkNode | undefined, + TDiscriminators extends Array | undefined = Array | undefined, +> { + readonly kind: 'accountNode'; + + // Data. + /** The name of the account. */ + readonly name: CamelCaseString; + /** 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. + /** The struct describing the account data. */ + readonly data: TData; + /** A link to the PDA the account is derived from, if applicable. */ + readonly pda?: TPda; + /** + * Discriminators that distinguish this account from others in the program. + * When multiple are listed, they are combined with a logical AND. + */ + readonly discriminators?: TDiscriminators; +} diff --git a/packages/node-types/src/ConstantNode.ts b/packages/node-types/src/generated/ConstantNode.ts similarity index 53% rename from packages/node-types/src/ConstantNode.ts rename to packages/node-types/src/generated/ConstantNode.ts index fe143fdb7..a4012d12e 100644 --- a/packages/node-types/src/ConstantNode.ts +++ b/packages/node-types/src/generated/ConstantNode.ts @@ -1,15 +1,21 @@ -import type { CamelCaseString, Docs } from './shared'; +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; 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 { readonly kind: 'constantNode'; // Data. + /** The name of the constant. */ readonly name: CamelCaseString; + /** Markdown documentation for the constant. */ readonly docs?: Docs; // Children. + /** The type of the constant. */ readonly type: TType; + /** The concrete value of the constant. */ readonly value: TValue; } diff --git a/packages/node-types/src/generated/DefinedTypeNode.ts b/packages/node-types/src/generated/DefinedTypeNode.ts new file mode 100644 index 000000000..ea25e6f98 --- /dev/null +++ b/packages/node-types/src/generated/DefinedTypeNode.ts @@ -0,0 +1,18 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { TypeNode } from './typeNodes/TypeNode'; + +/** A reusable named type that can be referenced by `definedTypeLinkNode` from elsewhere in the IDL. */ +export interface DefinedTypeNode { + readonly kind: 'definedTypeNode'; + + // Data. + /** The name of the defined type. */ + readonly name: CamelCaseString; + /** Markdown documentation for the type. */ + readonly docs?: Docs; + + // Children. + /** The type definition. */ + readonly type: TType; +} diff --git a/packages/node-types/src/generated/ErrorNode.ts b/packages/node-types/src/generated/ErrorNode.ts new file mode 100644 index 000000000..3165282e5 --- /dev/null +++ b/packages/node-types/src/generated/ErrorNode.ts @@ -0,0 +1,17 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; + +/** A program error — a numeric code paired with a name and human-readable message. */ +export interface ErrorNode { + readonly kind: 'errorNode'; + + // Data. + /** The name of the error. */ + readonly name: CamelCaseString; + /** The numeric error code returned by the program. */ + readonly code: number; + /** A human-readable description of the error. */ + readonly message: string; + /** Markdown documentation for the error. */ + readonly docs?: Docs; +} diff --git a/packages/node-types/src/generated/EventNode.ts b/packages/node-types/src/generated/EventNode.ts new file mode 100644 index 000000000..7f56cef14 --- /dev/null +++ b/packages/node-types/src/generated/EventNode.ts @@ -0,0 +1,24 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { DiscriminatorNode } from './discriminatorNodes/DiscriminatorNode'; +import type { TypeNode } from './typeNodes/TypeNode'; + +/** A program event: its data shape and optional discriminators used to identify it on the wire. */ +export interface EventNode< + TData extends TypeNode = TypeNode, + TDiscriminators extends Array | undefined = Array | undefined, +> { + readonly kind: 'eventNode'; + + // Data. + /** The name of the event. */ + readonly name: CamelCaseString; + /** Markdown documentation for the event. */ + readonly docs?: Docs; + + // Children. + /** The type describing the event payload. */ + readonly data: TData; + /** Discriminators that distinguish this event from others. When multiple are listed, they are combined with a logical AND. */ + readonly discriminators?: TDiscriminators; +} diff --git a/packages/node-types/src/generated/InstructionAccountNode.ts b/packages/node-types/src/generated/InstructionAccountNode.ts new file mode 100644 index 000000000..ec017e34f --- /dev/null +++ b/packages/node-types/src/generated/InstructionAccountNode.ts @@ -0,0 +1,29 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { InstructionInputValueNode } from './contextualValueNodes/InstructionInputValueNode'; + +/** An account participating in an instruction, with its name, signing/writability flags, and an optional default value. */ +export interface InstructionAccountNode< + TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, +> { + readonly kind: 'instructionAccountNode'; + + // Data. + /** The name of the account. */ + readonly name: CamelCaseString; + /** Whether the instruction may write to the account. */ + readonly isWritable: boolean; + /** + * Whether the account must sign the transaction. + * The literal `"either"` indicates a slot that may or may not sign depending on context. + */ + readonly isSigner: boolean | 'either'; + /** Whether the account slot may be omitted by callers. */ + readonly isOptional?: boolean; + /** Markdown documentation for the account slot. */ + readonly docs?: Docs; + + // Children. + /** A default value used to fill the slot when the caller does not provide one. */ + readonly defaultValue?: TDefaultValue; +} diff --git a/packages/node-types/src/generated/InstructionArgumentNode.ts b/packages/node-types/src/generated/InstructionArgumentNode.ts new file mode 100644 index 000000000..707eb7d71 --- /dev/null +++ b/packages/node-types/src/generated/InstructionArgumentNode.ts @@ -0,0 +1,27 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { InstructionInputValueNode } from './contextualValueNodes/InstructionInputValueNode'; +import type { DefaultValueStrategy } from './shared/defaultValueStrategy'; +import type { TypeNode } from './typeNodes/TypeNode'; + +/** A named argument of an instruction, with its type and an optional default value. */ +export interface InstructionArgumentNode< + TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, + TType extends TypeNode = TypeNode, +> { + readonly kind: 'instructionArgumentNode'; + + // Data. + /** The name of the argument. */ + readonly name: CamelCaseString; + /** How a configured default value is exposed in generated APIs. Required when `defaultValue` is set. */ + readonly defaultValueStrategy?: DefaultValueStrategy; + /** Markdown documentation for the argument. */ + readonly docs?: Docs; + + // Children. + /** The type of the argument. */ + readonly type: TType; + /** A default value used when the argument is omitted by callers. */ + readonly defaultValue?: TDefaultValue; +} diff --git a/packages/node-types/src/generated/InstructionByteDeltaNode.ts b/packages/node-types/src/generated/InstructionByteDeltaNode.ts new file mode 100644 index 000000000..a5be0305e --- /dev/null +++ b/packages/node-types/src/generated/InstructionByteDeltaNode.ts @@ -0,0 +1,16 @@ +import type { InstructionByteDeltaValue } from './InstructionByteDeltaValue'; + +/** A byte-size delta applied when computing rent or buffer size — typically used by instructions that resize accounts. */ +export interface InstructionByteDeltaNode { + readonly kind: 'instructionByteDeltaNode'; + + // Data. + /** Whether the delta includes the account header overhead. */ + readonly withHeader: boolean; + /** When `true`, the delta is subtracted from the running size instead of added. Defaults to `false`. */ + readonly subtract?: boolean; + + // Children. + /** The source of the delta value — a literal number, a referenced account or argument, or a resolver. */ + readonly value: TValue; +} diff --git a/packages/node-types/src/generated/InstructionByteDeltaValue.ts b/packages/node-types/src/generated/InstructionByteDeltaValue.ts new file mode 100644 index 000000000..1ea217312 --- /dev/null +++ b/packages/node-types/src/generated/InstructionByteDeltaValue.ts @@ -0,0 +1,7 @@ +import type { ArgumentValueNode } from './contextualValueNodes/ArgumentValueNode'; +import type { ResolverValueNode } from './contextualValueNodes/ResolverValueNode'; +import type { AccountLinkNode } from './linkNodes/AccountLinkNode'; +import type { NumberValueNode } from './valueNodes/NumberValueNode'; + +/** The value forms accepted by an `instructionByteDeltaNode`. */ +export type InstructionByteDeltaValue = AccountLinkNode | ArgumentValueNode | NumberValueNode | ResolverValueNode; diff --git a/packages/node-types/src/generated/InstructionNode.ts b/packages/node-types/src/generated/InstructionNode.ts new file mode 100644 index 000000000..cb48240c8 --- /dev/null +++ b/packages/node-types/src/generated/InstructionNode.ts @@ -0,0 +1,56 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { DiscriminatorNode } from './discriminatorNodes/DiscriminatorNode'; +import type { InstructionAccountNode } from './InstructionAccountNode'; +import type { InstructionArgumentNode } from './InstructionArgumentNode'; +import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode'; +import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode'; +import type { InstructionStatusNode } from './InstructionStatusNode'; +import type { OptionalAccountStrategy } from './shared/optionalAccountStrategy'; + +type SelfInstructionNode = InstructionNode; + +/** A program instruction: its accounts, arguments, byte-delta hints, discriminators, optional status, and optional sub-instructions. */ +export interface InstructionNode< + TAccounts extends Array = Array, + TArguments extends Array = Array, + TExtraArguments extends Array | undefined = Array | undefined, + TRemainingAccounts extends Array | undefined = + | Array + | undefined, + TByteDeltas extends Array | undefined = Array | undefined, + TDiscriminators extends Array | undefined = Array | undefined, + TSubInstructions extends Array | undefined = Array | undefined, + TStatus extends InstructionStatusNode | undefined = InstructionStatusNode | undefined, +> { + readonly kind: 'instructionNode'; + + // Data. + /** The name of the instruction. */ + readonly name: CamelCaseString; + /** Markdown documentation for the instruction. */ + readonly docs?: Docs; + /** How absent optional accounts are represented when serialising the instruction. */ + readonly optionalAccountStrategy?: OptionalAccountStrategy; + + // Children. + /** The accounts the instruction operates on, in order. */ + readonly accounts: TAccounts; + /** The serialised arguments of the instruction, in order. */ + readonly arguments: TArguments; + /** Additional arguments exposed in the generated client API but not serialised on the wire. */ + readonly extraArguments?: TExtraArguments; + /** Variable-length tails of accounts appended after the named account slots. */ + readonly remainingAccounts?: TRemainingAccounts; + /** Byte-size adjustments applied when computing rent or buffer size — for instructions that resize accounts. */ + readonly byteDeltas?: TByteDeltas; + /** + * Discriminators that distinguish this instruction from others. + * When multiple are listed, they are combined with a logical AND. + */ + readonly discriminators?: TDiscriminators; + /** The lifecycle status of the instruction. */ + readonly status?: TStatus; + /** Inner instructions invoked through CPI as part of executing this instruction. */ + readonly subInstructions?: TSubInstructions; +} diff --git a/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts b/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts new file mode 100644 index 000000000..822b04565 --- /dev/null +++ b/packages/node-types/src/generated/InstructionRemainingAccountsNode.ts @@ -0,0 +1,26 @@ +import type { Docs } from '../Docs'; +import type { InstructionRemainingAccountsValue } from './InstructionRemainingAccountsValue'; + +/** A "remaining accounts" slot in an instruction — a variable-length tail of accounts derived from a value. */ +export interface InstructionRemainingAccountsNode< + TValue extends InstructionRemainingAccountsValue = InstructionRemainingAccountsValue, +> { + readonly kind: 'instructionRemainingAccountsNode'; + + // Data. + /** Whether the remaining-accounts tail may be empty. */ + readonly isOptional?: boolean; + /** + * Whether each remaining account must sign the transaction. + * The literal `"either"` indicates a slot that may or may not sign depending on context. + */ + readonly isSigner?: boolean | 'either'; + /** Whether the instruction may write to each remaining account. */ + readonly isWritable?: boolean; + /** Markdown documentation for the remaining-accounts slot. */ + readonly docs?: Docs; + + // Children. + /** The source of the remaining-accounts list — a referenced argument or a resolver. */ + readonly value: TValue; +} diff --git a/packages/node-types/src/generated/InstructionRemainingAccountsValue.ts b/packages/node-types/src/generated/InstructionRemainingAccountsValue.ts new file mode 100644 index 000000000..906ce0a7f --- /dev/null +++ b/packages/node-types/src/generated/InstructionRemainingAccountsValue.ts @@ -0,0 +1,5 @@ +import type { ArgumentValueNode } from './contextualValueNodes/ArgumentValueNode'; +import type { ResolverValueNode } from './contextualValueNodes/ResolverValueNode'; + +/** The value forms accepted by an `instructionRemainingAccountsNode`. */ +export type InstructionRemainingAccountsValue = ArgumentValueNode | ResolverValueNode; diff --git a/packages/node-types/src/generated/InstructionStatusNode.ts b/packages/node-types/src/generated/InstructionStatusNode.ts new file mode 100644 index 000000000..531a4e051 --- /dev/null +++ b/packages/node-types/src/generated/InstructionStatusNode.ts @@ -0,0 +1,12 @@ +import type { InstructionLifecycle } from './shared/instructionLifecycle'; + +/** The lifecycle stage of an instruction (draft, live, deprecated, archived) with an optional accompanying message. */ +export interface InstructionStatusNode { + readonly kind: 'instructionStatusNode'; + + // Data. + /** The lifecycle stage. */ + readonly lifecycle: InstructionLifecycle; + /** Free-form prose accompanying the status — e.g. a deprecation notice with migration guidance. */ + readonly message?: string; +} diff --git a/packages/node-types/src/Node.ts b/packages/node-types/src/generated/Node.ts similarity index 78% rename from packages/node-types/src/Node.ts rename to packages/node-types/src/generated/Node.ts index eaca49be1..d3cc8eca0 100644 --- a/packages/node-types/src/Node.ts +++ b/packages/node-types/src/generated/Node.ts @@ -1,9 +1,9 @@ import type { AccountNode } from './AccountNode'; import type { ConstantNode } from './ConstantNode'; -import type { RegisteredContextualValueNode } from './contextualValueNodes/ContextualValueNode'; -import type { RegisteredCountNode } from './countNodes/CountNode'; +import type { RegisteredContextualValueNode } from './contextualValueNodes/RegisteredContextualValueNode'; +import type { RegisteredCountNode } from './countNodes/RegisteredCountNode'; import type { DefinedTypeNode } from './DefinedTypeNode'; -import type { RegisteredDiscriminatorNode } from './discriminatorNodes/DiscriminatorNode'; +import type { RegisteredDiscriminatorNode } from './discriminatorNodes/RegisteredDiscriminatorNode'; import type { ErrorNode } from './ErrorNode'; import type { EventNode } from './EventNode'; import type { InstructionAccountNode } from './InstructionAccountNode'; @@ -12,13 +12,13 @@ import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode'; import type { InstructionNode } from './InstructionNode'; import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode'; import type { InstructionStatusNode } from './InstructionStatusNode'; -import type { RegisteredLinkNode } from './linkNodes/LinkNode'; +import type { RegisteredLinkNode } from './linkNodes/RegisteredLinkNode'; import type { PdaNode } from './PdaNode'; -import type { RegisteredPdaSeedNode } from './pdaSeedNodes/PdaSeedNode'; +import type { RegisteredPdaSeedNode } from './pdaSeedNodes/RegisteredPdaSeedNode'; import type { ProgramNode } from './ProgramNode'; import type { RootNode } from './RootNode'; -import type { RegisteredTypeNode } from './typeNodes/TypeNode'; -import type { RegisteredValueNode } from './valueNodes/ValueNode'; +import type { RegisteredTypeNode } from './typeNodes/RegisteredTypeNode'; +import type { RegisteredValueNode } from './valueNodes/RegisteredValueNode'; // Node Registration. export type NodeKind = Node['kind']; diff --git a/packages/node-types/src/generated/PdaNode.ts b/packages/node-types/src/generated/PdaNode.ts new file mode 100644 index 000000000..c481fddd4 --- /dev/null +++ b/packages/node-types/src/generated/PdaNode.ts @@ -0,0 +1,20 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { PdaSeedNode } from './pdaSeedNodes/PdaSeedNode'; + +/** A program-derived address: its name, optional program ID override, and the seeds used to derive it. */ +export interface PdaNode = Array> { + readonly kind: 'pdaNode'; + + // Data. + /** The name of the PDA. */ + readonly name: CamelCaseString; + /** Markdown documentation for the PDA. */ + readonly docs?: Docs; + /** The base58-encoded program ID used to derive the PDA. When omitted, the surrounding program is assumed. */ + readonly programId?: string; + + // Children. + /** The seeds used to derive the PDA, in order. */ + readonly seeds: TSeeds; +} diff --git a/packages/node-types/src/generated/ProgramNode.ts b/packages/node-types/src/generated/ProgramNode.ts new file mode 100644 index 000000000..f47938286 --- /dev/null +++ b/packages/node-types/src/generated/ProgramNode.ts @@ -0,0 +1,52 @@ +import type { CamelCaseString } from '../brands'; +import type { Docs } from '../Docs'; +import type { Version } from '../Version'; +import type { AccountNode } from './AccountNode'; +import type { ConstantNode } from './ConstantNode'; +import type { DefinedTypeNode } from './DefinedTypeNode'; +import type { ErrorNode } from './ErrorNode'; +import type { EventNode } from './EventNode'; +import type { InstructionNode } from './InstructionNode'; +import type { PdaNode } from './PdaNode'; +import type { ProgramOrigin } from './shared/programOrigin'; + +/** A Solana program: its identity, version, accounts, instructions, defined types, PDAs, events, errors, and constants. */ +export interface ProgramNode< + TPdas extends Array = Array, + TAccounts extends Array = Array, + TInstructions extends Array = Array, + TDefinedTypes extends Array = Array, + TErrors extends Array = Array, + TEvents extends Array = Array, + TConstants extends Array = Array, +> { + readonly kind: 'programNode'; + + // Data. + /** The name of the program. */ + readonly name: CamelCaseString; + /** The base58-encoded program ID. */ + readonly publicKey: string; + /** The version of the program, in semver form. */ + readonly version: Version; + /** The toolchain that originally generated the program description, if known. */ + readonly origin?: ProgramOrigin; + /** Markdown documentation for the program. */ + readonly docs?: Docs; + + // Children. + /** The accounts owned by the program. */ + readonly accounts: TAccounts; + /** The instructions exposed by the program. */ + readonly instructions: TInstructions; + /** The reusable types defined by the program. */ + readonly definedTypes: TDefinedTypes; + /** The PDAs derived by the program. */ + readonly pdas: TPdas; + /** The events emitted by the program. */ + readonly events: TEvents; + /** The errors returned by the program. */ + readonly errors: TErrors; + /** The constants exposed by the program. */ + readonly constants: TConstants; +} diff --git a/packages/node-types/src/generated/RootNode.ts b/packages/node-types/src/generated/RootNode.ts new file mode 100644 index 000000000..53c187bbc --- /dev/null +++ b/packages/node-types/src/generated/RootNode.ts @@ -0,0 +1,25 @@ +import type { ProgramNode } from './ProgramNode'; +import type { CodamaVersion } from './shared/codamaVersion'; + +/** + * The root of a Codama IDL document. + * Pairs a primary program with any number of additional programs and tags the document with the spec version. + */ +export interface RootNode< + TProgram extends ProgramNode = ProgramNode, + TAdditionalPrograms extends Array = Array, +> { + readonly kind: 'rootNode'; + + // Data. + /** A literal marker identifying the document as a Codama IDL. */ + readonly standard: 'codama'; + /** The Codama spec version this document conforms to. */ + readonly version: CodamaVersion; + + // Children. + /** The primary program described by the document. */ + readonly program: TProgram; + /** Additional programs referenced by the primary program. */ + readonly additionalPrograms: TAdditionalPrograms; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/AccountBumpValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/AccountBumpValueNode.ts new file mode 100644 index 000000000..a87066318 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/AccountBumpValueNode.ts @@ -0,0 +1,10 @@ +import type { CamelCaseString } from '../../brands'; + +/** Refers to the bump seed of a named PDA-derived account in the surrounding instruction. */ +export interface AccountBumpValueNode { + readonly kind: 'accountBumpValueNode'; + + // Data. + /** The name of the account whose bump seed is referenced. */ + readonly name: CamelCaseString; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/AccountValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/AccountValueNode.ts new file mode 100644 index 000000000..5932264fe --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/AccountValueNode.ts @@ -0,0 +1,10 @@ +import type { CamelCaseString } from '../../brands'; + +/** Refers to a named account in the surrounding instruction. */ +export interface AccountValueNode { + readonly kind: 'accountValueNode'; + + // Data. + /** The name of the referenced account. */ + readonly name: CamelCaseString; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/ArgumentValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/ArgumentValueNode.ts new file mode 100644 index 000000000..786fecefd --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/ArgumentValueNode.ts @@ -0,0 +1,10 @@ +import type { CamelCaseString } from '../../brands'; + +/** Refers to a named argument of the surrounding instruction. */ +export interface ArgumentValueNode { + readonly kind: 'argumentValueNode'; + + // Data. + /** The name of the referenced argument. */ + readonly name: CamelCaseString; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/ConditionalValueCondition.ts b/packages/node-types/src/generated/contextualValueNodes/ConditionalValueCondition.ts new file mode 100644 index 000000000..07ab3d275 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/ConditionalValueCondition.ts @@ -0,0 +1,6 @@ +import type { AccountValueNode } from './AccountValueNode'; +import type { ArgumentValueNode } from './ArgumentValueNode'; +import type { ResolverValueNode } from './ResolverValueNode'; + +/** The condition forms accepted by a `conditionalValueNode`. */ +export type ConditionalValueCondition = AccountValueNode | ArgumentValueNode | ResolverValueNode; diff --git a/packages/node-types/src/generated/contextualValueNodes/ConditionalValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/ConditionalValueNode.ts new file mode 100644 index 000000000..a7df2e3a2 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/ConditionalValueNode.ts @@ -0,0 +1,29 @@ +import type { ValueNode } from '../valueNodes/ValueNode'; +import type { ConditionalValueCondition } from './ConditionalValueCondition'; +import type { InstructionInputValueNode } from './InstructionInputValueNode'; + +/** + * A branching contextual value. + * The condition resolves to a value at instruction time; that result selects between `ifTrue` and `ifFalse`. + */ +export interface ConditionalValueNode< + TCondition extends ConditionalValueCondition = ConditionalValueCondition, + TValue extends ValueNode | undefined = ValueNode | undefined, + TIfTrue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, + TIfFalse extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, +> { + readonly kind: 'conditionalValueNode'; + + // Children. + /** The value whose evaluation drives the branch. */ + readonly condition: TCondition; + /** + * When present, the condition result is compared for equality against this value. + * Otherwise the result is treated as a boolean. + */ + readonly value?: TValue; + /** The value used when the condition resolves truthy (or matches `value`). */ + readonly ifTrue?: TIfTrue; + /** The value used when the condition resolves falsy (or does not match `value`). */ + readonly ifFalse?: TIfFalse; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/ContextualValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/ContextualValueNode.ts new file mode 100644 index 000000000..60fffc17c --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/ContextualValueNode.ts @@ -0,0 +1,4 @@ +import type { StandaloneContextualValueNode } from './StandaloneContextualValueNode'; + +/** The composable form: any standalone contextual-value node. */ +export type ContextualValueNode = StandaloneContextualValueNode; diff --git a/packages/node-types/src/contextualValueNodes/IdentityValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/IdentityValueNode.ts similarity index 52% rename from packages/node-types/src/contextualValueNodes/IdentityValueNode.ts rename to packages/node-types/src/generated/contextualValueNodes/IdentityValueNode.ts index 9700d4d0f..17eec597c 100644 --- a/packages/node-types/src/contextualValueNodes/IdentityValueNode.ts +++ b/packages/node-types/src/generated/contextualValueNodes/IdentityValueNode.ts @@ -1,3 +1,4 @@ +/** Refers to the wallet identity providing the instruction context. */ export interface IdentityValueNode { readonly kind: 'identityValueNode'; } diff --git a/packages/node-types/src/generated/contextualValueNodes/InstructionInputValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/InstructionInputValueNode.ts new file mode 100644 index 000000000..ad65dd80b --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/InstructionInputValueNode.ts @@ -0,0 +1,9 @@ +import type { ProgramLinkNode } from '../linkNodes/ProgramLinkNode'; +import type { ValueNode } from '../valueNodes/ValueNode'; +import type { ContextualValueNode } from './ContextualValueNode'; + +/** + * Anything that can be used as the input value for an instruction account or argument default. + * Covers concrete values, contextual references, and program links. + */ +export type InstructionInputValueNode = ContextualValueNode | ProgramLinkNode | ValueNode; diff --git a/packages/node-types/src/contextualValueNodes/PayerValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/PayerValueNode.ts similarity index 51% rename from packages/node-types/src/contextualValueNodes/PayerValueNode.ts rename to packages/node-types/src/generated/contextualValueNodes/PayerValueNode.ts index d03879d98..68db12c68 100644 --- a/packages/node-types/src/contextualValueNodes/PayerValueNode.ts +++ b/packages/node-types/src/generated/contextualValueNodes/PayerValueNode.ts @@ -1,3 +1,4 @@ +/** Refers to the wallet paying for the surrounding transaction. */ export interface PayerValueNode { readonly kind: 'payerValueNode'; } diff --git a/packages/node-types/src/generated/contextualValueNodes/PdaSeedValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/PdaSeedValueNode.ts new file mode 100644 index 000000000..f721b4ae9 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/PdaSeedValueNode.ts @@ -0,0 +1,15 @@ +import type { CamelCaseString } from '../../brands'; +import type { PdaSeedValueValue } from './PdaSeedValueValue'; + +/** Pairs a PDA seed name with the value to substitute when deriving the PDA. */ +export interface PdaSeedValueNode { + readonly kind: 'pdaSeedValueNode'; + + // Data. + /** The name of the seed being filled in. */ + readonly name: CamelCaseString; + + // Children. + /** The value to substitute for the seed. */ + readonly value: TValue; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/PdaSeedValueValue.ts b/packages/node-types/src/generated/contextualValueNodes/PdaSeedValueValue.ts new file mode 100644 index 000000000..02bc237b0 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/PdaSeedValueValue.ts @@ -0,0 +1,6 @@ +import type { ValueNode } from '../valueNodes/ValueNode'; +import type { AccountValueNode } from './AccountValueNode'; +import type { ArgumentValueNode } from './ArgumentValueNode'; + +/** The value forms accepted by a `pdaSeedValueNode`. */ +export type PdaSeedValueValue = AccountValueNode | ArgumentValueNode | ValueNode; diff --git a/packages/node-types/src/generated/contextualValueNodes/PdaValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/PdaValueNode.ts new file mode 100644 index 000000000..4cd3fd1dd --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/PdaValueNode.ts @@ -0,0 +1,20 @@ +import type { PdaSeedValueNode } from './PdaSeedValueNode'; +import type { PdaValuePda } from './PdaValuePda'; +import type { PdaValueProgramId } from './PdaValueProgramId'; + +/** Resolves to a PDA derived from a list of seed values. */ +export interface PdaValueNode< + TSeeds extends Array = Array, + TProgramId extends PdaValueProgramId | undefined = PdaValueProgramId | undefined, + TPda extends PdaValuePda = PdaValuePda, +> { + readonly kind: 'pdaValueNode'; + + // Children. + /** The PDA being derived — either a link to a defined PDA or an inline `pdaNode`. */ + readonly pda: TPda; + /** The seed values used to derive the PDA, paired with their seed names. */ + readonly seeds: TSeeds; + /** The program ID used to derive the PDA. When omitted, the PDA’s declared program is used. */ + readonly programId?: TProgramId; +} diff --git a/packages/node-types/src/generated/contextualValueNodes/PdaValuePda.ts b/packages/node-types/src/generated/contextualValueNodes/PdaValuePda.ts new file mode 100644 index 000000000..21c425d7d --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/PdaValuePda.ts @@ -0,0 +1,5 @@ +import type { PdaLinkNode } from '../linkNodes/PdaLinkNode'; +import type { PdaNode } from '../PdaNode'; + +/** A `pdaValueNode` may reference a PDA either by link or inline. */ +export type PdaValuePda = PdaLinkNode | PdaNode; diff --git a/packages/node-types/src/generated/contextualValueNodes/PdaValueProgramId.ts b/packages/node-types/src/generated/contextualValueNodes/PdaValueProgramId.ts new file mode 100644 index 000000000..cd3a02401 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/PdaValueProgramId.ts @@ -0,0 +1,5 @@ +import type { AccountValueNode } from './AccountValueNode'; +import type { ArgumentValueNode } from './ArgumentValueNode'; + +/** The program-id forms accepted by a `pdaValueNode`. */ +export type PdaValueProgramId = AccountValueNode | ArgumentValueNode; diff --git a/packages/node-types/src/contextualValueNodes/ProgramIdValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/ProgramIdValueNode.ts similarity index 55% rename from packages/node-types/src/contextualValueNodes/ProgramIdValueNode.ts rename to packages/node-types/src/generated/contextualValueNodes/ProgramIdValueNode.ts index 97ce389a9..6dca2a515 100644 --- a/packages/node-types/src/contextualValueNodes/ProgramIdValueNode.ts +++ b/packages/node-types/src/generated/contextualValueNodes/ProgramIdValueNode.ts @@ -1,3 +1,4 @@ +/** Refers to the program ID of the surrounding instruction. */ export interface ProgramIdValueNode { readonly kind: 'programIdValueNode'; } diff --git a/packages/node-types/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts new file mode 100644 index 000000000..c751cdf08 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts @@ -0,0 +1,5 @@ +import type { PdaSeedValueNode } from './PdaSeedValueNode'; +import type { StandaloneContextualValueNode } from './StandaloneContextualValueNode'; + +/** Every node tagged as a contextual-value node, including helper variants. */ +export type RegisteredContextualValueNode = PdaSeedValueNode | StandaloneContextualValueNode; diff --git a/packages/node-types/src/generated/contextualValueNodes/ResolverDependency.ts b/packages/node-types/src/generated/contextualValueNodes/ResolverDependency.ts new file mode 100644 index 000000000..e28a7bd1c --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/ResolverDependency.ts @@ -0,0 +1,5 @@ +import type { AccountValueNode } from './AccountValueNode'; +import type { ArgumentValueNode } from './ArgumentValueNode'; + +/** The dependency forms accepted by a `resolverValueNode`. */ +export type ResolverDependency = AccountValueNode | ArgumentValueNode; diff --git a/packages/node-types/src/generated/contextualValueNodes/ResolverValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/ResolverValueNode.ts new file mode 100644 index 000000000..2516d5624 --- /dev/null +++ b/packages/node-types/src/generated/contextualValueNodes/ResolverValueNode.ts @@ -0,0 +1,23 @@ +import type { CamelCaseString } from '../../brands'; +import type { Docs } from '../../Docs'; +import type { ResolverDependency } from './ResolverDependency'; + +/** + * A custom resolver: a named function provided by the consumer that produces a value. + * May optionally depend on other accounts and arguments resolved at instruction-build time. + */ +export interface ResolverValueNode< + TDependsOn extends Array | undefined = Array | undefined, +> { + readonly kind: 'resolverValueNode'; + + // Data. + /** The name of the resolver function. */ + readonly name: CamelCaseString; + /** Markdown documentation for the resolver. */ + readonly docs?: Docs; + + // Children. + /** The accounts and arguments the resolver depends on. Used by clients to ensure the dependencies are resolved first. */ + readonly dependsOn?: TDependsOn; +} diff --git a/packages/node-types/src/contextualValueNodes/ContextualValueNode.ts b/packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts similarity index 59% rename from packages/node-types/src/contextualValueNodes/ContextualValueNode.ts rename to packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts index ce57a38df..2b7fee423 100644 --- a/packages/node-types/src/contextualValueNodes/ContextualValueNode.ts +++ b/packages/node-types/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts @@ -1,17 +1,14 @@ -import type { ProgramLinkNode } from '../linkNodes/ProgramLinkNode'; -import type { ValueNode } from '../valueNodes/ValueNode'; import type { AccountBumpValueNode } from './AccountBumpValueNode'; import type { AccountValueNode } from './AccountValueNode'; import type { ArgumentValueNode } from './ArgumentValueNode'; import type { ConditionalValueNode } from './ConditionalValueNode'; import type { IdentityValueNode } from './IdentityValueNode'; import type { PayerValueNode } from './PayerValueNode'; -import type { PdaSeedValueNode } from './PdaSeedValueNode'; import type { PdaValueNode } from './PdaValueNode'; import type { ProgramIdValueNode } from './ProgramIdValueNode'; import type { ResolverValueNode } from './ResolverValueNode'; -// Standalone Contextual Value Node Registration. +/** Every contextual-value node usable as a top-level value. */ export type StandaloneContextualValueNode = | AccountBumpValueNode | AccountValueNode @@ -22,10 +19,3 @@ export type StandaloneContextualValueNode = | PdaValueNode | ProgramIdValueNode | ResolverValueNode; - -// Contextual Value Node Registration. -export type RegisteredContextualValueNode = PdaSeedValueNode | StandaloneContextualValueNode; - -// Contextual Value Node Helpers. -export type ContextualValueNode = StandaloneContextualValueNode; -export type InstructionInputValueNode = ContextualValueNode | ProgramLinkNode | ValueNode; diff --git a/packages/node-types/src/contextualValueNodes/index.ts b/packages/node-types/src/generated/contextualValueNodes/index.ts similarity index 55% rename from packages/node-types/src/contextualValueNodes/index.ts rename to packages/node-types/src/generated/contextualValueNodes/index.ts index c26843d90..4df98fc0a 100644 --- a/packages/node-types/src/contextualValueNodes/index.ts +++ b/packages/node-types/src/generated/contextualValueNodes/index.ts @@ -1,11 +1,19 @@ export * from './AccountBumpValueNode'; export * from './AccountValueNode'; export * from './ArgumentValueNode'; +export * from './ConditionalValueCondition'; export * from './ConditionalValueNode'; export * from './ContextualValueNode'; export * from './IdentityValueNode'; +export * from './InstructionInputValueNode'; export * from './PayerValueNode'; export * from './PdaSeedValueNode'; +export * from './PdaSeedValueValue'; export * from './PdaValueNode'; +export * from './PdaValuePda'; +export * from './PdaValueProgramId'; export * from './ProgramIdValueNode'; +export * from './RegisteredContextualValueNode'; +export * from './ResolverDependency'; export * from './ResolverValueNode'; +export * from './StandaloneContextualValueNode'; diff --git a/packages/node-types/src/generated/countNodes/CountNode.ts b/packages/node-types/src/generated/countNodes/CountNode.ts new file mode 100644 index 000000000..c19ccffb6 --- /dev/null +++ b/packages/node-types/src/generated/countNodes/CountNode.ts @@ -0,0 +1,4 @@ +import type { RegisteredCountNode } from './RegisteredCountNode'; + +/** The composable form: any registered count node. */ +export type CountNode = RegisteredCountNode; diff --git a/packages/node-types/src/countNodes/FixedCountNode.ts b/packages/node-types/src/generated/countNodes/FixedCountNode.ts similarity index 50% rename from packages/node-types/src/countNodes/FixedCountNode.ts rename to packages/node-types/src/generated/countNodes/FixedCountNode.ts index e931c8dd9..b37e31cb9 100644 --- a/packages/node-types/src/countNodes/FixedCountNode.ts +++ b/packages/node-types/src/generated/countNodes/FixedCountNode.ts @@ -1,6 +1,8 @@ +/** A count strategy that fixes the number of items at a constant value. */ export interface FixedCountNode { readonly kind: 'fixedCountNode'; // Data. + /** The fixed number of items. */ readonly value: number; } diff --git a/packages/node-types/src/generated/countNodes/PrefixedCountNode.ts b/packages/node-types/src/generated/countNodes/PrefixedCountNode.ts new file mode 100644 index 000000000..8bafc8f49 --- /dev/null +++ b/packages/node-types/src/generated/countNodes/PrefixedCountNode.ts @@ -0,0 +1,11 @@ +import type { NestedTypeNode } from '../typeNodes/NestedTypeNode'; +import type { NumberTypeNode } from '../typeNodes/NumberTypeNode'; + +/** A count strategy where the number of items is read from a numeric prefix. */ +export interface PrefixedCountNode = NestedTypeNode> { + readonly kind: 'prefixedCountNode'; + + // Children. + /** The numeric type used as the count prefix. */ + readonly prefix: TPrefix; +} diff --git a/packages/node-types/src/countNodes/CountNode.ts b/packages/node-types/src/generated/countNodes/RegisteredCountNode.ts similarity index 73% rename from packages/node-types/src/countNodes/CountNode.ts rename to packages/node-types/src/generated/countNodes/RegisteredCountNode.ts index 00cbd5a69..81ed83c4a 100644 --- a/packages/node-types/src/countNodes/CountNode.ts +++ b/packages/node-types/src/generated/countNodes/RegisteredCountNode.ts @@ -2,8 +2,5 @@ import type { FixedCountNode } from './FixedCountNode'; import type { PrefixedCountNode } from './PrefixedCountNode'; import type { RemainderCountNode } from './RemainderCountNode'; -// Count Node Registration. +/** Every node tagged as a count strategy. */ export type RegisteredCountNode = FixedCountNode | PrefixedCountNode | RemainderCountNode; - -// Count Node Helpers. -export type CountNode = RegisteredCountNode; diff --git a/packages/node-types/src/countNodes/RemainderCountNode.ts b/packages/node-types/src/generated/countNodes/RemainderCountNode.ts similarity index 51% rename from packages/node-types/src/countNodes/RemainderCountNode.ts rename to packages/node-types/src/generated/countNodes/RemainderCountNode.ts index 29832b73e..e64259283 100644 --- a/packages/node-types/src/countNodes/RemainderCountNode.ts +++ b/packages/node-types/src/generated/countNodes/RemainderCountNode.ts @@ -1,3 +1,4 @@ +/** A count strategy where items are read until the buffer is exhausted. */ export interface RemainderCountNode { readonly kind: 'remainderCountNode'; } diff --git a/packages/node-types/src/countNodes/index.ts b/packages/node-types/src/generated/countNodes/index.ts similarity index 77% rename from packages/node-types/src/countNodes/index.ts rename to packages/node-types/src/generated/countNodes/index.ts index acd18ccd7..150a7228c 100644 --- a/packages/node-types/src/countNodes/index.ts +++ b/packages/node-types/src/generated/countNodes/index.ts @@ -1,4 +1,5 @@ export * from './CountNode'; export * from './FixedCountNode'; export * from './PrefixedCountNode'; +export * from './RegisteredCountNode'; export * from './RemainderCountNode'; diff --git a/packages/node-types/src/generated/discriminatorNodes/ConstantDiscriminatorNode.ts b/packages/node-types/src/generated/discriminatorNodes/ConstantDiscriminatorNode.ts new file mode 100644 index 000000000..a38c94599 --- /dev/null +++ b/packages/node-types/src/generated/discriminatorNodes/ConstantDiscriminatorNode.ts @@ -0,0 +1,14 @@ +import type { ConstantValueNode } from '../valueNodes/ConstantValueNode'; + +/** Identifies a node by a constant value at a known byte offset (e.g. a magic header). */ +export interface ConstantDiscriminatorNode { + readonly kind: 'constantDiscriminatorNode'; + + // Data. + /** The byte offset at which the constant begins. */ + readonly offset: number; + + // Children. + /** The constant value expected at the offset. */ + readonly constant: TConstant; +} diff --git a/packages/node-types/src/generated/discriminatorNodes/DiscriminatorNode.ts b/packages/node-types/src/generated/discriminatorNodes/DiscriminatorNode.ts new file mode 100644 index 000000000..14fa50483 --- /dev/null +++ b/packages/node-types/src/generated/discriminatorNodes/DiscriminatorNode.ts @@ -0,0 +1,4 @@ +import type { RegisteredDiscriminatorNode } from './RegisteredDiscriminatorNode'; + +/** The composable form: any registered discriminator node. */ +export type DiscriminatorNode = RegisteredDiscriminatorNode; diff --git a/packages/node-types/src/generated/discriminatorNodes/FieldDiscriminatorNode.ts b/packages/node-types/src/generated/discriminatorNodes/FieldDiscriminatorNode.ts new file mode 100644 index 000000000..a0bb25c31 --- /dev/null +++ b/packages/node-types/src/generated/discriminatorNodes/FieldDiscriminatorNode.ts @@ -0,0 +1,12 @@ +import type { CamelCaseString } from '../../brands'; + +/** Identifies a node by the value of a named field at a known byte offset. */ +export interface FieldDiscriminatorNode { + readonly kind: 'fieldDiscriminatorNode'; + + // Data. + /** The name of the discriminating field. */ + readonly name: CamelCaseString; + /** The byte offset of the field. */ + readonly offset: number; +} diff --git a/packages/node-types/src/discriminatorNodes/DiscriminatorNode.ts b/packages/node-types/src/generated/discriminatorNodes/RegisteredDiscriminatorNode.ts similarity index 72% rename from packages/node-types/src/discriminatorNodes/DiscriminatorNode.ts rename to packages/node-types/src/generated/discriminatorNodes/RegisteredDiscriminatorNode.ts index d0eb0b081..6cbff2bd5 100644 --- a/packages/node-types/src/discriminatorNodes/DiscriminatorNode.ts +++ b/packages/node-types/src/generated/discriminatorNodes/RegisteredDiscriminatorNode.ts @@ -2,8 +2,5 @@ import type { ConstantDiscriminatorNode } from './ConstantDiscriminatorNode'; import type { FieldDiscriminatorNode } from './FieldDiscriminatorNode'; import type { SizeDiscriminatorNode } from './SizeDiscriminatorNode'; -// Discriminator Node Registration. +/** Every node tagged as a discriminator strategy. */ export type RegisteredDiscriminatorNode = ConstantDiscriminatorNode | FieldDiscriminatorNode | SizeDiscriminatorNode; - -// Discriminator Node Helpers. -export type DiscriminatorNode = RegisteredDiscriminatorNode; diff --git a/packages/node-types/src/discriminatorNodes/SizeDiscriminatorNode.ts b/packages/node-types/src/generated/discriminatorNodes/SizeDiscriminatorNode.ts similarity index 57% rename from packages/node-types/src/discriminatorNodes/SizeDiscriminatorNode.ts rename to packages/node-types/src/generated/discriminatorNodes/SizeDiscriminatorNode.ts index b7264e768..c9d329a50 100644 --- a/packages/node-types/src/discriminatorNodes/SizeDiscriminatorNode.ts +++ b/packages/node-types/src/generated/discriminatorNodes/SizeDiscriminatorNode.ts @@ -1,6 +1,8 @@ +/** Identifies a node by its expected total byte size. */ export interface SizeDiscriminatorNode { readonly kind: 'sizeDiscriminatorNode'; // Data. + /** The expected byte size. */ readonly size: number; } diff --git a/packages/node-types/src/discriminatorNodes/index.ts b/packages/node-types/src/generated/discriminatorNodes/index.ts similarity index 77% rename from packages/node-types/src/discriminatorNodes/index.ts rename to packages/node-types/src/generated/discriminatorNodes/index.ts index db9e0ddd6..c569607f2 100644 --- a/packages/node-types/src/discriminatorNodes/index.ts +++ b/packages/node-types/src/generated/discriminatorNodes/index.ts @@ -1,4 +1,5 @@ export * from './ConstantDiscriminatorNode'; export * from './DiscriminatorNode'; export * from './FieldDiscriminatorNode'; +export * from './RegisteredDiscriminatorNode'; export * from './SizeDiscriminatorNode'; diff --git a/packages/node-types/src/generated/index.ts b/packages/node-types/src/generated/index.ts new file mode 100644 index 000000000..14e1d6761 --- /dev/null +++ b/packages/node-types/src/generated/index.ts @@ -0,0 +1,26 @@ +export * from './AccountNode'; +export * from './ConstantNode'; +export * from './DefinedTypeNode'; +export * from './ErrorNode'; +export * from './EventNode'; +export * from './InstructionAccountNode'; +export * from './InstructionArgumentNode'; +export * from './InstructionByteDeltaNode'; +export * from './InstructionByteDeltaValue'; +export * from './InstructionNode'; +export * from './InstructionRemainingAccountsNode'; +export * from './InstructionRemainingAccountsValue'; +export * from './InstructionStatusNode'; +export * from './Node'; +export * from './PdaNode'; +export * from './ProgramNode'; +export * from './RootNode'; + +export * from './contextualValueNodes'; +export * from './countNodes'; +export * from './discriminatorNodes'; +export * from './linkNodes'; +export * from './pdaSeedNodes'; +export * from './shared'; +export * from './typeNodes'; +export * from './valueNodes'; diff --git a/packages/node-types/src/linkNodes/AccountLinkNode.ts b/packages/node-types/src/generated/linkNodes/AccountLinkNode.ts similarity index 51% rename from packages/node-types/src/linkNodes/AccountLinkNode.ts rename to packages/node-types/src/generated/linkNodes/AccountLinkNode.ts index f6cebd7cf..da2048adf 100644 --- a/packages/node-types/src/linkNodes/AccountLinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/AccountLinkNode.ts @@ -1,12 +1,15 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { ProgramLinkNode } from './ProgramLinkNode'; +/** A reference to an account defined elsewhere — possibly in a different program. */ export interface AccountLinkNode { readonly kind: 'accountLinkNode'; - // Children. - readonly program?: TProgram; - // Data. + /** The name of the referenced account. */ readonly name: CamelCaseString; + + // Children. + /** The program the referenced account belongs to. When omitted, the surrounding program is assumed. */ + readonly program?: TProgram; } diff --git a/packages/node-types/src/linkNodes/DefinedTypeLinkNode.ts b/packages/node-types/src/generated/linkNodes/DefinedTypeLinkNode.ts similarity index 52% rename from packages/node-types/src/linkNodes/DefinedTypeLinkNode.ts rename to packages/node-types/src/generated/linkNodes/DefinedTypeLinkNode.ts index 4128fc10c..37c2b1983 100644 --- a/packages/node-types/src/linkNodes/DefinedTypeLinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/DefinedTypeLinkNode.ts @@ -1,12 +1,15 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { ProgramLinkNode } from './ProgramLinkNode'; +/** A reference to a defined type — possibly in a different program. */ export interface DefinedTypeLinkNode { readonly kind: 'definedTypeLinkNode'; - // Children. - readonly program?: TProgram; - // Data. + /** The name of the referenced defined type. */ readonly name: CamelCaseString; + + // Children. + /** The program the referenced type is defined in. When omitted, the surrounding program is assumed. */ + readonly program?: TProgram; } diff --git a/packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts b/packages/node-types/src/generated/linkNodes/InstructionAccountLinkNode.ts similarity index 56% rename from packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts rename to packages/node-types/src/generated/linkNodes/InstructionAccountLinkNode.ts index 07f193e42..529900760 100644 --- a/packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/InstructionAccountLinkNode.ts @@ -1,14 +1,17 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { InstructionLinkNode } from './InstructionLinkNode'; +/** A reference to an account of another instruction. */ export interface InstructionAccountLinkNode< TInstruction extends InstructionLinkNode | undefined = InstructionLinkNode | undefined, > { readonly kind: 'instructionAccountLinkNode'; - // Children. - readonly instruction?: TInstruction; - // Data. + /** The name of the referenced instruction account. */ readonly name: CamelCaseString; + + // Children. + /** The instruction the referenced account belongs to. When omitted, the surrounding instruction is assumed. */ + readonly instruction?: TInstruction; } diff --git a/packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts b/packages/node-types/src/generated/linkNodes/InstructionArgumentLinkNode.ts similarity index 56% rename from packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts rename to packages/node-types/src/generated/linkNodes/InstructionArgumentLinkNode.ts index 9e5e47dde..537c14568 100644 --- a/packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/InstructionArgumentLinkNode.ts @@ -1,14 +1,17 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { InstructionLinkNode } from './InstructionLinkNode'; +/** A reference to an argument of another instruction. */ export interface InstructionArgumentLinkNode< TInstruction extends InstructionLinkNode | undefined = InstructionLinkNode | undefined, > { readonly kind: 'instructionArgumentLinkNode'; - // Children. - readonly instruction?: TInstruction; - // Data. + /** The name of the referenced instruction argument. */ readonly name: CamelCaseString; + + // Children. + /** The instruction the referenced argument belongs to. When omitted, the surrounding instruction is assumed. */ + readonly instruction?: TInstruction; } diff --git a/packages/node-types/src/linkNodes/InstructionLinkNode.ts b/packages/node-types/src/generated/linkNodes/InstructionLinkNode.ts similarity index 50% rename from packages/node-types/src/linkNodes/InstructionLinkNode.ts rename to packages/node-types/src/generated/linkNodes/InstructionLinkNode.ts index 47621ee9a..317f3ac85 100644 --- a/packages/node-types/src/linkNodes/InstructionLinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/InstructionLinkNode.ts @@ -1,12 +1,15 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { ProgramLinkNode } from './ProgramLinkNode'; +/** A reference to an instruction defined elsewhere — possibly in a different program. */ export interface InstructionLinkNode { readonly kind: 'instructionLinkNode'; - // Children. - readonly program?: TProgram; - // Data. + /** The name of the referenced instruction. */ readonly name: CamelCaseString; + + // Children. + /** The program the referenced instruction belongs to. When omitted, the surrounding program is assumed. */ + readonly program?: TProgram; } diff --git a/packages/node-types/src/generated/linkNodes/LinkNode.ts b/packages/node-types/src/generated/linkNodes/LinkNode.ts new file mode 100644 index 000000000..80153d340 --- /dev/null +++ b/packages/node-types/src/generated/linkNodes/LinkNode.ts @@ -0,0 +1,4 @@ +import type { RegisteredLinkNode } from './RegisteredLinkNode'; + +/** The composable form: any registered link node. */ +export type LinkNode = RegisteredLinkNode; diff --git a/packages/node-types/src/linkNodes/PdaLinkNode.ts b/packages/node-types/src/generated/linkNodes/PdaLinkNode.ts similarity index 51% rename from packages/node-types/src/linkNodes/PdaLinkNode.ts rename to packages/node-types/src/generated/linkNodes/PdaLinkNode.ts index 4c50e46ff..f5e6edf4a 100644 --- a/packages/node-types/src/linkNodes/PdaLinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/PdaLinkNode.ts @@ -1,12 +1,15 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { ProgramLinkNode } from './ProgramLinkNode'; +/** A reference to a PDA defined elsewhere — possibly in a different program. */ export interface PdaLinkNode { readonly kind: 'pdaLinkNode'; - // Children. - readonly program?: TProgram; - // Data. + /** The name of the referenced PDA. */ readonly name: CamelCaseString; + + // Children. + /** The program the referenced PDA belongs to. When omitted, the surrounding program is assumed. */ + readonly program?: TProgram; } diff --git a/packages/node-types/src/generated/linkNodes/ProgramLinkNode.ts b/packages/node-types/src/generated/linkNodes/ProgramLinkNode.ts new file mode 100644 index 000000000..337072030 --- /dev/null +++ b/packages/node-types/src/generated/linkNodes/ProgramLinkNode.ts @@ -0,0 +1,10 @@ +import type { CamelCaseString } from '../../brands'; + +/** A reference to a program by name. */ +export interface ProgramLinkNode { + readonly kind: 'programLinkNode'; + + // Data. + /** The name of the referenced program. */ + readonly name: CamelCaseString; +} diff --git a/packages/node-types/src/linkNodes/LinkNode.ts b/packages/node-types/src/generated/linkNodes/RegisteredLinkNode.ts similarity index 87% rename from packages/node-types/src/linkNodes/LinkNode.ts rename to packages/node-types/src/generated/linkNodes/RegisteredLinkNode.ts index 77cbe8b21..3a18c30c2 100644 --- a/packages/node-types/src/linkNodes/LinkNode.ts +++ b/packages/node-types/src/generated/linkNodes/RegisteredLinkNode.ts @@ -6,7 +6,7 @@ import type { InstructionLinkNode } from './InstructionLinkNode'; import type { PdaLinkNode } from './PdaLinkNode'; import type { ProgramLinkNode } from './ProgramLinkNode'; -// Link Node Registration. +/** Every node tagged as a link to another part of the IDL. */ export type RegisteredLinkNode = | AccountLinkNode | DefinedTypeLinkNode @@ -15,6 +15,3 @@ export type RegisteredLinkNode = | InstructionLinkNode | PdaLinkNode | ProgramLinkNode; - -// Link Node Helpers. -export type LinkNode = RegisteredLinkNode; diff --git a/packages/node-types/src/linkNodes/index.ts b/packages/node-types/src/generated/linkNodes/index.ts similarity index 88% rename from packages/node-types/src/linkNodes/index.ts rename to packages/node-types/src/generated/linkNodes/index.ts index c056ec68c..0cad66538 100644 --- a/packages/node-types/src/linkNodes/index.ts +++ b/packages/node-types/src/generated/linkNodes/index.ts @@ -6,3 +6,4 @@ export * from './InstructionLinkNode'; export * from './LinkNode'; export * from './PdaLinkNode'; export * from './ProgramLinkNode'; +export * from './RegisteredLinkNode'; diff --git a/packages/node-types/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts b/packages/node-types/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts new file mode 100644 index 000000000..dd81904e4 --- /dev/null +++ b/packages/node-types/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts @@ -0,0 +1,16 @@ +import type { TypeNode } from '../typeNodes/TypeNode'; +import type { ConstantPdaSeedValue } from './ConstantPdaSeedValue'; + +/** A PDA seed with a constant value (e.g. a UTF-8 string or a fixed byte sequence). */ +export interface ConstantPdaSeedNode< + TType extends TypeNode = TypeNode, + TValue extends ConstantPdaSeedValue = ConstantPdaSeedValue, +> { + readonly kind: 'constantPdaSeedNode'; + + // Children. + /** The type of the seed value. */ + readonly type: TType; + /** The constant value to use as the seed — either a literal value or the program ID placeholder. */ + readonly value: TValue; +} diff --git a/packages/node-types/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts b/packages/node-types/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts new file mode 100644 index 000000000..5f17559e4 --- /dev/null +++ b/packages/node-types/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts @@ -0,0 +1,5 @@ +import type { ProgramIdValueNode } from '../contextualValueNodes/ProgramIdValueNode'; +import type { ValueNode } from '../valueNodes/ValueNode'; + +/** The value forms a `constantPdaSeedNode` may carry — either a literal value or the program ID placeholder. */ +export type ConstantPdaSeedValue = ProgramIdValueNode | ValueNode; diff --git a/packages/node-types/src/generated/pdaSeedNodes/PdaSeedNode.ts b/packages/node-types/src/generated/pdaSeedNodes/PdaSeedNode.ts new file mode 100644 index 000000000..0e4ca9ec7 --- /dev/null +++ b/packages/node-types/src/generated/pdaSeedNodes/PdaSeedNode.ts @@ -0,0 +1,4 @@ +import type { RegisteredPdaSeedNode } from './RegisteredPdaSeedNode'; + +/** The composable form: any registered PDA seed node. */ +export type PdaSeedNode = RegisteredPdaSeedNode; diff --git a/packages/node-types/src/pdaSeedNodes/PdaSeedNode.ts b/packages/node-types/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts similarity index 66% rename from packages/node-types/src/pdaSeedNodes/PdaSeedNode.ts rename to packages/node-types/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts index e9f01f3ed..671db3669 100644 --- a/packages/node-types/src/pdaSeedNodes/PdaSeedNode.ts +++ b/packages/node-types/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts @@ -1,8 +1,5 @@ import type { ConstantPdaSeedNode } from './ConstantPdaSeedNode'; import type { VariablePdaSeedNode } from './VariablePdaSeedNode'; -// Pda Seed Node Registration. +/** Every node tagged as a PDA seed. */ export type RegisteredPdaSeedNode = ConstantPdaSeedNode | VariablePdaSeedNode; - -// Pda Seed Node Helpers. -export type PdaSeedNode = RegisteredPdaSeedNode; diff --git a/packages/node-types/src/generated/pdaSeedNodes/VariablePdaSeedNode.ts b/packages/node-types/src/generated/pdaSeedNodes/VariablePdaSeedNode.ts new file mode 100644 index 000000000..d1a888614 --- /dev/null +++ b/packages/node-types/src/generated/pdaSeedNodes/VariablePdaSeedNode.ts @@ -0,0 +1,18 @@ +import type { CamelCaseString } from '../../brands'; +import type { Docs } from '../../Docs'; +import type { TypeNode } from '../typeNodes/TypeNode'; + +/** A PDA seed whose value is provided at derivation time, identified by name. */ +export interface VariablePdaSeedNode { + readonly kind: 'variablePdaSeedNode'; + + // Data. + /** The name of the seed variable. */ + readonly name: CamelCaseString; + /** Markdown documentation for the seed variable. */ + readonly docs?: Docs; + + // Children. + /** The expected type of the seed value. */ + readonly type: TType; +} diff --git a/packages/node-types/src/pdaSeedNodes/index.ts b/packages/node-types/src/generated/pdaSeedNodes/index.ts similarity index 57% rename from packages/node-types/src/pdaSeedNodes/index.ts rename to packages/node-types/src/generated/pdaSeedNodes/index.ts index 1699e00cd..3b6cd92b9 100644 --- a/packages/node-types/src/pdaSeedNodes/index.ts +++ b/packages/node-types/src/generated/pdaSeedNodes/index.ts @@ -1,3 +1,5 @@ export * from './ConstantPdaSeedNode'; +export * from './ConstantPdaSeedValue'; export * from './PdaSeedNode'; +export * from './RegisteredPdaSeedNode'; export * from './VariablePdaSeedNode'; diff --git a/packages/node-types/src/generated/shared/bytesEncoding.ts b/packages/node-types/src/generated/shared/bytesEncoding.ts new file mode 100644 index 000000000..ad0ab27a9 --- /dev/null +++ b/packages/node-types/src/generated/shared/bytesEncoding.ts @@ -0,0 +1,10 @@ +/** How a string of bytes is encoded for transport. */ +export type BytesEncoding = + /** Hexadecimal encoding (two characters per byte). */ + | 'base16' + /** Base58 encoding, the standard for Solana addresses. */ + | 'base58' + /** Base64 encoding (RFC 4648). */ + | 'base64' + /** UTF-8 text encoding. */ + | 'utf8'; diff --git a/packages/node-types/src/generated/shared/codamaVersion.ts b/packages/node-types/src/generated/shared/codamaVersion.ts new file mode 100644 index 000000000..e7936cd11 --- /dev/null +++ b/packages/node-types/src/generated/shared/codamaVersion.ts @@ -0,0 +1,6 @@ +/** + * The Codama spec version this package describes. Pinned to the literal + * version of the spec at generation time; documents conforming to this + * version of the spec carry this exact string. + */ +export type CodamaVersion = '1.6.0'; diff --git a/packages/node-types/src/generated/shared/defaultValueStrategy.ts b/packages/node-types/src/generated/shared/defaultValueStrategy.ts new file mode 100644 index 000000000..82e465190 --- /dev/null +++ b/packages/node-types/src/generated/shared/defaultValueStrategy.ts @@ -0,0 +1,6 @@ +/** How an attribute that carries a default value is exposed in generated APIs. */ +export type DefaultValueStrategy = + /** The attribute is not exposed as a parameter in the generated API; the default value is always used. */ + | 'omitted' + /** The attribute is exposed as an optional parameter; callers may override the default value. */ + | 'optional'; diff --git a/packages/node-types/src/generated/shared/endianness.ts b/packages/node-types/src/generated/shared/endianness.ts new file mode 100644 index 000000000..9889c0559 --- /dev/null +++ b/packages/node-types/src/generated/shared/endianness.ts @@ -0,0 +1,6 @@ +/** The byte order of a numeric serialization. */ +export type Endianness = + /** Big-endian: the most significant byte is written first. */ + | 'be' + /** Little-endian: the least significant byte is written first. */ + | 'le'; diff --git a/packages/node-types/src/generated/shared/index.ts b/packages/node-types/src/generated/shared/index.ts new file mode 100644 index 000000000..173d8155b --- /dev/null +++ b/packages/node-types/src/generated/shared/index.ts @@ -0,0 +1,10 @@ +export * from './bytesEncoding'; +export * from './codamaVersion'; +export * from './defaultValueStrategy'; +export * from './endianness'; +export * from './instructionLifecycle'; +export * from './numberFormat'; +export * from './optionalAccountStrategy'; +export * from './postOffsetStrategy'; +export * from './preOffsetStrategy'; +export * from './programOrigin'; diff --git a/packages/node-types/src/generated/shared/instructionLifecycle.ts b/packages/node-types/src/generated/shared/instructionLifecycle.ts new file mode 100644 index 000000000..cd6ca6e6d --- /dev/null +++ b/packages/node-types/src/generated/shared/instructionLifecycle.ts @@ -0,0 +1,10 @@ +/** The lifecycle stage of an instruction. */ +export type InstructionLifecycle = + /** No longer included in client SDKs. Retained in the IDL for historical reference only. */ + | 'archived' + /** Still callable but discouraged. Clients should migrate to a replacement instruction. */ + | 'deprecated' + /** Work-in-progress. The instruction may change before it stabilises. */ + | 'draft' + /** Stable and supported for production use. */ + | 'live'; diff --git a/packages/node-types/src/generated/shared/numberFormat.ts b/packages/node-types/src/generated/shared/numberFormat.ts new file mode 100644 index 000000000..b594a6ae0 --- /dev/null +++ b/packages/node-types/src/generated/shared/numberFormat.ts @@ -0,0 +1,28 @@ +/** The wire format of a numeric serialization. */ +export type NumberFormat = + /** IEEE-754 32-bit floating point. */ + | 'f32' + /** IEEE-754 64-bit floating point. */ + | 'f64' + /** Signed 128-bit integer. */ + | 'i128' + /** Signed 16-bit integer. */ + | 'i16' + /** Signed 32-bit integer. */ + | 'i32' + /** Signed 64-bit integer. */ + | 'i64' + /** Signed 8-bit integer. */ + | 'i8' + /** Solana compact-u16 encoding: a variable-length unsigned integer occupying 1 to 3 bytes. */ + | 'shortU16' + /** Unsigned 128-bit integer. */ + | 'u128' + /** Unsigned 16-bit integer. */ + | 'u16' + /** Unsigned 32-bit integer. */ + | 'u32' + /** Unsigned 64-bit integer. */ + | 'u64' + /** Unsigned 8-bit integer. */ + | 'u8'; diff --git a/packages/node-types/src/generated/shared/optionalAccountStrategy.ts b/packages/node-types/src/generated/shared/optionalAccountStrategy.ts new file mode 100644 index 000000000..ff7c74db1 --- /dev/null +++ b/packages/node-types/src/generated/shared/optionalAccountStrategy.ts @@ -0,0 +1,6 @@ +/** How an absent optional account is represented when serialising an instruction. */ +export type OptionalAccountStrategy = + /** The account slot is left out of the instruction entirely. Subsequent accounts shift up. */ + | 'omitted' + /** The account slot is filled with the program ID as a placeholder, preserving positional indices. */ + | 'programId'; diff --git a/packages/node-types/src/generated/shared/postOffsetStrategy.ts b/packages/node-types/src/generated/shared/postOffsetStrategy.ts new file mode 100644 index 000000000..0f6f31cac --- /dev/null +++ b/packages/node-types/src/generated/shared/postOffsetStrategy.ts @@ -0,0 +1,10 @@ +/** How a post-offset modifier interprets its offset value after serialising the wrapped type. */ +export type PostOffsetStrategy = + /** Move the cursor to the absolute byte position given by the offset. */ + | 'absolute' + /** Pad with zero bytes from the current cursor up to the offset bytes ahead. */ + | 'padded' + /** Restore the cursor to where it was before the wrapped type ran (cancelling its pre-offset). */ + | 'preOffset' + /** Advance the cursor by the offset bytes relative to its current position. */ + | 'relative'; diff --git a/packages/node-types/src/generated/shared/preOffsetStrategy.ts b/packages/node-types/src/generated/shared/preOffsetStrategy.ts new file mode 100644 index 000000000..35bec39c3 --- /dev/null +++ b/packages/node-types/src/generated/shared/preOffsetStrategy.ts @@ -0,0 +1,8 @@ +/** How a pre-offset modifier interprets its offset value before serialising the wrapped type. */ +export type PreOffsetStrategy = + /** Move the cursor to the absolute byte position given by the offset. */ + | 'absolute' + /** Pad with zero bytes from the current cursor up to the offset bytes ahead. */ + | 'padded' + /** Advance the cursor by the offset bytes relative to its current position. */ + | 'relative'; diff --git a/packages/node-types/src/generated/shared/programOrigin.ts b/packages/node-types/src/generated/shared/programOrigin.ts new file mode 100644 index 000000000..13eec0c2e --- /dev/null +++ b/packages/node-types/src/generated/shared/programOrigin.ts @@ -0,0 +1,6 @@ +/** The toolchain that originally generated a program description. */ +export type ProgramOrigin = + /** The program was originally described by an Anchor IDL. */ + | 'anchor' + /** The program was originally described by a Shank IDL. */ + | 'shank'; diff --git a/packages/node-types/src/generated/typeNodes/AmountTypeNode.ts b/packages/node-types/src/generated/typeNodes/AmountTypeNode.ts new file mode 100644 index 000000000..297ec5fa2 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/AmountTypeNode.ts @@ -0,0 +1,23 @@ +import type { NestedTypeNode } from './NestedTypeNode'; +import type { NumberTypeNode } from './NumberTypeNode'; + +/** + * Wraps a number type to provide additional context such as decimal places and a unit. + * Useful for amounts representing financial values. + */ +export interface AmountTypeNode = NestedTypeNode> { + readonly kind: 'amountTypeNode'; + + // Data. + /** + * The number of decimal places the wrapped integer carries. + * For example, an integer value of 12345 with 2 decimal places represents 123.45. + */ + readonly decimals: number; + /** The unit of the amount — e.g. "USD" or "%". */ + readonly unit?: string; + + // Children. + /** The number type the amount wraps. */ + readonly number: TNumber; +} diff --git a/packages/node-types/src/generated/typeNodes/ArrayTypeNode.ts b/packages/node-types/src/generated/typeNodes/ArrayTypeNode.ts new file mode 100644 index 000000000..5d6b3fb1a --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/ArrayTypeNode.ts @@ -0,0 +1,13 @@ +import type { CountNode } from '../countNodes/CountNode'; +import type { TypeNode } from './TypeNode'; + +/** A homogeneous list of items. The item type is defined by `item`; the length is determined by the `count` strategy. */ +export interface ArrayTypeNode { + readonly kind: 'arrayTypeNode'; + + // Children. + /** The type of each item in the array. */ + readonly item: TItem; + /** The strategy used to determine the number of items. */ + readonly count: TCount; +} diff --git a/packages/node-types/src/typeNodes/BooleanTypeNode.ts b/packages/node-types/src/generated/typeNodes/BooleanTypeNode.ts similarity index 66% rename from packages/node-types/src/typeNodes/BooleanTypeNode.ts rename to packages/node-types/src/generated/typeNodes/BooleanTypeNode.ts index eaec1588d..59578abbf 100644 --- a/packages/node-types/src/typeNodes/BooleanTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/BooleanTypeNode.ts @@ -1,9 +1,11 @@ import type { NestedTypeNode } from './NestedTypeNode'; import type { NumberTypeNode } from './NumberTypeNode'; +/** A boolean serialised as a numeric value. The wrapped number type determines the byte width. */ export interface BooleanTypeNode = NestedTypeNode> { readonly kind: 'booleanTypeNode'; // Children. + /** The numeric type used to serialise the boolean. */ readonly size: TSize; } diff --git a/packages/node-types/src/generated/typeNodes/BytesTypeNode.ts b/packages/node-types/src/generated/typeNodes/BytesTypeNode.ts new file mode 100644 index 000000000..c08e8194e --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/BytesTypeNode.ts @@ -0,0 +1,4 @@ +/** A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or sentinel-terminated wrapper. */ +export interface BytesTypeNode { + readonly kind: 'bytesTypeNode'; +} diff --git a/packages/node-types/src/typeNodes/DateTimeTypeNode.ts b/packages/node-types/src/generated/typeNodes/DateTimeTypeNode.ts similarity index 62% rename from packages/node-types/src/typeNodes/DateTimeTypeNode.ts rename to packages/node-types/src/generated/typeNodes/DateTimeTypeNode.ts index 2959fe21c..94949363b 100644 --- a/packages/node-types/src/typeNodes/DateTimeTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/DateTimeTypeNode.ts @@ -1,9 +1,11 @@ import type { NestedTypeNode } from './NestedTypeNode'; import type { NumberTypeNode } from './NumberTypeNode'; +/** A timestamp encoded as a number, typically seconds since the Unix epoch. The wrapped number type determines the byte width. */ export interface DateTimeTypeNode = NestedTypeNode> { readonly kind: 'dateTimeTypeNode'; // Children. + /** The numeric type used to serialise the timestamp. */ readonly number: TNumber; } diff --git a/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts new file mode 100644 index 000000000..3663e9598 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts @@ -0,0 +1,12 @@ +import type { CamelCaseString } from '../../brands'; + +/** A unit-style variant of an enum that carries no payload. */ +export interface EnumEmptyVariantTypeNode { + readonly kind: 'enumEmptyVariantTypeNode'; + + // Data. + /** The name of the variant. */ + readonly name: CamelCaseString; + /** Explicit discriminator value. When omitted, the discriminator is inferred from the variant position. */ + readonly discriminator?: number; +} diff --git a/packages/node-types/src/typeNodes/EnumStructVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts similarity index 56% rename from packages/node-types/src/typeNodes/EnumStructVariantTypeNode.ts rename to packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts index d3e43e658..d6ced0411 100644 --- a/packages/node-types/src/typeNodes/EnumStructVariantTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumStructVariantTypeNode.ts @@ -1,16 +1,20 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { NestedTypeNode } from './NestedTypeNode'; import type { StructTypeNode } from './StructTypeNode'; +/** A variant of an enum that carries a struct payload (named fields). */ export interface EnumStructVariantTypeNode< TStruct extends NestedTypeNode = NestedTypeNode, > { readonly kind: 'enumStructVariantTypeNode'; // Data. + /** The name of the variant. */ readonly name: CamelCaseString; + /** Explicit discriminator value. When omitted, the discriminator is inferred from the variant position. */ readonly discriminator?: number; // Children. + /** The struct of named fields carried by the variant. */ readonly struct: TStruct; } diff --git a/packages/node-types/src/typeNodes/EnumTupleVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts similarity index 55% rename from packages/node-types/src/typeNodes/EnumTupleVariantTypeNode.ts rename to packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts index 2794554d0..ce019eadd 100644 --- a/packages/node-types/src/typeNodes/EnumTupleVariantTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumTupleVariantTypeNode.ts @@ -1,16 +1,20 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { NestedTypeNode } from './NestedTypeNode'; import type { TupleTypeNode } from './TupleTypeNode'; +/** A variant of an enum that carries a tuple payload (positional fields). */ export interface EnumTupleVariantTypeNode< TTuple extends NestedTypeNode = NestedTypeNode, > { readonly kind: 'enumTupleVariantTypeNode'; // Data. + /** The name of the variant. */ readonly name: CamelCaseString; + /** Explicit discriminator value. When omitted, the discriminator is inferred from the variant position. */ readonly discriminator?: number; // Children. + /** The tuple of positional fields carried by the variant. */ readonly tuple: TTuple; } diff --git a/packages/node-types/src/typeNodes/EnumTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumTypeNode.ts similarity index 58% rename from packages/node-types/src/typeNodes/EnumTypeNode.ts rename to packages/node-types/src/generated/typeNodes/EnumTypeNode.ts index dce8dde6f..96c3a53f9 100644 --- a/packages/node-types/src/typeNodes/EnumTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumTypeNode.ts @@ -2,13 +2,16 @@ import type { EnumVariantTypeNode } from './EnumVariantTypeNode'; import type { NestedTypeNode } from './NestedTypeNode'; import type { NumberTypeNode } from './NumberTypeNode'; +/** A tagged union: a numeric discriminator followed by one of several variant payloads. */ export interface EnumTypeNode< - TVariants extends EnumVariantTypeNode[] = EnumVariantTypeNode[], + TVariants extends Array = Array, TSize extends NestedTypeNode = NestedTypeNode, > { readonly kind: 'enumTypeNode'; // Children. + /** The variants of the enum, in declaration order. */ readonly variants: TVariants; + /** The numeric type used to serialise the discriminator. */ readonly size: TSize; } diff --git a/packages/node-types/src/typeNodes/EnumVariantTypeNode.ts b/packages/node-types/src/generated/typeNodes/EnumVariantTypeNode.ts similarity index 87% rename from packages/node-types/src/typeNodes/EnumVariantTypeNode.ts rename to packages/node-types/src/generated/typeNodes/EnumVariantTypeNode.ts index a3d6b47b7..5ad10cbaf 100644 --- a/packages/node-types/src/typeNodes/EnumVariantTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/EnumVariantTypeNode.ts @@ -2,4 +2,5 @@ import type { EnumEmptyVariantTypeNode } from './EnumEmptyVariantTypeNode'; import type { EnumStructVariantTypeNode } from './EnumStructVariantTypeNode'; import type { EnumTupleVariantTypeNode } from './EnumTupleVariantTypeNode'; +/** The variant flavours of an `enumTypeNode`. */ export type EnumVariantTypeNode = EnumEmptyVariantTypeNode | EnumStructVariantTypeNode | EnumTupleVariantTypeNode; diff --git a/packages/node-types/src/typeNodes/FixedSizeTypeNode.ts b/packages/node-types/src/generated/typeNodes/FixedSizeTypeNode.ts similarity index 51% rename from packages/node-types/src/typeNodes/FixedSizeTypeNode.ts rename to packages/node-types/src/generated/typeNodes/FixedSizeTypeNode.ts index e69da9f19..df18c1c32 100644 --- a/packages/node-types/src/typeNodes/FixedSizeTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/FixedSizeTypeNode.ts @@ -1,11 +1,14 @@ import type { TypeNode } from './TypeNode'; +/** Wraps another type and asserts a fixed total byte size. Padding or truncation is applied as needed. */ export interface FixedSizeTypeNode { readonly kind: 'fixedSizeTypeNode'; // Data. + /** The total byte size the wrapped type must occupy. */ readonly size: number; // Children. + /** The wrapped type whose serialisation is constrained. */ readonly type: TType; } diff --git a/packages/node-types/src/generated/typeNodes/HiddenPrefixTypeNode.ts b/packages/node-types/src/generated/typeNodes/HiddenPrefixTypeNode.ts new file mode 100644 index 000000000..92e3e4fd5 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/HiddenPrefixTypeNode.ts @@ -0,0 +1,16 @@ +import type { ConstantValueNode } from '../valueNodes/ConstantValueNode'; +import type { TypeNode } from './TypeNode'; + +/** Prefixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. */ +export interface HiddenPrefixTypeNode< + TType extends TypeNode = TypeNode, + TPrefix extends Array = Array, +> { + readonly kind: 'hiddenPrefixTypeNode'; + + // Children. + /** The wrapped type whose serialisation is preceded by the hidden prefix. */ + readonly type: TType; + /** The constant values written before the wrapped type, in order. */ + readonly prefix: TPrefix; +} diff --git a/packages/node-types/src/generated/typeNodes/HiddenSuffixTypeNode.ts b/packages/node-types/src/generated/typeNodes/HiddenSuffixTypeNode.ts new file mode 100644 index 000000000..349c44c07 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/HiddenSuffixTypeNode.ts @@ -0,0 +1,16 @@ +import type { ConstantValueNode } from '../valueNodes/ConstantValueNode'; +import type { TypeNode } from './TypeNode'; + +/** Suffixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. */ +export interface HiddenSuffixTypeNode< + TType extends TypeNode = TypeNode, + TSuffix extends Array = Array, +> { + readonly kind: 'hiddenSuffixTypeNode'; + + // Children. + /** The wrapped type whose serialisation is followed by the hidden suffix. */ + readonly type: TType; + /** The constant values written after the wrapped type, in order. */ + readonly suffix: TSuffix; +} diff --git a/packages/node-types/src/generated/typeNodes/MapTypeNode.ts b/packages/node-types/src/generated/typeNodes/MapTypeNode.ts new file mode 100644 index 000000000..89b9b056c --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/MapTypeNode.ts @@ -0,0 +1,22 @@ +import type { CountNode } from '../countNodes/CountNode'; +import type { TypeNode } from './TypeNode'; + +/** + * A keyed map. + * The key and value types are described by their respective type nodes; the entry count is determined by a count strategy. + */ +export interface MapTypeNode< + TKey extends TypeNode = TypeNode, + TValue extends TypeNode = TypeNode, + TCount extends CountNode = CountNode, +> { + readonly kind: 'mapTypeNode'; + + // Children. + /** The type of each entry key. */ + readonly key: TKey; + /** The type of each entry value. */ + readonly value: TValue; + /** The strategy used to determine the number of entries. */ + readonly count: TCount; +} diff --git a/packages/node-types/src/typeNodes/NestedTypeNode.ts b/packages/node-types/src/generated/typeNodes/NestedTypeNode.ts similarity index 80% rename from packages/node-types/src/typeNodes/NestedTypeNode.ts rename to packages/node-types/src/generated/typeNodes/NestedTypeNode.ts index 3e07f199d..fa988dbeb 100644 --- a/packages/node-types/src/typeNodes/NestedTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/NestedTypeNode.ts @@ -7,6 +7,10 @@ import type { SentinelTypeNode } from './SentinelTypeNode'; import type { SizePrefixTypeNode } from './SizePrefixTypeNode'; import type { TypeNode } from './TypeNode'; +/** + * A type, possibly wrapped in zero-or-more size, offset, sentinel, or hidden prefix/suffix modifiers. + * The wrapping is recursive: each modifier wraps another `NestedTypeNode` until the inner `T` is reached. + */ export type NestedTypeNode = | FixedSizeTypeNode> | HiddenPrefixTypeNode> diff --git a/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts b/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts new file mode 100644 index 000000000..8364373d4 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/NumberTypeNode.ts @@ -0,0 +1,13 @@ +import type { Endianness } from '../shared/endianness'; +import type { NumberFormat } from '../shared/numberFormat'; + +/** A numeric type with a fixed wire format and byte order. */ +export interface NumberTypeNode { + readonly kind: 'numberTypeNode'; + + // Data. + /** The wire format used to serialise the number. */ + readonly format: TFormat; + /** The byte order used to serialise the number. */ + readonly endian: Endianness; +} diff --git a/packages/node-types/src/typeNodes/OptionTypeNode.ts b/packages/node-types/src/generated/typeNodes/OptionTypeNode.ts similarity index 57% rename from packages/node-types/src/typeNodes/OptionTypeNode.ts rename to packages/node-types/src/generated/typeNodes/OptionTypeNode.ts index 05130cac9..c8f06f642 100644 --- a/packages/node-types/src/typeNodes/OptionTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/OptionTypeNode.ts @@ -2,6 +2,7 @@ import type { NestedTypeNode } from './NestedTypeNode'; import type { NumberTypeNode } from './NumberTypeNode'; import type { TypeNode } from './TypeNode'; +/** A value that may be present or absent (Some/None), with an explicit numeric prefix indicating presence. */ export interface OptionTypeNode< TItem extends TypeNode = TypeNode, TPrefix extends NestedTypeNode = NestedTypeNode, @@ -9,9 +10,12 @@ export interface OptionTypeNode< readonly kind: 'optionTypeNode'; // Data. + /** When `true`, the absent variant still occupies the byte size of the present variant (zero-padded). Defaults to `false`. */ readonly fixed?: boolean; // Children. + /** The type carried by the option when present. */ readonly item: TItem; + /** The numeric type used as the presence flag. */ readonly prefix: TPrefix; } diff --git a/packages/node-types/src/generated/typeNodes/PostOffsetTypeNode.ts b/packages/node-types/src/generated/typeNodes/PostOffsetTypeNode.ts new file mode 100644 index 000000000..1c051361e --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/PostOffsetTypeNode.ts @@ -0,0 +1,17 @@ +import type { PostOffsetStrategy } from '../shared/postOffsetStrategy'; +import type { TypeNode } from './TypeNode'; + +/** After serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. */ +export interface PostOffsetTypeNode { + readonly kind: 'postOffsetTypeNode'; + + // Data. + /** The signed byte offset to apply after the wrapped type runs. */ + readonly offset: number; + /** How the `offset` value is interpreted. */ + readonly strategy: PostOffsetStrategy; + + // Children. + /** The wrapped type whose serialisation is followed by the offset. */ + readonly type: TType; +} diff --git a/packages/node-types/src/generated/typeNodes/PreOffsetTypeNode.ts b/packages/node-types/src/generated/typeNodes/PreOffsetTypeNode.ts new file mode 100644 index 000000000..affff8ad9 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/PreOffsetTypeNode.ts @@ -0,0 +1,17 @@ +import type { PreOffsetStrategy } from '../shared/preOffsetStrategy'; +import type { TypeNode } from './TypeNode'; + +/** Before serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. */ +export interface PreOffsetTypeNode { + readonly kind: 'preOffsetTypeNode'; + + // Data. + /** The signed byte offset to apply before the wrapped type runs. */ + readonly offset: number; + /** How the `offset` value is interpreted. */ + readonly strategy: PreOffsetStrategy; + + // Children. + /** The wrapped type whose serialisation is preceded by the offset. */ + readonly type: TType; +} diff --git a/packages/node-types/src/typeNodes/PublicKeyTypeNode.ts b/packages/node-types/src/generated/typeNodes/PublicKeyTypeNode.ts similarity index 68% rename from packages/node-types/src/typeNodes/PublicKeyTypeNode.ts rename to packages/node-types/src/generated/typeNodes/PublicKeyTypeNode.ts index 64d79e007..cd9745647 100644 --- a/packages/node-types/src/typeNodes/PublicKeyTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/PublicKeyTypeNode.ts @@ -1,3 +1,4 @@ +/** A 32-byte Solana public key. */ export interface PublicKeyTypeNode { readonly kind: 'publicKeyTypeNode'; } diff --git a/packages/node-types/src/generated/typeNodes/RegisteredTypeNode.ts b/packages/node-types/src/generated/typeNodes/RegisteredTypeNode.ts new file mode 100644 index 000000000..476f6715f --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/RegisteredTypeNode.ts @@ -0,0 +1,6 @@ +import type { EnumVariantTypeNode } from './EnumVariantTypeNode'; +import type { StandaloneTypeNode } from './StandaloneTypeNode'; +import type { StructFieldTypeNode } from './StructFieldTypeNode'; + +/** Every node tagged as a type-shaped node, including variants and struct fields. */ +export type RegisteredTypeNode = EnumVariantTypeNode | StandaloneTypeNode | StructFieldTypeNode; diff --git a/packages/node-types/src/typeNodes/RemainderOptionTypeNode.ts b/packages/node-types/src/generated/typeNodes/RemainderOptionTypeNode.ts similarity index 53% rename from packages/node-types/src/typeNodes/RemainderOptionTypeNode.ts rename to packages/node-types/src/generated/typeNodes/RemainderOptionTypeNode.ts index 591c6b037..6897fddc0 100644 --- a/packages/node-types/src/typeNodes/RemainderOptionTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/RemainderOptionTypeNode.ts @@ -1,8 +1,10 @@ import type { TypeNode } from './TypeNode'; +/** A value that may be present or absent. Presence is signalled by whether any bytes remain to be read, with no explicit prefix. */ export interface RemainderOptionTypeNode { readonly kind: 'remainderOptionTypeNode'; // Children. + /** The type carried by the option when present. */ readonly item: TItem; } diff --git a/packages/node-types/src/generated/typeNodes/SentinelTypeNode.ts b/packages/node-types/src/generated/typeNodes/SentinelTypeNode.ts new file mode 100644 index 000000000..f71e98f8f --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/SentinelTypeNode.ts @@ -0,0 +1,16 @@ +import type { ConstantValueNode } from '../valueNodes/ConstantValueNode'; +import type { TypeNode } from './TypeNode'; + +/** Wraps another type and delimits it with a constant sentinel value written immediately after the wrapped type. */ +export interface SentinelTypeNode< + TType extends TypeNode = TypeNode, + TSentinel extends ConstantValueNode = ConstantValueNode, +> { + readonly kind: 'sentinelTypeNode'; + + // Children. + /** The wrapped type whose extent is delimited by the sentinel. */ + readonly type: TType; + /** The constant value written immediately after the wrapped type to mark its end. */ + readonly sentinel: TSentinel; +} diff --git a/packages/node-types/src/generated/typeNodes/SetTypeNode.ts b/packages/node-types/src/generated/typeNodes/SetTypeNode.ts new file mode 100644 index 000000000..1cb05926a --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/SetTypeNode.ts @@ -0,0 +1,13 @@ +import type { CountNode } from '../countNodes/CountNode'; +import type { TypeNode } from './TypeNode'; + +/** A unique-valued collection. The item type is defined by `item`; the size is determined by the `count` strategy. */ +export interface SetTypeNode { + readonly kind: 'setTypeNode'; + + // Children. + /** The type of each item in the set. */ + readonly item: TItem; + /** The strategy used to determine the number of items. */ + readonly count: TCount; +} diff --git a/packages/node-types/src/typeNodes/SizePrefixTypeNode.ts b/packages/node-types/src/generated/typeNodes/SizePrefixTypeNode.ts similarity index 66% rename from packages/node-types/src/typeNodes/SizePrefixTypeNode.ts rename to packages/node-types/src/generated/typeNodes/SizePrefixTypeNode.ts index b7c548e59..20ba90818 100644 --- a/packages/node-types/src/typeNodes/SizePrefixTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/SizePrefixTypeNode.ts @@ -2,6 +2,7 @@ import type { NestedTypeNode } from './NestedTypeNode'; import type { NumberTypeNode } from './NumberTypeNode'; import type { TypeNode } from './TypeNode'; +/** Wraps another type with a numeric prefix indicating the byte length of the wrapped type. */ export interface SizePrefixTypeNode< TType extends TypeNode = TypeNode, TPrefix extends NestedTypeNode = NestedTypeNode, @@ -9,6 +10,8 @@ export interface SizePrefixTypeNode< readonly kind: 'sizePrefixTypeNode'; // Children. + /** The wrapped type whose serialisation is preceded by its size. */ readonly type: TType; + /** The numeric type used as the size prefix. */ readonly prefix: TPrefix; } diff --git a/packages/node-types/src/typeNodes/SolAmountTypeNode.ts b/packages/node-types/src/generated/typeNodes/SolAmountTypeNode.ts similarity index 69% rename from packages/node-types/src/typeNodes/SolAmountTypeNode.ts rename to packages/node-types/src/generated/typeNodes/SolAmountTypeNode.ts index 25ea98b3d..d4bdd92d9 100644 --- a/packages/node-types/src/typeNodes/SolAmountTypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/SolAmountTypeNode.ts @@ -1,9 +1,11 @@ import type { NestedTypeNode } from './NestedTypeNode'; import type { NumberTypeNode } from './NumberTypeNode'; +/** A SOL amount expressed in lamports under the wrapped numeric type. */ export interface SolAmountTypeNode = NestedTypeNode> { readonly kind: 'solAmountTypeNode'; // Children. + /** The numeric type used to serialise the lamport amount. */ readonly number: TNumber; } diff --git a/packages/node-types/src/typeNodes/TypeNode.ts b/packages/node-types/src/generated/typeNodes/StandaloneTypeNode.ts similarity index 68% rename from packages/node-types/src/typeNodes/TypeNode.ts rename to packages/node-types/src/generated/typeNodes/StandaloneTypeNode.ts index c73fce01d..a9e35f3f3 100644 --- a/packages/node-types/src/typeNodes/TypeNode.ts +++ b/packages/node-types/src/generated/typeNodes/StandaloneTypeNode.ts @@ -1,12 +1,8 @@ -import type { DefinedTypeLinkNode } from '../linkNodes/DefinedTypeLinkNode'; import type { AmountTypeNode } from './AmountTypeNode'; import type { ArrayTypeNode } from './ArrayTypeNode'; import type { BooleanTypeNode } from './BooleanTypeNode'; import type { BytesTypeNode } from './BytesTypeNode'; import type { DateTimeTypeNode } from './DateTimeTypeNode'; -import type { EnumEmptyVariantTypeNode } from './EnumEmptyVariantTypeNode'; -import type { EnumStructVariantTypeNode } from './EnumStructVariantTypeNode'; -import type { EnumTupleVariantTypeNode } from './EnumTupleVariantTypeNode'; import type { EnumTypeNode } from './EnumTypeNode'; import type { FixedSizeTypeNode } from './FixedSizeTypeNode'; import type { HiddenPrefixTypeNode } from './HiddenPrefixTypeNode'; @@ -23,12 +19,11 @@ import type { SetTypeNode } from './SetTypeNode'; import type { SizePrefixTypeNode } from './SizePrefixTypeNode'; import type { SolAmountTypeNode } from './SolAmountTypeNode'; import type { StringTypeNode } from './StringTypeNode'; -import type { StructFieldTypeNode } from './StructFieldTypeNode'; import type { StructTypeNode } from './StructTypeNode'; import type { TupleTypeNode } from './TupleTypeNode'; import type { ZeroableOptionTypeNode } from './ZeroableOptionTypeNode'; -// Standalone Type Node Registration. +/** Every type node that can be used as a top-level type. */ export type StandaloneTypeNode = | AmountTypeNode | ArrayTypeNode @@ -54,19 +49,3 @@ export type StandaloneTypeNode = | StructTypeNode | TupleTypeNode | ZeroableOptionTypeNode; - -// Type Node Registration. -export type RegisteredTypeNode = - | EnumEmptyVariantTypeNode - | EnumStructVariantTypeNode - | EnumTupleVariantTypeNode - | StandaloneTypeNode - | StructFieldTypeNode; - -/** - * Type Node Helper. - * This only includes type nodes that can be used as standalone types. - * E.g. this excludes structFieldTypeNode, enumEmptyVariantTypeNode, etc. - * It also includes the definedTypeLinkNode to compose types. - */ -export type TypeNode = DefinedTypeLinkNode | StandaloneTypeNode; diff --git a/packages/node-types/src/generated/typeNodes/StringTypeNode.ts b/packages/node-types/src/generated/typeNodes/StringTypeNode.ts new file mode 100644 index 000000000..fe28ef5c6 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/StringTypeNode.ts @@ -0,0 +1,14 @@ +import type { BytesEncoding } from '../shared/bytesEncoding'; + +/** + * A string value. + * The encoding describes how its bytes are written. + * The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`. + */ +export interface StringTypeNode { + readonly kind: 'stringTypeNode'; + + // Data. + /** The byte encoding used to serialise the string. */ + readonly encoding: TEncoding; +} diff --git a/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts b/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts new file mode 100644 index 000000000..ee5e5e4a3 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/StructFieldTypeNode.ts @@ -0,0 +1,27 @@ +import type { CamelCaseString } from '../../brands'; +import type { Docs } from '../../Docs'; +import type { DefaultValueStrategy } from '../shared/defaultValueStrategy'; +import type { ValueNode } from '../valueNodes/ValueNode'; +import type { TypeNode } from './TypeNode'; + +/** A named field within a struct type. */ +export interface StructFieldTypeNode< + TType extends TypeNode = TypeNode, + TDefaultValue extends ValueNode | undefined = ValueNode | undefined, +> { + readonly kind: 'structFieldTypeNode'; + + // Data. + /** The name of the field. */ + readonly name: CamelCaseString; + /** How a configured default value is exposed in generated APIs. Required when `defaultValue` is set. */ + readonly defaultValueStrategy?: DefaultValueStrategy; + /** Markdown documentation for the field. */ + readonly docs?: Docs; + + // Children. + /** The type of the field. */ + readonly type: TType; + /** A default value used when the field is omitted by callers. */ + readonly defaultValue?: TDefaultValue; +} diff --git a/packages/node-types/src/generated/typeNodes/StructTypeNode.ts b/packages/node-types/src/generated/typeNodes/StructTypeNode.ts new file mode 100644 index 000000000..5aa0f8726 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/StructTypeNode.ts @@ -0,0 +1,10 @@ +import type { StructFieldTypeNode } from './StructFieldTypeNode'; + +/** A composite type made of an ordered list of named fields. Fields are encoded and decoded in declaration order. */ +export interface StructTypeNode = Array> { + readonly kind: 'structTypeNode'; + + // Children. + /** The fields of the struct, in declaration order. */ + readonly fields: TFields; +} diff --git a/packages/node-types/src/generated/typeNodes/TupleTypeNode.ts b/packages/node-types/src/generated/typeNodes/TupleTypeNode.ts new file mode 100644 index 000000000..ae333bb78 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/TupleTypeNode.ts @@ -0,0 +1,10 @@ +import type { TypeNode } from './TypeNode'; + +/** A heterogeneous fixed-length sequence in which each positional slot has its own type. */ +export interface TupleTypeNode = Array> { + readonly kind: 'tupleTypeNode'; + + // Children. + /** The type of each positional slot, in order. */ + readonly items: TItems; +} diff --git a/packages/node-types/src/generated/typeNodes/TypeNode.ts b/packages/node-types/src/generated/typeNodes/TypeNode.ts new file mode 100644 index 000000000..ebb74fb29 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/TypeNode.ts @@ -0,0 +1,5 @@ +import type { DefinedTypeLinkNode } from '../linkNodes/DefinedTypeLinkNode'; +import type { StandaloneTypeNode } from './StandaloneTypeNode'; + +/** The composable form: any standalone type, or a reference to a defined type via `definedTypeLinkNode`. */ +export type TypeNode = DefinedTypeLinkNode | StandaloneTypeNode; diff --git a/packages/node-types/src/generated/typeNodes/ZeroableOptionTypeNode.ts b/packages/node-types/src/generated/typeNodes/ZeroableOptionTypeNode.ts new file mode 100644 index 000000000..b627bd499 --- /dev/null +++ b/packages/node-types/src/generated/typeNodes/ZeroableOptionTypeNode.ts @@ -0,0 +1,16 @@ +import type { ConstantValueNode } from '../valueNodes/ConstantValueNode'; +import type { TypeNode } from './TypeNode'; + +/** An optional value whose absence is signalled by a designated zero value rather than a presence flag. */ +export interface ZeroableOptionTypeNode< + TItem extends TypeNode = TypeNode, + TZeroValue extends ConstantValueNode | undefined = ConstantValueNode | undefined, +> { + readonly kind: 'zeroableOptionTypeNode'; + + // Children. + /** The type carried by the option when present. */ + readonly item: TItem; + /** The constant value that signals absence. When omitted, the all-zero byte pattern of the item type is used. */ + readonly zeroValue?: TZeroValue; +} diff --git a/packages/node-types/src/typeNodes/index.ts b/packages/node-types/src/generated/typeNodes/index.ts similarity index 93% rename from packages/node-types/src/typeNodes/index.ts rename to packages/node-types/src/generated/typeNodes/index.ts index 9f1b475d9..e9f252678 100644 --- a/packages/node-types/src/typeNodes/index.ts +++ b/packages/node-types/src/generated/typeNodes/index.ts @@ -18,11 +18,13 @@ export * from './OptionTypeNode'; export * from './PostOffsetTypeNode'; export * from './PreOffsetTypeNode'; export * from './PublicKeyTypeNode'; +export * from './RegisteredTypeNode'; export * from './RemainderOptionTypeNode'; export * from './SentinelTypeNode'; export * from './SetTypeNode'; export * from './SizePrefixTypeNode'; export * from './SolAmountTypeNode'; +export * from './StandaloneTypeNode'; export * from './StringTypeNode'; export * from './StructFieldTypeNode'; export * from './StructTypeNode'; diff --git a/packages/node-types/src/generated/valueNodes/ArrayValueNode.ts b/packages/node-types/src/generated/valueNodes/ArrayValueNode.ts new file mode 100644 index 000000000..35054a458 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/ArrayValueNode.ts @@ -0,0 +1,10 @@ +import type { ValueNode } from './ValueNode'; + +/** A concrete array value: a list of value nodes. */ +export interface ArrayValueNode = Array> { + readonly kind: 'arrayValueNode'; + + // Children. + /** The items of the array, in order. */ + readonly items: TItems; +} diff --git a/packages/node-types/src/valueNodes/BooleanValueNode.ts b/packages/node-types/src/generated/valueNodes/BooleanValueNode.ts similarity index 65% rename from packages/node-types/src/valueNodes/BooleanValueNode.ts rename to packages/node-types/src/generated/valueNodes/BooleanValueNode.ts index dcc1e806b..2482ca870 100644 --- a/packages/node-types/src/valueNodes/BooleanValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/BooleanValueNode.ts @@ -1,6 +1,8 @@ +/** A concrete boolean value. */ export interface BooleanValueNode { readonly kind: 'booleanValueNode'; // Data. + /** The boolean value. */ readonly boolean: boolean; } diff --git a/packages/node-types/src/generated/valueNodes/BytesValueNode.ts b/packages/node-types/src/generated/valueNodes/BytesValueNode.ts new file mode 100644 index 000000000..7dfa9f7eb --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/BytesValueNode.ts @@ -0,0 +1,12 @@ +import type { BytesEncoding } from '../shared/bytesEncoding'; + +/** A concrete bytes value, encoded as text in the chosen encoding. */ +export interface BytesValueNode { + readonly kind: 'bytesValueNode'; + + // Data. + /** The bytes encoded as a text string per the `encoding` attribute. */ + readonly data: string; + /** The encoding used to represent the bytes as text. */ + readonly encoding: BytesEncoding; +} diff --git a/packages/node-types/src/valueNodes/ConstantValueNode.ts b/packages/node-types/src/generated/valueNodes/ConstantValueNode.ts similarity index 67% rename from packages/node-types/src/valueNodes/ConstantValueNode.ts rename to packages/node-types/src/generated/valueNodes/ConstantValueNode.ts index ae403c327..bec430126 100644 --- a/packages/node-types/src/valueNodes/ConstantValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/ConstantValueNode.ts @@ -1,10 +1,13 @@ import type { TypeNode } from '../typeNodes/TypeNode'; import type { ValueNode } from './ValueNode'; +/** A typed constant: a type node paired with a concrete value node. */ export interface ConstantValueNode { readonly kind: 'constantValueNode'; // Children. + /** The type of the constant. */ readonly type: TType; + /** The concrete value of the constant. */ readonly value: TValue; } diff --git a/packages/node-types/src/generated/valueNodes/EnumValueNode.ts b/packages/node-types/src/generated/valueNodes/EnumValueNode.ts new file mode 100644 index 000000000..3c08b6f32 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/EnumValueNode.ts @@ -0,0 +1,24 @@ +import type { CamelCaseString } from '../../brands'; +import type { DefinedTypeLinkNode } from '../linkNodes/DefinedTypeLinkNode'; +import type { EnumValuePayload } from './EnumValuePayload'; + +/** A concrete value of a defined enum: a variant identifier plus an optional payload. */ +export interface EnumValueNode< + TEnum extends DefinedTypeLinkNode = DefinedTypeLinkNode, + TValue extends EnumValuePayload | undefined = EnumValuePayload | undefined, +> { + readonly kind: 'enumValueNode'; + + // Data. + /** The name of the selected variant. */ + readonly variant: CamelCaseString; + + // Children. + /** A link to the defined enum type the value belongs to. */ + readonly enum: TEnum; + /** + * The variant payload — a struct value for struct variants or a tuple value for tuple variants. + * Omitted for unit variants. + */ + readonly value?: TValue; +} diff --git a/packages/node-types/src/generated/valueNodes/EnumValuePayload.ts b/packages/node-types/src/generated/valueNodes/EnumValuePayload.ts new file mode 100644 index 000000000..2ffae0438 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/EnumValuePayload.ts @@ -0,0 +1,5 @@ +import type { StructValueNode } from './StructValueNode'; +import type { TupleValueNode } from './TupleValueNode'; + +/** The payload kinds an `enumValueNode` may carry — struct fields or positional tuple slots. */ +export type EnumValuePayload = StructValueNode | TupleValueNode; diff --git a/packages/node-types/src/valueNodes/MapEntryValueNode.ts b/packages/node-types/src/generated/valueNodes/MapEntryValueNode.ts similarity index 70% rename from packages/node-types/src/valueNodes/MapEntryValueNode.ts rename to packages/node-types/src/generated/valueNodes/MapEntryValueNode.ts index 7d7a111c7..bb82da156 100644 --- a/packages/node-types/src/valueNodes/MapEntryValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/MapEntryValueNode.ts @@ -1,9 +1,12 @@ import type { ValueNode } from './ValueNode'; +/** A single (key, value) pair inside a `mapValueNode`. */ export interface MapEntryValueNode { readonly kind: 'mapEntryValueNode'; // Children. + /** The entry key. */ readonly key: TKey; + /** The entry value. */ readonly value: TValue; } diff --git a/packages/node-types/src/generated/valueNodes/MapValueNode.ts b/packages/node-types/src/generated/valueNodes/MapValueNode.ts new file mode 100644 index 000000000..c73f9b6b8 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/MapValueNode.ts @@ -0,0 +1,10 @@ +import type { MapEntryValueNode } from './MapEntryValueNode'; + +/** A concrete map value: a list of (key, value) entries. */ +export interface MapValueNode = Array> { + readonly kind: 'mapValueNode'; + + // Children. + /** The entries of the map, in order. */ + readonly entries: TEntries; +} diff --git a/packages/node-types/src/valueNodes/NoneValueNode.ts b/packages/node-types/src/generated/valueNodes/NoneValueNode.ts similarity index 59% rename from packages/node-types/src/valueNodes/NoneValueNode.ts rename to packages/node-types/src/generated/valueNodes/NoneValueNode.ts index eb381ff25..02cf8ec66 100644 --- a/packages/node-types/src/valueNodes/NoneValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/NoneValueNode.ts @@ -1,3 +1,4 @@ +/** The "absent" value for an optional type. */ export interface NoneValueNode { readonly kind: 'noneValueNode'; } diff --git a/packages/node-types/src/generated/valueNodes/NumberValueNode.ts b/packages/node-types/src/generated/valueNodes/NumberValueNode.ts new file mode 100644 index 000000000..72986b446 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/NumberValueNode.ts @@ -0,0 +1,11 @@ +/** + * A concrete numeric value. + * Stored as a 64-bit float; consumers narrow to a specific integer or float width based on the surrounding type context. + */ +export interface NumberValueNode { + readonly kind: 'numberValueNode'; + + // Data. + /** The numeric value. */ + readonly number: number; +} diff --git a/packages/node-types/src/generated/valueNodes/PublicKeyValueNode.ts b/packages/node-types/src/generated/valueNodes/PublicKeyValueNode.ts new file mode 100644 index 000000000..162fadb70 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/PublicKeyValueNode.ts @@ -0,0 +1,12 @@ +import type { CamelCaseString } from '../../brands'; + +/** A concrete public key, with an optional symbolic identifier for the address. */ +export interface PublicKeyValueNode { + readonly kind: 'publicKeyValueNode'; + + // Data. + /** The base58-encoded public key. */ + readonly publicKey: string; + /** A symbolic name for the address, useful in generated client code. */ + readonly identifier?: CamelCaseString; +} diff --git a/packages/node-types/src/generated/valueNodes/RegisteredValueNode.ts b/packages/node-types/src/generated/valueNodes/RegisteredValueNode.ts new file mode 100644 index 000000000..3ea080f9d --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/RegisteredValueNode.ts @@ -0,0 +1,6 @@ +import type { MapEntryValueNode } from './MapEntryValueNode'; +import type { StandaloneValueNode } from './StandaloneValueNode'; +import type { StructFieldValueNode } from './StructFieldValueNode'; + +/** Every node tagged as a value-shaped node, including container variants. */ +export type RegisteredValueNode = MapEntryValueNode | StandaloneValueNode | StructFieldValueNode; diff --git a/packages/node-types/src/generated/valueNodes/SetValueNode.ts b/packages/node-types/src/generated/valueNodes/SetValueNode.ts new file mode 100644 index 000000000..4dc457099 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/SetValueNode.ts @@ -0,0 +1,10 @@ +import type { ValueNode } from './ValueNode'; + +/** A concrete set value: a list of unique value nodes. */ +export interface SetValueNode = Array> { + readonly kind: 'setValueNode'; + + // Children. + /** The items of the set. */ + readonly items: TItems; +} diff --git a/packages/node-types/src/valueNodes/SomeValueNode.ts b/packages/node-types/src/generated/valueNodes/SomeValueNode.ts similarity index 64% rename from packages/node-types/src/valueNodes/SomeValueNode.ts rename to packages/node-types/src/generated/valueNodes/SomeValueNode.ts index 9577edce9..aa6dd2b48 100644 --- a/packages/node-types/src/valueNodes/SomeValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/SomeValueNode.ts @@ -1,8 +1,10 @@ import type { ValueNode } from './ValueNode'; +/** The "present" value for an optional type, wrapping a concrete value node. */ export interface SomeValueNode { readonly kind: 'someValueNode'; // Children. + /** The wrapped value. */ readonly value: TValue; } diff --git a/packages/node-types/src/valueNodes/ValueNode.ts b/packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts similarity index 75% rename from packages/node-types/src/valueNodes/ValueNode.ts rename to packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts index 73d642e16..bf3a4f660 100644 --- a/packages/node-types/src/valueNodes/ValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/StandaloneValueNode.ts @@ -3,7 +3,6 @@ import type { BooleanValueNode } from './BooleanValueNode'; import type { BytesValueNode } from './BytesValueNode'; import type { ConstantValueNode } from './ConstantValueNode'; import type { EnumValueNode } from './EnumValueNode'; -import type { MapEntryValueNode } from './MapEntryValueNode'; import type { MapValueNode } from './MapValueNode'; import type { NoneValueNode } from './NoneValueNode'; import type { NumberValueNode } from './NumberValueNode'; @@ -11,11 +10,10 @@ import type { PublicKeyValueNode } from './PublicKeyValueNode'; import type { SetValueNode } from './SetValueNode'; import type { SomeValueNode } from './SomeValueNode'; import type { StringValueNode } from './StringValueNode'; -import type { StructFieldValueNode } from './StructFieldValueNode'; import type { StructValueNode } from './StructValueNode'; import type { TupleValueNode } from './TupleValueNode'; -// Standalone Value Node Registration. +/** Every value node that can be used as a top-level value. */ export type StandaloneValueNode = | ArrayValueNode | BooleanValueNode @@ -31,9 +29,3 @@ export type StandaloneValueNode = | StringValueNode | StructValueNode | TupleValueNode; - -// Value Node Registration. -export type RegisteredValueNode = MapEntryValueNode | StandaloneValueNode | StructFieldValueNode; - -// Value Node Helper. -export type ValueNode = StandaloneValueNode; diff --git a/packages/node-types/src/valueNodes/StringValueNode.ts b/packages/node-types/src/generated/valueNodes/StringValueNode.ts similarity index 65% rename from packages/node-types/src/valueNodes/StringValueNode.ts rename to packages/node-types/src/generated/valueNodes/StringValueNode.ts index da0d98162..e5ab1b014 100644 --- a/packages/node-types/src/valueNodes/StringValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/StringValueNode.ts @@ -1,6 +1,8 @@ +/** A concrete string value. */ export interface StringValueNode { readonly kind: 'stringValueNode'; // Data. + /** The string value. */ readonly string: string; } diff --git a/packages/node-types/src/valueNodes/StructFieldValueNode.ts b/packages/node-types/src/generated/valueNodes/StructFieldValueNode.ts similarity index 60% rename from packages/node-types/src/valueNodes/StructFieldValueNode.ts rename to packages/node-types/src/generated/valueNodes/StructFieldValueNode.ts index c83d0e12e..79a63461f 100644 --- a/packages/node-types/src/valueNodes/StructFieldValueNode.ts +++ b/packages/node-types/src/generated/valueNodes/StructFieldValueNode.ts @@ -1,12 +1,15 @@ -import type { CamelCaseString } from '../shared'; +import type { CamelCaseString } from '../../brands'; import type { ValueNode } from './ValueNode'; +/** A named field of a `structValueNode`. */ export interface StructFieldValueNode { readonly kind: 'structFieldValueNode'; // Data. + /** The name of the field. */ readonly name: CamelCaseString; // Children. + /** The concrete value of the field. */ readonly value: TValue; } diff --git a/packages/node-types/src/generated/valueNodes/StructValueNode.ts b/packages/node-types/src/generated/valueNodes/StructValueNode.ts new file mode 100644 index 000000000..085d857ad --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/StructValueNode.ts @@ -0,0 +1,10 @@ +import type { StructFieldValueNode } from './StructFieldValueNode'; + +/** A concrete struct value: a list of named field values. */ +export interface StructValueNode = Array> { + readonly kind: 'structValueNode'; + + // Children. + /** The named fields of the struct value. */ + readonly fields: TFields; +} diff --git a/packages/node-types/src/generated/valueNodes/TupleValueNode.ts b/packages/node-types/src/generated/valueNodes/TupleValueNode.ts new file mode 100644 index 000000000..4cf311f65 --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/TupleValueNode.ts @@ -0,0 +1,10 @@ +import type { ValueNode } from './ValueNode'; + +/** A concrete tuple value: a fixed-length sequence of positional value nodes. */ +export interface TupleValueNode = Array> { + readonly kind: 'tupleValueNode'; + + // Children. + /** The positional items of the tuple, in order. */ + readonly items: TItems; +} diff --git a/packages/node-types/src/generated/valueNodes/ValueNode.ts b/packages/node-types/src/generated/valueNodes/ValueNode.ts new file mode 100644 index 000000000..97a1d552c --- /dev/null +++ b/packages/node-types/src/generated/valueNodes/ValueNode.ts @@ -0,0 +1,4 @@ +import type { StandaloneValueNode } from './StandaloneValueNode'; + +/** The composable form: any standalone value node. */ +export type ValueNode = StandaloneValueNode; diff --git a/packages/node-types/src/valueNodes/index.ts b/packages/node-types/src/generated/valueNodes/index.ts similarity index 83% rename from packages/node-types/src/valueNodes/index.ts rename to packages/node-types/src/generated/valueNodes/index.ts index 56f55fe21..f9c35d7fb 100644 --- a/packages/node-types/src/valueNodes/index.ts +++ b/packages/node-types/src/generated/valueNodes/index.ts @@ -3,13 +3,16 @@ export * from './BooleanValueNode'; export * from './BytesValueNode'; export * from './ConstantValueNode'; export * from './EnumValueNode'; +export * from './EnumValuePayload'; export * from './MapEntryValueNode'; export * from './MapValueNode'; export * from './NoneValueNode'; export * from './NumberValueNode'; export * from './PublicKeyValueNode'; +export * from './RegisteredValueNode'; export * from './SetValueNode'; export * from './SomeValueNode'; +export * from './StandaloneValueNode'; export * from './StringValueNode'; export * from './StructFieldValueNode'; export * from './StructValueNode'; diff --git a/packages/node-types/src/index.ts b/packages/node-types/src/index.ts index 18775c7c0..5b58d1155 100644 --- a/packages/node-types/src/index.ts +++ b/packages/node-types/src/index.ts @@ -1,24 +1,22 @@ -export * from './AccountNode'; -export * from './ConstantNode'; -export * from './DefinedTypeNode'; -export * from './ErrorNode'; -export * from './EventNode'; -export * from './InstructionAccountNode'; -export * from './InstructionArgumentNode'; -export * from './InstructionByteDeltaNode'; -export * from './InstructionNode'; -export * from './InstructionRemainingAccountsNode'; -export * from './InstructionStatusNode'; -export * from './Node'; -export * from './PdaNode'; -export * from './ProgramNode'; -export * from './RootNode'; +/** + * `@codama/node-types` — public API. + * + * Most type interfaces in this package are generated from the + * `@codama/spec` description by `@codama-internal/spec-generators`. To + * regenerate, run `pnpm generate` from the repository root. Hand-edits + * to files under `./generated/` will not survive the next regeneration; + * if you need to extend the surface, add the spec change upstream and + * regenerate, or add a hand-written sibling file at this top level (as + * already done for the static helpers below). + */ -export * from './contextualValueNodes'; -export * from './countNodes'; -export * from './discriminatorNodes'; -export * from './linkNodes'; -export * from './pdaSeedNodes'; -export * from './shared'; -export * from './typeNodes'; -export * from './valueNodes'; +export * from './generated'; + +// Hand-written static helpers — referenced by the generated surface but +// kept outside `./generated/` because they don't depend on the spec. +export * from './brands'; +export * from './Docs'; +export * from './Version'; + +// Hand-written deprecated aliases — see each file for rationale. +export * from './ProgramVersion'; diff --git a/packages/node-types/src/linkNodes/ProgramLinkNode.ts b/packages/node-types/src/linkNodes/ProgramLinkNode.ts deleted file mode 100644 index e93e0e331..000000000 --- a/packages/node-types/src/linkNodes/ProgramLinkNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface ProgramLinkNode { - readonly kind: 'programLinkNode'; - - // Data. - readonly name: CamelCaseString; -} diff --git a/packages/node-types/src/pdaSeedNodes/ConstantPdaSeedNode.ts b/packages/node-types/src/pdaSeedNodes/ConstantPdaSeedNode.ts deleted file mode 100644 index bf7e17c0d..000000000 --- a/packages/node-types/src/pdaSeedNodes/ConstantPdaSeedNode.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ProgramIdValueNode } from '../contextualValueNodes'; -import type { TypeNode } from '../typeNodes'; -import type { ValueNode } from '../valueNodes'; - -export interface ConstantPdaSeedNode< - TType extends TypeNode = TypeNode, - TValue extends ProgramIdValueNode | ValueNode = ProgramIdValueNode | ValueNode, -> { - readonly kind: 'constantPdaSeedNode'; - - // Children. - readonly type: TType; - readonly value: TValue; -} diff --git a/packages/node-types/src/pdaSeedNodes/VariablePdaSeedNode.ts b/packages/node-types/src/pdaSeedNodes/VariablePdaSeedNode.ts deleted file mode 100644 index 14efe910b..000000000 --- a/packages/node-types/src/pdaSeedNodes/VariablePdaSeedNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { CamelCaseString, Docs } from '../shared'; -import type { TypeNode } from '../typeNodes'; - -export interface VariablePdaSeedNode { - readonly kind: 'variablePdaSeedNode'; - - // Data. - readonly name: CamelCaseString; - readonly docs?: Docs; - - // Children. - readonly type: TType; -} diff --git a/packages/node-types/src/shared/bytesEncoding.ts b/packages/node-types/src/shared/bytesEncoding.ts deleted file mode 100644 index 87bf72f60..000000000 --- a/packages/node-types/src/shared/bytesEncoding.ts +++ /dev/null @@ -1 +0,0 @@ -export type BytesEncoding = 'base16' | 'base58' | 'base64' | 'utf8'; diff --git a/packages/node-types/src/shared/docs.ts b/packages/node-types/src/shared/docs.ts deleted file mode 100644 index 9d210d9bb..000000000 --- a/packages/node-types/src/shared/docs.ts +++ /dev/null @@ -1 +0,0 @@ -export type Docs = string[]; diff --git a/packages/node-types/src/shared/index.ts b/packages/node-types/src/shared/index.ts deleted file mode 100644 index b2213bb87..000000000 --- a/packages/node-types/src/shared/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './bytesEncoding'; -export * from './docs'; -export * from './instructionLifecycle'; -export * from './stringCases'; -export * from './version'; diff --git a/packages/node-types/src/shared/instructionLifecycle.ts b/packages/node-types/src/shared/instructionLifecycle.ts deleted file mode 100644 index 6edfd75df..000000000 --- a/packages/node-types/src/shared/instructionLifecycle.ts +++ /dev/null @@ -1 +0,0 @@ -export type InstructionLifecycle = 'archived' | 'deprecated' | 'draft' | 'live'; diff --git a/packages/node-types/src/shared/stringCases.ts b/packages/node-types/src/shared/stringCases.ts deleted file mode 100644 index 1eed32641..000000000 --- a/packages/node-types/src/shared/stringCases.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type TitleCaseString = string & { - readonly ['__stringCase:codama']: 'titleCase'; -}; - -export type PascalCaseString = string & { - readonly ['__stringCase:codama']: 'pascalCase'; -}; - -export type CamelCaseString = string & { - readonly ['__stringCase:codama']: 'camelCase'; -}; - -export type KebabCaseString = string & { - readonly ['__stringCase:codama']: 'kebabCase'; -}; - -export type SnakeCaseString = string & { - readonly ['__stringCase:codama']: 'snakeCase'; -}; diff --git a/packages/node-types/src/shared/version.ts b/packages/node-types/src/shared/version.ts deleted file mode 100644 index 4bc692333..000000000 --- a/packages/node-types/src/shared/version.ts +++ /dev/null @@ -1,4 +0,0 @@ -type SemanticVersion = `${number}.${number}.${number}`; - -export type CodamaVersion = SemanticVersion; -export type ProgramVersion = SemanticVersion; diff --git a/packages/node-types/src/typeNodes/AmountTypeNode.ts b/packages/node-types/src/typeNodes/AmountTypeNode.ts deleted file mode 100644 index dbcac8278..000000000 --- a/packages/node-types/src/typeNodes/AmountTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { NestedTypeNode } from './NestedTypeNode'; -import type { NumberTypeNode } from './NumberTypeNode'; - -export interface AmountTypeNode = NestedTypeNode> { - readonly kind: 'amountTypeNode'; - - // Data. - readonly decimals: number; - readonly unit?: string; - - // Children. - readonly number: TNumber; -} diff --git a/packages/node-types/src/typeNodes/ArrayTypeNode.ts b/packages/node-types/src/typeNodes/ArrayTypeNode.ts deleted file mode 100644 index 1ca071caf..000000000 --- a/packages/node-types/src/typeNodes/ArrayTypeNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CountNode } from '../countNodes'; -import type { TypeNode } from './TypeNode'; - -export interface ArrayTypeNode { - readonly kind: 'arrayTypeNode'; - - // Children. - readonly item: TItem; - readonly count: TCount; -} diff --git a/packages/node-types/src/typeNodes/BytesTypeNode.ts b/packages/node-types/src/typeNodes/BytesTypeNode.ts deleted file mode 100644 index d76f0a520..000000000 --- a/packages/node-types/src/typeNodes/BytesTypeNode.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface BytesTypeNode { - readonly kind: 'bytesTypeNode'; -} diff --git a/packages/node-types/src/typeNodes/EnumEmptyVariantTypeNode.ts b/packages/node-types/src/typeNodes/EnumEmptyVariantTypeNode.ts deleted file mode 100644 index 654f37371..000000000 --- a/packages/node-types/src/typeNodes/EnumEmptyVariantTypeNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface EnumEmptyVariantTypeNode { - readonly kind: 'enumEmptyVariantTypeNode'; - - // Data. - readonly name: CamelCaseString; - readonly discriminator?: number; -} diff --git a/packages/node-types/src/typeNodes/HiddenPrefixTypeNode.ts b/packages/node-types/src/typeNodes/HiddenPrefixTypeNode.ts deleted file mode 100644 index 35ee73c24..000000000 --- a/packages/node-types/src/typeNodes/HiddenPrefixTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ConstantValueNode } from '../valueNodes'; -import type { TypeNode } from './TypeNode'; - -export interface HiddenPrefixTypeNode< - TType extends TypeNode = TypeNode, - TPrefix extends ConstantValueNode[] = ConstantValueNode[], -> { - readonly kind: 'hiddenPrefixTypeNode'; - - // Children. - readonly type: TType; - readonly prefix: TPrefix; -} diff --git a/packages/node-types/src/typeNodes/HiddenSuffixTypeNode.ts b/packages/node-types/src/typeNodes/HiddenSuffixTypeNode.ts deleted file mode 100644 index 3f45e1a12..000000000 --- a/packages/node-types/src/typeNodes/HiddenSuffixTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ConstantValueNode } from '../valueNodes'; -import type { TypeNode } from './TypeNode'; - -export interface HiddenSuffixTypeNode< - TType extends TypeNode = TypeNode, - TSuffix extends ConstantValueNode[] = ConstantValueNode[], -> { - readonly kind: 'hiddenSuffixTypeNode'; - - // Children. - readonly type: TType; - readonly suffix: TSuffix; -} diff --git a/packages/node-types/src/typeNodes/MapTypeNode.ts b/packages/node-types/src/typeNodes/MapTypeNode.ts deleted file mode 100644 index bd16ed788..000000000 --- a/packages/node-types/src/typeNodes/MapTypeNode.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CountNode } from '../countNodes'; -import type { TypeNode } from './TypeNode'; - -export interface MapTypeNode< - TKey extends TypeNode = TypeNode, - TValue extends TypeNode = TypeNode, - TCount extends CountNode = CountNode, -> { - readonly kind: 'mapTypeNode'; - - // Children. - readonly key: TKey; - readonly value: TValue; - readonly count: TCount; -} diff --git a/packages/node-types/src/typeNodes/NumberTypeNode.ts b/packages/node-types/src/typeNodes/NumberTypeNode.ts deleted file mode 100644 index 3676ebe8b..000000000 --- a/packages/node-types/src/typeNodes/NumberTypeNode.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type NumberFormat = - | 'f32' - | 'f64' - | 'i8' - | 'i16' - | 'i32' - | 'i64' - | 'i128' - | 'shortU16' - | 'u8' - | 'u16' - | 'u32' - | 'u64' - | 'u128'; - -export interface NumberTypeNode { - readonly kind: 'numberTypeNode'; - - // Data. - readonly format: TFormat; - readonly endian: 'be' | 'le'; -} diff --git a/packages/node-types/src/typeNodes/PostOffsetTypeNode.ts b/packages/node-types/src/typeNodes/PostOffsetTypeNode.ts deleted file mode 100644 index e40341702..000000000 --- a/packages/node-types/src/typeNodes/PostOffsetTypeNode.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TypeNode } from './TypeNode'; - -export interface PostOffsetTypeNode { - readonly kind: 'postOffsetTypeNode'; - - // Data. - readonly offset: number; - readonly strategy: 'absolute' | 'padded' | 'preOffset' | 'relative'; - - // Children. - readonly type: TType; -} diff --git a/packages/node-types/src/typeNodes/PreOffsetTypeNode.ts b/packages/node-types/src/typeNodes/PreOffsetTypeNode.ts deleted file mode 100644 index 558090770..000000000 --- a/packages/node-types/src/typeNodes/PreOffsetTypeNode.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TypeNode } from './TypeNode'; - -export interface PreOffsetTypeNode { - readonly kind: 'preOffsetTypeNode'; - - // Data. - readonly offset: number; - readonly strategy: 'absolute' | 'padded' | 'relative'; - - // Children. - readonly type: TType; -} diff --git a/packages/node-types/src/typeNodes/SentinelTypeNode.ts b/packages/node-types/src/typeNodes/SentinelTypeNode.ts deleted file mode 100644 index cc4208f2f..000000000 --- a/packages/node-types/src/typeNodes/SentinelTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ConstantValueNode } from '../valueNodes'; -import type { TypeNode } from './TypeNode'; - -export interface SentinelTypeNode< - TType extends TypeNode = TypeNode, - TSentinel extends ConstantValueNode = ConstantValueNode, -> { - readonly kind: 'sentinelTypeNode'; - - // Children. - readonly type: TType; - readonly sentinel: TSentinel; -} diff --git a/packages/node-types/src/typeNodes/SetTypeNode.ts b/packages/node-types/src/typeNodes/SetTypeNode.ts deleted file mode 100644 index 42df2b3a6..000000000 --- a/packages/node-types/src/typeNodes/SetTypeNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CountNode } from '../countNodes'; -import type { TypeNode } from './TypeNode'; - -export interface SetTypeNode { - readonly kind: 'setTypeNode'; - - // Children. - readonly item: TItem; - readonly count: TCount; -} diff --git a/packages/node-types/src/typeNodes/StringTypeNode.ts b/packages/node-types/src/typeNodes/StringTypeNode.ts deleted file mode 100644 index 6f8b762c7..000000000 --- a/packages/node-types/src/typeNodes/StringTypeNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { BytesEncoding } from '../shared'; - -export interface StringTypeNode { - readonly kind: 'stringTypeNode'; - - // Data. - readonly encoding: TEncoding; -} diff --git a/packages/node-types/src/typeNodes/StructFieldTypeNode.ts b/packages/node-types/src/typeNodes/StructFieldTypeNode.ts deleted file mode 100644 index 91a5306cb..000000000 --- a/packages/node-types/src/typeNodes/StructFieldTypeNode.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { CamelCaseString, Docs } from '../shared'; -import type { ValueNode } from '../valueNodes'; -import type { TypeNode } from './TypeNode'; - -export interface StructFieldTypeNode< - TType extends TypeNode = TypeNode, - TDefaultValue extends ValueNode | undefined = ValueNode | undefined, -> { - readonly kind: 'structFieldTypeNode'; - - // Data. - readonly name: CamelCaseString; - readonly defaultValueStrategy?: 'omitted' | 'optional'; - readonly docs?: Docs; - - // Children. - readonly type: TType; - readonly defaultValue?: TDefaultValue; -} diff --git a/packages/node-types/src/typeNodes/StructTypeNode.ts b/packages/node-types/src/typeNodes/StructTypeNode.ts deleted file mode 100644 index 015414ef0..000000000 --- a/packages/node-types/src/typeNodes/StructTypeNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { StructFieldTypeNode } from './StructFieldTypeNode'; - -export interface StructTypeNode { - readonly kind: 'structTypeNode'; - - // Children. - readonly fields: TFields; -} diff --git a/packages/node-types/src/typeNodes/TupleTypeNode.ts b/packages/node-types/src/typeNodes/TupleTypeNode.ts deleted file mode 100644 index 4761de58f..000000000 --- a/packages/node-types/src/typeNodes/TupleTypeNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { TypeNode } from './TypeNode'; - -export interface TupleTypeNode { - readonly kind: 'tupleTypeNode'; - - // Children. - readonly items: TItems; -} diff --git a/packages/node-types/src/typeNodes/ZeroableOptionTypeNode.ts b/packages/node-types/src/typeNodes/ZeroableOptionTypeNode.ts deleted file mode 100644 index 35cb45354..000000000 --- a/packages/node-types/src/typeNodes/ZeroableOptionTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ConstantValueNode } from '../valueNodes'; -import type { TypeNode } from './TypeNode'; - -export interface ZeroableOptionTypeNode< - TItem extends TypeNode = TypeNode, - TZeroValue extends ConstantValueNode | undefined = ConstantValueNode | undefined, -> { - readonly kind: 'zeroableOptionTypeNode'; - - // Children. - readonly item: TItem; - readonly zeroValue?: TZeroValue; -} diff --git a/packages/node-types/src/valueNodes/ArrayValueNode.ts b/packages/node-types/src/valueNodes/ArrayValueNode.ts deleted file mode 100644 index 6066467d2..000000000 --- a/packages/node-types/src/valueNodes/ArrayValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ValueNode } from './ValueNode'; - -export interface ArrayValueNode { - readonly kind: 'arrayValueNode'; - - // Children. - readonly items: TItems; -} diff --git a/packages/node-types/src/valueNodes/BytesValueNode.ts b/packages/node-types/src/valueNodes/BytesValueNode.ts deleted file mode 100644 index ef7cd3254..000000000 --- a/packages/node-types/src/valueNodes/BytesValueNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { BytesEncoding } from '../shared'; - -export interface BytesValueNode { - readonly kind: 'bytesValueNode'; - - // Data. - readonly data: string; - readonly encoding: BytesEncoding; -} diff --git a/packages/node-types/src/valueNodes/EnumValueNode.ts b/packages/node-types/src/valueNodes/EnumValueNode.ts deleted file mode 100644 index 869d1077c..000000000 --- a/packages/node-types/src/valueNodes/EnumValueNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DefinedTypeLinkNode } from '../linkNodes'; -import type { CamelCaseString } from '../shared'; -import type { StructValueNode } from './StructValueNode'; -import type { TupleValueNode } from './TupleValueNode'; - -export interface EnumValueNode< - TEnum extends DefinedTypeLinkNode = DefinedTypeLinkNode, - TValue extends StructValueNode | TupleValueNode | undefined = StructValueNode | TupleValueNode | undefined, -> { - readonly kind: 'enumValueNode'; - - // Data. - readonly variant: CamelCaseString; - - // Children. - readonly enum: TEnum; - readonly value?: TValue; -} diff --git a/packages/node-types/src/valueNodes/MapValueNode.ts b/packages/node-types/src/valueNodes/MapValueNode.ts deleted file mode 100644 index ab5ba43cc..000000000 --- a/packages/node-types/src/valueNodes/MapValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { MapEntryValueNode } from './MapEntryValueNode'; - -export interface MapValueNode { - readonly kind: 'mapValueNode'; - - // Children. - readonly entries: TEntries; -} diff --git a/packages/node-types/src/valueNodes/NumberValueNode.ts b/packages/node-types/src/valueNodes/NumberValueNode.ts deleted file mode 100644 index 4a9b4dea3..000000000 --- a/packages/node-types/src/valueNodes/NumberValueNode.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface NumberValueNode { - readonly kind: 'numberValueNode'; - - // Data. - readonly number: number; -} diff --git a/packages/node-types/src/valueNodes/PublicKeyValueNode.ts b/packages/node-types/src/valueNodes/PublicKeyValueNode.ts deleted file mode 100644 index b7c168060..000000000 --- a/packages/node-types/src/valueNodes/PublicKeyValueNode.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CamelCaseString } from '../shared'; - -export interface PublicKeyValueNode { - readonly kind: 'publicKeyValueNode'; - - // Data. - readonly publicKey: string; - readonly identifier?: CamelCaseString; -} diff --git a/packages/node-types/src/valueNodes/SetValueNode.ts b/packages/node-types/src/valueNodes/SetValueNode.ts deleted file mode 100644 index 693557d22..000000000 --- a/packages/node-types/src/valueNodes/SetValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ValueNode } from './ValueNode'; - -export interface SetValueNode { - readonly kind: 'setValueNode'; - - // Children. - readonly items: TItems; -} diff --git a/packages/node-types/src/valueNodes/StructValueNode.ts b/packages/node-types/src/valueNodes/StructValueNode.ts deleted file mode 100644 index 9f766ed36..000000000 --- a/packages/node-types/src/valueNodes/StructValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { StructFieldValueNode } from './StructFieldValueNode'; - -export interface StructValueNode { - readonly kind: 'structValueNode'; - - // Children. - readonly fields: TFields; -} diff --git a/packages/node-types/src/valueNodes/TupleValueNode.ts b/packages/node-types/src/valueNodes/TupleValueNode.ts deleted file mode 100644 index c38178324..000000000 --- a/packages/node-types/src/valueNodes/TupleValueNode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ValueNode } from './ValueNode'; - -export interface TupleValueNode { - readonly kind: 'tupleValueNode'; - - // Children. - readonly items: TItems; -} diff --git a/packages/node-types/tsconfig.declarations.json b/packages/node-types/tsconfig.declarations.json index dc2d27bb0..ef5861450 100644 --- a/packages/node-types/tsconfig.declarations.json +++ b/packages/node-types/tsconfig.declarations.json @@ -6,5 +6,5 @@ "outDir": "./dist/types" }, "extends": "./tsconfig.json", - "include": ["src/index.ts", "src/types"] + "include": ["src"] } diff --git a/packages/nodes-from-anchor/src/v00/ProgramNode.ts b/packages/nodes-from-anchor/src/v00/ProgramNode.ts index 695bd047e..67d700cf3 100644 --- a/packages/nodes-from-anchor/src/v00/ProgramNode.ts +++ b/packages/nodes-from-anchor/src/v00/ProgramNode.ts @@ -1,4 +1,4 @@ -import { ProgramNode, programNode, ProgramVersion } from '@codama/nodes'; +import { ProgramNode, programNode, Version } from '@codama/nodes'; import { accountNodeFromAnchorV00 } from './AccountNode'; import { constantNodeFromAnchorV00 } from './ConstantNode'; @@ -28,6 +28,6 @@ export function programNodeFromAnchorV00(idl: IdlV00): ProgramNode { origin, pdas, publicKey: (idl?.metadata as { address?: string })?.address ?? '', - version: idl.version as ProgramVersion, + version: idl.version as Version, }); } diff --git a/packages/nodes-from-anchor/src/v01/ProgramNode.ts b/packages/nodes-from-anchor/src/v01/ProgramNode.ts index 81230101f..abab4115e 100644 --- a/packages/nodes-from-anchor/src/v01/ProgramNode.ts +++ b/packages/nodes-from-anchor/src/v01/ProgramNode.ts @@ -1,4 +1,4 @@ -import { ProgramNode, programNode, ProgramVersion } from '@codama/nodes'; +import { ProgramNode, programNode, Version } from '@codama/nodes'; import { accountNodeFromAnchorV01 } from './AccountNode'; import { constantNodeFromAnchorV01 } from './ConstantNode'; @@ -34,6 +34,6 @@ export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode { name: idl.metadata.name, origin: 'anchor', publicKey: idl.address, - version: idl.metadata.version as ProgramVersion, + version: idl.metadata.version as Version, }); } diff --git a/packages/spec-generators/.gitignore b/packages/spec-generators/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/packages/spec-generators/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/spec-generators/.prettierignore b/packages/spec-generators/.prettierignore new file mode 100644 index 000000000..eae16afdd --- /dev/null +++ b/packages/spec-generators/.prettierignore @@ -0,0 +1,2 @@ +dist/ +CHANGELOG.md diff --git a/packages/spec-generators/README.md b/packages/spec-generators/README.md new file mode 100644 index 000000000..f728935f1 --- /dev/null +++ b/packages/spec-generators/README.md @@ -0,0 +1,41 @@ +# `@codama-internal/spec-generators` + +Private workspace package. Houses the code generators that turn the +`@codama/spec` encoded specification into the source code of the public +Codama monorepo packages. + +This package is **never published**. It is only invoked at development +time, via `pnpm generate` from the repository root, to regenerate +specific subtrees of the monorepo (today: `@codama/node-types/src/generated/`). + +## Architecture + +Each generator owns its full pipeline. It knows which spec major it +targets, which output directory it writes to, and which compatibility +knobs it needs. Generators do not take a spec as a parameter — they +import it directly from `@codama/spec`. Adding a new generator is a +matter of dropping a folder under `src/`, exposing a `generate()` from +its `index.ts`, and wiring it into the orchestrator. + +The orchestrator entrypoint (`src/index.ts`) re-exports a top-level +`generate()` that runs every registered generator sequentially. The bin +script (`bin/generate.ts`) wraps it for `pnpm generate`. + +## Layout + +``` +src/ +├── index.ts # orchestrator: re-exports a top-level generate() +└── nodeTypes/ # generator for @codama/node-types/src/generated/ + +bin/generate.ts # pnpm generate entry; wraps the orchestrator + +test/ +└── nodeTypes/ # unit tests for the @codama/node-types generator +``` + +## Output cleanup + +Each generator wipes its target `/generated/` folder before writing, so +stale files from a previous run never survive. Hand-written files in the +target package live as siblings of `/generated/`, never inside it. diff --git a/packages/spec-generators/bin/generate.ts b/packages/spec-generators/bin/generate.ts new file mode 100644 index 000000000..4c7ea9511 --- /dev/null +++ b/packages/spec-generators/bin/generate.ts @@ -0,0 +1,21 @@ +import { relative } from 'node:path'; +import process from 'node:process'; + +import { generate } from '../src/index'; +import { getRepoDirectory } from '../src/shared'; + +function main(): void { + const result = generate(); + const repoRoot = getRepoDirectory(); + for (const { generator, outputDir } of result.outputs) { + process.stdout.write(`generated [${generator}] → ${relative(repoRoot, outputDir)}\n`); + } +} + +try { + main(); +} catch (err: unknown) { + process.stderr.write(`spec-generators failed: ${err instanceof Error ? err.message : String(err)}\n`); + if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}\n`); + process.exit(1); +} diff --git a/packages/spec-generators/package.json b/packages/spec-generators/package.json new file mode 100644 index 000000000..2a2ad944e --- /dev/null +++ b/packages/spec-generators/package.json @@ -0,0 +1,30 @@ +{ + "name": "@codama-internal/spec-generators", + "version": "0.0.0", + "description": "Private code generators that produce parts of the public Codama monorepo from the encoded `@codama/spec`", + "private": true, + "type": "module", + "scripts": { + "build": "rimraf dist && tsup", + "generate": "pnpm build && node ./dist/generate.mjs && pnpm --filter @codama/node-types lint:fix", + "lint": "eslint . && prettier --check .", + "lint:fix": "eslint --fix . && prettier --write .", + "test": "pnpm test:types && pnpm test:unit", + "test:types": "tsc --noEmit", + "test:unit": "vitest run" + }, + "dependencies": { + "@codama/fragments": "workspace:*", + "@codama/spec": "1.6.0-rc.4" + }, + "devDependencies": { + "@types/node": "^25", + "rimraf": "6.1.2", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=20.18.0" + } +} diff --git a/packages/spec-generators/src/index.ts b/packages/spec-generators/src/index.ts new file mode 100644 index 000000000..2a853b9f8 --- /dev/null +++ b/packages/spec-generators/src/index.ts @@ -0,0 +1,34 @@ +import { joinPath } from '@codama/fragments/javascript'; +import { getSpec } from '@codama/spec'; + +import { generateNodeTypes, GENERIC_PARAM_ORDER, NARROWABLE_DATA_ATTRIBUTES } from './nodeTypes'; +import { getRepoDirectory } from './shared'; + +export interface GenerateResult { + /** One entry per generator that ran, in the order they ran. */ + readonly outputs: readonly { readonly generator: string; readonly outputDir: string }[]; +} + +/** + * Run every registered generator in turn. Each generator writes into a + * dedicated output directory under the repo; the returned list records + * where the freshly-generated files landed so the bin script can + * report on the run. + */ +export function generate(): GenerateResult { + const outputs: { generator: string; outputDir: string }[] = []; + const spec = getSpec(); + + { + const outputDir = joinPath(getRepoDirectory(), 'packages', 'node-types', 'src', 'generated'); + generateNodeTypes(spec, { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, + outputDir, + targetSpecMajor: 1, + }); + outputs.push({ generator: 'nodeTypes', outputDir }); + } + + return { outputs }; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts b/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts new file mode 100644 index 000000000..7e76ce723 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts @@ -0,0 +1,27 @@ +import { type Fragment, fragment, getDocblockFragment } from '@codama/fragments/javascript'; +import type { AttributeSpec } from '@codama/spec'; + +import { isAttributeLifted } from '../options'; +import type { RenderScope } from '../utils/scope'; +import { getTypeExprFragment } from './typeExpr'; +import { getTypeParameterIdentifierFragment } from './typeParameterIdentifier'; + +/** + * Render one attribute as a body line inside an interface declaration. + * Lifted attributes use their type-parameter identifier (e.g. + * `readonly data: TData;`); non-lifted attributes use the rendered + * type expression. Optional attributes carry a `?:` marker; `docs` + * (if any) become a JSDoc prefix. + */ +export function getAttributeBodyLineFragment( + nodeKind: string, + attr: AttributeSpec, + scope: Pick, +): Fragment { + const docPrefix = getDocblockFragment(attr.docs, { withLineJump: true }); + const optionalMark = attr.optional === true ? '?' : ''; + const typeFragment = isAttributeLifted(nodeKind, attr, scope) + ? getTypeParameterIdentifierFragment(attr.name) + : getTypeExprFragment(attr.type); + return fragment`${docPrefix}readonly ${attr.name}${optionalMark}: ${typeFragment};`; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/codamaVersion.ts b/packages/spec-generators/src/nodeTypes/fragments/codamaVersion.ts new file mode 100644 index 000000000..96d741c0e --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/codamaVersion.ts @@ -0,0 +1,13 @@ +import { type Fragment, fragment, getDocblockFragment } from '@codama/fragments/javascript'; + +export function getCodamaVersionFragment(specVersion: string): Fragment { + const docblock = getDocblockFragment( + [ + 'The Codama spec version this package describes. Pinned to the literal', + 'version of the spec at generation time; documents conforming to this', + 'version of the spec carry this exact string.', + ], + { withLineJump: true }, + ); + return fragment`${docblock}export type CodamaVersion = '${specVersion}';`; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/enumeration.ts b/packages/spec-generators/src/nodeTypes/fragments/enumeration.ts new file mode 100644 index 000000000..f6ec3ae90 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/enumeration.ts @@ -0,0 +1,18 @@ +import { type Fragment, fragment, getDocblockFragment, mergeFragments, pascalCase } from '@codama/fragments/javascript'; +import type { EnumerationSpec, EnumerationVariantSpec } from '@codama/spec'; + +export function getEnumerationFragment(enumeration: EnumerationSpec): Fragment { + const sortedVariants = [...enumeration.variants].sort((a, b) => a.name.localeCompare(b.name)); + const variantsBlock = getVariantsBlockFragment(sortedVariants); + const docComment = getDocblockFragment(enumeration.docs, { withLineJump: true }); + return fragment`${docComment}export type ${pascalCase(enumeration.name)} =\n${variantsBlock};`; +} + +function getVariantsBlockFragment(variants: readonly EnumerationVariantSpec[]): Fragment { + const variantFragments = variants.map(v => { + const literal = JSON.stringify(v.name); + const docblock = getDocblockFragment(v.docs); + return docblock ? fragment`${docblock}\n| ${literal}` : fragment`| ${literal}`; + }); + return mergeFragments(variantFragments, parts => parts.join('\n')); +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/index.ts b/packages/spec-generators/src/nodeTypes/fragments/index.ts new file mode 100644 index 000000000..81c3d592f --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/index.ts @@ -0,0 +1,13 @@ +export * from './attributeBodyLine'; +export * from './codamaVersion'; +export * from './enumeration'; +export * from './indexPage'; +export * from './kindLine'; +export * from './nestedUnion'; +export * from './node'; +export * from './nodeRegistry'; +export * from './page'; +export * from './typeExpr'; +export * from './typeParameterDefinition'; +export * from './typeParameterIdentifier'; +export * from './union'; diff --git a/packages/spec-generators/src/nodeTypes/fragments/indexPage.ts b/packages/spec-generators/src/nodeTypes/fragments/indexPage.ts new file mode 100644 index 000000000..2fa5008a1 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/indexPage.ts @@ -0,0 +1,10 @@ +import { type Fragment, getExportAllFragment, mergeFragments } from '@codama/fragments/javascript'; + +/** Render an `index.ts` page that alphabetically `export * from './';`s every supplied name. */ +export function getIndexPageFragment(names: readonly string[]): Fragment { + const sorted = [...names].sort((a, b) => a.localeCompare(b)); + return mergeFragments( + sorted.map(name => getExportAllFragment(`./${name}`)), + parts => parts.join('\n'), + ); +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/kindLine.ts b/packages/spec-generators/src/nodeTypes/fragments/kindLine.ts new file mode 100644 index 000000000..67b4a8b32 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/kindLine.ts @@ -0,0 +1,6 @@ +import { type Fragment, fragment } from '@codama/fragments/javascript'; + +/** Produce a node's `kind` discriminator line, e.g. `readonly kind: 'fooNode';`. */ +export function getKindLineFragment(kind: string): Fragment { + return fragment`readonly kind: '${kind}';`; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/nestedUnion.ts b/packages/spec-generators/src/nodeTypes/fragments/nestedUnion.ts new file mode 100644 index 000000000..abe970058 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/nestedUnion.ts @@ -0,0 +1,25 @@ +import { + type Fragment, + fragment, + getDocblockFragment, + mergeFragments, + pascalCase, + use, +} from '@codama/fragments/javascript'; +import type { NestedUnionSpec } from '@codama/spec'; + +import { getTypeExprFragment } from './typeExpr'; + +export function getNestedUnionFragment(nu: NestedUnionSpec): Fragment { + const sortedWrappers = [...nu.wrappers].sort(); + const aliasName = pascalCase(nu.name); + const baseFragment = getTypeExprFragment(nu.base); + const wrapperRefs = sortedWrappers.map(kind => use(`type ${pascalCase(kind)}`, `node:${kind}`)); + const docComment = getDocblockFragment(nu.docs, { withLineJump: true }); + + const arms = mergeFragments( + wrapperRefs.map(w => fragment`| ${w}<${aliasName}>`), + parts => parts.join('\n'), + ); + return fragment`${docComment}export type ${aliasName} =\n${arms}\n| TType;`; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/node.ts b/packages/spec-generators/src/nodeTypes/fragments/node.ts new file mode 100644 index 000000000..c5bbc989b --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/node.ts @@ -0,0 +1,89 @@ +import { type Fragment, fragment, getDocblockFragment, mergeFragments, pascalCase } from '@codama/fragments/javascript'; +import { type AttributeSpec, isChildAttribute, type NodeSpec } from '@codama/spec'; + +import { isAttributeLifted } from '../options'; +import type { RenderScope } from '../utils/scope'; +import { isTypeExprSelfReferential } from '../utils/selfReference'; +import { getAttributeBodyLineFragment } from './attributeBodyLine'; +import { getKindLineFragment } from './kindLine'; +import { getTypeParameterDefinitionFragment } from './typeParameterDefinition'; + +type NodeScope = Pick; + +export function getNodeFragment(node: NodeSpec, scope: NodeScope): Fragment { + const interfaceName = pascalCase(node.kind); + // Self-referential nodes need an outside `type SelfFooNode = FooNode;` + // alias: TS rejects a generic default that names the interface + // being declared (e.g. `TX extends FooNode[] = FooNode[]` inside + // `interface FooNode`). + const isSelfReferential = node.attributes.some( + attr => isChildAttribute(attr.type) && isTypeExprSelfReferential(attr.type, node.kind), + ); + const selfAlias = isSelfReferential ? { alias: `Self${interfaceName}`, kind: node.kind } : undefined; + + const genericsBlock = getGenericsBlockFragment( + orderLifted(node, scope).map(attr => getTypeParameterDefinitionFragment(attr, { selfAlias })), + ); + + const dataLines = node.attributes + .filter(attr => !isChildAttribute(attr.type)) + .map(attr => getAttributeBodyLineFragment(node.kind, attr, scope)); + const childLines = node.attributes + .filter(attr => isChildAttribute(attr.type)) + .map(attr => getAttributeBodyLineFragment(node.kind, attr, scope)); + + const dataSection = + dataLines.length > 0 + ? mergeFragments([fragment`// Data.`, ...dataLines], parts => parts.join('\n')) + : undefined; + const childSection = + childLines.length > 0 + ? mergeFragments([fragment`// Children.`, ...childLines], parts => parts.join('\n')) + : undefined; + const body = mergeFragments([getKindLineFragment(node.kind), dataSection, childSection], parts => + parts.join('\n\n'), + ); + + const aliasPrefix = selfAlias ? fragment`type ${selfAlias.alias} = ${interfaceName};\n\n` : undefined; + const docComment = getDocblockFragment(node.docs, { withLineJump: true }); + return fragment`${aliasPrefix}${docComment}export interface ${interfaceName}${genericsBlock} {\n${body}\n}`; +} + +/** + * Return the lifted attributes for `node` in their emission order. If + * `scope.genericParamOrder` declares an override for this kind, it must + * enumerate exactly the lifted set; mismatches throw rather than + * silently drop generics. + */ +function orderLifted(node: NodeSpec, scope: NodeScope): readonly AttributeSpec[] { + const lifted = node.attributes.filter(attr => isAttributeLifted(node.kind, attr, scope)); + const override = scope.genericParamOrder.get(node.kind); + if (!override) return lifted; + + const byName = new Map(lifted.map(attr => [attr.name, attr])); + const overrideSet = new Set(override); + const liftedSet = new Set(byName.keys()); + const missingFromOverride = [...liftedSet].filter(n => !overrideSet.has(n)); + const unknownInOverride = override.filter(n => !liftedSet.has(n)); + if (missingFromOverride.length > 0 || unknownInOverride.length > 0) { + const parts: string[] = []; + if (missingFromOverride.length > 0) { + parts.push(`missing lifted attribute(s) ${JSON.stringify(missingFromOverride)}`); + } + if (unknownInOverride.length > 0) { + parts.push(`unknown attribute(s) ${JSON.stringify(unknownInOverride)}`); + } + throw new Error( + `@codama/node-types generator: genericParamOrder for "${node.kind}" is out of sync with the spec: ${parts.join('; ')}.`, + ); + } + return override.map(name => byName.get(name)!); +} + +function getGenericsBlockFragment(genericParams: readonly Fragment[]): Fragment { + if (genericParams.length === 0) return fragment``; + return mergeFragments(genericParams, parts => { + const lines = parts.map(p => `${p},`).join('\n'); + return `<\n${lines}\n>`; + }); +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts b/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts new file mode 100644 index 000000000..5e2174614 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts @@ -0,0 +1,79 @@ +import { type Fragment, mergeFragments, pascalCase, use } from '@codama/fragments/javascript'; +import type { Spec, UnionMember } from '@codama/spec'; + +const REGISTERED_CATEGORY_UNIONS: readonly string[] = [ + 'RegisteredContextualValueNode', + 'RegisteredCountNode', + 'RegisteredDiscriminatorNode', + 'RegisteredLinkNode', + 'RegisteredPdaSeedNode', + 'RegisteredTypeNode', + 'RegisteredValueNode', +]; + +type UnionLookup = ReadonlyMap; + +export function getNodeRegistryFragment(spec: Spec): Fragment { + const unionByName: UnionLookup = new Map(spec.categories.flatMap(c => c.unions).map(u => [u.name, u])); + + const registeredUnionFragments = REGISTERED_CATEGORY_UNIONS.map(unionName => { + if (!unionByName.has(unionName)) { + throw new Error( + `@codama/node-types generator: missing union "${unionName}" expected by REGISTERED_CATEGORY_UNIONS. ` + + `Either the spec dropped this category-registry union or REGISTERED_CATEGORY_UNIONS is out of date.`, + ); + } + return use(`type ${pascalCase(unionName)}`, `union:${unionName}`); + }); + + const registeredKinds = new Set( + REGISTERED_CATEGORY_UNIONS.flatMap(unionName => [...collectKindsFromUnion(unionByName, unionName)]), + ); + + const directNodeFragments = spec.categories + .flatMap(c => c.nodes) + .filter(node => !registeredKinds.has(node.kind)) + .map(node => use(`type ${pascalCase(node.kind)}`, `node:${node.kind}`)); + + const sortedMembers = [...registeredUnionFragments, ...directNodeFragments].sort((a, b) => + a.content.localeCompare(b.content), + ); + return mergeFragments(sortedMembers, contents => { + const memberLines = contents.map(c => `| ${c}`).join('\n'); + return [ + '// Node Registration.', + "export type NodeKind = Node['kind'];", + 'export type Node =', + `${memberLines};`, + '', + '// Node Helpers.', + 'export type GetNodeFromKind = Extract;', + ].join('\n'); + }); +} + +/** + * Recursively expand `union` references in a union's member list into + * their own members, leaving only direct `node` / `nestedUnion` + * members. Used to walk the registry-union → sub-union → node graph. + */ +function flattenUnionMembers( + unionByName: UnionLookup, + unionName: string, + visited: Set = new Set(), +): readonly UnionMember[] { + if (visited.has(unionName)) return []; + visited.add(unionName); + const union = unionByName.get(unionName); + if (!union) return []; + return union.members.flatMap(m => (m.kind === 'union' ? flattenUnionMembers(unionByName, m.name, visited) : [m])); +} + +/** The set of node kinds reachable from a union (recursively through sub-unions). */ +function collectKindsFromUnion(unionByName: UnionLookup, unionName: string): ReadonlySet { + return new Set( + flattenUnionMembers(unionByName, unionName) + .filter(m => m.kind === 'node') + .map(m => m.name), + ); +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/page.ts b/packages/spec-generators/src/nodeTypes/fragments/page.ts new file mode 100644 index 000000000..b9e635c52 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/page.ts @@ -0,0 +1,53 @@ +import { + type Fragment, + fragment, + type ImportInfo, + importMapToString, + mergeFragments, + mergeImportMaps, + type Module, + type Path, + type UsedIdentifier, +} from '@codama/fragments/javascript'; + +import { relativeImportPath, type RenderScope, type SymbolicModule } from '../utils/scope'; + +/** + * Render a fully-resolved TS page from a renderer's symbolic-keyed + * fragment: rewrite the import map to relative paths against + * `currentPath` (dropping self-imports, throwing on unknown keys), + * then prepend the stringified import block to the body content. + */ +export function getPageFragment( + body: Fragment, + scope: Pick, + currentPath: Path, +): Fragment { + const resolved = resolveFragmentImports(body, scope, currentPath); + const importBlock = resolved.imports.size > 0 ? fragment`${importMapToString(resolved.imports)}` : undefined; + return mergeFragments([importBlock, resolved], parts => parts.join('\n\n').trimEnd() + '\n'); +} + +function resolveFragmentImports( + body: Fragment, + scope: Pick, + currentPath: Path, +): Fragment { + if (body.imports.size === 0) return body; + const resolvedMaps = [...body.imports.entries()].flatMap(([symbolicModule, identifiers]) => { + // The fragment library types `Module` as a bare `string`, but + // every key in this map originated from a `use(..., ':')` + // call so it is a SymbolicModule in practice. + const target = scope.symbolicModules.get(symbolicModule as SymbolicModule); + if (target === undefined) { + throw new Error( + `@codama/node-types generator: unknown symbolic module "${symbolicModule}" referenced from "${currentPath}". ` + + `Add an entry to the RenderScope.`, + ); + } + if (target === currentPath) return []; + const resolvedPath = relativeImportPath(currentPath, target); + return [new Map>([[resolvedPath, identifiers]])]; + }); + return Object.freeze({ ...body, imports: mergeImportMaps(resolvedMaps) }); +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts b/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts new file mode 100644 index 000000000..ef2f65935 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/typeExpr.ts @@ -0,0 +1,94 @@ +import { type Fragment, fragment, mergeFragments, pascalCase, use } from '@codama/fragments/javascript'; +import type { TypeExpr } from '@codama/spec'; + +export function getTypeExprFragment(expr: TypeExpr): Fragment { + switch (expr.kind) { + case 'string': + return getStringExprFragment(expr); + case 'integer': + case 'float': + return fragment`number`; + case 'boolean': + return fragment`boolean`; + case 'literal': + return fragment`${literalToTs(expr.value)}`; + case 'literalUnion': + return fragment`${formatLiteralUnionTs(expr.values)}`; + case 'codamaVersion': + return use('type CodamaVersion', 'version:CodamaVersion'); + case 'docs': + return use('type Docs', 'docs:Docs'); + case 'enumeration': + return use(`type ${pascalCase(expr.name)}`, `enumeration:${expr.name}`); + case 'node': + return use(`type ${pascalCase(expr.name)}`, `node:${expr.name}`); + case 'union': + return use(`type ${pascalCase(expr.name)}`, `union:${expr.name}`); + case 'nestedUnion': { + const wrapper = use(`type ${pascalCase(expr.alias)}`, `nestedUnion:${expr.alias}`); + const inner = use(`type ${pascalCase(expr.name)}`, `node:${expr.name}`); + return fragment`${wrapper}<${inner}>`; + } + case 'array': { + const inner = getTypeExprFragment(expr.of); + return fragment`Array<${inner}>`; + } + case 'tuple': { + const items = expr.items.map(item => getTypeExprFragment(item)); + return mergeFragments(items, parts => `[${parts.join(', ')}]`); + } + } +} + +/** + * Like {@link getTypeExprFragment} but substitutes `selfAlias` for any + * direct `node` reference matching `selfKind`. `union` / `nestedUnion` + * references are not recursed into — their named alias already breaks + * the cycle on the TS side. + */ +export function getTypeExprWithSelfAliasFragment(expr: TypeExpr, selfKind: string, selfAlias: string): Fragment { + switch (expr.kind) { + case 'node': + return expr.name === selfKind ? fragment`${selfAlias}` : getTypeExprFragment(expr); + case 'array': { + const inner = getTypeExprWithSelfAliasFragment(expr.of, selfKind, selfAlias); + return fragment`Array<${inner}>`; + } + case 'tuple': { + const items = expr.items.map(item => getTypeExprWithSelfAliasFragment(item, selfKind, selfAlias)); + return mergeFragments(items, parts => `[${parts.join(', ')}]`); + } + default: + return getTypeExprFragment(expr); + } +} + +function getStringExprFragment(expr: Extract): Fragment { + if (!expr.constraint) return fragment`string`; + if (expr.constraint === 'identifier') { + return use('type CamelCaseString', 'brand:CamelCaseString'); + } + if (expr.constraint === 'version') { + return use('type Version', 'version:Version'); + } + return fragment`string`; +} + +function literalToTs(value: boolean | number | string): string { + return typeof value === 'string' ? JSON.stringify(value) : String(value); +} + +/** + * Render a `literalUnion`'s values as a `|`-separated TS expression, + * collapsing `true | false` to `boolean` (placed first) as a TS-only + * readability normalisation. + */ +function formatLiteralUnionTs(values: readonly (boolean | number | string)[]): string { + const hasTrue = values.includes(true); + const hasFalse = values.includes(false); + if (hasTrue && hasFalse) { + const rest = values.filter(v => v !== true && v !== false).map(literalToTs); + return ['boolean', ...rest].join(' | '); + } + return values.map(literalToTs).join(' | '); +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/typeParameterDefinition.ts b/packages/spec-generators/src/nodeTypes/fragments/typeParameterDefinition.ts new file mode 100644 index 000000000..5e7653b28 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/typeParameterDefinition.ts @@ -0,0 +1,37 @@ +import { type Fragment, fragment } from '@codama/fragments/javascript'; +import type { AttributeSpec } from '@codama/spec'; + +import { isTypeExprSelfReferential } from '../utils/selfReference'; +import { getTypeExprFragment, getTypeExprWithSelfAliasFragment } from './typeExpr'; +import { getTypeParameterIdentifierFragment } from './typeParameterIdentifier'; + +export interface TypeParameterDefinitionOptions { + /** + * Substitute `selfAlias.alias` for direct `node` references to + * `selfAlias.kind` inside the constraint and default. Used by + * self-referential nodes to break TS's circular-default error. + */ + readonly selfAlias?: { + readonly alias: string; + readonly kind: string; + }; +} + +/** + * Render the type-parameter definition for one lifted attribute, e.g. + * `TData extends Foo = Foo` (or `… | undefined = … | undefined` when + * the attribute is optional). Callers must only invoke this for + * already-lifted attributes. + */ +export function getTypeParameterDefinitionFragment( + attr: AttributeSpec, + options: TypeParameterDefinitionOptions = {}, +): Fragment { + const identifier = getTypeParameterIdentifierFragment(attr.name); + const baseFragment = + options.selfAlias && isTypeExprSelfReferential(attr.type, options.selfAlias.kind) + ? getTypeExprWithSelfAliasFragment(attr.type, options.selfAlias.kind, options.selfAlias.alias) + : getTypeExprFragment(attr.type); + const constraint = attr.optional === true ? fragment`${baseFragment} | undefined` : baseFragment; + return fragment`${identifier} extends ${constraint} = ${constraint}`; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts b/packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts new file mode 100644 index 000000000..7c6f31c5c --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts @@ -0,0 +1,6 @@ +import { type Fragment, fragment, pascalCase } from '@codama/fragments/javascript'; + +/** Render the type-parameter identifier for an attribute: `data` → `TData`. */ +export function getTypeParameterIdentifierFragment(attributeName: string): Fragment { + return fragment`T${pascalCase(attributeName)}`; +} diff --git a/packages/spec-generators/src/nodeTypes/fragments/union.ts b/packages/spec-generators/src/nodeTypes/fragments/union.ts new file mode 100644 index 000000000..de6044f98 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/fragments/union.ts @@ -0,0 +1,27 @@ +import { + type Fragment, + fragment, + getDocblockFragment, + mergeFragments, + pascalCase, + use, +} from '@codama/fragments/javascript'; +import type { UnionMember, UnionSpec } from '@codama/spec'; + +export function getUnionFragment(union: UnionSpec): Fragment { + const memberFragments = sortMembers(union.members).map(getMemberFragment); + const docComment = getDocblockFragment(union.docs, { withLineJump: true }); + const body = mergeFragments(memberFragments, parts => parts.map(p => `| ${p}`).join('\n')); + return fragment`${docComment}export type ${pascalCase(union.name)} =\n${body};`; +} + +function getMemberFragment(member: UnionMember): Fragment { + if (member.kind === 'node') { + return use(`type ${pascalCase(member.name)}`, `node:${member.name}`); + } + return use(`type ${pascalCase(member.name)}`, `union:${member.name}`); +} + +function sortMembers(members: readonly UnionMember[]): readonly UnionMember[] { + return [...members].sort((a, b) => pascalCase(a.name).localeCompare(pascalCase(b.name))); +} diff --git a/packages/spec-generators/src/nodeTypes/index.ts b/packages/spec-generators/src/nodeTypes/index.ts new file mode 100644 index 000000000..2239f907b --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/index.ts @@ -0,0 +1,144 @@ +import { + createRenderMap, + deleteDirectory, + type Fragment, + mergeFragments, + mergeRenderMaps, + type Path, + pathBasename, + pathDirectory, + type RenderMap, + writeRenderMap, +} from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { + getCodamaVersionFragment, + getEnumerationFragment, + getIndexPageFragment, + getNestedUnionFragment, + getNodeFragment, + getNodeRegistryFragment, + getPageFragment, + getUnionFragment, +} from './fragments'; +import { type GenerateOptions, type RenderOptions, validateRenderOptions } from './options'; +import { buildRenderScope, type RenderScope, type SymbolicModule } from './utils'; + +export { + CATEGORY_DIRECTORIES, + type GenerateOptions, + GENERIC_PARAM_ORDER, + NARROWABLE_DATA_ATTRIBUTES, + type RenderOptions, + validateRenderOptions, +} from './options'; + +/** + * Build the render map and write it to disk under `outputDir`. The + * target directory is wiped before each run so stale files cannot + * survive. No formatter is applied — chain `lint:fix` afterwards. + */ +export function generateNodeTypes(spec: Spec, options: GenerateOptions): void { + const renderMap = getRenderMap(spec, options); + deleteDirectory(options.outputDir); + writeRenderMap(renderMap, options.outputDir); +} + +/** + * Walk the spec and assemble every file the generator produces into a + * single {@link RenderMap}, keyed by `.ts`-suffixed paths. Pure and + * sync: tests can call this directly without touching the filesystem. + */ +export function getRenderMap(spec: Spec, options: RenderOptions): RenderMap { + validateRenderOptions(spec, options); + const scope = buildRenderScope(spec, options); + const specPages = getSpecPagesRenderMap(spec, scope); + const indexPages = getIndexPagesRenderMap(specPages, scope); + return mergeRenderMaps([specPages, indexPages]); +} + +/** + * Walk every spec category plus the top-level `Node` registry and + * return one rendered page per emitted symbolic key. + */ +function getSpecPagesRenderMap(spec: Spec, scope: RenderScope): RenderMap { + const entries: Record = {}; + const emit = (symbolicKey: SymbolicModule, body: Fragment): void => { + const path = resolveEntryPath(scope, symbolicKey); + entries[`${path}.ts`] = getPageFragment(body, scope, path); + }; + + for (const category of spec.categories) { + for (const n of category.nodes) emit(`node:${n.kind}`, getNodeFragment(n, scope)); + for (const u of category.unions) emit(`union:${u.name}`, getUnionFragment(u)); + for (const e of category.enumerations) emit(`enumeration:${e.name}`, getEnumerationFragment(e)); + for (const nu of category.nestedUnions) emit(`nestedUnion:${nu.name}`, getNestedUnionFragment(nu)); + if (category.name === 'shared') { + emit('version:CodamaVersion', getCodamaVersionFragment(spec.version)); + } + } + emit('registry:Node', getNodeRegistryFragment(spec)); + + return createRenderMap(entries); +} + +/** + * Build the per-folder and root `index.ts` re-export pages from a set + * of already-emitted spec pages. + */ +function getIndexPagesRenderMap(specPages: RenderMap, scope: RenderScope): RenderMap { + const filesByFolder = groupPathsByFolder([...specPages.keys()]); + const entries: Record = {}; + + const topLevelFiles = filesByFolder.get('') ?? []; + const subdirs: string[] = []; + for (const [folder, names] of filesByFolder) { + if (folder === '') continue; + entries[`${folder}/index.ts`] = getPageFragment(getIndexPageFragment(names), scope, `${folder}/index`); + subdirs.push(folder); + } + + const topLevelIndex = topLevelFiles.length > 0 ? getIndexPageFragment(topLevelFiles) : undefined; + const subdirsIndex = subdirs.length > 0 ? getIndexPageFragment(subdirs) : undefined; + const rootBody = mergeFragments([topLevelIndex, subdirsIndex], parts => parts.join('\n\n')); + entries['index.ts'] = getPageFragment(rootBody, scope, 'index'); + + return createRenderMap(entries); +} + +/** Resolve a symbolic key to a safe output path, throwing if missing or pointing outside `generated/`. */ +function resolveEntryPath(scope: RenderScope, symbolicKey: SymbolicModule): Path { + const path = scope.symbolicModules.get(symbolicKey); + if (path === undefined) { + throw new Error(`@codama/node-types generator: missing symbolic key "${symbolicKey}" in scope.`); + } + if (path.startsWith('../')) { + throw new Error( + `@codama/node-types generator: refusing to emit "${symbolicKey}" — its location "${path}" points outside generated/.`, + ); + } + return path; +} + +/** + * Group `.ts`-suffixed paths by their parent folder. Top-level files + * (no slash) land under the `''` key. The `.ts` extension is stripped + * from each basename so the result feeds directly into + * {@link getIndexPageFragment}. + */ +function groupPathsByFolder(paths: readonly Path[]): Map { + const byFolder = new Map(); + for (const path of paths) { + const withoutExtension = path.endsWith('.ts') ? path.slice(0, -3) : path; + // `pathDirectory('AccountNode')` returns `'.'` on Node; normalise + // to `''` so the top-level sentinel stays consistent. + const directory = pathDirectory(withoutExtension); + const folder = directory === '.' ? '' : directory; + const basename = pathBasename(withoutExtension); + const names = byFolder.get(folder) ?? []; + names.push(basename); + byFolder.set(folder, names); + } + return byFolder; +} diff --git a/packages/spec-generators/src/nodeTypes/options.ts b/packages/spec-generators/src/nodeTypes/options.ts new file mode 100644 index 000000000..3f2daa792 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/options.ts @@ -0,0 +1,185 @@ +import type { Path } from '@codama/fragments/javascript'; +import { type AttributeSpec, isChildAttribute, type Spec } from '@codama/spec'; + +/** User-facing options for the `@codama/node-types` generator. */ +export interface RenderOptions { + /** + * Map from each spec `category.name` to the output subdirectory + * its entities are emitted into (relative to `generated/`). Use an + * empty string for the top-level (no subdirectory). Omitted means + * "use the v1 defaults" ({@link CATEGORY_DIRECTORIES}). + */ + readonly categoryDirectories?: ReadonlyMap; + /** + * Per-node override of the type-parameter emission order. Each + * value must enumerate exactly the set of attributes lifted for + * the node — no more, no fewer — otherwise the run fails. + */ + readonly genericParamOrder?: ReadonlyMap; + /** + * `${nodeKind}:${attribute}` keys whose data attribute should be + * lifted to a generic param even though the spec classifies it as + * data. Omitted means "lift only children". + */ + readonly narrowableDataAttributes?: ReadonlySet; + /** The spec major version this invocation targets. */ + readonly targetSpecMajor: number; +} + +/** Options consumed by {@link generateNodeTypes}, the disk-writing entry point. */ +export interface GenerateOptions extends RenderOptions { + readonly outputDir: Path; +} + +/** {@link RenderOptions} with every defaultable field resolved. */ +export interface ResolvedRenderOptions { + readonly categoryDirectories: ReadonlyMap; + readonly genericParamOrder: ReadonlyMap; + readonly narrowableDataAttributes: ReadonlySet; + readonly targetSpecMajor: number; +} + +export function resolveRenderOptions(options: RenderOptions): ResolvedRenderOptions { + return { + categoryDirectories: options.categoryDirectories ?? CATEGORY_DIRECTORIES, + genericParamOrder: options.genericParamOrder ?? new Map(), + narrowableDataAttributes: options.narrowableDataAttributes ?? new Set(), + targetSpecMajor: options.targetSpecMajor, + }; +} + +/** + * Default narrowable data attributes for the v1 spec. Each entry + * preserves a narrowing form supported by the legacy `@codama/node-types` + * interface (e.g. `NumberTypeNode<'u32'>`) that downstream constructors + * in `@codama/nodes` rely on. + */ +export const NARROWABLE_DATA_ATTRIBUTES: ReadonlySet = new Set([ + 'numberTypeNode:format', + 'stringTypeNode:encoding', +]); + +/** + * Default per-node type-parameter ordering for the v1 spec. Preserves + * the positional generic args of the legacy `@codama/node-types` + * package: legacy generics keep their original positions, any extra + * generics our renderer adds appear at the end. + */ +export const GENERIC_PARAM_ORDER: ReadonlyMap = new Map([ + ['programNode', ['pdas', 'accounts', 'instructions', 'definedTypes', 'errors', 'events', 'constants']], + ['pdaValueNode', ['seeds', 'programId', 'pda']], + ['instructionArgumentNode', ['defaultValue', 'type']], + [ + 'instructionNode', + [ + 'accounts', + 'arguments', + 'extraArguments', + 'remainingAccounts', + 'byteDeltas', + 'discriminators', + 'subInstructions', + 'status', + ], + ], +]); + +/** + * Default mapping from spec category name to output subdirectory for + * the v1 spec. The empty string places `topLevel` entities at the root + * of `generated/`. + */ +export const CATEGORY_DIRECTORIES: ReadonlyMap = new Map([ + ['contextualValue', 'contextualValueNodes'], + ['count', 'countNodes'], + ['discriminator', 'discriminatorNodes'], + ['link', 'linkNodes'], + ['pdaSeed', 'pdaSeedNodes'], + ['shared', 'shared'], + ['topLevel', ''], + ['type', 'typeNodes'], + ['value', 'valueNodes'], +]); + +/** + * Cross-check the caller-supplied options against the spec at + * generation time. Catches stale `narrowableDataAttributes` entries, + * stale `genericParamOrder` overrides, and missing `categoryDirectories` + * entries whose keys no longer match the spec. + */ +export function validateRenderOptions(spec: Spec, options: RenderOptions): void { + const actualMajor = parseSpecMajor(spec.version); + if (actualMajor !== options.targetSpecMajor) { + throw new Error( + `@codama/node-types generator: targetSpecMajor=${options.targetSpecMajor} but the supplied spec is at version "${spec.version}" (major ${actualMajor}).`, + ); + } + + const validKeys = new Set(); + const validNodeKinds = new Set(); + for (const category of spec.categories) { + for (const node of category.nodes) { + validNodeKinds.add(node.kind); + for (const attr of node.attributes) { + validKeys.add(`${node.kind}:${attr.name}`); + } + } + } + + if (options.categoryDirectories) { + for (const category of spec.categories) { + if (!options.categoryDirectories.has(category.name)) { + throw new Error( + `@codama/node-types generator: categoryDirectories is missing an entry for spec category "${category.name}".`, + ); + } + } + } + + if (options.narrowableDataAttributes) { + for (const key of options.narrowableDataAttributes) { + if (!validKeys.has(key)) { + throw new Error( + `@codama/node-types generator: narrowableDataAttributes references "${key}" which is not a (nodeKind, attribute) pair in the spec.`, + ); + } + } + } + + if (options.genericParamOrder) { + for (const [kind, order] of options.genericParamOrder) { + if (!validNodeKinds.has(kind)) { + throw new Error( + `@codama/node-types generator: genericParamOrder references unknown node kind "${kind}".`, + ); + } + for (const attrName of order) { + if (!validKeys.has(`${kind}:${attrName}`)) { + throw new Error( + `@codama/node-types generator: genericParamOrder for "${kind}" references attribute "${attrName}" which the spec does not declare.`, + ); + } + } + } + } +} + +/** + * Decide whether an attribute lifts to a generic param. An attribute + * lifts when its type tree contains a node / union / nested-union + * reference, or when its `${kind}:${name}` key appears in + * `narrowableDataAttributes`. + */ +export function isAttributeLifted( + nodeKind: string, + attr: AttributeSpec, + options: Pick, +): boolean { + return isChildAttribute(attr.type) || options.narrowableDataAttributes.has(`${nodeKind}:${attr.name}`); +} + +function parseSpecMajor(version: string): number { + const m = /^(\d+)\./.exec(version); + if (!m) throw new Error(`@codama/node-types generator: unable to parse spec version "${version}".`); + return Number(m[1]); +} diff --git a/packages/spec-generators/src/nodeTypes/utils/index.ts b/packages/spec-generators/src/nodeTypes/utils/index.ts new file mode 100644 index 000000000..47bd7e3b0 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/utils/index.ts @@ -0,0 +1,2 @@ +export * from './scope'; +export * from './selfReference'; diff --git a/packages/spec-generators/src/nodeTypes/utils/scope.ts b/packages/spec-generators/src/nodeTypes/utils/scope.ts new file mode 100644 index 000000000..714c1d653 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/utils/scope.ts @@ -0,0 +1,98 @@ +import { camelCase, joinPath, pascalCase, type Path, pathDirectory, relativePath } from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { type RenderOptions, type ResolvedRenderOptions, resolveRenderOptions } from '../options'; + +/** + * A `:` symbolic module string used as the second + * argument to `use(...)` inside renderers. Flavours: + * + * - `node:`, `union:`, `enumeration:`, + * `nestedUnion:` — derived from the spec. + * - `brand:`, `docs:Docs`, `version:Version`, + * `version:CodamaVersion` — hand-written sibling files. + * - `registry:Node` / `registry:NodeKind` / `registry:GetNodeFromKind` — + * identifiers from the top-level `Node.ts` registry file. + */ +export type SymbolicModule = `${string}:${string}`; + +/** + * Runtime context threaded through every fragment renderer. Extends + * the resolved {@link RenderOptions} so individual fragments can + * declare — via `Pick` — exactly which knobs they + * consult. {@link symbolicModules} is the layout knowledge used by + * {@link getPageFragment} to resolve symbolic imports. + * + * A {@link Path} value is a slashless, extension-less location inside + * `generated/` (e.g. `'AccountNode'`, `'typeNodes/StructTypeNode'`). + * A leading `../` denotes a hand-written sibling above `generated/`. + */ +export interface RenderScope extends ResolvedRenderOptions { + readonly symbolicModules: ReadonlyMap; +} + +/** Hand-written branded-string types, living above `generated/`. */ +const BRAND_NAMES: readonly string[] = [ + 'CamelCaseString', + 'KebabCaseString', + 'PascalCaseString', + 'SnakeCaseString', + 'TitleCaseString', +]; + +export function buildRenderScope(spec: Spec, options: RenderOptions): RenderScope { + const resolved = resolveRenderOptions(options); + const symbolicModules = new Map(); + + for (const category of spec.categories) { + const folder = resolved.categoryDirectories.get(category.name); + if (folder === undefined) { + throw new Error( + `@codama/node-types generator: unknown category "${category.name}". Extend categoryDirectories.`, + ); + } + for (const node of category.nodes) { + symbolicModules.set(`node:${node.kind}`, joinPath(folder, pascalCase(node.kind))); + } + for (const union of category.unions) { + symbolicModules.set(`union:${union.name}`, joinPath(folder, pascalCase(union.name))); + } + for (const enumeration of category.enumerations) { + symbolicModules.set(`enumeration:${enumeration.name}`, joinPath(folder, camelCase(enumeration.name))); + } + for (const nestedUnion of category.nestedUnions) { + symbolicModules.set(`nestedUnion:${nestedUnion.name}`, joinPath(folder, pascalCase(nestedUnion.name))); + } + } + + for (const brand of BRAND_NAMES) { + symbolicModules.set(`brand:${brand}`, '../brands'); + } + symbolicModules.set('docs:Docs', '../Docs'); + symbolicModules.set('version:Version', '../Version'); + + const sharedDir = resolved.categoryDirectories.get('shared') ?? 'shared'; + symbolicModules.set('version:CodamaVersion', joinPath(sharedDir, 'codamaVersion')); + + symbolicModules.set('registry:Node', 'Node'); + symbolicModules.set('registry:NodeKind', 'Node'); + symbolicModules.set('registry:GetNodeFromKind', 'Node'); + + return Object.freeze({ + ...resolved, + symbolicModules: Object.freeze(symbolicModules), + }); +} + +/** + * Produce the relative import path from `currentPath` to `targetPath`. + * Both omit the file extension. A `targetPath` starting with `../` + * denotes a hand-written sibling above `generated/`. + */ +export function relativeImportPath(currentPath: Path, targetPath: Path): Path { + if (currentPath === targetPath) { + throw new Error(`@codama/node-types generator: refusing to produce a self-import for "${currentPath}".`); + } + const rel = relativePath(pathDirectory(currentPath), targetPath); + return rel.startsWith('.') ? rel : `./${rel}`; +} diff --git a/packages/spec-generators/src/nodeTypes/utils/selfReference.ts b/packages/spec-generators/src/nodeTypes/utils/selfReference.ts new file mode 100644 index 000000000..07f965b10 --- /dev/null +++ b/packages/spec-generators/src/nodeTypes/utils/selfReference.ts @@ -0,0 +1,19 @@ +import type { TypeExpr } from '@codama/spec'; + +/** + * Does `expr`'s type tree contain a direct `node` reference matching + * `selfKind`? `union` / `nestedUnion` references are not recursed into + * — they always go through a named alias that breaks the cycle. + */ +export function isTypeExprSelfReferential(expr: TypeExpr, selfKind: string): boolean { + switch (expr.kind) { + case 'node': + return expr.name === selfKind; + case 'array': + return isTypeExprSelfReferential(expr.of, selfKind); + case 'tuple': + return expr.items.some(item => isTypeExprSelfReferential(item, selfKind)); + default: + return false; + } +} diff --git a/packages/spec-generators/src/shared.ts b/packages/spec-generators/src/shared.ts new file mode 100644 index 000000000..133a98ee6 --- /dev/null +++ b/packages/spec-generators/src/shared.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url'; + +import { joinPath, pathDirectory } from '@codama/fragments'; + +/** + * Resolve the absolute path to the monorepo root, regardless of where + * this module is loaded from. The compiled bin lives at + * `/packages/spec-generators/dist/.mjs`; resolving three + * levels up lands in the workspace root. + */ +export function getRepoDirectory(): string { + const here = pathDirectory(fileURLToPath(import.meta.url)); + return joinPath(here, '..', '..', '..'); +} diff --git a/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts b/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts new file mode 100644 index 000000000..8ddf2865b --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts @@ -0,0 +1,77 @@ +import { attribute, boolean, enumeration, node, optionalAttribute, u32 } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getAttributeBodyLineFragment } from '../../../src/nodeTypes/fragments/attributeBodyLine'; +import type { RenderScope } from '../../../src/nodeTypes/utils/scope'; + +type LiftScope = Pick; + +function buildScope(overrides: Partial = {}): LiftScope { + return { narrowableDataAttributes: new Set(), ...overrides }; +} + +describe('getAttributeBodyLineFragment', () => { + it('renders a required data attribute as a concrete typed line', () => { + const result = getAttributeBodyLineFragment('someTypeNode', attribute('flag', boolean()), buildScope()); + expect(result.content).toBe('readonly flag: boolean;'); + }); + + it('renders an optional data attribute with `?:` and the same typed body', () => { + const result = getAttributeBodyLineFragment('someTypeNode', optionalAttribute('count', u32()), buildScope()); + expect(result.content).toBe('readonly count?: number;'); + }); + + it('emits a single-line JSDoc above the body line when docs are a single paragraph', () => { + const result = getAttributeBodyLineFragment( + 'someTypeNode', + attribute('flag', boolean(), { docs: ['A flag.'] }), + buildScope(), + ); + expect(result.content).toBe('/** A flag. */\nreadonly flag: boolean;'); + }); + + it('emits a multi-line JSDoc above the body line when docs span multiple paragraphs', () => { + const result = getAttributeBodyLineFragment( + 'someTypeNode', + attribute('flag', boolean(), { docs: ['First paragraph.', 'Second paragraph.'] }), + buildScope(), + ); + expect(result.content).toBe('/**\n * First paragraph.\n * Second paragraph.\n */\nreadonly flag: boolean;'); + }); + + it('uses the lifted generic identifier when the attribute is a child reference', () => { + const result = getAttributeBodyLineFragment( + 'someTypeNode', + attribute('payload', node('innerTypeNode')), + buildScope(), + ); + expect(result.content).toBe('readonly payload: TPayload;'); + }); + + it('uses the lifted generic identifier on optional child attributes too', () => { + const result = getAttributeBodyLineFragment( + 'someTypeNode', + optionalAttribute('payload', node('innerTypeNode')), + buildScope(), + ); + expect(result.content).toBe('readonly payload?: TPayload;'); + }); + + it('uses the lifted generic identifier for narrowable data attributes', () => { + const result = getAttributeBodyLineFragment( + 'numberTypeNode', + attribute('format', enumeration('NumberFormat')), + buildScope({ narrowableDataAttributes: new Set(['numberTypeNode:format']) }), + ); + expect(result.content).toBe('readonly format: TFormat;'); + }); + + it('does not lift a data attribute that is not in the narrowable set', () => { + const result = getAttributeBodyLineFragment( + 'numberTypeNode', + attribute('format', enumeration('NumberFormat')), + buildScope(), + ); + expect(result.content).toBe('readonly format: NumberFormat;'); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/codamaVersion.test.ts b/packages/spec-generators/test/nodeTypes/fragments/codamaVersion.test.ts new file mode 100644 index 000000000..0ba0b524e --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/codamaVersion.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { getCodamaVersionFragment } from '../../../src/nodeTypes/fragments/codamaVersion'; + +describe('getCodamaVersionFragment', () => { + it('emits a single-string literal type pinned to the supplied spec version', () => { + expect(getCodamaVersionFragment('1.0.0').content).toContain(`export type CodamaVersion = '1.0.0';`); + }); + + it('embeds the version verbatim, even when it is a rc tag', () => { + expect(getCodamaVersionFragment('1.0.0-rc.4').content).toContain(`export type CodamaVersion = '1.0.0-rc.4';`); + }); + + it('prepends a JSDoc explaining what the alias means', () => { + const out = getCodamaVersionFragment('1.0.0').content; + expect(out).toContain('The Codama spec version this package describes.'); + // Multi-paragraph docs use the block-form `/** … */` with each + // paragraph on its own ` * ` line. + expect(out.startsWith('/**\n')).toBe(true); + expect(out).toMatch(/\*\/\nexport type CodamaVersion =/); + }); + + it('produces a fragment with no imports', () => { + expect(getCodamaVersionFragment('1.0.0').imports.size).toBe(0); + }); + + it('ends the rendered content with the type alias terminator', () => { + // The fragment itself does not carry a trailing newline; that is + // added by `getPageFragment` when the fragment becomes a file. + expect(getCodamaVersionFragment('1.0.0').content.endsWith(';')).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/enumeration.test.ts b/packages/spec-generators/test/nodeTypes/fragments/enumeration.test.ts new file mode 100644 index 000000000..3cfaf9e27 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/enumeration.test.ts @@ -0,0 +1,75 @@ +import { defineEnumeration, variant } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getEnumerationFragment } from '../../../src/nodeTypes/fragments/enumeration'; + +describe('getEnumerationFragment', () => { + it('emits a string-literal union with sorted variants', () => { + const e = defineEnumeration('Endianness', { + variants: [variant('le'), variant('be')], + }); + const result = getEnumerationFragment(e); + expect(result.content).toBe(['export type Endianness =', '| "be"', '| "le";'].join('\n')); + }); + + it('attaches the enumeration-level docs as a JSDoc block when present', () => { + const e = defineEnumeration('E', { docs: ['My enum.'], variants: [variant('a')] }); + expect(getEnumerationFragment(e).content.startsWith('/** My enum. */\nexport type E =')).toBe(true); + }); + + it('emits a multi-paragraph JSDoc when enumeration docs span multiple paragraphs', () => { + const e = defineEnumeration('E', { + docs: ['First paragraph.', 'Second paragraph.'], + variants: [variant('a')], + }); + expect( + getEnumerationFragment(e).content.startsWith( + '/**\n * First paragraph.\n * Second paragraph.\n */\nexport type E =', + ), + ).toBe(true); + }); + + it('attaches per-variant docs in a single-line JSDoc above each variant', () => { + const e = defineEnumeration('E', { + variants: [variant('a', { docs: ['A.'] }), variant('b')], + }); + const out = getEnumerationFragment(e).content; + expect(out).toContain('/** A. */\n| "a"'); + expect(out).toContain('| "b"'); + }); + + it('uses a multi-line JSDoc when a variant doc has multiple paragraphs', () => { + const e = defineEnumeration('E', { + variants: [variant('a', { docs: ['First line.', 'Second line.'] })], + }); + const out = getEnumerationFragment(e).content; + expect(out).toContain('/**\n * First line.\n * Second line.\n */'); + }); + + it('produces a fragment with no imports', () => { + const e = defineEnumeration('E', { variants: [variant('a')] }); + expect(getEnumerationFragment(e).imports.size).toBe(0); + }); + + it('escapes special characters in variant names', () => { + const e = defineEnumeration('E', { + variants: [variant("o'brien"), variant('back\\slash'), variant('new\nline')], + }); + const out = getEnumerationFragment(e).content; + // Variant strings are JSON-quoted, so apostrophes pass through + // verbatim, backslashes and newlines are escaped. + expect(out).toContain('| "back\\\\slash"'); + expect(out).toContain('| "new\\nline"'); + expect(out).toContain('| "o\'brien"'); + }); + + it('defangs */ inside variant docs so the comment cannot terminate early', () => { + const e = defineEnumeration('E', { + variants: [variant('a', { docs: ['closes here */ then continues'] })], + }); + const out = getEnumerationFragment(e).content; + // The original `*/` must not appear; the defanged form does. + expect(out).not.toContain('here */'); + expect(out).toContain('here *\\/ then'); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts b/packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts new file mode 100644 index 000000000..62b42aabe --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { getIndexPageFragment } from '../../../src/nodeTypes/fragments/indexPage'; + +describe('getIndexPageFragment', () => { + it('renders a single `export *` line', () => { + // No trailing newline on the fragment itself; the EOL convention + // is owned by `getPageFragment` when the index becomes a file. + expect(getIndexPageFragment(['AccountNode']).content).toBe("export * from './AccountNode';"); + }); + + it('sorts the names alphabetically', () => { + const result = getIndexPageFragment(['zNode', 'aNode', 'mNode']); + expect(result.content).toBe( + ["export * from './aNode';", "export * from './mNode';", "export * from './zNode';"].join('\n'), + ); + }); + + it('produces a fragment with no symbolic imports (the export-all references resolve at file level)', () => { + const result = getIndexPageFragment(['AccountNode']); + expect(result.imports.size).toBe(0); + }); + + it('renders a fragment with no `export *` lines when given no names', () => { + const result = getIndexPageFragment([]); + expect(result.content).not.toContain('export *'); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/nestedUnion.test.ts b/packages/spec-generators/test/nodeTypes/fragments/nestedUnion.test.ts new file mode 100644 index 000000000..f39752589 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/nestedUnion.test.ts @@ -0,0 +1,88 @@ +import { defineNestedUnion, node, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getNestedUnionFragment } from '../../../src/nodeTypes/fragments/nestedUnion'; + +describe('getNestedUnionFragment', () => { + it('emits a recursive alias whose generic constraint is the base type', () => { + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + wrappers: ['fixedSizeTypeNode'], + }); + const out = getNestedUnionFragment(nu).content; + expect(out).toContain('export type NestedTypeNode ='); + }); + + it('emits one wrapper arm per wrapper kind, each parameterised over the alias', () => { + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + wrappers: ['fixedSizeTypeNode', 'sizePrefixTypeNode'], + }); + const out = getNestedUnionFragment(nu).content; + expect(out).toContain('| FixedSizeTypeNode>'); + expect(out).toContain('| SizePrefixTypeNode>'); + }); + + it('emits the base case `| TType;` as the final arm', () => { + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + wrappers: ['fixedSizeTypeNode'], + }); + expect(getNestedUnionFragment(nu).content.trimEnd().endsWith('| TType;')).toBe(true); + }); + + it('sorts wrapper arms alphabetically by kind for stable output', () => { + // Wrappers are passed in non-alphabetical order; the renderer + // should sort them so the rendered file is deterministic. + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + wrappers: ['sizePrefixTypeNode', 'fixedSizeTypeNode'], + }); + const out = getNestedUnionFragment(nu).content; + const fixedIdx = out.indexOf('| FixedSizeTypeNode<'); + const sizePrefixIdx = out.indexOf('| SizePrefixTypeNode<'); + expect(fixedIdx).toBeGreaterThan(-1); + expect(sizePrefixIdx).toBeGreaterThan(-1); + expect(fixedIdx).toBeLessThan(sizePrefixIdx); + }); + + it('emits a `node:` symbolic import per wrapper, plus the base type imports', () => { + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + wrappers: ['fixedSizeTypeNode'], + }); + const imports = [...getNestedUnionFragment(nu).imports.keys()].sort(); + expect(imports).toEqual(['node:fixedSizeTypeNode', 'union:TypeNode']); + }); + + it('renders a node-base correctly (no extra wrappers, alias still recursive)', () => { + const nu = defineNestedUnion('NestedFoo', { + base: node('innerNode'), + wrappers: [], + }); + const out = getNestedUnionFragment(nu).content; + expect(out).toContain('export type NestedFoo ='); + // No wrapper arms; the body collapses to just the base case. + expect(out.trimEnd().endsWith('| TType;')).toBe(true); + expect(out).not.toContain('| InnerNode<'); + }); + + it('emits a JSDoc above the alias when docs are present', () => { + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + docs: ['A recursive alias.'], + wrappers: ['fixedSizeTypeNode'], + }); + expect(getNestedUnionFragment(nu).content.startsWith('/** A recursive alias. */\n')).toBe(true); + }); + + it('ends the rendered content with the type alias terminator', () => { + const nu = defineNestedUnion('NestedTypeNode', { + base: union('TypeNode'), + wrappers: ['fixedSizeTypeNode'], + }); + // No trailing newline on the fragment itself; that is added by + // `getPageFragment` when the fragment becomes a file. + expect(getNestedUnionFragment(nu).content.endsWith(';')).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/node.test.ts b/packages/spec-generators/test/nodeTypes/fragments/node.test.ts new file mode 100644 index 000000000..3730ac788 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/node.test.ts @@ -0,0 +1,159 @@ +import { + array, + attribute, + defineNode, + enumeration, + node, + optionalAttribute, + stringIdentifier, + u32, + union, +} from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getNodeFragment } from '../../../src/nodeTypes/fragments/node'; +import type { RenderScope } from '../../../src/nodeTypes/utils/scope'; + +type NodeScope = Pick; + +function buildScope(overrides: Partial = {}): NodeScope { + return { + genericParamOrder: new Map(), + narrowableDataAttributes: new Set(), + ...overrides, + }; +} + +// Minimum spec exercising the full body shape: a wrapping node with one +// data attribute (`endian`, an enumeration), one optional data attribute +// (`count`), and one child attribute (`payload`, a union). +function buildWrappingNode() { + return defineNode('wrappingTypeNode', { + attributes: [ + attribute('payload', union('TypeNode'), { docs: ['A wrapped payload.'] }), + attribute('endian', enumeration('Endianness'), { docs: ['A byte order.'] }), + optionalAttribute('count', u32(), { docs: ['Optional count.'] }), + ], + docs: ['A node referencing the union and the enumeration.'], + }); +} + +function buildArgumentNode() { + return defineNode('instructionArgumentNode', { + attributes: [ + attribute('type', union('TypeNode')), + optionalAttribute('defaultValue', union('InstructionInputValueNode')), + ], + }); +} + +describe('getNodeFragment', () => { + it('emits an interface with kind discriminator, generics, and section markers', () => { + const result = getNodeFragment(buildWrappingNode(), buildScope()); + const c = result.content; + expect(c).toContain('export interface WrappingTypeNode<'); + expect(c).toContain("readonly kind: 'wrappingTypeNode';"); + expect(c).toContain('// Data.'); + expect(c).toContain('// Children.'); + // The `endian` enumeration is data; the `payload` union is a child. + expect(c).toContain('readonly endian: Endianness;'); + expect(c).toContain('readonly payload: TPayload;'); + // Optional u32 attribute should land in Data with `?: number;`. + expect(c).toContain('readonly count?: number;'); + }); + + it('lifts every child attribute to a generic param', () => { + const result = getNodeFragment(buildWrappingNode(), buildScope()); + expect(result.content).toContain('TPayload extends TypeNode = TypeNode'); + }); + + it('emits no Data or Children section when the corresponding group is empty', () => { + // A child-only node: just one child attribute, no data attributes. + const childOnly = defineNode('childOnlyNode', { + attributes: [attribute('child', node('innerTypeNode'))], + }); + const result = getNodeFragment(childOnly, buildScope()); + expect(result.content).not.toContain('// Data.'); + expect(result.content).toContain('// Children.'); + }); + + it('inserts a SelfXxxNode alias for self-referential nodes', () => { + const recursive = defineNode('recursiveTypeNode', { + attributes: [ + attribute('name', stringIdentifier()), + optionalAttribute('children', array(node('recursiveTypeNode'))), + ], + }); + const result = getNodeFragment(recursive, buildScope()); + expect(result.content).toContain('type SelfRecursiveTypeNode = RecursiveTypeNode;'); + expect(result.content).toContain('TChildren extends Array'); + }); + + it('emits a JSDoc above the interface declaration when the node has docs', () => { + const result = getNodeFragment(buildWrappingNode(), buildScope()); + expect(result.content).toMatch( + /\/\*\* A node referencing the union and the enumeration\. \*\/\nexport interface WrappingTypeNode/, + ); + }); + + it('emits a multi-paragraph JSDoc when node docs span multiple paragraphs', () => { + const multiParagraph = defineNode('multiParagraphNode', { + attributes: [], + docs: ['First paragraph.', 'Second paragraph.'], + }); + const result = getNodeFragment(multiParagraph, buildScope()); + expect(result.content).toMatch( + /\/\*\*\n \* First paragraph\.\n \* Second paragraph\.\n \*\/\nexport interface/, + ); + }); + + it('emits a JSDoc above each attribute that has docs', () => { + const result = getNodeFragment(buildWrappingNode(), buildScope()); + // Data attribute. + expect(result.content).toContain('/** A byte order. */\nreadonly endian: Endianness;'); + // Optional data attribute. + expect(result.content).toContain('/** Optional count. */\nreadonly count?: number;'); + // Child attribute (generic-lifted). + expect(result.content).toContain('/** A wrapped payload. */\nreadonly payload: TPayload;'); + }); + + it('emits lifted generics in declaration order when no override is supplied', () => { + const result = getNodeFragment(buildArgumentNode(), buildScope()); + const tTypeIdx = result.content.indexOf('TType extends'); + const tDefaultIdx = result.content.indexOf('TDefaultValue extends'); + expect(tTypeIdx).toBeGreaterThan(-1); + expect(tDefaultIdx).toBeGreaterThan(-1); + // Declaration order on the node puts `type` first. + expect(tTypeIdx).toBeLessThan(tDefaultIdx); + }); + + it('reorders lifted generics according to genericParamOrder', () => { + const result = getNodeFragment( + buildArgumentNode(), + buildScope({ genericParamOrder: new Map([['instructionArgumentNode', ['defaultValue', 'type']]]) }), + ); + // The override pins `defaultValue` first, even though `type` + // appears first in the spec. + const tDefaultIdx = result.content.indexOf('TDefaultValue extends'); + const tTypeIdx = result.content.indexOf('TType extends'); + expect(tDefaultIdx).toBeGreaterThan(-1); + expect(tTypeIdx).toBeGreaterThan(-1); + expect(tDefaultIdx).toBeLessThan(tTypeIdx); + }); + + it('throws when genericParamOrder lists an attribute the node does not lift', () => { + // The override expects both `type` and `defaultValue` to be + // lifted. We omit `type` from the spec entirely, so the + // renderer's lifted set is `{ defaultValue }` while the override + // expects `{ defaultValue, type }`. + const incomplete = defineNode('instructionArgumentNode', { + attributes: [optionalAttribute('defaultValue', union('InstructionInputValueNode'))], + }); + expect(() => + getNodeFragment( + incomplete, + buildScope({ genericParamOrder: new Map([['instructionArgumentNode', ['defaultValue', 'type']]]) }), + ), + ).toThrow(/genericParamOrder for "instructionArgumentNode" is out of sync/); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts b/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts new file mode 100644 index 000000000..2deb823fb --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts @@ -0,0 +1,151 @@ +import type { Spec } from '@codama/spec'; +import { defineCategory, defineNode, defineUnion } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getNodeRegistryFragment } from '../../../src/nodeTypes/fragments/nodeRegistry'; + +// The renderer expects every entry in REGISTERED_CATEGORY_UNIONS to +// resolve. This helper plumbs a minimum but complete spec: one stub +// node per registered category, each registered union referencing that +// node, plus extra top-level nodes the caller can supply. +function buildSpecWithAllRegisteredUnions(extraTopLevelNodes: readonly string[] = []): Spec { + const stubKinds = [ + 'someContextualValueNode', + 'someCountNode', + 'someDiscriminatorNode', + 'someLinkNode', + 'somePdaSeedNode', + 'someTypeNode', + 'someValueNode', + ]; + return { + categories: [ + defineCategory('topLevel', { + nodes: [...stubKinds, ...extraTopLevelNodes].map(k => defineNode(k, { attributes: [] })), + unions: [ + defineUnion('RegisteredContextualValueNode', { members: ['someContextualValueNode'] }), + defineUnion('RegisteredCountNode', { members: ['someCountNode'] }), + defineUnion('RegisteredDiscriminatorNode', { members: ['someDiscriminatorNode'] }), + defineUnion('RegisteredLinkNode', { members: ['someLinkNode'] }), + defineUnion('RegisteredPdaSeedNode', { members: ['somePdaSeedNode'] }), + defineUnion('RegisteredTypeNode', { members: ['someTypeNode'] }), + defineUnion('RegisteredValueNode', { members: ['someValueNode'] }), + ], + }), + ], + version: '1.0.0', + }; +} + +describe('getNodeRegistryFragment', () => { + it('emits the Node Registration and Node Helpers section headers', () => { + const out = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions()).content; + expect(out).toContain('// Node Registration.'); + expect(out).toContain('// Node Helpers.'); + }); + + it("emits the NodeKind discriminant alias as Node['kind']", () => { + const out = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions()).content; + expect(out).toContain("export type NodeKind = Node['kind'];"); + }); + + it('emits the GetNodeFromKind helper as an Extract<…> over the kind discriminant', () => { + const out = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions()).content; + expect(out).toContain('export type GetNodeFromKind = Extract;'); + }); + + it('lists every RegisteredXxxNode union as a Node member', () => { + const result = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions()); + const imports = [...result.imports.keys()].sort(); + expect(imports).toContain('union:RegisteredContextualValueNode'); + expect(imports).toContain('union:RegisteredCountNode'); + expect(imports).toContain('union:RegisteredDiscriminatorNode'); + expect(imports).toContain('union:RegisteredLinkNode'); + expect(imports).toContain('union:RegisteredPdaSeedNode'); + expect(imports).toContain('union:RegisteredTypeNode'); + expect(imports).toContain('union:RegisteredValueNode'); + }); + + it('lists non-registered nodes as direct Node members', () => { + // `accountNode` and `rootNode` aren't inside any RegisteredXxxNode + // union, so they should appear as direct members of `Node`. + const result = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions(['accountNode', 'rootNode'])); + const imports = [...result.imports.keys()]; + expect(imports).toContain('node:accountNode'); + expect(imports).toContain('node:rootNode'); + }); + + it('omits nodes that are reachable through a RegisteredXxxNode union from direct membership', () => { + // `someTypeNode` is covered by `RegisteredTypeNode`; it must not + // appear as a direct member of `Node`. + const result = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions()); + const imports = [...result.imports.keys()]; + expect(imports).not.toContain('node:someTypeNode'); + }); + + it('walks nested unions and excludes any node reachable through them from direct membership', () => { + // Nest a sub-union inside `RegisteredTypeNode`. The sub-union's + // members must still be considered "covered" and excluded from + // direct `Node` membership. + const spec: Spec = { + categories: [ + defineCategory('topLevel', { + nodes: [ + defineNode('someContextualValueNode', { attributes: [] }), + defineNode('someCountNode', { attributes: [] }), + defineNode('someDiscriminatorNode', { attributes: [] }), + defineNode('someLinkNode', { attributes: [] }), + defineNode('somePdaSeedNode', { attributes: [] }), + defineNode('someValueNode', { attributes: [] }), + defineNode('deeplyNestedTypeNode', { attributes: [] }), + ], + unions: [ + defineUnion('RegisteredContextualValueNode', { members: ['someContextualValueNode'] }), + defineUnion('RegisteredCountNode', { members: ['someCountNode'] }), + defineUnion('RegisteredDiscriminatorNode', { members: ['someDiscriminatorNode'] }), + defineUnion('RegisteredLinkNode', { members: ['someLinkNode'] }), + defineUnion('RegisteredPdaSeedNode', { members: ['somePdaSeedNode'] }), + defineUnion('RegisteredValueNode', { members: ['someValueNode'] }), + defineUnion('InnerTypeNode', { members: ['deeplyNestedTypeNode'] }), + defineUnion('RegisteredTypeNode', { + members: [{ kind: 'union', name: 'InnerTypeNode' }], + }), + ], + }), + ], + version: '1.0.0', + }; + const result = getNodeRegistryFragment(spec); + const imports = [...result.imports.keys()]; + // The deeply-nested node is reachable through Inner → Registered…, + // so it must not appear as a direct Node member. + expect(imports).not.toContain('node:deeplyNestedTypeNode'); + }); + + it('throws when a RegisteredXxxNode union expected by the registry is absent from the spec', () => { + // Drop `RegisteredTypeNode` from the spec; the renderer should + // fail loudly rather than silently produce an incomplete Node. + const spec: Spec = { + categories: [ + defineCategory('topLevel', { + nodes: [defineNode('someContextualValueNode', { attributes: [] })], + unions: [defineUnion('RegisteredContextualValueNode', { members: ['someContextualValueNode'] })], + }), + ], + version: '1.0.0', + }; + expect(() => getNodeRegistryFragment(spec)).toThrow(/missing union "RegisteredCountNode"/); + }); + + it('sorts the Node members alphabetically for stable output', () => { + const out = getNodeRegistryFragment(buildSpecWithAllRegisteredUnions(['zzNode', 'aaNode'])).content; + const aaIdx = out.indexOf('| AaNode'); + const zzIdx = out.indexOf('| ZzNode'); + const regContextualIdx = out.indexOf('| RegisteredContextualValueNode'); + expect(aaIdx).toBeGreaterThan(-1); + expect(zzIdx).toBeGreaterThan(-1); + expect(regContextualIdx).toBeGreaterThan(-1); + expect(aaIdx).toBeLessThan(regContextualIdx); + expect(regContextualIdx).toBeLessThan(zzIdx); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/page.test.ts b/packages/spec-generators/test/nodeTypes/fragments/page.test.ts new file mode 100644 index 000000000..2fa423ca4 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/page.test.ts @@ -0,0 +1,127 @@ +import { fragment, use } from '@codama/fragments/javascript'; +import { describe, expect, it } from 'vitest'; + +import { getPageFragment } from '../../../src/nodeTypes/fragments/page'; +import type { RenderScope, SymbolicModule } from '../../../src/nodeTypes/utils/scope'; + +function buildScope(entries: Record): Pick { + return { symbolicModules: new Map(Object.entries(entries) as [SymbolicModule, string][]) }; +} + +describe('getPageFragment', () => { + // Import-map resolution behaviour. + + it('returns the input fragment content unchanged when its imports map is empty', () => { + const body = fragment`export type Foo = string;`; + const result = getPageFragment(body, buildScope({}), 'Foo'); + expect(result.imports.size).toBe(0); + expect(result.content).toBe('export type Foo = string;\n'); + }); + + it('rewrites a symbolic-keyed import to a relative path against currentLocation', () => { + // Current file lives at `typeNodes/StructTypeNode`; the referenced + // node lives at `typeNodes/NumberTypeNode` — a sibling. + const body = fragment`export type X = ${use('type NumberTypeNode', 'node:numberTypeNode')};`; + const scope = buildScope({ 'node:numberTypeNode': 'typeNodes/NumberTypeNode' }); + const result = getPageFragment(body, scope, 'typeNodes/StructTypeNode'); + expect([...result.imports.keys()]).toEqual(['./NumberTypeNode']); + }); + + it('reaches across directories via the computed relative path', () => { + const body = fragment`export type X = ${use('type Endianness', 'enumeration:Endianness')};`; + const scope = buildScope({ 'enumeration:Endianness': 'shared/endianness' }); + const result = getPageFragment(body, scope, 'typeNodes/Foo'); + expect([...result.imports.keys()]).toEqual(['../shared/endianness']); + }); + + it('resolves hand-written sibling locations (../X) to ../../X from a subdirectory file', () => { + // The `../X` location form denotes a hand-written sibling above + // `generated/`. From `typeNodes/Foo`, that resolves to `../../X`. + const body = fragment`export type X = ${use('type Docs', 'docs:Docs')};`; + const scope = buildScope({ 'docs:Docs': '../Docs' }); + const result = getPageFragment(body, scope, 'typeNodes/Foo'); + expect([...result.imports.keys()]).toEqual(['../../Docs']); + }); + + it('resolves hand-written sibling locations to ../X from a top-level file', () => { + const body = fragment`export type X = ${use('type Docs', 'docs:Docs')};`; + const scope = buildScope({ 'docs:Docs': '../Docs' }); + const result = getPageFragment(body, scope, 'Foo'); + expect([...result.imports.keys()]).toEqual(['../Docs']); + }); + + it('drops a symbolic import whose target equals the current file', () => { + // The renderer can legitimately emit a `use(...)` whose key + // resolves to the file it is currently emitting (e.g., when a + // node references its own kind). The resolver must omit such + // entries — TypeScript rejects self-imports. + const body = fragment`export type Self = ${use('type SelfNode', 'node:selfNode')};`; + const scope = buildScope({ 'node:selfNode': 'SelfNode' }); + const result = getPageFragment(body, scope, 'SelfNode'); + expect(result.imports.size).toBe(0); + }); + + it('drops only the self-import while keeping other imports', () => { + const body = fragment`export type X = ${use('type SelfNode', 'node:selfNode')} | ${use('type OtherNode', 'node:otherNode')};`; + const scope = buildScope({ + 'node:otherNode': 'OtherNode', + 'node:selfNode': 'SelfNode', + }); + const result = getPageFragment(body, scope, 'SelfNode'); + expect([...result.imports.keys()]).toEqual(['./OtherNode']); + }); + + it('merges two distinct symbolic keys that resolve to the same file into one entry', () => { + // Brands all live in the same hand-written `brands.ts` file but + // each is keyed independently in the symbol map. After + // resolution, both relative paths point at `../brands`, and the + // resolver should consolidate them into a single map entry + // carrying both identifiers. + const body = fragment`export type X = ${use('type CamelCaseString', 'brand:CamelCaseString')} & ${use('type KebabCaseString', 'brand:KebabCaseString')};`; + const scope = buildScope({ + 'brand:CamelCaseString': '../brands', + 'brand:KebabCaseString': '../brands', + }); + const result = getPageFragment(body, scope, 'Foo'); + expect([...result.imports.keys()]).toEqual(['../brands']); + const brandsEntry = result.imports.get('../brands')!; + expect([...brandsEntry.keys()].sort()).toEqual(['CamelCaseString', 'KebabCaseString']); + }); + + it('throws with the current file in the message when a symbolic key is unknown', () => { + const body = fragment`export type X = ${use('type Unknown', 'node:doesNotExist')};`; + const scope = buildScope({}); + expect(() => getPageFragment(body, scope, 'Foo')).toThrow( + /unknown symbolic module "node:doesNotExist".*from "Foo"/, + ); + }); + + // Import-block stringification and trailing-whitespace normalisation. + + it('emits just the body content (no import block) when imports are empty', () => { + const body = fragment`export type Foo = string;`; + expect(getPageFragment(body, buildScope({}), 'Foo').content).toBe('export type Foo = string;\n'); + }); + + it('trims trailing whitespace and ensures exactly one trailing newline', () => { + const body = fragment`export type Foo = string;\n\n\n`; + expect(getPageFragment(body, buildScope({}), 'Foo').content).toBe('export type Foo = string;\n'); + }); + + it('prepends an import block for a symbolic import resolved to a relative path', () => { + const body = fragment`export type X = ${use('type NumberTypeNode', 'node:numberTypeNode')};`; + const scope = buildScope({ 'node:numberTypeNode': 'typeNodes/NumberTypeNode' }); + const result = getPageFragment(body, scope, 'typeNodes/StructTypeNode'); + expect(result.content).toContain(`import type { NumberTypeNode } from './NumberTypeNode';`); + expect(result.content).toContain('export type X = NumberTypeNode;'); + }); + + it('preserves the resolved imports map on the rendered fragment', () => { + const body = fragment`export type X = ${use('type NumberTypeNode', 'node:numberTypeNode')};`; + const scope = buildScope({ 'node:numberTypeNode': 'typeNodes/NumberTypeNode' }); + const result = getPageFragment(body, scope, 'typeNodes/StructTypeNode'); + // The imports map carries the resolved relative-path entries + // even after the import block has been baked into `content`. + expect([...result.imports.keys()]).toEqual(['./NumberTypeNode']); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts b/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts new file mode 100644 index 000000000..8a6bc0b38 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/typeExpr.test.ts @@ -0,0 +1,161 @@ +import { + array, + boolean, + codamaVersion, + docs, + enumeration, + literal, + literalUnion, + nestedUnion, + node, + string, + stringIdentifier, + stringVersion, + u32, + union, +} from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getTypeExprFragment, getTypeExprWithSelfAliasFragment } from '../../../src/nodeTypes/fragments/typeExpr'; + +describe('getTypeExprFragment', () => { + it('renders plain string', () => { + expect(getTypeExprFragment(string()).content).toBe('string'); + }); + + it('renders integer as number', () => { + expect(getTypeExprFragment(u32()).content).toBe('number'); + }); + + it('renders boolean', () => { + expect(getTypeExprFragment(boolean()).content).toBe('boolean'); + }); + + it('renders string literal', () => { + expect(getTypeExprFragment(literal('codama')).content).toBe('"codama"'); + }); + + it('renders boolean literal', () => { + expect(getTypeExprFragment(literal(true)).content).toBe('true'); + }); + + it('joins literalUnion members with " | "', () => { + expect(getTypeExprFragment(literalUnion(1, 2, 'three')).content).toBe('1 | 2 | "three"'); + }); + + it('collapses `true | false` to `boolean` and appends remaining literals', () => { + expect(getTypeExprFragment(literalUnion(true, false, 'either')).content).toBe('boolean | "either"'); + }); + + it('collapses `true | false` alone to `boolean`', () => { + expect(getTypeExprFragment(literalUnion(true, false)).content).toBe('boolean'); + }); + + it('does not collapse a single boolean literal', () => { + expect(getTypeExprFragment(literalUnion(true, 'either')).content).toBe('true | "either"'); + }); + + it('emits CamelCaseString for stringIdentifier with a brand: import key', () => { + const result = getTypeExprFragment(stringIdentifier()); + expect(result.content).toBe('CamelCaseString'); + expect([...result.imports.keys()]).toEqual(['brand:CamelCaseString']); + }); + + it('emits Version for stringVersion with a version: import key', () => { + const result = getTypeExprFragment(stringVersion()); + expect(result.content).toBe('Version'); + expect([...result.imports.keys()]).toEqual(['version:Version']); + }); + + it('emits CodamaVersion for codamaVersion with a version: import key', () => { + const result = getTypeExprFragment(codamaVersion()); + expect(result.content).toBe('CodamaVersion'); + expect([...result.imports.keys()]).toEqual(['version:CodamaVersion']); + }); + + it('emits a Docs reference imported from docs:Docs', () => { + const result = getTypeExprFragment(docs()); + expect(result.content).toBe('Docs'); + expect([...result.imports.keys()]).toEqual(['docs:Docs']); + }); + + it('renders enumeration references with an enumeration: import key', () => { + const result = getTypeExprFragment(enumeration('Endianness')); + expect(result.content).toBe('Endianness'); + expect([...result.imports.keys()]).toEqual(['enumeration:Endianness']); + }); + + it('renders node references with PascalCase identifiers and a node: import key', () => { + const result = getTypeExprFragment(node('innerTypeNode')); + expect(result.content).toBe('InnerTypeNode'); + expect([...result.imports.keys()]).toEqual(['node:innerTypeNode']); + }); + + it('renders union references with a union: import key', () => { + const result = getTypeExprFragment(union('TypeNode')); + expect(result.content).toBe('TypeNode'); + expect([...result.imports.keys()]).toEqual(['union:TypeNode']); + }); + + it('renders array(T) as Array', () => { + expect(getTypeExprFragment(array(boolean())).content).toBe('Array'); + }); + + it('handles nested array types', () => { + expect(getTypeExprFragment(array(array(string()))).content).toBe('Array>'); + }); + + it('does not need extra parens around an inline literal-union array element', () => { + // The `Array<…>` wrapping makes precedence unambiguous, so a + // literal-union element is emitted without extra parens. + expect(getTypeExprFragment(array(literalUnion(true, 'either'))).content).toBe('Array'); + }); + + it('renders nestedUnion wrapping with both nestedUnion: and node: import keys', () => { + const result = getTypeExprFragment(nestedUnion('NestedTypeNode', 'innerTypeNode')); + expect(result.content).toBe('NestedTypeNode'); + expect([...result.imports.keys()].sort()).toEqual(['nestedUnion:NestedTypeNode', 'node:innerTypeNode']); + }); + + it('emits a valid TS string literal even when the value contains characters that need escaping', () => { + // Escaping is delegated to `JSON.stringify`. Confirm here that + // the renderer produces a double-quoted source-form literal + // whose backslashes and newlines have been escaped — i.e. the + // value cannot break out of the string. + const out = getTypeExprFragment(literal('a\\b\nc')).content; + expect(out.startsWith('"') && out.endsWith('"')).toBe(true); + expect(out).not.toContain('\n'); + }); +}); + +describe('getTypeExprWithSelfAliasFragment', () => { + it('substitutes the self-alias for direct node references matching the kind', () => { + const result = getTypeExprWithSelfAliasFragment(node('fooNode'), 'fooNode', 'SelfFooNode'); + expect(result.content).toBe('SelfFooNode'); + // The substitution emits a raw identifier — no symbolic import. + expect(result.imports.size).toBe(0); + }); + + it('leaves non-matching node references untouched, including their symbolic import', () => { + const result = getTypeExprWithSelfAliasFragment(node('otherNode'), 'fooNode', 'SelfFooNode'); + expect(result.content).toBe('OtherNode'); + expect([...result.imports.keys()]).toEqual(['node:otherNode']); + }); + + it('recurses the substitution through array(node(kind))', () => { + const result = getTypeExprWithSelfAliasFragment(array(node('fooNode')), 'fooNode', 'SelfFooNode'); + expect(result.content).toBe('Array'); + }); + + it('does not recurse into union references — they are name-aliased and break the cycle', () => { + const result = getTypeExprWithSelfAliasFragment(union('FooUnion'), 'fooNode', 'SelfFooNode'); + expect(result.content).toBe('FooUnion'); + expect([...result.imports.keys()]).toEqual(['union:FooUnion']); + }); + + it('delegates to getTypeExprFragment for primitive types', () => { + // Primitives have no node references, so the substitution is a + // no-op and the result matches the regular renderer. + expect(getTypeExprWithSelfAliasFragment(boolean(), 'fooNode', 'SelfFooNode').content).toBe('boolean'); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/typeParameterDefinition.test.ts b/packages/spec-generators/test/nodeTypes/fragments/typeParameterDefinition.test.ts new file mode 100644 index 000000000..dbf73f819 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/typeParameterDefinition.test.ts @@ -0,0 +1,45 @@ +import { array, attribute, enumeration, node, optionalAttribute } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getTypeParameterDefinitionFragment } from '../../../src/nodeTypes/fragments/typeParameterDefinition'; + +describe('getTypeParameterDefinitionFragment', () => { + it('renders a type-parameter definition for a child attribute, constraint = default = type', () => { + const result = getTypeParameterDefinitionFragment(attribute('payload', node('innerTypeNode'))); + expect(result.content).toBe('TPayload extends InnerTypeNode = InnerTypeNode'); + }); + + it('extends an optional child constraint with ` | undefined` on both sides', () => { + const result = getTypeParameterDefinitionFragment(optionalAttribute('payload', node('innerTypeNode'))); + expect(result.content).toBe('TPayload extends InnerTypeNode | undefined = InnerTypeNode | undefined'); + }); + + it('renders an array-of-node child as an Array type parameter', () => { + const result = getTypeParameterDefinitionFragment(attribute('items', array(node('innerTypeNode')))); + expect(result.content).toBe('TItems extends Array = Array'); + }); + + it('renders a narrowable data attribute as a type parameter over its enumeration constraint', () => { + const result = getTypeParameterDefinitionFragment(attribute('format', enumeration('NumberFormat'))); + expect(result.content).toBe('TFormat extends NumberFormat = NumberFormat'); + }); + + it('substitutes the self-alias in a child constraint when configured', () => { + const result = getTypeParameterDefinitionFragment( + optionalAttribute('children', array(node('recursiveTypeNode'))), + { selfAlias: { alias: 'SelfRecursiveTypeNode', kind: 'recursiveTypeNode' } }, + ); + expect(result.content).toBe( + 'TChildren extends Array | undefined = Array | undefined', + ); + }); + + it('does not substitute when the attribute does not reference the self kind', () => { + // The attribute references a different kind; the selfAlias + // context should be a no-op. + const result = getTypeParameterDefinitionFragment(attribute('payload', node('innerTypeNode')), { + selfAlias: { alias: 'SelfRecursiveTypeNode', kind: 'recursiveTypeNode' }, + }); + expect(result.content).toBe('TPayload extends InnerTypeNode = InnerTypeNode'); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/typeParameterIdentifier.test.ts b/packages/spec-generators/test/nodeTypes/fragments/typeParameterIdentifier.test.ts new file mode 100644 index 000000000..dd62146d1 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/typeParameterIdentifier.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { getTypeParameterIdentifierFragment } from '../../../src/nodeTypes/fragments/typeParameterIdentifier'; + +describe('getTypeParameterIdentifierFragment', () => { + it('prefixes the attribute name with `T` and PascalCases it', () => { + expect(getTypeParameterIdentifierFragment('data').content).toBe('TData'); + expect(getTypeParameterIdentifierFragment('pda').content).toBe('TPda'); + expect(getTypeParameterIdentifierFragment('subInstructions').content).toBe('TSubInstructions'); + }); + + it('produces a fragment with no imports', () => { + expect(getTypeParameterIdentifierFragment('data').imports.size).toBe(0); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/union.test.ts b/packages/spec-generators/test/nodeTypes/fragments/union.test.ts new file mode 100644 index 000000000..03eb6835c --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/fragments/union.test.ts @@ -0,0 +1,33 @@ +import { defineUnion, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getUnionFragment } from '../../../src/nodeTypes/fragments/union'; + +describe('getUnionFragment', () => { + it('renders a union of node references with sorted members', () => { + const u = defineUnion('TypeNode', { members: ['innerTypeNode'] }); + const result = getUnionFragment(u); + expect(result.content).toContain('export type TypeNode =\n| InnerTypeNode;'); + expect([...result.imports.keys()]).toEqual(['node:innerTypeNode']); + }); + + it('preserves nested union references and emits symbolic-keyed imports', () => { + const outer = defineUnion('Outer', { members: [union('Inner'), 'aNode'] }); + const result = getUnionFragment(outer); + // Members render in PascalCase order: ANode, Inner. + expect(result.content).toMatch(/export type Outer =\n\| ANode\n\| Inner;$/); + expect([...result.imports.keys()].sort()).toEqual(['node:aNode', 'union:Inner']); + }); + + it('emits the union-level docs as a single-line JSDoc when the docs array has one entry', () => { + const u = defineUnion('U', { docs: ['My union.'], members: ['aNode'] }); + const result = getUnionFragment(u); + expect(result.content.startsWith('/** My union. */\n')).toBe(true); + }); + + it('emits a multi-line JSDoc when the docs array has multiple paragraphs', () => { + const u = defineUnion('U', { docs: ['First paragraph.', 'Second paragraph.'], members: ['aNode'] }); + const result = getUnionFragment(u); + expect(result.content.startsWith('/**\n * First paragraph.\n * Second paragraph.\n */\n')).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/generate.test.ts b/packages/spec-generators/test/nodeTypes/generate.test.ts new file mode 100644 index 000000000..439af9be2 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/generate.test.ts @@ -0,0 +1,154 @@ +import { getFromRenderMap } from '@codama/fragments'; +import { getSpec, type Spec } from '@codama/spec'; +import { + attribute, + defineCategory, + defineEnumeration, + defineNode, + defineUnion, + enumeration, + union, + variant, +} from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { type GenerateOptions, getRenderMap, validateRenderOptions } from '../../src/nodeTypes'; + +// Minimum spec exercising both `narrowableDataAttributes` and +// `genericParamOrder` validation: one node with one data attribute +// (`endian`) and one child attribute (`payload`). +const spec: Spec = { + categories: [ + defineCategory('topLevel', { + enumerations: [defineEnumeration('Endianness', { variants: [variant('be'), variant('le')] })], + nodes: [ + defineNode('wrappingTypeNode', { + attributes: [ + attribute('payload', union('TypeNode')), + attribute('endian', enumeration('Endianness')), + ], + }), + ], + unions: [defineUnion('TypeNode', { members: ['wrappingTypeNode'] })], + }), + ], + version: '1.0.0', +}; + +function options(overrides: Partial = {}): GenerateOptions { + return { + outputDir: '/tmp/unused', + targetSpecMajor: 1, + ...overrides, + }; +} + +describe('validateRenderOptions', () => { + it('accepts valid options with no overrides', () => { + expect(() => validateRenderOptions(spec, options())).not.toThrow(); + }); + + it('accepts a narrowableDataAttributes set whose keys all resolve in the spec', () => { + expect(() => + validateRenderOptions(spec, options({ narrowableDataAttributes: new Set(['wrappingTypeNode:endian']) })), + ).not.toThrow(); + }); + + it('accepts a genericParamOrder map whose keys and attributes all resolve', () => { + expect(() => + validateRenderOptions(spec, options({ genericParamOrder: new Map([['wrappingTypeNode', ['payload']]]) })), + ).not.toThrow(); + }); + + it('throws when targetSpecMajor does not match the spec version', () => { + expect(() => validateRenderOptions(spec, options({ targetSpecMajor: 2 }))).toThrow( + /targetSpecMajor=2.*version "1\.0\.0".*major 1/, + ); + }); + + it('throws on a narrowableDataAttributes key that does not resolve in the spec', () => { + expect(() => + validateRenderOptions(spec, options({ narrowableDataAttributes: new Set(['ghostTypeNode:format']) })), + ).toThrow(/narrowableDataAttributes references "ghostTypeNode:format"/); + }); + + it('throws on a genericParamOrder key that names an unknown node kind', () => { + expect(() => + validateRenderOptions(spec, options({ genericParamOrder: new Map([['ghostTypeNode', ['x']]]) })), + ).toThrow(/genericParamOrder references unknown node kind "ghostTypeNode"/); + }); + + it('throws when genericParamOrder references an attribute the node does not declare', () => { + expect(() => + validateRenderOptions(spec, options({ genericParamOrder: new Map([['wrappingTypeNode', ['ghost']]]) })), + ).toThrow(/genericParamOrder for "wrappingTypeNode" references attribute "ghost"/); + }); + + it('throws on a malformed spec version', () => { + const broken = { ...spec, version: 'not-a-version' }; + expect(() => validateRenderOptions(broken, options())).toThrow(/unable to parse spec version "not-a-version"/); + }); + + it('accepts a categoryDirectories map covering every spec category', () => { + expect(() => + validateRenderOptions(spec, options({ categoryDirectories: new Map([['topLevel', 'roots']]) })), + ).not.toThrow(); + }); + + it('throws when categoryDirectories is supplied but misses a spec category', () => { + // The spec only has the `topLevel` category; an empty override + // omits it and so should fail. + expect(() => validateRenderOptions(spec, options({ categoryDirectories: new Map() }))).toThrow( + /categoryDirectories is missing an entry for spec category "topLevel"/, + ); + }); +}); + +describe('getRenderMap', () => { + const map = getRenderMap(getSpec(), { targetSpecMajor: 1 }); + + it('produces an entry per generated file, keyed by .ts-suffixed path', () => { + expect(map.has('AccountNode.ts')).toBe(true); + expect(map.has('typeNodes/StructTypeNode.ts')).toBe(true); + expect(map.has('shared/codamaVersion.ts')).toBe(true); + expect(map.has('Node.ts')).toBe(true); + }); + + it('keys every entry with a .ts suffix', () => { + for (const key of map.keys()) { + expect(key).toMatch(/\.ts$/); + } + }); + + it('preserves the Fragment imports map keyed by resolved relative paths', () => { + // `AccountNode` references the hand-written `Docs` and brands + // siblings, plus a few subdir nodes. After resolution every key + // is a relative path (no symbolic `node:` / `docs:` keys left). + const entry = getFromRenderMap(map, 'AccountNode.ts'); + for (const key of entry.imports.keys()) { + expect(key).not.toMatch(/^[a-z]+:/); + } + expect([...entry.imports.keys()]).toContain('../Docs'); + expect([...entry.imports.keys()]).toContain('../brands'); + expect([...entry.imports.keys()]).toContain('./typeNodes/NestedTypeNode'); + }); + + it('emits the rendered interface body as the Fragment content', () => { + const entry = getFromRenderMap(map, 'AccountNode.ts'); + expect(entry.content).toContain('export interface AccountNode'); + }); + + it('bakes the resolved import block into the Fragment content', () => { + // `getRenderMap` now applies `renderPage` per entry, so the + // import statements appear in the rendered content alongside + // the interface body. The `imports` map carries the same + // information as metadata. + const entry = getFromRenderMap(map, 'AccountNode.ts'); + expect(entry.content).toContain(`import type { Docs } from '../Docs';`); + expect(entry.content).toContain(`import type { CamelCaseString } from '../brands';`); + }); + + it('emits a frozen render map', () => { + expect(Object.isFrozen(map)).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/options.test.ts b/packages/spec-generators/test/nodeTypes/options.test.ts new file mode 100644 index 000000000..e62e96c20 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/options.test.ts @@ -0,0 +1,50 @@ +import { array, attribute, boolean, enumeration, node, optionalAttribute, u32, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { isAttributeLifted, type ResolvedRenderOptions } from '../../src/nodeTypes/options'; + +type LiftOptions = Pick; + +function buildOptions(overrides: Partial = {}): LiftOptions { + return { narrowableDataAttributes: new Set(), ...overrides }; +} + +describe('isAttributeLifted', () => { + it('lifts a child attribute whose type is a direct node reference', () => { + expect(isAttributeLifted('aNode', attribute('payload', node('innerNode')), buildOptions())).toBe(true); + }); + + it('lifts a child attribute whose type is a union reference', () => { + expect(isAttributeLifted('aNode', attribute('payload', union('TypeNode')), buildOptions())).toBe(true); + }); + + it('lifts a child attribute wrapped in an array', () => { + expect(isAttributeLifted('aNode', attribute('items', array(node('innerNode'))), buildOptions())).toBe(true); + }); + + it('does not lift a plain data attribute (boolean)', () => { + expect(isAttributeLifted('aNode', attribute('flag', boolean()), buildOptions())).toBe(false); + }); + + it('does not lift a plain data attribute (number)', () => { + expect(isAttributeLifted('aNode', attribute('count', u32()), buildOptions())).toBe(false); + }); + + it('lifts a narrowable data attribute when its key is in narrowableDataAttributes', () => { + const options = buildOptions({ narrowableDataAttributes: new Set(['numberTypeNode:format']) }); + expect(isAttributeLifted('numberTypeNode', attribute('format', enumeration('NumberFormat')), options)).toBe( + true, + ); + }); + + it('does not lift a data attribute whose key is absent from the narrowable set', () => { + const options = buildOptions({ narrowableDataAttributes: new Set(['otherNode:format']) }); + expect(isAttributeLifted('numberTypeNode', attribute('format', enumeration('NumberFormat')), options)).toBe( + false, + ); + }); + + it('respects optionality (still lifts an optional child attribute)', () => { + expect(isAttributeLifted('aNode', optionalAttribute('payload', node('innerNode')), buildOptions())).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/utils/scope.test.ts b/packages/spec-generators/test/nodeTypes/utils/scope.test.ts new file mode 100644 index 000000000..48ca636c1 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/utils/scope.test.ts @@ -0,0 +1,231 @@ +import type { Spec } from '@codama/spec'; +import { + defineCategory, + defineEnumeration, + defineNestedUnion, + defineNode, + defineUnion, + node, + variant, +} from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import type { RenderOptions } from '../../../src/nodeTypes/options'; +import { buildRenderScope, relativeImportPath } from '../../../src/nodeTypes/utils/scope'; + +function buildSpec(categories: Spec['categories']): Spec { + return { categories, version: '1.0.0' }; +} + +const options: RenderOptions = { targetSpecMajor: 1 }; + +describe('buildRenderScope', () => { + it('places type-category nodes under typeNodes/', () => { + const scope = buildRenderScope( + buildSpec([defineCategory('type', { nodes: [defineNode('arrayTypeNode', { attributes: [] })] })]), + options, + ); + expect(scope.symbolicModules.get('node:arrayTypeNode')).toBe('typeNodes/ArrayTypeNode'); + }); + + it('places value-category nodes under valueNodes/', () => { + const scope = buildRenderScope( + buildSpec([defineCategory('value', { nodes: [defineNode('stringValueNode', { attributes: [] })] })]), + options, + ); + expect(scope.symbolicModules.get('node:stringValueNode')).toBe('valueNodes/StringValueNode'); + }); + + it('places contextual-value nodes under contextualValueNodes/', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('contextualValue', { + nodes: [defineNode('argumentValueNode', { attributes: [] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('node:argumentValueNode')).toBe('contextualValueNodes/ArgumentValueNode'); + }); + + it('places top-level nodes at the root', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('topLevel', { + nodes: [defineNode('accountNode', { attributes: [] }), defineNode('rootNode', { attributes: [] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('node:accountNode')).toBe('AccountNode'); + expect(scope.symbolicModules.get('node:rootNode')).toBe('RootNode'); + }); + + it('places shared enumerations under shared/', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('shared', { + enumerations: [ + defineEnumeration('Endianness', { variants: [variant('be')] }), + defineEnumeration('NumberFormat', { variants: [variant('u32')] }), + ], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('enumeration:Endianness')).toBe('shared/endianness'); + expect(scope.symbolicModules.get('enumeration:NumberFormat')).toBe('shared/numberFormat'); + }); + + it('places category unions under their category subdirectory', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('type', { + nodes: [defineNode('innerTypeNode', { attributes: [] })], + unions: [defineUnion('TypeNode', { members: ['innerTypeNode'] })], + }), + defineCategory('pdaSeed', { + nodes: [defineNode('innerSeedNode', { attributes: [] })], + unions: [defineUnion('ConstantPdaSeedValue', { members: ['innerSeedNode'] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('union:TypeNode')).toBe('typeNodes/TypeNode'); + expect(scope.symbolicModules.get('union:ConstantPdaSeedValue')).toBe('pdaSeedNodes/ConstantPdaSeedValue'); + }); + + it('places top-level helper unions at the root', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('topLevel', { + nodes: [defineNode('innerNode', { attributes: [] })], + unions: [defineUnion('InstructionByteDeltaValue', { members: ['innerNode'] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('union:InstructionByteDeltaValue')).toBe('InstructionByteDeltaValue'); + }); + + it('places nested-union aliases under their parent category', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('type', { + nestedUnions: [ + defineNestedUnion('NestedTypeNode', { + base: node('innerTypeNode'), + wrappers: ['fixedSizeTypeNode'], + }), + ], + nodes: [ + defineNode('innerTypeNode', { attributes: [] }), + defineNode('fixedSizeTypeNode', { attributes: [] }), + ], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('nestedUnion:NestedTypeNode')).toBe('typeNodes/NestedTypeNode'); + }); + + it('points every brand at the hand-written ../brands sibling', () => { + // None of the brand / Docs / Version files are emitted by the + // generator; they live at the top of `@codama/node-types/src/`. + // The scope still carries entries so renderers can resolve + // symbolic imports against them. + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('brand:CamelCaseString')).toBe('../brands'); + expect(scope.symbolicModules.get('brand:KebabCaseString')).toBe('../brands'); + expect(scope.symbolicModules.get('brand:PascalCaseString')).toBe('../brands'); + expect(scope.symbolicModules.get('brand:SnakeCaseString')).toBe('../brands'); + expect(scope.symbolicModules.get('brand:TitleCaseString')).toBe('../brands'); + }); + + it('points docs:Docs at the hand-written ../Docs sibling', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('docs:Docs')).toBe('../Docs'); + }); + + it('points version:Version at the hand-written ../Version sibling', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('version:Version')).toBe('../Version'); + }); + + it('places version:CodamaVersion under shared/codamaVersion', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('version:CodamaVersion')).toBe('shared/codamaVersion'); + }); + + it('points every registry:* identifier at the top-level Node file', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('registry:Node')).toBe('Node'); + expect(scope.symbolicModules.get('registry:NodeKind')).toBe('Node'); + expect(scope.symbolicModules.get('registry:GetNodeFromKind')).toBe('Node'); + }); + + it('throws on an unknown category name', () => { + const spec: Spec = { + categories: [{ enumerations: [], name: 'mystery', nestedUnions: [], nodes: [], unions: [] }], + version: '1.0.0', + }; + expect(() => buildRenderScope(spec, options)).toThrow(/unknown category "mystery"/); + }); + + it('files every entity in a category under that category folder', () => { + // Sanity: a single category packing nodes, unions, enumerations, + // and nested unions all land under the same folder. + const scope = buildRenderScope( + buildSpec([ + defineCategory('type', { + enumerations: [defineEnumeration('SomeEnum', { variants: [variant('a')] })], + nestedUnions: [ + defineNestedUnion('SomeNested', { + base: node('a'), + wrappers: ['b'], + }), + ], + nodes: [defineNode('a', { attributes: [] }), defineNode('b', { attributes: [] })], + unions: [defineUnion('SomeUnion', { members: ['a'] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('node:a')).toBe('typeNodes/A'); + expect(scope.symbolicModules.get('union:SomeUnion')).toBe('typeNodes/SomeUnion'); + expect(scope.symbolicModules.get('enumeration:SomeEnum')).toBe('typeNodes/someEnum'); + expect(scope.symbolicModules.get('nestedUnion:SomeNested')).toBe('typeNodes/SomeNested'); + }); +}); + +describe('relativeImportPath', () => { + it('returns ./Sibling for files in the same directory', () => { + expect(relativeImportPath('typeNodes/AmountTypeNode', 'typeNodes/NumberTypeNode')).toBe('./NumberTypeNode'); + }); + + it('returns ./Sibling for top-level files', () => { + expect(relativeImportPath('AccountNode', 'RootNode')).toBe('./RootNode'); + }); + + it('returns ../sub/X when stepping out one level', () => { + expect(relativeImportPath('typeNodes/AmountTypeNode', 'shared/numberFormat')).toBe('../shared/numberFormat'); + }); + + it('returns ./sub/X when stepping into a sibling subdirectory', () => { + expect(relativeImportPath('AccountNode', 'typeNodes/StructTypeNode')).toBe('./typeNodes/StructTypeNode'); + }); + + it('reaches a hand-written sibling above generated/ from a top-level file with one ../', () => { + // From `generated/AccountNode.ts` to `src/Docs.ts` — one level up. + expect(relativeImportPath('AccountNode', '../Docs')).toBe('../Docs'); + }); + + it('reaches a hand-written sibling above generated/ from a subdirectory file with two ../', () => { + // From `generated/typeNodes/StructTypeNode.ts` to `src/Docs.ts` — two levels up. + expect(relativeImportPath('typeNodes/StructTypeNode', '../Docs')).toBe('../../Docs'); + }); + + it('refuses to produce a self-import', () => { + expect(() => relativeImportPath('foo', 'foo')).toThrow(/self-import/); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/utils/selfReference.test.ts b/packages/spec-generators/test/nodeTypes/utils/selfReference.test.ts new file mode 100644 index 000000000..6c75dd0c4 --- /dev/null +++ b/packages/spec-generators/test/nodeTypes/utils/selfReference.test.ts @@ -0,0 +1,39 @@ +import { array, boolean, literal, nestedUnion, node, tuple, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { isTypeExprSelfReferential } from '../../../src/nodeTypes/utils/selfReference'; + +describe('isTypeExprSelfReferential', () => { + it('returns true for a direct node reference matching the kind', () => { + expect(isTypeExprSelfReferential(node('fooNode'), 'fooNode')).toBe(true); + }); + + it('returns false for a direct node reference to a different kind', () => { + expect(isTypeExprSelfReferential(node('barNode'), 'fooNode')).toBe(false); + }); + + it('recurses through array() to find a matching node reference', () => { + expect(isTypeExprSelfReferential(array(node('fooNode')), 'fooNode')).toBe(true); + }); + + it('recurses through tuple() to find a matching node reference', () => { + expect(isTypeExprSelfReferential(tuple(node('barNode'), node('fooNode')), 'fooNode')).toBe(true); + }); + + it('does not recurse through union references — they are name-aliased and break cycles', () => { + expect(isTypeExprSelfReferential(union('FooUnion'), 'fooNode')).toBe(false); + }); + + it('does not recurse through nestedUnion references either', () => { + expect(isTypeExprSelfReferential(nestedUnion('NestedFoo', 'fooNode'), 'fooNode')).toBe(false); + }); + + it('returns false for primitive types', () => { + expect(isTypeExprSelfReferential(boolean(), 'fooNode')).toBe(false); + expect(isTypeExprSelfReferential(literal('something'), 'fooNode')).toBe(false); + }); + + it('returns false for nested array(array(barNode)) when looking for fooNode', () => { + expect(isTypeExprSelfReferential(array(array(node('barNode'))), 'fooNode')).toBe(false); + }); +}); diff --git a/packages/spec-generators/tsconfig.json b/packages/spec-generators/tsconfig.json new file mode 100644 index 000000000..9d9052400 --- /dev/null +++ b/packages/spec-generators/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + // The package is a Node-only build tool that consumes + // `@codama/spec`'s `/api` subpath. `moduleResolution: "bundler"` + // honours the package's `exports` map directly, so subpath types + // resolve without needing a `typesVersions` shim. + "module": "esnext", + "moduleResolution": "bundler" + }, + "display": "@codama-internal/spec-generators", + "extends": "../../tsconfig.json", + "include": ["bin", "src", "test"] +} diff --git a/packages/spec-generators/tsup.config.ts b/packages/spec-generators/tsup.config.ts new file mode 100644 index 000000000..1afc4fae1 --- /dev/null +++ b/packages/spec-generators/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'tsup'; + +/** + * `@codama-internal/spec-generators` is a private build-time tool, never + * published to npm and never imported by other workspace packages at + * runtime. We only need a single Node ESM build that the `generate` + * script can invoke directly. + * + * Two entries are emitted: the orchestrator surface (`src/index.ts`) and + * the bin script (`bin/generate.ts`). Both inline their dependencies + * (`splitting: false`) so each entry stands on its own and the dist + * layout remains predictable from the script that runs it. + */ +export default defineConfig({ + clean: false, + dts: false, + entry: { + generate: './bin/generate.ts', + index: './src/index.ts', + }, + format: 'esm', + outExtension() { + return { js: '.mjs' }; + }, + platform: 'node', + sourcemap: true, + splitting: false, + target: 'node20', + treeshake: true, +}); diff --git a/packages/spec-generators/vitest.config.mts b/packages/spec-generators/vitest.config.mts new file mode 100644 index 000000000..074d9a44c --- /dev/null +++ b/packages/spec-generators/vitest.config.mts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +import { getVitestConfig } from '../../vitest.config.base.mjs'; + +/** + * The spec-generators package is a Node-only build tool — its tests run + * `prettier`, `node:fs/promises`, and other Node APIs — so we register a + * single Node-flavoured project rather than the multi-platform matrix + * used by published packages. + */ +export default defineConfig({ + test: { + projects: [getVitestConfig('node')], + }, +}); diff --git a/packages/validators/test/getValidationItemsVisitor.test.ts b/packages/validators/test/getValidationItemsVisitor.test.ts index cd57309bf..c57d9866a 100644 --- a/packages/validators/test/getValidationItemsVisitor.test.ts +++ b/packages/validators/test/getValidationItemsVisitor.test.ts @@ -14,7 +14,7 @@ test('it validates program nodes', () => { name: '', origin: undefined, publicKey: '', - // @ts-expect-error Empty string does not match ProgramVersion. + // @ts-expect-error Empty string does not match Version. version: '', }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 138cde353..97fdaa686 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,31 @@ importers: specifier: workspace:* version: link:../visitors-core + packages/spec-generators: + dependencies: + '@codama/fragments': + specifier: workspace:* + version: link:../fragments + '@codama/spec': + specifier: 1.6.0-rc.4 + version: 1.6.0-rc.4 + devDependencies: + '@types/node': + specifier: ^25 + version: 25.5.0 + rimraf: + specifier: 6.1.2 + version: 6.1.2 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.16 + version: 4.0.16(@types/node@25.5.0)(happy-dom@20.5.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(yaml@2.8.2) + packages/validators: dependencies: '@codama/errors': @@ -582,6 +607,9 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@codama/spec@1.6.0-rc.4': + resolution: {integrity: sha512-jLTwjgu46MSd3lfuQL9g6SaTcpamlsd975u2GT7BPoXN1mpezY5NvGFMQh467IpnuddOm6NGnLV2M3GUpq0MmQ==} + '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} engines: {node: '>=18'} @@ -4801,6 +4829,8 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@codama/spec@1.6.0-rc.4': {} + '@esbuild/aix-ppc64@0.27.0': optional: true @@ -6744,7 +6774,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.0.3 + '@types/node': 25.5.0 '@types/yargs-parser@21.0.3': {} @@ -6913,6 +6943,14 @@ snapshots: optionalDependencies: vite: 7.3.0(@types/node@25.0.3)(yaml@2.8.2) + '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@25.5.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.16 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.0(@types/node@25.5.0)(yaml@2.8.2) + '@vitest/pretty-format@4.0.16': dependencies: tinyrainbow: 3.0.3 @@ -8905,6 +8943,19 @@ snapshots: fsevents: 2.3.3 yaml: 2.8.2 + vite@7.3.0(@types/node@25.5.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + fsevents: 2.3.3 + yaml: 2.8.2 + vitest@4.0.16(@types/node@25.0.3)(happy-dom@20.5.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.16 @@ -8943,6 +8994,44 @@ snapshots: - tsx - yaml + vitest@4.0.16(@types/node@25.5.0)(happy-dom@20.5.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.16 + '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@25.5.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.16 + '@vitest/runner': 4.0.16 + '@vitest/snapshot': 4.0.16 + '@vitest/spy': 4.0.16 + '@vitest/utils': 4.0.16 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.0(@types/node@25.5.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.5.0 + happy-dom: 20.5.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + walker@1.0.8: dependencies: makeerror: 1.0.12