Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js/cli): generate react hooks #527

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions js/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Overview

The `sails-js-cli` is a command-line tool designed to generate TypeScript client libraries from Sails IDL files. It automates the process of creating fully functional client libraries based on the interfaces defined in the Sails framework, streamlining development and ensuring consistency between client and on-chain programs.

# Installation

To install the `sails-js-cli` package globally, run the following command:

```bash
Expand All @@ -15,6 +17,7 @@ npx sails-js-cli command ...args
```

# Generate typescript client library using the IDL file

To generate a TypeScript client library run the following command:

```bash
Expand All @@ -33,12 +36,18 @@ If you want to generate only `lib.ts` file without the whole project structure,
sails-js generate path/to/sails.idl -o path/to/out/dir --no-project
```

React hooks generation is available via `--with-hooks` flag:

```bash
sails-js generate path/to/sails.idl -o path/to/out/dir --with-hooks
```

# Use generated library

## Create an instance

First, connect to the chain using `@gear-js/api`.

```javascript
import { GearApi } from '@gear-js/api';

Expand All @@ -60,7 +69,6 @@ const program = new Program(api, programId);

The `Program` class has all the functions available in the IDL file.


## Methods

There are a few types of methods available in the `Program` class.
Expand All @@ -71,6 +79,7 @@ There are a few types of methods available in the `Program` class.
- Event subscription methods

### Query methods

Query methods are used to query the program state.
These methods accept the arguments needed to call the function in the program and return the result. Apart from the arguments, these functions also accept optional parameters: `originAddress` is the address of the account that is calling the function (if this parameter isn't provided zero address is used as a default value), `value` is a parameter parameter can be used depending on the function to send some amount of tokens to the correct function execution and `atBlock` to query program state at a specific block.

Expand All @@ -81,6 +90,7 @@ console.log(result);
```

### Message methods

