@pyrpc/client 0.3.3 → 0.6.1
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.
- package/README.md +28 -13
- package/cli.js +194 -0
- package/dist/index.d.mts +15 -16
- package/dist/index.d.ts +15 -16
- package/dist/index.js +7 -31
- package/dist/index.mjs +7 -30
- package/package.json +12 -3
- package/postinstall.js +183 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pyrpc/client
|
|
2
2
|
|
|
3
|
-
Universal TypeScript client for [pyRPC](https://pyrpc.
|
|
3
|
+
Universal TypeScript client for [pyRPC](https://pyrpc.com). Type-safe RPC calls to your Python backend - install, import, call.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -12,26 +12,24 @@ pnpm add @pyrpc/client
|
|
|
12
12
|
bun add @pyrpc/client
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
On install, `@pyrpc/client` runs a `postinstall` script that prompts you to choose a **distribution mode**:
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- **Workspace** — types are written by the server-side `pyrpc dev` / `pyrpc codegen` commands.
|
|
18
|
+
- **Server** — types are fetched from a running server at `npx pyrpc sync` time.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
PYRPC_URL=https://api.example.com npm install @pyrpc/client
|
|
21
|
-
```
|
|
20
|
+
CI / non-TTY environments skip the prompt silently. Re-run with `npx pyrpc sync` to generate types later.
|
|
22
21
|
|
|
23
22
|
## Usage
|
|
24
23
|
|
|
25
24
|
```typescript
|
|
26
|
-
import { createClient } from "@pyrpc/client";
|
|
27
|
-
import type { Types } from "@pyrpc/types";
|
|
25
|
+
import { createClient, type Types } from "@pyrpc/client";
|
|
28
26
|
|
|
29
27
|
const client = createClient<Types>({
|
|
30
28
|
baseUrl: "https://api.example.com",
|
|
31
29
|
});
|
|
32
30
|
|
|
33
31
|
const user = await client.get_user(1);
|
|
34
|
-
console.log(user.name);
|
|
32
|
+
console.log(user.name); // Fully typed - no manual type definitions needed
|
|
35
33
|
```
|
|
36
34
|
|
|
37
35
|
The proxy-based API lets you call any remote procedure as a local method. Parameters are passed positionally or as a single object for named arguments.
|
|
@@ -50,14 +48,31 @@ try {
|
|
|
50
48
|
}
|
|
51
49
|
```
|
|
52
50
|
|
|
51
|
+
## CLI — `npx pyrpc sync`
|
|
52
|
+
|
|
53
|
+
After install, you can sync types from a server distribution:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx pyrpc sync
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Reads `pyrpc-client.json` and behaves according to the configured distribution:
|
|
60
|
+
|
|
61
|
+
- **workspace** — prints "Nothing to sync — server writes types directly."
|
|
62
|
+
- **server** — fetches schema from the configured `server_url` and regenerates `@pyrpc/types/src/index.ts`.
|
|
63
|
+
|
|
64
|
+
Run `npx pyrpc --help` for options.
|
|
65
|
+
|
|
53
66
|
## API
|
|
54
67
|
|
|
55
|
-
### `createClient<
|
|
68
|
+
### `createClient<T>(options?)`
|
|
69
|
+
|
|
70
|
+
Creates a proxy client that forwards method calls to the server. The generic parameter `T` is your `Types` interface for full type safety.
|
|
56
71
|
|
|
57
|
-
|
|
72
|
+
- `baseUrl` - Server root URL (defaults to `window.location.origin` in browsers)
|
|
73
|
+
- `headers` - Static or async `HeadersInit`
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
- `headers` — Static or async `HeadersInit`
|
|
75
|
+
**Note:** There is no `.rpc` property. Call methods directly on the client object.
|
|
61
76
|
|
|
62
77
|
## Keywords
|
|
63
78
|
|
package/cli.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var fs = require('fs');
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var http = require('http');
|
|
5
|
+
var https = require('https');
|
|
6
|
+
|
|
7
|
+
var TYPES_OUTPUT;
|
|
8
|
+
try {
|
|
9
|
+
var typesDir = path.dirname(require.resolve('@pyrpc/types/package.json'));
|
|
10
|
+
TYPES_OUTPUT = path.join(typesDir, 'src', 'index.ts');
|
|
11
|
+
} catch (e) {
|
|
12
|
+
TYPES_OUTPUT = path.join(__dirname, 'node_modules', '@pyrpc', 'types', 'src', 'index.ts');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findConfig() {
|
|
16
|
+
var dir = process.cwd();
|
|
17
|
+
while (true) {
|
|
18
|
+
var cfgPath = path.join(dir, 'pyrpc-client.json');
|
|
19
|
+
if (fs.existsSync(cfgPath)) {
|
|
20
|
+
return cfgPath;
|
|
21
|
+
}
|
|
22
|
+
var parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) return null;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var TYPE_MAP = {
|
|
29
|
+
int: 'number',
|
|
30
|
+
float: 'number',
|
|
31
|
+
str: 'string',
|
|
32
|
+
bool: 'boolean',
|
|
33
|
+
None: 'null',
|
|
34
|
+
NoneType: 'null',
|
|
35
|
+
Any: 'any',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function toTs(t) {
|
|
39
|
+
if (!t || t === 'None') return 'void';
|
|
40
|
+
var m = t.match(/^<class\s+'([^'>]+)'>$/);
|
|
41
|
+
if (m) {
|
|
42
|
+
var n = m[1];
|
|
43
|
+
if (TYPE_MAP[n]) return TYPE_MAP[n];
|
|
44
|
+
if (n[0] >= 'A' && n[0] <= 'Z') return n;
|
|
45
|
+
return 'any';
|
|
46
|
+
}
|
|
47
|
+
var s = t.replace(/^typing\./, '');
|
|
48
|
+
var o = s.match(/^Optional\[(.+)\]$/);
|
|
49
|
+
if (o) return toTs(o[1]) + ' | null';
|
|
50
|
+
var l = s.match(/^(?:List|list)\[(.+)\]$/);
|
|
51
|
+
if (l) return toTs(l[1]) + '[]';
|
|
52
|
+
var d = s.match(/^(?:Dict|dict)\[([^,]+),\s*(.+)\]$/);
|
|
53
|
+
if (d) return 'Record<' + toTs(d[1]) + ', ' + toTs(d[2]) + '>';
|
|
54
|
+
var u = s.match(/^Union\[(.+)\]$/);
|
|
55
|
+
if (u) return u[1].split(',').map(function(p) { return toTs(p.trim()); }).join(' | ');
|
|
56
|
+
var tup = s.match(/^(?:Tuple|tuple)\[([^\]]+)\]$/);
|
|
57
|
+
if (tup) return '[' + tup[1].split(',').map(function(p) { return toTs(p.trim()); }).join(', ') + ']';
|
|
58
|
+
return 'any';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generate(schemas) {
|
|
62
|
+
var lines = [];
|
|
63
|
+
lines.push('// Auto-generated by @pyrpc/types');
|
|
64
|
+
lines.push('// Schema fetched from: ' + (process.env.PYRPC_URL || 'http://localhost:8000'));
|
|
65
|
+
lines.push('');
|
|
66
|
+
var names = Object.keys(schemas);
|
|
67
|
+
for (var i = 0; i < names.length; i++) {
|
|
68
|
+
var name = names[i];
|
|
69
|
+
var schema = schemas[name];
|
|
70
|
+
lines.push('export interface ' + name + 'Params {');
|
|
71
|
+
var params = schema.parameters || [];
|
|
72
|
+
for (var j = 0; j < params.length; j++) {
|
|
73
|
+
var p = params[j];
|
|
74
|
+
lines.push(' ' + p.name + ': ' + toTs(p.type) + ';');
|
|
75
|
+
}
|
|
76
|
+
lines.push('}');
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('export interface ' + name + 'Result {');
|
|
79
|
+
lines.push(' data: ' + toTs(schema.return_type) + ';');
|
|
80
|
+
lines.push('}');
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
lines.push('export interface Types {');
|
|
84
|
+
for (var k = 0; k < names.length; k++) {
|
|
85
|
+
lines.push(' ' + names[k] + ': { params: ' + names[k] + 'Params; result: ' + names[k] + 'Result };');
|
|
86
|
+
}
|
|
87
|
+
lines.push('}');
|
|
88
|
+
lines.push('');
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fetchSchema(url) {
|
|
93
|
+
var endpoint = url.replace(/\/+$/, '');
|
|
94
|
+
if (endpoint.indexOf('/rpc') !== endpoint.length - 4) {
|
|
95
|
+
endpoint += '/rpc';
|
|
96
|
+
}
|
|
97
|
+
var mod = endpoint.indexOf('https') === 0 ? https : http;
|
|
98
|
+
return new Promise(function(resolve, reject) {
|
|
99
|
+
mod.get(endpoint, function(res) {
|
|
100
|
+
var data = '';
|
|
101
|
+
res.on('data', function(c) { data += c; });
|
|
102
|
+
res.on('end', function() {
|
|
103
|
+
if (res.statusCode !== 200) return reject(new Error('HTTP ' + res.statusCode));
|
|
104
|
+
try { resolve(JSON.parse(data)); }
|
|
105
|
+
catch (e) { reject(e); }
|
|
106
|
+
});
|
|
107
|
+
}).on('error', reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function printHelp() {
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(' pyRPC client CLI');
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(' Usage:');
|
|
116
|
+
console.log(' npx pyrpc Sync types from server (default)');
|
|
117
|
+
console.log(' npx pyrpc sync Same as above');
|
|
118
|
+
console.log(' npx pyrpc --help Show this help');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(' The sync command reads pyrpc-client.json from your project');
|
|
121
|
+
console.log(' root and fetches the latest RPC schema from the server.');
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(' In workspace mode, the server writes types directly to your');
|
|
124
|
+
console.log(' project \u2014 no sync needed.');
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function main() {
|
|
129
|
+
var args = process.argv.slice(2);
|
|
130
|
+
|
|
131
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
132
|
+
printHelp();
|
|
133
|
+
return Promise.resolve();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (args[0] && args[0] !== 'sync') {
|
|
137
|
+
console.log('Unknown command: ' + args[0]);
|
|
138
|
+
console.log('Usage: npx pyrpc sync');
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var cfgPath = findConfig();
|
|
143
|
+
if (!cfgPath) {
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(' No pyrpc-client.json found.');
|
|
146
|
+
console.log(' Run npm install @pyrpc/client to set up,');
|
|
147
|
+
console.log(' or create pyrpc-client.json manually.');
|
|
148
|
+
console.log('');
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var config;
|
|
153
|
+
try {
|
|
154
|
+
config = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.log(' \u2717 Error reading ' + cfgPath);
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (config.distribution === 'workspace') {
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(' Nothing to sync \u2014 server writes types directly to your project.');
|
|
163
|
+
console.log(' Run pyrpc dev on your backend to regenerate types on change.');
|
|
164
|
+
console.log('');
|
|
165
|
+
return Promise.resolve();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (config.distribution === 'server') {
|
|
169
|
+
var url = config.server_url;
|
|
170
|
+
if (!url) {
|
|
171
|
+
console.log(' \u2717 pyrpc-client.json is missing server_url');
|
|
172
|
+
return Promise.resolve();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return fetchSchema(url).then(function(schemas) {
|
|
176
|
+
var code = generate(schemas);
|
|
177
|
+
fs.mkdirSync(path.dirname(TYPES_OUTPUT), { recursive: true });
|
|
178
|
+
fs.writeFileSync(TYPES_OUTPUT, code, 'utf-8');
|
|
179
|
+
console.log(' \u2713 Types synced: ' + Object.keys(schemas).length + ' procedures');
|
|
180
|
+
console.log(' Import: import { createClient, type Types } from "@pyrpc/client"');
|
|
181
|
+
}).catch(function(err) {
|
|
182
|
+
console.log(' \u2717 Could not fetch schema from ' + url + ' (' + err.message + ')');
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log(' \u2717 Unknown distribution: ' + config.distribution);
|
|
187
|
+
return Promise.resolve();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (require.main === module) {
|
|
191
|
+
main().catch(function(e) {});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { main, toTs, generate, fetchSchema, findConfig, printHelp };
|
package/dist/index.d.mts
CHANGED
|
@@ -24,23 +24,22 @@ interface ClientOptions {
|
|
|
24
24
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
declare class PyRPCClient {
|
|
28
|
-
private url;
|
|
29
|
-
private options;
|
|
30
|
-
constructor(options?: ClientOptions);
|
|
31
|
-
/**
|
|
32
|
-
* Internal method to perform the fetch request.
|
|
33
|
-
*/
|
|
34
|
-
private request;
|
|
35
|
-
/**
|
|
36
|
-
* Creates a proxy that allows calling remote procedures as if they were local methods.
|
|
37
|
-
*/
|
|
38
|
-
get rpc(): any;
|
|
39
|
-
}
|
|
40
27
|
/**
|
|
41
|
-
*
|
|
28
|
+
* Creates a typed pyRPC client.
|
|
29
|
+
*
|
|
30
|
+
* Pass your generated `Types` interface as the generic parameter
|
|
31
|
+
* to get full type safety and auto-complete for all RPC procedures.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { createClient } from "@pyrpc/client"
|
|
36
|
+
* import type { Types } from "@pyrpc/types"
|
|
37
|
+
*
|
|
38
|
+
* const api = createClient<Types>({ baseUrl: "http://localhost:8000" })
|
|
39
|
+
* const user = await api.get_user("John")
|
|
40
|
+
* ```
|
|
42
41
|
*/
|
|
43
|
-
declare function createClient<
|
|
42
|
+
declare function createClient<T = any>(options?: ClientOptions): T;
|
|
44
43
|
|
|
45
44
|
declare class PyRPCError extends Error {
|
|
46
45
|
readonly code: number;
|
|
@@ -48,4 +47,4 @@ declare class PyRPCError extends Error {
|
|
|
48
47
|
constructor(code: number, message: string, data?: any);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
export { type ClientOptions,
|
|
50
|
+
export { type ClientOptions, PyRPCError, type RpcRequest, type RpcResponse, createClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,23 +24,22 @@ interface ClientOptions {
|
|
|
24
24
|
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
declare class PyRPCClient {
|
|
28
|
-
private url;
|
|
29
|
-
private options;
|
|
30
|
-
constructor(options?: ClientOptions);
|
|
31
|
-
/**
|
|
32
|
-
* Internal method to perform the fetch request.
|
|
33
|
-
*/
|
|
34
|
-
private request;
|
|
35
|
-
/**
|
|
36
|
-
* Creates a proxy that allows calling remote procedures as if they were local methods.
|
|
37
|
-
*/
|
|
38
|
-
get rpc(): any;
|
|
39
|
-
}
|
|
40
27
|
/**
|
|
41
|
-
*
|
|
28
|
+
* Creates a typed pyRPC client.
|
|
29
|
+
*
|
|
30
|
+
* Pass your generated `Types` interface as the generic parameter
|
|
31
|
+
* to get full type safety and auto-complete for all RPC procedures.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { createClient } from "@pyrpc/client"
|
|
36
|
+
* import type { Types } from "@pyrpc/types"
|
|
37
|
+
*
|
|
38
|
+
* const api = createClient<Types>({ baseUrl: "http://localhost:8000" })
|
|
39
|
+
* const user = await api.get_user("John")
|
|
40
|
+
* ```
|
|
42
41
|
*/
|
|
43
|
-
declare function createClient<
|
|
42
|
+
declare function createClient<T = any>(options?: ClientOptions): T;
|
|
44
43
|
|
|
45
44
|
declare class PyRPCError extends Error {
|
|
46
45
|
readonly code: number;
|
|
@@ -48,4 +47,4 @@ declare class PyRPCError extends Error {
|
|
|
48
47
|
constructor(code: number, message: string, data?: any);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
export { type ClientOptions,
|
|
50
|
+
export { type ClientOptions, PyRPCError, type RpcRequest, type RpcResponse, createClient };
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
PyRPCClient: () => PyRPCClient,
|
|
24
23
|
PyRPCError: () => PyRPCError,
|
|
25
24
|
createClient: () => createClient
|
|
26
25
|
});
|
|
@@ -67,9 +66,6 @@ var PyRPCClient = class {
|
|
|
67
66
|
this.url = clean.replace(/\/rpc$/i, "") + "/rpc";
|
|
68
67
|
this.options = options;
|
|
69
68
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Internal method to perform the fetch request.
|
|
72
|
-
*/
|
|
73
69
|
async request(method, params) {
|
|
74
70
|
const id = Math.random().toString(36).substring(7);
|
|
75
71
|
const body = { id, method, params };
|
|
@@ -95,41 +91,21 @@ var PyRPCClient = class {
|
|
|
95
91
|
}
|
|
96
92
|
return data.result;
|
|
97
93
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Creates a proxy that allows calling remote procedures as if they were local methods.
|
|
100
|
-
*/
|
|
101
|
-
get rpc() {
|
|
102
|
-
return new Proxy({}, {
|
|
103
|
-
get: (_, method) => {
|
|
104
|
-
return (...args) => {
|
|
105
|
-
const params = args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : args;
|
|
106
|
-
return this.request(method, params);
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
94
|
};
|
|
95
|
+
function normalizeArgs(args) {
|
|
96
|
+
return args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : args;
|
|
97
|
+
}
|
|
112
98
|
function createClient(options = {}) {
|
|
113
99
|
const client = new PyRPCClient(options);
|
|
114
|
-
return new Proxy(
|
|
115
|
-
get(
|
|
116
|
-
if (prop
|
|
117
|
-
|
|
118
|
-
get(_, method) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`Use client.${String(method)}() instead of client.rpc.${String(method)}(). The .rpc prefix was removed for a cleaner API.`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
if (prop in target) return Reflect.get(target, prop, receiver);
|
|
126
|
-
return target.rpc[prop];
|
|
100
|
+
return new Proxy({}, {
|
|
101
|
+
get(_target, prop) {
|
|
102
|
+
if (typeof prop !== "string") return void 0;
|
|
103
|
+
return (...args) => client.request(prop, normalizeArgs(args));
|
|
127
104
|
}
|
|
128
105
|
});
|
|
129
106
|
}
|
|
130
107
|
// Annotate the CommonJS export names for ESM import in node:
|
|
131
108
|
0 && (module.exports = {
|
|
132
|
-
PyRPCClient,
|
|
133
109
|
PyRPCError,
|
|
134
110
|
createClient
|
|
135
111
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -39,9 +39,6 @@ var PyRPCClient = class {
|
|
|
39
39
|
this.url = clean.replace(/\/rpc$/i, "") + "/rpc";
|
|
40
40
|
this.options = options;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Internal method to perform the fetch request.
|
|
44
|
-
*/
|
|
45
42
|
async request(method, params) {
|
|
46
43
|
const id = Math.random().toString(36).substring(7);
|
|
47
44
|
const body = { id, method, params };
|
|
@@ -67,40 +64,20 @@ var PyRPCClient = class {
|
|
|
67
64
|
}
|
|
68
65
|
return data.result;
|
|
69
66
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Creates a proxy that allows calling remote procedures as if they were local methods.
|
|
72
|
-
*/
|
|
73
|
-
get rpc() {
|
|
74
|
-
return new Proxy({}, {
|
|
75
|
-
get: (_, method) => {
|
|
76
|
-
return (...args) => {
|
|
77
|
-
const params = args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : args;
|
|
78
|
-
return this.request(method, params);
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
67
|
};
|
|
68
|
+
function normalizeArgs(args) {
|
|
69
|
+
return args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0]) ? args[0] : args;
|
|
70
|
+
}
|
|
84
71
|
function createClient(options = {}) {
|
|
85
72
|
const client = new PyRPCClient(options);
|
|
86
|
-
return new Proxy(
|
|
87
|
-
get(
|
|
88
|
-
if (prop
|
|
89
|
-
|
|
90
|
-
get(_, method) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Use client.${String(method)}() instead of client.rpc.${String(method)}(). The .rpc prefix was removed for a cleaner API.`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (prop in target) return Reflect.get(target, prop, receiver);
|
|
98
|
-
return target.rpc[prop];
|
|
73
|
+
return new Proxy({}, {
|
|
74
|
+
get(_target, prop) {
|
|
75
|
+
if (typeof prop !== "string") return void 0;
|
|
76
|
+
return (...args) => client.request(prop, normalizeArgs(args));
|
|
99
77
|
}
|
|
100
78
|
});
|
|
101
79
|
}
|
|
102
80
|
export {
|
|
103
|
-
PyRPCClient,
|
|
104
81
|
PyRPCError,
|
|
105
82
|
createClient
|
|
106
83
|
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyrpc/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Universal TypeScript client for pyRPC",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pyrpc": "./cli.js"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
|
-
"dist"
|
|
12
|
+
"dist",
|
|
13
|
+
"cli.js",
|
|
14
|
+
"postinstall.js"
|
|
10
15
|
],
|
|
11
16
|
"scripts": {
|
|
17
|
+
"postinstall": "node postinstall.js",
|
|
12
18
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
13
19
|
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
|
|
14
20
|
"lint": "eslint src/**/*.ts",
|
|
@@ -24,7 +30,10 @@
|
|
|
24
30
|
"author": "",
|
|
25
31
|
"license": "MIT",
|
|
26
32
|
"dependencies": {
|
|
27
|
-
"@pyrpc/types": "^0.
|
|
33
|
+
"@pyrpc/types": "^0.6.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@pyrpc/types": "^0.6.0"
|
|
28
37
|
},
|
|
29
38
|
"devDependencies": {
|
|
30
39
|
"@types/node": "^20.19.39",
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var fs = require('fs');
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var readline = require('readline');
|
|
5
|
+
var http = require('http');
|
|
6
|
+
var https = require('https');
|
|
7
|
+
|
|
8
|
+
var TYPES_DIR;
|
|
9
|
+
try {
|
|
10
|
+
TYPES_DIR = path.dirname(require.resolve('@pyrpc/types/package.json'));
|
|
11
|
+
} catch (e) {
|
|
12
|
+
TYPES_DIR = path.join(__dirname, 'node_modules', '@pyrpc', 'types');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var TYPES_FILE = path.join(TYPES_DIR, 'src', 'index.ts');
|
|
16
|
+
|
|
17
|
+
function findProjectRoot() {
|
|
18
|
+
var dir = process.cwd();
|
|
19
|
+
while (true) {
|
|
20
|
+
var pkgPath = path.join(dir, 'package.json');
|
|
21
|
+
if (fs.existsSync(pkgPath) && !pkgPath.replace(/\\/g, '/').includes('/node_modules/')) {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
var parent = path.dirname(dir);
|
|
25
|
+
if (parent === dir) return process.cwd();
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getConfigPath() {
|
|
31
|
+
return path.join(findProjectRoot(), 'pyrpc-client.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var TYPE_MAP = {
|
|
35
|
+
int: 'number',
|
|
36
|
+
float: 'number',
|
|
37
|
+
str: 'string',
|
|
38
|
+
bool: 'boolean',
|
|
39
|
+
None: 'null',
|
|
40
|
+
NoneType: 'null',
|
|
41
|
+
Any: 'any',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function toTs(t) {
|
|
45
|
+
if (!t || t === 'None') return 'void';
|
|
46
|
+
var m = t.match(/^<class\s+'([^'>]+)'>$/);
|
|
47
|
+
if (m) {
|
|
48
|
+
var n = m[1];
|
|
49
|
+
if (TYPE_MAP[n]) return TYPE_MAP[n];
|
|
50
|
+
if (n[0] >= 'A' && n[0] <= 'Z') return n;
|
|
51
|
+
return 'any';
|
|
52
|
+
}
|
|
53
|
+
var s = t.replace(/^typing\./, '');
|
|
54
|
+
var o = s.match(/^Optional\[(.+)\]$/);
|
|
55
|
+
if (o) return toTs(o[1]) + ' | null';
|
|
56
|
+
var l = s.match(/^(?:List|list)\[(.+)\]$/);
|
|
57
|
+
if (l) return toTs(l[1]) + '[]';
|
|
58
|
+
var d = s.match(/^(?:Dict|dict)\[([^,]+),\s*(.+)\]$/);
|
|
59
|
+
if (d) return 'Record<' + toTs(d[1]) + ', ' + toTs(d[2]) + '>';
|
|
60
|
+
var u = s.match(/^Union\[(.+)\]$/);
|
|
61
|
+
if (u) return u[1].split(',').map(function(p) { return toTs(p.trim()); }).join(' | ');
|
|
62
|
+
var tup = s.match(/^(?:Tuple|tuple)\[([^\]]+)\]$/);
|
|
63
|
+
if (tup) return '[' + tup[1].split(',').map(function(p) { return toTs(p.trim()); }).join(', ') + ']';
|
|
64
|
+
return 'any';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function generate(schemas) {
|
|
68
|
+
var lines = [];
|
|
69
|
+
lines.push('// Auto-generated by @pyrpc/types');
|
|
70
|
+
lines.push('// Schema fetched from: ' + (process.env.PYRPC_URL || 'http://localhost:8000'));
|
|
71
|
+
lines.push('');
|
|
72
|
+
var names = Object.keys(schemas);
|
|
73
|
+
for (var i = 0; i < names.length; i++) {
|
|
74
|
+
var name = names[i];
|
|
75
|
+
var schema = schemas[name];
|
|
76
|
+
lines.push('export interface ' + name + 'Params {');
|
|
77
|
+
var params = schema.parameters || [];
|
|
78
|
+
for (var j = 0; j < params.length; j++) {
|
|
79
|
+
var p = params[j];
|
|
80
|
+
lines.push(' ' + p.name + ': ' + toTs(p.type) + ';');
|
|
81
|
+
}
|
|
82
|
+
lines.push('}');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('export interface ' + name + 'Result {');
|
|
85
|
+
lines.push(' data: ' + toTs(schema.return_type) + ';');
|
|
86
|
+
lines.push('}');
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
lines.push('export interface Types {');
|
|
90
|
+
for (var k = 0; k < names.length; k++) {
|
|
91
|
+
lines.push(' ' + names[k] + ': { params: ' + names[k] + 'Params; result: ' + names[k] + 'Result };');
|
|
92
|
+
}
|
|
93
|
+
lines.push('}');
|
|
94
|
+
lines.push('');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function fetchSchema(url) {
|
|
99
|
+
var endpoint = url.replace(/\/+$/, '');
|
|
100
|
+
if (endpoint.indexOf('/rpc') !== endpoint.length - 4) {
|
|
101
|
+
endpoint += '/rpc';
|
|
102
|
+
}
|
|
103
|
+
var mod = endpoint.indexOf('https') === 0 ? https : http;
|
|
104
|
+
return new Promise(function(resolve, reject) {
|
|
105
|
+
mod.get(endpoint, function(res) {
|
|
106
|
+
var data = '';
|
|
107
|
+
res.on('data', function(c) { data += c; });
|
|
108
|
+
res.on('end', function() {
|
|
109
|
+
if (res.statusCode !== 200) return reject(new Error('HTTP ' + res.statusCode));
|
|
110
|
+
try { resolve(JSON.parse(data)); }
|
|
111
|
+
catch (e) { reject(e); }
|
|
112
|
+
});
|
|
113
|
+
}).on('error', reject);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function main() {
|
|
118
|
+
if (fs.existsSync(getConfigPath())) {
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var isInteractive = process.stdin.isTTY;
|
|
123
|
+
var isCI = process.env.CI;
|
|
124
|
+
|
|
125
|
+
if (!isInteractive || isCI) {
|
|
126
|
+
return Promise.resolve();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(' \u2728 Welcome to pyRPC!');
|
|
131
|
+
console.log(' Let\'s set up your client configuration.');
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
var rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
135
|
+
return new Promise(function(resolve) {
|
|
136
|
+
rl.question('How are types distributed to the client?\n 1) workspace (default) - server writes types directly to your project\n 2) server - client fetches types via HTTP\nEnter choice [1]: ', function(answer) {
|
|
137
|
+
rl.close();
|
|
138
|
+
resolve(answer.trim() === '2' ? 'server' : 'workspace');
|
|
139
|
+
});
|
|
140
|
+
}).then(function(distribution) {
|
|
141
|
+
if (distribution === 'workspace') {
|
|
142
|
+
var config = { distribution: 'workspace' };
|
|
143
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n');
|
|
144
|
+
console.log(' \u2713 pyrpc-client.json created (workspace mode)');
|
|
145
|
+
console.log(' Your server\'s pyrpc dev command will write types directly.');
|
|
146
|
+
console.log(' Import: import { createClient, type Types } from "@pyrpc/client"');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
151
|
+
return new Promise(function(resolve) {
|
|
152
|
+
rl2.question('pyRPC backend URL (default: http://localhost:8000): ', function(answer) {
|
|
153
|
+
rl2.close();
|
|
154
|
+
resolve(answer.trim() || 'http://localhost:8000');
|
|
155
|
+
});
|
|
156
|
+
}).then(function(url) {
|
|
157
|
+
return fetchSchema(url).then(function(schemas) {
|
|
158
|
+
var code = generate(schemas);
|
|
159
|
+
fs.mkdirSync(path.dirname(TYPES_FILE), { recursive: true });
|
|
160
|
+
fs.writeFileSync(TYPES_FILE, code, 'utf-8');
|
|
161
|
+
console.log(' \u2713 @pyrpc/types: generated for ' + Object.keys(schemas).length + ' procedures');
|
|
162
|
+
|
|
163
|
+
var config = { distribution: 'server', server_url: url };
|
|
164
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n');
|
|
165
|
+
console.log(' \u2713 pyrpc-client.json created (server mode)');
|
|
166
|
+
console.log(' Import: import { createClient, type Types } from "@pyrpc/client"');
|
|
167
|
+
}).catch(function(err) {
|
|
168
|
+
console.log(' \u2717 @pyrpc/types: could not connect (' + err.message + ')');
|
|
169
|
+
console.log(' Run later: npx pyrpc sync');
|
|
170
|
+
|
|
171
|
+
var config = { distribution: 'server', server_url: url };
|
|
172
|
+
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n');
|
|
173
|
+
console.log(' \u2713 pyrpc-client.json created (server mode)');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (require.main === module) {
|
|
180
|
+
main().catch(function(e) {});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { main, toTs, generate, fetchSchema, findProjectRoot, getConfigPath, TYPES_FILE };
|