atmx-cli 0.86.0 → 0.89.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.
@@ -1,190 +1,192 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateSdk = generateSdk;
4
- const utils_1 = require("./utils");
5
- function generateSdk(multiIr, isReact = false) {
6
- const lines = [
7
- `// GENERATED CODE – DO NOT EDIT.`,
8
- `/* eslint-disable @typescript-eslint/no-explicit-any */`,
9
- `/* eslint-disable @typescript-eslint/no-unused-vars */`,
10
- `import * as models from './models';\n`,
11
- ];
12
- if (isReact) {
13
- // ✨ FIX: Auto-import the auth helpers and QueryManager directly
14
- lines.push(`import { useAxiomQuery, useAxiomMutation, setAuthToken, clearAuthToken, axiomQueryManager } from 'atmx-react';`);
15
- lines.push(`import type { AxiomQueryDef } from 'atmx-react';\n`);
3
+ exports.generateSDKContent = generateSDKContent;
4
+ // Helper to convert Axiom TypeRef to TypeScript Types
5
+ function getTsType(namespace, typeRef) {
6
+ if (!typeRef)
7
+ return "any";
8
+ if (typeRef.kind === "named") {
9
+ return `models.${namespace}.${typeRef.value}`;
16
10
  }
17
- for (const [ns, ir] of Object.entries(multiIr)) {
18
- const camelNs = (0, utils_1.camelCase)(ns);
19
- lines.push(`export const ${camelNs}Module = {`);
20
- lines.push(` axiom: {`);
21
- if (isReact) {
22
- lines.push(` setAuthToken(methodName: string, token: string) {`);
23
- lines.push(` setAuthToken("${ns}", methodName, token);`);
24
- lines.push(` },`);
25
- lines.push(` clearAuthToken(methodName: string) {`);
26
- lines.push(` clearAuthToken("${ns}", methodName);`);
27
- lines.push(` },`);
28
- lines.push(` connect(methodName: string, args?: Record<string, any>) {`);
29
- // ✨ FIX: Use the specific module name instead of 'this'
30
- lines.push(` const def = (${camelNs}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);`);
31
- lines.push(` axiomQueryManager.connect(def);`);
32
- lines.push(` },`);
33
- lines.push(` disconnect(methodName: string, args?: Record<string, any>) {`);
34
- lines.push(` const def = (${camelNs}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);`);
35
- lines.push(` axiomQueryManager.disconnect(def);`);
36
- lines.push(` },`);
37
- lines.push(` send(methodName: string, payload: any, args?: Record<string, any>) {`);
38
- lines.push(` const def = (${camelNs}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);`);
39
- lines.push(` axiomQueryManager.send(def, payload);`);
40
- lines.push(` }`);
41
- }
42
- else {
43
- lines.push(` setAuthToken(methodName: string, token: string) {`);
44
- lines.push(` (window as any).atmx?.setAuthToken("${ns}", methodName, token);`);
45
- lines.push(` },`);
46
- lines.push(` clearAuthToken(methodName: string) {`);
47
- lines.push(` (window as any).atmx?.clearAuthToken("${ns}", methodName);`);
48
- lines.push(` },`);
49
- lines.push(` connect(methodName: string, args?: Record<string, any>) {`);
50
- lines.push(` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';`);
51
- lines.push(` (window as any).atmx?.connect(\`${ns}.\${methodName}(\${argsStr})\`);`);
52
- lines.push(` },`);
53
- lines.push(` disconnect(methodName: string, args?: Record<string, any>) {`);
54
- lines.push(` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';`);
55
- lines.push(` (window as any).atmx?.disconnect(\`${ns}.\${methodName}(\${argsStr})\`);`);
56
- lines.push(` },`);
57
- lines.push(` send(methodName: string, payload: any, args?: Record<string, any>) {`);
58
- lines.push(` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';`);
59
- lines.push(` (window as any).atmx?.send(\`${ns}.\${methodName}(\${argsStr})\`, payload);`);
60
- lines.push(` }`);
61
- }
62
- lines.push(` },`);
63
- const endpointsMap = ir.endpoints || {};
64
- const endpoints = Array.isArray(endpointsMap)
65
- ? endpointsMap
66
- : Object.values(endpointsMap);
67
- endpoints.forEach((ep) => {
68
- lines.push(generateEndpointMethod(ep, ns, camelNs, isReact));
69
- });
70
- lines.push(`};\n`);
11
+ else if (typeRef.kind === "list") {
12
+ return `${getTsType(namespace, typeRef.value)}[]`;
71
13
  }
72
- lines.push(`export const sdk = {`);
73
- for (const ns of Object.keys(multiIr)) {
74
- lines.push(` ${(0, utils_1.camelCase)(ns)}: ${(0, utils_1.camelCase)(ns)}Module,`);
14
+ else if (typeRef.kind === "primitive") {
15
+ if (["int32", "int64", "float32", "float64"].includes(typeRef.value))
16
+ return "number";
17
+ if (typeRef.value === "bool")
18
+ return "boolean";
19
+ if (typeRef.value === "string")
20
+ return "string";
75
21
  }
76
- lines.push(`};\n`);
77
- lines.push(`export const AxiomDefaultConfig = {`);
78
- lines.push(` contracts: {`);
79
- for (const ns of Object.keys(multiIr)) {
80
- lines.push(` "${ns}": {`);
81
- lines.push(` contractUrl: "/${ns}.axiom",`);
82
- lines.push(` baseUrl: "http://localhost:8000"`);
83
- lines.push(` },`);
22
+ else if (typeRef.kind === "void") {
23
+ return "void";
84
24
  }
85
- lines.push(` }`);
86
- lines.push(`};\n`);
87
- return lines.join("\n");
25
+ return "any";
88
26
  }
89
- function generateEndpointMethod(ep, ns, camelNs, isReact) {
90
- const rawParams = ep.parameters || [];
91
- const params = Array.isArray(rawParams)
92
- ? rawParams
93
- : Object.values(rawParams);
94
- // ✨ FIX: Treat "WS" as a query (subscription) instead of a mutation!
95
- const isQuery = ep.method
96
- ? ["GET", "WS"].includes(ep.method.toUpperCase())
97
- : true;
98
- const rawReturnType = (0, utils_1.mapTypeToTs)(ep.returnType, camelNs);
99
- const returnType = rawReturnType === "void" || rawReturnType === "any"
100
- ? rawReturnType
101
- : prefixModels(rawReturnType);
102
- const argsMapping = params
103
- .map((p) => `if (args && '${(0, utils_1.camelCase)(p.name)}' in args) { mappedArgs["${p.name}"] = (args as any)["${(0, utils_1.camelCase)(p.name)}"]; delete mappedArgs["${(0, utils_1.camelCase)(p.name)}"]; }`)
104
- .join("\n ");
105
- if (params.length === 0) {
27
+ // Helper to get the correct deserializer function for a given TypeRef
28
+ function getDecoder(namespace, typeRef) {
29
+ if (!typeRef)
30
+ return `(json: any) => json`;
31
+ if (typeRef.kind === "named") {
32
+ return `models.Mappers.${namespace}.${typeRef.value}.fromJson`;
33
+ }
34
+ else if (typeRef.kind === "list" &&
35
+ typeRef.value.kind === "named") {
36
+ const innerName = typeRef.value.value;
37
+ return `(json: any[]) => json.map(models.Mappers.${namespace}.${innerName}.fromJson)`;
38
+ }
39
+ return `(json: any) => json`;
40
+ }
41
+ function generateSDKContent(contracts, isReact) {
42
+ let content = `// GENERATED CODE – DO NOT EDIT.\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-unused-vars */\n\n`;
43
+ if (!isReact) {
44
+ content += `import * as models from './models.js';\n\n`;
45
+ }
46
+ else {
47
+ content += `import * as models from './models.js';\n`;
48
+ content += `import { useAxiomQuery, useAxiomMutation, setAuthToken, clearAuthToken, axiomQueryManager } from "atmx-react";\n`;
49
+ content += `import type { AxiomQueryDef } from "atmx-react";\n\n`;
50
+ }
51
+ // 1. Generate Individual Modules
52
+ for (const [namespace, contract] of Object.entries(contracts)) {
53
+ content += `export const ${namespace}Module = {\n`;
54
+ content += ` axiom: {\n`;
55
+ content += ` setAuthToken(methodName: string, token: string) {\n`;
56
+ if (isReact) {
57
+ content += ` setAuthToken("${namespace}", methodName, token);\n`;
58
+ }
59
+ else {
60
+ content += ` (window as any).atmx?.setAuthToken("${namespace}", methodName, token);\n`;
61
+ }
62
+ content += ` },\n`;
63
+ content += ` clearAuthToken(methodName: string) {\n`;
64
+ if (isReact) {
65
+ content += ` clearAuthToken("${namespace}", methodName);\n`;
66
+ }
67
+ else {
68
+ content += ` (window as any).atmx?.clearAuthToken("${namespace}", methodName);\n`;
69
+ }
70
+ content += ` },\n`;
71
+ content += ` connect(methodName: string, args?: Record<string, any>) {\n`;
72
+ if (isReact) {
73
+ content += ` const def = (${namespace}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);\n`;
74
+ content += ` axiomQueryManager.connect(def);\n`;
75
+ }
76
+ else {
77
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
78
+ content += ` (window as any).atmx?.connect(\`${namespace}.\${methodName}(\${argsStr})\`);\n`;
79
+ }
80
+ content += ` },\n`;
81
+ content += ` disconnect(methodName: string, args?: Record<string, any>) {\n`;
106
82
  if (isReact) {
107
- const decLogic = generateLambda(ep.returnType, "fromJson", camelNs);
108
- return `
109
- get${(0, utils_1.pascalCase)(ep.name)}Def(args?: Record<string, any>): AxiomQueryDef<${returnType}> {
110
- return {
111
- namespace: "${ns}", name: "${ep.name}", endpointId: ${ep.id},
112
- method: "${ep.method ? ep.method.toUpperCase() : "GET"}", path: "${ep.path}",
113
- args: args || {}, decoder: ${decLogic}, serializer: (p: any) => p, isStream: ${ep.isStream === true}
114
- };
115
- },
116
- use${(0, utils_1.pascalCase)(ep.name)}(${isQuery ? "options?: { enabled?: boolean }" : ""}) {
117
- ${isQuery ? `return useAxiomQuery<${returnType}>(this.get${(0, utils_1.pascalCase)(ep.name)}Def(), options);` : `return useAxiomMutation<${returnType}, void | Record<string,any>>((a) => this.get${(0, utils_1.pascalCase)(ep.name)}Def(a));`}
118
- },`;
83
+ content += ` const def = (${namespace}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);\n`;
84
+ content += ` axiomQueryManager.disconnect(def);\n`;
119
85
  }
120
86
  else {
121
- return `
122
- ${(0, utils_1.camelCase)(ep.name)}(args?: Record<string, any>): string {
123
- const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';
124
- return \`${ns}.${ep.name}(\${argsStr})\`;
125
- },`;
87
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
88
+ content += ` (window as any).atmx?.disconnect(\`${namespace}.\${methodName}(\${argsStr})\`);\n`;
126
89
  }
90
+ content += ` },\n`;
91
+ content += ` send(methodName: string, payload: any, args?: Record<string, any>) {\n`;
92
+ if (isReact) {
93
+ content += ` const def = (${namespace}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);\n`;
94
+ content += ` axiomQueryManager.send(def, payload);\n`;
95
+ }
96
+ else {
97
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
98
+ content += ` (window as any).atmx?.send(\`${namespace}.\${methodName}(\${argsStr})\`, payload);\n`;
99
+ }
100
+ content += ` }\n`;
101
+ content += ` },\n\n`;
102
+ // Extract endpoints handling objects vs arrays
103
+ const endpoints = Array.isArray(contract.ir.endpoints)
104
+ ? contract.ir.endpoints
105
+ : Object.values(contract.ir.endpoints || {});
106
+ for (const endpoint of endpoints) {
107
+ const fnName = endpoint.name.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
108
+ const capFnName = fnName.charAt(0).toUpperCase() + fnName.slice(1);
109
+ if (isReact) {
110
+ const tsType = getTsType(namespace, endpoint.returnType);
111
+ const decoder = getDecoder(namespace, endpoint.returnType);
112
+ content += ` get${capFnName}Def(\n`;
113
+ content += ` args?: Record<string, any>,\n`;
114
+ content += ` ): AxiomQueryDef<${tsType}> {\n`;
115
+ content += ` return {\n`;
116
+ content += ` namespace: "${namespace}",\n`;
117
+ content += ` name: "${endpoint.name}",\n`;
118
+ content += ` endpointId: ${endpoint.id || 0},\n`;
119
+ content += ` method: "${endpoint.method}",\n`;
120
+ content += ` path: "${endpoint.path}",\n`;
121
+ content += ` args: args || {},\n`;
122
+ content += ` decoder: ${decoder},\n`;
123
+ content += ` serializer: (p: any) => p,\n`;
124
+ content += ` isStream: ${endpoint.isStream ? "true" : "false"},\n`;
125
+ content += ` };\n`;
126
+ content += ` },\n`;
127
+ if (endpoint.method === "GET" || endpoint.method === "WS") {
128
+ content += ` use${capFnName}(options?: { enabled?: boolean }) {\n`;
129
+ content += ` return useAxiomQuery<${tsType}>(\n`;
130
+ content += ` this.get${capFnName}Def(),\n`;
131
+ content += ` options,\n`;
132
+ content += ` );\n`;
133
+ content += ` },\n`;
134
+ }
135
+ else {
136
+ content += ` use${capFnName}(options?: any) {\n`;
137
+ content += ` return useAxiomMutation<${tsType}>(\n`;
138
+ content += ` this.get${capFnName}Def(),\n`;
139
+ content += ` options,\n`;
140
+ content += ` );\n`;
141
+ content += ` },\n`;
142
+ }
143
+ }
144
+ else {
145
+ // Vanilla Web generator logic
146
+ content += ` ${fnName}(args?: Record<string, any>): string {\n`;
147
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
148
+ content += ` return \`${namespace}.${endpoint.name}(\${argsStr})\`;\n`;
149
+ content += ` },\n`;
150
+ }
151
+ }
152
+ content += `};\n\n`;
127
153
  }
128
- const argType = `{ ${params.map((p) => `${(0, utils_1.camelCase)(p.name)}${p.isOptional ? "?" : ""}: ${prefixModels((0, utils_1.mapTypeToTs)(p.typeRef, camelNs))}`).join(", ")} }`;
129
- if (isReact) {
130
- const bodyParam = params.find((p) => p.source === "body");
131
- const payloadLogic = bodyParam
132
- ? `const payload = (args as any)?.${(0, utils_1.camelCase)(bodyParam.name)};`
133
- : `const payload = undefined;`;
134
- const decLogic = generateLambda(ep.returnType, "fromJson", camelNs);
135
- const serLogic = bodyParam
136
- ? generateLambda(bodyParam.typeRef, "toJson", camelNs)
137
- : `(p: any) => p`;
138
- return `
139
- get${(0, utils_1.pascalCase)(ep.name)}Def(args?: ${argType}): AxiomQueryDef<${returnType}> {
140
- ${payloadLogic}
141
- const mappedArgs: any = { ...(args || {}) };
142
- ${argsMapping}
143
-
144
- return {
145
- namespace: "${ns}", name: "${ep.name}", endpointId: ${ep.id},
146
- method: "${ep.method ? ep.method.toUpperCase() : "GET"}", path: "${ep.path}",
147
- payload: payload, args: mappedArgs, decoder: ${decLogic}, serializer: ${serLogic}, isStream: ${ep.isStream === true}
148
- };
149
- },
150
- use${(0, utils_1.pascalCase)(ep.name)}(${isQuery ? `args?: ${argType}, options?: { enabled?: boolean }` : `args?: ${argType}`}) {
151
- ${isQuery ? `return useAxiomQuery<${returnType}>(this.get${(0, utils_1.pascalCase)(ep.name)}Def(args), options);` : `return useAxiomMutation<${returnType}, ${argType}>((a) => this.get${(0, utils_1.pascalCase)(ep.name)}Def(a || args));`}
152
- },`;
154
+ // 2. Generate the Smart Proxy SDK
155
+ content += `const internalSdk: Record<string, any> = {\n`;
156
+ for (const namespace of Object.keys(contracts)) {
157
+ content += ` ${namespace}: ${namespace}Module,\n`;
153
158
  }
154
- else {
155
- return `
156
- ${(0, utils_1.camelCase)(ep.name)}(args?: ${argType}): string {
157
- const mappedArgs: any = { ...(args || {}) };
158
- ${argsMapping}
159
- const argsStr = Object.keys(mappedArgs).length > 0 ? JSON.stringify(mappedArgs) : '';
160
- return \`${ns}.${ep.name}(\${argsStr})\`;
161
- },`;
159
+ content += `};\n\n`;
160
+ content += `// ✨ The Magic Proxy: Safely intercepts Alpine.js evaluations during boot!\n`;
161
+ content += `export const sdk = new Proxy(internalSdk, {\n`;
162
+ content += ` get(target: any, prop: string, receiver: any) {\n`;
163
+ content += ` if (prop in target) {\n`;
164
+ content += ` return Reflect.get(target, prop, receiver);\n`;
165
+ content += ` }\n`;
166
+ content += ` // If Alpine tries to access a namespace that doesn't exist yet, return a nested Proxy!\n`;
167
+ content += ` return new Proxy({}, {\n`;
168
+ content += ` get(subTarget: any, subProp: string) {\n`;
169
+ content += ` return () => \`\${String(prop)}.\${String(subProp)}()\`;\n`;
170
+ content += ` }\n`;
171
+ content += ` });\n`;
172
+ content += ` }\n`;
173
+ content += `});\n\n`;
174
+ content += `// Auto-attach to window for Alpine.js immediate hydration\n`;
175
+ content += `if (typeof window !== "undefined") {\n`;
176
+ content += ` (window as any).sdk = sdk;\n`;
177
+ content += `}\n\n`;
178
+ // 3. Generate Default Config
179
+ content += `export const AxiomDefaultConfig = {\n`;
180
+ content += ` contracts: {\n`;
181
+ for (const [ns, def] of Object.entries(contracts)) {
182
+ // Determine file path or default to namespace
183
+ const contractPath = def.file ? def.file : `/${ns}.axiom`;
184
+ content += ` "${ns}": {\n`;
185
+ content += ` contractUrl: "${contractPath}",\n`;
186
+ content += ` baseUrl: "${def.baseUrl}"\n`;
187
+ content += ` },\n`;
162
188
  }
163
- }
164
- function generateLambda(typeRef, mode, ns) {
165
- if (!typeRef || !typeRef.kind || typeRef.kind === "void")
166
- return mode === "fromJson" ? `() => undefined` : `(p: any) => p`;
167
- if (typeRef.kind === "list" && typeRef.value?.kind === "named")
168
- return `(data: any[]) => data.map(models.Mappers.${ns}.${(0, utils_1.pascalCase)(typeRef.value.value)}.${mode})`;
169
- if (typeRef.kind === "named")
170
- return `models.Mappers.${ns}.${(0, utils_1.pascalCase)(typeRef.value)}.${mode}`;
171
- return `(data: any) => data`;
172
- }
173
- function prefixModels(type) {
174
- const primitives = [
175
- "string",
176
- "number",
177
- "boolean",
178
- "Date",
179
- "Uint8Array",
180
- "void",
181
- "any",
182
- ];
183
- if (!type || primitives.includes(type))
184
- return type;
185
- if (type.endsWith("[]"))
186
- return `${prefixModels(type.slice(0, -2))}[]`;
187
- if (type.startsWith("models."))
188
- return type;
189
- return `models.${type}`;
189
+ content += ` }\n`;
190
+ content += `};\n`;
191
+ return content;
190
192
  }
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ const fs = __importStar(require("fs-extra"));
39
39
  const path = __importStar(require("path"));
40
40
  const toml = __importStar(require("@iarna/toml"));
41
41
  const model_generator_1 = require("./generators/model-generator");
42
+ // ✨ FIX: Import generateSDKContent and ContractPayload
42
43
  const sdk_generator_1 = require("./generators/sdk-generator");
43
44
  const utils_1 = require("./generators/utils");
44
45
  const program = new commander_1.Command();
@@ -66,6 +67,7 @@ program
66
67
  process.exit(1);
67
68
  }
68
69
  const multiIr = {};
70
+ const generatorPayload = {}; // ✨ NEW: Payload for SDK
69
71
  const projectRoot = path.dirname(configPath); // Frontend project root
70
72
  // 2. Loop through contracts
71
73
  for (const [namespace, contract] of Object.entries(rawConfig.contracts)) {
@@ -79,12 +81,20 @@ program
79
81
  if (!rawFile.ir)
80
82
  continue;
81
83
  multiIr[namespace] = (0, utils_1.normalizeIr)(rawFile.ir);
84
+ // ✨ NEW: Combine IR with TOML config for the SDK generator
85
+ generatorPayload[namespace] = {
86
+ ir: multiIr[namespace],
87
+ baseUrl: contract.base_url || "http://localhost:8080",
88
+ file: `/${namespace}.axiom`,
89
+ };
82
90
  console.log(`✅ Loaded contract: [${namespace}] -> ${axiomFilePath}`);
83
91
  }
84
92
  await fs.ensureDir(outputDir);
93
+ // 3. Generate Models (Needs raw MultiIR)
85
94
  const modelsContent = (0, model_generator_1.generateModels)(multiIr);
86
95
  await fs.writeFile(path.join(outputDir, "models.ts"), modelsContent);
87
- const sdkContent = (0, sdk_generator_1.generateSdk)(multiIr, options.react);
96
+ // 4. Generate SDK (Needs enriched ContractPayload)
97
+ const sdkContent = (0, sdk_generator_1.generateSDKContent)(generatorPayload, options.react);
88
98
  await fs.writeFile(path.join(outputDir, "sdk.ts"), sdkContent);
89
99
  console.log(`\n🎉 ATMX Multi-Contract SDK generated successfully in ${outputDir}`);
90
100
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atmx-cli",
3
- "version": "0.86.0",
3
+ "version": "0.89.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,255 +1,205 @@
1
- // FILE: atmx-cli/src/generators/sdk-generator.ts
2
- import { AxiomEndpoint, AxiomParameter, MultiIR } from "../types";
3
- import { pascalCase, camelCase, mapTypeToTs } from "./utils";
1
+ import { AxiomIR, AxiomEndpoint, AxiomTypeRef } from "../types.js";
4
2
 
5
- export function generateSdk(
6
- multiIr: MultiIR,
7
- isReact: boolean = false,
8
- ): string {
9
- const lines: string[] = [
10
- `// GENERATED CODE – DO NOT EDIT.`,
11
- `/* eslint-disable @typescript-eslint/no-explicit-any */`,
12
- `/* eslint-disable @typescript-eslint/no-unused-vars */`,
13
- `import * as models from './models';\n`,
14
- ];
15
-
16
- if (isReact) {
17
- // ✨ FIX: Auto-import the auth helpers and QueryManager directly
18
- lines.push(
19
- `import { useAxiomQuery, useAxiomMutation, setAuthToken, clearAuthToken, axiomQueryManager } from 'atmx-react';`,
20
- );
21
- lines.push(`import type { AxiomQueryDef } from 'atmx-react';\n`);
22
- }
23
-
24
- for (const [ns, ir] of Object.entries(multiIr)) {
25
- const camelNs = camelCase(ns);
26
- lines.push(`export const ${camelNs}Module = {`);
27
-
28
- lines.push(` axiom: {`);
29
- if (isReact) {
30
- lines.push(` setAuthToken(methodName: string, token: string) {`);
31
- lines.push(` setAuthToken("${ns}", methodName, token);`);
32
- lines.push(` },`);
33
- lines.push(` clearAuthToken(methodName: string) {`);
34
- lines.push(` clearAuthToken("${ns}", methodName);`);
35
- lines.push(` },`);
36
- lines.push(
37
- ` connect(methodName: string, args?: Record<string, any>) {`,
38
- );
39
- // ✨ FIX: Use the specific module name instead of 'this'
40
- lines.push(
41
- ` const def = (${camelNs}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);`,
42
- );
43
- lines.push(` axiomQueryManager.connect(def);`);
44
- lines.push(` },`);
45
- lines.push(
46
- ` disconnect(methodName: string, args?: Record<string, any>) {`,
47
- );
48
- lines.push(
49
- ` const def = (${camelNs}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);`,
50
- );
51
- lines.push(` axiomQueryManager.disconnect(def);`);
52
- lines.push(` },`);
53
- lines.push(
54
- ` send(methodName: string, payload: any, args?: Record<string, any>) {`,
55
- );
56
- lines.push(
57
- ` const def = (${camelNs}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);`,
58
- );
59
- lines.push(` axiomQueryManager.send(def, payload);`);
60
- lines.push(` }`);
61
- } else {
62
- lines.push(` setAuthToken(methodName: string, token: string) {`);
63
- lines.push(
64
- ` (window as any).atmx?.setAuthToken("${ns}", methodName, token);`,
65
- );
66
- lines.push(` },`);
67
- lines.push(` clearAuthToken(methodName: string) {`);
68
- lines.push(
69
- ` (window as any).atmx?.clearAuthToken("${ns}", methodName);`,
70
- );
71
- lines.push(` },`);
72
- lines.push(
73
- ` connect(methodName: string, args?: Record<string, any>) {`,
74
- );
75
- lines.push(
76
- ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';`,
77
- );
78
- lines.push(
79
- ` (window as any).atmx?.connect(\`${ns}.\${methodName}(\${argsStr})\`);`,
80
- );
81
- lines.push(` },`);
82
- lines.push(
83
- ` disconnect(methodName: string, args?: Record<string, any>) {`,
84
- );
85
- lines.push(
86
- ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';`,
87
- );
88
- lines.push(
89
- ` (window as any).atmx?.disconnect(\`${ns}.\${methodName}(\${argsStr})\`);`,
90
- );
91
- lines.push(` },`);
92
- lines.push(
93
- ` send(methodName: string, payload: any, args?: Record<string, any>) {`,
94
- );
95
- lines.push(
96
- ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';`,
97
- );
98
- lines.push(
99
- ` (window as any).atmx?.send(\`${ns}.\${methodName}(\${argsStr})\`, payload);`,
100
- );
101
- lines.push(` }`);
102
- }
103
- lines.push(` },`);
104
-
105
- const endpointsMap = ir.endpoints || {};
106
- const endpoints = Array.isArray(endpointsMap)
107
- ? endpointsMap
108
- : Object.values(endpointsMap);
3
+ export interface ContractPayload {
4
+ ir: AxiomIR;
5
+ baseUrl: string;
6
+ file: string;
7
+ }
109
8
 
110
- endpoints.forEach((ep: any) => {
111
- lines.push(generateEndpointMethod(ep, ns, camelNs, isReact));
112
- });
113
- lines.push(`};\n`);
9
+ // Helper to convert Axiom TypeRef to TypeScript Types
10
+ function getTsType(namespace: string, typeRef?: AxiomTypeRef): string {
11
+ if (!typeRef) return "any";
12
+ if (typeRef.kind === "named") {
13
+ return `models.${namespace}.${typeRef.value}`;
14
+ } else if (typeRef.kind === "list") {
15
+ return `${getTsType(namespace, typeRef.value as AxiomTypeRef)}[]`;
16
+ } else if (typeRef.kind === "primitive") {
17
+ if (["int32", "int64", "float32", "float64"].includes(typeRef.value))
18
+ return "number";
19
+ if (typeRef.value === "bool") return "boolean";
20
+ if (typeRef.value === "string") return "string";
21
+ } else if (typeRef.kind === "void") {
22
+ return "void";
114
23
  }
24
+ return "any";
25
+ }
115
26
 
116
- lines.push(`export const sdk = {`);
117
- for (const ns of Object.keys(multiIr)) {
118
- lines.push(` ${camelCase(ns)}: ${camelCase(ns)}Module,`);
119
- }
120
- lines.push(`};\n`);
121
-
122
- lines.push(`export const AxiomDefaultConfig = {`);
123
- lines.push(` contracts: {`);
124
- for (const ns of Object.keys(multiIr)) {
125
- lines.push(` "${ns}": {`);
126
- lines.push(` contractUrl: "/${ns}.axiom",`);
127
- lines.push(` baseUrl: "http://localhost:8000"`);
128
- lines.push(` },`);
27
+ // Helper to get the correct deserializer function for a given TypeRef
28
+ function getDecoder(namespace: string, typeRef?: AxiomTypeRef): string {
29
+ if (!typeRef) return `(json: any) => json`;
30
+
31
+ if (typeRef.kind === "named") {
32
+ return `models.Mappers.${namespace}.${typeRef.value}.fromJson`;
33
+ } else if (
34
+ typeRef.kind === "list" &&
35
+ (typeRef.value as AxiomTypeRef).kind === "named"
36
+ ) {
37
+ const innerName = (typeRef.value as any).value;
38
+ return `(json: any[]) => json.map(models.Mappers.${namespace}.${innerName}.fromJson)`;
129
39
  }
130
- lines.push(` }`);
131
- lines.push(`};\n`);
132
40
 
133
- return lines.join("\n");
41
+ return `(json: any) => json`;
134
42
  }
135
43
 
136
- function generateEndpointMethod(
137
- ep: AxiomEndpoint,
138
- ns: string,
139
- camelNs: string,
44
+ export function generateSDKContent(
45
+ contracts: Record<string, ContractPayload>,
140
46
  isReact: boolean,
141
47
  ): string {
142
- const rawParams = ep.parameters || [];
143
- const params = Array.isArray(rawParams)
144
- ? rawParams
145
- : Object.values(rawParams);
146
-
147
- // FIX: Treat "WS" as a query (subscription) instead of a mutation!
148
- const isQuery = ep.method
149
- ? ["GET", "WS"].includes(ep.method.toUpperCase())
150
- : true;
151
- const rawReturnType = mapTypeToTs(ep.returnType, camelNs);
152
- const returnType =
153
- rawReturnType === "void" || rawReturnType === "any"
154
- ? rawReturnType
155
- : prefixModels(rawReturnType);
156
-
157
- const argsMapping = params
158
- .map(
159
- (p: any) =>
160
- `if (args && '${camelCase(p.name)}' in args) { mappedArgs["${p.name}"] = (args as any)["${camelCase(p.name)}"]; delete mappedArgs["${camelCase(p.name)}"]; }`,
161
- )
162
- .join("\n ");
163
-
164
- if (params.length === 0) {
48
+ let content = `// GENERATED CODE – DO NOT EDIT.\n/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-unused-vars */\n\n`;
49
+
50
+ if (!isReact) {
51
+ content += `import * as models from './models.js';\n\n`;
52
+ } else {
53
+ content += `import * as models from './models.js';\n`;
54
+ content += `import { useAxiomQuery, useAxiomMutation, setAuthToken, clearAuthToken, axiomQueryManager } from "atmx-react";\n`;
55
+ content += `import type { AxiomQueryDef } from "atmx-react";\n\n`;
56
+ }
57
+
58
+ // 1. Generate Individual Modules
59
+ for (const [namespace, contract] of Object.entries(contracts)) {
60
+ content += `export const ${namespace}Module = {\n`;
61
+ content += ` axiom: {\n`;
62
+ content += ` setAuthToken(methodName: string, token: string) {\n`;
165
63
  if (isReact) {
166
- const decLogic = generateLambda(ep.returnType, "fromJson", camelNs);
167
- return `
168
- get${pascalCase(ep.name)}Def(args?: Record<string, any>): AxiomQueryDef<${returnType}> {
169
- return {
170
- namespace: "${ns}", name: "${ep.name}", endpointId: ${ep.id},
171
- method: "${ep.method ? ep.method.toUpperCase() : "GET"}", path: "${ep.path}",
172
- args: args || {}, decoder: ${decLogic}, serializer: (p: any) => p, isStream: ${ep.isStream === true}
173
- };
174
- },
175
- use${pascalCase(ep.name)}(${isQuery ? "options?: { enabled?: boolean }" : ""}) {
176
- ${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));`}
177
- },`;
64
+ content += ` setAuthToken("${namespace}", methodName, token);\n`;
178
65
  } else {
179
- return `
180
- ${camelCase(ep.name)}(args?: Record<string, any>): string {
181
- const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';
182
- return \`${ns}.${ep.name}(\${argsStr})\`;
183
- },`;
66
+ content += ` (window as any).atmx?.setAuthToken("${namespace}", methodName, token);\n`;
184
67
  }
68
+ content += ` },\n`;
69
+ content += ` clearAuthToken(methodName: string) {\n`;
70
+ if (isReact) {
71
+ content += ` clearAuthToken("${namespace}", methodName);\n`;
72
+ } else {
73
+ content += ` (window as any).atmx?.clearAuthToken("${namespace}", methodName);\n`;
74
+ }
75
+ content += ` },\n`;
76
+ content += ` connect(methodName: string, args?: Record<string, any>) {\n`;
77
+ if (isReact) {
78
+ content += ` const def = (${namespace}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);\n`;
79
+ content += ` axiomQueryManager.connect(def);\n`;
80
+ } else {
81
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
82
+ content += ` (window as any).atmx?.connect(\`${namespace}.\${methodName}(\${argsStr})\`);\n`;
83
+ }
84
+ content += ` },\n`;
85
+ content += ` disconnect(methodName: string, args?: Record<string, any>) {\n`;
86
+ if (isReact) {
87
+ content += ` const def = (${namespace}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);\n`;
88
+ content += ` axiomQueryManager.disconnect(def);\n`;
89
+ } else {
90
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
91
+ content += ` (window as any).atmx?.disconnect(\`${namespace}.\${methodName}(\${argsStr})\`);\n`;
92
+ }
93
+ content += ` },\n`;
94
+ content += ` send(methodName: string, payload: any, args?: Record<string, any>) {\n`;
95
+ if (isReact) {
96
+ content += ` const def = (${namespace}Module as any)[\`get\${methodName.charAt(0).toUpperCase() + methodName.slice(1)}Def\`](args);\n`;
97
+ content += ` axiomQueryManager.send(def, payload);\n`;
98
+ } else {
99
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
100
+ content += ` (window as any).atmx?.send(\`${namespace}.\${methodName}(\${argsStr})\`, payload);\n`;
101
+ }
102
+ content += ` }\n`;
103
+ content += ` },\n\n`;
104
+
105
+ // Extract endpoints handling objects vs arrays
106
+ const endpoints: AxiomEndpoint[] = Array.isArray(contract.ir.endpoints)
107
+ ? contract.ir.endpoints
108
+ : Object.values(contract.ir.endpoints || {});
109
+
110
+ for (const endpoint of endpoints) {
111
+ const fnName = endpoint.name.replace(/_([a-z])/g, (g) =>
112
+ g[1].toUpperCase(),
113
+ );
114
+ const capFnName = fnName.charAt(0).toUpperCase() + fnName.slice(1);
115
+
116
+ if (isReact) {
117
+ const tsType = getTsType(namespace, endpoint.returnType);
118
+ const decoder = getDecoder(namespace, endpoint.returnType);
119
+
120
+ content += ` get${capFnName}Def(\n`;
121
+ content += ` args?: Record<string, any>,\n`;
122
+ content += ` ): AxiomQueryDef<${tsType}> {\n`;
123
+ content += ` return {\n`;
124
+ content += ` namespace: "${namespace}",\n`;
125
+ content += ` name: "${endpoint.name}",\n`;
126
+ content += ` endpointId: ${endpoint.id || 0},\n`;
127
+ content += ` method: "${endpoint.method}",\n`;
128
+ content += ` path: "${endpoint.path}",\n`;
129
+ content += ` args: args || {},\n`;
130
+ content += ` decoder: ${decoder},\n`;
131
+ content += ` serializer: (p: any) => p,\n`;
132
+ content += ` isStream: ${endpoint.isStream ? "true" : "false"},\n`;
133
+ content += ` };\n`;
134
+ content += ` },\n`;
135
+
136
+ if (endpoint.method === "GET" || endpoint.method === "WS") {
137
+ content += ` use${capFnName}(options?: { enabled?: boolean }) {\n`;
138
+ content += ` return useAxiomQuery<${tsType}>(\n`;
139
+ content += ` this.get${capFnName}Def(),\n`;
140
+ content += ` options,\n`;
141
+ content += ` );\n`;
142
+ content += ` },\n`;
143
+ } else {
144
+ content += ` use${capFnName}(options?: any) {\n`;
145
+ content += ` return useAxiomMutation<${tsType}>(\n`;
146
+ content += ` this.get${capFnName}Def(),\n`;
147
+ content += ` options,\n`;
148
+ content += ` );\n`;
149
+ content += ` },\n`;
150
+ }
151
+ } else {
152
+ // Vanilla Web generator logic
153
+ content += ` ${fnName}(args?: Record<string, any>): string {\n`;
154
+ content += ` const argsStr = args && Object.keys(args).length > 0 ? JSON.stringify(args) : '';\n`;
155
+ content += ` return \`${namespace}.${endpoint.name}(\${argsStr})\`;\n`;
156
+ content += ` },\n`;
157
+ }
158
+ }
159
+ content += `};\n\n`;
185
160
  }
186
161
 
187
- const argType = `{ ${params.map((p: any) => `${camelCase(p.name)}${p.isOptional ? "?" : ""}: ${prefixModels(mapTypeToTs(p.typeRef, camelNs))}`).join(", ")} }`;
188
-
189
- if (isReact) {
190
- const bodyParam = params.find(
191
- (p: any) => p.source === "body",
192
- ) as AxiomParameter;
193
- const payloadLogic = bodyParam
194
- ? `const payload = (args as any)?.${camelCase(bodyParam.name)};`
195
- : `const payload = undefined;`;
196
- const decLogic = generateLambda(ep.returnType, "fromJson", camelNs);
197
- const serLogic = bodyParam
198
- ? generateLambda(bodyParam.typeRef, "toJson", camelNs)
199
- : `(p: any) => p`;
200
-
201
- return `
202
- get${pascalCase(ep.name)}Def(args?: ${argType}): AxiomQueryDef<${returnType}> {
203
- ${payloadLogic}
204
- const mappedArgs: any = { ...(args || {}) };
205
- ${argsMapping}
206
-
207
- return {
208
- namespace: "${ns}", name: "${ep.name}", endpointId: ${ep.id},
209
- method: "${ep.method ? ep.method.toUpperCase() : "GET"}", path: "${ep.path}",
210
- payload: payload, args: mappedArgs, decoder: ${decLogic}, serializer: ${serLogic}, isStream: ${ep.isStream === true}
211
- };
212
- },
213
- use${pascalCase(ep.name)}(${isQuery ? `args?: ${argType}, options?: { enabled?: boolean }` : `args?: ${argType}`}) {
214
- ${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));`}
215
- },`;
216
- } else {
217
- return `
218
- ${camelCase(ep.name)}(args?: ${argType}): string {
219
- const mappedArgs: any = { ...(args || {}) };
220
- ${argsMapping}
221
- const argsStr = Object.keys(mappedArgs).length > 0 ? JSON.stringify(mappedArgs) : '';
222
- return \`${ns}.${ep.name}(\${argsStr})\`;
223
- },`;
162
+ // 2. Generate the Smart Proxy SDK
163
+ content += `const internalSdk: Record<string, any> = {\n`;
164
+ for (const namespace of Object.keys(contracts)) {
165
+ content += ` ${namespace}: ${namespace}Module,\n`;
224
166
  }
225
- }
226
-
227
- function generateLambda(
228
- typeRef: any,
229
- mode: "fromJson" | "toJson",
230
- ns: string,
231
- ): string {
232
- if (!typeRef || !typeRef.kind || typeRef.kind === "void")
233
- return mode === "fromJson" ? `() => undefined` : `(p: any) => p`;
234
- if (typeRef.kind === "list" && typeRef.value?.kind === "named")
235
- return `(data: any[]) => data.map(models.Mappers.${ns}.${pascalCase(typeRef.value.value)}.${mode})`;
236
- if (typeRef.kind === "named")
237
- return `models.Mappers.${ns}.${pascalCase(typeRef.value)}.${mode}`;
238
- return `(data: any) => data`;
239
- }
167
+ content += `};\n\n`;
168
+
169
+ content += `// ✨ The Magic Proxy: Safely intercepts Alpine.js evaluations during boot!\n`;
170
+ content += `export const sdk = new Proxy(internalSdk, {\n`;
171
+ content += ` get(target: any, prop: string, receiver: any) {\n`;
172
+ content += ` if (prop in target) {\n`;
173
+ content += ` return Reflect.get(target, prop, receiver);\n`;
174
+ content += ` }\n`;
175
+ content += ` // If Alpine tries to access a namespace that doesn't exist yet, return a nested Proxy!\n`;
176
+ content += ` return new Proxy({}, {\n`;
177
+ content += ` get(subTarget: any, subProp: string) {\n`;
178
+ content += ` return () => \`\${String(prop)}.\${String(subProp)}()\`;\n`;
179
+ content += ` }\n`;
180
+ content += ` });\n`;
181
+ content += ` }\n`;
182
+ content += `});\n\n`;
183
+
184
+ content += `// Auto-attach to window for Alpine.js immediate hydration\n`;
185
+ content += `if (typeof window !== "undefined") {\n`;
186
+ content += ` (window as any).sdk = sdk;\n`;
187
+ content += `}\n\n`;
188
+
189
+ // 3. Generate Default Config
190
+ content += `export const AxiomDefaultConfig = {\n`;
191
+ content += ` contracts: {\n`;
192
+ for (const [ns, def] of Object.entries(contracts)) {
193
+ // Determine file path or default to namespace
194
+ const contractPath = def.file ? def.file : `/${ns}.axiom`;
195
+
196
+ content += ` "${ns}": {\n`;
197
+ content += ` contractUrl: "${contractPath}",\n`;
198
+ content += ` baseUrl: "${def.baseUrl}"\n`;
199
+ content += ` },\n`;
200
+ }
201
+ content += ` }\n`;
202
+ content += `};\n`;
240
203
 
241
- function prefixModels(type: string): string {
242
- const primitives = [
243
- "string",
244
- "number",
245
- "boolean",
246
- "Date",
247
- "Uint8Array",
248
- "void",
249
- "any",
250
- ];
251
- if (!type || primitives.includes(type)) return type;
252
- if (type.endsWith("[]")) return `${prefixModels(type.slice(0, -2))}[]`;
253
- if (type.startsWith("models.")) return type;
254
- return `models.${type}`;
204
+ return content;
255
205
  }
package/src/index.ts CHANGED
@@ -5,7 +5,11 @@ import * as path from "path";
5
5
  import * as toml from "@iarna/toml";
6
6
  import { MultiIR } from "./types";
7
7
  import { generateModels } from "./generators/model-generator";
8
- import { generateSdk } from "./generators/sdk-generator";
8
+ // FIX: Import generateSDKContent and ContractPayload
9
+ import {
10
+ generateSDKContent,
11
+ ContractPayload,
12
+ } from "./generators/sdk-generator";
9
13
  import { normalizeIr } from "./generators/utils";
10
14
 
11
15
  const program = new Command();
@@ -39,6 +43,7 @@ program
39
43
  }
40
44
 
41
45
  const multiIr: MultiIR = {};
46
+ const generatorPayload: Record<string, ContractPayload> = {}; // ✨ NEW: Payload for SDK
42
47
  const projectRoot = path.dirname(configPath); // Frontend project root
43
48
 
44
49
  // 2. Loop through contracts
@@ -60,15 +65,25 @@ program
60
65
  if (!rawFile.ir) continue;
61
66
 
62
67
  multiIr[namespace] = normalizeIr(rawFile.ir);
68
+
69
+ // ✨ NEW: Combine IR with TOML config for the SDK generator
70
+ generatorPayload[namespace] = {
71
+ ir: multiIr[namespace],
72
+ baseUrl: (contract as any).base_url || "http://localhost:8080",
73
+ file: `/${namespace}.axiom`,
74
+ };
75
+
63
76
  console.log(`✅ Loaded contract: [${namespace}] -> ${axiomFilePath}`);
64
77
  }
65
78
 
66
79
  await fs.ensureDir(outputDir);
67
80
 
81
+ // 3. Generate Models (Needs raw MultiIR)
68
82
  const modelsContent = generateModels(multiIr);
69
83
  await fs.writeFile(path.join(outputDir, "models.ts"), modelsContent);
70
84
 
71
- const sdkContent = generateSdk(multiIr, options.react);
85
+ // 4. Generate SDK (Needs enriched ContractPayload)
86
+ const sdkContent = generateSDKContent(generatorPayload, options.react);
72
87
  await fs.writeFile(path.join(outputDir, "sdk.ts"), sdkContent);
73
88
 
74
89
  console.log(