codama-renderers-dart 0.5.0 → 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.
Files changed (48) hide show
  1. package/{readme.md → README.md} +38 -42
  2. package/dist/index.browser.cjs +628 -147
  3. package/dist/index.browser.cjs.map +1 -1
  4. package/dist/index.browser.js +628 -148
  5. package/dist/index.browser.js.map +1 -1
  6. package/dist/index.node.cjs +628 -147
  7. package/dist/index.node.cjs.map +1 -1
  8. package/dist/index.node.js +628 -148
  9. package/dist/index.node.js.map +1 -1
  10. package/dist/index.react-native.js +628 -148
  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/instructionPage.d.ts.map +1 -1
  14. package/dist/types/fragments/typePage.d.ts.map +1 -1
  15. package/dist/types/index.d.ts +1 -0
  16. package/dist/types/index.d.ts.map +1 -1
  17. package/dist/types/utils/discriminators.d.ts +10 -0
  18. package/dist/types/utils/discriminators.d.ts.map +1 -0
  19. package/dist/types/utils/exactDecoder.d.ts +17 -0
  20. package/dist/types/utils/exactDecoder.d.ts.map +1 -0
  21. package/dist/types/utils/formatCode.d.ts +1 -1
  22. package/dist/types/utils/formatCode.d.ts.map +1 -1
  23. package/dist/types/utils/index.d.ts +4 -0
  24. package/dist/types/utils/index.d.ts.map +1 -1
  25. package/dist/types/utils/nameTransformers.d.ts +5 -0
  26. package/dist/types/utils/nameTransformers.d.ts.map +1 -1
  27. package/dist/types/utils/normalizeRootNode.d.ts +11 -0
  28. package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
  29. package/dist/types/utils/valueNodes.d.ts +16 -0
  30. package/dist/types/utils/valueNodes.d.ts.map +1 -0
  31. package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
  32. package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
  33. package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
  34. package/package.json +21 -18
  35. package/src/fragments/accountPage.ts +90 -17
  36. package/src/fragments/instructionPage.ts +112 -79
  37. package/src/fragments/typePage.ts +134 -31
  38. package/src/index.ts +1 -0
  39. package/src/utils/discriminators.ts +121 -0
  40. package/src/utils/exactDecoder.ts +82 -0
  41. package/src/utils/formatCode.ts +7 -11
  42. package/src/utils/index.ts +4 -0
  43. package/src/utils/nameTransformers.ts +28 -1
  44. package/src/utils/normalizeRootNode.ts +76 -0
  45. package/src/utils/valueNodes.ts +87 -0
  46. package/src/visitors/getRenderMapVisitor.ts +1 -1
  47. package/src/visitors/getTypeManifestVisitor.ts +37 -14
  48. package/src/visitors/renderVisitor.ts +4 -1
