Skip to content

Commit 506e7f1

Browse files
authored
feat(dynamic-client): types generation split (#1002)
This PR splits types generation between dynamic-instructions, dynamic-address-resolution, dynamic-client. Each package now owns the types for the concepts it implements, dynamic-client composes them.
1 parent 2f8a25a commit 506e7f1

59 files changed

Lines changed: 2028 additions & 352 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/loud-taxis-appear.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@codama/dynamic-address-resolution': minor
3+
'@codama/dynamic-instructions': minor
4+
'@codama/dynamic-client': patch
5+
---
6+
7+
Split types generation in `@codama/dynamic-address-resolution`, `@codama/dynamic-instructions` and `@codama/dynamic-client`.

packages/dynamic-address-resolution/README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,23 @@ pnpm install @codama/dynamic-address-resolution
2626

2727
## Types generation
2828

29-
> For now, types for arguments, accounts, custom resolvers, and PDA seeds can be produced via [`@codama/dynamic-client`](../dynamic-client/README.md)'s `generate-client-types` command, and passed as generics to the functions. Types generation for this package will be added in a follow-up release.
29+
This package can emit TypeScript the input types required for address resolution of each instruction — `${Name}Args`, `${Name}Accounts`, `${Name}Resolvers`.
30+
31+
### CLI
32+
33+
```sh
34+
npx @codama/dynamic-address-resolution generate-types <path/to/idl.json> <output-dir>
35+
```
36+
37+
Writes `<idl-name>-address-resolution-types.ts` to the output directory.
38+
39+
### Programmatic
40+
41+
```ts
42+
import { generateTypes } from '@codama/dynamic-address-resolution/codegen';
43+
44+
const source = generateTypes(idl);
45+
```
3046

3147
## Functions
3248

@@ -49,7 +65,7 @@ const address = await resolveInstructionAccountAddress({
4965
**Typed:**
5066

5167
```ts
52-
import type { TransferSolAccounts, TransferSolArgs } from './generated/system-program-idl-types';
68+
import type { TransferSolAccounts, TransferSolArgs } from './generated/system-program-idl-address-resolution-types';
5369

5470
const address = await resolveInstructionAccountAddress<TransferSolAccounts, TransferSolArgs>({
5571
accountsInput: { source, destination },
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env node
2+
3+
const run = require('../dist/cli.cjs').run;
4+
5+
run(process.argv);

packages/dynamic-address-resolution/package.json

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@
22
"name": "@codama/dynamic-address-resolution",
33
"version": "0.2.1",
44
"description": "Address resolution functionality for instruction accounts in Codama IDLs",
5+
"bin": {
6+
"dynamic-address-resolution": "./bin/cli.cjs"
7+
},
58
"exports": {
6-
"types": "./dist/types/index.d.ts",
7-
"react-native": "./dist/index.react-native.mjs",
8-
"browser": {
9-
"import": "./dist/index.browser.mjs",
10-
"require": "./dist/index.browser.cjs"
9+
".": {
10+
"types": "./dist/types/index.d.ts",
11+
"react-native": "./dist/index.react-native.mjs",
12+
"browser": {
13+
"import": "./dist/index.browser.mjs",
14+
"require": "./dist/index.browser.cjs"
15+
},
16+
"node": {
17+
"import": "./dist/index.node.mjs",
18+
"require": "./dist/index.node.cjs"
19+
}
1120
},
12-
"node": {
13-
"import": "./dist/index.node.mjs",
14-
"require": "./dist/index.node.cjs"
21+
"./codegen": {
22+
"types": "./dist/types/codegen/index.d.ts",
23+
"import": "./dist/codegen.node.mjs",
24+
"require": "./dist/codegen.node.cjs"
1525
}
1626
},
1727
"browser": {
@@ -22,10 +32,20 @@
2232
"module": "./dist/index.node.mjs",
2333
"react-native": "./dist/index.react-native.mjs",
2434
"types": "./dist/types/index.d.ts",
35+
"typesVersions": {
36+
"*": {
37+
"codegen": [
38+
"./dist/types/codegen/index.d.ts"
39+
]
40+
}
41+
},
2542
"type": "commonjs",
2643
"files": [
2744
"./dist/types",
28-
"./dist/index.*"
45+
"./dist/index.*",
46+
"./dist/cli.*",
47+
"./dist/codegen.*",
48+
"./bin/*"
2949
],
3050
"sideEffects": false,
3151
"keywords": [
@@ -50,7 +70,8 @@
5070
"@codama/errors": "workspace:*",
5171
"@solana/addresses": "^5.3.0",
5272
"@solana/codecs": "^5.3.0",
53-
"codama": "workspace:*"
73+
"codama": "workspace:*",
74+
"commander": "^14.0.2"
5475
},
5576
"devDependencies": {
5677
"@solana/kit": "6.5.0"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { generateTypes } from '../../../codegen/generate-types';
2+
import { generateTypesFromFile as generateTypesFromFileShared } from '../../../codegen/generate-types-from-file';
3+
4+
export function generateTypesFromFile(codamaIdlPath: string, outputDirPath: string): void {
5+
generateTypesFromFileShared({
6+
codamaIdlPath,
7+
generate: generateTypes,
8+
outputDirPath,
9+
outputFileSuffix: 'address-resolution-types',
10+
});
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Command } from 'commander';
2+
3+
import { generateTypesFromFile } from './generate-types-from-file';
4+
5+
export function registerGenerateTypesCommand(program: Command): void {
6+
program
7+
.command('generate-types')
8+
.description(
9+
'Generate TypeScript address-resolution types (PDA seeds, instruction Args/Accounts/Resolvers) from a Codama IDL JSON file',
10+
)
11+
.argument('<codama-idl>', 'Path to a Codama IDL JSON file (e.g., ./idl/codama.json)')
12+
.argument('<output-dir>', 'Path to the output directory for the generated .ts file, e.g., ./generated')
13+
.action((idlArg: string, outputDirArg: string) => {
14+
try {
15+
generateTypesFromFile(idlArg, outputDirArg);
16+
} catch (err) {
17+
console.error(err);
18+
process.exit(1);
19+
}
20+
});
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Command } from 'commander';
2+
3+
import { registerGenerateTypesCommand } from './generate-types/register-command';
4+
5+
export function registerCommands(program: Command): void {
6+
registerGenerateTypesCommand(program);
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createProgram } from './program';
2+
3+
const program = createProgram();
4+
5+
export function run(argv: string[]): void {
6+
if (argv.length <= 2) {
7+
program.outputHelp();
8+
return;
9+
}
10+
11+
program.parse(argv);
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Command } from 'commander';
2+
3+
import { registerCommands } from './commands';
4+
5+
export function createProgram(): Command {
6+
const program = new Command();
7+
8+
program
9+
.name('dynamic-address-resolution')
10+
.description('CLI for @codama/dynamic-address-resolution')
11+
.showHelpAfterError(true);
12+
13+
registerCommands(program);
14+
return program;
15+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { DefinedTypeNode, TypeNode } from 'codama';
2+
3+
/**
4+
* Convert Codama type to TypeScript type string.
5+
*/
6+
export function codamaTypeToTS(type: TypeNode | undefined, definedTypes: DefinedTypeNode[]): string {
7+
if (!type || typeof type !== 'object') return 'unknown';
8+
9+
switch (type.kind) {
10+
case 'numberTypeNode':
11+
return ['u64', 'u128', 'i64', 'i128'].includes(type.format) ? 'number | bigint' : 'number';
12+
case 'publicKeyTypeNode':
13+
return 'Address';
14+
case 'stringTypeNode':
15+
return 'string';
16+
case 'booleanTypeNode':
17+
return 'boolean';
18+
case 'optionTypeNode':
19+
return `${codamaTypeToTS(type.item, definedTypes)} | null`;
20+
case 'remainderOptionTypeNode':
21+
case 'zeroableOptionTypeNode':
22+
return `${codamaTypeToTS(type.item, definedTypes)} | null`;
23+
case 'bytesTypeNode':
24+
return 'Uint8Array';
25+
case 'fixedSizeTypeNode':
26+
case 'sizePrefixTypeNode':
27+
case 'hiddenPrefixTypeNode':
28+
case 'preOffsetTypeNode':
29+
case 'postOffsetTypeNode':
30+
case 'hiddenSuffixTypeNode':
31+
case 'sentinelTypeNode':
32+
return codamaTypeToTS(type.type, definedTypes);
33+
case 'amountTypeNode':
34+
case 'solAmountTypeNode':
35+
return 'number | bigint';
36+
case 'structTypeNode': {
37+
if (!type.fields || type.fields.length === 0) return '{}';
38+
const fields = type.fields
39+
.filter(f => f.defaultValueStrategy !== 'omitted')
40+
.map(f => `${f.name}: ${codamaTypeToTS(f.type, definedTypes)}`);
41+
if (fields.length === 0) return '{}';
42+
return `{ ${fields.join('; ')} }`;
43+
}
44+
case 'enumTypeNode': {
45+
if (!type.variants || type.variants.length === 0) return 'unknown /** empty variants in enumTypeNode */';
46+
const allEmpty = type.variants.every(v => v.kind === 'enumEmptyVariantTypeNode');
47+
if (allEmpty) {
48+
return type.variants.map(v => `'${v.name}'`).join(' | ');
49+
}
50+
const variantTypes = type.variants.map(v => {
51+
if (v.kind === 'enumEmptyVariantTypeNode') {
52+
return `{ __kind: '${v.name}' }`;
53+
}
54+
if (v.kind === 'enumStructVariantTypeNode' && v.struct) {
55+
const inner = codamaTypeToTS(v.struct, definedTypes);
56+
return `{ __kind: '${v.name}' } & ${inner}`;
57+
}
58+
if (v.kind === 'enumTupleVariantTypeNode' && v.tuple) {
59+
const inner = codamaTypeToTS(v.tuple, definedTypes);
60+
return `{ __kind: '${v.name}'; fields: ${inner} }`;
61+
}
62+
return `{ __kind: '${v.name}' }`;
63+
});
64+
return variantTypes.join(' | ');
65+
}
66+
case 'tupleTypeNode': {
67+
if (!type.items || type.items.length === 0) return '[]';
68+
const items = type.items.map(i => codamaTypeToTS(i, definedTypes));
69+
return `[${items.join(', ')}]`;
70+
}
71+
case 'arrayTypeNode':
72+
case 'setTypeNode': {
73+
const itemType = codamaTypeToTS(type.item, definedTypes);
74+
const needsParens = itemType.includes(' | ') || itemType.includes(' & ');
75+
return needsParens ? `(${itemType})[]` : `${itemType}[]`;
76+
}
77+
case 'mapTypeNode': {
78+
const v = codamaTypeToTS(type.value, definedTypes);
79+
return `Record<string, ${v}>`;
80+
}
81+
case 'definedTypeLinkNode': {
82+
if (!type.name) return 'unknown /** name missing in definedTypeLinkNode */';
83+
const def = definedTypes.find(d => d.name === type.name);
84+
if (!def) return 'unknown /** DefinedTypeNode not found for definedTypeLinkNode */';
85+
return codamaTypeToTS(def.type, definedTypes);
86+
}
87+
case 'dateTimeTypeNode': {
88+
return codamaTypeToTS(type.number, definedTypes);
89+
}
90+
default:
91+
type['kind'] satisfies never;
92+
return 'unknown';
93+
}
94+
}

0 commit comments

Comments
 (0)