Skip to content

Commit bce00d1

Browse files
committed
Add Codama CLI
1 parent 73e21de commit bce00d1

File tree

42 files changed

+985
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+985
-9
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@codama/renderers-vixen-parser': patch
3+
'@codama/renderers-js-umi': patch
4+
'@codama/renderers-rust': patch
5+
'@codama/renderers-js': patch
6+
---
7+
8+
Export `renderVisitor` function of all renderers packages as `default` export.

.changeset/poor-crabs-act.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'codama': patch
3+
'@codama/cli': patch
4+
---
5+
6+
Add new Codama CLI

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default tseslint.config([
88
extends: [solanaConfig],
99
},
1010
{
11-
files: ['packages/nodes/**', 'packages/node-types/**'],
11+
files: ['packages/cli/**', 'packages/nodes/**', 'packages/node-types/**'],
1212
rules: {
1313
'sort-keys-fix/sort-keys-fix': 'off',
1414
'typescript-sort-keys/interface': 'off',

packages/cli/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

packages/cli/.prettierignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
e2e/
3+
test-ledger/
4+
target/
5+
CHANGELOG.md

packages/cli/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Codama
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

packages/cli/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Codama ➤ CLI
2+
3+
[![npm][npm-image]][npm-url]
4+
[![npm-downloads][npm-downloads-image]][npm-url]
5+
6+
[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/cli.svg?style=flat
7+
[npm-image]: https://img.shields.io/npm/v/@codama/cli.svg?style=flat&label=%40codama%2Fcli
8+
[npm-url]: https://www.npmjs.com/package/@codama/cli
9+
10+
This package provides a CLI for the Codama library that can be used to run scripts on Codama IDLs.
11+
12+
Note that, whilst the CLI code is located in the `@codama/cli` package, the CLI binary is directly provided by the main `codama` library.
13+
14+
## Getting started
15+
16+
To get started with Codama, simply install `codama` to your project and run the `init` command like so:
17+
18+
```sh
19+
pnpm install codama
20+
codama init
21+
```
22+
23+
You will be prompted for the path of your IDL and asked to select any script presets you would like to use.
24+
25+
## `codama run`
26+
27+
Once you have your codama config file, you can run your Codama scripts using the `codama run` command as follows:
28+
29+
```sh
30+
codama run # Only runs your global visitors.
31+
codama run js rust # Runs your global visitors followed by the `js` and `rust` scripts.
32+
codama run --all # Runs your global visitors followed by all your scripts.
33+
```
34+
35+
## The configuration file
36+
37+
The codama config file defines an object containing the following fields:
38+
39+
- `idl` (string): The path to the IDL file. This can be a Codama IDL or an Anchor IDL which will be automatically converted to a Codama IDL.
40+
- `visitors` (array): An array of visitors that will run before every script.
41+
- `scripts` (object): An object defining the available Codama scripts. The keys identify the scripts and the values are arrays of visitors that make up the script.
42+
43+
Whether it is in the `visitors` array or in the `scripts` values, when defining a visitor you may either provide:
44+
45+
- an object with the following fields:
46+
- `path` (string): The path to the visitor.
47+
- `args` (array): An array of arguments to pass to the visitor.
48+
- a string: The path to the visitor. This is equivalent to providing an object with a `path` field and an empty `args` array.
49+
50+
Visitor paths can either be local paths (pointing to JavaScript files exporting visitors) or npm package names. By default, the `default` export will be used but you may specify a nammed export by appending a `#` followed by the export name. When resolved, the imported element inside the module should either be a `Visitor<any, 'rootNode'>` or a function that returns a `Visitor<any, 'rootNode'>` given the arguments provided. Here are some examples of valid visitor paths:
51+
52+
```js
53+
'./my-visitor.js'; // Relative local path to a visitor module.
54+
'/Users/me/my-visitor.js'; // Absolute local path to a visitor module.
55+
'some-library'; // npm package name.
56+
'@acme/some-library'; // Scoped npm package name.
57+
'./my-visitor.js#myExport'; // Named export from a local path.
58+
'@acme/some-library#myExport'; // Named export from an npm package.
59+
```
60+
61+
Here is an example of what a Codama configuration file might look like:
62+
63+
```json
64+
{
65+
"idl": "path/to/idl",
66+
"visitors": [
67+
"./my-global-visitor.js",
68+
{ "path": "some-library#removeTypes", "args": [["internalFoo", "internalBar"]] }
69+
],
70+
"scripts": {
71+
"js": [
72+
{
73+
"path": "@codama/renderers-js",
74+
"args": ["clients/js/src/generated"]
75+
}
76+
]
77+
}
78+
}
79+
```
80+
81+
Note that you can use the `--js` flag to generate a `.js` configuration file when running the `init` command.

packages/cli/package.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"name": "@codama/cli",
3+
"version": "1.0.0",
4+
"description": "The package that provides a CLI for the Codama standard",
5+
"exports": {
6+
"types": "./dist/types/index.d.ts",
7+
"node": {
8+
"import": "./dist/index.node.mjs",
9+
"require": "./dist/index.node.cjs"
10+
}
11+
},
12+
"main": "./dist/index.node.cjs",
13+
"module": "./dist/index.node.mjs",
14+
"types": "./dist/types/index.d.ts",
15+
"type": "commonjs",
16+
"files": [
17+
"./dist/types",
18+
"./dist/index.*"
19+
],
20+
"sideEffects": false,
21+
"keywords": [
22+
"codama",
23+
"standard",
24+
"cli"
25+
],
26+
"scripts": {
27+
"build": "rimraf dist && pnpm build:src && pnpm build:types",
28+
"build:src": "zx ../../node_modules/@codama/internals/scripts/build-src.mjs node",
29+
"build:types": "zx ../../node_modules/@codama/internals/scripts/build-types.mjs",
30+
"dev": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node --watch",
31+
"lint": "zx ../../node_modules/@codama/internals/scripts/lint.mjs",
32+
"lint:fix": "zx ../../node_modules/@codama/internals/scripts/lint.mjs --fix",
33+
"test": "pnpm test:types && pnpm test:treeshakability && pnpm test:node",
34+
"test:node": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node",
35+
"test:treeshakability": "zx ../../node_modules/@codama/internals/scripts/test-treeshakability.mjs",
36+
"test:types": "zx ../../node_modules/@codama/internals/scripts/test-types.mjs"
37+
},
38+
"dependencies": {
39+
"@codama/nodes": "workspace:*",
40+
"@codama/nodes-from-anchor": "workspace:*",
41+
"@codama/renderers": "workspace:*",
42+
"@codama/renderers-js": "workspace:*",
43+
"@codama/renderers-js-umi": "workspace:*",
44+
"@codama/renderers-rust": "workspace:*",
45+
"@codama/visitors-core": "workspace:*",
46+
"chalk": "^5.4.1",
47+
"commander": "^13.1.0",
48+
"prompts": "^2.4.2"
49+
},
50+
"devDependencies": {
51+
"@types/prompts": "^2.4.9"
52+
},
53+
"license": "MIT",
54+
"repository": {
55+
"type": "git",
56+
"url": "https://github.com/codama-idl/codama"
57+
},
58+
"bugs": {
59+
"url": "http://github.com/codama-idl/codama/issues"
60+
},
61+
"browserslist": [
62+
"supports bigint and not dead",
63+
"maintained node versions"
64+
]
65+
}

packages/cli/src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './init';
2+
export * from './run';

packages/cli/src/commands/init.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { Command } from 'commander';
2+
import prompts, { PromptType } from 'prompts';
3+
4+
import { canRead, logBanner, logSuccess, resolveRelativePath, writeFile } from '../utils';
5+
6+
export function setInitCommand(program: Command): void {
7+
program
8+
.command('init')
9+
.argument('[output]', 'Optional path used to output the configuration file')
10+
.option('-d, --default', 'Bypass prompts and select all defaults options')
11+
.option('--js', 'Forces the output to be a JavaScript file')
12+
.action(doInit);
13+
}
14+
15+
type InitOptions = {
16+
default?: boolean;
17+
js?: boolean;
18+
};
19+
20+
async function doInit(explicitOutput: string | undefined, options: InitOptions) {
21+
const output = getOutputPath(explicitOutput, options);
22+
const useJsFile = options.js || output.endsWith('.js');
23+
if (await canRead(output)) {
24+
throw new Error(`Configuration file already exists at "${output}".`);
25+
}
26+
27+
logBanner();
28+
const result = await getPromptResult(options);
29+
const content = getContentFromPromptResult(result, useJsFile);
30+
await writeFile(output, content);
31+
logSuccess(`Configuration file created at "${output}".`);
32+
}
33+
34+
function getOutputPath(explicitOutput: string | undefined, options: Pick<InitOptions, 'js'>): string {
35+
if (explicitOutput) {
36+
return resolveRelativePath(explicitOutput);
37+
}
38+
return resolveRelativePath(options.js ? 'codama.js' : 'codama.json');
39+
}
40+
41+
type PromptResult = {
42+
idlPath: string;
43+
jsPath?: string;
44+
rustCrate?: string;
45+
rustPath?: string;
46+
scripts: string[];
47+
};
48+
49+
async function getPromptResult(options: Pick<InitOptions, 'default'>): Promise<PromptResult> {
50+
const defaults = getDefaultPromptResult();
51+
if (options.default) {
52+
return defaults;
53+
}
54+
55+
const hasScript =
56+
(script: string, type: PromptType = 'text') =>
57+
(_: unknown, values: { scripts: string[] }) =>
58+
values.scripts.includes(script) ? type : null;
59+
const result: PromptResult = await prompts(
60+
[
61+
{
62+
initial: defaults.idlPath,
63+
message: 'Where is your IDL located? (Supports Codama and Anchor IDLs).',
64+
name: 'idlPath',
65+
type: 'text',
66+
},
67+
{
68+
choices: [
69+
{ selected: true, title: 'Generate JavaScript client', value: 'js' },
70+
{ selected: true, title: 'Generate Rust client', value: 'rust' },
71+
],
72+
instructions: '[space] to toggle / [a] to toggle all / [enter] to submit',
73+
message: 'Which script preset would you like to use?',
74+
name: 'scripts',
75+
type: 'multiselect',
76+
},
77+
{
78+
initial: defaults.jsPath,
79+
message: '[js] Where should the JavaScript code be generated?',
80+
name: 'jsPath',
81+
type: hasScript('js'),
82+
},
83+
{
84+
initial: defaults.rustCrate,
85+
message: '[rust] Where is the Rust client crate located?',
86+
name: 'rustCrate',
87+
type: hasScript('rust'),
88+
},
89+
{
90+
initial: (prev: string) => `${prev}/src/generated`,
91+
message: '[rust] Where should the Rust code be generated?',
92+
name: 'rustPath',
93+
type: hasScript('rust'),
94+
},
95+
],
96+
{
97+
onCancel: () => {
98+
throw new Error('Operation cancelled.');
99+
},
100+
},
101+
);
102+
103+
return result;
104+
}
105+
106+
function getDefaultPromptResult(): PromptResult {
107+
return {
108+
idlPath: 'program/idl.json',
109+
jsPath: 'clients/js/src/generated',
110+
rustCrate: 'clients/rust',
111+
rustPath: 'clients/rust/src/generated',
112+
scripts: ['js', 'rust'],
113+
};
114+
}
115+
116+
function getContentFromPromptResult(result: PromptResult, useJsFile: boolean): string {
117+
const scripts: Record<string, unknown> = {};
118+
if (result.scripts.includes('js')) {
119+
scripts.js = {
120+
path: '@codama/renderers-js',
121+
args: [result.jsPath],
122+
};
123+
}
124+
if (result.scripts.includes('rust')) {
125+
scripts.rust = {
126+
path: '@codama/renderers-rust',
127+
args: [result.rustPath, { crateFolder: result.rustCrate, formatCode: true }],
128+
};
129+
}
130+
const content = { idl: result.idlPath, visitors: [], scripts };
131+
132+
if (!useJsFile) {
133+
return JSON.stringify(content, null, 4);
134+
}
135+
136+
return (
137+
'export default ' +
138+
JSON.stringify(content, null, 4)
139+
// Remove quotes around property names
140+
.replace(/"([^"]+)":/g, '$1:')
141+
// Convert double-quoted strings to single quotes
142+
.replace(/"([^"]*)"/g, "'$1'")
143+
);
144+
}

0 commit comments

Comments
 (0)