Message methods are used to send messages to the program.
These methods accept the arguments needed to send the message and return [transaction builder](../README.md#transaction-builder) that has a few methods to build and send the transaction.

Expand All @@ -93,7 +103,7 @@ const transaction = program.serviceName.functionName(arg1, arg2);
import { Keyring } from '@polkadot/api';
const keyring = new Keyring({ type: 'sr25519' });
const pair = keyring.addFromUri('//Alice');
transaction.withAccount(pair)
transaction.withAccount(pair);

// Or the address and signerOptions
// This case is mostly used on the frontend with connected wallet.
Expand Down Expand Up @@ -122,10 +132,11 @@ const { msgId, blockHash, response } = await transaction.signAndSend();

const result = await response();

console.log(result)
console.log(result);
```

### Constructor methods

Constructor methods are postfixed with `CtorFromCode` and `CtorFromCodeId` in the `Program` class and are used to deploy the program on the chain.
These methods accept either bytes of the wasm or the id of the uploaded code.
They returns the same [transaction builder](../README.md#transaction-builder) as the message methods.
Expand All @@ -139,10 +150,71 @@ const transaction = program.newCtorFromCode(code);
```

### Event subscription methods

Event subscription methods are used to subscribe to the specific events emitted by the program.

```javascript
program.subscribeToSomeEvent((data) => {
console.log(data);
});
```

## React Hooks

Library generation with the `--with-hooks` flag creates custom React hooks that facilitate interaction with a `sails-js` program. These hooks are essentially wrappers around the generic hooks provided by the `@gear-js/react-hooks` library. The custom hooks are generated based on the `Program` class defined in the `lib.ts` file, and they are based on the specific types and names derived from it.

Feel free to refer to the `@gear-js/react-hooks` [README](https://github.com/gear-tech/gear-js/tree/main/utils/gear-hooks#sails) for a summary of the hooks' specifics.

### useProgram

Initializes the program with the provided parameters.

```jsx
import { useProgram } from './hooks';

const { data: program } = useProgram({ id: '0x...' });
```

### useSend `serviceName` `functionName` Transaction

Sends a transaction to a specified service and function.

```jsx
import { useProgram, useSendAdminMintTransaction } from './hooks';

const { data: program } = useProgram({ id: '0x...' });
const { sendTransaction } = useSendAdminMintTransaction({ program });
```

### usePrepare `serviceName` `functionName` Transaction

Prepares a transaction for a specified service and function.

```jsx
import { useProgram, usePrepareAdminMintTransaction } from './hooks';

const { data: program } = useProgram({ id: '0x...' });
const { prepareTransaction } = usePrepareAdminMintTransaction({ program });
```

### use `serviceName` `functionName` Query

Queries a specified service and function.

```jsx
import { useProgram, useErc20BalanceOfQuery } from './hooks';

const { data: program } = useProgram({ id: '0x...' });
const { data } = useErc20BalanceOfQuery({ program, args: ['0x...'] });
```

### use `serviceName` `functionName` Event

Subscribes to events from a specified service and event name.

```jsx
import { useProgram, useAdminMintedEvent } from './hooks';

const { data: program } = useProgram({ id: '0x...' });
const { data } = useAdminMintedEvent({ program, onData: (value) => console.log(value) });
```
1 change: 1 addition & 0 deletions js/cli/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function updateConfigVersions() {
config.versions['polkadot-api'] = sailsJs.peerDependencies['@polkadot/api'];
config.versions['sails-js'] = sailsJs.version;
config.versions['typescript'] = rootPkgJson.devDependencies.typescript;
config.versions['gear-js-hooks'] = sailsJs.devDependencies['@gear-js/react-hooks'];

writeFileSync('src/config.json', JSON.stringify(config, null, 2));
},
Expand Down
82 changes: 55 additions & 27 deletions js/cli/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { Sails } from 'sails-js';
import * as _path from 'path';
import { confirm } from '@inquirer/prompts';

import { generateLib } from './generate/index.js';
import { generateHooks, generateLib } from './generate/index.js';
import * as config from './config.json';

const program = new Command();

const handler = async (path: string, out: string, name: string, project: boolean) => {
const handler = async (path: string, out: string, name: string, project: boolean, withHooks: boolean) => {
const parser = new SailsIdlParser();
await parser.init();
const sails = new Sails(parser);
Expand All @@ -22,6 +22,11 @@ const handler = async (path: string, out: string, name: string, project: boolean
out = out || '.';
const dir = out;
const libFile = project ? _path.join(dir, 'src', 'lib.ts') : _path.join(dir, 'lib.ts');
let hooksFile = '';

if (withHooks) {
hooksFile = project ? _path.join(dir, 'src', 'hooks.ts') : _path.join(dir, 'hooks.ts');
}

if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
Expand All @@ -38,20 +43,39 @@ const handler = async (path: string, out: string, name: string, project: boolean
process.exit(0);
}
}

if (hooksFile && existsSync(hooksFile)) {
const answer = await confirm({
message: `File ${hooksFile} exists. Do you want to overwrite?`,
default: false,
});

if (!answer) {
process.exit(0);
}
}
}

let libCode: string;
let hooksCode: string;

try {
libCode = generateLib(sails.parseIdl(idl), name);
hooksCode = withHooks ? generateHooks(sails.parseIdl(idl)) : '';
} catch (e) {
console.log(e.message, e.stack);
process.exit(1);
}

if (!project) {
writeFileSync(libFile, libCode);
console.log(`Lib generated at ${libFile}`);

if (hooksFile && hooksCode) {
writeFileSync(hooksFile, hooksCode);
console.log(`Lib and hooks are generated at ${libFile} and ${hooksFile}`);
} else {
console.log(`Lib generated at ${libFile}`);
}
} else {
const srcDir = _path.join(dir, 'src');
const tsconfigPath = _path.join(dir, 'tsconfig.json');
Expand Down Expand Up @@ -88,33 +112,36 @@ const handler = async (path: string, out: string, name: string, project: boolean

writeFileSync(_path.join(srcDir, 'lib.ts'), libCode);

if (hooksCode) {
writeFileSync(_path.join(srcDir, 'hooks.ts'), hooksCode);
}

if (writeTsconfig) {
writeFileSync(_path.join(dir, 'tsconfig.json'), JSON.stringify(config.tsconfig, null, 2));
}

if (writePkgJson) {
writeFileSync(
_path.join(dir, 'package.json'),
JSON.stringify(
{
name,
type: 'module',
dependencies: {
'@gear-js/api': config.versions['gear-js'],
'@polkadot/api': config.versions['polkadot-api'],
'sails-js': config.versions['sails-js'],
},
devDependencies: {
typescript: config.versions['typescript'],
},
scripts: {
build: 'tsc',
},
},
null,
2,
),
);
const packageJson = {
name,
type: 'module',
dependencies: {
'@gear-js/api': config.versions['gear-js'],
'@polkadot/api': config.versions['polkadot-api'],
'sails-js': config.versions['sails-js'],
},
devDependencies: {
typescript: config.versions['typescript'],
},
scripts: {
build: 'tsc',
},
};

if (withHooks) {
packageJson.dependencies['@gear-js/react-hooks'] = config.versions['gear-js-hooks'];
}

writeFileSync(_path.join(dir, 'package.json'), JSON.stringify(packageJson, null, 2));
}

console.log(`Lib generated at ${dir}`);
Expand All @@ -124,12 +151,13 @@ const handler = async (path: string, out: string, name: string, project: boolean
program
.command('generate <path-to-file.sails.idl>')
.option('--no-project', 'Generate single file without project structure')
.option('--with-hooks', 'Generate React hooks')
.option('-n --name <name>', 'Name of the library', 'program')
.option('-o --out <path-to-dir>', 'Output directory')
.description('Generate typescript library based on .sails.idl file')
.action(async (path, options: { out: string; name: string; project: boolean }) => {
.action(async (path, options: { out: string; name: string; project: boolean; withHooks: boolean }) => {
try {
await handler(path, options.out, options.name, options.project);
await handler(path, options.out, options.name, options.project, options.withHooks);
} catch (error) {
console.error(error.message);
process.exit(1);
Expand Down
1 change: 1 addition & 0 deletions js/cli/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"versions": {
"gear-js": "0.38.2",
"gear-js-hooks": "0.13.0",
"polkadot-api": "12.0.1",
"sails-js": "0.2.0",
"typescript": "^5.5.4"
Expand Down
Loading