codama-renderers-dart 0.5.3 → 0.5.4

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.
Files changed (34) hide show
  1. package/README.md +6 -0
  2. package/dist/index.browser.cjs +176 -107
  3. package/dist/index.browser.cjs.map +1 -1
  4. package/dist/index.browser.js +177 -108
  5. package/dist/index.browser.js.map +1 -1
  6. package/dist/index.node.cjs +176 -107
  7. package/dist/index.node.cjs.map +1 -1
  8. package/dist/index.node.js +177 -108
  9. package/dist/index.node.js.map +1 -1
  10. package/dist/index.react-native.js +177 -108
  11. package/dist/index.react-native.js.map +1 -1
  12. package/dist/types/fragments/accountPage.d.ts.map +1 -1
  13. package/dist/types/fragments/errorPage.d.ts.map +1 -1
  14. package/dist/types/fragments/instructionPage.d.ts.map +1 -1
  15. package/dist/types/fragments/pdaPage.d.ts.map +1 -1
  16. package/dist/types/fragments/programPage.d.ts.map +1 -1
  17. package/dist/types/utils/nameTransformers.d.ts.map +1 -1
  18. package/dist/types/utils/valueNodes.d.ts +4 -0
  19. package/dist/types/utils/valueNodes.d.ts.map +1 -1
  20. package/dist/types/visitors/getRenderMapVisitor.d.ts.map +1 -1
  21. package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
  22. package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
  23. package/package.json +7 -7
  24. package/src/fragments/accountPage.ts +2 -1
  25. package/src/fragments/errorPage.ts +8 -5
  26. package/src/fragments/instructionPage.ts +51 -21
  27. package/src/fragments/pdaPage.ts +7 -14
  28. package/src/fragments/programPage.ts +3 -2
  29. package/src/utils/formatCode.ts +2 -2
  30. package/src/utils/nameTransformers.ts +11 -0
  31. package/src/utils/valueNodes.ts +24 -1
  32. package/src/visitors/getRenderMapVisitor.ts +30 -5
  33. package/src/visitors/getTypeManifestVisitor.ts +100 -75
  34. package/src/visitors/renderVisitor.ts +11 -13
