Przeglądaj źródła

Add visitors-core package (#4)

Loris Leiva 1 rok temu
rodzic
commit
e79f2ecad2
100 zmienionych plików z 4109 dodań i 7 usunięć
  1. 1 1
      package.json
  2. 1 1
      packages/node-types/src/linkNodes/AccountLinkNode.ts
  3. 1 1
      packages/node-types/src/linkNodes/DefinedTypeLinkNode.ts
  4. 1 1
      packages/node-types/src/linkNodes/PdaLinkNode.ts
  5. 1 1
      packages/node-types/src/linkNodes/ProgramLinkNode.ts
  6. 16 0
      packages/node-types/test/GetNodeFromKind.typetest.ts
  7. 1 1
      packages/node-types/tsconfig.json
  8. 1 1
      packages/nodes/src/contextualValueNodes/ContextualValueNode.ts
  9. 2 0
      packages/nodes/src/index.ts
  10. 1 0
      packages/visitors-core/.gitignore
  11. 1 0
      packages/visitors-core/.prettierignore
  12. 23 0
      packages/visitors-core/LICENSE
  13. 3 0
      packages/visitors-core/README.md
  14. 71 0
      packages/visitors-core/package.json
  15. 87 0
      packages/visitors-core/src/LinkableDictionary.ts
  16. 63 0
      packages/visitors-core/src/NodeSelector.ts
  17. 59 0
      packages/visitors-core/src/NodeStack.ts
  18. 42 0
      packages/visitors-core/src/bottomUpTransformerVisitor.ts
  19. 11 0
      packages/visitors-core/src/consoleLogVisitor.ts
  20. 19 0
      packages/visitors-core/src/deleteNodesVisitor.ts
  21. 64 0
      packages/visitors-core/src/extendVisitor.ts
  22. 110 0
      packages/visitors-core/src/getDebugStringVisitor.ts
  23. 14 0
      packages/visitors-core/src/getUniqueHashStringVisitor.ts
  24. 618 0
      packages/visitors-core/src/identityVisitor.ts
  25. 24 0
      packages/visitors-core/src/index.ts
  26. 31 0
      packages/visitors-core/src/interceptVisitor.ts
  27. 26 0
      packages/visitors-core/src/mapVisitor.ts
  28. 348 0
      packages/visitors-core/src/mergeVisitor.ts
  29. 10 0
      packages/visitors-core/src/nonNullableIdentityVisitor.ts
  30. 113 0
      packages/visitors-core/src/pipe.ts
  31. 51 0
      packages/visitors-core/src/recordLinkablesVisitor.ts
  32. 17 0
      packages/visitors-core/src/recordNodeStackVisitor.ts
  33. 13 0
      packages/visitors-core/src/removeDocsVisitor.ts
  34. 17 0
      packages/visitors-core/src/singleNodeVisitor.ts
  35. 14 0
      packages/visitors-core/src/staticVisitor.ts
  36. 21 0
      packages/visitors-core/src/tapVisitor.ts
  37. 47 0
      packages/visitors-core/src/topDownTransformerVisitor.ts
  38. 32 0
      packages/visitors-core/src/visitor.ts
  39. 12 0
      packages/visitors-core/src/voidVisitor.ts
  40. 113 0
      packages/visitors-core/test/bottomUpTransformerVisitor.test.ts
  41. 40 0
      packages/visitors-core/test/deleteNodesVisitor.test.ts
  42. 65 0
      packages/visitors-core/test/extendVisitor.test.ts
  43. 107 0
      packages/visitors-core/test/getDebugStringVisitor.test.ts
  44. 49 0
      packages/visitors-core/test/getUniqueHashStringVisitor.test.ts
  45. 66 0
      packages/visitors-core/test/identityVisitor.test.ts
  46. 34 0
      packages/visitors-core/test/interceptVisitor.test.ts
  47. 42 0
      packages/visitors-core/test/mapVisitor.test.ts
  48. 59 0
      packages/visitors-core/test/mergeVisitor.test.ts
  49. 49 0
      packages/visitors-core/test/nodes/AccountNode.test.ts
  50. 44 0
      packages/visitors-core/test/nodes/DefinedTypeNode.test.ts
  51. 20 0
      packages/visitors-core/test/nodes/ErrorNode.test.ts
  52. 29 0
      packages/visitors-core/test/nodes/InstructionAccountNode.test.ts
  53. 34 0
      packages/visitors-core/test/nodes/InstructionArgumentNode.test.ts
  54. 25 0
      packages/visitors-core/test/nodes/InstructionByteDeltaNode.test.ts
  55. 113 0
      packages/visitors-core/test/nodes/InstructionNode.test.ts
  56. 26 0
      packages/visitors-core/test/nodes/InstructionRemainingAccountsNode.test.ts
  57. 47 0
      packages/visitors-core/test/nodes/PdaNode.test.ts
  58. 68 0
      packages/visitors-core/test/nodes/ProgramNode.test.ts
  59. 39 0
      packages/visitors-core/test/nodes/RootNode.test.ts
  60. 62 0
      packages/visitors-core/test/nodes/_setup.ts
  61. 16 0
      packages/visitors-core/test/nodes/contextualValueNodes/AccountBumpValueNode.test.ts
  62. 16 0
      packages/visitors-core/test/nodes/contextualValueNodes/AccountValueNode.test.ts
  63. 16 0
      packages/visitors-core/test/nodes/contextualValueNodes/ArgumentValueNode.test.ts
  64. 41 0
      packages/visitors-core/test/nodes/contextualValueNodes/ConditionalValueNode.test.ts
  65. 16 0
      packages/visitors-core/test/nodes/contextualValueNodes/IdentityValueNode.test.ts
  66. 16 0
      packages/visitors-core/test/nodes/contextualValueNodes/PayerValueNode.test.ts
  67. 23 0
      packages/visitors-core/test/nodes/contextualValueNodes/PdaSeedValueNode.test.ts
  68. 34 0
      packages/visitors-core/test/nodes/contextualValueNodes/PdaValueNode.test.ts
  69. 16 0
      packages/visitors-core/test/nodes/contextualValueNodes/ProgramIdValueNode.test.ts
  70. 27 0
      packages/visitors-core/test/nodes/contextualValueNodes/ResolverValueNode.test.ts
  71. 25 0
      packages/visitors-core/test/nodes/discriminatorNodes/ConstantDiscriminatorNode.test.ts
  72. 16 0
      packages/visitors-core/test/nodes/discriminatorNodes/FieldDiscriminatorNode.test.ts
  73. 16 0
      packages/visitors-core/test/nodes/discriminatorNodes/SizeDiscriminatorNode.test.ts
  74. 16 0
      packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts
  75. 16 0
      packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts
  76. 16 0
      packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts
  77. 16 0
      packages/visitors-core/test/nodes/linkNodes/ProgramLinkNode.test.ts
  78. 25 0
      packages/visitors-core/test/nodes/pdaSeedNodes/ConstantPdaSeedNode.test.ts
  79. 23 0
      packages/visitors-core/test/nodes/pdaSeedNodes/VariablePdaSeedNode.test.ts
  80. 16 0
      packages/visitors-core/test/nodes/sizeNodes/FixedSizeNode.test.ts
  81. 23 0
      packages/visitors-core/test/nodes/sizeNodes/PrefixedSizeNode.test.ts
  82. 16 0
      packages/visitors-core/test/nodes/sizeNodes/RemainderSizeNode.test.ts
  83. 23 0
      packages/visitors-core/test/nodes/typeNodes/AmountTypeNode.test.ts
  84. 27 0
      packages/visitors-core/test/nodes/typeNodes/ArrayTypeNode.test.ts
  85. 23 0
      packages/visitors-core/test/nodes/typeNodes/BooleanTypeNode.test.ts
  86. 16 0
      packages/visitors-core/test/nodes/typeNodes/BytesTypeNode.test.ts
  87. 23 0
      packages/visitors-core/test/nodes/typeNodes/DateTimeTypeNode.test.ts
  88. 16 0
      packages/visitors-core/test/nodes/typeNodes/EnumEmptyVariantTypeNode.test.ts
  89. 40 0
      packages/visitors-core/test/nodes/typeNodes/EnumStructVariantTypeNode.test.ts
  90. 26 0
      packages/visitors-core/test/nodes/typeNodes/EnumTupleVariantTypeNode.test.ts
  91. 67 0
      packages/visitors-core/test/nodes/typeNodes/EnumTypeNode.test.ts
  92. 23 0
      packages/visitors-core/test/nodes/typeNodes/FixedSizeTypeNode.test.ts
  93. 45 0
      packages/visitors-core/test/nodes/typeNodes/HiddenPrefixTypeNode.test.ts
  94. 45 0
      packages/visitors-core/test/nodes/typeNodes/HiddenSuffixTypeNode.test.ts
  95. 40 0
      packages/visitors-core/test/nodes/typeNodes/MapTypeNode.test.ts
  96. 22 0
      packages/visitors-core/test/nodes/typeNodes/NumberTypeNode.test.ts
  97. 28 0
      packages/visitors-core/test/nodes/typeNodes/OptionTypeNode.test.ts
  98. 33 0
      packages/visitors-core/test/nodes/typeNodes/PostOffsetTypeNode.test.ts
  99. 33 0
      packages/visitors-core/test/nodes/typeNodes/PreOffsetTypeNode.test.ts
  100. 16 0
      packages/visitors-core/test/nodes/typeNodes/PublicKeyTypeNode.test.ts

+ 1 - 1
package.json

@@ -6,7 +6,7 @@
     "scripts": {
         "build": "turbo run build --log-order grouped",
         "lint": "turbo run test:lint --log-order grouped",
-        "style:fix": "turbo style:fix --log-order grouped && pnpm prettier --log-level warn --ignore-unknown --write '{.,!packages}/*'",
+        "lint:fix": "turbo style:fix --log-order grouped && pnpm prettier --log-level warn --ignore-unknown --write '{.,!packages}/*'",
         "test": "turbo run test --log-order grouped"
     },
     "devDependencies": {

+ 1 - 1
packages/node-types/src/linkNodes/AccountLinkNode.ts

@@ -1,4 +1,4 @@
-import type { ImportFrom, CamelCaseString } from '../shared';
+import type { CamelCaseString, ImportFrom } from '../shared';
 
 export interface AccountLinkNode {
     readonly kind: 'accountLinkNode';

+ 1 - 1
packages/node-types/src/linkNodes/DefinedTypeLinkNode.ts

@@ -1,4 +1,4 @@
-import type { ImportFrom, CamelCaseString } from '../shared';
+import type { CamelCaseString, ImportFrom } from '../shared';
 
 export interface DefinedTypeLinkNode {
     readonly kind: 'definedTypeLinkNode';

+ 1 - 1
packages/node-types/src/linkNodes/PdaLinkNode.ts

@@ -1,4 +1,4 @@
-import type { ImportFrom, CamelCaseString } from '../shared';
+import type { CamelCaseString, ImportFrom } from '../shared';
 
 export interface PdaLinkNode {
     readonly kind: 'pdaLinkNode';

+ 1 - 1
packages/node-types/src/linkNodes/ProgramLinkNode.ts

@@ -1,4 +1,4 @@
-import type { ImportFrom, CamelCaseString } from '../shared';
+import type { CamelCaseString, ImportFrom } from '../shared';
 
 export interface ProgramLinkNode {
     readonly kind: 'programLinkNode';

+ 16 - 0
packages/node-types/test/GetNodeFromKind.typetest.ts

@@ -0,0 +1,16 @@
+import type { GetNodeFromKind, PublicKeyTypeNode, StringTypeNode } from '../src/index.js';
+
+// [DESCRIBE] GetNodeFromKind.
+{
+    // It extracts the node from the kind.
+    {
+        const node = {} as GetNodeFromKind<'publicKeyTypeNode'>;
+        node satisfies PublicKeyTypeNode;
+    }
+
+    // It extracts node unions from multiple kinds.
+    {
+        const node = {} as GetNodeFromKind<'publicKeyTypeNode' | 'stringTypeNode'>;
+        node satisfies PublicKeyTypeNode | StringTypeNode;
+    }
+}

+ 1 - 1
packages/node-types/tsconfig.json

@@ -3,5 +3,5 @@
     "compilerOptions": { "lib": [] },
     "display": "@kinobi-so/node-types",
     "extends": "../internals/tsconfig.base.json",
-    "include": ["src"]
+    "include": ["src", "test"]
 }

+ 1 - 1
packages/nodes/src/contextualValueNodes/ContextualValueNode.ts

@@ -21,4 +21,4 @@ export const REGISTERED_CONTEXTUAL_VALUE_NODE_KINDS = [
 
 // Contextual Value Node Helpers.
 export const CONTEXTUAL_VALUE_NODES = STANDALONE_CONTEXTUAL_VALUE_NODE_KINDS;
-export const INSTRUCTION_INPUT_VALUE_NODE = [...VALUE_NODES, ...CONTEXTUAL_VALUE_NODES, 'programLinkNode' as const];
+export const INSTRUCTION_INPUT_VALUE_NODES = [...VALUE_NODES, ...CONTEXTUAL_VALUE_NODES, 'programLinkNode' as const];

+ 2 - 0
packages/nodes/src/index.ts

@@ -8,6 +8,8 @@ export * from './pdaSeedNodes';
 export * from './typeNodes';
 export * from './valueNodes';
 
+export * from './shared';
+
 export * from './AccountNode';
 export * from './DefinedTypeNode';
 export * from './ErrorNode';

+ 1 - 0
packages/visitors-core/.gitignore

@@ -0,0 +1 @@
+dist/

+ 1 - 0
packages/visitors-core/.prettierignore

@@ -0,0 +1 @@
+dist/

+ 23 - 0
packages/visitors-core/LICENSE

@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) 2024 Kinobi
+Copyright (c) 2024 Metaplex Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 3 - 0
packages/visitors-core/README.md

@@ -0,0 +1,3 @@
+# Kinobi ➤ Visitors ➤ Core
+
+TODO

+ 71 - 0
packages/visitors-core/package.json

@@ -0,0 +1,71 @@
+{
+    "name": "@kinobi-so/visitors-core",
+    "version": "0.20.0",
+    "description": "Core visitors for the Kinobi framework",
+    "exports": {
+        "react-native": "./dist/index.react-native.js",
+        "browser": {
+            "import": "./dist/index.browser.js",
+            "require": "./dist/index.browser.cjs"
+        },
+        "node": {
+            "import": "./dist/index.node.js",
+            "require": "./dist/index.node.cjs"
+        },
+        "types": "./dist/types/index.d.ts"
+    },
+    "browser": {
+        "./dist/index.node.cjs": "./dist/index.browser.cjs",
+        "./dist/index.node.js": "./dist/index.browser.js"
+    },
+    "main": "./dist/index.node.cjs",
+    "module": "./dist/index.node.js",
+    "react-native": "./dist/index.react-native.js",
+    "types": "./dist/types/index.d.ts",
+    "type": "module",
+    "files": [
+        "./dist/types",
+        "./dist/index.*"
+    ],
+    "sideEffects": false,
+    "keywords": [
+        "solana",
+        "framework",
+        "standard",
+        "specifications"
+    ],
+    "scripts": {
+        "build": "rimraf dist && pnpm build:src && pnpm build:types",
+        "build:src": "zx ../../node_modules/@kinobi-so/internals/scripts/build-src.mjs package",
+        "build:types": "zx ../../node_modules/@kinobi-so/internals/scripts/build-types.mjs",
+        "dev": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs browser --watch",
+        "lint": "zx ../../node_modules/@kinobi-so/internals/scripts/lint.mjs",
+        "lint:fix": "zx ../../node_modules/@kinobi-so/internals/scripts/lint.mjs --fix",
+        "prepublishOnly": "pnpm build",
+        "test": "pnpm test:types && pnpm test:treeshakability && pnpm test:browser && pnpm test:node && pnpm test:react-native",
+        "test:browser": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs browser",
+        "test:node": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs node",
+        "test:react-native": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs react-native",
+        "test:treeshakability": "zx ../../node_modules/@kinobi-so/internals/scripts/test-treeshakability.mjs",
+        "test:types": "zx ../../node_modules/@kinobi-so/internals/scripts/test-types.mjs"
+    },
+    "dependencies": {
+        "@kinobi-so/nodes": "workspace:*",
+        "json-stable-stringify": "^1.1.1"
+    },
+    "devDependencies": {
+        "@types/json-stable-stringify": "^1.0.36"
+    },
+    "license": "MIT",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/kinobi-so/kinobi"
+    },
+    "bugs": {
+        "url": "http://github.com/kinobi-so/kinobi/issues"
+    },
+    "browserslist": [
+        "supports bigint and not dead",
+        "maintained node versions"
+    ]
+}

+ 87 - 0
packages/visitors-core/src/LinkableDictionary.ts

@@ -0,0 +1,87 @@
+import {
+    AccountLinkNode,
+    AccountNode,
+    DefinedTypeLinkNode,
+    DefinedTypeNode,
+    isNode,
+    LinkNode,
+    PdaLinkNode,
+    PdaNode,
+    ProgramLinkNode,
+    ProgramNode,
+} from '@kinobi-so/nodes';
+
+export type LinkableNode = AccountNode | DefinedTypeNode | PdaNode | ProgramNode;
+
+export class LinkableDictionary {
+    private readonly programs: Map<string, ProgramNode> = new Map();
+
+    private readonly pdas: Map<string, PdaNode> = new Map();
+
+    private readonly accounts: Map<string, AccountNode> = new Map();
+
+    private readonly definedTypes: Map<string, DefinedTypeNode> = new Map();
+
+    record(node: LinkableNode): this {
+        if (isNode(node, 'programNode')) {
+            this.programs.set(node.name, node);
+        }
+        if (isNode(node, 'pdaNode')) {
+            this.pdas.set(node.name, node);
+        }
+        if (isNode(node, 'accountNode')) {
+            this.accounts.set(node.name, node);
+        }
+        if (isNode(node, 'definedTypeNode')) {
+            this.definedTypes.set(node.name, node);
+        }
+        return this;
+    }
+
+    recordAll(nodes: LinkableNode[]): this {
+        nodes.forEach(node => this.record(node));
+        return this;
+    }
+
+    get(linkNode: ProgramLinkNode): ProgramNode | undefined;
+    get(linkNode: PdaLinkNode): PdaNode | undefined;
+    get(linkNode: AccountLinkNode): AccountNode | undefined;
+    get(linkNode: DefinedTypeLinkNode): DefinedTypeNode | undefined;
+    get(linkNode: LinkNode): LinkableNode | undefined {
+        if (linkNode.importFrom) {
+            return undefined;
+        }
+        if (isNode(linkNode, 'programLinkNode')) {
+            return this.programs.get(linkNode.name);
+        }
+        if (isNode(linkNode, 'pdaLinkNode')) {
+            return this.pdas.get(linkNode.name);
+        }
+        if (isNode(linkNode, 'accountLinkNode')) {
+            return this.accounts.get(linkNode.name);
+        }
+        if (isNode(linkNode, 'definedTypeLinkNode')) {
+            return this.definedTypes.get(linkNode.name);
+        }
+        return undefined;
+    }
+
+    has(linkNode: LinkNode): boolean {
+        if (linkNode.importFrom) {
+            return false;
+        }
+        if (isNode(linkNode, 'programLinkNode')) {
+            return this.programs.has(linkNode.name);
+        }
+        if (isNode(linkNode, 'pdaLinkNode')) {
+            return this.pdas.has(linkNode.name);
+        }
+        if (isNode(linkNode, 'accountLinkNode')) {
+            return this.accounts.has(linkNode.name);
+        }
+        if (isNode(linkNode, 'definedTypeLinkNode')) {
+            return this.definedTypes.has(linkNode.name);
+        }
+        return false;
+    }
+}

+ 63 - 0
packages/visitors-core/src/NodeSelector.ts

@@ -0,0 +1,63 @@
+import { camelCase, CamelCaseString, Node } from '@kinobi-so/nodes';
+
+import type { NodeStack } from './NodeStack';
+
+export type NodeSelector = NodeSelectorFunction | NodeSelectorPath;
+
+/**
+ * A string that can be used to select a node in a Kinobi tree.
+ * - `*` matches any node.
+ * - `someText` matches the name of a node, if any.
+ * - `[someNode]` matches a node of the given kind.
+ * - `[someNode|someOtherNode]` matches a node with any of the given kind.
+ * - `[someNode]someText` matches both the kind and the name of a node.
+ * - `a.b.c` matches a node `c` such that its parent stack contains `a` and `b` in order (but not necessarily subsequent).
+ */
+export type NodeSelectorPath = string;
+
+export type NodeSelectorFunction = (node: Node, stack: NodeStack) => boolean;
+
+export const getNodeSelectorFunction = (selector: NodeSelector): NodeSelectorFunction => {
+    if (typeof selector === 'function') return selector;
+
+    const checkNode = (node: Node, nodeSelector: string): boolean => {
+        if (nodeSelector === '*') return true;
+        const matches = nodeSelector.match(/^(?:\[([^\]]+)\])?(.*)?$/);
+        if (!matches) return false;
+        const [, kinds, name] = matches;
+
+        // Check kinds.
+        const kindArray = kinds ? kinds.split('|').map(camelCase) : [];
+        if (kindArray.length > 0 && !kindArray.includes(node.kind as CamelCaseString)) {
+            return false;
+        }
+
+        // Check names.
+        if (name && (!('name' in node) || camelCase(name) !== node.name)) {
+            return false;
+        }
+
+        return true;
+    };
+
+    const checkStack = (nodeStack: Node[], nodeSelectors: string[]): boolean => {
+        if (nodeSelectors.length === 0) return true;
+        if (nodeStack.length === 0) return false;
+        const lastNode = nodeStack.pop() as Node;
+        const lastNodeSelector = nodeSelectors.pop() as string;
+        return checkNode(lastNode, lastNodeSelector)
+            ? checkStack(nodeStack, nodeSelectors)
+            : checkStack(nodeStack, [...nodeSelectors, lastNodeSelector]);
+    };
+
+    const nodeSelectors = selector.split('.');
+    const lastNodeSelector = nodeSelectors.pop() as string;
+
+    return (node, stack) => checkNode(node, lastNodeSelector) && checkStack(stack.all(), [...nodeSelectors]);
+};
+
+export const getConjunctiveNodeSelectorFunction = (selector: NodeSelector | NodeSelector[]): NodeSelectorFunction => {
+    const selectors = Array.isArray(selector) ? selector : [selector];
+    const selectorFunctions = selectors.map(getNodeSelectorFunction);
+    return (node, stack) => selectorFunctions.every(fn => fn(node, stack));
+};

+ 59 - 0
packages/visitors-core/src/NodeStack.ts

@@ -0,0 +1,59 @@
+import { camelCase, isNodeFilter, Node, ProgramNode } from '@kinobi-so/nodes';
+
+export class NodeStack {
+    private readonly stack: Node[];
+
+    constructor(stack: Node[] = []) {
+        this.stack = [...stack];
+    }
+
+    public push(node: Node): void {
+        this.stack.push(node);
+    }
+
+    public pop(): Node | undefined {
+        return this.stack.pop();
+    }
+
+    public peek(): Node | undefined {
+        return this.isEmpty() ? undefined : this.stack[this.stack.length - 1];
+    }
+
+    public getProgram(): ProgramNode | undefined {
+        return this.stack.find(isNodeFilter('programNode'));
+    }
+
+    public all(): Node[] {
+        return [...this.stack];
+    }
+
+    public isEmpty(): boolean {
+        return this.stack.length === 0;
+    }
+
+    public clone(): NodeStack {
+        return new NodeStack(this.stack);
+    }
+
+    public toString(): string {
+        return this.toStringArray().join(' > ');
+    }
+
+    public toStringArray(): string[] {
+        return this.stack.map((node): string => {
+            return 'name' in node ? `[${node.kind}]${node.name}` : `[${node.kind}]`;
+        });
+    }
+
+    public matchesWithNames(names: string[]): boolean {
+        const remainingNames = [...names].map(camelCase);
+        this.stack.forEach(node => {
+            const nodeName = (node as { name?: string }).name;
+            if (nodeName && remainingNames.length > 0 && remainingNames[0] === camelCase(nodeName)) {
+                remainingNames.shift();
+            }
+        });
+
+        return remainingNames.length === 0;
+    }
+}

+ 42 - 0
packages/visitors-core/src/bottomUpTransformerVisitor.ts

@@ -0,0 +1,42 @@
+import { Node, NodeKind } from '@kinobi-so/nodes';
+
+import { identityVisitor } from './identityVisitor';
+import { interceptVisitor } from './interceptVisitor';
+import { getConjunctiveNodeSelectorFunction, NodeSelector } from './NodeSelector';
+import { NodeStack } from './NodeStack';
+import { pipe } from './pipe';
+import { recordNodeStackVisitor } from './recordNodeStackVisitor';
+import { Visitor } from './visitor';
+
+export type BottomUpNodeTransformer<TNode extends Node = Node> = (node: TNode, stack: NodeStack) => Node | null;
+
+export type BottomUpNodeTransformerWithSelector<TNode extends Node = Node> = {
+    select: NodeSelector | NodeSelector[];
+    transform: BottomUpNodeTransformer<TNode>;
+};
+
+export function bottomUpTransformerVisitor<TNodeKind extends NodeKind = NodeKind>(
+    transformers: (BottomUpNodeTransformer | BottomUpNodeTransformerWithSelector)[],
+    nodeKeys?: TNodeKind[],
+): Visitor<Node | null, TNodeKind> {
+    const transformerFunctions = transformers.map((transformer): BottomUpNodeTransformer => {
+        if (typeof transformer === 'function') return transformer;
+        return (node, stack) =>
+            getConjunctiveNodeSelectorFunction(transformer.select)(node, stack)
+                ? transformer.transform(node, stack)
+                : node;
+    });
+
+    const stack = new NodeStack();
+    return pipe(
+        identityVisitor(nodeKeys),
+        v => recordNodeStackVisitor(v, stack),
+        v =>
+            interceptVisitor(v, (node, next) =>
+                transformerFunctions.reduce(
+                    (acc, transformer) => (acc === null ? null : transformer(acc, stack.clone())),
+                    next(node),
+                ),
+            ),
+    );
+}

+ 11 - 0
packages/visitors-core/src/consoleLogVisitor.ts

@@ -0,0 +1,11 @@
+import { NodeKind } from '@kinobi-so/nodes';
+
+import { mapVisitor } from './mapVisitor';
+import { Visitor } from './visitor';
+
+export function consoleLogVisitor<TNodeKind extends NodeKind = NodeKind>(
+    visitor: Visitor<string, TNodeKind>,
+): Visitor<void, TNodeKind> {
+    // eslint-disable-next-line no-console
+    return mapVisitor(visitor, value => console.log(value));
+}

+ 19 - 0
packages/visitors-core/src/deleteNodesVisitor.ts

@@ -0,0 +1,19 @@
+import { NodeKind } from '@kinobi-so/nodes';
+
+import { NodeSelector } from './NodeSelector';
+import { TopDownNodeTransformerWithSelector, topDownTransformerVisitor } from './topDownTransformerVisitor';
+
+export function deleteNodesVisitor<TNodeKind extends NodeKind = NodeKind>(
+    selectors: NodeSelector[],
+    nodeKeys?: TNodeKind[],
+) {
+    return topDownTransformerVisitor<TNodeKind>(
+        selectors.map(
+            (selector): TopDownNodeTransformerWithSelector => ({
+                select: selector,
+                transform: () => null,
+            }),
+        ),
+        nodeKeys,
+    );
+}

+ 64 - 0
packages/visitors-core/src/extendVisitor.ts

@@ -0,0 +1,64 @@
+import { GetNodeFromKind, Node, NodeKind, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+import { getVisitFunctionName, GetVisitorFunctionName, Visitor } from './visitor';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type DontInfer<T> = T extends any ? T : never;
+
+export type VisitorOverrideFunction<TReturn, TNodeKind extends NodeKind, TNode extends Node> = (
+    node: TNode,
+    scope: {
+        next: (node: TNode) => TReturn;
+        self: Visitor<TReturn, TNodeKind>;
+    },
+) => TReturn;
+
+export type VisitorOverrides<TReturn, TNodeKind extends NodeKind> = {
+    [K in TNodeKind as GetVisitorFunctionName<K>]?: VisitorOverrideFunction<TReturn, TNodeKind, GetNodeFromKind<K>>;
+};
+
+export function extendVisitor<TReturn, TNodeKind extends NodeKind>(
+    visitor: Visitor<TReturn, TNodeKind>,
+    overrides: DontInfer<VisitorOverrides<TReturn, TNodeKind>>,
+): Visitor<TReturn, TNodeKind> {
+    const registeredVisitFunctions = REGISTERED_NODE_KINDS.map(getVisitFunctionName);
+
+    const overriddenFunctions = Object.fromEntries(
+        Object.keys(overrides).flatMap(key => {
+            if (!(registeredVisitFunctions as string[]).includes(key)) {
+                return [];
+            }
+
+            const castedKey = key as GetVisitorFunctionName<TNodeKind>;
+
+            if (!visitor[castedKey]) {
+                throw new Error(
+                    `Cannot extend visitor with function "${castedKey}" as the base visitor does not support it.`,
+                );
+            }
+
+            return [
+                [
+                    castedKey,
+                    function extendedVisitNode<TNode extends Node>(this: Visitor<TReturn, TNodeKind>, node: TNode) {
+                        const extendedFunction = overrides[castedKey] as VisitorOverrideFunction<
+                            TReturn,
+                            TNodeKind,
+                            TNode
+                        >;
+                        const nextFunction = visitor[castedKey] as unknown as (node: TNode) => TReturn;
+                        return extendedFunction.bind(this)(node, {
+                            next: nextFunction.bind(this),
+                            self: this,
+                        });
+                    },
+                ],
+            ];
+        }),
+    ) as Partial<Visitor<TReturn, TNodeKind>>;
+
+    return {
+        ...visitor,
+        ...overriddenFunctions,
+    };
+}

+ 110 - 0
packages/visitors-core/src/getDebugStringVisitor.ts

@@ -0,0 +1,110 @@
+import { Node } from '@kinobi-so/nodes';
+
+import { interceptVisitor } from './interceptVisitor';
+import { mergeVisitor } from './mergeVisitor';
+import { pipe } from './pipe';
+import { Visitor } from './visitor';
+
+export function getDebugStringVisitor(options: { indent?: boolean; indentSeparator?: string } = {}): Visitor<string> {
+    const indent = options.indent ?? false;
+    const indentSeparator = options.indentSeparator ?? '|   ';
+    let stackLevel = -1;
+
+    return pipe(
+        mergeVisitor<string>(
+            node => {
+                const details = getNodeDetails(node).join('.');
+                if (indent) {
+                    return `${indentSeparator.repeat(stackLevel)}${node.kind}${details ? ` [${details}]` : ''}`;
+                }
+                return `${node.kind}${details ? `[${details}]` : ''}`;
+            },
+            (node, values) => {
+                const details = getNodeDetails(node).join('.');
+                if (indent) {
+                    return [
+                        `${indentSeparator.repeat(stackLevel)}${node.kind}${details ? ` [${details}]` : ''}`,
+                        ...values,
+                    ].join('\n');
+                }
+                return `${node.kind}${details ? `[${details}]` : ''}(${values.join(', ')})`;
+            },
+        ),
+        v =>
+            interceptVisitor(v, (node, next) => {
+                stackLevel += 1;
+                const newNode = next(node);
+                stackLevel -= 1;
+                return newNode;
+            }),
+    );
+}
+
+function getNodeDetails(node: Node): string[] {
+    switch (node.kind) {
+        case 'programNode':
+            return [node.name, node.publicKey];
+        case 'instructionAccountNode':
+            return [
+                node.name,
+                ...(node.isWritable ? ['writable'] : []),
+                ...(node.isSigner === true ? ['signer'] : []),
+                ...(node.isSigner === 'either' ? ['optionalSigner'] : []),
+                ...(node.isOptional ? ['optional'] : []),
+            ];
+        case 'instructionRemainingAccountsNode':
+            return [
+                ...(node.isOptional ? ['optional'] : []),
+                ...(node.isWritable ? ['writable'] : []),
+                ...(node.isSigner === true ? ['signer'] : []),
+                ...(node.isSigner === 'either' ? ['optionalSigner'] : []),
+            ];
+        case 'instructionByteDeltaNode':
+            return [...(node.subtract ? ['subtract'] : []), ...(node.withHeader ? ['withHeader'] : [])];
+        case 'errorNode':
+            return [node.code.toString(), node.name];
+        case 'programLinkNode':
+        case 'pdaLinkNode':
+        case 'accountLinkNode':
+        case 'definedTypeLinkNode':
+            return [node.name, ...(node.importFrom ? [`from:${node.importFrom}`] : [])];
+        case 'numberTypeNode':
+            return [node.format, ...(node.endian === 'be' ? ['bigEndian'] : [])];
+        case 'amountTypeNode':
+            return [node.decimals.toString(), ...(node.unit ? [node.unit] : [])];
+        case 'stringTypeNode':
+            return [node.encoding];
+        case 'optionTypeNode':
+            return node.fixed ? ['fixed'] : [];
+        case 'fixedCountNode':
+            return [node.value.toString()];
+        case 'numberValueNode':
+            return [node.number.toString()];
+        case 'stringValueNode':
+            return [node.string];
+        case 'booleanValueNode':
+            return [node.boolean ? 'true' : 'false'];
+        case 'bytesValueNode':
+            return [node.encoding, node.data];
+        case 'publicKeyValueNode':
+            return [...(node.identifier ? [`${node.identifier}`] : []), node.publicKey];
+        case 'enumValueNode':
+            return [node.variant];
+        case 'resolverValueNode':
+            return [node.name, ...(node.importFrom ? [`from:${node.importFrom}`] : [])];
+        case 'constantDiscriminatorNode':
+            return [...(node.offset > 0 ? [`offset:${node.offset}`] : [])];
+        case 'fieldDiscriminatorNode':
+            return [node.name, ...(node.offset > 0 ? [`offset:${node.offset}`] : [])];
+        case 'sizeDiscriminatorNode':
+            return [node.size.toString()];
+        case 'fixedSizeTypeNode':
+            return [node.size.toString()];
+        case 'preOffsetTypeNode':
+            return [node.offset.toString(), node.strategy ?? 'relative'];
+        case 'postOffsetTypeNode':
+            return [node.offset.toString(), node.strategy ?? 'relative'];
+        default:
+            return 'name' in node ? [node.name] : [];
+    }
+}

+ 14 - 0
packages/visitors-core/src/getUniqueHashStringVisitor.ts

@@ -0,0 +1,14 @@
+import stringify from 'json-stable-stringify';
+
+import { mapVisitor } from './mapVisitor';
+import { removeDocsVisitor } from './removeDocsVisitor';
+import { staticVisitor } from './staticVisitor';
+import { Visitor } from './visitor';
+
+export function getUniqueHashStringVisitor(options: { removeDocs?: boolean } = {}): Visitor<string> {
+    const removeDocs = options.removeDocs ?? false;
+    if (!removeDocs) {
+        return staticVisitor(node => stringify(node));
+    }
+    return mapVisitor(removeDocsVisitor(), node => stringify(node));
+}

+ 618 - 0
packages/visitors-core/src/identityVisitor.ts

@@ -0,0 +1,618 @@
+import {
+    accountNode,
+    amountTypeNode,
+    arrayTypeNode,
+    arrayValueNode,
+    assertIsNestedTypeNode,
+    assertIsNode,
+    booleanTypeNode,
+    conditionalValueNode,
+    constantDiscriminatorNode,
+    constantPdaSeedNode,
+    constantValueNode,
+    COUNT_NODES,
+    dateTimeTypeNode,
+    definedTypeNode,
+    DISCRIMINATOR_NODES,
+    ENUM_VARIANT_TYPE_NODES,
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTupleVariantTypeNode,
+    enumTypeNode,
+    enumValueNode,
+    fixedSizeTypeNode,
+    hiddenPrefixTypeNode,
+    hiddenSuffixTypeNode,
+    INSTRUCTION_INPUT_VALUE_NODES,
+    instructionAccountNode,
+    instructionArgumentNode,
+    instructionByteDeltaNode,
+    instructionNode,
+    instructionRemainingAccountsNode,
+    mapEntryValueNode,
+    mapTypeNode,
+    mapValueNode,
+    Node,
+    NodeKind,
+    optionTypeNode,
+    PDA_SEED_NODES,
+    pdaNode,
+    pdaSeedValueNode,
+    pdaValueNode,
+    postOffsetTypeNode,
+    prefixedCountNode,
+    preOffsetTypeNode,
+    programNode,
+    REGISTERED_NODE_KINDS,
+    removeNullAndAssertIsNodeFilter,
+    resolverValueNode,
+    rootNode,
+    sentinelTypeNode,
+    setTypeNode,
+    setValueNode,
+    sizePrefixTypeNode,
+    solAmountTypeNode,
+    someValueNode,
+    structFieldTypeNode,
+    structFieldValueNode,
+    structTypeNode,
+    structValueNode,
+    tupleTypeNode,
+    tupleValueNode,
+    TYPE_NODES,
+    VALUE_NODES,
+    variablePdaSeedNode,
+    zeroableOptionTypeNode,
+} from '@kinobi-so/nodes';
+
+import { staticVisitor } from './staticVisitor';
+import { visit as baseVisit, Visitor } from './visitor';
+
+export function identityVisitor<TNodeKind extends NodeKind = NodeKind>(
+    nodeKeys: TNodeKind[] = REGISTERED_NODE_KINDS as TNodeKind[],
+): Visitor<Node | null, TNodeKind> {
+    const castedNodeKeys: NodeKind[] = nodeKeys;
+    const visitor = staticVisitor(node => Object.freeze({ ...node }), castedNodeKeys) as Visitor<Node | null>;
+    const visit =
+        (v: Visitor<Node | null>) =>
+        (node: Node): Node | null =>
+            castedNodeKeys.includes(node.kind) ? baseVisit(node, v) : Object.freeze({ ...node });
+
+    if (castedNodeKeys.includes('rootNode')) {
+        visitor.visitRoot = function visitRoot(node) {
+            const program = visit(this)(node.program);
+            if (program === null) return null;
+            assertIsNode(program, 'programNode');
+            return rootNode(
+                program,
+                node.additionalPrograms.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('programNode')),
+            );
+        };
+    }
+
+    if (castedNodeKeys.includes('programNode')) {
+        visitor.visitProgram = function visitProgram(node) {
+            return programNode({
+                ...node,
+                accounts: node.accounts.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('accountNode')),
+                definedTypes: node.definedTypes
+                    .map(visit(this))
+                    .filter(removeNullAndAssertIsNodeFilter('definedTypeNode')),
+                errors: node.errors.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('errorNode')),
+                instructions: node.instructions
+                    .map(visit(this))
+                    .filter(removeNullAndAssertIsNodeFilter('instructionNode')),
+                pdas: node.pdas.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('pdaNode')),
+            });
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaNode')) {
+        visitor.visitPda = function visitPda(node) {
+            return pdaNode({
+                ...node,
+                seeds: node.seeds.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(PDA_SEED_NODES)),
+            });
+        };
+    }
+
+    if (castedNodeKeys.includes('accountNode')) {
+        visitor.visitAccount = function visitAccount(node) {
+            const data = visit(this)(node.data);
+            if (data === null) return null;
+            assertIsNode(data, 'structTypeNode');
+            const pda = node.pda ? visit(this)(node.pda) ?? undefined : undefined;
+            if (pda) assertIsNode(pda, 'pdaLinkNode');
+            return accountNode({ ...node, data, pda });
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionNode')) {
+        visitor.visitInstruction = function visitInstruction(node) {
+            return instructionNode({
+                ...node,
+                accounts: node.accounts
+                    .map(visit(this))
+                    .filter(removeNullAndAssertIsNodeFilter('instructionAccountNode')),
+                arguments: node.arguments
+                    .map(visit(this))
+                    .filter(removeNullAndAssertIsNodeFilter('instructionArgumentNode')),
+                byteDeltas: node.byteDeltas
+                    ? node.byteDeltas
+                          .map(visit(this))
+                          .filter(removeNullAndAssertIsNodeFilter('instructionByteDeltaNode'))
+                    : undefined,
+                discriminators: node.discriminators
+                    ? node.discriminators.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(DISCRIMINATOR_NODES))
+                    : undefined,
+                extraArguments: node.extraArguments
+                    ? node.extraArguments
+                          .map(visit(this))
+                          .filter(removeNullAndAssertIsNodeFilter('instructionArgumentNode'))
+                    : undefined,
+                remainingAccounts: node.remainingAccounts
+                    ? node.remainingAccounts
+                          .map(visit(this))
+                          .filter(removeNullAndAssertIsNodeFilter('instructionRemainingAccountsNode'))
+                    : undefined,
+                subInstructions: node.subInstructions
+                    ? node.subInstructions.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('instructionNode'))
+                    : undefined,
+            });
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionAccountNode')) {
+        visitor.visitInstructionAccount = function visitInstructionAccount(node) {
+            const defaultValue = node.defaultValue ? visit(this)(node.defaultValue) ?? undefined : undefined;
+            if (defaultValue) assertIsNode(defaultValue, INSTRUCTION_INPUT_VALUE_NODES);
+            return instructionAccountNode({ ...node, defaultValue });
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionArgumentNode')) {
+        visitor.visitInstructionArgument = function visitInstructionArgument(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            const defaultValue = node.defaultValue ? visit(this)(node.defaultValue) ?? undefined : undefined;
+            if (defaultValue) assertIsNode(defaultValue, INSTRUCTION_INPUT_VALUE_NODES);
+            return instructionArgumentNode({ ...node, defaultValue, type });
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionRemainingAccountsNode')) {
+        visitor.visitInstructionRemainingAccounts = function visitInstructionRemainingAccounts(node) {
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, ['argumentValueNode', 'resolverValueNode']);
+            return instructionRemainingAccountsNode(value, { ...node });
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionByteDeltaNode')) {
+        visitor.visitInstructionByteDelta = function visitInstructionByteDelta(node) {
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, ['numberValueNode', 'accountLinkNode', 'argumentValueNode', 'resolverValueNode']);
+            return instructionByteDeltaNode(value, { ...node });
+        };
+    }
+
+    if (castedNodeKeys.includes('definedTypeNode')) {
+        visitor.visitDefinedType = function visitDefinedType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return definedTypeNode({ ...node, type });
+        };
+    }
+
+    if (castedNodeKeys.includes('arrayTypeNode')) {
+        visitor.visitArrayType = function visitArrayType(node) {
+            const size = visit(this)(node.count);
+            if (size === null) return null;
+            assertIsNode(size, COUNT_NODES);
+            const item = visit(this)(node.item);
+            if (item === null) return null;
+            assertIsNode(item, TYPE_NODES);
+            return arrayTypeNode(item, size);
+        };
+    }
+
+    if (castedNodeKeys.includes('enumTypeNode')) {
+        visitor.visitEnumType = function visitEnumType(node) {
+            return enumTypeNode(
+                node.variants.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(ENUM_VARIANT_TYPE_NODES)),
+                { size: node.size },
+            );
+        };
+    }
+
+    if (castedNodeKeys.includes('enumStructVariantTypeNode')) {
+        visitor.visitEnumStructVariantType = function visitEnumStructVariantType(node) {
+            const newStruct = visit(this)(node.struct);
+            if (!newStruct) {
+                return enumEmptyVariantTypeNode(node.name);
+            }
+            assertIsNode(newStruct, 'structTypeNode');
+            if (newStruct.fields.length === 0) {
+                return enumEmptyVariantTypeNode(node.name);
+            }
+            return enumStructVariantTypeNode(node.name, newStruct);
+        };
+    }
+
+    if (castedNodeKeys.includes('enumTupleVariantTypeNode')) {
+        visitor.visitEnumTupleVariantType = function visitEnumTupleVariantType(node) {
+            const newTuple = visit(this)(node.tuple);
+            if (!newTuple) {
+                return enumEmptyVariantTypeNode(node.name);
+            }
+            assertIsNode(newTuple, 'tupleTypeNode');
+            if (newTuple.items.length === 0) {
+                return enumEmptyVariantTypeNode(node.name);
+            }
+            return enumTupleVariantTypeNode(node.name, newTuple);
+        };
+    }
+
+    if (castedNodeKeys.includes('mapTypeNode')) {
+        visitor.visitMapType = function visitMapType(node) {
+            const size = visit(this)(node.count);
+            if (size === null) return null;
+            assertIsNode(size, COUNT_NODES);
+            const key = visit(this)(node.key);
+            if (key === null) return null;
+            assertIsNode(key, TYPE_NODES);
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, TYPE_NODES);
+            return mapTypeNode(key, value, size);
+        };
+    }
+
+    if (castedNodeKeys.includes('optionTypeNode')) {
+        visitor.visitOptionType = function visitOptionType(node) {
+            const prefix = visit(this)(node.prefix);
+            if (prefix === null) return null;
+            assertIsNestedTypeNode(prefix, 'numberTypeNode');
+            const item = visit(this)(node.item);
+            if (item === null) return null;
+            assertIsNode(item, TYPE_NODES);
+            return optionTypeNode(item, { ...node, prefix });
+        };
+    }
+
+    if (castedNodeKeys.includes('zeroableOptionTypeNode')) {
+        visitor.visitZeroableOptionType = function visitZeroableOptionType(node) {
+            const item = visit(this)(node.item);
+            if (item === null) return null;
+            assertIsNode(item, TYPE_NODES);
+            const zeroValue = node.zeroValue ? visit(this)(node.zeroValue) ?? undefined : undefined;
+            if (zeroValue) assertIsNode(zeroValue, 'constantValueNode');
+            return zeroableOptionTypeNode(item, zeroValue);
+        };
+    }
+
+    if (castedNodeKeys.includes('booleanTypeNode')) {
+        visitor.visitBooleanType = function visitBooleanType(node) {
+            const size = visit(this)(node.size);
+            if (size === null) return null;
+            assertIsNestedTypeNode(size, 'numberTypeNode');
+            return booleanTypeNode(size);
+        };
+    }
+
+    if (castedNodeKeys.includes('setTypeNode')) {
+        visitor.visitSetType = function visitSetType(node) {
+            const size = visit(this)(node.count);
+            if (size === null) return null;
+            assertIsNode(size, COUNT_NODES);
+            const item = visit(this)(node.item);
+            if (item === null) return null;
+            assertIsNode(item, TYPE_NODES);
+            return setTypeNode(item, size);
+        };
+    }
+
+    if (castedNodeKeys.includes('structTypeNode')) {
+        visitor.visitStructType = function visitStructType(node) {
+            const fields = node.fields.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('structFieldTypeNode'));
+            return structTypeNode(fields);
+        };
+    }
+
+    if (castedNodeKeys.includes('structFieldTypeNode')) {
+        visitor.visitStructFieldType = function visitStructFieldType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            const defaultValue = node.defaultValue ? visit(this)(node.defaultValue) ?? undefined : undefined;
+            if (defaultValue) assertIsNode(defaultValue, VALUE_NODES);
+            return structFieldTypeNode({ ...node, defaultValue, type });
+        };
+    }
+
+    if (castedNodeKeys.includes('tupleTypeNode')) {
+        visitor.visitTupleType = function visitTupleType(node) {
+            const items = node.items.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(TYPE_NODES));
+            return tupleTypeNode(items);
+        };
+    }
+
+    if (castedNodeKeys.includes('amountTypeNode')) {
+        visitor.visitAmountType = function visitAmountType(node) {
+            const number = visit(this)(node.number);
+            if (number === null) return null;
+            assertIsNestedTypeNode(number, 'numberTypeNode');
+            return amountTypeNode(number, node.decimals, node.unit);
+        };
+    }
+
+    if (castedNodeKeys.includes('dateTimeTypeNode')) {
+        visitor.visitDateTimeType = function visitDateTimeType(node) {
+            const number = visit(this)(node.number);
+            if (number === null) return null;
+            assertIsNestedTypeNode(number, 'numberTypeNode');
+            return dateTimeTypeNode(number);
+        };
+    }
+
+    if (castedNodeKeys.includes('solAmountTypeNode')) {
+        visitor.visitSolAmountType = function visitSolAmountType(node) {
+            const number = visit(this)(node.number);
+            if (number === null) return null;
+            assertIsNestedTypeNode(number, 'numberTypeNode');
+            return solAmountTypeNode(number);
+        };
+    }
+
+    if (castedNodeKeys.includes('prefixedCountNode')) {
+        visitor.visitPrefixedCount = function visitPrefixedCount(node) {
+            const prefix = visit(this)(node.prefix);
+            if (prefix === null) return null;
+            assertIsNestedTypeNode(prefix, 'numberTypeNode');
+            return prefixedCountNode(prefix);
+        };
+    }
+
+    if (castedNodeKeys.includes('arrayValueNode')) {
+        visitor.visitArrayValue = function visitArrayValue(node) {
+            return arrayValueNode(node.items.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(VALUE_NODES)));
+        };
+    }
+
+    if (castedNodeKeys.includes('constantValueNode')) {
+        visitor.visitConstantValue = function visitConstantValue(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, VALUE_NODES);
+            return constantValueNode(type, value);
+        };
+    }
+
+    if (castedNodeKeys.includes('enumValueNode')) {
+        visitor.visitEnumValue = function visitEnumValue(node) {
+            const enumLink = visit(this)(node.enum);
+            if (enumLink === null) return null;
+            assertIsNode(enumLink, ['definedTypeLinkNode']);
+            const value = node.value ? visit(this)(node.value) ?? undefined : undefined;
+            if (value) assertIsNode(value, ['structValueNode', 'tupleValueNode']);
+            return enumValueNode(enumLink, node.variant, value);
+        };
+    }
+
+    if (castedNodeKeys.includes('mapValueNode')) {
+        visitor.visitMapValue = function visitMapValue(node) {
+            return mapValueNode(
+                node.entries.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('mapEntryValueNode')),
+            );
+        };
+    }
+
+    if (castedNodeKeys.includes('mapEntryValueNode')) {
+        visitor.visitMapEntryValue = function visitMapEntryValue(node) {
+            const key = visit(this)(node.key);
+            if (key === null) return null;
+            assertIsNode(key, VALUE_NODES);
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, VALUE_NODES);
+            return mapEntryValueNode(key, value);
+        };
+    }
+
+    if (castedNodeKeys.includes('setValueNode')) {
+        visitor.visitSetValue = function visitSetValue(node) {
+            return setValueNode(node.items.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(VALUE_NODES)));
+        };
+    }
+
+    if (castedNodeKeys.includes('someValueNode')) {
+        visitor.visitSomeValue = function visitSomeValue(node) {
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, VALUE_NODES);
+            return someValueNode(value);
+        };
+    }
+
+    if (castedNodeKeys.includes('structValueNode')) {
+        visitor.visitStructValue = function visitStructValue(node) {
+            return structValueNode(
+                node.fields.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('structFieldValueNode')),
+            );
+        };
+    }
+
+    if (castedNodeKeys.includes('structFieldValueNode')) {
+        visitor.visitStructFieldValue = function visitStructFieldValue(node) {
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, VALUE_NODES);
+            return structFieldValueNode(node.name, value);
+        };
+    }
+
+    if (castedNodeKeys.includes('tupleValueNode')) {
+        visitor.visitTupleValue = function visitTupleValue(node) {
+            return tupleValueNode(node.items.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(VALUE_NODES)));
+        };
+    }
+
+    if (castedNodeKeys.includes('constantPdaSeedNode')) {
+        visitor.visitConstantPdaSeed = function visitConstantPdaSeed(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, [...VALUE_NODES, 'programIdValueNode']);
+            return constantPdaSeedNode(type, value);
+        };
+    }
+
+    if (castedNodeKeys.includes('variablePdaSeedNode')) {
+        visitor.visitVariablePdaSeed = function visitVariablePdaSeed(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return variablePdaSeedNode(node.name, type, node.docs);
+        };
+    }
+
+    if (castedNodeKeys.includes('resolverValueNode')) {
+        visitor.visitResolverValue = function visitResolverValue(node) {
+            const dependsOn = (node.dependsOn ?? [])
+                .map(visit(this))
+                .filter(removeNullAndAssertIsNodeFilter(['accountValueNode', 'argumentValueNode']));
+            return resolverValueNode(node.name, {
+                ...node,
+                dependsOn: dependsOn.length === 0 ? undefined : dependsOn,
+            });
+        };
+    }
+
+    if (castedNodeKeys.includes('conditionalValueNode')) {
+        visitor.visitConditionalValue = function visitConditionalValue(node) {
+            const condition = visit(this)(node.condition);
+            if (condition === null) return null;
+            assertIsNode(condition, ['resolverValueNode', 'accountValueNode', 'argumentValueNode']);
+            const value = node.value ? visit(this)(node.value) ?? undefined : undefined;
+            if (value) assertIsNode(value, VALUE_NODES);
+            const ifTrue = node.ifTrue ? visit(this)(node.ifTrue) ?? undefined : undefined;
+            if (ifTrue) assertIsNode(ifTrue, INSTRUCTION_INPUT_VALUE_NODES);
+            const ifFalse = node.ifFalse ? visit(this)(node.ifFalse) ?? undefined : undefined;
+            if (ifFalse) assertIsNode(ifFalse, INSTRUCTION_INPUT_VALUE_NODES);
+            if (!ifTrue && !ifFalse) return null;
+            return conditionalValueNode({ condition, ifFalse, ifTrue, value });
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaValueNode')) {
+        visitor.visitPdaValue = function visitPdaValue(node) {
+            const pda = visit(this)(node.pda);
+            if (pda === null) return null;
+            assertIsNode(pda, 'pdaLinkNode');
+            const seeds = node.seeds.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('pdaSeedValueNode'));
+            return pdaValueNode(pda, seeds);
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaSeedValueNode')) {
+        visitor.visitPdaSeedValue = function visitPdaSeedValue(node) {
+            const value = visit(this)(node.value);
+            if (value === null) return null;
+            assertIsNode(value, [...VALUE_NODES, 'accountValueNode', 'argumentValueNode']);
+            return pdaSeedValueNode(node.name, value);
+        };
+    }
+
+    if (castedNodeKeys.includes('fixedSizeTypeNode')) {
+        visitor.visitFixedSizeType = function visitFixedSizeType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return fixedSizeTypeNode(type, node.size);
+        };
+    }
+
+    if (castedNodeKeys.includes('sizePrefixTypeNode')) {
+        visitor.visitSizePrefixType = function visitSizePrefixType(node) {
+            const prefix = visit(this)(node.prefix);
+            if (prefix === null) return null;
+            assertIsNestedTypeNode(prefix, 'numberTypeNode');
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return sizePrefixTypeNode(type, prefix);
+        };
+    }
+
+    if (castedNodeKeys.includes('preOffsetTypeNode')) {
+        visitor.visitPreOffsetType = function visitPreOffsetType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return preOffsetTypeNode(type, node.offset, node.strategy);
+        };
+    }
+
+    if (castedNodeKeys.includes('postOffsetTypeNode')) {
+        visitor.visitPostOffsetType = function visitPostOffsetType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return postOffsetTypeNode(type, node.offset, node.strategy);
+        };
+    }
+
+    if (castedNodeKeys.includes('sentinelTypeNode')) {
+        visitor.visitSentinelType = function visitSentinelType(node) {
+            const sentinel = visit(this)(node.sentinel);
+            if (sentinel === null) return null;
+            assertIsNode(sentinel, 'constantValueNode');
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            return sentinelTypeNode(type, sentinel);
+        };
+    }
+
+    if (castedNodeKeys.includes('hiddenPrefixTypeNode')) {
+        visitor.visitHiddenPrefixType = function visitHiddenPrefixType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            const prefix = node.prefix.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('constantValueNode'));
+            if (prefix.length === 0) return type;
+            return hiddenPrefixTypeNode(type, prefix);
+        };
+    }
+
+    if (castedNodeKeys.includes('hiddenSuffixTypeNode')) {
+        visitor.visitHiddenSuffixType = function visitHiddenSuffixType(node) {
+            const type = visit(this)(node.type);
+            if (type === null) return null;
+            assertIsNode(type, TYPE_NODES);
+            const suffix = node.suffix.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('constantValueNode'));
+            if (suffix.length === 0) return type;
+            return hiddenSuffixTypeNode(type, suffix);
+        };
+    }
+
+    if (castedNodeKeys.includes('constantDiscriminatorNode')) {
+        visitor.visitConstantDiscriminator = function visitConstantDiscriminator(node) {
+            const constant = visit(this)(node.constant);
+            if (constant === null) return null;
+            assertIsNode(constant, 'constantValueNode');
+            return constantDiscriminatorNode(constant, node.offset);
+        };
+    }
+
+    return visitor as Visitor<Node, TNodeKind>;
+}

