@xrpckit/target-react-client 0.0.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.
@@ -0,0 +1,66 @@
1
+ import { BaseCodeGenerator, GeneratorConfig, GeneratedFiles, CodeWriter } from '@xrpckit/codegen';
2
+ import { ContractDefinition } from '@xrpckit/parser';
3
+
4
+ declare class ReactCodeGenerator extends BaseCodeGenerator {
5
+ private typeGenerator;
6
+ private clientGenerator;
7
+ private contractPath;
8
+ constructor(config: GeneratorConfig);
9
+ generate(contract: ContractDefinition): GeneratedFiles;
10
+ }
11
+
12
+ declare class ReactTypeGenerator {
13
+ private w;
14
+ private contractPath;
15
+ private outputDir;
16
+ constructor(contractPath: string, outputDir: string);
17
+ generateTypes(contract: ContractDefinition): string;
18
+ private generateEndpointTypes;
19
+ private getSchemaName;
20
+ private getTypeName;
21
+ private calculateRelativePath;
22
+ private toPascalCase;
23
+ private toCamelCase;
24
+ }
25
+
26
+ declare class ReactClientGenerator {
27
+ private w;
28
+ constructor();
29
+ generateClient(contract: ContractDefinition): string;
30
+ private generateClientConfig;
31
+ private generateCallRpcFunction;
32
+ private generateEndpointFunction;
33
+ private generateQueryHook;
34
+ private generateMutationHook;
35
+ private getFunctionName;
36
+ private getSchemaName;
37
+ private getTypeName;
38
+ private toPascalCase;
39
+ private toCamelCase;
40
+ }
41
+
42
+ /**
43
+ * TypeScript/React-specific code builder with fluent DSL for common patterns
44
+ */
45
+ declare class ReactBuilder extends CodeWriter {
46
+ l(text: string): this;
47
+ n(): this;
48
+ i(): this;
49
+ u(): this;
50
+ import(module: string, imports?: string[]): this;
51
+ type(name: string, definition: string): this;
52
+ const(name: string, value: string, exported?: boolean): this;
53
+ interface(name: string, fn: (b: this) => void): this;
54
+ function(signature: string, fn?: (b: this) => void): this;
55
+ asyncFunction(signature: string, fn: (b: this) => void): this;
56
+ hook(signature: string, fn: (b: this) => void): this;
57
+ return(value?: string): this;
58
+ comment(text: string): this;
59
+ blockComment(lines: string[]): this;
60
+ useState<T>(name: string, initialValue: string, type?: string): this;
61
+ useEffect(fn: (b: this) => void, deps?: string[]): this;
62
+ useRef<T>(name: string, initialValue: string, type?: string): this;
63
+ private toPascalCase;
64
+ }
65
+
66
+ export { ReactBuilder, ReactClientGenerator, ReactCodeGenerator, ReactTypeGenerator };
package/dist/index.js ADDED
@@ -0,0 +1,408 @@
1
+ // src/generator.ts
2
+ import { BaseCodeGenerator } from "@xrpckit/codegen";
3
+
4
+ // src/react-builder.ts
5
+ import { CodeWriter } from "@xrpckit/codegen";
6
+ var ReactBuilder = class extends CodeWriter {
7
+ // Short aliases
8
+ l(text) {
9
+ return this.writeLine(text);
10
+ }
11
+ n() {
12
+ return this.newLine();
13
+ }
14
+ i() {
15
+ return this.indent();
16
+ }
17
+ u() {
18
+ return this.unindent();
19
+ }
20
+ // TypeScript/React-specific patterns
21
+ import(module, imports) {
22
+ if (!imports || imports.length === 0) {
23
+ return this.l(`import '${module}';`).n();
24
+ }
25
+ const valueImports = [];
26
+ const typeImports = [];
27
+ for (const imp of imports) {
28
+ if (imp.startsWith("type ")) {
29
+ typeImports.push(imp.replace("type ", ""));
30
+ } else {
31
+ valueImports.push(imp);
32
+ }
33
+ }
34
+ if (valueImports.length > 0 && typeImports.length > 0) {
35
+ this.l(`import { ${valueImports.join(", ")}, type ${typeImports.join(", type ")} } from '${module}';`).n();
36
+ } else if (valueImports.length > 0) {
37
+ this.l(`import { ${valueImports.join(", ")} } from '${module}';`).n();
38
+ } else if (typeImports.length > 0) {
39
+ this.l(`import type { ${typeImports.join(", ")} } from '${module}';`).n();
40
+ }
41
+ return this;
42
+ }
43
+ type(name, definition) {
44
+ return this.l(`export type ${name} = ${definition};`).n();
45
+ }
46
+ const(name, value, exported = true) {
47
+ const exportKeyword = exported ? "export " : "";
48
+ return this.l(`${exportKeyword}const ${name} = ${value};`).n();
49
+ }
50
+ interface(name, fn) {
51
+ this.l(`export interface ${name} {`).i();
52
+ fn(this);
53
+ return this.u().l("}").n();
54
+ }
55
+ function(signature, fn) {
56
+ if (fn) {
57
+ this.write(`export function ${signature} {`);
58
+ this.indent();
59
+ fn(this);
60
+ this.unindent();
61
+ this.writeLine("}");
62
+ this.newLine();
63
+ } else {
64
+ this.l(`export function ${signature};`);
65
+ this.n();
66
+ }
67
+ return this;
68
+ }
69
+ asyncFunction(signature, fn) {
70
+ this.write(`export async function ${signature} {`);
71
+ this.indent();
72
+ fn(this);
73
+ this.unindent();
74
+ this.writeLine("}");
75
+ this.newLine();
76
+ return this;
77
+ }
78
+ hook(signature, fn) {
79
+ return this.function(signature, fn);
80
+ }
81
+ return(value) {
82
+ return value ? this.l(`return ${value};`) : this.l("return;");
83
+ }
84
+ comment(text) {
85
+ return this.l(`// ${text}`);
86
+ }
87
+ blockComment(lines) {
88
+ this.l("/*");
89
+ for (const line of lines) {
90
+ this.l(` * ${line}`);
91
+ }
92
+ return this.l(" */");
93
+ }
94
+ // React-specific patterns
95
+ useState(name, initialValue, type) {
96
+ const typeParam = type ? `<${type}>` : "";
97
+ return this.l(`const [${name}, set${this.toPascalCase(name)}] = useState${typeParam}(${initialValue});`);
98
+ }
99
+ useEffect(fn, deps) {
100
+ this.l("useEffect(() => {").i();
101
+ fn(this);
102
+ this.u();
103
+ if (deps && deps.length > 0) {
104
+ return this.l(`}, [${deps.join(", ")}]);`).n();
105
+ }
106
+ return this.l("}, []);").n();
107
+ }
108
+ useRef(name, initialValue, type) {
109
+ const typeParam = type ? `<${type}>` : "";
110
+ return this.l(`const ${name}Ref = useRef${typeParam}(${initialValue});`);
111
+ }
112
+ toPascalCase(str) {
113
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
114
+ }
115
+ };
116
+
117
+ // src/type-generator.ts
118
+ import { relative, dirname } from "path";
119
+ var ReactTypeGenerator = class {
120
+ w;
121
+ contractPath;
122
+ outputDir;
123
+ constructor(contractPath, outputDir) {
124
+ this.w = new ReactBuilder();
125
+ this.contractPath = contractPath;
126
+ this.outputDir = outputDir;
127
+ }
128
+ generateTypes(contract) {
129
+ const w = this.w.reset();
130
+ const relativePath = this.calculateRelativePath(this.contractPath, this.outputDir);
131
+ w.import(relativePath, ["router"]);
132
+ w.import("@xrpckit/schema", ["type InferInput", "type InferOutput"]);
133
+ w.n();
134
+ for (const endpoint of contract.endpoints) {
135
+ this.generateEndpointTypes(endpoint, w);
136
+ }
137
+ return w.toString();
138
+ }
139
+ generateEndpointTypes(endpoint, w) {
140
+ const inputSchemaName = this.getSchemaName(endpoint, "input");
141
+ const outputSchemaName = this.getSchemaName(endpoint, "output");
142
+ const inputTypeName = this.getTypeName(endpoint, "Input");
143
+ const outputTypeName = this.getTypeName(endpoint, "Output");
144
+ const [groupName, endpointName] = endpoint.fullName.split(".");
145
+ const routerPath = `router.${groupName}.${endpointName}`;
146
+ w.const(inputSchemaName, `${routerPath}.input`, true);
147
+ w.const(outputSchemaName, `${routerPath}.output`, true);
148
+ w.type(inputTypeName, `InferInput<typeof ${routerPath}>`);
149
+ w.type(outputTypeName, `InferOutput<typeof ${routerPath}>`);
150
+ w.n();
151
+ }
152
+ getSchemaName(endpoint, suffix) {
153
+ const parts = endpoint.fullName.split(".");
154
+ const groupName = this.toCamelCase(parts[0]);
155
+ const endpointName = this.toCamelCase(parts[1]);
156
+ return `${groupName}${this.toPascalCase(endpointName)}${this.toPascalCase(suffix)}Schema`;
157
+ }
158
+ getTypeName(endpoint, suffix) {
159
+ const parts = endpoint.fullName.split(".");
160
+ const groupName = this.toPascalCase(parts[0]);
161
+ const endpointName = this.toPascalCase(parts[1]);
162
+ return `${groupName}${endpointName}${suffix}`;
163
+ }
164
+ calculateRelativePath(contractPath, outputDir) {
165
+ const contractPathWithoutExt = contractPath.replace(/\.ts$/, "");
166
+ const contractDir = dirname(contractPathWithoutExt);
167
+ const contractFile = contractPathWithoutExt.split("/").pop() || "api";
168
+ const relativePath = relative(outputDir, contractDir);
169
+ if (relativePath === "" || relativePath === ".") {
170
+ return `./${contractFile}`;
171
+ }
172
+ const normalizedPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
173
+ return `${normalizedPath}/${contractFile}`;
174
+ }
175
+ toPascalCase(str) {
176
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
177
+ }
178
+ toCamelCase(str) {
179
+ const pascal = this.toPascalCase(str);
180
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
181
+ }
182
+ };
183
+
184
+ // src/client-generator.ts
185
+ var ReactClientGenerator = class {
186
+ w;
187
+ constructor() {
188
+ this.w = new ReactBuilder();
189
+ }
190
+ generateClient(contract) {
191
+ const w = this.w.reset();
192
+ w.import("react", ["useState", "useEffect", "useRef"]);
193
+ const schemaImports = /* @__PURE__ */ new Set();
194
+ const typeImports = /* @__PURE__ */ new Set();
195
+ for (const ep of contract.endpoints) {
196
+ schemaImports.add(this.getSchemaName(ep, "input"));
197
+ schemaImports.add(this.getSchemaName(ep, "output"));
198
+ typeImports.add(this.getTypeName(ep, "Input"));
199
+ typeImports.add(this.getTypeName(ep, "Output"));
200
+ }
201
+ const allImports = [
202
+ ...Array.from(schemaImports),
203
+ ...Array.from(typeImports).map((t) => `type ${t}`)
204
+ ];
205
+ w.import("./types", allImports);
206
+ w.import("zod", ["z"]);
207
+ w.n();
208
+ this.generateClientConfig(w);
209
+ w.n();
210
+ this.generateCallRpcFunction(w);
211
+ for (const endpoint of contract.endpoints) {
212
+ w.n();
213
+ this.generateEndpointFunction(endpoint, w);
214
+ }
215
+ w.n();
216
+ w.comment("React Hooks");
217
+ w.n();
218
+ for (const endpoint of contract.endpoints) {
219
+ if (endpoint.type === "query") {
220
+ this.generateQueryHook(endpoint, w);
221
+ } else {
222
+ this.generateMutationHook(endpoint, w);
223
+ }
224
+ }
225
+ return w.toString();
226
+ }
227
+ generateClientConfig(w) {
228
+ w.interface("XRpcClientConfig", (b) => {
229
+ b.l("baseUrl: string;").l("validateInputs?: boolean;").l("validateOutputs?: boolean;").l("headers?: Record<string, string>;");
230
+ });
231
+ }
232
+ generateCallRpcFunction(w) {
233
+ w.comment("Base RPC call function (pure, no React dependencies)");
234
+ w.n();
235
+ w.asyncFunction(
236
+ "callRpc<T>(config: XRpcClientConfig, method: string, params: unknown, options?: { inputSchema?: z.ZodType; outputSchema?: z.ZodType; signal?: AbortSignal })",
237
+ (b) => {
238
+ b.comment("Validate input if enabled");
239
+ b.l("let validatedParams = params;");
240
+ b.l("if (config.validateInputs && options?.inputSchema) {");
241
+ b.i().l("validatedParams = options.inputSchema.parse(params);");
242
+ b.u().l("}").n();
243
+ b.comment("Make HTTP request");
244
+ b.l("const response = await fetch(config.baseUrl, {");
245
+ b.i().l("method: 'POST',").l("headers: {");
246
+ b.i().l("'Content-Type': 'application/json',").l("...config.headers,");
247
+ b.u().l("},").l("body: JSON.stringify({ method, params: validatedParams }),").l("signal: options?.signal,");
248
+ b.u().l("});").n();
249
+ b.comment("Handle errors");
250
+ b.l("if (!response.ok) {");
251
+ b.i().l("const error = await response.json().catch(() => ({ error: { message: response.statusText } }));").l("throw new Error(error.error?.message || `RPC call failed: ${response.statusText}`);");
252
+ b.u().l("}").n();
253
+ b.comment("Parse response");
254
+ b.l("const result = await response.json();");
255
+ b.comment("Handle JSON-RPC response format");
256
+ b.l("if (result.error) {");
257
+ b.i().l("throw new Error(result.error.message || result.error);");
258
+ b.u().l("}");
259
+ b.l("const data = result.result;").n();
260
+ b.comment("Validate output if enabled");
261
+ b.l("if (config.validateOutputs && options?.outputSchema) {");
262
+ b.i().l("return options.outputSchema.parse(data);");
263
+ b.u().l("}").n();
264
+ b.l("return data;");
265
+ }
266
+ );
267
+ }
268
+ generateEndpointFunction(endpoint, w) {
269
+ const functionName = this.getFunctionName(endpoint);
270
+ const inputType = this.getTypeName(endpoint, "Input");
271
+ const outputType = this.getTypeName(endpoint, "Output");
272
+ const inputSchema = this.getSchemaName(endpoint, "input");
273
+ const outputSchema = this.getSchemaName(endpoint, "output");
274
+ w.comment(`Type-safe wrapper for ${endpoint.fullName}`);
275
+ w.n();
276
+ w.asyncFunction(
277
+ `${functionName}(config: XRpcClientConfig, input: ${inputType}, options?: { signal?: AbortSignal })`,
278
+ (b) => {
279
+ b.l(`return callRpc<${outputType}>(`);
280
+ b.i().l("config,").l(`'${endpoint.fullName}',`).l("input,").l("{").i().l(`inputSchema: ${inputSchema},`).l(`outputSchema: ${outputSchema},`).l("signal: options?.signal,").u().l("}").u().l(");");
281
+ }
282
+ );
283
+ }
284
+ generateQueryHook(endpoint, w) {
285
+ const parts = endpoint.fullName.split(".");
286
+ const groupName = this.toPascalCase(parts[0]);
287
+ const endpointName = this.toPascalCase(parts[1]);
288
+ const hookName = `use${groupName}${endpointName}`;
289
+ const inputType = this.getTypeName(endpoint, "Input");
290
+ const outputType = this.getTypeName(endpoint, "Output");
291
+ const functionName = this.getFunctionName(endpoint);
292
+ w.comment(`React hook for ${endpoint.fullName} query`);
293
+ w.n();
294
+ w.hook(
295
+ `${hookName}(config: XRpcClientConfig, input: ${inputType}, options?: { enabled?: boolean })`,
296
+ (b) => {
297
+ b.l(`const [data, setData] = useState<${outputType} | null>(null);`);
298
+ b.l("const [loading, setLoading] = useState(false);");
299
+ b.l("const [error, setError] = useState<Error | null>(null);");
300
+ b.l("const abortControllerRef = useRef<AbortController | null>(null);").n();
301
+ b.l("useEffect(() => {");
302
+ b.i();
303
+ b.comment("Skip if disabled");
304
+ b.l("if (options?.enabled === false) return;").n();
305
+ b.comment("Cancel previous request");
306
+ b.l("if (abortControllerRef.current) {");
307
+ b.i().l("abortControllerRef.current.abort();");
308
+ b.u().l("}").n();
309
+ b.comment("Create new AbortController");
310
+ b.l("const abortController = new AbortController();");
311
+ b.l("abortControllerRef.current = abortController;").n();
312
+ b.l("setLoading(true);");
313
+ b.l("setError(null);").n();
314
+ b.l(`${functionName}(config, input, { signal: abortController.signal })`);
315
+ b.i().l(".then(setData)").l(".catch((err) => {");
316
+ b.i().l("if (err.name !== 'AbortError') {");
317
+ b.i().l("setError(err);");
318
+ b.u().l("}");
319
+ b.u().l("})").l(".finally(() => {");
320
+ b.i().l("if (!abortController.signal.aborted) {");
321
+ b.i().l("setLoading(false);");
322
+ b.u().l("}");
323
+ b.u().l("});").n();
324
+ b.comment("Cleanup on unmount or input change");
325
+ b.l("return () => {");
326
+ b.i().l("abortController.abort();");
327
+ b.u().l("};");
328
+ b.u();
329
+ b.comment("Note: input is serialized for stable dependency comparison");
330
+ b.l("}, [config.baseUrl, JSON.stringify(input), options?.enabled]);").n();
331
+ b.l("return { data, loading, error };");
332
+ }
333
+ );
334
+ }
335
+ generateMutationHook(endpoint, w) {
336
+ const parts = endpoint.fullName.split(".");
337
+ const groupName = this.toPascalCase(parts[0]);
338
+ const endpointName = this.toPascalCase(parts[1]);
339
+ const hookName = `use${groupName}${endpointName}`;
340
+ const inputType = this.getTypeName(endpoint, "Input");
341
+ const outputType = this.getTypeName(endpoint, "Output");
342
+ const functionName = this.getFunctionName(endpoint);
343
+ w.comment(`React hook for ${endpoint.fullName} mutation`);
344
+ w.n();
345
+ w.hook(`${hookName}(config: XRpcClientConfig)`, (b) => {
346
+ b.l("const [loading, setLoading] = useState(false);");
347
+ b.l("const [error, setError] = useState<Error | null>(null);").n();
348
+ b.l(`const mutate = async (input: ${inputType}): Promise<${outputType}> => {`);
349
+ b.i().l("setLoading(true);").l("setError(null);").n().l("try {").i().l(`const result = await ${functionName}(config, input);`).l("return result;").u().l("} catch (err) {").i().l("setError(err as Error);").l("throw err;").u().l("} finally {").i().l("setLoading(false);").u().l("}");
350
+ b.u().l("};").n();
351
+ b.l("return { mutate, loading, error };");
352
+ });
353
+ }
354
+ getFunctionName(endpoint) {
355
+ const parts = endpoint.fullName.split(".");
356
+ const groupName = this.toCamelCase(parts[0]);
357
+ const endpointName = this.toCamelCase(parts[1]);
358
+ return `${groupName}${this.toPascalCase(endpointName)}`;
359
+ }
360
+ getSchemaName(endpoint, suffix) {
361
+ const parts = endpoint.fullName.split(".");
362
+ const groupName = this.toCamelCase(parts[0]);
363
+ const endpointName = this.toCamelCase(parts[1]);
364
+ return `${groupName}${this.toPascalCase(endpointName)}${this.toPascalCase(suffix)}Schema`;
365
+ }
366
+ getTypeName(endpoint, suffix) {
367
+ const parts = endpoint.fullName.split(".");
368
+ const groupName = this.toPascalCase(parts[0]);
369
+ const endpointName = this.toPascalCase(parts[1]);
370
+ return `${groupName}${endpointName}${suffix}`;
371
+ }
372
+ toPascalCase(str) {
373
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
374
+ }
375
+ toCamelCase(str) {
376
+ const pascal = this.toPascalCase(str);
377
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
378
+ }
379
+ };
380
+
381
+ // src/generator.ts
382
+ var ReactCodeGenerator = class extends BaseCodeGenerator {
383
+ typeGenerator;
384
+ clientGenerator;
385
+ contractPath;
386
+ constructor(config) {
387
+ super(config);
388
+ const contractPath = config.options?.contractPath || config.outputDir;
389
+ if (!contractPath) {
390
+ throw new Error("contractPath is required for React target. Pass it via GeneratorConfig.options.contractPath");
391
+ }
392
+ this.contractPath = contractPath;
393
+ this.typeGenerator = new ReactTypeGenerator(contractPath, config.outputDir);
394
+ this.clientGenerator = new ReactClientGenerator();
395
+ }
396
+ generate(contract) {
397
+ return {
398
+ types: this.typeGenerator.generateTypes(contract),
399
+ client: this.clientGenerator.generateClient(contract)
400
+ };
401
+ }
402
+ };
403
+ export {
404
+ ReactBuilder,
405
+ ReactClientGenerator,
406
+ ReactCodeGenerator,
407
+ ReactTypeGenerator
408
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@xrpckit/target-react-client",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mwesox/xrpc.git",
9
+ "directory": "packages/target-react-client"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "bun": "./src/index.ts",
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup src/index.ts --format esm --dts --clean"
28
+ },
29
+ "dependencies": {
30
+ "@xrpckit/codegen": "^0.0.1",
31
+ "@xrpckit/parser": "^0.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.0.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ }
41
+ }