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.
- package/{readme.md → README.md} +38 -42
- package/dist/index.browser.cjs +628 -147
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +628 -148
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +628 -147
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +628 -148
- package/dist/index.node.js.map +1 -1
- package/dist/index.react-native.js +628 -148
- package/dist/index.react-native.js.map +1 -1
- package/dist/types/fragments/accountPage.d.ts.map +1 -1
- package/dist/types/fragments/instructionPage.d.ts.map +1 -1
- package/dist/types/fragments/typePage.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils/discriminators.d.ts +10 -0
- package/dist/types/utils/discriminators.d.ts.map +1 -0
- package/dist/types/utils/exactDecoder.d.ts +17 -0
- package/dist/types/utils/exactDecoder.d.ts.map +1 -0
- package/dist/types/utils/formatCode.d.ts +1 -1
- package/dist/types/utils/formatCode.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +4 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/nameTransformers.d.ts +5 -0
- package/dist/types/utils/nameTransformers.d.ts.map +1 -1
- package/dist/types/utils/normalizeRootNode.d.ts +11 -0
- package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
- package/dist/types/utils/valueNodes.d.ts +16 -0
- package/dist/types/utils/valueNodes.d.ts.map +1 -0
- package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
- package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
- package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
- package/package.json +21 -18
- package/src/fragments/accountPage.ts +90 -17
- package/src/fragments/instructionPage.ts +112 -79
- package/src/fragments/typePage.ts +134 -31
- package/src/index.ts +1 -0
- package/src/utils/discriminators.ts +121 -0
- package/src/utils/exactDecoder.ts +82 -0
- package/src/utils/formatCode.ts +7 -11
- package/src/utils/index.ts +4 -0
- package/src/utils/nameTransformers.ts +28 -1
- package/src/utils/normalizeRootNode.ts +76 -0
- package/src/utils/valueNodes.ts +87 -0
- package/src/visitors/getRenderMapVisitor.ts +1 -1
- package/src/visitors/getTypeManifestVisitor.ts +37 -14
- 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
|
+
}
|
package/src/utils/formatCode.ts
CHANGED
|
@@ -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
|
|
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 --
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
)}(${
|
|
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
|
-
|
|
324
|
+
tupleEncoderFn,
|
|
305
325
|
"solanaCodecsDataStructures",
|
|
306
|
-
)}(
|
|
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
|
-
|
|
332
|
+
tupleDecoderFn,
|
|
314
333
|
"solanaCodecsDataStructures",
|
|
315
|
-
)}(
|
|
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
|
|
331
|
-
const
|
|
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)}>(${
|
|
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
|
-
|
|
42
|
+
normalizedRoot,
|
|
40
43
|
getRenderMapVisitor({ nameApi, dependencyMap }),
|
|
41
44
|
);
|
|
42
45
|
|