Skip to content

Commit 3e80075

Browse files
committed
Add Codama CLI
1 parent 9d6b686 commit 3e80075

File tree

42 files changed

+990
-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

+990
-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 before visitors.
31+
codama run js rust # Runs your before visitors followed by the `js` and `rust` scripts.
32+
codama run --all # Runs your before 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+
- `before` (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 `before` array or in the `scripts` values, when defining a visitor you may either provide:
44+
45+
- an object with the following fields:
46+
- `from` (string): The import path to the visitor.
47+
- `args` (array): An array of arguments to pass to the visitor.
48+
- a string: The import path to the visitor. This is equivalent to providing an object with a `from` field and an empty `args` array.
49+
50+
Visitor import 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 named 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 import 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+
"before": [
67+
"./my-before-visitor.js",
68+
{ "from": "some-library#removeTypes", "args": [["internalFoo", "internalBar"]] }
69+
],
70+
"scripts": {
71+
"js": [
72+
{
73+
"from": "@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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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": "workspace:*",
46+
"@codama/visitors-core": "workspace:*",
47+
"chalk": "^5.4.1",
48+
"commander": "^13.1.0",
49+
"prompts": "^2.4.2"
50+
},
51+
"devDependencies": {
52+
"@types/prompts": "^2.4.9"
53+
},
54+
"license": "MIT",
55+
"repository": {
56+
"type": "git",
57+
"url": "https://github.com/codama-idl/codama"
58+
},
59+
"bugs": {
60+
"url": "http://github.com/codama-idl/codama/issues"
61+
},
62+
"browserslist": [
63+
"supports bigint and not dead",
64+
"maintained node versions"
65+
]
66+
}

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

0 commit comments

Comments
 (0)