@@ -0,0 +1,121 @@
1
+ import type {
2
+ AccountNode,
3
+ ConstantValueNode,
4
+ InstructionNode,
5
+ StructFieldTypeNode,
6
+ } from "@codama/nodes";
7
+ import { resolveNestedTypeNode } from "@codama/nodes";
8
+ import { visit } from "@codama/visitors-core";
9
+
10
+ import type { Fragment } from "./fragment.js";
11
+ import {
12
+ emptyFragment,
13
+ fragment,
14
+ fragmentFromString,
15
+ mergeFragments,
16
+ use,
17
+ } from "./fragment.js";
18
+ import type { RenderScope } from "./options.js";
19
+ import { getDartValueFragment } from "./valueNodes.js";
20
+
21
+ type DiscriminatedNode = AccountNode | InstructionNode;
22
+
23
+ /**
24
+ * Generate decoder guards for every discriminator declared by a Codama node.
25
+ */
26
+ export function getDiscriminatorValidationFragment(
27
+ node: DiscriminatedNode,
28
+ scope: RenderScope,
29
+ ): Fragment {
30
+ const discriminators = node.discriminators ?? [];
31
+ if (discriminators.length === 0) return emptyFragment();
32
+
33
+ const fields = getDiscriminatorFields(node);
34
+ const fragments = discriminators.map((discriminator): Fragment => {
35
+ switch (discriminator.kind) {
36
+ case "constantDiscriminatorNode":
37
+ return getConstantValidationFragment(
38
+ discriminator.constant,
39
+ discriminator.offset,
40
+ scope,
41
+ );
42
+ case "fieldDiscriminatorNode": {
43
+ const field = fields.find(
44
+ (candidate) => candidate.name === discriminator.name,
45
+ );
46
+ if (field?.defaultValue == null) {
47
+ throw new Error(
48
+ `Field discriminator "${discriminator.name}" on ${node.kind} "${node.name}" must reference a field with a default value.`,
49
+ );
50
+ }
51
+ return getConstantValidationFragment(
52
+ {
53
+ kind: "constantValueNode",
54
+ type: field.type,
55
+ value: field.defaultValue,
56
+ } as ConstantValueNode,
57
+ discriminator.offset,
58
+ scope,
59
+ );
60
+ }
61
+ case "sizeDiscriminatorNode": {
62
+ if (
63
+ !Number.isSafeInteger(discriminator.size) ||
64
+ discriminator.size < 0
65
+ ) {
66
+ throw new Error(
67
+ `Discriminator size must be a non-negative safe integer, got ${discriminator.size}.`,
68
+ );
69
+ }
70
+ return fragment`if (bytes.length - offset != ${fragmentFromString(String(discriminator.size))}) {
71
+ throw ${use("SolanaError", "solanaErrors")}(
72
+ ${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
73
+ {
74
+ 'codecDescription': '${fragmentFromString(node.name as string)} discriminator',
75
+ 'expected': ${fragmentFromString(String(discriminator.size))},
76
+ 'bytesLength': bytes.length - offset,
77
+ },
78
+ );
79
+ }`;
80
+ }
81
+ }
82
+ });
83
+
84
+ return mergeFragments(fragments, (contents) => contents.join("\n"));
85
+ }
86
+
87
+ function getConstantValidationFragment(
88
+ constant: ConstantValueNode,
89
+ discriminatorOffset: number,
90
+ scope: RenderScope,
91
+ ): Fragment {
92
+ if (!Number.isSafeInteger(discriminatorOffset) || discriminatorOffset < 0) {
93
+ throw new Error(
94
+ `Discriminator offset must be a non-negative safe integer, got ${discriminatorOffset}.`,
95
+ );
96
+ }
97
+
98
+ const manifest = visit(constant.type, scope.typeManifestVisitor);
99
+ const value = getDartValueFragment(
100
+ constant.value,
101
+ manifest.type.content,
102
+ );
103
+ return fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(
104
+ ${manifest.encoder}.encode(${value}),
105
+ ).read(bytes, offset + ${fragmentFromString(String(discriminatorOffset))});`;
106
+ }
107
+
108
+ function getDiscriminatorFields(
109
+ node: DiscriminatedNode,
110
+ ): readonly StructFieldTypeNode[] {
111
+ if (node.kind === "accountNode") {
112
+ return resolveNestedTypeNode(node.data).fields ?? [];
113
+ }
114
+ return (node.arguments ?? []).map(
115
+ (argument) =>
116
+ ({
117
+ ...argument,
118
+ kind: "structFieldTypeNode",
119
+ }) as StructFieldTypeNode,
120
+ );
121
+ }
@@ -0,0 +1,82 @@
1
+ import type { Fragment } from "./fragment.js";
2
+ import { fragment, fragmentFromString, use } from "./fragment.js";
3
+
4
+ export interface TopLevelDecoderFragmentOptions {
5
+ readonly typeName: string;
6
+ readonly description: string;
7
+ readonly discriminatorValidation: Fragment;
8
+ readonly fromMapFields: string;
9
+ readonly requireExactConsumption: boolean;
10
+ }
11
+
12
+ /**
13
+ * Wrap a generated top-level struct decoder with discriminator and length
14
+ * validation.
15
+ *
16
+ * The wrapper preserves fixed/variable decoder metadata so it remains
17
+ * compatible with the matching generated encoder when combined into a codec.
18
+ */
19
+ export function getTopLevelDecoderFragment({
20
+ typeName,
21
+ description,
22
+ discriminatorValidation,
23
+ fromMapFields,
24
+ requireExactConsumption,
25
+ }: TopLevelDecoderFragmentOptions): Fragment {
26
+ const validation = discriminatorValidation.content
27
+ ? ` ${discriminatorValidation.content.replaceAll("\n", "\n ")}\n`
28
+ : "";
29
+
30
+ const consumptionCheck = requireExactConsumption
31
+ ? ` if (newOffset != bytes.length) {
32
+ throwInvalidByteLength(newOffset - offset, bytes.length - offset);
33
+ }
34
+ `
35
+ : "";
36
+ const fixedSizeCheck = requireExactConsumption
37
+ ? "bytesLength != structDecoder.fixedSize"
38
+ : "bytesLength < structDecoder.fixedSize";
39
+
40
+ const result = fragment` Never throwInvalidByteLength(int expected, int bytesLength) {
41
+ throw ${use("SolanaError", "solanaErrors")}(
42
+ ${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
43
+ {
44
+ 'codecDescription': '${fragmentFromString(description)}',
45
+ 'expected': expected,
46
+ 'bytesLength': bytesLength,
47
+ },
48
+ );
49
+ }
50
+
51
+ (${fragmentFromString(typeName)}, int) readTopLevel(Uint8List bytes, int offset) {
52
+ ${fragmentFromString(validation)} final (map, newOffset) = structDecoder.read(bytes, offset);
53
+ ${fragmentFromString(consumptionCheck)}
54
+ return (
55
+ ${fragmentFromString(typeName)}(
56
+ ${fragmentFromString(fromMapFields)}
57
+ ),
58
+ newOffset,
59
+ );
60
+ }
61
+
62
+ return switch (structDecoder) {
63
+ ${use("FixedSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
64
+ ${use("FixedSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
65
+ fixedSize: structDecoder.fixedSize,
66
+ read: (bytes, offset) {
67
+ final bytesLength = bytes.length - offset;
68
+ if (${fragmentFromString(fixedSizeCheck)}) {
69
+ throwInvalidByteLength(structDecoder.fixedSize, bytesLength);
70
+ }
71
+ return readTopLevel(bytes, offset);
72
+ },
73
+ ),
74
+ ${use("VariableSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
75
+ ${use("VariableSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
76
+ read: readTopLevel,
77
+ maxSize: structDecoder.maxSize,
78
+ ),
79
+ };`;
80
+ result.imports.mergeWith(discriminatorValidation.imports);
81
+ return result;
82
+ }
@@ -2,11 +2,11 @@ import { execSync } from "node:child_process";
2
2
 
3
3
  /**
4
4
  * Format Dart code using `dart format`.
5
- * Falls back to returning the unformatted code if dart is not available.
5
+ * Falls back to returning the unformatted code if Dart is not available.
6
6
  */
7
7
  export function formatDartCode(code: string): string {
8
8
  try {
9
- const result = execSync("dart format --fix -", {
9
+ const result = execSync("dart format --output=show", {
10
10
  input: code,
11
11
  encoding: "utf-8",
12
12
  timeout: 30_000,
@@ -23,13 +23,9 @@ export function formatDartCode(code: string): string {
23
23
  * Format all Dart files in a directory using `dart format`.
24
24
  */
25
25
  export function formatDartDirectory(dir: string): void {
26
- try {
27
- execSync(`dart format --fix "${dir}"`, {
28
- encoding: "utf-8",
29
- timeout: 60_000,
30
- stdio: ["pipe", "pipe", "pipe"],
31
- });
32
- } catch {
33
- // Silently ignore if dart format is not available
34
- }
26
+ execSync(`dart format "${dir}"`, {
27
+ encoding: "utf-8",
28
+ timeout: 60_000,
29
+ stdio: ["pipe", "pipe", "pipe"],
30
+ });
35
31
  }
@@ -1,8 +1,12 @@
1
1
  export * from "./codecs.js";
2
+ export * from "./discriminators.js";
3
+ export * from "./exactDecoder.js";
2
4
  export * from "./formatCode.js";
3
5
  export * from "./fragment.js";
4
6
  export * from "./importMap.js";
5
7
  export * from "./nameTransformers.js";
8
+ export * from "./normalizeRootNode.js";
6
9
  export * from "./options.js";
7
10
  export * from "./typeManifest.js";
11
+ export * from "./valueNodes.js";
8
12
  export * from "./wellKnownAddresses.js";
@@ -11,10 +11,37 @@ export function pascalCase(str: string): string {
11
11
  .replace(/^(.)/, (c) => c.toUpperCase());
12
12
  }
13
13
 
14
+ /**
15
+ * Dart reserved words (plus `async`/`await`/`yield` where they cannot be
16
+ * plain identifiers). A lowercase declaration named with one of these must
17
+ * be escaped, e.g. an account named `null` becomes a `null_` parameter.
18
+ */
19
+ const DART_RESERVED_WORDS = new Set([
20
+ "abstract", "as", "assert", "async", "augment", "await", "base",
21
+ "break", "case", "catch", "class", "const", "continue", "covariant",
22
+ "default", "deferred", "do", "dynamic", "else", "enum", "export",
23
+ "extends", "extension", "external", "factory", "false", "final",
24
+ "finally", "for", "get", "hide", "if", "implements", "import", "in",
25
+ "interface", "is", "late", "library", "mixin", "native", "new", "null",
26
+ "of", "on", "operator", "part", "required", "rethrow", "return",
27
+ "sealed", "set", "show", "static", "super", "switch", "sync", "this",
28
+ "throw", "true", "try", "typedef", "var", "void", "when", "while",
29
+ "with", "yield",
30
+ ]);
31
+
32
+ /**
33
+ * Escape a lowercase identifier if it collides with a Dart reserved word
34
+ * by appending `_` (e.g. an account named `null` renders as `null_`).
35
+ */
36
+ export function escapeDartIdentifier(name: string): string {
37
+ return DART_RESERVED_WORDS.has(name) ? `${name}_` : name;
38
+ }
39
+
14
40
  /** Convert a string to camelCase */
15
41
  export function camelCase(str: string): string {
16
42
  const pascal = pascalCase(str);
17
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
43
+ const cased = pascal.charAt(0).toLowerCase() + pascal.slice(1);
44
+ return escapeDartIdentifier(cased);
18
45
  }
19
46
 
20
47
  /** Convert a string to snake_case */
@@ -0,0 +1,76 @@
1
+ import type { RootNode } from "@codama/nodes";
2
+
3
+ /**
4
+ * Normalizes a serialized Codama root before it reaches Codama's visitors.
5
+ *
6
+ * Codama constructors materialize empty child collections, while generated
7
+ * JSON commonly omits them. Pina intentionally omits those empty fields, so
8
+ * restore only the structural defaults required by Codama traversal. This is
9
+ * a copy: callers retain their original parsed IDL unchanged.
10
+ */
11
+ export function normalizeRootNode(root: RootNode): RootNode {
12
+ return normalizeNode(root) as RootNode;
13
+ }
14
+
15
+ function normalizeNode(value: unknown): unknown {
16
+ if (Array.isArray(value)) {
17
+ return value.map(normalizeNode);
18
+ }
19
+
20
+ if (!isRecord(value)) {
21
+ return value;
22
+ }
23
+
24
+ const normalized = Object.fromEntries(
25
+ Object.entries(value).map(([key, child]) => [key, normalizeNode(child)]),
26
+ ) as Record<string, unknown>;
27
+
28
+ switch (normalized.kind) {
29
+ case "rootNode":
30
+ setEmptyCollection(normalized, "additionalPrograms");
31
+ break;
32
+ case "programNode":
33
+ setEmptyCollection(normalized, "accounts");
34
+ setEmptyCollection(normalized, "constants");
35
+ setEmptyCollection(normalized, "definedTypes");
36
+ setEmptyCollection(normalized, "errors");
37
+ setEmptyCollection(normalized, "events");
38
+ setEmptyCollection(normalized, "instructions");
39
+ setEmptyCollection(normalized, "pdas");
40
+ break;
41
+ case "instructionNode":
42
+ setEmptyCollection(normalized, "accounts");
43
+ setEmptyCollection(normalized, "arguments");
44
+ break;
45
+ case "pdaNode":
46
+ setEmptyCollection(normalized, "seeds");
47
+ break;
48
+ case "structTypeNode":
49
+ setEmptyCollection(normalized, "fields");
50
+ break;
51
+ case "enumTypeNode":
52
+ setEmptyCollection(normalized, "variants");
53
+ break;
54
+ case "tupleTypeNode":
55
+ setEmptyCollection(normalized, "items");
56
+ break;
57
+ case "hiddenPrefixTypeNode":
58
+ setEmptyCollection(normalized, "prefix");
59
+ break;
60
+ case "hiddenSuffixTypeNode":
61
+ setEmptyCollection(normalized, "suffix");
62
+ break;
63
+ }
64
+
65
+ return normalized;
66
+ }
67
+
68
+ function isRecord(value: unknown): value is Record<string, unknown> {
69
+ return typeof value === "object" && value !== null;
70
+ }
71
+
72
+ function setEmptyCollection(record: Record<string, unknown>, key: string): void {
73
+ if (record[key] === undefined) {
74
+ record[key] = [];
75
+ }
76
+ }
@@ -0,0 +1,87 @@
1
+ import type {
2
+ InstructionInputValueNode,
3
+ ValueNode,
4
+ } from "@codama/nodes";
5
+
6
+ import type { Fragment } from "./fragment.js";
7
+ import { fragment, fragmentFromString, use } from "./fragment.js";
8
+ import { WELL_KNOWN_ADDRESSES } from "./wellKnownAddresses.js";
9
+
10
+ type RenderableValueNode = InstructionInputValueNode | ValueNode;
11
+
12
+ /**
13
+ * Render a standalone Codama value as a Dart expression.
14
+ *
15
+ * Contextual instruction values need resolver state and are deliberately
16
+ * rejected here. Defaults and discriminators must be deterministic at code
17
+ * generation time so generated encoders cannot silently choose a different
18
+ * wire value.
19
+ */
20
+ export function getDartValueFragment(
21
+ value: RenderableValueNode,
22
+ dartType?: string,
23
+ ): Fragment {
24
+ switch (value.kind) {
25
+ case "numberValueNode":
26
+ return fragmentFromString(
27
+ dartType === "BigInt"
28
+ ? `BigInt.from(${value.number})`
29
+ : String(value.number),
30
+ );
31
+ case "booleanValueNode":
32
+ return fragmentFromString(String(value.boolean));
33
+ case "stringValueNode":
34
+ return fragmentFromString(toDartStringLiteral(value.string));
35
+ case "noneValueNode":
36
+ return fragmentFromString("null");
37
+ case "bytesValueNode": {
38
+ if (value.encoding !== "base16") {
39
+ throw new Error(
40
+ `Unsupported deterministic bytes encoding: ${value.encoding}`,
41
+ );
42
+ }
43
+ const clean = value.data.replace(/^0x/, "");
44
+ if (clean.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(clean)) {
45
+ throw new Error(`Invalid hexadecimal bytes value: ${value.data}`);
46
+ }
47
+ const bytes = clean.match(/.{2}/g)?.map((byte) => `0x${byte}`) ?? [];
48
+ return fragment`${use("Uint8List", "dartTypedData")}.fromList([${fragmentFromString(bytes.join(", "))}])`;
49
+ }
50
+ case "publicKeyValueNode": {
51
+ const wellKnownName = WELL_KNOWN_ADDRESSES.get(value.publicKey);
52
+ return wellKnownName
53
+ ? fragment`${use(wellKnownName, "solanaAddresses")}`
54
+ : fragment`${use("Address", "solanaAddresses")}(${fragmentFromString(toDartStringLiteral(value.publicKey))})`;
55
+ }
56
+ default:
57
+ throw new Error(
58
+ `Unsupported deterministic Dart value node: ${value.kind}`,
59
+ );
60
+ }
61
+ }
62
+
63
+ /** Return whether a rendered standalone value is valid in a const initializer. */
64
+ export function isConstDartValueNode(value: RenderableValueNode): boolean {
65
+ switch (value.kind) {
66
+ case "numberValueNode":
67
+ case "booleanValueNode":
68
+ case "stringValueNode":
69
+ case "noneValueNode":
70
+ case "publicKeyValueNode":
71
+ return true;
72
+ default:
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function toDartStringLiteral(value: string): string {
78
+ return `'${value
79
+ .replaceAll("\\", "\\\\")
80
+ .replaceAll("\n", "\\n")
81
+ .replaceAll("\r", "\\r")
82
+ .replaceAll("\t", "\\t")
83
+ .replaceAll("\b", "\\b")
84
+ .replaceAll("\f", "\\f")
85
+ .replaceAll("$", "\\$")
86
+ .replaceAll("'", "\\'")}'`;
87
+ }
@@ -86,7 +86,7 @@ export function getRenderMapVisitor(
86
86
  visitRoot(node: RootNode, { self }) {
87
87
  const programMaps = [
88
88
  visit(node.program, self),
89
- ...node.additionalPrograms.map((p) => visit(p, self)),
89
+ ...(node.additionalPrograms ?? []).map((p) => visit(p, self)),
90
90
  ];
91
91
 
92
92
  const mergedProgramMap = mergeRenderMaps(programMaps);
@@ -228,13 +228,18 @@ export function getTypeManifestVisitor(input: {
228
228
  const itemManifest = visit(node.item, self);
229
229
  const encoderSizeExpr = getArraySizeExpression(node, "Encoder");
230
230
  const decoderSizeExpr = getArraySizeExpression(node, "Decoder");
231
+ const itemType = fragmentFromString(itemManifest.type.content);
232
+ const itemEncoder = fragment`${use(
233
+ "transformEncoder",
234
+ "solanaCodecsCore",
235
+ )}(${itemManifest.encoder}, (${itemType} value) => value)`;
231
236
 
232
237
  return {
233
238
  type: fragment`List<${itemManifest.type}>`,
234
239
  encoder: fragment`${use(
235
240
  "getArrayEncoder",
236
241
  "solanaCodecsDataStructures",
237
- )}(${itemManifest.encoder}${encoderSizeExpr})`,
242
+ )}(${itemEncoder}${encoderSizeExpr})`,
238
243
  decoder: fragment`${use(
239
244
  "getArrayDecoder",
240
245
  "solanaCodecsDataStructures",
@@ -286,7 +291,7 @@ export function getTypeManifestVisitor(input: {
286
291
  },
287
292
 
288
293
  visitTupleType(node: TupleTypeNode, { self }) {
289
- const itemManifests = node.items.map((item) => visit(item, self));
294
+ const itemManifests = (node.items ?? []).map((item) => visit(item, self));
290
295
  const typeList = itemManifests
291
296
  .map((m) => m.type.content)
292
297
  .join(", ");
@@ -300,19 +305,33 @@ export function getTypeManifestVisitor(input: {
300
305
  }
301
306
 
302
307
  const encoderListStr = encoderList.map((e) => e.content).join(", ");
308
+ const decoderListStr = decoderList.map((d) => d.content).join(", ");
309
+ // Two-element tuples render through the typed getTuple2 helpers so the
310
+ // generated value type is the Dart record `(T1, T2)` rather than a
311
+ // heterogeneous List<Object?>. Arity-2 is the only shape that currently
312
+ // appears in supported IDLs; wider tuples stay on the untyped helpers.
313
+ let tupleEncoderFn = "getTupleEncoder";
314
+ let tupleDecoderFn = "getTupleDecoder";
315
+ let tupleEncoderArgs = `[${encoderListStr}]`;
316
+ let tupleDecoderArgs = `[${decoderListStr}]`;
317
+ if (itemManifests.length === 2) {
318
+ tupleEncoderFn = "getTuple2Encoder";
319
+ tupleDecoderFn = "getTuple2Decoder";
320
+ tupleEncoderArgs = encoderListStr;
321
+ tupleDecoderArgs = decoderListStr;
322
+ }
303
323
  const encoderFrag = fragment`${use(
304
- "getTupleEncoder",
324
+ tupleEncoderFn,
305
325
  "solanaCodecsDataStructures",
306
- )}([${fragmentFromString(encoderListStr)}])`;
326
+ )}(${tupleEncoderArgs})`;
307
327
  for (const e of encoderList) {
308
328
  encoderFrag.imports.mergeWith(e.imports);
309
329
  }
310
330
 
311
- const decoderListStr = decoderList.map((d) => d.content).join(", ");
312
331
  const decoderFrag = fragment`${use(
313
- "getTupleDecoder",
332
+ tupleDecoderFn,
314
333
  "solanaCodecsDataStructures",
315
- )}([${fragmentFromString(decoderListStr)}])`;
334
+ )}(${tupleDecoderArgs})`;
316
335
  for (const d of decoderList) {
317
336
  decoderFrag.imports.mergeWith(d.imports);
318
337
  }
@@ -327,8 +346,9 @@ export function getTypeManifestVisitor(input: {
327
346
  },
328
347
 
329
348
  visitStructType(node: StructTypeNode, { self }) {
330
- const fields = node.fields.map((f) => visit(f, self));
331
- const fieldNames = node.fields.map((f) => f.name as string);
349
+ const nodeFields = node.fields ?? [];
350
+ const fields = nodeFields.map((f) => visit(f, self));
351
+ const fieldNames = nodeFields.map((f) => f.name as string);
332
352
 
333
353
  // Type: Map<String, Object?> (for inline structs without a name)
334
354
  const typeStr = "Map<String, Object?>";
@@ -371,7 +391,7 @@ export function getTypeManifestVisitor(input: {
371
391
 
372
392
  visitEnumType(node: EnumTypeNode, { self }) {
373
393
  // Check if this is a scalar enum (all empty variants)
374
- const isScalar = node.variants.every(
394
+ const isScalar = (node.variants ?? []).every(
375
395
  (v) => v.kind === "enumEmptyVariantTypeNode",
376
396
  );
377
397
 
@@ -436,7 +456,10 @@ export function getTypeManifestVisitor(input: {
436
456
  encoder: fragment`${use(
437
457
  "getNullableEncoder",
438
458
  "solanaCodecsDataStructures",
439
- )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.encoder}${fragmentFromString(prefixExpr)}${fragmentFromString(noneValueExpr)})`,
459
+ )}<${fragmentFromString(itemTypeStr)}>(${use(
460
+ "transformEncoder",
461
+ "solanaCodecsCore",
462
+ )}(${itemManifest.encoder}, (${fragmentFromString(itemTypeStr)} value) => value)${fragmentFromString(prefixExpr)}${fragmentFromString(noneValueExpr)})`,
440
463
  decoder: fragment`${use(
441
464
  "getNullableDecoder",
442
465
  "solanaCodecsDataStructures",
@@ -498,7 +521,7 @@ export function getTypeManifestVisitor(input: {
498
521
  encoder: fragment`${use(
499
522
  "fixEncoderSize",
500
523
  "solanaCodecsCore",
501
- )}(${innerManifest.encoder}, ${fragmentFromString(String(node.size))})`,
524
+ )}(${innerManifest.encoder}, ${fragmentFromString(String(node.size))}, allowTruncation: false)`,
502
525
  decoder: fragment`${use(
503
526
  "fixDecoderSize",
504
527
  "solanaCodecsCore",
@@ -549,7 +572,7 @@ export function getTypeManifestVisitor(input: {
549
572
 
550
573
  visitHiddenPrefixType(node: HiddenPrefixTypeNode, { self }) {
551
574
  const innerManifest = visit(node.type, self);
552
- const prefixes = node.prefix.map((p) => getHiddenAffixManifest(p, self));
575
+ const prefixes = (node.prefix ?? []).map((p) => getHiddenAffixManifest(p, self));
553
576
  const prefixEncoders = prefixes
554
577
  .map((p) => p.encoder.content)
555
578
  .join(", ");
@@ -584,7 +607,7 @@ export function getTypeManifestVisitor(input: {
584
607
 
585
608
  visitHiddenSuffixType(node: HiddenSuffixTypeNode, { self }) {
586
609
  const innerManifest = visit(node.type, self);
587
- const suffixes = node.suffix.map((s) => getHiddenAffixManifest(s, self));
610
+ const suffixes = (node.suffix ?? []).map((s) => getHiddenAffixManifest(s, self));
588
611
  const suffixEncoders = suffixes
589
612
  .map((s) => s.encoder.content)
590
613
  .join(", ");
@@ -9,6 +9,7 @@ import type { Fragment } from "../utils/fragment.js";
9
9
  import type { RenderOptions } from "../utils/options.js";
10
10
  import { DartImportMap, DART_EXTERNAL_PACKAGE_MAP } from "../utils/importMap.js";
11
11
  import { formatDartDirectory } from "../utils/formatCode.js";
12
+ import { normalizeRootNode } from "../utils/normalizeRootNode.js";
12
13
  import { getRenderMapVisitor } from "./getRenderMapVisitor.js";
13
14
 
14
15
  /**
@@ -29,6 +30,8 @@ export function renderVisitor(
29
30
  } = options;
30
31
 
31
32
  return rootNodeVisitor((root: RootNode) => {
33
+ const normalizedRoot = normalizeRootNode(root);
34
+
32
35
  // 1. Optionally delete the output directory
33
36
  if (deleteFolderBeforeRendering && existsSync(outputDir)) {
34
37
  deleteDirectory(outputDir);
@@ -36,7 +39,7 @@ export function renderVisitor(
36
39
 
37
40
  // 2. Build the render map
38
41
  const renderMap = visit(
39
- root,
42
+ normalizedRoot,
40
43
  getRenderMapVisitor({ nameApi, dependencyMap }),
41
44
  );
42
45