Api-Derives documentation generator (#6099)

* Initial Readme

* Initial Readme

* expand accounts derive docs

* parse and render derives documentation

* write to docs/derives

* Add code docs

* Lint

* Temporarily simplify derives Readme

* Remove unnecesary export

* Regenerate yarn.lock

* Improve Derives description
This commit is contained in:
Valentin Fernandez
2025-02-21 08:59:51 -05:00
committed by GitHub
parent 70cda770e1
commit 03fb086ac8
8 changed files with 206 additions and 3 deletions
+2
View File
@@ -30,6 +30,7 @@
"dependencies": {
"@polkadot/api": "15.6.2-1-x",
"@polkadot/api-augment": "15.6.2-1-x",
"@polkadot/api-derive": "15.6.2-1-x",
"@polkadot/rpc-augment": "15.6.2-1-x",
"@polkadot/rpc-provider": "15.6.2-1-x",
"@polkadot/types": "15.6.2-1-x",
@@ -40,6 +41,7 @@
"@polkadot/util": "^13.4.3",
"@polkadot/util-crypto": "^13.4.3",
"@polkadot/x-ws": "^13.4.3",
"comment-parser": "^1.4.1",
"handlebars": "^4.7.8",
"tslib": "^2.8.1",
"yargs": "^17.7.2"
+183
View File
@@ -7,12 +7,14 @@ import type { Text } from '@polkadot/types/primitive';
import type { Codec, DefinitionCall, DefinitionRpcParam, DefinitionsCall, Registry } from '@polkadot/types/types';
import type { HexString } from '@polkadot/util/types';
import { parse, type Spec } from 'comment-parser';
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { derive } from '@polkadot/api-derive';
import { Metadata, TypeRegistry, Vec } from '@polkadot/types';
import * as definitions from '@polkadot/types/interfaces/definitions';
import { getStorage as getSubstrateStorage } from '@polkadot/types/metadata/decorate/storage/getStorage';
@@ -62,6 +64,20 @@ interface StaticDef {
ver?: { apis: ApiDef[] }
}
interface Derive {
name: string | null;
description: string | null;
params: DeriveParam[];
returns: string | null;
example: string | null;
}
interface DeriveParam {
description: string | null;
name: string | null;
type: string | null;
}
const headerFn = (runtimeDesc: string) => `\n\n(NOTE: These were generated from a static/snapshot view of a recent ${runtimeDesc}. Some items may not be available in older nodes, or in any customized implementations.)`;
const ALL_STATIC: Record<string, StaticDef> = {
@@ -547,6 +563,169 @@ function addErrors (runtimeDesc: string, { lookup, pallets }: MetadataLatest): s
});
}
const BASE_DERIVE_PATH = '../api/packages/api-derive/src/';
// It finds all typescript file paths withing a given derive module.
const obtainDeriveFiles = (deriveModule: string) => {
const filePath = `${BASE_DERIVE_PATH}${deriveModule}`;
const files = fs.readdirSync(filePath);
return files
.filter((file) => file.endsWith('.ts') && !file.endsWith('.d.ts'))
.map((file) => `${deriveModule}/${file}`);
};
function extractDeriveDescription (tags: Spec[], name: string) {
const descriptionTag = tags.find((tag) => tag.tag === name);
return descriptionTag
? `${descriptionTag.name ?? ''} ${descriptionTag.description ?? ''}`.trim()
: null;
}
function extractDeriveParams (tags: Spec[]) {
const descriptionTag = tags
.filter((tag) => tag.tag === 'param')
.map((param) => {
return {
description: param.description ?? null,
name: param.name ?? null,
type: param.type ?? null
};
});
return descriptionTag;
}
function extractDeriveExample (tags: Spec[]) {
const exampleTag = tags.find((tag) => tag.tag === 'example');
if (!exampleTag) {
return null;
}
let example = '';
const inCodeBlock = { done: false, found: false };
// / Obtain code block from example tag.
exampleTag.source.forEach((line) => {
if (inCodeBlock.done) {
return;
}
if (line.source.indexOf('```') !== -1 && !inCodeBlock.found) {
inCodeBlock.found = true;
} else if (line.source.indexOf('```') !== -1 && inCodeBlock.found) {
inCodeBlock.done = true;
}
if (!inCodeBlock.found) {
return;
}
example += line.source.slice(2, line.source.length);
if (!inCodeBlock.done) {
example += '\n';
}
});
return example;
}
// Parses the comments of a given derive file and adds the
// relevant information (name, description, params, returns, example).
const getDeriveDocs = (
metadata: Record<string, Derive[]>,
file: string
) => {
const filePath = `${BASE_DERIVE_PATH}${file}`;
const deriveModule = file.split('/')[0];
const fileContent = fs.readFileSync(filePath, 'utf8');
const comments = parse(fileContent);
const docs: Derive[] = comments
.filter((comment) => comment.tags)
.map((comment) => {
return {
description: extractDeriveDescription(comment.tags, 'description'),
example: extractDeriveExample(comment.tags),
name: comment.tags.find((tag) => tag.tag === 'name')?.name || null,
params: extractDeriveParams(comment.tags),
returns: extractDeriveDescription(comment.tags, 'returns')
};
});
metadata[deriveModule]
? (metadata[deriveModule] = [...metadata[deriveModule], ...docs])
: (metadata[deriveModule] = [...docs]);
};
function renderDerives (metadata: Record<string, Derive[]>) {
let md = '---\ntitle: Derives\n---\n\nThis page lists the derives that can be encountered in the different modules. Designed to simplify the process of querying complex on-chain data by combining multiple RPC calls, storage queries, and runtime logic into a single, callable function. \n\nInstead of manually fetching and processing blockchain data, developers can use `api.derive.<module>.<method>()` to retrieve information.\n\n';
const deriveModules = Object.keys(metadata).filter(
(d) => metadata[d].length !== 0
);
// index
deriveModules.forEach((deriveModule) => {
md += `- **[${deriveModule}](#${deriveModule})**\n\n`;
});
// contents
deriveModules.forEach((deriveModule) => {
md += `\n___\n## ${deriveModule}\n`;
metadata[deriveModule]
.filter((item) => item.name)
.forEach((item) => {
const { description, example, name, params, returns } = item;
md += ` \n### [${name}](#${name})`;
if (description) {
md += `\n${description}`;
}
md += `\n- **interface**: \`api.derive.${deriveModule}.${name}\``;
if (params.length) {
md += '\n- **params**:\n';
params.forEach(
(param) =>
(md += ` - ${param.name} \`${param.type}\`: ${param.description}`)
);
}
if (returns) {
md += `\n- **returns**: ${returns}`;
}
if (example) {
md += `\n- **example**: \n${example}`;
}
});
});
return md;
}
function generateDerives () {
let fileList: string[] = [];
Object.keys(derive).forEach((deriveModule) => {
fileList = [...fileList, ...obtainDeriveFiles(deriveModule)];
});
const metadata = {};
fileList.forEach((file) => {
getDeriveDocs(metadata, file);
});
return renderDerives(metadata);
}
/** @internal */
function writeFile (name: string, ...chunks: any[]): void {
const writeStream = fs.createWriteStream(name, { encoding: 'utf8', flags: 'w' });
@@ -642,6 +821,10 @@ async function mainPromise (): Promise<void> {
writeFile(`${docRoot}/extrinsics.md`, addExtrinsics(runtimeDesc, latest));
writeFile(`${docRoot}/events.md`, addEvents(runtimeDesc, latest));
writeFile(`${docRoot}/errors.md`, addErrors(runtimeDesc, latest));
if (chainName === 'Substrate') {
writeFile('docs/derives/derives.md', generateDerives());
}
}
export function main (): void {
+1
View File
@@ -16,6 +16,7 @@
"references": [
{ "path": "../api/tsconfig.build.json" },
{ "path": "../api-augment/tsconfig.build.json" },
{ "path": "../api-derive/tsconfig.build.json" },
{ "path": "../rpc-augment/tsconfig.build.json" },
{ "path": "../rpc-provider/tsconfig.build.json" },
{ "path": "../types/tsconfig.build.json" },