codama-renderers-dart 0.4.3 → 0.5.1

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.browser.cjs +634 -159
  3. package/dist/index.browser.cjs.map +1 -1
  4. package/dist/index.browser.js +635 -161
  5. package/dist/index.browser.js.map +1 -1
  6. package/dist/index.node.cjs +634 -159
  7. package/dist/index.node.cjs.map +1 -1
  8. package/dist/index.node.js +635 -161
  9. package/dist/index.node.js.map +1 -1
  10. package/dist/index.react-native.js +635 -161
  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/programPage.d.ts +1 -1
  15. package/dist/types/fragments/programPage.d.ts.map +1 -1
  16. package/dist/types/fragments/typePage.d.ts.map +1 -1
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/types/utils/discriminators.d.ts +10 -0
  20. package/dist/types/utils/discriminators.d.ts.map +1 -0
  21. package/dist/types/utils/exactDecoder.d.ts +17 -0
  22. package/dist/types/utils/exactDecoder.d.ts.map +1 -0
  23. package/dist/types/utils/formatCode.d.ts +1 -1
  24. package/dist/types/utils/formatCode.d.ts.map +1 -1
  25. package/dist/types/utils/importMap.d.ts.map +1 -1
  26. package/dist/types/utils/index.d.ts +4 -0
  27. package/dist/types/utils/index.d.ts.map +1 -1
  28. package/dist/types/utils/nameTransformers.d.ts +1 -1
  29. package/dist/types/utils/nameTransformers.d.ts.map +1 -1
  30. package/dist/types/utils/normalizeRootNode.d.ts +11 -0
  31. package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
  32. package/dist/types/utils/valueNodes.d.ts +16 -0
  33. package/dist/types/utils/valueNodes.d.ts.map +1 -0
  34. package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
  35. package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
  36. package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
  37. package/package.json +31 -23
  38. package/readme.md +30 -3
  39. package/src/fragments/accountPage.ts +90 -17
  40. package/src/fragments/instructionPage.ts +109 -72
  41. package/src/fragments/programPage.ts +165 -6
  42. package/src/fragments/typePage.ts +36 -15
  43. package/src/index.ts +1 -0
  44. package/src/utils/discriminators.ts +121 -0
  45. package/src/utils/exactDecoder.ts +82 -0
  46. package/src/utils/formatCode.ts +7 -11
  47. package/src/utils/importMap.ts +16 -3
  48. package/src/utils/index.ts +4 -0
  49. package/src/utils/nameTransformers.ts +3 -2
  50. package/src/utils/normalizeRootNode.ts +76 -0
  51. package/src/utils/valueNodes.ts +87 -0
  52. package/src/utils/wellKnownAddresses.ts +4 -4
  53. package/src/visitors/getRenderMapVisitor.ts +1 -1
  54. package/src/visitors/getTypeManifestVisitor.ts +48 -29
  55. package/src/visitors/renderVisitor.ts +11 -4
@@ -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
  }
@@ -77,15 +77,20 @@ export class DartImportMap {
77
77
  for (const module of this._imports) {
78
78
  // Check internal map first (generated cross-references)
79
79
  if (module in internalMap) {
80
- uris.add(internalMap[module]);
80
+ const uri = internalMap[module];
81
+ if (uri) {
82
+ uris.add(uri);
83
+ }
81
84
  }
82
85
  // Then external package map
83
86
  else if (module in DART_EXTERNAL_PACKAGE_MAP) {
84
87
  uris.add(DART_EXTERNAL_PACKAGE_MAP[module]);
85
88
  }
86
- // Assume it's already a raw URI (e.g., "package:..." or relative path)
87
- else {
89
+ // Finally, accept explicit Dart URIs and relative Dart paths.
90
+ else if (isRawDartImportUri(module)) {
88
91
  uris.add(module);
92
+ } else {
93
+ throw new Error(`Unresolved Dart import module "${module}"`);
89
94
  }
90
95
  }
91
96
 
@@ -127,3 +132,11 @@ export class DartImportMap {
127
132
  return lines.join("\n");
128
133
  }
129
134
  }
135
+
136
+ function isRawDartImportUri(module: string): boolean {
137
+ return (
138
+ module.startsWith("dart:") ||
139
+ module.startsWith("package:") ||
140
+ (module.endsWith(".dart") && !module.includes(":"))
141
+ );
142
+ }
@@ -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";
@@ -66,7 +66,7 @@ export interface DartNameApi {
66
66
  errorMessageFunction(name: string): string;
67
67
  /** Enum variant: `camelCase` */
68
68
  enumVariant(name: string): string;
69
- /** Sealed class variant (data enum): `PascalCase` */
69
+ /** Sealed class variant (data enum): `{ParentName}{PascalCase}` */
70
70
  sealedClassVariant(parentName: string, variantName: string): string;
71
71
  /** File name: `snake_case.dart` */
72
72
  fileName(name: string): string;
@@ -97,7 +97,8 @@ export function createDartNameApi(): DartNameApi {
97
97
  `${camelCase(programName)}Error${pascalCase(errorName)}`,
98
98
  errorMessageFunction: (name) => `get${pascalCase(name)}ErrorMessage`,
99
99
  enumVariant: (name) => camelCase(name),
100
- sealedClassVariant: (_parentName, variantName) => pascalCase(variantName),
100
+ sealedClassVariant: (parentName, variantName) =>
101
+ `${pascalCase(parentName)}${pascalCase(variantName)}`,
101
102
  isTypeFunction: (name) => `is${pascalCase(name)}`,
102
103
  fileName: (name) => snakeCase(name),
103
104
  discriminatorConstant: (name) => `${camelCase(name)}Discriminator`,
@@ -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
+ }
@@ -44,10 +44,10 @@ export const WELL_KNOWN_ADDRESSES: ReadonlyMap<string, string> = new Map([
44
44
 
45
45
  // SPL programs
46
46
  ['TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'tokenProgramAddress'],
47
- ['TokenzQdBNb4qyze1S1U9AHB8MGXmNK1REkTPT5Z3Y', 'token2022ProgramAddress'],
48
- ['ATokenGPvbdGVxr1b2hvZbsiqW5xWH25ef7s3c8BnQKu', 'associatedTokenProgramAddress'],
49
- ['Memo1UhkJRfRhVq1sR7Y7Nto1s3J2mXTT3L6Wk4K2m6', 'memoProgramAddress'],
50
- ['MemoSq4gqABXKKYWVqKkV3m3kb1cTR7F2UMy9r6kFNK', 'memoLegacyProgramAddress'],
47
+ ['TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', 'token2022ProgramAddress'],
48
+ ['ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 'associatedTokenProgramAddress'],
49
+ ['MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', 'memoProgramAddress'],
50
+ ['Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo', 'memoLegacyProgramAddress'],
51
51
 
52
52
  // Metaplex programs
53
53
  ['metaqbxxU9qX8rZXkmUJ7haCrXXz2W1PSUq1Rn3E1od', 'tokenMetadataProgramAddress'],
@@ -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);