Skip to content

Commit

Permalink
feat: impl first pass new cli
Browse files Browse the repository at this point in the history
  • Loading branch information
grikomsn committed Jun 17, 2023
1 parent 4435fd2 commit a79cde6
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 1 deletion.
6 changes: 6 additions & 0 deletions packages/graz/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
type KeplrWindow = import("@keplr-wallet/types").Window;

declare namespace NodeJS {
interface ProcessEnv {
readonly GRAZ_REGISTRY_SRC?: string;
}
}

declare interface Window extends KeplrWindow {
leap: KeplrWindow["keplr"];
cosmostation: {
Expand Down
10 changes: 10 additions & 0 deletions packages/graz/src/cli/clone-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import path from "node:path";

import tiged from "tiged";

export const cloneRegistry = async (src?: string) => {
// eslint-disable-next-line no-param-reassign
src = src || process.env.GRAZ_REGISTRY_SRC || "github:cosmos/chain-registry";
const emitter = tiged(src, { force: true, mode: "tar" });
await emitter.clone(path.resolve(__dirname, "../../registry"));
};
24 changes: 24 additions & 0 deletions packages/graz/src/cli/get-chain-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import path from "node:path";

import type { Options as GlobbyOptions } from "globby";
import { globby } from "globby";

export const getChainPaths = async ({
mainnetFilter: mf,
testnetFilter: tf,
}: {
mainnetFilter?: string[];
testnetFilter?: string[];
} = {}) => {
const globOpts: GlobbyOptions = {
cwd: path.resolve(__dirname, "../../registry"),
onlyDirectories: true,
};

const [mainnetPaths, testnetPaths] = await Promise.all([
globby([...(mf && mf.length > 0 ? mf : ["*"]), "!_*", "!testnets"], globOpts),
globby([...(tf && tf.length > 0 ? tf.map((f) => `testnets/${f}`) : ["testnets/*"]), "!testnets/_*"], globOpts),
]);

return { mainnetPaths, testnetPaths };
};
74 changes: 73 additions & 1 deletion packages/graz/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,73 @@
// TODO
import fs from "node:fs/promises";
import os from "node:os";

import * as p from "@clack/prompts";
import { Command } from "commander";
import pMap from "p-map";

import { cloneRegistry } from "./clone-registry";
import { getChainPaths } from "./get-chain-paths";
import { makeRootSources } from "./make-root-sources";
import { makeSources } from "./make-sources";

const cli = async () => {
const program = new Command();

program
.name("graz")
.description("React hooks for Cosmos")
.addHelpText("afterAll", "\nhttps://github.com/strangelove-ventures/graz\n");

program
.command("generate")
.description('generate typescript chain definitions and export to "graz/chains"')
.option(
"-R, --registry <url>",
"specify a custom chain registry namespace (e.g. org/repo, github:org/repo, gitlab:org/repo)",
)
.option(
"-M, --mainnet <chainPaths...>",
'generate given mainnet chain paths separated by spaces (e.g. "axelar cosmoshub juno")',
)
.option(
"-T, --testnet <chainPaths...>",
'generate given testnet chain paths separated by spaces (e.g. "atlantic bitcannadev cheqdtestnet")',
)
.action(async (options) => {
const customRegistry = options.registry as string | undefined;
const mainnetFilter = options.mainnet as string[] | undefined;
const testnetFilter = options.testnet as string[] | undefined;

p.intro("graz generate");
const s = p.spinner();

// p.log.step("Cloning chain registry...");
s.start(`Cloning chain registry`);
await cloneRegistry(customRegistry);
s.stop("Cloned chain registry ✅");

// p.log.step("Retrieving chain paths...");
s.start("Retrieving chain paths");
const { mainnetPaths, testnetPaths } = await getChainPaths({ mainnetFilter, testnetFilter });
s.stop("Retrieved chain paths ✅");

// p.log.step("Generating chain sources...");
s.start("Generating chain sources");
await fs.rm("chains/", { recursive: true, force: true });
await pMap([...mainnetPaths, ...testnetPaths], makeSources, {
concurrency: Math.max(1, (os.cpus() || { length: 1 }).length - 1),
});
s.stop("Generated chain sources ✅");

// p.log.step("Generating chain index...");
s.start("Generating chain index");
await makeRootSources({ mainnetPaths, testnetPaths });
s.stop("Generated chain index ✅");

p.outro('Generate complete! You can import `mainnetChains` and `testnetChains` from "graz/chains". 🎉');
});

await program.parseAsync();
};

void cli();
103 changes: 103 additions & 0 deletions packages/graz/src/cli/make-root-sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import fs from "node:fs/promises";
import path from "node:path";

import { CodeGenerator } from "@babel/generator";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import * as t from "@babel/types";

export const makeRootSources = async ({
mainnetPaths,
testnetPaths,
}: {
mainnetPaths: string[];
testnetPaths: string[];
}) => {
// eslint-disable-next-line no-param-reassign
testnetPaths = testnetPaths.map((p) => p.replace("testnets/", ""));

const chainsGeneratedStub = await fs.readFile(
path.resolve(__dirname, "../../stubs/chains-generated.ts.stub"),
"utf-8",
);
const chainsGeneratedAst = parse(chainsGeneratedStub, { sourceType: "module", plugins: ["typescript"] });

const mainnetAstKeyvals = mainnetPaths.map((chainPath) => {
return t.objectMethod(
"get",
t.stringLiteral(chainPath),
[],
t.blockStatement([
t.returnStatement(
t.memberExpression(
t.callExpression(t.identifier("require"), [t.stringLiteral(`./${chainPath}`)]),
t.identifier("default"),
),
),
]),
);
});

const testnetAstKeyvals = testnetPaths.map((chainPath) => {
return t.objectMethod(
"get",
t.stringLiteral(chainPath),
[],
t.blockStatement([
t.returnStatement(
t.memberExpression(
t.callExpression(t.identifier("require"), [t.stringLiteral(`./${chainPath}`)]),
t.identifier("default"),
),
),
]),
);
});

traverse(chainsGeneratedAst, {
TSTypeAliasDeclaration: (current) => {
if (t.isIdentifier(current.node.id, { name: "MainnetChainName" })) {
current.node.typeAnnotation = t.tsUnionType(mainnetPaths.map((p) => t.tsLiteralType(t.stringLiteral(p))));
current.skip();
}
if (t.isIdentifier(current.node.id, { name: "TestnetChainName" })) {
current.node.typeAnnotation = t.tsUnionType(testnetPaths.map((p) => t.tsLiteralType(t.stringLiteral(p))));
current.skip();
}
},
VariableDeclarator: (current) => {
if (t.isIdentifier(current.node.id, { name: "mainnetChains" })) {
current.node.init = t.objectExpression(mainnetAstKeyvals.sort());
current.skip();
}
if (t.isIdentifier(current.node.id, { name: "testnetChains" })) {
current.node.init = t.objectExpression(testnetAstKeyvals.sort());
current.skip();
}
if (t.isIdentifier(current.node.id, { name: "chains" })) {
current.node.init = t.objectExpression([...mainnetAstKeyvals, ...testnetAstKeyvals].sort());
current.skip();
}
if (t.isIdentifier(current.node.id, { name: "mainnetChainNames" })) {
current.node.init = t.arrayExpression(mainnetPaths.map((p) => t.stringLiteral(p)));
current.skip();
}
if (t.isIdentifier(current.node.id, { name: "testnetChainNames" })) {
current.node.init = t.arrayExpression(testnetPaths.map((p) => t.stringLiteral(p)));
current.skip();
}
if (t.isIdentifier(current.node.id, { name: "chainNames" })) {
current.node.init = t.arrayExpression([...mainnetPaths, ...testnetPaths].map((p) => t.stringLiteral(p)));
current.skip();
}
},
});

const { code: chainsGeneratedCode } = new CodeGenerator(chainsGeneratedAst).generate();
await fs.writeFile(path.resolve(__dirname, "../../chains/generated.ts"), chainsGeneratedCode, "utf-8");

await fs.copyFile(
path.resolve(__dirname, "../../stubs/chains-index.ts.stub"),
path.resolve(__dirname, "../../chains/index.ts"),
);
};
114 changes: 114 additions & 0 deletions packages/graz/src/cli/make-sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import fs from "node:fs/promises";
import path from "node:path";

import { CodeGenerator } from "@babel/generator";
import * as t from "@babel/types";
import type { ChainInfo } from "@keplr-wallet/types";

import { registryToChainInfo } from "../registry/keplr";
import type { AssetList, Chain } from "../types/registry";

export const makeSources = async (chainPath: string) => {
const actualChainPath = chainPath.replace("testnets/", "");

await fs.mkdir(path.resolve(__dirname, `../../chains/${actualChainPath}`), { recursive: true });

let assetlist: AssetList;
try {
const assetlistContent = await fs.readFile(
path.resolve(__dirname, `../../registry/${chainPath}/assetlist.json`),
"utf-8",
);
assetlist = JSON.parse(assetlistContent) as AssetList;
} catch {
assetlist = {
assets: [],
chain_name: chainPath,
};
}

/**
* chains/[chainPath]/assetlist.ts
* ```js
* import { defineAssetList } from "../../dist";
* export default defineAssetList({ ... });
* ```
*/
const assetlistAst = t.program([
t.importDeclaration(
[t.importSpecifier(t.identifier("defineAssetList"), t.identifier("defineAssetList"))],
t.stringLiteral("../../dist"),
),
t.exportDefaultDeclaration(t.callExpression(t.identifier("defineAssetList"), [t.valueToNode(assetlist)])),
]);

const { code: assetlistCode } = new CodeGenerator(assetlistAst).generate();
await fs.writeFile(
path.resolve(__dirname, `../../chains/${actualChainPath}/assetlist.ts`),
`/* eslint-disable */\n${assetlistCode}`,
"utf-8",
);

const chainContent = await fs.readFile(path.resolve(__dirname, `../../registry/${chainPath}/chain.json`), "utf-8");
const chain = JSON.parse(chainContent) as Chain;

/**
* chains/[chainPath]/chain.ts
* ```js
* import { defineRegistryChain } from "../../dist";
* export default defineRegistryChain({ ... });
* ```
*/
const chainAst = t.program([
t.importDeclaration(
[t.importSpecifier(t.identifier("defineRegistryChain"), t.identifier("defineRegistryChain"))],
t.stringLiteral("../../dist"),
),
t.exportDefaultDeclaration(t.callExpression(t.identifier("defineRegistryChain"), [t.valueToNode(chain)])),
]);

const { code: chainCode } = new CodeGenerator(chainAst).generate();
await fs.writeFile(
path.resolve(__dirname, `../../chains/${actualChainPath}/chain.ts`),
`/* eslint-disable */\n${chainCode}`,
"utf-8",
);

let chainInfo: ChainInfo | undefined;
if (assetlist.assets.length > 0) {
chainInfo = registryToChainInfo({ assetlist, chain });
}

/**
* chains/[chainPath]/index.ts
* ```js
* import { defineChainInfo } from "../../dist";
* export default defineChainInfo({ ... });
* ```
*/
const indexAst = t.program(
chainInfo
? [
t.importDeclaration(
[t.importSpecifier(t.identifier("defineChainInfo"), t.identifier("defineChainInfo"))],
t.stringLiteral("../../dist"),
),
t.exportDefaultDeclaration(t.callExpression(t.identifier("defineChainInfo"), [t.valueToNode(chainInfo)])),
]
: [
t.expressionStatement(
t.callExpression(t.memberExpression(t.identifier("console"), t.identifier("error")), [
t.stringLiteral(`chain info for '${chain.chain_name}' is not generated due to invalid assetlist`),
]),
),
t.exportDefaultDeclaration(t.objectExpression([])),
],
);

const { code: indexCode } = new CodeGenerator(indexAst).generate();
await fs.writeFile(
path.resolve(__dirname, `../../chains/${actualChainPath}/index.ts`),
`/* eslint-disable */\n${indexCode}`,
"utf-8",
);
};
20 changes: 20 additions & 0 deletions packages/graz/stubs/chains-generated.ts.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ChainInfo } from "@keplr-wallet/types";

import type { AssetList, Chain } from "../dist";

export type MainnetChainName = "REPLACE_MAINNET_CHAIN_NAME";
export type TestnetChainName = "REPLACE_TESTNET_CHAIN_NAME";
export type ChainName = MainnetChainName | TestnetChainName;
export interface ChainData {
assetlist: Promise<AssetList>;
chain: Promise<Chain>;
chainInfo: Promise<ChainInfo>;
}
export type ReturnTuple<T> = T extends readonly [ChainName, ...infer Rest] ? [ChainData, ...ReturnTuple<Rest>] : [];

export const mainnetChains: Record<MainnetChainName, Chain> = {};
export const testnetChains: Record<TestnetChainName, Chain> = {};
export const chains: Record<ChainName, Chain> = {};
export const mainnetChainNames: MainnetChainName[] = [];
export const testnetChainNames: TestnetChainName[] = [];
export const chainNames: ChainName[] = [];
Loading

0 comments on commit a79cde6

Please sign in to comment.