Skip to content

Commit

Permalink
feat: adds new ts sdk generator and basic primitives
Browse files Browse the repository at this point in the history
  • Loading branch information
stalniy committed Feb 25, 2025
1 parent a902653 commit 328bb49
Show file tree
Hide file tree
Showing 36 changed files with 4,776 additions and 11,223 deletions.
3 changes: 0 additions & 3 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@ AKASH_TS_PACKAGE_FILE=${AKASH_TS_ROOT}/package.json
AKASH_TS_NODE_MODULES=${AKASH_TS_ROOT}/node_modules
AKASH_TS_NODE_BIN=${AKASH_TS_NODE_MODULES}/.bin
AKASH_DEVCACHE_TMP=${AKASH_DEVCACHE_BASE}/tmp
AKASH_DEVCACHE_TMP_TS=${AKASH_DEVCACHE_TMP}/ts
AKASH_DEVCACHE_TMP_TS_GRPC_JS=${AKASH_DEVCACHE_TMP_TS}/generated-grpc-js
AKASH_DEVCACHE_TMP_TS_PATCHES=${AKASH_DEVCACHE_TMP_TS}/patches
13 changes: 8 additions & 5 deletions buf.gen.ts.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
version: v2
clean: true
plugins:
- local: protoc-gen-es
- local: ./ts/node_modules/.bin/protoc-gen-es
strategy: all
out: ./ts/src/generated
out: ./ts/src/generated/protos
opt:
- target=ts
- json_types=true
- local: sdk-object
path: ./ts/bin/protoc-sdk-object.ts
out: ./ts/src/gen
- local: ./ts/bin/protoc-sdk-object.ts
out: ./ts/src/generated
strategy: all
opt:
- target=ts
inputs:
- directory: proto/node
- directory: proto/provider
- directory: go/vendor/github.com/cosmos/gogoproto
- directory: go/vendor/github.com/cosmos/cosmos-sdk/proto
- directory: go/vendor/github.com/cosmos/cosmos-proto/proto
- directory: go/vendor
paths:
- go/vendor/k8s.io/apimachinery/pkg/api/resource
- go/vendor/github.com/cosmos/gogoproto/protobuf
2 changes: 1 addition & 1 deletion make/lint.mk
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ proto-format:

.PHONY: lint-ts
lint-ts: $(AKASH_TS_NODE_MODULES)
cd ts && npm run lint;
cd $(TS_ROOT) && npm run lint;
2 changes: 1 addition & 1 deletion make/setup-cache.mk
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,5 @@ endif

$(AKASH_TS_NODE_MODULES): $(AKASH_TS_PACKAGE_FILE)
@echo "installing node modules..."
cd $(AKASH_TS_ROOT) && npm install
cd $(AKASH_TS_ROOT) && npm ci
@echo "node modules installed."
1 change: 0 additions & 1 deletion script/protocgen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ function gen_pulsar() {
}

function gen_ts() {
rm -rf "$ROOT_DIR/ts/src"
buf generate --template buf.gen.ts.yaml
}

Expand Down
35 changes: 0 additions & 35 deletions ts/.eslintrc.json

This file was deleted.

1 change: 1 addition & 0 deletions ts/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/build
/node_modules
tsconfig.paths.json
/src/generated/

# Logs
logs
Expand Down
1 change: 1 addition & 0 deletions ts/.husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged --cwd ts
19 changes: 0 additions & 19 deletions ts/.npmignore

This file was deleted.

1 change: 0 additions & 1 deletion ts/.npmrc

This file was deleted.

3 changes: 0 additions & 3 deletions ts/.prettierrc

This file was deleted.

32 changes: 0 additions & 32 deletions ts/.releaserc

This file was deleted.

39 changes: 3 additions & 36 deletions ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,11 @@ npm install @akashnetwork/akash-api

## Usage

You can import the generated namespaces from the package like this:
```typescript
import * as akashDeploymentV1beta1 from '@akashnetwork/akash-api/akash/deployment/v1beta1';
import * as akashDiscoveryV1 from '@akashnetwork/akash-api/akash/discovery/v1';
// ... and so on for other namespaces
```

### TypeScript 4.5 and above
If you're using TypeScript 4.5 or above, the package exports all the paths of the generated namespaces, so you can import them directly.

