codama-renderers-dart 0.5.1 → 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.
- package/{readme.md → README.md} +16 -43
- package/dist/index.browser.cjs +345 -124
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +346 -125
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +345 -124
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +346 -125
- package/dist/index.node.js.map +1 -1
- package/dist/index.react-native.js +346 -125
- package/dist/index.react-native.js.map +1 -1
- package/dist/types/fragments/accountPage.d.ts.map +1 -1
- package/dist/types/fragments/errorPage.d.ts.map +1 -1
- package/dist/types/fragments/instructionPage.d.ts.map +1 -1
- package/dist/types/fragments/pdaPage.d.ts.map +1 -1
- package/dist/types/fragments/programPage.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/valueNodes.d.ts +4 -0
- package/dist/types/utils/valueNodes.d.ts.map +1 -1
- package/dist/types/visitors/getRenderMapVisitor.d.ts.map +1 -1
- 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 +7 -7
- package/src/fragments/accountPage.ts +2 -1
- package/src/fragments/errorPage.ts +8 -5
- package/src/fragments/instructionPage.ts +54 -17
- package/src/fragments/pdaPage.ts +7 -14
- package/src/fragments/programPage.ts +3 -2
- package/src/fragments/typePage.ts +102 -19
- package/src/utils/formatCode.ts +2 -2
- package/src/utils/nameTransformers.ts +39 -1
- package/src/utils/valueNodes.ts +24 -1
- package/src/visitors/getRenderMapVisitor.ts +30 -5
- package/src/visitors/getTypeManifestVisitor.ts +123 -81
- package/src/visitors/renderVisitor.ts +11 -13
|
@@ -6,19 +6,50 @@ import type { CamelCaseString } from "@codama/nodes";
|
|
|
6
6
|
|
|
7
7
|
/** Convert a string to PascalCase */
|
|
8
8
|
export function pascalCase(str: string): string {
|
|
9
|
+
assertSafeName(str);
|
|
10
|
+
|
|
9
11
|
return str
|
|
10
12
|
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
11
13
|
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Dart reserved words (plus `async`/`await`/`yield` where they cannot be
|
|
18
|
+
* plain identifiers). A lowercase declaration named with one of these must
|
|
19
|
+
* be escaped, e.g. an account named `null` becomes a `null_` parameter.
|
|
20
|
+
*/
|
|
21
|
+
const DART_RESERVED_WORDS = new Set([
|
|
22
|
+
"abstract", "as", "assert", "async", "augment", "await", "base",
|
|
23
|
+
"break", "case", "catch", "class", "const", "continue", "covariant",
|
|
24
|
+
"default", "deferred", "do", "dynamic", "else", "enum", "export",
|
|
25
|
+
"extends", "extension", "external", "factory", "false", "final",
|
|
26
|
+
"finally", "for", "get", "hide", "if", "implements", "import", "in",
|
|
27
|
+
"interface", "is", "late", "library", "mixin", "native", "new", "null",
|
|
28
|
+
"of", "on", "operator", "part", "required", "rethrow", "return",
|
|
29
|
+
"sealed", "set", "show", "static", "super", "switch", "sync", "this",
|
|
30
|
+
"throw", "true", "try", "typedef", "var", "void", "when", "while",
|
|
31
|
+
"with", "yield",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Escape a lowercase identifier if it collides with a Dart reserved word
|
|
36
|
+
* by appending `_` (e.g. an account named `null` renders as `null_`).
|
|
37
|
+
*/
|
|
38
|
+
export function escapeDartIdentifier(name: string): string {
|
|
39
|
+
return DART_RESERVED_WORDS.has(name) ? `${name}_` : name;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
/** Convert a string to camelCase */
|
|
15
43
|
export function camelCase(str: string): string {
|
|
16
44
|
const pascal = pascalCase(str);
|
|
17
|
-
|
|
45
|
+
const cased = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
46
|
+
return escapeDartIdentifier(cased);
|
|
18
47
|
}
|
|
19
48
|
|
|
20
49
|
/** Convert a string to snake_case */
|
|
21
50
|
export function snakeCase(str: string): string {
|
|
51
|
+
assertSafeName(str);
|
|
52
|
+
|
|
22
53
|
return str
|
|
23
54
|
.replace(/([A-Z])/g, "_$1")
|
|
24
55
|
.replace(/[-\s]+/g, "_")
|
|
@@ -111,3 +142,10 @@ export function createDartNameApi(): DartNameApi {
|
|
|
111
142
|
export function fromCamelCaseString(str: CamelCaseString): string {
|
|
112
143
|
return str as string;
|
|
113
144
|
}
|
|
145
|
+
|
|
146
|
+
/** Reject IDL names that could escape generated identifiers, strings, or paths. */
|
|
147
|
+
function assertSafeName(name: string): void {
|
|
148
|
+
if (!/^[a-zA-Z0-9_ -]*$/.test(name)) {
|
|
149
|
+
throw new Error(`Invalid Dart node name: ${JSON.stringify(name)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/utils/valueNodes.ts
CHANGED
|
@@ -23,12 +23,25 @@ export function getDartValueFragment(
|
|
|
23
23
|
): Fragment {
|
|
24
24
|
switch (value.kind) {
|
|
25
25
|
case "numberValueNode":
|
|
26
|
+
// Serialized IDLs bypass TypeScript types; never interpolate unvalidated data.
|
|
27
|
+
if (typeof value.number !== "number" || !Number.isFinite(value.number)) {
|
|
28
|
+
throw new Error("Invalid Dart number: expected a finite number");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (dartType === "BigInt" && !Number.isSafeInteger(value.number)) {
|
|
32
|
+
throw new Error("Invalid Dart BigInt: expected a safe integer");
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
return fragmentFromString(
|
|
27
36
|
dartType === "BigInt"
|
|
28
37
|
? `BigInt.from(${value.number})`
|
|
29
38
|
: String(value.number),
|
|
30
39
|
);
|
|
31
40
|
case "booleanValueNode":
|
|
41
|
+
if (typeof value.boolean !== "boolean") {
|
|
42
|
+
throw new Error("Invalid Dart boolean: expected a boolean");
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
return fragmentFromString(String(value.boolean));
|
|
33
46
|
case "stringValueNode":
|
|
34
47
|
return fragmentFromString(toDartStringLiteral(value.string));
|
|
@@ -74,7 +87,8 @@ export function isConstDartValueNode(value: RenderableValueNode): boolean {
|
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
/** Escape IDL text so it remains data inside a Dart string literal. */
|
|
91
|
+
export function toDartStringLiteral(value: string): string {
|
|
78
92
|
return `'${value
|
|
79
93
|
.replaceAll("\\", "\\\\")
|
|
80
94
|
.replaceAll("\n", "\\n")
|
|
@@ -85,3 +99,12 @@ function toDartStringLiteral(value: string): string {
|
|
|
85
99
|
.replaceAll("$", "\\$")
|
|
86
100
|
.replaceAll("'", "\\'")}'`;
|
|
87
101
|
}
|
|
102
|
+
|
|
103
|
+
/** Reject executable or lossy sizes from serialized IDLs before generating Dart. */
|
|
104
|
+
export function getNonNegativeInteger(value: number): string {
|
|
105
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
106
|
+
throw new Error("Expected a non-negative safe integer value");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return String(value);
|
|
110
|
+
}
|
|
@@ -89,10 +89,13 @@ export function getRenderMapVisitor(
|
|
|
89
89
|
...(node.additionalPrograms ?? []).map((p) => visit(p, self)),
|
|
90
90
|
];
|
|
91
91
|
|
|
92
|
-
const mergedProgramMap =
|
|
92
|
+
const mergedProgramMap = mergeRenderMapsWithoutCollisions(programMaps);
|
|
93
93
|
const categoryMaps = getSharedCategoryIndexMaps(mergedProgramMap);
|
|
94
94
|
|
|
95
|
-
return
|
|
95
|
+
return mergeRenderMapsWithoutCollisions([
|
|
96
|
+
mergedProgramMap,
|
|
97
|
+
...categoryMaps,
|
|
98
|
+
]);
|
|
96
99
|
},
|
|
97
100
|
|
|
98
101
|
visitProgram(node: ProgramNode, { self }) {
|
|
@@ -173,7 +176,7 @@ export function getRenderMapVisitor(
|
|
|
173
176
|
maps.push(rootMap);
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
return
|
|
179
|
+
return mergeRenderMapsWithoutCollisions(maps);
|
|
177
180
|
},
|
|
178
181
|
|
|
179
182
|
visitAccount(node: AccountNode, { self }) {
|
|
@@ -213,6 +216,30 @@ export function getRenderMapVisitor(
|
|
|
213
216
|
);
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Merges generated files while rejecting ambiguous output paths.
|
|
221
|
+
*
|
|
222
|
+
* Silent overwrites could replace signer checks or data layouts when an
|
|
223
|
+
* untrusted IDL contains duplicate names.
|
|
224
|
+
*/
|
|
225
|
+
function mergeRenderMapsWithoutCollisions(
|
|
226
|
+
maps: RenderMap<Fragment>[],
|
|
227
|
+
): RenderMap<Fragment> {
|
|
228
|
+
const paths = new Set<string>();
|
|
229
|
+
|
|
230
|
+
for (const map of maps) {
|
|
231
|
+
for (const path of map.keys()) {
|
|
232
|
+
if (paths.has(path)) {
|
|
233
|
+
throw new Error(`Duplicate generated path "${path}"`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
paths.add(path);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return mergeRenderMaps(maps);
|
|
241
|
+
}
|
|
242
|
+
|
|
216
243
|
function getSharedCategoryIndexMaps(
|
|
217
244
|
renderMap: RenderMap<Fragment>,
|
|
218
245
|
): RenderMap<Fragment>[] {
|
|
@@ -233,8 +260,6 @@ function getSharedCategoryIndexMaps(
|
|
|
233
260
|
if (!match) continue;
|
|
234
261
|
|
|
235
262
|
const [, category, fileName] = match;
|
|
236
|
-
if (fileName === categoryIndexNames[category]) continue;
|
|
237
|
-
|
|
238
263
|
if (!categories.has(category)) {
|
|
239
264
|
categories.set(category, new Set());
|
|
240
265
|
}
|
|
@@ -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
|
|
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(
|
|
142
|
-
encoder:
|
|
143
|
-
|
|
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
|
};
|
|
@@ -239,7 +222,7 @@ export function getTypeManifestVisitor(input: {
|
|
|
239
222
|
encoder: fragment`${use(
|
|
240
223
|
"getArrayEncoder",
|
|
241
224
|
"solanaCodecsDataStructures",
|
|
242
|
-
)}
|
|
225
|
+
)}(${itemEncoder}${encoderSizeExpr})`,
|
|
243
226
|
decoder: fragment`${use(
|
|
244
227
|
"getArrayDecoder",
|
|
245
228
|
"solanaCodecsDataStructures",
|
|
@@ -305,19 +288,33 @@ export function getTypeManifestVisitor(input: {
|
|
|
305
288
|
}
|
|
306
289
|
|
|
307
290
|
const encoderListStr = encoderList.map((e) => e.content).join(", ");
|
|
291
|
+
const decoderListStr = decoderList.map((d) => d.content).join(", ");
|
|
292
|
+
// Two-element tuples render through the typed getTuple2 helpers so the
|
|
293
|
+
// generated value type is the Dart record `(T1, T2)` rather than a
|
|
294
|
+
// heterogeneous List<Object?>. Arity-2 is the only shape that currently
|
|
295
|
+
// appears in supported IDLs; wider tuples stay on the untyped helpers.
|
|
296
|
+
let tupleEncoderFn = "getTupleEncoder";
|
|
297
|
+
let tupleDecoderFn = "getTupleDecoder";
|
|
298
|
+
let tupleEncoderArgs = `[${encoderListStr}]`;
|
|
299
|
+
let tupleDecoderArgs = `[${decoderListStr}]`;
|
|
300
|
+
if (itemManifests.length === 2) {
|
|
301
|
+
tupleEncoderFn = "getTuple2Encoder";
|
|
302
|
+
tupleDecoderFn = "getTuple2Decoder";
|
|
303
|
+
tupleEncoderArgs = encoderListStr;
|
|
304
|
+
tupleDecoderArgs = decoderListStr;
|
|
305
|
+
}
|
|
308
306
|
const encoderFrag = fragment`${use(
|
|
309
|
-
|
|
307
|
+
tupleEncoderFn,
|
|
310
308
|
"solanaCodecsDataStructures",
|
|
311
|
-
)}(
|
|
309
|
+
)}(${tupleEncoderArgs})`;
|
|
312
310
|
for (const e of encoderList) {
|
|
313
311
|
encoderFrag.imports.mergeWith(e.imports);
|
|
314
312
|
}
|
|
315
313
|
|
|
316
|
-
const decoderListStr = decoderList.map((d) => d.content).join(", ");
|
|
317
314
|
const decoderFrag = fragment`${use(
|
|
318
|
-
|
|
315
|
+
tupleDecoderFn,
|
|
319
316
|
"solanaCodecsDataStructures",
|
|
320
|
-
)}(
|
|
317
|
+
)}(${tupleDecoderArgs})`;
|
|
321
318
|
for (const d of decoderList) {
|
|
322
319
|
decoderFrag.imports.mergeWith(d.imports);
|
|
323
320
|
}
|
|
@@ -341,7 +338,7 @@ export function getTypeManifestVisitor(input: {
|
|
|
341
338
|
|
|
342
339
|
// Build encoder fields list
|
|
343
340
|
const encoderFields = fields
|
|
344
|
-
.map((f, i) => `(
|
|
341
|
+
.map((f, i) => `(${toDartStringLiteral(fieldNames[i])}, ${f.encoder.content})`)
|
|
345
342
|
.join(", ");
|
|
346
343
|
const encoderFrag = fragment`${use(
|
|
347
344
|
"getStructEncoder",
|
|
@@ -352,7 +349,7 @@ export function getTypeManifestVisitor(input: {
|
|
|
352
349
|
}
|
|
353
350
|
|
|
354
351
|
const decoderFields = fields
|
|
355
|
-
.map((f, i) => `(
|
|
352
|
+
.map((f, i) => `(${toDartStringLiteral(fieldNames[i])}, ${f.decoder.content})`)
|
|
356
353
|
.join(", ");
|
|
357
354
|
const decoderFrag = fragment`${use(
|
|
358
355
|
"getStructDecoder",
|
|
@@ -428,10 +425,10 @@ export function getTypeManifestVisitor(input: {
|
|
|
428
425
|
const itemTypeStr = itemManifest.type.content;
|
|
429
426
|
const resolvedPrefix = resolveNestedTypeNode(node.prefix);
|
|
430
427
|
const prefixExpr = resolvedPrefix.format !== "u8"
|
|
431
|
-
? `, prefix: ${getNumberCodecExpression(resolvedPrefix
|
|
428
|
+
? fragment`, prefix: ${getNumberCodecExpression(resolvedPrefix, "Encoder")}`
|
|
432
429
|
: "";
|
|
433
430
|
const prefixDecoderExpr = resolvedPrefix.format !== "u8"
|
|
434
|
-
? `, prefix: ${getNumberCodecExpression(resolvedPrefix
|
|
431
|
+
? fragment`, prefix: ${getNumberCodecExpression(resolvedPrefix, "Decoder")}`
|
|
435
432
|
: "";
|
|
436
433
|
const noneValueExpr = node.fixed
|
|
437
434
|
? ", noneValue: const ZeroesNoneValue()"
|
|
@@ -442,11 +439,14 @@ export function getTypeManifestVisitor(input: {
|
|
|
442
439
|
encoder: fragment`${use(
|
|
443
440
|
"getNullableEncoder",
|
|
444
441
|
"solanaCodecsDataStructures",
|
|
445
|
-
)}<${fragmentFromString(itemTypeStr)}>(${
|
|
442
|
+
)}<${fragmentFromString(itemTypeStr)}>(${use(
|
|
443
|
+
"transformEncoder",
|
|
444
|
+
"solanaCodecsCore",
|
|
445
|
+
)}(${itemManifest.encoder}, (${fragmentFromString(itemTypeStr)} value) => value)${prefixExpr}${fragmentFromString(noneValueExpr)})`,
|
|
446
446
|
decoder: fragment`${use(
|
|
447
447
|
"getNullableDecoder",
|
|
448
448
|
"solanaCodecsDataStructures",
|
|
449
|
-
)}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}${
|
|
449
|
+
)}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}${prefixDecoderExpr}${fragmentFromString(noneValueExpr)})`,
|
|
450
450
|
value: emptyTypeManifest().value,
|
|
451
451
|
isEnum: false,
|
|
452
452
|
};
|
|
@@ -504,11 +504,11 @@ export function getTypeManifestVisitor(input: {
|
|
|
504
504
|
encoder: fragment`${use(
|
|
505
505
|
"fixEncoderSize",
|
|
506
506
|
"solanaCodecsCore",
|
|
507
|
-
)}(${innerManifest.encoder}, ${fragmentFromString(
|
|
507
|
+
)}(${innerManifest.encoder}, ${fragmentFromString(getNonNegativeInteger(node.size))}, allowTruncation: false)`,
|
|
508
508
|
decoder: fragment`${use(
|
|
509
509
|
"fixDecoderSize",
|
|
510
510
|
"solanaCodecsCore",
|
|
511
|
-
)}(${innerManifest.decoder}, ${fragmentFromString(
|
|
511
|
+
)}(${innerManifest.decoder}, ${fragmentFromString(getNonNegativeInteger(node.size))})`,
|
|
512
512
|
value: innerManifest.value,
|
|
513
513
|
isEnum: innerManifest.isEnum,
|
|
514
514
|
};
|
|
@@ -527,12 +527,12 @@ export function getTypeManifestVisitor(input: {
|
|
|
527
527
|
let prefixManifest;
|
|
528
528
|
if (
|
|
529
529
|
node.prefix.kind === "numberTypeNode" &&
|
|
530
|
-
bigIntFormats.has(
|
|
530
|
+
bigIntFormats.has(node.prefix.format)
|
|
531
531
|
) {
|
|
532
|
-
const
|
|
532
|
+
const numberManifest = visit(node.prefix, self);
|
|
533
533
|
prefixManifest = {
|
|
534
|
-
encoder: fragment
|
|
535
|
-
decoder: fragment
|
|
534
|
+
encoder: fragment`${use("transformEncoder", "solanaCodecsCore")}(${numberManifest.encoder}, (size) => BigInt.from(size))`,
|
|
535
|
+
decoder: fragment`${use("transformDecoder", "solanaCodecsCore")}(${numberManifest.decoder}, (size, _, __) => size.toInt())`,
|
|
536
536
|
};
|
|
537
537
|
} else {
|
|
538
538
|
prefixManifest = visit(node.prefix, self);
|
|
@@ -643,13 +643,11 @@ export function getTypeManifestVisitor(input: {
|
|
|
643
643
|
},
|
|
644
644
|
|
|
645
645
|
visitPreOffsetType(node: PreOffsetTypeNode, { self }) {
|
|
646
|
-
|
|
647
|
-
return visit(node.type, self);
|
|
646
|
+
return getOffsetManifest(node, self);
|
|
648
647
|
},
|
|
649
648
|
|
|
650
649
|
visitPostOffsetType(node: PostOffsetTypeNode, { self }) {
|
|
651
|
-
|
|
652
|
-
return visit(node.type, self);
|
|
650
|
+
return getOffsetManifest(node, self);
|
|
653
651
|
},
|
|
654
652
|
|
|
655
653
|
visitSolAmountType(node: SolAmountTypeNode, { self }) {
|
|
@@ -760,22 +758,26 @@ function getNumberCodecName(format: string): string {
|
|
|
760
758
|
}
|
|
761
759
|
|
|
762
760
|
function getNumberCodecExpression(
|
|
763
|
-
|
|
761
|
+
node: NumberTypeNode,
|
|
764
762
|
codecType: "Encoder" | "Decoder",
|
|
765
|
-
):
|
|
766
|
-
const name = getNumberCodecName(format);
|
|
767
|
-
|
|
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})`;
|
|
768
770
|
}
|
|
769
771
|
|
|
770
772
|
function getArraySizeExpression(
|
|
771
773
|
node: ArrayTypeNode,
|
|
772
774
|
codecType: "Encoder" | "Decoder",
|
|
773
|
-
): string {
|
|
775
|
+
): string | Fragment {
|
|
774
776
|
if (!("count" in node) || !node.count) return "";
|
|
775
777
|
const count = node.count;
|
|
776
778
|
switch (count.kind) {
|
|
777
779
|
case "fixedCountNode":
|
|
778
|
-
return `, size: FixedArraySize(${count.value})`;
|
|
780
|
+
return `, size: FixedArraySize(${getNonNegativeInteger(count.value)})`;
|
|
779
781
|
case "remainderCountNode":
|
|
780
782
|
return ", size: RemainderArraySize()";
|
|
781
783
|
case "prefixedCountNode": {
|
|
@@ -783,7 +785,7 @@ function getArraySizeExpression(
|
|
|
783
785
|
if (resolvedPrefix.format === "u32" && (!resolvedPrefix.endian || resolvedPrefix.endian === "le")) {
|
|
784
786
|
return ""; // Default
|
|
785
787
|
}
|
|
786
|
-
return `, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix
|
|
788
|
+
return fragment`, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix, codecType)})`;
|
|
787
789
|
}
|
|
788
790
|
default:
|
|
789
791
|
return "";
|
|
@@ -793,12 +795,12 @@ function getArraySizeExpression(
|
|
|
793
795
|
function getMapSizeExpression(
|
|
794
796
|
node: MapTypeNode,
|
|
795
797
|
codecType: "Encoder" | "Decoder",
|
|
796
|
-
): string {
|
|
798
|
+
): string | Fragment {
|
|
797
799
|
if (!("count" in node) || !node.count) return "";
|
|
798
800
|
const count = node.count;
|
|
799
801
|
switch (count.kind) {
|
|
800
802
|
case "fixedCountNode":
|
|
801
|
-
return `, size: FixedArraySize(${count.value})`;
|
|
803
|
+
return `, size: FixedArraySize(${getNonNegativeInteger(count.value)})`;
|
|
802
804
|
case "remainderCountNode":
|
|
803
805
|
return ", size: RemainderArraySize()";
|
|
804
806
|
case "prefixedCountNode": {
|
|
@@ -806,7 +808,7 @@ function getMapSizeExpression(
|
|
|
806
808
|
if (resolvedPrefix.format === "u32" && (!resolvedPrefix.endian || resolvedPrefix.endian === "le")) {
|
|
807
809
|
return "";
|
|
808
810
|
}
|
|
809
|
-
return `, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix
|
|
811
|
+
return fragment`, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix, codecType)})`;
|
|
810
812
|
}
|
|
811
813
|
default:
|
|
812
814
|
return "";
|
|
@@ -816,12 +818,12 @@ function getMapSizeExpression(
|
|
|
816
818
|
function getSetSizeExpression(
|
|
817
819
|
node: SetTypeNode,
|
|
818
820
|
codecType: "Encoder" | "Decoder",
|
|
819
|
-
): string {
|
|
821
|
+
): string | Fragment {
|
|
820
822
|
if (!("count" in node) || !node.count) return "";
|
|
821
823
|
const count = node.count;
|
|
822
824
|
switch (count.kind) {
|
|
823
825
|
case "fixedCountNode":
|
|
824
|
-
return `, size: FixedArraySize(${count.value})`;
|
|
826
|
+
return `, size: FixedArraySize(${getNonNegativeInteger(count.value)})`;
|
|
825
827
|
case "remainderCountNode":
|
|
826
828
|
return ", size: RemainderArraySize()";
|
|
827
829
|
case "prefixedCountNode": {
|
|
@@ -829,7 +831,7 @@ function getSetSizeExpression(
|
|
|
829
831
|
if (resolvedPrefix.format === "u32" && (!resolvedPrefix.endian || resolvedPrefix.endian === "le")) {
|
|
830
832
|
return "";
|
|
831
833
|
}
|
|
832
|
-
return `, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix
|
|
834
|
+
return fragment`, size: PrefixedArraySize(${getNumberCodecExpression(resolvedPrefix, codecType)})`;
|
|
833
835
|
}
|
|
834
836
|
default:
|
|
835
837
|
return "";
|
|
@@ -842,7 +844,7 @@ type HiddenAffixManifest = {
|
|
|
842
844
|
};
|
|
843
845
|
|
|
844
846
|
function getHiddenAffixManifest(
|
|
845
|
-
node:
|
|
847
|
+
node: ConstantValueNode | TypeNode,
|
|
846
848
|
self: Visitor<TypeManifest>,
|
|
847
849
|
): HiddenAffixManifest {
|
|
848
850
|
if (node?.kind !== "constantValueNode") {
|
|
@@ -850,40 +852,80 @@ function getHiddenAffixManifest(
|
|
|
850
852
|
return { encoder: manifest.encoder, decoder: manifest.decoder };
|
|
851
853
|
}
|
|
852
854
|
|
|
853
|
-
const constantBytes = getConstantBytesExpression(node);
|
|
855
|
+
const constantBytes = getConstantBytesExpression(node, self);
|
|
854
856
|
return {
|
|
855
857
|
encoder: fragment`${use("getConstantEncoder", "solanaCodecsDataStructures")}(${constantBytes})`,
|
|
856
858
|
decoder: fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(${constantBytes})`,
|
|
857
859
|
};
|
|
858
860
|
}
|
|
859
861
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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") {
|
|
865
868
|
throw new Error(
|
|
866
|
-
`Unsupported hidden affix constant value kind: ${
|
|
869
|
+
`Unsupported hidden affix constant value kind: ${node.value.kind}`,
|
|
867
870
|
);
|
|
868
871
|
}
|
|
869
872
|
|
|
870
|
-
const
|
|
873
|
+
const manifest = visit(node.type, self);
|
|
871
874
|
|
|
872
|
-
if (
|
|
873
|
-
|
|
875
|
+
if (!["int", "BigInt", "double"].includes(manifest.type.content)) {
|
|
876
|
+
throw new Error(`Unsupported hidden affix constant type kind: ${node.type.kind}`);
|
|
874
877
|
}
|
|
875
878
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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");
|
|
884
891
|
}
|
|
885
892
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
+
};
|
|
889
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
|
-
//
|
|
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
|
|