Browse Source

[0.22] Remove `importFrom` attribute from link nodes and resolvers (#175)

* Remove importFrom attribute from link nodes and resolvers

* Add tests

* Add more tests

* Add more packages to minor bump
Loris Leiva 1 năm trước cách đây
mục cha
commit
2b1259b566
71 tập tin đã thay đổi với 831 bổ sung242 xóa
  1. 17 0
      .changeset/moody-scissors-beg.md
  2. 1 2
      packages/node-types/src/contextualValueNodes/ResolverValueNode.ts
  3. 1 2
      packages/node-types/src/linkNodes/AccountLinkNode.ts
  4. 1 2
      packages/node-types/src/linkNodes/DefinedTypeLinkNode.ts
  5. 1 2
      packages/node-types/src/linkNodes/PdaLinkNode.ts
  6. 1 2
      packages/node-types/src/linkNodes/ProgramLinkNode.ts
  7. 0 10
      packages/node-types/src/shared/importFrom.ts
  8. 0 1
      packages/node-types/src/shared/index.ts
  9. 5 6
      packages/nodes/docs/contextualValueNodes/ResolverValueNode.md
  10. 5 6
      packages/nodes/docs/linkNodes/AccountLinkNode.md
  11. 5 6
      packages/nodes/docs/linkNodes/DefinedTypeLinkNode.md
  12. 5 6
      packages/nodes/docs/linkNodes/PdaLinkNode.md
  13. 5 6
      packages/nodes/docs/linkNodes/ProgramLinkNode.md
  14. 1 3
      packages/nodes/src/contextualValueNodes/ResolverValueNode.ts
  15. 2 3
      packages/nodes/src/linkNodes/AccountLinkNode.ts
  16. 2 3
      packages/nodes/src/linkNodes/DefinedTypeLinkNode.ts
  17. 2 3
      packages/nodes/src/linkNodes/PdaLinkNode.ts
  18. 2 3
      packages/nodes/src/linkNodes/ProgramLinkNode.ts
  19. 13 12
      packages/renderers-js-umi/README.md
  20. 6 8
      packages/renderers-js-umi/src/ImportMap.ts
  21. 12 14
      packages/renderers-js-umi/src/getRenderMapVisitor.ts
  22. 5 4
      packages/renderers-js-umi/src/getTypeManifestVisitor.ts
  23. 10 6
      packages/renderers-js-umi/src/renderInstructionDefaults.ts
  24. 3 4
      packages/renderers-js-umi/src/utils/customData.ts
  25. 1 0
      packages/renderers-js-umi/src/utils/index.ts
  26. 56 0
      packages/renderers-js-umi/src/utils/linkOverrides.ts
  27. 24 8
      packages/renderers-js-umi/test/_setup.ts
  28. 1 1
      packages/renderers-js-umi/test/instructionsPage.test.ts
  29. 15 14
      packages/renderers-js/README.md
  30. 9 11
      packages/renderers-js/src/ImportMap.ts
  31. 3 0
      packages/renderers-js/src/fragments/accountPdaHelpers.ts
  32. 3 4
      packages/renderers-js/src/fragments/common.ts
  33. 6 7
      packages/renderers-js/src/fragments/instructionByteDelta.ts
  34. 4 1
      packages/renderers-js/src/fragments/instructionFunction.ts
  35. 7 8
      packages/renderers-js/src/fragments/instructionInputDefault.ts
  36. 1 1
      packages/renderers-js/src/fragments/instructionInputResolved.ts
  37. 4 4
      packages/renderers-js/src/fragments/instructionRemainingAccounts.ts
  38. 9 2
      packages/renderers-js/src/getRenderMapVisitor.ts
  39. 5 4
      packages/renderers-js/src/getTypeManifestVisitor.ts
  40. 3 4
      packages/renderers-js/src/utils/customData.ts
  41. 1 0
      packages/renderers-js/src/utils/index.ts
  42. 56 0
      packages/renderers-js/src/utils/linkOverrides.ts
  43. 44 1
      packages/renderers-js/test/accountsPage.test.ts
  44. 40 0
      packages/renderers-js/test/instructionsPage.test.ts
  45. 65 0
      packages/renderers-js/test/links/account.test.ts
  46. 93 0
      packages/renderers-js/test/links/definedType.test.ts
  47. 86 0
      packages/renderers-js/test/links/pda.test.ts
  48. 92 0
      packages/renderers-js/test/links/program.test.ts
  49. 9 8
      packages/renderers-rust/README.md
  50. 2 4
      packages/renderers-rust/src/ImportMap.ts
  51. 12 6
      packages/renderers-rust/src/getRenderMapVisitor.ts
  52. 8 3
      packages/renderers-rust/src/getTypeManifestVisitor.ts
  53. 8 4
      packages/renderers-rust/src/renderValueNodeVisitor.ts
  54. 1 0
      packages/renderers-rust/src/utils/index.ts
  55. 44 0
      packages/renderers-rust/src/utils/linkOverrides.ts
  56. 1 1
      packages/validators/src/getValidationItemsVisitor.ts
  57. 0 6
      packages/visitors-core/src/LinkableDictionary.ts
  58. 0 2
      packages/visitors-core/src/getByteSizeVisitor.ts
  59. 2 2
      packages/visitors-core/src/getDebugStringVisitor.ts
  60. 1 2
      packages/visitors-core/test/nodes/contextualValueNodes/ResolverValueNode.test.ts
  61. 2 2
      packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts
  62. 2 2
      packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts
  63. 2 2
      packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts
  64. 2 2
      packages/visitors-core/test/nodes/linkNodes/ProgramLinkNode.test.ts
  65. 0 4
      packages/visitors/src/getDefinedTypeHistogramVisitor.ts
  66. 1 1
      packages/visitors/src/unwrapDefinedTypesVisitor.ts
  67. 0 1
      packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts
  68. 0 1
      packages/visitors/src/unwrapTypeDefinedLinksVisitor.ts
  69. 1 3
      packages/visitors/src/updateAccountsVisitor.ts
  70. 0 1
      packages/visitors/src/updateDefinedTypesVisitor.ts
  71. 0 1
      packages/visitors/src/updateProgramsVisitor.ts

+ 17 - 0
.changeset/moody-scissors-beg.md

@@ -0,0 +1,17 @@
+---
+'@kinobi-so/renderers': minor
+'@kinobi-so/renderers-core': minor
+'@kinobi-so/renderers-js-umi': minor
+'@kinobi-so/renderers-rust': minor
+'@kinobi-so/visitors-core': minor
+'@kinobi-so/renderers-js': minor
+'@kinobi-so/node-from-anchor': minor
+'@kinobi-so/node-types': minor
+'@kinobi-so/validators': minor
+'@kinobi-so/visitors': minor
+'@kinobi-so/nodes': minor
+---
+
+Remove `importFrom` attributes from link nodes and resolvers
+
+Instead, a new `linkOverrides` attribute is introduced on all renderers to redirect a link node or a resolver to a custom path or module.

+ 1 - 2
packages/node-types/src/contextualValueNodes/ResolverValueNode.ts

@@ -1,4 +1,4 @@
-import type { CamelCaseString, Docs, ImportFrom } from '../shared';
+import type { CamelCaseString, Docs } from '../shared';
 import type { AccountValueNode } from './AccountValueNode';
 import type { ArgumentValueNode } from './ArgumentValueNode';
 
@@ -9,7 +9,6 @@ export interface ResolverValueNode<
 
     // Data.
     readonly name: CamelCaseString;
-    readonly importFrom?: ImportFrom;
     readonly docs: Docs;
 
     // Children.

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

@@ -1,9 +1,8 @@
-import type { CamelCaseString, ImportFrom } from '../shared';
+import type { CamelCaseString } from '../shared';
 
 export interface AccountLinkNode {
     readonly kind: 'accountLinkNode';
 
     // Data.
     readonly name: CamelCaseString;
-    readonly importFrom?: ImportFrom;
 }

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

@@ -1,9 +1,8 @@
-import type { CamelCaseString, ImportFrom } from '../shared';
+import type { CamelCaseString } from '../shared';
 
 export interface DefinedTypeLinkNode {
     readonly kind: 'definedTypeLinkNode';
 
     // Data.
     readonly name: CamelCaseString;
-    readonly importFrom?: ImportFrom;
 }

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

@@ -1,9 +1,8 @@
-import type { CamelCaseString, ImportFrom } from '../shared';
+import type { CamelCaseString } from '../shared';
 
 export interface PdaLinkNode {
     readonly kind: 'pdaLinkNode';
 
     // Data.
     readonly name: CamelCaseString;
-    readonly importFrom?: ImportFrom;
 }

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

@@ -1,9 +1,8 @@
-import type { CamelCaseString, ImportFrom } from '../shared';
+import type { CamelCaseString } from '../shared';
 
 export interface ProgramLinkNode {
     readonly kind: 'programLinkNode';
 
     // Data.
     readonly name: CamelCaseString;
-    readonly importFrom?: ImportFrom;
 }

+ 0 - 10
packages/node-types/src/shared/importFrom.ts

@@ -1,10 +0,0 @@
-/**
- * Tells Kinobi where a dependency is located.
- *
- * This could be a `hooked` dependency, or any other arbitrary dependency
- * as long as renderers know how to map them into real libraries.
- *
- * `hooked` means the dependency is located in the source code but outside of the `generated` code.
- * This is typically a dedicated folder that is used internally by the auto-generated code.
- */
-export type ImportFrom = string | 'hooked';

+ 0 - 1
packages/node-types/src/shared/index.ts

@@ -1,5 +1,4 @@
 export * from './bytesEncoding';
 export * from './docs';
-export * from './importFrom';
 export * from './stringCases';
 export * from './version';

+ 5 - 6
packages/nodes/docs/contextualValueNodes/ResolverValueNode.md

@@ -8,12 +8,11 @@ This node acts as a fallback node for any value or logic that cannot easily be d
 
 ### Data
 
-| Attribute    | Type                  | Description                                                                                                                                                                                                                                                                                                                        |
-| ------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `kind`       | `"resolverValueNode"` | The node discriminator.                                                                                                                                                                                                                                                                                                            |
-| `name`       | `CamelCaseString`     | A unique name for the resolver. This will typically be the name of the function that the renderers will try to invoke.                                                                                                                                                                                                             |
-| `docs`       | `string[]`            | Detailed Markdown documentation for the resolver.                                                                                                                                                                                                                                                                                  |
-| `importFrom` | `string`              | (Optional) A unique string that identifies the module from which the resolver should be imported. Defaults to `"hooked"` meaning the function is locally available next to the generated code. Note that this is a renderer-specific piece of data that may be removed in the future and pushed to the renderers' options instead. |
+| Attribute | Type                  | Description                                                                                                            |
+| --------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `kind`    | `"resolverValueNode"` | The node discriminator.                                                                                                |
+| `name`    | `CamelCaseString`     | A unique name for the resolver. This will typically be the name of the function that the renderers will try to invoke. |
+| `docs`    | `string[]`            | Detailed Markdown documentation for the resolver.                                                                      |
 
 ### Children
 

+ 5 - 6
packages/nodes/docs/linkNodes/AccountLinkNode.md

@@ -6,11 +6,10 @@ This node represents a reference to an existing [`AccountNode`](../AccountNode.m
 
 ### Data
 
-| Attribute    | Type                | Description                                                                                                                                                                                                                                                                                                                                                                                                                       |
-| ------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `kind`       | `"accountLinkNode"` | The node discriminator.                                                                                                                                                                                                                                                                                                                                                                                                           |
-| `name`       | `CamelCaseString`   | The name of the [`AccountNode`](../AccountNode.md) we are referring to.                                                                                                                                                                                                                                                                                                                                                           |
-| `importFrom` | `CamelCaseString`   | (Optional) The reference of the module we are importing from or `"hooked"` if the account is defined in the same module but manually written when rendering clients. Default to referrencing directly from the Kinobi IDL instead of using an external referrence. Note that this information is only used for rendering clients. This, it will likely be removed from the Kinobi IDL and pushed to the renderer options instead. |
+| Attribute | Type                | Description                                                             |
+| --------- | ------------------- | ----------------------------------------------------------------------- |
+| `kind`    | `"accountLinkNode"` | The node discriminator.                                                 |
+| `name`    | `CamelCaseString`   | The name of the [`AccountNode`](../AccountNode.md) we are referring to. |
 
 ### Children
 
@@ -18,7 +17,7 @@ _This node has no children._
 
 ## Functions
 
-### `accountLinkNode(name, importFrom?)`
+### `accountLinkNode(name)`
 
 Helper function that creates a `AccountLinkNode` object from the name of the `AccountNode` we are referring to.
 

+ 5 - 6
packages/nodes/docs/linkNodes/DefinedTypeLinkNode.md

@@ -6,11 +6,10 @@ This node represents a reference to an existing [`DefinedTypeNode`](../DefinedTy
 
 ### Data
 
-| Attribute    | Type                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                    |
-| ------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `kind`       | `"definedTypeLinkNode"` | The node discriminator.                                                                                                                                                                                                                                                                                                                                                                                                        |
-| `name`       | `CamelCaseString`       | The name of the [`DefinedTypeNode`](../DefinedTypeNode.md) we are referring to.                                                                                                                                                                                                                                                                                                                                                |
-| `importFrom` | `CamelCaseString`       | (Optional) The reference of the module we are importing from or `"hooked"` if the type is defined in the same module but manually written when rendering clients. Default to referrencing directly from the Kinobi IDL instead of using an external referrence. Note that this information is only used for rendering clients. This, it will likely be removed from the Kinobi IDL and pushed to the renderer options instead. |
+| Attribute | Type                    | Description                                                                     |
+| --------- | ----------------------- | ------------------------------------------------------------------------------- |
+| `kind`    | `"definedTypeLinkNode"` | The node discriminator.                                                         |
+| `name`    | `CamelCaseString`       | The name of the [`DefinedTypeNode`](../DefinedTypeNode.md) we are referring to. |
 
 ### Children
 
@@ -18,7 +17,7 @@ _This node has no children._
 
 ## Functions
 
-### `definedTypeLinkNode(name, importFrom?)`
+### `definedTypeLinkNode(name)`
 
 Helper function that creates a `DefinedTypeLinkNode` object from the name of the `DefinedTypeNode` we are referring to.
 

+ 5 - 6
packages/nodes/docs/linkNodes/PdaLinkNode.md

@@ -6,11 +6,10 @@ This node represents a reference to an existing [`PdaNode`](../PdaNode.md) in th
 
 ### Data
 
-| Attribute    | Type              | Description                                                                                                                                                                                                                                                                                                                                                                                                                   |
-| ------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `kind`       | `"pdaLinkNode"`   | The node discriminator.                                                                                                                                                                                                                                                                                                                                                                                                       |
-| `name`       | `CamelCaseString` | The name of the [`PdaNode`](../PdaNode.md) we are referring to.                                                                                                                                                                                                                                                                                                                                                               |
-| `importFrom` | `CamelCaseString` | (Optional) The reference of the module we are importing from or `"hooked"` if the PDA is defined in the same module but manually written when rendering clients. Default to referrencing directly from the Kinobi IDL instead of using an external referrence. Note that this information is only used for rendering clients. This, it will likely be removed from the Kinobi IDL and pushed to the renderer options instead. |
+| Attribute | Type              | Description                                                     |
+| --------- | ----------------- | --------------------------------------------------------------- |
+| `kind`    | `"pdaLinkNode"`   | The node discriminator.                                         |
+| `name`    | `CamelCaseString` | The name of the [`PdaNode`](../PdaNode.md) we are referring to. |
 
 ### Children
 
@@ -18,7 +17,7 @@ _This node has no children._
 
 ## Functions
 
-### `pdaLinkNode(name, importFrom?)`
+### `pdaLinkNode(name)`
 
 Helper function that creates a `PdaLinkNode` object from the name of the `PdaNode` we are referring to.
 

+ 5 - 6
packages/nodes/docs/linkNodes/ProgramLinkNode.md

@@ -6,11 +6,10 @@ This node represents a reference to an existing [`ProgramNode`](../ProgramNode.m
 
 ### Data
 
-| Attribute    | Type                | Description                                                                                                                                                                                                                                                                                                                                                                                                                       |
-| ------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `kind`       | `"programLinkNode"` | The node discriminator.                                                                                                                                                                                                                                                                                                                                                                                                           |
-| `name`       | `CamelCaseString`   | The name of the [`ProgramNode`](../ProgramNode.md) we are referring to.                                                                                                                                                                                                                                                                                                                                                           |
-| `importFrom` | `CamelCaseString`   | (Optional) The reference of the module we are importing from or `"hooked"` if the program is defined in the same module but manually written when rendering clients. Default to referrencing directly from the Kinobi IDL instead of using an external referrence. Note that this information is only used for rendering clients. This, it will likely be removed from the Kinobi IDL and pushed to the renderer options instead. |
+| Attribute | Type                | Description                                                             |
+| --------- | ------------------- | ----------------------------------------------------------------------- |
+| `kind`    | `"programLinkNode"` | The node discriminator.                                                 |
+| `name`    | `CamelCaseString`   | The name of the [`ProgramNode`](../ProgramNode.md) we are referring to. |
 
 ### Children
 
@@ -18,7 +17,7 @@ _This node has no children._
 
 ## Functions
 
-### `programLinkNode(name, importFrom?)`
+### `programLinkNode(name)`
 
 Helper function that creates a `ProgramLinkNode` object from the name of the `ProgramNode` we are referring to.
 

+ 1 - 3
packages/nodes/src/contextualValueNodes/ResolverValueNode.ts

@@ -1,4 +1,4 @@
-import type { AccountValueNode, ArgumentValueNode, ImportFrom, ResolverValueNode } from '@kinobi-so/node-types';
+import type { AccountValueNode, ArgumentValueNode, ResolverValueNode } from '@kinobi-so/node-types';
 
 import { camelCase, DocsInput, parseDocs } from '../shared';
 
@@ -7,7 +7,6 @@ export function resolverValueNode<const TDependsOn extends (AccountValueNode | A
     options: {
         dependsOn?: TDependsOn;
         docs?: DocsInput;
-        importFrom?: ImportFrom;
     } = {},
 ): ResolverValueNode<TDependsOn> {
     return Object.freeze({
@@ -15,7 +14,6 @@ export function resolverValueNode<const TDependsOn extends (AccountValueNode | A
 
         // Data.
         name: camelCase(name),
-        ...(options.importFrom && { importFrom: options.importFrom }),
         docs: parseDocs(options.docs),
 
         // Children.

+ 2 - 3
packages/nodes/src/linkNodes/AccountLinkNode.ts

@@ -1,13 +1,12 @@
-import type { AccountLinkNode, ImportFrom } from '@kinobi-so/node-types';
+import type { AccountLinkNode } from '@kinobi-so/node-types';
 
 import { camelCase } from '../shared';
 
-export function accountLinkNode(name: string, importFrom?: ImportFrom): AccountLinkNode {
+export function accountLinkNode(name: string): AccountLinkNode {
     return Object.freeze({
         kind: 'accountLinkNode',
 
         // Data.
         name: camelCase(name),
-        importFrom,
     });
 }

+ 2 - 3
packages/nodes/src/linkNodes/DefinedTypeLinkNode.ts

@@ -1,13 +1,12 @@
-import type { DefinedTypeLinkNode, ImportFrom } from '@kinobi-so/node-types';
+import type { DefinedTypeLinkNode } from '@kinobi-so/node-types';
 
 import { camelCase } from '../shared';
 
-export function definedTypeLinkNode(name: string, importFrom?: ImportFrom): DefinedTypeLinkNode {
+export function definedTypeLinkNode(name: string): DefinedTypeLinkNode {
     return Object.freeze({
         kind: 'definedTypeLinkNode',
 
         // Data.
         name: camelCase(name),
-        importFrom,
     });
 }

+ 2 - 3
packages/nodes/src/linkNodes/PdaLinkNode.ts

@@ -1,13 +1,12 @@
-import type { ImportFrom, PdaLinkNode } from '@kinobi-so/node-types';
+import type { PdaLinkNode } from '@kinobi-so/node-types';
 
 import { camelCase } from '../shared';
 
-export function pdaLinkNode(name: string, importFrom?: ImportFrom): PdaLinkNode {
+export function pdaLinkNode(name: string): PdaLinkNode {
     return Object.freeze({
         kind: 'pdaLinkNode',
 
         // Data.
         name: camelCase(name),
-        importFrom,
     });
 }

+ 2 - 3
packages/nodes/src/linkNodes/ProgramLinkNode.ts

@@ -1,13 +1,12 @@
-import type { ImportFrom, ProgramLinkNode } from '@kinobi-so/node-types';
+import type { ProgramLinkNode } from '@kinobi-so/node-types';
 
 import { camelCase } from '../shared';
 
-export function programLinkNode(name: string, importFrom?: ImportFrom): ProgramLinkNode {
+export function programLinkNode(name: string): ProgramLinkNode {
     return Object.freeze({
         kind: 'programLinkNode',
 
         // Data.
         name: camelCase(name),
-        importFrom,
     });
 }

+ 13 - 12
packages/renderers-js-umi/README.md

@@ -37,15 +37,16 @@ kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 
 The `renderVisitor` accepts the following options.
 
-| Name                          | Type                                                | Default   | Description                                                                                                                                                                                  |
-| ----------------------------- | --------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `deleteFolderBeforeRendering` | `boolean`                                           | `true`    | Whether the base directory should be cleaned before generating new files.                                                                                                                    |
-| `formatCode`                  | `boolean`                                           | `true`    | Whether we should use Prettier to format the generated code.                                                                                                                                 |
-| `prettierOptions`             | `PrettierOptions`                                   | `{}`      | The options to use when formatting the code using Prettier.                                                                                                                                  |
-| `throwLevel`                  | `'debug' \| 'trace' \| 'info' \| 'warn' \| 'error'` | `'error'` | When validating the Kinobi IDL, the level at which the validation should throw an error.                                                                                                     |
-| `customAccountData`           | `string[]`                                          | `[]`      | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                         |
-| `customInstructionData`       | `string[]`                                          | `[]`      | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                     |
-| `dependencyMap`               | `Record<ImportFrom, string>`                        | `{}`      | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                        |
-| `internalNodes`               | `string[]`                                          | `[]`      | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                    |
-| `nonScalarEnums`              | `string[]`                                          | `[]`      | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL. |
-| `renderParentInstructions`    | `boolean`                                           | `false`   | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                      |
+| Name                          | Type                                                                                                  | Default   | Description                                                                                                                                                                                      |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `deleteFolderBeforeRendering` | `boolean`                                                                                             | `true`    | Whether the base directory should be cleaned before generating new files.                                                                                                                        |
+| `formatCode`                  | `boolean`                                                                                             | `true`    | Whether we should use Prettier to format the generated code.                                                                                                                                     |
+| `prettierOptions`             | `PrettierOptions`                                                                                     | `{}`      | The options to use when formatting the code using Prettier.                                                                                                                                      |
+| `throwLevel`                  | `'debug' \| 'trace' \| 'info' \| 'warn' \| 'error'`                                                   | `'error'` | When validating the Kinobi IDL, the level at which the validation should throw an error.                                                                                                         |
+| `customAccountData`           | `string[]`                                                                                            | `[]`      | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                             |
+| `customInstructionData`       | `string[]`                                                                                            | `[]`      | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                         |
+| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`      | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
+| `dependencyMap`               | `Record<string, string>`                                                                              | `{}`      | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                            |
+| `internalNodes`               | `string[]`                                                                                            | `[]`      | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                        |
+| `nonScalarEnums`              | `string[]`                                                                                            | `[]`      | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.     |
+| `renderParentInstructions`    | `boolean`                                                                                             | `false`   | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                          |

+ 6 - 8
packages/renderers-js-umi/src/ImportMap.ts

@@ -1,5 +1,3 @@
-import { ImportFrom } from '@kinobi-so/nodes';
-
 import { TypeManifest } from './getTypeManifestVisitor';
 
 const DEFAULT_MODULE_MAP: Record<string, string> = {
@@ -11,11 +9,11 @@ const DEFAULT_MODULE_MAP: Record<string, string> = {
 };
 
 export class ImportMap {
-    protected readonly _imports: Map<ImportFrom, Set<string>> = new Map();
+    protected readonly _imports: Map<string, Set<string>> = new Map();
 
-    protected readonly _aliases: Map<ImportFrom, Record<string, string>> = new Map();
+    protected readonly _aliases: Map<string, Record<string, string>> = new Map();
 
-    add(module: ImportFrom, imports: Set<string> | string[] | string): ImportMap {
+    add(module: string, imports: Set<string> | string[] | string): ImportMap {
         const currentImports = this._imports.get(module) ?? new Set();
         const newImports = typeof imports === 'string' ? [imports] : imports;
         newImports.forEach(i => currentImports.add(i));
@@ -23,7 +21,7 @@ export class ImportMap {
         return this;
     }
 
-    remove(module: ImportFrom, imports: Set<string> | string[] | string): ImportMap {
+    remove(module: string, imports: Set<string> | string[] | string): ImportMap {
         const currentImports = this._imports.get(module) ?? new Set();
         const importsToRemove = typeof imports === 'string' ? [imports] : imports;
         importsToRemove.forEach(i => currentImports.delete(i));
@@ -53,7 +51,7 @@ export class ImportMap {
         return this.mergeWith(manifest.strictImports, manifest.looseImports, manifest.serializerImports);
     }
 
-    addAlias(module: ImportFrom, name: string, alias: string): ImportMap {
+    addAlias(module: string, name: string, alias: string): ImportMap {
         const currentAliases = this._aliases.get(module) ?? {};
         currentAliases[name] = alias;
         this._aliases.set(module, currentAliases);
@@ -64,7 +62,7 @@ export class ImportMap {
         return this._imports.size === 0;
     }
 
-    toString(dependencies: Record<ImportFrom, string>): string {
+    toString(dependencies: Record<string, string>): string {
         const dependencyMap = { ...DEFAULT_MODULE_MAP, ...dependencies };
         const importStatements = [...this._imports.entries()]
             .map(([module, imports]) => {

+ 12 - 14
packages/renderers-js-umi/src/getRenderMapVisitor.ts

@@ -8,7 +8,6 @@ import {
     getAllInstructionArguments,
     getAllInstructionsWithSubs,
     getAllPrograms,
-    ImportFrom,
     InstructionNode,
     isDataEnum,
     isNode,
@@ -43,6 +42,8 @@ import {
     CustomDataOptions,
     getDefinedTypeNodesToExtract,
     getGpaFieldsFromAccount,
+    getImportFromFactory,
+    LinkOverrides,
     parseCustomDataOptions,
     render,
 } from './utils';
@@ -50,8 +51,9 @@ import {
 export type GetRenderMapOptions = {
     customAccountData?: CustomDataOptions[];
     customInstructionData?: CustomDataOptions[];
-    dependencyMap?: Record<ImportFrom, string>;
+    dependencyMap?: Record<string, string>;
     internalNodes?: string[];
+    linkOverrides?: LinkOverrides;
     nonScalarEnums?: string[];
     renderParentInstructions?: boolean;
 };
@@ -72,8 +74,8 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
         ...options.dependencyMap,
 
         generatedAccounts: '../accounts',
-
         generatedErrors: '../errors',
+
         // Custom relative dependencies to link generated files together.
         generatedPrograms: '../programs',
         generatedTypes: '../types',
@@ -82,11 +84,13 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
     const internalNodes = (options.internalNodes ?? []).map(camelCase);
     const customAccountData = parseCustomDataOptions(options.customAccountData ?? [], 'AccountData');
     const customInstructionData = parseCustomDataOptions(options.customInstructionData ?? [], 'InstructionData');
+    const getImportFrom = getImportFromFactory(options.linkOverrides ?? {}, customAccountData, customInstructionData);
 
     const getTypeManifestVisitor = (parentName?: { loose: string; strict: string }) =>
         baseGetTypeManifestVisitor({
             customAccountData,
             customInstructionData,
+            getImportFrom,
             linkables,
             nonScalarEnums,
             parentName,
@@ -336,6 +340,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
                                 typeManifestVisitor,
                                 node.optionalAccountStrategy,
                                 argObject,
+                                getImportFrom,
                             );
                             imports.mergeWith(renderedInput.imports);
                             interfaces.mergeWith(renderedInput.interfaces);
@@ -388,10 +393,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
                     // Arg defaults.
                     allArgumentsWithDefaultValue.forEach(argument => {
                         if (isNode(argument.defaultValue, 'resolverValueNode')) {
-                            imports.add(
-                                argument.defaultValue.importFrom ?? 'hooked',
-                                camelCase(argument.defaultValue.name),
-                            );
+                            imports.add(getImportFrom(argument.defaultValue), camelCase(argument.defaultValue.name));
                         }
                     });
                     if (argsWithDefaults.length > 0) {
@@ -404,18 +406,14 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
                     }
                     if (byteDelta && isNode(byteDelta.value, 'accountLinkNode')) {
                         const accountName = pascalCase(byteDelta.value.name);
-                        const importFrom = byteDelta.value.importFrom ?? 'generatedAccounts';
-                        imports.add(importFrom, `get${accountName}Size`);
+                        imports.add(getImportFrom(byteDelta.value), `get${accountName}Size`);
                     } else if (byteDelta && isNode(byteDelta.value, 'resolverValueNode')) {
-                        imports.add(byteDelta.value.importFrom ?? 'hooked', camelCase(byteDelta.value.name));
+                        imports.add(getImportFrom(byteDelta.value), camelCase(byteDelta.value.name));
                     }
 
                     // Remaining accounts.
                     if (remainingAccounts && isNode(remainingAccounts.value, 'resolverValueNode')) {
-                        imports.add(
-                            remainingAccounts.value.importFrom ?? 'hooked',
-                            camelCase(remainingAccounts.value.name),
-                        );
+                        imports.add(getImportFrom(remainingAccounts.value), camelCase(remainingAccounts.value.name));
                     }
 
                     return new RenderMap().add(

+ 5 - 4
packages/renderers-js-umi/src/getTypeManifestVisitor.ts

@@ -20,7 +20,7 @@ import {
 import { extendVisitor, LinkableDictionary, pipe, staticVisitor, visit, Visitor } from '@kinobi-so/visitors-core';
 
 import { ImportMap } from './ImportMap';
-import { getBytesFromBytesValueNode, jsDocblock, ParsedCustomDataOptions } from './utils';
+import { getBytesFromBytesValueNode, GetImportFromFunction, jsDocblock, ParsedCustomDataOptions } from './utils';
 
 export type TypeManifest = {
     isEnum: boolean;
@@ -51,11 +51,12 @@ function typeManifest(): TypeManifest {
 export function getTypeManifestVisitor(input: {
     customAccountData: ParsedCustomDataOptions;
     customInstructionData: ParsedCustomDataOptions;
+    getImportFrom: GetImportFromFunction;
     linkables: LinkableDictionary;
     nonScalarEnums: CamelCaseString[];
     parentName?: { loose: string; strict: string };
 }) {
-    const { linkables, nonScalarEnums, customAccountData, customInstructionData } = input;
+    const { linkables, nonScalarEnums, customAccountData, customInstructionData, getImportFrom } = input;
     let parentName = input.parentName ?? null;
     let parentSize: NumberTypeNode | number | null = null;
 
@@ -265,7 +266,7 @@ export function getTypeManifestVisitor(input: {
                 visitDefinedTypeLink(node) {
                     const pascalCaseDefinedType = pascalCase(node.name);
                     const serializerName = `get${pascalCaseDefinedType}Serializer`;
-                    const importFrom = node.importFrom ?? 'generatedTypes';
+                    const importFrom = getImportFrom(node);
 
                     return {
                         isEnum: false,
@@ -415,7 +416,7 @@ export function getTypeManifestVisitor(input: {
                     const imports = new ImportMap();
                     const enumName = pascalCase(node.enum.name);
                     const variantName = pascalCase(node.variant);
-                    const importFrom = node.enum.importFrom ?? 'generatedTypes';
+                    const importFrom = getImportFrom(node.enum);
 
                     const enumNode = linkables.get(node.enum)?.type;
                     const isScalar =

+ 10 - 6
packages/renderers-js-umi/src/renderInstructionDefaults.ts

@@ -5,12 +5,14 @@ import { ResolvedInstructionInput, visit } from '@kinobi-so/visitors-core';
 import { ContextMap } from './ContextMap';
 import { getTypeManifestVisitor } from './getTypeManifestVisitor';
 import { ImportMap } from './ImportMap';
+import { GetImportFromFunction } from './utils';
 
 export function renderInstructionDefaults(
     input: ResolvedInstructionInput,
     typeManifestVisitor: ReturnType<typeof getTypeManifestVisitor>,
     optionalAccountStrategy: 'omitted' | 'programId',
     argObject: string,
+    getImportFrom: GetImportFromFunction,
 ): {
     imports: ImportMap;
     interfaces: ContextMap;
@@ -127,8 +129,7 @@ export function renderInstructionDefaults(
 
             // Linked PDA value.
             const pdaFunction = `find${pascalCase(defaultValue.pda.name)}Pda`;
-            const pdaImportFrom = defaultValue.pda.importFrom ?? 'generatedAccounts';
-            imports.add(pdaImportFrom, pdaFunction);
+            imports.add(getImportFrom(defaultValue.pda), pdaFunction);
             interfaces.add('eddsa');
             const pdaArgs = ['context'];
             const pdaSeeds = defaultValue.seeds.map((seed): string => {
@@ -159,9 +160,8 @@ export function renderInstructionDefaults(
                 false,
             );
         case 'programLinkNode':
-            const importFrom = defaultValue.importFrom ?? 'generatedPrograms';
             const functionName = `get${pascalCase(defaultValue.name)}ProgramId`;
-            imports.add(importFrom, functionName);
+            imports.add(getImportFrom(defaultValue), functionName);
             return render(`${functionName}(context)`, false);
         case 'programIdValueNode':
             if (
@@ -193,7 +193,7 @@ export function renderInstructionDefaults(
         case 'resolverValueNode':
             const resolverName = camelCase(defaultValue.name);
             const isWritable = input.kind === 'instructionAccountNode' && input.isWritable ? 'true' : 'false';
-            imports.add(defaultValue.importFrom ?? 'hooked', resolverName);
+            imports.add(getImportFrom(defaultValue), resolverName);
             interfaces.add(['eddsa', 'identity', 'payer']);
             return render(`${resolverName}(context, resolvedAccounts, ${argObject}, programId, ${isWritable})`);
         case 'conditionalValueNode':
@@ -203,6 +203,7 @@ export function renderInstructionDefaults(
                 optionalAccountStrategy,
                 defaultValue.ifTrue,
                 argObject,
+                getImportFrom,
             );
             const ifFalseRenderer = renderNestedInstructionDefault(
                 input,
@@ -210,6 +211,7 @@ export function renderInstructionDefaults(
                 optionalAccountStrategy,
                 defaultValue.ifFalse,
                 argObject,
+                getImportFrom,
             );
             if (!ifTrueRenderer && !ifFalseRenderer) {
                 return { imports, interfaces, render: '' };
@@ -229,7 +231,7 @@ export function renderInstructionDefaults(
                 const conditionalResolverName = camelCase(defaultValue.condition.name);
                 const conditionalIsWritable =
                     input.kind === 'instructionAccountNode' && input.isWritable ? 'true' : 'false';
-                imports.add(defaultValue.condition.importFrom ?? 'hooked', conditionalResolverName);
+                imports.add(getImportFrom(defaultValue.condition), conditionalResolverName);
                 interfaces.add(['eddsa', 'identity', 'payer']);
                 condition = `${conditionalResolverName}(context, resolvedAccounts, ${argObject}, programId, ${conditionalIsWritable})`;
                 condition = negatedCondition ? `!${condition}` : condition;
@@ -273,6 +275,7 @@ function renderNestedInstructionDefault(
     optionalAccountStrategy: 'omitted' | 'programId',
     defaultValue: InstructionInputValueNode | undefined,
     argObject: string,
+    getImportFrom: GetImportFromFunction,
 ):
     | {
           imports: ImportMap;
@@ -286,5 +289,6 @@ function renderNestedInstructionDefault(
         typeManifestVisitor,
         optionalAccountStrategy,
         argObject,
+        getImportFrom,
     );
 }

+ 3 - 4
packages/renderers-js-umi/src/utils/customData.ts

@@ -6,7 +6,6 @@ import {
     definedTypeLinkNode,
     DefinedTypeNode,
     definedTypeNode,
-    ImportFrom,
     InstructionNode,
     isNode,
     structTypeNodeFromInstructionArgumentNodes,
@@ -18,7 +17,7 @@ export type CustomDataOptions =
           extract?: boolean;
           extractAs?: string;
           importAs?: string;
-          importFrom?: ImportFrom;
+          importFrom?: string;
           name: string;
       };
 
@@ -28,7 +27,7 @@ export type ParsedCustomDataOptions = Map<
         extract: boolean;
         extractAs: CamelCaseString;
         importAs: CamelCaseString;
-        importFrom: ImportFrom;
+        importFrom: string;
         linkNode: DefinedTypeLinkNode;
     }
 >;
@@ -49,7 +48,7 @@ export const parseCustomDataOptions = (
                     extractAs: options.extractAs ? camelCase(options.extractAs) : importAs,
                     importAs,
                     importFrom,
-                    linkNode: definedTypeLinkNode(importAs, importFrom),
+                    linkNode: definedTypeLinkNode(importAs),
                 },
             ];
         }),

+ 1 - 0
packages/renderers-js-umi/src/utils/index.ts

@@ -1,4 +1,5 @@
 export * from './codecs';
 export * from './customData';
 export * from './gpaField';
+export * from './linkOverrides';
 export * from './render';

+ 56 - 0
packages/renderers-js-umi/src/utils/linkOverrides.ts

@@ -0,0 +1,56 @@
+import { KINOBI_ERROR__UNEXPECTED_NODE_KIND, KinobiError } from '@kinobi-so/errors';
+import { LINK_NODES, LinkNode, ResolverValueNode } from '@kinobi-so/nodes';
+
+import { ParsedCustomDataOptions } from './customData';
+
+export type LinkOverrides = {
+    accounts?: Record<string, string>;
+    definedTypes?: Record<string, string>;
+    pdas?: Record<string, string>;
+    programs?: Record<string, string>;
+    resolvers?: Record<string, string>;
+};
+
+export type GetImportFromFunction = (node: LinkNode | ResolverValueNode, fallback?: string) => string;
+
+export function getImportFromFactory(
+    overrides: LinkOverrides,
+    customAccountData: ParsedCustomDataOptions,
+    customInstructionData: ParsedCustomDataOptions,
+): GetImportFromFunction {
+    const customDataOverrides = Object.fromEntries(
+        [...customAccountData.values(), ...customInstructionData.values()].map(({ importFrom, importAs }) => [
+            importAs,
+            importFrom,
+        ]),
+    );
+    const linkOverrides = {
+        accounts: overrides.accounts ?? {},
+        definedTypes: { ...customDataOverrides, ...overrides.definedTypes },
+        pdas: overrides.pdas ?? {},
+        programs: overrides.programs ?? {},
+        resolvers: overrides.resolvers ?? {},
+    };
+
+    return (node: LinkNode | ResolverValueNode) => {
+        const kind = node.kind;
+        switch (kind) {
+            case 'accountLinkNode':
+                return linkOverrides.accounts[node.name] ?? 'generatedAccounts';
+            case 'definedTypeLinkNode':
+                return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';
+            case 'pdaLinkNode':
+                return linkOverrides.pdas[node.name] ?? 'generatedAccounts';
+            case 'programLinkNode':
+                return linkOverrides.programs[node.name] ?? 'generatedPrograms';
+            case 'resolverValueNode':
+                return linkOverrides.resolvers[node.name] ?? 'hooked';
+            default:
+                throw new KinobiError(KINOBI_ERROR__UNEXPECTED_NODE_KIND, {
+                    expectedKinds: [...LINK_NODES, 'resolverValueNode'],
+                    kind: kind satisfies never,
+                    node,
+                });
+        }
+    };
+}

+ 24 - 8
packages/renderers-js-umi/test/_setup.ts

@@ -26,20 +26,25 @@ export async function codeContains(actual: string, expected: (RegExp | string)[]
     const normalizedActual = await normalizeCode(actual);
     expectedArray.forEach(expectedResult => {
         if (typeof expectedResult === 'string') {
-            const stringAsRegex = escapeRegex(expectedResult)
-                // Transform spaces between words into required whitespace.
-                .replace(/(\w)\s+(\w)/g, '$1\\s+$2')
-                // Do it again for single-character words — e.g. "as[ ]a[ ]token".
-                .replace(/(\w)\s+(\w)/g, '$1\\s+$2')
-                // Transform other spaces into optional whitespace.
-                .replace(/\s+/g, '\\s*');
-            expect(normalizedActual).toMatch(new RegExp(stringAsRegex));
+            expect(normalizedActual).toMatch(codeStringAsRegex(expectedResult));
         } else {
             expect(normalizedActual).toMatch(expectedResult);
         }
     });
 }
 
+export async function codeDoesNotContain(actual: string, expected: (RegExp | string)[] | RegExp | string) {
+    const expectedArray = Array.isArray(expected) ? expected : [expected];
+    const normalizedActual = await normalizeCode(actual);
+    expectedArray.forEach(expectedResult => {
+        if (typeof expectedResult === 'string') {
+            expect(normalizedActual).not.toMatch(codeStringAsRegex(expectedResult));
+        } else {
+            expect(normalizedActual).not.toMatch(expectedResult);
+        }
+    });
+}
+
 export function renderMapContainsImports(renderMap: RenderMap, key: string, expectedImports: Record<string, string[]>) {
     expect(renderMap.has(key), `RenderMap is missing key "${key}".`).toBe(true);
     return codeContainsImports(renderMap.get(key), expectedImports);
@@ -56,6 +61,17 @@ export async function codeContainsImports(actual: string, expectedImports: Recor
     });
 }
 
+export function codeStringAsRegex(code: string) {
+    const stringAsRegex = escapeRegex(code)
+        // Transform spaces between words into required whitespace.
+        .replace(/(\w)\s+(\w)/g, '$1\\s+$2')
+        // Do it again for single-character words — e.g. "as[ ]a[ ]token".
+        .replace(/(\w)\s+(\w)/g, '$1\\s+$2')
+        // Transform other spaces into optional whitespace.
+        .replace(/\s+/g, '\\s*');
+    return new RegExp(stringAsRegex);
+}
+
 async function normalizeCode(code: string) {
     try {
         code = await format(code, PRETTIER_OPTIONS);

+ 1 - 1
packages/renderers-js-umi/test/instructionsPage.test.ts

@@ -57,7 +57,7 @@ test('it renders instruction accounts with linked PDAs as default value', async
             'resolvedAccounts.counter.value = findCounterPda( context, { authority: expectPublicKey ( resolvedAccounts.authority.value ) } ); ' +
             '}',
     ]);
-    renderMapContainsImports(renderMap, 'instructions/increment.ts', { '../accounts': ['findCounterPda'] });
+    await renderMapContainsImports(renderMap, 'instructions/increment.ts', { '../accounts': ['findCounterPda'] });
 });
 
 test('it renders instruction accounts with inlined PDAs as default value', async () => {

+ 15 - 14
packages/renderers-js/README.md

@@ -37,17 +37,18 @@ kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 
 The `renderVisitor` accepts the following options.
 
-| Name                          | Type                         | Default | Description                                                                                                                                                                                                                                                     |
-| ----------------------------- | ---------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `deleteFolderBeforeRendering` | `boolean`                    | `true`  | Whether the base directory should be cleaned before generating new files.                                                                                                                                                                                       |
-| `formatCode`                  | `boolean`                    | `true`  | Whether we should use Prettier to format the generated code.                                                                                                                                                                                                    |
-| `prettierOptions`             | `PrettierOptions`            | `{}`    | The options to use when formatting the code using Prettier.                                                                                                                                                                                                     |
-| `asyncResolvers`              | `string[]`                   | `[]`    | The exhaustive list of `ResolverValueNode`'s names whose implementation is asynchronous in JavaScript.                                                                                                                                                          |
-| `customAccountData`           | `string[]`                   | `[]`    | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                                                                                            |
-| `customInstructionData`       | `string[]`                   | `[]`    | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                                                                                        |
-| `dependencyMap`               | `Record<ImportFrom, string>` | `{}`    | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                                                                                           |
-| `internalNodes`               | `string[]`                   | `[]`    | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                                                                                       |
-| `nameTransformers`            | `Partial<NameTransformers>`  | `{}`    | An object that enables us to override the names of any generated type, constant or function.                                                                                                                                                                    |
-| `nonScalarEnums`              | `string[]`                   | `[]`    | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.                                                                    |
-| `renderParentInstructions`    | `boolean`                    | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                                                                                         |
-| `useGranularImports`          | `boolean`                    | `false` | Whether to import the `@solana/web3.js` library using sub-packages such as `@solana/addresses` or `@solana/codecs-strings`. When set to `true`, the main `@solana/web3.js` library is used which enables generated clients to install it as a `peerDependency`. |
+| Name                          | Type                                                                                                  | Default | Description                                                                                                                                                                                                                                                     |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `deleteFolderBeforeRendering` | `boolean`                                                                                             | `true`  | Whether the base directory should be cleaned before generating new files.                                                                                                                                                                                       |
+| `formatCode`                  | `boolean`                                                                                             | `true`  | Whether we should use Prettier to format the generated code.                                                                                                                                                                                                    |
+| `prettierOptions`             | `PrettierOptions`                                                                                     | `{}`    | The options to use when formatting the code using Prettier.                                                                                                                                                                                                     |
+| `asyncResolvers`              | `string[]`                                                                                            | `[]`    | The exhaustive list of `ResolverValueNode`'s names whose implementation is asynchronous in JavaScript.                                                                                                                                                          |
+| `customAccountData`           | `string[]`                                                                                            | `[]`    | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                                                                                            |
+| `customInstructionData`       | `string[]`                                                                                            | `[]`    | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                                                                                        |
+| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`    | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type.                                                                |
+| `dependencyMap`               | `Record<string, string>`                                                                              | `{}`    | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                                                                                           |
+| `internalNodes`               | `string[]`                                                                                            | `[]`    | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                                                                                       |
+| `nameTransformers`            | `Partial<NameTransformers>`                                                                           | `{}`    | An object that enables us to override the names of any generated type, constant or function.                                                                                                                                                                    |
+| `nonScalarEnums`              | `string[]`                                                                                            | `[]`    | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.                                                                    |
+| `renderParentInstructions`    | `boolean`                                                                                             | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                                                                                         |
+| `useGranularImports`          | `boolean`                                                                                             | `false` | Whether to import the `@solana/web3.js` library using sub-packages such as `@solana/addresses` or `@solana/codecs-strings`. When set to `true`, the main `@solana/web3.js` library is used which enables generated clients to install it as a `peerDependency`. |

+ 9 - 11
packages/renderers-js/src/ImportMap.ts

@@ -1,5 +1,3 @@
-import { ImportFrom } from '@kinobi-so/nodes';
-
 import { Fragment } from './fragments';
 import { TypeManifest } from './TypeManifest';
 
@@ -48,11 +46,11 @@ const DEFAULT_INTERNAL_MODULE_MAP: Record<string, string> = {
 };
 
 export class ImportMap {
-    protected readonly _imports: Map<ImportFrom, Set<string>> = new Map();
+    protected readonly _imports: Map<string, Set<string>> = new Map();
 
-    protected readonly _aliases: Map<ImportFrom, Record<string, string>> = new Map();
+    protected readonly _aliases: Map<string, Record<string, string>> = new Map();
 
-    add(module: ImportFrom, imports: Set<string> | string[] | string): ImportMap {
+    add(module: string, imports: Set<string> | string[] | string): ImportMap {
         const newImports = new Set(typeof imports === 'string' ? [imports] : imports);
         if (newImports.size === 0) return this;
         const currentImports = this._imports.get(module) ?? new Set();
@@ -61,7 +59,7 @@ export class ImportMap {
         return this;
     }
 
-    remove(module: ImportFrom, imports: Set<string> | string[] | string): ImportMap {
+    remove(module: string, imports: Set<string> | string[] | string): ImportMap {
         const importsToRemove = new Set(typeof imports === 'string' ? [imports] : imports);
         if (importsToRemove.size === 0) return this;
         const currentImports = this._imports.get(module) ?? new Set();
@@ -93,7 +91,7 @@ export class ImportMap {
         return this.mergeWith(manifest.strictType, manifest.looseType, manifest.encoder, manifest.decoder);
     }
 
-    addAlias(module: ImportFrom, name: string, alias: string): ImportMap {
+    addAlias(module: string, name: string, alias: string): ImportMap {
         const currentAliases = this._aliases.get(module) ?? {};
         currentAliases[name] = alias;
         this._aliases.set(module, currentAliases);
@@ -104,9 +102,9 @@ export class ImportMap {
         return this._imports.size === 0;
     }
 
-    resolve(dependencies: Record<ImportFrom, string> = {}, useGranularImports = false): Map<ImportFrom, Set<string>> {
+    resolve(dependencies: Record<string, string> = {}, useGranularImports = false): Map<string, Set<string>> {
         // Resolve aliases.
-        const aliasedMap = new Map<ImportFrom, Set<string>>(
+        const aliasedMap = new Map<string, Set<string>>(
             [...this._imports.entries()].map(([module, imports]) => {
                 const aliasMap = this._aliases.get(module) ?? {};
                 const joinedImports = [...imports].map(i => (aliasMap[i] ? `${i} as ${aliasMap[i]}` : i));
@@ -120,7 +118,7 @@ export class ImportMap {
             ...DEFAULT_INTERNAL_MODULE_MAP,
             ...dependencies,
         };
-        const resolvedMap = new Map<ImportFrom, Set<string>>();
+        const resolvedMap = new Map<string, Set<string>>();
         aliasedMap.forEach((imports, module) => {
             const resolvedModule: string = dependencyMap[module] ?? module;
             const currentImports = resolvedMap.get(resolvedModule) ?? new Set();
@@ -131,7 +129,7 @@ export class ImportMap {
         return resolvedMap;
     }
 
-    toString(dependencies: Record<ImportFrom, string> = {}, useGranularImports = false): string {
+    toString(dependencies: Record<string, string> = {}, useGranularImports = false): string {
         return [...this.resolve(dependencies, useGranularImports).entries()]
             .sort(([a], [b]) => {
                 const aIsRelative = a.startsWith('.');

+ 3 - 0
packages/renderers-js/src/fragments/accountPdaHelpers.ts

@@ -21,6 +21,9 @@ export function getAccountPdaHelpersFragment(
         ? typeManifest.strictType.clone()
         : fragment(nameApi.dataType(accountNode.name));
 
+    // Here we cannot use the `getImportFrom` function because
+    // we need to know the seeds of the PDA in order to know
+    // if we need to render a `seeds` argument or not.
     const importFrom = 'generatedPdas';
     const pdaSeedsType = nameApi.pdaSeedsType(pdaNode.name);
     const findPdaFunction = nameApi.pdaFindFunction(pdaNode.name);

+ 3 - 4
packages/renderers-js/src/fragments/common.ts

@@ -1,6 +1,5 @@
 import { join } from 'node:path';
 
-import { ImportFrom } from '@kinobi-so/nodes';
 import { ConfigureOptions } from 'nunjucks';
 
 import { ImportMap } from '../ImportMap';
@@ -45,12 +44,12 @@ export class Fragment {
         return this;
     }
 
-    addImports(module: ImportFrom, imports: Set<string> | string[] | string): this {
+    addImports(module: string, imports: Set<string> | string[] | string): this {
         this.imports.add(module, imports);
         return this;
     }
 
-    removeImports(module: ImportFrom, imports: Set<string> | string[] | string): this {
+    removeImports(module: string, imports: Set<string> | string[] | string): this {
         this.imports.remove(module, imports);
         return this;
     }
@@ -60,7 +59,7 @@ export class Fragment {
         return this;
     }
 
-    addImportAlias(module: ImportFrom, name: string, alias: string): this {
+    addImportAlias(module: string, name: string, alias: string): this {
         this.imports.addAlias(module, name, alias);
         return this;
     }

+ 6 - 7
packages/renderers-js/src/fragments/instructionByteDelta.ts

@@ -4,7 +4,7 @@ import type { GlobalFragmentScope } from '../getRenderMapVisitor';
 import { Fragment, fragment, mergeFragments } from './common';
 
 export function getInstructionByteDeltaFragment(
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi'> & {
         instructionNode: InstructionNode;
         useAsync: boolean;
     },
@@ -22,7 +22,7 @@ export function getInstructionByteDeltaFragment(
 
 function getByteDeltaFragment(
     byteDelta: InstructionByteDeltaNode,
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi'> & {
         useAsync: boolean;
     },
 ): Fragment[] {
@@ -68,17 +68,16 @@ function getArgumentValueNodeFragment(byteDelta: InstructionByteDeltaNode): Frag
 
 function getAccountLinkNodeFragment(
     byteDelta: InstructionByteDeltaNode,
-    scope: Pick<GlobalFragmentScope, 'nameApi'>,
+    scope: Pick<GlobalFragmentScope, 'getImportFrom' | 'nameApi'>,
 ): Fragment {
     assertIsNode(byteDelta.value, 'accountLinkNode');
     const functionName = scope.nameApi.accountGetSizeFunction(byteDelta.value.name);
-    const importFrom = byteDelta.value.importFrom ?? 'generatedAccounts';
-    return fragment(`${functionName}()`).addImports(importFrom, functionName);
+    return fragment(`${functionName}()`).addImports(scope.getImportFrom(byteDelta.value), functionName);
 }
 
 function getResolverValueNodeFragment(
     byteDelta: InstructionByteDeltaNode,
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi'> & {
         useAsync: boolean;
     },
 ): Fragment | null {
@@ -89,6 +88,6 @@ function getResolverValueNodeFragment(
     const awaitKeyword = scope.useAsync && isAsync ? 'await ' : '';
     const functionName = scope.nameApi.resolverFunction(byteDelta.value.name);
     return fragment(`${awaitKeyword}${functionName}(resolverScope)`)
-        .addImports(byteDelta.value.importFrom ?? 'hooked', functionName)
+        .addImports(scope.getImportFrom(byteDelta.value), functionName)
         .addFeatures(['instruction:resolverScopeVariable']);
 }

+ 4 - 1
packages/renderers-js/src/fragments/instructionFunction.ts

@@ -20,7 +20,10 @@ import { getInstructionInputTypeFragment } from './instructionInputType';
 import { getInstructionRemainingAccountsFragment } from './instructionRemainingAccounts';
 
 export function getInstructionFunctionFragment(
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'customInstructionData' | 'nameApi' | 'typeManifestVisitor'> & {
+    scope: Pick<
+        GlobalFragmentScope,
+        'asyncResolvers' | 'customInstructionData' | 'getImportFrom' | 'nameApi' | 'typeManifestVisitor'
+    > & {
         dataArgsManifest: TypeManifest;
         extraArgsManifest: TypeManifest;
         instructionNode: InstructionNode;

+ 7 - 8
packages/renderers-js/src/fragments/instructionInputDefault.ts

@@ -7,13 +7,14 @@ import { isAsyncDefaultValue } from '../utils';
 import { Fragment, fragment, mergeFragments } from './common';
 
 export function getInstructionInputDefaultFragment(
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi' | 'typeManifestVisitor'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi' | 'typeManifestVisitor'> & {
         input: ResolvedInstructionInput;
         optionalAccountStrategy: 'omitted' | 'programId';
         useAsync: boolean;
     },
 ): Fragment {
-    const { input, optionalAccountStrategy, asyncResolvers, useAsync, nameApi, typeManifestVisitor } = scope;
+    const { input, optionalAccountStrategy, asyncResolvers, useAsync, nameApi, typeManifestVisitor, getImportFrom } =
+        scope;
     if (!input.defaultValue) {
         return fragment('');
     }
@@ -121,7 +122,6 @@ export function getInstructionInputDefaultFragment(
 
             // Linked PDA value.
             const pdaFunction = nameApi.pdaFindFunction(defaultValue.pda.name);
-            const pdaImportFrom = defaultValue.pda.importFrom ?? 'generatedPdas';
             const pdaArgs = [];
             const pdaSeeds = defaultValue.seeds.map((seed): Fragment => {
                 if (isNode(seed.value, 'accountValueNode')) {
@@ -143,7 +143,7 @@ export function getInstructionInputDefaultFragment(
             }
             return defaultFragment(`await ${pdaFunction}(${pdaArgs.join(', ')})`)
                 .mergeImportsWith(pdaSeedsFragment)
-                .addImports(pdaImportFrom, pdaFunction);
+                .addImports(getImportFrom(defaultValue.pda), pdaFunction);
 
         case 'publicKeyValueNode':
             return defaultFragment(`'${defaultValue.publicKey}' as Address<'${defaultValue.publicKey}'>`).addImports(
@@ -153,8 +153,7 @@ export function getInstructionInputDefaultFragment(
 
         case 'programLinkNode':
             const programAddress = nameApi.programAddressConstant(defaultValue.name);
-            const importFrom = defaultValue.importFrom ?? 'generatedPrograms';
-            return defaultFragment(programAddress, false).addImports(importFrom, programAddress);
+            return defaultFragment(programAddress, false).addImports(getImportFrom(defaultValue), programAddress);
 
         case 'programIdValueNode':
             if (
@@ -185,7 +184,7 @@ export function getInstructionInputDefaultFragment(
             const resolverFunction = nameApi.resolverFunction(defaultValue.name);
             const resolverAwait = useAsync && asyncResolvers.includes(defaultValue.name) ? 'await ' : '';
             return defaultFragment(`${resolverAwait}${resolverFunction}(resolverScope)`)
-                .addImports(defaultValue.importFrom ?? 'hooked', resolverFunction)
+                .addImports(getImportFrom(defaultValue), resolverFunction)
                 .addFeatures(['instruction:resolverScopeVariable']);
 
         case 'conditionalValueNode':
@@ -213,7 +212,7 @@ export function getInstructionInputDefaultFragment(
             if (isNode(defaultValue.condition, 'resolverValueNode')) {
                 const conditionalResolverFunction = nameApi.resolverFunction(defaultValue.condition.name);
                 conditionalFragment
-                    .addImports(defaultValue.condition.importFrom ?? 'hooked', conditionalResolverFunction)
+                    .addImports(getImportFrom(defaultValue.condition), conditionalResolverFunction)
                     .addFeatures(['instruction:resolverScopeVariable']);
                 const conditionalResolverAwait =
                     useAsync && asyncResolvers.includes(defaultValue.condition.name) ? 'await ' : '';

+ 1 - 1
packages/renderers-js/src/fragments/instructionInputResolved.ts

@@ -6,7 +6,7 @@ import { Fragment, fragment, mergeFragments } from './common';
 import { getInstructionInputDefaultFragment } from './instructionInputDefault';
 
 export function getInstructionInputResolvedFragment(
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi' | 'typeManifestVisitor'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi' | 'typeManifestVisitor'> & {
         instructionNode: InstructionNode;
         resolvedInputs: ResolvedInstructionInput[];
         useAsync: boolean;

+ 4 - 4
packages/renderers-js/src/fragments/instructionRemainingAccounts.ts

@@ -11,7 +11,7 @@ import type { GlobalFragmentScope } from '../getRenderMapVisitor';
 import { Fragment, fragment, mergeFragments } from './common';
 
 export function getInstructionRemainingAccountsFragment(
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi'> & {
         instructionNode: InstructionNode;
         useAsync: boolean;
     },
@@ -29,7 +29,7 @@ export function getInstructionRemainingAccountsFragment(
 
 function getRemainingAccountsFragment(
     remainingAccounts: InstructionRemainingAccountsNode,
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi'> & {
         instructionNode: InstructionNode;
         useAsync: boolean;
     },
@@ -94,7 +94,7 @@ function getArgumentValueNodeFragment(
 
 function getResolverValueNodeFragment(
     remainingAccounts: InstructionRemainingAccountsNode,
-    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'nameApi'> & {
+    scope: Pick<GlobalFragmentScope, 'asyncResolvers' | 'getImportFrom' | 'nameApi'> & {
         useAsync: boolean;
     },
 ): Fragment | null {
@@ -105,6 +105,6 @@ function getResolverValueNodeFragment(
     const awaitKeyword = scope.useAsync && isAsync ? 'await ' : '';
     const functionName = scope.nameApi.resolverFunction(remainingAccounts.value.name);
     return fragment(`${awaitKeyword}${functionName}(resolverScope)`)
-        .addImports(remainingAccounts.value.importFrom ?? 'hooked', functionName)
+        .addImports(scope.getImportFrom(remainingAccounts.value), functionName)
         .addFeatures(['instruction:resolverScopeVariable']);
 }

+ 9 - 2
packages/renderers-js/src/getRenderMapVisitor.ts

@@ -9,7 +9,6 @@ import {
     getAllInstructionsWithSubs,
     getAllPdas,
     getAllPrograms,
-    ImportFrom,
     InstructionNode,
     ProgramNode,
     resolveNestedTypeNode,
@@ -52,6 +51,9 @@ import { DEFAULT_NAME_TRANSFORMERS, getNameApi, NameApi, NameTransformers } from
 import {
     CustomDataOptions,
     getDefinedTypeNodesToExtract,
+    getImportFromFactory,
+    GetImportFromFunction,
+    LinkOverrides,
     parseCustomDataOptions,
     ParsedCustomDataOptions,
     render as baseRender,
@@ -61,8 +63,9 @@ export type GetRenderMapOptions = {
     asyncResolvers?: string[];
     customAccountData?: CustomDataOptions[];
     customInstructionData?: CustomDataOptions[];
-    dependencyMap?: Record<ImportFrom, string>;
+    dependencyMap?: Record<string, string>;
     internalNodes?: string[];
+    linkOverrides?: LinkOverrides;
     nameTransformers?: Partial<NameTransformers>;
     nonScalarEnums?: string[];
     renderParentInstructions?: boolean;
@@ -73,6 +76,7 @@ export type GlobalFragmentScope = {
     asyncResolvers: CamelCaseString[];
     customAccountData: ParsedCustomDataOptions;
     customInstructionData: ParsedCustomDataOptions;
+    getImportFrom: GetImportFromFunction;
     linkables: LinkableDictionary;
     nameApi: NameApi;
     nonScalarEnums: CamelCaseString[];
@@ -97,11 +101,13 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
     const internalNodes = (options.internalNodes ?? []).map(camelCase);
     const customAccountData = parseCustomDataOptions(options.customAccountData ?? [], 'AccountData');
     const customInstructionData = parseCustomDataOptions(options.customInstructionData ?? [], 'InstructionData');
+    const getImportFrom = getImportFromFactory(options.linkOverrides ?? {}, customAccountData, customInstructionData);
 
     const getTypeManifestVisitor = (parentName?: { loose: string; strict: string }) =>
         baseGetTypeManifestVisitor({
             customAccountData,
             customInstructionData,
+            getImportFrom,
             linkables,
             nameApi,
             nonScalarEnums,
@@ -114,6 +120,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
         asyncResolvers,
         customAccountData,
         customInstructionData,
+        getImportFrom,
         linkables,
         nameApi,
         nonScalarEnums,

+ 5 - 4
packages/renderers-js/src/getTypeManifestVisitor.ts

@@ -28,19 +28,20 @@ import { Fragment, fragment, mergeFragments } from './fragments';
 import { ImportMap } from './ImportMap';
 import { NameApi } from './nameTransformers';
 import { mergeManifests, TypeManifest, typeManifest } from './TypeManifest';
-import { getBytesFromBytesValueNode, jsDocblock, ParsedCustomDataOptions } from './utils';
+import { getBytesFromBytesValueNode, GetImportFromFunction, jsDocblock, ParsedCustomDataOptions } from './utils';
 
 export type TypeManifestVisitor = ReturnType<typeof getTypeManifestVisitor>;
 
 export function getTypeManifestVisitor(input: {
     customAccountData: ParsedCustomDataOptions;
     customInstructionData: ParsedCustomDataOptions;
+    getImportFrom: GetImportFromFunction;
     linkables: LinkableDictionary;
     nameApi: NameApi;
     nonScalarEnums: CamelCaseString[];
     parentName?: { loose: string; strict: string };
 }) {
-    const { nameApi, linkables, nonScalarEnums, customAccountData, customInstructionData } = input;
+    const { nameApi, linkables, nonScalarEnums, customAccountData, customInstructionData, getImportFrom } = input;
     const stack = new NodeStack();
     let parentName = input.parentName ?? null;
 
@@ -199,7 +200,7 @@ export function getTypeManifestVisitor(input: {
                     const looseName = nameApi.dataArgsType(node.name);
                     const encoderFunction = nameApi.encoderFunction(node.name);
                     const decoderFunction = nameApi.decoderFunction(node.name);
-                    const importFrom = node.importFrom ?? 'generatedTypes';
+                    const importFrom = getImportFrom(node);
 
                     return {
                         decoder: fragment(`${decoderFunction}()`).addImports(importFrom, decoderFunction),
@@ -351,7 +352,7 @@ export function getTypeManifestVisitor(input: {
                     const manifest = typeManifest();
                     const enumName = nameApi.dataType(node.enum.name);
                     const enumFunction = nameApi.discriminatedUnionFunction(node.enum.name);
-                    const importFrom = node.enum.importFrom ?? 'generatedTypes';
+                    const importFrom = getImportFrom(node.enum);
 
                     const enumNode = linkables.get(node.enum)?.type;
                     const isScalar =

+ 3 - 4
packages/renderers-js/src/utils/customData.ts

@@ -6,7 +6,6 @@ import {
     definedTypeLinkNode,
     DefinedTypeNode,
     definedTypeNode,
-    ImportFrom,
     InstructionNode,
     isNode,
     structTypeNodeFromInstructionArgumentNodes,
@@ -18,7 +17,7 @@ export type CustomDataOptions =
           extract?: boolean;
           extractAs?: string;
           importAs?: string;
-          importFrom?: ImportFrom;
+          importFrom?: string;
           name: string;
       };
 
@@ -28,7 +27,7 @@ export type ParsedCustomDataOptions = Map<
         extract: boolean;
         extractAs: CamelCaseString;
         importAs: CamelCaseString;
-        importFrom: ImportFrom;
+        importFrom: string;
         linkNode: DefinedTypeLinkNode;
     }
 >;
@@ -49,7 +48,7 @@ export const parseCustomDataOptions = (
                     extractAs: options.extractAs ? camelCase(options.extractAs) : importAs,
                     importAs,
                     importFrom,
-                    linkNode: definedTypeLinkNode(importAs, importFrom),
+                    linkNode: definedTypeLinkNode(importAs),
                 },
             ];
         }),

+ 1 - 0
packages/renderers-js/src/utils/index.ts

@@ -1,4 +1,5 @@
 export * from './async';
 export * from './codecs';
 export * from './customData';
+export * from './linkOverrides';
 export * from './render';

+ 56 - 0
packages/renderers-js/src/utils/linkOverrides.ts

@@ -0,0 +1,56 @@
+import { KINOBI_ERROR__UNEXPECTED_NODE_KIND, KinobiError } from '@kinobi-so/errors';
+import { LINK_NODES, LinkNode, ResolverValueNode } from '@kinobi-so/nodes';
+
+import { ParsedCustomDataOptions } from './customData';
+
+export type LinkOverrides = {
+    accounts?: Record<string, string>;
+    definedTypes?: Record<string, string>;
+    pdas?: Record<string, string>;
+    programs?: Record<string, string>;
+    resolvers?: Record<string, string>;
+};
+
+export type GetImportFromFunction = (node: LinkNode | ResolverValueNode, fallback?: string) => string;
+
+export function getImportFromFactory(
+    overrides: LinkOverrides,
+    customAccountData: ParsedCustomDataOptions,
+    customInstructionData: ParsedCustomDataOptions,
+): GetImportFromFunction {
+    const customDataOverrides = Object.fromEntries(
+        [...customAccountData.values(), ...customInstructionData.values()].map(({ importFrom, importAs }) => [
+            importAs,
+            importFrom,
+        ]),
+    );
+    const linkOverrides = {
+        accounts: overrides.accounts ?? {},
+        definedTypes: { ...customDataOverrides, ...overrides.definedTypes },
+        pdas: overrides.pdas ?? {},
+        programs: overrides.programs ?? {},
+        resolvers: overrides.resolvers ?? {},
+    };
+
+    return (node: LinkNode | ResolverValueNode) => {
+        const kind = node.kind;
+        switch (kind) {
+            case 'accountLinkNode':
+                return linkOverrides.accounts[node.name] ?? 'generatedAccounts';
+            case 'definedTypeLinkNode':
+                return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';
+            case 'pdaLinkNode':
+                return linkOverrides.pdas[node.name] ?? 'generatedPdas';
+            case 'programLinkNode':
+                return linkOverrides.programs[node.name] ?? 'generatedPrograms';
+            case 'resolverValueNode':
+                return linkOverrides.resolvers[node.name] ?? 'hooked';
+            default:
+                throw new KinobiError(KINOBI_ERROR__UNEXPECTED_NODE_KIND, {
+                    expectedKinds: [...LINK_NODES, 'resolverValueNode'],
+                    kind: kind satisfies never,
+                    node,
+                });
+        }
+    };
+}

+ 44 - 1
packages/renderers-js/test/accountsPage.test.ts

@@ -22,7 +22,7 @@ import { visit } from '@kinobi-so/visitors-core';
 import { test } from 'vitest';
 
 import { getRenderMapVisitor } from '../src';
-import { renderMapContains } from './_setup';
+import { renderMapContains, renderMapContainsImports } from './_setup';
 
 test('it renders PDA helpers for PDA with no seeds', async () => {
     // Given the following program with 1 account and 1 pda with empty seeds.
@@ -145,3 +145,46 @@ test('it renders constants for account constant discriminators', async () => {
         'export function getMyAccountDiscriminator2Bytes() { return getBytesEncoder().encode(MY_ACCOUNT_DISCRIMINATOR2); }',
     ]);
 });
+
+test('it can extracts account data and import it from another source', async () => {
+    // Given the following account.
+    const node = programNode({
+        accounts: [
+            accountNode({
+                data: structTypeNode([structFieldTypeNode({ name: 'value', type: numberTypeNode('u32') })]),
+                name: 'counter',
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it using the following custom account data options.
+    const renderMap = visit(
+        node,
+        getRenderMapVisitor({
+            customAccountData: [
+                {
+                    extract: true,
+                    importFrom: 'someModule',
+                    name: 'counter',
+                },
+            ],
+        }),
+    );
+
+    // Then we expect the account data to be fetched from the hooked directory (by default).
+    await renderMapContainsImports(renderMap, 'accounts/counter.ts', {
+        someModule: ['getCounterAccountDataDecoder', 'type CounterAccountData'],
+    });
+
+    // And we expect the existing account data to be extracted into a new type
+    // so it can be imported from the hooked directory.
+    await renderMapContains(renderMap, 'types/counterAccountData.ts', [
+        'export type CounterAccountData',
+        'export type CounterAccountDataArgs',
+        'export function getCounterAccountDataEncoder',
+        'export function getCounterAccountDataDecoder',
+        'export function getCounterAccountDataCodec',
+    ]);
+});

+ 40 - 0
packages/renderers-js/test/instructionsPage.test.ts

@@ -357,3 +357,43 @@ test('it renders constants for instruction constant discriminators', async () =>
         'export function getMyInstructionDiscriminator2Bytes() { return getBytesEncoder().encode(MY_INSTRUCTION_DISCRIMINATOR2); }',
     ]);
 });
+
+test('it can override the import of a resolver value node', async () => {
+    // Given the following node with a resolver value node.
+    const node = programNode({
+        instructions: [
+            instructionNode({
+                accounts: [
+                    instructionAccountNode({
+                        defaultValue: resolverValueNode('myResolver'),
+                        isSigner: false,
+                        isWritable: false,
+                        name: 'myAccount',
+                    }),
+                ],
+                name: 'myInstruction',
+            }),
+        ],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'counter', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we render it using a custom import.
+    const renderMap = visit(
+        node,
+        getRenderMapVisitor({
+            linkOverrides: {
+                resolvers: { myResolver: 'someModule' },
+            },
+        }),
+    );
+
+    // Then we expect the resolver to be exported.
+    await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['myResolver(resolverScope)']);
+
+    // And its import path to be overridden.
+    await renderMapContainsImports(renderMap, 'instructions/myInstruction.ts', {
+        someModule: ['myResolver'],
+    });
+});

+ 65 - 0
packages/renderers-js/test/links/account.test.ts

@@ -0,0 +1,65 @@
+import { accountLinkNode, accountNode, instructionByteDeltaNode, instructionNode, programNode } from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import { test } from 'vitest';
+
+import { getRenderMapVisitor } from '../../src';
+import { renderMapContains, renderMapContainsImports } from '../_setup';
+
+test('it imports functions from the linked account', async () => {
+    // Given the following node.
+    const node = programNode({
+        accounts: [accountNode({ name: 'counter', size: 42 })],
+        instructions: [
+            instructionNode({
+                byteDeltas: [instructionByteDeltaNode(accountLinkNode('counter'))],
+                name: 'createCounter',
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the following to be exported.
+    await renderMapContains(renderMap, 'instructions/createCounter.ts', ['getCounterSize()']);
+
+    // And we expect the following imports.
+    await renderMapContainsImports(renderMap, 'instructions/createCounter.ts', {
+        '../accounts': ['getCounterSize'],
+    });
+});
+
+test('it can override the import of a linked account', async () => {
+    // Given the following node.
+    const node = programNode({
+        accounts: [accountNode({ name: 'counter', size: 42 })],
+        instructions: [
+            instructionNode({
+                byteDeltas: [instructionByteDeltaNode(accountLinkNode('counter'))],
+                name: 'createCounter',
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it using a custom import.
+    const renderMap = visit(
+        node,
+        getRenderMapVisitor({
+            linkOverrides: {
+                accounts: { counter: 'hooked' },
+            },
+        }),
+    );
+
+    // Then we expect the following to be exported.
+    await renderMapContains(renderMap, 'instructions/createCounter.ts', ['getCounterSize()']);
+
+    // And we expect the imports to be overridden.
+    await renderMapContainsImports(renderMap, 'instructions/createCounter.ts', {
+        '../../hooked': ['getCounterSize'],
+    });
+});

+ 93 - 0
packages/renderers-js/test/links/definedType.test.ts

@@ -0,0 +1,93 @@
+import {
+    definedTypeLinkNode,
+    definedTypeNode,
+    fixedSizeTypeNode,
+    programNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import { test } from 'vitest';
+
+import { getRenderMapVisitor } from '../../src';
+import { renderMapContains, renderMapContainsImports } from '../_setup';
+
+test('it imports types and functions from the linked type', async () => {
+    // Given the following node.
+    const node = programNode({
+        definedTypes: [
+            definedTypeNode({
+                name: 'symbol',
+                type: fixedSizeTypeNode(stringTypeNode('utf8'), 5),
+            }),
+            definedTypeNode({
+                name: 'metadata',
+                type: structTypeNode([
+                    structFieldTypeNode({ name: 'identifier', type: definedTypeLinkNode('symbol') }),
+                ]),
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the following types and codecs to be exported.
+    await renderMapContains(renderMap, 'types/metadata.ts', [
+        'export type Metadata = { identifier: Symbol }',
+        'export type MetadataArgs = { identifier: SymbolArgs }',
+        'getSymbolEncoder()',
+        'getSymbolDecoder()',
+    ]);
+
+    // And we expect the following imports.
+    await renderMapContainsImports(renderMap, 'types/metadata.ts', {
+        '.': ['Symbol', 'SymbolArgs', 'getSymbolEncoder', 'getSymbolDecoder'],
+    });
+});
+
+test('it can override the import of a linked type', async () => {
+    // Given the following node.
+    const node = programNode({
+        definedTypes: [
+            definedTypeNode({
+                name: 'symbol',
+                type: fixedSizeTypeNode(stringTypeNode('utf8'), 5),
+            }),
+            definedTypeNode({
+                name: 'metadata',
+                type: structTypeNode([
+                    structFieldTypeNode({ name: 'identifier', type: definedTypeLinkNode('symbol') }),
+                ]),
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it using a custom import.
+    const renderMap = visit(
+        node,
+        getRenderMapVisitor({
+            linkOverrides: {
+                definedTypes: { symbol: 'hooked' },
+            },
+        }),
+    );
+
+    // Then we expect the following types and codecs to be exported.
+    await renderMapContains(renderMap, 'types/metadata.ts', [
+        'export type Metadata = { identifier: Symbol }',
+        'export type MetadataArgs = { identifier: SymbolArgs }',
+        'getSymbolEncoder()',
+        'getSymbolDecoder()',
+    ]);
+
+    // And we expect the imports to be overridden.
+    await renderMapContainsImports(renderMap, 'types/metadata.ts', {
+        '../../hooked': ['Symbol', 'SymbolArgs', 'getSymbolEncoder', 'getSymbolDecoder'],
+    });
+});

+ 86 - 0
packages/renderers-js/test/links/pda.test.ts

@@ -0,0 +1,86 @@
+import {
+    instructionAccountNode,
+    instructionNode,
+    pdaLinkNode,
+    pdaNode,
+    pdaValueNode,
+    programNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import { test } from 'vitest';
+
+import { getRenderMapVisitor } from '../../src';
+import { renderMapContains, renderMapContainsImports } from '../_setup';
+
+test('it imports functions from the linked pda', async () => {
+    // Given the following node.
+    const node = programNode({
+        instructions: [
+            instructionNode({
+                accounts: [
+                    instructionAccountNode({
+                        defaultValue: pdaValueNode(pdaLinkNode('counter')),
+                        isSigner: true,
+                        isWritable: true,
+                        name: 'counter',
+                    }),
+                ],
+                name: 'createCounter',
+            }),
+        ],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'counter', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the following to be exported.
+    await renderMapContains(renderMap, 'instructions/createCounter.ts', ['await findCounterPda()']);
+
+    // And we expect the following imports.
+    await renderMapContainsImports(renderMap, 'instructions/createCounter.ts', {
+        '../pdas': ['findCounterPda'],
+    });
+});
+
+test('it can override the import of a linked account', async () => {
+    // Given the following node.
+    const node = programNode({
+        instructions: [
+            instructionNode({
+                accounts: [
+                    instructionAccountNode({
+                        defaultValue: pdaValueNode(pdaLinkNode('counter')),
+                        isSigner: true,
+                        isWritable: true,
+                        name: 'counter',
+                    }),
+                ],
+                name: 'createCounter',
+            }),
+        ],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'counter', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we render it using a custom import.
+    const renderMap = visit(
+        node,
+        getRenderMapVisitor({
+            linkOverrides: {
+                pdas: { counter: 'hooked' },
+            },
+        }),
+    );
+
+    // Then we expect the following to be exported.
+    await renderMapContains(renderMap, 'instructions/createCounter.ts', ['await findCounterPda()']);
+
+    // And we expect the imports to be overridden.
+    await renderMapContainsImports(renderMap, 'instructions/createCounter.ts', {
+        '../../hooked': ['findCounterPda'],
+    });
+});

+ 92 - 0
packages/renderers-js/test/links/program.test.ts

@@ -0,0 +1,92 @@
+import {
+    instructionAccountNode,
+    instructionNode,
+    pdaNode,
+    programLinkNode,
+    programNode,
+    rootNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import { test } from 'vitest';
+
+import { getRenderMapVisitor } from '../../src';
+import { renderMapContains, renderMapContainsImports } from '../_setup';
+
+test('it imports constants from the linked program', async () => {
+    // Given the following node.
+    const node = rootNode(
+        programNode({
+            instructions: [
+                instructionNode({
+                    accounts: [
+                        instructionAccountNode({
+                            defaultValue: programLinkNode('someOtherProgram'),
+                            isSigner: false,
+                            isWritable: false,
+                            name: 'otherProgram',
+                        }),
+                    ],
+                    name: 'myInstruction',
+                }),
+            ],
+            name: 'myProgram',
+            pdas: [pdaNode({ name: 'counter', seeds: [] })],
+            publicKey: '1111',
+        }),
+        [programNode({ name: 'someOtherProgram', publicKey: '2222' })],
+    );
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the following to be exported.
+    await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['SOME_OTHER_PROGRAM_PROGRAM_ADDRESS']);
+
+    // And we expect the following imports.
+    await renderMapContainsImports(renderMap, 'instructions/myInstruction.ts', {
+        '../programs': ['SOME_OTHER_PROGRAM_PROGRAM_ADDRESS'],
+    });
+});
+
+test('it can override the import of a linked account', async () => {
+    // Given the following node.
+    const node = rootNode(
+        programNode({
+            instructions: [
+                instructionNode({
+                    accounts: [
+                        instructionAccountNode({
+                            defaultValue: programLinkNode('someOtherProgram'),
+                            isSigner: false,
+                            isWritable: false,
+                            name: 'otherProgram',
+                        }),
+                    ],
+                    name: 'myInstruction',
+                }),
+            ],
+            name: 'myProgram',
+            pdas: [pdaNode({ name: 'counter', seeds: [] })],
+            publicKey: '1111',
+        }),
+        [programNode({ name: 'someOtherProgram', publicKey: '2222' })],
+    );
+
+    // When we render it using a custom import.
+    const renderMap = visit(
+        node,
+        getRenderMapVisitor({
+            linkOverrides: {
+                programs: { someOtherProgram: 'hooked' },
+            },
+        }),
+    );
+
+    // Then we expect the following to be exported.
+    await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['SOME_OTHER_PROGRAM_PROGRAM_ADDRESS']);
+
+    // And we expect the imports to be overridden.
+    await renderMapContainsImports(renderMap, 'instructions/myInstruction.ts', {
+        '../../hooked': ['SOME_OTHER_PROGRAM_PROGRAM_ADDRESS'],
+    });
+});

+ 9 - 8
packages/renderers-rust/README.md

@@ -37,11 +37,12 @@ kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 
 The `renderVisitor` accepts the following options.
 
-| Name                          | Type                         | Default     | Description                                                                                                                                                             |
-| ----------------------------- | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `deleteFolderBeforeRendering` | `boolean`                    | `true`      | Whether the base directory should be cleaned before generating new files.                                                                                               |
-| `formatCode`                  | `boolean`                    | `false`     | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided.                                          |
-| `toolchain`                   | `string`                     | `"+stable"` | The toolchain to use when formatting the generated code.                                                                                                                |
-| `crateFolder`                 | `string`                     | none        | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`.                                                              |
-| `dependencyMap`               | `Record<ImportFrom, string>` | `{}`        | A mapping between import aliases and their actual crate name or path in Rust.                                                                                           |
-| `renderParentInstructions`    | `boolean`                    | `false`     | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered. |
+| Name                          | Type                                                                                                  | Default     | Description                                                                                                                                                                                      |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `deleteFolderBeforeRendering` | `boolean`                                                                                             | `true`      | Whether the base directory should be cleaned before generating new files.                                                                                                                        |
+| `formatCode`                  | `boolean`                                                                                             | `false`     | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided.                                                                   |
+| `toolchain`                   | `string`                                                                                              | `"+stable"` | The toolchain to use when formatting the generated code.                                                                                                                                         |
+| `crateFolder`                 | `string`                                                                                              | none        | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`.                                                                                       |
+| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`        | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
+| `dependencyMap`               | `Record<string, string>`                                                                              | `{}`        | A mapping between import aliases and their actual crate name or path in Rust.                                                                                                                    |
+| `renderParentInstructions`    | `boolean`                                                                                             | `false`     | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                          |

+ 2 - 4
packages/renderers-rust/src/ImportMap.ts

@@ -1,5 +1,3 @@
-import { ImportFrom } from '@kinobi-so/nodes';
-
 import { TypeManifest } from './getTypeManifestVisitor';
 
 const DEFAULT_MODULE_MAP: Record<string, string> = {
@@ -58,7 +56,7 @@ export class ImportMap {
         return this._imports.size === 0;
     }
 
-    resolveDependencyMap(dependencies: Record<ImportFrom, string>): ImportMap {
+    resolveDependencyMap(dependencies: Record<string, string>): ImportMap {
         const dependencyMap = { ...DEFAULT_MODULE_MAP, ...dependencies };
         const newImportMap = new ImportMap();
         const resolveDependency = (i: string): string => {
@@ -72,7 +70,7 @@ export class ImportMap {
         return newImportMap;
     }
 
-    toString(dependencies: Record<ImportFrom, string>): string {
+    toString(dependencies: Record<string, string>): string {
         const resolvedMap = this.resolveDependencyMap(dependencies);
         const importStatements = [...resolvedMap.imports].map(i => {
             const alias = resolvedMap.aliases.get(i);

+ 12 - 6
packages/renderers-rust/src/getRenderMapVisitor.ts

@@ -4,7 +4,6 @@ import {
     getAllDefinedTypes,
     getAllInstructionsWithSubs,
     getAllPrograms,
-    ImportFrom,
     InstructionNode,
     isNode,
     isNodeFilter,
@@ -28,10 +27,11 @@ import {
 import { getTypeManifestVisitor } from './getTypeManifestVisitor';
 import { ImportMap } from './ImportMap';
 import { renderValueNode } from './renderValueNodeVisitor';
-import { render } from './utils';
+import { getImportFromFactory, LinkOverrides, render } from './utils';
 
 export type GetRenderMapOptions = {
-    dependencyMap?: Record<ImportFrom, string>;
+    dependencyMap?: Record<string, string>;
+    linkOverrides?: LinkOverrides;
     renderParentInstructions?: boolean;
 };
 
@@ -41,7 +41,8 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
 
     const renderParentInstructions = options.renderParentInstructions ?? false;
     const dependencyMap = options.dependencyMap ?? {};
-    const typeManifestVisitor = getTypeManifestVisitor();
+    const getImportFrom = getImportFromFactory(options.linkOverrides ?? {});
+    const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom });
 
     return pipe(
         staticVisitor(
@@ -68,7 +69,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
                             return seed;
                         }
                         const seedManifest = visit(seed.type, typeManifestVisitor);
-                        const valueManifest = renderValueNode(seed.value, true);
+                        const valueManifest = renderValueNode(seed.value, getImportFrom, true);
                         seedsImports.mergeWith(valueManifest.imports);
                         const resolvedType = resolveNestedTypeNode(seed.type);
                         return { ...seed, resolvedType, typeManifest: seedManifest, valueManifest };
@@ -145,6 +146,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
 
                     node.arguments.forEach(argument => {
                         const argumentVisitor = getTypeManifestVisitor({
+                            getImportFrom,
                             nestedStruct: true,
                             parentName: `${pascalCase(node.name)}InstructionData`,
                         });
@@ -157,7 +159,10 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
                         const hasDefaultValue = !!argument.defaultValue && isNode(argument.defaultValue, VALUE_NODES);
                         let renderValue: string | null = null;
                         if (hasDefaultValue) {
-                            const { imports: argImports, render: value } = renderValueNode(argument.defaultValue);
+                            const { imports: argImports, render: value } = renderValueNode(
+                                argument.defaultValue,
+                                getImportFrom,
+                            );
                             imports.mergeWith(argImports);
                             renderValue = value;
                         }
@@ -181,6 +186,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
 
                     const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);
                     const structVisitor = getTypeManifestVisitor({
+                        getImportFrom,
                         parentName: `${pascalCase(node.name)}InstructionData`,
                     });
                     const typeManifest = visit(struct, structVisitor);

+ 8 - 3
packages/renderers-rust/src/getTypeManifestVisitor.ts

@@ -17,7 +17,7 @@ import {
 import { extendVisitor, mergeVisitor, pipe, visit } from '@kinobi-so/visitors-core';
 
 import { ImportMap } from './ImportMap';
-import { rustDocblock } from './utils';
+import { GetImportFromFunction, rustDocblock } from './utils';
 
 export type TypeManifest = {
     imports: ImportMap;
@@ -25,7 +25,12 @@ export type TypeManifest = {
     type: string;
 };
 
-export function getTypeManifestVisitor(options: { nestedStruct?: boolean; parentName?: string | null } = {}) {
+export function getTypeManifestVisitor(options: {
+    getImportFrom: GetImportFromFunction;
+    nestedStruct?: boolean;
+    parentName?: string | null;
+}) {
+    const { getImportFrom } = options;
     let parentName: string | null = options.parentName ?? null;
     let nestedStruct: boolean = options.nestedStruct ?? false;
     let inlineStruct: boolean = false;
@@ -175,7 +180,7 @@ export function getTypeManifestVisitor(options: { nestedStruct?: boolean; parent
 
                 visitDefinedTypeLink(node) {
                     const pascalCaseDefinedType = pascalCase(node.name);
-                    const importFrom = node.importFrom ?? 'generatedTypes';
+                    const importFrom = getImportFrom(node);
                     return {
                         imports: new ImportMap().add(`${importFrom}::${pascalCaseDefinedType}`),
                         nestedStructs: [],

+ 8 - 4
packages/renderers-rust/src/renderValueNodeVisitor.ts

@@ -10,19 +10,23 @@ import {
 import { visit, Visitor } from '@kinobi-so/visitors-core';
 
 import { ImportMap } from './ImportMap';
-import { getBytesFromBytesValueNode } from './utils';
+import { getBytesFromBytesValueNode, GetImportFromFunction } from './utils';
 
 export function renderValueNode(
     value: ValueNode,
+    getImportFrom: GetImportFromFunction,
     useStr: boolean = false,
 ): {
     imports: ImportMap;
     render: string;
 } {
-    return visit(value, renderValueNodeVisitor(useStr));
+    return visit(value, renderValueNodeVisitor(getImportFrom, useStr));
 }
 
-export function renderValueNodeVisitor(useStr: boolean = false): Visitor<
+export function renderValueNodeVisitor(
+    getImportFrom: GetImportFromFunction,
+    useStr: boolean = false,
+): Visitor<
     {
         imports: ImportMap;
         render: string;
@@ -68,7 +72,7 @@ export function renderValueNodeVisitor(useStr: boolean = false): Visitor<
             const imports = new ImportMap();
             const enumName = pascalCase(node.enum.name);
             const variantName = pascalCase(node.variant);
-            const importFrom = node.enum.importFrom ?? 'generatedTypes';
+            const importFrom = getImportFrom(node.enum);
             imports.add(`${importFrom}::${enumName}`);
             if (!node.value) {
                 return { imports, render: `${enumName}::${variantName}` };

+ 1 - 0
packages/renderers-rust/src/utils/index.ts

@@ -1,2 +1,3 @@
 export * from './codecs';
+export * from './linkOverrides';
 export * from './render';

+ 44 - 0
packages/renderers-rust/src/utils/linkOverrides.ts

@@ -0,0 +1,44 @@
+import { KINOBI_ERROR__UNEXPECTED_NODE_KIND, KinobiError } from '@kinobi-so/errors';
+import { LINK_NODES, LinkNode, ResolverValueNode } from '@kinobi-so/nodes';
+
+export type LinkOverrides = {
+    accounts?: Record<string, string>;
+    definedTypes?: Record<string, string>;
+    pdas?: Record<string, string>;
+    programs?: Record<string, string>;
+    resolvers?: Record<string, string>;
+};
+
+export type GetImportFromFunction = (node: LinkNode | ResolverValueNode, fallback?: string) => string;
+
+export function getImportFromFactory(overrides: LinkOverrides): GetImportFromFunction {
+    const linkOverrides = {
+        accounts: overrides.accounts ?? {},
+        definedTypes: overrides.definedTypes ?? {},
+        pdas: overrides.pdas ?? {},
+        programs: overrides.programs ?? {},
+        resolvers: overrides.resolvers ?? {},
+    };
+
+    return (node: LinkNode | ResolverValueNode) => {
+        const kind = node.kind;
+        switch (kind) {
+            case 'accountLinkNode':
+                return linkOverrides.accounts[node.name] ?? 'generatedAccounts';
+            case 'definedTypeLinkNode':
+                return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';
+            case 'pdaLinkNode':
+                return linkOverrides.pdas[node.name] ?? 'generatedAccounts';
+            case 'programLinkNode':
+                return linkOverrides.programs[node.name] ?? 'generatedPrograms';
+            case 'resolverValueNode':
+                return linkOverrides.resolvers[node.name] ?? 'hooked';
+            default:
+                throw new KinobiError(KINOBI_ERROR__UNEXPECTED_NODE_KIND, {
+                    expectedKinds: [...LINK_NODES, 'resolverValueNode'],
+                    kind: kind satisfies never,
+                    node,
+                });
+        }
+    };
+}

+ 1 - 1
packages/validators/src/getValidationItemsVisitor.ts

@@ -47,7 +47,7 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
                     const items = [] as ValidationItem[];
                     if (!node.name) {
                         items.push(validationItem('error', 'Pointing to a defined type with no name.', node, stack));
-                    } else if (!node.importFrom && !linkables.has(node)) {
+                    } else if (!linkables.has(node)) {
                         items.push(
                             validationItem(
                                 'error',

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

@@ -69,9 +69,6 @@ export class LinkableDictionary {
     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);
         }
@@ -88,9 +85,6 @@ export class LinkableDictionary {
     }
 
     has(linkNode: LinkNode): boolean {
-        if (linkNode.importFrom) {
-            return false;
-        }
         if (isNode(linkNode, 'programLinkNode')) {
             return this.programs.has(linkNode.name);
         }

+ 0 - 2
packages/visitors-core/src/getByteSizeVisitor.ts

@@ -59,8 +59,6 @@ export function getByteSizeVisitor(linkables: LinkableDictionary): Visitor<numbe
         },
 
         visitDefinedTypeLink(node) {
-            if (node.importFrom) return null;
-
             // Fetch the linked type and return null if not found.
             // The validator visitor will throw a proper error later on.
             const linkedDefinedType = linkables.get(node);

+ 2 - 2
packages/visitors-core/src/getDebugStringVisitor.ts

@@ -67,7 +67,7 @@ function getNodeDetails(node: Node): string[] {
         case 'pdaLinkNode':
         case 'accountLinkNode':
         case 'definedTypeLinkNode':
-            return [node.name, ...(node.importFrom ? [`from:${node.importFrom}`] : [])];
+            return [node.name];
         case 'numberTypeNode':
             return [node.format, ...(node.endian === 'be' ? ['bigEndian'] : [])];
         case 'amountTypeNode':
@@ -91,7 +91,7 @@ function getNodeDetails(node: Node): string[] {
         case 'enumValueNode':
             return [node.variant];
         case 'resolverValueNode':
-            return [node.name, ...(node.importFrom ? [`from:${node.importFrom}`] : [])];
+            return [node.name];
         case 'constantDiscriminatorNode':
             return [...(node.offset > 0 ? [`offset:${node.offset}`] : [])];
         case 'fieldDiscriminatorNode':

+ 1 - 2
packages/visitors-core/test/nodes/contextualValueNodes/ResolverValueNode.test.ts

@@ -10,7 +10,6 @@ import {
 
 const node = resolverValueNode('myCustomResolver', {
     dependsOn: [accountValueNode('mint'), argumentValueNode('tokenStandard')],
-    importFrom: 'hooked',
 });
 
 test('mergeVisitor', () => {
@@ -30,7 +29,7 @@ test('debugStringVisitor', () => {
     expectDebugStringVisitor(
         node,
         `
-resolverValueNode [myCustomResolver.from:hooked]
+resolverValueNode [myCustomResolver]
 |   accountValueNode [mint]
 |   argumentValueNode [tokenStandard]`,
     );

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

@@ -8,7 +8,7 @@ import {
     expectMergeVisitorCount,
 } from '../_setup';
 
-const node = accountLinkNode('token', 'splToken');
+const node = accountLinkNode('token');
 
 test('mergeVisitor', () => {
     expectMergeVisitorCount(node, 1);
@@ -23,5 +23,5 @@ test('deleteNodesVisitor', () => {
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `accountLinkNode [token.from:splToken]`);
+    expectDebugStringVisitor(node, `accountLinkNode [token]`);
 });

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

@@ -8,7 +8,7 @@ import {
     expectMergeVisitorCount,
 } from '../_setup';
 
-const node = definedTypeLinkNode('tokenState', 'splToken');
+const node = definedTypeLinkNode('tokenState');
 
 test('mergeVisitor', () => {
     expectMergeVisitorCount(node, 1);
@@ -23,5 +23,5 @@ test('deleteNodesVisitor', () => {
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `definedTypeLinkNode [tokenState.from:splToken]`);
+    expectDebugStringVisitor(node, `definedTypeLinkNode [tokenState]`);
 });

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

@@ -8,7 +8,7 @@ import {
     expectMergeVisitorCount,
 } from '../_setup';
 
-const node = pdaLinkNode('associatedToken', 'splToken');
+const node = pdaLinkNode('associatedToken');
 
 test('mergeVisitor', () => {
     expectMergeVisitorCount(node, 1);
@@ -23,5 +23,5 @@ test('deleteNodesVisitor', () => {
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `pdaLinkNode [associatedToken.from:splToken]`);
+    expectDebugStringVisitor(node, `pdaLinkNode [associatedToken]`);
 });

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

@@ -8,7 +8,7 @@ import {
     expectMergeVisitorCount,
 } from '../_setup';
 
-const node = programLinkNode('mplCandyGuard', 'mplCandyMachine');
+const node = programLinkNode('mplCandyGuard');
 
 test('mergeVisitor', () => {
     expectMergeVisitorCount(node, 1);
@@ -23,5 +23,5 @@ test('deleteNodesVisitor', () => {
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `programLinkNode [mplCandyGuard.from:mplCandyMachine]`);
+    expectDebugStringVisitor(node, `programLinkNode [mplCandyGuard]`);
 });

+ 0 - 4
packages/visitors/src/getDefinedTypeHistogramVisitor.ts

@@ -67,10 +67,6 @@ export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram>
                 },
 
                 visitDefinedTypeLink(node) {
-                    if (node.importFrom) {
-                        return {};
-                    }
-
                     return {
                         [node.name]: {
                             directlyAsInstructionArgs: Number(mode === 'instruction' && stackLevel <= 1),

+ 1 - 1
packages/visitors/src/unwrapDefinedTypesVisitor.ts

@@ -20,7 +20,7 @@ export function unwrapDefinedTypesVisitor(typesToInline: string[] | '*' = '*') {
         v =>
             extendVisitor(v, {
                 visitDefinedTypeLink(linkType, { self }) {
-                    if (!shouldInline(linkType.name) || linkType.importFrom) {
+                    if (!shouldInline(linkType.name)) {
                         return linkType;
                     }
                     return visit(linkables.getOrThrow(linkType).type, self);

+ 0 - 1
packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts

@@ -49,7 +49,6 @@ export function unwrapTupleEnumWithSingleStructVisitor(enumsOrVariantsToUnwrap:
                         if (tupleNode.items.length !== 1) return node;
                         let item = tupleNode.items[0];
                         if (isNode(item, 'definedTypeLinkNode')) {
-                            if (item.importFrom) return node;
                             const definedType = definedTypes.get(item.name);
                             if (!definedType) return node;
                             if (!isNode(definedType.type, 'structTypeNode')) return node;

+ 0 - 1
packages/visitors/src/unwrapTypeDefinedLinksVisitor.ts

@@ -14,7 +14,6 @@ export function unwrapTypeDefinedLinksVisitor(definedLinksType: string[]) {
         select: ['[definedTypeLinkNode]', selector],
         transform: node => {
             assertIsNode(node, 'definedTypeLinkNode');
-            if (node.importFrom) return node;
             return linkables.getOrThrow(node).type;
         },
     }));

+ 1 - 3
packages/visitors/src/updateAccountsVisitor.ts

@@ -39,7 +39,7 @@ export function updateAccountsVisitor(map: Record<string, AccountUpdates>) {
 
                         const { seeds, pda, ...assignableUpdates } = updates;
                         let newPda = node.pda;
-                        if (pda && !pda.importFrom && seeds !== undefined) {
+                        if (pda && seeds !== undefined) {
                             newPda = pda;
                             pdasToUpsert.push({
                                 pda: pdaNode({ name: pda.name, seeds }),
@@ -95,7 +95,6 @@ export function updateAccountsVisitor(map: Record<string, AccountUpdates>) {
                         select: ['[accountLinkNode]', selector],
                         transform: node => {
                             assertIsNode(node, 'accountLinkNode');
-                            if (node.importFrom) return node;
                             return accountLinkNode(newName);
                         },
                     },
@@ -110,7 +109,6 @@ export function updateAccountsVisitor(map: Record<string, AccountUpdates>) {
                         select: ['[pdaLinkNode]', selector],
                         transform: node => {
                             assertIsNode(node, 'pdaLinkNode');
-                            if (node.importFrom) return node;
                             return pdaLinkNode(newName);
                         },
                     },

+ 0 - 1
packages/visitors/src/updateDefinedTypesVisitor.ts

@@ -52,7 +52,6 @@ export function updateDefinedTypesVisitor(map: Record<string, DefinedTypeUpdates
                     select: ['[definedTypeLinkNode]', selector],
                     transform: node => {
                         assertIsNode(node, 'definedTypeLinkNode');
-                        if (node.importFrom) return node;
                         return definedTypeLinkNode(newName);
                     },
                 });

+ 0 - 1
packages/visitors/src/updateProgramsVisitor.ts

@@ -27,7 +27,6 @@ export function updateProgramsVisitor(map: Record<string, ProgramUpdates>) {
                     select: `[programLinkNode]${name}`,
                     transform: node => {
                         assertIsNode(node, 'programLinkNode');
-                        if (node.importFrom) return node;
                         return programLinkNode(newName);
                     },
                 });