@usearete/react 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ rules: {
3
+ 'fluent-hooks': require('./eslint-rules/arete-hooks.js'),
4
+ },
5
+ };
@@ -0,0 +1,171 @@
1
+ const FLUENT_HOOKS = new Set(['use', 'useOne', 'useMutation']);
2
+ const CONDITIONAL_ANCESTORS = new Set([
3
+ 'ArrowFunctionExpression',
4
+ 'ConditionalExpression',
5
+ 'DoWhileStatement',
6
+ 'ForInStatement',
7
+ 'ForOfStatement',
8
+ 'ForStatement',
9
+ 'FunctionDeclaration',
10
+ 'FunctionExpression',
11
+ 'IfStatement',
12
+ 'LogicalExpression',
13
+ 'SwitchCase',
14
+ 'TryStatement',
15
+ 'WhileStatement',
16
+ ]);
17
+
18
+ function memberNames(node) {
19
+ if (!node) return [];
20
+ if (node.type === 'ChainExpression') return memberNames(node.expression);
21
+ if (node.type === 'Identifier') return [node.name];
22
+ if (node.type !== 'MemberExpression') return [];
23
+ const property = !node.computed && node.property.type === 'Identifier'
24
+ ? node.property.name
25
+ : node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string'
26
+ ? node.property.value
27
+ : null;
28
+ if (property === null) return [];
29
+ return [...memberNames(node.object), property];
30
+ }
31
+
32
+ function rootIdentifier(node) {
33
+ if (!node) return null;
34
+ if (node.type === 'ChainExpression') return rootIdentifier(node.expression);
35
+ if (node.type === 'Identifier') return node;
36
+ if (node.type === 'MemberExpression') return rootIdentifier(node.object);
37
+ return null;
38
+ }
39
+
40
+ function bindingCallsUseArete(context, identifier) {
41
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
42
+ let scope = sourceCode.getScope(identifier);
43
+ while (scope) {
44
+ const variable = scope.set.get(identifier.name);
45
+ const definition = variable?.defs?.[0];
46
+ const initializer = definition?.node?.type === 'VariableDeclarator'
47
+ ? definition.node.init
48
+ : null;
49
+ if (initializer?.type === 'CallExpression') {
50
+ const calleeNames = memberNames(initializer.callee);
51
+ if (calleeNames[calleeNames.length - 1] === 'useArete') return true;
52
+ }
53
+ scope = scope.upper;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ function isAreteHook(context, node) {
59
+ const names = memberNames(node);
60
+ const hook = names[names.length - 1];
61
+ if (!FLUENT_HOOKS.has(hook)) return false;
62
+ const root = rootIdentifier(node);
63
+ if (!root || (root.name !== 'arete' && !bindingCallsUseArete(context, root))) {
64
+ return false;
65
+ }
66
+ if (hook === 'useMutation') return names.includes('programs');
67
+ return names.includes('views') || names.includes('read') || names.includes('reads');
68
+ }
69
+
70
+ function functionName(node) {
71
+ if (node.type === 'FunctionDeclaration' && node.id) return node.id.name;
72
+ const parent = node.parent;
73
+ if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
74
+ return parent.id.name;
75
+ }
76
+ if (parent?.type === 'Property' && !parent.computed && parent.key.type === 'Identifier') {
77
+ return parent.key.name;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function isComponentOrHook(node) {
83
+ const name = functionName(node);
84
+ return Boolean(name && (/^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name)));
85
+ }
86
+
87
+ function containsReturn(node) {
88
+ if (!node || typeof node !== 'object') return false;
89
+ if (node.type === 'ReturnStatement') return true;
90
+ if (
91
+ node.type === 'ArrowFunctionExpression'
92
+ || node.type === 'FunctionDeclaration'
93
+ || node.type === 'FunctionExpression'
94
+ ) {
95
+ return false;
96
+ }
97
+ for (const [key, value] of Object.entries(node)) {
98
+ if (key === 'parent' || key === 'loc' || key === 'range') continue;
99
+ if (Array.isArray(value) && value.some(containsReturn)) return true;
100
+ if (value && typeof value === 'object' && containsReturn(value)) return true;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ module.exports = {
106
+ meta: {
107
+ type: 'problem',
108
+ docs: {
109
+ description: 'enforce React hook ordering for Arete fluent hooks',
110
+ },
111
+ schema: [],
112
+ messages: {
113
+ outsideComponent: 'Arete hook "{{hook}}" must be called from a React component or custom hook.',
114
+ conditional: 'Arete hook "{{hook}}" must be called unconditionally at the top level.',
115
+ afterReturn: 'Arete hook "{{hook}}" cannot be called after an earlier conditional return.',
116
+ },
117
+ },
118
+ create(context) {
119
+ return {
120
+ CallExpression(node) {
121
+ if (!isAreteHook(context, node.callee)) return;
122
+
123
+ const hook = context.getSourceCode().getText(node.callee);
124
+ let current = node.parent;
125
+ let owner = null;
126
+ let conditional = false;
127
+
128
+ while (current) {
129
+ if (
130
+ current.type === 'ArrowFunctionExpression'
131
+ || current.type === 'FunctionDeclaration'
132
+ || current.type === 'FunctionExpression'
133
+ ) {
134
+ owner = current;
135
+ break;
136
+ }
137
+ if (CONDITIONAL_ANCESTORS.has(current.type)) conditional = true;
138
+ current = current.parent;
139
+ }
140
+
141
+ if (!owner || !isComponentOrHook(owner)) {
142
+ context.report({ node, messageId: 'outsideComponent', data: { hook } });
143
+ return;
144
+ }
145
+
146
+ if (conditional || owner.body.type !== 'BlockStatement') {
147
+ context.report({ node, messageId: 'conditional', data: { hook } });
148
+ return;
149
+ }
150
+
151
+ let statement = node;
152
+ while (statement.parent && statement.parent !== owner.body) {
153
+ if (CONDITIONAL_ANCESTORS.has(statement.parent.type)) {
154
+ context.report({ node, messageId: 'conditional', data: { hook } });
155
+ return;
156
+ }
157
+ statement = statement.parent;
158
+ }
159
+
160
+ const index = owner.body.body.indexOf(statement);
161
+ if (index < 0) {
162
+ context.report({ node, messageId: 'conditional', data: { hook } });
163
+ return;
164
+ }
165
+ if (owner.body.body.slice(0, index).some(containsReturn)) {
166
+ context.report({ node, messageId: 'afterReturn', data: { hook } });
167
+ }
168
+ },
169
+ };
170
+ },
171
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@usearete/react",
3
- "version": "0.1.5",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "React SDK for the Arete Solana developer platform",
6
- "main": "dist/index.js",
6
+ "main": "dist/index.cjs",
7
7
  "module": "dist/index.esm.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "repository": {
@@ -18,8 +18,9 @@
18
18
  "scripts": {
19
19
  "build": "rollup -c",
20
20
  "dev": "rollup -c -w",
21
- "test": "jest",
22
- "lint": "eslint src/**/*.ts",
21
+ "test": "jest && node eslint-rules/arete-hooks.test.cjs",
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "eslint src --ext .ts,.tsx --rulesdir eslint-rules",
23
24
  "prepublishOnly": "npm run build"
24
25
  },
25
26
  "keywords": [
@@ -38,28 +39,36 @@
38
39
  "node": ">=16.0.0"
39
40
  },
40
41
  "peerDependencies": {
41
- "react": "^18.0.0 || ^19.0.0",
42
- "zustand": "^4.0.0 || ^5.0.0"
42
+ "react": "^18.0.0 || ^19.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@usearete/sdk": "^0.1.5"
45
+ "@usearete/sdk": "^0.3.0",
46
+ "zustand": "^4.0.0 || ^5.0.0"
46
47
  },
47
48
  "devDependencies": {
49
+ "@rollup/plugin-commonjs": "^28.0.9",
48
50
  "@rollup/plugin-node-resolve": "^16.0.3",
49
51
  "@rollup/plugin-typescript": "^11.0.0",
50
52
  "@types/jest": "^29.0.0",
51
53
  "@types/react": "^19.0.0",
54
+ "@types/react-test-renderer": "^19.1.0",
55
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
56
+ "@typescript-eslint/parser": "^6.21.0",
52
57
  "eslint": "^8.0.0",
58
+ "eslint-plugin-react-hooks": "^5.2.0",
53
59
  "jest": "^29.0.0",
60
+ "react-test-renderer": "^19.2.5",
54
61
  "rollup": "^3.0.0",
55
62
  "rollup-plugin-dts": "^6.0.0",
56
63
  "ts-jest": "^29.0.0",
57
64
  "tslib": "^2.8.1",
58
- "typescript": "^5.0.0",
59
- "zustand": "^4.0.0 || ^5.0.0"
65
+ "typescript": "^5.0.0"
60
66
  },
61
67
  "files": [
62
68
  "dist",
69
+ "eslint-plugin.cjs",
70
+ "eslint-rules",
71
+ "!eslint-rules/*.test.cjs",
63
72
  "README.md"
64
73
  ]
65
74
  }