### TypeScript below 4.5
If you're using a version of TypeScript below 4.5, the package provides a tsconfig.paths.json file that you can extend in your local TypeScript configuration to resolve the paths. Here's how you can do it: In your tsconfig.json file, add the following:
```json
{
"extends": "@akashnetwork/akash-api/tsconfig.paths.json"
}
```
TBD

### Contributing
Contributions are welcome. Please submit a pull request or create an issue to discuss the changes you want to make.

### Contributing to Generated Files

The files in the `src/generated` directory are auto-generated and should not be modified directly. If you need to make changes to these files, follow the steps below:

1. Create a new file in the `src/patch` directory with the same path as the file you want to modify. For example, if you want to modify `src/generated/cosmos/base/v1beta1/coin.ts`, you should create the directory `src/patch/cosmos/base/v1beta1/coin.ts`.

2. Add your changes to a new file in the `src/patch` directory. The new file should have the same name as the file you want to modify.

3. Rename the original file in the `src/generated` directory by appending `.original.ts` to its name. For example, `src/generated/cosmos/base/v1beta1/coin.ts` should be renamed to `src/generated/cosmos/base/v1beta1/coin.original.ts`.

4. Create a new file in the `src/generated` directory with the same name as the original file. This new file should re-export everything from the corresponding file in the `src/patch` directory. For example, the content of `src/generated/cosmos/base/v1beta1/coin.ts` should be:

```typescript
export * from '../../../../patch/cosmos/base/v1beta1/coin';
```

NOTE: Naming and paths are important to prevent the original file from being overwritten when the code is regenerated. See `script/preserve-ts-patches.sh` and `script/restore-ts-patches.sh` for more implementation details.

### License
This package is licensed under the Apache-2.0.

This package is licensed under the Apache-2.0.
186 changes: 186 additions & 0 deletions ts/bin/protoc-sdk-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env npx ts-node -T

import { DescMethod, DescService } from "@bufbuild/protobuf";
import {
createEcmaScriptPlugin,
runNodeJs,
type Schema,
getComments,
} from "@bufbuild/protoplugin";
import { normalize} from 'path';

runNodeJs(
createEcmaScriptPlugin({
name: "protoc-gen-sdk-object",
version: "v1",
generateTs,
})
);

function generateTs(schema: Schema): void {
const servicesLoaderDefs: string[] = [];
const sdkDefs: Record<string, string> = {};
const imports = new Set<string>();
const services = schema.files.map(f => f.services).flat();
if (!services.length) return;

const f = schema.generateFile(getOutputFileName(schema));
let hasTxService = false;

services.forEach((service) => {
if (!hasTxService) {
hasTxService = isTxService(service);
}
const serviceImport = f.importSchema(service);
const serviceImportPath = normalize(serviceImport.from.replace(/\.js$/, ''));
servicesLoaderDefs.push(`() => import('./protos/${serviceImportPath}').then(m => m.${serviceImport.name})`);
const serviceIndex = servicesLoaderDefs.length - 1;
const serviceMethods = service.methods.map((method, methodIndex) => {
const inputType = f.importJson(method.input);
const importPath = inputType.from.replace(/\.js$/, '');
const isInputEmpty = method.input.fields.length === 0;
imports.add(importPath);
const methodArgs = [
`input: ${fileNameToScope(importPath)}.${inputType.name}${isInputEmpty ? ' = {}' : ''}`,
`options?: ${isTxService(service) ? 'TxCallOptions' : 'CallOptions'}`
];
const methodName = getSdkMethodName(method);
let comment = jsDoc(method);
if (comment) comment += '\n';

return comment +
`${methodName}: withMetadata(async function ${methodName}(${methodArgs.join(', ')}) {\n` +
` const service = await serviceLoader.loadAt(${serviceIndex});\n` +
` return clientFactory.getClient(service).${decapitalize(method.name)}(input, options);\n` +
`}, { path: [${serviceIndex}, ${methodIndex}] })`
;
});

if (serviceMethods.length > 0) {
const path = service.file.proto.package;
const tabSize = path.split('.').length;
const methods = indent(serviceMethods.join(',\n'), ' '.repeat(tabSize + 1));
const methodsTab = ' '.repeat(tabSize);
const existingValue = getByPath(sdkDefs, path);
if (existingValue) {
const value = existingValue.slice(0, -1).trim() + `,\n${methods}\n${methodsTab}}`;
setByPath(sdkDefs, path, value);
} else {
setByPath(sdkDefs, path, `{\n${methods}\n${methodsTab}}`);
}
}
});

Array.from(imports).forEach(importPath => {
f.print(`import type * as ${fileNameToScope(importPath)} from '${importPath.startsWith('./') ? `./protos/${importPath}` : importPath}'`);
});
f.print(`import type { ClientFactory } from '../sdk/ClientFactory';`);

const callOptionsTypes = ['CallOptions'];
if (hasTxService) callOptionsTypes.push('TxCallOptions');
f.print(`import type { ${callOptionsTypes.join(', ')} } from '../transport';`);
f.print(`import { createServiceLoader } from '../utils/createServiceLoader';`);
f.print(`import { withMetadata } from '../utils/sdkMetadata';`);
f.print('\n');
f.print(f.export('const', `serviceLoader = createServiceLoader([\n${indent(servicesLoaderDefs.join(',\n'))}\n] as const);`));
f.print(f.export('function', `createSDK<T extends ClientFactory>(clientFactory: T) {\n return ${indent(stringifyObject(sdkDefs)).trim()}\n}`));
}

function getOutputFileName(schema: Schema): string {
if (process.env.BUF_PLUGIN_SDK_OBJECT_OUTPUT_FILE) {
return process.env.BUF_PLUGIN_SDK_OBJECT_OUTPUT_FILE;
}

for (const file of schema.files) {
if (file.name.includes('akash/provider/lease')) {
return 'createProviderSDK.ts';
}
if (file.name.includes('akash/cert/v1/msg')) {
return 'createNodeSDK.ts';
}
if (file.name.includes('cosmos/base/tendermint/v1beta1/query') || file.name.includes('cosmos/base/query/v1/query')) {
return 'createCosmosSDK.ts';
}
}

throw new Error("Cannot determine sdk file name");
}

function getByPath(obj: Record<string, any>, path: string) {
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length; i++) {
if (current === undefined) return;
current = current[parts[i]];

}
return current;
}

function setByPath(obj: Record<string, any>, path: string, value: unknown) {
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current)) {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = value;
};

const indent = (value: string, tab = ' '.repeat(2)) => tab + value.replace(/\n/g, '\n' + tab);
const isTxService = (service: DescService) => service.name === 'Msg';

function getSdkMethodName(method: DescMethod) {
if (isTxService(method.parent) || method.name.startsWith('get') || method.name.startsWith('Get')) {
return decapitalize(method.name);
}

return `get${capitalize(method.name)}`;
}

function capitalize(str: string): string {
return str[0].toUpperCase() + str.slice(1);
}

function decapitalize(str: string): string {
return str[0].toLowerCase() + str.slice(1);
}

function stringifyObject(obj: Record<string, any>, tabSize = 0, wrap = (value: string) => value): string {
if (typeof obj !== 'object') return obj;

const spaces = ' '.repeat(tabSize);
const entries = Object.entries(obj).map(([key, value]) => {
if (typeof value === 'string') {
return `${spaces} ${key}: ${wrap(value)}`;
}
return `${spaces} ${key}: ${stringifyObject(value, tabSize + 2, wrap)}`;
});

return `{\n${entries.join(',\n')}\n${spaces}}`;
}

function fileNameToScope(fileName: string) {
return normalize(fileName).replace(/\W+/g, '_').replace(/^_+/, '');
}

function jsDoc(method: DescMethod) {
const comments = [];
const methodComments = getComments(method);

if (methodComments.leading) {
comments.push(methodComments.leading
.trim()
.replace(new RegExp(`\\b${method.name}\\b`, 'g'), getSdkMethodName(method))
.replace(/\n/g, '\n *')
);
}
if (method.deprecated || method.parent.deprecated) {
comments.push(`@deprecated`);
}

return comments.length ? `/**\n * ${comments.join('\n * ')}\n */` : '';
}
Loading

0 comments on commit 328bb49

Please sign in to comment.