From f81f5e5cdae8190b9afd81b42b881d430e573ecc Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 12 May 2026 12:50:33 +0100 Subject: [PATCH] Regenerate `@codama/nodes` constructors from `@codama/spec` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR extends `@codama-internal/spec-generators` with a second generator that produces the `xxxNodeInput` types, `xxxNode()` constructors, and runtime `*_NODE_KINDS` arrays of `@codama/nodes` from the encoded `@codama/spec` description. The bulk of `packages/nodes/src/` now lives under `src/generated/` and is rewritten on every `pnpm generate` run; the hand-written helper files (`isNode`/`assertIsNode`, `getAllPrograms`, `isScalarEnum`, the `NestedTypeNode` resolvers, etc.) survive at the top of `src/` and re-export the matching generated constructor so the public import surface is unchanged. The new generator lives at `packages/spec-generators/src/nodes/`. It consults a per-node configuration table (`getNodeConfigs(spec)`) that encodes the conveniences the spec doesn't carry — positional vs. object-input shape, defaulted attributes, `camelCase` wrapping, conditional-spread patterns — and derives the `Partial<>` wrapping of each `Input` type automatically. Runtime kinds-arrays are generated from the spec's `UnionSpec`s, with union-of-unions transitively expanded via spreads; the top-level `REGISTERED_NODE_KINDS` aggregate is composed from each category's `RegisteredNode` array. Two intentional behaviour changes shake out of the rebuild: - **`docs` is now omitted entirely from the encoded shape when it would be empty.** Matches the Rust side and keeps absent documentation out of serialised IDLs. `removeDocsVisitor` in `@codama/visitors-core` is updated to delete the `docs` key rather than blank it to `[]`, following the same convention. - **`rootNode().version` now reflects the spec version `@codama/nodes` was generated against, not the runtime package version.** The constructor previously read the `__VERSION__` build-time global; it now bakes the spec version as a literal at generation time. Invisible at HEAD since the two have always tracked the same release cadence, but it makes the intent explicit. The legacy plural-noun constants (`TYPE_NODES`, `VALUE_NODES`, `CONTEXTUAL_VALUE_NODES`, `INSTRUCTION_INPUT_VALUE_NODES`, `COUNT_NODES`, `DISCRIMINATOR_NODES`, `LINK_NODES`, `PDA_SEED_NODES`, `ENUM_VARIANT_TYPE_NODES`) are preserved as alias re-exports of the new canonical `*_NODE_KINDS` names. --- .changeset/regenerate-nodes-package.md | 17 + packages/nodes/src/ConstantNode.ts | 22 - packages/nodes/src/ConstantPdaSeedNode.ts | 21 + packages/nodes/src/ConstantValueNode.ts | 15 + packages/nodes/src/EnumTypeNode.ts | 9 + packages/nodes/src/InstructionArgumentNode.ts | 35 +- packages/nodes/src/InstructionNode.ts | 82 ---- .../src/{typeNodes => }/NestedTypeNode.ts | 6 +- packages/nodes/src/Node.ts | 35 -- .../src/{typeNodes => }/NumberTypeNode.ts | 15 +- packages/nodes/src/PdaNode.ts | 25 -- packages/nodes/src/ProgramNode.ts | 51 --- packages/nodes/src/RootNode.ts | 18 - .../ConditionalValueNode.ts | 32 -- .../ContextualValueNode.ts | 24 -- .../contextualValueNodes/IdentityValueNode.ts | 5 - .../contextualValueNodes/PayerValueNode.ts | 5 - .../contextualValueNodes/PdaSeedValueNode.ts | 17 - .../src/contextualValueNodes/PdaValueNode.ts | 28 -- .../ProgramIdValueNode.ts | 5 - .../contextualValueNodes/ResolverValueNode.ts | 22 - .../src/countNodes/RemainderCountNode.ts | 5 - .../nodes/src/{ => generated}/AccountNode.ts | 24 +- packages/nodes/src/generated/ConstantNode.ts | 23 + .../src/{ => generated}/DefinedTypeNode.ts | 13 +- .../nodes/src/{ => generated}/ErrorNode.ts | 9 +- .../nodes/src/{ => generated}/EventNode.ts | 16 +- .../{ => generated}/InstructionAccountNode.ts | 14 +- .../src/generated/InstructionArgumentNode.ts | 30 ++ .../InstructionByteDeltaNode.ts | 10 +- .../generated/InstructionByteDeltaValue.ts | 7 + .../nodes/src/generated/InstructionNode.ts | 92 ++++ .../InstructionRemainingAccountsNode.ts | 13 +- .../InstructionRemainingAccountsValue.ts | 2 + .../{ => generated}/InstructionStatusNode.ts | 1 + packages/nodes/src/generated/PdaNode.ts | 26 ++ packages/nodes/src/generated/ProgramNode.ts | 62 +++ packages/nodes/src/generated/RootNode.ts | 23 + packages/nodes/src/generated/codamaVersion.ts | 11 + .../AccountBumpValueNode.ts | 4 +- .../contextualValueNodes/AccountValueNode.ts | 4 +- .../contextualValueNodes/ArgumentValueNode.ts | 4 +- .../ConditionalValueCondition.ts | 6 + .../ConditionalValueNode.ts | 36 ++ .../ContextualValueNode.ts | 4 + .../contextualValueNodes/IdentityValueNode.ts | 8 + .../InstructionInputValueNode.ts | 12 + .../contextualValueNodes/PayerValueNode.ts | 8 + .../contextualValueNodes/PdaSeedValueNode.ts | 18 + .../contextualValueNodes/PdaSeedValueValue.ts | 8 + .../contextualValueNodes/PdaValueNode.ts | 22 + .../contextualValueNodes/PdaValuePda.ts | 2 + .../contextualValueNodes/PdaValueProgramId.ts | 2 + .../ProgramIdValueNode.ts | 8 + .../RegisteredContextualValueNode.ts | 7 + .../ResolverDependency.ts | 2 + .../contextualValueNodes/ResolverValueNode.ts | 26 ++ .../StandaloneContextualValueNode.ts | 12 + .../contextualValueNodes/index.ts | 8 + .../src/generated/countNodes/CountNode.ts | 4 + .../countNodes/FixedCountNode.ts | 1 + .../countNodes/PrefixedCountNode.ts | 3 +- .../countNodes/RegisteredCountNode.ts} | 7 +- .../countNodes/RemainderCountNode.ts | 8 + .../src/{ => generated}/countNodes/index.ts | 1 + .../ConstantDiscriminatorNode.ts | 5 +- .../discriminatorNodes/DiscriminatorNode.ts | 4 + .../FieldDiscriminatorNode.ts | 4 +- .../RegisteredDiscriminatorNode.ts} | 5 +- .../SizeDiscriminatorNode.ts | 1 + .../discriminatorNodes/index.ts | 1 + packages/nodes/src/generated/index.ts | 26 ++ .../generated/linkNodes/AccountLinkNode.ts | 21 + .../linkNodes/DefinedTypeLinkNode.ts | 21 + .../linkNodes/InstructionAccountLinkNode.ts | 23 + .../linkNodes/InstructionArgumentLinkNode.ts | 23 + .../linkNodes/InstructionLinkNode.ts | 21 + .../nodes/src/generated/linkNodes/LinkNode.ts | 4 + .../src/generated/linkNodes/PdaLinkNode.ts | 21 + .../linkNodes/ProgramLinkNode.ts | 4 +- .../linkNodes/RegisteredLinkNode.ts} | 5 +- .../src/{ => generated}/linkNodes/index.ts | 1 + packages/nodes/src/generated/nodeKinds.ts | 32 ++ .../pdaSeedNodes/ConstantPdaSeedNode.ts | 15 + .../pdaSeedNodes/ConstantPdaSeedValue.ts | 4 + .../src/generated/pdaSeedNodes/PdaSeedNode.ts | 4 + .../pdaSeedNodes/RegisteredPdaSeedNode.ts | 2 + .../pdaSeedNodes/VariablePdaSeedNode.ts | 9 +- .../src/{ => generated}/pdaSeedNodes/index.ts | 2 + .../typeNodes/AmountTypeNode.ts | 6 +- .../typeNodes/ArrayTypeNode.ts | 3 +- .../generated/typeNodes/BooleanTypeNode.ts | 14 + .../src/generated/typeNodes/BytesTypeNode.ts | 8 + .../typeNodes/DateTimeTypeNode.ts | 3 +- .../typeNodes/EnumEmptyVariantTypeNode.ts | 6 +- .../typeNodes/EnumStructVariantTypeNode.ts | 6 +- .../typeNodes/EnumTupleVariantTypeNode.ts | 6 +- .../src/generated/typeNodes/EnumTypeNode.ts | 21 + .../typeNodes/EnumVariantTypeNode.ts | 3 +- .../generated/typeNodes/FixedSizeTypeNode.ts | 14 + .../typeNodes/HiddenPrefixTypeNode.ts | 3 +- .../typeNodes/HiddenSuffixTypeNode.ts | 3 +- .../{ => generated}/typeNodes/MapTypeNode.ts | 6 +- .../src/generated/typeNodes/NumberTypeNode.ts | 15 + .../typeNodes/OptionTypeNode.ts | 10 +- .../generated/typeNodes/PostOffsetTypeNode.ts | 19 + .../generated/typeNodes/PreOffsetTypeNode.ts | 19 + .../typeNodes/PublicKeyTypeNode.ts | 5 +- .../generated/typeNodes/RegisteredTypeNode.ts | 9 + .../typeNodes/RemainderOptionTypeNode.ts | 11 + .../typeNodes/SentinelTypeNode.ts | 3 +- .../{ => generated}/typeNodes/SetTypeNode.ts | 3 +- .../generated/typeNodes/SizePrefixTypeNode.ts | 15 + .../typeNodes/SolAmountTypeNode.ts | 3 +- .../typeNodes/StandaloneTypeNode.ts} | 19 +- .../src/generated/typeNodes/StringTypeNode.ts | 17 + .../typeNodes/StructFieldTypeNode.ts | 16 +- .../typeNodes/StructTypeNode.ts | 3 +- .../src/generated/typeNodes/TupleTypeNode.ts | 11 + .../nodes/src/generated/typeNodes/TypeNode.ts | 4 + .../typeNodes/ZeroableOptionTypeNode.ts | 15 + .../src/{ => generated}/typeNodes/index.ts | 3 +- .../valueNodes/ArrayValueNode.ts | 3 +- .../valueNodes/BooleanValueNode.ts | 3 +- .../valueNodes/BytesValueNode.ts | 1 + .../generated/valueNodes/ConstantValueNode.ts | 15 + .../src/generated/valueNodes/EnumValueNode.ts | 20 + .../generated/valueNodes/EnumValuePayload.ts | 2 + .../valueNodes/MapEntryValueNode.ts | 3 +- .../src/generated/valueNodes/MapValueNode.ts | 13 + .../src/generated/valueNodes/NoneValueNode.ts | 8 + .../valueNodes/NumberValueNode.ts | 4 + .../valueNodes/PublicKeyValueNode.ts | 4 +- .../valueNodes/RegisteredValueNode.ts | 8 + .../valueNodes/SetValueNode.ts | 3 +- .../src/generated/valueNodes/SomeValueNode.ts | 11 + .../valueNodes/StandaloneValueNode.ts} | 18 +- .../valueNodes/StringValueNode.ts | 1 + .../valueNodes/StructFieldValueNode.ts | 6 +- .../valueNodes/StructValueNode.ts | 3 +- .../generated/valueNodes/TupleValueNode.ts | 11 + .../src/generated/valueNodes/ValueNode.ts | 4 + .../src/{ => generated}/valueNodes/index.ts | 3 + packages/nodes/src/index.ts | 45 +- .../nodes/src/linkNodes/AccountLinkNode.ts | 16 - .../src/linkNodes/DefinedTypeLinkNode.ts | 16 - .../linkNodes/InstructionAccountLinkNode.ts | 21 - .../linkNodes/InstructionArgumentLinkNode.ts | 21 - .../src/linkNodes/InstructionLinkNode.ts | 16 - packages/nodes/src/linkNodes/PdaLinkNode.ts | 16 - .../src/pdaSeedNodes/ConstantPdaSeedNode.ts | 33 -- .../nodes/src/pdaSeedNodes/PdaSeedNode.ts | 5 - .../nodes/src/typeNodes/BooleanTypeNode.ts | 14 - packages/nodes/src/typeNodes/BytesTypeNode.ts | 5 - packages/nodes/src/typeNodes/EnumTypeNode.ts | 24 -- .../nodes/src/typeNodes/FixedSizeTypeNode.ts | 13 - .../nodes/src/typeNodes/PostOffsetTypeNode.ts | 18 - .../nodes/src/typeNodes/PreOffsetTypeNode.ts | 18 - .../src/typeNodes/RemainderOptionTypeNode.ts | 10 - .../nodes/src/typeNodes/SizePrefixTypeNode.ts | 14 - .../nodes/src/typeNodes/StringTypeNode.ts | 10 - packages/nodes/src/typeNodes/TupleTypeNode.ts | 10 - .../src/typeNodes/ZeroableOptionTypeNode.ts | 14 - packages/nodes/src/types/global.d.ts | 6 - .../nodes/src/valueNodes/ConstantValueNode.ts | 26 -- .../nodes/src/valueNodes/EnumValueNode.ts | 20 - packages/nodes/src/valueNodes/MapValueNode.ts | 10 - .../nodes/src/valueNodes/NoneValueNode.ts | 5 - .../nodes/src/valueNodes/SomeValueNode.ts | 10 - .../nodes/src/valueNodes/TupleValueNode.ts | 10 - packages/nodes/test/RootNode.test.ts | 6 +- packages/nodes/test/types/global.d.ts | 6 - packages/spec-generators/package.json | 2 +- packages/spec-generators/src/index.ts | 18 +- .../nodeTypes/fragments/attributeBodyLine.ts | 17 +- .../src/nodeTypes/fragments/index.ts | 3 - .../src/nodeTypes/fragments/indexPage.ts | 10 - .../src/nodeTypes/fragments/node.ts | 42 +- .../src/nodeTypes/fragments/nodeRegistry.ts | 56 +-- .../src/nodeTypes/fragments/page.ts | 53 --- .../fragments/typeParameterDefinition.ts | 14 +- .../fragments/typeParameterIdentifier.ts | 6 - .../spec-generators/src/nodeTypes/index.ts | 96 +---- .../spec-generators/src/nodeTypes/options.ts | 235 ++++------- .../nodeTypes/{utils => }/selfReference.ts | 0 .../src/nodeTypes/utils/index.ts | 2 - .../src/nodeTypes/utils/scope.ts | 98 ----- packages/spec-generators/src/nodes/config.ts | 395 ++++++++++++++++++ .../nodes/fragments/codamaVersionConstant.ts | 22 + .../src/nodes/fragments/index.ts | 13 + .../src/nodes/fragments/inputType.ts | 113 +++++ .../src/nodes/fragments/kindUnionConstant.ts | 41 ++ .../src/nodes/fragments/nodeFunction.ts | 49 +++ .../nodes/fragments/nodeFunctionAttribute.ts | 80 ++++ .../src/nodes/fragments/nodeFunctionBody.ts | 80 ++++ .../nodeFunctionPositionalArguments.ts | 117 ++++++ .../nodes/fragments/nodeFunctionReturnType.ts | 15 + .../fragments/nodeFunctionTypeParameters.ts | 28 ++ .../nodes/fragments/nodeKindUnionConstant.ts | 32 ++ .../src/nodes/fragments/nodePage.ts | 31 ++ .../src/nodes/fragments/nodeTypeParameters.ts | 78 ++++ .../src/nodes/fragments/typeExpr.ts | 77 ++++ packages/spec-generators/src/nodes/index.ts | 89 ++++ .../src/nodes/kindsArrayConstantName.ts | 6 + packages/spec-generators/src/nodes/options.ts | 115 +++++ .../src/nodes/paramIdentifier.ts | 19 + .../src/nodes/reservedParamNames.ts | 50 +++ .../spec-generators/src/shared/defaults.ts | 59 +++ .../src/shared/fragments/index.ts | 3 + .../src/shared/fragments/indexPage.ts | 76 ++++ .../src/shared/fragments/page.ts | 62 +++ .../fragments/typeParameterIdentifier.ts | 13 + packages/spec-generators/src/shared/index.ts | 6 + .../spec-generators/src/shared/options.ts | 150 +++++++ .../{shared.ts => shared/repoDirectory.ts} | 7 +- .../src/shared/symbolicModule.ts | 39 ++ packages/spec-generators/src/shared/unions.ts | 50 +++ .../fragments/attributeBodyLine.test.ts | 14 +- .../nodeTypes/fragments/indexPage.test.ts | 28 -- .../test/nodeTypes/fragments/node.test.ts | 18 +- .../nodeTypes/fragments/nodeRegistry.test.ts | 21 +- .../test/nodeTypes/options.test.ts | 50 --- .../test/nodeTypes/{utils => }/scope.test.ts | 35 +- .../{utils => }/selfReference.test.ts | 2 +- .../test/nodes/fragments/inputType.test.ts | 118 ++++++ .../fragments/nodeFunctionAttribute.test.ts | 115 +++++ .../nodes/fragments/nodeFunctionBody.test.ts | 28 ++ .../nodeFunctionPositionalArguments.test.ts | 91 ++++ .../test/nodes/fragments/nodePage.test.ts | 82 ++++ .../fragments/nodeTypeParameters.test.ts | 190 +++++++++ .../test/nodes/fragments/typeExpr.test.ts | 142 +++++++ .../test/nodes/generate.test.ts | 259 ++++++++++++ .../test/nodes/kindsArrayConstantName.test.ts | 11 + .../spec-generators/test/nodes/scope.test.ts | 103 +++++ .../test/shared/fragments/indexPage.test.ts | 56 +++ .../fragments/page.test.ts | 52 ++- .../fragments/typeParameterIdentifier.test.ts | 2 +- .../test/shared/options.test.ts | 58 +++ .../test/shared/unions.test.ts | 135 ++++++ .../visitors-core/src/removeDocsVisitor.ts | 13 +- .../test/getUniqueHashStringVisitor.test.ts | 7 +- 241 files changed, 4761 insertions(+), 1673 deletions(-) create mode 100644 .changeset/regenerate-nodes-package.md delete mode 100644 packages/nodes/src/ConstantNode.ts create mode 100644 packages/nodes/src/ConstantPdaSeedNode.ts create mode 100644 packages/nodes/src/ConstantValueNode.ts create mode 100644 packages/nodes/src/EnumTypeNode.ts rename packages/nodes/src/{typeNodes => }/NestedTypeNode.ts (93%) rename packages/nodes/src/{typeNodes => }/NumberTypeNode.ts (56%) delete mode 100644 packages/nodes/src/PdaNode.ts delete mode 100644 packages/nodes/src/RootNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/ConditionalValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/ContextualValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/IdentityValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/PayerValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/PdaSeedValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/PdaValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/ProgramIdValueNode.ts delete mode 100644 packages/nodes/src/contextualValueNodes/ResolverValueNode.ts delete mode 100644 packages/nodes/src/countNodes/RemainderCountNode.ts rename packages/nodes/src/{ => generated}/AccountNode.ts (52%) create mode 100644 packages/nodes/src/generated/ConstantNode.ts rename packages/nodes/src/{ => generated}/DefinedTypeNode.ts (52%) rename packages/nodes/src/{ => generated}/ErrorNode.ts (62%) rename packages/nodes/src/{ => generated}/EventNode.ts (57%) rename packages/nodes/src/{ => generated}/InstructionAccountNode.ts (60%) create mode 100644 packages/nodes/src/generated/InstructionArgumentNode.ts rename packages/nodes/src/{ => generated}/InstructionByteDeltaNode.ts (55%) create mode 100644 packages/nodes/src/generated/InstructionByteDeltaValue.ts create mode 100644 packages/nodes/src/generated/InstructionNode.ts rename packages/nodes/src/{ => generated}/InstructionRemainingAccountsNode.ts (55%) create mode 100644 packages/nodes/src/generated/InstructionRemainingAccountsValue.ts rename packages/nodes/src/{ => generated}/InstructionStatusNode.ts (75%) create mode 100644 packages/nodes/src/generated/PdaNode.ts create mode 100644 packages/nodes/src/generated/ProgramNode.ts create mode 100644 packages/nodes/src/generated/RootNode.ts create mode 100644 packages/nodes/src/generated/codamaVersion.ts rename packages/nodes/src/{ => generated}/contextualValueNodes/AccountBumpValueNode.ts (66%) rename packages/nodes/src/{ => generated}/contextualValueNodes/AccountValueNode.ts (70%) rename packages/nodes/src/{ => generated}/contextualValueNodes/ArgumentValueNode.ts (70%) create mode 100644 packages/nodes/src/generated/contextualValueNodes/ConditionalValueCondition.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/ConditionalValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/ContextualValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/IdentityValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/InstructionInputValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/PayerValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/PdaSeedValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/PdaSeedValueValue.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/PdaValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/PdaValuePda.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/PdaValueProgramId.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/ProgramIdValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/ResolverDependency.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/ResolverValueNode.ts create mode 100644 packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts rename packages/nodes/src/{ => generated}/contextualValueNodes/index.ts (55%) create mode 100644 packages/nodes/src/generated/countNodes/CountNode.ts rename packages/nodes/src/{ => generated}/countNodes/FixedCountNode.ts (74%) rename packages/nodes/src/{ => generated}/countNodes/PrefixedCountNode.ts (60%) rename packages/nodes/src/{countNodes/CountNode.ts => generated/countNodes/RegisteredCountNode.ts} (57%) create mode 100644 packages/nodes/src/generated/countNodes/RemainderCountNode.ts rename packages/nodes/src/{ => generated}/countNodes/index.ts (77%) rename packages/nodes/src/{ => generated}/discriminatorNodes/ConstantDiscriminatorNode.ts (57%) create mode 100644 packages/nodes/src/generated/discriminatorNodes/DiscriminatorNode.ts rename packages/nodes/src/{ => generated}/discriminatorNodes/FieldDiscriminatorNode.ts (71%) rename packages/nodes/src/{discriminatorNodes/DiscriminatorNode.ts => generated/discriminatorNodes/RegisteredDiscriminatorNode.ts} (55%) rename packages/nodes/src/{ => generated}/discriminatorNodes/SizeDiscriminatorNode.ts (81%) rename packages/nodes/src/{ => generated}/discriminatorNodes/index.ts (77%) create mode 100644 packages/nodes/src/generated/index.ts create mode 100644 packages/nodes/src/generated/linkNodes/AccountLinkNode.ts create mode 100644 packages/nodes/src/generated/linkNodes/DefinedTypeLinkNode.ts create mode 100644 packages/nodes/src/generated/linkNodes/InstructionAccountLinkNode.ts create mode 100644 packages/nodes/src/generated/linkNodes/InstructionArgumentLinkNode.ts create mode 100644 packages/nodes/src/generated/linkNodes/InstructionLinkNode.ts create mode 100644 packages/nodes/src/generated/linkNodes/LinkNode.ts create mode 100644 packages/nodes/src/generated/linkNodes/PdaLinkNode.ts rename packages/nodes/src/{ => generated}/linkNodes/ProgramLinkNode.ts (74%) rename packages/nodes/src/{linkNodes/LinkNode.ts => generated/linkNodes/RegisteredLinkNode.ts} (74%) rename packages/nodes/src/{ => generated}/linkNodes/index.ts (88%) create mode 100644 packages/nodes/src/generated/nodeKinds.ts create mode 100644 packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts create mode 100644 packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts create mode 100644 packages/nodes/src/generated/pdaSeedNodes/PdaSeedNode.ts create mode 100644 packages/nodes/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts rename packages/nodes/src/{ => generated}/pdaSeedNodes/VariablePdaSeedNode.ts (50%) rename packages/nodes/src/{ => generated}/pdaSeedNodes/index.ts (57%) rename packages/nodes/src/{ => generated}/typeNodes/AmountTypeNode.ts (61%) rename packages/nodes/src/{ => generated}/typeNodes/ArrayTypeNode.ts (55%) create mode 100644 packages/nodes/src/generated/typeNodes/BooleanTypeNode.ts create mode 100644 packages/nodes/src/generated/typeNodes/BytesTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/DateTimeTypeNode.ts (53%) rename packages/nodes/src/{ => generated}/typeNodes/EnumEmptyVariantTypeNode.ts (64%) rename packages/nodes/src/{ => generated}/typeNodes/EnumStructVariantTypeNode.ts (67%) rename packages/nodes/src/{ => generated}/typeNodes/EnumTupleVariantTypeNode.ts (66%) create mode 100644 packages/nodes/src/generated/typeNodes/EnumTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/EnumVariantTypeNode.ts (56%) create mode 100644 packages/nodes/src/generated/typeNodes/FixedSizeTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/HiddenPrefixTypeNode.ts (55%) rename packages/nodes/src/{ => generated}/typeNodes/HiddenSuffixTypeNode.ts (55%) rename packages/nodes/src/{ => generated}/typeNodes/MapTypeNode.ts (52%) create mode 100644 packages/nodes/src/generated/typeNodes/NumberTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/OptionTypeNode.ts (64%) create mode 100644 packages/nodes/src/generated/typeNodes/PostOffsetTypeNode.ts create mode 100644 packages/nodes/src/generated/typeNodes/PreOffsetTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/PublicKeyTypeNode.ts (53%) create mode 100644 packages/nodes/src/generated/typeNodes/RegisteredTypeNode.ts create mode 100644 packages/nodes/src/generated/typeNodes/RemainderOptionTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/SentinelTypeNode.ts (57%) rename packages/nodes/src/{ => generated}/typeNodes/SetTypeNode.ts (55%) create mode 100644 packages/nodes/src/generated/typeNodes/SizePrefixTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/SolAmountTypeNode.ts (61%) rename packages/nodes/src/{typeNodes/TypeNode.ts => generated/typeNodes/StandaloneTypeNode.ts} (56%) create mode 100644 packages/nodes/src/generated/typeNodes/StringTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/StructFieldTypeNode.ts (62%) rename packages/nodes/src/{ => generated}/typeNodes/StructTypeNode.ts (54%) create mode 100644 packages/nodes/src/generated/typeNodes/TupleTypeNode.ts create mode 100644 packages/nodes/src/generated/typeNodes/TypeNode.ts create mode 100644 packages/nodes/src/generated/typeNodes/ZeroableOptionTypeNode.ts rename packages/nodes/src/{ => generated}/typeNodes/index.ts (93%) rename packages/nodes/src/{ => generated}/valueNodes/ArrayValueNode.ts (51%) rename packages/nodes/src/{ => generated}/valueNodes/BooleanValueNode.ts (65%) rename packages/nodes/src/{ => generated}/valueNodes/BytesValueNode.ts (79%) create mode 100644 packages/nodes/src/generated/valueNodes/ConstantValueNode.ts create mode 100644 packages/nodes/src/generated/valueNodes/EnumValueNode.ts create mode 100644 packages/nodes/src/generated/valueNodes/EnumValuePayload.ts rename packages/nodes/src/{ => generated}/valueNodes/MapEntryValueNode.ts (63%) create mode 100644 packages/nodes/src/generated/valueNodes/MapValueNode.ts create mode 100644 packages/nodes/src/generated/valueNodes/NoneValueNode.ts rename packages/nodes/src/{ => generated}/valueNodes/NumberValueNode.ts (59%) rename packages/nodes/src/{ => generated}/valueNodes/PublicKeyValueNode.ts (73%) create mode 100644 packages/nodes/src/generated/valueNodes/RegisteredValueNode.ts rename packages/nodes/src/{ => generated}/valueNodes/SetValueNode.ts (50%) create mode 100644 packages/nodes/src/generated/valueNodes/SomeValueNode.ts rename packages/nodes/src/{valueNodes/ValueNode.ts => generated/valueNodes/StandaloneValueNode.ts} (61%) rename packages/nodes/src/{ => generated}/valueNodes/StringValueNode.ts (87%) rename packages/nodes/src/{ => generated}/valueNodes/StructFieldValueNode.ts (66%) rename packages/nodes/src/{ => generated}/valueNodes/StructValueNode.ts (62%) create mode 100644 packages/nodes/src/generated/valueNodes/TupleValueNode.ts create mode 100644 packages/nodes/src/generated/valueNodes/ValueNode.ts rename packages/nodes/src/{ => generated}/valueNodes/index.ts (83%) delete mode 100644 packages/nodes/src/linkNodes/AccountLinkNode.ts delete mode 100644 packages/nodes/src/linkNodes/DefinedTypeLinkNode.ts delete mode 100644 packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts delete mode 100644 packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts delete mode 100644 packages/nodes/src/linkNodes/InstructionLinkNode.ts delete mode 100644 packages/nodes/src/linkNodes/PdaLinkNode.ts delete mode 100644 packages/nodes/src/pdaSeedNodes/ConstantPdaSeedNode.ts delete mode 100644 packages/nodes/src/pdaSeedNodes/PdaSeedNode.ts delete mode 100644 packages/nodes/src/typeNodes/BooleanTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/BytesTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/EnumTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/FixedSizeTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/PostOffsetTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/PreOffsetTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/RemainderOptionTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/SizePrefixTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/StringTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/TupleTypeNode.ts delete mode 100644 packages/nodes/src/typeNodes/ZeroableOptionTypeNode.ts delete mode 100644 packages/nodes/src/types/global.d.ts delete mode 100644 packages/nodes/src/valueNodes/ConstantValueNode.ts delete mode 100644 packages/nodes/src/valueNodes/EnumValueNode.ts delete mode 100644 packages/nodes/src/valueNodes/MapValueNode.ts delete mode 100644 packages/nodes/src/valueNodes/NoneValueNode.ts delete mode 100644 packages/nodes/src/valueNodes/SomeValueNode.ts delete mode 100644 packages/nodes/src/valueNodes/TupleValueNode.ts delete mode 100644 packages/nodes/test/types/global.d.ts delete mode 100644 packages/spec-generators/src/nodeTypes/fragments/indexPage.ts delete mode 100644 packages/spec-generators/src/nodeTypes/fragments/page.ts delete mode 100644 packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts rename packages/spec-generators/src/nodeTypes/{utils => }/selfReference.ts (100%) delete mode 100644 packages/spec-generators/src/nodeTypes/utils/index.ts delete mode 100644 packages/spec-generators/src/nodeTypes/utils/scope.ts create mode 100644 packages/spec-generators/src/nodes/config.ts create mode 100644 packages/spec-generators/src/nodes/fragments/codamaVersionConstant.ts create mode 100644 packages/spec-generators/src/nodes/fragments/index.ts create mode 100644 packages/spec-generators/src/nodes/fragments/inputType.ts create mode 100644 packages/spec-generators/src/nodes/fragments/kindUnionConstant.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeFunction.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeFunctionAttribute.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeFunctionBody.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeFunctionPositionalArguments.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeFunctionReturnType.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeFunctionTypeParameters.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeKindUnionConstant.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodePage.ts create mode 100644 packages/spec-generators/src/nodes/fragments/nodeTypeParameters.ts create mode 100644 packages/spec-generators/src/nodes/fragments/typeExpr.ts create mode 100644 packages/spec-generators/src/nodes/index.ts create mode 100644 packages/spec-generators/src/nodes/kindsArrayConstantName.ts create mode 100644 packages/spec-generators/src/nodes/options.ts create mode 100644 packages/spec-generators/src/nodes/paramIdentifier.ts create mode 100644 packages/spec-generators/src/nodes/reservedParamNames.ts create mode 100644 packages/spec-generators/src/shared/defaults.ts create mode 100644 packages/spec-generators/src/shared/fragments/index.ts create mode 100644 packages/spec-generators/src/shared/fragments/indexPage.ts create mode 100644 packages/spec-generators/src/shared/fragments/page.ts create mode 100644 packages/spec-generators/src/shared/fragments/typeParameterIdentifier.ts create mode 100644 packages/spec-generators/src/shared/index.ts create mode 100644 packages/spec-generators/src/shared/options.ts rename packages/spec-generators/src/{shared.ts => shared/repoDirectory.ts} (52%) create mode 100644 packages/spec-generators/src/shared/symbolicModule.ts create mode 100644 packages/spec-generators/src/shared/unions.ts delete mode 100644 packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts delete mode 100644 packages/spec-generators/test/nodeTypes/options.test.ts rename packages/spec-generators/test/nodeTypes/{utils => }/scope.test.ts (83%) rename packages/spec-generators/test/nodeTypes/{utils => }/selfReference.test.ts (95%) create mode 100644 packages/spec-generators/test/nodes/fragments/inputType.test.ts create mode 100644 packages/spec-generators/test/nodes/fragments/nodeFunctionAttribute.test.ts create mode 100644 packages/spec-generators/test/nodes/fragments/nodeFunctionBody.test.ts create mode 100644 packages/spec-generators/test/nodes/fragments/nodeFunctionPositionalArguments.test.ts create mode 100644 packages/spec-generators/test/nodes/fragments/nodePage.test.ts create mode 100644 packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts create mode 100644 packages/spec-generators/test/nodes/fragments/typeExpr.test.ts create mode 100644 packages/spec-generators/test/nodes/generate.test.ts create mode 100644 packages/spec-generators/test/nodes/kindsArrayConstantName.test.ts create mode 100644 packages/spec-generators/test/nodes/scope.test.ts create mode 100644 packages/spec-generators/test/shared/fragments/indexPage.test.ts rename packages/spec-generators/test/{nodeTypes => shared}/fragments/page.test.ts (71%) rename packages/spec-generators/test/{nodeTypes => shared}/fragments/typeParameterIdentifier.test.ts (93%) create mode 100644 packages/spec-generators/test/shared/options.test.ts create mode 100644 packages/spec-generators/test/shared/unions.test.ts diff --git a/.changeset/regenerate-nodes-package.md b/.changeset/regenerate-nodes-package.md new file mode 100644 index 000000000..0ba72ec59 --- /dev/null +++ b/.changeset/regenerate-nodes-package.md @@ -0,0 +1,17 @@ +--- +'@codama/nodes': minor +'@codama/visitors-core': patch +--- + +Regenerate the `xxxNodeInput` types and `xxxNode()` constructors of `@codama/nodes` from the encoded `@codama/spec` description, via a new `nodes` generator inside `@codama-internal/spec-generators`. The runtime `*_NODE_KINDS` arrays (`STANDALONE_TYPE_NODE_KINDS`, `REGISTERED_VALUE_NODE_KINDS`, `INSTRUCTION_INPUT_VALUE_NODE_KINDS`, …, and the top-level `REGISTERED_NODE_KINDS`) are now generated from the spec's union definitions instead of being maintained by hand. A new top-level `CODAMA_VERSION` constant, typed as `CodamaVersion` and pinned to the spec version at generation time, is the single source of truth for the version `@codama/nodes` was built against — `rootNode()` reads it directly when tagging the document. + +The bulk of the surface lives under `packages/nodes/src/generated/` and is produced on every `pnpm generate` run. The previously hand-maintained constructors and kinds-arrays are gone; only hand-written helpers (`isNode`, `assertIsNode`, `getAllPrograms`, `getAllInstructions`, `getAllInstructionsWithSubs`, `isScalarEnum`, `isDataEnum`, `isSignedInteger`, the `NestedTypeNode` resolvers, `parseOptionalAccountStrategy`, the legacy `constantValueNodeFromString` / `constantPdaSeedNodeFromString` flavours, etc.) survive at the top of `packages/nodes/src/`. The package's `index.ts` re-exports the generated tree alongside them. + +Two intentional behaviour changes shake out of the rebuild: + +- **`docs` is now omitted entirely from the encoded shape when it would be empty.** Constructors that accept a `docs?: DocsInput` parameter previously emitted `docs: []` on the frozen node when the caller said nothing about docs; they now drop the `docs` key altogether. This matches the Rust side, keeps absent documentation out of serialised IDLs, and aligns with the `docs?: Docs` optional field already declared by `@codama/node-types`. `removeDocsVisitor` in `@codama/visitors-core` is updated to delete the `docs` key rather than blank it to `[]`, following the same convention. +- **`rootNode().version` now reflects the spec version `@codama/nodes` was generated against, not the runtime package version.** The constructor previously read the `__VERSION__` build-time global injected from the package's `npm_package_version`; it now reads the generated `CODAMA_VERSION` constant. In practice the two have always tracked the same release cadence so the change is invisible at HEAD, but it makes the architectural intent explicit: the version pinned in the IDL is the spec version, not the package version. The `__VERSION__` build-time global and its `packages/nodes/src/types/global.d.ts` declaration are removed accordingly. + +The legacy plural-noun constants (`TYPE_NODES`, `VALUE_NODES`, `CONTEXTUAL_VALUE_NODES`, `INSTRUCTION_INPUT_VALUE_NODES`, `COUNT_NODES`, `DISCRIMINATOR_NODES`, `LINK_NODES`, `PDA_SEED_NODES`, `ENUM_VARIANT_TYPE_NODES`) are preserved as alias re-exports of the new canonical `*_NODE_KINDS` names. + +The generator drives almost entirely from the spec, with a minimal per-node configuration table carrying only the conveniences the spec can't express: which spec attributes appear as bare positional parameters (the rest land in a trailing `options` bag), and per-attribute overrides for defaulted values, string-coercion patterns on link targets, and the handful of bespoke body expressions (`instructionByteDeltaNode.withHeader`). The renderer derives signature shapes, generic parameters, return types, the `XxxNodeInput` declarations, the `Partial<>` wrapping decision, the `name: string` relaxation, the `docs?: DocsInput` / drop-if-empty handling, and the conditional-spread of optional attributes from the spec directly. An auto-import scan walks each rendered file and pulls in any spec or hand-written identifier the source references, so the configuration never declares imports. Generic-parameter lifting and ordering rely on the same `narrowableDataAttributes` + `genericParamOrder` tables the `nodeTypes` generator uses, keeping the constructor's generics in lockstep with the interface's. diff --git a/packages/nodes/src/ConstantNode.ts b/packages/nodes/src/ConstantNode.ts deleted file mode 100644 index 2dd21df16..000000000 --- a/packages/nodes/src/ConstantNode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { ConstantNode, Docs, TypeNode, ValueNode } from '@codama/node-types'; - -import { camelCase } from './shared'; - -export function constantNode( - name: string, - type: TType, - value: TValue, - docs?: Docs, -): ConstantNode { - return Object.freeze({ - kind: 'constantNode', - - // data. - name: camelCase(name), - docs, - - // children. - type, - value, - }); -} diff --git a/packages/nodes/src/ConstantPdaSeedNode.ts b/packages/nodes/src/ConstantPdaSeedNode.ts new file mode 100644 index 000000000..4a25df72a --- /dev/null +++ b/packages/nodes/src/ConstantPdaSeedNode.ts @@ -0,0 +1,21 @@ +import type { BytesEncoding } from '@codama/node-types'; + +import { programIdValueNode } from './generated/contextualValueNodes/ProgramIdValueNode'; +import { constantPdaSeedNode } from './generated/pdaSeedNodes/ConstantPdaSeedNode'; +import { bytesTypeNode } from './generated/typeNodes/BytesTypeNode'; +import { publicKeyTypeNode } from './generated/typeNodes/PublicKeyTypeNode'; +import { stringTypeNode } from './generated/typeNodes/StringTypeNode'; +import { bytesValueNode } from './generated/valueNodes/BytesValueNode'; +import { stringValueNode } from './generated/valueNodes/StringValueNode'; + +export function constantPdaSeedNodeFromProgramId() { + return constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode()); +} + +export function constantPdaSeedNodeFromString(encoding: TEncoding, string: string) { + return constantPdaSeedNode(stringTypeNode(encoding), stringValueNode(string)); +} + +export function constantPdaSeedNodeFromBytes(encoding: TEncoding, data: string) { + return constantPdaSeedNode(bytesTypeNode(), bytesValueNode(encoding, data)); +} diff --git a/packages/nodes/src/ConstantValueNode.ts b/packages/nodes/src/ConstantValueNode.ts new file mode 100644 index 000000000..0dc996fcf --- /dev/null +++ b/packages/nodes/src/ConstantValueNode.ts @@ -0,0 +1,15 @@ +import type { BytesEncoding } from '@codama/node-types'; + +import { bytesTypeNode } from './generated/typeNodes/BytesTypeNode'; +import { stringTypeNode } from './generated/typeNodes/StringTypeNode'; +import { bytesValueNode } from './generated/valueNodes/BytesValueNode'; +import { constantValueNode } from './generated/valueNodes/ConstantValueNode'; +import { stringValueNode } from './generated/valueNodes/StringValueNode'; + +export function constantValueNodeFromString(encoding: TEncoding, string: string) { + return constantValueNode(stringTypeNode(encoding), stringValueNode(string)); +} + +export function constantValueNodeFromBytes(encoding: TEncoding, data: string) { + return constantValueNode(bytesTypeNode(), bytesValueNode(encoding, data)); +} diff --git a/packages/nodes/src/EnumTypeNode.ts b/packages/nodes/src/EnumTypeNode.ts new file mode 100644 index 000000000..ef280569b --- /dev/null +++ b/packages/nodes/src/EnumTypeNode.ts @@ -0,0 +1,9 @@ +import type { EnumTypeNode } from '@codama/node-types'; + +export function isScalarEnum(node: EnumTypeNode): boolean { + return node.variants.every(variant => variant.kind === 'enumEmptyVariantTypeNode'); +} + +export function isDataEnum(node: EnumTypeNode): boolean { + return !isScalarEnum(node); +} diff --git a/packages/nodes/src/InstructionArgumentNode.ts b/packages/nodes/src/InstructionArgumentNode.ts index 172cf2796..e8a8b13cb 100644 --- a/packages/nodes/src/InstructionArgumentNode.ts +++ b/packages/nodes/src/InstructionArgumentNode.ts @@ -1,41 +1,16 @@ -import type { InstructionArgumentNode, InstructionInputValueNode } from '@codama/node-types'; +import type { InstructionArgumentNode } from '@codama/node-types'; +import { structFieldTypeNode } from './generated/typeNodes/StructFieldTypeNode'; +import { structTypeNode } from './generated/typeNodes/StructTypeNode'; +import { VALUE_NODE_KINDS } from './generated/valueNodes/ValueNode'; import { isNode } from './Node'; -import { camelCase, DocsInput, parseDocs } from './shared'; -import { structFieldTypeNode } from './typeNodes/StructFieldTypeNode'; -import { structTypeNode } from './typeNodes/StructTypeNode'; -import { VALUE_NODES } from './valueNodes'; - -export type InstructionArgumentNodeInput< - TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, -> = Omit, 'docs' | 'kind' | 'name'> & { - readonly docs?: DocsInput; - readonly name: string; -}; - -export function instructionArgumentNode( - input: InstructionArgumentNodeInput, -): InstructionArgumentNode { - return Object.freeze({ - kind: 'instructionArgumentNode', - - // Data. - name: camelCase(input.name), - ...(input.defaultValueStrategy !== undefined && { defaultValueStrategy: input.defaultValueStrategy }), - docs: parseDocs(input.docs), - - // Children. - type: input.type, - ...(input.defaultValue !== undefined && { defaultValue: input.defaultValue }), - }); -} export function structTypeNodeFromInstructionArgumentNodes(nodes: InstructionArgumentNode[]) { return structTypeNode(nodes.map(structFieldTypeNodeFromInstructionArgumentNode)); } export function structFieldTypeNodeFromInstructionArgumentNode(node: InstructionArgumentNode) { - if (isNode(node.defaultValue, VALUE_NODES)) { + if (isNode(node.defaultValue, VALUE_NODE_KINDS)) { return structFieldTypeNode({ ...node, defaultValue: node.defaultValue }); } return structFieldTypeNode({ diff --git a/packages/nodes/src/InstructionNode.ts b/packages/nodes/src/InstructionNode.ts index 03d2cfd29..0351d2482 100644 --- a/packages/nodes/src/InstructionNode.ts +++ b/packages/nodes/src/InstructionNode.ts @@ -1,10 +1,6 @@ import type { - DiscriminatorNode, - InstructionAccountNode, InstructionArgumentNode, - InstructionByteDeltaNode, InstructionNode, - InstructionRemainingAccountsNode, OptionalAccountStrategy, ProgramNode, RootNode, @@ -12,84 +8,6 @@ import type { import { isNode } from './Node'; import { getAllInstructions } from './ProgramNode'; -import { camelCase, DocsInput, parseDocs } from './shared'; - -type SubInstructionNode = InstructionNode; - -export type InstructionNodeInput< - TAccounts extends InstructionAccountNode[] = InstructionAccountNode[], - TArguments extends InstructionArgumentNode[] = InstructionArgumentNode[], - TExtraArguments extends InstructionArgumentNode[] | undefined = InstructionArgumentNode[] | undefined, - TRemainingAccounts extends InstructionRemainingAccountsNode[] | undefined = - | InstructionRemainingAccountsNode[] - | undefined, - TByteDeltas extends InstructionByteDeltaNode[] | undefined = InstructionByteDeltaNode[] | undefined, - TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, - TSubInstructions extends SubInstructionNode[] | undefined = SubInstructionNode[] | undefined, -> = Omit< - Partial< - InstructionNode< - TAccounts, - TArguments, - TExtraArguments, - TRemainingAccounts, - TByteDeltas, - TDiscriminators, - TSubInstructions - > - >, - 'docs' | 'kind' | 'name' -> & { - readonly docs?: DocsInput; - readonly name: string; -}; - -export function instructionNode< - const TAccounts extends InstructionAccountNode[] = [], - const TArguments extends InstructionArgumentNode[] = [], - const TExtraArguments extends InstructionArgumentNode[] | undefined = undefined, - const TRemainingAccounts extends InstructionRemainingAccountsNode[] | undefined = undefined, - const TByteDeltas extends InstructionByteDeltaNode[] | undefined = undefined, - const TDiscriminators extends DiscriminatorNode[] | undefined = undefined, - const TSubInstructions extends SubInstructionNode[] | undefined = undefined, ->( - input: InstructionNodeInput< - TAccounts, - TArguments, - TExtraArguments, - TRemainingAccounts, - TByteDeltas, - TDiscriminators, - TSubInstructions - >, -): InstructionNode< - TAccounts, - TArguments, - TExtraArguments, - TRemainingAccounts, - TByteDeltas, - TDiscriminators, - TSubInstructions -> { - return Object.freeze({ - kind: 'instructionNode', - - // Data. - name: camelCase(input.name), - docs: parseDocs(input.docs), - optionalAccountStrategy: parseOptionalAccountStrategy(input.optionalAccountStrategy), - ...(input.status !== undefined && { status: input.status }), - - // Children. - accounts: (input.accounts ?? []) as TAccounts, - arguments: (input.arguments ?? []) as TArguments, - extraArguments: input.extraArguments, - remainingAccounts: input.remainingAccounts, - byteDeltas: input.byteDeltas, - discriminators: input.discriminators, - subInstructions: input.subInstructions, - }); -} export function parseOptionalAccountStrategy( optionalAccountStrategy: OptionalAccountStrategy | undefined, diff --git a/packages/nodes/src/typeNodes/NestedTypeNode.ts b/packages/nodes/src/NestedTypeNode.ts similarity index 93% rename from packages/nodes/src/typeNodes/NestedTypeNode.ts rename to packages/nodes/src/NestedTypeNode.ts index 9c7d495a3..76da4ac40 100644 --- a/packages/nodes/src/typeNodes/NestedTypeNode.ts +++ b/packages/nodes/src/NestedTypeNode.ts @@ -1,8 +1,8 @@ import { CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND, CodamaError } from '@codama/errors'; import type { NestedTypeNode, Node, TypeNode } from '@codama/node-types'; -import { isNode } from '../Node'; -import { TYPE_NODES } from './TypeNode'; +import { TYPE_NODE_KINDS } from './generated/typeNodes/TypeNode'; +import { isNode } from './Node'; export function resolveNestedTypeNode(typeNode: NestedTypeNode): TType { switch (typeNode.kind) { @@ -44,7 +44,7 @@ export function isNestedTypeNode( node: Node | null | undefined, kind: TKind | TKind[], ): node is NestedTypeNode> { - if (!isNode(node, TYPE_NODES)) return false; + if (!isNode(node, TYPE_NODE_KINDS)) return false; const kinds = Array.isArray(kind) ? kind : [kind]; const resolved = resolveNestedTypeNode(node); return !!node && kinds.includes(resolved.kind as TKind); diff --git a/packages/nodes/src/Node.ts b/packages/nodes/src/Node.ts index b8c85671f..b22b6eec5 100644 --- a/packages/nodes/src/Node.ts +++ b/packages/nodes/src/Node.ts @@ -1,41 +1,6 @@ import { CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError } from '@codama/errors'; import type { GetNodeFromKind, Node, NodeKind } from '@codama/node-types'; -import { REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS } from './contextualValueNodes/ContextualValueNode'; -import { REGISTERED_COUNT_NODE_KINDS } from './countNodes/CountNode'; -import { REGISTERED_DISCRIMINATOR_NODE_KINDS } from './discriminatorNodes/DiscriminatorNode'; -import { REGISTERED_LINK_NODE_KINDS } from './linkNodes/LinkNode'; -import { REGISTERED_PDA_SEED_NODE_KINDS } from './pdaSeedNodes/PdaSeedNode'; -import { REGISTERED_TYPE_NODE_KINDS } from './typeNodes/TypeNode'; -import { REGISTERED_VALUE_NODE_KINDS } from './valueNodes/ValueNode'; - -// Node Registration. -export const REGISTERED_NODE_KINDS = [ - ...REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS, - ...REGISTERED_DISCRIMINATOR_NODE_KINDS, - ...REGISTERED_LINK_NODE_KINDS, - ...REGISTERED_PDA_SEED_NODE_KINDS, - ...REGISTERED_COUNT_NODE_KINDS, - ...REGISTERED_TYPE_NODE_KINDS, - ...REGISTERED_VALUE_NODE_KINDS, - 'rootNode' as const, - 'programNode' as const, - 'pdaNode' as const, - 'accountNode' as const, - 'eventNode' as const, - 'constantNode' as const, - 'instructionAccountNode' as const, - 'instructionArgumentNode' as const, - 'instructionByteDeltaNode' as const, - 'instructionNode' as const, - 'instructionRemainingAccountsNode' as const, - 'instructionStatusNode' as const, - 'errorNode' as const, - 'definedTypeNode' as const, -]; - -// Node Helpers. - export function isNode( node: Node | null | undefined, kind: TKind | TKind[], diff --git a/packages/nodes/src/typeNodes/NumberTypeNode.ts b/packages/nodes/src/NumberTypeNode.ts similarity index 56% rename from packages/nodes/src/typeNodes/NumberTypeNode.ts rename to packages/nodes/src/NumberTypeNode.ts index 473849fc6..5bf0166fc 100644 --- a/packages/nodes/src/typeNodes/NumberTypeNode.ts +++ b/packages/nodes/src/NumberTypeNode.ts @@ -1,17 +1,4 @@ -import type { NumberFormat, NumberTypeNode } from '@codama/node-types'; - -export function numberTypeNode( - format: TFormat, - endian: 'be' | 'le' = 'le', -): NumberTypeNode { - return Object.freeze({ - kind: 'numberTypeNode', - - // Data. - format, - endian, - }); -} +import type { NumberTypeNode } from '@codama/node-types'; export function isSignedInteger(node: NumberTypeNode): boolean { return node.format.startsWith('i'); diff --git a/packages/nodes/src/PdaNode.ts b/packages/nodes/src/PdaNode.ts deleted file mode 100644 index 3150e26ea..000000000 --- a/packages/nodes/src/PdaNode.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { PdaNode, PdaSeedNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from './shared'; - -export type PdaNodeInput = Omit< - PdaNode, - 'docs' | 'kind' | 'name' -> & { - readonly docs?: DocsInput; - readonly name: string; -}; - -export function pdaNode(input: PdaNodeInput): PdaNode { - return Object.freeze({ - kind: 'pdaNode', - - // Data. - name: camelCase(input.name), - docs: parseDocs(input.docs), - ...(input.programId && { programId: input.programId }), - - // Children. - seeds: input.seeds, - }); -} diff --git a/packages/nodes/src/ProgramNode.ts b/packages/nodes/src/ProgramNode.ts index e4cf60b0e..0b7203d17 100644 --- a/packages/nodes/src/ProgramNode.ts +++ b/packages/nodes/src/ProgramNode.ts @@ -10,57 +10,6 @@ import type { RootNode, } from '@codama/node-types'; -import { camelCase, DocsInput, parseDocs } from './shared'; - -export type ProgramNodeInput< - TPdas extends PdaNode[] = PdaNode[], - TAccounts extends AccountNode[] = AccountNode[], - TInstructions extends InstructionNode[] = InstructionNode[], - TDefinedTypes extends DefinedTypeNode[] = DefinedTypeNode[], - TErrors extends ErrorNode[] = ErrorNode[], - TEvents extends EventNode[] = EventNode[], - TConstants extends ConstantNode[] = ConstantNode[], -> = Omit< - Partial>, - 'docs' | 'kind' | 'name' | 'publicKey' -> & { - readonly docs?: DocsInput; - readonly name: string; - readonly publicKey: ProgramNode['publicKey']; -}; - -export function programNode< - const TPdas extends PdaNode[] = [], - const TAccounts extends AccountNode[] = [], - const TInstructions extends InstructionNode[] = [], - const TDefinedTypes extends DefinedTypeNode[] = [], - const TErrors extends ErrorNode[] = [], - const TEvents extends EventNode[] = [], - const TConstants extends ConstantNode[] = [], ->( - input: ProgramNodeInput, -): ProgramNode { - return Object.freeze({ - kind: 'programNode', - - // Data. - name: camelCase(input.name), - publicKey: input.publicKey, - version: input.version ?? '0.0.0', - ...(input.origin !== undefined && { origin: input.origin }), - docs: parseDocs(input.docs), - - // Children. - accounts: (input.accounts ?? []) as TAccounts, - instructions: (input.instructions ?? []) as TInstructions, - definedTypes: (input.definedTypes ?? []) as TDefinedTypes, - pdas: (input.pdas ?? []) as TPdas, - events: (input.events ?? []) as TEvents, - errors: (input.errors ?? []) as TErrors, - constants: (input.constants ?? []) as TConstants, - }); -} - export function getAllPrograms(node: ProgramNode | ProgramNode[] | RootNode): ProgramNode[] { if (Array.isArray(node)) return node; if (node.kind === 'programNode') return [node]; diff --git a/packages/nodes/src/RootNode.ts b/packages/nodes/src/RootNode.ts deleted file mode 100644 index 0d73eaa8a..000000000 --- a/packages/nodes/src/RootNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { CodamaVersion, ProgramNode, RootNode } from '@codama/node-types'; - -export function rootNode( - program: TProgram, - additionalPrograms?: TAdditionalPrograms, -): RootNode { - return Object.freeze({ - kind: 'rootNode', - - // Data. - standard: 'codama', - version: __VERSION__ as CodamaVersion, - - // Children. - program, - additionalPrograms: (additionalPrograms ?? []) as TAdditionalPrograms, - }); -} diff --git a/packages/nodes/src/contextualValueNodes/ConditionalValueNode.ts b/packages/nodes/src/contextualValueNodes/ConditionalValueNode.ts deleted file mode 100644 index 290ed4039..000000000 --- a/packages/nodes/src/contextualValueNodes/ConditionalValueNode.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - AccountValueNode, - ArgumentValueNode, - ConditionalValueNode, - InstructionInputValueNode, - ResolverValueNode, - ValueNode, -} from '@codama/node-types'; - -type ConditionNode = AccountValueNode | ArgumentValueNode | ResolverValueNode; - -export function conditionalValueNode< - TCondition extends ConditionNode, - TValue extends ValueNode | undefined = undefined, - TIfTrue extends InstructionInputValueNode | undefined = undefined, - TIfFalse extends InstructionInputValueNode | undefined = undefined, ->(input: { - condition: TCondition; - ifFalse?: TIfFalse; - ifTrue?: TIfTrue; - value?: TValue; -}): ConditionalValueNode { - return Object.freeze({ - kind: 'conditionalValueNode', - - // Children. - condition: input.condition, - ...(input.value !== undefined && { value: input.value }), - ...(input.ifTrue !== undefined && { ifTrue: input.ifTrue }), - ...(input.ifFalse !== undefined && { ifFalse: input.ifFalse }), - }); -} diff --git a/packages/nodes/src/contextualValueNodes/ContextualValueNode.ts b/packages/nodes/src/contextualValueNodes/ContextualValueNode.ts deleted file mode 100644 index b2568b73f..000000000 --- a/packages/nodes/src/contextualValueNodes/ContextualValueNode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { VALUE_NODES } from '../valueNodes/ValueNode'; - -// Standalone Contextual Value Node Registration. -export const STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS = [ - 'accountBumpValueNode' as const, - 'accountValueNode' as const, - 'argumentValueNode' as const, - 'conditionalValueNode' as const, - 'identityValueNode' as const, - 'payerValueNode' as const, - 'pdaValueNode' as const, - 'programIdValueNode' as const, - 'resolverValueNode' as const, -]; - -// Contextual Value Node Registration. -export const REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS = [ - ...STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS, - 'pdaSeedValueNode' as const, -]; - -// Contextual Value Node Helpers. -export const CONTEXTUAL_VALUE_NODES = STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS; -export const INSTRUCTION_INPUT_VALUE_NODES = [...VALUE_NODES, ...CONTEXTUAL_VALUE_NODES, 'programLinkNode' as const]; diff --git a/packages/nodes/src/contextualValueNodes/IdentityValueNode.ts b/packages/nodes/src/contextualValueNodes/IdentityValueNode.ts deleted file mode 100644 index 8a6763526..000000000 --- a/packages/nodes/src/contextualValueNodes/IdentityValueNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { IdentityValueNode } from '@codama/node-types'; - -export function identityValueNode(): IdentityValueNode { - return Object.freeze({ kind: 'identityValueNode' }); -} diff --git a/packages/nodes/src/contextualValueNodes/PayerValueNode.ts b/packages/nodes/src/contextualValueNodes/PayerValueNode.ts deleted file mode 100644 index e44471fd2..000000000 --- a/packages/nodes/src/contextualValueNodes/PayerValueNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PayerValueNode } from '@codama/node-types'; - -export function payerValueNode(): PayerValueNode { - return Object.freeze({ kind: 'payerValueNode' }); -} diff --git a/packages/nodes/src/contextualValueNodes/PdaSeedValueNode.ts b/packages/nodes/src/contextualValueNodes/PdaSeedValueNode.ts deleted file mode 100644 index 1ab252efd..000000000 --- a/packages/nodes/src/contextualValueNodes/PdaSeedValueNode.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AccountValueNode, ArgumentValueNode, PdaSeedValueNode, ValueNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; - -export function pdaSeedValueNode< - TValue extends AccountValueNode | ArgumentValueNode | ValueNode = AccountValueNode | ArgumentValueNode | ValueNode, ->(name: string, value: TValue): PdaSeedValueNode { - return Object.freeze({ - kind: 'pdaSeedValueNode', - - // Data. - name: camelCase(name), - - // Children. - value, - }); -} diff --git a/packages/nodes/src/contextualValueNodes/PdaValueNode.ts b/packages/nodes/src/contextualValueNodes/PdaValueNode.ts deleted file mode 100644 index 951cbb230..000000000 --- a/packages/nodes/src/contextualValueNodes/PdaValueNode.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { - AccountValueNode, - ArgumentValueNode, - PdaLinkNode, - PdaNode, - PdaSeedValueNode, - PdaValueNode, -} from '@codama/node-types'; - -import { pdaLinkNode } from '../linkNodes'; - -export function pdaValueNode< - const TSeeds extends PdaSeedValueNode[] = [], - const TProgram extends AccountValueNode | ArgumentValueNode | undefined = undefined, ->( - pda: PdaLinkNode | PdaNode | string, - seeds: TSeeds = [] as PdaSeedValueNode[] as TSeeds, - programId: TProgram = undefined as TProgram, -): PdaValueNode { - return Object.freeze({ - kind: 'pdaValueNode', - - // Children. - pda: typeof pda === 'string' ? pdaLinkNode(pda) : pda, - seeds, - ...(programId ? { programId } : {}), - }); -} diff --git a/packages/nodes/src/contextualValueNodes/ProgramIdValueNode.ts b/packages/nodes/src/contextualValueNodes/ProgramIdValueNode.ts deleted file mode 100644 index 6030bb881..000000000 --- a/packages/nodes/src/contextualValueNodes/ProgramIdValueNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ProgramIdValueNode } from '@codama/node-types'; - -export function programIdValueNode(): ProgramIdValueNode { - return Object.freeze({ kind: 'programIdValueNode' }); -} diff --git a/packages/nodes/src/contextualValueNodes/ResolverValueNode.ts b/packages/nodes/src/contextualValueNodes/ResolverValueNode.ts deleted file mode 100644 index 0451ca75a..000000000 --- a/packages/nodes/src/contextualValueNodes/ResolverValueNode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { AccountValueNode, ArgumentValueNode, ResolverValueNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from '../shared'; - -export function resolverValueNode( - name: string, - options: { - dependsOn?: TDependsOn; - docs?: DocsInput; - } = {}, -): ResolverValueNode { - return Object.freeze({ - kind: 'resolverValueNode', - - // Data. - name: camelCase(name), - docs: parseDocs(options.docs), - - // Children. - dependsOn: options.dependsOn, - }); -} diff --git a/packages/nodes/src/countNodes/RemainderCountNode.ts b/packages/nodes/src/countNodes/RemainderCountNode.ts deleted file mode 100644 index 82775beec..000000000 --- a/packages/nodes/src/countNodes/RemainderCountNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { RemainderCountNode } from '@codama/node-types'; - -export function remainderCountNode(): RemainderCountNode { - return Object.freeze({ kind: 'remainderCountNode' }); -} diff --git a/packages/nodes/src/AccountNode.ts b/packages/nodes/src/generated/AccountNode.ts similarity index 52% rename from packages/nodes/src/AccountNode.ts rename to packages/nodes/src/generated/AccountNode.ts index a89b991c8..ae8c47c68 100644 --- a/packages/nodes/src/AccountNode.ts +++ b/packages/nodes/src/generated/AccountNode.ts @@ -1,30 +1,30 @@ import type { AccountNode, DiscriminatorNode, NestedTypeNode, PdaLinkNode, StructTypeNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from './shared'; -import { structTypeNode } from './typeNodes'; +import { camelCase, DocsInput, parseDocs } from '../shared'; +import { structTypeNode } from './typeNodes/StructTypeNode'; export type AccountNodeInput< TData extends NestedTypeNode = NestedTypeNode, TPda extends PdaLinkNode | undefined = PdaLinkNode | undefined, - TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, -> = Omit, 'data' | 'docs' | 'kind' | 'name'> & { - readonly data?: TData; - readonly docs?: DocsInput; + TDiscriminators extends Array | undefined = Array | undefined, +> = Omit>, 'docs' | 'kind' | 'name'> & { readonly name: string; + readonly docs?: DocsInput; }; +/** An on-chain account: its name, data structure, optional fixed size, optional PDA, and optional discriminators. */ export function accountNode< - TData extends NestedTypeNode = StructTypeNode<[]>, - TPda extends PdaLinkNode | undefined = undefined, - const TDiscriminators extends DiscriminatorNode[] | undefined = undefined, + const TData extends NestedTypeNode = StructTypeNode<[]>, + const TPda extends PdaLinkNode | undefined = undefined, + const TDiscriminators extends Array | undefined = undefined, >(input: AccountNodeInput): AccountNode { + const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'accountNode', // Data. name: camelCase(input.name), - ...(input.size === undefined ? {} : { size: input.size }), - docs: parseDocs(input.docs), + ...(input.size !== undefined && { size: input.size }), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), // Children. data: (input.data ?? structTypeNode([])) as TData, diff --git a/packages/nodes/src/generated/ConstantNode.ts b/packages/nodes/src/generated/ConstantNode.ts new file mode 100644 index 000000000..485677eb7 --- /dev/null +++ b/packages/nodes/src/generated/ConstantNode.ts @@ -0,0 +1,23 @@ +import type { ConstantNode, TypeNode, ValueNode } from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../shared'; + +/** A named constant exposed by the program: a typed value associated with a name. */ +export function constantNode( + name: string, + type: TType, + value: TValue, + docs?: DocsInput, +): ConstantNode { + const parsedDocs = parseDocs(docs); + return Object.freeze({ + kind: 'constantNode', + + // Data. + name: camelCase(name), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), + + // Children. + type, + value, + }); +} diff --git a/packages/nodes/src/DefinedTypeNode.ts b/packages/nodes/src/generated/DefinedTypeNode.ts similarity index 52% rename from packages/nodes/src/DefinedTypeNode.ts rename to packages/nodes/src/generated/DefinedTypeNode.ts index 241aa9811..eead30d73 100644 --- a/packages/nodes/src/DefinedTypeNode.ts +++ b/packages/nodes/src/generated/DefinedTypeNode.ts @@ -1,22 +1,25 @@ import type { DefinedTypeNode, TypeNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from './shared'; +import { camelCase, DocsInput, parseDocs } from '../shared'; export type DefinedTypeNodeInput = Omit< DefinedTypeNode, 'docs' | 'kind' | 'name' > & { - readonly docs?: DocsInput; readonly name: string; + readonly docs?: DocsInput; }; -export function definedTypeNode(input: DefinedTypeNodeInput): DefinedTypeNode { +/** A reusable named type that can be referenced by `definedTypeLinkNode` from elsewhere in the IDL. */ +export function definedTypeNode( + input: DefinedTypeNodeInput, +): DefinedTypeNode { + const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'definedTypeNode', // Data. name: camelCase(input.name), - docs: parseDocs(input.docs), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), // Children. type: input.type, diff --git a/packages/nodes/src/ErrorNode.ts b/packages/nodes/src/generated/ErrorNode.ts similarity index 62% rename from packages/nodes/src/ErrorNode.ts rename to packages/nodes/src/generated/ErrorNode.ts index 47cb0bbcc..43efb45f5 100644 --- a/packages/nodes/src/ErrorNode.ts +++ b/packages/nodes/src/generated/ErrorNode.ts @@ -1,13 +1,14 @@ import type { ErrorNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from './shared'; +import { camelCase, DocsInput, parseDocs } from '../shared'; export type ErrorNodeInput = Omit & { - readonly docs?: DocsInput; readonly name: string; + readonly docs?: DocsInput; }; +/** A program error — a numeric code paired with a name and human-readable message. */ export function errorNode(input: ErrorNodeInput): ErrorNode { + const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'errorNode', @@ -15,6 +16,6 @@ export function errorNode(input: ErrorNodeInput): ErrorNode { name: camelCase(input.name), code: input.code, message: input.message, - docs: parseDocs(input.docs), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), }); } diff --git a/packages/nodes/src/EventNode.ts b/packages/nodes/src/generated/EventNode.ts similarity index 57% rename from packages/nodes/src/EventNode.ts rename to packages/nodes/src/generated/EventNode.ts index 0bf54ad5e..5a973d891 100644 --- a/packages/nodes/src/EventNode.ts +++ b/packages/nodes/src/generated/EventNode.ts @@ -1,26 +1,26 @@ import type { DiscriminatorNode, EventNode, TypeNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from './shared'; -import { structTypeNode } from './typeNodes'; +import { camelCase, DocsInput, parseDocs } from '../shared'; export type EventNodeInput< TData extends TypeNode = TypeNode, - TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, + TDiscriminators extends Array | undefined = Array | undefined, > = Omit, 'docs' | 'kind' | 'name'> & { - readonly docs?: DocsInput; readonly name: string; + readonly docs?: DocsInput; }; +/** A program event: its data shape and optional discriminators used to identify it on the wire. */ export function eventNode< - TData extends TypeNode = ReturnType, - const TDiscriminators extends DiscriminatorNode[] | undefined = undefined, + const TData extends TypeNode, + const TDiscriminators extends Array | undefined = undefined, >(input: EventNodeInput): EventNode { + const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'eventNode', // Data. name: camelCase(input.name), - docs: parseDocs(input.docs), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), // Children. data: input.data, diff --git a/packages/nodes/src/InstructionAccountNode.ts b/packages/nodes/src/generated/InstructionAccountNode.ts similarity index 60% rename from packages/nodes/src/InstructionAccountNode.ts rename to packages/nodes/src/generated/InstructionAccountNode.ts index 4b5a337c5..6e5123964 100644 --- a/packages/nodes/src/InstructionAccountNode.ts +++ b/packages/nodes/src/generated/InstructionAccountNode.ts @@ -1,18 +1,18 @@ import type { InstructionAccountNode, InstructionInputValueNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from './shared'; +import { camelCase, DocsInput, parseDocs } from '../shared'; export type InstructionAccountNodeInput< TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, -> = Omit, 'docs' | 'isOptional' | 'kind' | 'name'> & { - readonly docs?: DocsInput; - readonly isOptional?: boolean; +> = Omit, 'docs' | 'kind' | 'name'> & { readonly name: string; + readonly docs?: DocsInput; }; -export function instructionAccountNode( +/** An account participating in an instruction, with its name, signing/writability flags, and an optional default value. */ +export function instructionAccountNode( input: InstructionAccountNodeInput, ): InstructionAccountNode { + const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'instructionAccountNode', @@ -21,7 +21,7 @@ export function instructionAccountNode 0 && { docs: parsedDocs }), // Children. ...(input.defaultValue !== undefined && { defaultValue: input.defaultValue }), diff --git a/packages/nodes/src/generated/InstructionArgumentNode.ts b/packages/nodes/src/generated/InstructionArgumentNode.ts new file mode 100644 index 000000000..127aaf321 --- /dev/null +++ b/packages/nodes/src/generated/InstructionArgumentNode.ts @@ -0,0 +1,30 @@ +import type { InstructionArgumentNode, InstructionInputValueNode, TypeNode } from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../shared'; + +export type InstructionArgumentNodeInput< + TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, + TType extends TypeNode = TypeNode, +> = Omit, 'docs' | 'kind' | 'name'> & { + readonly name: string; + readonly docs?: DocsInput; +}; + +/** A named argument of an instruction, with its type and an optional default value. */ +export function instructionArgumentNode< + const TDefaultValue extends InstructionInputValueNode | undefined = undefined, + const TType extends TypeNode = TypeNode, +>(input: InstructionArgumentNodeInput): InstructionArgumentNode { + const parsedDocs = parseDocs(input.docs); + return Object.freeze({ + kind: 'instructionArgumentNode', + + // Data. + name: camelCase(input.name), + ...(input.defaultValueStrategy !== undefined && { defaultValueStrategy: input.defaultValueStrategy }), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), + + // Children. + type: input.type, + ...(input.defaultValue !== undefined && { defaultValue: input.defaultValue }), + }); +} diff --git a/packages/nodes/src/InstructionByteDeltaNode.ts b/packages/nodes/src/generated/InstructionByteDeltaNode.ts similarity index 55% rename from packages/nodes/src/InstructionByteDeltaNode.ts rename to packages/nodes/src/generated/InstructionByteDeltaNode.ts index d96eb921a..51e3a0bbc 100644 --- a/packages/nodes/src/InstructionByteDeltaNode.ts +++ b/packages/nodes/src/generated/InstructionByteDeltaNode.ts @@ -1,12 +1,12 @@ -import type { InstructionByteDeltaNode } from '@codama/node-types'; +import type { InstructionByteDeltaNode, InstructionByteDeltaValue } from '@codama/node-types'; +import { isNode } from '../Node'; -import { isNode } from './Node'; - -export function instructionByteDeltaNode( +/** A byte-size delta applied when computing rent or buffer size — typically used by instructions that resize accounts. */ +export function instructionByteDeltaNode( value: TValue, options: { - subtract?: boolean; withHeader?: boolean; + subtract?: boolean; } = {}, ): InstructionByteDeltaNode { return Object.freeze({ diff --git a/packages/nodes/src/generated/InstructionByteDeltaValue.ts b/packages/nodes/src/generated/InstructionByteDeltaValue.ts new file mode 100644 index 000000000..005b1ea1d --- /dev/null +++ b/packages/nodes/src/generated/InstructionByteDeltaValue.ts @@ -0,0 +1,7 @@ +/** The value forms accepted by an `instructionByteDeltaNode`. */ +export const INSTRUCTION_BYTE_DELTA_VALUE_KINDS = [ + 'accountLinkNode' as const, + 'argumentValueNode' as const, + 'numberValueNode' as const, + 'resolverValueNode' as const, +]; diff --git a/packages/nodes/src/generated/InstructionNode.ts b/packages/nodes/src/generated/InstructionNode.ts new file mode 100644 index 000000000..7885d19b6 --- /dev/null +++ b/packages/nodes/src/generated/InstructionNode.ts @@ -0,0 +1,92 @@ +import type { + DiscriminatorNode, + InstructionAccountNode, + InstructionArgumentNode, + InstructionByteDeltaNode, + InstructionNode, + InstructionRemainingAccountsNode, + InstructionStatusNode, +} from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../shared'; + +export type InstructionNodeInput< + 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, +> = Omit< + Partial< + InstructionNode< + TAccounts, + TArguments, + TExtraArguments, + TRemainingAccounts, + TByteDeltas, + TDiscriminators, + TSubInstructions, + TStatus + > + >, + 'docs' | 'kind' | 'name' +> & { + readonly name: string; + readonly docs?: DocsInput; +}; + +/** A program instruction: its accounts, arguments, byte-delta hints, discriminators, optional status, and optional sub-instructions. */ +export function instructionNode< + const TAccounts extends Array = [], + const TArguments extends Array = [], + const TExtraArguments extends Array | undefined = undefined, + const TRemainingAccounts extends Array | undefined = undefined, + const TByteDeltas extends Array | undefined = undefined, + const TDiscriminators extends Array | undefined = undefined, + const TSubInstructions extends Array | undefined = undefined, + const TStatus extends InstructionStatusNode | undefined = undefined, +>( + input: InstructionNodeInput< + TAccounts, + TArguments, + TExtraArguments, + TRemainingAccounts, + TByteDeltas, + TDiscriminators, + TSubInstructions, + TStatus + >, +): InstructionNode< + TAccounts, + TArguments, + TExtraArguments, + TRemainingAccounts, + TByteDeltas, + TDiscriminators, + TSubInstructions, + TStatus +> { + const parsedDocs = parseDocs(input.docs); + return Object.freeze({ + kind: 'instructionNode', + + // Data. + name: camelCase(input.name), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), + optionalAccountStrategy: input.optionalAccountStrategy ?? 'programId', + + // Children. + accounts: (input.accounts ?? []) as TAccounts, + arguments: (input.arguments ?? []) as TArguments, + ...(input.extraArguments !== undefined && { extraArguments: input.extraArguments }), + ...(input.remainingAccounts !== undefined && { remainingAccounts: input.remainingAccounts }), + ...(input.byteDeltas !== undefined && { byteDeltas: input.byteDeltas }), + ...(input.discriminators !== undefined && { discriminators: input.discriminators }), + ...(input.status !== undefined && { status: input.status }), + ...(input.subInstructions !== undefined && { subInstructions: input.subInstructions }), + }); +} diff --git a/packages/nodes/src/InstructionRemainingAccountsNode.ts b/packages/nodes/src/generated/InstructionRemainingAccountsNode.ts similarity index 55% rename from packages/nodes/src/InstructionRemainingAccountsNode.ts rename to packages/nodes/src/generated/InstructionRemainingAccountsNode.ts index 59e4ef972..58dff1beb 100644 --- a/packages/nodes/src/InstructionRemainingAccountsNode.ts +++ b/packages/nodes/src/generated/InstructionRemainingAccountsNode.ts @@ -1,16 +1,17 @@ -import type { ArgumentValueNode, InstructionRemainingAccountsNode, ResolverValueNode } from '@codama/node-types'; +import type { InstructionRemainingAccountsNode, InstructionRemainingAccountsValue } from '@codama/node-types'; +import { DocsInput, parseDocs } from '../shared'; -import { DocsInput, parseDocs } from './shared'; - -export function instructionRemainingAccountsNode( +/** A "remaining accounts" slot in an instruction — a variable-length tail of accounts derived from a value. */ +export function instructionRemainingAccountsNode( value: TValue, options: { - docs?: DocsInput; isOptional?: boolean; isSigner?: boolean | 'either'; isWritable?: boolean; + docs?: DocsInput; } = {}, ): InstructionRemainingAccountsNode { + const parsedDocs = parseDocs(options.docs); return Object.freeze({ kind: 'instructionRemainingAccountsNode', @@ -18,7 +19,7 @@ export function instructionRemainingAccountsNode 0 && { docs: parsedDocs }), // Children. value, diff --git a/packages/nodes/src/generated/InstructionRemainingAccountsValue.ts b/packages/nodes/src/generated/InstructionRemainingAccountsValue.ts new file mode 100644 index 000000000..d22a62fec --- /dev/null +++ b/packages/nodes/src/generated/InstructionRemainingAccountsValue.ts @@ -0,0 +1,2 @@ +/** The value forms accepted by an `instructionRemainingAccountsNode`. */ +export const INSTRUCTION_REMAINING_ACCOUNTS_VALUE_KINDS = ['argumentValueNode' as const, 'resolverValueNode' as const]; diff --git a/packages/nodes/src/InstructionStatusNode.ts b/packages/nodes/src/generated/InstructionStatusNode.ts similarity index 75% rename from packages/nodes/src/InstructionStatusNode.ts rename to packages/nodes/src/generated/InstructionStatusNode.ts index 68681068f..19d79775c 100644 --- a/packages/nodes/src/InstructionStatusNode.ts +++ b/packages/nodes/src/generated/InstructionStatusNode.ts @@ -1,5 +1,6 @@ import type { InstructionLifecycle, InstructionStatusNode } from '@codama/node-types'; +/** The lifecycle stage of an instruction (draft, live, deprecated, archived) with an optional accompanying message. */ export function instructionStatusNode(lifecycle: InstructionLifecycle, message?: string): InstructionStatusNode { return Object.freeze({ kind: 'instructionStatusNode', diff --git a/packages/nodes/src/generated/PdaNode.ts b/packages/nodes/src/generated/PdaNode.ts new file mode 100644 index 000000000..f7c7c5363 --- /dev/null +++ b/packages/nodes/src/generated/PdaNode.ts @@ -0,0 +1,26 @@ +import type { PdaNode, PdaSeedNode } from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../shared'; + +export type PdaNodeInput = Array> = Omit< + PdaNode, + 'docs' | 'kind' | 'name' +> & { + readonly name: string; + readonly docs?: DocsInput; +}; + +/** A program-derived address: its name, optional program ID override, and the seeds used to derive it. */ +export function pdaNode>(input: PdaNodeInput): PdaNode { + const parsedDocs = parseDocs(input.docs); + return Object.freeze({ + kind: 'pdaNode', + + // Data. + name: camelCase(input.name), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), + ...(input.programId !== undefined && { programId: input.programId }), + + // Children. + seeds: input.seeds, + }); +} diff --git a/packages/nodes/src/generated/ProgramNode.ts b/packages/nodes/src/generated/ProgramNode.ts new file mode 100644 index 000000000..2cbcab780 --- /dev/null +++ b/packages/nodes/src/generated/ProgramNode.ts @@ -0,0 +1,62 @@ +import type { + AccountNode, + ConstantNode, + DefinedTypeNode, + ErrorNode, + EventNode, + InstructionNode, + PdaNode, + ProgramNode, +} from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../shared'; + +export type ProgramNodeInput< + 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, +> = Omit< + Partial>, + 'docs' | 'kind' | 'name' | 'publicKey' +> & { + readonly name: string; + readonly docs?: DocsInput; + readonly publicKey: ProgramNode['publicKey']; +}; + +/** A Solana program: its identity, version, accounts, instructions, defined types, PDAs, events, errors, and constants. */ +export function programNode< + const TPdas extends Array = [], + const TAccounts extends Array = [], + const TInstructions extends Array = [], + const TDefinedTypes extends Array = [], + const TErrors extends Array = [], + const TEvents extends Array = [], + const TConstants extends Array = [], +>( + input: ProgramNodeInput, +): ProgramNode { + const parsedDocs = parseDocs(input.docs); + return Object.freeze({ + kind: 'programNode', + + // Data. + name: camelCase(input.name), + publicKey: input.publicKey, + version: input.version ?? '0.0.0', + ...(input.origin !== undefined && { origin: input.origin }), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), + + // Children. + accounts: (input.accounts ?? []) as TAccounts, + instructions: (input.instructions ?? []) as TInstructions, + definedTypes: (input.definedTypes ?? []) as TDefinedTypes, + pdas: (input.pdas ?? []) as TPdas, + events: (input.events ?? []) as TEvents, + errors: (input.errors ?? []) as TErrors, + constants: (input.constants ?? []) as TConstants, + }); +} diff --git a/packages/nodes/src/generated/RootNode.ts b/packages/nodes/src/generated/RootNode.ts new file mode 100644 index 000000000..fd5d59034 --- /dev/null +++ b/packages/nodes/src/generated/RootNode.ts @@ -0,0 +1,23 @@ +import type { ProgramNode, RootNode } from '@codama/node-types'; +import { CODAMA_VERSION } from './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 function rootNode = []>( + program: TProgram, + additionalPrograms: TAdditionalPrograms = [] as Array as TAdditionalPrograms, +): RootNode { + return Object.freeze({ + kind: 'rootNode', + + // Data. + standard: 'codama', + version: CODAMA_VERSION, + + // Children. + program, + additionalPrograms, + }); +} diff --git a/packages/nodes/src/generated/codamaVersion.ts b/packages/nodes/src/generated/codamaVersion.ts new file mode 100644 index 000000000..e20913c9a --- /dev/null +++ b/packages/nodes/src/generated/codamaVersion.ts @@ -0,0 +1,11 @@ +import type { CodamaVersion } from '@codama/node-types'; + +/** + * The Codama spec version this package was generated against. + * + * Pinned to the literal version of `@codama/spec` at generation time. + * Used by `rootNode()` to tag the document and by downstream consumers + * that need to compare an IDL document's `version` against the spec + * shape `@codama/nodes` understands. + */ +export const CODAMA_VERSION: CodamaVersion = '1.6.0'; diff --git a/packages/nodes/src/contextualValueNodes/AccountBumpValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/AccountBumpValueNode.ts similarity index 66% rename from packages/nodes/src/contextualValueNodes/AccountBumpValueNode.ts rename to packages/nodes/src/generated/contextualValueNodes/AccountBumpValueNode.ts index ba781731c..95cd606be 100644 --- a/packages/nodes/src/contextualValueNodes/AccountBumpValueNode.ts +++ b/packages/nodes/src/generated/contextualValueNodes/AccountBumpValueNode.ts @@ -1,7 +1,7 @@ import type { AccountBumpValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** Refers to the bump seed of a named PDA-derived account in the surrounding instruction. */ export function accountBumpValueNode(name: string): AccountBumpValueNode { return Object.freeze({ kind: 'accountBumpValueNode', diff --git a/packages/nodes/src/contextualValueNodes/AccountValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/AccountValueNode.ts similarity index 70% rename from packages/nodes/src/contextualValueNodes/AccountValueNode.ts rename to packages/nodes/src/generated/contextualValueNodes/AccountValueNode.ts index 0620540d3..c2e7c8ee9 100644 --- a/packages/nodes/src/contextualValueNodes/AccountValueNode.ts +++ b/packages/nodes/src/generated/contextualValueNodes/AccountValueNode.ts @@ -1,7 +1,7 @@ import type { AccountValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** Refers to a named account in the surrounding instruction. */ export function accountValueNode(name: string): AccountValueNode { return Object.freeze({ kind: 'accountValueNode', diff --git a/packages/nodes/src/contextualValueNodes/ArgumentValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/ArgumentValueNode.ts similarity index 70% rename from packages/nodes/src/contextualValueNodes/ArgumentValueNode.ts rename to packages/nodes/src/generated/contextualValueNodes/ArgumentValueNode.ts index 8f6ba9ea4..358519ed3 100644 --- a/packages/nodes/src/contextualValueNodes/ArgumentValueNode.ts +++ b/packages/nodes/src/generated/contextualValueNodes/ArgumentValueNode.ts @@ -1,7 +1,7 @@ import type { ArgumentValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** Refers to a named argument of the surrounding instruction. */ export function argumentValueNode(name: string): ArgumentValueNode { return Object.freeze({ kind: 'argumentValueNode', diff --git a/packages/nodes/src/generated/contextualValueNodes/ConditionalValueCondition.ts b/packages/nodes/src/generated/contextualValueNodes/ConditionalValueCondition.ts new file mode 100644 index 000000000..0fe223ef9 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/ConditionalValueCondition.ts @@ -0,0 +1,6 @@ +/** The condition forms accepted by a `conditionalValueNode`. */ +export const CONDITIONAL_VALUE_CONDITION_KINDS = [ + 'accountValueNode' as const, + 'argumentValueNode' as const, + 'resolverValueNode' as const, +]; diff --git a/packages/nodes/src/generated/contextualValueNodes/ConditionalValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/ConditionalValueNode.ts new file mode 100644 index 000000000..d53149ee1 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/ConditionalValueNode.ts @@ -0,0 +1,36 @@ +import type { + ConditionalValueCondition, + ConditionalValueNode, + InstructionInputValueNode, + ValueNode, +} from '@codama/node-types'; + +export type ConditionalValueNodeInput< + TCondition extends ConditionalValueCondition = ConditionalValueCondition, + TValue extends ValueNode | undefined = ValueNode | undefined, + TIfTrue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, + TIfFalse extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined, +> = Omit, 'kind'>; + +/** + * A branching contextual value. + * The condition resolves to a value at instruction time; that result selects between `ifTrue` and `ifFalse`. + */ +export function conditionalValueNode< + const TCondition extends ConditionalValueCondition, + const TValue extends ValueNode | undefined = undefined, + const TIfTrue extends InstructionInputValueNode | undefined = undefined, + const TIfFalse extends InstructionInputValueNode | undefined = undefined, +>( + input: ConditionalValueNodeInput, +): ConditionalValueNode { + return Object.freeze({ + kind: 'conditionalValueNode', + + // Children. + condition: input.condition, + ...(input.value !== undefined && { value: input.value }), + ...(input.ifTrue !== undefined && { ifTrue: input.ifTrue }), + ...(input.ifFalse !== undefined && { ifFalse: input.ifFalse }), + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/ContextualValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/ContextualValueNode.ts new file mode 100644 index 000000000..9be9e1b40 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/ContextualValueNode.ts @@ -0,0 +1,4 @@ +import { STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS } from './StandaloneContextualValueNode'; + +/** The composable form: any standalone contextual-value node. */ +export const CONTEXTUAL_VALUE_NODE_KINDS = [...STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS]; diff --git a/packages/nodes/src/generated/contextualValueNodes/IdentityValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/IdentityValueNode.ts new file mode 100644 index 000000000..48d566787 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/IdentityValueNode.ts @@ -0,0 +1,8 @@ +import type { IdentityValueNode } from '@codama/node-types'; + +/** Refers to the wallet identity providing the instruction context. */ +export function identityValueNode(): IdentityValueNode { + return Object.freeze({ + kind: 'identityValueNode', + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/InstructionInputValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/InstructionInputValueNode.ts new file mode 100644 index 000000000..de8bfd3fc --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/InstructionInputValueNode.ts @@ -0,0 +1,12 @@ +import { VALUE_NODE_KINDS } from '../valueNodes/ValueNode'; +import { CONTEXTUAL_VALUE_NODE_KINDS } 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 const INSTRUCTION_INPUT_VALUE_NODE_KINDS = [ + ...CONTEXTUAL_VALUE_NODE_KINDS, + 'programLinkNode' as const, + ...VALUE_NODE_KINDS, +]; diff --git a/packages/nodes/src/generated/contextualValueNodes/PayerValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/PayerValueNode.ts new file mode 100644 index 000000000..6240a5edc --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/PayerValueNode.ts @@ -0,0 +1,8 @@ +import type { PayerValueNode } from '@codama/node-types'; + +/** Refers to the wallet paying for the surrounding transaction. */ +export function payerValueNode(): PayerValueNode { + return Object.freeze({ + kind: 'payerValueNode', + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/PdaSeedValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/PdaSeedValueNode.ts new file mode 100644 index 000000000..2685be01d --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/PdaSeedValueNode.ts @@ -0,0 +1,18 @@ +import type { PdaSeedValueNode, PdaSeedValueValue } from '@codama/node-types'; +import { camelCase } from '../../shared'; + +/** Pairs a PDA seed name with the value to substitute when deriving the PDA. */ +export function pdaSeedValueNode( + name: string, + value: TValue, +): PdaSeedValueNode { + return Object.freeze({ + kind: 'pdaSeedValueNode', + + // Data. + name: camelCase(name), + + // Children. + value, + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/PdaSeedValueValue.ts b/packages/nodes/src/generated/contextualValueNodes/PdaSeedValueValue.ts new file mode 100644 index 000000000..2b450c267 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/PdaSeedValueValue.ts @@ -0,0 +1,8 @@ +import { VALUE_NODE_KINDS } from '../valueNodes/ValueNode'; + +/** The value forms accepted by a `pdaSeedValueNode`. */ +export const PDA_SEED_VALUE_VALUE_KINDS = [ + 'accountValueNode' as const, + 'argumentValueNode' as const, + ...VALUE_NODE_KINDS, +]; diff --git a/packages/nodes/src/generated/contextualValueNodes/PdaValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/PdaValueNode.ts new file mode 100644 index 000000000..d1d4cfd90 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/PdaValueNode.ts @@ -0,0 +1,22 @@ +import type { PdaSeedValueNode, PdaValueNode, PdaValuePda, PdaValueProgramId } from '@codama/node-types'; +import { pdaLinkNode } from '../linkNodes/PdaLinkNode'; + +/** Resolves to a PDA derived from a list of seed values. */ +export function pdaValueNode< + const TSeeds extends Array = [], + const TProgramId extends PdaValueProgramId | undefined = undefined, + const TPda extends PdaValuePda = PdaValuePda, +>( + pda: TPda | string, + seeds: TSeeds = [] as Array as TSeeds, + programId?: TProgramId, +): PdaValueNode { + return Object.freeze({ + kind: 'pdaValueNode', + + // Children. + pda: (typeof pda === 'string' ? pdaLinkNode(pda) : pda) as TPda, + seeds, + ...(programId !== undefined && { programId }), + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/PdaValuePda.ts b/packages/nodes/src/generated/contextualValueNodes/PdaValuePda.ts new file mode 100644 index 000000000..f0eb86ea1 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/PdaValuePda.ts @@ -0,0 +1,2 @@ +/** A `pdaValueNode` may reference a PDA either by link or inline. */ +export const PDA_VALUE_PDA_KINDS = ['pdaLinkNode' as const, 'pdaNode' as const]; diff --git a/packages/nodes/src/generated/contextualValueNodes/PdaValueProgramId.ts b/packages/nodes/src/generated/contextualValueNodes/PdaValueProgramId.ts new file mode 100644 index 000000000..de431cc36 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/PdaValueProgramId.ts @@ -0,0 +1,2 @@ +/** The program-id forms accepted by a `pdaValueNode`. */ +export const PDA_VALUE_PROGRAM_ID_KINDS = ['accountValueNode' as const, 'argumentValueNode' as const]; diff --git a/packages/nodes/src/generated/contextualValueNodes/ProgramIdValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/ProgramIdValueNode.ts new file mode 100644 index 000000000..e27ea6dbf --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/ProgramIdValueNode.ts @@ -0,0 +1,8 @@ +import type { ProgramIdValueNode } from '@codama/node-types'; + +/** Refers to the program ID of the surrounding instruction. */ +export function programIdValueNode(): ProgramIdValueNode { + return Object.freeze({ + kind: 'programIdValueNode', + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts new file mode 100644 index 000000000..5175ee00f --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/RegisteredContextualValueNode.ts @@ -0,0 +1,7 @@ +import { STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS } from './StandaloneContextualValueNode'; + +/** Every node tagged as a contextual-value node, including helper variants. */ +export const REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS = [ + 'pdaSeedValueNode' as const, + ...STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS, +]; diff --git a/packages/nodes/src/generated/contextualValueNodes/ResolverDependency.ts b/packages/nodes/src/generated/contextualValueNodes/ResolverDependency.ts new file mode 100644 index 000000000..67ef04dc0 --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/ResolverDependency.ts @@ -0,0 +1,2 @@ +/** The dependency forms accepted by a `resolverValueNode`. */ +export const RESOLVER_DEPENDENCY_KINDS = ['accountValueNode' as const, 'argumentValueNode' as const]; diff --git a/packages/nodes/src/generated/contextualValueNodes/ResolverValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/ResolverValueNode.ts new file mode 100644 index 000000000..9ce99b5ad --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/ResolverValueNode.ts @@ -0,0 +1,26 @@ +import type { ResolverDependency, ResolverValueNode } from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../../shared'; + +/** + * 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 function resolverValueNode | undefined = undefined>( + name: string, + options: { + docs?: DocsInput; + dependsOn?: TDependsOn; + } = {}, +): ResolverValueNode { + const parsedDocs = parseDocs(options.docs); + return Object.freeze({ + kind: 'resolverValueNode', + + // Data. + name: camelCase(name), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), + + // Children. + ...(options.dependsOn !== undefined && { dependsOn: options.dependsOn }), + }); +} diff --git a/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts b/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts new file mode 100644 index 000000000..63ad96dda --- /dev/null +++ b/packages/nodes/src/generated/contextualValueNodes/StandaloneContextualValueNode.ts @@ -0,0 +1,12 @@ +/** Every contextual-value node usable as a top-level value. */ +export const STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS = [ + 'accountBumpValueNode' as const, + 'accountValueNode' as const, + 'argumentValueNode' as const, + 'conditionalValueNode' as const, + 'identityValueNode' as const, + 'payerValueNode' as const, + 'pdaValueNode' as const, + 'programIdValueNode' as const, + 'resolverValueNode' as const, +]; diff --git a/packages/nodes/src/contextualValueNodes/index.ts b/packages/nodes/src/generated/contextualValueNodes/index.ts similarity index 55% rename from packages/nodes/src/contextualValueNodes/index.ts rename to packages/nodes/src/generated/contextualValueNodes/index.ts index c26843d90..4df98fc0a 100644 --- a/packages/nodes/src/contextualValueNodes/index.ts +++ b/packages/nodes/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/nodes/src/generated/countNodes/CountNode.ts b/packages/nodes/src/generated/countNodes/CountNode.ts new file mode 100644 index 000000000..51def04ef --- /dev/null +++ b/packages/nodes/src/generated/countNodes/CountNode.ts @@ -0,0 +1,4 @@ +import { REGISTERED_COUNT_NODE_KINDS } from './RegisteredCountNode'; + +/** The composable form: any registered count node. */ +export const COUNT_NODE_KINDS = [...REGISTERED_COUNT_NODE_KINDS]; diff --git a/packages/nodes/src/countNodes/FixedCountNode.ts b/packages/nodes/src/generated/countNodes/FixedCountNode.ts similarity index 74% rename from packages/nodes/src/countNodes/FixedCountNode.ts rename to packages/nodes/src/generated/countNodes/FixedCountNode.ts index d5c9399bd..a104c5b0f 100644 --- a/packages/nodes/src/countNodes/FixedCountNode.ts +++ b/packages/nodes/src/generated/countNodes/FixedCountNode.ts @@ -1,5 +1,6 @@ import type { FixedCountNode } from '@codama/node-types'; +/** A count strategy that fixes the number of items at a constant value. */ export function fixedCountNode(value: number): FixedCountNode { return Object.freeze({ kind: 'fixedCountNode', diff --git a/packages/nodes/src/countNodes/PrefixedCountNode.ts b/packages/nodes/src/generated/countNodes/PrefixedCountNode.ts similarity index 60% rename from packages/nodes/src/countNodes/PrefixedCountNode.ts rename to packages/nodes/src/generated/countNodes/PrefixedCountNode.ts index 6017d33e5..6d15a22c2 100644 --- a/packages/nodes/src/countNodes/PrefixedCountNode.ts +++ b/packages/nodes/src/generated/countNodes/PrefixedCountNode.ts @@ -1,6 +1,7 @@ import type { NestedTypeNode, NumberTypeNode, PrefixedCountNode } from '@codama/node-types'; -export function prefixedCountNode>( +/** A count strategy where the number of items is read from a numeric prefix. */ +export function prefixedCountNode>( prefix: TPrefix, ): PrefixedCountNode { return Object.freeze({ diff --git a/packages/nodes/src/countNodes/CountNode.ts b/packages/nodes/src/generated/countNodes/RegisteredCountNode.ts similarity index 57% rename from packages/nodes/src/countNodes/CountNode.ts rename to packages/nodes/src/generated/countNodes/RegisteredCountNode.ts index adb52d59d..36f74b1e4 100644 --- a/packages/nodes/src/countNodes/CountNode.ts +++ b/packages/nodes/src/generated/countNodes/RegisteredCountNode.ts @@ -1,9 +1,6 @@ -// Count Node Registration. +/** Every node tagged as a count strategy. */ export const REGISTERED_COUNT_NODE_KINDS = [ 'fixedCountNode' as const, - 'remainderCountNode' as const, 'prefixedCountNode' as const, + 'remainderCountNode' as const, ]; - -// Count Node Helpers. -export const COUNT_NODES = REGISTERED_COUNT_NODE_KINDS; diff --git a/packages/nodes/src/generated/countNodes/RemainderCountNode.ts b/packages/nodes/src/generated/countNodes/RemainderCountNode.ts new file mode 100644 index 000000000..c8ffdd4ba --- /dev/null +++ b/packages/nodes/src/generated/countNodes/RemainderCountNode.ts @@ -0,0 +1,8 @@ +import type { RemainderCountNode } from '@codama/node-types'; + +/** A count strategy where items are read until the buffer is exhausted. */ +export function remainderCountNode(): RemainderCountNode { + return Object.freeze({ + kind: 'remainderCountNode', + }); +} diff --git a/packages/nodes/src/countNodes/index.ts b/packages/nodes/src/generated/countNodes/index.ts similarity index 77% rename from packages/nodes/src/countNodes/index.ts rename to packages/nodes/src/generated/countNodes/index.ts index acd18ccd7..150a7228c 100644 --- a/packages/nodes/src/countNodes/index.ts +++ b/packages/nodes/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/nodes/src/discriminatorNodes/ConstantDiscriminatorNode.ts b/packages/nodes/src/generated/discriminatorNodes/ConstantDiscriminatorNode.ts similarity index 57% rename from packages/nodes/src/discriminatorNodes/ConstantDiscriminatorNode.ts rename to packages/nodes/src/generated/discriminatorNodes/ConstantDiscriminatorNode.ts index 1ad664c7e..6f7c8370e 100644 --- a/packages/nodes/src/discriminatorNodes/ConstantDiscriminatorNode.ts +++ b/packages/nodes/src/generated/discriminatorNodes/ConstantDiscriminatorNode.ts @@ -1,9 +1,10 @@ import type { ConstantDiscriminatorNode, ConstantValueNode } from '@codama/node-types'; -export function constantDiscriminatorNode( +/** Identifies a node by a constant value at a known byte offset (e.g. a magic header). */ +export function constantDiscriminatorNode( constant: TConstant, offset: number = 0, -): ConstantDiscriminatorNode { +): ConstantDiscriminatorNode { return Object.freeze({ kind: 'constantDiscriminatorNode', diff --git a/packages/nodes/src/generated/discriminatorNodes/DiscriminatorNode.ts b/packages/nodes/src/generated/discriminatorNodes/DiscriminatorNode.ts new file mode 100644 index 000000000..7f2f28e89 --- /dev/null +++ b/packages/nodes/src/generated/discriminatorNodes/DiscriminatorNode.ts @@ -0,0 +1,4 @@ +import { REGISTERED_DISCRIMINATOR_NODE_KINDS } from './RegisteredDiscriminatorNode'; + +/** The composable form: any registered discriminator node. */ +export const DISCRIMINATOR_NODE_KINDS = [...REGISTERED_DISCRIMINATOR_NODE_KINDS]; diff --git a/packages/nodes/src/discriminatorNodes/FieldDiscriminatorNode.ts b/packages/nodes/src/generated/discriminatorNodes/FieldDiscriminatorNode.ts similarity index 71% rename from packages/nodes/src/discriminatorNodes/FieldDiscriminatorNode.ts rename to packages/nodes/src/generated/discriminatorNodes/FieldDiscriminatorNode.ts index 64c832e15..4a826d146 100644 --- a/packages/nodes/src/discriminatorNodes/FieldDiscriminatorNode.ts +++ b/packages/nodes/src/generated/discriminatorNodes/FieldDiscriminatorNode.ts @@ -1,7 +1,7 @@ import type { FieldDiscriminatorNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** Identifies a node by the value of a named field at a known byte offset. */ export function fieldDiscriminatorNode(name: string, offset: number = 0): FieldDiscriminatorNode { return Object.freeze({ kind: 'fieldDiscriminatorNode', diff --git a/packages/nodes/src/discriminatorNodes/DiscriminatorNode.ts b/packages/nodes/src/generated/discriminatorNodes/RegisteredDiscriminatorNode.ts similarity index 55% rename from packages/nodes/src/discriminatorNodes/DiscriminatorNode.ts rename to packages/nodes/src/generated/discriminatorNodes/RegisteredDiscriminatorNode.ts index 1922c1a5a..d40a2f482 100644 --- a/packages/nodes/src/discriminatorNodes/DiscriminatorNode.ts +++ b/packages/nodes/src/generated/discriminatorNodes/RegisteredDiscriminatorNode.ts @@ -1,9 +1,6 @@ -// Discriminator Node Registration. +/** Every node tagged as a discriminator strategy. */ export const REGISTERED_DISCRIMINATOR_NODE_KINDS = [ 'constantDiscriminatorNode' as const, 'fieldDiscriminatorNode' as const, 'sizeDiscriminatorNode' as const, ]; - -// Discriminator Node Helpers. -export const DISCRIMINATOR_NODES = REGISTERED_DISCRIMINATOR_NODE_KINDS; diff --git a/packages/nodes/src/discriminatorNodes/SizeDiscriminatorNode.ts b/packages/nodes/src/generated/discriminatorNodes/SizeDiscriminatorNode.ts similarity index 81% rename from packages/nodes/src/discriminatorNodes/SizeDiscriminatorNode.ts rename to packages/nodes/src/generated/discriminatorNodes/SizeDiscriminatorNode.ts index 15e83ed6c..c3158a5bd 100644 --- a/packages/nodes/src/discriminatorNodes/SizeDiscriminatorNode.ts +++ b/packages/nodes/src/generated/discriminatorNodes/SizeDiscriminatorNode.ts @@ -1,5 +1,6 @@ import type { SizeDiscriminatorNode } from '@codama/node-types'; +/** Identifies a node by its expected total byte size. */ export function sizeDiscriminatorNode(size: number): SizeDiscriminatorNode { return Object.freeze({ kind: 'sizeDiscriminatorNode', diff --git a/packages/nodes/src/discriminatorNodes/index.ts b/packages/nodes/src/generated/discriminatorNodes/index.ts similarity index 77% rename from packages/nodes/src/discriminatorNodes/index.ts rename to packages/nodes/src/generated/discriminatorNodes/index.ts index db9e0ddd6..c569607f2 100644 --- a/packages/nodes/src/discriminatorNodes/index.ts +++ b/packages/nodes/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/nodes/src/generated/index.ts b/packages/nodes/src/generated/index.ts new file mode 100644 index 000000000..49811402e --- /dev/null +++ b/packages/nodes/src/generated/index.ts @@ -0,0 +1,26 @@ +export * from './AccountNode'; +export * from './codamaVersion'; +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 './nodeKinds'; +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 './typeNodes'; +export * from './valueNodes'; diff --git a/packages/nodes/src/generated/linkNodes/AccountLinkNode.ts b/packages/nodes/src/generated/linkNodes/AccountLinkNode.ts new file mode 100644 index 000000000..477cbdbc6 --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/AccountLinkNode.ts @@ -0,0 +1,21 @@ +import type { AccountLinkNode, ProgramLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { programLinkNode } from './ProgramLinkNode'; + +/** A reference to an account defined elsewhere — possibly in a different program. */ +export function accountLinkNode( + name: string, + program?: TProgram | string, +): AccountLinkNode { + return Object.freeze({ + kind: 'accountLinkNode', + + // Data. + name: camelCase(name), + + // Children. + ...(program !== undefined && { + program: (typeof program === 'string' ? programLinkNode(program) : program) as TProgram, + }), + }); +} diff --git a/packages/nodes/src/generated/linkNodes/DefinedTypeLinkNode.ts b/packages/nodes/src/generated/linkNodes/DefinedTypeLinkNode.ts new file mode 100644 index 000000000..9ab24c5e6 --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/DefinedTypeLinkNode.ts @@ -0,0 +1,21 @@ +import type { DefinedTypeLinkNode, ProgramLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { programLinkNode } from './ProgramLinkNode'; + +/** A reference to a defined type — possibly in a different program. */ +export function definedTypeLinkNode( + name: string, + program?: TProgram | string, +): DefinedTypeLinkNode { + return Object.freeze({ + kind: 'definedTypeLinkNode', + + // Data. + name: camelCase(name), + + // Children. + ...(program !== undefined && { + program: (typeof program === 'string' ? programLinkNode(program) : program) as TProgram, + }), + }); +} diff --git a/packages/nodes/src/generated/linkNodes/InstructionAccountLinkNode.ts b/packages/nodes/src/generated/linkNodes/InstructionAccountLinkNode.ts new file mode 100644 index 000000000..a9141c350 --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/InstructionAccountLinkNode.ts @@ -0,0 +1,23 @@ +import type { InstructionAccountLinkNode, InstructionLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { instructionLinkNode } from './InstructionLinkNode'; + +/** A reference to an account of another instruction. */ +export function instructionAccountLinkNode( + name: string, + instruction?: TInstruction | string, +): InstructionAccountLinkNode { + return Object.freeze({ + kind: 'instructionAccountLinkNode', + + // Data. + name: camelCase(name), + + // Children. + ...(instruction !== undefined && { + instruction: (typeof instruction === 'string' + ? instructionLinkNode(instruction) + : instruction) as TInstruction, + }), + }); +} diff --git a/packages/nodes/src/generated/linkNodes/InstructionArgumentLinkNode.ts b/packages/nodes/src/generated/linkNodes/InstructionArgumentLinkNode.ts new file mode 100644 index 000000000..c57fdc5b5 --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/InstructionArgumentLinkNode.ts @@ -0,0 +1,23 @@ +import type { InstructionArgumentLinkNode, InstructionLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { instructionLinkNode } from './InstructionLinkNode'; + +/** A reference to an argument of another instruction. */ +export function instructionArgumentLinkNode( + name: string, + instruction?: TInstruction | string, +): InstructionArgumentLinkNode { + return Object.freeze({ + kind: 'instructionArgumentLinkNode', + + // Data. + name: camelCase(name), + + // Children. + ...(instruction !== undefined && { + instruction: (typeof instruction === 'string' + ? instructionLinkNode(instruction) + : instruction) as TInstruction, + }), + }); +} diff --git a/packages/nodes/src/generated/linkNodes/InstructionLinkNode.ts b/packages/nodes/src/generated/linkNodes/InstructionLinkNode.ts new file mode 100644 index 000000000..81c6941dd --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/InstructionLinkNode.ts @@ -0,0 +1,21 @@ +import type { InstructionLinkNode, ProgramLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { programLinkNode } from './ProgramLinkNode'; + +/** A reference to an instruction defined elsewhere — possibly in a different program. */ +export function instructionLinkNode( + name: string, + program?: TProgram | string, +): InstructionLinkNode { + return Object.freeze({ + kind: 'instructionLinkNode', + + // Data. + name: camelCase(name), + + // Children. + ...(program !== undefined && { + program: (typeof program === 'string' ? programLinkNode(program) : program) as TProgram, + }), + }); +} diff --git a/packages/nodes/src/generated/linkNodes/LinkNode.ts b/packages/nodes/src/generated/linkNodes/LinkNode.ts new file mode 100644 index 000000000..679a8d549 --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/LinkNode.ts @@ -0,0 +1,4 @@ +import { REGISTERED_LINK_NODE_KINDS } from './RegisteredLinkNode'; + +/** The composable form: any registered link node. */ +export const LINK_NODE_KINDS = [...REGISTERED_LINK_NODE_KINDS]; diff --git a/packages/nodes/src/generated/linkNodes/PdaLinkNode.ts b/packages/nodes/src/generated/linkNodes/PdaLinkNode.ts new file mode 100644 index 000000000..33880db8d --- /dev/null +++ b/packages/nodes/src/generated/linkNodes/PdaLinkNode.ts @@ -0,0 +1,21 @@ +import type { PdaLinkNode, ProgramLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { programLinkNode } from './ProgramLinkNode'; + +/** A reference to a PDA defined elsewhere — possibly in a different program. */ +export function pdaLinkNode( + name: string, + program?: TProgram | string, +): PdaLinkNode { + return Object.freeze({ + kind: 'pdaLinkNode', + + // Data. + name: camelCase(name), + + // Children. + ...(program !== undefined && { + program: (typeof program === 'string' ? programLinkNode(program) : program) as TProgram, + }), + }); +} diff --git a/packages/nodes/src/linkNodes/ProgramLinkNode.ts b/packages/nodes/src/generated/linkNodes/ProgramLinkNode.ts similarity index 74% rename from packages/nodes/src/linkNodes/ProgramLinkNode.ts rename to packages/nodes/src/generated/linkNodes/ProgramLinkNode.ts index 13ddd59a3..9acf29fb3 100644 --- a/packages/nodes/src/linkNodes/ProgramLinkNode.ts +++ b/packages/nodes/src/generated/linkNodes/ProgramLinkNode.ts @@ -1,7 +1,7 @@ import type { ProgramLinkNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** A reference to a program by name. */ export function programLinkNode(name: string): ProgramLinkNode { return Object.freeze({ kind: 'programLinkNode', diff --git a/packages/nodes/src/linkNodes/LinkNode.ts b/packages/nodes/src/generated/linkNodes/RegisteredLinkNode.ts similarity index 74% rename from packages/nodes/src/linkNodes/LinkNode.ts rename to packages/nodes/src/generated/linkNodes/RegisteredLinkNode.ts index f16621f37..706886ac9 100644 --- a/packages/nodes/src/linkNodes/LinkNode.ts +++ b/packages/nodes/src/generated/linkNodes/RegisteredLinkNode.ts @@ -1,4 +1,4 @@ -// Link Node Registration. +/** Every node tagged as a link to another part of the IDL. */ export const REGISTERED_LINK_NODE_KINDS = [ 'accountLinkNode' as const, 'definedTypeLinkNode' as const, @@ -8,6 +8,3 @@ export const REGISTERED_LINK_NODE_KINDS = [ 'pdaLinkNode' as const, 'programLinkNode' as const, ]; - -// Link Node Helpers. -export const LINK_NODES = REGISTERED_LINK_NODE_KINDS; diff --git a/packages/nodes/src/linkNodes/index.ts b/packages/nodes/src/generated/linkNodes/index.ts similarity index 88% rename from packages/nodes/src/linkNodes/index.ts rename to packages/nodes/src/generated/linkNodes/index.ts index c056ec68c..0cad66538 100644 --- a/packages/nodes/src/linkNodes/index.ts +++ b/packages/nodes/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/nodes/src/generated/nodeKinds.ts b/packages/nodes/src/generated/nodeKinds.ts new file mode 100644 index 000000000..d9b78697f --- /dev/null +++ b/packages/nodes/src/generated/nodeKinds.ts @@ -0,0 +1,32 @@ +import { REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS } from './contextualValueNodes/RegisteredContextualValueNode'; +import { REGISTERED_COUNT_NODE_KINDS } from './countNodes/RegisteredCountNode'; +import { REGISTERED_DISCRIMINATOR_NODE_KINDS } from './discriminatorNodes/RegisteredDiscriminatorNode'; +import { REGISTERED_LINK_NODE_KINDS } from './linkNodes/RegisteredLinkNode'; +import { REGISTERED_PDA_SEED_NODE_KINDS } from './pdaSeedNodes/RegisteredPdaSeedNode'; +import { REGISTERED_TYPE_NODE_KINDS } from './typeNodes/RegisteredTypeNode'; +import { REGISTERED_VALUE_NODE_KINDS } from './valueNodes/RegisteredValueNode'; + +// Node Registration. +export const REGISTERED_NODE_KINDS = [ + 'accountNode' as const, + 'constantNode' as const, + 'definedTypeNode' as const, + 'errorNode' as const, + 'eventNode' as const, + 'instructionAccountNode' as const, + 'instructionArgumentNode' as const, + 'instructionByteDeltaNode' as const, + 'instructionNode' as const, + 'instructionRemainingAccountsNode' as const, + 'instructionStatusNode' as const, + 'pdaNode' as const, + 'programNode' as const, + 'rootNode' as const, + ...REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS, + ...REGISTERED_COUNT_NODE_KINDS, + ...REGISTERED_DISCRIMINATOR_NODE_KINDS, + ...REGISTERED_LINK_NODE_KINDS, + ...REGISTERED_PDA_SEED_NODE_KINDS, + ...REGISTERED_TYPE_NODE_KINDS, + ...REGISTERED_VALUE_NODE_KINDS, +]; diff --git a/packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts b/packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts new file mode 100644 index 000000000..ac2faeaa1 --- /dev/null +++ b/packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedNode.ts @@ -0,0 +1,15 @@ +import type { ConstantPdaSeedNode, ConstantPdaSeedValue, TypeNode } from '@codama/node-types'; + +/** A PDA seed with a constant value (e.g. a UTF-8 string or a fixed byte sequence). */ +export function constantPdaSeedNode( + type: TType, + value: TValue, +): ConstantPdaSeedNode { + return Object.freeze({ + kind: 'constantPdaSeedNode', + + // Children. + type, + value, + }); +} diff --git a/packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts b/packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts new file mode 100644 index 000000000..3e60388a1 --- /dev/null +++ b/packages/nodes/src/generated/pdaSeedNodes/ConstantPdaSeedValue.ts @@ -0,0 +1,4 @@ +import { VALUE_NODE_KINDS } from '../valueNodes/ValueNode'; + +/** The value forms a `constantPdaSeedNode` may carry — either a literal value or the program ID placeholder. */ +export const CONSTANT_PDA_SEED_VALUE_KINDS = ['programIdValueNode' as const, ...VALUE_NODE_KINDS]; diff --git a/packages/nodes/src/generated/pdaSeedNodes/PdaSeedNode.ts b/packages/nodes/src/generated/pdaSeedNodes/PdaSeedNode.ts new file mode 100644 index 000000000..e4f77b19e --- /dev/null +++ b/packages/nodes/src/generated/pdaSeedNodes/PdaSeedNode.ts @@ -0,0 +1,4 @@ +import { REGISTERED_PDA_SEED_NODE_KINDS } from './RegisteredPdaSeedNode'; + +/** The composable form: any registered PDA seed node. */ +export const PDA_SEED_NODE_KINDS = [...REGISTERED_PDA_SEED_NODE_KINDS]; diff --git a/packages/nodes/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts b/packages/nodes/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts new file mode 100644 index 000000000..53ff48d6c --- /dev/null +++ b/packages/nodes/src/generated/pdaSeedNodes/RegisteredPdaSeedNode.ts @@ -0,0 +1,2 @@ +/** Every node tagged as a PDA seed. */ +export const REGISTERED_PDA_SEED_NODE_KINDS = ['constantPdaSeedNode' as const, 'variablePdaSeedNode' as const]; diff --git a/packages/nodes/src/pdaSeedNodes/VariablePdaSeedNode.ts b/packages/nodes/src/generated/pdaSeedNodes/VariablePdaSeedNode.ts similarity index 50% rename from packages/nodes/src/pdaSeedNodes/VariablePdaSeedNode.ts rename to packages/nodes/src/generated/pdaSeedNodes/VariablePdaSeedNode.ts index d8d3beba7..468e70da6 100644 --- a/packages/nodes/src/pdaSeedNodes/VariablePdaSeedNode.ts +++ b/packages/nodes/src/generated/pdaSeedNodes/VariablePdaSeedNode.ts @@ -1,18 +1,19 @@ import type { TypeNode, VariablePdaSeedNode } from '@codama/node-types'; +import { camelCase, DocsInput, parseDocs } from '../../shared'; -import { camelCase, DocsInput, parseDocs } from '../shared'; - -export function variablePdaSeedNode( +/** A PDA seed whose value is provided at derivation time, identified by name. */ +export function variablePdaSeedNode( name: string, type: TType, docs?: DocsInput, ): VariablePdaSeedNode { + const parsedDocs = parseDocs(docs); return Object.freeze({ kind: 'variablePdaSeedNode', // Data. name: camelCase(name), - docs: parseDocs(docs), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), // Children. type, diff --git a/packages/nodes/src/pdaSeedNodes/index.ts b/packages/nodes/src/generated/pdaSeedNodes/index.ts similarity index 57% rename from packages/nodes/src/pdaSeedNodes/index.ts rename to packages/nodes/src/generated/pdaSeedNodes/index.ts index 1699e00cd..3b6cd92b9 100644 --- a/packages/nodes/src/pdaSeedNodes/index.ts +++ b/packages/nodes/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/nodes/src/typeNodes/AmountTypeNode.ts b/packages/nodes/src/generated/typeNodes/AmountTypeNode.ts similarity index 61% rename from packages/nodes/src/typeNodes/AmountTypeNode.ts rename to packages/nodes/src/generated/typeNodes/AmountTypeNode.ts index 5bb0db42a..60c1832f1 100644 --- a/packages/nodes/src/typeNodes/AmountTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/AmountTypeNode.ts @@ -1,6 +1,10 @@ import type { AmountTypeNode, NestedTypeNode, NumberTypeNode } from '@codama/node-types'; -export function amountTypeNode>( +/** + * Wraps a number type to provide additional context such as decimal places and a unit. + * Useful for amounts representing financial values. + */ +export function amountTypeNode>( number: TNumber, decimals: number, unit?: string, diff --git a/packages/nodes/src/typeNodes/ArrayTypeNode.ts b/packages/nodes/src/generated/typeNodes/ArrayTypeNode.ts similarity index 55% rename from packages/nodes/src/typeNodes/ArrayTypeNode.ts rename to packages/nodes/src/generated/typeNodes/ArrayTypeNode.ts index 7e7a5e749..3f508a45c 100644 --- a/packages/nodes/src/typeNodes/ArrayTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/ArrayTypeNode.ts @@ -1,6 +1,7 @@ import type { ArrayTypeNode, CountNode, TypeNode } from '@codama/node-types'; -export function arrayTypeNode( +/** A homogeneous list of items. The item type is defined by `item`; the length is determined by the `count` strategy. */ +export function arrayTypeNode( item: TItem, count: TCount, ): ArrayTypeNode { diff --git a/packages/nodes/src/generated/typeNodes/BooleanTypeNode.ts b/packages/nodes/src/generated/typeNodes/BooleanTypeNode.ts new file mode 100644 index 000000000..742181795 --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/BooleanTypeNode.ts @@ -0,0 +1,14 @@ +import type { BooleanTypeNode, NestedTypeNode, NumberTypeNode } from '@codama/node-types'; +import { numberTypeNode } from './NumberTypeNode'; + +/** A boolean serialised as a numeric value. The wrapped number type determines the byte width. */ +export function booleanTypeNode = NumberTypeNode<'u8'>>( + size: TSize = numberTypeNode('u8') as NumberTypeNode<'u8'> as TSize, +): BooleanTypeNode { + return Object.freeze({ + kind: 'booleanTypeNode', + + // Children. + size, + }); +} diff --git a/packages/nodes/src/generated/typeNodes/BytesTypeNode.ts b/packages/nodes/src/generated/typeNodes/BytesTypeNode.ts new file mode 100644 index 000000000..ab68f3367 --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/BytesTypeNode.ts @@ -0,0 +1,8 @@ +import type { BytesTypeNode } from '@codama/node-types'; + +/** A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or sentinel-terminated wrapper. */ +export function bytesTypeNode(): BytesTypeNode { + return Object.freeze({ + kind: 'bytesTypeNode', + }); +} diff --git a/packages/nodes/src/typeNodes/DateTimeTypeNode.ts b/packages/nodes/src/generated/typeNodes/DateTimeTypeNode.ts similarity index 53% rename from packages/nodes/src/typeNodes/DateTimeTypeNode.ts rename to packages/nodes/src/generated/typeNodes/DateTimeTypeNode.ts index ec25e16cf..0e6636833 100644 --- a/packages/nodes/src/typeNodes/DateTimeTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/DateTimeTypeNode.ts @@ -1,6 +1,7 @@ import type { DateTimeTypeNode, NestedTypeNode, NumberTypeNode } from '@codama/node-types'; -export function dateTimeTypeNode = NestedTypeNode>( +/** A timestamp encoded as a number, typically seconds since the Unix epoch. The wrapped number type determines the byte width. */ +export function dateTimeTypeNode>( number: TNumber, ): DateTimeTypeNode { return Object.freeze({ diff --git a/packages/nodes/src/typeNodes/EnumEmptyVariantTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts similarity index 64% rename from packages/nodes/src/typeNodes/EnumEmptyVariantTypeNode.ts rename to packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts index 1cfe46a75..5b3d86f6a 100644 --- a/packages/nodes/src/typeNodes/EnumEmptyVariantTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/EnumEmptyVariantTypeNode.ts @@ -1,13 +1,13 @@ import type { EnumEmptyVariantTypeNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** A unit-style variant of an enum that carries no payload. */ export function enumEmptyVariantTypeNode(name: string, discriminator?: number): EnumEmptyVariantTypeNode { return Object.freeze({ kind: 'enumEmptyVariantTypeNode', // Data. name: camelCase(name), - discriminator, + ...(discriminator !== undefined && { discriminator }), }); } diff --git a/packages/nodes/src/typeNodes/EnumStructVariantTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts similarity index 67% rename from packages/nodes/src/typeNodes/EnumStructVariantTypeNode.ts rename to packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts index 1fc9e8545..d22cf7210 100644 --- a/packages/nodes/src/typeNodes/EnumStructVariantTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/EnumStructVariantTypeNode.ts @@ -1,8 +1,8 @@ import type { EnumStructVariantTypeNode, NestedTypeNode, StructTypeNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - -export function enumStructVariantTypeNode>( +/** A variant of an enum that carries a struct payload (named fields). */ +export function enumStructVariantTypeNode>( name: string, struct: TStruct, discriminator?: number, diff --git a/packages/nodes/src/typeNodes/EnumTupleVariantTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumTupleVariantTypeNode.ts similarity index 66% rename from packages/nodes/src/typeNodes/EnumTupleVariantTypeNode.ts rename to packages/nodes/src/generated/typeNodes/EnumTupleVariantTypeNode.ts index 9038e24cf..15a115abd 100644 --- a/packages/nodes/src/typeNodes/EnumTupleVariantTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/EnumTupleVariantTypeNode.ts @@ -1,8 +1,8 @@ import type { EnumTupleVariantTypeNode, NestedTypeNode, TupleTypeNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - -export function enumTupleVariantTypeNode>( +/** A variant of an enum that carries a tuple payload (positional fields). */ +export function enumTupleVariantTypeNode>( name: string, tuple: TTuple, discriminator?: number, diff --git a/packages/nodes/src/generated/typeNodes/EnumTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumTypeNode.ts new file mode 100644 index 000000000..5db44e015 --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/EnumTypeNode.ts @@ -0,0 +1,21 @@ +import type { EnumTypeNode, EnumVariantTypeNode, NestedTypeNode, NumberTypeNode } from '@codama/node-types'; +import { numberTypeNode } from './NumberTypeNode'; + +/** A tagged union: a numeric discriminator followed by one of several variant payloads. */ +export function enumTypeNode< + const TVariants extends Array, + const TSize extends NestedTypeNode = NumberTypeNode<'u8'>, +>( + variants: TVariants, + options: { + size?: TSize; + } = {}, +): EnumTypeNode { + return Object.freeze({ + kind: 'enumTypeNode', + + // Children. + variants, + size: (options.size ?? numberTypeNode('u8')) as TSize, + }); +} diff --git a/packages/nodes/src/typeNodes/EnumVariantTypeNode.ts b/packages/nodes/src/generated/typeNodes/EnumVariantTypeNode.ts similarity index 56% rename from packages/nodes/src/typeNodes/EnumVariantTypeNode.ts rename to packages/nodes/src/generated/typeNodes/EnumVariantTypeNode.ts index 62a233ebf..20fd59413 100644 --- a/packages/nodes/src/typeNodes/EnumVariantTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/EnumVariantTypeNode.ts @@ -1,4 +1,5 @@ -export const ENUM_VARIANT_TYPE_NODES = [ +/** The variant flavours of an `enumTypeNode`. */ +export const ENUM_VARIANT_TYPE_NODE_KINDS = [ 'enumEmptyVariantTypeNode' as const, 'enumStructVariantTypeNode' as const, 'enumTupleVariantTypeNode' as const, diff --git a/packages/nodes/src/generated/typeNodes/FixedSizeTypeNode.ts b/packages/nodes/src/generated/typeNodes/FixedSizeTypeNode.ts new file mode 100644 index 000000000..bf09eb0c7 --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/FixedSizeTypeNode.ts @@ -0,0 +1,14 @@ +import type { FixedSizeTypeNode, TypeNode } from '@codama/node-types'; + +/** Wraps another type and asserts a fixed total byte size. Padding or truncation is applied as needed. */ +export function fixedSizeTypeNode(type: TType, size: number): FixedSizeTypeNode { + return Object.freeze({ + kind: 'fixedSizeTypeNode', + + // Data. + size, + + // Children. + type, + }); +} diff --git a/packages/nodes/src/typeNodes/HiddenPrefixTypeNode.ts b/packages/nodes/src/generated/typeNodes/HiddenPrefixTypeNode.ts similarity index 55% rename from packages/nodes/src/typeNodes/HiddenPrefixTypeNode.ts rename to packages/nodes/src/generated/typeNodes/HiddenPrefixTypeNode.ts index 7e282c3d4..74f9dd6e7 100644 --- a/packages/nodes/src/typeNodes/HiddenPrefixTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/HiddenPrefixTypeNode.ts @@ -1,6 +1,7 @@ import type { ConstantValueNode, HiddenPrefixTypeNode, TypeNode } from '@codama/node-types'; -export function hiddenPrefixTypeNode( +/** Prefixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. */ +export function hiddenPrefixTypeNode>( type: TType, prefix: TPrefix, ): HiddenPrefixTypeNode { diff --git a/packages/nodes/src/typeNodes/HiddenSuffixTypeNode.ts b/packages/nodes/src/generated/typeNodes/HiddenSuffixTypeNode.ts similarity index 55% rename from packages/nodes/src/typeNodes/HiddenSuffixTypeNode.ts rename to packages/nodes/src/generated/typeNodes/HiddenSuffixTypeNode.ts index 83e67dd79..5df9a8c46 100644 --- a/packages/nodes/src/typeNodes/HiddenSuffixTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/HiddenSuffixTypeNode.ts @@ -1,6 +1,7 @@ import type { ConstantValueNode, HiddenSuffixTypeNode, TypeNode } from '@codama/node-types'; -export function hiddenSuffixTypeNode( +/** Suffixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. */ +export function hiddenSuffixTypeNode>( type: TType, suffix: TSuffix, ): HiddenSuffixTypeNode { diff --git a/packages/nodes/src/typeNodes/MapTypeNode.ts b/packages/nodes/src/generated/typeNodes/MapTypeNode.ts similarity index 52% rename from packages/nodes/src/typeNodes/MapTypeNode.ts rename to packages/nodes/src/generated/typeNodes/MapTypeNode.ts index 63760a9cd..ede58743f 100644 --- a/packages/nodes/src/typeNodes/MapTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/MapTypeNode.ts @@ -1,6 +1,10 @@ import type { CountNode, MapTypeNode, TypeNode } from '@codama/node-types'; -export function mapTypeNode( +/** + * 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 function mapTypeNode( key: TKey, value: TValue, count: TCount, diff --git a/packages/nodes/src/generated/typeNodes/NumberTypeNode.ts b/packages/nodes/src/generated/typeNodes/NumberTypeNode.ts new file mode 100644 index 000000000..064b0f9cb --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/NumberTypeNode.ts @@ -0,0 +1,15 @@ +import type { Endianness, NumberFormat, NumberTypeNode } from '@codama/node-types'; + +/** A numeric type with a fixed wire format and byte order. */ +export function numberTypeNode( + format: TFormat, + endian: Endianness = 'le', +): NumberTypeNode { + return Object.freeze({ + kind: 'numberTypeNode', + + // Data. + format, + endian, + }); +} diff --git a/packages/nodes/src/typeNodes/OptionTypeNode.ts b/packages/nodes/src/generated/typeNodes/OptionTypeNode.ts similarity index 64% rename from packages/nodes/src/typeNodes/OptionTypeNode.ts rename to packages/nodes/src/generated/typeNodes/OptionTypeNode.ts index 339e52523..bac2342df 100644 --- a/packages/nodes/src/typeNodes/OptionTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/OptionTypeNode.ts @@ -1,15 +1,15 @@ import type { NestedTypeNode, NumberTypeNode, OptionTypeNode, TypeNode } from '@codama/node-types'; - import { numberTypeNode } from './NumberTypeNode'; +/** A value that may be present or absent (Some/None), with an explicit numeric prefix indicating presence. */ export function optionTypeNode< - TItem extends TypeNode, - TPrefix extends NestedTypeNode = NumberTypeNode<'u8'>, + const TItem extends TypeNode, + const TPrefix extends NestedTypeNode = NumberTypeNode<'u8'>, >( item: TItem, options: { - readonly fixed?: boolean; - readonly prefix?: TPrefix; + fixed?: boolean; + prefix?: TPrefix; } = {}, ): OptionTypeNode { return Object.freeze({ diff --git a/packages/nodes/src/generated/typeNodes/PostOffsetTypeNode.ts b/packages/nodes/src/generated/typeNodes/PostOffsetTypeNode.ts new file mode 100644 index 000000000..1ba222fdc --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/PostOffsetTypeNode.ts @@ -0,0 +1,19 @@ +import type { PostOffsetStrategy, PostOffsetTypeNode, TypeNode } from '@codama/node-types'; + +/** After serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. */ +export function postOffsetTypeNode( + type: TType, + offset: number, + strategy: PostOffsetStrategy = 'relative', +): PostOffsetTypeNode { + return Object.freeze({ + kind: 'postOffsetTypeNode', + + // Data. + offset, + strategy, + + // Children. + type, + }); +} diff --git a/packages/nodes/src/generated/typeNodes/PreOffsetTypeNode.ts b/packages/nodes/src/generated/typeNodes/PreOffsetTypeNode.ts new file mode 100644 index 000000000..3037d49ff --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/PreOffsetTypeNode.ts @@ -0,0 +1,19 @@ +import type { PreOffsetStrategy, PreOffsetTypeNode, TypeNode } from '@codama/node-types'; + +/** Before serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. */ +export function preOffsetTypeNode( + type: TType, + offset: number, + strategy: PreOffsetStrategy = 'relative', +): PreOffsetTypeNode { + return Object.freeze({ + kind: 'preOffsetTypeNode', + + // Data. + offset, + strategy, + + // Children. + type, + }); +} diff --git a/packages/nodes/src/typeNodes/PublicKeyTypeNode.ts b/packages/nodes/src/generated/typeNodes/PublicKeyTypeNode.ts similarity index 53% rename from packages/nodes/src/typeNodes/PublicKeyTypeNode.ts rename to packages/nodes/src/generated/typeNodes/PublicKeyTypeNode.ts index ec2568ffb..dc3e34ff5 100644 --- a/packages/nodes/src/typeNodes/PublicKeyTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/PublicKeyTypeNode.ts @@ -1,5 +1,8 @@ import type { PublicKeyTypeNode } from '@codama/node-types'; +/** A 32-byte Solana public key. */ export function publicKeyTypeNode(): PublicKeyTypeNode { - return Object.freeze({ kind: 'publicKeyTypeNode' }); + return Object.freeze({ + kind: 'publicKeyTypeNode', + }); } diff --git a/packages/nodes/src/generated/typeNodes/RegisteredTypeNode.ts b/packages/nodes/src/generated/typeNodes/RegisteredTypeNode.ts new file mode 100644 index 000000000..dfddbba1c --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/RegisteredTypeNode.ts @@ -0,0 +1,9 @@ +import { ENUM_VARIANT_TYPE_NODE_KINDS } from './EnumVariantTypeNode'; +import { STANDALONE_TYPE_NODE_KINDS } from './StandaloneTypeNode'; + +/** Every node tagged as a type-shaped node, including variants and struct fields. */ +export const REGISTERED_TYPE_NODE_KINDS = [ + ...ENUM_VARIANT_TYPE_NODE_KINDS, + ...STANDALONE_TYPE_NODE_KINDS, + 'structFieldTypeNode' as const, +]; diff --git a/packages/nodes/src/generated/typeNodes/RemainderOptionTypeNode.ts b/packages/nodes/src/generated/typeNodes/RemainderOptionTypeNode.ts new file mode 100644 index 000000000..0957657af --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/RemainderOptionTypeNode.ts @@ -0,0 +1,11 @@ +import type { RemainderOptionTypeNode, TypeNode } from '@codama/node-types'; + +/** A value that may be present or absent. Presence is signalled by whether any bytes remain to be read, with no explicit prefix. */ +export function remainderOptionTypeNode(item: TItem): RemainderOptionTypeNode { + return Object.freeze({ + kind: 'remainderOptionTypeNode', + + // Children. + item, + }); +} diff --git a/packages/nodes/src/typeNodes/SentinelTypeNode.ts b/packages/nodes/src/generated/typeNodes/SentinelTypeNode.ts similarity index 57% rename from packages/nodes/src/typeNodes/SentinelTypeNode.ts rename to packages/nodes/src/generated/typeNodes/SentinelTypeNode.ts index d3ea4a85d..3429add4e 100644 --- a/packages/nodes/src/typeNodes/SentinelTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/SentinelTypeNode.ts @@ -1,6 +1,7 @@ import type { ConstantValueNode, SentinelTypeNode, TypeNode } from '@codama/node-types'; -export function sentinelTypeNode( +/** Wraps another type and delimits it with a constant sentinel value written immediately after the wrapped type. */ +export function sentinelTypeNode( type: TType, sentinel: TSentinel, ): SentinelTypeNode { diff --git a/packages/nodes/src/typeNodes/SetTypeNode.ts b/packages/nodes/src/generated/typeNodes/SetTypeNode.ts similarity index 55% rename from packages/nodes/src/typeNodes/SetTypeNode.ts rename to packages/nodes/src/generated/typeNodes/SetTypeNode.ts index 6baddbd75..744ad4596 100644 --- a/packages/nodes/src/typeNodes/SetTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/SetTypeNode.ts @@ -1,6 +1,7 @@ import type { CountNode, SetTypeNode, TypeNode } from '@codama/node-types'; -export function setTypeNode( +/** A unique-valued collection. The item type is defined by `item`; the size is determined by the `count` strategy. */ +export function setTypeNode( item: TItem, count: TCount, ): SetTypeNode { diff --git a/packages/nodes/src/generated/typeNodes/SizePrefixTypeNode.ts b/packages/nodes/src/generated/typeNodes/SizePrefixTypeNode.ts new file mode 100644 index 000000000..54a858b20 --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/SizePrefixTypeNode.ts @@ -0,0 +1,15 @@ +import type { NestedTypeNode, NumberTypeNode, SizePrefixTypeNode, TypeNode } from '@codama/node-types'; + +/** Wraps another type with a numeric prefix indicating the byte length of the wrapped type. */ +export function sizePrefixTypeNode>( + type: TType, + prefix: TPrefix, +): SizePrefixTypeNode { + return Object.freeze({ + kind: 'sizePrefixTypeNode', + + // Children. + type, + prefix, + }); +} diff --git a/packages/nodes/src/typeNodes/SolAmountTypeNode.ts b/packages/nodes/src/generated/typeNodes/SolAmountTypeNode.ts similarity index 61% rename from packages/nodes/src/typeNodes/SolAmountTypeNode.ts rename to packages/nodes/src/generated/typeNodes/SolAmountTypeNode.ts index 6275cdeee..27e38c907 100644 --- a/packages/nodes/src/typeNodes/SolAmountTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/SolAmountTypeNode.ts @@ -1,6 +1,7 @@ import type { NestedTypeNode, NumberTypeNode, SolAmountTypeNode } from '@codama/node-types'; -export function solAmountTypeNode>( +/** A SOL amount expressed in lamports under the wrapped numeric type. */ +export function solAmountTypeNode>( number: TNumber, ): SolAmountTypeNode { return Object.freeze({ diff --git a/packages/nodes/src/typeNodes/TypeNode.ts b/packages/nodes/src/generated/typeNodes/StandaloneTypeNode.ts similarity index 56% rename from packages/nodes/src/typeNodes/TypeNode.ts rename to packages/nodes/src/generated/typeNodes/StandaloneTypeNode.ts index 91b2c9ca6..094012dd1 100644 --- a/packages/nodes/src/typeNodes/TypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/StandaloneTypeNode.ts @@ -1,4 +1,4 @@ -// Standalone Type Node Registration. +/** Every type node that can be used as a top-level type. */ export const STANDALONE_TYPE_NODE_KINDS = [ 'amountTypeNode' as const, 'arrayTypeNode' as const, @@ -25,20 +25,3 @@ export const STANDALONE_TYPE_NODE_KINDS = [ 'tupleTypeNode' as const, 'zeroableOptionTypeNode' as const, ]; - -// Type Node Registration. -export const REGISTERED_TYPE_NODE_KINDS = [ - ...STANDALONE_TYPE_NODE_KINDS, - 'structFieldTypeNode' as const, - 'enumEmptyVariantTypeNode' as const, - 'enumStructVariantTypeNode' as const, - 'enumTupleVariantTypeNode' as const, -]; - -/** - * Type Node Helpers. - * 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 const TYPE_NODES = [...STANDALONE_TYPE_NODE_KINDS, 'definedTypeLinkNode' as const]; diff --git a/packages/nodes/src/generated/typeNodes/StringTypeNode.ts b/packages/nodes/src/generated/typeNodes/StringTypeNode.ts new file mode 100644 index 000000000..358510b6e --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/StringTypeNode.ts @@ -0,0 +1,17 @@ +import type { BytesEncoding, StringTypeNode } from '@codama/node-types'; + +/** + * 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 function stringTypeNode( + encoding: TEncoding, +): StringTypeNode { + return Object.freeze({ + kind: 'stringTypeNode', + + // Data. + encoding, + }); +} diff --git a/packages/nodes/src/typeNodes/StructFieldTypeNode.ts b/packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts similarity index 62% rename from packages/nodes/src/typeNodes/StructFieldTypeNode.ts rename to packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts index 8a7a5e386..19f618975 100644 --- a/packages/nodes/src/typeNodes/StructFieldTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/StructFieldTypeNode.ts @@ -1,25 +1,27 @@ import type { StructFieldTypeNode, TypeNode, ValueNode } from '@codama/node-types'; - -import { camelCase, DocsInput, parseDocs } from '../shared'; +import { camelCase, DocsInput, parseDocs } from '../../shared'; export type StructFieldTypeNodeInput< TType extends TypeNode = TypeNode, TDefaultValue extends ValueNode | undefined = ValueNode | undefined, > = Omit, 'docs' | 'kind' | 'name'> & { - readonly docs?: DocsInput; readonly name: string; + readonly docs?: DocsInput; }; -export function structFieldTypeNode( - input: StructFieldTypeNodeInput, -): StructFieldTypeNode { +/** A named field within a struct type. */ +export function structFieldTypeNode< + const TType extends TypeNode, + const TDefaultValue extends ValueNode | undefined = undefined, +>(input: StructFieldTypeNodeInput): StructFieldTypeNode { + const parsedDocs = parseDocs(input.docs); return Object.freeze({ kind: 'structFieldTypeNode', // Data. name: camelCase(input.name), ...(input.defaultValueStrategy !== undefined && { defaultValueStrategy: input.defaultValueStrategy }), - docs: parseDocs(input.docs), + ...(parsedDocs.length > 0 && { docs: parsedDocs }), // Children. type: input.type, diff --git a/packages/nodes/src/typeNodes/StructTypeNode.ts b/packages/nodes/src/generated/typeNodes/StructTypeNode.ts similarity index 54% rename from packages/nodes/src/typeNodes/StructTypeNode.ts rename to packages/nodes/src/generated/typeNodes/StructTypeNode.ts index 594bd651f..d29dbbde4 100644 --- a/packages/nodes/src/typeNodes/StructTypeNode.ts +++ b/packages/nodes/src/generated/typeNodes/StructTypeNode.ts @@ -1,6 +1,7 @@ import type { StructFieldTypeNode, StructTypeNode } from '@codama/node-types'; -export function structTypeNode( +/** A composite type made of an ordered list of named fields. Fields are encoded and decoded in declaration order. */ +export function structTypeNode>( fields: TFields, ): StructTypeNode { return Object.freeze({ diff --git a/packages/nodes/src/generated/typeNodes/TupleTypeNode.ts b/packages/nodes/src/generated/typeNodes/TupleTypeNode.ts new file mode 100644 index 000000000..119ee63bc --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/TupleTypeNode.ts @@ -0,0 +1,11 @@ +import type { TupleTypeNode, TypeNode } from '@codama/node-types'; + +/** A heterogeneous fixed-length sequence in which each positional slot has its own type. */ +export function tupleTypeNode>(items: TItems): TupleTypeNode { + return Object.freeze({ + kind: 'tupleTypeNode', + + // Children. + items, + }); +} diff --git a/packages/nodes/src/generated/typeNodes/TypeNode.ts b/packages/nodes/src/generated/typeNodes/TypeNode.ts new file mode 100644 index 000000000..330d2aaac --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/TypeNode.ts @@ -0,0 +1,4 @@ +import { STANDALONE_TYPE_NODE_KINDS } from './StandaloneTypeNode'; + +/** The composable form: any standalone type, or a reference to a defined type via `definedTypeLinkNode`. */ +export const TYPE_NODE_KINDS = ['definedTypeLinkNode' as const, ...STANDALONE_TYPE_NODE_KINDS]; diff --git a/packages/nodes/src/generated/typeNodes/ZeroableOptionTypeNode.ts b/packages/nodes/src/generated/typeNodes/ZeroableOptionTypeNode.ts new file mode 100644 index 000000000..cf1398c57 --- /dev/null +++ b/packages/nodes/src/generated/typeNodes/ZeroableOptionTypeNode.ts @@ -0,0 +1,15 @@ +import type { ConstantValueNode, TypeNode, ZeroableOptionTypeNode } from '@codama/node-types'; + +/** An optional value whose absence is signalled by a designated zero value rather than a presence flag. */ +export function zeroableOptionTypeNode< + const TItem extends TypeNode, + const TZeroValue extends ConstantValueNode | undefined = undefined, +>(item: TItem, zeroValue?: TZeroValue): ZeroableOptionTypeNode { + return Object.freeze({ + kind: 'zeroableOptionTypeNode', + + // Children. + item, + ...(zeroValue !== undefined && { zeroValue }), + }); +} diff --git a/packages/nodes/src/typeNodes/index.ts b/packages/nodes/src/generated/typeNodes/index.ts similarity index 93% rename from packages/nodes/src/typeNodes/index.ts rename to packages/nodes/src/generated/typeNodes/index.ts index 9f1b475d9..402fab328 100644 --- a/packages/nodes/src/typeNodes/index.ts +++ b/packages/nodes/src/generated/typeNodes/index.ts @@ -12,17 +12,18 @@ export * from './FixedSizeTypeNode'; export * from './HiddenPrefixTypeNode'; export * from './HiddenSuffixTypeNode'; export * from './MapTypeNode'; -export * from './NestedTypeNode'; export * from './NumberTypeNode'; 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/nodes/src/valueNodes/ArrayValueNode.ts b/packages/nodes/src/generated/valueNodes/ArrayValueNode.ts similarity index 51% rename from packages/nodes/src/valueNodes/ArrayValueNode.ts rename to packages/nodes/src/generated/valueNodes/ArrayValueNode.ts index 2dbe7c834..a910a5d18 100644 --- a/packages/nodes/src/valueNodes/ArrayValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/ArrayValueNode.ts @@ -1,6 +1,7 @@ import type { ArrayValueNode, ValueNode } from '@codama/node-types'; -export function arrayValueNode(items: TItems): ArrayValueNode { +/** A concrete array value: a list of value nodes. */ +export function arrayValueNode>(items: TItems): ArrayValueNode { return Object.freeze({ kind: 'arrayValueNode', diff --git a/packages/nodes/src/valueNodes/BooleanValueNode.ts b/packages/nodes/src/generated/valueNodes/BooleanValueNode.ts similarity index 65% rename from packages/nodes/src/valueNodes/BooleanValueNode.ts rename to packages/nodes/src/generated/valueNodes/BooleanValueNode.ts index 1c0c5a7a1..37ab186cf 100644 --- a/packages/nodes/src/valueNodes/BooleanValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/BooleanValueNode.ts @@ -1,5 +1,6 @@ -import { BooleanValueNode } from '@codama/node-types'; +import type { BooleanValueNode } from '@codama/node-types'; +/** A concrete boolean value. */ export function booleanValueNode(boolean: boolean): BooleanValueNode { return Object.freeze({ kind: 'booleanValueNode', diff --git a/packages/nodes/src/valueNodes/BytesValueNode.ts b/packages/nodes/src/generated/valueNodes/BytesValueNode.ts similarity index 79% rename from packages/nodes/src/valueNodes/BytesValueNode.ts rename to packages/nodes/src/generated/valueNodes/BytesValueNode.ts index 0fb64d87a..1154cc21b 100644 --- a/packages/nodes/src/valueNodes/BytesValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/BytesValueNode.ts @@ -1,5 +1,6 @@ import type { BytesEncoding, BytesValueNode } from '@codama/node-types'; +/** A concrete bytes value, encoded as text in the chosen encoding. */ export function bytesValueNode(encoding: BytesEncoding, data: string): BytesValueNode { return Object.freeze({ kind: 'bytesValueNode', diff --git a/packages/nodes/src/generated/valueNodes/ConstantValueNode.ts b/packages/nodes/src/generated/valueNodes/ConstantValueNode.ts new file mode 100644 index 000000000..53153df33 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/ConstantValueNode.ts @@ -0,0 +1,15 @@ +import type { ConstantValueNode, TypeNode, ValueNode } from '@codama/node-types'; + +/** A typed constant: a type node paired with a concrete value node. */ +export function constantValueNode( + type: TType, + value: TValue, +): ConstantValueNode { + return Object.freeze({ + kind: 'constantValueNode', + + // Children. + type, + value, + }); +} diff --git a/packages/nodes/src/generated/valueNodes/EnumValueNode.ts b/packages/nodes/src/generated/valueNodes/EnumValueNode.ts new file mode 100644 index 000000000..2702819cd --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/EnumValueNode.ts @@ -0,0 +1,20 @@ +import type { DefinedTypeLinkNode, EnumValueNode, EnumValuePayload } from '@codama/node-types'; +import { camelCase } from '../../shared'; +import { definedTypeLinkNode } from '../linkNodes/DefinedTypeLinkNode'; + +/** A concrete value of a defined enum: a variant identifier plus an optional payload. */ +export function enumValueNode< + const TEnum extends DefinedTypeLinkNode = DefinedTypeLinkNode, + const TValue extends EnumValuePayload | undefined = undefined, +>(enumLink: TEnum | string, variant: string, value?: TValue): EnumValueNode { + return Object.freeze({ + kind: 'enumValueNode', + + // Data. + variant: camelCase(variant), + + // Children. + enum: (typeof enumLink === 'string' ? definedTypeLinkNode(enumLink) : enumLink) as TEnum, + ...(value !== undefined && { value }), + }); +} diff --git a/packages/nodes/src/generated/valueNodes/EnumValuePayload.ts b/packages/nodes/src/generated/valueNodes/EnumValuePayload.ts new file mode 100644 index 000000000..dc0422d45 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/EnumValuePayload.ts @@ -0,0 +1,2 @@ +/** The payload kinds an `enumValueNode` may carry — struct fields or positional tuple slots. */ +export const ENUM_VALUE_PAYLOAD_KINDS = ['structValueNode' as const, 'tupleValueNode' as const]; diff --git a/packages/nodes/src/valueNodes/MapEntryValueNode.ts b/packages/nodes/src/generated/valueNodes/MapEntryValueNode.ts similarity index 63% rename from packages/nodes/src/valueNodes/MapEntryValueNode.ts rename to packages/nodes/src/generated/valueNodes/MapEntryValueNode.ts index 77c5da07b..13a1914eb 100644 --- a/packages/nodes/src/valueNodes/MapEntryValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/MapEntryValueNode.ts @@ -1,6 +1,7 @@ import type { MapEntryValueNode, ValueNode } from '@codama/node-types'; -export function mapEntryValueNode( +/** A single (key, value) pair inside a `mapValueNode`. */ +export function mapEntryValueNode( key: TKey, value: TValue, ): MapEntryValueNode { diff --git a/packages/nodes/src/generated/valueNodes/MapValueNode.ts b/packages/nodes/src/generated/valueNodes/MapValueNode.ts new file mode 100644 index 000000000..721e1514d --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/MapValueNode.ts @@ -0,0 +1,13 @@ +import type { MapEntryValueNode, MapValueNode } from '@codama/node-types'; + +/** A concrete map value: a list of (key, value) entries. */ +export function mapValueNode>( + entries: TEntries, +): MapValueNode { + return Object.freeze({ + kind: 'mapValueNode', + + // Children. + entries, + }); +} diff --git a/packages/nodes/src/generated/valueNodes/NoneValueNode.ts b/packages/nodes/src/generated/valueNodes/NoneValueNode.ts new file mode 100644 index 000000000..fc32c5acc --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/NoneValueNode.ts @@ -0,0 +1,8 @@ +import type { NoneValueNode } from '@codama/node-types'; + +/** The "absent" value for an optional type. */ +export function noneValueNode(): NoneValueNode { + return Object.freeze({ + kind: 'noneValueNode', + }); +} diff --git a/packages/nodes/src/valueNodes/NumberValueNode.ts b/packages/nodes/src/generated/valueNodes/NumberValueNode.ts similarity index 59% rename from packages/nodes/src/valueNodes/NumberValueNode.ts rename to packages/nodes/src/generated/valueNodes/NumberValueNode.ts index 094546ec3..f4619940f 100644 --- a/packages/nodes/src/valueNodes/NumberValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/NumberValueNode.ts @@ -1,5 +1,9 @@ import type { NumberValueNode } from '@codama/node-types'; +/** + * 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 function numberValueNode(number: number): NumberValueNode { return Object.freeze({ kind: 'numberValueNode', diff --git a/packages/nodes/src/valueNodes/PublicKeyValueNode.ts b/packages/nodes/src/generated/valueNodes/PublicKeyValueNode.ts similarity index 73% rename from packages/nodes/src/valueNodes/PublicKeyValueNode.ts rename to packages/nodes/src/generated/valueNodes/PublicKeyValueNode.ts index 87668ee33..21116158a 100644 --- a/packages/nodes/src/valueNodes/PublicKeyValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/PublicKeyValueNode.ts @@ -1,7 +1,7 @@ import type { PublicKeyValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - +/** A concrete public key, with an optional symbolic identifier for the address. */ export function publicKeyValueNode(publicKey: string, identifier?: string): PublicKeyValueNode { return Object.freeze({ kind: 'publicKeyValueNode', diff --git a/packages/nodes/src/generated/valueNodes/RegisteredValueNode.ts b/packages/nodes/src/generated/valueNodes/RegisteredValueNode.ts new file mode 100644 index 000000000..239481c52 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/RegisteredValueNode.ts @@ -0,0 +1,8 @@ +import { STANDALONE_VALUE_NODE_KINDS } from './StandaloneValueNode'; + +/** Every node tagged as a value-shaped node, including container variants. */ +export const REGISTERED_VALUE_NODE_KINDS = [ + 'mapEntryValueNode' as const, + ...STANDALONE_VALUE_NODE_KINDS, + 'structFieldValueNode' as const, +]; diff --git a/packages/nodes/src/valueNodes/SetValueNode.ts b/packages/nodes/src/generated/valueNodes/SetValueNode.ts similarity index 50% rename from packages/nodes/src/valueNodes/SetValueNode.ts rename to packages/nodes/src/generated/valueNodes/SetValueNode.ts index d905b5779..4b6b20317 100644 --- a/packages/nodes/src/valueNodes/SetValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/SetValueNode.ts @@ -1,6 +1,7 @@ import type { SetValueNode, ValueNode } from '@codama/node-types'; -export function setValueNode(items: TItems): SetValueNode { +/** A concrete set value: a list of unique value nodes. */ +export function setValueNode>(items: TItems): SetValueNode { return Object.freeze({ kind: 'setValueNode', diff --git a/packages/nodes/src/generated/valueNodes/SomeValueNode.ts b/packages/nodes/src/generated/valueNodes/SomeValueNode.ts new file mode 100644 index 000000000..a9510a128 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/SomeValueNode.ts @@ -0,0 +1,11 @@ +import type { SomeValueNode, ValueNode } from '@codama/node-types'; + +/** The "present" value for an optional type, wrapping a concrete value node. */ +export function someValueNode(value: TValue): SomeValueNode { + return Object.freeze({ + kind: 'someValueNode', + + // Children. + value, + }); +} diff --git a/packages/nodes/src/valueNodes/ValueNode.ts b/packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts similarity index 61% rename from packages/nodes/src/valueNodes/ValueNode.ts rename to packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts index 88d3e8bb3..246eaa643 100644 --- a/packages/nodes/src/valueNodes/ValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/StandaloneValueNode.ts @@ -1,27 +1,17 @@ -// Standalone Value Node Registration. +/** Every value node that can be used as a top-level value. */ export const STANDALONE_VALUE_NODE_KINDS = [ 'arrayValueNode' as const, - 'bytesValueNode' as const, 'booleanValueNode' as const, + 'bytesValueNode' as const, 'constantValueNode' as const, 'enumValueNode' as const, 'mapValueNode' as const, 'noneValueNode' as const, 'numberValueNode' as const, + 'publicKeyValueNode' as const, 'setValueNode' as const, 'someValueNode' as const, + 'stringValueNode' as const, 'structValueNode' as const, 'tupleValueNode' as const, - 'publicKeyValueNode' as const, - 'stringValueNode' as const, -]; - -// Value Node Registration. -export const REGISTERED_VALUE_NODE_KINDS = [ - ...STANDALONE_VALUE_NODE_KINDS, - 'mapEntryValueNode' as const, - 'structFieldValueNode' as const, ]; - -// Value Node Helpers. -export const VALUE_NODES = STANDALONE_VALUE_NODE_KINDS; diff --git a/packages/nodes/src/valueNodes/StringValueNode.ts b/packages/nodes/src/generated/valueNodes/StringValueNode.ts similarity index 87% rename from packages/nodes/src/valueNodes/StringValueNode.ts rename to packages/nodes/src/generated/valueNodes/StringValueNode.ts index 81ccdfcd4..462b2cf10 100644 --- a/packages/nodes/src/valueNodes/StringValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/StringValueNode.ts @@ -1,5 +1,6 @@ import type { StringValueNode } from '@codama/node-types'; +/** A concrete string value. */ export function stringValueNode(string: string): StringValueNode { return Object.freeze({ kind: 'stringValueNode', diff --git a/packages/nodes/src/valueNodes/StructFieldValueNode.ts b/packages/nodes/src/generated/valueNodes/StructFieldValueNode.ts similarity index 66% rename from packages/nodes/src/valueNodes/StructFieldValueNode.ts rename to packages/nodes/src/generated/valueNodes/StructFieldValueNode.ts index 8f3ff37d3..21d6f1ca6 100644 --- a/packages/nodes/src/valueNodes/StructFieldValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/StructFieldValueNode.ts @@ -1,8 +1,8 @@ import type { StructFieldValueNode, ValueNode } from '@codama/node-types'; +import { camelCase } from '../../shared'; -import { camelCase } from '../shared'; - -export function structFieldValueNode( +/** A named field of a `structValueNode`. */ +export function structFieldValueNode( name: string, value: TValue, ): StructFieldValueNode { diff --git a/packages/nodes/src/valueNodes/StructValueNode.ts b/packages/nodes/src/generated/valueNodes/StructValueNode.ts similarity index 62% rename from packages/nodes/src/valueNodes/StructValueNode.ts rename to packages/nodes/src/generated/valueNodes/StructValueNode.ts index c39f71c5b..637ab2d75 100644 --- a/packages/nodes/src/valueNodes/StructValueNode.ts +++ b/packages/nodes/src/generated/valueNodes/StructValueNode.ts @@ -1,6 +1,7 @@ import type { StructFieldValueNode, StructValueNode } from '@codama/node-types'; -export function structValueNode( +/** A concrete struct value: a list of named field values. */ +export function structValueNode>( fields: TFields, ): StructValueNode { return Object.freeze({ diff --git a/packages/nodes/src/generated/valueNodes/TupleValueNode.ts b/packages/nodes/src/generated/valueNodes/TupleValueNode.ts new file mode 100644 index 000000000..ad311c4b1 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/TupleValueNode.ts @@ -0,0 +1,11 @@ +import type { TupleValueNode, ValueNode } from '@codama/node-types'; + +/** A concrete tuple value: a fixed-length sequence of positional value nodes. */ +export function tupleValueNode>(items: TItems): TupleValueNode { + return Object.freeze({ + kind: 'tupleValueNode', + + // Children. + items, + }); +} diff --git a/packages/nodes/src/generated/valueNodes/ValueNode.ts b/packages/nodes/src/generated/valueNodes/ValueNode.ts new file mode 100644 index 000000000..e272bbcf8 --- /dev/null +++ b/packages/nodes/src/generated/valueNodes/ValueNode.ts @@ -0,0 +1,4 @@ +import { STANDALONE_VALUE_NODE_KINDS } from './StandaloneValueNode'; + +/** The composable form: any standalone value node. */ +export const VALUE_NODE_KINDS = [...STANDALONE_VALUE_NODE_KINDS]; diff --git a/packages/nodes/src/valueNodes/index.ts b/packages/nodes/src/generated/valueNodes/index.ts similarity index 83% rename from packages/nodes/src/valueNodes/index.ts rename to packages/nodes/src/generated/valueNodes/index.ts index 56f55fe21..f9c35d7fb 100644 --- a/packages/nodes/src/valueNodes/index.ts +++ b/packages/nodes/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/nodes/src/index.ts b/packages/nodes/src/index.ts index 25d72d60d..9d5bf0170 100644 --- a/packages/nodes/src/index.ts +++ b/packages/nodes/src/index.ts @@ -1,27 +1,34 @@ export * from '@codama/node-types'; -export * from './contextualValueNodes'; -export * from './countNodes'; -export * from './discriminatorNodes'; -export * from './linkNodes'; -export * from './pdaSeedNodes'; -export * from './typeNodes'; -export * from './valueNodes'; - +// The bulk of the surface — every `xxxNodeInput` type, every `xxxNode()` +// constructor, every `*_NODE_KINDS` runtime array — is generated from +// `@codama/spec` by `@codama-internal/spec-generators`. +export * from './generated'; export * from './shared'; -export * from './AccountNode'; -export * from './ConstantNode'; -export * from './DefinedTypeNode'; -export * from './EventNode'; -export * from './ErrorNode'; -export * from './InstructionAccountNode'; +// Hand-written helpers that layer convenience predicates and factories +// on top of the generated constructors. +export * from './ConstantPdaSeedNode'; +export * from './ConstantValueNode'; +export * from './EnumTypeNode'; export * from './InstructionArgumentNode'; -export * from './InstructionByteDeltaNode'; export * from './InstructionNode'; -export * from './InstructionRemainingAccountsNode'; -export * from './InstructionStatusNode'; +export * from './NestedTypeNode'; export * from './Node'; -export * from './PdaNode'; +export * from './NumberTypeNode'; export * from './ProgramNode'; -export * from './RootNode'; + +// Legacy plural-noun aliases preserved for API stability. Each maps to +// the canonical `*_NODE_KINDS` name generated from the matching spec +// union. +export { + CONTEXTUAL_VALUE_NODE_KINDS as CONTEXTUAL_VALUE_NODES, + ENUM_VARIANT_TYPE_NODE_KINDS as ENUM_VARIANT_TYPE_NODES, + INSTRUCTION_INPUT_VALUE_NODE_KINDS as INSTRUCTION_INPUT_VALUE_NODES, + REGISTERED_COUNT_NODE_KINDS as COUNT_NODES, + REGISTERED_DISCRIMINATOR_NODE_KINDS as DISCRIMINATOR_NODES, + REGISTERED_LINK_NODE_KINDS as LINK_NODES, + REGISTERED_PDA_SEED_NODE_KINDS as PDA_SEED_NODES, + TYPE_NODE_KINDS as TYPE_NODES, + VALUE_NODE_KINDS as VALUE_NODES, +} from './generated'; diff --git a/packages/nodes/src/linkNodes/AccountLinkNode.ts b/packages/nodes/src/linkNodes/AccountLinkNode.ts deleted file mode 100644 index 7b4f77c27..000000000 --- a/packages/nodes/src/linkNodes/AccountLinkNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { AccountLinkNode, ProgramLinkNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; -import { programLinkNode } from './ProgramLinkNode'; - -export function accountLinkNode(name: string, program?: ProgramLinkNode | string): AccountLinkNode { - return Object.freeze({ - kind: 'accountLinkNode', - - // Children. - ...(program === undefined ? {} : { program: typeof program === 'string' ? programLinkNode(program) : program }), - - // Data. - name: camelCase(name), - }); -} diff --git a/packages/nodes/src/linkNodes/DefinedTypeLinkNode.ts b/packages/nodes/src/linkNodes/DefinedTypeLinkNode.ts deleted file mode 100644 index ccd80aaf4..000000000 --- a/packages/nodes/src/linkNodes/DefinedTypeLinkNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { DefinedTypeLinkNode, ProgramLinkNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; -import { programLinkNode } from './ProgramLinkNode'; - -export function definedTypeLinkNode(name: string, program?: ProgramLinkNode | string): DefinedTypeLinkNode { - return Object.freeze({ - kind: 'definedTypeLinkNode', - - // Children. - ...(program === undefined ? {} : { program: typeof program === 'string' ? programLinkNode(program) : program }), - - // Data. - name: camelCase(name), - }); -} diff --git a/packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts b/packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts deleted file mode 100644 index 74b88e782..000000000 --- a/packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { InstructionAccountLinkNode, InstructionLinkNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; -import { instructionLinkNode } from './InstructionLinkNode'; - -export function instructionAccountLinkNode( - name: string, - instruction?: InstructionLinkNode | string, -): InstructionAccountLinkNode { - return Object.freeze({ - kind: 'instructionAccountLinkNode', - - // Children. - ...(instruction === undefined - ? {} - : { instruction: typeof instruction === 'string' ? instructionLinkNode(instruction) : instruction }), - - // Data. - name: camelCase(name), - }); -} diff --git a/packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts b/packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts deleted file mode 100644 index 6c0f71918..000000000 --- a/packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { InstructionArgumentLinkNode, InstructionLinkNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; -import { instructionLinkNode } from './InstructionLinkNode'; - -export function instructionArgumentLinkNode( - name: string, - instruction?: InstructionLinkNode | string, -): InstructionArgumentLinkNode { - return Object.freeze({ - kind: 'instructionArgumentLinkNode', - - // Children. - ...(instruction === undefined - ? {} - : { instruction: typeof instruction === 'string' ? instructionLinkNode(instruction) : instruction }), - - // Data. - name: camelCase(name), - }); -} diff --git a/packages/nodes/src/linkNodes/InstructionLinkNode.ts b/packages/nodes/src/linkNodes/InstructionLinkNode.ts deleted file mode 100644 index c76caf54b..000000000 --- a/packages/nodes/src/linkNodes/InstructionLinkNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { InstructionLinkNode, ProgramLinkNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; -import { programLinkNode } from './ProgramLinkNode'; - -export function instructionLinkNode(name: string, program?: ProgramLinkNode | string): InstructionLinkNode { - return Object.freeze({ - kind: 'instructionLinkNode', - - // Children. - ...(program === undefined ? {} : { program: typeof program === 'string' ? programLinkNode(program) : program }), - - // Data. - name: camelCase(name), - }); -} diff --git a/packages/nodes/src/linkNodes/PdaLinkNode.ts b/packages/nodes/src/linkNodes/PdaLinkNode.ts deleted file mode 100644 index 7b1348c39..000000000 --- a/packages/nodes/src/linkNodes/PdaLinkNode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { PdaLinkNode, ProgramLinkNode } from '@codama/node-types'; - -import { camelCase } from '../shared'; -import { programLinkNode } from './ProgramLinkNode'; - -export function pdaLinkNode(name: string, program?: ProgramLinkNode | string): PdaLinkNode { - return Object.freeze({ - kind: 'pdaLinkNode', - - // Children. - ...(program === undefined ? {} : { program: typeof program === 'string' ? programLinkNode(program) : program }), - - // Data. - name: camelCase(name), - }); -} diff --git a/packages/nodes/src/pdaSeedNodes/ConstantPdaSeedNode.ts b/packages/nodes/src/pdaSeedNodes/ConstantPdaSeedNode.ts deleted file mode 100644 index 5cf9174de..000000000 --- a/packages/nodes/src/pdaSeedNodes/ConstantPdaSeedNode.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { BytesEncoding, ConstantPdaSeedNode, ProgramIdValueNode, TypeNode, ValueNode } from '@codama/node-types'; - -import { programIdValueNode } from '../contextualValueNodes/ProgramIdValueNode'; -import { bytesTypeNode } from '../typeNodes/BytesTypeNode'; -import { publicKeyTypeNode } from '../typeNodes/PublicKeyTypeNode'; -import { stringTypeNode } from '../typeNodes/StringTypeNode'; -import { bytesValueNode } from '../valueNodes/BytesValueNode'; -import { stringValueNode } from '../valueNodes/StringValueNode'; - -export function constantPdaSeedNode( - type: TType, - value: TValue, -): ConstantPdaSeedNode { - return Object.freeze({ - kind: 'constantPdaSeedNode', - - // Children. - type, - value, - }); -} - -export function constantPdaSeedNodeFromProgramId() { - return constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode()); -} - -export function constantPdaSeedNodeFromString(encoding: TEncoding, string: string) { - return constantPdaSeedNode(stringTypeNode(encoding), stringValueNode(string)); -} - -export function constantPdaSeedNodeFromBytes(encoding: TEncoding, data: string) { - return constantPdaSeedNode(bytesTypeNode(), bytesValueNode(encoding, data)); -} diff --git a/packages/nodes/src/pdaSeedNodes/PdaSeedNode.ts b/packages/nodes/src/pdaSeedNodes/PdaSeedNode.ts deleted file mode 100644 index c8c9e6864..000000000 --- a/packages/nodes/src/pdaSeedNodes/PdaSeedNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Pda Seed Node Registration. -export const REGISTERED_PDA_SEED_NODE_KINDS = ['constantPdaSeedNode' as const, 'variablePdaSeedNode' as const]; - -// Pda Seed Node Helpers. -export const PDA_SEED_NODES = REGISTERED_PDA_SEED_NODE_KINDS; diff --git a/packages/nodes/src/typeNodes/BooleanTypeNode.ts b/packages/nodes/src/typeNodes/BooleanTypeNode.ts deleted file mode 100644 index efb29d18b..000000000 --- a/packages/nodes/src/typeNodes/BooleanTypeNode.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { BooleanTypeNode, NestedTypeNode, NumberTypeNode } from '@codama/node-types'; - -import { numberTypeNode } from './NumberTypeNode'; - -export function booleanTypeNode = NumberTypeNode<'u8'>>( - size?: TSize, -): BooleanTypeNode { - return Object.freeze({ - kind: 'booleanTypeNode', - - // Children. - size: (size ?? numberTypeNode('u8')) as TSize, - }); -} diff --git a/packages/nodes/src/typeNodes/BytesTypeNode.ts b/packages/nodes/src/typeNodes/BytesTypeNode.ts deleted file mode 100644 index 57605d699..000000000 --- a/packages/nodes/src/typeNodes/BytesTypeNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { BytesTypeNode } from '@codama/node-types'; - -export function bytesTypeNode(): BytesTypeNode { - return Object.freeze({ kind: 'bytesTypeNode' }); -} diff --git a/packages/nodes/src/typeNodes/EnumTypeNode.ts b/packages/nodes/src/typeNodes/EnumTypeNode.ts deleted file mode 100644 index d71939222..000000000 --- a/packages/nodes/src/typeNodes/EnumTypeNode.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { EnumTypeNode, EnumVariantTypeNode, NestedTypeNode, NumberTypeNode } from '@codama/node-types'; - -import { numberTypeNode } from './NumberTypeNode'; - -export function enumTypeNode< - const TVariants extends EnumVariantTypeNode[], - TSize extends NestedTypeNode = NumberTypeNode<'u8'>, ->(variants: TVariants, options: { size?: TSize } = {}): EnumTypeNode { - return Object.freeze({ - kind: 'enumTypeNode', - - // Children. - variants, - size: (options.size ?? numberTypeNode('u8')) as TSize, - }); -} - -export function isScalarEnum(node: EnumTypeNode): boolean { - return node.variants.every(variant => variant.kind === 'enumEmptyVariantTypeNode'); -} - -export function isDataEnum(node: EnumTypeNode): boolean { - return !isScalarEnum(node); -} diff --git a/packages/nodes/src/typeNodes/FixedSizeTypeNode.ts b/packages/nodes/src/typeNodes/FixedSizeTypeNode.ts deleted file mode 100644 index d692a464c..000000000 --- a/packages/nodes/src/typeNodes/FixedSizeTypeNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { FixedSizeTypeNode, TypeNode } from '@codama/node-types'; - -export function fixedSizeTypeNode(type: TType, size: number): FixedSizeTypeNode { - return Object.freeze({ - kind: 'fixedSizeTypeNode', - - // Data. - size, - - // Children. - type, - }); -} diff --git a/packages/nodes/src/typeNodes/PostOffsetTypeNode.ts b/packages/nodes/src/typeNodes/PostOffsetTypeNode.ts deleted file mode 100644 index 67ae341aa..000000000 --- a/packages/nodes/src/typeNodes/PostOffsetTypeNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PostOffsetTypeNode, TypeNode } from '@codama/node-types'; - -export function postOffsetTypeNode( - type: TType, - offset: number, - strategy?: PostOffsetTypeNode['strategy'], -): PostOffsetTypeNode { - return Object.freeze({ - kind: 'postOffsetTypeNode', - - // Data. - offset, - strategy: strategy ?? 'relative', - - // Children. - type, - }); -} diff --git a/packages/nodes/src/typeNodes/PreOffsetTypeNode.ts b/packages/nodes/src/typeNodes/PreOffsetTypeNode.ts deleted file mode 100644 index 17f116090..000000000 --- a/packages/nodes/src/typeNodes/PreOffsetTypeNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PreOffsetTypeNode, TypeNode } from '@codama/node-types'; - -export function preOffsetTypeNode( - type: TType, - offset: number, - strategy?: PreOffsetTypeNode['strategy'], -): PreOffsetTypeNode { - return Object.freeze({ - kind: 'preOffsetTypeNode', - - // Data. - offset, - strategy: strategy ?? 'relative', - - // Children. - type, - }); -} diff --git a/packages/nodes/src/typeNodes/RemainderOptionTypeNode.ts b/packages/nodes/src/typeNodes/RemainderOptionTypeNode.ts deleted file mode 100644 index 357eebc3f..000000000 --- a/packages/nodes/src/typeNodes/RemainderOptionTypeNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RemainderOptionTypeNode, TypeNode } from '@codama/node-types'; - -export function remainderOptionTypeNode(item: TItem): RemainderOptionTypeNode { - return Object.freeze({ - kind: 'remainderOptionTypeNode', - - // Children. - item, - }); -} diff --git a/packages/nodes/src/typeNodes/SizePrefixTypeNode.ts b/packages/nodes/src/typeNodes/SizePrefixTypeNode.ts deleted file mode 100644 index 6c44adcec..000000000 --- a/packages/nodes/src/typeNodes/SizePrefixTypeNode.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NestedTypeNode, NumberTypeNode, SizePrefixTypeNode, TypeNode } from '@codama/node-types'; - -export function sizePrefixTypeNode< - TType extends TypeNode = TypeNode, - TPrefix extends NestedTypeNode = NestedTypeNode, ->(type: TType, prefix: TPrefix): SizePrefixTypeNode { - return Object.freeze({ - kind: 'sizePrefixTypeNode', - - // Children. - type, - prefix, - }); -} diff --git a/packages/nodes/src/typeNodes/StringTypeNode.ts b/packages/nodes/src/typeNodes/StringTypeNode.ts deleted file mode 100644 index 669ced022..000000000 --- a/packages/nodes/src/typeNodes/StringTypeNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { BytesEncoding, StringTypeNode } from '@codama/node-types'; - -export function stringTypeNode(encoding: TEncoding): StringTypeNode { - return Object.freeze({ - kind: 'stringTypeNode', - - // Data. - encoding, - }); -} diff --git a/packages/nodes/src/typeNodes/TupleTypeNode.ts b/packages/nodes/src/typeNodes/TupleTypeNode.ts deleted file mode 100644 index a0a1b77dc..000000000 --- a/packages/nodes/src/typeNodes/TupleTypeNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TupleTypeNode, TypeNode } from '@codama/node-types'; - -export function tupleTypeNode(items: TItems): TupleTypeNode { - return Object.freeze({ - kind: 'tupleTypeNode', - - // Children. - items, - }); -} diff --git a/packages/nodes/src/typeNodes/ZeroableOptionTypeNode.ts b/packages/nodes/src/typeNodes/ZeroableOptionTypeNode.ts deleted file mode 100644 index b1273b03e..000000000 --- a/packages/nodes/src/typeNodes/ZeroableOptionTypeNode.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ConstantValueNode, TypeNode, ZeroableOptionTypeNode } from '@codama/node-types'; - -export function zeroableOptionTypeNode( - item: TItem, - zeroValue?: TZeroValue, -): ZeroableOptionTypeNode { - return Object.freeze({ - kind: 'zeroableOptionTypeNode', - - // Children. - item, - ...(zeroValue !== undefined && { zeroValue }), - }); -} diff --git a/packages/nodes/src/types/global.d.ts b/packages/nodes/src/types/global.d.ts deleted file mode 100644 index 13de8a7ce..000000000 --- a/packages/nodes/src/types/global.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare const __BROWSER__: boolean; -declare const __ESM__: boolean; -declare const __NODEJS__: boolean; -declare const __REACTNATIVE__: boolean; -declare const __TEST__: boolean; -declare const __VERSION__: string; diff --git a/packages/nodes/src/valueNodes/ConstantValueNode.ts b/packages/nodes/src/valueNodes/ConstantValueNode.ts deleted file mode 100644 index d95fb02ab..000000000 --- a/packages/nodes/src/valueNodes/ConstantValueNode.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { BytesEncoding, ConstantValueNode, TypeNode, ValueNode } from '@codama/node-types'; - -import { bytesTypeNode, stringTypeNode } from '../typeNodes'; -import { bytesValueNode } from './BytesValueNode'; -import { stringValueNode } from './StringValueNode'; - -export function constantValueNode( - type: TType, - value: TValue, -): ConstantValueNode { - return Object.freeze({ - kind: 'constantValueNode', - - // Children. - type, - value, - }); -} - -export function constantValueNodeFromString(encoding: TEncoding, string: string) { - return constantValueNode(stringTypeNode(encoding), stringValueNode(string)); -} - -export function constantValueNodeFromBytes(encoding: TEncoding, data: string) { - return constantValueNode(bytesTypeNode(), bytesValueNode(encoding, data)); -} diff --git a/packages/nodes/src/valueNodes/EnumValueNode.ts b/packages/nodes/src/valueNodes/EnumValueNode.ts deleted file mode 100644 index fbaa0160b..000000000 --- a/packages/nodes/src/valueNodes/EnumValueNode.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { DefinedTypeLinkNode, EnumValueNode, StructValueNode, TupleValueNode } from '@codama/node-types'; - -import { definedTypeLinkNode } from '../linkNodes'; -import { camelCase } from '../shared'; - -export function enumValueNode< - TEnum extends DefinedTypeLinkNode = DefinedTypeLinkNode, - TValue extends StructValueNode | TupleValueNode | undefined = undefined, ->(enumLink: TEnum | string, variant: string, value?: TValue): EnumValueNode { - return Object.freeze({ - kind: 'enumValueNode', - - // Data. - variant: camelCase(variant), - - // Children. - enum: (typeof enumLink === 'string' ? definedTypeLinkNode(enumLink) : enumLink) as TEnum, - ...(value !== undefined && { value }), - }); -} diff --git a/packages/nodes/src/valueNodes/MapValueNode.ts b/packages/nodes/src/valueNodes/MapValueNode.ts deleted file mode 100644 index ad8b69327..000000000 --- a/packages/nodes/src/valueNodes/MapValueNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { MapEntryValueNode, MapValueNode } from '@codama/node-types'; - -export function mapValueNode(entries: TEntries): MapValueNode { - return Object.freeze({ - kind: 'mapValueNode', - - // Children. - entries, - }); -} diff --git a/packages/nodes/src/valueNodes/NoneValueNode.ts b/packages/nodes/src/valueNodes/NoneValueNode.ts deleted file mode 100644 index 4267d8242..000000000 --- a/packages/nodes/src/valueNodes/NoneValueNode.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { NoneValueNode } from '@codama/node-types'; - -export function noneValueNode(): NoneValueNode { - return Object.freeze({ kind: 'noneValueNode' }); -} diff --git a/packages/nodes/src/valueNodes/SomeValueNode.ts b/packages/nodes/src/valueNodes/SomeValueNode.ts deleted file mode 100644 index d61519c21..000000000 --- a/packages/nodes/src/valueNodes/SomeValueNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { SomeValueNode, ValueNode } from '@codama/node-types'; - -export function someValueNode(value: TValue): SomeValueNode { - return Object.freeze({ - kind: 'someValueNode', - - // Children. - value, - }); -} diff --git a/packages/nodes/src/valueNodes/TupleValueNode.ts b/packages/nodes/src/valueNodes/TupleValueNode.ts deleted file mode 100644 index 6d1c3a957..000000000 --- a/packages/nodes/src/valueNodes/TupleValueNode.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TupleValueNode, ValueNode } from '@codama/node-types'; - -export function tupleValueNode(items: TItems): TupleValueNode { - return Object.freeze({ - kind: 'tupleValueNode', - - // Children. - items, - }); -} diff --git a/packages/nodes/test/RootNode.test.ts b/packages/nodes/test/RootNode.test.ts index e88bde30a..c1fd78b0f 100644 --- a/packages/nodes/test/RootNode.test.ts +++ b/packages/nodes/test/RootNode.test.ts @@ -1,7 +1,7 @@ import type { CodamaVersion } from '@codama/node-types'; import { expect, expectTypeOf, test } from 'vitest'; -import { programNode, rootNode } from '../src'; +import { CODAMA_VERSION, programNode, rootNode } from '../src'; test('it returns the right node kind', () => { const root = rootNode(programNode({ name: 'foo', publicKey: '1111' })); @@ -13,9 +13,9 @@ test('it returns the right Codama standard', () => { expect(root.standard).toBe('codama'); }); -test('it returns the right Codama version', () => { +test('it tags the root with the spec version it was generated against', () => { const root = rootNode(programNode({ name: 'foo', publicKey: '1111' })); - expect(root.version).toBe(__VERSION__); + expect(root.version).toBe(CODAMA_VERSION); expectTypeOf(root.version).toMatchTypeOf(); }); diff --git a/packages/nodes/test/types/global.d.ts b/packages/nodes/test/types/global.d.ts deleted file mode 100644 index 13de8a7ce..000000000 --- a/packages/nodes/test/types/global.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare const __BROWSER__: boolean; -declare const __ESM__: boolean; -declare const __NODEJS__: boolean; -declare const __REACTNATIVE__: boolean; -declare const __TEST__: boolean; -declare const __VERSION__: string; diff --git a/packages/spec-generators/package.json b/packages/spec-generators/package.json index 2a2ad944e..907aa2392 100644 --- a/packages/spec-generators/package.json +++ b/packages/spec-generators/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "build": "rimraf dist && tsup", - "generate": "pnpm build && node ./dist/generate.mjs && pnpm --filter @codama/node-types lint:fix", + "generate": "pnpm build && node ./dist/generate.mjs && pnpm --filter @codama/node-types lint:fix && pnpm --filter @codama/nodes lint:fix", "lint": "eslint . && prettier --check .", "lint:fix": "eslint --fix . && prettier --write .", "test": "pnpm test:types && pnpm test:unit", diff --git a/packages/spec-generators/src/index.ts b/packages/spec-generators/src/index.ts index 2a853b9f8..f2159ee3d 100644 --- a/packages/spec-generators/src/index.ts +++ b/packages/spec-generators/src/index.ts @@ -1,8 +1,9 @@ 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'; +import { generateNodes, NODE_CONFIGS } from './nodes'; +import { generateNodeTypes } from './nodeTypes'; +import { CATEGORY_DIRECTORIES, GENERIC_PARAM_ORDER, getRepoDirectory, NARROWABLE_DATA_ATTRIBUTES } from './shared'; export interface GenerateResult { /** One entry per generator that ran, in the order they ran. */ @@ -30,5 +31,18 @@ export function generate(): GenerateResult { outputs.push({ generator: 'nodeTypes', outputDir }); } + { + const outputDir = joinPath(getRepoDirectory(), 'packages', 'nodes', 'src', 'generated'); + generateNodes(spec, { + categoryDirectories: CATEGORY_DIRECTORIES, + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, + nodeConfigs: NODE_CONFIGS, + outputDir, + targetSpecMajor: 1, + }); + outputs.push({ generator: 'nodes', outputDir }); + } + return { outputs }; } diff --git a/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts b/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts index 7e76ce723..db7da621b 100644 --- a/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts +++ b/packages/spec-generators/src/nodeTypes/fragments/attributeBodyLine.ts @@ -1,17 +1,16 @@ 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 { getTypeParameterIdentifierFragment } from '../../shared'; +import { isNodeTypeParameterAttribute, type RenderScope } from '../options'; 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. + * Type-parameter attributes use their type-parameter identifier (e.g. + * `readonly data: TData;`); other attributes use the rendered type + * expression. Optional attributes carry a `?:` marker; `docs` (if any) + * become a JSDoc prefix. */ export function getAttributeBodyLineFragment( nodeKind: string, @@ -20,8 +19,8 @@ export function getAttributeBodyLineFragment( ): Fragment { const docPrefix = getDocblockFragment(attr.docs, { withLineJump: true }); const optionalMark = attr.optional === true ? '?' : ''; - const typeFragment = isAttributeLifted(nodeKind, attr, scope) - ? getTypeParameterIdentifierFragment(attr.name) + const typeFragment = isNodeTypeParameterAttribute(nodeKind, attr, scope) + ? getTypeParameterIdentifierFragment(attr) : getTypeExprFragment(attr.type); return fragment`${docPrefix}readonly ${attr.name}${optionalMark}: ${typeFragment};`; } diff --git a/packages/spec-generators/src/nodeTypes/fragments/index.ts b/packages/spec-generators/src/nodeTypes/fragments/index.ts index 81c3d592f..bd172fc35 100644 --- a/packages/spec-generators/src/nodeTypes/fragments/index.ts +++ b/packages/spec-generators/src/nodeTypes/fragments/index.ts @@ -1,13 +1,10 @@ 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 deleted file mode 100644 index 2fa5008a1..000000000 --- a/packages/spec-generators/src/nodeTypes/fragments/indexPage.ts +++ /dev/null @@ -1,10 +0,0 @@ -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/node.ts b/packages/spec-generators/src/nodeTypes/fragments/node.ts index c5bbc989b..da6741f9b 100644 --- a/packages/spec-generators/src/nodeTypes/fragments/node.ts +++ b/packages/spec-generators/src/nodeTypes/fragments/node.ts @@ -1,9 +1,8 @@ import { type Fragment, fragment, getDocblockFragment, mergeFragments, pascalCase } from '@codama/fragments/javascript'; -import { type AttributeSpec, isChildAttribute, type NodeSpec } from '@codama/spec'; +import { isChildAttribute, type NodeSpec } from '@codama/spec'; -import { isAttributeLifted } from '../options'; -import type { RenderScope } from '../utils/scope'; -import { isTypeExprSelfReferential } from '../utils/selfReference'; +import { getNodeTypeParameterAttributes, type RenderScope } from '../options'; +import { isTypeExprSelfReferential } from '../selfReference'; import { getAttributeBodyLineFragment } from './attributeBodyLine'; import { getKindLineFragment } from './kindLine'; import { getTypeParameterDefinitionFragment } from './typeParameterDefinition'; @@ -22,7 +21,9 @@ export function getNodeFragment(node: NodeSpec, scope: NodeScope): Fragment { const selfAlias = isSelfReferential ? { alias: `Self${interfaceName}`, kind: node.kind } : undefined; const genericsBlock = getGenericsBlockFragment( - orderLifted(node, scope).map(attr => getTypeParameterDefinitionFragment(attr, { selfAlias })), + getNodeTypeParameterAttributes(node, scope).map(attr => + getTypeParameterDefinitionFragment(attr, { selfAlias }), + ), ); const dataLines = node.attributes @@ -49,37 +50,6 @@ export function getNodeFragment(node: NodeSpec, scope: NodeScope): Fragment { 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 => { diff --git a/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts b/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts index 5e2174614..2019934e8 100644 --- a/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts +++ b/packages/spec-generators/src/nodeTypes/fragments/nodeRegistry.ts @@ -1,34 +1,14 @@ import { type Fragment, mergeFragments, pascalCase, use } from '@codama/fragments/javascript'; -import type { Spec, UnionMember } from '@codama/spec'; +import type { Spec } from '@codama/spec'; -const REGISTERED_CATEGORY_UNIONS: readonly string[] = [ - 'RegisteredContextualValueNode', - 'RegisteredCountNode', - 'RegisteredDiscriminatorNode', - 'RegisteredLinkNode', - 'RegisteredPdaSeedNode', - 'RegisteredTypeNode', - 'RegisteredValueNode', -]; - -type UnionLookup = ReadonlyMap; +import { flattenNodeUnion, getRegisteredCategoryUnions } from '../../shared'; export function getNodeRegistryFragment(spec: Spec): Fragment { - const unionByName: UnionLookup = new Map(spec.categories.flatMap(c => c.unions).map(u => [u.name, u])); + const registeredUnions = getRegisteredCategoryUnions(spec); - 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 registeredUnionFragments = registeredUnions.map(u => use(`type ${pascalCase(u.name)}`, `union:${u.name}`)); - const registeredKinds = new Set( - REGISTERED_CATEGORY_UNIONS.flatMap(unionName => [...collectKindsFromUnion(unionByName, unionName)]), - ); + const registeredKinds = new Set(registeredUnions.flatMap(u => flattenNodeUnion(u, spec).map(n => n.kind))); const directNodeFragments = spec.categories .flatMap(c => c.nodes) @@ -51,29 +31,3 @@ export function getNodeRegistryFragment(spec: Spec): Fragment { ].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 deleted file mode 100644 index b9e635c52..000000000 --- a/packages/spec-generators/src/nodeTypes/fragments/page.ts +++ /dev/null @@ -1,53 +0,0 @@ -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/typeParameterDefinition.ts b/packages/spec-generators/src/nodeTypes/fragments/typeParameterDefinition.ts index 5e7653b28..e6a9e31c3 100644 --- a/packages/spec-generators/src/nodeTypes/fragments/typeParameterDefinition.ts +++ b/packages/spec-generators/src/nodeTypes/fragments/typeParameterDefinition.ts @@ -1,9 +1,9 @@ import { type Fragment, fragment } from '@codama/fragments/javascript'; import type { AttributeSpec } from '@codama/spec'; -import { isTypeExprSelfReferential } from '../utils/selfReference'; +import { getTypeParameterIdentifierFragment } from '../../shared'; +import { isTypeExprSelfReferential } from '../selfReference'; import { getTypeExprFragment, getTypeExprWithSelfAliasFragment } from './typeExpr'; -import { getTypeParameterIdentifierFragment } from './typeParameterIdentifier'; export interface TypeParameterDefinitionOptions { /** @@ -18,16 +18,16 @@ export interface TypeParameterDefinitionOptions { } /** - * 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. + * Render the type-parameter definition for one type-parameter + * attribute, e.g. `TData extends Foo = Foo` (or `… | undefined = … + * | undefined` when the attribute is optional). Callers must only + * invoke this for attributes that already surface as type parameters. */ export function getTypeParameterDefinitionFragment( attr: AttributeSpec, options: TypeParameterDefinitionOptions = {}, ): Fragment { - const identifier = getTypeParameterIdentifierFragment(attr.name); + const identifier = getTypeParameterIdentifierFragment(attr); const baseFragment = options.selfAlias && isTypeExprSelfReferential(attr.type, options.selfAlias.kind) ? getTypeExprWithSelfAliasFragment(attr.type, options.selfAlias.kind, options.selfAlias.alias) diff --git a/packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts b/packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts deleted file mode 100644 index 7c6f31c5c..000000000 --- a/packages/spec-generators/src/nodeTypes/fragments/typeParameterIdentifier.ts +++ /dev/null @@ -1,6 +0,0 @@ -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/index.ts b/packages/spec-generators/src/nodeTypes/index.ts index 2239f907b..0857e1416 100644 --- a/packages/spec-generators/src/nodeTypes/index.ts +++ b/packages/spec-generators/src/nodeTypes/index.ts @@ -2,28 +2,29 @@ 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 { getIndexPagesRenderMap, getPageFragment, resolveEntryPath, type SymbolicModule } from '../shared'; 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'; +import { + buildRenderScope, + type GenerateOptions, + type RenderOptions, + type RenderScope, + validateRenderOptions, +} from './options'; export { CATEGORY_DIRECTORIES, @@ -35,8 +36,8 @@ export { } 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 + * Build the render map and write it to disk under `options.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 { @@ -45,28 +46,21 @@ export function generateNodeTypes(spec: Spec, options: GenerateOptions): void { 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. - */ +/** Pure-and-sync render-map entry point. 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); + const indexPages = getIndexPagesRenderMap(specPages, scope.symbolicModules); return mergeRenderMaps([specPages, indexPages]); } -/** - * Walk every spec category plus the top-level `Node` registry and - * return one rendered page per emitted symbolic key. - */ +/** 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); + const path = resolveEntryPath(scope.symbolicModules, symbolicKey); + entries[`${path}.ts`] = getPageFragment(body, scope.symbolicModules, path); }; for (const category of spec.categories) { @@ -82,63 +76,3 @@ function getSpecPagesRenderMap(spec: Spec, scope: RenderScope): 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 index 3f2daa792..9087d2850 100644 --- a/packages/spec-generators/src/nodeTypes/options.ts +++ b/packages/spec-generators/src/nodeTypes/options.ts @@ -1,30 +1,25 @@ -import type { Path } from '@codama/fragments/javascript'; -import { type AttributeSpec, isChildAttribute, type Spec } from '@codama/spec'; +import { camelCase, joinPath, pascalCase, type Path } from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { + resolveSharedRenderOptions, + type SharedRenderOptions, + type SymbolicModule, + type SymbolicModuleMap, + validateSharedRenderOptions, +} from '../shared'; + +export { + CATEGORY_DIRECTORIES, + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + isNodeTypeParameterAttribute, + NARROWABLE_DATA_ATTRIBUTES, +} from '../shared'; /** 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; -} +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface RenderOptions extends SharedRenderOptions {} /** Options consumed by {@link generateNodeTypes}, the disk-writing entry point. */ export interface GenerateOptions extends RenderOptions { @@ -32,154 +27,80 @@ export interface GenerateOptions extends RenderOptions { } /** {@link RenderOptions} with every defaultable field resolved. */ -export interface ResolvedRenderOptions { - readonly categoryDirectories: ReadonlyMap; - readonly genericParamOrder: ReadonlyMap; - readonly narrowableDataAttributes: ReadonlySet; - readonly targetSpecMajor: number; +export type ResolvedRenderOptions = Required; + +/** + * Runtime context threaded through every fragment renderer. Carries + * the resolved options plus the symbolic-module lookup table. + * + * Symbolic-module flavours emitted by this generator: + * + * - `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 interface RenderScope extends ResolvedRenderOptions { + readonly symbolicModules: SymbolicModuleMap; } 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, - }; + return resolveSharedRenderOptions(options); } -/** - * 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', -]); +export function validateRenderOptions(spec: Spec, options: RenderOptions): void { + validateSharedRenderOptions(spec, options); +} -/** - * 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', - ], - ], -]); +/** Hand-written branded-string types, living above `generated/`. */ +const BRAND_NAMES: readonly string[] = [ + 'CamelCaseString', + 'KebabCaseString', + 'PascalCaseString', + 'SnakeCaseString', + 'TitleCaseString', +]; -/** - * 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'], -]); +export function buildRenderScope(spec: Spec, options: RenderOptions): RenderScope { + const resolved = resolveRenderOptions(options); + const symbolicModules = new Map(); -/** - * 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) { + const folder = resolved.categoryDirectories.get(category.name); + if (folder === undefined) { + throw new Error(`unknown category "${category.name}". Extend categoryDirectories.`); + } for (const node of category.nodes) { - validNodeKinds.add(node.kind); - for (const attr of node.attributes) { - validKeys.add(`${node.kind}:${attr.name}`); - } + symbolicModules.set(`node:${node.kind}`, joinPath(folder, pascalCase(node.kind))); } - } - - 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}".`, - ); - } + for (const union of category.unions) { + symbolicModules.set(`union:${union.name}`, joinPath(folder, pascalCase(union.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.`, - ); - } + 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))); } } - 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.`, - ); - } - } - } + for (const brand of BRAND_NAMES) { + symbolicModules.set(`brand:${brand}`, '../brands'); } -} + symbolicModules.set('docs:Docs', '../Docs'); + symbolicModules.set('version:Version', '../Version'); -/** - * 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}`); -} + 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'); -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]); + return Object.freeze({ + ...resolved, + symbolicModules: Object.freeze(symbolicModules), + }); } diff --git a/packages/spec-generators/src/nodeTypes/utils/selfReference.ts b/packages/spec-generators/src/nodeTypes/selfReference.ts similarity index 100% rename from packages/spec-generators/src/nodeTypes/utils/selfReference.ts rename to packages/spec-generators/src/nodeTypes/selfReference.ts diff --git a/packages/spec-generators/src/nodeTypes/utils/index.ts b/packages/spec-generators/src/nodeTypes/utils/index.ts deleted file mode 100644 index 47bd7e3b0..000000000 --- a/packages/spec-generators/src/nodeTypes/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 714c1d653..000000000 --- a/packages/spec-generators/src/nodeTypes/utils/scope.ts +++ /dev/null @@ -1,98 +0,0 @@ -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/nodes/config.ts b/packages/spec-generators/src/nodes/config.ts new file mode 100644 index 000000000..d0319b60a --- /dev/null +++ b/packages/spec-generators/src/nodes/config.ts @@ -0,0 +1,395 @@ +/** + * Per-node configuration overrides for the `@codama/nodes` generator. + * + * The generator drives most of its output from the spec; this table + * carries only the information the spec can't express: which spec + * attributes are positional parameters (the rest go in a trailing + * `options` bag), and per-attribute overrides for defaults, + * string-coercions, and bespoke body expressions. + * + * A node with no entry uses the default rules: a single + * `input: XxxNodeInput` object param, `camelCase` on the `name` + * attribute, drop-if-empty for `docs`, conditional spread for every + * other optional attribute, and pass-through (with shorthand) for + * required ones. + */ + +import { type Fragment, fragment, use } from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { TS_RESERVED_PARAM_NAMES } from './reservedParamNames'; + +/** + * Per-attribute render override. + * + * - `coerce` widens the parameter type to ` | string` + * and emits the fragment verbatim as the body line's value + * (e.g. `typeof program === 'string' ? programLinkNode(program) : program`). + * + * - `default` populates the attribute when the caller passes + * `undefined`, emitting `attr: ?? ,`. When the + * attribute surfaces as a type parameter, `genericDefault` + * overrides the type parameter's default expression. `hidden: + * true` removes the attribute from the signature entirely and + * emits the default directly in the body (used for + * `rootNode.standard` and `rootNode.version`). + * + * - `value` is a bespoke body expression for attributes whose + * value depends on a sibling (currently only + * `instructionByteDeltaNode.withHeader`). + * + * `paramName` overrides the JS identifier when the spec attribute + * name collides with a TS reserved word (`enum` → `enumLink`). + * + * Override fragments declare their own imports via `use(...)`; the + * fragment pipeline composes them automatically. + */ +export type AttributeOverride = + | { + readonly default: Fragment; + readonly genericDefault?: Fragment; + readonly hidden?: boolean; + readonly paramName?: string; + } + | { readonly coerce: Fragment; readonly paramName?: string } + | { readonly value: Fragment }; + +export interface NodeConstructorConfig { + readonly attributes?: Readonly>; + /** + * Spec attributes surfaced as bare positional parameters, in this + * exact order. Remaining attributes land in a trailing `options` + * bag. When omitted, the constructor takes a single `input` object. + */ + readonly positionalArgs?: readonly string[]; +} + +export const NODE_CONFIGS: ReadonlyMap = new Map([ + [ + 'accountNode', + { + attributes: { + data: { + default: fragment`${use('structTypeNode', 'constructor:structTypeNode')}([])`, + genericDefault: fragment`${use('type StructTypeNode', '@codama/node-types')}<[]>`, + }, + }, + }, + ], + ['constantNode', { positionalArgs: ['name', 'type', 'value', 'docs'] }], + ['instructionAccountNode', { attributes: { isOptional: { default: fragment`false` } } }], + [ + 'instructionByteDeltaNode', + { + attributes: { + withHeader: { + value: fragment`options.withHeader ?? !${use('isNode', 'shared:isNode')}(value, 'resolverValueNode')`, + }, + }, + positionalArgs: ['value'], + }, + ], + [ + 'instructionNode', + { + attributes: { + accounts: { default: fragment`[]` }, + arguments: { default: fragment`[]` }, + optionalAccountStrategy: { default: fragment`'programId'` }, + }, + }, + ], + ['instructionRemainingAccountsNode', { positionalArgs: ['value'] }], + ['instructionStatusNode', { positionalArgs: ['lifecycle', 'message'] }], + [ + 'programNode', + { + attributes: { + accounts: { default: fragment`[]` }, + constants: { default: fragment`[]` }, + definedTypes: { default: fragment`[]` }, + errors: { default: fragment`[]` }, + events: { default: fragment`[]` }, + instructions: { default: fragment`[]` }, + pdas: { default: fragment`[]` }, + version: { default: fragment`'0.0.0'` }, + }, + }, + ], + [ + 'rootNode', + { + attributes: { + additionalPrograms: { default: fragment`[]` }, + standard: { default: fragment`'codama'`, hidden: true }, + version: { default: use('CODAMA_VERSION', 'generated:CodamaVersion'), hidden: true }, + }, + positionalArgs: ['program', 'additionalPrograms'], + }, + ], + + ['amountTypeNode', { positionalArgs: ['number', 'decimals', 'unit'] }], + ['arrayTypeNode', { positionalArgs: ['item', 'count'] }], + [ + 'booleanTypeNode', + { + attributes: { + size: { + default: fragment`${use('numberTypeNode', 'constructor:numberTypeNode')}('u8')`, + genericDefault: fragment`${use('type NumberTypeNode', '@codama/node-types')}<'u8'>`, + }, + }, + positionalArgs: ['size'], + }, + ], + ['bytesTypeNode', { positionalArgs: [] }], + ['dateTimeTypeNode', { positionalArgs: ['number'] }], + ['enumEmptyVariantTypeNode', { positionalArgs: ['name', 'discriminator'] }], + ['enumStructVariantTypeNode', { positionalArgs: ['name', 'struct', 'discriminator'] }], + ['enumTupleVariantTypeNode', { positionalArgs: ['name', 'tuple', 'discriminator'] }], + [ + 'enumTypeNode', + { + attributes: { + size: { + default: fragment`${use('numberTypeNode', 'constructor:numberTypeNode')}('u8')`, + genericDefault: fragment`${use('type NumberTypeNode', '@codama/node-types')}<'u8'>`, + }, + }, + positionalArgs: ['variants'], + }, + ], + ['fixedSizeTypeNode', { positionalArgs: ['type', 'size'] }], + ['hiddenPrefixTypeNode', { positionalArgs: ['type', 'prefix'] }], + ['hiddenSuffixTypeNode', { positionalArgs: ['type', 'suffix'] }], + ['mapTypeNode', { positionalArgs: ['key', 'value', 'count'] }], + [ + 'numberTypeNode', + { + attributes: { endian: { default: fragment`'le'` } }, + positionalArgs: ['format', 'endian'], + }, + ], + [ + 'optionTypeNode', + { + attributes: { + fixed: { default: fragment`false` }, + prefix: { + default: fragment`${use('numberTypeNode', 'constructor:numberTypeNode')}('u8')`, + genericDefault: fragment`${use('type NumberTypeNode', '@codama/node-types')}<'u8'>`, + }, + }, + positionalArgs: ['item'], + }, + ], + [ + 'postOffsetTypeNode', + { + attributes: { strategy: { default: fragment`'relative'` } }, + positionalArgs: ['type', 'offset', 'strategy'], + }, + ], + [ + 'preOffsetTypeNode', + { + attributes: { strategy: { default: fragment`'relative'` } }, + positionalArgs: ['type', 'offset', 'strategy'], + }, + ], + ['publicKeyTypeNode', { positionalArgs: [] }], + ['remainderOptionTypeNode', { positionalArgs: ['item'] }], + ['sentinelTypeNode', { positionalArgs: ['type', 'sentinel'] }], + ['setTypeNode', { positionalArgs: ['item', 'count'] }], + ['sizePrefixTypeNode', { positionalArgs: ['type', 'prefix'] }], + ['solAmountTypeNode', { positionalArgs: ['number'] }], + ['stringTypeNode', { positionalArgs: ['encoding'] }], + ['structTypeNode', { positionalArgs: ['fields'] }], + ['tupleTypeNode', { positionalArgs: ['items'] }], + ['zeroableOptionTypeNode', { positionalArgs: ['item', 'zeroValue'] }], + + ['arrayValueNode', { positionalArgs: ['items'] }], + ['booleanValueNode', { positionalArgs: ['boolean'] }], + ['bytesValueNode', { positionalArgs: ['encoding', 'data'] }], + ['constantValueNode', { positionalArgs: ['type', 'value'] }], + [ + 'enumValueNode', + { + attributes: { + enum: { + coerce: fragment`typeof enumLink === 'string' ? ${use('definedTypeLinkNode', 'constructor:definedTypeLinkNode')}(enumLink) : enumLink`, + paramName: 'enumLink', + }, + }, + positionalArgs: ['enum', 'variant', 'value'], + }, + ], + ['mapEntryValueNode', { positionalArgs: ['key', 'value'] }], + ['mapValueNode', { positionalArgs: ['entries'] }], + ['noneValueNode', { positionalArgs: [] }], + ['numberValueNode', { positionalArgs: ['number'] }], + ['publicKeyValueNode', { positionalArgs: ['publicKey', 'identifier'] }], + ['setValueNode', { positionalArgs: ['items'] }], + ['someValueNode', { positionalArgs: ['value'] }], + ['stringValueNode', { positionalArgs: ['string'] }], + ['structFieldValueNode', { positionalArgs: ['name', 'value'] }], + ['structValueNode', { positionalArgs: ['fields'] }], + ['tupleValueNode', { positionalArgs: ['items'] }], + + ['accountBumpValueNode', { positionalArgs: ['name'] }], + ['accountValueNode', { positionalArgs: ['name'] }], + ['argumentValueNode', { positionalArgs: ['name'] }], + // `conditionalValueNode` falls through to the default object-input + // rendering with no overrides — its shape is `{ condition, + // ifTrue?, ifFalse?, value? }` with no `name`/`docs` field. + ['identityValueNode', { positionalArgs: [] }], + ['payerValueNode', { positionalArgs: [] }], + ['pdaSeedValueNode', { positionalArgs: ['name', 'value'] }], + [ + 'pdaValueNode', + { + attributes: { + pda: { + coerce: fragment`typeof pda === 'string' ? ${use('pdaLinkNode', 'constructor:pdaLinkNode')}(pda) : pda`, + }, + seeds: { default: fragment`[]` }, + }, + positionalArgs: ['pda', 'seeds', 'programId'], + }, + ], + ['programIdValueNode', { positionalArgs: [] }], + ['resolverValueNode', { positionalArgs: ['name'] }], + + ['fixedCountNode', { positionalArgs: ['value'] }], + ['prefixedCountNode', { positionalArgs: ['prefix'] }], + ['remainderCountNode', { positionalArgs: [] }], + + [ + 'constantDiscriminatorNode', + { + attributes: { offset: { default: fragment`0` } }, + positionalArgs: ['constant', 'offset'], + }, + ], + [ + 'fieldDiscriminatorNode', + { + attributes: { offset: { default: fragment`0` } }, + positionalArgs: ['name', 'offset'], + }, + ], + ['sizeDiscriminatorNode', { positionalArgs: ['size'] }], + + [ + 'accountLinkNode', + { + attributes: { + program: { + coerce: fragment`typeof program === 'string' ? ${use('programLinkNode', 'constructor:programLinkNode')}(program) : program`, + }, + }, + positionalArgs: ['name', 'program'], + }, + ], + [ + 'definedTypeLinkNode', + { + attributes: { + program: { + coerce: fragment`typeof program === 'string' ? ${use('programLinkNode', 'constructor:programLinkNode')}(program) : program`, + }, + }, + positionalArgs: ['name', 'program'], + }, + ], + [ + 'instructionAccountLinkNode', + { + attributes: { + instruction: { + coerce: fragment`typeof instruction === 'string' ? ${use('instructionLinkNode', 'constructor:instructionLinkNode')}(instruction) : instruction`, + }, + }, + positionalArgs: ['name', 'instruction'], + }, + ], + [ + 'instructionArgumentLinkNode', + { + attributes: { + instruction: { + coerce: fragment`typeof instruction === 'string' ? ${use('instructionLinkNode', 'constructor:instructionLinkNode')}(instruction) : instruction`, + }, + }, + positionalArgs: ['name', 'instruction'], + }, + ], + [ + 'instructionLinkNode', + { + attributes: { + program: { + coerce: fragment`typeof program === 'string' ? ${use('programLinkNode', 'constructor:programLinkNode')}(program) : program`, + }, + }, + positionalArgs: ['name', 'program'], + }, + ], + [ + 'pdaLinkNode', + { + attributes: { + program: { + coerce: fragment`typeof program === 'string' ? ${use('programLinkNode', 'constructor:programLinkNode')}(program) : program`, + }, + }, + positionalArgs: ['name', 'program'], + }, + ], + ['programLinkNode', { positionalArgs: ['name'] }], + + ['constantPdaSeedNode', { positionalArgs: ['type', 'value'] }], + ['variablePdaSeedNode', { positionalArgs: ['name', 'type', 'docs'] }], +]); + +/** + * Cross-check a `nodeConfigs` map against the spec at generation time. + * Catches stale config entries, attribute typos, and reserved + * positional-arg names without a `paramName` override. + */ +export function validateNodeConfigs(spec: Spec, nodeConfigs: ReadonlyMap): void { + const allNodes = spec.categories.flatMap(c => c.nodes); + const validNodeKinds = new Set(allNodes.map(n => n.kind)); + const validKeys = new Set(allNodes.flatMap(n => n.attributes.map(a => `${n.kind}:${a.name}`))); + + for (const [kind, config] of nodeConfigs) { + if (!validNodeKinds.has(kind)) { + throw new Error(`nodeConfigs references unknown node kind "${kind}".`); + } + for (const attrName of Object.keys(config.attributes ?? {})) { + if (!validKeys.has(`${kind}:${attrName}`)) { + throw new Error( + `nodeConfigs.attributes for "${kind}" references attribute "${attrName}" which the spec does not declare.`, + ); + } + } + for (const name of config.positionalArgs ?? []) { + if (!validKeys.has(`${kind}:${name}`)) { + throw new Error( + `nodeConfigs.positionalArgs for "${kind}" references attribute "${name}" which the spec does not declare.`, + ); + } + if (TS_RESERVED_PARAM_NAMES.has(name)) { + const override = config.attributes?.[name]; + const hasParamName = + override !== undefined && 'paramName' in override && override.paramName !== undefined; + if (!hasParamName) { + throw new Error( + `nodeConfigs for "${kind}" lists "${name}" as a positional arg but it's a TS reserved word; ` + + `add a \`paramName\` override on the attribute.`, + ); + } + } + } + } +} diff --git a/packages/spec-generators/src/nodes/fragments/codamaVersionConstant.ts b/packages/spec-generators/src/nodes/fragments/codamaVersionConstant.ts new file mode 100644 index 000000000..ced883ffc --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/codamaVersionConstant.ts @@ -0,0 +1,22 @@ +import { type Fragment, fragment, getDocblockFragment, use } from '@codama/fragments/javascript'; + +/** + * Build the file body for `generated/codamaVersion.ts` — the + * `CODAMA_VERSION` constant pinned to the spec version at generation + * time and typed as `CodamaVersion` from `@codama/node-types`. + */ +export function getCodamaVersionConstantFragment(specVersion: string): Fragment { + const docblock = getDocblockFragment( + [ + 'The Codama spec version this package was generated against.', + '', + 'Pinned to the literal version of `@codama/spec` at generation time.', + 'Used by `rootNode()` to tag the document and by downstream consumers', + "that need to compare an IDL document's `version` against the spec", + 'shape `@codama/nodes` understands.', + ], + { withLineJump: true }, + ); + const codamaVersionType = use('type CodamaVersion', '@codama/node-types'); + return fragment`${docblock}export const CODAMA_VERSION: ${codamaVersionType} = '${specVersion}';\n`; +} diff --git a/packages/spec-generators/src/nodes/fragments/index.ts b/packages/spec-generators/src/nodes/fragments/index.ts new file mode 100644 index 000000000..d5075e033 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/index.ts @@ -0,0 +1,13 @@ +export * from './codamaVersionConstant'; +export * from './inputType'; +export * from './kindUnionConstant'; +export * from './nodeFunction'; +export * from './nodeFunctionAttribute'; +export * from './nodeFunctionBody'; +export * from './nodeFunctionPositionalArguments'; +export * from './nodeFunctionReturnType'; +export * from './nodeFunctionTypeParameters'; +export * from './nodeKindUnionConstant'; +export * from './nodePage'; +export * from './nodeTypeParameters'; +export * from './typeExpr'; diff --git a/packages/spec-generators/src/nodes/fragments/inputType.ts b/packages/spec-generators/src/nodes/fragments/inputType.ts new file mode 100644 index 000000000..815f6a3b3 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/inputType.ts @@ -0,0 +1,113 @@ +/** + * Render the `XxxNodeInput` type declaration for an object-input node + * function. + * + * The Input type takes the same type parameters as the node function + * (with the "wide" defaults: each type parameter defaults to its own + * constraint) and relaxes the strict shape of the matching node + * interface so callers can pass `name: string` instead of the branded + * `CamelCaseString`, `docs?: DocsInput` instead of `Docs`, and omit any + * attribute the node function defaults (via a `Partial<>` wrap when + * present). + */ + +import { type Fragment, fragment, mergeFragments, use } from '@codama/fragments/javascript'; +import type { AttributeSpec, NodeSpec } from '@codama/spec'; + +import { getTypeParameterIdentifierFragment, getTypeParameterIdentifierListFragment } from '../../shared'; +import type { NodeConstructorConfig } from '../config'; +import { getNodeTypeParameterConstraint } from './nodeTypeParameters'; + +export function getInputTypeFragment( + node: NodeSpec, + interfaceName: string, + config: NodeConstructorConfig | undefined, + typeParameterAttributes: readonly AttributeSpec[], +): Fragment { + const inputName = `${interfaceName}Input`; + const typeParametersBlock = getInputTypeParametersFragment(typeParameterAttributes); + + const reassertedFields = collectReassertedFields(node); + const omittedFromBase = collectOmittedFields(reassertedFields); + + const hasDefaults = nodeHasDefaultedRequiredAttribute(node, config); + const baseTypeRef = use(`type ${interfaceName}`, '@codama/node-types'); + const baseWithGenerics = renderBaseWithGenerics(baseTypeRef, typeParameterAttributes); + const omitClause = `'${omittedFromBase.join("' | '")}'`; + const baseClause = hasDefaults + ? fragment`Omit, ${omitClause}>` + : fragment`Omit<${baseWithGenerics}, ${omitClause}>`; + + if (reassertedFields.length === 0) { + return fragment`export type ${inputName}${typeParametersBlock} = ${baseClause};\n`; + } + const intersectionLines = reassertedFields.map(f => fragment`readonly ${f.name}: ${f.tsType};`); + const intersectionBlock = mergeFragments(intersectionLines, parts => parts.join('\n')); + return fragment`export type ${inputName}${typeParametersBlock} = ${baseClause} & {\n${intersectionBlock}\n};\n`; +} + +/** + * The `` type-parameter block + * on a `XxxNodeInput` type. Each parameter uses the "wide" default — + * its own constraint — so callers who don't narrow get a usable Input + * type. + */ +function getInputTypeParametersFragment(typeParameterAttributes: readonly AttributeSpec[]): Fragment { + if (typeParameterAttributes.length === 0) return fragment``; + const parts = typeParameterAttributes.map(attr => { + const constraint = getNodeTypeParameterConstraint(attr); + return fragment`${getTypeParameterIdentifierFragment(attr.name)} extends ${constraint} = ${constraint}`; + }); + const joined = mergeFragments(parts, ps => ps.join(',\n')); + return fragment`<\n${joined}\n>`; +} + +/** + * `true` if the node has a spec-required attribute that the node + * function defaults via an override. The Input type then wraps the + * base interface in `Partial<>` so callers can omit those fields. + */ +function nodeHasDefaultedRequiredAttribute(node: NodeSpec, config: NodeConstructorConfig | undefined): boolean { + const attributes = config?.attributes; + if (!attributes) return false; + return node.attributes.some(attr => { + if (attr.optional === true) return false; + const override = attributes[attr.name]; + return override !== undefined && 'default' in override; + }); +} + +/** + * Fields the Input type re-asserts in the intersection block: + * + * - `name: string` (relaxed from `CamelCaseString`) + * - `docs?: DocsInput` (relaxed from `Docs`) + * - `publicKey: ProgramNode['publicKey']` for `programNode` — a + * required-not-defaulted field that survives the `Partial<>` wrap. + */ +function collectReassertedFields(node: NodeSpec): readonly { readonly name: string; readonly tsType: Fragment }[] { + const out: { name: string; tsType: Fragment }[] = []; + if (node.attributes.some(a => a.name === 'name')) { + out.push({ name: 'name', tsType: fragment`string` }); + } + if (node.attributes.some(a => a.name === 'docs')) { + const docsInput = use('DocsInput', 'shared:DocsInput'); + out.push({ name: 'docs?', tsType: fragment`${docsInput}` }); + } + if (node.kind === 'programNode') { + out.push({ name: 'publicKey', tsType: fragment`ProgramNode['publicKey']` }); + } + return out; +} + +function collectOmittedFields(reassertedFields: readonly { readonly name: string }[]): readonly string[] { + const set = new Set(['kind']); + for (const f of reassertedFields) set.add(f.name.replace(/\?$/, '')); + return [...set].sort(); +} + +function renderBaseWithGenerics(baseRef: Fragment, typeParameterAttributes: readonly AttributeSpec[]): Fragment { + if (typeParameterAttributes.length === 0) return baseRef; + const names = getTypeParameterIdentifierListFragment(typeParameterAttributes); + return fragment`${baseRef}<${names}>`; +} diff --git a/packages/spec-generators/src/nodes/fragments/kindUnionConstant.ts b/packages/spec-generators/src/nodes/fragments/kindUnionConstant.ts new file mode 100644 index 000000000..daeb5fc4b --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/kindUnionConstant.ts @@ -0,0 +1,41 @@ +/** + * Render a runtime `*_KINDS` array from a spec union. + * + * Each leaf kind is tagged `as const` rather than the whole array, so + * the resulting type is the convenient `('kindA' | 'kindB' | ...)[]` + * shape — wider (mutable) than a `readonly [...]` tuple but compatible + * with the legacy hand-written arrays' type and with the `isNode` + * helpers' `kind: TKind | TKind[]` parameter. Composed unions spread + * their members' arrays. + */ + +import { type Fragment, fragment, getDocblockFragment, mergeFragments, use } from '@codama/fragments/javascript'; +import type { UnionMember, UnionSpec } from '@codama/spec'; + +import { kindsArrayConstantName } from '../kindsArrayConstantName'; + +export function getKindUnionConstantFragment(union: UnionSpec): Fragment { + const constantName = kindsArrayConstantName(union.name); + const sortedMembers = sortMembers(union.members); + const memberLines = sortedMembers.map(renderMember); + + const body = mergeFragments(memberLines, parts => parts.join('\n')); + const docComment = getDocblockFragment(union.docs, { withLineJump: true }); + return fragment`${docComment}export const ${constantName} = [\n${body}\n];\n`; +} + +function renderMember(member: UnionMember): Fragment { + if (member.kind === 'node') { + return fragment`'${member.name}' as const,`; + } + const constantName = kindsArrayConstantName(member.name); + return fragment`...${use(constantName, `kinds:${member.name}`)},`; +} + +function sortMembers(members: readonly UnionMember[]): readonly UnionMember[] { + return [...members].sort((a, b) => sortKey(a).localeCompare(sortKey(b))); +} + +function sortKey(member: UnionMember): string { + return member.kind === 'node' ? member.name : kindsArrayConstantName(member.name); +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeFunction.ts b/packages/spec-generators/src/nodes/fragments/nodeFunction.ts new file mode 100644 index 000000000..689660c36 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeFunction.ts @@ -0,0 +1,49 @@ +import { type Fragment, fragment, getDocblockFragment } from '@codama/fragments/javascript'; +import type { AttributeSpec, NodeSpec } from '@codama/spec'; + +import { getTypeParameterIdentifierListFragment } from '../../shared'; +import type { NodeConstructorConfig } from '../config'; +import type { ResolvedRenderOptions } from '../options'; +import { getNodeFunctionBodyFragment } from './nodeFunctionBody'; +import { getNodeFunctionPositionalArgumentsFragment } from './nodeFunctionPositionalArguments'; +import { getNodeFunctionReturnTypeFragment } from './nodeFunctionReturnType'; +import { getNodeFunctionTypeParametersFragment } from './nodeFunctionTypeParameters'; +import { computeConstructorDefaults } from './nodeTypeParameters'; + +/** The `export function xxxNode(...)` declaration: signature + body. */ +export function getNodeFunctionFragment( + node: NodeSpec, + interfaceName: string, + config: NodeConstructorConfig | undefined, + typeParameterAttributes: readonly AttributeSpec[], + scope: Pick, +): Fragment { + const isPositional = config?.positionalArgs !== undefined; + const constructorDefaults = computeConstructorDefaults(node, typeParameterAttributes, config, scope); + + const docComment = getDocblockFragment(node.docs, { withLineJump: true }); + const typeParameters = getNodeFunctionTypeParametersFragment(typeParameterAttributes, constructorDefaults); + const returnType = getNodeFunctionReturnTypeFragment(interfaceName, typeParameterAttributes); + const params = isPositional + ? getNodeFunctionPositionalArgumentsFragment(node, typeParameterAttributes, config) + : getNodeFunctionInputParameterFragment(interfaceName, typeParameterAttributes); + const body = getNodeFunctionBodyFragment(node, config, typeParameterAttributes); + + const fnName = camelCaseFirst(interfaceName); + return fragment`${docComment}export function ${fnName}${typeParameters}(${params}): ${returnType} {\n${body}\n}\n`; +} + +/** The single-`input`-object parameter: `input: XxxNodeInput`. */ +function getNodeFunctionInputParameterFragment( + interfaceName: string, + typeParameterAttributes: readonly AttributeSpec[], +): Fragment { + const inputName = `${interfaceName}Input`; + if (typeParameterAttributes.length === 0) return fragment`input: ${inputName}`; + const args = getTypeParameterIdentifierListFragment(typeParameterAttributes); + return fragment`input: ${inputName}<${args}>`; +} + +function camelCaseFirst(s: string): string { + return s.charAt(0).toLowerCase() + s.slice(1); +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeFunctionAttribute.ts b/packages/spec-generators/src/nodes/fragments/nodeFunctionAttribute.ts new file mode 100644 index 000000000..ecce1ca26 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeFunctionAttribute.ts @@ -0,0 +1,80 @@ +import { type Fragment, fragment, use } from '@codama/fragments/javascript'; +import type { AttributeSpec } from '@codama/spec'; + +import { getTypeParameterIdentifierFragment } from '../../shared'; +import type { AttributeOverride } from '../config'; +import { isStringIdentifierAttr } from '../paramIdentifier'; + +/** + * Render one attribute as a field of the node function's + * `return Object.freeze({ … })` literal. Rules, in priority order: + * + * 1. `hidden`-defaulted → emit the default expression directly. + * Hidden attributes are never exposed to the caller, so no + * other rule applies. + * 2. `value` override → emit the override's expression verbatim. + * 3. `stringIdentifier()`-typed → wrap reader in `camelCase(...)`, + * under a conditional spread when optional. + * 4. `coerce` override → emit the coerce fragment, with `as TGeneric` + * cast when the attribute is a type parameter, under a conditional + * spread when optional. + * 5. `default` override → bare positional args carry the default at + * the signature level; bag/object-input attributes get a + * body-level `?? ` fallback. + * 6. Optional attribute → conditional spread. + * 7. Required attribute → pass-through, with shorthand `{ key }` when + * the reader equals the key. + */ +export function getNodeFunctionAttributeFragment( + attr: AttributeSpec, + reader: string, + override: AttributeOverride | undefined, + typeParameterAttribute: AttributeSpec | undefined, + isBarePositional: boolean, +): Fragment { + const key = attr.name; + + if (override && 'default' in override && override.hidden) { + return fragment`${key}: ${override.default},`; + } + + if (override && 'value' in override) { + return fragment`${key}: ${override.value},`; + } + + if (isStringIdentifierAttr(attr)) { + const camelCaseRef = use('camelCase', 'shared:camelCase'); + if (attr.optional) { + return fragment`...(${reader} !== undefined && { ${key}: ${camelCaseRef}(${reader}) }),`; + } + return fragment`${key}: ${camelCaseRef}(${reader}),`; + } + + if (override && 'coerce' in override) { + const valueExpr = typeParameterAttribute + ? fragment`(${override.coerce}) as ${getTypeParameterIdentifierFragment(typeParameterAttribute)}` + : override.coerce; + if (attr.optional) { + return fragment`...(${reader} !== undefined && { ${key}: ${valueExpr} }),`; + } + return fragment`${key}: ${valueExpr},`; + } + + if (override && 'default' in override) { + if (isBarePositional) { + return reader === key ? fragment`${key},` : fragment`${key}: ${reader},`; + } + const valueExpr = typeParameterAttribute + ? fragment`(${reader} ?? ${override.default}) as ${getTypeParameterIdentifierFragment(typeParameterAttribute)}` + : fragment`${reader} ?? ${override.default}`; + return fragment`${key}: ${valueExpr},`; + } + + if (attr.optional) { + return reader === key + ? fragment`...(${reader} !== undefined && { ${key} }),` + : fragment`...(${reader} !== undefined && { ${key}: ${reader} }),`; + } + + return reader === key ? fragment`${key},` : fragment`${key}: ${reader},`; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeFunctionBody.ts b/packages/spec-generators/src/nodes/fragments/nodeFunctionBody.ts new file mode 100644 index 000000000..dbc0bd580 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeFunctionBody.ts @@ -0,0 +1,80 @@ +import { type Fragment, fragment, mergeFragments, use } from '@codama/fragments/javascript'; +import { type AttributeSpec, isChildAttribute, type NodeSpec } from '@codama/spec'; + +import type { AttributeOverride, NodeConstructorConfig } from '../config'; +import { paramIdentifier } from '../paramIdentifier'; +import { getNodeFunctionAttributeFragment } from './nodeFunctionAttribute'; + +/** + * The node function body — a `return Object.freeze({ kind, ... });` + * block, optionally preceded by a `const parsedDocs = parseDocs(...);` + * hoist when the node has a `docs` attribute. + */ +export function getNodeFunctionBodyFragment( + node: NodeSpec, + config: NodeConstructorConfig | undefined, + typeParameterAttributes: readonly AttributeSpec[], +): Fragment { + const isPositional = config?.positionalArgs !== undefined; + const positionalSet = new Set(config?.positionalArgs ?? []); + const typeParameterAttrsByName = new Map(typeParameterAttributes.map(attr => [attr.name, attr])); + + const dataLines: Fragment[] = []; + const childLines: Fragment[] = []; + let docsPreStatement: Fragment | undefined; + + for (const attr of node.attributes) { + const isChild = isChildAttribute(attr.type); + const override = config?.attributes?.[attr.name]; + const isBarePositional = isPositional && positionalSet.has(attr.name); + const reader = computeReader(attr, override, isPositional, positionalSet); + const typeParamAttr = typeParameterAttrsByName.get(attr.name); + + if (attr.type.kind === 'docs') { + // Hoist `parseDocs()` to a `parsedDocs` local so + // we can both test its length and reuse the result. Named + // `parsedDocs` rather than `docs` to avoid shadowing a + // positional `docs` parameter. + const parseDocsRef = use('parseDocs', 'shared:parseDocs'); + docsPreStatement = fragment`const parsedDocs = ${parseDocsRef}(${reader});`; + dataLines.push(fragment`...(parsedDocs.length > 0 && { docs: parsedDocs }),`); + continue; + } + + const line = getNodeFunctionAttributeFragment(attr, reader, override, typeParamAttr, isBarePositional); + (isChild ? childLines : dataLines).push(line); + } + + const kindLine = fragment`kind: '${node.kind}',`; + const sectionFragments: Fragment[] = [kindLine]; + if (dataLines.length > 0) { + sectionFragments.push(fragment``, fragment`// Data.`, ...dataLines); + } + if (childLines.length > 0) { + sectionFragments.push(fragment``, fragment`// Children.`, ...childLines); + } + + const objectLiteral = mergeFragments(sectionFragments, parts => parts.join('\n')); + const returnBlock = fragment`return Object.freeze({\n${objectLiteral}\n});`; + + if (!docsPreStatement) return returnBlock; + return fragment`${docsPreStatement}\n${returnBlock}`; +} + +/** + * How the body refers to one attribute's incoming value: + * + * - Object-input → `input.` + * - Positional bare arg → `` (the JS identifier) + * - Positional bag arg → `options.` + */ +function computeReader( + attr: AttributeSpec, + override: AttributeOverride | undefined, + isPositional: boolean, + positionalSet: ReadonlySet, +): string { + if (!isPositional) return `input.${attr.name}`; + if (positionalSet.has(attr.name)) return paramIdentifier(attr, override); + return `options.${attr.name}`; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeFunctionPositionalArguments.ts b/packages/spec-generators/src/nodes/fragments/nodeFunctionPositionalArguments.ts new file mode 100644 index 000000000..c792aa2d0 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeFunctionPositionalArguments.ts @@ -0,0 +1,117 @@ +import { type Fragment, fragment, mergeFragments, use } from '@codama/fragments/javascript'; +import type { AttributeSpec, NodeSpec } from '@codama/spec'; + +import { getTypeParameterIdentifierFragment } from '../../shared'; +import type { AttributeOverride, NodeConstructorConfig } from '../config'; +import { isStringIdentifierAttr, paramIdentifier } from '../paramIdentifier'; +import { getNodeTypeParameterConstraint } from './nodeTypeParameters'; +import { getTypeExprFragment } from './typeExpr'; + +/** + * The node function's positional-argument list plus a trailing + * `options` bag for every attribute not listed in `positionalArgs`. + * `hidden`-defaulted attributes are excluded from both — they're + * emitted directly in the body. Returned fragment has no leading or + * trailing whitespace; the caller composes it into a larger fragment. + */ +export function getNodeFunctionPositionalArgumentsFragment( + node: NodeSpec, + typeParameterAttributes: readonly AttributeSpec[], + config: NodeConstructorConfig, +): Fragment { + const positionalNames = config.positionalArgs ?? []; + const typeParameterAttrsByName = new Map(typeParameterAttributes.map(attr => [attr.name, attr])); + + const bareParams = positionalNames.map(name => { + const attr = findAttr(node, name); + return renderParamForAttribute(attr, typeParameterAttrsByName.get(name), config); + }); + + const bagAttrs = node.attributes.filter(a => { + if (positionalNames.includes(a.name)) return false; + const override = config.attributes?.[a.name]; + if (override && 'default' in override && override.hidden) return false; + return true; + }); + if (bagAttrs.length > 0) { + const bagFields = bagAttrs.map(a => renderBagField(a, typeParameterAttrsByName.get(a.name))); + const fieldLines = mergeFragments(bagFields, parts => parts.map(p => `${p};`).join('\n')); + const bagType = fragment`{\n${fieldLines}\n}`; + bareParams.push(fragment`options: ${bagType} = {}`); + } + + if (bareParams.length === 0) return fragment``; + if (bareParams.length === 1) return bareParams[0]; + return mergeFragments(bareParams, parts => parts.map(p => `${p},`).join('\n')); +} + +function findAttr(node: NodeSpec, name: string): AttributeSpec { + const attr = node.attributes.find(a => a.name === name); + if (!attr) { + throw new Error(`spec node "${node.kind}" has no attribute "${name}".`); + } + return attr; +} + +function renderParamForAttribute( + attr: AttributeSpec, + typeParameterAttribute: AttributeSpec | undefined, + config: NodeConstructorConfig, +): Fragment { + const override = config.attributes?.[attr.name]; + const paramName = paramIdentifier(attr, override); + const baseTsType = renderParamTsType(attr, typeParameterAttribute, override); + const optionalMark = attr.optional ? '?' : ''; + const defaultClause = renderParamDefaultClause(override, typeParameterAttribute); + return fragment`${paramName}${optionalMark}: ${baseTsType}${defaultClause}`; +} + +function renderBagField(attr: AttributeSpec, typeParameterAttribute: AttributeSpec | undefined): Fragment { + return fragment`${attr.name}?: ${renderAttributeTsType(attr, typeParameterAttribute)}`; +} + +function renderParamTsType( + attr: AttributeSpec, + typeParameterAttribute: AttributeSpec | undefined, + override: AttributeOverride | undefined, +): Fragment { + const base = renderAttributeTsType(attr, typeParameterAttribute); + return override && 'coerce' in override ? fragment`${base} | string` : base; +} + +/** + * The TS type expression for one attribute in the node function's + * positional-parameter signature or bag-field type. Type-parameter + * attributes render as their generic identifier; `docs` widens to + * `DocsInput`; `stringIdentifier()`-typed attributes widen to plain + * `string`. Everything else falls through to the spec type. + */ +function renderAttributeTsType(attr: AttributeSpec, typeParameterAttribute: AttributeSpec | undefined): Fragment { + if (typeParameterAttribute) return fragment`${getTypeParameterIdentifierFragment(typeParameterAttribute)}`; + if (attr.type.kind === 'docs') return use('DocsInput', 'shared:DocsInput'); + if (isStringIdentifierAttr(attr)) return fragment`string`; + return getTypeExprFragment(attr.type); +} + +function renderParamDefaultClause( + override: AttributeOverride | undefined, + typeParameterAttribute: AttributeSpec | undefined, +): Fragment { + if (!override || !('default' in override)) return fragment``; + // For type-parameter attributes, cast the value-level default + // through to the generic so the caller can still narrow. The + // intermediate cast (e.g. `as ProgramNode[]` for an `[]` default + // on a `ProgramNode[]` attribute) widens the literal expression + // before the final narrowing — matches the existing hand-written + // pattern `[] as ProgramNode[] as TAdditionalPrograms`. + if (typeParameterAttribute) { + const genericName = getTypeParameterIdentifierFragment(typeParameterAttribute); + const intermediate = override.genericDefault ?? getNodeTypeParameterConstraint(typeParameterAttribute); + const needsIntermediate = intermediate.content !== override.default.content; + const castChain = needsIntermediate + ? fragment`${override.default} as ${intermediate} as ${genericName}` + : fragment`${override.default} as ${genericName}`; + return fragment` = ${castChain}`; + } + return fragment` = ${override.default}`; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeFunctionReturnType.ts b/packages/spec-generators/src/nodes/fragments/nodeFunctionReturnType.ts new file mode 100644 index 000000000..3f6d1cbf6 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeFunctionReturnType.ts @@ -0,0 +1,15 @@ +import { type Fragment, fragment, use } from '@codama/fragments/javascript'; +import type { AttributeSpec } from '@codama/spec'; + +import { getTypeParameterIdentifierListFragment } from '../../shared'; + +/** The node function's return type: `XxxNode` or just `XxxNode`. */ +export function getNodeFunctionReturnTypeFragment( + interfaceName: string, + typeParameterAttributes: readonly AttributeSpec[], +): Fragment { + const interfaceRef = use(`type ${interfaceName}`, '@codama/node-types'); + if (typeParameterAttributes.length === 0) return interfaceRef; + const args = getTypeParameterIdentifierListFragment(typeParameterAttributes); + return fragment`${interfaceRef}<${args}>`; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeFunctionTypeParameters.ts b/packages/spec-generators/src/nodes/fragments/nodeFunctionTypeParameters.ts new file mode 100644 index 000000000..14a410a75 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeFunctionTypeParameters.ts @@ -0,0 +1,28 @@ +import { type Fragment, fragment, mergeFragments } from '@codama/fragments/javascript'; +import type { AttributeSpec } from '@codama/spec'; + +import { getTypeParameterIdentifierFragment } from '../../shared'; +import { getNodeTypeParameterConstraint } from './nodeTypeParameters'; + +/** + * The `` type-parameter + * block on a node function. + * + * Every type parameter gets a `const` prefix: it's a no-op for + * constraint-narrowed cases (`TFormat extends NumberFormat` already + * narrows literal arguments via the union) and a strict win for the + * array / object literal cases where TS would otherwise widen. + */ +export function getNodeFunctionTypeParametersFragment( + typeParameterAttributes: readonly AttributeSpec[], + constructorDefaults: readonly Fragment[], +): Fragment { + if (typeParameterAttributes.length === 0) return fragment``; + const parts = typeParameterAttributes.map((attr, i) => { + const defaultExpr = constructorDefaults[i]; + const defaultClause = defaultExpr.content ? fragment` = ${defaultExpr}` : fragment``; + return fragment`const ${getTypeParameterIdentifierFragment(attr)} extends ${getNodeTypeParameterConstraint(attr)}${defaultClause}`; + }); + const joined = mergeFragments(parts, ps => ps.map(p => `${p},`).join('\n')); + return fragment`<\n${joined}\n>`; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeKindUnionConstant.ts b/packages/spec-generators/src/nodes/fragments/nodeKindUnionConstant.ts new file mode 100644 index 000000000..be999ed0c --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeKindUnionConstant.ts @@ -0,0 +1,32 @@ +/** + * Render the top-level `REGISTERED_NODE_KINDS` registry: a runtime + * `readonly string[]` listing every node kind in the spec, composed by + * spreading each `RegisteredNode` kinds array plus the bare + * kind of every node not already covered by one of those unions. + */ + +import { type Fragment, fragment, mergeFragments, use } from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { flattenNodeUnion, getRegisteredCategoryUnions } from '../../shared'; +import { kindsArrayConstantName } from '../kindsArrayConstantName'; + +export function getNodeKindUnionConstantFragment(spec: Spec): Fragment { + const registeredUnions = getRegisteredCategoryUnions(spec); + + const coveredKinds = new Set(registeredUnions.flatMap(u => flattenNodeUnion(u, spec).map(n => n.kind))); + + const spreadLines = registeredUnions.map( + u => fragment`...${use(kindsArrayConstantName(u.name), `kinds:${u.name}`)},`, + ); + + const bareKinds = spec.categories + .flatMap(c => c.nodes) + .map(node => node.kind) + .filter(kind => !coveredKinds.has(kind)) + .sort(); + const bareLines = bareKinds.map(k => fragment`'${k}' as const,`); + + const body = mergeFragments([...bareLines, ...spreadLines], parts => parts.join('\n')); + return fragment`// Node Registration.\nexport const REGISTERED_NODE_KINDS = [\n${body}\n];\n`; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodePage.ts b/packages/spec-generators/src/nodes/fragments/nodePage.ts new file mode 100644 index 000000000..53aeaa5e0 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodePage.ts @@ -0,0 +1,31 @@ +import { type Fragment, fragment } from '@codama/fragments/javascript'; +import type { NodeSpec } from '@codama/spec'; + +import type { NodeConstructorConfig } from '../config'; +import { getNodeTypeParameterAttributes, type ResolvedRenderOptions } from '../options'; +import { getInputTypeFragment } from './inputType'; +import { getNodeFunctionFragment } from './nodeFunction'; + +/** + * The full file body for one node's constructor file: an optional + * `XxxNodeInput` type declaration followed by the node function. + * Positional-arg node functions don't emit an Input type. + * + * The type-parameter-attribute list is computed once here and threaded + * down to the input type and the node function so neither has to + * recompute it. + */ +export function getNodePageFragment( + node: NodeSpec, + interfaceName: string, + config: NodeConstructorConfig | undefined, + scope: Pick, +): Fragment { + const isPositional = config?.positionalArgs !== undefined; + const typeParameterAttributes = getNodeTypeParameterAttributes(node, scope); + const inputType = isPositional + ? undefined + : getInputTypeFragment(node, interfaceName, config, typeParameterAttributes); + const nodeFunction = getNodeFunctionFragment(node, interfaceName, config, typeParameterAttributes, scope); + return inputType ? fragment`${inputType}\n${nodeFunction}` : nodeFunction; +} diff --git a/packages/spec-generators/src/nodes/fragments/nodeTypeParameters.ts b/packages/spec-generators/src/nodes/fragments/nodeTypeParameters.ts new file mode 100644 index 000000000..c79418a63 --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/nodeTypeParameters.ts @@ -0,0 +1,78 @@ +import { type Fragment, fragment } from '@codama/fragments/javascript'; +import { type AttributeSpec, type NodeSpec } from '@codama/spec'; + +import { type AttributeOverride, type NodeConstructorConfig } from '../config'; +import { type ResolvedRenderOptions } from '../options'; +import { getTypeExprFragment } from './typeExpr'; + +/** + * The constraint Fragment for the type parameter derived from `attr`. + * Optional attributes get `| undefined` appended; required attributes + * use the bare type expression. + */ +export function getNodeTypeParameterConstraint(attr: AttributeSpec): Fragment { + const baseType = getTypeExprFragment(attr.type); + return attr.optional === true ? fragment`${baseType} | undefined` : baseType; +} + +/** + * Compute the node function's per-type-parameter default expressions + * in lockstep with the type-parameter-attribute list's output order. + * Returns an empty fragment for required type parameters with no + * default. + * + * As a final pass, any required-no-default type parameter that follows + * a defaulted one is broadened to its wide constraint — TS requires + * required type parameters to appear before defaulted ones. + */ +export function computeConstructorDefaults( + node: NodeSpec, + typeParameterAttributes: readonly AttributeSpec[], + config: NodeConstructorConfig | undefined, + scope: Pick, +): readonly Fragment[] { + const raw = typeParameterAttributes.map(attr => { + const override = config?.attributes?.[attr.name]; + return computeConstructorDefault(node.kind, attr, override, getTypeExprFragment(attr.type), scope); + }); + return broadenTrailingRequiredTypeParameters(raw, typeParameterAttributes); +} + +function broadenTrailingRequiredTypeParameters( + defaults: readonly Fragment[], + typeParameterAttributes: readonly AttributeSpec[], +): readonly Fragment[] { + let seenDefaulted = false; + return defaults.map((d, i) => { + if (d.content) { + seenDefaulted = true; + return d; + } + if (seenDefaulted) { + return getNodeTypeParameterConstraint(typeParameterAttributes[i]); + } + return d; + }); +} + +function computeConstructorDefault( + nodeKind: string, + attr: AttributeSpec, + override: AttributeOverride | undefined, + baseType: Fragment, + scope: Pick, +): Fragment { + if (override && 'default' in override) { + return override.genericDefault ?? override.default; + } + if (attr.optional === true) { + return fragment`undefined`; + } + if (override && 'coerce' in override) { + return baseType; + } + if (scope.narrowableDataAttributes.has(`${nodeKind}:${attr.name}`)) { + return baseType; + } + return fragment``; +} diff --git a/packages/spec-generators/src/nodes/fragments/typeExpr.ts b/packages/spec-generators/src/nodes/fragments/typeExpr.ts new file mode 100644 index 000000000..7181ffa5e --- /dev/null +++ b/packages/spec-generators/src/nodes/fragments/typeExpr.ts @@ -0,0 +1,77 @@ +/** + * Render a spec {@link TypeExpr} as a TypeScript type expression + * suitable for an `XxxNodeInput` declaration in `@codama/nodes`. + * + * Named references (`node`, `union`, `enumeration`, `nestedUnion`) and + * brand-flavoured strings all resolve to identifiers exported from + * `@codama/node-types`. Array types render as `Array` rather than + * `T[]` to keep the renderer free of precedence-aware parenthesisation. + */ + +import { type Fragment, fragment, mergeFragments, pascalCase, use } from '@codama/fragments/javascript'; +import type { TypeExpr } from '@codama/spec'; + +const NODE_TYPES_PACKAGE = '@codama/node-types'; + +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`${renderLiteralUnion(expr.values)}`; + case 'codamaVersion': + return use('type CodamaVersion', NODE_TYPES_PACKAGE); + case 'docs': + return use('type Docs', NODE_TYPES_PACKAGE); + case 'enumeration': + case 'node': + case 'union': + return use(`type ${pascalCase(expr.name)}`, NODE_TYPES_PACKAGE); + case 'nestedUnion': { + const wrapper = use(`type ${pascalCase(expr.alias)}`, NODE_TYPES_PACKAGE); + const inner = use(`type ${pascalCase(expr.name)}`, NODE_TYPES_PACKAGE); + return fragment`${wrapper}<${inner}>`; + } + case 'array': { + const inner = getTypeExprFragment(expr.of); + return fragment`Array<${inner}>`; + } + case 'tuple': { + if (expr.items.length === 0) return fragment`[]`; + const items = expr.items.map(item => getTypeExprFragment(item)); + return mergeFragments(items, parts => `[${parts.join(', ')}]`); + } + } +} + +function getStringExprFragment(expr: Extract): Fragment { + if (!expr.constraint) return fragment`string`; + if (expr.constraint === 'identifier') { + return use('type CamelCaseString', NODE_TYPES_PACKAGE); + } + if (expr.constraint === 'version') { + return use('type Version', NODE_TYPES_PACKAGE); + } + return fragment`string`; +} + +function literalToTs(value: boolean | number | string): string { + return typeof value === 'string' ? JSON.stringify(value) : String(value); +} + +function renderLiteralUnion(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/nodes/index.ts b/packages/spec-generators/src/nodes/index.ts new file mode 100644 index 000000000..5b572b9f0 --- /dev/null +++ b/packages/spec-generators/src/nodes/index.ts @@ -0,0 +1,89 @@ +import { + createRenderMap, + deleteDirectory, + type Fragment, + mergeRenderMaps, + pascalCase, + type Path, + type RenderMap, + writeRenderMap, +} from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { getIndexPagesRenderMap, getPageFragment, resolveEntryPath, type SymbolicModule } from '../shared'; +import { + getCodamaVersionConstantFragment, + getKindUnionConstantFragment, + getNodeKindUnionConstantFragment, + getNodePageFragment, +} from './fragments'; +import { + buildRenderScope, + type GenerateOptions, + type RenderOptions, + type RenderScope, + validateRenderOptions, +} from './options'; + +export { + type AttributeOverride, + CATEGORY_DIRECTORIES, + type GenerateOptions, + GENERIC_PARAM_ORDER, + NARROWABLE_DATA_ATTRIBUTES, + NODE_CONFIGS, + type NodeConstructorConfig, + type RenderOptions, + validateRenderOptions, +} from './options'; + +/** + * Build the render map and write it to disk under `options.outputDir`. + * The target directory is wiped before each run so stale files cannot + * survive. No formatter is applied — chain `lint:fix` afterwards. + */ +export function generateNodes(spec: Spec, options: GenerateOptions): void { + const renderMap = getRenderMap(spec, options); + deleteDirectory(options.outputDir); + writeRenderMap(renderMap, options.outputDir); +} + +/** Pure-and-sync render-map entry point. 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.symbolicModules); + return mergeRenderMaps([specPages, indexPages]); +} + +/** + * Walk every spec category plus the top-level `nodeKinds` registry and + * the `codamaVersion` constant. Returns 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.symbolicModules, symbolicKey); + entries[`${path}.ts`] = getPageFragment(body, scope.symbolicModules, path); + }; + + for (const category of spec.categories) { + for (const node of category.nodes) { + const interfaceName = pascalCase(node.kind); + emit( + `constructor:${node.kind}`, + getNodePageFragment(node, interfaceName, scope.nodeConfigs.get(node.kind), scope), + ); + } + for (const union of category.unions) { + emit(`kinds:${union.name}`, getKindUnionConstantFragment(union)); + } + } + + emit('kinds:Node', getNodeKindUnionConstantFragment(spec)); + emit('generated:CodamaVersion', getCodamaVersionConstantFragment(spec.version)); + + return createRenderMap(entries); +} diff --git a/packages/spec-generators/src/nodes/kindsArrayConstantName.ts b/packages/spec-generators/src/nodes/kindsArrayConstantName.ts new file mode 100644 index 000000000..667790f93 --- /dev/null +++ b/packages/spec-generators/src/nodes/kindsArrayConstantName.ts @@ -0,0 +1,6 @@ +import { snakeCase } from '@codama/fragments/javascript'; + +/** `StandaloneTypeNode` → `STANDALONE_TYPE_NODE_KINDS`. */ +export function kindsArrayConstantName(unionName: string): string { + return `${snakeCase(unionName).toUpperCase()}_KINDS`; +} diff --git a/packages/spec-generators/src/nodes/options.ts b/packages/spec-generators/src/nodes/options.ts new file mode 100644 index 000000000..613a47cae --- /dev/null +++ b/packages/spec-generators/src/nodes/options.ts @@ -0,0 +1,115 @@ +import { joinPath, pascalCase, type Path } from '@codama/fragments/javascript'; +import type { Spec } from '@codama/spec'; + +import { + resolveSharedRenderOptions, + type SharedRenderOptions, + type SymbolicModule, + type SymbolicModuleMap, + validateSharedRenderOptions, +} from '../shared'; +import { NODE_CONFIGS, type NodeConstructorConfig, validateNodeConfigs } from './config'; + +export { + CATEGORY_DIRECTORIES, + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + isNodeTypeParameterAttribute, + NARROWABLE_DATA_ATTRIBUTES, +} from '../shared'; +export { type AttributeOverride, NODE_CONFIGS, type NodeConstructorConfig } from './config'; + +/** User-facing options for the `@codama/nodes` generator. */ +export interface RenderOptions extends SharedRenderOptions { + /** + * Per-node constructor configuration: positional args, attribute + * overrides, defaults, coercions. Omitted means "use the v1 + * defaults" ({@link NODE_CONFIGS}). + */ + readonly nodeConfigs?: ReadonlyMap; +} + +/** Options consumed by {@link generateNodes}, the disk-writing entry point. */ +export interface GenerateOptions extends RenderOptions { + readonly outputDir: Path; +} + +/** {@link RenderOptions} with every defaultable field resolved. */ +export type ResolvedRenderOptions = Required; + +/** + * Runtime context threaded through every fragment renderer. Carries + * the resolved options plus the symbolic-module lookup table. + * + * Symbolic-module flavours emitted by this generator: + * + * - `constructor:` — a sibling node-constructor file. + * - `kinds:` — a generated runtime kinds array. + * - `kinds:Node` — the top-level `nodeKinds` registry. + * - `generated:CodamaVersion` — the generated `codamaVersion` constant. + * - `shared:` — a hand-written sibling utility above + * `generated/` (`camelCase`, `DocsInput`, + * `isNode`, `parseDocs`). + * + * Imports from `@codama/node-types` do NOT use this resolver: renderers + * call `use(, '@codama/node-types')` directly and the + * fragment pipeline passes the bare specifier through. + */ +export interface RenderScope extends ResolvedRenderOptions { + readonly symbolicModules: SymbolicModuleMap; +} + +export function resolveRenderOptions(options: RenderOptions): ResolvedRenderOptions { + return { + ...resolveSharedRenderOptions(options), + nodeConfigs: options.nodeConfigs ?? NODE_CONFIGS, + }; +} + +export function validateRenderOptions(spec: Spec, options: RenderOptions): void { + validateSharedRenderOptions(spec, options); + validateNodeConfigs(spec, options.nodeConfigs ?? NODE_CONFIGS); +} + +/** + * Hand-written sibling files at `@codama/nodes/src/`, one directory + * above `generated/`. Each entry's leading `../` makes the relative + * import resolve to `'../'` from a top-level generated file or + * `'../../'` from a subdirectory file. + */ +const SHARED_HELPER_PATHS: Readonly> = Object.freeze({ + DocsInput: '../shared', + camelCase: '../shared', + isNode: '../Node', + parseDocs: '../shared', +}); + +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(`unknown category "${category.name}". Extend categoryDirectories.`); + } + for (const node of category.nodes) { + symbolicModules.set(`constructor:${node.kind}`, joinPath(folder, pascalCase(node.kind))); + } + for (const union of category.unions) { + symbolicModules.set(`kinds:${union.name}`, joinPath(folder, pascalCase(union.name))); + } + } + + symbolicModules.set('kinds:Node', 'nodeKinds'); + symbolicModules.set('generated:CodamaVersion', 'codamaVersion'); + + for (const [name, path] of Object.entries(SHARED_HELPER_PATHS)) { + symbolicModules.set(`shared:${name}`, path); + } + + return Object.freeze({ + ...resolved, + symbolicModules: Object.freeze(symbolicModules), + }); +} diff --git a/packages/spec-generators/src/nodes/paramIdentifier.ts b/packages/spec-generators/src/nodes/paramIdentifier.ts new file mode 100644 index 000000000..867e2625f --- /dev/null +++ b/packages/spec-generators/src/nodes/paramIdentifier.ts @@ -0,0 +1,19 @@ +import type { AttributeSpec } from '@codama/spec'; + +import type { AttributeOverride } from './config'; + +/** + * The JS identifier used for an attribute when it appears as a + * positional parameter. Defaults to the spec attribute name; can be + * overridden via `paramName` when the spec name collides with a TS + * reserved word (`enum` → `enumLink`). + */ +export function paramIdentifier(attr: AttributeSpec, override: AttributeOverride | undefined): string { + if (override && 'paramName' in override && override.paramName) return override.paramName; + return attr.name; +} + +/** True when the attribute's spec type is `string({ constraint: 'identifier' })`. */ +export function isStringIdentifierAttr(attr: AttributeSpec): boolean { + return attr.type.kind === 'string' && attr.type.constraint === 'identifier'; +} diff --git a/packages/spec-generators/src/nodes/reservedParamNames.ts b/packages/spec-generators/src/nodes/reservedParamNames.ts new file mode 100644 index 000000000..f793705e5 --- /dev/null +++ b/packages/spec-generators/src/nodes/reservedParamNames.ts @@ -0,0 +1,50 @@ +/** Reserved words that TypeScript rejects as function-parameter identifiers. */ +export const TS_RESERVED_PARAM_NAMES: ReadonlySet = new Set([ + 'arguments', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'eval', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'interface', + 'let', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', +]); diff --git a/packages/spec-generators/src/shared/defaults.ts b/packages/spec-generators/src/shared/defaults.ts new file mode 100644 index 000000000..c260d32c4 --- /dev/null +++ b/packages/spec-generators/src/shared/defaults.ts @@ -0,0 +1,59 @@ +/** + * Per-package default options for the v1 spec, shared by the + * `nodeTypes` and `nodes` generators so the interface and constructor + * sides stay in lockstep. Future spec versions can ship their own + * defaults alongside these without breaking v1 callers. + */ + +/** + * Data attributes that surface as type parameters even though the + * spec classifies them as data. Each entry preserves a narrowing form + * (e.g. `NumberTypeNode<'u32'>`) that downstream code relies on. + */ +export const NARROWABLE_DATA_ATTRIBUTES: ReadonlySet = new Set([ + 'numberTypeNode:format', + 'stringTypeNode:encoding', +]); + +/** + * Per-node override of the type-parameter emission order. Each value + * must enumerate exactly the set of attributes that surface as type + * parameters for the node — no missing, no extras — otherwise both + * generators throw at startup rather than silently drop or reorder + * type parameters. + */ +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', + ], + ], +]); + +/** + * Mapping from spec category name to the output subdirectory each + * generator emits its entities into (relative to `generated/`). The + * empty string places `topLevel` entities at the root. + */ +export const CATEGORY_DIRECTORIES: ReadonlyMap = new Map([ + ['contextualValue', 'contextualValueNodes'], + ['count', 'countNodes'], + ['discriminator', 'discriminatorNodes'], + ['link', 'linkNodes'], + ['pdaSeed', 'pdaSeedNodes'], + ['shared', 'shared'], + ['topLevel', ''], + ['type', 'typeNodes'], + ['value', 'valueNodes'], +]); diff --git a/packages/spec-generators/src/shared/fragments/index.ts b/packages/spec-generators/src/shared/fragments/index.ts new file mode 100644 index 000000000..6e64868a8 --- /dev/null +++ b/packages/spec-generators/src/shared/fragments/index.ts @@ -0,0 +1,3 @@ +export * from './indexPage'; +export * from './page'; +export * from './typeParameterIdentifier'; diff --git a/packages/spec-generators/src/shared/fragments/indexPage.ts b/packages/spec-generators/src/shared/fragments/indexPage.ts new file mode 100644 index 000000000..cecc7b9c2 --- /dev/null +++ b/packages/spec-generators/src/shared/fragments/indexPage.ts @@ -0,0 +1,76 @@ +import { + createRenderMap, + type Fragment, + getExportAllFragment, + mergeFragments, + type Path, + pathBasename, + pathDirectory, + type RenderMap, +} from '@codama/fragments/javascript'; + +import type { SymbolicModuleMap } from '../symbolicModule'; +import { getPageFragment } from './page'; + +/** + * Build the per-folder and root `index.ts` re-export pages from a set + * of already-emitted spec pages. Each `index.ts` lists subdirectory + * re-exports plus any top-level files at the root level. + */ +export function getIndexPagesRenderMap( + specPages: RenderMap, + modules: SymbolicModuleMap, +): 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), modules, `${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, modules, 'index'); + + return createRenderMap(entries); +} + +/** + * Render an `index.ts` page body that alphabetically `export * from + * './';`s every supplied name. The caller wraps this in a + * page-level renderer to add imports / EOL handling. + */ +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'), + ); +} + +/** + * 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}. + */ +export 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/shared/fragments/page.ts b/packages/spec-generators/src/shared/fragments/page.ts new file mode 100644 index 000000000..d4d237628 --- /dev/null +++ b/packages/spec-generators/src/shared/fragments/page.ts @@ -0,0 +1,62 @@ +import { + type Fragment, + fragment, + type ImportInfo, + importMapToString, + mergeFragments, + mergeImportMaps, + type Module, + type Path, + pathDirectory, + relativePath, + type UsedIdentifier, +} from '@codama/fragments/javascript'; + +import { type SymbolicModule, type SymbolicModuleMap } from '../symbolicModule'; + +/** + * A `:` symbolic key. The flavour part is camelCase + * (`node`, `nestedUnion`, `constructor`, …). Anything that doesn't + * match (e.g. a bare `'@codama/node-types'` package specifier) is + * treated as already-resolved and passes through to the import block + * verbatim. + */ +const SYMBOLIC_KEY_PATTERN = /^[a-z][a-zA-Z]*:/; + +/** + * 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, passing bare specifiers + * through, throwing on unknown symbolic keys), then prepend the + * stringified import block to the body content. + */ +export function getPageFragment(body: Fragment, modules: SymbolicModuleMap, currentPath: Path): Fragment { + const resolved = resolveFragmentImports(body, modules, 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, modules: SymbolicModuleMap, currentPath: Path): Fragment { + if (body.imports.size === 0) return body; + const resolvedMaps = [...body.imports.entries()].flatMap(([module, identifiers]) => { + if (!SYMBOLIC_KEY_PATTERN.test(module)) { + return [new Map>([[module, identifiers]])]; + } + const target = modules.get(module as SymbolicModule); + if (target === undefined) { + throw new Error(`unknown symbolic module "${module}" referenced from "${currentPath}".`); + } + if (target === currentPath) return []; + const resolvedPath = relativeImportPath(currentPath, target); + return [new Map>([[resolvedPath, identifiers]])]; + }); + return Object.freeze({ ...body, imports: mergeImportMaps(resolvedMaps) }); +} + +function relativeImportPath(currentPath: Path, targetPath: Path): Path { + if (currentPath === targetPath) { + throw new Error(`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/shared/fragments/typeParameterIdentifier.ts b/packages/spec-generators/src/shared/fragments/typeParameterIdentifier.ts new file mode 100644 index 000000000..c9a362544 --- /dev/null +++ b/packages/spec-generators/src/shared/fragments/typeParameterIdentifier.ts @@ -0,0 +1,13 @@ +import { type Fragment, fragment, mergeFragments, pascalCase } from '@codama/fragments/javascript'; +import { AttributeSpec } from '@codama/spec'; + +/** `data` → `TData` as a Fragment. Use inside `fragment\`…\`` templates to avoid the wrapper. */ +export function getTypeParameterIdentifierFragment(attribute: AttributeSpec | string): Fragment { + const attributeName = typeof attribute === 'string' ? attribute : attribute.name; + const identifier = `T${pascalCase(attributeName)}`; + return fragment`${identifier}`; +} + +export function getTypeParameterIdentifierListFragment(attributes: readonly (AttributeSpec | string)[]): Fragment { + return mergeFragments(attributes.map(getTypeParameterIdentifierFragment), parts => parts.join(', ')); +} diff --git a/packages/spec-generators/src/shared/index.ts b/packages/spec-generators/src/shared/index.ts new file mode 100644 index 000000000..e12d978c1 --- /dev/null +++ b/packages/spec-generators/src/shared/index.ts @@ -0,0 +1,6 @@ +export * from './defaults'; +export * from './fragments'; +export * from './options'; +export * from './repoDirectory'; +export * from './symbolicModule'; +export * from './unions'; diff --git a/packages/spec-generators/src/shared/options.ts b/packages/spec-generators/src/shared/options.ts new file mode 100644 index 000000000..a81758a83 --- /dev/null +++ b/packages/spec-generators/src/shared/options.ts @@ -0,0 +1,150 @@ +import { type AttributeSpec, isChildAttribute, type NodeSpec, type Spec } from '@codama/spec'; + +import { CATEGORY_DIRECTORIES } from './defaults'; + +/** + * Render-option fields shared by both generators. Each generator + * declares its own `RenderOptions extends SharedRenderOptions` so it + * can add generator-specific knobs alongside the shared ones. + */ +export interface SharedRenderOptions { + /** + * 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 that surface + * as type parameters for the node — no more, no fewer — otherwise + * the run fails. + */ + readonly genericParamOrder?: ReadonlyMap; + /** + * `${nodeKind}:${attribute}` keys whose data attribute should + * surface as a type parameter even though the spec classifies it + * as data. Omitted means "only child attributes become type + * parameters". + */ + readonly narrowableDataAttributes?: ReadonlySet; + /** The spec major version this invocation targets. */ + readonly targetSpecMajor: number; +} + +/** {@link SharedRenderOptions} with every defaultable field resolved. */ +export type SharedResolvedRenderOptions = Required; + +export function resolveSharedRenderOptions(options: SharedRenderOptions): SharedResolvedRenderOptions { + return { + categoryDirectories: options.categoryDirectories ?? CATEGORY_DIRECTORIES, + genericParamOrder: options.genericParamOrder ?? new Map(), + narrowableDataAttributes: options.narrowableDataAttributes ?? new Set(), + targetSpecMajor: options.targetSpecMajor, + }; +} + +/** + * 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 validateSharedRenderOptions(spec: Spec, options: SharedRenderOptions): void { + const actualMajor = parseSpecMajor(spec.version); + if (actualMajor !== options.targetSpecMajor) { + throw new Error( + `targetSpecMajor=${options.targetSpecMajor} but the supplied spec is at version "${spec.version}" (major ${actualMajor}).`, + ); + } + + const allNodes = spec.categories.flatMap(c => c.nodes); + const validNodeKinds = new Set(allNodes.map(n => n.kind)); + const validKeys = new Set(allNodes.flatMap(n => n.attributes.map(a => `${n.kind}:${a.name}`))); + + if (options.categoryDirectories) { + const missing = spec.categories.find(c => !options.categoryDirectories!.has(c.name)); + if (missing) { + throw new Error(`categoryDirectories is missing an entry for spec category "${missing.name}".`); + } + } + + if (options.narrowableDataAttributes) { + for (const key of options.narrowableDataAttributes) { + if (!validKeys.has(key)) { + throw new Error( + `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(`genericParamOrder references unknown node kind "${kind}".`); + } + for (const attrName of order) { + if (!validKeys.has(`${kind}:${attrName}`)) { + throw new Error( + `genericParamOrder for "${kind}" references attribute "${attrName}" which the spec does not declare.`, + ); + } + } + } + } +} + +/** + * Decide whether an attribute surfaces as a type parameter on the + * generated node interface or node function. An attribute becomes a + * type parameter when its type tree contains a node / union / nested- + * union reference, or when its `${kind}:${name}` key appears in + * `narrowableDataAttributes`. + */ +export function isNodeTypeParameterAttribute( + nodeKind: string, + attr: AttributeSpec, + options: Pick, +): boolean { + return isChildAttribute(attr.type) || options.narrowableDataAttributes.has(`${nodeKind}:${attr.name}`); +} + +/** + * Return the spec attributes that surface as type parameters for + * `node`, in their emission order. Filters the node's attributes via + * {@link isNodeTypeParameterAttribute}, then applies the per-node + * `genericParamOrder` override when one is configured. + * + * The override must enumerate exactly the resulting type-parameter + * set — no missing, no extras — otherwise we throw rather than + * silently drop or reorder type parameters. + */ +export function getNodeTypeParameterAttributes( + node: NodeSpec, + options: Pick, +): readonly AttributeSpec[] { + const filtered = node.attributes.filter(a => isNodeTypeParameterAttribute(node.kind, a, options)); + const order = options.genericParamOrder.get(node.kind); + if (!order) return filtered; + + const byName = new Map(filtered.map(a => [a.name, a])); + const declared = new Set(byName.keys()); + const overrideSet = new Set(order); + const missing = [...declared].filter(n => !overrideSet.has(n)); + const unknown = order.filter(n => !declared.has(n)); + if (missing.length > 0 || unknown.length > 0) { + const parts: string[] = []; + if (missing.length > 0) parts.push(`missing type-parameter attribute(s) ${JSON.stringify(missing)}`); + if (unknown.length > 0) parts.push(`unknown attribute(s) ${JSON.stringify(unknown)}`); + throw new Error(`genericParamOrder for "${node.kind}" is out of sync with the spec: ${parts.join('; ')}.`); + } + return order.map(name => byName.get(name)!); +} + +function parseSpecMajor(version: string): number { + const m = /^(\d+)\./.exec(version); + if (!m) throw new Error(`unable to parse spec version "${version}".`); + return Number(m[1]); +} diff --git a/packages/spec-generators/src/shared.ts b/packages/spec-generators/src/shared/repoDirectory.ts similarity index 52% rename from packages/spec-generators/src/shared.ts rename to packages/spec-generators/src/shared/repoDirectory.ts index 133a98ee6..54114c713 100644 --- a/packages/spec-generators/src/shared.ts +++ b/packages/spec-generators/src/shared/repoDirectory.ts @@ -3,10 +3,9 @@ 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. + * Resolve the absolute path to the monorepo root. 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)); diff --git a/packages/spec-generators/src/shared/symbolicModule.ts b/packages/spec-generators/src/shared/symbolicModule.ts new file mode 100644 index 000000000..326ac69e0 --- /dev/null +++ b/packages/spec-generators/src/shared/symbolicModule.ts @@ -0,0 +1,39 @@ +import { type Path } from '@codama/fragments/javascript'; + +/** + * A `:` symbolic module string used as the second + * argument to `use(...)` inside renderers. Each generator owns its own + * flavour vocabulary (`node:`, `union:`, `constructor:`, …); the page + * renderer resolves these to relative paths via a {@link SymbolicModuleMap}. + */ +export type SymbolicModule = `${string}:${string}`; + +/** + * Resolved layout knowledge: maps every symbolic module key the + * generator emits or references to the in-tree file path that exports + * its identifier(s). Built once per generation run by each generator's + * `buildRenderScope` and consumed by the shared page / index-page / + * orchestration helpers. + * + * 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 type SymbolicModuleMap = ReadonlyMap; + +/** + * Resolve a symbolic key to a safe output path. Throws if the key is + * missing from the map or points outside `generated/`. Generators + * that drive an `emit(key, body)` style closure call this from inside + * their `getSpecPagesRenderMap`. + */ +export function resolveEntryPath(modules: SymbolicModuleMap, symbolicKey: SymbolicModule): Path { + const path = modules.get(symbolicKey); + if (path === undefined) { + throw new Error(`missing symbolic key "${symbolicKey}" in scope.`); + } + if (path.startsWith('../')) { + throw new Error(`refusing to emit "${symbolicKey}" — its location "${path}" points outside generated/.`); + } + return path; +} diff --git a/packages/spec-generators/src/shared/unions.ts b/packages/spec-generators/src/shared/unions.ts new file mode 100644 index 000000000..3d648fc44 --- /dev/null +++ b/packages/spec-generators/src/shared/unions.ts @@ -0,0 +1,50 @@ +import type { NodeSpec, Spec, UnionSpec } from '@codama/spec'; + +/** + * Any union whose name starts with `Registered` is a category-registry + * union (e.g. `RegisteredContextualValueNode`). Derived from the spec + * so future categories are picked up automatically. + */ +const REGISTERED_CATEGORY_UNION_PREFIX = 'Registered'; + +/** + * Return the spec's per-category registry unions (those whose names + * start with `Registered`), sorted alphabetically by name. + */ +export function getRegisteredCategoryUnions(spec: Spec): readonly UnionSpec[] { + return spec.categories + .flatMap(c => c.unions) + .filter(u => u.name.startsWith(REGISTERED_CATEGORY_UNION_PREFIX)) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Return the leaf nodes reachable from `union` by walking through + * nested `{ kind: 'union' }` members recursively. `nestedUnion` + * members are not followed — they are name-aliased and break the + * cycle on the TS side. Unknown member references are skipped + * silently; the spec validator catches those upstream. + */ +export function flattenNodeUnion(union: UnionSpec, spec: Spec): readonly NodeSpec[] { + const unionByName = new Map(spec.categories.flatMap(c => c.unions).map(u => [u.name, u])); + const nodeByKind = new Map(spec.categories.flatMap(c => c.nodes).map(n => [n.kind, n])); + const out: NodeSpec[] = []; + const visited = new Set(); + const stack: string[] = [union.name]; + while (stack.length > 0) { + const name = stack.pop()!; + if (visited.has(name)) continue; + visited.add(name); + const u = unionByName.get(name); + if (!u) continue; + for (const m of u.members) { + if (m.kind === 'node') { + const node = nodeByKind.get(m.name); + if (node) out.push(node); + } else if (m.kind === 'union') { + stack.push(m.name); + } + } + } + return out; +} diff --git a/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts b/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts index 8ddf2865b..af9855383 100644 --- a/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts +++ b/packages/spec-generators/test/nodeTypes/fragments/attributeBodyLine.test.ts @@ -2,11 +2,11 @@ import { attribute, boolean, enumeration, node, optionalAttribute, u32 } from '@ import { describe, expect, it } from 'vitest'; import { getAttributeBodyLineFragment } from '../../../src/nodeTypes/fragments/attributeBodyLine'; -import type { RenderScope } from '../../../src/nodeTypes/utils/scope'; +import type { RenderScope } from '../../../src/nodeTypes/options'; -type LiftScope = Pick; +type TypeParameterScope = Pick; -function buildScope(overrides: Partial = {}): LiftScope { +function buildScope(overrides: Partial = {}): TypeParameterScope { return { narrowableDataAttributes: new Set(), ...overrides }; } @@ -39,7 +39,7 @@ describe('getAttributeBodyLineFragment', () => { 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', () => { + it('uses the type-parameter identifier when the attribute is a child reference', () => { const result = getAttributeBodyLineFragment( 'someTypeNode', attribute('payload', node('innerTypeNode')), @@ -48,7 +48,7 @@ describe('getAttributeBodyLineFragment', () => { expect(result.content).toBe('readonly payload: TPayload;'); }); - it('uses the lifted generic identifier on optional child attributes too', () => { + it('uses the type-parameter identifier on optional child attributes too', () => { const result = getAttributeBodyLineFragment( 'someTypeNode', optionalAttribute('payload', node('innerTypeNode')), @@ -57,7 +57,7 @@ describe('getAttributeBodyLineFragment', () => { expect(result.content).toBe('readonly payload?: TPayload;'); }); - it('uses the lifted generic identifier for narrowable data attributes', () => { + it('uses the type-parameter identifier for narrowable data attributes', () => { const result = getAttributeBodyLineFragment( 'numberTypeNode', attribute('format', enumeration('NumberFormat')), @@ -66,7 +66,7 @@ describe('getAttributeBodyLineFragment', () => { expect(result.content).toBe('readonly format: TFormat;'); }); - it('does not lift a data attribute that is not in the narrowable set', () => { + it('does not surface a data attribute that is not in the narrowable set as a type parameter', () => { const result = getAttributeBodyLineFragment( 'numberTypeNode', attribute('format', enumeration('NumberFormat')), diff --git a/packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts b/packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts deleted file mode 100644 index 62b42aabe..000000000 --- a/packages/spec-generators/test/nodeTypes/fragments/indexPage.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -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/node.test.ts b/packages/spec-generators/test/nodeTypes/fragments/node.test.ts index 3730ac788..27b7281c2 100644 --- a/packages/spec-generators/test/nodeTypes/fragments/node.test.ts +++ b/packages/spec-generators/test/nodeTypes/fragments/node.test.ts @@ -12,7 +12,7 @@ import { import { describe, expect, it } from 'vitest'; import { getNodeFragment } from '../../../src/nodeTypes/fragments/node'; -import type { RenderScope } from '../../../src/nodeTypes/utils/scope'; +import type { RenderScope } from '../../../src/nodeTypes/options'; type NodeScope = Pick; @@ -62,7 +62,7 @@ describe('getNodeFragment', () => { expect(c).toContain('readonly count?: number;'); }); - it('lifts every child attribute to a generic param', () => { + it('surfaces every child attribute as a type parameter', () => { const result = getNodeFragment(buildWrappingNode(), buildScope()); expect(result.content).toContain('TPayload extends TypeNode = TypeNode'); }); @@ -113,11 +113,11 @@ describe('getNodeFragment', () => { 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). + // Child attribute (surfaces as a type parameter). expect(result.content).toContain('/** A wrapped payload. */\nreadonly payload: TPayload;'); }); - it('emits lifted generics in declaration order when no override is supplied', () => { + it('emits type parameters 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'); @@ -127,7 +127,7 @@ describe('getNodeFragment', () => { expect(tTypeIdx).toBeLessThan(tDefaultIdx); }); - it('reorders lifted generics according to genericParamOrder', () => { + it('reorders type parameters according to genericParamOrder', () => { const result = getNodeFragment( buildArgumentNode(), buildScope({ genericParamOrder: new Map([['instructionArgumentNode', ['defaultValue', 'type']]]) }), @@ -141,11 +141,11 @@ describe('getNodeFragment', () => { expect(tDefaultIdx).toBeLessThan(tTypeIdx); }); - it('throws when genericParamOrder lists an attribute the node does not lift', () => { + it('throws when genericParamOrder lists an attribute the node does not surface as a type parameter', () => { // 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 }`. + // type parameters. We omit `type` from the spec entirely, so + // the renderer's type-parameter set is `{ defaultValue }` while + // the override expects `{ defaultValue, type }`. const incomplete = defineNode('instructionArgumentNode', { attributes: [optionalAttribute('defaultValue', union('InstructionInputValueNode'))], }); diff --git a/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts b/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts index 2deb823fb..a2e55bffb 100644 --- a/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts +++ b/packages/spec-generators/test/nodeTypes/fragments/nodeRegistry.test.ts @@ -4,10 +4,11 @@ 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. +// The renderer derives the registered-union list from any `Registered*` +// union in the spec. 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', @@ -122,9 +123,10 @@ describe('getNodeRegistryFragment', () => { 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. + it('only emits the RegisteredXxxNode unions present in the spec', () => { + // The registry is derived from `spec` — unions whose names start + // with `Registered`. If the spec ships only some of them, the + // renderer emits exactly those and quietly omits the rest. const spec: Spec = { categories: [ defineCategory('topLevel', { @@ -134,7 +136,10 @@ describe('getNodeRegistryFragment', () => { ], version: '1.0.0', }; - expect(() => getNodeRegistryFragment(spec)).toThrow(/missing union "RegisteredCountNode"/); + const result = getNodeRegistryFragment(spec); + const imports = [...result.imports.keys()]; + expect(imports).toContain('union:RegisteredContextualValueNode'); + expect(imports).not.toContain('union:RegisteredCountNode'); }); it('sorts the Node members alphabetically for stable output', () => { diff --git a/packages/spec-generators/test/nodeTypes/options.test.ts b/packages/spec-generators/test/nodeTypes/options.test.ts deleted file mode 100644 index e62e96c20..000000000 --- a/packages/spec-generators/test/nodeTypes/options.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -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/scope.test.ts similarity index 83% rename from packages/spec-generators/test/nodeTypes/utils/scope.test.ts rename to packages/spec-generators/test/nodeTypes/scope.test.ts index 48ca636c1..1e92a7f9f 100644 --- a/packages/spec-generators/test/nodeTypes/utils/scope.test.ts +++ b/packages/spec-generators/test/nodeTypes/scope.test.ts @@ -10,8 +10,7 @@ import { } 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'; +import { buildRenderScope, type RenderOptions } from '../../src/nodeTypes/options'; function buildSpec(categories: Spec['categories']): Spec { return { categories, version: '1.0.0' }; @@ -197,35 +196,3 @@ describe('buildRenderScope', () => { 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/selfReference.test.ts similarity index 95% rename from packages/spec-generators/test/nodeTypes/utils/selfReference.test.ts rename to packages/spec-generators/test/nodeTypes/selfReference.test.ts index 6c75dd0c4..dde4cdcc6 100644 --- a/packages/spec-generators/test/nodeTypes/utils/selfReference.test.ts +++ b/packages/spec-generators/test/nodeTypes/selfReference.test.ts @@ -1,7 +1,7 @@ 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'; +import { isTypeExprSelfReferential } from '../../src/nodeTypes/selfReference'; describe('isTypeExprSelfReferential', () => { it('returns true for a direct node reference matching the kind', () => { diff --git a/packages/spec-generators/test/nodes/fragments/inputType.test.ts b/packages/spec-generators/test/nodes/fragments/inputType.test.ts new file mode 100644 index 000000000..c0368e086 --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/inputType.test.ts @@ -0,0 +1,118 @@ +import { fragment } from '@codama/fragments/javascript'; +import { array, attribute, defineNode, docs, optionalAttribute, stringIdentifier, u32, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import type { NodeConstructorConfig } from '../../../src/nodes/config'; +import { getInputTypeFragment } from '../../../src/nodes/fragments/inputType'; +import { + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + NARROWABLE_DATA_ATTRIBUTES, + type ResolvedRenderOptions, +} from '../../../src/nodes/options'; + +const scope: Pick = { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, +}; + +describe('getInputTypeFragment', () => { + it('emits a `XxxNodeInput` alias with name and docs reasserted to relaxed shapes', () => { + const n = defineNode('myNode', { + attributes: [ + attribute('name', stringIdentifier()), + attribute('docs', docs()), + attribute('data', union('TypeNode')), + ], + }); + const result = getInputTypeFragment(n, 'MyNode', undefined, getNodeTypeParameterAttributes(n, scope)); + expect(result.content).toContain('export type MyNodeInput<'); + // The reasserted fields appear in the intersection, name as `string` and docs as `DocsInput`. + expect(result.content).toContain('readonly name: string;'); + expect(result.content).toContain('readonly docs?: DocsInput;'); + // Reasserted fields are stripped from the base interface via Omit<>. + expect(result.content).toContain("Omit, 'docs' | 'kind' | 'name'>"); + }); + + it('wraps the base in Partial<> when the config defaults a spec-required attribute', () => { + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), attribute('items', array(union('ValueNode')))], + }); + const config: NodeConstructorConfig = { + attributes: { items: { default: fragment`[]` } }, + }; + const result = getInputTypeFragment(n, 'MyNode', config, getNodeTypeParameterAttributes(n, scope)); + expect(result.content).toContain('Omit>'); + }); + + it('does not wrap in Partial<> when no required attribute carries a default override', () => { + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), attribute('data', union('TypeNode'))], + }); + const result = getInputTypeFragment(n, 'MyNode', undefined, getNodeTypeParameterAttributes(n, scope)); + expect(result.content).not.toContain('Omit'); + }); + + it('does not wrap in Partial<> when the only default override targets an already-optional attribute', () => { + // The Partial<> wrap is to let callers omit *required* fields the + // constructor defaults. Optional fields are already omittable, so + // defaulting an optional attribute doesn't need Partial<>. + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), optionalAttribute('count', u32())], + }); + const config: NodeConstructorConfig = { + attributes: { count: { default: fragment`0` } }, + }; + const result = getInputTypeFragment(n, 'MyNode', config, getNodeTypeParameterAttributes(n, scope)); + expect(result.content).not.toContain('Omit clause", () => { + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), attribute('docs', docs())], + }); + const result = getInputTypeFragment(n, 'MyNode', undefined, getNodeTypeParameterAttributes(n, scope)); + // The Omit clause lists `kind`, `name`, and `docs` (sorted). + expect(result.content).toContain("Omit"); + }); + + it('reasserts programNode.publicKey as the original wide shape (a non-relaxed required field)', () => { + // `programNode` is special-cased in `collectReassertedFields`. + // Mirror enough of the real spec shape that genericParamOrder's + // `[pdas, accounts, instructions, definedTypes, errors, events, + // constants]` order can be honoured. + const n = defineNode('programNode', { + attributes: [ + attribute('name', stringIdentifier()), + attribute('publicKey', stringIdentifier()), + attribute('pdas', array(union('PdaNode'))), + attribute('accounts', array(union('AccountNode'))), + attribute('instructions', array(union('InstructionNode'))), + attribute('definedTypes', array(union('DefinedTypeNode'))), + attribute('errors', array(union('ErrorNode'))), + attribute('events', array(union('EventNode'))), + attribute('constants', array(union('ConstantNode'))), + ], + }); + const result = getInputTypeFragment(n, 'ProgramNode', undefined, getNodeTypeParameterAttributes(n, scope)); + expect(result.content).toContain("readonly publicKey: ProgramNode['publicKey'];"); + // `publicKey` is also stripped from the base via Omit so the + // intersection's reassertion takes precedence. + expect(result.content).toContain("'kind' | 'name' | 'publicKey'"); + }); + + it('emits a parameterless Input type for a node with no type-parameter attributes', () => { + const n = defineNode('myNode', { attributes: [attribute('name', stringIdentifier())] }); + const result = getInputTypeFragment(n, 'MyNode', undefined, getNodeTypeParameterAttributes(n, scope)); + // No generics block. + expect(result.content).toContain('export type MyNodeInput = '); + }); + + it('emits a no-intersection Input type for a node with no name/docs/publicKey reassertions', () => { + const n = defineNode('myNode', { attributes: [attribute('data', union('TypeNode'))] }); + const result = getInputTypeFragment(n, 'MyNode', undefined, getNodeTypeParameterAttributes(n, scope)); + // No trailing `& { … }`. + expect(result.content).not.toContain('& {'); + }); +}); diff --git a/packages/spec-generators/test/nodes/fragments/nodeFunctionAttribute.test.ts b/packages/spec-generators/test/nodes/fragments/nodeFunctionAttribute.test.ts new file mode 100644 index 000000000..1f9c1739a --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/nodeFunctionAttribute.test.ts @@ -0,0 +1,115 @@ +import { fragment } from '@codama/fragments/javascript'; +import type { AttributeSpec } from '@codama/spec'; +import { array, attribute, defineNode, node, optionalAttribute, stringIdentifier, u32, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import type { AttributeOverride } from '../../../src/nodes/config'; +import { getNodeFunctionAttributeFragment } from '../../../src/nodes/fragments/nodeFunctionAttribute'; +import { + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + NARROWABLE_DATA_ATTRIBUTES, + type ResolvedRenderOptions, +} from '../../../src/nodes/options'; + +const scope: Pick = { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, +}; + +/** + * Helper: build an `AttributeSpec` in isolation by wrapping it in a + * throwaway node and pulling the first attribute off. + */ +function specAttr(attr: ReturnType) { + return defineNode('dummy', { attributes: [attr] }).attributes[0]; +} + +/** + * Helper: derive the type-parameter `AttributeSpec` for a synthetic + * node. Returns `undefined` if the attribute does not surface as a + * type parameter. + */ +function typeParamAttrFor(attr: ReturnType): AttributeSpec | undefined { + const n = defineNode('dummy', { attributes: [attr] }); + return getNodeTypeParameterAttributes(n, scope)[0]; +} + +describe('getNodeFunctionAttributeFragment', () => { + it('rule 1: hidden-defaulted attribute emits the default expression directly', () => { + const attr = specAttr(attribute('standard', u32())); + const override: AttributeOverride = { default: fragment`'codama'`, hidden: true }; + const out = getNodeFunctionAttributeFragment(attr, 'options.standard', override, undefined, false).content; + expect(out).toBe("standard: 'codama',"); + }); + + it('rule 2: value override emits expression verbatim', () => { + const attr = specAttr(attribute('withHeader', u32())); + const override: AttributeOverride = { value: fragment`options.withHeader ?? someFn(value)` }; + const out = getNodeFunctionAttributeFragment(attr, 'options.withHeader', override, undefined, false).content; + expect(out).toBe('withHeader: options.withHeader ?? someFn(value),'); + }); + + it('rule 3: stringIdentifier wraps the reader in `camelCase(...)`', () => { + const attr = specAttr(attribute('name', stringIdentifier())); + const out = getNodeFunctionAttributeFragment(attr, 'input.name', undefined, undefined, false).content; + expect(out).toBe('name: camelCase(input.name),'); + }); + + it('rule 3 (optional path): optional stringIdentifier wraps in a conditional spread', () => { + const attr = specAttr(optionalAttribute('identifier', stringIdentifier())); + const out = getNodeFunctionAttributeFragment(attr, 'input.identifier', undefined, undefined, false).content; + expect(out).toBe('...(input.identifier !== undefined && { identifier: camelCase(input.identifier) }),'); + }); + + it('uses the spec attribute name as the body key regardless of `paramName`', () => { + // The `enum` → `enumLink` rename only applies to the JS + // parameter identifier; the body's object-literal key must + // stay `enum` so the encoded shape matches the interface. + const attr = specAttr(attribute('enum', node('definedTypeLinkNode'))); + const override: AttributeOverride = { coerce: fragment`enumLink`, paramName: 'enumLink' }; + const typeParamAttr = typeParamAttrFor(attribute('enum', node('definedTypeLinkNode'))); + const out = getNodeFunctionAttributeFragment(attr, 'enumLink', override, typeParamAttr, true).content; + expect(out.startsWith('enum:')).toBe(true); + }); + + it('rule 4: coerce override emits the fragment verbatim with an `as TGeneric` cast for type-parameter attrs', () => { + const attr = specAttr(attribute('program', node('programLinkNode'))); + const override: AttributeOverride = { + coerce: fragment`typeof program === 'string' ? programLinkNode(program) : program`, + }; + const typeParamAttr = typeParamAttrFor(attribute('program', node('programLinkNode'))); + const out = getNodeFunctionAttributeFragment(attr, 'program', override, typeParamAttr, true).content; + expect(out).toContain("typeof program === 'string' ? programLinkNode(program) : program"); + }); + + it('rule 4 (optional path): optional coerce drops behind a conditional spread', () => { + const attr = specAttr(optionalAttribute('program', node('programLinkNode'))); + const override: AttributeOverride = { + coerce: fragment`typeof program === 'string' ? programLinkNode(program) : program`, + }; + const typeParamAttr = typeParamAttrFor(optionalAttribute('program', node('programLinkNode'))); + const out = getNodeFunctionAttributeFragment(attr, 'program', override, typeParamAttr, true).content; + expect(out).toContain('...(program !== undefined && {'); + }); + + it('rule 5: default override on a non-bare type-parameter attribute emits a `?? ` fallback', () => { + const attr = specAttr(attribute('items', array(union('ValueNode')))); + const override: AttributeOverride = { default: fragment`[]` }; + const typeParamAttr = typeParamAttrFor(attribute('items', array(union('ValueNode')))); + const out = getNodeFunctionAttributeFragment(attr, 'input.items', override, typeParamAttr, false).content; + expect(out).toBe('items: (input.items ?? []) as TItems,'); + }); + + it('rule 6: optional attribute without overrides becomes a conditional spread', () => { + const attr = specAttr(optionalAttribute('foo', u32())); + const out = getNodeFunctionAttributeFragment(attr, 'input.foo', undefined, undefined, false).content; + expect(out).toBe('...(input.foo !== undefined && { foo: input.foo }),'); + }); + + it('rule 7: required attribute with reader === key collapses to the shorthand `key,`', () => { + const attr = specAttr(attribute('format', u32())); + const out = getNodeFunctionAttributeFragment(attr, 'format', undefined, undefined, true).content; + expect(out).toBe('format,'); + }); +}); diff --git a/packages/spec-generators/test/nodes/fragments/nodeFunctionBody.test.ts b/packages/spec-generators/test/nodes/fragments/nodeFunctionBody.test.ts new file mode 100644 index 000000000..910dfbee8 --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/nodeFunctionBody.test.ts @@ -0,0 +1,28 @@ +import { attribute, defineNode, docs, stringIdentifier } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getNodeFunctionBodyFragment } from '../../../src/nodes/fragments/nodeFunctionBody'; +import { + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + NARROWABLE_DATA_ATTRIBUTES, + type ResolvedRenderOptions, +} from '../../../src/nodes/options'; + +const scope: Pick = { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, +}; + +describe('getNodeFunctionBodyFragment', () => { + it('drops empty docs via the conditional spread + parsedDocs pattern', () => { + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), attribute('docs', docs())], + }); + const out = getNodeFunctionBodyFragment(n, undefined, getNodeTypeParameterAttributes(n, scope)).content; + // The `docs` handling hoists `parseDocs()` to a local + // and conditionally spreads it into the encoded shape. + expect(out).toContain('const parsedDocs = parseDocs(input.docs);'); + expect(out).toContain('...(parsedDocs.length > 0 && { docs: parsedDocs }),'); + }); +}); diff --git a/packages/spec-generators/test/nodes/fragments/nodeFunctionPositionalArguments.test.ts b/packages/spec-generators/test/nodes/fragments/nodeFunctionPositionalArguments.test.ts new file mode 100644 index 000000000..9705bfd71 --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/nodeFunctionPositionalArguments.test.ts @@ -0,0 +1,91 @@ +import { fragment } from '@codama/fragments/javascript'; +import { attribute, defineNode, enumeration, node, optionalAttribute, u32, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import type { NodeConstructorConfig } from '../../../src/nodes/config'; +import { getNodeFunctionPositionalArgumentsFragment } from '../../../src/nodes/fragments/nodeFunctionPositionalArguments'; +import { + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + NARROWABLE_DATA_ATTRIBUTES, + type ResolvedRenderOptions, +} from '../../../src/nodes/options'; + +const scope: Pick = { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, +}; + +describe('getNodeFunctionPositionalArgumentsFragment', () => { + it('emits bare positional parameters in the declared order', () => { + const n = defineNode('myNode', { + attributes: [ + attribute('format', enumeration('NumberFormat')), + attribute('endian', enumeration('Endianness')), + ], + }); + const config: NodeConstructorConfig = { positionalArgs: ['format', 'endian'] }; + const out = getNodeFunctionPositionalArgumentsFragment( + n, + getNodeTypeParameterAttributes(n, scope), + config, + ).content; + expect(out).toMatch(/format: \w+/); + expect(out).toMatch(/endian: \w+/); + }); + + it("renames the spec attribute `enum` to `enumLink` via the override's `paramName`", () => { + const n = defineNode('myNode', { + attributes: [attribute('enum', node('definedTypeLinkNode'))], + }); + const config: NodeConstructorConfig = { + attributes: { + enum: { coerce: fragment`enumLink`, paramName: 'enumLink' }, + }, + positionalArgs: ['enum'], + }; + const out = getNodeFunctionPositionalArgumentsFragment( + n, + getNodeTypeParameterAttributes(n, scope), + config, + ).content; + expect(out).toMatch(/\benumLink:\s/); + }); + + it('puts non-positional attributes in a trailing `options` bag', () => { + const n = defineNode('myNode', { + attributes: [ + attribute('value', union('ValueNode')), + optionalAttribute('subtract', enumeration('Endianness')), + ], + }); + const config: NodeConstructorConfig = { positionalArgs: ['value'] }; + const out = getNodeFunctionPositionalArgumentsFragment( + n, + getNodeTypeParameterAttributes(n, scope), + config, + ).content; + expect(out).toContain('options: {'); + expect(out).toContain('subtract?:'); + expect(out).toContain('= {}'); + }); + + it('excludes `hidden`-defaulted attributes from both signature and bag', () => { + const n = defineNode('myNode', { + attributes: [attribute('program', union('ProgramNode')), attribute('standard', u32())], + }); + const config: NodeConstructorConfig = { + attributes: { standard: { default: fragment`'codama'`, hidden: true } }, + positionalArgs: ['program'], + }; + const out = getNodeFunctionPositionalArgumentsFragment( + n, + getNodeTypeParameterAttributes(n, scope), + config, + ).content; + // No `options` bag — `standard` is the only non-positional and it's hidden. + expect(out).not.toContain('options:'); + // And `standard` doesn't appear as a positional either. + expect(out).not.toContain('standard'); + }); +}); diff --git a/packages/spec-generators/test/nodes/fragments/nodePage.test.ts b/packages/spec-generators/test/nodes/fragments/nodePage.test.ts new file mode 100644 index 000000000..2fa5143a0 --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/nodePage.test.ts @@ -0,0 +1,82 @@ +import { fragment, use } from '@codama/fragments/javascript'; +import { array, attribute, defineNode, docs, stringIdentifier, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import type { NodeConstructorConfig } from '../../../src/nodes/config'; +import { getNodePageFragment } from '../../../src/nodes/fragments/nodePage'; +import { + GENERIC_PARAM_ORDER, + NARROWABLE_DATA_ATTRIBUTES, + type ResolvedRenderOptions, +} from '../../../src/nodes/options'; + +const scope: Pick = { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, +}; + +describe('getNodePageFragment', () => { + it('emits an `export function ` declaration', () => { + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), attribute('data', union('TypeNode'))], + }); + const out = getNodePageFragment(n, 'MyNode', undefined, scope).content; + expect(out).toContain('export function myNode<'); + }); + + it('uses positional shape when positionalArgs is supplied — no Input type emitted', () => { + const n = defineNode('myNode', { + attributes: [attribute('format', union('TypeNode')), attribute('endian', union('TypeNode'))], + }); + const config: NodeConstructorConfig = { positionalArgs: ['format', 'endian'] }; + const out = getNodePageFragment(n, 'MyNode', config, scope).content; + expect(out).not.toContain('MyNodeInput'); + }); + + it('imports `camelCase` from shared when the body uses it', () => { + const n = defineNode('myNode', { attributes: [attribute('name', stringIdentifier())] }); + const out = getNodePageFragment(n, 'MyNode', undefined, scope); + expect([...out.imports.keys()]).toContain('shared:camelCase'); + }); + + it('imports `parseDocs` from shared when the node has a `docs` attribute', () => { + const n = defineNode('myNode', { + attributes: [attribute('name', stringIdentifier()), attribute('docs', docs())], + }); + const out = getNodePageFragment(n, 'MyNode', undefined, scope); + expect([...out.imports.keys()]).toContain('shared:parseDocs'); + }); + + it('pulls in imports declared by an override Fragment via use(...)', () => { + // The override declares its own import via `use(...)`. The + // fragment pipeline composes that import into the rendered + // file's import map — no scanner involved. + const n = defineNode('myNode', { + attributes: [attribute('data', union('TypeNode'))], + }); + const config: NodeConstructorConfig = { + attributes: { + data: { default: fragment`${use('structTypeNode', 'constructor:structTypeNode')}([])` }, + }, + }; + const out = getNodePageFragment(n, 'MyNode', config, scope); + expect([...out.imports.keys()]).toContain('constructor:structTypeNode'); + }); + + it('does not pull in any spurious import when the override is a plain literal', () => { + // A `default: fragment\`[]\`` override has no `use(...)` call, + // so no extra import is emitted. + const n = defineNode('myNode', { + attributes: [attribute('items', array(union('ValueNode')))], + }); + const config: NodeConstructorConfig = { + attributes: { items: { default: fragment`[]` } }, + }; + const out = getNodePageFragment(n, 'MyNode', config, scope); + // The base spec walk imports `ValueNode` from @codama/node-types + // (the generic constraint refers to it), but no constructor-style + // import comes from the override. + const constructorImports = [...out.imports.keys()].filter(k => k.startsWith('constructor:')); + expect(constructorImports).toEqual([]); + }); +}); diff --git a/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts b/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts new file mode 100644 index 000000000..f9cddcc99 --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/nodeTypeParameters.test.ts @@ -0,0 +1,190 @@ +import { fragment } from '@codama/fragments/javascript'; +import { array, attribute, defineNode, enumeration, node, optionalAttribute, u32, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import type { NodeConstructorConfig } from '../../../src/nodes/config'; +import { computeConstructorDefaults } from '../../../src/nodes/fragments/nodeTypeParameters'; +import { + GENERIC_PARAM_ORDER, + getNodeTypeParameterAttributes, + NARROWABLE_DATA_ATTRIBUTES, + type ResolvedRenderOptions, +} from '../../../src/nodes/options'; + +const scope: Pick = { + genericParamOrder: GENERIC_PARAM_ORDER, + narrowableDataAttributes: NARROWABLE_DATA_ATTRIBUTES, +}; + +describe('getNodeTypeParameterAttributes', () => { + it('selects child attributes by default', () => { + const n = defineNode('myNode', { + attributes: [ + attribute('plainData', u32()), + attribute('childUnion', union('TypeNode')), + attribute('childNode', node('innerNode')), + ], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + expect(attrs.map(a => a.name)).toEqual(['childUnion', 'childNode']); + }); + + it('also selects narrowable-data attributes listed in NARROWABLE_DATA_ATTRIBUTES', () => { + // `numberTypeNode:format` is on the shared narrowable list. + const n = defineNode('numberTypeNode', { + attributes: [ + attribute('format', enumeration('NumberFormat')), + attribute('endian', enumeration('Endianness')), + ], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + // `format` surfaces as a type parameter because it's narrowable; + // `endian` is plain data and does not. + expect(attrs.map(a => a.name)).toEqual(['format']); + }); + + it('uses genericParamOrder overrides to reorder type-parameter attributes', () => { + // `instructionArgumentNode` surfaces both `type` (child) and + // `defaultValue` (child) as type parameters; the + // GENERIC_PARAM_ORDER table says `defaultValue` must come first. + const n = defineNode('instructionArgumentNode', { + attributes: [ + attribute('type', union('TypeNode')), + optionalAttribute('defaultValue', union('InstructionInputValueNode')), + ], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + expect(attrs.map(a => a.name)).toEqual(['defaultValue', 'type']); + }); + + it('throws when GENERIC_PARAM_ORDER references attributes the spec does not surface', () => { + // `programNode`'s order lists `pdas` first. If we construct a + // synthetic `programNode` without it, the override is out of + // sync with the spec. + const n = defineNode('programNode', { attributes: [attribute('publicKey', u32())] }); + expect(() => getNodeTypeParameterAttributes(n, scope)).toThrow( + /genericParamOrder for "programNode" is out of sync.*unknown attribute/, + ); + }); + + it('throws when the spec surfaces a type-parameter attribute that GENERIC_PARAM_ORDER does not list', () => { + // `instructionArgumentNode`'s order is `[defaultValue, type]`. + // Synthesise a third type-parameter attribute the override + // doesn't know about; the strict check should reject it. + const n = defineNode('instructionArgumentNode', { + attributes: [ + attribute('type', union('TypeNode')), + optionalAttribute('defaultValue', union('InstructionInputValueNode')), + attribute('extra', union('TypeNode')), + ], + }); + expect(() => getNodeTypeParameterAttributes(n, scope)).toThrow( + /genericParamOrder for "instructionArgumentNode" is out of sync.*missing type-parameter attribute/, + ); + }); +}); + +describe('computeConstructorDefaults', () => { + it('defaults every type parameter to `undefined` when the spec marks it optional', () => { + const n = defineNode('myNode', { + attributes: [optionalAttribute('child', union('TypeNode'))], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const [d] = computeConstructorDefaults(n, attrs, undefined, scope); + expect(d.content).toBe('undefined'); + }); + + it('produces no constructor default for a required child attribute with no override', () => { + const n = defineNode('myNode', { + attributes: [attribute('child', union('TypeNode'))], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const [d] = computeConstructorDefaults(n, attrs, undefined, scope); + expect(d.content).toBe(''); + }); + + it('falls back to the wide constraint as default for a narrowable-data attribute with no override', () => { + // `numberTypeNode:format` is narrowable; with no override, the + // constructor default is the wide constraint (`NumberFormat`). + const n = defineNode('numberTypeNode', { + attributes: [attribute('format', enumeration('NumberFormat'))], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const [d] = computeConstructorDefaults(n, attrs, undefined, scope); + expect(d.content).toBe('NumberFormat'); + }); + + it('uses an override-supplied default verbatim for primitive defaults', () => { + const config: NodeConstructorConfig = { + attributes: { items: { default: fragment`[]` } }, + }; + const n = defineNode('myNode', { + attributes: [attribute('items', array(union('ValueNode')))], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const [d] = computeConstructorDefaults(n, attrs, config, scope); + expect(d.content).toBe('[]'); + }); + + it('uses `genericDefault` over `default` when supplied', () => { + const config: NodeConstructorConfig = { + attributes: { + size: { + default: fragment`numberTypeNode('u8')`, + genericDefault: fragment`NumberTypeNode<'u8'>`, + }, + }, + }; + const n = defineNode('myNode', { + attributes: [attribute('size', union('TypeNode'))], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const [d] = computeConstructorDefaults(n, attrs, config, scope); + expect(d.content).toBe("NumberTypeNode<'u8'>"); + }); + + it('falls back to the wide constraint for a required attribute with a `coerce` override', () => { + const config: NodeConstructorConfig = { + attributes: { + program: { coerce: fragment`typeof program === 'string' ? programLinkNode(program) : program` }, + }, + }; + const n = defineNode('myNode', { + attributes: [attribute('program', node('programLinkNode'))], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const [d] = computeConstructorDefaults(n, attrs, config, scope); + expect(d.content).toBe('ProgramLinkNode'); + }); + + it('broadens a trailing required type parameter that follows a defaulted one', () => { + // `pdaValueNode`'s GENERIC_PARAM_ORDER is `[seeds, programId, pda]` + // — `seeds` is defaulted via the override `[]`, `programId` is + // optional (`undefined`), and `pda` is required. TS would + // disallow a required type parameter after a defaulted one, so + // the renderer broadens `pda`'s default to its wide constraint. + const config: NodeConstructorConfig = { + attributes: { + seeds: { default: fragment`[]` }, + }, + }; + const n = defineNode('pdaValueNode', { + attributes: [ + attribute('seeds', array(union('PdaSeedValueNode'))), + optionalAttribute('programId', union('PdaValueProgramId')), + attribute('pda', union('PdaValuePda')), + ], + }); + const attrs = getNodeTypeParameterAttributes(n, scope); + const defaults = computeConstructorDefaults(n, attrs, config, scope); + const pdaIdx = attrs.findIndex(a => a.name === 'pda'); + // Broadened to the wide constraint rather than left empty. + expect(defaults[pdaIdx].content).toBe('PdaValuePda'); + }); + + it('returns an empty list when the node has no type-parameter attributes', () => { + const n = defineNode('myNode', { attributes: [attribute('plain', u32())] }); + const attrs = getNodeTypeParameterAttributes(n, scope); + expect(computeConstructorDefaults(n, attrs, undefined, scope)).toEqual([]); + }); +}); diff --git a/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts b/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts new file mode 100644 index 000000000..8166bbc00 --- /dev/null +++ b/packages/spec-generators/test/nodes/fragments/typeExpr.test.ts @@ -0,0 +1,142 @@ +import { + array, + boolean, + codamaVersion, + docs, + enumeration, + literal, + literalUnion, + nestedUnion, + node, + string, + stringIdentifier, + stringVersion, + tuple, + u32, + union, +} from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { getTypeExprFragment } from '../../../src/nodes/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 a string literal as a JSON-quoted source-form literal', () => { + expect(getTypeExprFragment(literal('codama')).content).toBe('"codama"'); + }); + + it('renders a 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('routes stringIdentifier to the CamelCaseString brand imported from @codama/node-types', () => { + const result = getTypeExprFragment(stringIdentifier()); + expect(result.content).toBe('CamelCaseString'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + it('routes stringVersion to the Version alias imported from @codama/node-types', () => { + const result = getTypeExprFragment(stringVersion()); + expect(result.content).toBe('Version'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + it('routes codamaVersion to the CodamaVersion alias imported from @codama/node-types', () => { + const result = getTypeExprFragment(codamaVersion()); + expect(result.content).toBe('CodamaVersion'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + it('routes docs to the Docs alias imported from @codama/node-types', () => { + const result = getTypeExprFragment(docs()); + expect(result.content).toBe('Docs'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + it('routes enumeration references to @codama/node-types', () => { + const result = getTypeExprFragment(enumeration('Endianness')); + expect(result.content).toBe('Endianness'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + it('routes node references to @codama/node-types under their PascalCase name', () => { + const result = getTypeExprFragment(node('innerTypeNode')); + expect(result.content).toBe('InnerTypeNode'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + it('routes union references to @codama/node-types under their PascalCase name', () => { + const result = getTypeExprFragment(union('TypeNode')); + expect(result.content).toBe('TypeNode'); + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + }); + + 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 wrapper and inner imports merged into one @codama/node-types entry', () => { + const result = getTypeExprFragment(nestedUnion('NestedTypeNode', 'innerTypeNode')); + expect(result.content).toBe('NestedTypeNode'); + // Both identifiers come from the same package; they collapse to a + // single import-map entry holding two identifiers. + expect([...result.imports.keys()]).toEqual(['@codama/node-types']); + const ids = [...result.imports.get('@codama/node-types')!.values()].map(i => i.usedIdentifier).sort(); + expect(ids).toEqual(['InnerTypeNode', 'NestedTypeNode']); + }); + + it('renders an empty tuple as []', () => { + expect(getTypeExprFragment(tuple()).content).toBe('[]'); + }); + + it('renders a multi-element tuple with comma-separated items', () => { + expect(getTypeExprFragment(tuple(boolean(), string())).content).toBe('[boolean, string]'); + }); + + 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'); + }); +}); diff --git a/packages/spec-generators/test/nodes/generate.test.ts b/packages/spec-generators/test/nodes/generate.test.ts new file mode 100644 index 000000000..fe1ad1bc3 --- /dev/null +++ b/packages/spec-generators/test/nodes/generate.test.ts @@ -0,0 +1,259 @@ +import { getFromRenderMap } from '@codama/fragments'; +import type { Spec } from '@codama/spec'; +import { getSpec } from '@codama/spec'; +import { defineNode } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { + type GenerateOptions, + getRenderMap, + NODE_CONFIGS, + type NodeConstructorConfig, + validateRenderOptions, +} from '../../src/nodes'; + +function options(overrides: Partial = {}): GenerateOptions { + return { + outputDir: '/tmp/unused', + targetSpecMajor: 1, + ...overrides, + }; +} + +describe('validateRenderOptions', () => { + const spec = getSpec(); + + it('accepts the active v1 spec with defaulted options', () => { + expect(() => validateRenderOptions(spec, options())).not.toThrow(); + }); + + it('throws when targetSpecMajor does not match the spec version', () => { + expect(() => validateRenderOptions(spec, options({ targetSpecMajor: 9 }))).toThrow( + /targetSpecMajor=9.*major 1/, + ); + }); + + 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('throws when the default NODE_CONFIGS references a node kind the spec does not declare', () => { + // Emptying the spec triggers the cross-check on the first + // configured node kind (`accountNode`, etc.). + const empty: Spec = { categories: [], version: '1.0.0' }; + expect(() => validateRenderOptions(empty, options())).toThrow(/nodeConfigs references unknown node kind/); + }); + + it('throws when the default NODE_CONFIGS.attributes references an attribute the spec does not declare', () => { + // Build a minimal spec that *does* declare every node kind + // mentioned in NODE_CONFIGS, but with empty attribute lists. + // The first config entry with an `attributes` override + // (`accountNode.data`) will trip the second cross-check branch. + const synthetic: Spec = { + categories: [ + { + enumerations: [], + name: 'topLevel', + nestedUnions: [], + nodes: spec.categories.flatMap(c => c.nodes).map(n => ({ ...n, attributes: [] })), + unions: [], + }, + ], + version: '1.0.0', + }; + expect(() => validateRenderOptions(synthetic, options())).toThrow( + /nodeConfigs\.attributes for ".+" references attribute ".+" which the spec does not declare/, + ); + }); + + it('accepts the live spec — every reserved-word positional arg has a `paramName` override', () => { + // `enumValueNode.enum` is the one positional arg that names a + // TS reserved word in the live spec; its config declares + // `paramName: 'enumLink'` and passes. This test guards against + // future spec changes that surface a new reserved-word + // positional arg without an accompanying `paramName`. + expect(() => validateRenderOptions(spec, options())).not.toThrow(); + }); + + it('uses the caller-supplied nodeConfigs override instead of the default', () => { + // Spec declares one synthetic node; the override references a + // *different* kind that the spec does not declare, so the + // validator should reject it. + const synthetic: Spec = { + categories: [ + { + enumerations: [], + name: 'topLevel', + nestedUnions: [], + nodes: [defineNode('realNode', { attributes: [] })], + unions: [], + }, + ], + version: '1.0.0', + }; + const customNodeConfigs = new Map([['ghostNode', {}]]); + expect(() => validateRenderOptions(synthetic, options({ nodeConfigs: customNodeConfigs }))).toThrow( + /nodeConfigs references unknown node kind "ghostNode"/, + ); + }); + + it('accepts an empty nodeConfigs override', () => { + // Override completely opts out of the default NODE_CONFIGS + // table. The shared render-option checks still pass against + // the live spec. + expect(() => validateRenderOptions(spec, options({ nodeConfigs: new Map() }))).not.toThrow(); + }); + + it('exposes NODE_CONFIGS so callers can pass it explicitly', () => { + expect(() => validateRenderOptions(spec, options({ nodeConfigs: NODE_CONFIGS }))).not.toThrow(); + }); +}); + +describe('getRenderMap', () => { + const map = getRenderMap(getSpec(), { targetSpecMajor: 1 }); + + it('produces a constructor file per spec node, keyed by .ts-suffixed path', () => { + expect(map.has('AccountNode.ts')).toBe(true); + expect(map.has('typeNodes/StructTypeNode.ts')).toBe(true); + expect(map.has('contextualValueNodes/PdaValueNode.ts')).toBe(true); + expect(map.has('linkNodes/PdaLinkNode.ts')).toBe(true); + }); + + it('produces a runtime kinds-array file per spec union', () => { + expect(map.has('typeNodes/StandaloneTypeNode.ts')).toBe(true); + expect(map.has('typeNodes/RegisteredTypeNode.ts')).toBe(true); + expect(map.has('typeNodes/TypeNode.ts')).toBe(true); + expect(map.has('valueNodes/ValueNode.ts')).toBe(true); + }); + + it('produces the top-level node registry file', () => { + expect(map.has('nodeKinds.ts')).toBe(true); + }); + + it('produces per-folder index files', () => { + expect(map.has('index.ts')).toBe(true); + expect(map.has('typeNodes/index.ts')).toBe(true); + expect(map.has('valueNodes/index.ts')).toBe(true); + }); + + it('keys every entry with a .ts suffix', () => { + for (const key of map.keys()) { + expect(key).toMatch(/\.ts$/); + } + }); + + it('renders the constructor function in the Fragment content', () => { + const entry = getFromRenderMap(map, 'AccountNode.ts'); + expect(entry.content).toContain('export function accountNode'); + expect(entry.content).toContain("kind: 'accountNode'"); + }); + + it('emits the matching XxxNodeInput type for object-input constructors', () => { + const entry = getFromRenderMap(map, 'AccountNode.ts'); + expect(entry.content).toContain('export type AccountNodeInput'); + }); + + it('omits the XxxNodeInput type for positional-arg constructors', () => { + const entry = getFromRenderMap(map, 'typeNodes/NumberTypeNode.ts'); + expect(entry.content).not.toContain('NumberTypeNodeInput'); + expect(entry.content).toContain('export function numberTypeNode'); + }); + + it('drops empty docs at the call site via the conditional-spread pattern', () => { + const entry = getFromRenderMap(map, 'AccountNode.ts'); + // Single `parseDocs` call hoisted to a `parsedDocs` local; the + // conditional spread reuses the local. The local is named + // `parsedDocs` (not `docs`) to avoid shadowing a positional + // `docs` parameter on the few constructors that take one. + expect(entry.content).toContain('const parsedDocs = parseDocs(input.docs);'); + expect(entry.content).toContain('...(parsedDocs.length > 0 && { docs: parsedDocs }),'); + }); + + it('emits the spec-version constant as its own file, typed as `CodamaVersion`', () => { + expect(map.has('codamaVersion.ts')).toBe(true); + const entry = getFromRenderMap(map, 'codamaVersion.ts'); + expect(entry.content).toMatch(/export const CODAMA_VERSION: CodamaVersion = '\d+\.\d+\.\d+[^']*';/); + }); + + it('reads the spec version through CODAMA_VERSION in the rootNode constructor', () => { + const entry = getFromRenderMap(map, 'RootNode.ts'); + // No `as CodamaVersion` cast needed — the constant carries the + // type natively. + expect(entry.content).toContain('version: CODAMA_VERSION,'); + expect(entry.content).toContain("import { CODAMA_VERSION } from './codamaVersion';"); + }); + + it('produces a runtime kinds-array as a value (no `as const` on the array)', () => { + // Each member is `as const`-tagged individually so the array's + // element type is the convenient `('kindA' | 'kindB' | ...)[]` + // shape — wider than a `readonly [...]` tuple but compatible + // with the `isNode` helpers. + const entry = getFromRenderMap(map, 'typeNodes/StandaloneTypeNode.ts'); + expect(entry.content).toContain('export const STANDALONE_TYPE_NODE_KINDS = ['); + expect(entry.content).not.toContain('] as const;'); + expect(entry.content).toContain("'numberTypeNode' as const,"); + }); + + it('expands union-of-unions transitively by spreading the dependent kinds array', () => { + // `TypeNode` ⊇ `StandaloneTypeNode` ∪ `definedTypeLinkNode`. + const entry = getFromRenderMap(map, 'typeNodes/TypeNode.ts'); + expect(entry.content).toContain('...STANDALONE_TYPE_NODE_KINDS,'); + expect(entry.content).toContain("'definedTypeLinkNode' as const,"); + }); + + it('aggregates every spec node-kind into REGISTERED_NODE_KINDS', () => { + const entry = getFromRenderMap(map, 'nodeKinds.ts'); + expect(entry.content).toContain('export const REGISTERED_NODE_KINDS = ['); + // Every per-category registered-* kinds array is spread in. + expect(entry.content).toContain('...REGISTERED_TYPE_NODE_KINDS,'); + expect(entry.content).toContain('...REGISTERED_VALUE_NODE_KINDS,'); + // Top-level node kinds appear as bare literals. + expect(entry.content).toContain("'accountNode' as const,"); + expect(entry.content).toContain("'rootNode' as const,"); + }); + + it('resolves all symbolic imports into relative paths or package specifiers', () => { + const entry = getFromRenderMap(map, 'AccountNode.ts'); + for (const key of entry.imports.keys()) { + // No symbolic-key prefixes left (`node:foo`, `nodeType:Bar`, etc.). + expect(key).not.toMatch(/^[a-z]+:/); + } + // The interface type comes from the published package. + expect([...entry.imports.keys()]).toContain('@codama/node-types'); + }); + + it('collapses `key: key` pass-throughs to the shorthand `key`', () => { + // `numberTypeNode` is a positional constructor whose body + // simply forwards `format` and `endian` from the params. + const entry = getFromRenderMap(map, 'typeNodes/NumberTypeNode.ts'); + expect(entry.content).toContain('format,'); + expect(entry.content).toContain('endian,'); + expect(entry.content).not.toContain('format: format'); + expect(entry.content).not.toContain('endian: endian'); + }); + + it('widens `name` parameters from the spec brand to plain `string`', () => { + // The spec types `name` attributes as `stringIdentifier()`, + // which would render as `CamelCaseString`. The constructor + // relaxes this to `string` so callers can pass any input; + // the body wraps in `camelCase(...)` to brand the value. + const entry = getFromRenderMap(map, 'linkNodes/AccountLinkNode.ts'); + expect(entry.content).toMatch(/name: string\b/); + expect(entry.content).toContain('name: camelCase(name)'); + }); + + it('renders spec-union references by name in generic constraints', () => { + // `conditionalValueNode.condition` is typed as + // `union('ConditionalValueCondition')` in the spec. The + // generated constructor should reference that named union + // directly rather than re-deriving its inline `A | B | C` + // form. + const entry = getFromRenderMap(map, 'contextualValueNodes/ConditionalValueNode.ts'); + expect(entry.content).toContain('TCondition extends ConditionalValueCondition'); + }); + + it('emits a frozen render map', () => { + expect(Object.isFrozen(map)).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/nodes/kindsArrayConstantName.test.ts b/packages/spec-generators/test/nodes/kindsArrayConstantName.test.ts new file mode 100644 index 000000000..28bc26442 --- /dev/null +++ b/packages/spec-generators/test/nodes/kindsArrayConstantName.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; + +import { kindsArrayConstantName } from '../../src/nodes/kindsArrayConstantName'; + +describe('kindsArrayConstantName', () => { + it('snake-cases and upper-cases the union name with a _KINDS suffix', () => { + expect(kindsArrayConstantName('StandaloneTypeNode')).toBe('STANDALONE_TYPE_NODE_KINDS'); + expect(kindsArrayConstantName('TypeNode')).toBe('TYPE_NODE_KINDS'); + expect(kindsArrayConstantName('InstructionInputValueNode')).toBe('INSTRUCTION_INPUT_VALUE_NODE_KINDS'); + }); +}); diff --git a/packages/spec-generators/test/nodes/scope.test.ts b/packages/spec-generators/test/nodes/scope.test.ts new file mode 100644 index 000000000..4f44b364d --- /dev/null +++ b/packages/spec-generators/test/nodes/scope.test.ts @@ -0,0 +1,103 @@ +import type { Spec } from '@codama/spec'; +import { defineCategory, defineNode, defineUnion } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { buildRenderScope, type RenderOptions } from '../../src/nodes/options'; + +function buildSpec(categories: Spec['categories']): Spec { + return { categories, version: '1.0.0' }; +} + +const options: RenderOptions = { targetSpecMajor: 1 }; + +describe('buildRenderScope', () => { + it('places top-level node constructors at the root', () => { + const scope = buildRenderScope( + buildSpec([defineCategory('topLevel', { nodes: [defineNode('accountNode', { attributes: [] })] })]), + options, + ); + expect(scope.symbolicModules.get('constructor:accountNode')).toBe('AccountNode'); + }); + + it('places type-category constructors under typeNodes/', () => { + const scope = buildRenderScope( + buildSpec([defineCategory('type', { nodes: [defineNode('arrayTypeNode', { attributes: [] })] })]), + options, + ); + expect(scope.symbolicModules.get('constructor:arrayTypeNode')).toBe('typeNodes/ArrayTypeNode'); + }); + + it('places contextual-value constructors under contextualValueNodes/', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('contextualValue', { + nodes: [defineNode('argumentValueNode', { attributes: [] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('constructor:argumentValueNode')).toBe( + 'contextualValueNodes/ArgumentValueNode', + ); + }); + + it('places category unions under their category folder', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('type', { + nodes: [defineNode('innerTypeNode', { attributes: [] })], + unions: [defineUnion('TypeNode', { members: ['innerTypeNode'] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('kinds:TypeNode')).toBe('typeNodes/TypeNode'); + }); + + it('places top-level unions at the root', () => { + const scope = buildRenderScope( + buildSpec([ + defineCategory('topLevel', { + nodes: [defineNode('innerNode', { attributes: [] })], + unions: [defineUnion('InstructionByteDeltaValue', { members: ['innerNode'] })], + }), + ]), + options, + ); + expect(scope.symbolicModules.get('kinds:InstructionByteDeltaValue')).toBe('InstructionByteDeltaValue'); + }); + + it('routes the top-level Node registry to a fixed location', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('kinds:Node')).toBe('nodeKinds'); + }); + + it('routes generated:CodamaVersion to a fixed location', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('generated:CodamaVersion')).toBe('codamaVersion'); + }); + + it('points shared helpers at the hand-written sibling above generated/', () => { + const scope = buildRenderScope(buildSpec([]), options); + expect(scope.symbolicModules.get('shared:DocsInput')).toBe('../shared'); + expect(scope.symbolicModules.get('shared:parseDocs')).toBe('../shared'); + expect(scope.symbolicModules.get('shared:camelCase')).toBe('../shared'); + expect(scope.symbolicModules.get('shared:isNode')).toBe('../Node'); + }); + + it('throws on an unknown category', () => { + const spec = { + categories: [ + { + enumerations: [], + name: 'mystery', + nestedUnions: [], + nodes: [defineNode('someNode', { attributes: [] })], + unions: [], + }, + ], + version: '1.0.0', + } as unknown as Spec; + expect(() => buildRenderScope(spec, options)).toThrow(/unknown category "mystery"/); + }); +}); diff --git a/packages/spec-generators/test/shared/fragments/indexPage.test.ts b/packages/spec-generators/test/shared/fragments/indexPage.test.ts new file mode 100644 index 000000000..c1a95e77c --- /dev/null +++ b/packages/spec-generators/test/shared/fragments/indexPage.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; + +import { getIndexPageFragment, groupPathsByFolder } from '../../../src/shared'; + +describe('getIndexPageFragment', () => { + it('renders a single `export *` line', () => { + // No trailing newline on the fragment itself; the EOL convention + // is owned by the page-level renderer 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 *'); + }); +}); + +describe('groupPathsByFolder', () => { + it('groups top-level files under the empty-string sentinel', () => { + const result = groupPathsByFolder(['AccountNode.ts', 'RootNode.ts']); + expect(result.get('')).toEqual(['AccountNode', 'RootNode']); + }); + + it('groups subdirectory files under their folder name', () => { + const result = groupPathsByFolder(['typeNodes/StructTypeNode.ts', 'typeNodes/ArrayTypeNode.ts']); + expect(result.get('typeNodes')).toEqual(['StructTypeNode', 'ArrayTypeNode']); + }); + + it('strips the `.ts` extension from basenames', () => { + const result = groupPathsByFolder(['AccountNode.ts']); + expect(result.get('')).toEqual(['AccountNode']); + }); + + it('handles paths with no extension', () => { + const result = groupPathsByFolder(['AccountNode']); + expect(result.get('')).toEqual(['AccountNode']); + }); + + it('returns an empty map when given no paths', () => { + const result = groupPathsByFolder([]); + expect(result.size).toBe(0); + }); +}); diff --git a/packages/spec-generators/test/nodeTypes/fragments/page.test.ts b/packages/spec-generators/test/shared/fragments/page.test.ts similarity index 71% rename from packages/spec-generators/test/nodeTypes/fragments/page.test.ts rename to packages/spec-generators/test/shared/fragments/page.test.ts index 2fa423ca4..cdcd65e54 100644 --- a/packages/spec-generators/test/nodeTypes/fragments/page.test.ts +++ b/packages/spec-generators/test/shared/fragments/page.test.ts @@ -1,11 +1,10 @@ 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'; +import { getPageFragment, type SymbolicModule, type SymbolicModuleMap } from '../../../src/shared'; -function buildScope(entries: Record): Pick { - return { symbolicModules: new Map(Object.entries(entries) as [SymbolicModule, string][]) }; +function buildModules(entries: Record): SymbolicModuleMap { + return new Map(Object.entries(entries) as [SymbolicModule, string][]); } describe('getPageFragment', () => { @@ -13,7 +12,7 @@ describe('getPageFragment', () => { 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'); + const result = getPageFragment(body, buildModules({}), 'Foo'); expect(result.imports.size).toBe(0); expect(result.content).toBe('export type Foo = string;\n'); }); @@ -22,15 +21,15 @@ describe('getPageFragment', () => { // 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'); + const modules = buildModules({ 'node:numberTypeNode': 'typeNodes/NumberTypeNode' }); + const result = getPageFragment(body, modules, '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'); + const modules = buildModules({ 'enumeration:Endianness': 'shared/endianness' }); + const result = getPageFragment(body, modules, 'typeNodes/Foo'); expect([...result.imports.keys()]).toEqual(['../shared/endianness']); }); @@ -38,15 +37,15 @@ describe('getPageFragment', () => { // 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'); + const modules = buildModules({ 'docs:Docs': '../Docs' }); + const result = getPageFragment(body, modules, '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'); + const modules = buildModules({ 'docs:Docs': '../Docs' }); + const result = getPageFragment(body, modules, 'Foo'); expect([...result.imports.keys()]).toEqual(['../Docs']); }); @@ -56,18 +55,18 @@ describe('getPageFragment', () => { // 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'); + const modules = buildModules({ 'node:selfNode': 'SelfNode' }); + const result = getPageFragment(body, modules, '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({ + const modules = buildModules({ 'node:otherNode': 'OtherNode', 'node:selfNode': 'SelfNode', }); - const result = getPageFragment(body, scope, 'SelfNode'); + const result = getPageFragment(body, modules, 'SelfNode'); expect([...result.imports.keys()]).toEqual(['./OtherNode']); }); @@ -78,11 +77,11 @@ describe('getPageFragment', () => { // 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({ + const modules = buildModules({ 'brand:CamelCaseString': '../brands', 'brand:KebabCaseString': '../brands', }); - const result = getPageFragment(body, scope, 'Foo'); + const result = getPageFragment(body, modules, 'Foo'); expect([...result.imports.keys()]).toEqual(['../brands']); const brandsEntry = result.imports.get('../brands')!; expect([...brandsEntry.keys()].sort()).toEqual(['CamelCaseString', 'KebabCaseString']); @@ -90,8 +89,7 @@ describe('getPageFragment', () => { 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( + expect(() => getPageFragment(body, buildModules({}), 'Foo')).toThrow( /unknown symbolic module "node:doesNotExist".*from "Foo"/, ); }); @@ -100,26 +98,26 @@ describe('getPageFragment', () => { 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'); + expect(getPageFragment(body, buildModules({}), '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'); + expect(getPageFragment(body, buildModules({}), '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'); + const modules = buildModules({ 'node:numberTypeNode': 'typeNodes/NumberTypeNode' }); + const result = getPageFragment(body, modules, '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'); + const modules = buildModules({ 'node:numberTypeNode': 'typeNodes/NumberTypeNode' }); + const result = getPageFragment(body, modules, '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/typeParameterIdentifier.test.ts b/packages/spec-generators/test/shared/fragments/typeParameterIdentifier.test.ts similarity index 93% rename from packages/spec-generators/test/nodeTypes/fragments/typeParameterIdentifier.test.ts rename to packages/spec-generators/test/shared/fragments/typeParameterIdentifier.test.ts index dd62146d1..cd4d1a034 100644 --- a/packages/spec-generators/test/nodeTypes/fragments/typeParameterIdentifier.test.ts +++ b/packages/spec-generators/test/shared/fragments/typeParameterIdentifier.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { getTypeParameterIdentifierFragment } from '../../../src/nodeTypes/fragments/typeParameterIdentifier'; +import { getTypeParameterIdentifierFragment } from '../../../src/shared'; describe('getTypeParameterIdentifierFragment', () => { it('prefixes the attribute name with `T` and PascalCases it', () => { diff --git a/packages/spec-generators/test/shared/options.test.ts b/packages/spec-generators/test/shared/options.test.ts new file mode 100644 index 000000000..8cc69ff08 --- /dev/null +++ b/packages/spec-generators/test/shared/options.test.ts @@ -0,0 +1,58 @@ +import { array, attribute, boolean, enumeration, node, optionalAttribute, u32, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { isNodeTypeParameterAttribute, type SharedResolvedRenderOptions } from '../../src/shared'; + +type TypeParameterScope = Pick; + +function buildOptions(overrides: Partial = {}): TypeParameterScope { + return { narrowableDataAttributes: new Set(), ...overrides }; +} + +describe('isNodeTypeParameterAttribute', () => { + it('selects a child attribute whose type is a direct node reference', () => { + expect(isNodeTypeParameterAttribute('aNode', attribute('payload', node('innerNode')), buildOptions())).toBe( + true, + ); + }); + + it('selects a child attribute whose type is a union reference', () => { + expect(isNodeTypeParameterAttribute('aNode', attribute('payload', union('TypeNode')), buildOptions())).toBe( + true, + ); + }); + + it('selects a child attribute wrapped in an array', () => { + expect( + isNodeTypeParameterAttribute('aNode', attribute('items', array(node('innerNode'))), buildOptions()), + ).toBe(true); + }); + + it('rejects a plain data attribute (boolean)', () => { + expect(isNodeTypeParameterAttribute('aNode', attribute('flag', boolean()), buildOptions())).toBe(false); + }); + + it('rejects a plain data attribute (number)', () => { + expect(isNodeTypeParameterAttribute('aNode', attribute('count', u32()), buildOptions())).toBe(false); + }); + + it('selects a narrowable data attribute when its key is in narrowableDataAttributes', () => { + const options = buildOptions({ narrowableDataAttributes: new Set(['numberTypeNode:format']) }); + expect( + isNodeTypeParameterAttribute('numberTypeNode', attribute('format', enumeration('NumberFormat')), options), + ).toBe(true); + }); + + it('rejects a data attribute whose key is absent from the narrowable set', () => { + const options = buildOptions({ narrowableDataAttributes: new Set(['otherNode:format']) }); + expect( + isNodeTypeParameterAttribute('numberTypeNode', attribute('format', enumeration('NumberFormat')), options), + ).toBe(false); + }); + + it('respects optionality (still selects an optional child attribute)', () => { + expect( + isNodeTypeParameterAttribute('aNode', optionalAttribute('payload', node('innerNode')), buildOptions()), + ).toBe(true); + }); +}); diff --git a/packages/spec-generators/test/shared/unions.test.ts b/packages/spec-generators/test/shared/unions.test.ts new file mode 100644 index 000000000..b20181874 --- /dev/null +++ b/packages/spec-generators/test/shared/unions.test.ts @@ -0,0 +1,135 @@ +import type { Spec } from '@codama/spec'; +import { defineCategory, defineNode, defineUnion, node, union } from '@codama/spec/api'; +import { describe, expect, it } from 'vitest'; + +import { flattenNodeUnion, getRegisteredCategoryUnions } from '../../src/shared'; + +describe('getRegisteredCategoryUnions', () => { + it('returns only unions whose names start with `Registered`, sorted', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] })], + unions: [ + defineUnion('TypeNode', { members: ['aNode'] }), + defineUnion('RegisteredTypeNode', { members: ['aNode'] }), + ], + }), + defineCategory('value', { + nodes: [defineNode('bNode', { attributes: [] })], + unions: [defineUnion('RegisteredValueNode', { members: ['bNode'] })], + }), + ], + version: '1.0.0', + }; + expect(getRegisteredCategoryUnions(spec).map(u => u.name)).toEqual([ + 'RegisteredTypeNode', + 'RegisteredValueNode', + ]); + }); + + it('returns an empty list when no union has the prefix', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] })], + unions: [defineUnion('TypeNode', { members: ['aNode'] })], + }), + ], + version: '1.0.0', + }; + expect(getRegisteredCategoryUnions(spec)).toEqual([]); + }); + + it('returns full UnionSpec values so callers can read members', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] })], + unions: [defineUnion('RegisteredTypeNode', { members: ['aNode'] })], + }), + ], + version: '1.0.0', + }; + const [first] = getRegisteredCategoryUnions(spec); + expect(first.name).toBe('RegisteredTypeNode'); + expect(first.members).toEqual([{ kind: 'node', name: 'aNode' }]); + }); +}); + +describe('flattenNodeUnion', () => { + it('returns the leaf node specs of a flat union', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] }), defineNode('bNode', { attributes: [] })], + unions: [defineUnion('TypeNode', { members: ['aNode', 'bNode'] })], + }), + ], + version: '1.0.0', + }; + const [typeNode] = spec.categories[0].unions; + expect( + flattenNodeUnion(typeNode, spec) + .map(n => n.kind) + .sort(), + ).toEqual(['aNode', 'bNode']); + }); + + it('recurses through nested `union` members', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] }), defineNode('bNode', { attributes: [] })], + unions: [ + defineUnion('Inner', { members: ['bNode'] }), + defineUnion('Outer', { members: [union('Inner'), node('aNode')] }), + ], + }), + ], + version: '1.0.0', + }; + const outer = spec.categories[0].unions.find(u => u.name === 'Outer')!; + expect( + flattenNodeUnion(outer, spec) + .map(n => n.kind) + .sort(), + ).toEqual(['aNode', 'bNode']); + }); + + it('skips unresolved member references silently', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] })], + // `union('Ghost')` points at a union not declared anywhere. + unions: [defineUnion('Outer', { members: [union('Ghost'), node('aNode')] })], + }), + ], + version: '1.0.0', + }; + const [outer] = spec.categories[0].unions; + expect(flattenNodeUnion(outer, spec).map(n => n.kind)).toEqual(['aNode']); + }); + + it('breaks cycles in the union graph', () => { + const spec: Spec = { + categories: [ + defineCategory('type', { + nodes: [defineNode('aNode', { attributes: [] }), defineNode('bNode', { attributes: [] })], + unions: [ + defineUnion('A', { members: [union('B'), node('aNode')] }), + defineUnion('B', { members: [union('A'), node('bNode')] }), + ], + }), + ], + version: '1.0.0', + }; + const a = spec.categories[0].unions.find(u => u.name === 'A')!; + expect( + flattenNodeUnion(a, spec) + .map(n => n.kind) + .sort(), + ).toEqual(['aNode', 'bNode']); + }); +}); diff --git a/packages/visitors-core/src/removeDocsVisitor.ts b/packages/visitors-core/src/removeDocsVisitor.ts index e1ad81eec..e53c3d9c5 100644 --- a/packages/visitors-core/src/removeDocsVisitor.ts +++ b/packages/visitors-core/src/removeDocsVisitor.ts @@ -3,10 +3,21 @@ import { NodeKind } from '@codama/nodes'; import { interceptVisitor } from './interceptVisitor'; import { nonNullableIdentityVisitor } from './nonNullableIdentityVisitor'; +/** + * Strip docs from every node the visitor passes over. + * + * Constructors in `@codama/nodes` omit the `docs` attribute entirely + * when its value would be empty (matching the Rust side and avoiding + * gratuitous `docs: []` noise in encoded IDLs). This visitor follows + * the same convention — it removes the `docs` key altogether rather + * than blanking it to `[]`. + */ export function removeDocsVisitor(options: { keys?: TNodeKind[] } = {}) { return interceptVisitor(nonNullableIdentityVisitor(options), (node, next) => { if ('docs' in node) { - return next({ ...node, docs: [] }); + const copy = { ...node } as typeof node & { docs?: unknown }; + delete copy.docs; + return next(copy as typeof node); } return next(node); }); diff --git a/packages/visitors-core/test/getUniqueHashStringVisitor.test.ts b/packages/visitors-core/test/getUniqueHashStringVisitor.test.ts index 0109a77ea..ea7ad434b 100644 --- a/packages/visitors-core/test/getUniqueHashStringVisitor.test.ts +++ b/packages/visitors-core/test/getUniqueHashStringVisitor.test.ts @@ -32,10 +32,13 @@ test('it returns a unique string whilst discard docs', () => { // When we get its unique hash string whilst discarding docs. const result = visit(node, getUniqueHashStringVisitor({ removeDocs: true })); - // Then we expect the following string. + // Then we expect the following string. `docs` is absent because + // `@codama/nodes` constructors omit `docs` from the encoded shape + // when it would be empty; the `removeDocs` visitor preserves that + // omission on the rendered hash. expect(result).toEqual( '{"fields":[' + - '{"docs":[],"kind":"structFieldTypeNode","name":"owner","type":{"kind":"publicKeyTypeNode"}}' + + '{"kind":"structFieldTypeNode","name":"owner","type":{"kind":"publicKeyTypeNode"}}' + '],"kind":"structTypeNode"}', ); });