@xrpckit/parser 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,97 @@
1
+ interface ContractDefinition {
2
+ routers: Router[];
3
+ types: TypeDefinition[];
4
+ endpoints: Endpoint[];
5
+ middleware?: MiddlewareDefinition[];
6
+ }
7
+ interface Router {
8
+ name: string;
9
+ endpointGroups: EndpointGroup[];
10
+ middleware?: MiddlewareDefinition[];
11
+ }
12
+ /**
13
+ * Middleware definition extracted from router
14
+ * Middleware functions are stored as metadata and implemented by users in generated code
15
+ */
16
+ interface MiddlewareDefinition {
17
+ name?: string;
18
+ }
19
+ interface EndpointGroup {
20
+ name: string;
21
+ endpoints: Endpoint[];
22
+ }
23
+ interface Endpoint {
24
+ name: string;
25
+ type: 'query' | 'mutation';
26
+ input: TypeReference;
27
+ output: TypeReference;
28
+ fullName: string;
29
+ }
30
+ interface TypeDefinition {
31
+ name: string;
32
+ kind: 'object' | 'array' | 'union' | 'primitive' | 'nullable' | 'optional' | 'enum' | 'literal' | 'record' | 'tuple' | 'date';
33
+ properties?: Property[];
34
+ elementType?: TypeReference;
35
+ baseType?: string;
36
+ unionTypes?: TypeReference[];
37
+ enumValues?: (string | number)[];
38
+ literalValue?: string | number | boolean;
39
+ keyType?: TypeReference;
40
+ valueType?: TypeReference;
41
+ tupleElements?: TypeReference[];
42
+ }
43
+ interface ValidationRules {
44
+ minLength?: number;
45
+ maxLength?: number;
46
+ email?: boolean;
47
+ url?: boolean;
48
+ uuid?: boolean;
49
+ regex?: string;
50
+ min?: number;
51
+ max?: number;
52
+ int?: boolean;
53
+ positive?: boolean;
54
+ negative?: boolean;
55
+ minItems?: number;
56
+ maxItems?: number;
57
+ }
58
+ interface Property {
59
+ name: string;
60
+ type: TypeReference;
61
+ required: boolean;
62
+ validation?: ValidationRules;
63
+ }
64
+ interface TypeReference {
65
+ name?: string;
66
+ kind: TypeDefinition['kind'];
67
+ baseType?: string | TypeReference;
68
+ elementType?: TypeReference;
69
+ properties?: Property[];
70
+ validation?: ValidationRules;
71
+ unionTypes?: TypeReference[];
72
+ enumValues?: (string | number)[];
73
+ literalValue?: string | number | boolean;
74
+ keyType?: TypeReference;
75
+ valueType?: TypeReference;
76
+ tupleElements?: TypeReference[];
77
+ }
78
+
79
+ /**
80
+ * Parses a contract file and returns a normalized contract definition.
81
+ *
82
+ * The contract file must export a router created with `createRouter()`.
83
+ * This function imports the file and extracts type information from Zod schemas.
84
+ *
85
+ * @param filePath - Path to the TypeScript file containing the router definition
86
+ * @returns A promise that resolves to a ContractDefinition containing all routers, endpoints, and types
87
+ * @throws Error if the file cannot be imported, doesn't export a router, or has invalid structure
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const contract = await parseContract('src/api.ts');
92
+ * console.log(`Found ${contract.endpoints.length} endpoints`);
93
+ * ```
94
+ */
95
+ declare function parseContract(filePath: string): Promise<ContractDefinition>;
96
+
97
+ export { type ContractDefinition, type Endpoint, type EndpointGroup, type MiddlewareDefinition, type Property, type Router, type TypeDefinition, type TypeReference, type ValidationRules, parseContract };
package/dist/index.js ADDED
@@ -0,0 +1,409 @@
1
+ // src/index.ts
2
+ import { resolve } from "path";
3
+ import { existsSync } from "fs";
4
+ import { dirname, join } from "path";
5
+
6
+ // src/zod-extractor.ts
7
+ import { z } from "zod";
8
+ var SAFE_INTEGER_MIN = Number.MIN_SAFE_INTEGER;
9
+ var SAFE_INTEGER_MAX = Number.MAX_SAFE_INTEGER;
10
+ function extractValidationRules(schema) {
11
+ const rules = {};
12
+ let hasRules = false;
13
+ let baseSchema = schema;
14
+ while (baseSchema instanceof z.ZodOptional || baseSchema instanceof z.ZodNullable) {
15
+ if (baseSchema instanceof z.ZodOptional) {
16
+ baseSchema = baseSchema.unwrap();
17
+ } else if (baseSchema instanceof z.ZodNullable) {
18
+ baseSchema = baseSchema.unwrap();
19
+ }
20
+ }
21
+ let jsonSchema;
22
+ try {
23
+ jsonSchema = baseSchema.toJSONSchema();
24
+ } catch (e) {
25
+ return void 0;
26
+ }
27
+ if (!jsonSchema || typeof jsonSchema !== "object") {
28
+ return void 0;
29
+ }
30
+ if (baseSchema instanceof z.ZodString) {
31
+ if (typeof jsonSchema.minLength === "number") {
32
+ rules.minLength = jsonSchema.minLength;
33
+ hasRules = true;
34
+ }
35
+ if (typeof jsonSchema.maxLength === "number") {
36
+ rules.maxLength = jsonSchema.maxLength;
37
+ hasRules = true;
38
+ }
39
+ if (jsonSchema.format === "email") {
40
+ rules.email = true;
41
+ hasRules = true;
42
+ }
43
+ if (jsonSchema.format === "url" || jsonSchema.format === "uri") {
44
+ rules.url = true;
45
+ hasRules = true;
46
+ }
47
+ if (jsonSchema.format === "uuid") {
48
+ rules.uuid = true;
49
+ hasRules = true;
50
+ }
51
+ if (jsonSchema.pattern && typeof jsonSchema.pattern === "string") {
52
+ rules.regex = jsonSchema.pattern;
53
+ hasRules = true;
54
+ }
55
+ } else if (baseSchema instanceof z.ZodNumber) {
56
+ const isInteger = jsonSchema.type === "integer";
57
+ if (typeof jsonSchema.minimum === "number") {
58
+ if (!isInteger || jsonSchema.minimum !== SAFE_INTEGER_MIN) {
59
+ rules.min = jsonSchema.minimum;
60
+ hasRules = true;
61
+ }
62
+ }
63
+ if (typeof jsonSchema.maximum === "number") {
64
+ if (!isInteger || jsonSchema.maximum !== SAFE_INTEGER_MAX) {
65
+ rules.max = jsonSchema.maximum;
66
+ hasRules = true;
67
+ }
68
+ }
69
+ if (isInteger) {
70
+ rules.int = true;
71
+ hasRules = true;
72
+ }
73
+ if (jsonSchema.exclusiveMinimum === 0 || typeof jsonSchema.minimum === "number" && jsonSchema.minimum > 0 && (!isInteger || jsonSchema.minimum !== SAFE_INTEGER_MIN)) {
74
+ rules.positive = true;
75
+ hasRules = true;
76
+ }
77
+ if (jsonSchema.exclusiveMaximum === 0 || typeof jsonSchema.maximum === "number" && jsonSchema.maximum < 0 && (!isInteger || jsonSchema.maximum !== SAFE_INTEGER_MAX)) {
78
+ rules.negative = true;
79
+ hasRules = true;
80
+ }
81
+ } else if (baseSchema instanceof z.ZodArray) {
82
+ if (typeof jsonSchema.minItems === "number") {
83
+ rules.minItems = jsonSchema.minItems;
84
+ hasRules = true;
85
+ }
86
+ if (typeof jsonSchema.maxItems === "number") {
87
+ rules.maxItems = jsonSchema.maxItems;
88
+ hasRules = true;
89
+ }
90
+ }
91
+ return hasRules ? rules : void 0;
92
+ }
93
+ function extractTypeInfo(schema) {
94
+ if (schema instanceof z.ZodOptional) {
95
+ const unwrapped = schema.unwrap();
96
+ return {
97
+ kind: "optional",
98
+ baseType: extractTypeInfo(unwrapped)
99
+ };
100
+ }
101
+ if (schema instanceof z.ZodNullable) {
102
+ const unwrapped = schema.unwrap();
103
+ return {
104
+ kind: "nullable",
105
+ baseType: extractTypeInfo(unwrapped)
106
+ };
107
+ }
108
+ if (schema instanceof z.ZodObject) {
109
+ const shape = schema.shape;
110
+ const properties = [];
111
+ for (const [key, value] of Object.entries(shape)) {
112
+ const valueType = extractTypeInfo(value);
113
+ const isOptional = value instanceof z.ZodOptional;
114
+ const validation = extractValidationRules(value);
115
+ properties.push({
116
+ name: key,
117
+ type: valueType,
118
+ required: !isOptional,
119
+ validation
120
+ });
121
+ }
122
+ return {
123
+ kind: "object",
124
+ properties
125
+ };
126
+ }
127
+ if (schema instanceof z.ZodArray) {
128
+ const elementType = extractTypeInfo(schema.element);
129
+ const validation = extractValidationRules(schema);
130
+ return {
131
+ kind: "array",
132
+ elementType,
133
+ validation
134
+ };
135
+ }
136
+ if (schema instanceof z.ZodString) {
137
+ return {
138
+ kind: "primitive",
139
+ baseType: "string"
140
+ };
141
+ }
142
+ if (schema instanceof z.ZodNumber) {
143
+ return {
144
+ kind: "primitive",
145
+ baseType: "number"
146
+ };
147
+ }
148
+ if (schema instanceof z.ZodBoolean) {
149
+ return {
150
+ kind: "primitive",
151
+ baseType: "boolean"
152
+ };
153
+ }
154
+ if (schema instanceof z.ZodUnion) {
155
+ const options = schema.options;
156
+ return {
157
+ kind: "union",
158
+ unionTypes: options.map((opt) => extractTypeInfo(opt))
159
+ };
160
+ }
161
+ if (schema instanceof z.ZodEnum) {
162
+ return {
163
+ kind: "enum",
164
+ enumValues: schema.options
165
+ };
166
+ }
167
+ if (schema instanceof z.ZodLiteral) {
168
+ return {
169
+ kind: "literal",
170
+ literalValue: schema.value,
171
+ baseType: typeof schema.value
172
+ };
173
+ }
174
+ if (schema instanceof z.ZodRecord) {
175
+ return {
176
+ kind: "record",
177
+ keyType: { kind: "primitive", baseType: "string" },
178
+ valueType: extractTypeInfo(schema.valueSchema)
179
+ };
180
+ }
181
+ if (schema instanceof z.ZodTuple) {
182
+ return {
183
+ kind: "tuple",
184
+ tupleElements: schema.items.map((item) => extractTypeInfo(item))
185
+ };
186
+ }
187
+ if (schema instanceof z.ZodDate) {
188
+ return {
189
+ kind: "date",
190
+ baseType: "date"
191
+ };
192
+ }
193
+ return {
194
+ kind: "primitive",
195
+ baseType: "unknown"
196
+ };
197
+ }
198
+ function generateTypeName(prefix, suffix) {
199
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
200
+ return capitalize(prefix) + capitalize(suffix);
201
+ }
202
+
203
+ // src/index.ts
204
+ import { getRouterMiddleware } from "@xrpckit/schema";
205
+ async function importWithTimeout(path, timeout = 5e3) {
206
+ const timeoutPromise = new Promise(
207
+ (_, reject) => setTimeout(() => reject(new Error(`Import timeout after ${timeout}ms`)), timeout)
208
+ );
209
+ return Promise.race([import(path), timeoutPromise]);
210
+ }
211
+ async function parseContract(filePath) {
212
+ const absolutePath = resolve(filePath);
213
+ let routerModule;
214
+ try {
215
+ routerModule = await importWithTimeout(absolutePath);
216
+ } catch (error) {
217
+ if (error instanceof Error) {
218
+ if (error.message.includes("timeout")) {
219
+ throw new Error(
220
+ `Contract file import timed out: ${filePath}
221
+ This may indicate circular dependencies or slow initialization.
222
+ Ensure the file exports a router synchronously.`
223
+ );
224
+ }
225
+ if (error.message.includes("Cannot find module")) {
226
+ throw new Error(
227
+ `Failed to import contract file: ${filePath}
228
+ Error: ${error.message}
229
+ Make sure the file exists and all dependencies are installed.`
230
+ );
231
+ }
232
+ if (error.message.includes("Unexpected token") || error.message.includes("SyntaxError")) {
233
+ throw new Error(
234
+ `Syntax error in contract file: ${filePath}
235
+ Error: ${error.message}
236
+ Please check the file for syntax errors.`
237
+ );
238
+ }
239
+ throw new Error(
240
+ `Failed to parse contract file: ${filePath}
241
+ Error: ${error.message}`
242
+ );
243
+ }
244
+ throw error;
245
+ }
246
+ if (!routerModule.router) {
247
+ if ("router" in routerModule) {
248
+ throw new Error(
249
+ `Invalid router export in ${filePath}.
250
+ The router export exists but is not a valid RouterDefinition.
251
+ Make sure you're using: export const router = createRouter({ ... });`
252
+ );
253
+ }
254
+ const exports = Object.keys(routerModule);
255
+ if (exports.length === 0) {
256
+ throw new Error(
257
+ `No exports found in ${filePath}.
258
+ The contract file must export a router. Example:
259
+ export const router = createRouter({ ... });`
260
+ );
261
+ }
262
+ const possibleExports = exports.filter((e) => e.toLowerCase().includes("router"));
263
+ if (possibleExports.length > 0) {
264
+ throw new Error(
265
+ `No router export found in ${filePath}.
266
+ Found exports: ${exports.join(", ")}
267
+ Did you mean to export one of these? Make sure to use: export const router = createRouter({ ... });`
268
+ );
269
+ }
270
+ throw new Error(
271
+ `No router export found in ${filePath}.
272
+ The contract file must export a router. Example:
273
+ export const router = createRouter({ ... });
274
+ Found exports: ${exports.length > 0 ? exports.join(", ") : "none"}`
275
+ );
276
+ }
277
+ const routerDef = routerModule.router;
278
+ try {
279
+ return buildContractDefinition(routerDef);
280
+ } catch (error) {
281
+ if (error instanceof Error) {
282
+ throw new Error(
283
+ `Failed to build contract from router definition: ${error.message}
284
+ File: ${filePath}`
285
+ );
286
+ }
287
+ throw error;
288
+ }
289
+ }
290
+ function buildContractDefinition(routerDef) {
291
+ const routers = [];
292
+ const endpoints = [];
293
+ const typeMap = /* @__PURE__ */ new Map();
294
+ if (!routerDef || typeof routerDef !== "object") {
295
+ throw new Error("Router definition must be an object. Use: createRouter({ ... })");
296
+ }
297
+ const middleware = getRouterMiddleware(routerDef);
298
+ const middlewareDefinitions = middleware?.map((_, index) => ({
299
+ name: `middleware_${index}`
300
+ })) || [];
301
+ const router = {
302
+ name: "router",
303
+ endpointGroups: [],
304
+ middleware: middlewareDefinitions.length > 0 ? middlewareDefinitions : void 0
305
+ };
306
+ for (const [groupName, groupDef] of Object.entries(routerDef)) {
307
+ if (!groupDef || typeof groupDef !== "object") {
308
+ throw new Error(
309
+ `Invalid endpoint group "${groupName}". Endpoint groups must be created with createEndpoint({ ... }).`
310
+ );
311
+ }
312
+ const endpointGroup = {
313
+ name: groupName,
314
+ endpoints: []
315
+ };
316
+ for (const [endpointName, endpointDef] of Object.entries(groupDef)) {
317
+ const fullName = `${groupName}.${endpointName}`;
318
+ const epDef = endpointDef;
319
+ if (!epDef || typeof epDef !== "object") {
320
+ throw new Error(
321
+ `Invalid endpoint "${fullName}". Endpoints must be created with query({ ... }) or mutation({ ... }).`
322
+ );
323
+ }
324
+ if (!epDef.type || epDef.type !== "query" && epDef.type !== "mutation") {
325
+ throw new Error(
326
+ `Invalid endpoint type for "${fullName}". Type must be "query" or "mutation", got: ${epDef.type}`
327
+ );
328
+ }
329
+ if (!epDef.input) {
330
+ throw new Error(
331
+ `Endpoint "${fullName}" is missing input schema. Use: ${epDef.type}({ input: z.object({ ... }), output: z.object({ ... }) })`
332
+ );
333
+ }
334
+ if (!epDef.output) {
335
+ throw new Error(
336
+ `Endpoint "${fullName}" is missing output schema. Use: ${epDef.type}({ input: z.object({ ... }), output: z.object({ ... }) })`
337
+ );
338
+ }
339
+ try {
340
+ const inputType = extractTypeInfo(epDef.input);
341
+ const inputTypeName = generateTypeName(groupName, endpointName) + "Input";
342
+ addTypeDefinition(typeMap, inputTypeName, inputType);
343
+ const outputType = extractTypeInfo(epDef.output);
344
+ const outputTypeName = generateTypeName(groupName, endpointName) + "Output";
345
+ addTypeDefinition(typeMap, outputTypeName, outputType);
346
+ const endpoint = {
347
+ name: endpointName,
348
+ type: epDef.type,
349
+ input: { name: inputTypeName, ...inputType },
350
+ output: { name: outputTypeName, ...outputType },
351
+ fullName
352
+ };
353
+ endpointGroup.endpoints.push(endpoint);
354
+ endpoints.push(endpoint);
355
+ } catch (error) {
356
+ if (error instanceof Error) {
357
+ throw new Error(
358
+ `Failed to extract type information for endpoint "${fullName}": ${error.message}`
359
+ );
360
+ }
361
+ throw error;
362
+ }
363
+ }
364
+ if (endpointGroup.endpoints.length === 0) {
365
+ throw new Error(
366
+ `Endpoint group "${groupName}" has no endpoints. Add endpoints using: createEndpoint({ endpointName: query({ ... }) })`
367
+ );
368
+ }
369
+ router.endpointGroups.push(endpointGroup);
370
+ }
371
+ if (router.endpointGroups.length === 0) {
372
+ throw new Error(
373
+ "Router has no endpoint groups. Add endpoint groups using: createRouter({ groupName: createEndpoint({ ... }) })"
374
+ );
375
+ }
376
+ routers.push(router);
377
+ return {
378
+ routers,
379
+ types: Array.from(typeMap.values()),
380
+ endpoints,
381
+ middleware: middlewareDefinitions.length > 0 ? middlewareDefinitions : void 0
382
+ };
383
+ }
384
+ function addTypeDefinition(typeMap, name, typeRef) {
385
+ if (typeMap.has(name)) {
386
+ return;
387
+ }
388
+ const typeDef = {
389
+ name,
390
+ kind: typeRef.kind,
391
+ properties: typeRef.properties,
392
+ elementType: typeRef.elementType,
393
+ baseType: typeRef.baseType
394
+ };
395
+ typeMap.set(name, typeDef);
396
+ if (typeRef.properties) {
397
+ for (const prop of typeRef.properties) {
398
+ if (prop.type.name && !typeMap.has(prop.type.name)) {
399
+ addTypeDefinition(typeMap, prop.type.name, prop.type);
400
+ }
401
+ }
402
+ }
403
+ if (typeRef.elementType && typeRef.elementType.name && !typeMap.has(typeRef.elementType.name)) {
404
+ addTypeDefinition(typeMap, typeRef.elementType.name, typeRef.elementType);
405
+ }
406
+ }
407
+ export {
408
+ parseContract
409
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@xrpckit/parser",
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/parser"
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
+ "test": "bun test src"
29
+ },
30
+ "dependencies": {
31
+ "@xrpckit/schema": "^0.0.1",
32
+ "typescript": "^5.0.0",
33
+ "zod": "^4.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "tsup": "^8.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ }
42
+ }