Skip to content

Commit

Permalink
Add Codama CLI (#425)
Browse files Browse the repository at this point in the history
This PR adds a brand new `@codama/cli` package which is then imported (**not** re-exported) by the main `codama` library in order to use the `codama` keyword as CLI binary.

The CLI currently supports the following two commands:
- `codama init`: which initialises a Codama configuration file by using prompts and selected script presets.
- `codama run`: which runs any script defined in your configuration file.

You can learn more about the CLI commands and the Codama config file from the `@codama/cli` README copy/pasted below:

---


This package provides a CLI for the Codama library that can be used to run scripts on Codama IDLs.

Note that, whilst the CLI code is located in the `@codama/cli` package, the CLI binary is directly provided by the main `codama` library.

## Getting started

To get started with Codama, simply install `codama` to your project and run the `init` command like so:

```sh
pnpm install codama
codama init
```

You will be prompted for the path of your IDL and asked to select any script presets you would like to use.

## `codama run`

Once you have your codama config file, you can run your Codama scripts using the `codama run` command as follows:

```sh
codama run         # Only runs your before visitors.
codama run js rust # Runs your before visitors followed by the `js` and `rust` scripts.
codama run --all   # Runs your before visitors followed by all your scripts.
```

## The configuration file

The codama config file defines an object containing the following fields:

- `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.
- `before` (array): An array of visitors that will run before every script.
- `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.

Whether it is in the `before` array or in the `scripts` values, when defining a visitor you may either provide:

- an object with the following fields:
    - `from` (string): The import path to the visitor.
    - `args` (array): An array of arguments to pass to the visitor.
- a string: The import path to the visitor. This is equivalent to providing an object with a `from` field and an empty `args` array.

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 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 import paths:

```js
'./my-visitor.js'; // Relative local path to a visitor module.
'/Users/me/my-visitor.js'; // Absolute local path to a visitor module.
'some-library'; // npm package name.
'@acme/some-library'; // Scoped npm package name.
'./my-visitor.js#myExport'; // Named export from a local path.
'@acme/some-library#myExport'; // Named export from an npm package.
```

Here is an example of what a Codama configuration file might look like:

```json
{
    "idl": "path/to/idl",
    "before": [
        "./my-before-visitor.js",
        { "from": "some-library#removeTypes", "args": [["internalFoo", "internalBar"]] }
    ],
    "scripts": {
        "js": [
            {
                "from": "@codama/renderers-js",
                "args": ["clients/js/src/generated"]
            }
        ]
    }
}
```

Note that you can use the `--js` flag to generate a `.js` configuration file when running the `init` command.
  • Loading branch information
lorisleiva authored Jan 31, 2025
1 parent bc2e306 commit 7bb6920
Show file tree
Hide file tree
Showing 42 changed files with 990 additions and 9 deletions.
8 changes: 8 additions & 0 deletions .changeset/little-spiders-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@codama/renderers-vixen-parser': patch
'@codama/renderers-js-umi': patch
'@codama/renderers-rust': patch
'@codama/renderers-js': patch
---

Export `renderVisitor` function of all renderers packages as `default` export.
6 changes: 6 additions & 0 deletions .changeset/poor-crabs-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'codama': patch
'@codama/cli': patch
---

Add new Codama CLI
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default tseslint.config([
extends: [solanaConfig],
},
{
files: ['packages/nodes/**', 'packages/node-types/**'],
files: ['packages/cli/**', 'packages/nodes/**', 'packages/node-types/**'],
rules: {
'sort-keys-fix/sort-keys-fix': 'off',
'typescript-sort-keys/interface': 'off',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
5 changes: 5 additions & 0 deletions packages/cli/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist/
e2e/
test-ledger/
target/
CHANGELOG.md
22 changes: 22 additions & 0 deletions packages/cli/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2024 Codama

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
81 changes: 81 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Codama ➤ CLI

[![npm][npm-image]][npm-url]
[![npm-downloads][npm-downloads-image]][npm-url]

[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/cli.svg?style=flat
[npm-image]: https://img.shields.io/npm/v/@codama/cli.svg?style=flat&label=%40codama%2Fcli
[npm-url]: https://www.npmjs.com/package/@codama/cli

This package provides a CLI for the Codama library that can be used to run scripts on Codama IDLs.

Note that, whilst the CLI code is located in the `@codama/cli` package, the CLI binary is directly provided by the main `codama` library.

## Getting started

To get started with Codama, simply install `codama` to your project and run the `init` command like so:

```sh
pnpm install codama
codama init
```

You will be prompted for the path of your IDL and asked to select any script presets you would like to use.

## `codama run`

Once you have your codama config file, you can run your Codama scripts using the `codama run` command as follows:

```sh
codama run # Only runs your before visitors.
codama run js rust # Runs your before visitors followed by the `js` and `rust` scripts.
codama run --all # Runs your before visitors followed by all your scripts.
```

## The configuration file

The codama config file defines an object containing the following fields:

- `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.
- `before` (array): An array of visitors that will run before every script.
- `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.

Whether it is in the `before` array or in the `scripts` values, when defining a visitor you may either provide:

- an object with the following fields:
- `from` (string): The import path to the visitor.
- `args` (array): An array of arguments to pass to the visitor.
- a string: The import path to the visitor. This is equivalent to providing an object with a `from` field and an empty `args` array.

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:

```js
'./my-visitor.js'; // Relative local path to a visitor module.
'/Users/me/my-visitor.js'; // Absolute local path to a visitor module.
'some-library'; // npm package name.
'@acme/some-library'; // Scoped npm package name.
'./my-visitor.js#myExport'; // Named export from a local path.
'@acme/some-library#myExport'; // Named export from an npm package.
```

Here is an example of what a Codama configuration file might look like:

```json
{
"idl": "path/to/idl",
"before": [
"./my-before-visitor.js",
{ "from": "some-library#removeTypes", "args": [["internalFoo", "internalBar"]] }
],
"scripts": {
"js": [
{
"from": "@codama/renderers-js",
"args": ["clients/js/src/generated"]
}
]
}
}
```

Note that you can use the `--js` flag to generate a `.js` configuration file when running the `init` command.
66 changes: 66 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@codama/cli",
"version": "1.0.0",
"description": "The package that provides a CLI for the Codama standard",
"exports": {
"types": "./dist/types/index.d.ts",
"node": {
"import": "./dist/index.node.mjs",
"require": "./dist/index.node.cjs"
}
},
"main": "./dist/index.node.cjs",
"module": "./dist/index.node.mjs",
"types": "./dist/types/index.d.ts",
"type": "commonjs",
"files": [
"./dist/types",
"./dist/index.*"
],
"sideEffects": false,
"keywords": [
"codama",
"standard",
"cli"
],
"scripts": {
"build": "rimraf dist && pnpm build:src && pnpm build:types",
"build:src": "zx ../../node_modules/@codama/internals/scripts/build-src.mjs node",
"build:types": "zx ../../node_modules/@codama/internals/scripts/build-types.mjs",
"dev": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node --watch",
"lint": "zx ../../node_modules/@codama/internals/scripts/lint.mjs",
"lint:fix": "zx ../../node_modules/@codama/internals/scripts/lint.mjs --fix",
"test": "pnpm test:types && pnpm test:treeshakability && pnpm test:node",
"test:node": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node",
"test:treeshakability": "zx ../../node_modules/@codama/internals/scripts/test-treeshakability.mjs",
"test:types": "zx ../../node_modules/@codama/internals/scripts/test-types.mjs"
},
"dependencies": {
"@codama/nodes": "workspace:*",
"@codama/nodes-from-anchor": "workspace:*",
"@codama/renderers": "workspace:*",
"@codama/renderers-js": "workspace:*",
"@codama/renderers-js-umi": "workspace:*",
"@codama/renderers-rust": "workspace:*",
"@codama/visitors": "workspace:*",
"@codama/visitors-core": "workspace:*",
"chalk": "^5.4.1",
"commander": "^13.1.0",
"prompts": "^2.4.2"
},
"devDependencies": {
"@types/prompts": "^2.4.9"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/codama-idl/codama"
},
"bugs": {
"url": "http://github.com/codama-idl/codama/issues"
},
"browserslist": [
"supports bigint and not dead",
"maintained node versions"
]
}
2 changes: 2 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './init';
export * from './run';
145 changes: 145 additions & 0 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Command } from 'commander';
import prompts, { PromptType } from 'prompts';

import { Config, ScriptConfig, ScriptName } from '../config';
import { canRead, logBanner, logSuccess, resolveRelativePath, writeFile } from '../utils';

export function setInitCommand(program: Command): void {
program
.command('init')
.argument('[output]', 'Optional path used to output the configuration file')
.option('-d, --default', 'Bypass prompts and select all defaults options')
.option('--js', 'Forces the output to be a JavaScript file')
.action(doInit);
}

type InitOptions = {
default?: boolean;
js?: boolean;
};

async function doInit(explicitOutput: string | undefined, options: InitOptions) {
const output = getOutputPath(explicitOutput, options);
const useJsFile = options.js || output.endsWith('.js');
if (await canRead(output)) {
throw new Error(`Configuration file already exists at "${output}".`);
}

logBanner();
const result = await getPromptResult(options);
const content = getContentFromPromptResult(result, useJsFile);
await writeFile(output, content);
logSuccess(`Configuration file created at "${output}".`);
}

function getOutputPath(explicitOutput: string | undefined, options: Pick<InitOptions, 'js'>): string {
if (explicitOutput) {
return resolveRelativePath(explicitOutput);
}
return resolveRelativePath(options.js ? 'codama.js' : 'codama.json');
}

type PromptResult = {
idlPath: string;
jsPath?: string;
rustCrate?: string;
rustPath?: string;
scripts: string[];
};

async function getPromptResult(options: Pick<InitOptions, 'default'>): Promise<PromptResult> {
const defaults = getDefaultPromptResult();
if (options.default) {
return defaults;
}

const hasScript =
(script: string, type: PromptType = 'text') =>
(_: unknown, values: { scripts: string[] }) =>
values.scripts.includes(script) ? type : null;
const result: PromptResult = await prompts(
[
{
initial: defaults.idlPath,
message: 'Where is your IDL located? (Supports Codama and Anchor IDLs).',
name: 'idlPath',
type: 'text',
},
{
choices: [
{ selected: true, title: 'Generate JavaScript client', value: 'js' },
{ selected: true, title: 'Generate Rust client', value: 'rust' },
],
instructions: '[space] to toggle / [a] to toggle all / [enter] to submit',
message: 'Which script preset would you like to use?',
name: 'scripts',
type: 'multiselect',
},
{
initial: defaults.jsPath,
message: '[js] Where should the JavaScript code be generated?',
name: 'jsPath',
type: hasScript('js'),
},
{
initial: defaults.rustCrate,
message: '[rust] Where is the Rust client crate located?',
name: 'rustCrate',
type: hasScript('rust'),
},
{
initial: (prev: string) => `${prev}/src/generated`,
message: '[rust] Where should the Rust code be generated?',
name: 'rustPath',
type: hasScript('rust'),
},
],
{
onCancel: () => {
throw new Error('Operation cancelled.');
},
},
);

return result;
}

function getDefaultPromptResult(): PromptResult {
return {
idlPath: 'program/idl.json',
jsPath: 'clients/js/src/generated',
rustCrate: 'clients/rust',
rustPath: 'clients/rust/src/generated',
scripts: ['js', 'rust'],
};
}

function getContentFromPromptResult(result: PromptResult, useJsFile: boolean): string {
const scripts: Record<ScriptName, ScriptConfig> = {};
if (result.scripts.includes('js')) {
scripts.js = {
from: '@codama/renderers-js',
args: [result.jsPath],
};
}
if (result.scripts.includes('rust')) {
scripts.rust = {
from: '@codama/renderers-rust',
args: [result.rustPath, { crateFolder: result.rustCrate, formatCode: true }],
};
}
const content: Config = { idl: result.idlPath, before: [], scripts };

if (!useJsFile) {
return JSON.stringify(content, null, 4);
}

return (
'export default ' +
JSON.stringify(content, null, 4)
// Remove quotes around property names
.replace(/"([^"]+)":/g, '$1:')
// Convert double-quoted strings to single quotes
.replace(/"([^"]*)"/g, "'$1'")
);
}
Loading

0 comments on commit 7bb6920

Please sign in to comment.