atmx-cli 0.62.0 → 0.63.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,665 @@
|
|
|
1
|
+
Project Root: /Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli
|
|
2
|
+
Project Structure:
|
|
3
|
+
```
|
|
4
|
+
.
|
|
5
|
+
|-- package-lock.json
|
|
6
|
+
|-- package.json
|
|
7
|
+
|-- src
|
|
8
|
+
|-- generators
|
|
9
|
+
|-- model-generator.ts
|
|
10
|
+
|-- sdk-generator.ts
|
|
11
|
+
|-- utils.ts
|
|
12
|
+
|-- index.ts
|
|
13
|
+
|-- templates
|
|
14
|
+
|-- types.ts
|
|
15
|
+
|-- tsconfig.json
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
## File: package.json
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"name": "atmx-cli",
|
|
25
|
+
"version": "0.62.0",
|
|
26
|
+
"description": "",
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"atmx": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"type": "commonjs",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@iarna/toml": "^2.2.5",
|
|
40
|
+
"commander": "^14.0.3",
|
|
41
|
+
"fs-extra": "^11.3.4"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/fs-extra": "^11.0.4",
|
|
45
|
+
"@types/iarna__toml": "^2.0.5",
|
|
46
|
+
"@types/node": "^25.5.0",
|
|
47
|
+
"ts-node": "^10.9.2",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
---
|
|
54
|
+
## File: src/generators/model-generator.ts
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// FILE: atmx-cli/src/generators/model-generator.ts
|
|
58
|
+
import { AxiomEnum, AxiomModel, MultiIR } from "../types";
|
|
59
|
+
import { pascalCase, camelCase, mapTypeToTs } from "./utils";
|
|
60
|
+
|
|
61
|
+
export function generateModels(multiIr: MultiIR): string {
|
|
62
|
+
const sections: string[] = [
|
|
63
|
+
`// GENERATED CODE – DO NOT EDIT.\n/* eslint-disable @typescript-eslint/no-explicit-any */\n`,
|
|
64
|
+
`/* eslint-disable @typescript-eslint/no-namespace */\n`, // ✨ FIX: Disable namespace lint error
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
for (const [ns, ir] of Object.entries(multiIr)) {
|
|
68
|
+
const camelNs = camelCase(ns);
|
|
69
|
+
// ✨ FIX: Use proper TS namespaces
|
|
70
|
+
sections.push(`export namespace ${camelNs} {`);
|
|
71
|
+
|
|
72
|
+
const enumsList = Array.isArray(ir.enums)
|
|
73
|
+
? ir.enums
|
|
74
|
+
: Object.values(ir.enums || {});
|
|
75
|
+
const modelsList = Array.isArray(ir.models)
|
|
76
|
+
? ir.models
|
|
77
|
+
: Object.values(ir.models || {});
|
|
78
|
+
|
|
79
|
+
enumsList.forEach((en: any) => sections.push(generateEnum(en)));
|
|
80
|
+
modelsList.forEach((model: any) =>
|
|
81
|
+
sections.push(generateInterface(model, camelNs)),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
sections.push(`}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sections.push(generateMappers(multiIr));
|
|
88
|
+
return sections.join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function generateEnum(en: AxiomEnum): string {
|
|
92
|
+
const name = pascalCase(en.name);
|
|
93
|
+
const values = en.values.map((v) => ` ${pascalCase(v)}: "${v}"`).join(",\n");
|
|
94
|
+
return `
|
|
95
|
+
export const ${name} = {
|
|
96
|
+
${values}
|
|
97
|
+
} as const;
|
|
98
|
+
export type ${name} = typeof ${name}[keyof typeof ${name}];
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function generateInterface(model: AxiomModel, ns: string): string {
|
|
103
|
+
const name = pascalCase(model.name);
|
|
104
|
+
const fields = model.fields
|
|
105
|
+
.map((f) => {
|
|
106
|
+
const type = mapTypeToTs(f.typeRef, ns);
|
|
107
|
+
return ` ${camelCase(f.name)}${f.isOptional ? "?" : ""}: ${type};`;
|
|
108
|
+
})
|
|
109
|
+
.join("\n");
|
|
110
|
+
|
|
111
|
+
return `
|
|
112
|
+
export interface ${name} {
|
|
113
|
+
${fields}
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generateMappers(multiIr: MultiIR): string {
|
|
119
|
+
const lines: string[] = [`export const Mappers: Record<string, any> = {`];
|
|
120
|
+
|
|
121
|
+
for (const [ns, ir] of Object.entries(multiIr)) {
|
|
122
|
+
const camelNs = camelCase(ns);
|
|
123
|
+
lines.push(` ${camelNs}: {`);
|
|
124
|
+
|
|
125
|
+
const modelsList = Array.isArray(ir.models)
|
|
126
|
+
? ir.models
|
|
127
|
+
: Object.values(ir.models || {});
|
|
128
|
+
|
|
129
|
+
modelsList.forEach((model: any) => {
|
|
130
|
+
const name = pascalCase(model.name);
|
|
131
|
+
const fullType = `${camelNs}.${name}`;
|
|
132
|
+
|
|
133
|
+
lines.push(
|
|
134
|
+
` ${name}: {\n fromJson: (json: any): ${fullType} => ({`,
|
|
135
|
+
);
|
|
136
|
+
model.fields.forEach((f: any) => {
|
|
137
|
+
lines.push(
|
|
138
|
+
` ${camelCase(f.name)}: ${generateJsonLogic(f.typeRef, `json["${f.name}"]`, f.isOptional, "fromJson", camelNs)},`,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
lines.push(` }),\n toJson: (obj: any): any => ({`);
|
|
142
|
+
model.fields.forEach((f: any) => {
|
|
143
|
+
lines.push(
|
|
144
|
+
` "${f.name}": ${generateJsonLogic(f.typeRef, `obj.${camelCase(f.name)}`, f.isOptional, "toJson", camelNs)},`,
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
lines.push(` })\n },`);
|
|
148
|
+
});
|
|
149
|
+
lines.push(` },`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(`};\n`);
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function generateJsonLogic(
|
|
156
|
+
typeRef: any,
|
|
157
|
+
access: string,
|
|
158
|
+
isOpt: boolean,
|
|
159
|
+
mode: "fromJson" | "toJson",
|
|
160
|
+
ns: string,
|
|
161
|
+
): string {
|
|
162
|
+
const wrap = (logic: string) =>
|
|
163
|
+
isOpt ? `(${access} == null ? undefined : ${logic})` : logic;
|
|
164
|
+
if (!typeRef || !typeRef.kind) return access;
|
|
165
|
+
if (typeRef.kind === "dateTime")
|
|
166
|
+
return mode === "fromJson"
|
|
167
|
+
? wrap(`new Date(${access})`)
|
|
168
|
+
: wrap(`${access}.toISOString()`);
|
|
169
|
+
if (typeRef.kind === "bytes")
|
|
170
|
+
return mode === "fromJson"
|
|
171
|
+
? wrap(`new Uint8Array(${access})`)
|
|
172
|
+
: wrap(`Array.from(${access})`);
|
|
173
|
+
if (typeRef.kind === "named") {
|
|
174
|
+
const name = pascalCase(typeRef.value);
|
|
175
|
+
return wrap(
|
|
176
|
+
`(Mappers.${ns}["${name}"] ? Mappers.${ns}["${name}"].${mode}(${access}) : ${access})`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (typeRef.kind === "list")
|
|
180
|
+
return wrap(
|
|
181
|
+
`${access}.map((e: any) => ${generateJsonLogic(typeRef.value, "e", false, mode, ns)})`,
|
|
182
|
+
);
|
|
183
|
+
return access;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
---
|
|
188
|
+
## File: src/generators/sdk-generator.ts
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// FILE: atmx-cli/src/generators/sdk-generator.ts
|
|
192
|
+
import { AxiomEndpoint, AxiomParameter, MultiIR } from "../types";
|
|
193
|
+
import { pascalCase, camelCase, mapTypeToTs } from "./utils";
|
|
194
|
+
|
|
195
|
+
export function generateSdk(
|
|
196
|
+
multiIr: MultiIR,
|
|
197
|
+
isReact: boolean = false,
|
|
198
|
+
): string {
|
|
199
|
+
const lines: string[] = [
|
|
200
|
+
`// GENERATED CODE – DO NOT EDIT.`,
|
|
201
|
+
`/* eslint-disable @typescript-eslint/no-explicit-any */`,
|
|
202
|
+
`import * as models from './models';\n`,
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
if (isReact) {
|
|
206
|
+
// ✨ FIX: Auto-import the auth helpers to bind them to the module
|
|
207
|
+
lines.push(
|
|
208
|
+
`import { useAxiomQuery, useAxiomMutation, setAuthToken, clearAuthToken } from 'atmx-react';`,
|
|
209
|
+
);
|
|
210
|
+
lines.push(`import type { AxiomQueryDef } from 'atmx-react';\n`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const [ns, ir] of Object.entries(multiIr)) {
|
|
214
|
+
const camelNs = camelCase(ns);
|
|
215
|
+
lines.push(`export const ${camelNs}Module = {`);
|
|
216
|
+
|
|
217
|
+
// ✨ FIX: Safely bind auth tokens using the EXACT namespace string from the TOML file!
|
|
218
|
+
if (isReact) {
|
|
219
|
+
lines.push(` setAuthToken(methodName: string, token: string) {`);
|
|
220
|
+
lines.push(` setAuthToken("${ns}", methodName, token);`);
|
|
221
|
+
lines.push(` },`);
|
|
222
|
+
lines.push(` clearAuthToken(methodName: string) {`);
|
|
223
|
+
lines.push(` clearAuthToken("${ns}", methodName);`);
|
|
224
|
+
lines.push(` },`);
|
|
225
|
+
} else {
|
|
226
|
+
lines.push(` setAuthToken(methodName: string, token: string) {`);
|
|
227
|
+
lines.push(
|
|
228
|
+
` (window as any).atmx?.setAuthToken("${ns}", methodName, token);`,
|
|
229
|
+
);
|
|
230
|
+
lines.push(` },`);
|
|
231
|
+
lines.push(` clearAuthToken(methodName: string) {`);
|
|
232
|
+
lines.push(
|
|
233
|
+
` (window as any).atmx?.clearAuthToken("${ns}", methodName);`,
|
|
234
|
+
);
|
|
235
|
+
lines.push(` },`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const endpointsMap = ir.endpoints || {};
|
|
239
|
+
const endpoints = Array.isArray(endpointsMap)
|
|
240
|
+
? endpointsMap
|
|
241
|
+
: Object.values(endpointsMap);
|
|
242
|
+
|
|
243
|
+
endpoints.forEach((ep: any) => {
|
|
244
|
+
lines.push(generateEndpointMethod(ep, ns, camelNs, isReact));
|
|
245
|
+
});
|
|
246
|
+
lines.push(`};\n`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
lines.push(`export const sdk = {`);
|
|
250
|
+
for (const ns of Object.keys(multiIr)) {
|
|
251
|
+
lines.push(` ${camelCase(ns)}: ${camelCase(ns)}Module,`);
|
|
252
|
+
}
|
|
253
|
+
lines.push(`};\n`);
|
|
254
|
+
|
|
255
|
+
lines.push(`export const AxiomDefaultConfig = {`);
|
|
256
|
+
lines.push(` contracts: {`);
|
|
257
|
+
for (const ns of Object.keys(multiIr)) {
|
|
258
|
+
lines.push(` "${ns}": {`);
|
|
259
|
+
lines.push(` contractUrl: "/${ns}.axiom",`);
|
|
260
|
+
lines.push(` baseUrl: "http://localhost:8000"`);
|
|
261
|
+
lines.push(` },`);
|
|
262
|
+
}
|
|
263
|
+
lines.push(` }`);
|
|
264
|
+
lines.push(`};\n`);
|
|
265
|
+
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// FILE: atmx-cli/src/generators/sdk-generator.ts (Partial replacement)
|
|
270
|
+
|
|
271
|
+
function generateEndpointMethod(
|
|
272
|
+
ep: AxiomEndpoint,
|
|
273
|
+
ns: string,
|
|
274
|
+
camelNs: string,
|
|
275
|
+
isReact: boolean,
|
|
276
|
+
): string {
|
|
277
|
+
const rawParams = ep.parameters || [];
|
|
278
|
+
const params = Array.isArray(rawParams)
|
|
279
|
+
? rawParams
|
|
280
|
+
: Object.values(rawParams);
|
|
281
|
+
|
|
282
|
+
const isQuery = ep.method ? ep.method.toUpperCase() === "GET" : true;
|
|
283
|
+
const rawReturnType = mapTypeToTs(ep.returnType, camelNs);
|
|
284
|
+
const returnType =
|
|
285
|
+
rawReturnType === "void" || rawReturnType === "any"
|
|
286
|
+
? rawReturnType
|
|
287
|
+
: prefixModels(rawReturnType);
|
|
288
|
+
|
|
289
|
+
// Map camelCase TS args back to original IR snake_case names!
|
|
290
|
+
const argsMapping = params
|
|
291
|
+
.map(
|
|
292
|
+
(p: any) =>
|
|
293
|
+
`if (args && '${camelCase(p.name)}' in args) { mappedArgs["${p.name}"] = (args as any)["${camelCase(p.name)}"]; delete mappedArgs["${camelCase(p.name)}"]; }`,
|
|
294
|
+
)
|
|
295
|
+
.join("\n ");
|
|
296
|
+
|
|
297
|
+
if (params.length === 0) {
|
|
298
|
+
if (isReact) {
|
|
299
|
+
const decLogic = generateLambda(ep.returnType, "fromJson", camelNs);
|
|
300
|
+
return `
|
|
301
|
+
get${pascalCase(ep.name)}Def(args?: Record<string, any>): AxiomQueryDef<${returnType}> {
|
|
302
|
+
return {
|
|
303
|
+
namespace: "${ns}", name: "${ep.name}", endpointId: ${ep.id},
|
|
304
|
+
method: "${ep.method ? ep.method.toUpperCase() : "GET"}", path: "${ep.path}",
|
|
305
|
+
args: args || {}, decoder: ${decLogic}, serializer: (p: any) => p, isStream: ${ep.isStream === true}
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
use${pascalCase(ep.name)}${!isQuery ? "Mutation" : ""}(options?: { enabled?: boolean }) {
|
|
309
|
+
${isQuery ? `return useAxiomQuery<${returnType}>(this.get${pascalCase(ep.name)}Def(), options);` : `return useAxiomMutation<${returnType}, void | Record<string,any>>((a) => this.get${pascalCase(ep.name)}Def(a));`}
|
|
310
|
+
},`;
|
|
311
|
+
} else {
|
|
312
|
+
return `
|
|
313
|
+
${camelCase(ep.name)}(args?: Record<string, any>): string {
|
|
314
|
+
const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';
|
|
315
|
+
return \`${ns}.${ep.name}(\${argsStr})\`;
|
|
316
|
+
},`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const argType = `{ ${params.map((p: any) => `${camelCase(p.name)}${p.isOptional ? "?" : ""}: ${prefixModels(mapTypeToTs(p.typeRef, camelNs))}`).join(", ")} }`;
|
|
321
|
+
|
|
322
|
+
if (isReact) {
|
|
323
|
+
const bodyParam = params.find(
|
|
324
|
+
(p: any) => p.source === "body",
|
|
325
|
+
) as AxiomParameter;
|
|
326
|
+
const payloadLogic = bodyParam
|
|
327
|
+
? `const payload = (args as any)?.${camelCase(bodyParam.name)};`
|
|
328
|
+
: `const payload = undefined;`;
|
|
329
|
+
const decLogic = generateLambda(ep.returnType, "fromJson", camelNs);
|
|
330
|
+
const serLogic = bodyParam
|
|
331
|
+
? generateLambda(bodyParam.typeRef, "toJson", camelNs)
|
|
332
|
+
: `(p: any) => p`;
|
|
333
|
+
|
|
334
|
+
return `
|
|
335
|
+
get${pascalCase(ep.name)}Def(args?: ${argType}): AxiomQueryDef<${returnType}> {
|
|
336
|
+
${payloadLogic}
|
|
337
|
+
const mappedArgs: any = { ...(args || {}) };
|
|
338
|
+
${argsMapping}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
namespace: "${ns}", name: "${ep.name}", endpointId: ${ep.id},
|
|
342
|
+
method: "${ep.method ? ep.method.toUpperCase() : "GET"}", path: "${ep.path}",
|
|
343
|
+
payload: payload, args: mappedArgs, decoder: ${decLogic}, serializer: ${serLogic}, isStream: ${ep.isStream === true}
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
use${pascalCase(ep.name)}${!isQuery ? "Mutation" : ""}(args?: ${argType}, options?: { enabled?: boolean }) {
|
|
347
|
+
${isQuery ? `return useAxiomQuery<${returnType}>(this.get${pascalCase(ep.name)}Def(args), options);` : `return useAxiomMutation<${returnType}, ${argType}>((a) => this.get${pascalCase(ep.name)}Def(a || args));`}
|
|
348
|
+
},`;
|
|
349
|
+
} else {
|
|
350
|
+
return `
|
|
351
|
+
${camelCase(ep.name)}(args?: ${argType}): string {
|
|
352
|
+
const mappedArgs: any = { ...(args || {}) };
|
|
353
|
+
${argsMapping}
|
|
354
|
+
const argsStr = Object.keys(mappedArgs).length > 0 ? JSON.stringify(mappedArgs) : '';
|
|
355
|
+
return \`${ns}.${ep.name}(\${argsStr})\`;
|
|
356
|
+
},`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function generateLambda(
|
|
361
|
+
typeRef: any,
|
|
362
|
+
mode: "fromJson" | "toJson",
|
|
363
|
+
ns: string,
|
|
364
|
+
): string {
|
|
365
|
+
if (!typeRef || !typeRef.kind || typeRef.kind === "void")
|
|
366
|
+
return mode === "fromJson" ? `() => undefined` : `(p: any) => p`;
|
|
367
|
+
if (typeRef.kind === "list" && typeRef.value?.kind === "named")
|
|
368
|
+
return `(data: any[]) => data.map(models.Mappers.${ns}.${pascalCase(typeRef.value.value)}.${mode})`;
|
|
369
|
+
if (typeRef.kind === "named")
|
|
370
|
+
return `models.Mappers.${ns}.${pascalCase(typeRef.value)}.${mode}`;
|
|
371
|
+
return `(data: any) => data`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function prefixModels(type: string): string {
|
|
375
|
+
const primitives = [
|
|
376
|
+
"string",
|
|
377
|
+
"number",
|
|
378
|
+
"boolean",
|
|
379
|
+
"Date",
|
|
380
|
+
"Uint8Array",
|
|
381
|
+
"void",
|
|
382
|
+
"any",
|
|
383
|
+
];
|
|
384
|
+
if (!type || primitives.includes(type)) return type;
|
|
385
|
+
if (type.endsWith("[]")) return `${prefixModels(type.slice(0, -2))}[]`;
|
|
386
|
+
if (type.startsWith("models.")) return type;
|
|
387
|
+
return `models.${type}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
---
|
|
392
|
+
## File: src/generators/utils.ts
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
// FILE: atmx-cli/src/generators/utils.ts
|
|
396
|
+
export function pascalCase(str: string): string {
|
|
397
|
+
if (!str) return "";
|
|
398
|
+
return str
|
|
399
|
+
.split(/[_\-\s]+/)
|
|
400
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
401
|
+
.join("");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function camelCase(str: string): string {
|
|
405
|
+
const pascal = pascalCase(str);
|
|
406
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function normalizeIr(obj: any): any {
|
|
410
|
+
if (Array.isArray(obj)) return obj.map(normalizeIr);
|
|
411
|
+
if (obj !== null && typeof obj === "object") {
|
|
412
|
+
const newObj: any = {};
|
|
413
|
+
for (const key of Object.keys(obj)) {
|
|
414
|
+
const camelKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
|
415
|
+
newObj[camelKey] = normalizeIr(obj[key]);
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
newObj.endpoints &&
|
|
419
|
+
typeof newObj.endpoints === "object" &&
|
|
420
|
+
!Array.isArray(newObj.endpoints)
|
|
421
|
+
) {
|
|
422
|
+
newObj.endpoints = Object.values(newObj.endpoints);
|
|
423
|
+
}
|
|
424
|
+
if (
|
|
425
|
+
newObj.models &&
|
|
426
|
+
typeof newObj.models === "object" &&
|
|
427
|
+
!Array.isArray(newObj.models)
|
|
428
|
+
) {
|
|
429
|
+
newObj.models = Object.values(newObj.models);
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
newObj.enums &&
|
|
433
|
+
typeof newObj.enums === "object" &&
|
|
434
|
+
!Array.isArray(newObj.enums)
|
|
435
|
+
) {
|
|
436
|
+
newObj.enums = Object.values(newObj.enums);
|
|
437
|
+
}
|
|
438
|
+
if (Array.isArray(newObj.models)) {
|
|
439
|
+
newObj.models = newObj.models.map((model: any) => {
|
|
440
|
+
if (
|
|
441
|
+
model.fields &&
|
|
442
|
+
typeof model.fields === "object" &&
|
|
443
|
+
!Array.isArray(model.fields)
|
|
444
|
+
) {
|
|
445
|
+
model.fields = Object.values(model.fields);
|
|
446
|
+
}
|
|
447
|
+
return model;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return newObj;
|
|
451
|
+
}
|
|
452
|
+
return obj;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function mapTypeToTs(typeRef: any, ns?: string): string {
|
|
456
|
+
if (!typeRef || !typeRef.kind) return "any";
|
|
457
|
+
|
|
458
|
+
switch (typeRef.kind) {
|
|
459
|
+
case "string":
|
|
460
|
+
return "string";
|
|
461
|
+
case "int32":
|
|
462
|
+
case "int64":
|
|
463
|
+
case "float32":
|
|
464
|
+
case "float64":
|
|
465
|
+
return "number";
|
|
466
|
+
case "bool":
|
|
467
|
+
return "boolean";
|
|
468
|
+
case "dateTime":
|
|
469
|
+
return "Date";
|
|
470
|
+
case "bytes":
|
|
471
|
+
return "Uint8Array";
|
|
472
|
+
case "void":
|
|
473
|
+
return "void";
|
|
474
|
+
case "json":
|
|
475
|
+
return "any";
|
|
476
|
+
case "named":
|
|
477
|
+
const name = pascalCase(typeRef.value);
|
|
478
|
+
return ns ? `${ns}.${name}` : name;
|
|
479
|
+
case "list":
|
|
480
|
+
return `${mapTypeToTs(typeRef.value, ns)}[]`;
|
|
481
|
+
case "map":
|
|
482
|
+
const valType = typeRef.value?.[1]
|
|
483
|
+
? mapTypeToTs(typeRef.value[1], ns)
|
|
484
|
+
: "any";
|
|
485
|
+
return `Record<string, ${valType}>`;
|
|
486
|
+
default:
|
|
487
|
+
return "any";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
---
|
|
493
|
+
## File: src/index.ts
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
#!/usr/bin/env node
|
|
497
|
+
import { Command } from "commander";
|
|
498
|
+
import * as fs from "fs-extra";
|
|
499
|
+
import * as path from "path";
|
|
500
|
+
import * as toml from "@iarna/toml";
|
|
501
|
+
import { MultiIR } from "./types";
|
|
502
|
+
import { generateModels } from "./generators/model-generator";
|
|
503
|
+
import { generateSdk } from "./generators/sdk-generator";
|
|
504
|
+
import { normalizeIr } from "./generators/utils";
|
|
505
|
+
|
|
506
|
+
const program = new Command();
|
|
507
|
+
|
|
508
|
+
program
|
|
509
|
+
.name("atmx")
|
|
510
|
+
.description("Generate TypeScript SDK from AxiomDeps.toml")
|
|
511
|
+
.version("0.2.0");
|
|
512
|
+
|
|
513
|
+
program
|
|
514
|
+
.command("generate")
|
|
515
|
+
.requiredOption("-c, --config <path>", "Path to AxiomDeps.toml")
|
|
516
|
+
.requiredOption("-o, --output <dir>", "Output directory for generated files")
|
|
517
|
+
.option("-r, --react", "Generate React Hooks instead of Vanilla JS strings")
|
|
518
|
+
.action(async (options) => {
|
|
519
|
+
const configPath = path.resolve(options.config);
|
|
520
|
+
const outputDir = path.resolve(options.output);
|
|
521
|
+
|
|
522
|
+
if (!fs.existsSync(configPath)) {
|
|
523
|
+
console.error(`❌ Error: Config file not found at ${configPath}`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 1. Read and Parse TOML
|
|
528
|
+
const tomlString = await fs.readFile(configPath, "utf-8");
|
|
529
|
+
const rawConfig = toml.parse(tomlString) as any;
|
|
530
|
+
|
|
531
|
+
if (!rawConfig.contracts || Object.keys(rawConfig.contracts).length === 0) {
|
|
532
|
+
console.error("❌ Error: No contracts defined in AxiomDeps.toml.");
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const multiIr: MultiIR = {};
|
|
537
|
+
const projectRoot = path.dirname(configPath); // Frontend project root
|
|
538
|
+
|
|
539
|
+
// 2. Loop through contracts
|
|
540
|
+
for (const [namespace, contract] of Object.entries(rawConfig.contracts)) {
|
|
541
|
+
// Rust CLI safely copies files to `public/[namespace].axiom`
|
|
542
|
+
const axiomFilePath = path.resolve(
|
|
543
|
+
projectRoot,
|
|
544
|
+
`public/${namespace}.axiom`,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
if (!fs.existsSync(axiomFilePath)) {
|
|
548
|
+
console.warn(
|
|
549
|
+
`⚠️ Warning: Contract file not found at ${axiomFilePath}. Skipping...`,
|
|
550
|
+
);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const rawFile = await fs.readJSON(axiomFilePath);
|
|
555
|
+
if (!rawFile.ir) continue;
|
|
556
|
+
|
|
557
|
+
multiIr[namespace] = normalizeIr(rawFile.ir);
|
|
558
|
+
console.log(`✅ Loaded contract: [${namespace}] -> ${axiomFilePath}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
await fs.ensureDir(outputDir);
|
|
562
|
+
|
|
563
|
+
const modelsContent = generateModels(multiIr);
|
|
564
|
+
await fs.writeFile(path.join(outputDir, "models.ts"), modelsContent);
|
|
565
|
+
|
|
566
|
+
const sdkContent = generateSdk(multiIr, options.react);
|
|
567
|
+
await fs.writeFile(path.join(outputDir, "sdk.ts"), sdkContent);
|
|
568
|
+
|
|
569
|
+
console.log(
|
|
570
|
+
`\n🎉 ATMX Multi-Contract SDK generated successfully in ${outputDir}`,
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
program.parse();
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
---
|
|
578
|
+
## File: src/types.ts
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
export interface AxiomIR {
|
|
582
|
+
serviceName: string;
|
|
583
|
+
endpoints: AxiomEndpoint[];
|
|
584
|
+
models: Record<string, AxiomModel>;
|
|
585
|
+
enums: Record<string, AxiomEnum>;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export interface AxiomEndpoint {
|
|
589
|
+
id: number;
|
|
590
|
+
name: string;
|
|
591
|
+
path: string;
|
|
592
|
+
method: string;
|
|
593
|
+
parameters: AxiomParameter[];
|
|
594
|
+
returnType: AxiomTypeRef;
|
|
595
|
+
returnIsOptional: boolean;
|
|
596
|
+
isStream: boolean;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export interface AxiomParameter {
|
|
600
|
+
name: string;
|
|
601
|
+
source: "path" | "query" | "body";
|
|
602
|
+
typeRef: AxiomTypeRef;
|
|
603
|
+
isOptional: boolean;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export type AxiomTypeRef =
|
|
607
|
+
| { kind: "primitive" | "named"; value: string }
|
|
608
|
+
| { kind: "list"; value: AxiomTypeRef }
|
|
609
|
+
| { kind: "map"; value: [AxiomTypeRef, AxiomTypeRef] }
|
|
610
|
+
| { kind: "void" };
|
|
611
|
+
|
|
612
|
+
export interface AxiomModel {
|
|
613
|
+
name: string;
|
|
614
|
+
fields: AxiomField[];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export interface AxiomField {
|
|
618
|
+
name: string;
|
|
619
|
+
typeRef: AxiomTypeRef;
|
|
620
|
+
isOptional: boolean;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export interface AxiomEnum {
|
|
624
|
+
name: string;
|
|
625
|
+
values: string[];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export interface AtmxContractConfig {
|
|
629
|
+
file: string; // Path relative to the config file (e.g., "./auth.axiom")
|
|
630
|
+
baseUrl: string; // The URL for runtime (not used during code generation, but part of schema)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export interface AtmxMultiConfig {
|
|
634
|
+
contracts: Record<string, AtmxContractConfig>;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// A Map holding the normalized IR for each contract
|
|
638
|
+
export type MultiIR = Record<string, AxiomIR>;
|
|
639
|
+
|
|
640
|
+
```
|
|
641
|
+
---
|
|
642
|
+
## File: tsconfig.json
|
|
643
|
+
|
|
644
|
+
```json
|
|
645
|
+
{
|
|
646
|
+
"compilerOptions": {
|
|
647
|
+
"target": "ES2020",
|
|
648
|
+
"module": "CommonJS",
|
|
649
|
+
"lib": [
|
|
650
|
+
"ES2020"
|
|
651
|
+
],
|
|
652
|
+
"rootDir": "src",
|
|
653
|
+
"outDir": "dist",
|
|
654
|
+
"moduleResolution": "node",
|
|
655
|
+
"esModuleInterop": true,
|
|
656
|
+
"forceConsistentCasingInFileNames": true,
|
|
657
|
+
"strict": true,
|
|
658
|
+
"skipLibCheck": true
|
|
659
|
+
},
|
|
660
|
+
"include": [
|
|
661
|
+
"src/**/*"
|
|
662
|
+
]
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"selected_files": [
|
|
3
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/package.json",
|
|
4
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/src/generators/model-generator.ts",
|
|
5
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/src/generators/sdk-generator.ts",
|
|
6
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/src/generators/utils.ts",
|
|
7
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/src/index.ts",
|
|
8
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/src/types.ts",
|
|
9
|
+
"/Users/yashmakan/AxiomCore/AxiomCore/release/atmx-cli/tsconfig.json"
|
|
10
|
+
],
|
|
11
|
+
"compressed_files": []
|
|
12
|
+
}
|