Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/fragments/constantPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ConstantNode, isNode } from '@codama/nodes';
import { visit } from '@codama/visitors-core';

import { Fragment, fragment, getDocblockFragment, mergeFragments, RenderScope } from '../utils';

/**
* Renders program constants as TypeScript value declarations.
*
* @param scope - The rendering scope and constants to render.
* @returns The generated constants page fragment, if constants exist.
*/
export function getConstantsPageFragment(
scope: Pick<RenderScope, 'nameApi' | 'typeManifestVisitor'> & { nodes: ConstantNode[] },
): Fragment | undefined {
if (scope.nodes.length === 0) return;

return mergeFragments(
[...scope.nodes].sort((a, b) => a.name.localeCompare(b.name)).map(node => getConstantFragment(node, scope)),
constants => constants.join('\n\n'),
);
}

function getConstantFragment(
node: ConstantNode,
scope: Pick<RenderScope, 'nameApi' | 'typeManifestVisitor'>,
): Fragment {
const typeManifest = visit(node.type, scope.typeManifestVisitor);
const rawValue = visit(node.value, scope.typeManifestVisitor).value;
const isNumberValue = isNode(node.value, 'numberValueNode');
const isNumberType = isNode(node.type, 'numberTypeNode');
const isSafeNumberType = isNumberType && ['u8', 'u16', 'u32'].includes(node.type.format);
const useBigInt = isNumberValue && isNumberType && !isSafeNumberType;
const value = useBigInt ? fragment`${rawValue}n` : rawValue;
const valueType = isNode(node.value, 'stringValueNode')
? fragment`string`
: useBigInt
? fragment`bigint`
: typeManifest.strictType;
const docs = getDocblockFragment(node.docs ?? [], true);

return fragment`${docs}export const ${scope.nameApi.constant(node.name)}: ${valueType} = ${value};`;
}
1 change: 1 addition & 0 deletions src/fragments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './accountPage';
export * from './accountPdaHelpers';
export * from './accountSizeHelpers';
export * from './accountType';
export * from './constantPage';
export * from './discriminatorCondition';
export * from './discriminatorConstants';
export * from './errorPage';
Expand Down
5 changes: 4 additions & 1 deletion src/fragments/rootIndexPage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AccountNode, DefinedTypeNode, InstructionNode, PdaNode, ProgramNode } from '@codama/nodes';
import { AccountNode, ConstantNode, DefinedTypeNode, InstructionNode, PdaNode, ProgramNode } from '@codama/nodes';

import { Fragment, fragment, getExportAllFragment, mergeFragments } from '../utils';