+ 24 - 0
packages/visitors-core/src/index.ts

@@ -0,0 +1,24 @@
+export * from './LinkableDictionary';
+export * from './NodeSelector';
+export * from './NodeStack';
+export * from './bottomUpTransformerVisitor';
+export * from './consoleLogVisitor';
+export * from './deleteNodesVisitor';
+export * from './extendVisitor';
+export * from './getDebugStringVisitor';
+export * from './getUniqueHashStringVisitor';
+export * from './identityVisitor';
+export * from './interceptVisitor';
+export * from './mapVisitor';
+export * from './mergeVisitor';
+export * from './nonNullableIdentityVisitor';
+export * from './pipe';
+export * from './recordLinkablesVisitor';
+export * from './recordNodeStackVisitor';
+export * from './removeDocsVisitor';
+export * from './singleNodeVisitor';
+export * from './staticVisitor';
+export * from './tapVisitor';
+export * from './topDownTransformerVisitor';
+export * from './visitor';
+export * from './voidVisitor';

+ 31 - 0
packages/visitors-core/src/interceptVisitor.ts

@@ -0,0 +1,31 @@
+import { Node, NodeKind, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+import { getVisitFunctionName, GetVisitorFunctionName, Visitor } from './visitor';
+
+export type VisitorInterceptor<TReturn> = <TNode extends Node>(node: TNode, next: (node: TNode) => TReturn) => TReturn;
+
+export function interceptVisitor<TReturn, TNodeKind extends NodeKind>(
+    visitor: Visitor<TReturn, TNodeKind>,
+    interceptor: VisitorInterceptor<TReturn>,
+): Visitor<TReturn, TNodeKind> {
+    const registeredVisitFunctions = REGISTERED_NODE_KINDS.map(getVisitFunctionName);
+
+    return Object.fromEntries(
+        Object.keys(visitor).flatMap(key => {
+            const castedKey = key as GetVisitorFunctionName<TNodeKind>;
+            if (!registeredVisitFunctions.includes(castedKey)) {
+                return [];
+            }
+
+            return [
+                [
+                    castedKey,
+                    function interceptedVisitNode<TNode extends Node>(this: Visitor<TReturn, TNodeKind>, node: TNode) {
+                        const baseFunction = visitor[castedKey] as (node: TNode) => TReturn;
+                        return interceptor<TNode>(node, baseFunction.bind(this));
+                    },
+                ],
+            ];
+        }),
+    ) as Visitor<TReturn, TNodeKind>;
+}

+ 26 - 0
packages/visitors-core/src/mapVisitor.ts

@@ -0,0 +1,26 @@
+import { GetNodeFromKind, NodeKind, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+import { getVisitFunctionName, GetVisitorFunctionName, Visitor } from './visitor';
+
+export function mapVisitor<TReturnFrom, TReturnTo, TNodeKind extends NodeKind>(
+    visitor: Visitor<TReturnFrom, TNodeKind>,
+    map: (from: TReturnFrom) => TReturnTo,
+): Visitor<TReturnTo, TNodeKind> {
+    const registeredVisitFunctions = REGISTERED_NODE_KINDS.map(getVisitFunctionName);
+    return Object.fromEntries(
+        Object.keys(visitor).flatMap(key => {
+            const castedKey = key as GetVisitorFunctionName<TNodeKind>;
+            if (!registeredVisitFunctions.includes(castedKey)) {
+                return [];
+            }
+
+            return [
+                [
+                    castedKey,
+                    (node: GetNodeFromKind<TNodeKind>) =>
+                        map((visitor[castedKey] as (node: GetNodeFromKind<TNodeKind>) => TReturnFrom)(node)),
+                ],
+            ];
+        }),
+    ) as unknown as Visitor<TReturnTo, TNodeKind>;
+}

+ 348 - 0
packages/visitors-core/src/mergeVisitor.ts

@@ -0,0 +1,348 @@
+import { getAllPrograms, Node, NodeKind, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+import { staticVisitor } from './staticVisitor';
+import { visit as baseVisit, Visitor } from './visitor';
+
+export function mergeVisitor<TReturn, TNodeKind extends NodeKind = NodeKind>(
+    leafValue: (node: Node) => TReturn,
+    merge: (node: Node, values: TReturn[]) => TReturn,
+    nodeKeys: TNodeKind[] = REGISTERED_NODE_KINDS as TNodeKind[],
+): Visitor<TReturn, TNodeKind> {
+    const castedNodeKeys: NodeKind[] = nodeKeys;
+    const visitor = staticVisitor(leafValue, castedNodeKeys) as Visitor<TReturn>;
+    const visit =
+        (v: Visitor<TReturn>) =>
+        (node: Node): TReturn[] =>
+            castedNodeKeys.includes(node.kind) ? [baseVisit(node, v)] : [];
+
+    if (castedNodeKeys.includes('rootNode')) {
+        visitor.visitRoot = function visitRoot(node) {
+            return merge(node, getAllPrograms(node).flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('programNode')) {
+        visitor.visitProgram = function visitProgram(node) {
+            return merge(node, [
+                ...node.pdas.flatMap(visit(this)),
+                ...node.accounts.flatMap(visit(this)),
+                ...node.instructions.flatMap(visit(this)),
+                ...node.definedTypes.flatMap(visit(this)),
+                ...node.errors.flatMap(visit(this)),
+            ]);
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaNode')) {
+        visitor.visitPda = function visitPda(node) {
+            return merge(node, node.seeds.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('accountNode')) {
+        visitor.visitAccount = function visitAccount(node) {
+            return merge(node, [
+                ...visit(this)(node.data),
+                ...(node.pda ? visit(this)(node.pda) : []),
+                ...(node.discriminators ?? []).flatMap(visit(this)),
+            ]);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionNode')) {
+        visitor.visitInstruction = function visitInstruction(node) {
+            return merge(node, [
+                ...node.accounts.flatMap(visit(this)),
+                ...node.arguments.flatMap(visit(this)),
+                ...(node.extraArguments ?? []).flatMap(visit(this)),
+                ...(node.remainingAccounts ?? []).flatMap(visit(this)),
+                ...(node.byteDeltas ?? []).flatMap(visit(this)),
+                ...(node.discriminators ?? []).flatMap(visit(this)),
+                ...(node.subInstructions ?? []).flatMap(visit(this)),
+            ]);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionAccountNode')) {
+        visitor.visitInstructionAccount = function visitInstructionAccount(node) {
+            return merge(node, [...(node.defaultValue ? visit(this)(node.defaultValue) : [])]);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionArgumentNode')) {
+        visitor.visitInstructionArgument = function visitInstructionArgument(node) {
+            return merge(node, [
+                ...visit(this)(node.type),
+                ...(node.defaultValue ? visit(this)(node.defaultValue) : []),
+            ]);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionRemainingAccountsNode')) {
+        visitor.visitInstructionRemainingAccounts = function visitInstructionRemainingAccounts(node) {
+            return merge(node, visit(this)(node.value));
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionByteDeltaNode')) {
+        visitor.visitInstructionByteDelta = function visitInstructionByteDelta(node) {
+            return merge(node, visit(this)(node.value));
+        };
+    }
+
+    if (castedNodeKeys.includes('definedTypeNode')) {
+        visitor.visitDefinedType = function visitDefinedType(node) {
+            return merge(node, visit(this)(node.type));
+        };
+    }
+
+    if (castedNodeKeys.includes('arrayTypeNode')) {
+        visitor.visitArrayType = function visitArrayType(node) {
+            return merge(node, [...visit(this)(node.count), ...visit(this)(node.item)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('enumTypeNode')) {
+        visitor.visitEnumType = function visitEnumType(node) {
+            return merge(node, [...visit(this)(node.size), ...node.variants.flatMap(visit(this))]);
+        };
+    }
+
+    if (castedNodeKeys.includes('enumStructVariantTypeNode')) {
+        visitor.visitEnumStructVariantType = function visitEnumStructVariantType(node) {
+            return merge(node, visit(this)(node.struct));
+        };
+    }
+
+    if (castedNodeKeys.includes('enumTupleVariantTypeNode')) {
+        visitor.visitEnumTupleVariantType = function visitEnumTupleVariantType(node) {
+            return merge(node, visit(this)(node.tuple));
+        };
+    }
+
+    if (castedNodeKeys.includes('mapTypeNode')) {
+        visitor.visitMapType = function visitMapType(node) {
+            return merge(node, [...visit(this)(node.count), ...visit(this)(node.key), ...visit(this)(node.value)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('optionTypeNode')) {
+        visitor.visitOptionType = function visitOptionType(node) {
+            return merge(node, [...visit(this)(node.prefix), ...visit(this)(node.item)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('zeroableOptionTypeNode')) {
+        visitor.visitZeroableOptionType = function visitZeroableOptionType(node) {
+            return merge(node, [...visit(this)(node.item), ...(node.zeroValue ? visit(this)(node.zeroValue) : [])]);
+        };
+    }
+
+    if (castedNodeKeys.includes('booleanTypeNode')) {
+        visitor.visitBooleanType = function visitBooleanType(node) {
+            return merge(node, visit(this)(node.size));
+        };
+    }
+
+    if (castedNodeKeys.includes('setTypeNode')) {
+        visitor.visitSetType = function visitSetType(node) {
+            return merge(node, [...visit(this)(node.count), ...visit(this)(node.item)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('structTypeNode')) {
+        visitor.visitStructType = function visitStructType(node) {
+            return merge(node, node.fields.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('structFieldTypeNode')) {
+        visitor.visitStructFieldType = function visitStructFieldType(node) {
+            return merge(node, [
+                ...visit(this)(node.type),
+                ...(node.defaultValue ? visit(this)(node.defaultValue) : []),
+            ]);
+        };
+    }
+
+    if (castedNodeKeys.includes('tupleTypeNode')) {
+        visitor.visitTupleType = function visitTupleType(node) {
+            return merge(node, node.items.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('amountTypeNode')) {
+        visitor.visitAmountType = function visitAmountType(node) {
+            return merge(node, visit(this)(node.number));
+        };
+    }
+
+    if (castedNodeKeys.includes('dateTimeTypeNode')) {
+        visitor.visitDateTimeType = function visitDateTimeType(node) {
+            return merge(node, visit(this)(node.number));
+        };
+    }
+
+    if (castedNodeKeys.includes('solAmountTypeNode')) {
+        visitor.visitSolAmountType = function visitSolAmountType(node) {
+            return merge(node, visit(this)(node.number));
+        };
+    }
+
+    if (castedNodeKeys.includes('prefixedCountNode')) {
+        visitor.visitPrefixedCount = function visitPrefixedCount(node) {
+            return merge(node, visit(this)(node.prefix));
+        };
+    }
+
+    if (castedNodeKeys.includes('arrayValueNode')) {
+        visitor.visitArrayValue = function visitArrayValue(node) {
+            return merge(node, node.items.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('constantValueNode')) {
+        visitor.visitConstantValue = function visitConstantValue(node) {
+            return merge(node, [...visit(this)(node.type), ...visit(this)(node.value)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('enumValueNode')) {
+        visitor.visitEnumValue = function visitEnumValue(node) {
+            return merge(node, [...visit(this)(node.enum), ...(node.value ? visit(this)(node.value) : [])]);
+        };
+    }
+
+    if (castedNodeKeys.includes('mapValueNode')) {
+        visitor.visitMapValue = function visitMapValue(node) {
+            return merge(node, node.entries.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('mapEntryValueNode')) {
+        visitor.visitMapEntryValue = function visitMapEntryValue(node) {
+            return merge(node, [...visit(this)(node.key), ...visit(this)(node.value)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('setValueNode')) {
+        visitor.visitSetValue = function visitSetValue(node) {
+            return merge(node, node.items.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('someValueNode')) {
+        visitor.visitSomeValue = function visitSomeValue(node) {
+            return merge(node, visit(this)(node.value));
+        };
+    }
+
+    if (castedNodeKeys.includes('structValueNode')) {
+        visitor.visitStructValue = function visitStructValue(node) {
+            return merge(node, node.fields.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('structFieldValueNode')) {
+        visitor.visitStructFieldValue = function visitStructFieldValue(node) {
+            return merge(node, visit(this)(node.value));
+        };
+    }
+
+    if (castedNodeKeys.includes('tupleValueNode')) {
+        visitor.visitTupleValue = function visitTupleValue(node) {
+            return merge(node, node.items.flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('constantPdaSeedNode')) {
+        visitor.visitConstantPdaSeed = function visitConstantPdaSeed(node) {
+            return merge(node, [...visit(this)(node.type), ...visit(this)(node.value)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('variablePdaSeedNode')) {
+        visitor.visitVariablePdaSeed = function visitVariablePdaSeed(node) {
+            return merge(node, visit(this)(node.type));
+        };
+    }
+
+    if (castedNodeKeys.includes('resolverValueNode')) {
+        visitor.visitResolverValue = function visitResolverValue(node) {
+            return merge(node, (node.dependsOn ?? []).flatMap(visit(this)));
+        };
+    }
+
+    if (castedNodeKeys.includes('conditionalValueNode')) {
+        visitor.visitConditionalValue = function visitConditionalValue(node) {
+            return merge(node, [
+                ...visit(this)(node.condition),
+                ...(node.value ? visit(this)(node.value) : []),
+                ...(node.ifTrue ? visit(this)(node.ifTrue) : []),
+                ...(node.ifFalse ? visit(this)(node.ifFalse) : []),
+            ]);
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaValueNode')) {
+        visitor.visitPdaValue = function visitPdaValue(node) {
+            return merge(node, [...visit(this)(node.pda), ...node.seeds.flatMap(visit(this))]);
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaSeedValueNode')) {
+        visitor.visitPdaSeedValue = function visitPdaSeedValue(node) {
+            return merge(node, visit(this)(node.value));
+        };
+    }
+
+    if (castedNodeKeys.includes('fixedSizeTypeNode')) {
+        visitor.visitFixedSizeType = function visitFixedSizeType(node) {
+            return merge(node, visit(this)(node.type));
+        };
+    }
+
+    if (castedNodeKeys.includes('sizePrefixTypeNode')) {
+        visitor.visitSizePrefixType = function visitSizePrefixType(node) {
+            return merge(node, [...visit(this)(node.prefix), ...visit(this)(node.type)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('preOffsetTypeNode')) {
+        visitor.visitPreOffsetType = function visitPreOffsetType(node) {
+            return merge(node, visit(this)(node.type));
+        };
+    }
+
+    if (castedNodeKeys.includes('postOffsetTypeNode')) {
+        visitor.visitPostOffsetType = function visitPostOffsetType(node) {
+            return merge(node, visit(this)(node.type));
+        };
+    }
+
+    if (castedNodeKeys.includes('sentinelTypeNode')) {
+        visitor.visitSentinelType = function visitSentinelType(node) {
+            return merge(node, [...visit(this)(node.sentinel), ...visit(this)(node.type)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('hiddenPrefixTypeNode')) {
+        visitor.visitHiddenPrefixType = function visitHiddenPrefixType(node) {
+            return merge(node, [...node.prefix.flatMap(visit(this)), ...visit(this)(node.type)]);
+        };
+    }
+
+    if (castedNodeKeys.includes('hiddenSuffixTypeNode')) {
+        visitor.visitHiddenSuffixType = function visitHiddenSuffixType(node) {
+            return merge(node, [...visit(this)(node.type), ...node.suffix.flatMap(visit(this))]);
+        };
+    }
+
+    if (castedNodeKeys.includes('constantDiscriminatorNode')) {
+        visitor.visitConstantDiscriminator = function visitConstantDiscriminator(node) {
+            return merge(node, visit(this)(node.constant));
+        };
+    }
+
+    return visitor as Visitor<TReturn, TNodeKind>;
+}

+ 10 - 0
packages/visitors-core/src/nonNullableIdentityVisitor.ts

@@ -0,0 +1,10 @@
+import { Node, NodeKind, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+import { identityVisitor } from './identityVisitor';
+import { Visitor } from './visitor';
+
+export function nonNullableIdentityVisitor<TNodeKind extends NodeKind = NodeKind>(
+    nodeKeys: TNodeKind[] = REGISTERED_NODE_KINDS as TNodeKind[],
+): Visitor<Node, TNodeKind> {
+    return identityVisitor<TNodeKind>(nodeKeys) as Visitor<Node, TNodeKind>;
+}

+ 113 - 0
packages/visitors-core/src/pipe.ts

@@ -0,0 +1,113 @@
+/**
+ * Copied from @solana/functional.
+ * @see https://github.com/solana-labs/solana-web3.js/blob/master/packages/functional/src/pipe.ts
+ *
+ * ---
+ *
+ * General pipe function.
+ * Provide an initial value and a list of functions to pipe it through.
+ *
+ * Following common implementations of pipe functions that use TypeScript,
+ * this function supports a maximum arity of 10 for type safety.
+ * @see https://github.com/ramda/ramda/blob/master/source/pipe.js
+ * @see https://github.com/darky/rocket-pipes/blob/master/index.ts
+ *
+ * Note you can use nested pipes to extend this limitation, like so:
+ * ```typescript
+ * const myValue = pipe(
+ *      pipe(
+ *          1,
+ *          (x) => x + 1,
+ *          (x) => x * 2,
+ *          (x) => x - 1,
+ *      ),
+ *      (y) => y / 3,
+ *      (y) => y + 1,
+ * );
+ * ```
+ * @param init  The initial value
+ * @param fns   Any number of functions to pipe the value through
+ * @returns     The final value with all functions applied
+ */
+export function pipe<TInitial>(init: TInitial): TInitial;
+export function pipe<TInitial, R1>(init: TInitial, init_r1: (init: TInitial) => R1): R1;
+export function pipe<TInitial, R1, R2>(init: TInitial, init_r1: (init: TInitial) => R1, r1_r2: (r1: R1) => R2): R2;
+export function pipe<TInitial, R1, R2, R3>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+): R3;
+export function pipe<TInitial, R1, R2, R3, R4>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+): R4;
+export function pipe<TInitial, R1, R2, R3, R4, R5>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+    r4_r5: (r4: R4) => R5,
+): R5;
+export function pipe<TInitial, R1, R2, R3, R4, R5, R6>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+    r4_r5: (r4: R4) => R5,
+    r5_r6: (r5: R5) => R6,
+): R6;
+export function pipe<TInitial, R1, R2, R3, R4, R5, R6, R7>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+    r4_r5: (r4: R4) => R5,
+    r5_r6: (r5: R5) => R6,
+    r6_r7: (r6: R6) => R7,
+): R7;
+export function pipe<TInitial, R1, R2, R3, R4, R5, R6, R7, R8>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+    r4_r5: (r4: R4) => R5,
+    r5_r6: (r5: R5) => R6,
+    r6_r7: (r6: R6) => R7,
+    r7_r8: (r7: R7) => R8,
+): R8;
+export function pipe<TInitial, R1, R2, R3, R4, R5, R6, R7, R8, R9>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+    r4_r5: (r4: R4) => R5,
+    r5_r6: (r5: R5) => R6,
+    r6_r7: (r6: R6) => R7,
+    r7_r8: (r7: R7) => R8,
+    r8_r9: (r8: R8) => R9,
+): R9;
+export function pipe<TInitial, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10>(
+    init: TInitial,
+    init_r1: (init: TInitial) => R1,
+    r1_r2: (r1: R1) => R2,
+    r2_r3: (r2: R2) => R3,
+    r3_r4: (r3: R3) => R4,
+    r4_r5: (r4: R4) => R5,
+    r5_r6: (r5: R5) => R6,
+    r6_r7: (r6: R6) => R7,
+    r7_r8: (r7: R7) => R8,
+    r8_r9: (r8: R8) => R9,
+    r9_r10: (r9: R9) => R10,
+): R10;
+export function pipe<TInitial>(init: TInitial, ...fns: CallableFunction[]) {
+    return fns.reduce((acc, fn) => fn(acc), init);
+}

+ 51 - 0
packages/visitors-core/src/recordLinkablesVisitor.ts

@@ -0,0 +1,51 @@
+import { getAllAccounts, getAllDefinedTypes, getAllPdas, getAllPrograms, NodeKind } from '@kinobi-so/nodes';
+
+import { extendVisitor, VisitorOverrides } from './extendVisitor';
+import { LinkableDictionary } from './LinkableDictionary';
+import { Visitor } from './visitor';
+
+export function recordLinkablesVisitor<TReturn, TNodeKind extends NodeKind>(
+    visitor: Visitor<TReturn, TNodeKind>,
+    linkables: LinkableDictionary,
+): Visitor<TReturn, TNodeKind> {
+    const overriddenFunctions: VisitorOverrides<
+        TReturn,
+        'accountNode' | 'definedTypeNode' | 'pdaNode' | 'programNode' | 'rootNode'
+    > = {};
+    if ('visitRoot' in visitor) {
+        overriddenFunctions.visitRoot = function visitRoot(node, { next }) {
+            linkables.recordAll([
+                ...getAllPrograms(node),
+                ...getAllPdas(node),
+                ...getAllAccounts(node),
+                ...getAllDefinedTypes(node),
+            ]);
+            return next(node);
+        };
+    }
+    if ('visitProgram' in visitor) {
+        overriddenFunctions.visitProgram = function visitProgram(node, { next }) {
+            linkables.recordAll([node, ...node.pdas, ...node.accounts, ...node.definedTypes]);
+            return next(node);
+        };
+    }
+    if ('visitPda' in visitor) {
+        overriddenFunctions.visitPda = function visitPda(node, { next }) {
+            linkables.record(node);
+            return next(node);
+        };
+    }
+    if ('visitAccount' in visitor) {
+        overriddenFunctions.visitAccount = function visitAccount(node, { next }) {
+            linkables.record(node);
+            return next(node);
+        };
+    }
+    if ('visitDefinedType' in visitor) {
+        overriddenFunctions.visitDefinedType = function visitDefinedType(node, { next }) {
+            linkables.record(node);
+            return next(node);
+        };
+    }
+    return extendVisitor(visitor, overriddenFunctions as VisitorOverrides<TReturn, TNodeKind>);
+}

+ 17 - 0
packages/visitors-core/src/recordNodeStackVisitor.ts

@@ -0,0 +1,17 @@
+import { NodeKind } from '@kinobi-so/nodes';
+
+import { interceptVisitor } from './interceptVisitor';
+import { NodeStack } from './NodeStack';
+import { Visitor } from './visitor';
+
+export function recordNodeStackVisitor<TReturn, TNodeKind extends NodeKind>(
+    visitor: Visitor<TReturn, TNodeKind>,
+    stack: NodeStack,
+): Visitor<TReturn, TNodeKind> {
+    return interceptVisitor(visitor, (node, next) => {
+        stack.push(node);
+        const newNode = next(node);
+        stack.pop();
+        return newNode;
+    });
+}

+ 13 - 0
packages/visitors-core/src/removeDocsVisitor.ts

@@ -0,0 +1,13 @@
+import { NodeKind } from '@kinobi-so/nodes';
+
+import { interceptVisitor } from './interceptVisitor';
+import { nonNullableIdentityVisitor } from './nonNullableIdentityVisitor';
+
+export function removeDocsVisitor<TNodeKind extends NodeKind = NodeKind>(nodeKeys?: TNodeKind[]) {
+    return interceptVisitor(nonNullableIdentityVisitor(nodeKeys), (node, next) => {
+        if ('docs' in node) {
+            return next({ ...node, docs: [] });
+        }
+        return next(node);
+    });
+}

+ 17 - 0
packages/visitors-core/src/singleNodeVisitor.ts

@@ -0,0 +1,17 @@
+import { GetNodeFromKind, NodeKind, RootNode } from '@kinobi-so/nodes';
+
+import { getVisitFunctionName, GetVisitorFunctionName, Visitor } from './visitor';
+
+export function singleNodeVisitor<TReturn, TNodeKey extends NodeKind = NodeKind>(
+    key: TNodeKey,
+    fn: (node: GetNodeFromKind<TNodeKey>) => TReturn,
+): Visitor<TReturn, TNodeKey> {
+    const visitor = {} as Visitor<TReturn, TNodeKey>;
+    visitor[getVisitFunctionName(key)] = fn as unknown as Visitor<TReturn, TNodeKey>[GetVisitorFunctionName<TNodeKey>];
+
+    return visitor;
+}
+
+export function rootNodeVisitor<TReturn = RootNode>(fn: (node: RootNode) => TReturn) {
+    return singleNodeVisitor('rootNode', fn);
+}

+ 14 - 0
packages/visitors-core/src/staticVisitor.ts

@@ -0,0 +1,14 @@
+import { Node, NodeKind, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+import { getVisitFunctionName, Visitor } from './visitor';
+
+export function staticVisitor<TReturn, TNodeKind extends NodeKind = NodeKind>(
+    fn: (node: Node) => TReturn,
+    nodeKeys: TNodeKind[] = REGISTERED_NODE_KINDS as TNodeKind[],
+): Visitor<TReturn, TNodeKind> {
+    const visitor = {} as Visitor<TReturn>;
+    nodeKeys.forEach(key => {
+        visitor[getVisitFunctionName(key)] = fn.bind(visitor);
+    });
+    return visitor;
+}

+ 21 - 0
packages/visitors-core/src/tapVisitor.ts

@@ -0,0 +1,21 @@
+import { GetNodeFromKind, NodeKind } from '@kinobi-so/nodes';
+
+import { getVisitFunctionName, GetVisitorFunctionName, Visitor } from './visitor';
+
+export function tapVisitor<TReturn, TNodeKey extends NodeKind, TVisitor extends Visitor<TReturn, TNodeKey>>(
+    visitor: TVisitor,
+    key: TNodeKey,
+    tap: (node: GetNodeFromKind<TNodeKey>) => void,
+): TVisitor {
+    const newVisitor = { ...visitor };
+    newVisitor[getVisitFunctionName(key)] = function tappedVisitNode(
+        this: TVisitor,
+        node: GetNodeFromKind<TNodeKey>,
+    ): TReturn {
+        tap(node);
+        const parentFunction = visitor[getVisitFunctionName(key)] as (node: GetNodeFromKind<TNodeKey>) => TReturn;
+        return parentFunction.bind(this)(node);
+    } as TVisitor[GetVisitorFunctionName<TNodeKey>];
+
+    return newVisitor;
+}

+ 47 - 0
packages/visitors-core/src/topDownTransformerVisitor.ts

@@ -0,0 +1,47 @@
+import { Node, NodeKind } from '@kinobi-so/nodes';
+
+import { identityVisitor } from './identityVisitor';
+import { interceptVisitor } from './interceptVisitor';
+import { getConjunctiveNodeSelectorFunction, NodeSelector } from './NodeSelector';
+import { NodeStack } from './NodeStack';
+import { pipe } from './pipe';
+import { recordNodeStackVisitor } from './recordNodeStackVisitor';
+import { Visitor } from './visitor';
+
+export type TopDownNodeTransformer<TNode extends Node = Node> = <T extends TNode = TNode>(
+    node: T,
+    stack: NodeStack,
+) => T | null;
+
+export type TopDownNodeTransformerWithSelector<TNode extends Node = Node> = {
+    select: NodeSelector | NodeSelector[];
+    transform: TopDownNodeTransformer<TNode>;
+};
+
+export function topDownTransformerVisitor<TNodeKind extends NodeKind = NodeKind>(
+    transformers: (TopDownNodeTransformer | TopDownNodeTransformerWithSelector)[],
+    nodeKeys?: TNodeKind[],
+): Visitor<Node | null, TNodeKind> {
+    const transformerFunctions = transformers.map((transformer): TopDownNodeTransformer => {
+        if (typeof transformer === 'function') return transformer;
+        return (node, stack) =>
+            getConjunctiveNodeSelectorFunction(transformer.select)(node, stack)
+                ? transformer.transform(node, stack)
+                : node;
+    });
+
+    const stack = new NodeStack();
+    return pipe(
+        identityVisitor(nodeKeys),
+        v => recordNodeStackVisitor(v, stack),
+        v =>
+            interceptVisitor(v, (node, next) => {
+                const appliedNode = transformerFunctions.reduce(
+                    (acc, transformer) => (acc === null ? null : transformer(acc, stack.clone())),
+                    node as Parameters<typeof next>[0] | null,
+                );
+                if (appliedNode === null) return null;
+                return next(appliedNode);
+            }),
+    );
+}

+ 32 - 0
packages/visitors-core/src/visitor.ts

@@ -0,0 +1,32 @@
+import { type GetNodeFromKind, type Node, type NodeKind, pascalCase, REGISTERED_NODE_KINDS } from '@kinobi-so/nodes';
+
+export type Visitor<TReturn, TNodeKind extends NodeKind = NodeKind> = {
+    [K in TNodeKind as GetVisitorFunctionName<K>]: (node: GetNodeFromKind<K>) => TReturn;
+};
+
+export type GetVisitorFunctionName<T extends Node['kind']> = T extends `${infer TWithoutNode}Node`
+    ? `visit${Capitalize<TWithoutNode>}`
+    : never;
+
+export function visit<TReturn, TNode extends Node>(node: TNode, visitor: Visitor<TReturn, TNode['kind']>): TReturn {
+    const key = getVisitFunctionName(node.kind) as GetVisitorFunctionName<TNode['kind']>;
+    return (visitor[key] as (typeof visitor)[typeof key] & ((node: TNode) => TReturn))(node);
+}
+
+export function visitOrElse<TReturn, TNode extends Node, TNodeKind extends NodeKind>(
+    node: TNode,
+    visitor: Visitor<TReturn, TNodeKind>,
+    fallback: (node: TNode) => TReturn,
+): TReturn {
+    const key = getVisitFunctionName<TNode['kind']>(node.kind);
+    return (key in visitor ? (visitor[key] as (node: TNode) => TReturn) : fallback)(node);
+}
+
+export function getVisitFunctionName<TNodeKind extends NodeKind>(nodeKind: TNodeKind) {
+    if (!REGISTERED_NODE_KINDS.includes(nodeKind)) {
+        // TODO: Coded error.
+        throw new Error(`Unrecognized node [${nodeKind}]`);
+    }
+
+    return `visit${pascalCase(nodeKind.slice(0, -4))}` as GetVisitorFunctionName<TNodeKind>;
+}

+ 12 - 0
packages/visitors-core/src/voidVisitor.ts

@@ -0,0 +1,12 @@
+import type { NodeKind } from '@kinobi-so/nodes';
+
+import { mergeVisitor } from './mergeVisitor';
+import { Visitor } from './visitor';
+
+export function voidVisitor<TNodeKind extends NodeKind = NodeKind>(nodeKeys?: TNodeKind[]): Visitor<void, TNodeKind> {
+    return mergeVisitor(
+        () => undefined,
+        () => undefined,
+        nodeKeys,
+    );
+}

+ 113 - 0
packages/visitors-core/test/bottomUpTransformerVisitor.test.ts

@@ -0,0 +1,113 @@
+import { isNode, numberTypeNode, publicKeyTypeNode, stringTypeNode, tupleTypeNode, TYPE_NODES } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { bottomUpTransformerVisitor, visit } from '../src/index.js';
+
+test('it can transform nodes into other nodes', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a transformer visitor that transforms all number nodes into string nodes.
+    const visitor = bottomUpTransformerVisitor([
+        node => (isNode(node, 'numberTypeNode') ? stringTypeNode('utf8') : node),
+    ]);
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the number nodes to have been transformed into string nodes.
+    t.deepEqual(
+        result,
+        tupleTypeNode([stringTypeNode('utf8'), tupleTypeNode([stringTypeNode('utf8'), publicKeyTypeNode()])]),
+    );
+});
+
+test('it can transform nodes using node selectors', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a transformer visitor that selects all number nodes and transforms them into string nodes.
+    const visitor = bottomUpTransformerVisitor([
+        {
+            select: '[numberTypeNode]',
+            transform: () => stringTypeNode('utf8'),
+        },
+    ]);
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the number nodes to have been transformed into string nodes.
+    t.deepEqual(
+        result,
+        tupleTypeNode([stringTypeNode('utf8'), tupleTypeNode([stringTypeNode('utf8'), publicKeyTypeNode()])]),
+    );
+});
+
+test('it can create partial transformer visitors', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a transformer visitor that wraps every node into another tuple node
+    // but that does not transform public key nodes.
+    const visitor = bottomUpTransformerVisitor(
+        [node => (isNode(node, TYPE_NODES) ? tupleTypeNode([node]) : node)],
+        ['tupleTypeNode', 'numberTypeNode'],
+    );
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the following tree.
+    t.deepEqual(
+        result,
+        tupleTypeNode([
+            tupleTypeNode([
+                tupleTypeNode([numberTypeNode('u32')]),
+                tupleTypeNode([tupleTypeNode([tupleTypeNode([numberTypeNode('u32')]), publicKeyTypeNode()])]),
+            ]),
+        ]),
+    );
+
+    // And the public key node cannot be visited.
+    // @ts-expect-error PublicKeyTypeNode is not supported.
+    t.throws(() => visit(publicKeyTypeNode(), visitor));
+});
+
+test('it can be used to delete nodes', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a transformer visitor that deletes all number nodes.
+    const visitor = bottomUpTransformerVisitor([{ select: '[numberTypeNode]', transform: () => null }]);
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the number nodes to have been deleted.
+    t.deepEqual(result, tupleTypeNode([tupleTypeNode([publicKeyTypeNode()])]));
+});
+
+test('it can transform nodes using multiple node selectors', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a transformer visitor that uses two node selectors such that
+    // - the first one selects all number nodes, and
+    // - the second one selects all nodes with more than one ancestor.
+    const visitor = bottomUpTransformerVisitor([
+        {
+            select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.all().length > 1],
+            transform: () => stringTypeNode('utf8'),
+        },
+    ]);
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect both node selectors to have been applied.
+    t.deepEqual(
+        result,
+        tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([stringTypeNode('utf8'), publicKeyTypeNode()])]),
+    );
+});

+ 40 - 0
packages/visitors-core/test/deleteNodesVisitor.test.ts

@@ -0,0 +1,40 @@
+import { numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { deleteNodesVisitor, visit } from '../src/index.js';
+
+test('it can delete nodes using selectors', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a visitor that deletes all number nodes.
+    const visitor = deleteNodesVisitor(['[numberTypeNode]']);
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the number nodes to have been deleted.
+    t.deepEqual(result, tupleTypeNode([tupleTypeNode([publicKeyTypeNode()])]));
+});
+
+test('it can create partial visitors', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // And a visitor that deletes all number nodes and public key nodes
+    // but does not support public key nodes.
+    const visitor = deleteNodesVisitor(
+        ['[numberTypeNode]', '[publicKeyTypeNode]'],
+        ['tupleTypeNode', 'numberTypeNode'],
+    );
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then only the number nodes have been deleted.
+    t.deepEqual(result, tupleTypeNode([tupleTypeNode([publicKeyTypeNode()])]));
+
+    // And the public key node cannot be visited.
+    // @ts-expect-error PublicKeyTypeNode is not supported.
+    t.throws(() => visit(publicKeyTypeNode(), visitor));
+});

+ 65 - 0
packages/visitors-core/test/extendVisitor.test.ts

@@ -0,0 +1,65 @@
+import { numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { extendVisitor, mergeVisitor, visit, voidVisitor } from '../src/index.js';
+
+test('it returns a new visitor that extends a subset of visits with a next function', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u64'), publicKeyTypeNode()])]);
+
+    // And an extended sum visitor that adds an extra 10 to tuple and public key nodes.
+    const baseVisitor = mergeVisitor(
+        () => 1,
+        (_, values) => values.reduce((a, b) => a + b, 1),
+    );
+    const visitor = extendVisitor(baseVisitor, {
+        visitPublicKeyType: (node, { next }) => next(node) + 10,
+        visitTupleType: (node, { next }) => next(node) + 10,
+    });
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the following count.
+    t.is(result, 35);
+
+    // And the extended visitor is a new instance.
+    t.not(baseVisitor, visitor);
+});
+
+test('it can visit itself using the exposed self argument', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u64'), publicKeyTypeNode()])]);
+
+    // And an extended sum visitor that only visit the first item of tuple nodes.
+    const baseVisitor = mergeVisitor(
+        () => 1,
+        (_, values) => values.reduce((a, b) => a + b, 1),
+    );
+    const visitor = extendVisitor(baseVisitor, {
+        visitTupleType: (node, { self }) => (node.items.length > 1 ? visit(node.items[0], self) : 0) + 1,
+    });
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the following count.
+    t.is(result, 2);
+});
+
+test('it cannot extends nodes that are not supported by the base visitor', t => {
+    // Given a base visitor that only supports tuple nodes.
+    const baseVisitor = voidVisitor(['tupleTypeNode']);
+
+    // Then we expect an error when we try to extend other nodes for that visitor.
+    t.throws(
+        () =>
+            extendVisitor(baseVisitor, {
+                // @ts-expect-error NumberTypeNode is not part of the base visitor.
+                visitNumberType: () => undefined,
+            }),
+        {
+            message: 'Cannot extend visitor with function "visitNumberType" as the base visitor does not support it.',
+        },
+    );
+});

+ 107 - 0
packages/visitors-core/test/getDebugStringVisitor.test.ts

@@ -0,0 +1,107 @@
+import {
+    enumEmptyVariantTypeNode,
+    enumTypeNode,
+    numberTypeNode,
+    optionTypeNode,
+    publicKeyTypeNode,
+    sizePrefixTypeNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    tupleTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { getDebugStringVisitor, visit } from '../src/index.js';
+
+test('it returns a string representing the main information of a node for debugging purposes', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([
+        numberTypeNode('u32'),
+        structTypeNode([
+            structFieldTypeNode({
+                name: 'firstname',
+                type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u64')),
+            }),
+            structFieldTypeNode({ name: 'age', type: numberTypeNode('u32') }),
+            structFieldTypeNode({
+                name: 'wallet',
+                type: optionTypeNode(publicKeyTypeNode(), {
+                    prefix: numberTypeNode('u16'),
+                }),
+            }),
+            structFieldTypeNode({
+                name: 'industry',
+                type: enumTypeNode([
+                    enumEmptyVariantTypeNode('programming'),
+                    enumEmptyVariantTypeNode('crypto'),
+                    enumEmptyVariantTypeNode('music'),
+                ]),
+            }),
+        ]),
+    ]);
+
+    // When we get its unique hash string.
+    const result = visit(node, getDebugStringVisitor());
+
+    // Then we expect the following string.
+    t.deepEqual(
+        result,
+        'tupleTypeNode(numberTypeNode[u32], structTypeNode(structFieldTypeNode[firstname](sizePrefixTypeNode(numberTypeNode[u64], stringTypeNode[utf8])), structFieldTypeNode[age](numberTypeNode[u32]), structFieldTypeNode[wallet](optionTypeNode(numberTypeNode[u16], publicKeyTypeNode)), structFieldTypeNode[industry](enumTypeNode(numberTypeNode[u8], enumEmptyVariantTypeNode[programming], enumEmptyVariantTypeNode[crypto], enumEmptyVariantTypeNode[music]))))',
+    );
+});
+
+test('it can create indented strings', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([
+        numberTypeNode('u32'),
+        structTypeNode([
+            structFieldTypeNode({
+                name: 'firstname',
+                type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u64')),
+            }),
+            structFieldTypeNode({ name: 'age', type: numberTypeNode('u32') }),
+            structFieldTypeNode({
+                name: 'wallet',
+                type: optionTypeNode(publicKeyTypeNode(), {
+                    prefix: numberTypeNode('u16'),
+                }),
+            }),
+            structFieldTypeNode({
+                name: 'industry',
+                type: enumTypeNode([
+                    enumEmptyVariantTypeNode('programming'),
+                    enumEmptyVariantTypeNode('crypto'),
+                    enumEmptyVariantTypeNode('music'),
+                ]),
+            }),
+        ]),
+    ]);
+
+    // When we get its unique hash string.
+    const result = visit(node, getDebugStringVisitor({ indent: true }));
+
+    // Then we expect the following string.
+    t.deepEqual(
+        result,
+        `tupleTypeNode
+|   numberTypeNode [u32]
+|   structTypeNode
+|   |   structFieldTypeNode [firstname]
+|   |   |   sizePrefixTypeNode
+|   |   |   |   numberTypeNode [u64]
+|   |   |   |   stringTypeNode [utf8]
+|   |   structFieldTypeNode [age]
+|   |   |   numberTypeNode [u32]
+|   |   structFieldTypeNode [wallet]
+|   |   |   optionTypeNode
+|   |   |   |   numberTypeNode [u16]
+|   |   |   |   publicKeyTypeNode
+|   |   structFieldTypeNode [industry]
+|   |   |   enumTypeNode
+|   |   |   |   numberTypeNode [u8]
+|   |   |   |   enumEmptyVariantTypeNode [programming]
+|   |   |   |   enumEmptyVariantTypeNode [crypto]
+|   |   |   |   enumEmptyVariantTypeNode [music]`,
+    );
+});

+ 49 - 0
packages/visitors-core/test/getUniqueHashStringVisitor.test.ts

@@ -0,0 +1,49 @@
+import {
+    numberTypeNode,
+    publicKeyTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    tupleTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { getUniqueHashStringVisitor, visit } from '../src/index.js';
+
+test('it returns a unique string representing the whole node', t => {
+    // Given the following tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()])]);
+
+    // When we get its unique hash string.
+    const result = visit(node, getUniqueHashStringVisitor());
+
+    // Then we expect the following string.
+    t.deepEqual(
+        result,
+        '{"items":[' +
+            '{"endian":"le","format":"u32","kind":"numberTypeNode"},' +
+            '{"items":[{"endian":"le","format":"u32","kind":"numberTypeNode"},{"kind":"publicKeyTypeNode"}],"kind":"tupleTypeNode"}' +
+            '],"kind":"tupleTypeNode"}',
+    );
+});
+
+test('it returns a unique string whilst discard docs', t => {
+    // Given the following tree with docs.
+    const node = structTypeNode([
+        structFieldTypeNode({
+            docs: ['The owner of the account.'],
+            name: 'owner',
+            type: publicKeyTypeNode(),
+        }),
+    ]);
+
+    // When we get its unique hash string whilst discarding docs.
+    const result = visit(node, getUniqueHashStringVisitor({ removeDocs: true }));
+
+    // Then we expect the following string.
+    t.deepEqual(
+        result,
+        '{"fields":[' +
+            '{"docs":[],"kind":"structFieldTypeNode","name":"owner","type":{"kind":"publicKeyTypeNode"}}' +
+            '],"kind":"structTypeNode"}',
+    );
+});

+ 66 - 0
packages/visitors-core/test/identityVisitor.test.ts

@@ -0,0 +1,66 @@
+import { assertIsNode, numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { identityVisitor, interceptVisitor, visit } from '../src/index.js';
+
+test('it visits all nodes and returns different instances of the same nodes', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // When we visit it using the identity visitor.
+    const result = visit(node, identityVisitor());
+
+    // Then we get the same tree back.
+    t.deepEqual(result, node);
+
+    // But the nodes are different instances.
+    t.not(result, node);
+    assertIsNode(result, 'tupleTypeNode');
+    t.not(result.items[0], node.items[0]);
+    t.not(result.items[1], node.items[1]);
+});
+
+test('it can remove nodes by returning null', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And given an identity visitor overidden to remove all public key nodes.
+    const visitor = identityVisitor();
+    visitor.visitPublicKeyType = () => null;
+
+    // When we visit it using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we expect the following tree back.
+    t.deepEqual(result, tupleTypeNode([numberTypeNode('u32')]));
+});
+
+test('it can create partial visitors', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And an identity visitor that only supports 2 of these nodes
+    // whilst using an interceptor to record the events that happened.
+    const events: string[] = [];
+    const visitor = interceptVisitor(identityVisitor(['tupleTypeNode', 'numberTypeNode']), (node, next) => {
+        events.push(`visiting:${node.kind}`);
+        return next(node);
+    });
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we still get the full tree back as different instances.
+    t.deepEqual(result, node);
+    t.not(result, node);
+    assertIsNode(result, 'tupleTypeNode');
+    t.not(result.items[0], node.items[0]);
+    t.not(result.items[1], node.items[1]);
+
+    // But the unsupported node was not visited.
+    t.deepEqual(events, ['visiting:tupleTypeNode', 'visiting:numberTypeNode']);
+
+    // And the unsupported node cannot be visited.
+    // @ts-expect-error PublicKeyTypeNode is not supported.
+    t.throws(() => visit(publicKeyTypeNode(), visitor));
+});

+ 34 - 0
packages/visitors-core/test/interceptVisitor.test.ts

@@ -0,0 +1,34 @@
+import { numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { interceptVisitor, visit, voidVisitor } from '../src/index.js';
+
+test('it returns a new visitor that intercepts all visits of a visitor', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And an intercepted void visitor that records the events that happened during each visit.
+    const events: string[] = [];
+    const baseVisitor = voidVisitor();
+    const visitor = interceptVisitor(baseVisitor, (node, next) => {
+        events.push(`down:${node.kind}`);
+        next(node);
+        events.push(`up:${node.kind}`);
+    });
+
+    // When we visit the tree using that visitor.
+    visit(node, visitor);
+
+    // Then we expect the following events to have happened.
+    t.deepEqual(events, [
+        'down:tupleTypeNode',
+        'down:numberTypeNode',
+        'up:numberTypeNode',
+        'down:publicKeyTypeNode',
+        'up:publicKeyTypeNode',
+        'up:tupleTypeNode',
+    ]);
+
+    // And the intercepted visitor is a new instance.
+    t.not(baseVisitor, visitor);
+});

+ 42 - 0
packages/visitors-core/test/mapVisitor.test.ts

@@ -0,0 +1,42 @@
+import { numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { mapVisitor, mergeVisitor, staticVisitor, visit, Visitor } from '../src/index.js';
+
+test('it maps the return value of a visitor to another', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And a merge visitor A that lists the kind of each node.
+    const visitorA = mergeVisitor(
+        node => node.kind as string,
+        (node, values) => `${node.kind}(${values.join(',')})`,
+    );
+
+    // And a mapped visitor B that returns the number of characters returned by visitor A.
+    const visitorB = mapVisitor(visitorA, value => value.length);
+
+    // Then we expect the following results when visiting different nodes.
+    t.is(visit(node, visitorB), 47);
+    t.is(visit(node.items[0], visitorB), 14);
+    t.is(visit(node.items[1], visitorB), 17);
+});
+
+test('it creates partial visitors from partial visitors', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And partial static visitor A that supports only 2 of these nodes.
+    const visitorA = staticVisitor(node => node.kind, ['tupleTypeNode', 'numberTypeNode']);
+
+    // And a mapped visitor B that returns the number of characters returned by visitor A.
+    const visitorB = mapVisitor(visitorA, value => value.length);
+
+    // Then both visitors are partial.
+    visitorA satisfies Visitor<string, 'numberTypeNode' | 'tupleTypeNode'>;
+    visitorB satisfies Visitor<number, 'numberTypeNode' | 'tupleTypeNode'>;
+
+    // Then we expect an error when visiting an unsupported node.
+    // @ts-expect-error PublicKeyTypeNode is not supported.
+    t.throws(() => visit(node.items[1], visitorB));
+});

+ 59 - 0
packages/visitors-core/test/mergeVisitor.test.ts

@@ -0,0 +1,59 @@
+import { numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import { mergeVisitor, visit } from '../src/index.js';
+
+test('it sets a value for all leaves and merges node values together', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And a visitor that sets the node kind for all leaves and combines
+    // them together such that each node lists the kind of its children.
+    const visitor = mergeVisitor(
+        node => node.kind as string,
+        (node, values) => `${node.kind}(${values.join(',')})`,
+    );
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then we get the following result.
+    t.is(result, 'tupleTypeNode(numberTypeNode,publicKeyTypeNode)');
+});
+
+test('it can be used to count nodes', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // When we visit the tree with a visitor that counts nodes.
+    const visitor = mergeVisitor(
+        () => 1,
+        (_, values) => values.reduce((a, b) => a + b, 1),
+    );
+    const result = visit(node, visitor);
+
+    // Then we expect to have 3 nodes.
+    t.is(result, 3);
+});
+
+test('it can create partial visitors', t => {
+    // Given the following 3-nodes tree.
+    const node = tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]);
+
+    // And a visitor that only supports 2 of these nodes.
+    const visitor = mergeVisitor(
+        node => node.kind as string,
+        (node, values) => `${node.kind}(${values.join(',')})`,
+        ['tupleTypeNode', 'numberTypeNode'],
+    );
+
+    // When we visit the tree using that visitor.
+    const result = visit(node, visitor);
+
+    // Then the unsupported node is not included in the result.
+    t.is(result, 'tupleTypeNode(numberTypeNode)');
+
+    // And the unsupported node cannot be visited.
+    // @ts-expect-error PublicKeyTypeNode is not supported.
+    t.throws(() => visit(publicKeyTypeNode(), visitor));
+});

+ 49 - 0
packages/visitors-core/test/nodes/AccountNode.test.ts

@@ -0,0 +1,49 @@
+import {
+    accountNode,
+    numberTypeNode,
+    pdaLinkNode,
+    publicKeyTypeNode,
+    sizeDiscriminatorNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = accountNode({
+    data: structTypeNode([
+        structFieldTypeNode({ name: 'mint', type: publicKeyTypeNode() }),
+        structFieldTypeNode({ name: 'owner', type: publicKeyTypeNode() }),
+        structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }),
+    ]),
+    discriminators: [sizeDiscriminatorNode(72)],
+    name: 'token',
+    pda: pdaLinkNode('associatedToken'),
+    size: 72,
+});
+
+test(mergeVisitorMacro, node, 10);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[accountNode]', null);
+test(deleteNodesVisitorMacro, node, '[pdaLinkNode]', accountNode({ ...node, pda: undefined }));
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+accountNode [token]
+|   structTypeNode
+|   |   structFieldTypeNode [mint]
+|   |   |   publicKeyTypeNode
+|   |   structFieldTypeNode [owner]
+|   |   |   publicKeyTypeNode
+|   |   structFieldTypeNode [amount]
+|   |   |   numberTypeNode [u64]
+|   pdaLinkNode [associatedToken]
+|   sizeDiscriminatorNode [72]`,
+);

+ 44 - 0
packages/visitors-core/test/nodes/DefinedTypeNode.test.ts

@@ -0,0 +1,44 @@
+import {
+    definedTypeNode,
+    fixedSizeTypeNode,
+    numberTypeNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = definedTypeNode({
+    name: 'person',
+    type: structTypeNode([
+        structFieldTypeNode({
+            name: 'name',
+            type: fixedSizeTypeNode(stringTypeNode('utf8'), 42),
+        }),
+        structFieldTypeNode({ name: 'age', type: numberTypeNode('u64') }),
+    ]),
+});
+
+test(mergeVisitorMacro, node, 7);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[definedTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[structTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+definedTypeNode [person]
+|   structTypeNode
+|   |   structFieldTypeNode [name]
+|   |   |   fixedSizeTypeNode [42]
+|   |   |   |   stringTypeNode [utf8]
+|   |   structFieldTypeNode [age]
+|   |   |   numberTypeNode [u64]`,
+);

+ 20 - 0
packages/visitors-core/test/nodes/ErrorNode.test.ts

@@ -0,0 +1,20 @@
+import { errorNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = errorNode({
+    code: 42,
+    message: 'The provided account does not match the owner of the token account.',
+    name: 'InvalidTokenOwner',
+});
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[errorNode]', null);
+test(getDebugStringVisitorMacro, node, `errorNode [42.invalidTokenOwner]`);

+ 29 - 0
packages/visitors-core/test/nodes/InstructionAccountNode.test.ts

@@ -0,0 +1,29 @@
+import { accountValueNode, instructionAccountNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = instructionAccountNode({
+    defaultValue: accountValueNode('authority'),
+    isOptional: false,
+    isSigner: 'either',
+    isWritable: true,
+    name: 'owner',
+});
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[instructionAccountNode]', null);
+test(deleteNodesVisitorMacro, node, '[accountValueNode]', instructionAccountNode({ ...node, defaultValue: undefined }));
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+instructionAccountNode [owner.writable.optionalSigner]
+|   accountValueNode [authority]`,
+);

+ 34 - 0
packages/visitors-core/test/nodes/InstructionArgumentNode.test.ts

@@ -0,0 +1,34 @@
+import { instructionArgumentNode, numberTypeNode, numberValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = instructionArgumentNode({
+    defaultValue: numberValueNode(1),
+    name: 'amount',
+    type: numberTypeNode('u64'),
+});
+
+test(mergeVisitorMacro, node, 3);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[instructionArgumentNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    deleteNodesVisitorMacro,
+    node,
+    '[numberValueNode]',
+    instructionArgumentNode({ name: 'amount', type: numberTypeNode('u64') }),
+);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+instructionArgumentNode [amount]
+|   numberTypeNode [u64]
+|   numberValueNode [1]`,
+);

+ 25 - 0
packages/visitors-core/test/nodes/InstructionByteDeltaNode.test.ts

@@ -0,0 +1,25 @@
+import { instructionByteDeltaNode, numberValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = instructionByteDeltaNode(numberValueNode(42), {
+    subtract: true,
+});
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[instructionByteDeltaNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberValueNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+instructionByteDeltaNode [subtract.withHeader]
+|   numberValueNode [42]`,
+);

+ 113 - 0
packages/visitors-core/test/nodes/InstructionNode.test.ts

@@ -0,0 +1,113 @@
+import {
+    fieldDiscriminatorNode,
+    instructionAccountNode,
+    instructionArgumentNode,
+    instructionByteDeltaNode,
+    instructionNode,
+    instructionRemainingAccountsNode,
+    numberTypeNode,
+    numberValueNode,
+    publicKeyTypeNode,
+    resolverValueNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = instructionNode({
+    accounts: [
+        instructionAccountNode({
+            isSigner: true,
+            isWritable: true,
+            name: 'source',
+        }),
+        instructionAccountNode({
+            isSigner: false,
+            isWritable: true,
+            name: 'destination',
+        }),
+    ],
+    arguments: [
+        instructionArgumentNode({
+            name: 'discriminator',
+            type: numberTypeNode('u32'),
+        }),
+        instructionArgumentNode({
+            name: 'amount',
+            type: numberTypeNode('u64'),
+        }),
+    ],
+    discriminators: [fieldDiscriminatorNode('discriminator')],
+    name: 'transferSol',
+});
+
+test(mergeVisitorMacro, node, 8);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[instructionNode]', null);
+test(deleteNodesVisitorMacro, node, '[instructionAccountNode]', {
+    ...node,
+    accounts: [],
+});
+test(deleteNodesVisitorMacro, node, '[instructionArgumentNode]', {
+    ...node,
+    arguments: [],
+});
+test(deleteNodesVisitorMacro, node, '[fieldDiscriminatorNode]', {
+    ...node,
+    discriminators: [],
+});
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+instructionNode [transferSol]
+|   instructionAccountNode [source.writable.signer]
+|   instructionAccountNode [destination.writable]
+|   instructionArgumentNode [discriminator]
+|   |   numberTypeNode [u32]
+|   instructionArgumentNode [amount]
+|   |   numberTypeNode [u64]
+|   fieldDiscriminatorNode [discriminator]`,
+);
+
+// Extra arguments.
+const nodeWithExtraArguments = instructionNode({
+    extraArguments: [
+        instructionArgumentNode({
+            name: 'myExtraArgument',
+            type: publicKeyTypeNode(),
+        }),
+    ],
+    name: 'myInstruction',
+});
+test('mergeVisitor: extraArguments', mergeVisitorMacro, nodeWithExtraArguments, 3);
+test('identityVisitor: extraArguments', identityVisitorMacro, nodeWithExtraArguments);
+
+// Remaining accounts.
+const nodeWithRemainingAccounts = instructionNode({
+    name: 'myInstruction',
+    remainingAccounts: [instructionRemainingAccountsNode(resolverValueNode('myResolver'))],
+});
+test('mergeVisitor: remainingAccounts', mergeVisitorMacro, nodeWithRemainingAccounts, 3);
+test('identityVisitor: remainingAccounts', identityVisitorMacro, nodeWithRemainingAccounts);
+
+// Byte deltas.
+const nodeWithByteDeltas = instructionNode({
+    byteDeltas: [instructionByteDeltaNode(numberValueNode(42))],
+    name: 'myInstruction',
+});
+test('mergeVisitor: byteDeltas', mergeVisitorMacro, nodeWithByteDeltas, 3);
+test('identityVisitor: byteDeltas', identityVisitorMacro, nodeWithByteDeltas);
+
+// Sub-instructions.
+const nodeWithSubInstructions = instructionNode({
+    name: 'myInstruction',
+    subInstructions: [instructionNode({ name: 'mySubInstruction1' }), instructionNode({ name: 'mySubInstruction2' })],
+});
+test('mergeVisitor: subInstructions', mergeVisitorMacro, nodeWithSubInstructions, 3);
+test('identityVisitor: subInstructions', identityVisitorMacro, nodeWithSubInstructions);

+ 26 - 0
packages/visitors-core/test/nodes/InstructionRemainingAccountsNode.test.ts

@@ -0,0 +1,26 @@
+import { argumentValueNode, instructionRemainingAccountsNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = instructionRemainingAccountsNode(argumentValueNode('remainingAccounts'), {
+    isSigner: 'either',
+    isWritable: true,
+});
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[instructionRemainingAccountsNode]', null);
+test(deleteNodesVisitorMacro, node, '[argumentValueNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+instructionRemainingAccountsNode [writable.optionalSigner]
+|   argumentValueNode [remainingAccounts]`,
+);

+ 47 - 0
packages/visitors-core/test/nodes/PdaNode.test.ts

@@ -0,0 +1,47 @@
+import {
+    constantPdaSeedNode,
+    numberTypeNode,
+    numberValueNode,
+    pdaNode,
+    publicKeyTypeNode,
+    variablePdaSeedNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = pdaNode({
+    name: 'associatedToken',
+    seeds: [
+        variablePdaSeedNode('owner', publicKeyTypeNode()),
+        constantPdaSeedNode(numberTypeNode('u8'), numberValueNode(123456)),
+        variablePdaSeedNode('mint', publicKeyTypeNode()),
+    ],
+});
+
+test(mergeVisitorMacro, node, 8);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[pdaNode]', null);
+test(deleteNodesVisitorMacro, node, ['[variablePdaSeedNode]', '[constantPdaSeedNode]'], { ...node, seeds: [] });
+test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', {
+    ...node,
+    seeds: [constantPdaSeedNode(numberTypeNode('u8'), numberValueNode(123456))],
+});
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+pdaNode [associatedToken]
+|   variablePdaSeedNode [owner]
+|   |   publicKeyTypeNode
+|   constantPdaSeedNode
+|   |   numberTypeNode [u8]
+|   |   numberValueNode [123456]
+|   variablePdaSeedNode [mint]
+|   |   publicKeyTypeNode`,
+);

+ 68 - 0
packages/visitors-core/test/nodes/ProgramNode.test.ts

@@ -0,0 +1,68 @@
+import {
+    accountNode,
+    definedTypeNode,
+    enumTypeNode,
+    errorNode,
+    instructionNode,
+    pdaNode,
+    programNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = programNode({
+    accounts: [
+        accountNode({ data: structTypeNode([]), name: 'mint' }),
+        accountNode({ data: structTypeNode([]), name: 'token' }),
+    ],
+    definedTypes: [definedTypeNode({ name: 'tokenState', type: enumTypeNode([]) })],
+    errors: [
+        errorNode({ code: 1, message: 'Invalid mint', name: 'invalidMint' }),
+        errorNode({ code: 2, message: 'Invalid token', name: 'invalidToken' }),
+    ],
+    instructions: [instructionNode({ name: 'mintTokens' }), instructionNode({ name: 'transferTokens' })],
+    name: 'splToken',
+    pdas: [pdaNode({ name: 'associatedToken', seeds: [] })],
+    publicKey: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
+    version: '1.2.3',
+});
+
+test(mergeVisitorMacro, node, 13);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[programNode]', null);
+test(deleteNodesVisitorMacro, node, '[pdaNode]', { ...node, pdas: [] });
+test(deleteNodesVisitorMacro, node, '[accountNode]', { ...node, accounts: [] });
+test(deleteNodesVisitorMacro, node, '[instructionNode]', {
+    ...node,
+    instructions: [],
+});
+test(deleteNodesVisitorMacro, node, '[definedTypeNode]', {
+    ...node,
+    definedTypes: [],
+});
+test(deleteNodesVisitorMacro, node, '[errorNode]', { ...node, errors: [] });
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+programNode [splToken.TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA]
+|   pdaNode [associatedToken]
+|   accountNode [mint]
+|   |   structTypeNode
+|   accountNode [token]
+|   |   structTypeNode
+|   instructionNode [mintTokens]
+|   instructionNode [transferTokens]
+|   definedTypeNode [tokenState]
+|   |   enumTypeNode
+|   |   |   numberTypeNode [u8]
+|   errorNode [1.invalidMint]
+|   errorNode [2.invalidToken]`,
+);

+ 39 - 0
packages/visitors-core/test/nodes/RootNode.test.ts

@@ -0,0 +1,39 @@
+import { programNode, rootNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from './_setup.js';
+
+const node = rootNode(
+    programNode({
+        name: 'splToken',
+        publicKey: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
+    }),
+    [
+        programNode({
+            name: 'splAddressLookupTable',
+            publicKey: 'AddressLookupTab1e1111111111111111111111111',
+        }),
+    ],
+);
+
+test(mergeVisitorMacro, node, 3);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[rootNode]', null);
+test(deleteNodesVisitorMacro, node, '[programNode]', null);
+test(deleteNodesVisitorMacro, node, '[programNode]splAddressLookupTable', {
+    ...node,
+    additionalPrograms: [],
+});
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+rootNode
+|   programNode [splToken.TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA]
+|   programNode [splAddressLookupTable.AddressLookupTab1e1111111111111111111111111]`,
+);

+ 62 - 0
packages/visitors-core/test/nodes/_setup.ts

@@ -0,0 +1,62 @@
+import type { Node } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitor,
+    getDebugStringVisitor,
+    identityVisitor,
+    mergeVisitor,
+    NodeSelector,
+    visit,
+} from '../../src/index.js';
+
+export const mergeVisitorMacro = test.macro({
+    exec(t, node: Node, expectedNodeCount: number) {
+        const visitor = mergeVisitor(
+            () => 1,
+            (_, values) => values.reduce((a, b) => a + b, 1),
+        );
+        const result = visit(node, visitor);
+        t.is(result, expectedNodeCount);
+    },
+    title: title => title ?? 'mergeVisitor',
+});
+
+export const identityVisitorMacro = test.macro({
+    exec(t, node: Node) {
+        const visitor = identityVisitor();
+        const result = visit(node, visitor);
+        t.deepEqual(result, node);
+        t.not(result, node);
+        t.true(Object.isFrozen(result));
+    },
+    title: title => title ?? 'identityVisitor',
+});
+
+export const deleteNodesVisitorMacro = test.macro({
+    exec(t, node: Node, selector: NodeSelector | NodeSelector[], expectedResult: Node | null) {
+        const selectors = Array.isArray(selector) ? selector : [selector];
+        const visitor = deleteNodesVisitor(selectors);
+        const result = visit(node, visitor);
+        if (expectedResult === null) {
+            t.is(result, null);
+        } else {
+            t.deepEqual(result, expectedResult);
+            t.not(result, expectedResult);
+            t.true(Object.isFrozen(result));
+        }
+    },
+    title(title, _node, selector: NodeSelector | NodeSelector[]) {
+        const selectors = Array.isArray(selector) ? selector : [selector];
+        return title ?? `deleteNodesVisitor: ${selectors.join(', ')}`;
+    },
+});
+
+export const getDebugStringVisitorMacro = test.macro({
+    exec(t, node: Node, expectedIndentedString: string) {
+        const visitor = getDebugStringVisitor({ indent: true });
+        const result = visit(node, visitor);
+        t.is(result, expectedIndentedString.trim());
+    },
+    title: title => title ?? 'getDebugStringVisitor',
+});

+ 16 - 0
packages/visitors-core/test/nodes/contextualValueNodes/AccountBumpValueNode.test.ts

@@ -0,0 +1,16 @@
+import { accountBumpValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = accountBumpValueNode('metadata');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[accountBumpValueNode]', null);
+test(getDebugStringVisitorMacro, node, `accountBumpValueNode [metadata]`);

+ 16 - 0
packages/visitors-core/test/nodes/contextualValueNodes/AccountValueNode.test.ts

@@ -0,0 +1,16 @@
+import { accountValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = accountValueNode('mint');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[accountValueNode]', null);
+test(getDebugStringVisitorMacro, node, `accountValueNode [mint]`);

+ 16 - 0
packages/visitors-core/test/nodes/contextualValueNodes/ArgumentValueNode.test.ts

@@ -0,0 +1,16 @@
+import { argumentValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = argumentValueNode('space');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[argumentValueNode]', null);
+test(getDebugStringVisitorMacro, node, `argumentValueNode [space]`);

+ 41 - 0
packages/visitors-core/test/nodes/contextualValueNodes/ConditionalValueNode.test.ts

@@ -0,0 +1,41 @@
+import {
+    accountValueNode,
+    argumentValueNode,
+    conditionalValueNode,
+    enumValueNode,
+    programIdValueNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = conditionalValueNode({
+    condition: argumentValueNode('tokenStandard'),
+    ifFalse: programIdValueNode(),
+    ifTrue: accountValueNode('mint'),
+    value: enumValueNode('tokenStandard', 'ProgrammableNonFungible'),
+});
+
+test(mergeVisitorMacro, node, 6);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[conditionalValueNode]', null);
+test(deleteNodesVisitorMacro, node, '[enumValueNode]', conditionalValueNode({ ...node, value: undefined }));
+test(deleteNodesVisitorMacro, node, '[accountValueNode]', conditionalValueNode({ ...node, ifTrue: undefined }));
+test(deleteNodesVisitorMacro, node, '[programIdValueNode]', conditionalValueNode({ ...node, ifFalse: undefined }));
+test(deleteNodesVisitorMacro, node, ['[accountValueNode]', '[programIdValueNode]'], null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+conditionalValueNode
+|   argumentValueNode [tokenStandard]
+|   enumValueNode [programmableNonFungible]
+|   |   definedTypeLinkNode [tokenStandard]
+|   accountValueNode [mint]
+|   programIdValueNode`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/contextualValueNodes/IdentityValueNode.test.ts

@@ -0,0 +1,16 @@
+import { identityValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = identityValueNode();
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[identityValueNode]', null);
+test(getDebugStringVisitorMacro, node, `identityValueNode`);

+ 16 - 0
packages/visitors-core/test/nodes/contextualValueNodes/PayerValueNode.test.ts

@@ -0,0 +1,16 @@
+import { payerValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = payerValueNode();
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[payerValueNode]', null);
+test(getDebugStringVisitorMacro, node, `payerValueNode`);

+ 23 - 0
packages/visitors-core/test/nodes/contextualValueNodes/PdaSeedValueNode.test.ts

@@ -0,0 +1,23 @@
+import { accountValueNode, pdaSeedValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = pdaSeedValueNode('mint', accountValueNode('mint'));
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[pdaSeedValueNode]', null);
+test(deleteNodesVisitorMacro, node, '[accountValueNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+pdaSeedValueNode [mint]
+|   accountValueNode [mint]`,
+);

+ 34 - 0
packages/visitors-core/test/nodes/contextualValueNodes/PdaValueNode.test.ts

@@ -0,0 +1,34 @@
+import { accountValueNode, pdaLinkNode, pdaSeedValueNode, pdaValueNode, publicKeyValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = pdaValueNode(pdaLinkNode('associatedToken'), [
+    pdaSeedValueNode('mint', accountValueNode('mint')),
+    pdaSeedValueNode('owner', publicKeyValueNode('8sphVBHQxufE4Jc1HMuWwWdKgoDjncQyPHwxYhfATRtF')),
+]);
+
+test(mergeVisitorMacro, node, 6);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[pdaValueNode]', null);
+test(deleteNodesVisitorMacro, node, '[pdaLinkNode]', null);
+test(deleteNodesVisitorMacro, node, '[pdaSeedValueNode]', {
+    ...node,
+    seeds: [],
+});
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+pdaValueNode
+|   pdaLinkNode [associatedToken]
+|   pdaSeedValueNode [mint]
+|   |   accountValueNode [mint]
+|   pdaSeedValueNode [owner]
+|   |   publicKeyValueNode [8sphVBHQxufE4Jc1HMuWwWdKgoDjncQyPHwxYhfATRtF]`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/contextualValueNodes/ProgramIdValueNode.test.ts

@@ -0,0 +1,16 @@
+import { programIdValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = programIdValueNode();
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[programIdValueNode]', null);
+test(getDebugStringVisitorMacro, node, `programIdValueNode`);

+ 27 - 0
packages/visitors-core/test/nodes/contextualValueNodes/ResolverValueNode.test.ts

@@ -0,0 +1,27 @@
+import { accountValueNode, argumentValueNode, resolverValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = resolverValueNode('myCustomResolver', {
+    dependsOn: [accountValueNode('mint'), argumentValueNode('tokenStandard')],
+    importFrom: 'hooked',
+});
+
+test(mergeVisitorMacro, node, 3);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[resolverValueNode]', null);
+test(deleteNodesVisitorMacro, node, ['[accountValueNode]', '[argumentValueNode]'], { ...node, dependsOn: undefined });
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+resolverValueNode [myCustomResolver.from:hooked]
+|   accountValueNode [mint]
+|   argumentValueNode [tokenStandard]`,
+);

+ 25 - 0
packages/visitors-core/test/nodes/discriminatorNodes/ConstantDiscriminatorNode.test.ts

@@ -0,0 +1,25 @@
+import { constantDiscriminatorNode, constantValueNodeFromBytes } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = constantDiscriminatorNode(constantValueNodeFromBytes('base16', '01020304'), 42);
+
+test(mergeVisitorMacro, node, 4);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[constantDiscriminatorNode]', null);
+test(deleteNodesVisitorMacro, node, '[constantValueNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+constantDiscriminatorNode [offset:42]
+|   constantValueNode
+|   |   bytesTypeNode
+|   |   bytesValueNode [base16.01020304]`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/discriminatorNodes/FieldDiscriminatorNode.test.ts

@@ -0,0 +1,16 @@
+import { fieldDiscriminatorNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = fieldDiscriminatorNode('discriminator', 42);
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[fieldDiscriminatorNode]', null);
+test(getDebugStringVisitorMacro, node, `fieldDiscriminatorNode [discriminator.offset:42]`);

+ 16 - 0
packages/visitors-core/test/nodes/discriminatorNodes/SizeDiscriminatorNode.test.ts

@@ -0,0 +1,16 @@
+import { sizeDiscriminatorNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = sizeDiscriminatorNode(42);
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[sizeDiscriminatorNode]', null);
+test(getDebugStringVisitorMacro, node, `sizeDiscriminatorNode [42]`);

+ 16 - 0
packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts

@@ -0,0 +1,16 @@
+import { accountLinkNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = accountLinkNode('token', 'splToken');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[accountLinkNode]', null);
+test(getDebugStringVisitorMacro, node, `accountLinkNode [token.from:splToken]`);

+ 16 - 0
packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts

@@ -0,0 +1,16 @@
+import { definedTypeLinkNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = definedTypeLinkNode('tokenState', 'splToken');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[definedTypeLinkNode]', null);
+test(getDebugStringVisitorMacro, node, `definedTypeLinkNode [tokenState.from:splToken]`);

+ 16 - 0
packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts

@@ -0,0 +1,16 @@
+import { pdaLinkNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = pdaLinkNode('associatedToken', 'splToken');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[pdaLinkNode]', null);
+test(getDebugStringVisitorMacro, node, `pdaLinkNode [associatedToken.from:splToken]`);

+ 16 - 0
packages/visitors-core/test/nodes/linkNodes/ProgramLinkNode.test.ts

@@ -0,0 +1,16 @@
+import { programLinkNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = programLinkNode('mplCandyGuard', 'mplCandyMachine');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[programLinkNode]', null);
+test(getDebugStringVisitorMacro, node, `programLinkNode [mplCandyGuard.from:mplCandyMachine]`);

+ 25 - 0
packages/visitors-core/test/nodes/pdaSeedNodes/ConstantPdaSeedNode.test.ts

@@ -0,0 +1,25 @@
+import { constantPdaSeedNode, numberTypeNode, numberValueNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = constantPdaSeedNode(numberTypeNode('u8'), numberValueNode(42));
+
+test(mergeVisitorMacro, node, 3);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[constantPdaSeedNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberValueNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+constantPdaSeedNode
+|   numberTypeNode [u8]
+|   numberValueNode [42]`,
+);

+ 23 - 0
packages/visitors-core/test/nodes/pdaSeedNodes/VariablePdaSeedNode.test.ts

@@ -0,0 +1,23 @@
+import { publicKeyTypeNode, variablePdaSeedNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = variablePdaSeedNode('mint', publicKeyTypeNode());
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[variablePdaSeedNode]', null);
+test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+variablePdaSeedNode [mint]
+|   publicKeyTypeNode`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/sizeNodes/FixedSizeNode.test.ts

@@ -0,0 +1,16 @@
+import { fixedCountNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = fixedCountNode(42);
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[fixedCountNode]', null);
+test(getDebugStringVisitorMacro, node, `fixedCountNode [42]`);

+ 23 - 0
packages/visitors-core/test/nodes/sizeNodes/PrefixedSizeNode.test.ts

@@ -0,0 +1,23 @@
+import { numberTypeNode, prefixedCountNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = prefixedCountNode(numberTypeNode('u64'));
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[prefixedCountNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+prefixedCountNode
+|   numberTypeNode [u64]`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/sizeNodes/RemainderSizeNode.test.ts

@@ -0,0 +1,16 @@
+import { remainderCountNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = remainderCountNode();
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[remainderCountNode]', null);
+test(getDebugStringVisitorMacro, node, `remainderCountNode`);

+ 23 - 0
packages/visitors-core/test/nodes/typeNodes/AmountTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { amountTypeNode, numberTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = amountTypeNode(numberTypeNode('u64'), 9, 'SOL');
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[amountTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+amountTypeNode [9.SOL]
+|   numberTypeNode [u64]`,
+);

+ 27 - 0
packages/visitors-core/test/nodes/typeNodes/ArrayTypeNode.test.ts

@@ -0,0 +1,27 @@
+import { arrayTypeNode, numberTypeNode, prefixedCountNode, publicKeyTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = arrayTypeNode(publicKeyTypeNode(), prefixedCountNode(numberTypeNode('u64')));
+
+test(mergeVisitorMacro, node, 4);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[arrayTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[prefixedCountNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+arrayTypeNode
+|   prefixedCountNode
+|   |   numberTypeNode [u64]
+|   publicKeyTypeNode`,
+);

+ 23 - 0
packages/visitors-core/test/nodes/typeNodes/BooleanTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { booleanTypeNode, numberTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = booleanTypeNode(numberTypeNode('u32'));
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[booleanTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+booleanTypeNode
+|   numberTypeNode [u32]`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/typeNodes/BytesTypeNode.test.ts

@@ -0,0 +1,16 @@
+import { bytesTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = bytesTypeNode();
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[bytesTypeNode]', null);
+test(getDebugStringVisitorMacro, node, `bytesTypeNode`);

+ 23 - 0
packages/visitors-core/test/nodes/typeNodes/DateTimeTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { dateTimeTypeNode, numberTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = dateTimeTypeNode(numberTypeNode('u64'));
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[dateTimeTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+dateTimeTypeNode
+|   numberTypeNode [u64]`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/typeNodes/EnumEmptyVariantTypeNode.test.ts

@@ -0,0 +1,16 @@
+import { enumEmptyVariantTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = enumEmptyVariantTypeNode('initialized');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[enumEmptyVariantTypeNode]', null);
+test(getDebugStringVisitorMacro, node, `enumEmptyVariantTypeNode [initialized]`);

+ 40 - 0
packages/visitors-core/test/nodes/typeNodes/EnumStructVariantTypeNode.test.ts

@@ -0,0 +1,40 @@
+import {
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    numberTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = enumStructVariantTypeNode(
+    'mouseClick',
+    structTypeNode([
+        structFieldTypeNode({ name: 'x', type: numberTypeNode('u32') }),
+        structFieldTypeNode({ name: 'y', type: numberTypeNode('u32') }),
+    ]),
+);
+
+test(mergeVisitorMacro, node, 6);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[enumStructVariantTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[structTypeNode]', enumEmptyVariantTypeNode('mouseClick'));
+test(deleteNodesVisitorMacro, node, '[structFieldTypeNode]', enumEmptyVariantTypeNode('mouseClick'));
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+enumStructVariantTypeNode [mouseClick]
+|   structTypeNode
+|   |   structFieldTypeNode [x]
+|   |   |   numberTypeNode [u32]
+|   |   structFieldTypeNode [y]
+|   |   |   numberTypeNode [u32]`,
+);

+ 26 - 0
packages/visitors-core/test/nodes/typeNodes/EnumTupleVariantTypeNode.test.ts

@@ -0,0 +1,26 @@
+import { enumEmptyVariantTypeNode, enumTupleVariantTypeNode, numberTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = enumTupleVariantTypeNode('coordinates', tupleTypeNode([numberTypeNode('u32'), numberTypeNode('u32')]));
+
+test(mergeVisitorMacro, node, 4);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[enumTupleVariantTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[tupleTypeNode]', enumEmptyVariantTypeNode('coordinates'));
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', enumEmptyVariantTypeNode('coordinates'));
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+enumTupleVariantTypeNode [coordinates]
+|   tupleTypeNode
+|   |   numberTypeNode [u32]
+|   |   numberTypeNode [u32]`,
+);

+ 67 - 0
packages/visitors-core/test/nodes/typeNodes/EnumTypeNode.test.ts

@@ -0,0 +1,67 @@
+import {
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTupleVariantTypeNode,
+    enumTypeNode,
+    fixedSizeTypeNode,
+    numberTypeNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    tupleTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = enumTypeNode(
+    [
+        enumEmptyVariantTypeNode('quit'),
+        enumTupleVariantTypeNode('write', tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 32)])),
+        enumStructVariantTypeNode(
+            'move',
+            structTypeNode([
+                structFieldTypeNode({ name: 'x', type: numberTypeNode('u32') }),
+                structFieldTypeNode({ name: 'y', type: numberTypeNode('u32') }),
+            ]),
+        ),
+    ],
+    { size: numberTypeNode('u64') },
+);
+
+test(mergeVisitorMacro, node, 13);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[enumTypeNode]', null);
+test(
+    deleteNodesVisitorMacro,
+    node,
+    ['[enumEmptyVariantTypeNode]', '[enumTupleVariantTypeNode]', '[enumStructVariantTypeNode]'],
+    { ...node, variants: [] },
+);
+test(deleteNodesVisitorMacro, node, ['[tupleTypeNode]', '[structFieldTypeNode]'], {
+    ...node,
+    variants: [enumEmptyVariantTypeNode('quit'), enumEmptyVariantTypeNode('write'), enumEmptyVariantTypeNode('move')],
+});
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+enumTypeNode
+|   numberTypeNode [u64]
+|   enumEmptyVariantTypeNode [quit]
+|   enumTupleVariantTypeNode [write]
+|   |   tupleTypeNode
+|   |   |   fixedSizeTypeNode [32]
+|   |   |   |   stringTypeNode [utf8]
+|   enumStructVariantTypeNode [move]
+|   |   structTypeNode
+|   |   |   structFieldTypeNode [x]
+|   |   |   |   numberTypeNode [u32]
+|   |   |   structFieldTypeNode [y]
+|   |   |   |   numberTypeNode [u32]`,
+);

+ 23 - 0
packages/visitors-core/test/nodes/typeNodes/FixedSizeTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { fixedSizeTypeNode, stringTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = fixedSizeTypeNode(stringTypeNode('utf8'), 42);
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[stringTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[fixedSizeTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+fixedSizeTypeNode [42]
+|   stringTypeNode [utf8]`,
+);

+ 45 - 0
packages/visitors-core/test/nodes/typeNodes/HiddenPrefixTypeNode.test.ts

@@ -0,0 +1,45 @@
+import {
+    constantValueNodeFromBytes,
+    constantValueNodeFromString,
+    hiddenPrefixTypeNode,
+    numberTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = hiddenPrefixTypeNode(numberTypeNode('u32'), [
+    constantValueNodeFromString('utf8', 'hello world'),
+    constantValueNodeFromBytes('base16', 'ffff'),
+]);
+
+test(mergeVisitorMacro, node, 8);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[hiddenPrefixTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[constantValueNode]', numberTypeNode('u32'));
+test(
+    deleteNodesVisitorMacro,
+    node,
+    '[stringTypeNode]',
+    hiddenPrefixTypeNode(numberTypeNode('u32'), [constantValueNodeFromBytes('base16', 'ffff')]),
+);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+hiddenPrefixTypeNode
+|   constantValueNode
+|   |   stringTypeNode [utf8]
+|   |   stringValueNode [hello world]
+|   constantValueNode
+|   |   bytesTypeNode
+|   |   bytesValueNode [base16.ffff]
+|   numberTypeNode [u32]
+`,
+);

+ 45 - 0
packages/visitors-core/test/nodes/typeNodes/HiddenSuffixTypeNode.test.ts

@@ -0,0 +1,45 @@
+import {
+    constantValueNodeFromBytes,
+    constantValueNodeFromString,
+    hiddenSuffixTypeNode,
+    numberTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = hiddenSuffixTypeNode(numberTypeNode('u32'), [
+    constantValueNodeFromString('utf8', 'hello world'),
+    constantValueNodeFromBytes('base16', 'ffff'),
+]);
+
+test(mergeVisitorMacro, node, 8);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[hiddenSuffixTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[constantValueNode]', numberTypeNode('u32'));
+test(
+    deleteNodesVisitorMacro,
+    node,
+    '[stringTypeNode]',
+    hiddenSuffixTypeNode(numberTypeNode('u32'), [constantValueNodeFromBytes('base16', 'ffff')]),
+);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+hiddenSuffixTypeNode
+|   numberTypeNode [u32]
+|   constantValueNode
+|   |   stringTypeNode [utf8]
+|   |   stringValueNode [hello world]
+|   constantValueNode
+|   |   bytesTypeNode
+|   |   bytesValueNode [base16.ffff]
+`,
+);

+ 40 - 0
packages/visitors-core/test/nodes/typeNodes/MapTypeNode.test.ts

@@ -0,0 +1,40 @@
+import {
+    fixedSizeTypeNode,
+    mapTypeNode,
+    numberTypeNode,
+    prefixedCountNode,
+    publicKeyTypeNode,
+    stringTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = mapTypeNode(
+    fixedSizeTypeNode(stringTypeNode('utf8'), 32),
+    publicKeyTypeNode(),
+    prefixedCountNode(numberTypeNode('u8')),
+);
+
+test(mergeVisitorMacro, node, 6);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[mapTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[stringTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[prefixedCountNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+ mapTypeNode
+|   prefixedCountNode
+|   |   numberTypeNode [u8]
+|   fixedSizeTypeNode [32]
+|   |   stringTypeNode [utf8]
+|   publicKeyTypeNode`,
+);

+ 22 - 0
packages/visitors-core/test/nodes/typeNodes/NumberTypeNode.test.ts

@@ -0,0 +1,22 @@
+import { numberTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = numberTypeNode('f64');
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(getDebugStringVisitorMacro, node, `numberTypeNode [f64]`);
+test(
+    'getDebugStringVisitor: bigEndian',
+    getDebugStringVisitorMacro,
+    numberTypeNode('f64', 'be'),
+    `numberTypeNode [f64.bigEndian]`,
+);

+ 28 - 0
packages/visitors-core/test/nodes/typeNodes/OptionTypeNode.test.ts

@@ -0,0 +1,28 @@
+import { numberTypeNode, optionTypeNode, publicKeyTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = optionTypeNode(publicKeyTypeNode(), {
+    fixed: true,
+    prefix: numberTypeNode('u64'),
+});
+
+test(mergeVisitorMacro, node, 3);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[optionTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[numberTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+optionTypeNode [fixed]
+|   numberTypeNode [u64]
+|   publicKeyTypeNode`,
+);

+ 33 - 0
packages/visitors-core/test/nodes/typeNodes/PostOffsetTypeNode.test.ts

@@ -0,0 +1,33 @@
+import { postOffsetTypeNode, stringTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = postOffsetTypeNode(stringTypeNode('utf8'), 42);
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[stringTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[postOffsetTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+postOffsetTypeNode [42.relative]
+|   stringTypeNode [utf8]`,
+);
+
+// Different strategy.
+test(
+    'getDebugStringVisitor: different strategy',
+    getDebugStringVisitorMacro,
+    postOffsetTypeNode(stringTypeNode('utf8'), 42, 'absolute'),
+    `
+postOffsetTypeNode [42.absolute]
+|   stringTypeNode [utf8]`,
+);

+ 33 - 0
packages/visitors-core/test/nodes/typeNodes/PreOffsetTypeNode.test.ts

@@ -0,0 +1,33 @@
+import { preOffsetTypeNode, stringTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = preOffsetTypeNode(stringTypeNode('utf8'), 42);
+
+test(mergeVisitorMacro, node, 2);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[stringTypeNode]', null);
+test(deleteNodesVisitorMacro, node, '[preOffsetTypeNode]', null);
+test(
+    getDebugStringVisitorMacro,
+    node,
+    `
+preOffsetTypeNode [42.relative]
+|   stringTypeNode [utf8]`,
+);
+
+// Different strategy.
+test(
+    'getDebugStringVisitor: different strategy',
+    getDebugStringVisitorMacro,
+    preOffsetTypeNode(stringTypeNode('utf8'), 42, 'absolute'),
+    `
+preOffsetTypeNode [42.absolute]
+|   stringTypeNode [utf8]`,
+);

+ 16 - 0
packages/visitors-core/test/nodes/typeNodes/PublicKeyTypeNode.test.ts

@@ -0,0 +1,16 @@
+import { publicKeyTypeNode } from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    deleteNodesVisitorMacro,
+    getDebugStringVisitorMacro,
+    identityVisitorMacro,
+    mergeVisitorMacro,
+} from '../_setup.js';
+
+const node = publicKeyTypeNode();
+
+test(mergeVisitorMacro, node, 1);
+test(identityVisitorMacro, node);
+test(deleteNodesVisitorMacro, node, '[publicKeyTypeNode]', null);
+test(getDebugStringVisitorMacro, node, `publicKeyTypeNode`);

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików