Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 65c28b6

Browse files
authoredMay 29, 2025··
Merge pull request #19 from hyperweb-io/feat/parse
adding parse
2 parents a05d1c4 + 048f072 commit 65c28b6

File tree

9 files changed

+424
-2
lines changed

9 files changed

+424
-2
lines changed
 

‎.github/workflows/run-tests.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ jobs:
3232
yarn build
3333
yarn symlink
3434
35-
- name: Test @hyperweb/build 🔍
35+
- name: Test @hyperweb/build
3636
run: cd packages/build && yarn test
37+
38+
- name: Test @hyperweb/parse
39+
run: cd packages/parse && yarn test
3740

38-
- name: Test @hyperweb/ts-json-schema 🔍
41+
- name: Test @hyperweb/ts-json-schema
3942
run: cd packages/ts-json-schema && yarn test
4043

‎packages/parse/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Hyperweb Parse
2+
3+
<p align="center" width="100%">
4+
<img src="https://raw.githubusercontent.com/hyperweb-io/.github/refs/heads/main/assets/logo.svg" alt="hyperweb" width="80"><br />
5+
</p>
6+
7+
<p align="center" width="100%">
8+
<a href="https://github.com/hyperweb-io/hyperweb-build/actions/workflows/run-tests.yml">
9+
<img height="20" src="https://github.com/hyperweb-io/hyperweb-build/actions/workflows/run-tests.yml/badge.svg" />
10+
</a>
11+
<br />
12+
<a href="https://github.com/hyperweb-io/hyperweb-build/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
13+
<a href="https://www.npmjs.com/package/@hyperweb/build"><img height="20" src="https://img.shields.io/github/package-json/v/hyperweb-io/hyperweb-build?filename=packages%2Fbuild%2Fpackage.json"></a>
14+
</p>
15+
16+
17+
## Interchain JavaScript Stack
18+
19+
A unified toolkit for building applications and smart contracts in the Interchain ecosystem ⚛️
20+
21+
| Category | Tools | Description |
22+
|----------------------|------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
23+
| **Chain Information** | [**Chain Registry**](https://github.com/hyperweb-io/chain-registry), [**Utils**](https://www.npmjs.com/package/@chain-registry/utils), [**Client**](https://www.npmjs.com/package/@chain-registry/client) | Everything from token symbols, logos, and IBC denominations for all assets you want to support in your application. |
24+
| **Wallet Connectors**| [**Interchain Kit**](https://github.com/hyperweb-io/interchain-kit)<sup>beta</sup>, [**Cosmos Kit**](https://github.com/hyperweb-io/cosmos-kit) | Experience the convenience of connecting with a variety of web3 wallets through a single, streamlined interface. |
25+
| **Signing Clients** | [**InterchainJS**](https://github.com/hyperweb-io/interchainjs)<sup>beta</sup>, [**CosmJS**](https://github.com/cosmos/cosmjs) | A single, universal signing interface for any network |
26+
| **SDK Clients** | [**Telescope**](https://github.com/hyperweb-io/telescope) | Your Frontend Companion for Building with TypeScript with Cosmos SDK Modules. |
27+
| **Starter Kits** | [**Create Interchain App**](https://github.com/hyperweb-io/create-interchain-app)<sup>beta</sup>, [**Create Cosmos App**](https://github.com/hyperweb-io/create-cosmos-app) | Set up a modern Interchain app by running one command. |
28+
| **UI Kits** | [**Interchain UI**](https://github.com/hyperweb-io/interchain-ui) | The Interchain Design System, empowering developers with a flexible, easy-to-use UI kit. |
29+
| **Testing Frameworks** | [**Starship**](https://github.com/hyperweb-io/starship) | Unified Testing and Development for the Interchain. |
30+
| **TypeScript Smart Contracts** | [**Create Hyperweb App**](https://github.com/hyperweb-io/create-hyperweb-app) | Build and deploy full-stack blockchain applications with TypeScript |
31+
| **CosmWasm Contracts** | [**CosmWasm TS Codegen**](https://github.com/CosmWasm/ts-codegen) | Convert your CosmWasm smart contracts into dev-friendly TypeScript classes. |
32+
33+
## Credits
34+
35+
🛠 Built by Hyperweb (formerly Cosmology) — if you like our tools, please checkout and contribute to [our github ⚛️](https://github.com/hyperweb-io)
36+
37+
## Disclaimer
38+
39+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED “AS IS”, AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
40+
41+
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { ContractAnalyzer } from '../src/ContractAnalyzer';
2+
3+
describe('ContractAnalyzer', () => {
4+
let analyzer: ContractAnalyzer;
5+
6+
beforeEach(() => {
7+
analyzer = new ContractAnalyzer();
8+
});
9+
10+
it('should identify query methods', () => {
11+
const code = `
12+
export default class Contract {
13+
state: any;
14+
15+
getState() {
16+
return this.state;
17+
}
18+
19+
getValue() {
20+
const value = this.state.value;
21+
return value;
22+
}
23+
}
24+
`;
25+
26+
const result = analyzer.analyzeFromCode(code);
27+
expect(result.queries).toEqual(['getState', 'getValue']);
28+
expect(result.mutations).toEqual([]);
29+
});
30+
31+
it('should identify mutation methods', () => {
32+
const code = `
33+
export default class Contract {
34+
state: any;
35+
36+
setState(newState: any) {
37+
this.state = newState;
38+
}
39+
40+
updateValue(value: any) {
41+
this.state.value = value;
42+
}
43+
}
44+
`;
45+
46+
const result = analyzer.analyzeFromCode(code);
47+
expect(result.queries).toEqual([]);
48+
expect(result.mutations).toEqual(['setState', 'updateValue']);
49+
});
50+
51+
it('should identify both query and mutation methods', () => {
52+
const code = `
53+
export default class Contract {
54+
state: any;
55+
56+
getState() {
57+
return this.state;
58+
}
59+
60+
setState(newState: any) {
61+
this.state = newState;
62+
}
63+
64+
updateAndGet() {
65+
this.state.value = 42;
66+
return this.state.value;
67+
}
68+
}
69+
`;
70+
71+
const result = analyzer.analyzeFromCode(code);
72+
expect(result.queries).toEqual(['getState']);
73+
expect(result.mutations).toEqual(['setState', 'updateAndGet']);
74+
});
75+
76+
it('should ignore static methods and constructors', () => {
77+
const code = `
78+
export default class Contract {
79+
state: any;
80+
81+
constructor() {
82+
this.state = {};
83+
}
84+
85+
static getStaticState() {
86+
return this.state;
87+
}
88+
89+
getState() {
90+
return this.state;
91+
}
92+
}
93+
`;
94+
95+
const result = analyzer.analyzeFromCode(code);
96+
expect(result.queries).toEqual(['getState']);
97+
expect(result.mutations).toEqual([]);
98+
});
99+
100+
it('should handle class expression with named export', () => {
101+
const code = `
102+
var Contract = class {
103+
state = {};
104+
105+
initialize() {
106+
this.state.value = 2.02234;
107+
}
108+
109+
getState() {
110+
return this.state.value;
111+
}
112+
113+
exp() {
114+
this.state.value = this.state.value * this.state.value;
115+
return this.state.value;
116+
}
117+
};
118+
119+
export { Contract as default };
120+
`;
121+
122+
const result = analyzer.analyzeFromCode(code);
123+
expect(result.queries).toEqual(['getState']);
124+
expect(result.mutations).toEqual(['initialize', 'exp']);
125+
});
126+
127+
it('should throw error when no default export is found', () => {
128+
const code = `
129+
class Contract {
130+
state: any;
131+
132+
getState() {
133+
return this.state;
134+
}
135+
}
136+
`;
137+
138+
expect(() => analyzer.analyzeFromCode(code)).toThrow('No default exported class found in the code');
139+
});
140+
141+
it('should throw error when default export is not a class', () => {
142+
const code = `
143+
export default function Contract() {
144+
return {};
145+
}
146+
`;
147+
148+
expect(() => analyzer.analyzeFromCode(code)).toThrow('No default exported class found in the code');
149+
});
150+
});

‎packages/parse/jest.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
transform: {
6+
'^.+\\.tsx?$': [
7+
'ts-jest',
8+
{
9+
babelConfig: false,
10+
tsconfig: 'tsconfig.json',
11+
},
12+
],
13+
},
14+
transformIgnorePatterns: [`/node_modules/*`],
15+
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
16+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
17+
modulePathIgnorePatterns: ['dist/*']
18+
};

‎packages/parse/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@hyperweb/parse",
3+
"version": "1.0.1",
4+
"author": "Hyperweb <developers@hyperweb.io>",
5+
"description": "Parse Hyperweb contracts",
6+
"main": "index.js",
7+
"module": "esm/index.js",
8+
"types": "index.d.ts",
9+
"homepage": "https://github.com/hyperweb-io/hyperweb-build",
10+
"license": "SEE LICENSE IN LICENSE",
11+
"publishConfig": {
12+
"access": "public",
13+
"directory": "dist"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/hyperweb-io/hyperweb-build"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/hyperweb-io/hyperweb-build/issues"
21+
},
22+
"scripts": {
23+
"copy": "copyfiles -f ../../LICENSE README.md package.json dist",
24+
"clean": "rimraf dist/**",
25+
"prepare": "npm run build",
26+
"build": "npm run clean; tsc; tsc -p tsconfig.esm.json; npm run copy",
27+
"build:dev": "npm run clean; tsc --declarationMap; tsc -p tsconfig.esm.json; npm run copy",
28+
"lint": "eslint . --fix",
29+
"test": "jest",
30+
"test:watch": "jest --watch"
31+
},
32+
"keywords": [],
33+
"dependencies": {
34+
"@babel/parser": "^7.24.0",
35+
"@babel/traverse": "^7.24.0",
36+
"@babel/types": "^7.24.0"
37+
}
38+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// @ts-nocheck
2+
import * as parser from '@babel/parser';
3+
import traverse, { NodePath } from '@babel/traverse';
4+
import * as t from '@babel/types';
5+
6+
export interface AnalysisResult {
7+
queries: string[];
8+
mutations: string[];
9+
}
10+
11+
export class ContractAnalyzer {
12+
private ast: parser.ParseResult<t.File> | null = null;
13+
14+
/**
15+
* Analyzes a TypeScript contract class to identify query and mutation methods
16+
* @param code The TypeScript source code to analyze
17+
* @returns An object containing arrays of query and mutation method names
18+
*/
19+
public analyzeFromCode(code: string): AnalysisResult {
20+
this.parseCode(code);
21+
return this.analyze();
22+
}
23+
24+
/**
25+
* Parses the TypeScript code into an AST
26+
* @param code The TypeScript source code to parse
27+
*/
28+
private parseCode(code: string): void {
29+
this.ast = parser.parse(code, {
30+
sourceType: 'module',
31+
plugins: ['typescript', 'classProperties', 'decorators-legacy'],
32+
});
33+
}
34+
35+
/**
36+
* Analyzes the parsed AST to identify query and mutation methods
37+
* @returns An object containing arrays of query and mutation method names
38+
*/
39+
private analyze(): AnalysisResult {
40+
if (!this.ast) {
41+
throw new Error('No code has been parsed. Call analyzeFromCode first.');
42+
}
43+
44+
const queries: string[] = [];
45+
const mutations: string[] = [];
46+
let foundDefaultExport = false;
47+
48+
const self = this;
49+
50+
traverse(this.ast, {
51+
// Handle both export default class and export { Contract as default }
52+
ExportDefaultDeclaration(path) {
53+
const declaration = path.node.declaration;
54+
55+
// Check if the default export is a class declaration
56+
if (t.isClassDeclaration(declaration)) {
57+
foundDefaultExport = true;
58+
self.analyzeClassMethods(path, declaration, queries, mutations);
59+
}
60+
},
61+
// Handle named exports with default alias
62+
ExportNamedDeclaration(path) {
63+
const specifiers = path.node.specifiers;
64+
for (const specifier of specifiers) {
65+
if (
66+
t.isExportSpecifier(specifier) &&
67+
specifier.exported.type === 'Identifier' &&
68+
specifier.exported.name === 'default'
69+
) {
70+
// Find the variable declaration for the exported class
71+
const binding = path.scope.getBinding(specifier.local.name);
72+
if (binding) {
73+
const declaration = binding.path.node;
74+
if (
75+
t.isVariableDeclarator(declaration) &&
76+
t.isClassExpression(declaration.init)
77+
) {
78+
foundDefaultExport = true;
79+
self.analyzeClassMethods(binding.path, declaration.init, queries, mutations);
80+
}
81+
}
82+
}
83+
}
84+
},
85+
});
86+
87+
if (!foundDefaultExport) {
88+
throw new Error('No default exported class found in the code');
89+
}
90+
91+
return { queries, mutations };
92+
}
93+
94+
/**
95+
* Analyzes methods within a class declaration or expression
96+
*/
97+
private analyzeClassMethods(
98+
parentPath: NodePath,
99+
classNode: t.ClassDeclaration | t.ClassExpression,
100+
queries: string[],
101+
mutations: string[]
102+
): void {
103+
parentPath.traverse({
104+
ClassMethod(methodPath: NodePath<t.ClassMethod>) {
105+
// Skip static methods and constructors
106+
if (methodPath.node.static || methodPath.node.kind === 'constructor') {
107+
return;
108+
}
109+
110+
const methodName = methodPath.node.key.type === 'Identifier' ? methodPath.node.key.name : '';
111+
if (!methodName) return;
112+
113+
let readsState = false;
114+
let writesState = false;
115+
116+
// Check for state access
117+
methodPath.traverse({
118+
MemberExpression(memberPath: NodePath<t.MemberExpression>) {
119+
const object = memberPath.node.object;
120+
const property = memberPath.node.property;
121+
122+
// Check if this is a this.state access
123+
if (
124+
t.isThisExpression(object) &&
125+
t.isIdentifier(property) &&
126+
property.name === 'state'
127+
) {
128+
// Check if this is part of an assignment
129+
const parent = memberPath.parentPath;
130+
if (
131+
t.isAssignmentExpression(parent.node) ||
132+
(t.isMemberExpression(parent.node) &&
133+
t.isAssignmentExpression(parent.parentPath.node))
134+
) {
135+
writesState = true;
136+
} else {
137+
readsState = true;
138+
}
139+
}
140+
},
141+
});
142+
143+
// If a method writes to state, it's a mutation
144+
// If it only reads from state, it's a query
145+
if (writesState) {
146+
mutations.push(methodName);
147+
} else if (readsState) {
148+
queries.push(methodName);
149+
}
150+
},
151+
});
152+
}
153+
}

‎packages/parse/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ContractAnalyzer';

‎packages/parse/tsconfig.esm.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist/esm",
5+
"module": "es2022",
6+
"rootDir": "src/",
7+
"declaration": false
8+
}
9+
}

‎packages/parse/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "src/"
6+
},
7+
"include": ["src/**/*.ts"],
8+
"exclude": ["dist", "node_modules", "**/*.spec.*", "**/*.test.*"]
9+
}

0 commit comments

Comments
 (0)
Please sign in to comment.