export function getRootIndexPageFragment(scope: {
accountsToExport: AccountNode[];
constantsToExport: ConstantNode[];
definedTypesToExport: DefinedTypeNode[];
instructionsToExport: InstructionNode[];
pdasToExport: PdaNode[];
Expand All @@ -12,6 +13,7 @@ export function getRootIndexPageFragment(scope: {
const hasAnythingToExport =
scope.programsToExport.length > 0 ||
scope.accountsToExport.length > 0 ||
scope.constantsToExport.length > 0 ||
scope.instructionsToExport.length > 0 ||
scope.definedTypesToExport.length > 0;

Expand All @@ -24,6 +26,7 @@ export function getRootIndexPageFragment(scope: {
return mergeFragments(
[
scope.accountsToExport.length > 0 ? getExportAllFragment('./accounts') : undefined,
scope.constantsToExport.length > 0 ? getExportAllFragment('./constants') : undefined,
programsWithErrorsToExport.length > 0 ? getExportAllFragment('./errors') : undefined,
scope.instructionsToExport.length > 0 ? getExportAllFragment('./instructions') : undefined,
scope.pdasToExport.length > 0 ? getExportAllFragment('./pdas') : undefined,
Expand Down
8 changes: 8 additions & 0 deletions src/visitors/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
camelCase,
CamelCaseString,
getAllAccounts,
getAllConstants,
getAllDefinedTypes,
getAllInstructionsWithSubs,
getAllPdas,
Expand All @@ -24,6 +25,7 @@ import {

import {
getAccountPageFragment,
getConstantsPageFragment,
getErrorPageFragment,
getIndexPageFragment,
getInstructionPageFragment,
Expand Down Expand Up @@ -166,6 +168,7 @@ export function getRenderMapVisitor(
const programsWithErrorsToExport = programsToExport.filter(p => (p.errors ?? []).length > 0);
const pdasToExport = getAllPdas(node);
const accountsToExport = getAllAccounts(node).filter(isNotInternal);
const constantsToExport = getAllConstants(node).filter(isNotInternal);
const instructionsToExport = getAllInstructionsWithSubs(node, {
leavesOnly: !renderScope.renderParentInstructions,
}).filter(isNotInternal);
Expand All @@ -174,6 +177,7 @@ export function getRenderMapVisitor(
const scope = {
...renderScope,
accountsToExport,
constantsToExport,
definedTypesToExport,
instructionsToExport,
pdasToExport,
Expand All @@ -183,6 +187,10 @@ export function getRenderMapVisitor(
return mergeRenderMaps([
createRenderMap({
['accounts/index.ts']: asPage(getIndexPageFragment(accountsToExport)),
['constants.ts']: asPage(
getConstantsPageFragment({ ...renderScope, nodes: constantsToExport }),
{ generatedTypes: './types' },
),
['errors/index.ts']: asPage(getIndexPageFragment(programsWithErrorsToExport)),
['index.ts']: asPage(getRootIndexPageFragment(scope)),
['instructions/index.ts']: asPage(getIndexPageFragment(instructionsToExport)),
Expand Down
76 changes: 76 additions & 0 deletions test/constantsPage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
constantNode,
definedTypeLinkNode,
definedTypeNode,
numberTypeNode,
numberValueNode,
programNode,
rootNode,
stringValueNode,
} from '@codama/nodes';
import { visit } from '@codama/visitors-core';
import { expect, test } from 'vitest';

import { getRenderMapVisitor } from '../src';
import { renderMapContains, renderMapContainsImports, renderMapDoesNotContainImports } from './_setup';

test('it renders program constants in a top-level constants page', async () => {
const node = rootNode(
programNode({
constants: [
constantNode('maxOption', numberTypeNode('u8'), numberValueNode(10), ['Maximum options.']),
constantNode('minOption', numberTypeNode('u8'), numberValueNode(1)),
constantNode('signedOption', numberTypeNode('i32'), numberValueNode(2)),
],
name: 'governance',
publicKey: 'GovER5Lthms3bLBqWub97yVrQm9WLZ7YgRrxYQYy2P',
}),
);

const renderMap = visit(node, getRenderMapVisitor());

await renderMapContains(renderMap, 'constants.ts', [
'/** Maximum options. */',
'export const MAX_OPTION: number = 10;',
'export const MIN_OPTION: number = 1;',
'export const SIGNED_OPTION: bigint = 2n;',
]);
await renderMapContains(renderMap, 'index.ts', "export * from './constants';");

expect(renderMap.has('constants/index.ts')).toBe(false);
expect(renderMap.size).toBeGreaterThan(0);
});

test('it imports linked types from the top-level types directory', async () => {
const node = rootNode(
programNode({
constants: [constantNode('optionCount', definedTypeLinkNode('optionCountType'), numberValueNode(10))],
definedTypes: [definedTypeNode({ name: 'optionCountType', type: numberTypeNode('u8') })],
name: 'governance',
publicKey: 'GovER5Lthms3bLBqWub97yVrQm9WLZ7YgRrxYQYy2P',
}),
);

const renderMap = visit(node, getRenderMapVisitor());

await renderMapContainsImports(renderMap, 'constants.ts', {
'./types': ['OptionCountType'],
});
});

test('it renders string constants without importing their declared type', async () => {
const node = rootNode(
programNode({
constants: [constantNode('abstainVoteIndex', definedTypeLinkNode('usize'), stringValueNode('0'))],
name: 'governance',
publicKey: 'GovER5Lthms3bLBqWub97yVrQm9WLZ7YgRrxYQYy2P',
}),
);

const renderMap = visit(node, getRenderMapVisitor());

await renderMapContains(renderMap, 'constants.ts', "export const ABSTAIN_VOTE_INDEX: string = '0';");
await renderMapDoesNotContainImports(renderMap, 'constants.ts', {
'./types': ['Usize'],
});
});