@@ -5,6 +5,7 @@ import {
5
5
  type BooleanTypeNode,
6
6
  type BytesTypeNode,
7
7
  type CamelCaseString,
8
+ type ConstantValueNode,
8
9
  type DateTimeTypeNode,
9
10
  type DefinedTypeLinkNode,
10
11
  type DefinedTypeNode,
@@ -31,6 +32,7 @@ import {
31
32
  type StructFieldTypeNode,
32
33
  type StructTypeNode,
33
34
  type TupleTypeNode,
35
+ type TypeNode,
34
36
  type ZeroableOptionTypeNode,
35
37
  isNode,
36
38
  resolveNestedTypeNode,
@@ -55,6 +57,7 @@ import {
55
57
  use,
56
58
  } from "../utils/index.js";
57
59
  import { type DartNameApi, snakeCase } from "../utils/nameTransformers.js";
60
+ import { getDartValueFragment, getNonNegativeInteger, toDartStringLiteral } from "../utils/valueNodes.js";
58
61
 
59
62
  /**
60
63
  * Type alias for the type manifest visitor.
@@ -121,32 +124,12 @@ export function getTypeManifestVisitor(input: {
121
124
  );
122
125
 
123
126
  return extendVisitor(baseVisitor, {
124
- visitNumberType(node: NumberTypeNode, { self }) {
125
- const { format, endian } = node;
126
- const endianSuffix =
127
- endian === "be"
128
- ? ", NumberCodecConfig(endian: Endian.big)"
129
- : "";
130
- const endianDecoderSuffix =
131
- endian === "be"
132
- ? ", NumberCodecConfig(endian: Endian.big)"
133
- : "";
134
-
135
- // Map format to Dart type and codec
136
- const dartType = getNumberDartType(format);
137
- const codecName = getNumberCodecName(format);
138
-
127
+ visitNumberType(node: NumberTypeNode) {
139
128
  return {
140
129
  // `BigInt` and other number types live in `dart:core` and need no import.
141
- type: fragmentFromString(dartType),
142
- encoder: fragment`${use(
143
- `get${codecName}Encoder`,
144
- "solanaCodecsNumbers",
145
- )}(${endianSuffix ? fragmentFromString(endianSuffix.slice(2)) : fragmentFromString("")})`,
146
- decoder: fragment`${use(
147
- `get${codecName}Decoder`,
148
- "solanaCodecsNumbers",
149
- )}(${endianDecoderSuffix ? fragmentFromString(endianDecoderSuffix.slice(2)) : fragmentFromString("")})`,
130
+ type: fragmentFromString(getNumberDartType(node.format)),
131
+ encoder: getNumberCodecExpression(node, "Encoder"),
132
+ decoder: getNumberCodecExpression(node, "Decoder"),
150
133
  value: emptyTypeManifest().value,
151
134
  isEnum: false,
152
135
  };
@@ -355,7 +338,7 @@ export function getTypeManifestVisitor(input: {
355
338
 
356
339
  // Build encoder fields list
357
340
  const encoderFields = fields
358
- .map((f, i) => `('${fieldNames[i]}', ${f.encoder.content})`)
341
+ .map((f, i) => `(${toDartStringLiteral(fieldNames[i])}, ${f.encoder.content})`)
359
342
  .join(", ");
360
343
  const encoderFrag = fragment`${use(
361
344
  "getStructEncoder",
@@ -366,7 +349,7 @@ export function getTypeManifestVisitor(input: {
366
349
  }
367
350
 
368
351
  const decoderFields = fields
369
- .map((f, i) => `('${fieldNames[i]}', ${f.decoder.content})`)
352
+ .map((f, i) => `(${toDartStringLiteral(fieldNames[i])}, ${f.decoder.content})`)
370
353
  .join(", ");
371
354
  const decoderFrag = fragment`${use(
372
355
  "getStructDecoder",
@@ -442,10 +425,10 @@ export function getTypeManifestVisitor(input: {
442
425
  const itemTypeStr = itemManifest.type.content;
443
426
  const resolvedPrefix = resolveNestedTypeNode(node.prefix);
444
427
  const prefixExpr = resolvedPrefix.format !== "u8"
445
- ? `, prefix: ${getNumberCodecExpression(resolvedPrefix.format, "Encoder")}`
428
+ ? fragment`, prefix: ${getNumberCodecExpression(resolvedPrefix, "Encoder")}`
446
429
  : "";
447
430
  const prefixDecoderExpr = resolvedPrefix.format !== "u8"
448
- ? `, prefix: ${getNumberCodecExpression(resolvedPrefix.format, "Decoder")}`
431
+ ? fragment`, prefix: ${getNumberCodecExpression(resolvedPrefix, "Decoder")}`
449
432
  : "";
450
433
  const noneValueExpr = node.fixed
451
434
  ? ", noneValue: const ZeroesNoneValue()"
@@ -459,11 +442,11 @@ export function getTypeManifestVisitor(input: {
459
442
  )}<${fragmentFromString(itemTypeStr)}>(${use(
460
443
  "transformEncoder",
461
444
  "solanaCodecsCore",
462
- )}(${itemManifest.encoder}, (${fragmentFromString(itemTypeStr)} value) => value)${fragmentFromString(prefixExpr)}${fragmentFromString(noneValueExpr)})`,
445
+ )}(${itemManifest.encoder}, (${fragmentFromString(itemTypeStr)} value) => value)${prefixExpr}${fragmentFromString(noneValueExpr)})`,
463
446
  decoder: fragment`${use(
464
447
  "getNullableDecoder",
465
448
  "solanaCodecsDataStructures",
466
- )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}${fragmentFromString(prefixDecoderExpr)}${fragmentFromString(noneValueExpr)})`,
449
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}${prefixDecoderExpr}${fragmentFromString(noneValueExpr)})`,
467
450
  value: emptyTypeManifest().value,
468
451
  isEnum: false,
469
452
  };
@@ -521,11 +504,11 @@ export function getTypeManifestVisitor(input: {
521
504
  encoder: fragment`${use(
522
505
  "fixEncoderSize",
523
506
  "solanaCodecsCore",
524
- )}(${innerManifest.encoder}, ${fragmentFromString(String(node.size))}, allowTruncation: false)`,
507
+ )}(${innerManifest.encoder}, ${fragmentFromString(getNonNegativeInteger(node.size))}, allowTruncation: false)`,
525
508
  decoder: fragment`${use(
526
509
  "fixDecoderSize",
527
510
  "solanaCodecsCore",
528
- )}(${innerManifest.decoder}, ${fragmentFromString(String(node.size))})`,
511
+ )}(${innerManifest.decoder}, ${fragmentFromString(getNonNegativeInteger(node.size))})`,
529
512
  value: innerManifest.value,
530
513
  isEnum: innerManifest.isEnum,
531
514
  };
@@ -544,12 +527,12 @@ export function getTypeManifestVisitor(input: {
544
527
  let prefixManifest;
545
528
  if (
546
529
  node.prefix.kind === "numberTypeNode" &&
547
- bigIntFormats.has((node.prefix as any).format)
530
+ bigIntFormats.has(node.prefix.format)
548
531
  ) {
549
- const codecName = getNumberCodecName((node.prefix as any).format);
532
+ const numberManifest = visit(node.prefix, self);
550
533
  prefixManifest = {
551
- encoder: fragment`transformEncoder(${use(`get${codecName}Encoder`, "solanaCodecsNumbers")}(), (size) => BigInt.from(size))`,
552
- decoder: fragment`transformDecoder(${use(`get${codecName}Decoder`, "solanaCodecsNumbers")}(), (size, _, __) => size.toInt())`,
534
+ encoder: fragment`${use("transformEncoder", "solanaCodecsCore")}(${numberManifest.encoder}, (size) => BigInt.from(size))`,
535
+ decoder: fragment`${use("transformDecoder", "solanaCodecsCore")}(${numberManifest.decoder}, (size, _, __) => size.toInt())`,
553
536
  };
554
537
  } else {
555
538
  prefixManifest = visit(node.prefix, self);
@@ -660,13 +643,11 @@ export function getTypeManifestVisitor(input: {
660
643
  },
661
644
 
662
645
  visitPreOffsetType(node: PreOffsetTypeNode, { self }) {
663
- // Pre-offset types pass through to inner type in Dart
664
- return visit(node.type, self);
646
+ return getOffsetManifest(node, self);
665
647
  },
666
648
 
667
649
  visitPostOffsetType(node: PostOffsetTypeNode, { self }) {
668
- // Post-offset types pass through to inner type in Dart
669
- return visit(node.type, self);
650
+ return getOffsetManifest(node, self);
670
651
  },
671
652
 
672
653
  visitSolAmountType(node: SolAmountTypeNode, { self }) {
@@ -777,22 +758,26 @@ function getNumberCodecName(format: string): string {
777
758
  }
778
759
 
779
760
  function getNumberCodecExpression(
780
- format: string,
761
+ node: NumberTypeNode,
781
762
  codecType: "Encoder" | "Decoder",
782
- ): string {
783
- const name = getNumberCodecName(format);
784
- return `get${name}${codecType}()`;
763
+ ): Fragment {
764
+ const name = getNumberCodecName(node.format);
765
+ const config = node.endian === "be"
766
+ ? fragment`${use("NumberCodecConfig", "solanaCodecsNumbers")}(endian: ${use("Endian", "dartTypedData")}.big)`
767
+ : "";
768
+
769
+ return fragment`${use(`get${name}${codecType}`, "solanaCodecsNumbers")}(${config})`;
785
770
  }
786
771
 
787
772
  function getArraySizeExpression(
788
773
  node: ArrayTypeNode,
789
774
  codecType: "Encoder" | "Decoder",
790
- ): string {
775
+ ): string | Fragment {
791
776
  if (!("count" in node) || !node.count) return "";
792
777
  const count = node.count;
793
778
  switch (count.kind) {
794
779
  case "fixedCountNode":
795
- return `, size: FixedArraySize(${count.value})`;
780
+ return `, size: FixedArraySize(${getNonNegativeInteger(count.value)})`;
796
781
  case "remainderCountNode":
797
782
  return ", size: RemainderArraySize()";
798
783
  case "prefixedCountNode": {
@@ -800,7 +785,7 @@ function getArraySizeExpression(
800
785
  if (resolvedPrefix.format === "u32" && (!resolvedPrefix.endian || resolvedPrefix.endian === "le")) {
801
786
  return ""; // Default
802
787
  }
803
- return `, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix.format, codecType)})`;
788
+ return fragment`, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix, codecType)})`;
804
789
  }
805
790
  default:
806
791
  return "";
@@ -810,12 +795,12 @@ function getArraySizeExpression(
810
795
  function getMapSizeExpression(
811
796
  node: MapTypeNode,
812
797
  codecType: "Encoder" | "Decoder",
813
- ): string {
798
+ ): string | Fragment {
814
799
  if (!("count" in node) || !node.count) return "";
815
800
  const count = node.count;
816
801
  switch (count.kind) {
817
802
  case "fixedCountNode":
818
- return `, size: FixedArraySize(${count.value})`;
803
+ return `, size: FixedArraySize(${getNonNegativeInteger(count.value)})`;
819
804
  case "remainderCountNode":
820
805
  return ", size: RemainderArraySize()";
821
806
  case "prefixedCountNode": {
@@ -823,7 +808,7 @@ function getMapSizeExpression(
823
808
  if (resolvedPrefix.format === "u32" && (!resolvedPrefix.endian || resolvedPrefix.endian === "le")) {
824
809
  return "";
825
810
  }
826
- return `, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix.format, codecType)})`;
811
+ return fragment`, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix, codecType)})`;
827
812
  }
828
813
  default:
829
814
  return "";
@@ -833,12 +818,12 @@ function getMapSizeExpression(
833
818
  function getSetSizeExpression(
834
819
  node: SetTypeNode,
835
820
  codecType: "Encoder" | "Decoder",
836
- ): string {
821
+ ): string | Fragment {
837
822
  if (!("count" in node) || !node.count) return "";
838
823
  const count = node.count;
839
824
  switch (count.kind) {
840
825
  case "fixedCountNode":
841
- return `, size: FixedArraySize(${count.value})`;
826
+ return `, size: FixedArraySize(${getNonNegativeInteger(count.value)})`;
842
827
  case "remainderCountNode":
843
828
  return ", size: RemainderArraySize()";
844
829
  case "prefixedCountNode": {
@@ -846,7 +831,7 @@ function getSetSizeExpression(
846
831
  if (resolvedPrefix.format === "u32" && (!resolvedPrefix.endian || resolvedPrefix.endian === "le")) {
847
832
  return "";
848
833
  }
849
- return `, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix.format, codecType)})`;
834
+ return fragment`, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix, codecType)})`;
850
835
  }
851
836
  default:
852
837
  return "";
@@ -859,7 +844,7 @@ type HiddenAffixManifest = {
859
844
  };
860
845
 
861
846
  function getHiddenAffixManifest(
862
- node: any,
847
+ node: ConstantValueNode | TypeNode,
863
848
  self: Visitor<TypeManifest>,
864
849
  ): HiddenAffixManifest {
865
850
  if (node?.kind !== "constantValueNode") {
@@ -867,40 +852,80 @@ function getHiddenAffixManifest(
867
852
  return { encoder: manifest.encoder, decoder: manifest.decoder };
868
853
  }
869
854
 
870
- const constantBytes = getConstantBytesExpression(node);
855
+ const constantBytes = getConstantBytesExpression(node, self);
871
856
  return {
872
857
  encoder: fragment`${use("getConstantEncoder", "solanaCodecsDataStructures")}(${constantBytes})`,
873
858
  decoder: fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(${constantBytes})`,
874
859
  };
875
860
  }
876
861
 
877
- function getConstantBytesExpression(node: any): Fragment {
878
- const valueNode = node.value;
879
- const typeNode = node.type;
880
-
881
- if (valueNode?.kind !== "numberValueNode") {
862
+ /** Serialize affix constants using the same validated codec as ordinary fields. */
863
+ function getConstantBytesExpression(
864
+ node: ConstantValueNode,
865
+ self: Visitor<TypeManifest>,
866
+ ): Fragment {
867
+ if (node.value.kind !== "numberValueNode") {
882
868
  throw new Error(
883
- `Unsupported hidden affix constant value kind: ${valueNode?.kind}`,
869
+ `Unsupported hidden affix constant value kind: ${node.value.kind}`,
884
870
  );
885
871
  }
886
872
 
887
- const value = Number(valueNode.number);
873
+ const manifest = visit(node.type, self);
888
874
 
889
- if (typeNode?.kind === "numberTypeNode") {
890
- return fragment`${use(`get${getNumberCodecName(typeNode.format)}Encoder`, "solanaCodecsNumbers")}().encode(${value})`;
875
+ if (!["int", "BigInt", "double"].includes(manifest.type.content)) {
876
+ throw new Error(`Unsupported hidden affix constant type kind: ${node.type.kind}`);
891
877
  }
892
878
 
893
- if (typeNode?.kind === "preOffsetTypeNode") {
894
- const inner = typeNode.type;
895
- if (inner?.kind !== "numberTypeNode") {
896
- throw new Error(
897
- `Unsupported hidden affix pre-offset inner type kind: ${inner?.kind}`,
898
- );
899
- }
900
- return fragment`${use("padLeftEncoder", "solanaCodecsCore")}(${use(`get${getNumberCodecName(inner.format)}Encoder`, "solanaCodecsNumbers")}(), ${typeNode.offset}).encode(${value})`;
879
+ const value = getDartValueFragment(node.value, manifest.type.content);
880
+
881
+ return fragment`${manifest.encoder}.encode(${value})`;
882
+ }
883
+
884
+ /** Preserve the IDL's cursor strategy instead of silently discarding offsets. */
885
+ function getOffsetManifest(
886
+ node: PreOffsetTypeNode | PostOffsetTypeNode,
887
+ self: Visitor<TypeManifest>,
888
+ ): TypeManifest {
889
+ if (!Number.isSafeInteger(node.offset)) {
890
+ throw new Error("Expected a safe integer for a codec offset");
901
891
  }
902
892
 
903
- throw new Error(
904
- `Unsupported hidden affix constant type kind: ${typeNode?.kind}`,
905
- );
893
+ const manifest = visit(node.type, self);
894
+ const pre = node.kind === "preOffsetTypeNode";
895
+ const key = pre ? "preOffset" : "postOffset";
896
+
897
+ if (node.strategy === "padded") {
898
+ const side = pre ? "Left" : "Right";
899
+ const offset = getNonNegativeInteger(node.offset);
900
+
901
+ return {
902
+ ...manifest,
903
+ encoder: fragment`${use(`pad${side}Encoder`, "solanaCodecsCore")}(${manifest.encoder}, ${offset})`,
904
+ decoder: fragment`${use(`pad${side}Decoder`, "solanaCodecsCore")}(${manifest.decoder}, ${offset})`,
905
+ };
906
+ }
907
+
908
+ let expression: string;
909
+
910
+ switch (node.strategy) {
911
+ case "absolute":
912
+ expression = node.offset < 0 ? `scope.wrapBytes(${node.offset})` : String(node.offset);
913
+ break;
914
+ case "relative":
915
+ expression = `scope.${key} + ${node.offset}`;
916
+ break;
917
+ case "preOffset":
918
+ expression = `scope.preOffset + ${node.offset}`;
919
+ break;
920
+ default:
921
+ throw new Error(`Unsupported offset strategy: ${node.strategy}`);
922
+ }
923
+
924
+ const config = fragment`${use("OffsetConfig", "solanaCodecsCore")}(${key}: (scope) => ${expression})`;
925
+
926
+ return {
927
+ ...manifest,
928
+ encoder: fragment`${use("offsetEncoder", "solanaCodecsCore")}(${manifest.encoder}, ${config})`,
929
+ decoder: fragment`${use("offsetDecoder", "solanaCodecsCore")}(${manifest.decoder}, ${config})`,
930
+ };
906
931
  }
@@ -32,11 +32,6 @@ export function renderVisitor(
32
32
  return rootNodeVisitor((root: RootNode) => {
33
33
  const normalizedRoot = normalizeRootNode(root);
34
34
 
35
- // 1. Optionally delete the output directory
36
- if (deleteFolderBeforeRendering && existsSync(outputDir)) {
37
- deleteDirectory(outputDir);
38
- }
39
-
40
35
  // 2. Build the render map
41
36
  const renderMap = visit(
42
37
  normalizedRoot,
@@ -52,16 +47,10 @@ export function renderVisitor(
52
47
  }
53
48
  }
54
49
 
55
- // 4. Resolve imports and write files
50
+ // Resolve every file before deleting previous output, so invalid IDLs fail safely.
51
+ const files: [string, string][] = [];
56
52
  for (const [filePath, frag] of renderMap.entries()) {
57
53
  const fullPath = join(outputDir, filePath);
58
- const dir = dirname(fullPath);
59
-
60
- // Ensure directory exists
61
- if (!existsSync(dir)) {
62
- mkdirSync(dir, { recursive: true });
63
- }
64
-
65
54
  // Compute per-file internal import map (relative paths to defined types)
66
55
  const fileDir = dirname(filePath);
67
56
  const internalMap: Record<string, string> = {};
@@ -81,6 +70,15 @@ export function renderVisitor(
81
70
 
82
71
  // Resolve fragment content with imports
83
72
  const content = resolveFragmentContent(frag, dependencyMap ?? {}, internalMap);
73
+ files.push([fullPath, content]);
74
+ }
75
+
76
+ if (deleteFolderBeforeRendering && existsSync(outputDir)) {
77
+ deleteDirectory(outputDir);
78
+ }
79
+
80
+ for (const [fullPath, content] of files) {
81
+ mkdirSync(dirname(fullPath), { recursive: true });
84
82
  writeFileSync(fullPath, content, "utf-8");
85
83
  }
86
84