codama-renderers-dart 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,18 +13,14 @@ Fixed-size Codama types generate non-truncating encoders. Values within the decl
13
13
  ```bash
14
14
  pnpm add codama-renderers-dart
15
15
  # or
16
-
17
- [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
18
- [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
19
- [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
20
16
  npm install codama-renderers-dart
21
17
  ```
22
18
 
23
- ## Quick Start
19
+ ## Quick start
24
20
 
25
21
  ### Programmatic API
26
22
 
27
- ```typescript
23
+ ```ts
28
24
  import { renderVisitor } from "codama-renderers-dart";
29
25
  import { visit } from "@codama/visitors-core";
30
26
  import { rootNode, programNode /* ... */ } from "@codama/nodes";
@@ -43,7 +39,7 @@ visit(root, renderVisitor("lib/src/generated", {
43
39
 
44
40
  `renderVisitor` accepts parsed Codama JSON as well as roots made with Codama constructors. Pina omits empty collections from its serialized IDLs; the renderer restores those structural defaults on an internal copy before traversal, leaving the parsed object unchanged.
45
41
 
46
- ```typescript
42
+ ```ts
47
43
  import { readFile } from "node:fs/promises";
48
44
  import { visit } from "@codama/visitors-core";
49
45
  import { renderVisitor } from "codama-renderers-dart";
@@ -76,7 +72,7 @@ Then run:
76
72
  codama run dart
77
73
  ```
78
74
 
79
- ## Generated Output Structure
75
+ ## Generated output structure
80
76
 
81
77
  For a program called `myProgram`, the renderer generates:
82
78
 
@@ -104,7 +100,7 @@ lib/src/generated/
104
100
  my_pda.dart # PDA seeds class + finder function
105
101
  ```
106
102
 
107
- ## Generated Code Patterns
103
+ ## Generated code patterns
108
104
 
109
105
  ### Accounts
110
106
 
@@ -166,7 +162,7 @@ Top-level instruction decoders require exact input consumption. Account decoders
166
162
 
167
163
  Optional instruction accounts use Codama's `programId` strategy by default. When an optional account is absent, the builder emits a readonly program-address placeholder so every later account keeps its declared index. The account slot is removed only when the instruction explicitly selects the legacy `omitted` strategy.
168
164
 
169
- ### Scalar Enums
165
+ ### Scalar enums
170
166
 
171
167
  Scalar enums (all-empty variants) generate a Dart `enum` with index-based encoder/decoder:
172
168
 
@@ -183,7 +179,7 @@ Decoder<AccountStatus> getAccountStatusDecoder() { ... }
183
179
 
184
180
  The generated enum API is the same for `u8`, `u16`, `u32`, and `u64` discriminators. The renderer converts `u64` indices to and from `BigInt` at the codec boundary and rejects out-of-range discriminators before converting them to Dart enum indices.
185
181
 
186
- ### Data Enums (Discriminated Unions)
182
+ ### Data enums (discriminated unions)
187
183
 
188
184
  Data enums generate Dart 3 `sealed class` hierarchies:
189
185
 
@@ -231,7 +227,7 @@ Future<(Address, int)> findMyPdaPda({
231
227
  }) async { ... }
232
228
  ```
233
229
 
234
- ## Type Mapping
230
+ ## Type mapping
235
231
 
236
232
  | Codama Type | Dart Type | Codec |
237
233
  | ----------------------------------- | --------------- | -------------------------------- |
@@ -266,7 +262,7 @@ Future<(Address, int)> findMyPdaPda({
266
262
 
267
263
  All naming conventions are customizable:
268
264
 
269
- ```typescript
265
+ ```ts
270
266
  import { renderVisitor, createDartNameApi } from "codama-renderers-dart";
271
267
 
272
268
  const nameApi = {
@@ -277,7 +273,7 @@ const nameApi = {
277
273
  visit(root, renderVisitor("output", { nameApi }));
278
274
  ```
279
275
 
280
- ## Target Packages
276
+ ## Target packages
281
277
 
282
278
  Generated Dart code depends on these solana_kit packages:
283
279
 
@@ -311,39 +307,10 @@ Key source files:
311
307
  ## Development
312
308
 
313
309
  ```bash
314
- # Install dependencies
315
-
316
- [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
317
- [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
318
- [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
319
310
  pnpm install
320
-
321
- # Type check
322
-
323
- [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
324
- [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
325
- [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
326
311
  npx tsc --noEmit
327
-
328
- # Run tests
329
-
330
- [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
331
- [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
332
- [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
333
312
  pnpm test
334
-
335
- # Watch mode
336
-
337
- [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
338
- [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
339
- [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
340
313
  pnpm test:watch
341
-
342
- # Build
343
-
344
- [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
345
- [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
346
- [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
347
314
  pnpm build
348
315
  ```
349
316
 
@@ -84,9 +84,83 @@ function setEmptyCollection(record, key) {
84
84
  function pascalCase(str) {
85
85
  return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase());
86
86
  }
87
+ var DART_RESERVED_WORDS = /* @__PURE__ */ new Set([
88
+ "abstract",
89
+ "as",
90
+ "assert",
91
+ "async",
92
+ "augment",
93
+ "await",
94
+ "base",
95
+ "break",
96
+ "case",
97
+ "catch",
98
+ "class",
99
+ "const",
100
+ "continue",
101
+ "covariant",
102
+ "default",
103
+ "deferred",
104
+ "do",
105
+ "dynamic",
106
+ "else",
107
+ "enum",
108
+ "export",
109
+ "extends",
110
+ "extension",
111
+ "external",
112
+ "factory",
113
+ "false",
114
+ "final",
115
+ "finally",
116
+ "for",
117
+ "get",
118
+ "hide",
119
+ "if",
120
+ "implements",
121
+ "import",
122
+ "in",
123
+ "interface",
124
+ "is",
125
+ "late",
126
+ "library",
127
+ "mixin",
128
+ "native",
129
+ "new",
130
+ "null",
131
+ "of",
132
+ "on",
133
+ "operator",
134
+ "part",
135
+ "required",
136
+ "rethrow",
137
+ "return",
138
+ "sealed",
139
+ "set",
140
+ "show",
141
+ "static",
142
+ "super",
143
+ "switch",
144
+ "sync",
145
+ "this",
146
+ "throw",
147
+ "true",
148
+ "try",
149
+ "typedef",
150
+ "var",
151
+ "void",
152
+ "when",
153
+ "while",
154
+ "with",
155
+ "yield"
156
+ ]);
157
+ function escapeDartIdentifier(name) {
158
+ return DART_RESERVED_WORDS.has(name) ? `${name}_` : name;
159
+ }
87
160
  function camelCase(str) {
88
161
  const pascal = pascalCase(str);
89
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
162
+ const cased = pascal.charAt(0).toLowerCase() + pascal.slice(1);
163
+ return escapeDartIdentifier(cased);
90
164
  }
91
165
  function snakeCase(str) {
92
166
  return str.replace(/([A-Z])/g, "_$1").replace(/[-\s]+/g, "_").replace(/^_/, "").replace(/_+/g, "_").toLowerCase();
@@ -674,7 +748,7 @@ function getTypeManifestVisitor(input) {
674
748
  encoder: fragment`${use(
675
749
  "getArrayEncoder",
676
750
  "solanaCodecsDataStructures"
677
- )}<${itemType}>(${itemEncoder}${encoderSizeExpr})`,
751
+ )}(${itemEncoder}${encoderSizeExpr})`,
678
752
  decoder: fragment`${use(
679
753
  "getArrayDecoder",
680
754
  "solanaCodecsDataStructures"
@@ -730,18 +804,28 @@ function getTypeManifestVisitor(input) {
730
804
  typeFragment.imports.mergeWith(m.type.imports);
731
805
  }
732
806
  const encoderListStr = encoderList.map((e) => e.content).join(", ");
807
+ const decoderListStr = decoderList.map((d) => d.content).join(", ");
808
+ let tupleEncoderFn = "getTupleEncoder";
809
+ let tupleDecoderFn = "getTupleDecoder";
810
+ let tupleEncoderArgs = `[${encoderListStr}]`;
811
+ let tupleDecoderArgs = `[${decoderListStr}]`;
812
+ if (itemManifests.length === 2) {
813
+ tupleEncoderFn = "getTuple2Encoder";
814
+ tupleDecoderFn = "getTuple2Decoder";
815
+ tupleEncoderArgs = encoderListStr;
816
+ tupleDecoderArgs = decoderListStr;
817
+ }
733
818
  const encoderFrag = fragment`${use(
734
- "getTupleEncoder",
819
+ tupleEncoderFn,
735
820
  "solanaCodecsDataStructures"
736
- )}([${fragmentFromString(encoderListStr)}])`;
821
+ )}(${tupleEncoderArgs})`;
737
822
  for (const e of encoderList) {
738
823
  encoderFrag.imports.mergeWith(e.imports);
739
824
  }
740
- const decoderListStr = decoderList.map((d) => d.content).join(", ");
741
825
  const decoderFrag = fragment`${use(
742
- "getTupleDecoder",
826
+ tupleDecoderFn,
743
827
  "solanaCodecsDataStructures"
744
- )}([${fragmentFromString(decoderListStr)}])`;
828
+ )}(${tupleDecoderArgs})`;
745
829
  for (const d of decoderList) {
746
830
  decoderFrag.imports.mergeWith(d.imports);
747
831
  }
@@ -828,7 +912,10 @@ function getTypeManifestVisitor(input) {
828
912
  encoder: fragment`${use(
829
913
  "getNullableEncoder",
830
914
  "solanaCodecsDataStructures"
831
- )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.encoder}${fragmentFromString(prefixExpr)}${fragmentFromString(noneValueExpr)})`,
915
+ )}<${fragmentFromString(itemTypeStr)}>(${use(
916
+ "transformEncoder",
917
+ "solanaCodecsCore"
918
+ )}(${itemManifest.encoder}, (${fragmentFromString(itemTypeStr)} value) => value)${fragmentFromString(prefixExpr)}${fragmentFromString(noneValueExpr)})`,
832
919
  decoder: fragment`${use(
833
920
  "getNullableDecoder",
834
921
  "solanaCodecsDataStructures"
@@ -1589,6 +1676,10 @@ ${dataCtorInitializers}` : ""};`;
1589
1676
  const accountNames = new Set(accounts.map((a) => camelCase(a.name)));
1590
1677
  const hasProgramAddressCollision = argNames.has("programAddress") || accountNames.has("programAddress");
1591
1678
  const instrProgramParam = hasProgramAddressCollision ? "instructionProgramAddress" : "programAddress";
1679
+ let instructionDataLocal = "instructionData";
1680
+ while (argNames.has(instructionDataLocal) || accountNames.has(instructionDataLocal)) {
1681
+ instructionDataLocal = `${instructionDataLocal}_`;
1682
+ }
1592
1683
  const accountParams = accounts.map((acc) => {
1593
1684
  const fieldName = camelCase(acc.name);
1594
1685
  const isRequired = !(acc.isOptional ?? false);
@@ -1697,7 +1788,7 @@ Instruction ${fragmentFromString(instrFnName)}({
1697
1788
  ${fragmentFromString(accountParams)}
1698
1789
  ${fragmentFromString(argParams)}
1699
1790
  }) {
1700
- final instructionData = ${fragmentFromString(dataClassName)}(
1791
+ final ${instructionDataLocal} = ${fragmentFromString(dataClassName)}(
1701
1792
  ${fragmentFromString(dataConstruction)}
1702
1793
  );
1703
1794
 
@@ -1706,7 +1797,7 @@ ${fragmentFromString(dataConstruction)}
1706
1797
  accounts: [
1707
1798
  ${fragmentFromString(accountMetas)}
1708
1799
  ],
1709
- data: ${fragmentFromString(dataEncoderName)}().encode(instructionData),
1800
+ data: ${fragmentFromString(dataEncoderName)}().encode(${instructionDataLocal}),
1710
1801
  );
1711
1802
  }`);
1712
1803
  parts.push(fragment`
@@ -2130,6 +2221,7 @@ function getDataEnumPageFragment(node, variants, scope) {
2130
2221
  const decoderVariants = [];
2131
2222
  const encodeCases = [];
2132
2223
  const decodeCases = [];
2224
+ let pageHasListEnums = false;
2133
2225
  const allVariantManifests = [];
2134
2226
  for (let i = 0; i < variants.length; i++) {
2135
2227
  const variant = variants[i];
@@ -2177,10 +2269,14 @@ function getDataEnumPageFragment(node, variants, scope) {
2177
2269
  const ctorSignature = fields.length === 0 ? ` const ${variantClassName}();` : ` const ${variantClassName}({
2178
2270
  ${ctorParams}
2179
2271
  });`;
2180
- const eqChecks = fields.length === 0 ? "true" : fields.map((f) => `${camelCase(f.name)} == other.${camelCase(f.name)}`).join(" &&\n ");
2181
- const hashExpression = getHashExpression(
2182
- fields.map((f) => camelCase(f.name))
2183
- );
2272
+ const eqChecks = fields.length === 0 ? "true" : fields.map((f, index) => {
2273
+ const fieldName = camelCase(f.name);
2274
+ return isListManifest(fieldManifests[index].manifest) ? `_listEquals(${fieldName}, other.${fieldName})` : `${fieldName} == other.${fieldName}`;
2275
+ }).join(" &&\n ");
2276
+ const hashExpression = getValueHashExpression(fields, fieldManifests);
2277
+ if (fieldManifests.some(({ manifest }) => isListManifest(manifest))) {
2278
+ pageHasListEnums = true;
2279
+ }
2184
2280
  const toStringFields = fields.map((f) => `${camelCase(f.name)}: $${camelCase(f.name)}`).join(", ");
2185
2281
  variantClasses.push(`final class ${variantClassName} extends ${typeName} {
2186
2282
  ${ctorSignature}
@@ -2286,7 +2382,7 @@ sealed class ${fragmentFromString(typeName)} {
2286
2382
  const ${fragmentFromString(typeName)}();
2287
2383
  }
2288
2384
 
2289
- ${fragmentFromString(variantClasses.join("\n\n"))}
2385
+ ${fragmentFromString(variantClasses.join("\n\n"))}${pageHasListEnums ? LIST_VALUE_HELPERS : ""}
2290
2386
 
2291
2387
  Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
2292
2388
  return transformEncoder<Map<String, Object?>, ${fragmentFromString(typeName)}>(
@@ -2329,6 +2425,7 @@ function getStructPageFragment(node, scope) {
2329
2425
  const structNode = node.type;
2330
2426
  if (structNode.kind !== "structTypeNode") return emptyFragment();
2331
2427
  const fields = structNode.fields ?? [];
2428
+ let pageHasListStructs = false;
2332
2429
  const fieldManifests = fields.map((f) => ({
2333
2430
  field: f,
2334
2431
  manifest: visitorsCore.visit(f.type, scope.typeManifestVisitor)
@@ -2340,10 +2437,13 @@ function getStructPageFragment(node, scope) {
2340
2437
  const ctorSignature = fields.length === 0 ? ` const ${typeName}();` : ` const ${typeName}({
2341
2438
  ${ctorParams}
2342
2439
  });`;
2343
- const eqChecks = fields.length === 0 ? "true" : fields.map(
2344
- (f) => `${camelCase(f.name)} == other.${camelCase(f.name)}`
2345
- ).join(" &&\n ");
2346
- fields.length === 0 ? "runtimeType" : fields.map((f) => camelCase(f.name)).join(", ");
2440
+ const eqChecks = fields.length === 0 ? "true" : fieldManifests.map(({ field: f, manifest }, index) => {
2441
+ const fieldName = camelCase(f.name);
2442
+ return isListManifest(manifest) ? `_listEquals(${fieldName}, other.${fieldName})` : `${fieldName} == other.${fieldName}`;
2443
+ }).join(" &&\n ");
2444
+ if (fieldManifests.some(({ manifest }) => isListManifest(manifest))) {
2445
+ pageHasListStructs = true;
2446
+ }
2347
2447
  const toStringFields = fields.map(
2348
2448
  (f) => `${camelCase(f.name)}: $${camelCase(f.name)}`
2349
2449
  ).join(", ");
@@ -2393,12 +2493,14 @@ ${fragmentFromString(fieldDecls)}
2393
2493
  ${fragmentFromString(eqChecks)};
2394
2494
 
2395
2495
  @override
2396
- int get hashCode => ${fragmentFromString(getHashExpression(fields.map((f) => camelCase(f.name))))};
2496
+ int get hashCode => ${fragmentFromString(
2497
+ getValueHashExpression(fields, fieldManifests)
2498
+ )};
2397
2499
 
2398
2500
  @override
2399
2501
  String toString() => '${fragmentFromString(typeName)}(${fragmentFromString(toStringFields)})';
2400
2502
  }
2401
-
2503
+ ${pageHasListStructs ? LIST_VALUE_HELPERS : ""}
2402
2504
  Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
2403
2505
  final structEncoder = getStructEncoder(<(String, Encoder<Object?>)>[
2404
2506
  ${fragmentFromString(encFields)}
@@ -2464,6 +2566,56 @@ Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragme
2464
2566
  return combineCodec(${fragmentFromString(encoderName)}(), ${fragmentFromString(decoderName)}());
2465
2567
  }`;
2466
2568
  }
2569
+ function isListManifest(manifest) {
2570
+ return manifest.type.content.startsWith("List<") || manifest.type.content === "Uint8List";
2571
+ }
2572
+ var LIST_VALUE_HELPERS = `
2573
+ bool _listEquals<T>(List<T>? a, List<T>? b) {
2574
+ if (identical(a, b)) return true;
2575
+ if (a == null || b == null) return a == b;
2576
+ if (a.length != b.length) return false;
2577
+ for (var i = 0; i < a.length; i++) {
2578
+ final left = a[i];
2579
+ final right = b[i];
2580
+ if (left is List<Object?> && right is List<Object?>) {
2581
+ if (!_listEquals(left, right)) return false;
2582
+ } else if (left != right) {
2583
+ return false;
2584
+ }
2585
+ }
2586
+ return true;
2587
+ }
2588
+
2589
+ Object? _deepHash(Object? value) {
2590
+ if (value is List<Object?>) {
2591
+ return Object.hashAll(value.map(_deepHash));
2592
+ }
2593
+ return value;
2594
+ }
2595
+
2596
+ int _listHashCode<T>(List<T>? a) {
2597
+ if (a == null) return 0;
2598
+ return Object.hashAll(a.map(_deepHash));
2599
+ }
2600
+ `;
2601
+ function getValueHashExpression(fields, fieldManifests) {
2602
+ const fieldNames = fields.map(
2603
+ (f) => camelCase(f.name)
2604
+ );
2605
+ const hasListFields = fieldManifests.some(
2606
+ ({ manifest }) => isListManifest(manifest)
2607
+ );
2608
+ if (!hasListFields) {
2609
+ return getHashExpression(fieldNames);
2610
+ }
2611
+ if (fieldNames.length === 1) {
2612
+ return `_listHashCode(${fieldNames[0]})`;
2613
+ }
2614
+ const hashArgs = fields.map(
2615
+ (f, index) => isListManifest(fieldManifests[index].manifest) ? `_listHashCode(${camelCase(f.name)})` : camelCase(f.name)
2616
+ );
2617
+ return `Object.hash(${hashArgs.join(", ")})`;
2618
+ }
2467
2619
  function getHashExpression(fields) {
2468
2620
  if (fields.length === 0) return "runtimeType.hashCode";
2469
2621
  if (fields.length === 1) return `${fields[0]}.hashCode`;